Files
onion-mc/analysis/client-perf/proposed-configs/build.py
T
logan f949c169a2 feat(perf): add one-step tier setup script for existing players
Default Options only seeds configs when absent, so existing players never
got the tuned settings. build.py --ship now also writes the Deck/Mid/High
tier files to config/onion-perf/<tier>/, and onion-perf-setup.{sh,ps1,bat}
(installed into the instance folder) backs up and replaces the tier's
config files and merges only the tuned video keys into options.txt,
keeping keybinds, audio and vsync/max-FPS choices.

PLAYER-TUNING: point players at the script; bench B4 GC verdict (G1 for
everyone; ZGC/Shenandoah tested worse); 16 GB PCs 5-6 GB heap.
2026-10-18 15:31:20 -04:00

409 lines
20 KiB
Python

#!/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) and the
# player tiers into config/onion-perf/ (see PERF), 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 ships as an *enforced* packwiz file
# (ModernFix reads it before Default Options runs): bench B6 (proxy) showed
# dynamic_resources -7.6 s startup and better S1/S2 frametimes; owner waived the
# missing-texture check (2026-10-18) - report issues from play.
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",
"config/modernfix-mixins.properties": "config/modernfix-mixins.properties",
}
# Player tiers `--ship` also writes to config/onion-perf/<tier>/ (paths relative to
# the game dir) for onion-perf-setup.sh/.ps1, which existing players run to apply a
# tier over their configs. A tier carries only the files its layers tune, so
# untuned files (sable-client.toml; sodium-options.json except on deck) keep the
# player's own settings. modernfix-mixins.properties is enforced by packwiz already.
PERF = "config/onion-perf"
PERF_TIERS = {"deck": "T-deck", "mid": "T-default", "high": "T-high"} # mid == pack default
PERF_SKIP = {"config/modernfix-mixins.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",
# Harness: suppress DH's "new update available" modal on every launch.
("client.advanced.autoUpdater", "enableAutoUpdater"): False,
},
}
# Resource packs (names = zips in the bench instance's resourcepacks/). Every bench
# variant pins the list: apply_variant.py merges options.txt key by key, so a variant
# without the key would keep the previous session's packs. Later entries win.
FA = "file/FreshAnimations_v1.10.4.zip"
FA_EXT = "file/FA+All_Extensions-v1.8.1.zip"
BETTER_LEAVES = "file/Better-Leaves-9.6.zip"
def packs(*extra):
return {"options.txt": {"resourcePacks": json.dumps(["vanilla", "mod_resources", *extra],
separators=(",", ":"))}}
PACKS_BASE = packs()
# Keys the bench layers may add to a base that lacks them (the options.txt template
# has no resourcePacks line; tiers never set it, so shipped output is unaffected).
ALLOW_NEW = {"options.txt": {"resourcePacks"}}
def c1(*extra):
return [DEFAULT, PACKS_BASE, *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": ([PACKS_BASE], True),
"C1": (c1(), True),
"C1-deck": (c1(DECK), True),
"C1-high": (c1(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),
"C1-fa": (c1(packs(FA)), True),
"C1-fa-ext": (c1(packs(FA, FA_EXT)), True),
"emf-lod20-fa": (c1({"config/entity_model_features.json": {"animationLODDistance": 20,
"modelUpdateFrequency": "Average"}},
packs(FA)), True),
"emf-lod8-fa": (c1({"config/entity_model_features.json": {"animationLODDistance": 8,
"retainDetailOnLowFps": False}},
packs(FA)), True),
"C1-bl": (c1(packs(BETTER_LEAVES)), 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)]
may_add = append if isinstance(append, bool) else key in append
if len(hits) > 1 or (not hits and not may_add):
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 or ALLOW_NEW.get(name, set())), 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`")
shutil.rmtree(REPO / PERF, ignore_errors=True)
for tier, vid in PERF_TIERS.items():
layers, _ = VARIANTS[vid]
names = sorted({n for layer in layers for n in layer} - PERF_SKIP)
for name in names:
dst = REPO / PERF / tier / name
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(OUT / vid / name, dst)
print(f"shipped tier {tier} ({vid}): {len(names)} files to {REPO / PERF / tier}")
main()