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:
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
|
||||
+1
@@ -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
|
||||
Reference in new issue
Block a user