Everything the next agent needs to run the Phase 2 optimisation loop on a machine that can launch the client: - analysis/client-perf/: LEVERS (ranked levers + corrections), BENCH-MATRIX (world, scenes, config IDs, blocks B0-B14 with decision rules), PLAYER-TUNING, SPAWN-TUNING, research briefs, the jar scanner and its output, observed default configs, and proposed-configs/ (build.py generates tiers and bench variants and fails on unknown keys; apply_variant.py applies a variant and merges options.txt). - analysis/client-perf/bench/onion-bench/: bench-world datapack (scene teleports, Create grid, mob pen, sound storm, 120 s teleport tour). - analysis/audit/: the static recipe/dependency audit, including gen_fixes.py, which generates the shipped Paxi datapack and was never tracked. - phase0/: per-machine measurement harness (Steam Deck, 7700K/1060, 6700K/1080 Ti). - .pi/journal.md: running work log; read it first. All of it is packwizignored, so nothing here ships to players.
13 KiB
Client-perf configs: generator, tiers, bench variants
Status (a01bc3b): the owner approved the mechanism below. Default Options is in the
pack, and the T-default files are shipped under config/defaultoptions/ via
build.py --ship. Every value is still unmeasured. ../BENCH-MATRIX.md decides them.
ModernFix dynamic_resources is held back until B6 passes.
python3 build.py # regenerates out/ (stdlib only, ~1 s)
python3 build.py --ship # + copies T-default into config/defaultoptions/
~/go/bin/packwiz refresh # from the repo root, after --ship
python3 apply_variant.py C1 <instance>/.minecraft # bench: apply a variant (merges options.txt)
Iteration loop: edit DEFAULT / DECK / HIGH in build.py, then build.py --ship,
then packwiz refresh, then commit. The shipped defaults are seed-once: they reach fresh
installs only. Players who already have the files follow ../PLAYER-TUNING.md §1.
| Path | What |
|---|---|
build.py |
Single source of truth: tiers, bench variants, base files. Fails if an override names a key that isn't already in the base file. That enforces the "confirmed keys only" rule mechanically. |
templates/ |
Hand-written bases for files without a same-version observed default (keys taken from the pinned jar or its source, see §3) |
out/T-default, T-deck, T-mid, T-high |
Player tiers. T-mid = T-default |
out/C0, C1, C1-*, dh-*, fw-*, … |
Phase 2 bench variants (see ../BENCH-MATRIX.md) |
out/MANIFEST.md |
Generated list of every changed key per variant |
apply_variant.py |
Bench: apply out/<ID>/ to an instance, merging options.txt |
1. Delivery mechanism (LEVERS M1): resolved
Recommendation: add Default Options, and stage the tuned files under
config/defaultoptions/. The one exception is modernfix-mixins.properties, which ships as
a normal (enforced) packwiz file. Evidence follows. All of it was read from the exact
versions in use: packwiz ef87d96 (the commit ~/go/bin/packwiz was built from, per
go version -m), packwiz-installer v0.5.14 (the jar in the Prism instance reports
Implementation-Version: v0.5.14), and Default Options 21.1.8 (source tag
1.21.1@94e2f17 plus the Modrinth jar).
packwiz preserve = true
| Question | Answer | Evidence |
|---|---|---|
| How is it set? | Only by hand-editing index.toml; no CLI flag |
Preserve exists only in core/indexfiles.go:29; no cmd/ reference |
Does packwiz refresh keep it? |
Yes, as long as the file stays on disk and not ignored. updateFileEntry mutates the loaded entry in place. |
core/indexfiles.go updateFileEntry. Tested: kept across refresh, a content change + refresh, and an unrelated packwiz update |
| When is it lost? | Any refresh while the file is absent or ignored deletes the entry (Refresh() drops entries that are not markedFound). Re-adding it comes back without preserve. |
core/index.go Refresh(). Tested: delete → refresh → restore → refresh = preserve gone |
| What does the installer do? | Writes the file only if the destination does not exist. It never updates it afterwards. | DownloadTask.kt:222-227 |
| Side effects | (a) When the installer skips the file, the manifest entry gets no cachedLocation, so every launch logs "invalidated, marked for redownloading" and the installer can't take its "pack unchanged" fast path (it re-reads the index each launch). (b) If the installer did write the file, a later removal from the index deletes the player's copy (UpdateManager.kt:250-262). |
source |
| Timing | Pre-launch (Prism PreLaunchCommand), so it beats everything, including mixin-time configs |
instance.cfg |
| Sided? | No. Plain index files have no side. This repo is the server directory, so a file in config/ also configures the server. |
indexFile struct |
Default Options 21.1.8 (BlayTheNinth)
| Question | Answer | Evidence |
|---|---|---|
| Compatible? | NeoForge [21-beta,), MC [1.21,), Balm [21.0.39,). The pack has Balm 21.0.66. |
jar neoforge.mods.toml |
options.txt |
config/defaultoptions/options.txt is copied only if options.txt is absent. key_* lines are stripped (keybinds have their own handler). |
DefaultOptionsDefaultHandlers, SimpleDefaultOptionsFileHandler.shouldLoadDefaults |
| Any config file | config/defaultoptions/extra/<path> is copied to <gamedir>/<path> only if the target is absent |
ExtraDefaultOptionsHandler.loadDefaults |
| Timing | EARLY_LOAD runs at GameConfig.<init> RETURN, which is in Main, before new Minecraft(). NeoForge 1.21.1 constructs mods inside Minecraft.<init> (ClientModLoader.begin, Minecraft.java.patch), so the files land before any mod constructor creates its config. They land after mixin bootstrap. |
GameConfigMixin; NeoForge 1.21.1 patch |
| Sided? | Client-only mod (dist = CLIENT), and the files sit in config/defaultoptions/, which no server-side code reads |
— |
| Updating defaults later | Editing the shipped defaults never clobbers a player's live file. Removing them never deletes anything. | copy-if-absent |
Why this split
- Server bleed rules out
preservefor most files.DistantHorizons.tomlcarries[server]and[common.multiThreading]sections that the server-side DH also reads.ssrd-common.tomlis a NeoForge COMMON config: SSRD 1.8.7 registers it withModConfig.Type.COMMONand readsphysicsRenderDistanceserver-side inChunkMapTrackedEntityMixinandSubLevelTrackingSystemMixin. Shipping a client value of 16 into the repo'sconfig/would also change the server's tracking.config/defaultoptions/extra/keeps them client-only. - Both mechanisms are seed-once. Neither reaches an instance that already has the file.
Existing players need a one-time reset (see
../PLAYER-TUNING.md§1). That's a wash. - ModernFix is the exception. It reads
config/modernfix-mixins.propertiesfrom its mixin plugin (ModernFixMixinPlugin), which runs beforeGameConfig, so Default Options is too late on first launch. Ship it enforced (a plain packwiz file, nopreserve). It's a pack-correctness flag, not a preference, and enforcing it keeps it revertible for everyone ifdynamic_resourcesmisbehaves. The server reads it too, butdynamic_resourcesonly patches client resource loading. - Default Options also gives default keybinds and default resource packs (its journal is used only for resource packs), with no index hand-editing and nothing to silently lose on a refresh.
Proposed repo layout (not applied)
config/defaultoptions/options.txt <- out/T-default/options.txt
config/defaultoptions/extra/config/<file> <- out/T-default/config/<file> (all except ModernFix)
config/modernfix-mixins.properties <- out/T-default/config/modernfix-mixins.properties
.packwizignore, appended at the bottom (tested with packwiz refresh in a scratch copy:
it indexes exactly these 11 files and still ignores runtime config/*.json):
# Client perf defaults: seeded by Default Options (copy-if-absent, client only)
!config/defaultoptions
!config/defaultoptions/**
# Read at mixin bootstrap, before Default Options runs: enforced by packwiz
!config/modernfix-mixins.properties
Applied in a01bc3b, minus the ModernFix lines (held for B6). packwiz wrote
side = "client" for Default Options by itself.
2. Tiers
T-default is the pack-wide default and equals T-mid. T-deck and T-high are deltas on
top of it. Every fleet machine is 4C/8T, so the CPU-side levers stay on in every tier.
"High" means a strong GPU, not a strong CPU.
| File | Key | Upstream | Default / mid | Deck | High |
|---|---|---|---|---|---|
| options.txt | renderDistance / simulationDistance | 12 / 12 | 8 / 8 | ||
| biomeBlendRadius | 2 | 1 | 0 | 2 | |
| mipmapLevels | 4 | 2 | 4 | ||
| entityDistanceScaling | 1.0 | 0.75 | 1.0 | ||
| particles / entityShadows | 0 / true | 1 / false | |||
| DistantHorizons.toml | lodChunkRenderDistanceRadius | 256 | 128 | 64 | |
| enableSsao | true | false | true | ||
| numberOfThreads | ceil(nproc/2) = 4 | 2 | |||
| horizontal / vertical quality, transparency, vanillaFadeMode, lodBiomeBlending | MEDIUM / MEDIUM, COMPLETE, DOUBLE_PASS, 3 | LOW / LOW, DISABLED, SINGLE_PASS, 1 (= DH's own LOW preset) | |||
| sodium-options.json | quality.weather_quality / leaves_quality | DEFAULT | FAST / FAST | ||
| entity_model_features.json | animationLODDistance | 20 | 12 | 8 | |
| modelUpdateFrequency | Average | Slow | |||
| retainDetailOnLowFps | true | false | |||
| flywheel-client.toml | flw_backends.lightSmoothness | SMOOTH | TRI_LINEAR | FLAT | SMOOTH |
| lambdynlights.toml | mode | fancy | fast | fastest | |
| light_sources.entities | true | false | false | ||
| soundphysics.properties | environment_evaluation_ray_count / ray_bounces | 32 / 4 | 16 / 2 | ||
| strict_occlusion | false | true | true | ||
| sound_rates.properties | 19 noisy Create sounds | -1 (unlimited) | 4 per tick | ||
| ssrd-common.toml | physicsRenderDistance | 128 | 16 | 32 | |
| modernfix-mixins.properties | mixin.perf.dynamic_resources | false | true (gated on B6) | ||
| sable-client.toml | — | — | unchanged |
Notes:
- Sable: the defaults are already right (
sub_level_skylight_shadows=false). The file is staged only so the bench variantsable-noshade(sub_level_dynamic_shading=false) has a base. Don't ship it unless B4 shows a win. - SSRD: 16 is the floor of SSRD's own slider (
minPhysicsRenderDistance = 16). Players can still raise it in Sodium's video settings. - High-tier DH radius stays 128 because P2 (the fleet's "high" GPU) has only ~8 GB of
free RAM. A player with ≥16 GB free and a 6+ core CPU can go to 192–256
(
PLAYER-TUNING.md). - DH presets are GUI-only (
qualityPresetSettingandthreadPresetSettingareONLY_IN_GUI), so the files set the raw keys. The values are taken from DH's own preset tables (RenderQualityPresetConfigEventHandler,ThreadPresetConfigEventHandler@ 3.3.2). - Stripped: DH
[server] serverIdis a per-installnew Random().nextInt()("DO NOT CHANGE"). It's removed so each install generates its own. DH re-adds missing keys with their defaults (ConfigFileHandler.loadEntry→saveEntry).
3. Key provenance
"Observed" means the file was generated on 2026-09-24 by the same jar version that's pinned
now. Versions were checked against that launch's latest.log and the instance's mods/:
DH 3.3.2, Sodium 0.8.13, EMF 3.3.9, Create 6.0.10 / Flywheel 1.0.6, Sable 2.0.5, SPR 1.5.1.
For DH, all 172 observed values were also diffed against Config.java @ 3.3.2. The only
differences are the two machine-computed ones (numberOfThreads, serverId), so the file
holds no owner edits.
| File | Base | Keys confirmed in |
|---|---|---|
| DistantHorizons.toml | observed (3.3.2) | enum values from EDhApi* classes in the jar; defaults and presets from DH core 3.3.2 source |
| sodium-options.json | observed (0.8.13) | SodiumOptions$LeavesQuality / $WeatherQuality = DEFAULT/FANCY/FAST/FABULOUS (nested mod jar); Gson LOWER_CASE_WITH_UNDERSCORES |
| entity_model_features.json | observed (3.3.9) | ETFConfig$UpdateFrequency = Never/Slow/Average/Fast/Instant (ETF 7.2.4 jar) |
| flywheel-client.toml | observed (1.0.6) | allowed values are listed in the file's own comment |
| sable-client.toml | observed (2.0.5) | — |
| soundphysics.properties | observed (1.5.1) | — |
| sound_rates.properties | template, partial | SoundRateConfig.load() starts from all registered sounds at -1 and overlays file entries (SPR 1.21.1-1.5.1 source). Over-cap sounds play without physics (SoundPhysics.java). Create IDs come from the observed list. |
| lambdynlights.toml | template | DynamicLightsConfig @ v4.8.11+1.21.1. Strings match the nested NeoForge jar. Modes are lowercase off/fastest/fast/fancy; dotted keys become TOML tables; missing keys fall back via getOrElse. |
| ssrd-common.toml | template | Config.java @ SSRD HEAD = 1.8.7 (comments and translation keys match the jar). ModConfig.Type.COMMON from ssrd.class. |
| modernfix-mixins.properties | template | option name and default from the observed file's own option list (5.27.24) |
| options.txt | template, partial | key names from the instance's real 1.21.1 options.txt (version:3955). version is included so vanilla doesn't run the options DataFixer from 0. |
Not drafted (keys unverified this session, Tier 2): Twilight Forest
cloudBlockPrecipitationDistance, Alex's Caves biome_ambient_light_coloring, Jade,
Xaero's, WATUT, Perception, Subtle Effects, FWA.