docs: client-perf analysis, bench harness and work journal

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.
This commit is contained in:
logan committed 2026-10-14 18:15:32 -04:00
1 parent d80f85b3f6
commit 611a1a5952
102 files changed
+25853

No files matched your search

@@ -0,0 +1,182 @@
# 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.
```sh
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
1. **Server bleed rules out `preserve` for most files.** `DistantHorizons.toml` carries
`[server]` and `[common.multiThreading]` sections that the server-side DH also reads.
`ssrd-common.toml` is a NeoForge **COMMON** config: SSRD 1.8.7 registers it with
`ModConfig.Type.COMMON` and reads `physicsRenderDistance` server-side in
`ChunkMapTrackedEntityMixin` and `SubLevelTrackingSystemMixin`. Shipping a client value of
16 into the repo's `config/` would also change the server's tracking.
`config/defaultoptions/extra/` keeps them client-only.
2. **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.
3. **ModernFix is the exception.** It reads `config/modernfix-mixins.properties` from its
mixin plugin (`ModernFixMixinPlugin`), which runs before `GameConfig`, so Default Options
is too late on first launch. Ship it **enforced** (a plain packwiz file, no `preserve`).
It's a pack-correctness flag, not a preference, and enforcing it keeps it revertible for
everyone if `dynamic_resources` misbehaves. The server reads it too, but
`dynamic_resources` only patches client resource loading.
4. 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`):
```gitignore
# 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 variant `sable-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** (`qualityPresetSetting` and `threadPresetSetting` are
`ONLY_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] serverId` is a per-install `new 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.
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Apply a generated variant to a Minecraft instance (game closed!).
python3 apply_variant.py <ID> <path/to/.minecraft> # e.g. C1, dh-r64, T-deck
Copies every config file of out/<ID>/ over the instance. options.txt is
*merged* key by key, so GUI scale, resolution, keybinds etc. survive variant
switches. Prints what changed. Stdlib only; works on Windows too.
"""
import shutil
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
def merge_options(src, dst):
new = dict(l.split(":", 1) for l in src.read_text().splitlines() if ":" in l)
lines = dst.read_text().splitlines() if dst.exists() else []
seen, out, changed = set(), [], []
for l in lines:
k = l.split(":", 1)[0]
if k in new:
seen.add(k)
if l != f"{k}:{new[k]}":
changed.append(f"{k}:{new[k]}")
l = f"{k}:{new[k]}"
out.append(l)
for k, v in new.items():
if k not in seen:
out.append(f"{k}:{v}")
changed.append(f"{k}:{v} (added)")
dst.write_text("\n".join(out) + "\n")
return changed
def main():
if len(sys.argv) != 3:
sys.exit(__doc__)
vid, game = sys.argv[1], Path(sys.argv[2])
src = HERE / "out" / vid
if not src.is_dir():
sys.exit(f"{src} missing - run build.py first")
if not (game / "mods").is_dir():
sys.exit(f"{game} doesn't look like a .minecraft dir (no mods/)")
for f in sorted(p for p in src.rglob("*") if p.is_file()):
rel = f.relative_to(src)
dst = game / rel
if rel.as_posix() == "options.txt":
for c in merge_options(f, dst):
print(f"options.txt {c}")
continue
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(f, dst)
print(f"copied {rel.as_posix()}")
(game / "BENCH_VARIANT.txt").write_text(vid + "\n")
print(f"applied {vid}; recorded in BENCH_VARIANT.txt")
main()
@@ -0,0 +1,355 @@
#!/usr/bin/env python3
"""Generate the staged client-perf configs (tiers + Phase 2 bench variants).
Every override is applied to a *base* file and must hit a key that already
exists in that base, otherwise the build fails. Bases are either the observed
defaults generated by the exact pinned jar (DH 3.3.2, Sodium 0.8.13, EMF 3.3.9,
Flywheel 1.0.6, Sable 2.0.5, SPR 1.5.1 - versions verified against the
2026-09-24 launch log) or hand-authored templates whose keys were read from the
pinned jar's source (see README.md, "Key provenance").
Output: out/<variant>/<path relative to the game dir>, plus out/MANIFEST.md.
python3 build.py # from this directory; stdlib only
python3 build.py --ship # also copy T-default into the repo (see SHIP), then
# run `packwiz refresh` from the repo root
"""
import json
import re
import shutil
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
OBSERVED = HERE.parent / "data" / "observed-default-configs"
TEMPLATES = HERE / "templates"
OUT = HERE / "out"
REPO = HERE.parents[2]
# What `--ship` copies from out/T-default into the repo, and where.
# Default Options seeds these client-side only, and only if the file is absent
# (see README §1). Not shipped: sable-client.toml and sodium-options.json (no change
# from upstream). modernfix-mixins.properties is held back until bench block B6
# passes; when it does, add it here with destination "config/modernfix-mixins.properties"
# (enforced packwiz file, because ModernFix reads it before Default Options runs) and
# re-include it in .packwizignore.
DO = "config/defaultoptions"
SHIP = {
"options.txt": f"{DO}/options.txt",
"config/DistantHorizons.toml": f"{DO}/extra/config/DistantHorizons.toml",
"config/entity_model_features.json": f"{DO}/extra/config/entity_model_features.json",
"config/flywheel-client.toml": f"{DO}/extra/config/flywheel-client.toml",
"config/lambdynlights.toml": f"{DO}/extra/config/lambdynlights.toml",
"config/ssrd-common.toml": f"{DO}/extra/config/ssrd-common.toml",
"config/sound_physics_remastered/soundphysics.properties":
f"{DO}/extra/config/sound_physics_remastered/soundphysics.properties",
"config/sound_physics_remastered/sound_rates.properties":
f"{DO}/extra/config/sound_physics_remastered/sound_rates.properties",
}
# game-dir path -> (base file, format, may_append_new_keys)
FILES = {
"options.txt": (TEMPLATES / "options.txt", "options", False),
"config/DistantHorizons.toml": (OBSERVED / "DistantHorizons.toml", "toml", False),
"config/sodium-options.json": (OBSERVED / "sodium-options.json", "json", False),
"config/entity_model_features.json": (OBSERVED / "entity_model_features.json", "json", False),
"config/flywheel-client.toml": (OBSERVED / "flywheel-client.toml", "toml", False),
"config/sable-client.toml": (OBSERVED / "sable-client.toml", "toml", False),
"config/ssrd-common.toml": (TEMPLATES / "config/ssrd-common.toml", "toml", False),
"config/lambdynlights.toml": (TEMPLATES / "config/lambdynlights.toml", "toml", False),
"config/modernfix-mixins.properties": (TEMPLATES / "config/modernfix-mixins.properties", "properties", False),
"config/sound_physics_remastered/soundphysics.properties": (
OBSERVED / "sound_physics_remastered/soundphysics.properties", "properties", False),
# SPR merges partial files over its generated defaults (SoundRateConfig.load)
"config/sound_physics_remastered/sound_rates.properties": (
TEMPLATES / "config/sound_physics_remastered/sound_rates.properties", "properties", True),
}
DH_Q = "client.advanced.graphics.quality"
DH_G = "client.advanced.graphics"
DH_T = "common.multiThreading"
DH_W = "common.worldGenerator"
# Create 6.0.10 sound IDs (from the observed sound_rates.properties, same Create version).
# Over the cap, SPR plays the sound dry (SoundPhysics.java: setDefaultEnvironment), it is not dropped.
CREATE_NOISY = [
"create:cogs", "create:mixing", "create:mixing_compounded_1",
"create:mechanical_press_activation", "create:mechanical_press_activation_compounded_1",
"create:mechanical_press_activation_belt", "create:mechanical_press_activation_belt_compounded_1",
"create:crushing_1", "create:crushing_2", "create:crushing_3",
"create:saw_activate_stone", "create:saw_activate_wood",
"create:funnel_flap", "create:funnel_flap_compounded_1",
"create:depot_slide", "create:depot_plop", "create:steam",
"create:sanding_short", "create:sanding_long",
]
def sound_caps(n):
return {s.replace(":", "\\:"): n for s in CREATE_NOISY}
# Observed DH values were computed on a 4-thread box; DH's real default is
# ceil(nproc * 0.5) (ThreadPresetConfigEventHandler) = 4 on every 8-thread fleet
# machine. Applied first to every variant so C0 is the fleet's true default.
UPSTREAM = {"config/DistantHorizons.toml": {(DH_T, "numberOfThreads"): 4}}
# Per-install values that must not ship: DH regenerates missing keys with their
# default (ConfigFileHandler.loadEntry -> saveEntry); serverId is new Random().nextInt().
STRIP = {"config/DistantHorizons.toml": [("server", "serverId")]}
# --------------------------------------------------------------------------
# Tiers. "default" is the pack-wide conservative default; deck/high are deltas
# on top of it; mid == default.
# --------------------------------------------------------------------------
DEFAULT = {
"options.txt": {
"renderDistance": 8, # server view-distance is 8; more loads nothing in MP
"simulationDistance": 8, # singleplayer only
"biomeBlendRadius": 1, # 3x3; chunk-build cost (Sodium tooltip)
"mipmapLevels": 2, # block atlas is >=4096^2
"entityDistanceScaling": 0.75, # slider steps are 0.25
},
"config/DistantHorizons.toml": {
(DH_Q, "lodChunkRenderDistanceRadius"): 128, # default 256
(DH_G, "enableSsao"): False, # default true
(DH_T, "numberOfThreads"): 2, # default ceil(nproc*0.5) = 4 on the fleet
},
"config/entity_model_features.json": {
"animationLODDistance": 12, # default 20; lower = more frame skipping
"modelUpdateFrequency": "Slow", # default Average; model refresh, not animation
},
"config/flywheel-client.toml": {
("flw_backends", "lightSmoothness"): "TRI_LINEAR", # default SMOOTH
},
"config/ssrd-common.toml": {
("", "physicsRenderDistance"): 16, # default 128; slider floor is minPhysicsRenderDistance=16
},
"config/lambdynlights.toml": {
("", "mode"): "fast", # default fancy; fast = TickMode.SLOW
},
"config/modernfix-mixins.properties": {
"mixin.perf.dynamic_resources": "true", # GATED on bench block B6
},
"config/sound_physics_remastered/soundphysics.properties": {
"environment_evaluation_ray_count": 16, # default 32
"environment_evaluation_ray_bounces": 2, # default 4
},
"config/sound_physics_remastered/sound_rates.properties": sound_caps(4),
}
DECK = {
"options.txt": {"biomeBlendRadius": 0, "particles": 1, "entityShadows": "false"},
# = DH's own LOW quality preset (RenderQualityPresetConfigEventHandler) + radius 64
"config/DistantHorizons.toml": {
(DH_Q, "lodChunkRenderDistanceRadius"): 64,
(DH_Q, "horizontalQuality"): "LOW",
(DH_Q, "verticalQuality"): "LOW",
(DH_Q, "transparency"): "DISABLED",
(DH_Q, "vanillaFadeMode"): "SINGLE_PASS",
(DH_Q, "lodBiomeBlending"): 1,
},
"config/sodium-options.json": {
"quality.weather_quality": "FAST",
"quality.leaves_quality": "FAST",
},
"config/entity_model_features.json": {
"animationLODDistance": 8,
"retainDetailOnLowFps": False,
},
"config/flywheel-client.toml": {("flw_backends", "lightSmoothness"): "FLAT"},
"config/lambdynlights.toml": {
("", "mode"): "fastest",
("light_sources", "entities"): False,
},
"config/sound_physics_remastered/soundphysics.properties": {"strict_occlusion": "true"},
}
HIGH = { # strong GPU, but still a 4C/8T CPU (true of every fleet machine)
"options.txt": {"mipmapLevels": 4, "biomeBlendRadius": 2, "entityDistanceScaling": 1.0},
"config/DistantHorizons.toml": {(DH_G, "enableSsao"): True},
"config/flywheel-client.toml": {("flw_backends", "lightSmoothness"): "SMOOTH"},
"config/ssrd-common.toml": {("", "physicsRenderDistance"): 32},
"config/lambdynlights.toml": {("light_sources", "entities"): False},
"config/sound_physics_remastered/soundphysics.properties": {"strict_occlusion": "true"},
}
# --------------------------------------------------------------------------
# Phase 2 bench variants. C0 = every file at upstream default (explicit, so stale
# instance edits can't leak in). Everything else is C1 (= T-mid) plus ONE change.
# BENCH is layered on every bench variant: uncapped, vsync off, phase0 pins,
# no DH world generation during runs (world is pre-generated).
# --------------------------------------------------------------------------
BENCH = {
"options.txt": {"maxFps": 260, "enableVsync": "false", "renderDistance": 8,
"simulationDistance": 8},
"config/DistantHorizons.toml": {(DH_W, "generatorPlan"): "DISABLED"},
}
def c1(extra):
return [DEFAULT, extra]
VARIANTS = {
# player tiers (no BENCH layer)
"T-default": ([DEFAULT], False),
"T-deck": ([DEFAULT, DECK], False),
"T-mid": ([DEFAULT], False),
"T-high": ([DEFAULT, HIGH], False),
# bench: composites
"C0": ([], True),
"C1": ([DEFAULT], True),
"C1-deck": ([DEFAULT, DECK], True),
"C1-high": ([DEFAULT, HIGH], True),
# bench: Distant Horizons
"dh-r256": (c1({"config/DistantHorizons.toml": {(DH_Q, "lodChunkRenderDistanceRadius"): 256}}), True),
"dh-r192": (c1({"config/DistantHorizons.toml": {(DH_Q, "lodChunkRenderDistanceRadius"): 192}}), True),
"dh-r64": (c1({"config/DistantHorizons.toml": {(DH_Q, "lodChunkRenderDistanceRadius"): 64}}), True),
"dh-ssao": (c1({"config/DistantHorizons.toml": {(DH_G, "enableSsao"): True}}), True),
"dh-thr4": (c1({"config/DistantHorizons.toml": {(DH_T, "numberOfThreads"): 4}}), True),
"dh-qlow": (c1({"config/DistantHorizons.toml": DECK["config/DistantHorizons.toml"]
| {(DH_Q, "lodChunkRenderDistanceRadius"): 128}}), True),
# bench: Flywheel / ModernFix / Sable / SSRD
"fw-flat": (c1({"config/flywheel-client.toml": {("flw_backends", "lightSmoothness"): "FLAT"}}), True),
"fw-smooth": (c1({"config/flywheel-client.toml": {("flw_backends", "lightSmoothness"): "SMOOTH"}}), True),
"mf-dynres-off": (c1({"config/modernfix-mixins.properties": {"mixin.perf.dynamic_resources": "false"}}), True),
"ssrd-32": (c1({"config/ssrd-common.toml": {("", "physicsRenderDistance"): 32}}), True),
"ssrd-64": (c1({"config/ssrd-common.toml": {("", "physicsRenderDistance"): 64}}), True),
"ssrd-128": (c1({"config/ssrd-common.toml": {("", "physicsRenderDistance"): 128}}), True),
"sable-noshade": (c1({"config/sable-client.toml": {("", "sub_level_dynamic_shading"): False}}), True),
# bench: entities / lights / sound
"emf-lod20": (c1({"config/entity_model_features.json": {"animationLODDistance": 20,
"modelUpdateFrequency": "Average"}}), True),
"emf-lod8": (c1({"config/entity_model_features.json": {"animationLODDistance": 8,
"retainDetailOnLowFps": False}}), True),
"ldl-fancy": (c1({"config/lambdynlights.toml": {("", "mode"): "fancy"}}), True),
"ldl-fastest": (c1({"config/lambdynlights.toml": {("", "mode"): "fastest",
("light_sources", "entities"): False}}), True),
"spr-up": (c1({"config/sound_physics_remastered/soundphysics.properties": {
"environment_evaluation_ray_count": 32, "environment_evaluation_ray_bounces": 4}}), True),
"spr-strict": (c1({"config/sound_physics_remastered/soundphysics.properties": {"strict_occlusion": "true"}}), True),
"spr-nocap": (c1({"config/sound_physics_remastered/sound_rates.properties": sound_caps(-1)}), True),
# bench: vanilla / Sodium
"sod-thr3": (c1({"config/sodium-options.json": {"performance.chunk_builder_threads": 3}}), True),
"opt-blend2": (c1({"options.txt": {"biomeBlendRadius": 2}}), True),
"opt-blend0": (c1({"options.txt": {"biomeBlendRadius": 0}}), True),
"opt-mip4": (c1({"options.txt": {"mipmapLevels": 4}}), True),
}
# --------------------------------------------------------------------------
def toml_val(v):
if isinstance(v, bool):
return "true" if v else "false"
if isinstance(v, (int, float)):
return str(v)
return json.dumps(v)
def apply_toml(text, ov, name):
lines = text.split("\n")
hdr = re.compile(r"^\s*\[([^\]]+)\]\s*$")
for (section, key), val in ov.items():
cur, hits = "", []
for i, ln in enumerate(lines):
m = hdr.match(ln)
if m:
cur = m.group(1).strip()
continue
if cur == section and re.match(rf"^\s*{re.escape(key)}\s*=", ln):
hits.append(i)
if len(hits) != 1:
raise SystemExit(f"{name}: [{section}] {key}: {len(hits)} matches in base")
i = hits[0]
indent = re.match(r"^(\s*)", lines[i]).group(1)
lines[i] = f"{indent}{key} = {toml_val(val)}"
return "\n".join(lines)
def apply_json(text, ov, name):
data = json.loads(text)
for path, val in ov.items():
node, parts = data, path.split(".")
for p in parts[:-1]:
node = node[p]
if parts[-1] not in node:
raise SystemExit(f"{name}: {path} not in base")
if type(node[parts[-1]]) is not type(val):
raise SystemExit(f"{name}: {path} type {type(node[parts[-1]]).__name__} != {type(val).__name__}")
node[parts[-1]] = val
return json.dumps(data, indent=2) + "\n"
def apply_kv(text, ov, name, sep, append):
lines = text.rstrip("\n").split("\n")
for key, val in ov.items():
sval = toml_val(val) if isinstance(val, bool) else str(val)
rx = re.compile(rf"^{re.escape(key)}{re.escape(sep)}")
hits = [i for i, ln in enumerate(lines) if rx.match(ln)]
if len(hits) > 1 or (not hits and not append):
raise SystemExit(f"{name}: {key}: {len(hits)} matches in base")
if hits:
lines[hits[0]] = f"{key}{sep}{sval}"
else:
lines.append(f"{key}{sep}{sval}")
return "\n".join(lines) + "\n"
def strip(text, keys, name):
lines = text.split("\n")
hdr = re.compile(r"^\s*\[([^\]]+)\]\s*$")
for section, key in keys:
cur, hits = "", []
for i, ln in enumerate(lines):
m = hdr.match(ln)
if m:
cur = m.group(1).strip()
elif cur == section and re.match(rf"^\s*{re.escape(key)}\s*=", ln):
hits.append(i)
if len(hits) != 1:
raise SystemExit(f"{name}: strip [{section}] {key}: {len(hits)} matches")
del lines[hits[0]]
return "\n".join(lines)
def render(layers, name):
base, fmt, append = FILES[name]
text = strip(base.read_text(), STRIP.get(name, []), name)
merged = {}
for layer in [UPSTREAM] + layers:
merged.update(layer.get(name, {}))
if fmt == "toml":
return apply_toml(text, merged, name), merged
if fmt == "json":
return apply_json(text, merged, name), merged
sep = ":" if fmt == "options" else "="
return apply_kv(text, merged, name, sep, append), merged
def main():
if OUT.exists():
shutil.rmtree(OUT)
manifest = ["# Generated by build.py - do not edit\n",
"Changed keys per variant, relative to the upstream default of each file.\n"]
for vid, (layers, bench) in VARIANTS.items():
layers = list(layers) + ([BENCH] if bench else [])
manifest.append(f"\n## {vid}\n")
for name in FILES:
text, merged = render(layers, name)
dest = OUT / vid / name
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(text)
for k, v in merged.items():
k = f"[{k[0]}] {k[1]}" if isinstance(k, tuple) else k
manifest.append(f"- `{name}`: `{k}` = `{v}`\n")
(OUT / "MANIFEST.md").write_text("".join(manifest))
print(f"wrote {len(VARIANTS)} variants x {len(FILES)} files to {OUT}")
if "--ship" in sys.argv:
shutil.rmtree(REPO / DO, ignore_errors=True)
for src, dst in SHIP.items():
(REPO / dst).parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(OUT / "T-default" / src, REPO / dst)
print(f"shipped {len(SHIP)} files to {REPO / DO}; now run `packwiz refresh`")
main()
@@ -0,0 +1,16 @@
mode = "fancy"
chunk_rebuild_scheduler = "culling"
[adaptive_ticking]
slow = 5
slower = 8
background_sleep = true
[light_sources]
entities = true
self = true
water_sensitive_check = true
beam = true
guardian_laser = true
sonic_boom = true
glowing_effect = true
@@ -0,0 +1 @@
mixin.perf.dynamic_resources=false
@@ -0,0 +1 @@
# Partial file: SPR merges these over its generated per-sound defaults (-1 = unlimited).
@@ -0,0 +1,12 @@
#The render distance for physics objects (SubLevels) in chunks.
# Default: 128
# Range: 1 ~ 1000000
physicsRenderDistance = 128
#The minimum render distance for physics objects (SubLevels) in chunks.
# Default: 16
# Range: 1 ~ 1000000
minPhysicsRenderDistance = 16
#The maximum render distance for physics objects (SubLevels) in chunks.
# Default: 4096
# Range: 1 ~ 1000000
maxPhysicsRenderDistance = 4096
@@ -0,0 +1,11 @@
version:3955
renderDistance:12
simulationDistance:12
biomeBlendRadius:2
mipmapLevels:4
entityDistanceScaling:1.0
particles:0
entityShadows:true
graphicsMode:1
maxFps:120
enableVsync:true