Compare commits

...
17 Commits
Author SHA1 Message Date
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
logan cf7ea5617e docs(perf): record owner decisions on Flerovium and spawn tuning 2026-10-18 15:09:41 -04:00
logan 217c346c02 revert(perf): remove Flerovium and the spawn tuning
Run 3 benches (analysis/client-perf/RESULTS-proxy.md): Flerovium's run-2
win did not reproduce (S1 slightly worse, S3 within noise), and the
moderate spawn tuning (d80f85b, 8e8483d) left total mob counts unchanged
across 4 runs per config. Owner approved removing both. The mods now
regenerate their default spawn configs.
2026-10-18 15:06:51 -04:00
logan 4f450e9612 docs(perf): add owner report, run-3 results and player tuning notes 2026-10-18 14:53:23 -04:00
logan c5d25faa7c fix(pack): re-hash spawn config index entries after boot re-save
7841361 refreshed the index while alexsmobs/akashictome configs held their
runtime re-saved contents, so packwiz-installer rejected the committed files
(Hash invalid) and the server could not start.
2026-10-18 14:18:32 -04:00
logan 784136142a feat(perf): ship ModernFix dynamic_resources, add Flerovium and Iris Flywheel Compat
Bench (proxy 9900X/7800XT, see analysis/client-perf/RESULTS-proxy.md):
- ModernFix mixin.perf.dynamic_resources=true: startup -7.6 s, S1 p99
  9.11 -> 8.48 ms, S2 1% low 236 -> 264 fps (B6). Enforced packwiz file
  because ModernFix reads it before Default Options. Missing-texture check
  waived by owner; issues to be reported from play.
- Flerovium 1.1.2 (client): mob-pen S3 p99 ~0.9 ms better (B12b).
- Iris Flywheel Compat 2.4.0 (client): with a shaderpack, Create floor
  p99 13.3 -> 4.0 ms (Flywheel instancing kept under Iris) (B12b).
Deck tier already ships DH's LOW quality preset, so no DH change.
2026-10-18 10:16:45 -04:00
logan a9f33f3a52 docs(bench): add run-2 results (B4h, B7, B12b, B14) and journal 2026-10-18 09:57:30 -04:00
logan 60e10b35a3 fix(bench): reset instance JVM settings for sessions without --jvm
apply_jvm wrote instance.cfg and never restored it, so B5-B12a in run 1
ran on B4's last set (Shenandoah, 6 GB). Within-block verdicts hold
(same JVM on both sides); B7's no-shader baseline is re-run in run 2.

Also: B14 spawn-count automation (b14/: fresh seeded world per run,
offline-located biome sites, datapack entity counts, journaled config
variants) and RESULTS-proxy.md with run-1 verdicts.
2026-10-17 22:28:20 -04:00
logan d20b0fb11a docs(journal): matrix run 1 complete; 06:00 pause notes 2026-10-17 06:06:09 -04:00
logan ccc9695250 feat(bench): add --since filter to analyze; journal session 2026-10-17 01:35:41 -04:00
logan 4fa9a6ab81 feat(bench): automate B7 shader profiles, wire B12b trials, settle 45s
- gen_shader_profiles.py + shader-profiles/: Iris per-pack option files
  resolved from each pack's profile.* lines (Iris has no profile key)
- bench.py: flag rows shader_not_loaded when Iris falls back to vanilla
  rendering (analyze.py treats it as invalid)
- matrix: shader_options session key, trial jar globs pinned to the
  downloaded Modrinth builds, post-join settle 45 s (B0 v1 showed rep 1
  still warming at 10 s)
2026-10-16 23:34:52 -04:00
logan a15c912ad0 feat(bench): add matrix runner, gamescope and per-session controls
- bench.py: gamescope-headless display (default, 1920x1080; ~2% cost vs
  desktop for isolation from the shared Wayland desktop), --shader,
  --dh, --disable-mods/--add-mods, --world-zip, crash-safe restore
  journal, spectator mode in the plan (elevated cameras fell to the
  sea floor and S4 measured underwater darkness)
- matrix.toml/matrix.py: declarative, resumable B0-B14 runner keyed on
  mods hash (warns when B0 must be re-run)
- build.py: bench-only resource-pack variants (C1-fa, C1-fa-ext,
  emf-lod*-fa, C1-bl); every bench variant pins resourcePacks so packs
  don't leak between sessions; shipped tier output unchanged
- analyze.py: tagged labels use their own block's baseline
2026-10-16 22:14:45 -04:00
logan 8f56e2f780 docs(journal): log WATUT fix and harness v2 session 2026-10-15 17:08:29 -04:00
logan e18bd3ccbb feat(bench): add datapack-driven harness v2 with boot checks
xdotool chat injection silently fails under KDE Wayland/Xwayland, so the
v1 harness recorded runs whose scene commands never executed. v2 drives
scenes from a generated world datapack, detects crashes, aligns MangoHud
CSVs by file birth time, flags vsync/backup/low-n windows, and records
full run metadata to JSONL; analyze.py applies the BENCH-MATRIX win rule.
2026-10-15 17:07:52 -04:00
logan a84319bb2f fix(mods): add MidnightLib 1.10.0 to fix startup CME crash
Fright's Delight jar-in-jars MidnightLib 1.9.2, whose MidnightConfig.init
races during NeoForge parallel mod construction (ConcurrentModificationException).
The broken mod state left mod_resources unloaded, so WATUT's reloadShaders
mixin crashed on particle.json - WATUT was the victim, not the cause.
1.10.0 synchronizes the entries map (upstream 1fe69729); FML selects it over
the JiJ copy. Verified: 10/10 clean cold launches to world join.

Also adds analysis/state/ (Phase 2 state capture).
2026-10-15 17:07:52 -04:00
Onion Agent e5e369a693 chore(bench): add client bench harness and Phase 2 handoff
The client now boots cleanly on the bench machine (the CAC spawn-key
crash was fixed in 8e8483d). Add the harness used to drive bench scenes
and capture frametimes, the bench-world generation workflow, and a
self-contained handoff brief. All files live under analysis/ (packwizignored),
so nothing player-facing changes.

- harness/rcon.py: minimal RCON client for the temp worldgen server
- harness/run_scene.py: apply variant -> launch Prism under MangoHud ->
  xdotool scene commands -> p50/p95/p99/1%-low from the CSV
- HANDOFF.md: state, gotchas, next steps
- build.py: disable DH autoUpdater for bench variants (suppress modal)
2026-10-14 23:16:31 -04:00
logan 8e8483d072 fix(config): correct Critters and Companions spawn keys to snake_case
YACL serialises config with FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES
(and serializeNulls), so the camelCase keys biomeSpec, leafInsect, seaBunny,
stagBeetle, rolyPoly and stickBug were never read. Every SpawnEntry then
deserialised with biomeSpec = null, and CAC 2.7.0's SpawnEntry.isTag() calls
biomeSpec.startsWith("#") without a null check, crashing FMLCommonSetup on
both the dedicated server and the client.

Rename the keys; all tuned weights and biome specs are unchanged. Missing
keys still fall back to mod defaults (partial file). Refresh the index and
pack hashes.
2026-10-14 20:55:13 -04:00
79 changed files with 12037 additions and 130 deletions

No files matched your search

+1
View File
@@ -7,3 +7,4 @@ immersive_paintings_cache/
# Large or regenerable local artifacts
*.tar.gz
analysis/client-perf/proposed-configs/out/
__pycache__/
+13 -5
View File
@@ -65,8 +65,16 @@ config/paxi/datapacks/*
# Default Options copies them into place client-side, only when the target is absent.
!config/defaultoptions
!config/defaultoptions/**
# Server spawn tuning (analysis/client-perf/SPAWN-TUNING.md); partial files, the mods
# fill in defaults for every other key. Enforced: the server reads them from here.
!config/alexsmobs-common.toml
!config/fowlplay.json5
!config/crittersandcompanions-common.json5
# Spawn tuning reverted 2026-10-18: bench B14 showed no measurable load reduction.
# ModernFix mixin overrides (build.py SHIP; enforced - ModernFix reads it before
# Default Options runs). dynamic_resources=true per bench B6.
!config/modernfix-mixins.properties
# Player tier overrides (build.py --ship) plus the per-machine setup script that
# applies a tier to an existing install (see PLAYER-TUNING.md §1). Must come after
# the blanket config/* and *.json patterns above (last matching pattern wins), and
# the directory itself must be re-included before anything under it.
!config/onion-perf
!config/onion-perf/**
!onion-perf-setup.sh
!onion-perf-setup.ps1
!onion-perf-setup.bat
+188
View File
@@ -411,3 +411,191 @@ machine has.
2. Build the bench world (§2), then run B0 → B14. Write the winners into `build.py`, ship,
refresh, commit.
3. Report shader-policy data (B7) and any trial-mod verdicts (B12b) to the owner.
## Session 2026-10-15 — Phase 2 begins: client boots, bench harness built
Machine: **Manjaro**, Java 21 (OpenJDK), Prism 11.0.3, RX 7800 XT / Ryzen 9 9900X / 91 GB.
Unlike the previous machine this one runs the client. Full brief: `analysis/client-perf/HANDOFF.md`.
### Boot check (client) — essentially PASS
Pack reaches the main menu cleanly (ModernFix "Game took 29.3 s"; atlas 4096x4096x2;
Default Options seeds `options.txt` correctly). No missing deps, no mixin apply failures,
no `ftbteams`, no fishermans-haven errors, no YACL "Failed to deserialize". 22 benign ERRORs.
The one real blocker — **Critters & Companions spawn keys camelCase vs YACL's snake_case
policy**, NPE in `CACSpawnConfig$SpawnEntry.isTag`, broken mod state — was fixed and pushed
by the owner as **`8e8483d`** while this session ran. Alex's Mobs / Fowl Play spawn tuning
verified applied in-game.
**Open:** intermittent **world-load** crash in WATUT
(`ShaderInstanceBlur` → `FileNotFoundException: minecraft:shaders/core/particle.json`).
First load worked, next crashed. Needs repro; owner says remove no mods.
### Built this session (all under `analysis/client-perf/`, packwizignored)
- `harness/rcon.py`, `harness/run_scene.py` — apply variant → launch Prism into the bench
world under user-local MangoHud → drive scenes via xdotool → slice p99/1%-low from CSV.
Verified end-to-end once (C1/S1, but vsync-locked; `vblank_mode=0` added after).
- User-local MangoHud 0.8.4 (`~/.local/mangohud`, wrapper `~/.local/bin/mangohud-local`).
- Bench world `saves/onionbench` generated by temp server `~/onion-bench/server-bench`
(S2 Create floor, S3 mob pen placed+verified). S4 airships not built.
- `build.py` BENCH layer: `enableAutoUpdater=false` (suppress DH update modal). Tiers
unchanged; **nothing shipped**.
### Not done
The matrix blocks B0–B14. Fleet P0/P1/P2 are unreachable → any data here is a high-tier
proxy. Only one non-decision-grade run exists. Next agent: kill the WATUT crash, confirm
uncapped frametimes, then run B0→B14 per `BENCH-MATRIX.md`.
### Pushed
`8e8483d` (owner, CAC keys). This session's analysis/harness commit follows; it touches no
shipped file.
## Session 2026-10-14 (d) — state capture for handoff (orchestrator)
No pack changes; HEAD stayed `e5e369a`. Three read-only scouts inventoried the
interrupted session's artifacts; findings folded into the new **`analysis/state/`**
directory (6 markdown files: README, pack+environment, timeline, current-state, plan,
artifact-map) — that directory is now the canonical onboarding point for a new agent.
Facts established this session (all evidence-backed):
- The 20:34 FML crash report is the **CAC spawn-key NPE** (fixed by `8e8483d`) — *not*
the WATUT crash. No crash report exists for WATUT yet; it remains open and blocking.
- The dedicated server from the boot check is **still running** (tmux `onion-mc`,
PID 1699563, since 21:01) with an unidentified RCON keepalive polling every 30 s.
Left alone per rules.
- Working-tree diffs (`akashictome`, `alexsmobs` configs) are benign boot re-saves:
whitelist intact; tuned spawn values retained. Per rule, restore before staging —
not committed.
- Bench instance `~/onion-bench/prism/instances/OnionBench` is fully provisioned
(`.minecraft`, `saves/onionbench` 26 MB, Default Options seeded, bench pins,
`BENCH_VARIANT.txt = C1`). One earlier scout misread it as unprovisioned — verified
false by direct `ls`.
- Jar cache `/tmp/onion/jars` is gone (ephemeral; `analysis/audit/fetch.py` rebuilds).
State of the effort at capture: **Phase 2 begun, zero matrix blocks run.** Next agent:
follow `analysis/state/04-plan.md` §"Next actions" (WATUT → verify uncapped frametimes
→ finish S4 → B0→B14 → ship → report). `analysis/state/` itself is uncommitted — fold
it into the next commit.
## Session 2026-10-15 (b) — WATUT crash root-caused + fixed; harness v2
Commits: `a84319b` (fix(mods): MidnightLib 1.10.0 + `analysis/state/`), `e18bd3c`
(feat(bench): harness v2). Not pushed.
- **WATUT crash = victim, not cause.** Fright's Delight 1.4.8 jar-in-jars MidnightLib
1.9.2; its `MidnightConfig.init` races under NeoForge parallel mod construction →
`ConcurrentModificationException` → broken mod state → `mod_resources` never loads →
WATUT's unguarded `reloadShaders` mixin throws on `particle.json`. Fix: standalone
`midnightlib` 1.10.0 (`side=both`; upstream fix 1fe69729, synchronizedMap). FML picks
it over the JiJ copy (log: "Attempted to select a dependency jar ... Using Mod File").
Verified 10/10 cold launches to world join, 0 CME, all on 1.10.0. Frightsdelight
doesn't touch `entries` (binary-compat risk checked). **Dedicated server still runs
1.9.2 until its next restart + jar install** — server-side exposure untested.
- **Uncapped frametimes verified**: C1/S1 p50 2.9 ms, p99 9.5 ms, avg 297 fps
(harness v2 smoke, `~/onion-bench/results/session.jsonl`, label `smoke`). The earlier
60 fps row in `harness-results.csv` is void.
- **Harness v1 was silently broken**: KDE Plasma is Wayland; xdotool into Xwayland `:1`
drops input when a native window has focus → no `/function`, no markers, ever. Also:
partial `options.txt` re-triggered the accessibility onboarding screen (blocks
`--world`), and FTB Backups fired ~7 s after every join (inside capture windows).
- **Harness v2** (`analysis/client-perf/harness/bench.py`, `analyze.py`, `tests/`,
README): `boot --n N --join` reliability loop; `session` drives scenes via a
per-session generated datapack (armed only during the session, disarmed in
`finally`), log markers with ms timestamps, CSV picked by birth time, validity flags,
full metadata JSONL; `analyze.py` applies the BENCH-MATRIX win rule. 18 offline tests.
- Bench-instance-only edits: FTB Backups `auto: false`
(`config/ftbbackups3-server.snbt`); harness pins in `options.txt` (re-applied per run).
- Observed noise: the idle dedicated server uses 23–78 % of a core; amdgpu DPM is
`auto`; the bench window shares the owner's live desktop.
Next: owner calls on noise controls (see harness README / session report), then S4
build → B0 (`bench.py session --variant C0 --plan s1 --reps 3 --label B0`, ×3
sessions) → matrix.
## Session 2026-10-16 — bench world finished, harness v3, matrix running
Owner: stop the dedicated server while benching (done via console `stop`; tmux
`onion-mc` is gone — restart with `./start.sh` in tmux when benching ends; packwiz will
pull MidnightLib 1.10.0, jar also dropped in `mods/`). Gamescope integration approved.
- **Harness**: `--display gamescope-headless` default @1920x1080 (~2 % cost vs desktop,
isolates from the shared KDE Wayland desktop). Per-session `--shader/--shader-options/
--dh/--disable-mods/--add-mods/--world-zip` with a crash-safe restore journal
(`bench.py restore`). Plan forces **spectator** — elevated scene cameras previously fell
(S4 measured the sea floor at y=34). `shader_not_loaded` flag. `matrix.py` +
`matrix.toml` = resumable B0–B14 runner; B7 profiles generated from pack
`profile.*` lines (`gen_shader_profiles.py`); B12b trial jars in `~/onion-bench/trials/`.
Post-join settle 45 s (rep 1 still warming at 10 s). Commits `c6341d0`, `4fa9a6a`.
- **Bench world** built headlessly on `~/onion-bench/server-bench` via RCON:
S4 = 4 Sable sub-levels (5×7×15 planks/glass, levitite floor + 2 interior layers —
the 1-layer design sank ~14 blocks per physics wake), via `/sable assemble area`;
stable at y≈169.24 across restarts and a 60 s client session. Removed 370 stray
force-loaded chunks (S2/S3/S4 ticked in every scene). DH LODs pre-generated with
`/dh pregen start minecraft:overworld <x> 0 256` at x=0 and x=3680 (1.2 GB sqlite).
S2 rotates (Speed 16 sampled), S3 = 30 villagers.
Zips in `~/onion-bench/worlds/` (sha256):
`bench-world-v1.zip` 144caf5fcdfea1a0eac0c787fdcded949e1a67df6ac112400f37be30ce7fb76e,
`-s4-1.zip` 213b9b8871be527d0b5ac2e34615e8c2c1e3b206277fb37549a3014e2b895a38,
`-s4-0.zip` 6b2a2bbd16f9f2e5aba34d53b09752d30fb7f9c3a97c24c46131b54d12bdc234.
- **Matrix** run 1 (tmux `bench-matrix`, log `~/onion-bench/results/matrix-run1.log`):
B0,B1,B2,B3,B4,B5,B6,B7(noshader),B8–B12a. B0 noise (settle 45): p99 spread 0.40 ms,
1%-low spread 6.6 fps. **B2: C1 beats C0 on every scene** (S1 p99 8.23 vs 9.33;
S3 1%-low 186 vs 110) → no bisect stop. Early B3: dh-qlow wins S1, dh-ssao loses,
dh-thr4 loses S5 (threads stay 2). Analyze with `--since 2026-10-16T22:24`.
- Remaining after run 1: B4h (`--param gc=`), B7 shader rows, B12b, B13, B14, ship,
owner report. Analysis proxy-only; noise floor gpu_dpm_auto (no root).
- **2026-10-17 06:00 pause**: matrix run 1 finished first (**56/56 ok, 0 failures**,
B0–B12a incl. proxy B5). Timer `pause-bench-start-server.sh` started the server, but
tmux was born inside the oneshot service's cgroup and systemd killed it (and the
server, SIGKILL 1 s after `Done`, no players) when the unit exited. Restarted 06:05 in
its own scope (`systemd-run --user --scope tmux …`, unit `onion-mc-tmux.scope`);
script fixed the same way. **Server is up (25565).**
- B4 GC verdict: J1 vs J0 S5 1%-low +1.3 fps < 6.6 spread → tie → **J0**.
- Resume (stop the server first): `cd analysis/client-perf/harness &&
python3 matrix.py run B4h,B7,B12b --param gc=J0` (20 sessions, ~3.5 h). Then B13
(manual spark), B14 (manual spawn counts), ship, owner report.
## Session 2026-10-18 — run 2 + B14 done; server back
Overnight: 23:25 pause (owner) → 01:26 resume timer → matrix run 2 finished 03:18
(B4h, B7, B12b; all sessions ok after retries) → B7.noshader re-run on default JVM →
B14 defaults + shipped (33 rows each, biome_ok, config check ok) → server up 04:31.
06:00 safety timer saw the server running and left it (its "not Done" warning was a
scrolled-pane false alarm; readiness check now uses port 25565).
Results (full tables in `analysis/client-perf/RESULTS-proxy.md` §Run 2):
- B4h: smallest heap within spread = **5 GB** (4 GB loses S5) → PLAYER-TUNING note.
- B7: shaders' big cost is S2 — Flywheel backends drop (974 → ~120 fps avg) for every
profile; MakeUp shadowless near-free elsewhere, Complementary ~2× on S4.
- B12b: **Iris Flywheel Compat wins hugely under shaders** (S2 p99 13.3 → 4.0);
**Flerovium wins S3** (entities; B12b's S3 baseline anomalous, so confirm once).
Gnetum tie; BBE, Super Resolution, Ixeris (+1 crash), Cull Leaves rejected.
- B14 (n=1): rule says back off, but driven by one goose/duck herd at the warm-ocean
site; sea bunny never spawned in either config → inconclusive, needs ≥3 runs/config.
- Super Resolution's first launch rewrote bench `fml.toml` (`earlyWindowControl=false`,
unjournaled) → reset. Harness should snapshot/diff the whole config tree per session.
Remaining: B6 missing-texture check → ship modernfix-mixins; add Iris Flywheel Compat +
Flerovium (visual check, then `packwiz modrinth add`, then re-run B0); DH low-quality
visual check; B13 spark; B14 ×3 more; ship via build.py; owner report.
## Session 2026-10-18 (b) — shipped, run 3, owner report
- Shipped `7841361`: ModernFix dynamic_resources (enforced config; owner waived texture
check), Flerovium 1.1.2, Iris Flywheel Compat 2.4.0. Deck already had DH LOW.
- **Incident**: 7841361 refreshed the index while alexsmobs/akashictome held boot
re-saves → server packwiz install "Hash invalid" → server down 14:06–14:18. Fixed
`c5d25fa`. Rule: `git checkout -- config/` **before** `packwiz refresh`, not just
before staging.
- Run 3 (chain-0418): B0v2 noise p99 0.41 ms / 1%-low 1.6 fps. **Flerovium did not
hold up** (S1 loss, S3 too noisy) → recommend removal (owner decision pending).
B14 ×4: totals ~flat (jungle 130→128, river 208→212, warm ocean 145→136) → spawn
tuning harmless but ineffective; keep/revert is the owner's call.
- Docs: `analysis/client-perf/OWNER-REPORT.md` (new), RESULTS-proxy §Run 3,
PLAYER-TUNING (5 GB heap; Iris Flywheel Compat under shaders).
- Verified ModernFix honors the shipped override on the server boot log.
- Open: owner decisions (Flerovium, spawn tuning); B13 spark (optional); fleet
confirmation; push when the owner says so.
- Owner approved: **Flerovium removed, spawn tuning reverted** (`217c346`). Server was
stopped while the three spawn files were deleted so it couldn't keep re-saved tuned
copies; restarted, Alex's Mobs regenerated defaults (gazelle weight 40). Bench
instance jar removed too. b14.py's `shipped` config now has nothing to ship.
+109
View File
@@ -0,0 +1,109 @@
# Onion MC client-perf — handoff (2026-10-15)
Self-contained brief for the next agent. Read `.pi/journal.md` (last sections) and
`BENCH-MATRIX.md` too, but this file is the live state on this machine.
## Goal
Finish the **Phase 2 client-performance loop**: measure the staged configs, decide by
`BENCH-MATRIX.md`'s rules, write winners into `proposed-configs/build.py`
(`DEFAULT` / `DECK` / `HIGH`), `build.py --ship`, `packwiz refresh`, commit (Conventional
Commits) and push. Pushes go live to players on their next Prism launch.
## Repo / pack state
- Pack: **217 mods**, MC 1.21.1, NeoForge 21.1.251. Git `HEAD == origin/main == 8e8483d`.
- `8e8483d` fixed the one real boot blocker: the Critters & Companions partial spawn config
used camelCase keys (`biomeSpec`) but YACL serialises with
`FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES`; every entry loaded `biomeSpec = null` and
`CACSpawnConfig$SpawnEntry.isTag()` NPE'd in `FMLCommonSetupEvent`, leaving the client in a
"broken mod state" (119 refused events). Fixed to snake_case; **already pushed**.
- The Alex's Mobs (TOML) and Fowl Play (YACL camelCase) spawn files are correct and were
verified applied in-game. Do not commit the mod/server **full re-saves** of
`config/*` (the running server rewrites them): always `git checkout -- config/` before
staging.
- Nothing was shipped this session. Shipped `config/defaultoptions/**` is unchanged from
`a01bc3b`. `build.py` was only patched in the **BENCH** layer (below); the tiers are
untouched.
## Boot check (client, this machine) — essentially PASS
Reached main menu cleanly. `Sound engine started` 01:26:53; ModernFix "Game took 29.3 s to
start"; blocks atlas `4096x4096x2`; Default Options correctly seeded `options.txt`
(renderDistance 8, maxFps 120, vsync true). No missing deps, no mixin apply failures, no
`ftbteams` refs, no fishermans-haven biome errors, no YACL "Failed to deserialize". 22 ERROR
lines, all benign (pride access-transformer, Controlify @Shadow, FTB Library legacy
textures, 4 GeckoLib molang parses, icys-better-horses pack path, Xaero version expiry,
Veil failing to recompile 2 lodestone `nineslice` shaders).
**Open boot-adjacent issue (not yet fixed):** an intermittent crash on **world load**:
```
java.io.FileNotFoundException: minecraft:shaders/core/particle.json
at com.corosus.watut.ShaderInstanceBlur.<init>(ShaderInstanceBlur.java:27)
at GameRenderer.handler$jhl000$watut$onLoadShaders(...)
```
First world load succeeded and measured; the next crashed during "Rendering overlay". WATUT
is a LEVERS §5 removal candidate but the owner says **remove no mods** — look for a WATUT
config/patch or a reliable retry. Confirm whether real players hit it before shipping.
## Tooling installed on this machine (all user-local, no root)
- **Prism Launcher 11.0.3** (`/usr/bin/prismlauncher`). Data root `~/onion-bench/prism`,
instance id **`OnionBench`**, game dir
`~/onion-bench/prism/instances/OnionBench/.minecraft` (214 jars). A valid Microsoft
account is in `~/onion-bench/prism/accounts.json` (device-code login, owner-approved).
- **MangoHud 0.8.4** extracted to `~/.local/mangohud`; wrapper **`~/.local/bin/mangohud-local`**
(exports `MANGOHUD_CONFIG`, `LD_PRELOAD` shim, `vblank_mode=0`). CSVs land in
`~/onion-bench/mangologs/`. The instance has `WrapperCommand=mangohud-local`.
- **Bench world**: `~/onion-bench/prism/instances/OnionBench/.minecraft/saves/onionbench`
(seed `onionbench`, creative, cheats). S2 Create floor (2304 kinetic BEs) and S3 mob pen
(30 villagers/15 cows/15 sheep/10 allays) were placed via RCON and verified. S4 airships
NOT built. Datapack lives in the world's `datapacks/onion-bench`.
- **Temp server** for worldgen: `~/onion-bench/server-bench` (symlinks repo `mods`/
`libraries`, copies `config`), port 25566 / RCON 25576, currently **stopped**.
- **Harness** (this is new, untracked, packwizignored): `analysis/client-perf/harness/`
- `rcon.py` — minimal RCON client.
- `run_scene.py` — applies a variant, launches Prism into the bench world under MangoHud,
drives scenes with xdotool chat, slices the CSV into p50/p95/p99/1%-low. Verified working
end-to-end once (C1/S1).
## Measurements so far — NOT decision-grade
One run: **C1 / S1** p50 16.72 ms, p99 17.24 ms, peak 17.60 ms, avg 59.8 FPS, 1% low 57.5,
n=1889. It was **vsync-locked at ~60 FPS** (tight 16.7 ms floor). `vblank_mode=0` was added
afterwards but the next launch crashed on world load, so uncapped data is not collected yet.
Treat all of this as pipeline validation only.
## Next steps (in order)
1. Resolve the WATUT world-load crash; re-run one C1/S1 and confirm FPS > 60 (vblank off).
2. Run the matrix in order, per `BENCH-MATRIX.md`: **B0** noise floor (C0/S1 ×3) → **B1**
baseline (C0, S1–S6) → **B2** composite (C1, and C1-deck/C1-high) → **B3** DH radius
(`dh-r256`, `dh-r192`, `C1`, `dh-r64`, `dh-off` on S1/S5) → **B4** heap/GC (use `jcmd`/
`jstat`, or spark, at 5/6 G and J0–J3) → **B6** ModernFix → **B7** shaders → **B8**
entities → **B9** Flywheel → **B10** SPR → **B11** Sodium/blend → **B12a/b** ablation and
trials → **B13** spark profile → **B14** spawn counts.
3. Write winners into `build.py`, `python3 build.py --ship`, `~/go/bin/packwiz refresh`,
commit. Ship `config/modernfix-mixins.properties` only if B6 passes (see SHIP comment).
4. Update `PLAYER-TUNING.md` (tiers, heap/GC, shader profiles), `LEVERS.md`,
`BENCH-MATRIX.md`, and append a journal section.
## Fleet note
P0 (Steam Deck), P1 (7700K/1060) and P2 (6700K/1080 Ti) are **not reachable** from this box
(no routes / no SSH targets). Everything measured here is a **high-tier proxy** on a Ryzen 9
9900X + RX 7800 XT, 91 GB RAM. Label results as proxies; absolute FPS floors do not map to
tier decisions.
## Gotchas learned this session
- Prism 11 `--offline` fails a fresh instance with "Required libraries are missing"
(`EnsureOfflineLibraries`). Launch **online** once to download libs/assets; after that
offline works.
- `xdotool windowactivate --sync` **hangs** under kwin; use `windowactivate` + `windowfocus`.
- MangoHud needs `MANGOHUD_CONFIG` set explicitly and `LD_PRELOAD` **exported**.
- The owner's dedicated server runs from the repo root (`java @user_jvm_args.txt
@libraries/.../unix_args.txt`, port 25565). Never kill it; never touch `world/`. The temp
bench server used separate ports/paths and is stopped.
- `analysis/` is packwizignored (safe to commit); `config/` is server-rewritten (restore
before staging).
+102
View File
@@ -0,0 +1,102 @@
# Client performance — owner report (2026-10-18)
Plain-language summary of the Phase 2 benchmarks. Full tables and method:
[`RESULTS-proxy.md`](RESULTS-proxy.md). Player-facing advice:
[`PLAYER-TUNING.md`](PLAYER-TUNING.md). All numbers come from **one stand-in machine**
(Ryzen 9 9900X / RX 7800 XT, 1920×1080), not the fleet machines — see Caveats.
## TL;DR
- The tuned defaults we already ship beat upstream defaults in **every** test scene, and
start the game about 6 s faster.
- This session shipped faster startup (ModernFix dynamic resources, −7.6 s) and a fix that
keeps Create's fast renderer working with shaders on (Iris Flywheel Compat).
- Shaders stay **opt-in**. MakeUp shadowless is nearly free except on big Create builds;
Complementary is expensive. Fresh Animations is the most expensive optional visual.
- Players should give Minecraft **5 GB** of memory — 4 GB is measurably worse, more is not faster.
- Two decisions for you: **remove Flerovium** (its win didn't hold up — I recommend yes), and
**keep or revert the spawn tuning** (harmless, but it doesn't measurably cut mob load).
## What shipped
| Commit | Change |
|---|---|
| `7841361` | ModernFix `mixin.perf.dynamic_resources=true` (enforced `config/modernfix-mixins.properties`); added **Flerovium 1.1.2** and **Iris Flywheel Compat 2.4.0** (both client-only) |
| `c5d25fa` | Fix: `7841361`'s pack index held hashes of server-re-saved spawn configs, so the server's install failed ("Hash invalid") and it didn't start at 14:06. Re-hashed; server up 14:18. Nothing was pushed. |
The Deck tier already uses Distant Horizons' LOW quality preset — the setting that won the
DH test — so no DH change was needed. Nothing has been pushed yet.
## Measured gains
| What | Before | After |
|---|---|---|
| Tuned defaults vs upstream — vista p99 frametime | 9.33 ms | 8.23 ms |
| Tuned defaults vs upstream — mob pen 1 % low | 110 fps | 186 fps |
| Tuned defaults vs upstream — startup | 31.4 s | 25.4 s |
| ModernFix dynamic resources — startup | 33.9 s | 26.3 s (plus better frametimes) |
| Iris Flywheel Compat — Create floor with MakeUp shaders, p99 | 13.26 ms | 3.98 ms |
| Iris Flywheel Compat — same scene, average fps | 117 | 644 |
(Lower p99 = fewer stutters; higher 1 % low = smoother worst moments.)
## Shaders
- Keep them **opt-in** (the pack downloads them; nothing turns them on).
- **MakeUp – Ultra Fast, shadowless profile** is close to free on terrain and airships.
- Without help, every shaderpack turns off Create's fast renderer: the Create floor dropped
from ~974 to ~120 fps average in every profile. **Iris Flywheel Compat**, now shipped, fixes
that (table above). It's an unofficial mod, so it may break when Create or Iris update.
- **Complementary** is costly: about 2.7 ms worse p99 on the vista and about twice the frame
time on the airship scene.
- Shader **shadows corrupt airships** (Iris issue #3164) — recommend shadowless profiles.
## Fresh Animations
Mob-pen p99 goes from 4.32 ms to **8.65 ms** with Fresh Animations, and **10.20 ms** with
the Extensions pack too. Keep it opt-in; Deck players shouldn't use it.
## Memory
**5 GB** is the smallest heap that performed as well as the best. 4 GB lost about 23 fps of
1 % low on the teleport tour; 6 and 8 GB were no better. Players set this in their launcher.
## Spawn tuning
Measured 4 times per config at three biome sites. Total mob counts barely changed
(jungle 129.5 → 128.0, river 208.0 → 211.5, warm ocean 145.0 → 136.0). The tuned species
are a bit rarer, as intended, and nothing really disappeared (the automated "back off"
flag came from species that only ever showed up once per run). **It's harmless, but it
doesn't measurably reduce the load.** No config was changed.
## Decisions needed from you
> **Resolved 2026-10-18:** both removed in `217c346` (Flerovium dropped; spawn tuning
> reverted, the mods regenerated their defaults on the server).
1. **Remove Flerovium?** Its earlier win was measured against an unusually bad baseline.
On a fair re-test it was worse on the vista and a tie on the mob pen. **Recommended: yes.**
2. **Spawn tuning: keep or revert?** Keeping it costs nothing; reverting returns the mods'
own spawn rates. Either is fine performance-wise.
## Caveats
- **One stand-in machine.** The fleet machines (Steam Deck, 7700K + GTX 1060, 6700K +
1080 Ti) were unreachable, so results are relative and unconfirmed on real player
hardware. This GPU makes some costs (airships, DH distance) look cheaper than they are
on older cards.
- GPU clocks weren't pinned (no root access), which adds a little noise.
- A harness bug ran part of the first run on the wrong Java settings. Comparisons inside
each test are still fair; details in `RESULTS-proxy.md`.
## Not done
- Spark CPU profile of the worst scene (B13).
- Confirmation on the fleet machines.
- The airship decay test (B5), which needs the fleet machines.
## Please report texture problems
ModernFix dynamic resources loads textures and models on demand. We skipped the
missing-texture check at your request, so if anyone sees purple/black "missing" blocks or
items in play, report the block/item name.
+34 -23
View File
@@ -1,7 +1,7 @@
# Onion MC: performance settings for players
These are things the pack can't set for you, plus the settings to change for your machine.
Values marked *(provisional)* are waiting on Phase 2 benchmarks and may change.
All values below are final (benchmarked in Phase 2; see `RESULTS-proxy.md` in this folder).
## 1. Which tier are you?
@@ -12,24 +12,34 @@ Values marked *(provisional)* are waiting on Phase 2 benchmarks and may change.
| **High** | 8 GB+ VRAM card | GTX 1080 Ti, RTX 3060+ |
The pack's defaults are the **mid** tier. They're applied only when the config file doesn't
exist yet. **If you played before the defaults shipped**, close the game and delete these
files from the instance folder to pick them up on the next launch. This resets those mods'
settings.
exist yet. **If you played before the defaults shipped**, close Minecraft and run the
tier setup script in the instance folder to apply a tier over your current settings:
- **Windows:** double-click `onion-perf-setup.bat` in the instance folder.
- **Linux / Steam Deck / macOS:** run `sh onion-perf-setup.sh` in the instance folder.
Pick your tier when prompted. The script backs up everything it changes (to
`onion-perf-backup/<date-time>/`), replaces these 7 config files (the **deck** tier also
ships `config/sodium-options.json`; **high** has the same 7 as mid), and merges the tuned
video keys into `options.txt` — your keybinds, audio and every other setting stay as
they are. Nothing else is touched.
Manual fallback: copy the files below from `config/onion-perf/<tier>/` in the instance
folder over your own configs, and set the `options.txt` values from the table below by
hand (don't copy a tier's `options.txt` over yours — that resets all your video, audio
and control settings, keybinds included):
```
config/DistantHorizons.toml
config/sodium-options.json
config/entity_model_features.json
config/flywheel-client.toml
config/lambdynlights.toml
config/ssrd-common.toml
config/sound_physics_remastered/soundphysics.properties
config/sound_physics_remastered/sound_rates.properties
```
Don't delete `options.txt`: that resets all your video, audio and control settings,
keybinds included. Set the video values in the table below by hand instead.
Then change these for your tier *(provisional)*:
The whole tier table below is applied by the script:
| Setting | Where | Deck | Mid | High |
| --- | --- | --- | --- | --- |
@@ -49,30 +59,29 @@ extra in multiplayer. Distant Horizons draws everything beyond that.
## 2. Java memory and garbage collector (Prism → Edit Instance → Settings → Java)
Use **Java 21** (Temurin 21 recommended). More memory is *not* faster: a bigger heap makes
each GC pause longer.
each GC pause longer. Bench (B4h): **5 GB is enough; 4 GB is measurably worse** (tour 1 % low
−23 fps); 6 and 8 GB were no faster than 5. Don't go below 5 GB on any machine.
| Your machine | Max memory (`-Xmx`) | Min memory |
| --- | --- | --- |
| Steam Deck (16 GB shared with the GPU) | **5 GB**, never above 6 | leave low (512 MB) |
| 16 GB PC | **6 GB** | leave low |
| 16 GB PC | **5–6 GB** | leave low |
| 32 GB PC | **6–8 GB** | may equal max |
| PC with little free RAM (other apps/containers running) | min(6 GB, free RAM − 2 GB) | leave low |
**JVM arguments** *(provisional; Phase 2 block B4 picks the winner)*. Paste one line into
*Java arguments* (Prism adds `-Xmx` itself from the memory box):
**JVM arguments** — bench block B4 settled this: the client-tuned G1 line below and the
phase-0 G1 set tied (S5 tour 1 % low 226.8 vs 228.1 fps), while Generational ZGC and
Shenandoah were measurably worse on the tour scene (1 % low 216.6 and 173.4 vs 226.8 fps).
So: **the G1 line below, for everyone.** Paste it into *Java arguments* (Prism adds
`-Xmx` itself from the memory box):
- **Default (all machines):** client-tuned G1
```
-XX:+UseG1GC -XX:MaxGCPauseMillis=37 -XX:G1HeapRegionSize=16M -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=23 -XX:G1ReservePercent=20 -XX:G1MixedGCCountTarget=3 -XX:G1HeapWastePercent=20 -XX:InitiatingHeapOccupancyPercent=10 -XX:G1RSetUpdatingPauseTimePercent=0 -XX:MaxTenuringThreshold=1 -XX:GCTimeRatio=99 -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem
```
- **32 GB machines, if you still get stutter:** Generational ZGC. Distant Horizons
recommends it in its log warning about G1.
```
-XX:+UseZGC -XX:+ZGenerational -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem
```
`-XX:+ZGenerational` is required: on Java 21, `-XX:+UseZGC` alone selects the older
non-generational ZGC. Avoid ZGC on the Deck or on low-RAM PCs. It needs spare memory and
CPU those machines don't have.
- Don't switch to Generational ZGC — not even on 32 GB machines. Distant Horizons
recommends it in a log warning about G1, but here it **tested worse** (tour 1 % low
216.6 fps vs 226.8 for G1; Shenandoah was worse still at 173.4).
Don't add `-XX:+AlwaysPreTouch` on the Deck or on low-RAM PCs. Don't copy
`-XX:ShenandoahGCMode=iu` from older guides: Java 21 refuses to start with it.
@@ -82,8 +91,10 @@ Don't add `-XX:+AlwaysPreTouch` on the Deck or on low-RAM PCs. Don't copy
*Pending the owner's shader policy. Interim advice:* **shaders off** is the pack default.
They cost more in this pack than usual:
- With any shaderpack on, **Create's GPU renderer (Flywheel) switches off**, so every
rotating shaft and machine is drawn the slow way, and drawn again for shadows.
- On its own, Flywheel (Create's GPU renderer) **switches off when any shaderpack is on**.
The pack now ships **Iris Flywheel Compat**, which keeps it working under shaders (bench:
Create floor p99 13.3 → 4.0 ms with MakeUp). It's unofficial, so it may lag behind
Create/Iris updates; if Create machines render wrong with shaders, report it.
- Shader **shadows corrupt airships** (Sable sub-levels; Iris issue #3164).
- Distant Horizons adds a shadow pass over all its terrain.
+218
View File
@@ -0,0 +1,218 @@
# Bench results — proxy machine, run 1 (B0–B12a)
**Proxy data only.** One machine (Ryzen 9 9900X / RX 7800 XT, Manjaro), not the fleet (P0 Deck,
P1 7700K+1060, P2 6700K+1080 Ti). Run 1: 2026-10-16 22:25 → ~2026-10-17 05:00 EDT, 56 sessions
(57 launches incl. one auto-retry), 306/309 capture windows valid. Source:
`~/onion-bench/results/session.jsonl` (`ts >= 2026-10-16T22:24`), verdicts from
`harness/analyze.py --baseline C1 --noise B0 --since 2026-10-16T22:24`.
Run 2 (B4h, B7 shader rows, B12b, B14) and Run 3 (new mod set, Flerovium confirmation,
B14 ×4) are below; B13 is not done.
## Method
- **Harness**: `harness/bench.py` + `matrix.py`. Every session is one cold launch at
1920×1080 in headless gamescope, world restored from `bench-world-v1*.zip` (DH LODs
pre-generated, `generatorPlan=DISABLED`), scenes driven by an in-world datapack, spectator
camera, FTB Backups off, dedicated server stopped.
- **Captures**: per scene 15 s warm-up + 30 s capture (S5 tour 120 s, no warm-up) × 3 reps,
45 s settle after join. Frametimes from MangoHud; metrics = p50/p99 frametime, 1 % low
(frametime-based), avg fps.
- **Win rule (BENCH-MATRIX §4)**: win = median p99 improves by more than the B0 p99 spread
AND 1 % low doesn't drop by more than the B0 1 %-low spread; loss = worse beyond either;
else tie (ties keep the current/upstream value).
- **Noise floor (B0, C0 × 3 sessions on S1)**: p99 spread **0.40 ms**, 1 %-low spread
**6.6 fps** (p99 9.64 ms, 1 % low 88.3 fps).
- **Baselines**: each variant is compared with the C1 session(s) of its own block
(`C1@B3`, `C1@B11.baseline`, …), i.e. same JVM and same time window.
## Highlights
- **The shipped tuning (C1) beats upstream defaults (C0) on every scene.** C0 (B1) vs C1
(B2), same JVM (Prism default, 8 GB):
| Scene | p99 C0 → C1 (ms) | 1 % low C0 → C1 (fps) | avg fps C0 → C1 |
|---|---|---|---|
| S1 vista | 9.33 → 8.23 | 93.0 → 104.9 | 218 → 272 |
| S2 Create floor | 3.79 → 3.22 | 218.7 → 259.8 | 694 → 975 |
| S3 mob pen | 7.43 → 4.46 | 110.1 → 186.0 | 255 → 395 |
| S4 airships | 2.93 → 2.44 | 298.0 → 341.3 | 789 → 1073 |
| S5 tour | 3.47 → 2.95 | 194.2 → 223.8 | 788 → 1017 |
Startup ("Game took"): C0 31.38 s → C1 25.36 s. Atlas: C0 4096×4096×4 → C1 4096×4096×2.
- **Distant Horizons is the biggest single render cost.** Turning DH off (B3.dh-off) gives
S1 p99 7.99 → 5.38 ms, 1 % low 105.5 → 144.0; S5 3.09 → 2.34 ms, 211.2 → 282.4.
DH's LOW quality preset at C1's radius 128 (`dh-qlow`) is a **win** on S1 (−0.96 ms,
+9.5 fps). SSAO costs 1.25 ms p99 on S1.
- **Fresh Animations is expensive.** S3 p99 4.32 → 8.65 ms (FA) / 10.20 ms (FA +
Extensions); 1 % low 184.6 → 104.9 / 89.0. It is opt-in (not enabled by default) — keep
it that way and document the cost.
- **The added performance mods earn their place.** Removing BadOptimizations, Particle Core
or Sodium Extra each loses (Particle Core / Sodium Extra: S3 1 % low −24 / −25 fps).
- **ModernFix `dynamic_resources` (B6)**: startup 7.6 s faster (26.32 vs 33.92 s median) and
better frametimes with it on. Ship is gated only on the manual missing-texture check
(see B6 row).
## Per-block verdicts
Numbers are p99 ms / 1 % low fps; Δ is vs the block's C1 baseline (+ = better p99).
| Block | Change tested | Result | Verdict | Ship action |
|---|---|---|---|---|
| B0 | noise floor, C0 × 3 on S1 | p99 9.64 (spread 0.40), 1 % low 88.3 (spread 6.6) | — | thresholds for all blocks |
| B1 | C0 baseline, S1–S5 | see highlights table; startup 31.38 s | reference | — |
| B2 | C1 vs C0 | C1 wins S1–S5 (table above) | **win** | C1 stays shipped; no bisect needed |
| B2 | C1-deck vs C1 | S1 7.08/115.0 vs 8.23/104.9 (win); S2/S4/S5 tie; S3 4.84/167.5 vs 4.46/186.0 (loss) | mixed | keep Deck tier as is (S3 loss: check `entityShadows`/particles on fleet P0) |
| B2 | C1-high vs C1 | S1 tie; S2–S5 losses (e.g. S4 2.66/311.6 vs 2.44/341.3) | expected cost | keep High as opt-in quality tier |
| B3 | DH radius 64 | S1 7.46/111.1 vs 7.99/105.5 (win); S5 tie | win S1 | already the Deck value; no change to C1 |
| B3 | DH radius 192 / 256 | S1 tie (7.69/105.5, 8.07/101.5); S5 tie | tie | **decision**: on this GPU 256 (upstream) ties 128; fleet floors decide — keep 128 until fleet data |
| B3 | DH LOW quality preset @ r128 (`dh-qlow`) | S1 7.03/115.0 vs 7.99/105.5 (+0.96, +9.5) | **win** (S1 only) | candidate for DEFAULT after an S5 run + visual check |
| B3 | DH SSAO on | S1 9.24/95.3 (−1.25, −10.2) | loss | confirms C1 SSAO off |
| B3 | DH threads 4 | S1 tie; S5 3.32/197.4 vs 3.09/211.2 (loss) | loss | threads stay 2 |
| B3 | DH off (reference) | S1 5.38/144.0; S5 2.34/282.4 | — | cost reference only |
| B4 | GC J0/J1/J2/J3 @ 6 GB, S2+S5 | S5: J0 3.07/226.8, J1 3.11/228.1 (tie), J2 3.40/216.6 (loss), J3 3.75/173.4 (loss); S2 all tie | **J0** | keep J0; heap ladder = B4h (run 2) |
| B5 | Sable ships 0/1/4 (proxy) | S4 3.00/280.6, 3.12/261.6, 2.97/273.2 | — | ships nearly free on this GPU |
| B5 | SSRD 32/64/128, sable-noshade (4 ships) | 3.02/265.0, 3.03/264.7, 3.11/261.9, 3.04/263.0 vs pooled C1 (−8 to −11 fps 1 % low) | loss/noise | keep C1 (SSRD 16, shading on); decay run deferred (fleet P0/P1) |
| B6 | ModernFix dynamic_resources off vs on (C1) | S1 9.11/83.8 vs 8.48/98.6; S2 3.29/236.1 vs 3.18/264.0; startup 33.92 vs 26.32 s | dynres on **wins** | ship `modernfix-mixins.properties` **after** the manual missing-texture check |
| B8 | Fresh Animations (+Ext) on S3 | 8.65/104.9 (FA), 10.20/89.0 (FA+Ext) vs 4.32/184.6 | large cost | keep opt-in; document in PLAYER-TUNING |
| B8 | EMF animation LOD 20 / 8 (with FA) | vs C1-fa (LOD 12) 8.65/104.9: LOD 20 8.91/101.8 (tie), LOD 8 8.16/108.0 (+0.49, +3.1 → win) | LOD 8 wins under FA | optional: LOD 8 for FA users (PLAYER-TUNING note) |
| B8 | LambDynamicLights fancy / fastest | 4.41/181.8, 4.35/187.7 vs 4.32/184.6 | tie | keep C1 `fast` |
| B9 | Flywheel light flat / smooth (S2) | 3.45/246.6 (tie), 3.46/238.9 (−8.6, loss) vs 3.44/247.5 | tie/loss | keep C1 TRI_LINEAR |
| B10 | Sound Physics rays up / strict / no caps (S2s) | 3.69/216.2 (loss), 3.58/233.7 (tie), 3.64/216.9 (loss) vs 3.52/237.6 | loss/tie | keep C1 |
| B11 | Sodium 3 builder threads, blend 0/2, mipmaps 4 | all losses except blend 2 (S5 tie); e.g. mip4 S1 8.82/97.6 vs 8.12/104.2 | loss/tie | keep C1 |
| B12a | remove BadOptimizations / Particle Core / Sodium Extra | each loses ≥1 scene (e.g. S3 3.93/205.3, 4.63/188.4, 4.61/187.5 vs 3.87/212.5) | keep all | no change |
| B4h, B7, B12b, B14 | see **Run 2** below | | | |
| B13 | spark profile | — | **pending (manual)** | — |
## Run 2 (2026-10-17/18): B4h, B7, B12b, B14
Same machine and method; **default JVM** (run 1's B5–B12a ran on a leaked J3/6 GB JVM —
harness bug fixed in `60e10b3`; within-block verdicts unaffected). Verdicts use the B0
thresholds (p99 0.40 ms, 1 % low 6.6 fps); one session per row, so treat single-scene
margins near the threshold as soft. Numbers: p99 ms / 1 % low fps.
### B4h — heap ladder (J0, S2 + S5)
| heap | S2 | S5 |
|---|---|---|
| 4 GB | 3.58 / 229.1 | 3.32 / 188.7 |
| 5 GB | 3.58 / 238.6 | 3.13 / 209.1 |
| 6 GB | 3.63 / 225.7 | 3.14 / 211.8 |
| 8 GB | 3.58 / 234.5 | 3.18 / 209.2 |
**Smallest heap within the winner's spread: 5 GB** (S5 1 % low 209.1 vs best 211.8).
4 GB loses on S5 (−23 fps). Heap is a launcher setting, not a shipped config →
PLAYER-TUNING recommendation (≥ 5 GB), not a `build.py` change.
### B7 — shaders (data for owner; no auto-decision)
| profile | S1 vegetation | S2 Create floor | S4 airships |
|---|---|---|---|
| none (run-2 re-run) | 8.13 / 103.6 (274 fps avg) | 3.28 / 260.4 (974) | 2.50 / 332.5 (1072) |
| MakeUp shadowless-medium | 8.69 / 97.3 (263) | 12.88 / 67.9 (120) | 2.58 / 314.0 (1050) |
| MakeUp medium | 10.04 / 88.4 (213) | 12.32 / 71.9 (121) | 2.94 / 280.4 (779) |
| Complementary LOW | 10.85 / 83.2 (182) | 13.23 / 64.0 (120) | 4.16 / 205.2 (444) |
| Complementary MEDIUM | 10.92 / 78.5 (175) | 13.27 / 67.4 (118) | 4.33 / 192.8 (401) |
All shader rows loaded the pack (no `shader_not_loaded`). The dominant cost is **S2**:
any shaderpack makes Flywheel drop its GPU backends, so the Create floor goes from
974 to ~120 fps average (p99 3.3 → ~13 ms) regardless of profile. Elsewhere MakeUp
shadowless is nearly free; Complementary costs ~2.7 ms p99 on S1 and ~2× frame time on S4.
→ Supports **opt-in shaders**, MakeUp first, and **Iris Flywheel Compat** (B12b).
### B12b — trial mods (each vs its own baseline)
| trial | result | verdict |
|---|---|---|
| **Iris Flywheel Compat** 2.4.0 (MakeUp default, vs shader-baseline) | S2 **3.98 / 218.3 vs 13.26 / 65.8** (avg 644 vs 117 fps); S1 9.92 / 84.8 vs 10.23 / 83.8 (tie); S4 3.10 / 266.1 vs 3.01 / 277.6 (1 % low −11.5, loss) | **win under shaders** (restores Flywheel instancing) — add after a visual check of Create contraptions/airships with a shaderpack (unofficial mod; breaks on Create/Iris updates) |
| **Flerovium** 1.1.2 (vs C1) | S3 **3.48 / 229.9 vs 5.71 / 147.4**; S1/S2/S5 tie | **win** on entities — but the B12b C1 S3 baseline (5.71) is worse than every run-1 C1 S3 (4.32–4.46), so the true gain is ~0.9 ms / +45 fps vs run 1. Add after one confirmation pair. **Superseded by Run 3** (did not hold up) |
| Gnetum 3.3.6 | S3 4.41 / 189.3 (vs anomalous 5.71; ≈ run-1 C1) ; S1/S2/S5 tie | tie in practice → no add |
| Better Block Entities 1.3.4 | S2 3.51 / 245.3 (loss), S3 loss; with FA 9.86 / 89.8 vs 9.52 / 87.4 (tie) | reject |
| Super Resolution 0.9.2-alpha.2 | S1 8.74 / 99.7 (loss), S5 1 % low −16.6 (loss) | reject on this proxy (P0-only lever; its first launch rewrites `fml.toml` and demands a restart) |
| Ixeris 4.6.8 | S1 p99 −0.49 (loss), S5 1 % low −12.2 (loss); **one crash** (NPE in `GameRules.getInt` during server tick, retry ok) | reject |
| Cull Leaves 4.1.1 (vs Better Leaves baseline) | S1 tie; S5 1 % low 208.8 vs 217.1 (loss) | reject |
New mod adds (Iris Flywheel Compat, Flerovium) ⇒ **re-run B0** before any later matrix work.
### B14 — spawn tuning (defaults vs shipped, 10 min per site, **1 run each**)
*Superseded by Run 3 (4 runs per config).*
| site | defaults total / tuned | shipped total / tuned |
|---|---|---|
| jungle (−8 107 −201) | 123 / 5 | 118 / 3 |
| river (904 63 88) | 208 / 29 | 208 / 25 |
| warm ocean (−200 63 56) | 155 / 22 | 130 / 0 |
Automated rule says **back off**, but it is driven by one Fowl Play herd: at the warm-ocean
site defaults spawned 14 geese + 8 ducks (on nearby land/water inside the 128-block
sphere), shipped none; the other "vanished" species (gazelle, dragonfly) had max count 1.
Sea bunny — the warm-ocean target — appeared under neither config, so that site doesn't
exercise it. **Inconclusive at n = 1**: needs ≥ 3 interleaved runs per config (≈ 3 h)
before any back-off; if it persists, the candidate is Fowl Play duck/goose weights.
All runs: 33 count rows, `biome_ok=1`, config check ok.
## Run 3 (2026-10-18): new mod set, Flerovium confirmation, B14 ×4
Same machine and method, default JVM, **new mod set** (shipped in `7841361`: + Flerovium
1.1.2, + Iris Flywheel Compat 2.4.0, ModernFix `dynamic_resources=true`). Numbers are
p99 ms / 1 % low fps, medians of session medians.
### B0v2 — noise floor for the new mod set (C0, S1, 3 sessions)
| p99 | p99 spread | 1 % low | 1 % low spread | avg fps |
|---|---|---|---|---|
| 9.74 | **0.41 ms** | 87.2 | **1.6 fps** | 219 |
These are the thresholds for any later matrix work on this mod set.
### Flerovium confirmation (C1, 2 sessions each, interleaved on/off)
| scene | Flerovium on | Flerovium off | Δ (on vs off) | verdict |
|---|---|---|---|---|
| S1 vista | 8.42 / 99.8 | 8.02 / 105.0 | p99 −0.40, 1 % low −5.2 | **loss** (1 % low drop > 1.6) |
| S3 mob pen | 5.55 / 151.9 | 5.94 / 141.1 | p99 +0.39 (just under 0.41) | **tie / inconclusive** — S3 session spread is huge (p99 1.42–2.34 ms, 1 % low 32–61 fps) |
**The run-2 win did not hold up** (it was measured against an anomalous S3 baseline).
**Recommendation: remove Flerovium** — awaiting owner OK; it is currently shipped in `7841361`.
### B14 — spawn tuning, 4 runs per config
Median final count at 10 min within 128 blocks (total / tuned species):
| site | defaults | shipped |
|---|---|---|
| jungle | 129.5 / 4.0 | 128.0 / 2.5 |
| river | 208.0 / 34.5 | 211.5 / 28.5 |
| warm ocean | 145.0 / 9.5 | 136.0 / 0.5 |
The automated rule says **back off** (snail vanished at the jungle site, gazelle at the warm
ocean), but both peaked at 1 per run — noise, not a disappearance. Sea bunny never appeared
under either config, so this site doesn't exercise it. The warm-ocean tuned drop is mostly
a Fowl Play goose/duck herd. **Verdict: the tuning is harmless but does not measurably
reduce total entity load.** Owner decides keep vs revert; no config change made.
### Shipping incident
`7841361`'s `index.toml` captured hashes of runtime re-saved spawn configs, so the server's
packwiz install failed (`Hash invalid`) at 14:06 and the server didn't start. Fixed by
re-hashing in `c5d25fa` (server up 14:18). Nothing was pushed.
## Caveats
- Run 2: Super Resolution's first launch flipped `earlyWindowControl` in the bench `fml.toml` (not journaled by the harness); later run-2 sessions started with it off. Loading-screen only — frametimes unaffected; startup times after 01:54 are not comparable. Reset 2026-10-18.
- **Single proxy machine.** Fleet machines (P0–P2) were unreachable; the fleet fps floors
(P0 1 % low ≥ 30, P1/P2 ≥ 60) don't map to a 7800 XT — every verdict here is relative.
B2's "C1 wins on ≥ 2 machines" gate is satisfied on one machine only.
- **This GPU hides render costs.** Airships (B5), Flywheel lighting and DH radius are cheap
here; the fleet's older GPUs may rank them differently. B5 decay is deferred to P0/P1.
- **GPU clocks unpinned** (amdgpu DPM `auto`, no root); every row carries `gpu_dpm_auto`.
- **JVM differed between block groups (harness bug).** B0–B3 ran Prism's default args at
8 GB; B4 set each GC at 6 GB; `bench.py` never restored `instance.cfg`, so **B5–B12a all
ran on J3 Shenandoah, 6 GB**. Every verdict compares sessions inside one block (same JVM),
so verdicts stand, but absolute numbers aren't comparable across those groups and the B0
noise floor was measured under the other JVM.
- **One crash**: B10 `spr-nocap`, first attempt — `OutOfMemoryError: Ran out of memory
reading level data` after an unusually slow (~1.5 min) resource load, on the leaked
Shenandoah/6 GB JVM. The automatic retry was clean.
- Single sessions per variant (except B0/B6, 3 each); 3 reps per scene per session.
+142
View File
@@ -0,0 +1,142 @@
# Bench harness v2
`bench.py` drives the OnionBench Prism instance (`~/onion-bench/prism/instances/OnionBench`)
with **no input injection**: a generated world datapack runs the scenes and prints
markers, the harness only reads logs and the MangoHud CSV. `run_scene.py` is the legacy
xdotool harness, kept for reference. Stdlib only, Python 3.
## Loop
```sh
cd analysis/client-perf/harness
python3 bench.py boot --n 3 --variant C1 # cold launches -> results/boot.jsonl
python3 bench.py boot --n 3 --join # ... into the world (join time)
python3 bench.py session --variant C0 --plan s1 --reps 3 --label B0 # x3 sessions for B0
python3 bench.py session --variant C1 --plan s1,s2,s3,s4,s5 --label B2
python3 bench.py session --variant C1 --plan s5 --jvm J2 --heap 6144 --label B4
python3 analyze.py --baseline C0 --noise B0 [--label B2] # markdown table + verdicts
python3 -m unittest discover -s tests # offline tests (from this dir)
```
`session --dry-run` prints the datapack it would write and changes nothing. `bench.py meta`
prints the environment snapshot; `bench.py disarm` removes a leftover plan pack.
## Per-session instance changes (undone after every session)
```sh
python3 bench.py session --variant C1 --plan s1,s2,s4 --shader ComplementaryReimagined_r5.9.3.zip \
[--shader-options profile.txt] --label B7
python3 bench.py session --variant C1 --plan s1 --dh off
python3 bench.py session --variant C1 --plan s1 --disable-mods 'badoptimizations*,particle_core*'
python3 bench.py session --variant C1 --plan s2 --add-mods ~/trials/gnetum.jar
python3 bench.py session --variant C1 --plan s4 --world-zip ~/onion-bench/worlds/bench-world-v1-s4-1.zip
python3 bench.py restore # after a hard kill: replay .harness-pending.json
```
- `--shader NAME|off` (default `off`): `config/iris.properties` `enableShaders` /
`shaderPack`; NAME must exist in `shaderpacks/`. `--shader-options FILE` is copied to
`shaderpacks/<NAME>.txt` (Iris' per-pack options file; journaled whenever a shader is set).
- `--dh on|off` (default `on`): `[client.advanced.debugging] rendererMode` =
`DEFAULT` / `DISABLED` in `config/DistantHorizons.toml`, set after the variant (DH's
`quickEnableRendering` is GUI-only and maps to this key).
- `--disable-mods GLOB,..`: case-insensitive globs over `mods/*.jar`, renamed to
`.jar.disabled`; a glob that matches nothing is an error. `--add-mods PATH,..` copies jars
in (name collision = error). `mods_hash` is taken after these changes.
- `--world-zip PATH`: `saves/onionbench` -> `saves/onionbench.harness-bak`, zip extracted
(world at the zip root or under one top directory, needs `level.dat`); sha256 recorded.
- Every session first resets `options.txt` `resourcePacks` / `incompatibleResourcePacks`
to `["vanilla","mod_resources"]` / `[]` (variants may override); not undone.
- Crash safety: before the first change `session` writes `.minecraft/.harness-pending.json`
(write-ahead: each entry precedes its change; file backups in `.minecraft/.harness-backup/`).
The normal `finally` and `bench.py restore` run the same restore, newest entry first;
failed entries stay in the journal. `session` and `boot` refuse to start while it exists.
SIGTERM unwinds through the restore; SIGKILL leaves the journal for `bench.py restore`.
- Rows record `shader`, `shader_options(_sha256)`, `dh`, `disabled_mods`, `added_mods`,
`world_zip(_sha256)`, `resource_packs`, `restore_errors`; warn flags `gpu_dpm_auto` (RX
7800 XT `power_dpm_force_performance_level` != `high`), `server_running`,
`restore_failed`.
- **boot**: N launches; waits for `Game took N seconds` (or `joined the game` with
`--join`); outcome `ok` / `crash` / `timeout`. Crash = new file in `crash-reports/` or
`hs_err_pid*.log`, client java exited, or a log line with `Failed to create mod
instance`, a `/FATAL]` level tag, `Cowardly refusing`, or a crash-report header.
- **session**: applies the variant (`proposed-configs/apply_variant.py`), then pins
`options.txt` (no onboarding/narrator, no pause on focus loss, `maxFps:260`, vsync
off), hashes `config/` (minus `.bak`/`.tmp`/spark buffers) and `options.txt`, writes the
plan datapack, launches once into `onionbench`, tails `latest.log` until `BENCH_DONE`,
crash, exit or timeout, kills the client, removes the plan pack, then slices the CSV.
One JSON row per scene x rep window goes to `~/onion-bench/results/session.jsonl`;
every session's `latest.log` is archived under `~/onion-bench/results/logs/`.
- Only java processes whose command line contains `instances/OnionBench` are ever
signalled. The dedicated server (`unix_args.txt`) is only observed (running + %CPU).
- `--jvm J0..J3` / `--heap MB` rewrite `JvmArgs` / `MaxMemAlloc` in the bench
`instance.cfg` (first original saved as `instance.cfg.harness-orig`). Without them the
instance is left as is; the effective `-X*` flags are read from the running client.
## Datapack mechanism
The scene functions live in the permanent pack `saves/onionbench/datapacks/onion-bench`
(`bench:s1` ... `bench:tour_start`, source in `../bench/onion-bench`). Per session the
harness writes `datapacks/onion-bench-autoplan/` (pack_format 48, 1.21.1 `function/`
dirs) with a session id `pYYYYMMDDHHMMSS`:
- `#minecraft:load` -> `bench:autoplan/<sid>/load`: unless score `#<sid> bench_plan` is
set, poll every second (`schedule`) until `execute if entity @a`;
- `begin`: set the score (a `/reload` won't restart the plan), clear stale
`storm_tick` / tour schedules, `say BENCH_PLAN <sid> begin`, wait `--settle` s;
- chained `stepN` functions: `execute as @a[limit=1] at @s run function bench:<scene>`,
`say BENCH_MARK warm|start|end <scene> <rep>`, `schedule` the warm-up / capture ticks;
`say BENCH_DONE <sid>` at the end.
S5 is fixed by BENCH-MATRIX: `bench:s1`, 10 s, `bench:tour_start`, all 120 s captured
(warm-up ignored). S2s = S2 + `bench:storm_on` during warm-up and capture. Plans must
follow S1 -> S5 order (`--any-order` overrides). Function names contain the session id, so
schedules saved into `level.dat` by an autosave can never fire in a later session. The pack
is deleted in a `finally`, so normal play of the world is unaffected; after a hard kill
of the harness itself run `bench.py disarm`.
Markers are taken from the server log line (`[Server] BENCH_MARK ...`, ms timestamps).
The MangoHud CSV used is one created at/after launch (never a stale one); wall time =
file birth time (`stat -c %.9W`, else the name's timestamp) + `elapsed`, and the method is
recorded as `align`. Per window: n, p50/p95/p99/peak ms, avg fps (1000/mean frametime),
1 % low = 1000/mean of the worst 1 % frametimes, and the legacy fps-column 1 % low.
Flags: `vsync_suspect` (p50 within 3 % of 60 Hz or an xrandr refresh), `low_n`
(n < 500 or < half of duration x avg fps), `crashed`/`timeout` (window never ended),
`session_crash` (window ended, session crashed later), `backup_in_window` (backup or
autosave line from 5 s before start to end; warn only). `analyze.py` drops rows with
any non-warn flag unless `--include-invalid`.
Metadata per row: label, machine (`proxy-9900X-7800XT`), git sha + dirty, harness sha,
variant + config/options hashes, mods hash/count, Java version, cfg JVM args + effective
flags, MaxMemAlloc, kernel, CPU governor, per-GPU DPM level + sclk, MangoHud's GPU
string, MemAvailable before/after, load average, server running + %CPU, startup and
join seconds, block-atlas size, `level.dat` hash.
## Known limits
- The desktop is **KDE Plasma on Wayland** (kwin_wayland + Xwayland at `:1`). xdotool
only reaches Xwayland windows, `windowactivate --sync` hangs under kwin, focus-steal
prevention can drop keystrokes, and typed chat lands in whatever has focus. Hence no
input injection at all in v2.
- B7 shader profiles are automated: `gen_shader_profiles.py` resolves each pack's
`profile.NAME` (shaders.properties, Iris ProfileSet semantics) into
`shader-profiles/<pack-stem>.<profile>.txt`; matrix.toml passes it as `shader_options`
-> `--shader-options`. Re-run the generator (or `--check`) after a shaderpack update.
- Still manual: **S6** (JEI/GUI overlay needs a key press), checks needing F3
reads, `/sparkc` heap/GC reads (B1/B4).
- MangoHud's `log_duration=1800` in `~/.local/bin/mangohud-local` caps one CSV at 30 min;
`session` warns when a plan is longer (B5's 45-min decay run needs a split or a larger
value).
- The integrated server ticks the plan, so scene timing stretches if it lags; windows use
the marker wall times, not tick counts. The marker chat line is visible for ~10 s at
the start of each capture (identical across variants).
- **FTB Backups 3** backs up the world right after join (its index
`ftbbackups3/backups.json` doesn't parse, so it treats the world as never backed up)
and every `backup_timer` = 120 min. **Done (bench instance only):** `auto: false` in
the instance's `config/ftbbackups3-server.snbt`. Never edit the repo's `config/` for this.
- Stop the dedicated server (tmux `onion-mc`) while benching (owner-approved); it used up
to ~78 % of a core idle. Rows are flagged `server_running` if it is up.
- amdgpu DPM is `auto` (no root); rows are flagged `gpu_dpm_auto`. For tighter noise:
`echo high | sudo tee /sys/class/drm/card1/device/power_dpm_force_performance_level`.
- Scenes run in **spectator** mode (set by the plan) so elevated cameras don't fall.
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""Summarise bench.py session rows and apply the BENCH-MATRIX win rule.
analyze.py [--file ~/onion-bench/results/session.jsonl] [--label B2]
[--baseline C0 --noise B0] [--include-invalid] [--exclude-backup]
Groups windows by (label, variant, scene). Each session contributes its median p99 and
median 1 % low (frametime-based); a group's value is the median of its session medians
and its spread is max - min of those. With --baseline/--noise, every non-baseline group
is compared with the baseline variant on the same scene: same label if present, else the
same block (a tagged label 'B3.dh-off' looks in 'B3', 'B3.baseline', then all 'B3.*'
pooled), else all labels pooled. A tagged session of the baseline variant itself is
compared with 'B3' / 'B3.baseline' when one exists: win = median p99 improves by more than the noise label's p99 spread
AND 1 % low does not worsen by more than its 1 % low spread; loss = p99 worsens beyond
the spread or 1 % low drops beyond it; otherwise tie (ties keep upstream defaults).
Rows flagged crashed / timeout / no_data / no_markers / low_n / vsync_suspect are
excluded unless --include-invalid. Stdlib only.
"""
import argparse
import json
import statistics
import sys
from pathlib import Path
INVALID = {"crashed", "timeout", "no_data", "no_markers", "low_n", "vsync_suspect",
"shader_not_loaded"}
DEFAULT_FILE = Path.home() / "onion-bench" / "results" / "session.jsonl"
def load_rows(path):
rows = []
for line in Path(path).read_text().splitlines():
line = line.strip()
if line:
rows.append(json.loads(line))
return [r for r in rows if r.get("kind", "window") == "window"]
def is_valid(row, exclude_backup=False):
flags = set(row.get("flags") or [])
if not row.get("metrics") or flags & INVALID:
return False
return not (exclude_backup and "backup_in_window" in flags)
def summarise(rows):
"""{(label, variant, scene): stats} from valid window rows."""
by_session = {}
for r in rows:
key = (r.get("label"), r["variant"], r["scene"])
by_session.setdefault(key, {}).setdefault(r["session"], []).append(r["metrics"])
out = {}
for key, sessions in by_session.items():
p99s = [statistics.median(m["p99_ms"] for m in ms) for ms in sessions.values()]
lows = [statistics.median(m["one_low_fps"] for m in ms) for ms in sessions.values()]
fps = [statistics.median(m["avg_fps"] for m in ms) for ms in sessions.values()]
out[key] = {
"sessions": len(sessions),
"windows": sum(len(ms) for ms in sessions.values()),
"p99": statistics.median(p99s), "p99_spread": max(p99s) - min(p99s),
"low": statistics.median(lows), "low_spread": max(lows) - min(lows),
"fps": statistics.median(fps),
}
return out
def noise_spread(summary, noise_label):
"""(p99 spread, 1 % low spread) of the noise block, worst case across its groups."""
groups = [v for (lab, _, _), v in summary.items() if lab == noise_label]
if not groups:
return None
return max(g["p99_spread"] for g in groups), max(g["low_spread"] for g in groups)
def verdict(base, cand, spread_p99, spread_low):
gain = base["p99"] - cand["p99"] # > 0: candidate's p99 is lower (better)
low_drop = base["low"] - cand["low"] # > 0: candidate's 1 % low is worse
if gain > spread_p99 and low_drop <= spread_low:
return "win"
if gain < -spread_p99 or low_drop > spread_low:
return "loss"
return "tie"
def block_of(label):
"""'B3.dh-off' -> 'B3'; untagged labels (and None) are their own block."""
return label.split(".", 1)[0] if label else label
def _pooled(rows, labels, baseline, scene, name):
pooled = [r for r in rows if r["variant"] == baseline and r["scene"] == scene
and (labels is None or r.get("label") in labels)]
if not pooled:
return None
return summarise([dict(r, label=name) for r in pooled]).get((name, baseline, scene))
def baseline_for(summary, rows, label, scene, baseline):
"""(baseline stats, where-label) for one candidate group, or (None, None).
Order: the candidate's own label; then its block - the untagged block label
('B3', for tagged candidates), '<block>.baseline', then every '<block>.*' label pooled; then all labels
pooled. A tagged session of the baseline variant itself (e.g. 'B3.dh-off' running
C1 against baseline C1) is compared only with 'B3' or 'B3.baseline'."""
block = block_of(label)
tagged = bool(label) and block != label
order = [label] + ([block] if tagged else []) + ([f"{block}.baseline"] if block else [])
for lab in order:
if (lab, baseline, scene) in summary:
return summary[(lab, baseline, scene)], lab
if block:
labels = {r.get("label") for r in rows
if r.get("label") == block or str(r.get("label")).startswith(block + ".")}
b = _pooled(rows, labels, baseline, scene, f"{block}.*")
if b is not None:
return b, f"{block}.*"
b = _pooled(rows, None, baseline, scene, "*")
return (b, "*") if b is not None else (None, None)
def self_baseline(summary, label, scene, baseline):
"""Baseline for a tagged group that runs the baseline variant itself, or None
(then the group *is* the baseline)."""
block = block_of(label)
if not label or block == label:
return None, None
for lab in (block, f"{block}.baseline"):
if lab != label and (lab, baseline, scene) in summary:
return summary[(lab, baseline, scene)], lab
return None, None
def table(summary, rows, baseline=None, noise=None):
spreads = noise_spread(summary, noise) if noise else None
full = summarise(rows) # baselines may live in labels filtered out of the table
hdr = ["label", "variant", "scene", "sess", "win#", "p99 ms", "p99 spr", "1%low",
"1%low spr", "avg fps"]
if baseline:
hdr += ["vs", "Δp99 ms", "Δ1%low", "verdict"]
out = ["| " + " | ".join(hdr) + " |", "|" + "---|" * len(hdr)]
for key in sorted(summary, key=lambda k: (str(k[0]), k[2], k[1])):
lab, var, scene = key
s = summary[key]
cells = [str(lab), var, scene, str(s["sessions"]), str(s["windows"]),
f"{s['p99']:.2f}", f"{s['p99_spread']:.2f}", f"{s['low']:.1f}",
f"{s['low_spread']:.1f}", f"{s['fps']:.0f}"]
if baseline:
b, blab = (self_baseline(full, lab, scene, baseline) if var == baseline
else baseline_for(full, rows, lab, scene, baseline))
if var == baseline and b is None:
cells += ["—", "", "", "baseline"]
else:
if b is None:
cells += ["—", "", "", "no baseline"]
elif spreads is None:
cells += [f"{baseline}@{blab}", f"{b['p99'] - s['p99']:+.2f}",
f"{s['low'] - b['low']:+.1f}", "no noise data"]
else:
cells += [f"{baseline}@{blab}", f"{b['p99'] - s['p99']:+.2f}",
f"{s['low'] - b['low']:+.1f}", verdict(b, s, *spreads)]
out.append("| " + " | ".join(cells) + " |")
if baseline and noise:
out.append("")
out.append(f"noise ({noise}): " + (
f"p99 spread {spreads[0]:.2f} ms, 1%low spread {spreads[1]:.1f} fps"
if spreads else "no rows - verdicts unavailable"))
return "\n".join(out)
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("--file", default=str(DEFAULT_FILE))
ap.add_argument("--label", action="append", help="only these labels (repeatable); "
"the noise label is always kept")
ap.add_argument("--baseline", help="baseline variant, e.g. C0")
ap.add_argument("--noise", help="label whose spread is the win threshold, e.g. B0")
ap.add_argument("--include-invalid", action="store_true")
ap.add_argument("--exclude-backup", action="store_true")
ap.add_argument("--since", help="only rows with ts >= this ISO time, e.g. 2026-10-16T22:24")
a = ap.parse_args(argv)
rows = load_rows(a.file)
if a.since:
rows = [r for r in rows if r.get("ts", "") >= a.since]
total = len(rows)
if not a.include_invalid:
rows = [r for r in rows if is_valid(r, a.exclude_backup)]
warn = sum(1 for r in rows if "backup_in_window" in (r.get("flags") or []))
shown = rows
if a.label:
keep = set(a.label) | ({a.noise} if a.noise else set())
shown = [r for r in rows if r.get("label") in keep]
if not shown:
print("no valid rows", file=sys.stderr)
return 1
print(table(summarise(shown), rows, a.baseline, a.noise))
print(f"\n{len(rows)}/{total} windows valid; {warn} with backup_in_window (warn only)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+169
View File
@@ -0,0 +1,169 @@
# B14: spawn-tuning measurement
`b14.py` automates SPAWN-TUNING.md §B14. It compares the upstream spawn defaults with the
shipped partial files (`d80f85b`, `8e8483d`: `alexsmobs-common.toml`, `fowlplay.json5`,
`crittersandcompanions-common.json5`). Each comparison stands still for 10 minutes at a
river, a jungle and a warm ocean, and counts entities within 128 blocks.
It reuses `../bench.py` as a module: `launch`, `kill_client`, `LogTail`, `monitor`, the
`Journal` / `restore_pending` crash-safe restore, `apply_pins`, `apply_dh`, and
`zip_world_root`. It needs nothing beyond the Python 3 standard library.
## Loop
```sh
cd analysis/client-perf/harness
python3 b14/b14.py run --config defaults --dry-run # world, config plan, datapack
python3 b14/b14.py run --config defaults # ~32 min, all three biomes
python3 b14/b14.py run --config shipped
python3 b14/b14.py summary # SPAWN-TUNING §B14 verdict
```
- `--biome river,jungle,warm_ocean` (default `all`) runs the sites in that order in one
launch.
- `--minutes 10` (default) sets the stay at each site.
- Repeating runs (for example two of each config, interleaved
defaults → shipped → defaults → shipped) gives medians. The summary takes the median
across runs.
- Results go to `~/onion-bench/results/b14.jsonl`. Each count line is a `b14_count` row,
and each launch is a `b14_session` row (outcome, config check, git/mods hash, sites,
archived log at `results/logs/session-<sid>.log`).
- `b14.py restore` (or `bench.py restore`, which uses the same journal) undoes a
hard-killed run.
- `b14.py parse LOG` prints the count rows found in any archived log.
The script refuses to start in these cases:
- a restore journal is pending;
- a bench client is running;
- any `matrix.py run`, `bench.py session` or other `b14.py run` process exists. It shares
the OnionBench instance with the matrix, so **never run it while the matrix runs.**
## Design
### Throwaway world `saves/onionspawn`, one per run
- It contains only the bench world's `level.dat` (seed `onionbench`, gamerules, datapack
list), the `onion-bench` datapack (it has no load or tick tags) and `serverconfig/`.
- It has no `region/`, `entities/` or `data/` (no forceloads, no DH database). Every chunk
around a site is therefore generated during the run, under the config being measured.
That covers both kinds of spawning: the initial creatures placed at world generation
and natural spawning.
- The seed is the same, so the terrain is identical between runs. The world is journaled
as a `tree` entry and deleted on restore. The bench world `saves/onionbench` is never
touched.
### Sites are pre-located offline (`b14.py locate`)
`/locate biome` can't be used from a datapack. Functions run with suppressed output, and
`execute store` only captures the distance, not the coordinates.
So `locate` parses the overworld region files in `bench-world-v1.zip` with a small NBT
reader (`nbt.py`):
- It reads each chunk's biome palette at the surface, using the `MOTION_BLOCKING_NO_LEAVES`
heightmap in full chunks and `WORLD_SURFACE_WG` in proto chunks.
- For each target biome it picks the chunk whose 5×5 neighbourhood has the largest share
of that biome.
- Sites must be at least 192 blocks from the world spawn and the saved player position
(spawn chunks and the first-load area stay outside the 128-block count sphere), and at
least 320 blocks from each other (the count spheres don't overlap).
Biomes depend only on the seed, so the coordinates are valid for any world with this seed
and mod set. They are committed in `sites.json` (from zip sha256
`144caf5f…`, seed −306676217):
| biome | x y z | biome at site | share in 5×5 chunks |
|---|---|---|---|
| river | 904 63 88 | minecraft:river | 0.37 |
| jungle | −8 107 −201 | minecraft:jungle | 0.52 |
| warm_ocean | −200 63 56 | minecraft:warm_ocean | 0.75 |
Every count line carries `biome_ok`, an in-game `execute if biome` check at the player's
feet (`minecraft:river`, `#minecraft:is_jungle`, `minecraft:warm_ocean`). If the
regenerated terrain doesn't match the offline lookup, the summary marks that biome
invalid instead of silently measuring the wrong place.
### Config variants (journaled, bench instance only)
- `defaults` deletes the three files. Each mod regenerates its upstream defaults on load
(the same as "these three config files removed" in SPAWN-TUNING). They aren't in
`config/defaultoptions/extra/`, so Default Options can't put them back.
- `shipped` writes `git show <rev>:config/<file>` (default `HEAD`), meaning the committed
partials, not the working tree's boot re-saves.
- After the run, and before the restore, the files the mods re-saved are read back and
checked against the expected values. A mismatch fails the run (`config_check.ok=false`),
and the summary ignores that run. The expected upstream defaults were verified from the
jars:
- Alex's Mobs: the `# Default:` comments in its re-saved TOML: gazelle 40, capuchin 28,
elephant 30, straddler 70, rocky roller 60, underminer 50, blobfish 30,
mudskipper 28, caiman 29.
- Fowl Play: `FPConfig.<init>` bytecode: duck 4/max 6, goose 4/4, gull 5/8,
sparrow 20/6.
- Critters and Companions: `CACCommonConfig$SpawningConfig.<init>`: sea bunny warm
ocean 32 (max 4), other oceans 16; leaf insect jungle 14, forest 6; dragonfly lush 10.
### Datapack (`saves/onionspawn/datapacks/b14-autoplan`, pack_format 48)
- **Start-up:** a load tag polls once a second until a player exists. Then it pins the
rules that matter for spawning:
- `gamemode creative` (it counts for spawning and despawning, where spectators don't,
and the player can't die);
- `doMobSpawning true`;
- the daylight, weather, patrol, trader, insomnia and warden rules;
- time 6000 and clear weather.
- **Per site:**
1. Teleport above the site.
2. 5 s later: clear a 3×3×3 pocket, put a barrier at y−1 (a platform over water), and
teleport onto it.
3. Count at t=0 (+10 s), then every 60 s up to t=10. Each count re-asserts the barrier
and the teleport, so the player never drifts.
- **Each count** runs `execute store result score #<alias> b14 if entity
@e[type=<id>,distance=..128]` as the player, for the 22 tuned species and 10 vanilla
spot-check species. It also stores:
- `total` = `@e[type=!player,distance=..128]`, as SPAWN-TUNING defines it;
- `nonitem`, which also excludes items and XP orbs;
- `tuned`, the sum of the tuned species.
- **Output:** a `tellraw` prints one line, which the client logs as `[System] [CHAT]`:
`B14 <sid> <biome> t=<min> biome_ok=1 total=N nonitem=N tuned=N alexsmobs:gazelle=N ...`.
- **Errors:** `Failed to load function b14:` in the log (an unknown entity id makes the
whole function unloadable) aborts the run as `datapack_error`.
### Entity ids
All are checked against the jars in the bench instance. Each has an `entity.<mod>.<name>`
lang key, and is either in the mod's registry class strings (`CACEntities`) or has a
`loot_table/entities/<name>.json`.
- **Alex's Mobs** (`alexsmobs-1.22.17`): `gazelle`, `capuchin_monkey`, `elephant`,
`straddler`, `rocky_roller`, `underminer`, `blobfish`, `mudskipper`, `caiman`.
- **Fowl Play** (`fowlplay-1.2.3`): `duck`, `goose`, `gull`, `sparrow`.
- **Critters and Companions** (`crittersandcompanions 2.7.0`): `leaf_insect`, `sea_bunny`,
`dragonfly`, `ladybug`, `stag_beetle`, `roly_poly`, `snail`, `stick_bug`, `weevil`.
### Verdict (`summary`, SPAWN-TUNING §B14)
- **Per biome**, using the final count (t = minutes):
- **keep** if the median `total` drops under shipped *and* no tuned species that
appeared at any minute under defaults is absent under shipped;
- **back off** (listing the species) if one disappears;
- **no drop** otherwise.
- **Warnings:** `rare_warn` flags species whose shipped maximum is under 25 % of the
defaults maximum (when defaults saw at least 4). `vanilla_gone` flags cows or sheep
that vanished.
- **Overall:** `keep` only if all three biomes keep.
- **Excluded runs:** runs with a failed config check or a non-ok outcome are ignored. A
biome with `biome_ok != 1` is `invalid`.
## Manual prep and limits
- **Prep:** nothing beyond what's committed. `sites.json` is already generated. Re-run
`b14.py locate --out b14/sites.json` only if the seed, the source zip or the
biome-affecting mods (Terralith, Tectonic) change.
- **Thin biome coverage:** the source world has only about 2300 chunks with final biomes,
so the river and jungle patches are small (37 % and 52 % of the 5×5 neighbourhood). The
128-block count sphere also covers neighbouring biomes. This is the same for both
configs, but it lowers the absolute counts.
- **Better sites (optional):** on `~/onion-bench/server-bench` (seed `onionbench`), run
`/locate biome minecraft:river` (and jungle, warm_ocean), check the share around each
result, and put the coordinates into `sites.json` by hand. The `y` value is the first
free block above the surface or the water.
- **Jungle site from a proto chunk:** its `y` is the height before trees are placed. The
3×3×3 air pocket keeps a tree from burying the player, and `biome_ok` still guards the
biome.
- **Hostile mobs in `total`:** difficulty stays as in `level.dat` (normal), so hostiles
from caves within 128 blocks count toward `total`. `tuned` and `nonitem` are recorded
separately for reading the result.
- **Machine:** this is proxy-machine data (singleplayer, simulation distance 8 pinned).
Spawning is server logic, so the GPU doesn't matter, but CPU load does affect chunk
generation speed.
+855
View File
@@ -0,0 +1,855 @@
#!/usr/bin/env python3
"""B14 spawn-tuning measurement (SPAWN-TUNING.md §B14), automated.
b14.py locate [--zip bench-world-v1.zip] [--out sites.json] # offline, once
b14.py run --config {defaults,shipped} [--biome river,jungle,warm_ocean]
[--minutes 10] [--dry-run]
b14.py summary [--file ~/onion-bench/results/b14.jsonl]
b14.py parse LOG # debug: count lines
b14.py restore # = bench.py restore
One `run` = one client launch into a throwaway world `saves/onionspawn` that holds only
the bench world's level.dat (seed `onionbench`, gamerules, datapack list), so every
chunk around the sites is generated fresh under the config being measured. A generated
datapack sets doMobSpawning true + Creative, parks the player on a barrier block at each
pre-located site (sites.json), and every 60 s prints a parsable chat line
`B14 <sid> <biome> t=<min> biome_ok=1 total=N <entity id>=N ...` counted within 128
blocks. The spawn config variant is journaled through bench.Journal, so `bench.py
restore` undoes a hard-killed run. Stdlib only.
"""
import argparse
import json
import os
import re
import signal
import statistics
import subprocess
import sys
import time
import zipfile
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent))
import bench # noqa: E402 (harness module: launch, Journal, LogTail, monitor, ...)
import nbt # noqa: E402
WORLD = "onionspawn"
PACK = "b14-autoplan"
SITES_FILE = HERE / "sites.json"
SOURCE_ZIP = bench.BENCH / "worlds" / "bench-world-v1.zip"
RESULTS_FILE = bench.RESULTS / "b14.jsonl"
# The three shipped partial files (d80f85b, 8e8483d). `defaults` deletes them so each mod
# regenerates its upstream defaults; `shipped` writes the committed partials.
SPAWN_FILES = ["alexsmobs-common.toml", "fowlplay.json5", "crittersandcompanions-common.json5"]
# Tuned species (verified against the jars' lang keys + registries, see README).
TUNED = [
("am_gazelle", "alexsmobs:gazelle"),
("am_capuchin", "alexsmobs:capuchin_monkey"),
("am_elephant", "alexsmobs:elephant"),
("am_straddler", "alexsmobs:straddler"),
("am_rocky_roller", "alexsmobs:rocky_roller"),
("am_underminer", "alexsmobs:underminer"),
("am_blobfish", "alexsmobs:blobfish"),
("am_mudskipper", "alexsmobs:mudskipper"),
("am_caiman", "alexsmobs:caiman"),
("fp_duck", "fowlplay:duck"),
("fp_goose", "fowlplay:goose"),
("fp_gull", "fowlplay:gull"),
("fp_sparrow", "fowlplay:sparrow"),
("cac_leaf_insect", "crittersandcompanions:leaf_insect"),
("cac_sea_bunny", "crittersandcompanions:sea_bunny"),
("cac_dragonfly", "crittersandcompanions:dragonfly"),
("cac_ladybug", "crittersandcompanions:ladybug"),
("cac_stag_beetle", "crittersandcompanions:stag_beetle"),
("cac_roly_poly", "crittersandcompanions:roly_poly"),
("cac_snail", "crittersandcompanions:snail"),
("cac_stick_bug", "crittersandcompanions:stick_bug"),
("cac_weevil", "crittersandcompanions:weevil"),
]
# Vanilla spot-check (SPAWN-TUNING: "cows, sheep still spawn normally") + biome natives.
VANILLA = [
("mc_cow", "minecraft:cow"), ("mc_sheep", "minecraft:sheep"), ("mc_pig", "minecraft:pig"),
("mc_chicken", "minecraft:chicken"), ("mc_parrot", "minecraft:parrot"),
("mc_ocelot", "minecraft:ocelot"), ("mc_tropical_fish", "minecraft:tropical_fish"),
("mc_cod", "minecraft:cod"), ("mc_squid", "minecraft:squid"),
("mc_dolphin", "minecraft:dolphin"),
]
# Species SPAWN-TUNING §B14 names as the point of each biome (reported first).
FOCUS = {
"river": ["fowlplay:duck", "fowlplay:gull"],
"jungle": ["alexsmobs:capuchin_monkey", "crittersandcompanions:leaf_insect"],
"warm_ocean": ["crittersandcompanions:sea_bunny"],
}
BIOMES = {
"river": {"match": {"minecraft:river"}, "check": "minecraft:river"},
"jungle": {"match": {"minecraft:jungle", "minecraft:sparse_jungle",
"minecraft:bamboo_jungle"}, "check": "#minecraft:is_jungle"},
"warm_ocean": {"match": {"minecraft:warm_ocean"}, "check": "minecraft:warm_ocean"},
}
# Upstream defaults of the keys we check after load (SPAWN-TUNING "default -> new").
EXPECT_DEFAULTS = {
"alexsmobs-common.toml": {
"gazelleSpawnWeight": 40, "capuchinMonkeySpawnWeight": 28, "elephantSpawnWeight": 30,
"straddlerSpawnWeight": 70, "rockyRollerSpawnWeight": 60, "underminerSpawnWeight": 50,
"blobfishSpawnWeight": 30, "mudskipperSpawnWeight": 28, "caimanSpawnWeight": 29},
"fowlplay.json5": {
"duckSpawnWeight": 4, "duckMaxGroupSize": 6, "gooseSpawnWeight": 4,
"gooseMaxGroupSize": 4, "gullSpawnWeight": 5, "gullMaxGroupSize": 8,
"sparrowSpawnWeight": 20, "sparrowMaxGroupSize": 6},
"crittersandcompanions-common.json5": {
"sea_bunny@minecraft:warm_ocean": 32, "leaf_insect@#c:is_jungle": 14,
"leaf_insect@#c:is_forest": 6, "dragonfly@#c:is_lush": 10},
}
COUNT_RE = re.compile(r"\bB14 (?P<sid>b14-\d+) (?P<biome>[a-z_]+) t=(?P<t>\d+) (?P<rest>.*)$")
DONE_RE = re.compile(r"B14_DONE (b14-\d+)")
FUNC_FAIL_RE = re.compile(r"Failed to load function b14:|Unknown function b14:")
def log(msg):
print(f"[b14] {msg}", flush=True)
# ---------------------------------------------------------------- config variants
def shipped_texts(rev="HEAD", repo=bench.REPO):
out = {}
for name in SPAWN_FILES:
r = subprocess.run(["git", "-C", str(repo), "show", f"{rev}:config/{name}"],
capture_output=True, text=True)
if r.returncode != 0:
raise RuntimeError(f"git show {rev}:config/{name} failed: {r.stderr.strip()}")
out[name] = r.stdout
return out
def render_variant(config, shipped):
"""{file name: text to write, or None = delete (mod regenerates defaults)}."""
if config == "defaults":
return {name: None for name in SPAWN_FILES}
if config == "shipped":
return {name: shipped[name] for name in SPAWN_FILES}
raise ValueError(f"unknown config {config!r}")
def _cac_weight(text, mob, spec):
m = re.search(rf'"?{re.escape(mob)}"?\s*:\s*\[(.*?)\]', text, re.S)
if not m:
return None
for entry in m.group(1).split("}"):
if f'"{spec}"' in entry:
w = re.search(r'"?weight"?\s*:\s*(-?\d+)', entry)
return int(w.group(1)) if w else None
return None
def extract_values(name, text, keys):
"""Values of `keys` from a partial or mod-re-saved spawn file (None = absent)."""
out = {}
for k in keys:
if name.endswith(".toml"):
m = re.search(rf"^\s*{re.escape(k)}\s*=\s*(-?\d+)", text, re.M)
out[k] = int(m.group(1)) if m else None
elif "@" in k:
mob, spec = k.split("@", 1)
out[k] = _cac_weight(text, mob, spec)
else:
m = re.search(rf'"?{re.escape(k)}"?\s*:\s*(-?\d+)', text)
out[k] = int(m.group(1)) if m else None
return out
def expected_values(config, shipped):
if config == "defaults":
return {n: dict(v) for n, v in EXPECT_DEFAULTS.items()}
return {n: extract_values(n, shipped[n], EXPECT_DEFAULTS[n]) for n in SPAWN_FILES}
def check_config(game, config, shipped):
"""Compare what the mods re-saved after load with what the variant should produce."""
want = expected_values(config, shipped)
got, mismatch = {}, []
for name in SPAWN_FILES:
p = Path(game) / "config" / name
if not p.is_file() and config == "defaults":
got[name] = dict(want[name]) # never written: the mod ran on its defaults
continue
text = p.read_text(errors="replace") if p.is_file() else ""
got[name] = extract_values(name, text, want[name])
for k, v in want[name].items():
if got[name][k] != v:
mismatch.append(f"{name}:{k} want {v} got {got[name][k]}")
return {"ok": not mismatch, "mismatch": mismatch, "values": got}
def apply_spawn_config(game, journal, files):
for name, text in files.items():
rel = f"config/{name}"
journal.backup_file(rel)
p = Path(game) / rel
if text is None:
if p.exists():
p.unlink()
else:
p.write_text(text)
def config_diff(game, files, shipped):
"""Key-level plan of the config change (for --dry-run): checked keys now -> after."""
out = []
for name, text in files.items():
p = Path(game) / "config" / name
cur = p.read_text(errors="replace") if p.is_file() else None
keys = EXPECT_DEFAULTS[name]
now = extract_values(name, cur, keys) if cur else {}
if text is None:
out.append(f"--- config/{name}: DELETE ({len(cur)} bytes now)" if cur
else f"--- config/{name}: DELETE (absent now)")
out.append(" mod regenerates upstream defaults on load")
after = keys
else:
out.append(f"--- config/{name}: WRITE shipped partial ({len(text)} bytes)")
after = extract_values(name, shipped[name], keys)
for k in keys:
mark = "" if now.get(k) == after[k] else " <- changes"
out.append(f" {k}: {now.get(k)} -> {after[k]}{mark}")
if text is not None:
out.extend(" | " + ln for ln in text.splitlines())
return "\n".join(out)
# ---------------------------------------------------------------- datapack
def _score(name):
return {"score": {"name": f"#{name}", "objective": "b14"}}
def count_function():
lines = ["# B14 entity counts around the executing position (the player)",
"scoreboard players set #tuned b14 0"]
for alias, eid in TUNED + VANILLA:
lines.append(f"execute store result score #{alias} b14 "
f"if entity @e[type={eid},distance=..128]")
for alias, _ in TUNED:
lines.append(f"scoreboard players operation #tuned b14 += #{alias} b14")
lines.append("execute store result score #total b14 "
"if entity @e[type=!minecraft:player,distance=..128]")
lines.append("execute store result score #nonitem b14 if entity "
"@e[type=!minecraft:player,type=!minecraft:item,"
"type=!minecraft:experience_orb,distance=..128]")
return "\n".join(lines) + "\n"
def tellraw(sid, biome, t):
parts = ["", {"text": f"B14 {sid} {biome} t={t} biome_ok="}, _score("biome_ok")]
for key, alias in [("total", "total"), ("nonitem", "nonitem"), ("tuned", "tuned")]:
parts += [{"text": f" {key}="}, _score(alias)]
for alias, eid in TUNED + VANILLA:
parts += [{"text": f" {eid}="}, _score(alias)]
return "tellraw @a " + json.dumps(parts, separators=(",", ":"))
def build_timeline(sid, sites, minutes, settle_s, arrive_s=10, gen_s=5):
"""[(commands, delay_ticks_to_next)] for the unrolled plan."""
ns = f"b14:{sid}"
tl = []
for biome, s in sites:
x, y, z = s["x"], s["y"], s["z"]
pos = f"{x + 0.5} {y} {z + 0.5}"
# proto-chunk sites come from the pre-feature heightmap: clear a 3x3x3 pocket in
# case a tree landed on the column, then stand on a barrier (also over water).
hold = [f"fill {x - 1} {y} {z - 1} {x + 1} {y + 2} {z + 1} minecraft:air",
f"setblock {x} {y - 1} {z} minecraft:barrier",
f"execute in minecraft:overworld run tp @a {pos} 0 0"]
tl.append(([f"execute in minecraft:overworld run tp @a {x + 0.5} {y + 2} {z + 0.5} 0 0"],
gen_s * 20))
tl.append((hold + [f"say B14_ARRIVE {sid} {biome} {x} {y} {z}"], arrive_s * 20))
for t in range(minutes + 1):
cmds = hold + [
f"execute as @a[limit=1] at @s run function {ns}/count",
f"execute as @a[limit=1] at @s store success score #biome_ok b14 "
f"if biome ~ ~ ~ {BIOMES[biome]['check']}",
tellraw(sid, biome, t)]
tl.append((cmds, 1200 if t < minutes else 20))
tl.append(([f"say B14_DONE {sid}"], 0))
return tl
def plan_seconds(n_sites, minutes, settle_s, arrive_s=10, gen_s=5):
return settle_s + n_sites * (gen_s + arrive_s + minutes * 60 + 1)
def generate_datapack(sid, sites, minutes, settle_s):
"""{relative path: text}; namespace b14, functions under b14:<sid>/."""
ns, holder = f"b14:{sid}", "#armed"
base = f"data/b14/function/{sid}"
files = {
"pack.mcmeta": json.dumps({"pack": {
"pack_format": 48,
"description": f"Onion B14 spawn plan {sid} (generated by b14.py; "
f"throwaway world)"}}) + "\n",
"data/minecraft/tags/function/load.json": json.dumps({"values": [f"{ns}/load"]}) + "\n",
f"{base}/load.mcfunction":
"scoreboard objectives add b14 dummy\n"
f"execute unless score {holder} b14 matches 1.. run schedule function {ns}/poll 20t\n",
f"{base}/poll.mcfunction":
f"execute if entity @a run function {ns}/begin\n"
f"execute unless entity @a run schedule function {ns}/poll 20t\n",
f"{base}/begin.mcfunction":
f"scoreboard players set {holder} b14 1\n"
# Creative: counts as a spawning/despawning player (spectators don't), and
# can't die. Every rule that changes spawning is pinned, not inherited.
"gamemode creative @a\n"
"gamerule doMobSpawning true\n"
"gamerule doDaylightCycle false\n"
"gamerule doWeatherCycle false\n"
"gamerule doPatrolSpawning false\n"
"gamerule doTraderSpawning false\n"
"gamerule doInsomnia false\n"
"gamerule doWardenSpawning false\n"
"time set 6000\n"
"weather clear\n"
f"say B14_PLAN {sid} begin\n"
f"schedule function {ns}/step0 {max(1, settle_s * 20)}t\n",
f"{base}/count.mcfunction": count_function(),
}
tl = build_timeline(sid, sites, minutes, settle_s)
for i, (cmds, delay) in enumerate(tl):
body = list(cmds)
if delay > 0 and i + 1 < len(tl):
body.append(f"schedule function {ns}/step{i + 1} {delay}t")
files[f"{base}/step{i}.mcfunction"] = "\n".join(body) + "\n"
return files
# ---------------------------------------------------------------- world
def world_members(zip_path):
"""Members copied into the throwaway world: level.dat, the bench datapack (it is in
level.dat's enabled list; no load/tick tags) and serverconfig/. No chunks/entities/
data/ (forceloads, DH db) so every chunk is generated under the measured config."""
prefix = bench.zip_world_root(zip_path)
with zipfile.ZipFile(zip_path) as z:
names = z.namelist()
keep = []
for n in names:
rel = n[len(prefix):]
if n.endswith("/") or not n.startswith(prefix):
continue
if rel == "level.dat" or rel.startswith(("datapacks/onion-bench/", "serverconfig/")):
keep.append((n, rel))
if not any(rel == "level.dat" for _, rel in keep):
raise ValueError(f"{zip_path}: no level.dat")
return keep
def build_world(game, journal, zip_path, files):
root = Path(game) / "saves" / WORLD
if root.exists():
raise ValueError(f"{root} exists (stale B14 world?) - run `b14.py restore` or remove it")
members = world_members(zip_path)
journal.record({"op": "tree", "path": f"saves/{WORLD}"})
with zipfile.ZipFile(zip_path) as z:
for n, rel in members:
dst = root / rel
dst.parent.mkdir(parents=True, exist_ok=True)
dst.write_bytes(z.read(n))
write_pack(root, files)
return root
def write_pack(world_root, files):
d = Path(world_root) / "datapacks" / PACK
for rel, text in files.items():
p = d / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(text)
# ---------------------------------------------------------------- locate (offline)
# Chunk statuses whose biome palette is final (before "biomes" it is a placeholder).
BIOME_STATUSES = {"biomes", "noise", "surface", "carvers", "features", "initialize_light",
"light", "spawn", "full"}
def _chunk_info(chunk):
"""Surface samples of one chunk, or None. Full chunks use MOTION_BLOCKING_NO_LEAVES
(ground under trees, water surface); proto chunks the worldgen WORLD_SURFACE_WG."""
status = str(chunk.get("Status", "")).split(":")[-1]
if status not in BIOME_STATUSES:
return None
hm = nbt.heightmap(chunk) or nbt.heightmap(chunk, "WORLD_SURFACE_WG")
if hm is None:
return None
ys = {}
for y in hm:
ys[y] = ys.get(y, 0) + 1
mode_y = max(ys, key=lambda k: (ys[k], -k))
samples = []
for lx, lz in ((2, 2), (2, 10), (10, 2), (10, 10), (8, 8)):
y = hm[lz * 16 + lx]
samples.append(nbt.biome_at(chunk, lx, y - 1, lz))
col = None # a column at the modal height (ground, not a trunk or hole), near centre
best = None
for lz in range(16):
for lx in range(16):
if hm[lz * 16 + lx] == mode_y:
d = (lx - 8) ** 2 + (lz - 8) ** 2
if best is None or d < best:
best, col = d, (lx, lz)
centre = nbt.biome_at(chunk, col[0], mode_y - 1, col[1])
return {"samples": samples, "y": mode_y, "col": col, "centre": centre,
"full": status == "full"}
def scan_world(zip_path, stride=1):
"""{(cx, cz): info} for every fully generated overworld chunk in the zip."""
prefix = bench.zip_world_root(zip_path)
out = {}
with zipfile.ZipFile(zip_path) as z:
regions = [i for i in z.infolist()
if i.filename.startswith(prefix + "region/") and i.filename.endswith(".mca")
and i.file_size > 8192]
for info in regions:
rx, rz = (int(v) for v in info.filename.rsplit("/", 1)[1].split(".")[1:3])
for lx, lz, chunk in nbt.region_chunks(z.read(info.filename)):
if stride > 1 and (lx % stride or lz % stride):
continue
ci = _chunk_info(chunk)
if ci:
out[(rx * 32 + lx, rz * 32 + lz)] = ci
return out
def pick_sites(chunks, avoid, min_gap=320, avoid_gap=192, radius=2):
"""Best chunk per target biome: centre column in the biome, maximal share of the
biome in the (2r+1)^2 neighbourhood; far from `avoid` points and from each other."""
chosen = {}
for biome, spec in BIOMES.items():
match = spec["match"]
cands = []
for (cx, cz), ci in chunks.items():
if ci["centre"] not in match:
continue
hit = tot = forest = 0
for dx in range(-radius, radius + 1):
for dz in range(-radius, radius + 1):
n = chunks.get((cx + dx, cz + dz))
if not n:
continue
for b in n["samples"]:
tot += 1
hit += b in match
forest += bool(b) and "forest" in b
if tot < 5 * 9:
continue # fewer than 9 known chunks around it
score = hit / tot + (0.5 * forest / tot if biome == "river" else 0.0) \
+ (0.05 if ci["full"] else 0.0)
x, z = cx * 16 + ci["col"][0], cz * 16 + ci["col"][1]
cands.append((score, x, ci["y"], z, hit / tot, ci["full"], ci["centre"]))
for score, x, y, z, frac, full, centre in sorted(cands, reverse=True):
pts = list(avoid) + [(s["x"], s["z"]) for s in chosen.values()]
gaps = [avoid_gap] * len(avoid) + [min_gap] * len(chosen)
if all((x - px) ** 2 + (z - pz) ** 2 >= g * g for (px, pz), g in zip(pts, gaps)):
chosen[biome] = {"x": x, "y": y, "z": z, "biome": centre,
"frac": round(frac, 3), "score": round(score, 3),
"full_chunk": full, "candidates": len(cands)}
break
return chosen
def avoid_points(zip_path):
prefix = bench.zip_world_root(zip_path)
with zipfile.ZipFile(zip_path) as z:
_, ld = nbt.load_gzip(z.read(prefix + "level.dat"))
d = ld.get("Data", {})
pts = [(d.get("SpawnX", 0), d.get("SpawnZ", 0))]
pos = (d.get("Player") or {}).get("Pos")
if pos:
pts.append((int(pos[0]), int(pos[2])))
seed = (d.get("WorldGenSettings") or {}).get("seed")
return pts, seed
def cmd_locate(a):
t0 = time.time()
chunks = scan_world(a.zip, a.stride)
avoid, seed = avoid_points(a.zip)
sites = pick_sites(chunks, avoid)
hist = {}
for ci in chunks.values():
hist[ci["centre"]] = hist.get(ci["centre"], 0) + 1
log(f"scanned {len(chunks)} chunks with final biomes in {time.time() - t0:.0f}s; avoid {avoid}")
for b, n in sorted(hist.items(), key=lambda kv: -kv[1])[:25]:
print(f" {n:6d} {b}")
missing = sorted(set(BIOMES) - set(sites))
doc = {"source_zip": str(a.zip), "source_sha256": bench.sha256_file(a.zip),
"seed": seed, "avoid": avoid, "stride": a.stride,
"note": "y = first free block (full chunks: MOTION_BLOCKING_NO_LEAVES, proto "
"chunks: WORLD_SURFACE_WG); the player stands on a barrier at y-1. "
"Valid only for this seed + mod set (biomes are seed-deterministic).",
"sites": sites}
print(json.dumps(doc, indent=1))
if a.out:
Path(a.out).write_text(json.dumps(doc, indent=1) + "\n")
log(f"wrote {a.out}")
if missing:
log(f"NO SITE for {missing}: not in the generated chunks; see README (manual locate)")
return 1
return 0
# ---------------------------------------------------------------- parse / summary
def parse_count_line(line):
m = COUNT_RE.search(line)
if not m:
return None
vals = {}
for tok in m.group("rest").split():
k, sep, v = tok.partition("=")
if sep and re.fullmatch(r"-?\d+", v):
vals[k] = int(v)
return {"sid": m.group("sid"), "biome": m.group("biome"), "t": int(m.group("t")),
"biome_ok": vals.pop("biome_ok", None), "total": vals.pop("total", None),
"nonitem": vals.pop("nonitem", None), "tuned": vals.pop("tuned", None),
"counts": vals}
def parse_lines(lines, sid=None):
"""Count rows from log lines; the client logs each tellraw once as [CHAT]."""
rows, seen = [], set()
for ln in lines:
r = parse_count_line(ln)
if not r or (sid and r["sid"] != sid):
continue
key = (r["sid"], r["biome"], r["t"])
if key in seen:
continue
seen.add(key)
rows.append(r)
return rows
def load_rows(path):
p = Path(path)
if not p.exists():
return [], []
rows = [json.loads(ln) for ln in p.read_text().splitlines() if ln.strip()]
return ([r for r in rows if r.get("kind") == "b14_count"],
[r for r in rows if r.get("kind") == "b14_session"])
def summarize(counts, sessions):
"""{biome: {config: stats}} plus verdict per SPAWN-TUNING §B14."""
bad = {s["sid"] for s in sessions
if s.get("outcome") != "ok" or not (s.get("config_check") or {}).get("ok")}
runs = {}
for r in counts:
if r["sid"] in bad:
continue
runs.setdefault((r["biome"], r["config"], r["sid"]), []).append(r)
out = {}
for (biome, config, sid), rs in runs.items():
rs.sort(key=lambda r: r["t"])
final = rs[-1]
st = out.setdefault(biome, {}).setdefault(config, {
"runs": 0, "final_total": [], "final_nonitem": [], "final_tuned": [],
"seen": {}, "final": {}, "biome_ok": True, "minutes": []})
st["runs"] += 1
st["minutes"].append(final["t"])
st["final_total"].append(final["total"])
st["final_nonitem"].append(final["nonitem"])
st["final_tuned"].append(final["tuned"])
st["biome_ok"] &= all(r.get("biome_ok") == 1 for r in rs)
for eid in set().union(*(r["counts"] for r in rs)):
mx = max(r["counts"].get(eid, 0) for r in rs)
st["seen"][eid] = max(st["seen"].get(eid, 0), mx)
st["final"].setdefault(eid, []).append(final["counts"].get(eid, 0))
tuned_ids = {eid for _, eid in TUNED}
verdicts = {}
for biome, by in out.items():
d, s = by.get("defaults"), by.get("shipped")
if not (d and s):
verdicts[biome] = {"verdict": "incomplete",
"have": sorted(by)}
continue
dt, stt = statistics.median(d["final_total"]), statistics.median(s["final_total"])
gone = sorted(e for e in tuned_ids if d["seen"].get(e, 0) > 0 and s["seen"].get(e, 0) == 0)
rare = sorted(e for e in tuned_ids
if d["seen"].get(e, 0) >= 4 and 0 < s["seen"].get(e, 0) < 0.25 * d["seen"][e])
vanilla_gone = sorted(e for _, e in VANILLA if e in ("minecraft:cow", "minecraft:sheep")
and d["seen"].get(e, 0) > 0 and s["seen"].get(e, 0) == 0)
ok_biome = d["biome_ok"] and s["biome_ok"]
v = "keep" if stt < dt and not gone else ("back off" if gone else "no drop")
verdicts[biome] = {"verdict": v if ok_biome else "invalid (biome check failed)",
"total_defaults": dt, "total_shipped": stt, "disappeared": gone,
"rare_warn": rare, "vanilla_gone": vanilla_gone}
vs = [v["verdict"] for v in verdicts.values()]
overall = ("incomplete" if not vs or len(verdicts) < len(BIOMES)
or any(v not in ("keep", "back off", "no drop") for v in vs)
else "back off" if "back off" in vs else "keep" if all(v == "keep" for v in vs)
else "review")
return out, verdicts, overall
def render_summary(out, verdicts, overall):
lines = ["| biome | config | runs | final total | nonitem | tuned | focus (max seen) |",
"|---|---|---|---|---|---|---|"]
for biome in sorted(out):
for config in ("defaults", "shipped"):
st = out[biome].get(config)
if not st:
continue
focus = ", ".join(f"{e.split(':')[1]}={st['seen'].get(e, 0)}"
for e in FOCUS.get(biome, []))
lines.append(f"| {biome} | {config} | {st['runs']} | "
f"{statistics.median(st['final_total'])} | "
f"{statistics.median(st['final_nonitem'])} | "
f"{statistics.median(st['final_tuned'])} | {focus} |")
lines.append("")
for biome, v in sorted(verdicts.items()):
lines.append(f"- **{biome}**: {v['verdict']}"
+ (f" (total {v['total_defaults']} -> {v['total_shipped']}"
f"{'; disappeared ' + ', '.join(v['disappeared']) if v['disappeared'] else ''}"
f"{'; rare ' + ', '.join(v['rare_warn']) if v['rare_warn'] else ''}"
f"{'; vanilla gone ' + ', '.join(v['vanilla_gone']) if v['vanilla_gone'] else ''})"
if "total_defaults" in v else f" (have {v.get('have')})"))
lines.append(f"\n**Overall: {overall}** (keep = totals drop in every biome and no tuned "
f"species that appears under defaults vanishes; SPAWN-TUNING §B14)")
return "\n".join(lines)
def cmd_summary(a):
counts, sessions = load_rows(a.file)
if not counts:
log(f"no B14 rows in {a.file}")
return 1
out, verdicts, overall = summarize(counts, sessions)
print(render_summary(out, verdicts, overall))
return 0
def cmd_parse(a):
rows = parse_lines(Path(a.log).read_text(errors="replace").splitlines())
for r in rows:
print(json.dumps(r, sort_keys=True))
return 0 if rows else 1
# ---------------------------------------------------------------- run
def _ancestors(pid):
out = set()
while pid > 1 and pid not in out:
out.add(pid)
try:
stat = Path(f"/proc/{pid}/stat").read_text()
pid = int(stat.rsplit(")", 1)[1].split()[1])
except (OSError, ValueError, IndexError):
break
return out
def bench_busy():
"""Other harness processes (matrix runner / bench.py session / another B14 run) that
share the instance; our own process and its parent shells are excluded."""
mine = _ancestors(os.getpid())
busy = []
for d in Path("/proc").iterdir():
if not d.name.isdigit() or int(d.name) in mine:
continue
try:
args = (d / "cmdline").read_bytes().split(b"\0")
except OSError:
continue
s = " ".join(x.decode(errors="replace") for x in args)
if re.search(r"\bmatrix\.py run\b|\bbench\.py session\b|\bb14\.py run\b", s) \
and "--dry-run" not in s:
busy.append(int(d.name))
return busy
def load_sites(path, biomes):
doc = json.loads(Path(path).read_text())
sites = doc.get("sites", {})
missing = [b for b in biomes if b not in sites]
if missing:
raise ValueError(f"{path}: no site for {missing} (run `b14.py locate` or add by hand)")
return [(b, sites[b]) for b in biomes], doc
def cmd_run(a):
biomes = list(BIOMES) if a.biome == "all" else bench.split_list(a.biome)
for b in biomes:
if b not in BIOMES:
sys.exit(f"unknown biome {b!r}; choose from {sorted(BIOMES)}")
try:
sites, sites_doc = load_sites(a.sites, biomes)
shipped = shipped_texts(a.rev)
files = render_variant(a.config, shipped)
world_members(a.source_zip)
except (ValueError, OSError, RuntimeError, zipfile.BadZipFile) as e:
sys.exit(str(e))
sid = "b14-" + time.strftime("%Y%m%d%H%M%S")
pack = generate_datapack(sid, sites, a.minutes, a.settle)
budget = plan_seconds(len(sites), a.minutes, a.settle)
timeout = a.timeout or int(a.boot_timeout + budget + 180)
game = bench.GAME
if a.dry_run:
log(f"dry run {sid}: config={a.config} rev={a.rev} biomes={biomes} "
f"minutes={a.minutes} plan {budget}s timeout {timeout}s dh={a.dh}")
for b, s in sites:
log(f" site {b}: {s}")
log(f"world saves/{WORLD} <- {a.source_zip}: "
f"{[rel for _, rel in world_members(a.source_zip)][:6]}...")
print(config_diff(game, files, shipped))
if bench.pending_exists():
log(f"WARNING: {game / bench.PENDING} exists; a real run would refuse")
busy = bench_busy()
if busy:
log(f"WARNING: harness processes running {busy}; a real run would refuse")
for rel, text in pack.items():
print(f"--- {PACK}/{rel}")
print(text, end="")
return 0
bench.refuse_pending()
busy = bench_busy()
if busy:
sys.exit(f"harness processes running (pids {busy}); B14 shares the bench instance")
if bench.server_procs() and not a.allow_server:
log("WARNING: a dedicated server is running (noise only; spawning is client-side)")
env = bench.bench_env()
bench.preflight(a.kill_stale)
signal.signal(signal.SIGTERM, lambda *_: sys.exit(143))
journal = bench.Journal.begin(game, sid)
prism, state, tail = None, {"done": False, "fail": None}, None
outcome, detail, check, meta, failed = "error", None, None, None, []
t_launch = time.time()
try:
bench.reset_resource_packs(game)
bench.apply_pins()
bench.apply_dh(game, journal, a.dh == "on")
apply_spawn_config(game, journal, files)
build_world(game, journal, a.source_zip, pack)
meta = {"git": bench.git_state(), "mods": bench.mods_hash(game / "mods"),
"variant": bench.read_text(game / "BENCH_VARIANT.txt")}
before = bench.crash_files()
log(f"{sid}: config={a.config} biomes={biomes} {a.minutes} min, timeout {timeout}s")
t_launch = time.time()
prism = bench.launch(env, WORLD)
tail = bench.LogTail(game / "logs" / "latest.log")
def on_line(line):
r = parse_count_line(line)
if r and r["sid"] == sid and "[CHAT]" in line:
log(f" {r['biome']} t={r['t']} total={r['total']} tuned={r['tuned']} "
f"biome_ok={r['biome_ok']}")
if DONE_RE.search(line):
state["done"] = True
if FUNC_FAIL_RE.search(line):
state["fail"] = line.strip()[:300]
outcome, detail, _ = bench.monitor(
tail, prism, before, lambda: state["done"] or state["fail"], timeout, on_line)
if state["fail"]:
outcome, detail = "datapack_error", state["fail"]
finally:
if prism is not None:
bench.kill_client(prism)
if tail is not None:
tail.poll()
try:
check = check_config(game, a.config, shipped) # mods' re-saved files, pre-restore
except OSError as e:
check = {"ok": False, "mismatch": [f"read failed: {e}"], "values": {}}
failed = bench.restore_pending(game)
for e, err in failed:
log(f"RESTORE FAILED {e}: {err}")
if failed:
log(f"journal kept at {game / bench.PENDING}; fix and run `b14.py restore`")
log_path = bench.archive_log(f"session-{sid}")
rows = parse_lines(tail.lines if tail else [], sid)
base = {"sid": sid, "config": a.config, "rev": a.rev, "minutes": a.minutes,
"ts": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(t_launch)),
"machine": bench.MACHINE}
for r in rows:
bench.append_jsonl(RESULTS_FILE, {**base, **r, "kind": "b14_count",
"site": dict(sites)[r["biome"]]})
bench.append_jsonl(RESULTS_FILE, {
**base, "kind": "b14_session", "outcome": outcome, "detail": detail,
"biomes": biomes, "sites": dict(sites), "sites_sha256": sites_doc.get("source_sha256"),
"config_check": check, "rows": len(rows), "log": log_path,
"git_sha": meta["git"][0] if meta else None,
"git_dirty": meta["git"][1] if meta else None,
"mods_hash": meta["mods"][0] if meta else None,
"mods_count": meta["mods"][1] if meta else None,
"bench_variant": meta["variant"] if meta else None,
"restore_failed": [e for e, _ in failed]})
log(f"{sid} {outcome}{': ' + detail if detail else ''}; {len(rows)} count rows; "
f"config check {'ok' if check and check['ok'] else check and check['mismatch']}")
want = len(sites) * (a.minutes + 1)
return 0 if outcome == "ok" and len(rows) == want and check and check["ok"] else 1
def cmd_restore(a):
if bench.client_pids():
sys.exit("bench client is running; close it before restoring")
if not bench.pending_exists():
log("nothing pending")
return 0
failed = bench.restore_pending(bench.GAME)
for e, err in failed:
log(f"FAILED {e}: {err}")
return 1 if failed else 0
def build_parser():
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
sub = ap.add_subparsers(dest="cmd", required=True)
r = sub.add_parser("run", help="one client launch: config variant x biome sites")
r.add_argument("--config", required=True, choices=["defaults", "shipped"])
r.add_argument("--biome", default="all",
help="comma list of river,jungle,warm_ocean (in this order), or all")
r.add_argument("--minutes", type=int, default=10)
r.add_argument("--settle", type=int, default=20, help="seconds after join before site 1")
r.add_argument("--sites", default=str(SITES_FILE))
r.add_argument("--source-zip", default=str(SOURCE_ZIP),
help="world whose level.dat (seed, gamerules) seeds the throwaway world")
r.add_argument("--rev", default="HEAD", help="git rev of the shipped partial files")
r.add_argument("--dh", choices=["on", "off"], default="off")
r.add_argument("--boot-timeout", type=int, default=300)
r.add_argument("--timeout", type=int, default=None)
r.add_argument("--kill-stale", action="store_true")
r.add_argument("--allow-server", action="store_true", help="silence the server warning")
r.add_argument("--dry-run", action="store_true")
r.set_defaults(fn=cmd_run)
lo = sub.add_parser("locate", help="find biome sites offline from a world zip")
lo.add_argument("--zip", default=str(SOURCE_ZIP))
lo.add_argument("--out", default=None, help=f"write sites json (e.g. {SITES_FILE})")
lo.add_argument("--stride", type=int, default=1, help="sample every Nth chunk")
lo.set_defaults(fn=cmd_locate)
s = sub.add_parser("summary", help="defaults vs shipped, SPAWN-TUNING §B14 rule")
s.add_argument("--file", default=str(RESULTS_FILE))
s.set_defaults(fn=cmd_summary)
p = sub.add_parser("parse", help="print count rows found in a log file")
p.add_argument("log")
p.set_defaults(fn=cmd_parse)
rs = sub.add_parser("restore", help="undo a hard-killed run (same journal as bench.py)")
rs.set_defaults(fn=cmd_restore)
return ap
def main(argv=None):
a = build_parser().parse_args(argv)
return a.fn(a)
if __name__ == "__main__":
sys.exit(main())
+162
View File
@@ -0,0 +1,162 @@
"""Minimal read-only NBT / Anvil region reader (stdlib only) for B14 site finding.
Enough of the format to read level.dat and 1.18+ chunk biomes + heightmaps:
https://minecraft.wiki/w/NBT_format, /Region_file_format, /Chunk_format.
"""
import gzip
import io
import math
import struct
import zlib
END, BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, BYTE_ARRAY, STRING, LIST, COMPOUND, \
INT_ARRAY, LONG_ARRAY = range(13)
class _Reader:
def __init__(self, data):
self.b, self.i = data, 0
def take(self, n):
s = self.b[self.i:self.i + n]
if len(s) != n:
raise ValueError("truncated NBT")
self.i += n
return s
def unpack(self, fmt):
size = struct.calcsize(fmt)
return struct.unpack(fmt, self.take(size))
def string(self):
(n,) = self.unpack(">H")
return self.take(n).decode("utf-8", "replace")
def payload(self, t):
if t == BYTE:
return self.unpack(">b")[0]
if t == SHORT:
return self.unpack(">h")[0]
if t == INT:
return self.unpack(">i")[0]
if t == LONG:
return self.unpack(">q")[0]
if t == FLOAT:
return self.unpack(">f")[0]
if t == DOUBLE:
return self.unpack(">d")[0]
if t == BYTE_ARRAY:
(n,) = self.unpack(">i")
return self.take(n)
if t == STRING:
return self.string()
if t == LIST:
et, n = self.unpack(">bi")
return [self.payload(et) for _ in range(max(0, n))]
if t == COMPOUND:
out = {}
while True:
(ct,) = self.unpack(">b")
if ct == END:
return out
name = self.string()
out[name] = self.payload(ct)
if t == INT_ARRAY:
(n,) = self.unpack(">i")
return list(struct.unpack(f">{n}i", self.take(4 * n)))
if t == LONG_ARRAY:
(n,) = self.unpack(">i")
return list(struct.unpack(f">{n}q", self.take(8 * n)))
raise ValueError(f"bad NBT tag type {t}")
def loads(data):
"""Parse an uncompressed NBT blob -> (root name, root compound)."""
r = _Reader(data)
(t,) = r.unpack(">b")
if t != COMPOUND:
raise ValueError("NBT root is not a compound")
name = r.string()
return name, r.payload(COMPOUND)
def load_gzip(data):
"""level.dat style (gzip) NBT."""
return loads(gzip.decompress(data))
def region_chunks(data):
"""Yield (local_x, local_z, chunk_compound) for every chunk in an .mca blob."""
if len(data) < 8192:
return
for idx in range(1024):
off = int.from_bytes(data[idx * 4:idx * 4 + 3], "big")
if off == 0:
continue
start = off * 4096
if start + 5 > len(data):
continue
length = int.from_bytes(data[start:start + 4], "big")
comp = data[start + 4]
blob = data[start + 5:start + 4 + length]
try:
if comp == 2:
raw = zlib.decompress(blob)
elif comp == 1:
raw = gzip.GzipFile(fileobj=io.BytesIO(blob)).read()
elif comp == 3:
raw = blob
else: # 4 = lz4 (not in vanilla defaults) / 128+ = external .mcc
continue
_, chunk = loads(raw)
except (ValueError, zlib.error, OSError, struct.error):
continue
yield idx % 32, idx // 32, chunk
def _unsigned(v):
return v & 0xFFFFFFFFFFFFFFFF
def unpack_packed(longs, bits, count):
"""1.16+ packed array: entries never span longs."""
per = 64 // bits
mask = (1 << bits) - 1
out = []
for i in range(count):
word = _unsigned(longs[i // per])
out.append((word >> ((i % per) * bits)) & mask)
return out
def section_biome_grid(section):
"""64 biome ids (index = (y4*4 + z4)*4 + x4) for one chunk section, or None."""
b = section.get("biomes")
if not b or not b.get("palette"):
return None
pal = b["palette"]
if len(pal) == 1 or "data" not in b:
return [pal[0]] * 64
bits = max(1, math.ceil(math.log2(len(pal))))
return [pal[i] if i < len(pal) else pal[0] for i in unpack_packed(b["data"], bits, 64)]
def heightmap(chunk, kind="MOTION_BLOCKING_NO_LEAVES", min_y=-64, height=384):
"""256 world-y values (first free block above the top matching block), idx = z*16+x."""
hm = (chunk.get("Heightmaps") or {}).get(kind)
if not hm:
return None
bits = math.ceil(math.log2(height + 1))
return [v + min_y for v in unpack_packed(hm, bits, 256)]
def biome_at(chunk, x, y, z):
"""Biome id at block (x, y, z) in this chunk (x, z may be world coords)."""
sy = y >> 4
for s in chunk.get("sections", []):
if s.get("Y") == sy:
grid = section_biome_grid(s)
if grid is None:
return None
return grid[(((y & 15) >> 2) * 4 + ((z & 15) >> 2)) * 4 + ((x & 15) >> 2)]
return None
@@ -0,0 +1,49 @@
{
"source_zip": "/home/logan/onion-bench/worlds/bench-world-v1.zip",
"source_sha256": "144caf5fcdfea1a0eac0c787fdcded949e1a67df6ac112400f37be30ce7fb76e",
"seed": -306676217,
"avoid": [
[
0,
0
],
[
3000,
-9
]
],
"stride": 1,
"note": "y = first free block (full chunks: MOTION_BLOCKING_NO_LEAVES, proto chunks: WORLD_SURFACE_WG); the player stands on a barrier at y-1. Valid only for this seed + mod set (biomes are seed-deterministic).",
"sites": {
"river": {
"x": 904,
"y": 63,
"z": 88,
"biome": "minecraft:river",
"frac": 0.373,
"score": 0.373,
"full_chunk": false,
"candidates": 17
},
"jungle": {
"x": -8,
"y": 107,
"z": -201,
"biome": "minecraft:jungle",
"frac": 0.52,
"score": 0.52,
"full_chunk": false,
"candidates": 19
},
"warm_ocean": {
"x": -200,
"y": 63,
"z": 56,
"biome": "minecraft:warm_ocean",
"frac": 0.75,
"score": 0.75,
"full_chunk": false,
"candidates": 31
}
}
}
@@ -0,0 +1,413 @@
"""Offline tests for b14.py (temp dirs only; no game, no git writes)."""
import gzip
import json
import struct
import sys
import tempfile
import unittest
import zipfile
import zlib
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
sys.path.insert(0, str(HERE.parent.parent))
import b14 # noqa: E402
import bench # noqa: E402
import nbt # noqa: E402
AM_PARTIAL = """# partial
[spawning]
\tgazelleSpawnWeight = 20
\tcapuchinMonkeySpawnWeight = 14
\telephantSpawnWeight = 15
\tstraddlerSpawnWeight = 35
\trockyRollerSpawnWeight = 30
\tunderminerSpawnWeight = 25
\tblobfishSpawnWeight = 15
\tmudskipperSpawnWeight = 14
\tcaimanSpawnWeight = 15
"""
FP_PARTIAL = """// partial
{
"duckSpawnWeight": 2, "duckMaxGroupSize": 4, "gooseSpawnWeight": 2,
"gooseMaxGroupSize": 3, "gullSpawnWeight": 3, "gullMaxGroupSize": 5,
"sparrowSpawnWeight": 12, "sparrowMaxGroupSize": 4
}
"""
CAC_PARTIAL = """{
"configVersion": 2,
"spawning": {
"leaf_insect": [
{"biome_spec": "#c:is_jungle", "weight": 8, "min": 1, "max": 1},
{"biome_spec": "#c:is_forest", "weight": 4, "min": 1, "max": 1}
],
"sea_bunny": [
{"biome_spec": "minecraft:ocean", "weight": 10, "min": 1, "max": 2},
{"biome_spec": "minecraft:warm_ocean", "weight": 20, "min": 1, "max": 3}
],
"dragonfly": [
{"biome_spec": "#c:is_river", "weight": 4, "min": 1, "max": 1},
{"biome_spec": "#c:is_lush", "weight": 6, "min": 1, "max": 1}
]
}
}
"""
SHIPPED = {"alexsmobs-common.toml": AM_PARTIAL, "fowlplay.json5": FP_PARTIAL,
"crittersandcompanions-common.json5": CAC_PARTIAL}
# What the mods write back (re-saved, json5 unquoted keys, tabs) with default values.
AM_RESAVED_DEFAULT = "[spawning]\n" + "".join(
f"\t#Spawn Weight\n\t# Default: {v}\n\t{k} = {v}\n"
for k, v in b14.EXPECT_DEFAULTS["alexsmobs-common.toml"].items())
FP_RESAVED_DEFAULT = "{\n" + ",\n".join(
f"\t{k}: {v}" for k, v in b14.EXPECT_DEFAULTS["fowlplay.json5"].items()) + "\n}"
CAC_RESAVED_DEFAULT = """{
\tconfigVersion: 2,
\tspawning: {
\t\tleaf_insect: [
\t\t\t{
\t\t\t\tbiome_spec: "#c:is_jungle",
\t\t\t\tweight: 14,
\t\t\t\tmin: 1,
\t\t\t\tmax: 1
\t\t\t},
\t\t\t{ biome_spec: "#c:is_forest", weight: 6, min: 1, max: 1 }
\t\t],
\t\tred_panda: [ { biome_spec: "#c:is_jungle", weight: 8, min: 1, max: 2 } ],
\t\tsea_bunny: [
\t\t\t{ biome_spec: "minecraft:ocean", weight: 16, min: 1, max: 2 },
\t\t\t{ biome_spec: "minecraft:warm_ocean", weight: 32, min: 1, max: 4 }
\t\t],
\t\tdragonfly: [
\t\t\t{ biome_spec: "#c:is_river", weight: 4, min: 1, max: 1 },
\t\t\t{ biome_spec: "#c:is_lush", weight: 10, min: 1, max: 1 }
\t\t]
\t}
}"""
class ConfigVariants(unittest.TestCase):
def test_render(self):
d = b14.render_variant("defaults", SHIPPED)
self.assertEqual(set(d), set(b14.SPAWN_FILES))
self.assertTrue(all(v is None for v in d.values()))
s = b14.render_variant("shipped", SHIPPED)
self.assertEqual(s["fowlplay.json5"], FP_PARTIAL)
with self.assertRaises(ValueError):
b14.render_variant("nope", SHIPPED)
def test_extract_partial_and_resaved(self):
keys = b14.EXPECT_DEFAULTS
got = b14.extract_values("crittersandcompanions-common.json5", CAC_PARTIAL,
keys["crittersandcompanions-common.json5"])
self.assertEqual(got["sea_bunny@minecraft:warm_ocean"], 20)
self.assertEqual(got["leaf_insect@#c:is_jungle"], 8)
self.assertEqual(got["dragonfly@#c:is_lush"], 6)
got = b14.extract_values("crittersandcompanions-common.json5", CAC_RESAVED_DEFAULT,
keys["crittersandcompanions-common.json5"])
self.assertEqual(got, keys["crittersandcompanions-common.json5"])
self.assertEqual(b14.extract_values("fowlplay.json5", FP_RESAVED_DEFAULT,
keys["fowlplay.json5"]), keys["fowlplay.json5"])
self.assertEqual(b14.extract_values("alexsmobs-common.toml", AM_RESAVED_DEFAULT,
keys["alexsmobs-common.toml"]),
keys["alexsmobs-common.toml"])
# "Default: 40" comment lines must not be mistaken for the value
self.assertEqual(b14.extract_values("alexsmobs-common.toml", AM_PARTIAL,
["gazelleSpawnWeight"]), {"gazelleSpawnWeight": 20})
def test_check_config_and_journaled_apply(self):
with tempfile.TemporaryDirectory() as td:
game = Path(td)
(game / "config").mkdir()
originals = {"alexsmobs-common.toml": AM_RESAVED_DEFAULT + "# orig\n",
"fowlplay.json5": FP_RESAVED_DEFAULT} # CAC absent on purpose
for n, t in originals.items():
(game / "config" / n).write_text(t)
j = bench.Journal.begin(game, "b14-1")
b14.apply_spawn_config(game, j, b14.render_variant("shipped", SHIPPED))
for n in b14.SPAWN_FILES:
self.assertEqual((game / "config" / n).read_text(), SHIPPED[n])
self.assertTrue(b14.check_config(game, "shipped", SHIPPED)["ok"])
chk = b14.check_config(game, "defaults", SHIPPED)
self.assertFalse(chk["ok"])
self.assertIn("fowlplay.json5:duckSpawnWeight want 4 got 2", chk["mismatch"])
self.assertEqual(bench.restore_pending(game), [])
for n, t in originals.items():
self.assertEqual((game / "config" / n).read_text(), t)
self.assertFalse((game / "config" / "crittersandcompanions-common.json5").exists())
self.assertFalse((game / bench.PENDING).exists())
def test_defaults_delete_and_restore(self):
with tempfile.TemporaryDirectory() as td:
game = Path(td)
(game / "config").mkdir()
for n in b14.SPAWN_FILES:
(game / "config" / n).write_text("x" + n)
j = bench.Journal.begin(game, "b14-2")
b14.apply_spawn_config(game, j, b14.render_variant("defaults", SHIPPED))
self.assertFalse(any((game / "config" / n).exists() for n in b14.SPAWN_FILES))
self.assertTrue(b14.check_config(game, "defaults", SHIPPED)["ok"]) # absent = defaults
self.assertFalse(b14.check_config(game, "shipped", SHIPPED)["ok"])
# what the mods write after load
(game / "config" / "alexsmobs-common.toml").write_text(AM_RESAVED_DEFAULT)
(game / "config" / "fowlplay.json5").write_text(FP_RESAVED_DEFAULT)
(game / "config" / "crittersandcompanions-common.json5").write_text(CAC_RESAVED_DEFAULT)
self.assertTrue(b14.check_config(game, "defaults", SHIPPED)["ok"])
bench.restore_pending(game)
for n in b14.SPAWN_FILES:
self.assertEqual((game / "config" / n).read_text(), "x" + n)
def test_config_diff(self):
with tempfile.TemporaryDirectory() as td:
game = Path(td)
(game / "config").mkdir()
(game / "config" / "fowlplay.json5").write_text(FP_RESAVED_DEFAULT)
txt = b14.config_diff(game, b14.render_variant("shipped", SHIPPED), SHIPPED)
self.assertIn("duckSpawnWeight: 4 -> 2 <- changes", txt)
self.assertIn("config/alexsmobs-common.toml: WRITE", txt)
txt = b14.config_diff(game, b14.render_variant("defaults", SHIPPED), SHIPPED)
self.assertIn("config/fowlplay.json5: DELETE", txt)
SITES = [("river", {"x": 904, "y": 63, "z": 88}), ("warm_ocean", {"x": -200, "y": 63, "z": 56})]
class Datapack(unittest.TestCase):
def setUp(self):
self.files = b14.generate_datapack("b14-20261018000000", SITES, 2, 20)
self.base = "data/b14/function/b14-20261018000000"
def test_structure(self):
f = self.files
self.assertEqual(json.loads(f["pack.mcmeta"])["pack"]["pack_format"], 48)
self.assertEqual(json.loads(f["data/minecraft/tags/function/load.json"]),
{"values": ["b14:b14-20261018000000/load"]})
begin = f[f"{self.base}/begin.mcfunction"]
self.assertIn("gamerule doMobSpawning true", begin)
self.assertIn("gamemode creative @a", begin)
self.assertNotIn("spectator", "".join(f.values()))
self.assertIn("step0 400t", begin)
# 2 sites x (tp, arrive, t=0..2) + done
steps = [k for k in f if "/step" in k]
self.assertEqual(len(steps), 2 * (2 + 3) + 1)
self.assertIn("B14_DONE", f[f"{self.base}/step{len(steps) - 1}.mcfunction"])
def test_counts_and_chain(self):
count = self.files[f"{self.base}/count.mcfunction"]
for _, eid in b14.TUNED + b14.VANILLA:
self.assertIn(f"@e[type={eid},distance=..128]", count)
self.assertIn("@e[type=!minecraft:player,distance=..128]", count)
self.assertEqual(count.count("+= #"), len(b14.TUNED))
tl = b14.build_timeline("b14-20261018000000", SITES, 2, 20)
delays = [d for _, d in tl]
self.assertEqual(delays[:5], [100, 200, 1200, 1200, 20])
self.assertEqual(delays[-1], 0)
for i in range(len(tl) - 1):
body = self.files[f"{self.base}/step{i}.mcfunction"]
self.assertIn(f"schedule function b14:b14-20261018000000/step{i + 1} ", body)
s2 = self.files[f"{self.base}/step2.mcfunction"]
self.assertIn("setblock 904 62 88 minecraft:barrier", s2)
self.assertIn("tp @a 904.5 63 88.5", s2)
self.assertIn("if biome ~ ~ ~ minecraft:river", s2)
def test_tellraw_roundtrip(self):
"""The rendered tellraw text, with scores substituted, parses back."""
cmd = b14.tellraw("b14-20261018000000", "river", 3)
parts = json.loads(cmd[len("tellraw @a "):])
text = "".join(p if isinstance(p, str) else p.get("text", "7") for p in parts)
line = ("[18Oct2026 00:10:00.000] [Render thread/INFO] [net.minecraft.client.gui."
f"components.ChatComponent/]: [System] [CHAT] {text}")
r = b14.parse_count_line(line)
self.assertEqual((r["sid"], r["biome"], r["t"], r["total"], r["biome_ok"]),
("b14-20261018000000", "river", 3, 7, 7))
self.assertEqual(set(r["counts"]), {eid for _, eid in b14.TUNED + b14.VANILLA})
def test_world_build(self):
with tempfile.TemporaryDirectory() as td:
td = Path(td)
zp = td / "w.zip"
with zipfile.ZipFile(zp, "w") as z:
z.writestr("onionbench/level.dat", b"lvl")
z.writestr("onionbench/region/r.0.0.mca", b"x")
z.writestr("onionbench/data/chunks.dat", b"x")
z.writestr("onionbench/datapacks/onion-bench/pack.mcmeta", "{}")
z.writestr("onionbench/serverconfig/a.toml", "a")
game = td / "game"
(game / "saves").mkdir(parents=True)
j = bench.Journal.begin(game, "b14-3")
root = b14.build_world(game, j, zp, self.files)
self.assertEqual((root / "level.dat").read_bytes(), b"lvl")
self.assertFalse((root / "region").exists())
self.assertFalse((root / "data").exists())
self.assertTrue((root / "serverconfig/a.toml").exists())
self.assertTrue((root / "datapacks" / b14.PACK / "pack.mcmeta").exists())
with self.assertRaises(ValueError):
b14.build_world(game, bench.Journal(game, "x"), zp, self.files)
bench.restore_pending(game)
self.assertFalse(root.exists())
def _line(sid, biome, t, total, counts, ok=1):
body = " ".join(f"{k}={v}" for k, v in counts.items())
return (f"[x] [Render thread/INFO] [ChatComponent/]: [System] [CHAT] B14 {sid} {biome} "
f"t={t} biome_ok={ok} total={total} nonitem={total} tuned=0 {body}")
class ParseAndSummary(unittest.TestCase):
def test_parse_dedupes_and_filters(self):
lines = [_line("b14-1", "river", 0, 5, {"fowlplay:duck": 1}),
_line("b14-1", "river", 0, 5, {"fowlplay:duck": 1}),
"[Server thread/INFO]: [Server] B14_ARRIVE b14-1 river 1 2 3",
_line("b14-2", "river", 0, 9, {"fowlplay:duck": 2}),
"noise B14 garbage"]
rows = b14.parse_lines(lines)
self.assertEqual(len(rows), 2)
self.assertEqual(len(b14.parse_lines(lines, "b14-2")), 1)
self.assertEqual(rows[0]["counts"], {"fowlplay:duck": 1})
def _rows(self, config, sid, biome, totals, duck):
out = []
for t, (tot, d) in enumerate(zip(totals, duck)):
r = b14.parse_count_line(_line(sid, biome, t, tot, {"fowlplay:duck": d,
"minecraft:cow": 1}))
out.append({**r, "config": config, "kind": "b14_count"})
return out
def _sess(self, sid, ok=True):
return {"kind": "b14_session", "sid": sid, "outcome": "ok",
"config_check": {"ok": ok}}
def test_keep_and_back_off(self):
counts = (self._rows("defaults", "b14-1", "river", [10, 40], [2, 6])
+ self._rows("shipped", "b14-2", "river", [8, 25], [0, 3]))
sess = [self._sess("b14-1"), self._sess("b14-2")]
out, verdicts, overall = b14.summarize(counts, sess)
self.assertEqual(verdicts["river"]["verdict"], "keep")
self.assertEqual(out["river"]["shipped"]["seen"]["fowlplay:duck"], 3)
self.assertEqual(overall, "incomplete") # jungle / warm_ocean missing
counts2 = (self._rows("defaults", "b14-1", "river", [10, 40], [2, 6])
+ self._rows("shipped", "b14-2", "river", [8, 25], [0, 0]))
_, v2, o2 = b14.summarize(counts2, sess)
self.assertEqual(v2["river"]["verdict"], "back off")
self.assertEqual(v2["river"]["disappeared"], ["fowlplay:duck"])
self.assertIn("back off", b14.render_summary(*b14.summarize(counts2, sess)))
def test_bad_sessions_and_biome_check_excluded(self):
counts = (self._rows("defaults", "b14-1", "river", [10, 40], [2, 6])
+ self._rows("shipped", "b14-2", "river", [8, 25], [1, 3]))
_, v, _ = b14.summarize(counts, [self._sess("b14-1"), self._sess("b14-2", ok=False)])
self.assertEqual(v["river"]["verdict"], "incomplete")
for r in counts:
r["biome_ok"] = 0
_, v, _ = b14.summarize(counts, [self._sess("b14-1"), self._sess("b14-2")])
self.assertTrue(v["river"]["verdict"].startswith("invalid"))
def test_all_biomes_keep(self):
counts, sess = [], []
for i, biome in enumerate(b14.BIOMES):
counts += self._rows("defaults", f"b14-{i}0", biome, [5, 30], [1, 2])
counts += self._rows("shipped", f"b14-{i}1", biome, [5, 20], [1, 1])
sess += [self._sess(f"b14-{i}0"), self._sess(f"b14-{i}1")]
self.assertEqual(b14.summarize(counts, sess)[2], "keep")
# ---------------------------------------------------------------- nbt / locate
def _tag(t, name, payload):
n = name.encode()
return bytes([t]) + struct.pack(">H", len(n)) + n + payload
def _str(s):
b = s.encode()
return struct.pack(">H", len(b)) + b
def _compound(*items):
return b"".join(items) + b"\x00"
def _chunk_nbt(biome, height=70, status="minecraft:full"):
hm_longs = []
vals = [height + 64] * 256 # stored relative to min_y -64, 9 bits, 7 per long
for i in range(0, 256, 7):
w = 0
for k, v in enumerate(vals[i:i + 7]):
w |= v << (9 * k)
hm_longs.append(w if w < 2 ** 63 else w - 2 ** 64)
hm = struct.pack(">i", len(hm_longs)) + struct.pack(f">{len(hm_longs)}q", *hm_longs)
section = _compound(
_tag(nbt.BYTE, "Y", struct.pack(">b", (height - 1) >> 4)),
_tag(nbt.COMPOUND, "biomes", _compound(
_tag(nbt.LIST, "palette", bytes([nbt.STRING]) + struct.pack(">i", 1) + _str(biome)))))
root = _compound(
_tag(nbt.STRING, "Status", _str(status)),
_tag(nbt.LIST, "sections", bytes([nbt.COMPOUND]) + struct.pack(">i", 1) + section),
_tag(nbt.COMPOUND, "Heightmaps", _compound(
_tag(nbt.LONG_ARRAY, "MOTION_BLOCKING_NO_LEAVES", hm))))
return bytes([nbt.COMPOUND]) + _str("") + root
def _region(chunks):
"""chunks: {(lx, lz): nbt bytes} -> .mca bytes."""
header, body, sector = bytearray(8192), bytearray(), 2
for (lx, lz), raw in chunks.items():
data = zlib.compress(raw)
blob = struct.pack(">I", len(data) + 1) + b"\x02" + data
blob += b"\x00" * (-len(blob) % 4096)
n = len(blob) // 4096
idx = lz * 32 + lx
header[idx * 4:idx * 4 + 4] = sector.to_bytes(3, "big") + bytes([n])
body += blob
sector += n
return bytes(header) + bytes(body)
class NbtAndLocate(unittest.TestCase):
def test_packed(self):
self.assertEqual(nbt.unpack_packed([0b10_01_00 | (3 << 62)], 2, 3), [0, 1, 2])
self.assertEqual(nbt.unpack_packed([-1], 4, 2), [15, 15])
def test_chunk_roundtrip(self):
_, c = nbt.loads(_chunk_nbt("minecraft:river", 63))
self.assertEqual(nbt.heightmap(c)[0], 63)
self.assertEqual(nbt.biome_at(c, 5, 62, 9), "minecraft:river")
ld = gzip.compress(bytes([nbt.COMPOUND]) + _str("") + _compound(
_tag(nbt.COMPOUND, "Data", _compound(_tag(nbt.INT, "SpawnX", struct.pack(">i", 7))))))
self.assertEqual(nbt.load_gzip(ld)[1]["Data"]["SpawnX"], 7)
def test_scan_and_pick(self):
reg = {}
for lx in range(5):
for lz in range(5):
reg[(lx, lz)] = _chunk_nbt("minecraft:river" if lx < 3 else "minecraft:forest", 63)
reg[(lx + 20, lz)] = _chunk_nbt("minecraft:warm_ocean", 63)
reg[(lx + 10, lz + 20)] = _chunk_nbt("minecraft:jungle", 80,
"minecraft:structure_starts")
with tempfile.TemporaryDirectory() as td:
zp = Path(td) / "w.zip"
with zipfile.ZipFile(zp, "w") as z:
z.writestr("w/level.dat", b"")
z.writestr("w/region/r.0.0.mca", _region(reg))
chunks = b14.scan_world(zp)
self.assertEqual(len(chunks), 50) # structure_starts placeholders skipped
sites = b14.pick_sites(chunks, avoid=[(-1000, -1000)])
self.assertEqual(sites["river"]["biome"], "minecraft:river")
self.assertEqual(sites["river"]["y"], 63)
self.assertIn("warm_ocean", sites)
self.assertNotIn("jungle", sites)
self.assertGreaterEqual(abs(sites["warm_ocean"]["x"] - sites["river"]["x"]), 320)
# avoid radius excludes everything near the avoid point
self.assertEqual(b14.pick_sites(chunks, avoid=[(40, 40), (360, 40)]), {})
class Busy(unittest.TestCase):
def test_ancestors_include_self(self):
import os
self.assertIn(os.getpid(), b14._ancestors(os.getpid()))
self.assertNotIn(os.getpid(), b14.bench_busy())
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""Generate Iris per-pack option files that reproduce a shaderpack's built-in profile.
Iris 1.8.x reads `shaderpacks/<pack name>.txt` (java.util.Properties) when it loads a
pack and applies the key=value pairs as option overrides; there is no "selected
profile" key - the GUI's profile button just writes the profile's option values, and
the profile shown is re-derived by matching values (ProfileSet.scan). So a profile is
reproduced by writing its resolved option list. bench.py --shader-options copies the
file into place for one session.
Profile syntax (shaders/shaders.properties, mirrored from Iris ProfileSet.parse):
profile.NAME = tok tok ... whitespace-separated, applied in order, later wins
profile.OTHER include another profile
!program.X disable a program (cannot be expressed in the options file -> error)
!opt opt=false
opt=val/opt:val opt=val
opt opt=true
python3 gen_shader_profiles.py # (re)write every B7 profile in matrix.toml
python3 gen_shader_profiles.py --check # exit 1 if any file is missing/stale
"""
import argparse
import hashlib
import re
import sys
import tomllib
import zipfile
from pathlib import Path
HERE = Path(__file__).resolve().parent
OUT = HERE / "shader-profiles"
PACKS = Path.home() / "onion-bench" / "prism" / "instances" / "OnionBench" / ".minecraft" / "shaderpacks"
MATRIX = HERE / "matrix.toml"
IF_RE = re.compile(r"^\s*#\s*if(n?def)?\b")
ENDIF_RE = re.compile(r"^\s*#\s*endif\b")
NAME_RE = re.compile(r"^[A-Za-z0-9_.\-]+$")
SRC_EXT = (".glsl", ".fsh", ".vsh", ".gsh", ".csh", ".tcs", ".tes", ".inc")
DEFINE_RE = re.compile(r"^\s*(?://\s*)?#define\s+(\w+)", re.M)
CONST_RE = re.compile(r"\bconst\s+\w+\s+(\w+)\s*=")
class ProfileError(Exception):
pass
def logical_lines(text):
"""Java-properties logical lines: CRLF normalised, trailing-backslash continuation."""
out, buf = [], ""
for raw in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
line = raw if not buf else raw.lstrip()
if line.endswith("\\") and not line.endswith("\\\\"):
buf += line[:-1]
continue
out.append(buf + line)
buf = ""
if buf:
out.append(buf)
return out
def parse_profiles(text):
"""{name: [tokens]} for every profile.NAME line. Profiles inside preprocessor
conditionals can't be evaluated without the pack's macro environment -> error."""
profiles, depth = {}, 0
for line in logical_lines(text):
s = line.strip()
if IF_RE.match(s):
depth += 1
continue
if ENDIF_RE.match(s):
depth = max(0, depth - 1)
continue
if not s or s[0] in "#!":
continue
m = re.match(r"([^=:\s]+)\s*[=:]\s*(.*)$", s)
if not m or not m.group(1).startswith("profile."):
continue
name = m.group(1)[len("profile."):]
if depth:
raise ProfileError(f"profile.{name} is inside a preprocessor conditional")
profiles[name] = m.group(2).split()
return profiles
def resolve(name, profiles, _stack=()):
"""(options dict in application order, disabled programs) for one profile."""
if name not in profiles:
raise ProfileError(f"unknown profile {name!r} (have {sorted(profiles)})")
if name in _stack:
raise ProfileError("profile cycle: " + " -> ".join((*_stack, name)))
opts, programs = {}, []
for tok in profiles[name]:
if tok.startswith("!program."):
programs.append(tok[len("!program."):])
elif tok.startswith("profile."):
sub, subprog = resolve(tok[len("profile."):], profiles, (*_stack, name))
for k, v in sub.items():
opts.pop(k, None)
opts[k] = v
programs += subprog
else:
if tok.startswith("!"):
k, v = tok[1:], "false"
elif "=" in tok:
k, v = tok.split("=", 1)
elif ":" in tok:
k, v = tok.split(":", 1)
else:
k, v = tok, "true"
opts.pop(k, None)
opts[k] = v
return opts, programs
def render(opts, header=()):
lines = [f"# {h}" for h in header]
for k, v in opts.items():
if not NAME_RE.match(k) or not v or any(c.isspace() for c in v) or "\\" in v:
raise ProfileError(f"option {k}={v!r} would need properties escaping")
lines.append(f"{k}={v}")
return "\n".join(lines) + "\n"
def pack_sources(zf):
props = [n for n in zf.namelist() if n.endswith("shaders/shaders.properties")]
if len(props) != 1:
raise ProfileError(f"expected one shaders/shaders.properties, found {props}")
return zf.read(props[0]).decode("utf-8", "replace")
def known_options(zf):
names = set()
for n in zf.namelist():
if n.endswith(SRC_EXT):
t = zf.read(n).decode("utf-8", "replace")
names |= set(DEFINE_RE.findall(t)) | set(CONST_RE.findall(t))
return names
def out_name(pack, profile):
stem = pack[:-4] if pack.endswith(".zip") else pack
return f"{stem}.{profile}.txt"
def generate(pack_path, profile):
"""(file text, option count, unknown option names) for one pack/profile."""
pack_path = Path(pack_path)
with zipfile.ZipFile(pack_path) as zf:
opts, programs = resolve(profile, parse_profiles(pack_sources(zf)))
if programs:
raise ProfileError(f"{profile} disables programs {programs}; an options file "
"cannot express that")
unknown = sorted(set(opts) - known_options(zf))
sha = hashlib.sha256(pack_path.read_bytes()).hexdigest()
header = [f"Iris options reproducing profile.{profile} of {pack_path.name}",
f"pack sha256 {sha}",
"generated by gen_shader_profiles.py - do not edit; Iris rewrites the live",
"copy keeping only non-default values (bench.py journals and restores it)"]
return render(opts, header), len(opts), unknown
def matrix_targets(path=MATRIX):
"""(pack, profile) for every session in matrix.toml with a shader_profile."""
with open(path, "rb") as f:
m = tomllib.load(f)
seen = []
for b in m.get("block", []):
for s in b.get("sessions", []):
if s.get("shader_profile"):
t = (s["shader"], s["shader_profile"])
if t not in seen:
seen.append(t)
return seen
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--packs", type=Path, default=PACKS, help="shaderpacks directory")
ap.add_argument("--out", type=Path, default=OUT)
ap.add_argument("--target", action="append", metavar="PACK:PROFILE",
help="instead of the matrix.toml B7 rows")
ap.add_argument("--check", action="store_true", help="verify files are current, write nothing")
a = ap.parse_args(argv)
targets = ([tuple(t.rsplit(":", 1)) for t in a.target] if a.target else matrix_targets())
rc = 0
for pack, profile in targets:
text, n, unknown = generate(a.packs / pack, profile)
dst = a.out / out_name(pack, profile)
note = f" (not found in pack sources: {', '.join(unknown)})" if unknown else ""
if a.check:
ok = dst.is_file() and dst.read_text() == text
print(f"{'ok ' if ok else 'STALE'} {dst.name}: {n} options{note}")
rc |= 0 if ok else 1
else:
a.out.mkdir(parents=True, exist_ok=True)
dst.write_text(text)
print(f"wrote {dst.relative_to(HERE) if dst.is_relative_to(HERE) else dst}: "
f"{n} options{note}")
return rc
if __name__ == "__main__":
sys.exit(main())
+622
View File
@@ -0,0 +1,622 @@
#!/usr/bin/env python3
"""Run the BENCH-MATRIX blocks declared in matrix.toml through `bench.py session`.
python3 matrix.py list # blocks, session counts, est. time
python3 matrix.py plan B3 [--param k=v] # the bench.py commands a block runs
python3 matrix.py run B0,B1 [--dry-run] # sequential, resumable
python3 matrix.py status # progress for the current mod set
Resume: ~/onion-bench/results/matrix-state.json records each session (key
"<block>#<index>") with a hash of its exact command and the instance mods/ hash; a
session is skipped only when both still match. A failed session (non-zero exit: crash,
timeout, no join) is retried once, then the run stops. Running any block other than B0
warns loudly when B0 was not run under the current mod set. Stdlib only.
"""
import argparse
import glob
import hashlib
import json
import os
import re
import shlex
import subprocess
import sys
import time
import tomllib
from pathlib import Path
HERE = Path(__file__).resolve().parent
MATRIX = HERE / "matrix.toml"
OUT_DIR = HERE.parent / "proposed-configs" / "out"
GAME = Path.home() / "onion-bench" / "prism" / "instances" / "OnionBench" / ".minecraft"
STATE = Path.home() / "onion-bench" / "results" / "matrix-state.json"
SCENE_ORDER = ["s1", "s2", "s2s", "s3", "s4", "s5"]
S5_SECONDS = 10 + 120 # bench.py: at S1, wait 10 s, then the 120 s tour (all captured)
JVM_SETS = {"J0", "J1", "J2", "J3"}
DISPLAYS = {"desktop", "gamescope", "gamescope-headless"}
PARAM_RE = re.compile(r"\$\{(\w+)\}")
TOP_KEYS = {"defaults", "worlds", "trials", "missing_variants", "block"}
DEFAULT_KEYS = {"reps", "warmup", "capture", "settle", "display", "res", "world", "shader", "dh",
"launch_overhead_s"}
BLOCK_KEYS = {"id", "title", "rule", "proxy_note", "manual_checks", "sessions", "params",
"deferred", "manual", "interleave"}
SESSION_KEYS = {"variant", "plan", "reps", "repeat", "tag", "jvm", "heap", "shader",
"shader_profile", "shader_options", "dh", "disable_mods", "add_mods", "world", "warmup",
"capture", "settle", "manual", "note", "display", "res"}
MISSING_KEYS = {"id", "blocks", "contents"}
# Flags `bench.py session` accepts; bench_argv must emit only these.
BENCH_SESSION_FLAGS = {
"--variant", "--plan", "--reps", "--warmup", "--capture", "--settle", "--label", "--jvm",
"--heap", "--boot-timeout", "--timeout", "--any-order", "--kill-stale", "--dry-run",
"--display", "--res", "--shader", "--shader-options", "--dh", "--disable-mods",
"--add-mods", "--world-zip",
}
class MatrixError(Exception):
pass
def log(msg):
print(f"[matrix] {msg}", flush=True)
# ------------------------------------------------------------------ loading / validation
def load(path=MATRIX, out_dir=OUT_DIR):
"""Parse + validate matrix.toml. Raises MatrixError listing every problem."""
with open(path, "rb") as f:
m = tomllib.load(f)
errors = validate(m, out_dir)
if errors:
raise MatrixError("matrix.toml invalid:\n " + "\n ".join(errors))
return m
def split_plan(plan):
if isinstance(plan, str):
return [s.strip() for s in plan.split(",") if s.strip()]
return list(plan)
def _pos_int(v):
return isinstance(v, int) and not isinstance(v, bool) and v > 0
def validate(m, out_dir=OUT_DIR):
err = []
for k in m:
if k not in TOP_KEYS:
err.append(f"unknown top-level key {k!r}")
d = m.get("defaults", {})
for k in d:
if k not in DEFAULT_KEYS:
err.append(f"defaults: unknown key {k!r}")
worlds, trials = m.get("worlds", {}), m.get("trials", {})
missing_ids = set()
for mv in m.get("missing_variants", []):
for k in mv:
if k not in MISSING_KEYS:
err.append(f"missing_variants {mv.get('id')}: unknown key {k!r}")
if not mv.get("id") or not mv.get("contents"):
err.append("missing_variants entry needs id and contents")
missing_ids.add(mv.get("id"))
_check_common(d, "defaults", worlds, trials, err)
seen = set()
for b in m.get("block", []):
bid = b.get("id")
where = f"block {bid}"
if not bid:
err.append("block without id")
continue
if bid in seen:
err.append(f"{where}: duplicate id")
seen.add(bid)
for k in b:
if k not in BLOCK_KEYS:
err.append(f"{where}: unknown key {k!r}")
if not b.get("rule"):
err.append(f"{where}: missing decision rule")
params = set(b.get("params", []))
sessions = b.get("sessions", [])
if not sessions and not (b.get("manual") or b.get("deferred")):
err.append(f"{where}: no sessions (mark manual or deferred)")
groups = set()
for i, s in enumerate(sessions):
sw = f"{where} session {i}"
for k in s:
if k not in SESSION_KEYS:
err.append(f"{sw}: unknown key {k!r}")
for k in ("variant", "jvm", "plan"):
v = s.get(k)
for p in PARAM_RE.findall(v if isinstance(v, str) else ",".join(v or [])):
if p not in params:
err.append(f"{sw}: ${{{p}}} not declared in params")
var = s.get("variant")
if not var:
err.append(f"{sw}: missing variant")
elif not PARAM_RE.search(var) and not (Path(out_dir) / var).is_dir() \
and var not in missing_ids:
err.append(f"{sw}: variant {var!r} not in {out_dir} nor in missing_variants")
plan = split_plan(s.get("plan", []))
if not plan:
err.append(f"{sw}: empty plan")
concrete = [p for p in plan if not PARAM_RE.search(p)]
for p in concrete:
if p not in SCENE_ORDER:
err.append(f"{sw}: unknown scene {p!r} (S6 is manual-only)")
idx = [SCENE_ORDER.index(p) for p in concrete if p in SCENE_ORDER]
if idx != sorted(set(idx)):
err.append(f"{sw}: plan must be in S1->S5 order without duplicates")
if "repeat" in s and not _pos_int(s["repeat"]):
err.append(f"{sw}: repeat must be a positive int")
if "heap" in s and not _pos_int(s["heap"]):
err.append(f"{sw}: heap must be a positive int (MB)")
jvm = s.get("jvm")
if jvm is not None and jvm not in JVM_SETS and not PARAM_RE.search(jvm):
err.append(f"{sw}: jvm {jvm!r} not in {sorted(JVM_SETS)}")
for t in s.get("add_mods", []):
if t not in trials:
err.append(f"{sw}: add_mods {t!r} not in [trials]")
if not isinstance(s.get("disable_mods", []), list):
err.append(f"{sw}: disable_mods must be a list")
_check_common(s, sw, worlds, trials, err)
g = (var, s.get("tag"))
if g in groups:
err.append(f"{sw}: variant {var!r} repeated without a distinct tag "
f"(analyze.py would merge them); use repeat or tag")
groups.add(g)
return err
def _check_common(s, where, worlds, trials, err):
for k in ("reps", "warmup", "capture", "settle"):
if k in s and not _pos_int(s[k]):
err.append(f"{where}: {k} must be a positive int")
if "display" in s and s["display"] not in DISPLAYS:
err.append(f"{where}: display {s['display']!r} not in {sorted(DISPLAYS)}")
if "res" in s and not re.fullmatch(r"\d+x\d+", str(s["res"])):
err.append(f"{where}: res must be WxH")
if "dh" in s and s["dh"] not in ("on", "off"):
err.append(f"{where}: dh must be on/off")
if "world" in s and s["world"] not in worlds:
err.append(f"{where}: world {s['world']!r} not in [worlds]")
if "shader_options" in s and s.get("shader", "off") == "off":
err.append(f"{where}: shader_options needs shader = <pack>")
def block_map(m):
return {b["id"]: b for b in m.get("block", [])}
def missing_variant_ids(m):
return {mv["id"] for mv in m.get("missing_variants", [])}
# ------------------------------------------------------------------ expansion / commands
def _subst(v, params):
if not isinstance(v, str):
return v
return PARAM_RE.sub(lambda mo: params.get(mo.group(1), mo.group(0)), v)
def expand(m, block, params=None, overrides=None):
"""Flat list of concrete sessions for a block (repeat / interleave applied)."""
params, overrides = params or {}, overrides or {}
d = dict(m.get("defaults", {}))
d.update({k: v for k, v in overrides.items() if v is not None})
groups = []
for si, s in enumerate(block.get("sessions", [])):
merged = {k: v for k, v in d.items() if k != "launch_overhead_s"}
merged.update(s)
for k in ("display", "res"): # CLI overrides beat per-session values too
if overrides.get(k) is not None:
merged[k] = overrides[k]
merged["variant"] = _subst(merged["variant"], params)
if merged.get("jvm") is not None:
merged["jvm"] = _subst(merged["jvm"], params)
merged["plan"] = [_subst(p, params) for p in split_plan(merged["plan"])]
tag = s.get("tag")
merged["label"] = f"{block['id']}.{tag}" if tag else block["id"]
merged["source"] = si
groups.append([dict(merged, run=r + 1) for r in range(s.get("repeat", 1))])
out = []
if block.get("interleave"):
for r in range(max((len(g) for g in groups), default=0)):
out += [g[r] for g in groups if r < len(g)]
else:
for g in groups:
out += g
for i, s in enumerate(out):
s["index"] = i
s["block"] = block["id"]
return out
def unresolved(sess):
"""Placeholder names still present in a session (missing --param)."""
vals = [sess["variant"], sess.get("jvm") or "", *sess["plan"]]
return sorted({p for v in vals for p in PARAM_RE.findall(v)})
def resolve_path(p):
p = os.path.expanduser(p)
hits = sorted(glob.glob(p))
return hits[-1] if hits else None
def shader_options_path(p):
"""shader_options are harness-relative unless absolute / ~."""
p = Path(os.path.expanduser(p))
return p if p.is_absolute() else HERE / p
def bench_argv(m, sess, live_world=False):
"""`bench.py session` argv for one session, plus a list of missing inputs."""
missing = []
a = ["session", "--variant", sess["variant"], "--plan", ",".join(sess["plan"]),
"--reps", str(sess.get("reps", 3)), "--label", sess["label"]]
for k in ("warmup", "capture", "settle"):
if k in sess:
a += [f"--{k}", str(sess[k])]
for k in ("display", "res", "shader", "dh", "jvm"):
if sess.get(k) is not None:
a += [f"--{k}", str(sess[k])]
if sess.get("heap"):
a += ["--heap", str(sess["heap"])]
if sess.get("shader_options"):
so = shader_options_path(sess["shader_options"])
if not so.is_file():
missing.append(f"shader options {so} (run gen_shader_profiles.py)")
a += ["--shader-options", str(so)]
if sess.get("disable_mods"):
a += ["--disable-mods", ",".join(sess["disable_mods"])]
if sess.get("add_mods"):
jars = []
for t in sess["add_mods"]:
pat = m["trials"][t]
hit = resolve_path(pat)
if hit is None:
missing.append(f"trial jar {t}: {pat}")
hit = os.path.expanduser(pat)
jars.append(hit)
a += ["--add-mods", ",".join(jars)]
if sess.get("world") and not live_world:
pat = m["worlds"][sess["world"]]
hit = resolve_path(pat)
if hit is None:
missing.append(f"world {sess['world']}: {pat} (or run with --live-world)")
hit = os.path.expanduser(pat)
a += ["--world-zip", hit]
return a, missing
def variant_missing(sess, out_dir=OUT_DIR):
return not (Path(out_dir) / sess["variant"]).is_dir()
def session_seconds(sess, overhead):
reps, w, c = sess.get("reps", 3), sess.get("warmup", 15), sess.get("capture", 30)
scenes = sum(S5_SECONDS if p == "s5" else w + c for p in sess["plan"]) * reps
return overhead + sess.get("settle", 10) + scenes
def spec_hash(argv, sess):
spec = {"argv": argv, "manual": bool(sess.get("manual"))}
if sess.get("shader_options"): # a regenerated profile file means a new spec
so = shader_options_path(sess["shader_options"])
spec["shader_options_sha256"] = (hashlib.sha256(so.read_bytes()).hexdigest()
if so.is_file() else None)
blob = json.dumps(spec, sort_keys=True)
return hashlib.sha256(blob.encode()).hexdigest()[:16]
def fmt_cmd(argv):
return "python3 bench.py " + shlex.join(argv)
# ------------------------------------------------------------------ state
def mods_hash(mods_dir):
"""Same algorithm as bench.py mods_hash (name + size of every file in mods/)."""
h = hashlib.sha256()
files = sorted(p for p in Path(mods_dir).glob("*") if p.is_file())
for p in files:
h.update(f"{p.name}\0{p.stat().st_size}\n".encode())
return h.hexdigest()
class State:
def __init__(self, path):
self.path = Path(path)
self.data = {"version": 1, "sessions": {}, "b0": None}
if self.path.exists():
self.data.update(json.loads(self.path.read_text()))
def save(self):
self.path.parent.mkdir(parents=True, exist_ok=True)
tmp = self.path.with_suffix(".tmp")
tmp.write_text(json.dumps(self.data, indent=1, sort_keys=True))
tmp.replace(self.path)
@staticmethod
def key(sess):
return f"{sess['block']}#{sess['index']}"
def entry(self, sess):
return self.data["sessions"].get(self.key(sess))
def is_done(self, sess, spec, mods):
e = self.entry(sess)
return bool(e and e["status"] == "ok" and e["spec"] == spec and e["mods_hash"] == mods)
def record(self, sess, spec, mods, status, rc, attempts):
self.data["sessions"][self.key(sess)] = {
"spec": spec, "mods_hash": mods, "status": status, "rc": rc,
"attempts": attempts, "label": sess["label"], "variant": sess["variant"],
"ts": time.strftime("%Y-%m-%dT%H:%M:%S")}
def b0_warning(state, current):
b0 = state.data.get("b0")
if not b0:
return "B0 (noise floor) has not been completed - win thresholds are undefined"
if b0["mods_hash"] != current:
return (f"MOD SET CHANGED since B0 (B0 under {b0['mods_hash'][:12]}, now "
f"{current[:12]}): B0 MUST BE RE-RUN before results are comparable")
return None
def banner(msg):
bar = "!" * 78
print(f"{bar}\n!! {msg}\n{bar}", file=sys.stderr, flush=True)
# ------------------------------------------------------------------ run
def default_runner(argv):
return subprocess.run([sys.executable, str(HERE / "bench.py"), *argv], cwd=HERE).returncode
def run_blocks(m, block_ids, state, mods_dir, runner=default_runner, params=None,
overrides=None, dry_run=False, include_manual=False, include_deferred=False,
live_world=False, skip_blocked=False, out_dir=OUT_DIR):
"""Run blocks in order. Returns 0 ok, 1 session failed, 2 preflight problem."""
bm = block_map(m)
for bid in block_ids:
if bid not in bm:
raise MatrixError(f"unknown block {bid}; known: {', '.join(bm)}")
# preflight: build every session's command, collect what can't run
todo, problems = [], []
for bid in block_ids:
b = bm[bid]
if b.get("deferred") and not include_deferred:
log(f"{bid}: deferred ({b.get('proxy_note', 'fleet-only')}) - skipped")
continue
if b.get("manual"):
log(f"{bid}: manual block - run by hand ({b.get('proxy_note', '')})")
continue
for s in expand(m, b, params, overrides):
if s.get("manual") and not include_manual:
log(f"{bid}#{s['index']} {s['label']}: manual - skipped")
continue
miss = [f"param ${{{p}}} (use --param {p}=...)" for p in unresolved(s)]
if not miss and variant_missing(s, out_dir):
miss.append(f"variant {s['variant']} not generated (see missing_variants)")
argv, inputs = bench_argv(m, s, live_world)
miss += inputs
if miss:
problems.append((s, miss))
continue
todo.append((s, argv))
for s, miss in problems:
log(f"BLOCKED {s['block']}#{s['index']} {s['label']}: " + "; ".join(miss))
if problems and not (skip_blocked or dry_run):
log("fix the above, or pass --skip-blocked (and/or --live-world)")
return 2
mods0 = mods_hash(mods_dir)
if "B0" not in block_ids and any(s["block"] != "B0" for s, _ in todo):
w = b0_warning(state, mods0)
if w:
banner(w)
total = len(todo)
for n, (s, argv) in enumerate(todo, 1):
spec = spec_hash(argv, s)
tagline = f"[{n}/{total}] {s['block']}#{s['index']} {s['label']}"
if state.is_done(s, spec, mods0):
log(f"{tagline}: done - skip")
continue
if dry_run:
log(f"{tagline}: {fmt_cmd(argv)}")
continue
now = mods_hash(mods_dir)
if now != mods0:
banner(f"instance mods/ changed during the run ({mods0[:12]} -> {now[:12]}); "
"a session did not restore it - stopping")
return 1
rc = None
for attempt in (1, 2):
log(f"{tagline}: attempt {attempt}: {fmt_cmd(argv)}")
rc = runner(argv)
if rc == 0:
break
log(f"{tagline}: exit {rc}" + (" - retrying once" if attempt == 1 else ""))
status = "ok" if rc == 0 else "failed"
state.record(s, spec, mods0, status, rc, attempt)
if s["block"] == "B0":
_maybe_close_b0(m, state, mods0, params, overrides, live_world)
state.save()
if status != "ok":
log(f"{tagline}: failed twice - stopping the run")
return 1
return 0
def _maybe_close_b0(m, state, mods, params, overrides, live_world):
b = block_map(m).get("B0")
if not b:
return
for s in expand(m, b, params, overrides):
argv, _ = bench_argv(m, s, live_world)
if not state.is_done(s, spec_hash(argv, s), mods):
return
state.data["b0"] = {"mods_hash": mods, "ts": time.strftime("%Y-%m-%dT%H:%M:%S")}
# ------------------------------------------------------------------ list / plan / status
def block_summary(m, b, overhead, out_dir=OUT_DIR):
ss = expand(m, b)
auto = [s for s in ss if not s.get("manual") and not b.get("manual")]
blocked = [s for s in auto if not unresolved(s) and variant_missing(s, out_dir)]
secs = sum(session_seconds(s, overhead) for s in auto)
return {"sessions": len(ss), "auto": len(auto), "manual": len(ss) - len(auto),
"blocked": len(blocked), "seconds": secs}
def cmd_list(m, a):
overhead = m.get("defaults", {}).get("launch_overhead_s", 60)
print(f"| block | title | sessions | auto | manual | blocked (variant) | est. min | notes |")
print("| --- | --- | --- | --- | --- | --- | --- | --- |")
tot = {"sessions": 0, "auto": 0, "seconds": 0}
for b in m.get("block", []):
s = block_summary(m, b, overhead)
notes = []
if b.get("deferred"):
notes.append("deferred")
if b.get("manual"):
notes.append("manual block")
if b.get("params"):
notes.append("params: " + ",".join(b["params"]))
print(f"| {b['id']} | {b.get('title', '')} | {s['sessions']} | {s['auto']} | "
f"{s['manual']} | {s['blocked']} | {s['seconds'] / 60:.0f} | {'; '.join(notes)} |")
if not b.get("deferred"):
for k in tot:
tot[k] += s[k]
print(f"\nnot deferred: {tot['sessions']} sessions ({tot['auto']} unattended), "
f"est. {tot['seconds'] / 3600:.1f} h unattended")
return 0
def cmd_plan(m, a):
bm = block_map(m)
for bid in a.blocks.split(","):
if bid not in bm:
raise MatrixError(f"unknown block {bid}")
b = bm[bid]
print(f"## {bid} {b.get('title', '')}" + (" (DEFERRED)" if b.get("deferred") else "")
+ (" (MANUAL)" if b.get("manual") else ""))
print(f"rule: {b['rule']}")
if b.get("proxy_note"):
print(f"proxy: {b['proxy_note']}")
for c in b.get("manual_checks", []):
print(f"manual check: {c}")
for s in expand(m, b, a.params, overrides(a)):
argv, miss = bench_argv(m, s, a.live_world)
pre = "# MANUAL: " if s.get("manual") else ""
print(f"{pre}{fmt_cmd(argv)}")
if s.get("shader_profile") and not s.get("shader_options"):
print(f"# set Iris profile {s['shader_profile']} for {s['shader']} first")
elif s.get("shader_profile"):
print(f"# Iris profile {s['shader_profile']} via shader options file")
if s.get("note"):
print(f"# note: {s['note']}")
if unresolved(s):
print(f"# needs --param {','.join(unresolved(s))}")
if variant_missing(s) and not unresolved(s):
print(f"# BLOCKED: variant {s['variant']} not generated")
for x in miss:
print(f"# missing: {x}")
print()
return 0
def cmd_status(m, a):
state = State(a.state)
mods = mods_hash(a.mods_dir) if Path(a.mods_dir).is_dir() else ""
w = b0_warning(state, mods)
if w:
banner(w)
print(f"mods hash {mods[:12]} | state {a.state}")
print("| block | done | failed | pending | manual/deferred |")
print("| --- | --- | --- | --- | --- |")
for b in m.get("block", []):
done = failed = pending = skipped = 0
for s in expand(m, b, a.params, overrides(a)):
if s.get("manual") or b.get("manual") or b.get("deferred"):
skipped += 1
continue
argv, _ = bench_argv(m, s, a.live_world)
e = state.entry(s)
if state.is_done(s, spec_hash(argv, s), mods):
done += 1
elif e and e["status"] == "failed":
failed += 1
else:
pending += 1
print(f"| {b['id']} | {done} | {failed} | {pending} | {skipped} |")
return 0
def cmd_run(m, a):
state = State(a.state)
return run_blocks(m, a.blocks.split(","), state, a.mods_dir, params=a.params,
overrides=overrides(a), dry_run=a.dry_run,
include_manual=a.include_manual, include_deferred=a.include_deferred,
live_world=a.live_world, skip_blocked=a.skip_blocked)
def overrides(a):
return {"display": getattr(a, "display", None), "res": getattr(a, "res", None)}
def parse_params(items):
out = {}
for it in items or []:
k, sep, v = it.partition("=")
if not sep:
raise MatrixError(f"--param needs name=value, got {it!r}")
out[k] = v
return out
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("--matrix", default=str(MATRIX))
ap.add_argument("--state", default=str(STATE))
ap.add_argument("--mods-dir", default=str(GAME / "mods"))
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("list", help="blocks, session counts, estimated time")
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--param", action="append", default=[], help="name=value")
common.add_argument("--display", choices=sorted(DISPLAYS))
common.add_argument("--res")
common.add_argument("--live-world", action="store_true",
help="don't restore world zips; use saves/onionbench as-is")
p = sub.add_parser("plan", parents=[common], help="print the bench.py commands")
p.add_argument("blocks")
r = sub.add_parser("run", parents=[common], help="run blocks sequentially (resumable)")
r.add_argument("blocks", help="comma list, e.g. B0,B1")
r.add_argument("--dry-run", action="store_true")
r.add_argument("--include-manual", action="store_true")
r.add_argument("--include-deferred", action="store_true")
r.add_argument("--skip-blocked", action="store_true",
help="skip sessions with missing variants/inputs instead of aborting")
sub.add_parser("status", parents=[common], help="progress for the current mod set")
a = ap.parse_args(argv)
try:
a.params = parse_params(getattr(a, "param", []))
m = load(a.matrix)
return {"list": cmd_list, "plan": cmd_plan, "run": cmd_run,
"status": cmd_status}[a.cmd](m, a)
except MatrixError as e:
print(f"matrix.py: {e}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+298
View File
@@ -0,0 +1,298 @@
# Declarative encoding of BENCH-MATRIX.md §4 (blocks B0-B14) for the proxy bench machine
# (Ryzen 9 9900X / RX 7800 XT). Driven by matrix.py; every session becomes one
# `python3 bench.py session ...` launch. BENCH-MATRIX.md is authoritative - if this file
# disagrees with it, fix this file.
#
# Session keys:
# variant proposed-configs/out/<ID> (or an entry in [[missing_variants]] -> blocked)
# plan scenes in S1->S5 order: s1,s2,s2s,s3,s4,s5 (S6 is not automatable)
# reps windows per scene inside the session (phase0: 3 runs)
# repeat separate sessions (= cold launches) of the same config (B0: 3)
# tag distinguishes sessions that share a variant but differ by flags; the
# bench label becomes "<block>.<tag>" so analyze.py groups them apart
# jvm/heap BENCH-MATRIX JVM set / MaxMemAlloc MB
# shader shaderpack file name or "off"; shader_profile = the pack's profile name
# shader_options Iris per-pack options file (harness-relative), copied to
# shaderpacks/<pack>.txt; gen_shader_profiles.py builds them from the zips
# dh "on" | "off" (dh-off row of B3)
# disable_mods globs matched in the instance mods/ (B12a)
# add_mods jar paths/globs added for the session only (B12b)
# world key of [worlds]; restored from the zip before the session
# warmup/capture per-window seconds (bench.py defaults 15 / 30; S5 is fixed 10 + 120)
# manual true = cannot run unattended; `run` skips it unless --include-manual
# note free text shown by `plan`
# ${name} in variant/jvm/plan is a block parameter, supplied with `--param name=value`.
[defaults]
reps = 3
warmup = 15
capture = 30
settle = 45 # post-join settle; B0 v1 showed rep 1 still warming at settle 10
display = "gamescope-headless"
res = "1920x1080"
world = "v1"
shader = "off" # pin: a shader/DH state must never leak from one session to the next
dh = "on"
launch_overhead_s = 60 # launch->join (~31 s measured) + world-zip restore + teardown
[worlds]
# BENCH-MATRIX §2. Built 2026-10-16 (sha256 in .pi/journal.md). `run --live-world`
# uses the instance's saves/onionbench as-is (no restore).
v1 = "~/onion-bench/worlds/bench-world-v1.zip"
s4-0 = "~/onion-bench/worlds/bench-world-v1-s4-0.zip"
s4-1 = "~/onion-bench/worlds/bench-world-v1-s4-1.zip"
s4-4 = "~/onion-bench/worlds/bench-world-v1.zip" # the main zip carries all 4 ships
[trials]
# B12b trial jars (NeoForge 1.21.1, Modrinth, sha512-verified 2026-10-16; see /tmp/pi-trials.md
# or the journal). Each glob matches exactly one file in ~/onion-bench/trials/.
gnetum = "~/onion-bench/trials/gnetum-3.3.6.jar"
better_block_entities = "~/onion-bench/trials/bbe-neoforge-1.3.4+mc1.21.1.jar"
flerovium = "~/onion-bench/trials/flerovium-neoforge-1.21.1-1.1.2-all.jar"
super_resolution = "~/onion-bench/trials/super_resolution-neoforge-*-0.9.2-alpha.2+opengl.jar"
ixeris = "~/onion-bench/trials/Ixeris-4.6.8+1.21.1-neoforge.jar"
iris_flywheel_compat = "~/onion-bench/trials/iris-flywheel-compat-NeoForge-2.4.0.jar"
cull_leaves = "~/onion-bench/trials/cullleaves-neoforge-4.1.1+1.21.1.jar"
# Variants a block needs that build.py does not generate. Sessions using them are
# reported as blocked until they exist in proposed-configs/out/. (C1-fa, C1-fa-ext,
# emf-lod20-fa, emf-lod8-fa and C1-bl are now generated by build.py; every bench
# variant pins options.txt resourcePacks.)
[[missing_variants]]
id = "dh-off"
blocks = []
contents = """Not needed as a directory: BENCH-MATRIX defines dh-off as C1 with DH rendering \
toggled off in the GUI. Encoded as variant C1 + `dh = "off"` (bench.py --dh off)."""
# ---------------------------------------------------------------------------------------
[[block]]
id = "B0"
title = "noise floor"
rule = "Defines each machine's threshold. P2 also checks `MemAvailable` drift (phase0 Step 4)."
proxy_note = "Single proxy machine. Re-run whenever the mod set changes (matrix.py warns)."
manual_checks = ["MemAvailable drift: compare mem_available_mb before/after across the 3 sessions (recorded per row)"]
sessions = [
{ variant = "C0", plan = "s1", repeat = 3 },
]
[[block]]
id = "B1"
title = "baseline"
rule = "Reference numbers for everything below"
manual_checks = [
"S6 (GUI/JEI overlay) needs a key press - run by hand",
"/sparkc heapsummary after S1 loads",
"VRAM peak",
]
sessions = [
{ variant = "C0", plan = "s1,s2,s3,s4,s5" },
]
[[block]]
id = "B2"
title = "composite"
rule = "If C1 isn't a win on ≥ 2 machines, stop and bisect with B3–B11 before shipping anything"
proxy_note = "Proxy runs all three composites; one machine = single-machine evidence, fleet confirmation pending. Baseline is C0 (pooled from B1)."
manual_checks = ["S6 by hand", "/sparkc heapsummary after S1 loads", "VRAM peak"]
sessions = [
{ variant = "C1", plan = "s1,s2,s3,s4,s5" },
{ variant = "C1-deck", plan = "s1,s2,s3,s4,s5" },
{ variant = "C1-high", plan = "s1,s2,s3,s4,s5" },
]
[[block]]
id = "B3"
title = "Distant Horizons"
rule = """**Radius per tier** = the largest radius meeting the FPS floor on S1 *and* S5: P0 → deck, P1 → default, P2 → high. \
| Adopt SSAO-off / LOW-quality for that tier on a win. \
| Keep 2 threads unless 4 is a win on S5 *and* not a loss on S1"""
proxy_note = "Absolute floors are fleet values; on the proxy use relative wins only. P0/P1-only rows (dh-ssao, dh-qlow) run here too."
manual_checks = ["DH memory in F3"]
sessions = [
{ variant = "dh-r256", plan = "s1,s5" },
{ variant = "dh-r192", plan = "s1,s5" },
{ variant = "C1", plan = "s1,s5" },
{ variant = "dh-r64", plan = "s1,s5" },
{ variant = "C1", plan = "s1,s5", dh = "off", tag = "dh-off" },
{ variant = "dh-ssao", plan = "s1" },
{ variant = "dh-qlow", plan = "s1" },
{ variant = "dh-thr4", plan = "s1,s5" },
]
[[block]]
id = "B4"
title = "heap & GC: GC"
rule = "GC = best S5 1 % low. Heap = the smallest size within the spread of the best. Feeds `PLAYER-TUNING.md` §2. P1 isn't run (32 GB, not a constraint)."
proxy_note = "P2 values (-Xmx6G) on the proxy; the heap ladder (B4h) adds 8 G as the top rung."
manual_checks = ["/sparkc gc after S5 (pause count / max / total)", "swap delta"]
sessions = [
{ variant = "C1", plan = "s2,s5", jvm = "J0", heap = 6144, tag = "J0" },
{ variant = "C1", plan = "s2,s5", jvm = "J1", heap = 6144, tag = "J1" },
{ variant = "C1", plan = "s2,s5", jvm = "J2", heap = 6144, tag = "J2" },
{ variant = "C1", plan = "s2,s5", jvm = "J3", heap = 6144, tag = "J3" },
]
[[block]]
id = "B4h"
title = "heap & GC: heap ladder for the GC winner"
rule = "GC = best S5 1 % low. Heap = the smallest size within the spread of the best. Feeds `PLAYER-TUNING.md` §2. P1 isn't run (32 GB, not a constraint)."
params = ["gc"]
proxy_note = "Run after B4 with --param gc=<winning J set>."
sessions = [
{ variant = "C1", plan = "s2,s5", jvm = "${gc}", heap = 4096, tag = "heap4" },
{ variant = "C1", plan = "s2,s5", jvm = "${gc}", heap = 5120, tag = "heap5" },
{ variant = "C1", plan = "s2,s5", jvm = "${gc}", heap = 6144, tag = "heap6" },
{ variant = "C1", plan = "s2,s5", jvm = "${gc}", heap = 8192, tag = "heap8" },
]
[[block]]
id = "B5"
title = "Sable / SSRD"
deferred = true
rule = """SSRD per tier = the largest value meeting the floor with 4 ships. sable-noshade ships only on a win *and* if it looks acceptable. \
| Decay > spread is a Sable #1011-class finding: report it, no config fix"""
proxy_note = "Fleet-only (P0/P1) per BENCH-MATRIX; deferred on the proxy. Needs the S4 world zips. The 45-min decay run exceeds mangohud-local log_duration=1800 and needs FPS at 0/15/30/45 min + heapsummary by hand."
manual_checks = ["sub-level count", "sable-noshade visual acceptability", "/sparkc heapsummary at 0/15/30/45 min"]
sessions = [
{ variant = "C1", plan = "s4", world = "s4-0", tag = "ships0" },
{ variant = "C1", plan = "s4", world = "s4-1", tag = "ships1" },
{ variant = "C1", plan = "s4", world = "s4-4", tag = "ships4" },
{ variant = "ssrd-32", plan = "s4", world = "s4-4" },
{ variant = "ssrd-64", plan = "s4", world = "s4-4" },
{ variant = "ssrd-128", plan = "s4", world = "s4-4" },
{ variant = "sable-noshade", plan = "s4", world = "s4-4" },
{ variant = "C1", plan = "s4", world = "s4-4", jvm = "J0", reps = 1, capture = 2700, repeat = 2, tag = "decay", manual = true, note = "45-min decay; CSV capped at 30 min by mangohud-local" },
]
[[block]]
id = "B6"
title = "ModernFix dynamic_resources"
rule = "Ship `dynamic_resources=true` only if: heap −300 MB or more *or* startup faster beyond spread, **and** zero missing textures, **and** no crash in 3 launches"
interleave = true
manual_checks = [
"S6 by hand",
"heap after load (/sparkc heapsummary)",
"Missing-texture check: JEI @chipped/@rechiseled/@handcrafted, 4x4 Rechiseled CTM wall",
]
sessions = [
{ variant = "C1", plan = "s1,s2", repeat = 3 },
{ variant = "mf-dynres-off", plan = "s1,s2", repeat = 3 },
]
[[block]]
id = "B7"
title = "shaders"
rule = "Data for the owner's shader policy, no auto-decision. Report the FPS cost per profile and whether ship corruption appears with shadow profiles"
proxy_note = "Profiles are reproduced by shader-profiles/*.txt (gen_shader_profiles.py resolves the pack's profile.NAME into an Iris options file); rows run unattended. Visual checks stay manual."
manual_checks = ["Flywheel backend line in F3 (expect off with shaders)", "visual check for Iris #3164 on ships (shadow profiles: makeup-medium, compl-medium)", "confirm Iris' shader screen shows the intended profile once per pack"]
sessions = [
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "off", tag = "noshader" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", shader_profile = "shadowless_medium", shader_options = "shader-profiles/MakeUp-UltraFast-9.5f.shadowless_medium.txt", tag = "makeup-shadowless" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", shader_profile = "medium", shader_options = "shader-profiles/MakeUp-UltraFast-9.5f.medium.txt", tag = "makeup-medium" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "ComplementaryReimagined_r5.9.3.zip", shader_profile = "LOW", shader_options = "shader-profiles/ComplementaryReimagined_r5.9.3.LOW.txt", tag = "compl-low" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "ComplementaryReimagined_r5.9.3.zip", shader_profile = "MEDIUM", shader_options = "shader-profiles/ComplementaryReimagined_r5.9.3.MEDIUM.txt", tag = "compl-medium" },
]
[[block]]
id = "B8"
title = "entities"
rule = "EMF: adopt the smallest LOD that's a win with FA on. LDL: per tier, the cheapest mode that's a win. FA guidance → `PLAYER-TUNING.md` §4"
sessions = [
{ variant = "C1", plan = "s3" },
{ variant = "C1-fa", plan = "s3" },
{ variant = "C1-fa-ext", plan = "s3" },
{ variant = "emf-lod20-fa", plan = "s3" },
{ variant = "emf-lod8-fa", plan = "s3" },
{ variant = "ldl-fancy", plan = "s3" },
{ variant = "ldl-fastest", plan = "s3" },
]
[[block]]
id = "B9"
title = "Flywheel"
rule = "Per tier: the smoothest value meeting the floor"
sessions = [
{ variant = "fw-flat", plan = "s2", shader = "off" },
{ variant = "C1", plan = "s2", shader = "off" },
{ variant = "fw-smooth", plan = "s2", shader = "off" },
]
[[block]]
id = "B10"
title = "Sound Physics"
rule = "Adopt each change that's a win. Listen for obvious audio breakage."
manual_checks = ["listen for audio breakage"]
sessions = [
{ variant = "spr-up", plan = "s2s" },
{ variant = "C1", plan = "s2s" },
{ variant = "spr-strict", plan = "s2s" },
{ variant = "spr-nocap", plan = "s2s" },
]
[[block]]
id = "B11"
title = "vanilla / Sodium"
rule = """3 threads only if S5 wins and S1 doesn't lose \
| Lowest blend radius that's a win, per tier \
| Mip 4 for mid/high only if VRAM peak ≤ 5.5 GB and no loss"""
proxy_note = "C1 baseline session added in-block so analyze.py compares within the label."
manual_checks = ["VRAM peak for opt-mip4"]
sessions = [
{ variant = "C1", plan = "s1,s2,s5", tag = "baseline" },
{ variant = "sod-thr3", plan = "s1,s5" },
{ variant = "opt-blend2", plan = "s5" },
{ variant = "opt-blend0", plan = "s5" },
{ variant = "opt-mip4", plan = "s1,s2" },
]
[[block]]
id = "B12a"
title = "added-mod ablation"
rule = "Keep the mod unless removing it is a win (it costs FPS) or it breaks something visible. Sodium Extra + RSO stay regardless for their per-machine options."
sessions = [
{ variant = "C1", plan = "s1,s2s,s3" },
{ variant = "C1", plan = "s1,s2s,s3", disable_mods = ["BadOptimizations-*.jar"], tag = "no-badopt" },
{ variant = "C1", plan = "s1,s2s,s3", disable_mods = ["particle_core-*.jar"], tag = "no-particlecore" },
{ variant = "C1", plan = "s1,s2s,s3", disable_mods = ["sodium-extra-*.jar"], tag = "no-sodiumextra" },
]
[[block]]
id = "B12b"
title = "trial mods"
rule = "Owner pre-approved helpful adds. Add with `packwiz modrinth add` only on a win with no breakage. **Not More Culling**: its NeoForge build declares `minecraft [1.21,1.21.1)`. A new mod set means re-running B0 first."
proxy_note = "add_mods entries name [trials] keys. Trials are temporary for the session; a permanent add changes the mod set and invalidates B0."
manual_checks = ["breakage check per trial", "S6 for Gnetum/Flerovium (GUI paths) by hand"]
sessions = [
{ variant = "C1", plan = "s1,s2,s3,s5", tag = "baseline" },
{ variant = "C1", plan = "s1,s2,s3,s5", add_mods = ["gnetum"], tag = "gnetum" },
{ variant = "C1", plan = "s2,s3", add_mods = ["better_block_entities"], tag = "bbe" },
{ variant = "C1-fa", plan = "s3", tag = "fa-baseline" },
{ variant = "C1-fa", plan = "s3", add_mods = ["better_block_entities"], tag = "bbe-fa" },
{ variant = "C1", plan = "s1,s2,s3,s5", add_mods = ["flerovium"], tag = "flerovium" },
{ variant = "C1", plan = "s1,s2,s5", add_mods = ["super_resolution"], tag = "superres" },
{ variant = "C1", plan = "s1,s2,s5", add_mods = ["ixeris"], tag = "ixeris" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", tag = "shader-baseline" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", add_mods = ["iris_flywheel_compat"], tag = "irisflw" },
{ variant = "C1-bl", plan = "s1,s5", tag = "bl-baseline" },
{ variant = "C1-bl", plan = "s1,s5", add_mods = ["cull_leaves"], tag = "cull-leaves", note = "MidnightLib is already in the pack (1.10.0)" },
]
[[block]]
id = "B13"
title = "profile"
rule = "Flag any mod over 5 % of render-thread time that isn't Sodium, Iris, DH or Flywheel"
params = ["scene"]
proxy_note = "Scene = the worst scene from B2 under C1 (--param scene=sN)."
manual_checks = ["/sparkc profiler start --timeout 60 during the capture; save the report"]
sessions = [
{ variant = "C1", plan = "${scene}", reps = 1, capture = 75, manual = true, note = "/sparkc is a client command; start it by hand at BENCH_MARK start" },
]
[[block]]
id = "B14"
title = "spawn tuning"
manual = true
rule = "Procedure and keep/back-off rule: `SPAWN-TUNING.md` §B14"
proxy_note = "Singleplayer fresh world `onionspawn`; river, jungle, warm ocean 10 min each; per-species and total entity counts, defaults vs shipped spawn files. Not a frametime session - run by hand."
sessions = []
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Minimal Minecraft RCON client (stdlib only).
Usage: python3 rcon.py <host> <port> <password> <command> [command ...]
Exit non-zero if the server reports a failure.
"""
import socket
import struct
import sys
class Rcon:
def __init__(self, host: str, port: int, password: str, timeout: float = 10.0):
self.host, self.port, self.password = host, port, password
self.timeout = timeout
self._id = 0
def _send(self, sock: socket.socket, ptype: int, body: str) -> int:
self._id += 1
rid = self._id
payload = struct.pack("<ii", rid, ptype) + body.encode("utf-8") + b"\x00\x00"
sock.sendall(struct.pack("<i", len(payload)) + payload)
return rid
def _recv(self, sock: socket.socket):
raw = b""
while len(raw) < 4:
chunk = sock.recv(4 - len(raw))
if not chunk:
raise ConnectionError("RCON connection closed")
raw += chunk
(length,) = struct.unpack("<i", raw)
body = b""
while len(body) < length:
chunk = sock.recv(length - len(body))
if not chunk:
raise ConnectionError("RCON connection closed")
body += chunk
rid, ptype = struct.unpack("<ii", body[:8])
return rid, ptype, body[8:-2].decode("utf-8", "replace")
def command(self, cmd: str) -> str:
with socket.create_connection((self.host, self.port), timeout=self.timeout) as sock:
sock.settimeout(self.timeout)
rid = self._send(sock, 3, self.password)
rid2, _, _ = self._recv(sock)
if rid2 == -1:
raise PermissionError("RCON auth failed")
self._send(sock, 2, cmd)
_, _, text = self._recv(sock)
return text
def main(argv):
if len(argv) < 5:
print(__doc__)
return 2
host, port, password = argv[1], int(argv[2]), argv[3]
rc = 0
for cmd in argv[4:]:
try:
out = Rcon(host, port, password).command(cmd)
except Exception as exc: # noqa: BLE001 - CLI surface
print(f"! {cmd}: {exc}", file=sys.stderr)
rc = 1
continue
if out.strip():
print(out.strip())
lowered = out.lower()
if "unknown" in lowered or "failed" in lowered or "error" in lowered:
rc = 1
return rc
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
+267
View File
@@ -0,0 +1,267 @@
#!/usr/bin/env python3
"""Client-perf bench harness: drive one variant/scene and extract frametime metrics.
Applies a generated variant, launches the Prism instance under the user-local
MangoHud wrapper straight into the bench world, runs the scene via xdotool chat
commands, records wall-clock markers, then slices the MangoHud CSV.
Usage:
python3 run_scene.py --variant C1 --scene s1 [--warmup 15] [--capture 30]
[--runs 1] [--record out.csv] [--pre storm_on]
Scenes: s1, s2, s3, s4, s5 (tour = 120s capture, no warmup), s2s (= s2 + storm).
Only stdlib. Must run on the desktop session that owns DISPLAY :1.
"""
import argparse
import bisect
import csv as csvmod
import os
import re
import shutil
import signal
import subprocess
import sys
import time
from pathlib import Path
HERE = Path(__file__).resolve().parent
PROPOSED = HERE.parent / "proposed-configs"
ROOT = Path("/home/logan/onion-bench/prism")
INSTANCE = "OnionBench"
GAME = ROOT / "instances" / INSTANCE / ".minecraft"
LOGS = GAME / "logs" / "latest.log"
MANGOLOGS = Path("/home/logan/onion-bench/mangologs")
WORLD = "onionbench"
ENV = {
**os.environ,
"DISPLAY": ":1",
"XAUTHORITY": "/run/user/1000/xauth_HvvPAq",
"XDG_RUNTIME_DIR": "/run/user/1000",
"PATH": "/home/logan/.local/bin:" + os.environ.get("PATH", ""),
}
SCENE_FN = {
"s1": "bench:s1",
"s2": "bench:s2",
"s3": "bench:s3",
"s4": "bench:s4",
"s5": "bench:tour_start",
"s2s": "bench:s2",
}
def log(msg):
print(f"[run_scene] {msg}", flush=True)
def xdo(*args):
subprocess.run(["xdotool", *args], env=ENV, check=False,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def mc_window():
out = subprocess.run(["xdotool", "search", "--name", "Minecraft"],
env=ENV, capture_output=True, text=True).stdout.split()
return out[-1] if out else None
def client_java_pids():
out = subprocess.run(["ps", "-C", "java", "-o", "pid=,args="],
capture_output=True, text=True).stdout
pids = []
for line in out.splitlines():
if f"instances/{INSTANCE}/.minecraft" in line or f"instances/{INSTANCE}/natives" in line:
pids.append(int(line.split()[0]))
return pids
def apply_variant(vid):
subprocess.run([sys.executable, str(PROPOSED / "apply_variant.py"), vid, str(GAME)],
check=True, stdout=subprocess.DEVNULL)
def launch_client():
if LOGS.exists():
LOGS.unlink()
p = subprocess.Popen(
["setsid", "prismlauncher", "-d", str(ROOT), "--launch", INSTANCE, "--world", WORLD],
env=ENV, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
return p
def wait_for_world(timeout=240):
t0 = time.time()
while time.time() - t0 < timeout:
if LOGS.exists():
txt = LOGS.read_text(errors="replace")
if "Sound engine started" in txt:
return True
time.sleep(2)
return False
def chat(command, settle=0.6):
wid = mc_window()
if wid:
xdo("windowactivate", wid)
xdo("windowfocus", wid)
xdo("key", "--clearmodifiers", "t")
time.sleep(0.4)
xdo("type", "--clearmodifiers", "--delay", "12", command)
time.sleep(0.2)
xdo("key", "--clearmodifiers", "Return")
time.sleep(settle)
def newest_csv():
files = [p for p in MANGOLOGS.glob("*.csv") if not p.name.endswith("_summary.csv")]
return max(files, key=lambda p: p.stat().st_mtime) if files else None
def parse_csv(path):
"""Return (columns, rows) where rows are lists of floats; line 3 is the header."""
lines = path.read_text(errors="replace").splitlines()
hdr_i = None
for i, line in enumerate(lines[:10]):
if "frametime" in line and "elapsed" in line:
hdr_i = i
break
if hdr_i is None:
raise RuntimeError(f"no frametime header in {path}")
cols = lines[hdr_i].split(",")
e_i = cols.index("elapsed")
ft_i = cols.index("frametime")
fps_i = cols.index("fps")
rows = []
for line in lines[hdr_i + 1:]:
parts = line.split(",")
if len(parts) != len(cols):
continue
try:
rows.append((float(parts[e_i]), float(parts[ft_i]), float(parts[fps_i]), parts))
except ValueError:
continue
return cols, rows
def pct(values, p):
if not values:
return float("nan")
s = sorted(values)
k = (len(s) - 1) * p
lo, hi = int(k), min(int(k) + 1, len(s) - 1)
return s[lo] + (s[hi] - s[lo]) * (k - lo)
def metrics_between(rows, t_start, t_end, run_start):
"""rows: list of (elapsed_ns, frametime_ms, fps, raw). Wall = run_start + elapsed/1e9."""
ft, fps = [], []
for e, frametime, f, raw in rows:
wall = run_start + e / 1e9
if t_start <= wall <= t_end:
ft.append(frametime)
fps.append(f)
if not ft:
return None
fps_sorted = sorted(fps)
n1 = max(1, int(len(fps_sorted) * 0.01))
one_low = sum(fps_sorted[:n1]) / n1
return {
"n": len(ft),
"p50_ms": round(pct(ft, 0.50), 3),
"p95_ms": round(pct(ft, 0.95), 3),
"p99_ms": round(pct(ft, 0.99), 3),
"peak_ms": round(max(ft), 3),
"avg_fps": round(sum(fps) / len(fps), 2),
"one_low_fps": round(one_low, 2),
}
def kill_client(prism):
try:
os.killpg(os.getpgid(prism.pid), signal.SIGTERM)
except Exception:
pass
for pid in client_java_pids():
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
pass
time.sleep(5)
for pid in client_java_pids():
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
def run_once(variant, scene, warmup, capture, pre):
apply_variant(variant)
p = launch_client()
run_start = time.time()
try:
if not wait_for_world():
log("ERROR: world never loaded")
return None
time.sleep(8) # let the world settle / place player
fn = SCENE_FN[scene]
if scene != "s5":
chat(f"/function {fn}", settle=1.0)
if pre:
chat(f"/function bench:{pre}", settle=1.0)
if scene == "s5":
capture = 120
warmup = 0
chat("/function bench:tour_start", settle=0.5)
time.sleep(warmup)
t_start = time.time()
chat(f"/say BENCH_CAPTURE_START {variant} {scene}")
time.sleep(capture)
t_end = time.time()
chat("/say BENCH_CAPTURE_END")
chat("/save-all", settle=2.0)
finally:
csv_path = newest_csv()
kill_client(p)
cols, rows = parse_csv(csv_path)
m = metrics_between(rows, t_start, t_end, run_start)
if m:
m.update({"variant": variant, "scene": scene, "csv": csv_path.name,
"run_start": round(run_start, 3)})
return m
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--variant", required=True)
ap.add_argument("--scene", required=True, choices=sorted(SCENE_FN))
ap.add_argument("--warmup", type=int, default=15)
ap.add_argument("--capture", type=int, default=30)
ap.add_argument("--runs", type=int, default=1)
ap.add_argument("--pre", default=None)
ap.add_argument("--record", default=None)
a = ap.parse_args()
results = []
for i in range(a.runs):
log(f"run {i + 1}/{a.runs}: {a.variant} {a.scene}")
m = run_once(a.variant, a.scene, a.warmup, a.capture, a.pre)
if m:
results.append(m)
print("RESULT " + str(m), flush=True)
time.sleep(3)
if a.record and results:
fields = ["variant", "scene", "n", "p50_ms", "p95_ms", "p99_ms", "peak_ms",
"avg_fps", "one_low_fps", "csv", "run_start"]
exists = Path(a.record).exists()
with open(a.record, "a", newline="") as fh:
w = csvmod.DictWriter(fh, fieldnames=fields)
if not exists:
w.writeheader()
for m in results:
w.writerow({k: m.get(k, "") for k in fields})
return 0 if results else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,18 @@
# Iris options reproducing profile.LOW of ComplementaryReimagined_r5.9.3.zip
# pack sha256 fed6c879e732cf7fc7bc6570ebb1def43235aa7e96f62859fbdbdbfa0b484b38
# generated by gen_shader_profiles.py - do not edit; Iris rewrites the live
# copy keeping only non-default values (bench.py journals and restores it)
SHADOW_QUALITY=0
shadowDistance=96.0
WATER_REFLECT_QUALITY=2
BLOCK_REFLECT_QUALITY=1
LIGHTSHAFT_QUALI_DEFINE=0
SSAO_QUALI_DEFINE=2
FXAA_DEFINE=-1
DETAIL_QUALITY=2
CLOUD_QUALITY=2
ANISOTROPIC_FILTER=0
COLORED_LIGHTING=0
WORLD_SPACE_REFLECTIONS=-1
ENTITY_SHADOW=-1
RAIN_PUDDLES=0
@@ -0,0 +1,18 @@
# Iris options reproducing profile.MEDIUM of ComplementaryReimagined_r5.9.3.zip
# pack sha256 fed6c879e732cf7fc7bc6570ebb1def43235aa7e96f62859fbdbdbfa0b484b38
# generated by gen_shader_profiles.py - do not edit; Iris rewrites the live
# copy keeping only non-default values (bench.py journals and restores it)
SHADOW_QUALITY=2
shadowDistance=128.0
WATER_REFLECT_QUALITY=2
BLOCK_REFLECT_QUALITY=1
LIGHTSHAFT_QUALI_DEFINE=1
SSAO_QUALI_DEFINE=2
FXAA_DEFINE=1
DETAIL_QUALITY=2
CLOUD_QUALITY=2
ANISOTROPIC_FILTER=0
COLORED_LIGHTING=0
WORLD_SPACE_REFLECTIONS=-1
ENTITY_SHADOW=-1
RAIN_PUDDLES=0
@@ -0,0 +1,33 @@
# Iris options reproducing profile.medium of MakeUp-UltraFast-9.5f.zip
# pack sha256 afbf622b983d29ffa5fe5f15197f321d3921915bd44584d85f14bc11b5e0e5e8
# generated by gen_shader_profiles.py - do not edit; Iris rewrites the live
# copy keeping only non-default values (bench.py journals and restores it)
SHADOW_CASTING=true
SHADOW_QTY_SLIDER=2
SHADOW_DISTANCE_SLIDER=1
REFLECTION_SLIDER=2
DOF=false
DOF_STRENGTH=0.09
AO=1
AOSTEPS=5
AO_STRENGTH=0.70
AA_TYPE=2
MOTION_BLUR=false
REFRACTION=1
SUN_REFLECTION=1
WAVING=1
V_CLOUDS=1
CLOUD_STEPS_AVG=10
CLOUD_VOL_STYLE=0
BLOOM_SAMPLES=3.0
BLOOM=true
CHROMA_ABER=0
VOL_LIGHT=1
CLOUD_REFLECTION=true
WATER_ABSORPTION=0.10
COLOR_SCHEME=1
VANILLA_WATER=false
WATER_COLOR_SOURCE=0
COLORED_SHADOW=true
MOTION_BLUR_SAMPLES=4
MATERIAL_GLOSS=true
@@ -0,0 +1,33 @@
# Iris options reproducing profile.shadowless_medium of MakeUp-UltraFast-9.5f.zip
# pack sha256 afbf622b983d29ffa5fe5f15197f321d3921915bd44584d85f14bc11b5e0e5e8
# generated by gen_shader_profiles.py - do not edit; Iris rewrites the live
# copy keeping only non-default values (bench.py journals and restores it)
SHADOW_CASTING=false
SHADOW_QTY_SLIDER=1
SHADOW_DISTANCE_SLIDER=0
REFLECTION_SLIDER=1
DOF=false
DOF_STRENGTH=0.09
AO=1
AOSTEPS=5
AO_STRENGTH=0.70
AA_TYPE=0
MOTION_BLUR=false
REFRACTION=1
SUN_REFLECTION=1
WAVING=1
V_CLOUDS=1
CLOUD_STEPS_AVG=10
CLOUD_VOL_STYLE=0
BLOOM_SAMPLES=3.0
BLOOM=false
CHROMA_ABER=0
VOL_LIGHT=0
CLOUD_REFLECTION=false
WATER_ABSORPTION=0.10
COLOR_SCHEME=1
VANILLA_WATER=false
WATER_COLOR_SOURCE=0
COLORED_SHADOW=false
MOTION_BLUR_SAMPLES=4
MATERIAL_GLOSS=true
+4
View File
@@ -0,0 +1,4 @@
[14Oct2026 22:21:53.972] [modloading-worker-0/ERROR] [net.neoforged.fml.javafmlmod.FMLModContainer/LOADING]: Failed to create mod instance. ModID: midnightlib, class eu.midnightdust.core.MidnightLib
[14Oct2026 22:21:57.100] [Render thread/FATAL] [net.minecraft.client.Minecraft/]: Unreported exception thrown!
[14Oct2026 22:21:57.200] [Render thread/ERROR] [net.neoforged.fml.ModLoader/]: Cowardly refusing to send event net.neoforged.neoforge.client.event.RenderFrameEvent to a broken mod state
[14Oct2026 22:21:57.300] [Render thread/INFO] [mixin/]: harmless line mentioning FATAL in text only
+11
View File
@@ -0,0 +1,11 @@
[15Oct2026 15:25:44.104] [Render thread/INFO] [net.minecraft.client.renderer.texture.TextureAtlas/]: Created: 4096x4096x2 minecraft:textures/atlas/blocks.png-atlas
[15Oct2026 15:25:52.383] [Render thread/WARN] [ModernFix/]: Game took 28.366 seconds to start
[15Oct2026 15:25:53.147] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Onion4Dinner joined the game
[15Oct2026 15:25:59.974] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Server Backup started!
[15Oct2026 15:26:03.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_PLAN p20261015152526 begin
[15Oct2026 15:26:13.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_MARK warm s1 1
[15Oct2026 15:26:13.050] [Render thread/INFO] [net.minecraft.client.gui.components.ChatComponent/]: [System] [CHAT] [Server] BENCH_MARK warm s1 1
[15Oct2026 15:26:28.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_MARK start s1 1
[15Oct2026 15:26:28.040] [Render thread/INFO] [net.minecraft.client.gui.components.ChatComponent/]: [System] [CHAT] [Server] BENCH_MARK start s1 1
[15Oct2026 15:26:58.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_MARK end s1 1
[15Oct2026 15:26:58.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_DONE p20261015152526
@@ -0,0 +1,204 @@
os,cpu,gpu,ram,kernel,driver,cpuscheduler
Manjaro Linux,AMD Ryzen 9 9900X 12-Core Processor,AMD Radeon RX 7800 XT ,96396388,7.1.8-1-MANJARO,,performance
fps,frametime,cpu_load,cpu_power,gpu_load,cpu_temp,gpu_temp,gpu_core_clock,gpu_mem_clock,gpu_vram_used,gpu_power,ram_used,swap_used,process_rss,cpu_mhz,elapsed
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1000000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2000000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3000000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4000000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5000000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6000000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7000000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8000000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9000000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19000000000
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19100000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19200000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19300000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19400000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19500000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19600000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19700000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19800000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19900000000
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,20000000000
truncated,row
1 os,cpu,gpu,ram,kernel,driver,cpuscheduler
2 Manjaro Linux,AMD Ryzen 9 9900X 12-Core Processor,AMD Radeon RX 7800 XT ,96396388,7.1.8-1-MANJARO,,performance
3 fps,frametime,cpu_load,cpu_power,gpu_load,cpu_temp,gpu_temp,gpu_core_clock,gpu_mem_clock,gpu_vram_used,gpu_power,ram_used,swap_used,process_rss,cpu_mhz,elapsed
4 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,100000000
5 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,200000000
6 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,300000000
7 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,400000000
8 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,500000000
9 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,600000000
10 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,700000000
11 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,800000000
12 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,900000000
13 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1000000000
14 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1100000000
15 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1200000000
16 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1300000000
17 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1400000000
18 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1500000000
19 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1600000000
20 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1700000000
21 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1800000000
22 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1900000000
23 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2000000000
24 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2100000000
25 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2200000000
26 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2300000000
27 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2400000000
28 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2500000000
29 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2600000000
30 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2700000000
31 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2800000000
32 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2900000000
33 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3000000000
34 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3100000000
35 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3200000000
36 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3300000000
37 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3400000000
38 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3500000000
39 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3600000000
40 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3700000000
41 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3800000000
42 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3900000000
43 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4000000000
44 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4100000000
45 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4200000000
46 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4300000000
47 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4400000000
48 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4500000000
49 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4600000000
50 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4700000000
51 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4800000000
52 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4900000000
53 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5000000000
54 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5100000000
55 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5200000000
56 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5300000000
57 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5400000000
58 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5500000000
59 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5600000000
60 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5700000000
61 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5800000000
62 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5900000000
63 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6000000000
64 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6100000000
65 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6200000000
66 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6300000000
67 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6400000000
68 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6500000000
69 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6600000000
70 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6700000000
71 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6800000000
72 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6900000000
73 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7000000000
74 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7100000000
75 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7200000000
76 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7300000000
77 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7400000000
78 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7500000000
79 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7600000000
80 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7700000000
81 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7800000000
82 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7900000000
83 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8000000000
84 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8100000000
85 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8200000000
86 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8300000000
87 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8400000000
88 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8500000000
89 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8600000000
90 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8700000000
91 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8800000000
92 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8900000000
93 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9000000000
94 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9100000000
95 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9200000000
96 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9300000000
97 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9400000000
98 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9500000000
99 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9600000000
100 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9700000000
101 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9800000000
102 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9900000000
103 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10000000000
104 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10100000000
105 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10200000000
106 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10300000000
107 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10400000000
108 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10500000000
109 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10600000000
110 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10700000000
111 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10800000000
112 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10900000000
113 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11000000000
114 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11100000000
115 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11200000000
116 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11300000000
117 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11400000000
118 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11500000000
119 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11600000000
120 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11700000000
121 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11800000000
122 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11900000000
123 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12000000000
124 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12100000000
125 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12200000000
126 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12300000000
127 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12400000000
128 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12500000000
129 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12600000000
130 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12700000000
131 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12800000000
132 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12900000000
133 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13000000000
134 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13100000000
135 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13200000000
136 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13300000000
137 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13400000000
138 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13500000000
139 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13600000000
140 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13700000000
141 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13800000000
142 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13900000000
143 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14000000000
144 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14100000000
145 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14200000000
146 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14300000000
147 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14400000000
148 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14500000000
149 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14600000000
150 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14700000000
151 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14800000000
152 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14900000000
153 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15000000000
154 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15100000000
155 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15200000000
156 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15300000000
157 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15400000000
158 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15500000000
159 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15600000000
160 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15700000000
161 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15800000000
162 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15900000000
163 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16000000000
164 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16100000000
165 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16200000000
166 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16300000000
167 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16400000000
168 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16500000000
169 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16600000000
170 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16700000000
171 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16800000000
172 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16900000000
173 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17000000000
174 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17100000000
175 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17200000000
176 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17300000000
177 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17400000000
178 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17500000000
179 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17600000000
180 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17700000000
181 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17800000000
182 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17900000000
183 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18000000000
184 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18100000000
185 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18200000000
186 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18300000000
187 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18400000000
188 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18500000000
189 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18600000000
190 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18700000000
191 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18800000000
192 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18900000000
193 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19000000000
194 50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19100000000
195 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19200000000
196 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19300000000
197 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19400000000
198 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19500000000
199 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19600000000
200 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19700000000
201 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19800000000
202 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19900000000
203 250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,20000000000
204 truncated,row
@@ -0,0 +1,97 @@
"""analyze.py: tagged labels ('B3.dh-off') take their baseline from the same block.
python3 -m unittest discover -s analysis/client-perf/harness/tests
"""
import sys
import unittest
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
import analyze # noqa: E402
def row(label, variant, scene, session, p99, low=100.0, fps=200.0):
return {"kind": "window", "label": label, "variant": variant, "scene": scene,
"session": session, "flags": [],
"metrics": {"p99_ms": p99, "one_low_fps": low, "avg_fps": fps}}
def cell(table, label, variant, scene):
for line in table.splitlines():
cells = [c.strip() for c in line.strip("|").split("|")]
if cells[:3] == [label, variant, scene]:
return cells
raise AssertionError(f"no row {label}/{variant}/{scene} in\n{table}")
class BlockBaseline(unittest.TestCase):
def setUp(self):
self.rows = [
# noise: spread 0.2 ms / 2 fps
row("B0", "C0", "s1", "n1", 10.0, 100), row("B0", "C0", "s1", "n2", 10.2, 102),
# global C1 reference elsewhere (B2) is much slower than the B3 one
row("B2", "C1", "s1", "g1", 20.0),
row("B3", "C1", "s1", "b1", 10.0),
row("B3", "dh-r64", "s1", "b2", 8.0),
row("B3.dh-off", "C1", "s1", "b3", 6.0),
]
def test_tagged_self_variant_uses_untagged_block_label(self):
s = analyze.summarise(self.rows)
b, where = analyze.self_baseline(s, "B3.dh-off", "s1", "C1")
self.assertEqual(where, "B3")
self.assertEqual(b["p99"], 10.0)
def test_untagged_block_label_is_its_own_baseline(self):
s = analyze.summarise(self.rows)
self.assertEqual(analyze.self_baseline(s, "B3", "s1", "C1"), (None, None))
def test_tagged_other_variant_prefers_block_over_global(self):
rows = self.rows + [row("B12a.no-x", "X", "s1", "t1", 9.0)]
rows += [row("B12a", "C1", "s1", "t0", 12.0)]
s = analyze.summarise(rows)
b, where = analyze.baseline_for(s, rows, "B12a.no-x", "s1", "C1")
self.assertEqual((where, b["p99"]), ("B12a", 12.0))
def test_block_baseline_tag(self):
rows = self.rows + [row("B11.baseline", "C1", "s5", "x0", 12.0),
row("B11", "sod-thr3", "s5", "x1", 11.0),
row("B2", "C1", "s5", "x2", 30.0)]
s = analyze.summarise(rows)
b, where = analyze.baseline_for(s, rows, "B11", "s5", "C1")
# untagged candidate: own label has no C1 -> '<block>.baseline', not global
self.assertEqual((where, b["p99"]), ("B11.baseline", 12.0))
rows.append(row("B11.x", "opt-mip4", "s5", "x3", 11.0))
s = analyze.summarise(rows)
b, where = analyze.baseline_for(s, rows, "B11.x", "s5", "C1")
self.assertEqual((where, b["p99"]), ("B11.baseline", 12.0))
def test_block_pool_then_global(self):
rows = self.rows + [row("B4.J0", "C1", "s2", "j0", 10.0),
row("B4.J1", "C1", "s2", "j1", 12.0),
row("B4.x", "Y", "s2", "j2", 9.0),
row("B2", "C1", "s2", "g2", 40.0)]
s = analyze.summarise(rows)
b, where = analyze.baseline_for(s, rows, "B4.x", "s2", "C1")
self.assertEqual((where, b["p99"]), ("B4.*", 11.0))
b, where = analyze.baseline_for(s, rows, "B9.z", "s2", "C1")
self.assertEqual(where, "*")
self.assertIsNone(analyze.baseline_for(s, rows, "B9.z", "s9", "C1")[0])
def test_table_verdict_against_block(self):
t = analyze.table(analyze.summarise(self.rows), self.rows, "C1", "B0")
c = cell(t, "B3.dh-off", "C1", "s1")
self.assertEqual(c[-4:], ["C1@B3", "+4.00", "+0.0", "win"])
self.assertEqual(cell(t, "B3", "C1", "s1")[-1], "baseline")
self.assertEqual(cell(t, "B3", "dh-r64", "s1")[-4], "C1@B3")
def test_table_label_filter_still_finds_block_baseline(self):
shown = [r for r in self.rows if r["label"] in ("B3.dh-off", "B0")]
t = analyze.table(analyze.summarise(shown), self.rows, "C1", "B0")
self.assertEqual(cell(t, "B3.dh-off", "C1", "s1")[-4], "C1@B3")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,294 @@
"""Offline tests for bench.py session instance changes (temp dirs only, no game).
python3 -m unittest discover -s analysis/client-perf/harness/tests
"""
import json
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
import bench # noqa: E402
IRIS = """#This file stores configuration options for Iris, such as the currently active shaderpack
#Fri Oct 16 19:37:17 EDT 2026
allowUnknownShaders=false
enableShaders=true
maxShadowRenderDistance=32
shaderPack=
"""
DH = """[server]
\tenableRealTimeUpdates = true
[client]
\tshowDhOptionsButtonInMinecraftUi = true
\t\t[client.advanced.debugging]
\t\t\tlodOnlyMode = false
\t\t\t# DISABLED: Disable rendering
\t\t\trendererMode = "DEFAULT"
\t\t\t[client.advanced.debugging.debugWireframe]
\t\t\t\tenableRendering = false
\t\t\t\trendererMode = "DEFAULT"
"""
class GameDir(unittest.TestCase):
def setUp(self):
self._td = tempfile.TemporaryDirectory()
self.game = Path(self._td.name)
(self.game / "config").mkdir()
(self.game / "mods").mkdir()
(self.game / "shaderpacks").mkdir()
(self.game / "saves" / "onionbench").mkdir(parents=True)
(self.game / "saves" / "onionbench" / "level.dat").write_bytes(b"orig")
(self.game / "config" / "iris.properties").write_text(IRIS)
(self.game / "config" / "DistantHorizons.toml").write_text(DH)
(self.game / "shaderpacks" / "Pack A.zip").write_bytes(b"zip")
for n in ("BadOptimizations-2.4.1.jar", "particle_core-0.3.3.jar", "sodium-0.8.jar"):
(self.game / "mods" / n).write_bytes(n.encode())
self.j = bench.Journal.begin(self.game, "ptest")
def tearDown(self):
self._td.cleanup()
def snapshot(self):
"""Instance contents, excluding the journal itself (setUp opened one)."""
return {p.relative_to(self.game).as_posix(): p.read_bytes()
for p in sorted(self.game.rglob("*")) if p.is_file()
and p.relative_to(self.game).parts[0] not in (bench.PENDING, bench.BACKUP_DIR)}
class IrisAndDh(GameDir):
def test_properties_rewrite(self):
out = bench.set_properties(IRIS, {"enableShaders": "false", "shaderPack": "a:b=c.zip"})
self.assertIn("enableShaders=false\n", out)
self.assertIn("shaderPack=a\\:b\\=c.zip\n", out)
self.assertIn("maxShadowRenderDistance=32", out)
self.assertTrue(out.startswith("#This file"))
self.assertIn("newKey=1", bench.set_properties(IRIS, {"newKey": "1"}))
def test_shader_on_off_restore(self):
before = self.snapshot()
opts = self.game.parent / f"{self.game.name}-opts.txt"
opts.write_text("SHADOW=off\n")
try:
bench.apply_shader(self.game, self.j, "Pack A.zip", opts)
finally:
text = opts.read_text()
opts.unlink()
cfg = (self.game / "config" / "iris.properties").read_text()
self.assertIn("enableShaders=true", cfg)
self.assertIn("shaderPack=Pack A.zip", cfg)
self.assertEqual((self.game / "shaderpacks" / "Pack A.zip.txt").read_text(), text)
self.assertEqual(bench.restore_pending(self.game), [])
self.assertEqual(self.snapshot(), before)
j = bench.Journal.begin(self.game, "p2")
bench.apply_shader(self.game, j, "off")
cfg = (self.game / "config" / "iris.properties").read_text()
self.assertIn("enableShaders=false", cfg)
self.assertIn("shaderPack=\n", cfg)
bench.restore_pending(self.game)
self.assertEqual(self.snapshot(), before)
def test_dh_rewrite_only_debugging_section(self):
off = bench.set_dh_rendering(DH, False)
self.assertIn('\t\t\trendererMode = "DISABLED"\n\n', off)
self.assertIn('\t\t\t\trendererMode = "DEFAULT"', off) # wireframe section untouched
self.assertIn("enableRendering = false", off)
self.assertEqual(bench.set_dh_rendering(off, True), DH)
with self.assertRaises(ValueError):
bench.set_dh_rendering("[client]\n\trendererMode = \"DEFAULT\"\n", False)
def test_apply_dh_restore(self):
bench.apply_dh(self.game, self.j, False)
self.assertIn('"DISABLED"', (self.game / "config" / "DistantHorizons.toml").read_text())
bench.restore_pending(self.game)
self.assertEqual((self.game / "config" / "DistantHorizons.toml").read_text(), DH)
class Mods(GameDir):
def test_disable_add_restore(self):
before = self.snapshot()
names = bench.resolve_disable(self.game, ["badopt*", "PARTICLE_*"])
self.assertEqual(names, ["BadOptimizations-2.4.1.jar", "particle_core-0.3.3.jar"])
with self.assertRaises(ValueError):
bench.resolve_disable(self.game, ["badopt*", "nomatch*"])
extra = self.game.parent / f"{self.game.name}-trial.jar"
extra.write_bytes(b"trial")
try:
jars = bench.check_add_mods(self.game, [str(extra)])
h0 = bench.mods_hash(self.game / "mods")
bench.disable_mods(self.game, self.j, names)
bench.add_mods(self.game, self.j, jars)
finally:
extra.unlink()
mods = sorted(p.name for p in (self.game / "mods").iterdir())
self.assertEqual(mods, sorted(["BadOptimizations-2.4.1.jar.disabled",
f"{self.game.name}-trial.jar",
"particle_core-0.3.3.jar.disabled", "sodium-0.8.jar"]))
self.assertNotEqual(bench.mods_hash(self.game / "mods"), h0)
self.assertEqual(bench.restore_pending(self.game), [])
self.assertEqual(self.snapshot(), before)
self.assertEqual(bench.mods_hash(self.game / "mods"), h0)
def test_add_mods_validation(self):
with self.assertRaises(ValueError):
bench.check_add_mods(self.game, [str(self.game / "nope.jar")])
with self.assertRaises(ValueError): # name collision
bench.check_add_mods(self.game, [str(self.game / "mods" / "sodium-0.8.jar")])
class WorldZip(GameDir):
def make_zip(self, prefix):
z = self.game.parent / f"{self.game.name}-{prefix.strip('/') or 'flat'}.zip"
with zipfile.ZipFile(z, "w") as zf:
zf.writestr(prefix + "level.dat", b"zipped")
zf.writestr(prefix + "region/r.0.0.mca", b"region")
self.addCleanup(z.unlink)
return z
def check_swap(self, prefix):
before = self.snapshot()
z = self.make_zip(prefix)
self.assertEqual(bench.zip_world_root(z), prefix)
sha = bench.swap_world(self.game, self.j, z)
self.assertEqual(sha, bench.sha256_file(z))
w = self.game / "saves" / "onionbench"
self.assertEqual((w / "level.dat").read_bytes(), b"zipped")
self.assertTrue((w / "region" / "r.0.0.mca").exists())
self.assertTrue((self.game / "saves" / "onionbench.harness-bak" / "level.dat").exists())
self.assertEqual(sorted(p.name for p in (self.game / "saves").iterdir()),
["onionbench", "onionbench.harness-bak"])
self.assertEqual(bench.restore_pending(self.game), [])
self.assertEqual(self.snapshot(), before)
self.assertEqual([p.name for p in (self.game / "saves").iterdir()], ["onionbench"])
def test_flat_layout(self):
self.check_swap("")
def test_single_top_dir_layout(self):
self.check_swap("bench-world-v1/")
def test_bad_zips(self):
bad = self.game.parent / f"{self.game.name}-bad.zip"
self.addCleanup(lambda: bad.unlink(missing_ok=True))
with zipfile.ZipFile(bad, "w") as zf:
zf.writestr("a/level.dat", b"x")
zf.writestr("b/level.dat", b"x")
with self.assertRaises(ValueError):
bench.zip_world_root(bad)
with zipfile.ZipFile(bad, "w") as zf:
zf.writestr("../evil/level.dat", b"x")
with self.assertRaises(ValueError):
bench.zip_world_root(bad)
class PendingJournal(GameDir):
def test_crash_then_restore_from_disk(self):
before = self.snapshot()
z = WorldZip.make_zip(self, "w/")
bench.apply_shader(self.game, self.j, "Pack A.zip")
bench.apply_dh(self.game, self.j, False)
bench.disable_mods(self.game, self.j, ["sodium-0.8.jar"])
bench.swap_world(self.game, self.j, z)
plan = self.game / "saves" / "onionbench" / "datapacks" / "autoplan"
self.j.record({"op": "tree", "path": plan.relative_to(self.game).as_posix()})
plan.mkdir(parents=True)
(plan / "pack.mcmeta").write_text("{}")
# simulate a hard kill: only the on-disk journal survives
del self.j
self.assertTrue(bench.pending_exists(self.game))
data = json.loads((self.game / bench.PENDING).read_text())
self.assertEqual([e["op"] for e in data["entries"]],
["file", "file", "file", "rename", "world", "tree"])
with self.assertRaises(RuntimeError):
bench.Journal.begin(self.game, "again")
self.assertEqual(bench.restore_pending(self.game), [])
self.assertFalse(bench.pending_exists(self.game))
self.assertFalse((self.game / bench.BACKUP_DIR).exists())
self.assertEqual(self.snapshot(), before)
def test_entry_written_before_mutation_is_tolerated(self):
# journal says "renamed", but the kill came before os.rename ran
self.j.record({"op": "rename", "src": "mods/sodium-0.8.jar",
"dst": "mods/sodium-0.8.jar.disabled"})
self.j.record({"op": "added", "path": "mods/never-copied.jar"})
before = self.snapshot()
self.assertEqual(bench.restore_pending(self.game), [])
self.assertEqual(self.snapshot(), before)
def test_failed_entry_stays_journaled(self):
self.j.record({"op": "bogus"})
failed = bench.restore_pending(self.game)
self.assertEqual(len(failed), 1)
data = json.loads((self.game / bench.PENDING).read_text())
self.assertEqual(data["entries"], [{"op": "bogus"}])
def test_nothing_pending(self):
bench.restore_pending(self.game)
self.assertEqual(bench.restore_pending(self.game), [])
class OptionsAndFlags(unittest.TestCase):
def test_resource_pack_reset(self):
with tempfile.TemporaryDirectory() as td:
g = Path(td)
(g / "options.txt").write_text('version:3955\nresourcePacks:["vanilla","x.zip"]\n'
"maxFps:260\n")
bench.reset_resource_packs(g)
vals = bench.options_values(g, {"resourcePacks", "incompatibleResourcePacks"})
self.assertEqual(vals, {"resourcePacks": '["vanilla","mod_resources"]',
"incompatibleResourcePacks": "[]"})
text = (g / "options.txt").read_text()
self.assertTrue(text.startswith("version:3955\n"))
self.assertIn("maxFps:260", text)
(g / "options.txt").unlink()
bench.reset_resource_packs(g) # absent file -> just the baseline
self.assertEqual(len((g / "options.txt").read_text().splitlines()), 2)
def test_env_flags(self):
gpus = [{"device": "0x13c0", "dpm_level": "auto"},
{"device": bench.DGPU_DEVICE, "dpm_level": "auto"}]
self.assertEqual(bench.env_flags(gpus, True), ["gpu_dpm_auto", "server_running"])
gpus[1]["dpm_level"] = "high"
self.assertEqual(bench.env_flags(gpus, False), [])
def test_build_parser_session_defaults(self):
a = bench.build_parser().parse_args(["session", "--variant", "C1"])
self.assertEqual((a.shader, a.dh, a.disable_mods, a.add_mods, a.world_zip),
("off", "on", None, None, None))
a = bench.build_parser().parse_args(["restore"])
self.assertIs(a.fn, bench.cmd_restore)
if __name__ == "__main__":
unittest.main()
class ShaderLoaded(unittest.TestCase):
def test_flags(self):
ok = ["[Render thread/INFO] [Iris/]: Using shaderpack: MakeUp-UltraFast-9.5f.zip"]
bad = ok + ["[Render thread/ERROR] [Iris/]: Failed to compile shaders"]
self.assertEqual(bench.shader_flags("off", []), [])
self.assertEqual(bench.shader_flags("X.zip", ok), [])
self.assertEqual(bench.shader_flags("X.zip", []), ["shader_not_loaded"])
self.assertEqual(bench.shader_flags("X.zip", bad), ["shader_not_loaded"])
class ResetJvm(unittest.TestCase):
def test_reset(self):
with tempfile.TemporaryDirectory() as d:
d = Path(d)
(d / "instance.cfg").write_text("JvmArgs=-XX:+UseShenandoahGC\nMaxMemAlloc=6144\n")
self.assertFalse(bench.reset_jvm(d))
(d / "instance.cfg.harness-orig").write_text("JvmArgs=\nMaxMemAlloc=8192\n")
self.assertTrue(bench.reset_jvm(d))
self.assertEqual((d / "instance.cfg").read_text(), "JvmArgs=\nMaxMemAlloc=8192\n")
@@ -0,0 +1,241 @@
"""Offline tests for bench.py / analyze.py (no game, no display).
python3 -m unittest discover -s analysis/client-perf/harness/tests
"""
import contextlib
import io
import json
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
import analyze # noqa: E402
import bench # noqa: E402
FIX = HERE / "fixtures"
CSV = FIX / "mangohud_java_2026-10-15_15-25-26.csv"
def lines(name):
return (FIX / name).read_text().splitlines()
class CsvParsing(unittest.TestCase):
def test_header_and_rows(self):
meta, rows = bench.parse_mangohud_csv(CSV)
self.assertIn("RX 7800 XT", meta["gpu"])
self.assertEqual(len(rows), 200) # malformed trailing row skipped
self.assertAlmostEqual(rows[0][0], 0.1) # elapsed ns -> s
self.assertEqual(rows[0][1], 4.0)
def test_name_time(self):
import time
t = bench.csv_name_time("java_2026-10-15_15-25-26.csv")
self.assertEqual(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t)),
"2026-10-15 15:25:26")
self.assertIsNone(bench.csv_name_time("weird.csv"))
def test_file_birth_method(self):
with tempfile.TemporaryDirectory() as d:
p = Path(d) / CSV.name
shutil.copyfile(CSV, p)
t, method = bench.file_birth(p)
self.assertIn(method, {"st_birthtime", "stat-W", "filename", "mtime"})
self.assertGreater(t, 0)
class Math(unittest.TestCase):
def test_pct(self):
self.assertEqual(bench.pct([1, 2, 3, 4, 5], 0.5), 3)
self.assertAlmostEqual(bench.pct(list(range(1, 101)), 0.99), 99.01)
self.assertNotEqual(bench.pct([], 0.5), bench.pct([], 0.5)) # nan
def test_one_low_from_frametimes(self):
# 200 frames: 198 x 5 ms, 2 x 25 ms -> worst 1 % (2 frames) mean 25 ms -> 40 fps
rows = [(i * 0.005, 25.0 if i in (50, 150) else 5.0, 200.0) for i in range(200)]
m = bench.window_metrics(rows, 1000.0, 1000.0, 1001.0)
self.assertEqual(m["n"], 200)
self.assertEqual(m["one_low_fps"], 40.0)
self.assertEqual(m["p50_ms"], 5.0)
self.assertEqual(m["peak_ms"], 25.0)
self.assertAlmostEqual(m["avg_fps"], 1000 / (1040 / 200), places=1)
self.assertEqual(m["one_low_fps_legacy"], 200.0) # fps column was flat
def test_window_slicing_alignment(self):
_, rows = bench.parse_mangohud_csv(CSV)
origin = 5000.0
m = bench.window_metrics(rows, origin, origin + 10.0, origin + 20.0)
self.assertEqual(m["n"], 101)
self.assertEqual(m["p50_ms"], 4.0)
self.assertEqual(m["p99_ms"], 20.0)
self.assertEqual(m["one_low_fps"], 50.0)
self.assertEqual(m["one_low_fps_legacy"], 50.0)
self.assertIsNone(bench.window_metrics(rows, origin, origin + 30, origin + 40))
# first 10 s contain no 20 ms frames
early = bench.window_metrics(rows, origin, origin, origin + 9.9)
self.assertEqual(early["p99_ms"], 4.0)
class Markers(unittest.TestCase):
def test_parse(self):
marks, done = bench.parse_markers(lines("latest.log"))
st, en = marks[("start", "s1", 1)], marks[("end", "s1", 1)]
self.assertAlmostEqual(en - st, 30.0, places=3)
self.assertAlmostEqual(marks[("warm", "s1", 1)] % 1, 0.0, places=3) # server line, not CHAT
self.assertAlmostEqual(done, en)
def test_events_and_alignment(self):
ln = lines("latest.log")
ev = bench.scan_events(ln)
self.assertEqual(ev["game_took_s"], 28.366)
self.assertEqual(ev["atlas"], "4096x4096x2")
self.assertIsNone(ev["crash"])
self.assertAlmostEqual(ev["joined_t"] - ev["title_t"], 0.764, places=3)
# CSV born 10 s before the start marker -> window covers elapsed 10..40 s,
# CSV ends at 20 s -> 101 frames -> low_n
marks, _ = bench.parse_markers(ln)
ts, te = marks[("start", "s1", 1)], marks[("end", "s1", 1)]
_, rows = bench.parse_mangohud_csv(CSV)
m = bench.window_metrics(rows, ts - 10.0, ts, te)
self.assertEqual(m["n"], 101)
self.assertIn("low_n", bench.validity_flags(m, te - ts, [], bench.backup_times(ln), ts, te))
def test_backup_flag(self):
ln = lines("latest.log")
bk = bench.backup_times(ln)
self.assertEqual(len(bk), 1)
m = {"n": 6000, "p50_ms": 4.0, "avg_fps": 200.0}
self.assertEqual(bench.validity_flags(m, 30, [], bk, bk[0] + 30, bk[0] + 60), [])
self.assertIn("backup_in_window", bench.validity_flags(m, 30, [], bk, bk[0] - 1, bk[0] + 29))
self.assertIn("backup_in_window", bench.validity_flags(m, 30, [], bk, bk[0] + 3, bk[0] + 33))
def test_vsync_flag(self):
m = {"n": 1800, "p50_ms": 16.7, "avg_fps": 59.8}
self.assertIn("vsync_suspect", bench.validity_flags(m, 30))
m144 = {"n": 4320, "p50_ms": 6.95, "avg_fps": 144}
self.assertIn("vsync_suspect", bench.validity_flags(m144, 30, [144.0]))
self.assertNotIn("vsync_suspect", bench.validity_flags(m144, 30, [60.0]))
self.assertEqual(bench.validity_flags(None, 30), ["no_data"])
class Crash(unittest.TestCase):
def test_classify(self):
got = [bench.classify_line(l) for l in lines("crash.log")]
self.assertEqual(got, ["mod_instance", "fatal", "broken_mod_state", None])
self.assertIsNone(bench.classify_line(lines("latest.log")[3])) # backup line
self.assertEqual(bench.scan_events(lines("crash.log"))["crash"], "mod_instance")
class Datapack(unittest.TestCase):
def test_generate(self):
files = bench.generate_datapack("p1", ["s1", "s2s", "s5"], 2, 15, 30, 10)
tag = json.loads(files["data/minecraft/tags/function/load.json"])
self.assertEqual(tag["values"], ["bench:autoplan/p1/load"])
self.assertEqual(json.loads(files["pack.mcmeta"])["pack"]["pack_format"], 48)
load = files["data/bench/function/autoplan/p1/load.mcfunction"]
self.assertIn("unless score #p1 bench_plan matches 1..", load)
steps = sorted((k for k in files if "/step" in k),
key=lambda k: int(k.rsplit("step", 1)[1].split(".")[0]))
body = "".join(files[k] for k in steps)
for kind in ("warm", "start", "end"):
for sc in ("s1", "s2s", "s5"):
for rep in (1, 2):
self.assertEqual(body.count(f"say BENCH_MARK {kind} {sc} {rep}\n"), 1)
self.assertIn("function bench:tour_start", body)
self.assertIn("function bench:storm_on", body)
self.assertIn("function bench:storm_off", body)
self.assertIn("schedule function bench:autoplan/p1/step", body)
self.assertNotIn(" 0t", body)
self.assertTrue(body.rstrip().endswith("say BENCH_DONE p1"))
def test_timeline_ticks(self):
tl = bench.build_timeline(["s1", "s5"], 1, 15, 30, "x")
delays = [d for _, d in tl]
self.assertEqual(delays, [300, 600, 200, 2400, 0])
self.assertEqual(tl[1][0], ["say BENCH_MARK start s1 1"])
self.assertIn("say BENCH_MARK end s1 1", tl[2][0]) # merged into next scene's step
self.assertEqual(bench.plan_seconds(["s1", "s5"], 1, 15, 30), 45 + 130)
def test_order_rule(self):
class A:
plan, any_order = "s2,s1", False
with self.assertRaises(SystemExit):
bench.session_plan(A)
A.any_order = True
self.assertEqual(bench.session_plan(A), ["s2", "s1"])
A.plan, A.any_order = "s1,s6", False
with self.assertRaises(SystemExit):
bench.session_plan(A)
def test_pins(self):
out = bench.merge_pins("version:3955\nmaxFps:120\nenableVsync:true\nfov:0.5\n")
self.assertIn("maxFps:260", out)
self.assertIn("enableVsync:false", out)
self.assertIn("fov:0.5", out)
self.assertIn("pauseOnLostFocus:false", out)
self.assertEqual(out.count("maxFps"), 1)
def row(label, variant, session, p99, low, scene="s1", flags=()):
return {"kind": "window", "label": label, "variant": variant, "scene": scene,
"session": session, "flags": list(flags),
"metrics": {"p99_ms": p99, "one_low_fps": low, "avg_fps": 200.0}}
class Analyze(unittest.TestCase):
def setUp(self):
self.rows = [
row("B0", "C0", "a", 10.0, 100), row("B0", "C0", "b", 10.2, 98),
row("B0", "C0", "c", 10.4, 99),
row("B2", "C0", "d", 10.2, 99),
row("B2", "WIN", "e", 9.0, 100), row("B2", "TIE", "f", 10.0, 98),
row("B2", "LOSS", "g", 9.0, 90),
row("B2", "BAD", "h", 1.0, 999, flags=["crashed"]),
]
def test_summary_spread(self):
s = analyze.summarise(self.rows[:3])
g = s[("B0", "C0", "s1")]
self.assertEqual(g["sessions"], 3)
self.assertAlmostEqual(g["p99"], 10.2)
self.assertAlmostEqual(g["p99_spread"], 0.4)
self.assertAlmostEqual(g["low_spread"], 2)
def test_verdicts(self):
valid = [r for r in self.rows if analyze.is_valid(r)]
self.assertEqual(len(valid), 7)
s = analyze.summarise(valid)
sp = analyze.noise_spread(s, "B0")
base = s[("B2", "C0", "s1")]
self.assertEqual(analyze.verdict(base, s[("B2", "WIN", "s1")], *sp), "win")
self.assertEqual(analyze.verdict(base, s[("B2", "TIE", "s1")], *sp), "tie")
self.assertEqual(analyze.verdict(base, s[("B2", "LOSS", "s1")], *sp), "loss")
t = analyze.table(s, valid, "C0", "B0")
self.assertIn("| B2 | WIN | s1 |", t)
self.assertIn("| win |", t)
self.assertNotIn("BAD", t)
def test_pooled_baseline_and_cli(self):
rows = [r for r in self.rows if not (r["label"] == "B2" and r["variant"] == "C0")]
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "s.jsonl"
p.write_text("".join(json.dumps(r) + "\n" for r in rows))
loaded = analyze.load_rows(p)
s = analyze.summarise([r for r in loaded if analyze.is_valid(r)])
b, lab = analyze.baseline_for(s, loaded, "B2", "s1", "C0")
self.assertEqual(lab, "*")
self.assertAlmostEqual(b["p99"], 10.2)
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
rc = analyze.main(["--file", str(p), "--baseline", "C0", "--noise", "B0"])
self.assertEqual(rc, 0)
self.assertIn("| B2 | WIN | s1 |", buf.getvalue())
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,404 @@
"""Offline tests for matrix.py (no game; the bench.py runner is faked).
python3 -m unittest discover -s analysis/client-perf/harness/tests
"""
import contextlib
import io
import json
import sys
import tempfile
import tomllib
import unittest
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
import matrix # noqa: E402
MINI = """
[defaults]
reps = 3
warmup = 15
capture = 30
display = "gamescope"
res = "1280x720"
world = "v1"
shader = "off"
dh = "on"
launch_overhead_s = 60
[worlds]
v1 = "{tmp}/worlds/v1.zip"
s4 = "{tmp}/worlds/s4-*.zip"
[trials]
gnetum = "{tmp}/trials/gnetum-*.jar"
[[missing_variants]]
id = "C1-fa"
blocks = ["B8"]
contents = "C1 + Fresh Animations"
[[block]]
id = "B0"
rule = "noise"
sessions = [{{ variant = "C0", plan = "s1", repeat = 3 }}]
[[block]]
id = "B3"
rule = "dh"
sessions = [
{{ variant = "C1", plan = "s1,s5" }},
{{ variant = "C1", plan = "s1,s5", dh = "off", tag = "dh-off" }},
{{ variant = "C1", plan = "s2", jvm = "J2", heap = 6144, world = "s4", tag = "x" }},
]
[[block]]
id = "B6"
rule = "mf"
interleave = true
sessions = [
{{ variant = "C1", plan = "s1", repeat = 2 }},
{{ variant = "mf", plan = "s1", repeat = 2 }},
]
[[block]]
id = "B8"
rule = "entities"
sessions = [{{ variant = "C1-fa", plan = "s3" }}]
[[block]]
id = "B12b"
rule = "trials"
sessions = [{{ variant = "C1", plan = "s1", add_mods = ["gnetum"], disable_mods = ["a-*.jar"], tag = "g" }}]
[[block]]
id = "B4h"
rule = "heap"
params = ["gc"]
sessions = [{{ variant = "C1", plan = "s5", jvm = "${{gc}}", heap = 4096 }}]
[[block]]
id = "B13"
rule = "profile"
sessions = [{{ variant = "C1", plan = "s1", manual = true }}]
[[block]]
id = "B5"
rule = "sable"
deferred = true
sessions = [{{ variant = "C1", plan = "s4" }}]
[[block]]
id = "B14"
rule = "spawn"
manual = true
sessions = []
"""
class Fixture(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp())
self.out = self.tmp / "out"
for v in ("C0", "C1", "mf"):
(self.out / v).mkdir(parents=True)
self.mods = self.tmp / "mods"
self.mods.mkdir()
(self.mods / "a-1.jar").write_bytes(b"x")
(self.tmp / "worlds").mkdir()
(self.tmp / "worlds" / "v1.zip").write_bytes(b"z")
(self.tmp / "worlds" / "s4-4.zip").write_bytes(b"z")
(self.tmp / "trials").mkdir()
(self.tmp / "trials" / "gnetum-1.0.jar").write_bytes(b"j")
self.path = self.tmp / "matrix.toml"
self.path.write_text(MINI.format(tmp=self.tmp))
self.m = matrix.load(self.path, self.out)
self.state_path = self.tmp / "state.json"
self.calls = []
def runner(self, codes=None):
codes = list(codes or [])
def fake(argv):
self.calls.append(argv)
return codes.pop(0) if codes else 0
return fake
def run_blocks(self, ids, codes=None, **kw):
state = matrix.State(self.state_path)
with contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()) as err:
rc = matrix.run_blocks(self.m, ids, state, self.mods, runner=self.runner(codes),
out_dir=self.out, **kw)
return rc, state, err.getvalue()
class Loading(Fixture):
def test_real_matrix_validates(self):
m = matrix.load() # the shipped matrix.toml against the real out/ dir
ids = [b["id"] for b in m["block"]]
for want in ["B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "B10",
"B11", "B12a", "B12b", "B13", "B14"]:
self.assertIn(want, ids)
bm = matrix.block_map(m)
self.assertTrue(bm["B5"]["deferred"])
self.assertTrue(bm["B14"]["manual"])
b0 = matrix.expand(m, bm["B0"])
self.assertEqual([(s["variant"], s["plan"]) for s in b0], [("C0", ["s1"])] * 3)
def test_real_matrix_emits_only_bench_flags(self):
m = matrix.load()
self.assertEqual((m["defaults"]["display"], m["defaults"]["res"]),
("gamescope-headless", "1920x1080"))
params = {"gc": "J1", "scene": "s2"}
n = 0
for b in m["block"]:
for s in matrix.expand(m, b, params):
argv, _ = matrix.bench_argv(m, s)
self.assertEqual(argv[0], "session")
flags = {a for a in argv[1:] if a.startswith("--")}
self.assertLessEqual(flags, matrix.BENCH_SESSION_FLAGS, (b["id"], argv))
n += 1
self.assertGreater(n, 50)
def test_fa_and_leaves_variants_generated(self):
m = matrix.load()
missing = matrix.missing_variant_ids(m)
for v in ("C1-fa", "C1-fa-ext", "emf-lod20-fa", "emf-lod8-fa", "C1-bl"):
self.assertNotIn(v, missing)
self.assertTrue((matrix.OUT_DIR / v / "options.txt").is_file(), v)
def test_validation_errors(self):
d = tomllib.loads(MINI.format(tmp=self.tmp))
d["block"][0]["sessions"][0]["plan"] = "s5,s1"
d["block"][0]["sessions"][0]["typo"] = 1
d["block"][1]["sessions"].append({"variant": "nope", "plan": "s6"})
d["block"][1]["sessions"].append({"variant": "C1", "plan": "s1", "dh": "off",
"tag": "dh-off"})
d["block"][4]["sessions"][0]["add_mods"] = ["unknown"]
d["block"][5]["sessions"][0]["jvm"] = "${other}"
d["block"][0]["sessions"][0]["world"] = "nowhere"
d["defaults"]["display"] = "tv"
errs = "\n".join(matrix.validate(d, self.out))
for frag in ["S1->S5 order", "unknown key 'typo'", "variant 'nope'",
"unknown scene 's6'", "distinct tag", "not in [trials]",
"${other} not declared", "world 'nowhere'", "display 'tv'"]:
self.assertIn(frag, errs)
def test_missing_variant_is_valid_but_blocked(self):
s = matrix.expand(self.m, matrix.block_map(self.m)["B8"])[0]
self.assertTrue(matrix.variant_missing(s, self.out))
class Commands(Fixture):
def test_argv(self):
b3 = matrix.expand(self.m, matrix.block_map(self.m)["B3"])
argv, miss = matrix.bench_argv(self.m, b3[1])
self.assertEqual(miss, [])
self.assertEqual(argv[:9], ["session", "--variant", "C1", "--plan", "s1,s5",
"--reps", "3", "--label", "B3.dh-off"])
self.assertIn("--dh", argv)
self.assertEqual(argv[argv.index("--dh") + 1], "off")
self.assertEqual(argv[argv.index("--world-zip") + 1], str(self.tmp / "worlds/v1.zip"))
argv, _ = matrix.bench_argv(self.m, b3[2])
for flag, val in [("--jvm", "J2"), ("--heap", "6144"), ("--display", "gamescope"),
("--res", "1280x720"), ("--shader", "off"),
("--world-zip", str(self.tmp / "worlds/s4-4.zip"))]:
self.assertEqual(argv[argv.index(flag) + 1], val)
def test_trials_and_disable(self):
s = matrix.expand(self.m, matrix.block_map(self.m)["B12b"])[0]
argv, miss = matrix.bench_argv(self.m, s)
self.assertEqual(argv[argv.index("--add-mods") + 1],
str(self.tmp / "trials/gnetum-1.0.jar"))
self.assertEqual(argv[argv.index("--disable-mods") + 1], "a-*.jar")
(self.tmp / "trials" / "gnetum-1.0.jar").unlink()
_, miss = matrix.bench_argv(self.m, s)
self.assertTrue(any("gnetum" in x for x in miss))
def test_live_world_and_overrides(self):
s = matrix.expand(self.m, matrix.block_map(self.m)["B0"],
overrides={"display": "desktop", "res": None})[0]
argv, _ = matrix.bench_argv(self.m, s, live_world=True)
self.assertNotIn("--world-zip", argv)
self.assertEqual(argv[argv.index("--display") + 1], "desktop")
self.assertEqual(argv[argv.index("--res") + 1], "1280x720")
def test_interleave_and_params(self):
bm = matrix.block_map(self.m)
self.assertEqual([s["variant"] for s in matrix.expand(self.m, bm["B6"])],
["C1", "mf", "C1", "mf"])
s = matrix.expand(self.m, bm["B4h"])[0]
self.assertEqual(matrix.unresolved(s), ["gc"])
s = matrix.expand(self.m, bm["B4h"], params={"gc": "J2"})[0]
self.assertEqual(matrix.unresolved(s), [])
self.assertEqual(s["jvm"], "J2")
def test_seconds(self):
b3 = matrix.expand(self.m, matrix.block_map(self.m)["B3"])
self.assertEqual(matrix.session_seconds(b3[0], 60), 60 + 10 + 3 * 45 + 3 * 130)
class ShaderOptions(Fixture):
def b7(self, **extra):
d = tomllib.loads(MINI.format(tmp=self.tmp))
sess = {"variant": "C1", "plan": "s1,s4", "shader": "Pack.zip",
"shader_profile": "low", "tag": "p-low", **extra}
d["block"].append({"id": "B7", "rule": "shaders", "sessions": [sess]})
return d
def test_emitted(self):
f = self.tmp / "Pack.low.txt"
f.write_text("A=1\n")
d = self.b7(shader_options=str(f))
self.assertEqual(matrix.validate(d, self.out), [])
s = matrix.expand(d, matrix.block_map(d)["B7"])[0]
argv, miss = matrix.bench_argv(d, s)
self.assertEqual(miss, [])
self.assertEqual(argv[argv.index("--shader") + 1], "Pack.zip")
self.assertEqual(argv[argv.index("--shader-options") + 1], str(f))
self.assertIn("--shader-options", matrix.BENCH_SESSION_FLAGS)
def test_missing_file_and_relative_path(self):
d = self.b7(shader_options="shader-profiles/does-not-exist.txt")
s = matrix.expand(d, matrix.block_map(d)["B7"])[0]
argv, miss = matrix.bench_argv(d, s)
self.assertEqual(argv[argv.index("--shader-options") + 1],
str(matrix.HERE / "shader-profiles/does-not-exist.txt"))
self.assertTrue(any("shader options" in x for x in miss))
def test_needs_shader(self):
d = self.b7(shader_options="x.txt", shader="off")
self.assertIn("shader_options needs shader", "\n".join(matrix.validate(d, self.out)))
def test_spec_hash_tracks_file_content(self):
f = self.tmp / "Pack.low.txt"
f.write_text("A=1\n")
d = self.b7(shader_options=str(f))
s = matrix.expand(d, matrix.block_map(d)["B7"])[0]
argv, _ = matrix.bench_argv(d, s)
h1 = matrix.spec_hash(argv, s)
f.write_text("A=2\n")
self.assertNotEqual(h1, matrix.spec_hash(argv, s))
# sessions without shader_options keep the original hash formula (resume state)
b0 = matrix.expand(self.m, matrix.block_map(self.m)["B0"])[0]
a0, _ = matrix.bench_argv(self.m, b0)
blob = json.dumps({"argv": a0, "manual": False}, sort_keys=True)
import hashlib
self.assertEqual(matrix.spec_hash(a0, b0),
hashlib.sha256(blob.encode()).hexdigest()[:16])
def test_real_b7_automated(self):
m = matrix.load()
rows = matrix.expand(m, matrix.block_map(m)["B7"])
shaded = [s for s in rows if s["shader"] != "off"]
self.assertEqual(len(shaded), 4)
for s in rows:
self.assertFalse(s.get("manual"), s["label"])
for s in shaded:
argv, miss = matrix.bench_argv(m, s)
self.assertEqual([x for x in miss if "shader options" in x], [], s["label"])
self.assertTrue(Path(argv[argv.index("--shader-options") + 1]).is_file())
class Resume(Fixture):
def test_runs_then_skips(self):
rc, state, _ = self.run_blocks(["B0"])
self.assertEqual(rc, 0)
self.assertEqual(len(self.calls), 3)
self.assertEqual(state.data["b0"]["mods_hash"], matrix.mods_hash(self.mods))
saved = json.loads(self.state_path.read_text())
self.assertEqual({e["status"] for e in saved["sessions"].values()}, {"ok"})
self.calls.clear()
rc, _, _ = self.run_blocks(["B0"])
self.assertEqual((rc, self.calls), (0, []))
def test_mod_change_invalidates_and_warns(self):
self.run_blocks(["B0"])
(self.mods / "new-mod.jar").write_bytes(b"y")
self.calls.clear()
rc, _, err = self.run_blocks(["B3"])
self.assertEqual(rc, 0)
self.assertIn("B0 MUST BE RE-RUN", err)
self.calls.clear()
rc, _, _ = self.run_blocks(["B0"])
self.assertEqual(len(self.calls), 3) # B0 sessions are stale under the new set
def test_warns_without_b0(self):
_, _, err = self.run_blocks(["B3"])
self.assertIn("B0 (noise floor) has not been completed", err)
def test_spec_change_reruns(self):
self.run_blocks(["B0"])
self.calls.clear()
self.run_blocks(["B0"], live_world=True) # different command -> not done
self.assertEqual(len(self.calls), 3)
def test_retry_once_then_ok(self):
rc, state, _ = self.run_blocks(["B0"], codes=[1, 0])
self.assertEqual(rc, 0)
self.assertEqual(len(self.calls), 4)
self.assertEqual(state.data["sessions"]["B0#0"]["attempts"], 2)
def test_stop_after_two_failures(self):
rc, state, _ = self.run_blocks(["B0"], codes=[1, 1])
self.assertEqual(rc, 1)
self.assertEqual(len(self.calls), 2)
self.assertEqual(state.data["sessions"]["B0#0"]["status"], "failed")
self.assertNotIn("B0#1", state.data["sessions"])
self.calls.clear()
rc, _, _ = self.run_blocks(["B0"]) # resume retries the failed one
self.assertEqual((rc, len(self.calls)), (0, 3))
def test_dry_run_runs_nothing(self):
rc, _, _ = self.run_blocks(["B0", "B3"], dry_run=True)
self.assertEqual((rc, self.calls), (0, []))
self.assertFalse(self.state_path.exists())
def test_blocked_aborts_or_skips(self):
rc, _, _ = self.run_blocks(["B8"])
self.assertEqual((rc, self.calls), (2, []))
rc, _, _ = self.run_blocks(["B4h"]) # unresolved ${gc}
self.assertEqual((rc, self.calls), (2, []))
rc, _, _ = self.run_blocks(["B8", "B6"], skip_blocked=True)
self.assertEqual((rc, len(self.calls)), (0, 4))
def test_manual_deferred_skipped(self):
rc, _, _ = self.run_blocks(["B13", "B5", "B14"])
self.assertEqual((rc, self.calls), (0, []))
rc, _, _ = self.run_blocks(["B13", "B5"], include_manual=True, include_deferred=True)
self.assertEqual(len(self.calls), 2)
def test_mods_restored_check(self):
def mutating(argv):
self.calls.append(argv)
(self.mods / f"leak{len(self.calls)}.jar").write_bytes(b"l")
return 0
state = matrix.State(self.state_path)
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
rc = matrix.run_blocks(self.m, ["B0"], state, self.mods, runner=mutating,
out_dir=self.out)
self.assertEqual((rc, len(self.calls)), (1, 1))
def test_unknown_block(self):
with self.assertRaises(matrix.MatrixError):
self.run_blocks(["B99"])
class Cli(unittest.TestCase):
def test_list_and_plan(self):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
self.assertEqual(matrix.main(["list"]), 0)
self.assertEqual(matrix.main(["plan", "B4h", "--param", "gc=J1",
"--display", "desktop"]), 0)
out = buf.getvalue()
self.assertIn("| B0 | noise floor | 3 |", out)
self.assertIn("--jvm J1 --heap 4096", out)
self.assertIn("--display desktop", out)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,100 @@
"""Offline tests for gen_shader_profiles.py (synthetic packs; no game)."""
import contextlib
import io
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
import gen_shader_profiles as g # noqa: E402
PROPS = (
"# Profiles\r\n"
" profile.BASE = A=1 !B C D:4 \\\r\n"
" E=x\r\n"
"profile.LOW = profile.BASE A=2 !C\r\n"
"profile.TOP = profile.LOW B E=y\r\n"
"screen=<profile> A B\r\n"
"#if A == 1\r\n"
" program.world0/shadow.enabled=false\r\n"
"#endif\r\n"
)
class Parse(unittest.TestCase):
def test_tokens_and_continuation(self):
p = g.parse_profiles(PROPS)
self.assertEqual(sorted(p), ["BASE", "LOW", "TOP"])
self.assertEqual(p["BASE"], ["A=1", "!B", "C", "D:4", "E=x"])
def test_resolve_semantics(self):
p = g.parse_profiles(PROPS)
opts, prog = g.resolve("BASE", p)
self.assertEqual(opts, {"A": "1", "B": "false", "C": "true", "D": "4", "E": "x"})
self.assertEqual(prog, [])
opts, _ = g.resolve("TOP", p) # includes recurse; later tokens win
self.assertEqual(opts, {"D": "4", "A": "2", "C": "false", "B": "true", "E": "y"})
def test_errors(self):
p = {"X": ["profile.Y"], "Y": ["profile.X"]}
with self.assertRaisesRegex(g.ProfileError, "cycle"):
g.resolve("X", p)
with self.assertRaisesRegex(g.ProfileError, "unknown profile"):
g.resolve("NOPE", g.parse_profiles(PROPS))
with self.assertRaisesRegex(g.ProfileError, "conditional"):
g.parse_profiles("#ifdef MC_OS_MAC\nprofile.M = A=1\n#endif\n")
with self.assertRaisesRegex(g.ProfileError, "escaping"):
g.render({"A": "has space"})
def test_programs_collected(self):
_, prog = g.resolve("P", {"P": ["!program.world0/shadow", "A"]})
self.assertEqual(prog, ["world0/shadow"])
class Generate(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp())
self.zip = self.tmp / "Pack_1.0.zip"
with zipfile.ZipFile(self.zip, "w") as z:
z.writestr("shaders/shaders.properties", PROPS)
z.writestr("shaders/lib/settings.glsl",
"#define A 1 // [1 2]\n//#define B\n#define C\nconst int D = 4; // [4 8]\n")
def test_file_and_unknowns(self):
text, n, unknown = g.generate(self.zip, "LOW")
self.assertEqual(n, 5)
self.assertEqual(unknown, ["E"])
body = [ln for ln in text.splitlines() if not ln.startswith("#")]
self.assertEqual(body, ["B=false", "D=4", "E=x", "A=2", "C=false"])
self.assertIn("profile.LOW of Pack_1.0.zip", text)
self.assertEqual(g.out_name("Pack_1.0.zip", "LOW"), "Pack_1.0.LOW.txt")
def test_program_disabling_profile_rejected(self):
z2 = self.tmp / "P2.zip"
with zipfile.ZipFile(z2, "w") as z:
z.writestr("shaders/shaders.properties", "profile.X = !program.composite A\n")
with self.assertRaisesRegex(g.ProfileError, "disables programs"):
g.generate(z2, "X")
def test_cli_write_then_check(self):
out = self.tmp / "out"
args = ["--packs", str(self.tmp), "--out", str(out), "--target", "Pack_1.0.zip:TOP"]
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(g.main(args), 0)
self.assertTrue((out / "Pack_1.0.TOP.txt").is_file())
self.assertEqual(g.main(args + ["--check"]), 0)
(out / "Pack_1.0.TOP.txt").write_text("stale\n")
self.assertEqual(g.main(args + ["--check"]), 1)
def test_matrix_targets(self):
t = g.matrix_targets()
self.assertIn(("MakeUp-UltraFast-9.5f.zip", "medium"), t)
self.assertIn(("ComplementaryReimagined_r5.9.3.zip", "LOW"), t)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,57 @@
# Phase 0 distill — capture protocol, sheet columns, per-machine prep, LEVERS §7 corrections
Sources: `phase0/P0-steamdeck.md`, `phase0/P1-windows-7700k-1060.md`, `phase0/P2-linux-6700k-1080ti.md`, `LEVERS.md` §7.
## (a) Benchmark capture procedure (shared protocol, all machines)
- [ ] **Uncapped, vsync OFF during measurement.** Apply the FPS cap only *after* tuning; with a cap/vsync every config "hits" the target.
- [ ] Cap/vsync settings: in-game **Max framerate: Unlimited**, **VSync: Off**; MangoHud `fps_limit=0`; SteamOS **Framerate Limit → Off**; P1: NVIDIA panel Vertical sync → Off too (**two vsync switches**, check both each session); P2: compositor vsync disabled.
- [ ] **Metric: p99 frametime + 1% low FPS, not average FPS** (sheet also logs p50/p95, avg FPS, peak frametime).
- [ ] **Discard 10–15 s of warmup per scene** (JIT, chunk load, initial GC).
- [ ] **Settle after any render-distance or DH change** — both trigger rebuilds; otherwise you measure the rebuild, not steady state.
- [ ] **3 runs per configuration; report median + spread.** A single run is noise. **One variable per run.**
- [ ] Capture duration: MangoHud `log_duration=30` (seconds; match scene length); PresentMon Option B `--timed 30`. P0 extra: ~5 min thermal soak before first measured run. P0 runtime log toggle: `toggle_logging` (default `Shift_L+F2`).
- [ ] Invalid-run guardrails: FPS limiter active; governor moved (P0/P2); RAM headroom shifted materially (P0/P2); power plan changed or GPU throttling (P1) — `nvidia-smi -q -d PERFORMANCE` must show no `SW Power Cap` / `HW Slowdown`.
- [ ] GPU background logging (P1+P2, identical command): `nvidia-smi --query-gpu=timestamp,memory.used,clocks.sm,clocks.mem,power.draw,temperature.gpu,utilization.gpu --format=csv -l 1` → `gpu-<timestamp>.csv` (`memory.used` = VRAM series).
- [ ] Fleet-wide JVM: Java 21, **same JVM build on all three** (Temurin 21); identical G1 args except `-Xmx` (P0 `-Xmx5G` start/6 G max, P1 `-Xmx8G`, P2 `-Xmx6G`); **omit `-XX:+AlwaysPreTouch` on P0 and P2**.
## (b) Recording-sheet columns (one row per run)
- **P0**: `Date / session`, `Machine`, `Config ID`, `Scene`, `Run #`, `CSV file`, `p50 / p95 / p99 frametime (ms)`, `1% low FPS`, `Avg FPS`, `Peak measured frametime (ms)`, `Notes`.
- **P1**: same plus `CSV file (raw + summary)`, `VRAM peak (MB)` (headline metric), `Throttle flag` (none / SW Power Cap / HW Slowdown).
- **P2**: same plus `VRAM peak (MB)`, `Throttle flag`, `MemAvailable before / after`, `Swap delta` (must be 0).
- Example values: Machine `P0-SteamDeck-LCD` / `P1-7700K-1060` / `P2-6700K-1080Ti`; Config ID `baseline / dh-off / emf-off / …`; Scene `A-base / B-terrain / C-gui`; Run # `1 / 2 / 3`; CSV e.g. `Minecraft_2026-09-28_14-22-01.csv`.
## (c) Per-machine prep specifics
### P0 — Steam Deck (GPU/shared-RAM bound; LCD 60 Hz can only validate the 30/60 ladder rows)
- [ ] Charger plugged in; **Desktop Mode** (Gaming Mode routes through MangoApp, which cannot log); instance on **internal storage**; record panel refresh rate.
- [ ] SteamOS overlay: Quick Access `...` → Performance → **Overlay Level 0**, **Framerate Limit Off**, **TDP Off or pinned (e.g. 15 W)** — recorded, constant; check no per-game profile re-enables them; disable Decky MangoApp-routed plugins.
- [ ] Governor: leave `schedutil` as set but record it (`/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor`); pinning `performance` risks thermal throttling.
- [ ] MangoHud install (re-check every session — SteamOS updates remove pacman packages): `sudo steamos-readonly disable && sudo pacman -S --needed mangohud && sudo steamos-readonly enable`; find real paths with `pacman -Ql mangohud | grep -E 'mangohud$|libMangoHud'`. Alternative: MangoHudPy (verify it can write CSV first).
- [ ] Config from packaged example → `~/.config/MangoHud/MangoHud.conf`: `output_folder=/home/deck/mcbench/logs`, `autostart_log=1`, `log_duration=30`, `fps_limit=0`, `frametime=1`, `frame_timing=1`; percentile key/syntax: **not stated** — confirm in packaged example.
- [ ] Prism wrapper command (Custom Commands): `mangohud`; fallback `mangohud --dlsym`.
### P1 — Windows 7700K / GTX 1060 6 GB (VRAM bound; only Windows box)
- [ ] `powercfg /setactive SCHEME_MIN` (High performance); Game Bar off; Game DVR background recording off; Defender real-time exclusion for instance folder (record it); close browsers/Discord/OneDrive.
- [ ] NVIDIA per-program settings for **`javaw.exe`** (verify process name in Task Manager): Power management → **Prefer maximum performance**; Vertical sync → **Off**; Low Latency Mode, Threaded Optimization, Texture filtering → fixed + recorded; HAGS recorded + constant; disable *Fullscreen optimizations* for `javaw.exe`; same fullscreen display mode every session.
- [ ] `nvidia-smi` (PowerShell, same command as P2) + `-q -d PERFORMANCE` throttle check; VRAM logging = headline metric (peak VRAM on 6 GB).
- [ ] Tooling: **PresentMon** (no MangoHud on Windows), run **as Administrator**. Option A: Frame Time Analysis `Run.bat` → `presentmon.exe --multi_csv --no_console_stats --hotkey "F10"` (F10 starts/stops; new CSV per capture). Option B: `presentmon.exe --process_name javaw.exe --output_file ~\mcbench\logs\pm.csv --timed 30`. Emits raw CSV + stats summary CSV (avg/min/max/percentile FPS). Analysis: Frame Time Analysis (browser, reads PresentMon/CapFrameX/OCAT/MangoHud/FrameView, merges CSVs).
- [ ] Cross-OS caveat: ETW vs MangoHud don't measure identically — absolute numbers not cross-comparable; compare orderings per OS.
### P2 — Linux 6700K / GTX 1080 Ti (CPU main-thread + RAM/GC bound; ~8 GB taken by Docker)
- [ ] **Do not alter Docker or any running process** — treat load as constant; verify instead.
- [ ] Proprietary NVIDIA driver confirmed (`nvidia-smi`; `glxinfo -B | grep -E 'OpenGL renderer|OpenGL version'`); compositor vsync off; record resolution/refresh.
- [ ] `nvidia-settings -a '[gpu:0]/GPUPowerMizerMode=1'`; throttle check via `nvidia-smi -q -d PERFORMANCE`; start the shared `nvidia-smi --query-gpu=…` logging.
- [ ] **Noise floor FIRST**: same config, same scene, 3 runs, no changes — p50/p95/p99 spread = detection threshold; any delta below it is not evidence.
- [ ] RAM baseline: record `MemAvailable` (`free -h`, `/proc/meminfo`) at start and before/after each block; drift = runs not comparable; `grep -E 'pswpin|pswpout' /proc/vmstat` must not move (swap during a run invalidates p99 outright).
- [ ] Governor: `sudo cpupower frequency-set -g performance` (record; revert afterwards). Bench at stock clocks.
- [ ] MangoHud: config keys same as P0 (`output_folder=/home/<user>/mcbench/logs`, `autostart_log=1`, `log_duration=30`, `fps_limit=0`, `frametime=1`, `frame_timing=1`); percentile key: **not stated**. Prism wrapper `mangohud`, fallback `mangohud --dlsym`.
- [ ] Heap rule: `-Xmx` = min(6 GB, `MemAvailable` − 2 GB); rig **over-weights RAM/GC ladder steps** (Docker) — record as rig property, don't attribute to a mod.
## (d) LEVERS §7 corrections (every bullet, corrected fact)
- **DH default threads is 4 on the fleet, not 2** — default is `ceil(nproc × 0.5)` (`ThreadPresetConfigEventHandler` @ 3.3.2); the observed `numberOfThreads = 2` came from the owner's 4-thread machine (nproc=4). All other observed DH values match source defaults. *(Phase 1b)*
- **Puzzles Lib 21.1.64 has no perf change on NeoForge** (lever 14 is a bugfix only) — r9's lead and this file's earlier "the fix is in 21.1.64 for 1.21.1" were wrong for our loader. *(Phase 1b)*
- **r6's Shenandoah flags crash JDK 21**: `-XX:ShenandoahGCMode=iu` needs `-XX:+UnlockExperimentalVMOptions` (tested OpenJDK 21.0.12); use plain `-XX:+UseShenandoahGC`. *(Phase 1b)*
- **SSRD's distance is a COMMON config**, not a Sodium option: `config/ssrd-common.toml`, `physicsRenderDistance`, default 128, slider floor 16; the server reads it too. *(Phase 1b)*
- **r4 "remove one CTM engine" is wrong** — Chipped requires Athena and Rechiseled requires Fusion; both are hard dependencies (§5).
- **r9 "Puzzles Lib ~8 % render-thread overhead"** was measured on MC 26.2/Fabric; the fix is in 21.1.64 for 1.21.1; gain size unknown. *(superseded by the Phase 1b Puzzles bullet above — both appear in §7 as written)*
- **r3/r8's EMF #443 magnitude** is from the closed Oculus/Forge era; treat the 4× figure as directional only.
- **"Sable overrides vanilla core shaders"**: real but small — just `worldborder.*` from the nested Veil; the substantive part is Sable's runtime patching of Flywheel's internal shaders.
- **r1's Flywheel-under-shaders claim**: confirmed in `Backends.java` on the 1.21.1 branch (with any shaderpack active, Flywheel marks both GPU backends unsupported → Create renders kinetic BEs on CPU + shadow pass).
@@ -0,0 +1,60 @@
# Triage: /home/logan/onion-mc/logs/latest.log (dedicated server)
7165 lines, 986,551 bytes. **Covers mid-session only**: starts `14Oct2026 00:00:29` with
RCON keepalive lines (server was already running); no mod-loading/startup phase is in
this file. Ends ~19:50. NOTE: log timestamps say 2026 — quoted as-is.
## 1) Missing dependency / missing mod / "Mod file ... requires" errors
**no hits.** Searched (case-insensitive): `missing (mod|dependency)`, `Mod file .* requires`,
`mandatory`, `dependency`, `requires` — 0 matches in the whole file.
## 2) Mixin apply failures / fatal crashes
**no hits.** 0 lines containing `mixin` at all; 0 `/FATAL]` lines; no `Critical injection
failure`, `crash report`, `Exception`, `Segfault`, or `Failed to apply`. Server ran
continuously (periodic Sable sub-level saves every ~5 min) with no crash.
## 3) "fishermans" / biome-modifier parse errors or warnings
**no hits.** 0 matches for `fisher` (any case) and 0 for `biome` — so no biome-modifier
parse errors/warnings either.
## 4) ftbteams / dev/ftb/mods/ftbteams
**no hits** for `ftbteams` (any case). `ftb` case-insensitively matches 15 lines, **all**
from `dev.ftb.mods.ftbbackups.Backups` (routine FTB Backups runs at 12:28, 14:28, 16:28),
e.g. line 4465:
```
4465:[14Oct2026 12:28:03.472] [Server thread/INFO] [dev.ftb.mods.ftbbackups.Backups/]: backup starting: Server
```
No errors or references to ftbteams jar paths.
## 5) YACL "Failed to deserialize" (fowlplay / crittersandcompanions)
**no hits.** 0 lines contain `yacl`, 0 contain `Failed to deserialize`, 0 mention
`fowlplay` or `crittersandcompanions`.
## 6) Startup-complete marker / "Done (N.NNNs)!"
**Absent from this file.** No `Done (`, `Starting minecraft server`, `Preparing level`,
or startup banner — the file begins mid-run (`[14Oct2026 00:00:29.211] ... Thread RCON
Client /127.0.0.1 started`, line 1). Startup output is in an earlier rotated log
(e.g. `logs/2026-10-1*.log.gz`). The server was up throughout; line 68 shows a LAN
connection attempt refused by whitelist:
```
68:[14Oct2026 00:12:07.186] [Server thread/INFO] [net.minecraft.server.network.ServerLoginPacketListenerImpl/]: Disconnecting FishNet (/127.0.0.1:51840): You are not white-listed on this server!
```
## 7) Error/fatal counts + sample
- Literal `[ERROR]`: **0** — this log's format is `[Server thread/ERROR]`; counting `/ERROR]` gives **8 lines**.
- `[FATAL]` / `/FATAL]`: **0**. `[WARN]`: **0**.
- All 8 ERROR lines collapse to 4 distinct messages (each twice), all benign
`ServerRecipeBook` noise at 12:28:02 (right before the 12:28:03 backup), lines 4450–4457:
```
4450:[14Oct2026 12:28:02.967] [Server thread/ERROR] [net.minecraft.stats.ServerRecipeBook/]: Tried to load unrecognized recipe: etched:radio removed now.
4451:[14Oct2026 12:28:02.968] [Server thread/ERROR] [net.minecraft.stats.ServerRecipeBook/]: Tried to load unrecognized recipe: etched:music_label removed now.
4452:[14Oct2026 12:28:02.968] [Server thread/ERROR] [net.minecraft.stats.ServerRecipeBook/]: Tried to load unrecognized recipe: etched:boombox removed now.
4453:[14Oct2026 12:28:02.968] [Server thread/ERROR] [net.minecraft.stats.ServerRecipeBook/]: Tried to load unrecognized recipe: etched:blank_music_disc removed now.
```
## Bottom line
No evidence here of missing-mods, mixin failures, fishermans/biome-modifier parse issues,
ftbteams references, or YACL deserialize problems — and no crash. If client-side symptom
reports stem from startup-time mod loading, the signal will be in the **rotated startup
log** or the **client** log, not this mid-session server `latest.log`. No files were
modified; no processes touched.
+67 -14
View File
@@ -11,7 +11,8 @@ 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
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
@@ -29,10 +30,10 @@ 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.
# 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",
@@ -45,8 +46,18 @@ SHIP = {
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),
@@ -182,12 +193,35 @@ HIGH = { # strong GPU, but still a 4C/8T CPU (true of every fleet machine)
BENCH = {
"options.txt": {"maxFps": 260, "enableVsync": "false", "renderDistance": 8,
"simulationDistance": 8},
"config/DistantHorizons.toml": {(DH_W, "generatorPlan"): "DISABLED"},
"config/DistantHorizons.toml": {
(DH_W, "generatorPlan"): "DISABLED",
# Harness: suppress DH's "new update available" modal on every launch.
("client.advanced.autoUpdater", "enableAutoUpdater"): False,
},
}
def c1(extra):
return [DEFAULT, extra]
# 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 = {
@@ -197,10 +231,10 @@ VARIANTS = {
"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),
"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),
@@ -222,6 +256,15 @@ VARIANTS = {
"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),
@@ -286,7 +329,8 @@ def apply_kv(text, ov, name, sep, append):
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):
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}"
@@ -323,7 +367,7 @@ def render(layers, name):
if fmt == "json":
return apply_json(text, merged, name), merged
sep = ":" if fmt == "options" else "="
return apply_kv(text, merged, name, sep, append), merged
return apply_kv(text, merged, name, sep, append or ALLOW_NEW.get(name, set())), merged
def main():
@@ -350,6 +394,15 @@ def main():
(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()
@@ -0,0 +1,237 @@
"""Tests for onion-perf-setup.sh (and .ps1 when pwsh exists) against temp instance dirs.
Needs `python3 build.py --ship` to have generated config/onion-perf/ in the repo.
Run: python3 -m unittest discover -s analysis/client-perf/proposed-configs/tests
"""
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parents[4]
PERF = REPO / "config" / "onion-perf"
SCRIPTS = ["onion-perf-setup.sh", "onion-perf-setup.ps1", "onion-perf-setup.bat"]
OLD_OPTIONS = (
"version:3955\n"
"maxFps:144\n"
"enableVsync:false\n"
"renderDistance:12\n"
"mipmapLevels:4\n"
"soundCategory_master:0.5\n"
"key_key.attack:key.mouse.left\n"
"key_key.jump:key.keyboard.space\n"
'resourcePacks:["vanilla","file/MyPack.zip"]\n'
)
def runners():
"""(name, argv prefix) for every available POSIX-sh implementation, plus pwsh."""
out = [("sh", ["sh"])]
if shutil.which("dash"):
out.append(("dash", ["dash"]))
if shutil.which("zsh"):
out.append(("zsh-emulate-sh", ["zsh", "--emulate", "sh"]))
return out
PWSH = shutil.which("pwsh")
def tier_options(tier):
kv = {}
for line in (PERF / tier / "options.txt").read_text().splitlines():
k, _, v = line.partition(":")
kv[k] = v
return kv
class Base:
"""Mixin; subclasses set self.cmd(script, args) -> argv."""
def setUp(self):
if not (PERF / "mid").is_dir():
self.skipTest("config/onion-perf not generated (run build.py --ship)")
self.tmp = Path(tempfile.mkdtemp())
self.game = self.tmp / "instance"
(self.game / "config").mkdir(parents=True)
shutil.copytree(PERF, self.game / "config" / "onion-perf")
for s in SCRIPTS:
shutil.copy(REPO / s, self.game / s)
(self.game / "config" / "DistantHorizons.toml").write_text("old dh\n")
(self.game / "config" / "unrelated.toml").write_text("keep me\n")
(self.game / "options.txt").write_text(OLD_OPTIONS)
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
def run_setup(self, *args, cwd=None):
return subprocess.run(self.cmd(*args), cwd=cwd or self.tmp, stdin=subprocess.DEVNULL,
capture_output=True, text=True, timeout=60)
def backups(self):
d = self.game / "onion-perf-backup"
return sorted(d.iterdir()) if d.is_dir() else []
def test_tier_copy_backup_and_merge(self):
for tier in ("deck", "mid", "high"):
with self.subTest(tier=tier):
if tier != "deck":
self.tearDown()
self.setUp()
r = self.run_setup(tier, "--yes")
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
src = PERF / tier
for f in (src / "config").rglob("*"):
if f.is_file():
rel = f.relative_to(src)
self.assertEqual((self.game / rel).read_bytes(), f.read_bytes(), rel)
self.assertEqual((self.game / "config/unrelated.toml").read_text(), "keep me\n")
# backup holds the pre-run files
(b,) = self.backups()
self.assertEqual((b / "config/DistantHorizons.toml").read_text(), "old dh\n")
self.assertEqual((b / "options.txt").read_text(), OLD_OPTIONS)
self.assertFalse((b / "config/unrelated.toml").exists())
# options merged key by key
want = tier_options(tier)
lines = (self.game / "options.txt").read_text().splitlines()
kv = dict(line.partition(":")[::2] for line in lines)
for k in ("version", "maxFps", "enableVsync"):
self.assertEqual(kv[k], dict(x.partition(":")[::2]
for x in OLD_OPTIONS.splitlines())[k], k)
for k, v in want.items():
if k not in ("version", "maxFps", "enableVsync"):
self.assertEqual(kv[k], v, k)
for keep in ("soundCategory_master:0.5", "key_key.attack:key.mouse.left",
"key_key.jump:key.keyboard.space",
'resourcePacks:["vanilla","file/MyPack.zip"]'):
self.assertIn(keep, lines)
self.assertEqual(len(lines), len(set(line.partition(":")[0] for line in lines)))
# existing lines keep their positions; appended keys go at the end
self.assertEqual(lines[0], "version:3955")
self.assertTrue(lines[3].startswith("renderDistance:"))
if want["mipmapLevels"] != "4":
self.assertIn(f"mipmapLevels: 4 -> {want['mipmapLevels']}", r.stdout)
else:
self.assertNotIn("mipmapLevels:", r.stdout)
self.assertIn("(unset) ->", r.stdout)
def test_value_with_colon_replaced_whole(self):
# split on the FIRST ':' only: a tier value containing ':' replaces as a whole
tier_opt = self.game / "config/onion-perf/high/options.txt"
tier_opt.write_text(tier_opt.read_text() + "onionProbe:part1:part2\n")
(self.game / "options.txt").write_text("version:3955\nonionProbe:wrong\n")
r = self.run_setup("high", "--yes")
self.assertEqual(r.returncode, 0, r.stderr)
lines = (self.game / "options.txt").read_text().splitlines()
self.assertIn("onionProbe:part1:part2", lines)
self.assertNotIn("onionProbe:wrong", lines)
self.assertIn("onionProbe: wrong -> part1:part2", r.stdout)
def test_idempotent(self):
self.assertEqual(self.run_setup("deck", "--yes").returncode, 0)
snap = {p: p.read_bytes() for p in self.game.rglob("*") if p.is_file()
and "onion-perf-backup" not in p.parts}
r = self.run_setup("deck", "--yes")
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("nothing changed", r.stdout)
self.assertEqual(len(self.backups()), 1)
for p, data in snap.items():
self.assertEqual(p.read_bytes(), data, p)
def test_default_tier_is_mid_when_noninteractive(self):
r = self.run_setup("--yes")
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("tier 'mid'", r.stdout)
self.assertEqual((self.game / "config/DistantHorizons.toml").read_bytes(),
(PERF / "mid/config/DistantHorizons.toml").read_bytes())
def test_missing_options(self):
(self.game / "options.txt").unlink()
r = self.run_setup("high", "--yes")
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("options.txt not found", r.stdout)
self.assertFalse((self.game / "options.txt").exists())
def test_crlf_options_preserved(self):
(self.game / "options.txt").write_bytes(OLD_OPTIONS.replace("\n", "\r\n").encode())
r = self.run_setup("mid", "--yes")
self.assertEqual(r.returncode, 0, r.stderr)
data = (self.game / "options.txt").read_bytes()
self.assertNotIn(b"\r\r", data)
self.assertEqual(data.count(b"\n"), data.count(b"\r\n"))
self.assertIn(b"key_key.attack:key.mouse.left\r\n", data)
self.assertIn(b"biomeBlendRadius:1\r\n", data)
def test_lf_options_preserved(self):
self.run_setup("mid", "--yes")
self.assertNotIn(b"\r", (self.game / "options.txt").read_bytes())
def test_bad_tier(self):
r = self.run_setup("ultra", "--yes")
self.assertNotEqual(r.returncode, 0)
self.assertEqual((self.game / "config/DistantHorizons.toml").read_text(), "old dh\n")
def test_missing_onion_perf(self):
shutil.rmtree(self.game / "config" / "onion-perf")
r = self.run_setup("mid", "--yes")
self.assertNotEqual(r.returncode, 0)
self.assertIn("Update the modpack", r.stderr)
self.assertEqual(self.backups(), [])
def test_needs_yes_when_noninteractive(self):
r = self.run_setup("mid")
self.assertNotEqual(r.returncode, 0)
self.assertIn("--yes", r.stderr)
self.assertEqual((self.game / "config/DistantHorizons.toml").read_text(), "old dh\n")
def make_sh_case(name, prefix):
class Case(Base, unittest.TestCase):
def cmd(self, *args):
return [*prefix, str(self.game / "onion-perf-setup.sh"), *args]
def test_run_from_game_dir_with_relative_path(self):
r = subprocess.run([*prefix, "onion-perf-setup.sh", "mid", "--yes"], cwd=self.game,
stdin=subprocess.DEVNULL, capture_output=True, text=True)
self.assertEqual(r.returncode, 0, r.stderr)
Case.__name__ = Case.__qualname__ = f"Sh_{name.replace('-', '_')}"
return Case
for _n, _p in runners():
globals()[f"Sh_{_n.replace('-', '_')}"] = make_sh_case(_n, _p)
@unittest.skipUnless(PWSH, "pwsh not installed: onion-perf-setup.ps1 untested here")
class Pwsh(Base, unittest.TestCase):
def cmd(self, *args):
return [PWSH, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File",
str(self.game / "onion-perf-setup.ps1"), *args]
class Packaging(unittest.TestCase):
def test_bat_is_crlf_and_calls_ps1(self):
data = (REPO / "onion-perf-setup.bat").read_bytes()
self.assertEqual(data.count(b"\n"), data.count(b"\r\n"))
self.assertIn(b'-File "%~dp0onion-perf-setup.ps1" %*', data)
def test_scripts_ascii(self):
for s in SCRIPTS:
(REPO / s).read_bytes().decode("ascii")
def test_mid_matches_default_options(self):
do = REPO / "config" / "defaultoptions"
self.assertEqual((PERF / "mid/options.txt").read_bytes(),
(do / "options.txt").read_bytes())
for f in (PERF / "mid/config").rglob("*"):
if f.is_file():
rel = f.relative_to(PERF / "mid")
self.assertEqual(f.read_bytes(), (do / "extra" / rel).read_bytes(), rel)
self.assertFalse((PERF / "mid/config/modernfix-mixins.properties").exists())
if __name__ == "__main__":
unittest.main()
+73
View File
@@ -0,0 +1,73 @@
# Pack and environment
## The pack
- **Onion MC** — curated NeoForge modpack managed with **packwiz**; MC **1.21.1**,
NeoForge **21.1.251**, **217 mods** at HEAD `e5e369a`.
- The repo **doubles as the dedicated-server directory** (`/home/logan/onion-mc`).
The live server runs from here (see `03-current-state.md`). `world/`, `libraries/`,
`logs/`, `crash-reports/`, `*.jar` are `.packwizignore`d — never blanket-un-ignore.
- Remote: `ssh://git@git.ljones.me:2222/logan/onion-mc.git`, branch `main`.
- **Conventional Commits**, imperative subject, body explains *why*. Don't commit or
push unless asked / plan calls for it.
- packwiz lives at `~/go/bin/packwiz` (build `ef87d96`). On this machine it is on PATH.
- Config shipping is **opt-in**: `.packwizignore` ignores `config/*`; shipping a file
needs a negation at the **bottom** of `.packwizignore` (last match wins; negations
must not end in `/` or packwiz SkipDirs the parent).
- Hand-set `side = "server"` on **petprotect, c2me-neoforge, noisium** is silently
reset by bulk `packwiz update` (single-mod update is safe).
- Tuned defaults ship via the **Default Options** mod (`config/defaultoptions/`),
copy-if-absent → reaches fresh installs only. Change values **only** through
`analysis/client-perf/proposed-configs/build.py`, never by hand-editing outputs.
- `.pi/` and `analysis/` are packwizignored — safe to commit to git, never shipped
to players.
## This machine — the Phase 2 "proxy" bench box (Manjaro)
The three fleet machines (Steam Deck, 7700K/1060, 6700K/1080 Ti — see `phase0/`) are
**unreachable**; all Phase 2 data so far is from this **high-tier proxy**. Label all
results as proxy data: absolute FPS floors do not map to tier decisions.
| Item | Value |
| --- | --- |
| OS | Manjaro Linux, kernel 7.1.8 |
| CPU | AMD Ryzen 9 9900X, 12C/24T |
| GPU | AMD RX 7800 XT (DRI_PRIME=1 used in launch scripts) + Granite Ridge iGPU |
| RAM | 91 GB, **no swap** (good for p99 validity) |
| Disk | `/` 84% used, 140 GB free |
| Java | OpenJDK **21.0.12** at `/usr/lib/jvm/java-21-openjdk` |
| Display | Harness hardcodes `DISPLAY=:1`, `XAUTHORITY=/run/user/1000/xauth_HvvPAq`; runs must happen on the desktop session owning :1. X sockets X0/X1/X99 exist. |
| git / python | git 2.55.0, python 3.14.6 |
## Tool and data paths on this machine
| What | Path |
| --- | --- |
| packwiz | `~/go/bin/packwiz` |
| Prism data dir (bench, launched with `-d`) | `~/onion-bench/prism` |
| Bench Prism instance | `~/onion-bench/prism/instances/OnionBench` |
| Instance game dir | `.../OnionBench/.minecraft` (provisioned; `saves/onionbench` 26 MB; Default Options seeded; `BENCH_VARIANT.txt` = `C1` = last applied variant; MaxMemAlloc 8192) |
| Instance PreLaunchCommand | **empty** (intentional — no packwiz self-update mid-matrix, per BENCH-MATRIX §1) |
| Bench worldgen server | `~/onion-bench/server-bench` (full NeoForge server; `benchworld/` = the generated world source, 26 MB) |
| MangoHud | user-local build: `~/.local/mangohud`, wrapper `~/.local/bin/mangohud-local` (sets LD_PRELOAD shim, `output_folder=~/onion-bench/mangologs`, autostart, 1800 s cap, fps_limit=0, vblank off). **System `mangohud` NOT in PATH.** |
| MangoHud frametime CSVs | `~/onion-bench/mangologs/` |
| Harness results CSV | `~/onion-bench/harness-results.csv` (append via `run_scene.py --record`) |
| Bench launch scripts | `~/onion-bench/launch-{bench,gui,online,xvfb,...}.sh` (`launch-bench.sh` exports DISPLAY/XAUTHORITY/DRI_PRIME and launches OnionBench offline as user `Tester`) |
| Jar cache for jarscan | `/tmp/onion/jars` — **currently absent** (ephemeral). Refetch with `analysis/audit/fetch.py` only if re-running jarscan. |
| xdotool | `/usr/bin/xdotool` (present) |
## Harness wiring (quick reference)
- `analysis/client-perf/harness/run_scene.py` — apply variant → launch Prism into
world `onionbench` under MangoHud → drive scenes via xdotool → slice p50/p95/p99/
peak/1%-low from the MangoHud CSV. Constants: `ROOT=~/onion-bench/prism`,
`INSTANCE=OnionBench`, `GAME=$ROOT/instances/OnionBench/.minecraft`,
`MANGOLOGS=~/onion-bench/mangologs`, `WORLD=onionbench`. CLI:
`python3 run_scene.py --variant C1 --scene s1 [--warmup 15] [--capture 30]
[--runs 1] [--record FILE] [--pre storm_on]`. Scenes: `s1 s2 s2s s3 s4 s5`.
Waits for "Sound engine started" in the instance log. `--record` appends one CSV row.
- `analysis/client-perf/harness/rcon.py` — stdlib RCON client; args are
`host port password command`. No config file.
- `analysis/client-perf/proposed-configs/apply_variant.py` — applies variant
`<ID>` from `proposed-configs/out/<ID>/` into the game dir and writes
`BENCH_VARIANT.txt`. **Relaunch for every config change.**
+45
View File
@@ -0,0 +1,45 @@
# Timeline of the optimization effort
Condensed from `.pi/journal.md` (the authoritative running log — read it for detail).
Commit hashes are on `main`.
## Background (pre-effort)
- 2026-09: pack curation era — FTB Chunks → Open Parties & Claims + Xaero's swap,
FTB Quests/Teams removed, map migration tooling (`analysis/FTB-XAERO-MAP-MIGRATION.md`).
- 2026-10-06: 60-mod expansion to 209/212 mods + full static audit of all jars
(`analysis/audit/`), then the **Paxi `onion-fixes` datapack** (`fc649b4`) generated by
`analysis/audit/gen_fixes.py` to resolve cross-mod recipe collisions. Rules: edit the
generator, not the JSON.
## Client-performance initiative
| Date | Phase | What happened |
| --- | --- | --- |
| 2026-10-14 | **Phase 1 — static baseline** | `analysis/client-perf/` created: jarscan (951 per-frame mixin classes; Handcrafted+Chipped+Rechiseled = 58% of block atlas), 9 research briefs, **LEVERS.md** (ranked levers), observed-default configs. Nothing changed in the pack. |
| 2026-10-14 | **Phase 1b — delivery + bench design** | `proposed-configs/` generator (`build.py`; tiers T-default/deck/mid/high + 32 bench variants C0…spr-strict), **BENCH-MATRIX.md** (run sheet), bench datapack `bench/onion-bench/`, PLAYER-TUNING.md. Verified packwiz `preserve` semantics and Default Options 21.1.8 behavior. Puzzles Lib 21.1.64 patch prepared. |
| 2026-10-14 | **Owner decisions applied, shipped** | Owner settled the pending list (see `05-decisions-and-rules.md`). Commits: `a01bc3b` (Default Options + 8 provisional mid-tier files + Sodium Extra, Reese's Sodium Options, BadOptimizations, Particle Core + Puzzles Lib 64 → **217 mods**), `d80f85b` (spawn tuning: alexsmobs/fowlplay/critters partial files), `5919955` (archive cleanup), docs commit (client-perf, audit, phase0, journal). |
| 2026-10-14/15 | **Phase 2 begins — boot + harness** | Machine moved to Manjaro (client-capable). Client boot check **essentially PASS** (main menu clean, 29.3 s, atlas 4096×4096×2, Default Options seeded). The one real blocker — **Critters & Companions camelCase spawn keys** NPE — fixed by owner as `8e8483d`. A 20:34 FML crash report is that same CAC bug (not WATUT). Harness built (`harness/run_scene.py`, `rcon.py`), user-local MangoHud 0.8.4, bench world generated via temp server `~/onion-bench/server-bench` (S2 Create floor + S3 mob pen placed and verified; **S4 airships not built**). One end-to-end harness run (C1/S1) — **vsync-locked, non-decision-grade**; `vblank_mode=0` added to the wrapper afterwards, never re-verified. Dedicated server booted clean from the repo at 21:01 (idle, RCON-keepalive). Committed as `e5e369a`. |
| 2026-10-14 late | **Session interrupted** | After `e5e369a` (23:16): server kept running in tmux `onion-mc`, mods re-saved two config files (benign), RCON poller pinging every 30 s until ~23:58. **No matrix block was ever run.** State captured into `analysis/state/` (this directory). |
## HEAD ancestry (optimization-relevant commits)
```
e5e369a chore(bench): add client bench harness and Phase 2 handoff ← HEAD, pushed
8e8483d fix(config): correct Critters and Companions spawn keys ← owner, unblocks boot
611a1a5 docs: client-perf analysis, bench harness and work journal
d80f85b feat(config): moderate spawn tuning (Alex's Mobs, Fowl Play, Critters)
5919955 chore(analysis): archive finished reviews
a01bc3b feat(perf): Default Options + 4 perf mods + Puzzles Lib 64 ← 217 mods
aa1d273 ... (Phase 1 baseline was taken here)
fc649b4 feat(pack): Paxi onion-fixes datapack (recipe collisions)
```
## What has NOT happened yet
- Matrix blocks **B0–B14: zero runs**.
- Any config value shipped in `a01bc3b` is still **unmeasured** (provisional mid-tier).
- WATUT world-load crash unresolved.
- S4 bench scene unbuilt; S5 tour only partially exercised.
- Nothing has been reported back to the owner from Phase 2 data (B7 shader data, B12b
trial verdicts are owed).
+90
View File
@@ -0,0 +1,90 @@
# Current state — exact position
As of capture (2026-10-14 ~23:45 EDT, HEAD `e5e369a` pushed).
## Multi-step plan position
The initiative's plan (from the journal + BENCH-MATRIX) is:
**boot check → fix blockers → build bench world → confirm uncapped frametimes →
run B0→B14 → write winners into build.py → ship → report to owner.**
| Step | Status |
| --- | --- |
| 1. Boot check, client | ✅ **essentially PASS** (2026-10-14/15 session; HANDOFF.md). 22 benign ERRORs. |
| 1b. Boot check, dedicated server | ✅ **PASS** — booted from repo at 21:01, `Done (3.004s)`, no missing deps / mixin failures / `ftbteams` / fishermans-haven / YACL errors (see `notes-serverlog-triage.md`). |
| 2. Fix boot blockers | ✅ CAC spawn-key NPE fixed by owner (`8e8483d`). The 20:34 crash report is this bug — **do not** confuse it with the WATUT crash. |
| 3. WATUT intermittent world-load crash | ❌ **OPEN — first task.** Details below. |
| 4. Confirm uncapped frametimes | ❌ Not verified after `vblank_mode=0` fix. |
| 5. Bench world complete | ⚠️ **PARTIAL** — S1–S3 built & verified; **S4 airships not built**; S5 tour only partially exercised; S2 `/fill` motors may need manual re-placement (BENCH-MATRIX §2). |
| 6. Run matrix B0→B14 | ❌ **Zero blocks run.** |
| 7. Winners → `build.py` → ship → commit | ❌ Not started. |
| 8. Report to owner (B7 shader data, B12b trial verdicts, B14) | ❌ Not started. |
## Open defect: WATUT world-load crash (blocking bench runs)
- Symptom: intermittent crash on world load —
`corosus.watut.ShaderInstanceBlur.<init>` → `FileNotFoundException:
minecraft:shaders/core/particle.json`, during "Rendering overlay".
- First world load works, a later load crashed (2026-10-14/15 session).
- **The 20:34 crash report in `crash-reports/` is NOT this** — it is the already-fixed
CAC spawn-key NPE. No crash report exists for the WATUT crash yet.
- Owner constraint: **remove no mods** → find a WATUT config/patch/reliable-retry, or
reproduce and characterize it first.
- It must be resolved (or made reliably recoverable) before trusting long bench runs.
## Live processes / environment left by the interrupted session
- **The dedicated server is still running**: java PID 1699563 in tmux session
`onion-mc`, started 21:01:35 from the repo root (port 25565, RCON :25575).
Idle boot-check run; zero players joined. **Never kill it; never touch `world/`.**
- An RCON client from 127.0.0.1 connects/disconnects **every 30 s** (through #353 by
23:57). The poller process was not identified (candidates: a keepalive loop or
`tools/rcon.py` under some wrapper). Investigate before assuming it's yours to kill.
- Uncommitted working-tree changes are **benign mod re-saves**, expected on boot:
- `config/alexsmobs-common.toml`: 13-line tuned partial expanded to 913 full lines
(all tuned values retained — e.g. `gazelleSpawnWeight = 20`).
- `config/akashictome-common.toml`: re-serialized; curated whitelist **intact**,
comments stripped, arrays collapsed.
- Rule (`05-decisions-and-rules.md`): `git checkout -- config/` before staging;
do not commit these.
- `config/*.toml.bak` files + `config/spark/tmp-client/*.jfr.tmp` (spark's live
background-profiler buffer, still growing while the server runs) = runtime junk.
- `context.md` in the repo root was scout scratch from this capture session — safe to
delete (content folded into this directory).
## Measurements so far (all of it)
Exactly **one** harness run, **not decision-grade** (vsync-locked):
```
~/onion-bench/harness-results.csv:
C1,s1,n=1889, p50=16.7ms p95=17.0ms p99=17.2ms peak=17.6ms avg_fps=59.8 1%low=57.5
```
Supporting MangoHud CSVs in `~/onion-bench/mangologs/`:
`java_2026-10-14_22-13-53.csv` (2353 rows), `java_2026-10-14_22-14-54.csv` (3762),
`java_2026-10-14_22-21-46.csv` (178, truncated), plus two `glxgears` sanity runs.
The vsync fix (`fps_limit=0`, vblank off in `~/.local/bin/mangohud-local`) has been
applied but **not re-verified** — first task after WATUT: one C1/S1 run must show
well above 60 fps.
## Bench instance state (ready to use)
`~/onion-bench/prism/instances/OnionBench/.minecraft`: provisioned, mods installed,
`saves/onionbench` present (26 MB, from 21:54 worldgen), `config/defaultoptions/`
seeded, `options.txt` carries the bench pins (`maxFps:260`, `enableVsync:false`,
`renderDistance:8`, `simulationDistance:8`), `BENCH_VARIANT.txt` = `C1` (last applied).
PreLaunchCommand empty by design. MaxMemAlloc 8192. Launched offline as user `Tester`
via `~/onion-bench/launch-bench.sh` (DISPLAY=:1).
## Known open questions
1. WATUT crash root cause + fix path (top priority).
2. Identity of the 30 s RCON poller.
3. Whether uncommitted config re-saves should simply be checked out (recommended) —
and whether AkashicTome stripping comments matters for future edits (edit the
shipped source-of-truth, then expect the mod to re-serialize locally).
4. S2 Create floor: `/fill`-placed kinetic network may need motors re-placed by hand
(checklist in BENCH-MATRIX §2 step 2) — verify during S4 build.
5. B5 (SSRD/Sable airship decay) is scoped to fleet P0/P1 — needs a documented proxy
adaptation or a "deferred, fleet-only" note in results.
+75
View File
@@ -0,0 +1,75 @@
# Remaining plan (with current status)
The authoritative run sheet is `analysis/client-perf/BENCH-MATRIX.md` — read it fully
before benching. This file summarizes it, marks what's done, and adds proxy-machine
notes. **Win** = median p99 improves by more than the machine's B0 noise spread AND
1%-low does not worsen by more than the spread; ties keep upstream defaults.
Floors (fleet): P0 1%-low ≥ 30, P1/P2 ≥ 60 — on this proxy, floors don't map; use
relative wins only.
## Next actions, in order
1. **WATUT crash** (open): reproduce (repeated world loads in the OnionBench
instance), capture a crash report, then fix via config/patch/retry. Owner says
remove no mods. If unfixable, document a reliable workaround for bench runs.
2. **Verify uncapped frametimes**: one `run_scene.py --variant C1 --scene s1` run;
expect p99 well under 16.7 ms (vsync previously locked it). Do not proceed to B0
until FPS is visibly uncapped.
3. **Finish the bench world** (BENCH-MATRIX §2): build S4 — ONE Aeronautics ship
(~5×7×15, planks+glass+1 propeller) hovering y≈170, copied via Create schematic to
4 ships at z = 32/96/192/384; produce `bench-world-v1-s4-0/1/4.zip` copies.
Verify S2 shafts rotate (re-place motors by hand if the `/fill` network didn't
initialise) and S3 villager count. DH pregenerate per §2 step 5 (radius 256, tour,
`generatorPlan=DISABLED` afterwards). Leave world at S1, save, zip as
`bench-world-v1.zip` (record sha256).
4. **Run the matrix** B0 → B1 → B2 → B3 → B4 → B6 → B7 → B8 → B9 → B10 → B11 →
B12a → B12b → B13 → B14 (B5 deferred, fleet-only). Budget ≈25 h machine time.
Apply variants with `apply_variant.py`; relaunch per config change; scenes in
S1→S6 order per session; record with `--record`.
5. **Ship**: write winners into `proposed-configs/build.py` (DEFAULT/DECK/HIGH layers),
`build.py --ship`, `packwiz refresh`, commit (after `git checkout -- config/`).
Ship `config/modernfix-mixins.properties` **only if B6 passes** (see the `SHIP`
comment in `build.py`). New mod adds (B12b winners) ⇒ re-run B0 first.
6. **Report to owner**: B7 shader-policy data, B12b trial verdicts, B14 spawn-tuning
measurement, and any proxy-vs-fleet caveats.
## Matrix blocks (one line each; decision rules verbatim from BENCH-MATRIX §4)
| Block | Tests | Decision rule |
| --- | --- | --- |
| B0 | noise floor: C0 ×3 on S1 | defines each machine's win threshold; watch MemAvailable drift |
| B1 | baseline: C0 on S1–S6 | reference numbers only (startup, atlas, heap summary, VRAM) |
| B2 | composite: C1 / C1-deck / C1-high, all scenes | if C1 isn't a win on ≥2 machines, STOP and bisect via B3–B11 before shipping anything |
| B3 | DH: radius ladder + SSAO/quality/threads | per tier: largest radius meeting the floor on S1+S5; SSAO-off on a win; threads stay 2 unless 4 wins S5 w/o losing S1 |
| B4 | heap & GC: C1 × J0–J3, then heap 4/5/6 G | GC = best S5 1%-low; heap = smallest within winner's spread |
| B5 | Sable/SSRD sub-levels + decay (**fleet P0/P1 only**) | SSRD = largest meeting floor with 4 ships; sable-noshade only on win + acceptable looks; decay beyond spread = report, no config fix |
| B6 | ModernFix `dynamic_resources` | ship only if heap −300 MB or faster startup beyond spread AND zero missing textures AND 3 clean cold launches |
| B7 | shaders ×4 profiles on S2/S1/S4 | **data for owner, no auto-decision**; verify Flywheel backends drop under shaders; watch Iris #3164 corruption |
| B8 | entities: FA/Extensions, EMF LOD 20/8, LDL modes on S3 | smallest winning EMF LOD; cheapest winning LDL mode per tier |
| B9 | Flywheel lightSmoothness flat/C1/smooth on S2 | smoothest value meeting the floor, per tier |
| B10 | Sound Physics: spr-up/strict/nocap on S2s | adopt each winning change; listen for audio breakage |
| B11 | vanilla/Sodium: threads 3, blend 0/2, mip 4 | adopt only wins (S5 + not losing S1; mip4 needs VRAM ≤ 5.5 GB on P1) |
| B12a | added-mod ablation (BadOptimizations / Particle Core / Sodium Extra out) | keep unless removal wins or mod breaks something; Sodium Extra + RSO stay regardless |
| B12b | trial mods (Gnetum, Better Block Entities, Flerovium, Super Resolution, Ixeris, Iris Flywheel Compat, Cull Leaves) | add only on win w/o breakage via `packwiz modrinth add`; **new mod set ⇒ re-run B0**; not More Culling (excludes 1.21.1) |
| B13 | spark profile of worst B2 scene under C1 | flag any mod >5% of render-thread time that isn't Sodium/Iris/DH/Flywheel |
| B14 | spawn tuning: defaults vs shipped, river/jungle/warm ocean ×10 min | keep if totals drop and every species still appears; back off if one effectively disappears (SPAWN-TUNING §B14) |
## Scenes and JVM sets (fixed inputs)
- **S1** vista (terrain + DH) · **S2** Create floor, 2304 kinetic BEs · **S2s** = S2 +
24-sounds/tick storm · **S3** mob pen (30 villagers, allays w/ torches = moving LDL
sources) · **S4** 0/1/4 Sable airship sub-levels · **S5** 24-teleport tour, 120 s,
no warm-up discard · **S6** GUI/JEI overlay at S1.
- **J0** phase0 G1 baseline · **J1** brucethemoose G1 · **J2** generational ZGC ·
**J3** Shenandoah (plain; NOT `ShenandoahGCMode=iu` — aborts JDK 21). All parse-
checked on OpenJDK 21.0.12. Default heap here: 8 G (instance), matrix suggests 6 G
for P2 / 5 G for P0.
## Proxy-machine adaptations to document as you go
- B2's "≥2 machines" gate: run C1 vs C0 here; treat as single-machine evidence and
mark fleet confirmation as pending.
- B5 has no Sable-decay proxy path as specified — either adapt (4-ship scene exists
once S4 is built) or record as deferred.
- B4 heap ladder should use the instance's 8 G as the top rung.
- Absolute floors (30/60 fps) are fleet values; use relative wins on this box.
+76
View File
@@ -0,0 +1,76 @@
# Settled decisions, rules of the road, traps
## Owner decisions (2026-10-14) — settled, do not relitigate
1. **Delivery mechanism = Default Options** (+ enforced `config/modernfix-mixins.properties`
**after B6 passes**). Done in `a01bc3b`.
2. **Puzzles Lib 21.1.64** — done (bugfix only; no NeoForge perf change, LEVERS #14 corrected).
3. **Shaders: opt-in** (packwiz downloads them; nothing enables them). B7 produces the
policy data; owner decision still pending that data.
4. **Spawn tuning allowed if not detrimental to gameplay** — done, moderate
(`d80f85b`: alexsmobs/fowlplay/crittersandcompanions partial files). B14 measures it.
5. **Remove no mods.** LEVERS §5 removal candidates are reference only.
(This includes WATUT — so the WATUT crash must be fixed, not removed away.)
6. **Add any helpful mods** — the five Add-verdict mods went in `a01bc3b` (Sodium Extra,
Reese's Sodium Options, BadOptimizations, Particle Core, + Default Options).
Trial mods are B12b's call; the agent may add trial winners.
## Rules of the road (iteration rules from the journal)
- Change tuned defaults **only** via `analysis/client-perf/proposed-configs/build.py`
(DEFAULT / DECK / HIGH layers) → `build.py --ship` → `packwiz refresh` → commit.
Never hand-edit `proposed-configs/out/*` or shipped files.
- Shipped defaults are **copy-if-absent**: changes reach fresh installs only. Existing
players follow PLAYER-TUNING §1 (delete 7 re-seeding config files; do NOT delete
`options.txt`; set tier video values by hand).
- **`git checkout -- config/` before staging** — the server's mods re-save partial
spawn files in full on boot; committing those expansions is the known mistake.
- `packwiz update` in bulk resets hand-set `side = "server"` on **petprotect,
c2me-neoforge, noisium**. Single-mod updates are safe.
- Any new mod: verify NeoForge 1.21.1 build + `neoforge.mods.toml` ranges against pins,
check `side`, then **re-run B0** (new mod set invalidates the noise floor).
- One recorded commit per matrix row; label all data as **proxy-machine** data.
- Never kill the dedicated server (tmux `onion-mc`, repo root, port 25565) and never
touch `world/`. `sp-world.tar.gz` (650 MB, untracked) — leave alone.
- `.pi/journal.md` is the running log — append a session entry when you finish.
## Verified facts / traps (don't re-derive, don't re-trip)
- **packwiz `preserve`**: settable only by hand-editing `index.toml`; survives refresh
and unrelated updates; silently dropped if the file is absent/ignored at refresh
time; installer writes the file only if absent. Index files have no `side`, so
shipped configs land on the server too (this repo IS the server).
- **Default Options 21.1.8**: copy-if-absent for `config/defaultoptions/options.txt`
(key_ lines stripped) and `config/defaultoptions/extra/**`; runs before NeoForge
constructs mods — too late for `modernfix-mixins.properties` (hence B6 gating).
- Observed defaults were generated from the **same jar versions as current pins**
(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). DH `numberOfThreads` true default = ceil(nproc/2) = 4 here (observed 2
came from a 4-thread box).
- Flywheel 1.0.x **disables both GPU backends when any shaderpack is active**
(verified in `Backends.java`). No Iris Flywheel Compat in the pack (B12b trial).
- SSRD 1.8.7 → `config/ssrd-common.toml` (COMMON; server reads it too; slider floor 16).
LambDynamicLights 4.8.11 → `config/lambdynlights.toml` (lowercase modes).
SPR merges partial `sound_rates.properties` over generated defaults.
- **More Culling is unusable**: its NeoForge build declares `minecraft [1.21,1.21.1)`.
- `-XX:ShenandoahGCMode=iu` aborts JDK 21 without UnlockExperimentalVMOptions — J3 uses
plain `-XX:+UseShenandoahGC`.
- Prism 11 `--offline` fails a fresh instance ("Required libraries are missing");
launch online once, then offline works.
- `xdotool windowactivate --sync` hangs under kwin — use `windowactivate` +
`windowfocus`.
- MangoHud needs `MANGOHUD_CONFIG` explicit + LD_PRELOAD exported — use the wrapper
`~/.local/bin/mangohud-local`; system mangohud is not installed.
- cdn.modrinth.com 501s Range requests — download whole jars. No `unzip`; use python
zipfile. `lodestonelib` is the real slug (`lodestone` has no 1.21.1 build).
- Duplicate `version_number` traps: tarotcards-remastered, room-for-two, too-many-bows
reuse version strings across MC versions — pin exact filenames by hand.
- Iris 1.8.14-beta.1 ↔ Sodium 0.8.13 mixin incompatibility reported on NeoForge
(r5) — check when testing shaders (B7).
## Open owner-facing deliverables (owed from Phase 2)
- B7 shader-policy data → owner decision on default-off vs opt-in messaging.
- B12b trial-mod verdicts (which trials earned a permanent add).
- B14 spawn-tuning measurement (keep/back-off call).
- Any B13 "mod >5% of render thread" findings.
+60
View File
@@ -0,0 +1,60 @@
# Artifact map
Everything that exists because of this effort, with status. Paths relative to
`/home/logan/onion-mc` unless marked `~/` (= `/home/logan`).
## Repo — analysis/ (packwizignored; committed to git)
| Path | What | Status |
| --- | --- | --- |
| `analysis/state/` | **This capture.** Uncommitted yet — commit alongside next work. | current |
| `analysis/client-perf/HANDOFF.md` | Prior session's self-contained brief (2026-10-15). | committed `e5e369a` |
| `analysis/client-perf/BENCH-MATRIX.md` | **The run sheet**: §0 boot check, §1 fixed inputs, §2 bench world recipe, §3 config IDs, §4 B0–B14 + decision rules, §5 CSV format. | committed |
| `analysis/client-perf/LEVERS.md` | Ranked levers (meta M1–M3, tier 1–3), add/remove candidates, research corrections. §6 superseded by BENCH-MATRIX. | committed |
| `analysis/client-perf/PLAYER-TUNING.md` | Tier table (Deck/Mid/High), §1 file-reset for existing players, heap/GC, shader/RP guidance, Deck env vars. Values provisional until Phase 2. | committed |
| `analysis/client-perf/SPAWN-TUNING.md` | The shipped spawn tuning + B14 measurement plan. Header: "Unmeasured: bench block B14". | committed |
| `analysis/client-perf/README.md` | Phase 1 index + method/caveats. Minor staleness: says B0–B13, matrix has B14. | committed |
| `analysis/client-perf/notes-phase0-distill.md` | Distilled capture protocol from phase0 (uncapped, p99+1%low, warm-up discard, 3 runs, one variable; per-machine prep). | committed |
| `analysis/client-perf/notes-serverlog-triage.md` | Server-log triage: no missing-dep/mixin/ftbteams/YACL findings; 8 benign recipe-book ERRORs. | committed |
| `analysis/client-perf/proposed-configs/` | `build.py` (**single source of truth** for shipped defaults; BENCH variant layer; `--ship`), `apply_variant.py`, `README.md` (§1 M1 decision, §2 tiers, §3 key provenance), `templates/`, and `out/` (generated: tiers T-* and variants C0, C1, C1-deck, C1-high, dh-*, emf-*, fw-*, ldl-*, mf-*, opt-*, sable-*, sod-*, spr-*, ssrd-* + MANIFEST.md). | committed (out/ excluded) |
| `analysis/client-perf/harness/` | `run_scene.py` (variant→launch→scene→p99 pipeline; verified once), `rcon.py`. | committed `e5e369a` |
| `analysis/client-perf/bench/onion-bench/` | Bench datapack (pack_format 48): scene functions s1–s4 (+setups, storm, tour steps). S2/S3 exercised in game; S4 content unbuilt; full syntax never wholly verified. | committed |
| `analysis/client-perf/data/` | `jarscan.json`, `jarscan-report.md` (static mixin/atlas/model counts), `observed-default-configs/` (real defaults from same-version jars; reference for build.py). | committed |
| `analysis/client-perf/research/r1…r9-*.md` | Nine research briefs with confidence tags. | committed |
| `analysis/client-perf/proposed-changes/puzzles-lib-21.1.64.patch` | Applied already (in `a01bc3b`); kept as record. | committed |
| `analysis/audit/` | Static audit scripts: `fetch.py` (rebuild /tmp/onion/jars), `jarscan` inputs, `gen_fixes.py` (**generates the shipped Paxi onion-fixes datapack — edit this, not the JSON**), deps/collision/recipe chain tools. | committed |
| `analysis/archive/` | Finished 2026-09 reviews (README maps old paths). | committed |
| `analysis/FTB-XAERO-MAP-MIGRATION.md` | Pre-effort map migration doc. | committed |
## Repo — pack-facing (shipped to players)
| Path | What |
| --- | --- |
| `pack.toml`, `index.toml`, `packwiz.json`, `mods/*.pw.toml` (217) | packwiz manifest. |
| `config/defaultoptions/` | Default Options seeds (8 provisional mid-tier files, T-mid equivalent). |
| `config/alexsmobs-common.toml`, `config/fowlplay.json5`, `config/crittersandcompanions-common.json5` | Enforced partial spawn tuning (`d80f85b`). **Local copies currently re-saved-in-full by mods — do not commit.** |
| `config/paxi/datapacks/onion-fixes/` | Recipe-collision fixes, generated by `analysis/audit/gen_fixes.py`. |
| `config/akashictome-common.toml` | Curated guidebook whitelist. Local copy re-serialized by mod (whitelist intact). |
| `phase0/P0-steamdeck.md`, `P1-windows-7700k-1060.md`, `P2-linux-6700k-1080ti.md` | Per-fleet-machine measurement plans. **Machines unreachable** — status: plan only. |
| `tools/` | Map-migration/UUID/RCON utilities (pre-effort). `tools/rcon.py` is a candidate for the mystery 30 s poller. |
| `.pi/journal.md` | The running work log — the richest source; read the last three sessions. |
## Outside the repo — bench machine
| Path | What | Status |
| --- | --- | --- |
| `~/onion-bench/prism/` | Bench Prism data dir (`-d`). Instance `OnionBench`: `.minecraft` provisioned, `saves/onionbench` (26 MB), Default Options seeded, bench pins in options.txt, `BENCH_VARIANT.txt = C1`. MaxMemAlloc 8192. | ready |
| `~/onion-bench/server-bench/` | Temp NeoForge server that generated the bench world; `benchworld/` = world source (S1–S3 built; S4 missing). | ready, incomplete |
| `~/onion-bench/mangologs/` | MangoHud frametime CSVs (2 glxgears sanity, 3 java runs incl. one truncated). | data |
| `~/onion-bench/harness-results.csv` | The single C1/S1 result row (vsync-locked, non-decision-grade). | data |
| `~/onion-bench/launch-*.sh` | Launch wrappers (DISPLAY=:1, XAUTHORITY, DRI_PRIME=1, offline `Tester`). | ready |
| `~/.local/mangohud` + `~/.local/bin/mangohud-local` | User-local MangoHud 0.8.4 + wrapper (vblank off, fps_limit=0, CSVs to mangologs). | ready |
| `~/onion-bench/*.out`, `mc-menu-*.png`, `/tmp/mangohud.pkg.tar.zst`, `/tmp/cac-*.json5` | Session debug scraps. | ignorable |
| `/tmp/onion/jars` | Static-analysis jar cache. | **gone** (ephemeral; `analysis/audit/fetch.py` rebuilds — only needed for jarscan re-runs) |
| tmux session `onion-mc` | The live dedicated server (java PID 1699563). | **running — leave alone** |
## Repo — runtime (never commit / never touch)
`world/`, `libraries/`, `logs/` (latest.log + debug.log = the 21:01 server run),
`crash-reports/` (2026-10-14 crash = fixed CAC bug), `server.log`, `cache/`,
`config/*.toml.bak`, `config/spark/tmp-client/*`, `sp-world.tar.gz`, `usercache.json`.
+52
View File
@@ -0,0 +1,52 @@
# Onion MC — optimization effort: state capture
Captured **2026-10-14 ~23:45 EDT** (Manjaro bench machine), at git **HEAD `e5e369a`**
(`chore(bench): add client bench harness and Phase 2 handoff`), branch `main`, pushed.
Pack: **217 mods**, MC **1.21.1**, NeoForge **21.1.251**.
> **Update 2026-10-15 (b):** WATUT crash FIXED (`a84319b`, MidnightLib 1.10.0 — root
> cause was a MidnightLib CME); uncapped frametimes VERIFIED; harness v2 (`bench.py`)
> replaces `run_scene.py` (`e18bd3c`). See `.pi/journal.md` latest entry. Plan steps
> 1–2 of `04-plan.md` are done; next is S4 build → B0.
## Where we are (one paragraph)
The client-performance initiative is in **Phase 2 (benchmarking)**, which has just
begun and was interrupted mid-setup. Static analysis (Phase 1), the tuned-config
generator, the owner's decisions, and the first wave of perf-mod adds are **done and
shipped** (`a01bc3b`, `d80f85b`). The client **boots clean**; the dedicated server
**boots clean** (a live idle instance from the boot check is still running in tmux —
leave it alone). The bench harness works end-to-end but has produced exactly **one
non-decision-grade measurement** (C1/S1, vsync-locked ~60 fps). The bench world has
scenes S1–S3 built; **S4 (airships) is not built**. **Matrix blocks B0–B14 have not
run at all.** The one known open defect is an **intermittent WATUT world-load crash**.
Immediate next actions: resolve/work around WATUT → confirm uncapped frametimes →
finish S4 → run B0→B14 → write winners into `build.py` → ship → report to owner.
## Reading order
| File | Contents |
| --- | --- |
| [`01-pack-and-environment.md`](01-pack-and-environment.md) | What the pack is, repo conventions, this machine's inventory, tool paths |
| [`02-timeline.md`](02-timeline.md) | How the effort got here: phases, commits, decisions |
| [`03-current-state.md`](03-current-state.md) | Exact current step, done/open/blocked, interrupted-session artifacts and evidence |
| [`04-plan.md`](04-plan.md) | The remaining plan with per-step status, B0–B14 run sheet summary, ship procedure |
| [`05-decisions-and-rules.md`](05-decisions-and-rules.md) | Settled owner decisions, rules of the road, traps and gotchas |
| [`06-artifact-map.md`](06-artifact-map.md) | Every artifact, where it lives, what it is, current status |
## Authoritative sources this capture distills
- `.pi/journal.md` — the running work log (read the last three sessions first).
- `analysis/client-perf/HANDOFF.md` — prior session's own handoff brief.
- `analysis/client-perf/BENCH-MATRIX.md` — **the run sheet**; when this capture and
the matrix disagree, the matrix wins.
- `analysis/client-perf/LEVERS.md` — ranked levers with verification notes.
- `phase0/P0|P1|P2-*.md` — per-fleet-machine measurement plans (machines unreachable).
## For the incoming agent
Start at `03-current-state.md`, then `04-plan.md`. Do the next actions in
`04-plan.md` §"Next actions" **in order**. Before touching anything, read
`05-decisions-and-rules.md` — several rules exist because a previous session already
made the mistake once (config re-save commits, packwiz `side` resets, vsync-locked
data). Append your session to `.pi/journal.md` when you finish.
-13
View File
@@ -1,13 +0,0 @@
# Onion MC spawn tuning (analysis/client-perf/SPAWN-TUNING.md). Partial file: NeoForge
# fills every other key with its default on load. Halves only the common mobs that are
# heavy by weight x hard-coded group size; rare mobs keep their defaults.
[spawning]
gazelleSpawnWeight = 20
capuchinMonkeySpawnWeight = 14
elephantSpawnWeight = 15
straddlerSpawnWeight = 35
rockyRollerSpawnWeight = 30
underminerSpawnWeight = 25
blobfishSpawnWeight = 15
mudskipperSpawnWeight = 14
caimanSpawnWeight = 15
-51
View File
@@ -1,51 +0,0 @@
// Onion MC spawn tuning (analysis/client-perf/SPAWN-TUNING.md). Partial file: YACL keeps
// defaults for missing keys. Each listed rule list REPLACES that mob's defaults, so every
// biome rule of a changed mob is listed. configVersion 2 skips the v1->v2 migration.
{
"configVersion": 2,
"spawning": {
"leafInsect": [
{"biomeSpec": "#c:is_jungle", "weight": 8, "min": 1, "max": 1},
{"biomeSpec": "#c:is_forest", "weight": 4, "min": 1, "max": 1}
],
"seaBunny": [
{"biomeSpec": "minecraft:ocean", "weight": 10, "min": 1, "max": 2},
{"biomeSpec": "minecraft:deep_ocean", "weight": 10, "min": 1, "max": 2},
{"biomeSpec": "minecraft:warm_ocean", "weight": 20, "min": 1, "max": 3},
{"biomeSpec": "minecraft:lukewarm_ocean", "weight": 10, "min": 1, "max": 3},
{"biomeSpec": "minecraft:deep_lukewarm_ocean", "weight": 10, "min": 1, "max": 3}
],
"dragonfly": [
{"biomeSpec": "#c:is_river", "weight": 4, "min": 1, "max": 1},
{"biomeSpec": "#c:is_swamp", "weight": 5, "min": 1, "max": 1},
{"biomeSpec": "#c:is_lush", "weight": 6, "min": 1, "max": 1}
],
"ladybug": [
{"biomeSpec": "#c:is_forest", "weight": 4, "min": 1, "max": 3},
{"biomeSpec": "#c:is_lush", "weight": 6, "min": 1, "max": 2},
{"biomeSpec": "#c:is_floral", "weight": 7, "min": 1, "max": 2}
],
"stagBeetle": [
{"biomeSpec": "#c:is_forest", "weight": 3, "min": 1, "max": 2},
{"biomeSpec": "#c:is_lush", "weight": 6, "min": 1, "max": 2}
],
"rolyPoly": [
{"biomeSpec": "#c:is_forest", "weight": 3, "min": 1, "max": 3},
{"biomeSpec": "#c:is_lush", "weight": 6, "min": 1, "max": 2},
{"biomeSpec": "#c:is_swamp", "weight": 6, "min": 1, "max": 2}
],
"snail": [
{"biomeSpec": "#c:is_forest", "weight": 3, "min": 1, "max": 2},
{"biomeSpec": "#c:is_lush", "weight": 5, "min": 1, "max": 2},
{"biomeSpec": "#c:is_swamp", "weight": 6, "min": 1, "max": 2}
],
"stickBug": [
{"biomeSpec": "#c:is_forest", "weight": 3, "min": 1, "max": 2},
{"biomeSpec": "#c:is_lush", "weight": 6, "min": 1, "max": 2}
],
"weevil": [
{"biomeSpec": "#c:is_forest", "weight": 3, "min": 1, "max": 3},
{"biomeSpec": "#c:is_lush", "weight": 6, "min": 1, "max": 2}
]
}
}
-13
View File
@@ -1,13 +0,0 @@
// Onion MC spawn tuning (analysis/client-perf/SPAWN-TUNING.md). Partial file: YACL keeps
// defaults for every missing key and re-saves. Targets the waterfowl/gull/sparrow flocks
// behind Fowl Play #214; songbirds and raptors keep their defaults.
{
"duckSpawnWeight": 2,
"duckMaxGroupSize": 4,
"gooseSpawnWeight": 2,
"gooseMaxGroupSize": 3,
"gullSpawnWeight": 3,
"gullMaxGroupSize": 5,
"sparrowSpawnWeight": 12,
"sparrowMaxGroupSize": 4
}
+1
View File
@@ -0,0 +1 @@
mixin.perf.dynamic_resources=true
@@ -0,0 +1,980 @@
_version = 4
[server]
#
# Defines the distance the player will receive updates around.
realTimeUpdateDistanceRadiusInChunks = 256
#
# Prefix of the level keys sent to the clients.
# If the mod is running behind a proxy, each backend should use a unique value.
# If this value is empty, level key will be based on the server's seed hash.
levelKeyPrefix = ""
#
# Defines the distance allowed to be synchronized around the player.
# Should be the same or larger than maxGenerationRequestDistance in most cases.
maxSyncOnLoadRequestDistance = 4096
#
# If true, clients will receive updated LODs when joining or loading new LODs.
synchronizeOnLoad = true
#
# Custom server key used which can be used to always reuse the same LOD data folder,
# for cases when the server doesn't have a static IP for some reason.
# If this value is empty, the client itself decides which folder name to use.
# Requires rejoining the server to apply after changing.
serverKey = ""
#
# Defines the distance allowed to generate around the player.
maxGenerationRequestDistance = 4096
#
# Makes the server send level keys for each world.
# Disable this if you use alternative ways to send level keys.
sendLevelKeys = true
#
# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING.
# Autogenerated ID used to prevent multiple independent servers from accidentally
# writing over each other's LODs when the same serverKey is set on both.
#
# How many LOD generation requests per second should a client send?
# Also limits the number of client requests allowed to stay in the server's queue.
generationRequestRateLimit = 20
#
# How many LOD sync requests per second should a client send?
# Also limits the amount of player's requests allowed to stay in the server's queue.
syncOnLoadRateLimit = 50
#
# If true, clients will receive real-time LOD updates for chunks outside the client's render distance.
enableRealTimeUpdates = true
#
# Maximum global speed for uploading LODs to the clients, in KB/s.
# Value of 0 disables the limit.
globalBandwidthLimit = 0
#
# Maximum per-player speed for uploading LODs to the clients, in KB/s.
# Value of 0 disables the limit.
playerBandwidthLimit = 500
#
# Enables adaptive transfer speed based on client performance.
# If true, DH will automatically adjust transfer rate to minimize connection lag.
# If false, transfer speed will remain fixed.
enableAdaptiveTransferSpeed = false
[common.lodBuilding]
#
# How should block data be compressed when creating LOD data?
# This setting will only affect new or updated LOD data,
# any data already generated when this setting is changed will be
# unaffected until it is modified or re-loaded.
#
# MERGE_SAME_BLOCKS
# Every block/biome change is recorded in the database.
# This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data.
# Expected Compression Ratio: 1.0
#
# VISUALLY_EQUAL
# Only visible block/biome changes are recorded in the database.
# Hidden blocks (IE ores) are ignored.
# Expected Compression Ratio: 0.7
worldCompression = "VISUALLY_EQUAL"
#
# Requires a level restart to apply.
#
# Setting this to a less strict mode increases DB read/write
# speed at the cost of potential data loss/corruption if
# the computer looses power
# (unexpected program shutdown should be safe for all options).
#
# FULL
# ACID - data will never be lost or corrupted
# Slow
# At max throughput: ~2k saves per sec, ~70% disk usage
#
# NORMAL
# May not be durable - some data may be lost on power loss, but DB should be safe from corruption
# Fast
# At max throughput: ~14k saves per sec, ~40% disk usage
#
# OFF
# Not consistent - database may be corrupted during power loss
# Very Fast
# At max throughput: ~16k saves per sec, ~6% disk usage
databaseSyncMode = "NORMAL"
dataCompression = "Z_STD_BLOCK"
#
# Enabling this will drastically speed up chunk processing time
# if you are CPU limited.
# Disabling it will drastically speed up chunk processing time
# if you are disk speed/IO limited.
#
# Normally DH will attempt to skip creating LODs for chunks it's already seen
# and that haven't changed.
#
# Disabling this option can also fix a rare bug where
# modified chunks are skipped due to being too similar to
# their saved hash.
disableUnchangedChunkCheck = false
[common.multiThreading]
#
# How many threads should be used by Distant Horizons?
numberOfThreads = 2
#
# A value between 1.0 and 0.0 that represents the percentage
# of time each thread can run before going idle.
#
# This can be used to reduce CPU usage if the thread count
# is already set to 1 for the given option, or more finely
# tune CPU performance.
threadRunTimeRatio = "1.0"
#
# What Java thread priority should DH's primary thread pools run with?
#
# You probably don't need to change this unless you are also
# running C2ME and are seeing thread starvation in either C2ME or DH.
threadPriority = 5
[common.logging]
#
# If enabled, the mod will log information about the renderer setup, cleanup, and any issues it may encounter.
# This can be useful for debugging.
logRendererEventToFile = "INFO"
#
# If enabled, the mod will log information about the world generation process.
# This can be useful for debugging.
logWorldGenEventToFile = "INFO"
#
# If enabled, the mod will log information about the renderer OpenGL process.
# This can be useful for debugging.
logRendererGLEventToChat = "ERROR"
#
#
globalChatMaxLevel = "ERROR"
#
# If enabled, the mod will log information about the world generation process.
# This can be useful for debugging.
logWorldGenChunkLoadEventToFile = "INFO"
#
# If enabled, the mod will log information about the renderer OpenGL process.
# This can be useful for debugging.
logRendererGLEventToFile = "INFO"
#
# If enabled, the mod will log information about network operations.
# This can be useful for debugging.
logNetworkEventToFile = "INFO"
#
# If enabled, config changes sent by the server will be logged.
logConnectionConfigChangesToFile = "WARN"
#
#
globalFileMaxLevel = "INFO"
[common.logging.warning]
#
# If enabled, a message will be displayed in chat if explicit garbage collection
# is disabled.
# This is known to cause out-of-memory issues
# and is better solved with a concurrent garbage collector.
showExplicitGcDisabledWarning = false
#
# If enabled, a chat message will be displayed when DH has too many chunks
# queued for updating.
showSlowWorldGenSettingWarnings = true
#
# If enabled, a chat message will be displayed when DH has too many chunks
# queued for updating.
showUpdateQueueOverloadedChatWarning = false
#
# If enabled, a message will be logged if explicit garbage collection
# is disabled.
# This is known to cause out-of-memory issues
# and is better solved with a concurrent garbage collector.
logExplicitGcDisabledWarning = false
#
# If enabled, a chat message will be displayed if Java doesn't have enough
# memory allocated to run DH well.
showLowMemoryWarningOnStartup = true
#
# If enabled, a chat message will be displayed when a replay is started
# giving some basic information about how DH will function.
showReplayWarningOnStartup = true
#
# If enabled, a chat message will be displayed when a potentially problematic
# mod is installed alongside DH.
showModCompatibilityWarningsOnStartup = true
#
# If enabled, a chat message will be displayed if vanilla MC's
# render distance is higher than the recommended amount.
showHighVanillaRenderDistanceWarning = true
#
# If enabled, a chat message will be displayed if DH detects
# that any pooled objects have been garbage collected.
showPoolInsufficientMemoryWarning = true
#
# If enabled, a message will be logged if the garbage
# collector Java is currently using is known
# to cause frame stuttering and/or other issues.
logGarbageCollectorWarning = true
#
# If enabled, a chat message will be displayed in chat if the garbage
# collector Java is currently using is known
# to cause frame stuttering and/or other issues.
showGarbageCollectorWarning = true
[common.worldGenerator]
#
# How should distant generator progress be displayed?
#
# OVERLAY: may be the same as CHAT for some Minecraft versions
# CHAT
# LOG
# DISABLED
showGenerationProgress = "DISABLED"
#
# If the SURFACE_THEN_CHUNKS generator plan is
# in use, this percentage can be defined to reduce how far the
# second high-quality generation pass will go
# as a percentage of your total render distance:
# -1.0 = Infinity
# 0.0 = 0%
# 0.5 = 50%
# 1.0 = 100%
# 2.0 = 200%
#
# Does nothing on a dedicated server.
surfaceRegenMaxDistancePercent = "-1.0"
#
# The max radius in chunks around the central point where world generation is allowed.
# If this value is set to 0, generation bounds are disabled and the render distance will be used.
#
# This should only be set if you have a pre-generated world that has a very limited size.
# Setting this on a normal MC world will prevent the world generator from filling
# out your render distance.
generationMaxChunkRadius = 0
#
# If the Minecraft server thread is taking too long
# Distant Horizons will pause world gen to reduce server stuttering/lag.
limitIfServerUnhealthy = true
#
# How often should the distant generator progress be displayed?
generationProgressDisplayIntervalInSeconds = 2
#
# When logging generation progress also include the rate at which chunks
# are being generated.
# This can be useful for troubleshooting performance.
generationProgressIncludeChunksPerSecond = true
#
# How detailed should LODs be generated outside the vanilla render distance?
#
# PRE_EXISTING_ONLY
# Only create LOD data for already generated chunks.
#
# FEATURES
# Generate everything except structures.
# Note: This may cause world generator bugs or instability when paired with certain world generator mods.
#
# INTERNAL_SERVER
# Ask the local server to generate/load each chunk.
# This is the most compatible and will generate structures correctly,
# but may cause server/simulation lag.
# Note: unlike other modes this option DOES save generated chunks to
# Minecraft's region files.
chunkGeneratorMode = "FEATURES"
#
# Defines how LODs will be generated
# outside the vanilla render distance.
# In multiplayer, this may be overridden by the server.
#
# SURFACE_THEN_CHUNKS
# The rough surface will be generated first
# then chunks will be generated to fill in
# missing features (ie trees and villages).
#
# Recommended for modded or vanilla worlds.
#
# SURFACE_ONLY
# Only the rough surface will be generated.
# Chunk features like trees and villages
# won't be generated.
#
# Recommended for computers that don't have
# enough CPU power or RAM space to handle
# the chunk generator.
#
# CHUNKS_ONLY
# Slower than either surface options,
# but provides more accurate terrain.
#
# Recommended for custom worlds
# where the surface generator would
# generate incorrect terrain.
#
# DISABLED
# No distant terrain will be generated.
generatorPlan = "SURFACE_THEN_CHUNKS"
#
# If true and the player/camera is moving quickly
# Distant Horizons will pause world gen to improve loading speed
# and reduce CPU load.
pauseIfMovingQuickly = true
#
# For how many seconds should instructions for disabling the distant generator progress be displayed?
# Setting this to 0 hides the instructional message so the world gen progress is shown immediately when it starts.
generationProgressDisableMessageDisplayTimeInSeconds = 20
#
# The center Z chunk position that the world gen max radius is centered around.
generationCenterChunkZ = 0
#
# The center X chunk position that the world gen max radius is centered around.
generationCenterChunkX = 0
[client]
#
# Should Distant Horizon's config button appear in Minecraft's options screen next to the fov slider?
showDhOptionsButtonInMinecraftUi = true
[client.advanced.autoUpdater]
#
# If DH should use the nightly (provided by Gitlab), or stable (provided by Modrinth) build.
# If [AUTO] is selected DH will update to new stable releases if the current jar is a stable jar
# and will update to new nightly builds if the current jar is a nightly jar (IE the version number ends in '-dev').
updateBranch = "AUTO"
#
# Automatically check for updates on game launch?
enableAutoUpdater = true
#
# Should Distant Horizons silently, automatically download and install new versions?
# This setting is force disabled on dedicated servers for stability reasons.
enableSilentUpdates = false
[client.advanced.debugging]
#
# If enabled this will disable (most) vanilla Minecraft rendering.
#
# NOTE: Do not report any issues when this mode is on!
# This setting is only for fun and debugging.
# Mod compatibility is not guaranteed.
lodOnlyMode = false
#
# What renderer is active?
#
# DEFAULT: Default lod renderer
# DEBUG_TRIANGLE: Debug testing renderer
# DISABLED: Disable rendering
rendererMode = "DEFAULT"
#
# If true overlapping quads will be rendered as bright red for easy identification.
# If false the quads will be rendered normally.
showOverlappingQuadErrors = false
#
# If true OpenGL Buffer garbage collection will be logged
# this also includes the number of live buffers.
logBufferGarbageCollection = false
#
# Should specialized colors/rendering modes be used?
#
# OFF: LODs will be drawn with their normal colors.
# SHOW_DETAIL: LODs' color will be based on their detail level.
# SHOW_BLOCK_MATERIAL: LODs' color will be based on their material.
# SHOW_OVERLAPPING_QUADS: LODs will be drawn with total white, but overlapping quads will be drawn with red.
debugRenderingColors = "OFF"
#
# Stops vertex colors from being passed.
# Useful for debugging shaders
enableWhiteWorld = false
#
# If enabled the LODs will render as wireframe.
renderWireframe = false
#
# If true several keys can be used to toggle debug states.
# F6 - enable/disable LOD rendering
# F7 - enable/disable LOD only rendering
# F8 - cycle through the different debug rendering modes
enableDebugKeybindings = false
[client.advanced.debugging.debugWireframe]
#
# Render LOD section status?
showRenderSectionStatus = false
#
# Render queued network sync on load tasks?
showNetworkSyncOnLoadQueue = false
#
# Render full data update/lock status?
showFullDataUpdateStatus = false
#
# Render queued world gen tasks?
showWorldGenQueue = false
#
# Render Quad Tree Rendering status?
showQuadTreeRenderStatus = false
#
# If enabled, various wireframes for debugging internal functions will be drawn.
#
# NOTE: There WILL be performance hit!
# Additionally, only stuff that's loaded after you enable this
# will render their debug wireframes.
enableRendering = false
[client.advanced.debugging.openGl]
#
# Defines how OpenGL errors are handled.
# Requires rebooting Minecraft to change.
# Will catch OpenGL errors thrown by other mods.
overrideVanillaGLLogger = true
#
# Defines how OpenGL errors are handled.
# May incorrectly catch OpenGL errors thrown by other mods.
#
# IGNORE: Do nothing.
# LOG: write an error to the log.
# LOG_THROW: write to the log and throw an exception.
# Warning: this should only be enabled when debugging the LOD renderer
# as it may break Minecraft's renderer when an exception is thrown.
glErrorHandlingMode = "IGNORE"
#
# If true each Open GL error will only be logged once.
# Enabling this may cause some error logs to be missed.
# Does nothing if overrideVanillaGLLogger is set to false.
#
# Generally this can be kept as 'true' to prevent log spam.
# However, Please set this to 'false' if a developer needs your log to debug a GL issue.
onlyLogGlErrorsOnce = true
[client.advanced.debugging.exampleConfigScreen]
shortTest = "69"
mapTest = "{}"
byteTest = "8"
longTest = "42069"
listTest = ["option 1", "option 2", "option 3"]
boolTest = false
doubleTest = "420.69"
floatTest = "0.42069"
linkableTest = 420
intTest = 69420
stringTest = "Test input box"
[client.advanced.debugging.f3Screen]
#
# Shows info about the render thread tasks.
showRenderThreadTasks = false
#
# Shows how many chunks are queued for processing and the max count that can be queued.
showQueuedChunkUpdateCount = true
#
# Shows the memory use and array counts for each DH object pool.
showSeparatedObjectPools = false
#
# Shows the player's LOD position.
showPlayerPos = true
#
# Shows the combined memory use and array counts for all DH pooled objects.
showCombinedObjectPools = false
#
# Defines what internal detail level the player position will be shown as.
# Internal detail level means: 6 = 1x1 block, 7 = 2x2 blocks, etc.
playerPosSectionDetailLevel = 6
#
# Only show levels that DH is actively rendering.
onlyShowRenderingLevels = true
#
# Shows info about each thread pool.
showThreadPools = true
#
# Shows what levels are loaded and world gen/rendering info about those levels.
showLevelStatus = true
[client.advanced.debugging.positionFinderDebugging]
positionFinderEnable = false
positionFinderMinBlockY = -64
positionFinderZPos = 0
positionFinderXPos = 0
positionFinderMaxBlockY = 125
positionFinderDetailLevel = 6
positionFinderMarginPercent = "0.0"
[client.advanced.graphics]
#
# Enabling Anti-Aliasing smooths edges and reduces flickering when moving.
enableAntiAliasing = true
#
# Enable Screen Space Ambient Occlusion
enableSsao = false
#
# If true some vanilla graphics settings will be automatically changed
# during DH setup to provide a better experience.
#
# IE disabling vanilla clouds (which render on top of DH LODs),
# and chunk fading (DH already fades MC chunks)
overrideVanillaGraphicsSettings = true
[client.advanced.graphics.culling]
#
# Determines how far from the camera Distant Horizons will start rendering.
# Measured as a percentage of the vanilla render distance.
#
# -1 = auto, overdraw will change based on the vanilla render distance.
#
# Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,
# but may cause holes in the world.
# Holes are most likely to appear when flying through unloaded terrain.
#
# Increasing the vanilla render distance increases the effectiveness of this setting.
overdrawPrevention = "-1.0"
#
# Identical to the other frustum culling option
# only used when a shader mod is present using the DH API
# and the shadow pass is being rendered.
#
# Disable this if shadows render incorrectly.
disableShadowPassFrustumCulling = false
#
# A comma separated list of block resource locations that will be replaced by water
# if they're visible on the water's surface.
waterSubSurfaceBlockReplacementCsv = "minecraft:kelp,minecraft:tall_seagrass,minecraft:seagrass"
#
# A comma separated list of block resource locations that won't be rendered by DH.
# Air is always included in this list.
#
# Note:
# If you see gaps, or holes you may have to change
# worldCompression to [MERGE_SAME_BLOCKS] and re-generate the LODs.
ignoredRenderBlockCsv = "minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom"
#
# A comma separated list of block resource locations that will be removed
# when on top of water.
waterSurfaceBlockReplacementCsv = "minecraft:lily_pad"
#
# Defines what blocks should be rendered as LODs.
#
# NONE: Include all blocks in the LODs
# NON_COLLIDING: Only render solid blocks in the LODs (tall grass, torches, etc. will be ignored)
blocksToIgnore = "NON_COLLIDING"
#
# If true LODs outside the player's camera
# aren't drawn, increasing GPU performance.
#
# If false all LODs are drawn, even those behind
# the player's camera, decreasing GPU performance.
#
# Disable this if you see LODs disappearing at the corners of your vision.
disableFrustumCulling = false
#
# If false all beacons near the camera won't be drawn to prevent vanilla overdraw.
# If true all beacons will be rendered.
#
# Generally this should be left as true. It's main purpose is for debugging
# beacon updating/rendering.
disableBeaconDistanceCulling = true
#
# Should the blocks underneath avoided blocks gain the color of the avoided block?
#
# True: a red flower will tint the grass below it red.
# False: skipped blocks will not change color of surface below them.
tintWithAvoidedBlocks = true
#
# If enabled caves won't be rendered.
#
# Note: for some world types this can cause
# overhangs or walls for floating objects.
# Tweaking the caveCullingHeight, can resolve some
# of those issues.
enableCaveCulling = true
#
# At what Y value should cave culling start?
# Lower this value if you get walls for areas with 0 light.
caveCullingHeight = 60
#
# A comma separated list of block resource locations that shouldn't be rendered
# if they are in a 0 sky light underground area.
# Air is always included in this list.
#
# Defaults to an empty list since most cave blocks will be automatically ignored due to being:
# transparent, non-solid, or liquids, but new blocks can be added here if needed.
ignoredRenderCaveBlockCsv = ""
#
# If set to true the overdraw prevention radius will get closer
# to the camera when flying/moving quickly.
#
# This helps reduce issues where Minecraft can't load or
# generate chunks fast enough to keep up with DH.
reduceOverdrawWithFastMovement = true
[client.advanced.graphics.texture]
#
# A comma separated list of block resource locations
# that DH will render their sides using the bottom texture.
# Partial matches/incomplete resource locations will also match.
#
# Example: "minecraft:grass_block,nylium"
#
# Changes require a restart.
blocksDontUseSideTextureCsv = "grass_block,mycelium,nylium,dirt_path,grass"
#
# The highest detail level number that can render with block textures.
# At higher detail levels each LOD covers multiple blocks,
# which can essentially make textures smaller than a pixel and therefore invisible.
#
# Larger numbers texture more distant LODs
# but increase memory usage.
maxTexturedLodDetailLevel = 2
#
# If true nearby and zoomed-in LODs will render with their block's
# actual texture instead of a single flat color.
#
# Only applies to high detail LODs where the texture is actually visible,
# distant LODs always use flat colors.
# Some shader packs nay not have an affect.
enableTexturedLods = true
#
# A comma separated list of block resource locations
# that DH won't render textures on.
# Partial matches/incomplete resource locations will also match.
#
# Example: "minecraft:grass_block,nylium"
#
# Changes require a restart.
blocksDontRenderTextureCsv = "minecraft:bamboo"
#
# A comma separated list of tag names
# that DH will render their sides using the bottom texture.
#
# Example: "grass_blocks,nylium"
#
# Changes require a restart.
blockTagsDontUseSideTextureCsv = "grass_blocks,nylium"
#
# A comma separated list of block resource locations
# that DH will use rasterization to determine
# the face textures.
#
# Can be used to fix issues with mods/blocks where
# a side uses just part of a modeled block's texture.
#
# Example: "minecraft:beacon"
#
# Changes require a restart.
blocksAlwaysRasterizeTextureCsv = "minecraft:beacon"
[client.advanced.graphics.noiseTexture]
#
# Should a noise texture be applied to LODs?
#
# This is done to simulate textures and make the LODs appear more detailed.
enableNoiseTexture = true
#
# Defines how far should the noise texture render before it fades away. (in blocks)
# Set to 0 to disable noise from fading away
noiseDropoff = 1024
#
# How many steps of noise should be applied to LODs?
noiseSteps = 4
#
# How intense should the noise should be?
noiseIntensity = "0.05"
[client.advanced.graphics.experimental]
#
# A comma separated list of dimension resource locations where DH won't render.
#
# Example: "minecraft:the_nether,minecraft:the_end"
# For Minecraft 1.12.2 and older:
# Open the F3 screen and copy the dimension_name:ID shown in yellow. Example: "overworld:0"
#
# Note:
# Some DH settings will be disabled and/or changed to improve
# visuals when DH rendering is disabled.
ignoredDimensionCsv = ""
#
# Requires a restart to change.
#
# Options:
# AUTO - changes based on the most likely API for that MC version
# OPEN_GL - The Default for MC 1.21.11 and older (supports Iris shaders)
# BLAZE_3D - The Default for MC 26.1.2 and newer (supports Vulkan)
renderingEngine = "AUTO"
#
# This is the earth size ratio when applying the curvature shader effect.
# Note: Enabling this feature may cause rendering bugs.
#
# 0 = flat/disabled
# 1 = 1 to 1 (6,371,000 blocks)
# 100 = 1 to 100 (63,710 blocks)
# 10000 = 1 to 10000 (637.1 blocks)
#
# Note: Due to current limitations, the min value is [50]
# and the max value is 5000. Any values outside this range
# will be set to 0 (disabled).
earthCurveRatio = 0
[client.advanced.graphics.genericRendering]
#
# A comma separated separated list of dimension resource locations where DH clouds will render.
#
# Example: "minecraft:overworld,minecraft:the_end"
#
# Changes require a world re-load.
dimensionEnabledCloudRenderingCsv = "minecraft:overworld"
#
# If true LOD clouds will be rendered.
enableCloudRendering = true
#
# If true LOD beacon beams will be rendered wider at extreme distances,
# making them easier to see.
# If false all LOD beacon beams will only ever be 1 block wide.
expandDistantBeacons = true
#
# Sets the maximum height beacons will render up to.
#
# Requires a world re-load to take affect.
beaconRenderHeight = 6000
#
# If true LOD beacon beams will be rendered.
enableBeaconRendering = true
#
# If true non terrain objects will be rendered by DH.
# i.e. beacon beams and clouds.
enableGenericRendering = true
#
# False = DH will render a single layer of clouds, like vanilla Minecraft.
# True = DH will render 3 layers of clouds at different heights.
enableMultiLayerClouds = true
[client.advanced.graphics.quality]
#
# What is the maximum detail LODs can render at?
# Higher settings will increase memory and GPU usage.
#
# CHUNK: render 1 LOD for each Chunk.
# HALF_CHUNK: render 4 LODs for each Chunk.
# FOUR_BLOCKS: render 16 LODs for each Chunk.
# TWO_BLOCKS: render 64 LODs for each Chunk.
# BLOCK: render 256 LODs for each Chunk (width of one block).
#
# Fastest: CHUNK
# Fanciest: BLOCK
maxHorizontalResolution = "BLOCK"
#
# If true LODs will fade away as you get closer to them.
# If false LODs will cut off abruptly at a set distance from the camera.
# This setting is affected by the vanilla overdraw prevention config.
ditherDhFade = true
#
# If true DH will try to use the camera position when
# determining LOD quality drop-off.
# If false DH will use the player's position.
#
# Enabling helps free-cam mods render correctly.
# Disabling helps multi-camera mods render correctly (ie Immersive Portals or camera mods).
useCameraPositionForQualityDropOff = true
#
# How bright LOD colors are.
#
# 0 = black
# 1 = normal
# 2 = near white
brightnessMultiplier = "1.0"
#
# How many detail levels zooming in can increase LOD quality by.
# Higher numbers allow stronger zooms to render crisper terrain,
# but will increase memory and GPU usage while zoomed in.
#
# A vanilla spyglass needs 4 detail levels to reach its full quality.
maxZoomQualityIncrease = 4
#
# How should LODs be shaded?
#
# AUTO: Uses the same side shading as vanilla Minecraft blocks.
# ENABLED: Simulates Minecraft's block shading for LODs.
# Can be used to force LOD shading when using some shaders.
# DISABLED: All LOD sides will be rendered with the same brightness.
lodShading = "AUTO"
#
# How saturated LOD colors are.
#
# 0 = black and white
# 1 = normal
# 2 = very saturated
saturationMultiplier = "1.0"
#
# This indicates how well LODs will represent
# overhangs, caves, floating islands, etc.
# Higher options will make the world more accurate, butwill increase memory and GPU usage.
#
# Lowest Quality: HEIGHT_MAP
# Highest Quality: PIXEL_ART
verticalQuality = "LOW"
#
# The radius of the mod's render distance. (measured in chunks)
#
# Note: this is a best effort number.
# The actual render distance may be above or below this number
# depending on your other graphic settings.
lodChunkRenderDistanceRadius = 64
#
# How should the sides and bottom of grass block LODs render?
#
# AS_GRASS: all sides of dirt LOD's render using the top (green) color.
# FADE_TO_DIRT: sides fade from grass to dirt.
# AS_DIRT: sides render entirely as dirt.
grassSideRendering = "FADE_TO_DIRT"
#
# This indicates how far apart drops in LOD quality are.
#
# Higher settings will increase the distance between drops
# but will increase memory and GPU usage.
horizontalQuality = "LOW"
#
# How should LOD transparency be handled.
#
# COMPLETE: LODs will render transparent.
# DISABLED: LODs will be opaque.
transparency = "DISABLED"
#
# This is the same as vanilla Biome Blending settings for Lod area.
# Note: anything above '0' will slow down LOD loading time.
#
# '0' equals to Vanilla Biome Blending of '1x1' or 'OFF',
# '1' equals to Vanilla Biome Blending of '3x3',
# '2' equals to Vanilla Biome Blending of '5x5'...
lodBiomeBlending = 1
#
# How should vanilla Minecraft fade into Distant Horizons LODs?
#
# NONE: Fastest, there will be a pronounced border between DH and MC rendering.
# SINGLE_PASS: Fades after MC's transparent pass, opaque blocks underwater won't be faded.
# DOUBLE_PASS: Slowest, fades after both MC's opaque and transparent passes, provides the smoothest transition.
vanillaFadeMode = "SINGLE_PASS"
#
# If true LOD quality will increase when the camera is zoomed in,
# IE when using a spyglass or zoom mod.
#
# Only LODs visible through the camera view are affected.
#
# When zoomed in LODs will load to the same detail level
# they would have if you were close to them.
increaseQualityWhenZoomedIn = true
[client.advanced.graphics.fog]
#
# Should Minecraft's fog render?
# Note: Other mods may conflict with this setting.
enableVanillaFog = false
#
# What is the maximum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
farFogMax = "1.0"
#
# Determines if fog is drawn on DH LODs.
enableDhFog = true
#
# At what distance should the far fog start?
#
# 0.0: Fog starts at the player's position.
# 1.0: Fog starts at the closest edge of the vanilla render distance.
# 1.414: Fog starts at the corner of the vanilla render distance.
farFogStart = "0.4"
#
# What is the minimum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
farFogMin = "0.0"
#
# What color should fog use?
#
# USE_WORLD_FOG_COLOR: Use the world's fog color.
# USE_SKY_COLOR: Use the sky's color.
colorMode = "USE_WORLD_FOG_COLOR"
#
# How should the fog thickness should be calculated?
#
# LINEAR: Linear based on distance (will ignore 'density')
# EXPONENTIAL: 1/(e^(distance*density))
# EXPONENTIAL_SQUARED: 1/(e^((distance*density)^2)
farFogFalloff = "EXPONENTIAL_SQUARED"
#
# Used in conjunction with the Fog Falloff.
farFogDensity = "2.5"
#
# Where should the far fog end?
#
# 0.0: Fog ends at player's position.
# 1.0: Fog ends at the closest edge of the vanilla render distance.
# 1.414: Fog ends at the corner of the vanilla render distance.
farFogEnd = "1.0"
[client.advanced.graphics.fog.heightFog]
#
# Where should the height fog start?
#
# ABOVE_CAMERA: Height fog starts at the camera and goes towards the sky
# BELOW_CAMERA: Height fog starts at the camera and goes towards the void
# ABOVE_AND_BELOW_CAMERA: Height fog starts from the camera to goes towards both the sky and void
# ABOVE_SET_HEIGHT: Height fog starts from a set height and goes towards the sky
# BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards the void
# ABOVE_AND_BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards both the sky and void
heightFogDirection = "BELOW_SET_HEIGHT"
#
# What is the minimum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
heightFogMin = "0.0"
#
# If the height fog is calculated around a set height, what is that height position?
heightFogBaseHeight = "80.0"
#
# What is the maximum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
heightFogMax = "1.0"
#
# How should the height fog thickness should be calculated?
#
# LINEAR: Linear based on height (will ignore 'density')
# EXPONENTIAL: 1/(e^(height*density))
# EXPONENTIAL_SQUARED: 1/(e^((height*density)^2)
heightFogFalloff = "EXPONENTIAL_SQUARED"
#
# What is the height fog's density?
heightFogDensity = "20.0"
#
# How should height effect the fog thickness?
# Note: height fog is combined with the other fog settings.
#
# SPHERICAL: Fog is calculated based on camera distance.
# CYLINDRICAL: Ignore height, fog is calculated based on horizontal distance.
#
# MAX: max(heightFog, farFog)
# ADDITION: heightFog + farFog
# MULTIPLY: heightFog * farFog
# INVERSE_MULTIPLY: 1 - (1-heightFog) * (1-farFog)
# LIMITED_ADDITION: farFog + max(farFog, heightFog)
# MULTIPLY_ADDITION: farFog + farFog * heightFog
# INVERSE_MULTIPLY_ADDITION: farFog + 1 - (1-heightFog) * (1-farFog)
# AVERAGE: farFog*0.5 + heightFog*0.5
heightFogMixMode = "SPHERICAL"
#
# Should the start of the height fog be offset?
#
# 0.0: Fog start with no offset.
# 1.0: Fog start with offset of the entire world's height. (Includes depth)
heightFogStart = "0.0"
#
# Should the end of the height fog be offset?
#
# 0.0: Fog end with no offset.
# 1.0: Fog end with offset of the entire world's height. (Include depth)
heightFogEnd = "0.6"
[client.advanced.multiplayer]
#
# How should multiplayer save folders should be named?
#
# NAME_ONLY: Example: "Minecraft Server"
# IP_ONLY: Example: "192.168.1.40"
# NAME_IP: Example: "Minecraft Server IP 192.168.1.40"
# NAME_IP_PORT: Example: "Minecraft Server IP 192.168.1.40:25565"NAME_IP_PORT_MC_VERSION: Example: "Minecraft Server IP 192.168.1.40:25565 GameVersion 1.16.5"
serverFolderNameMode = "NAME_ONLY"
@@ -0,0 +1,36 @@
{
"allowedCEM": "ALL",
"logModelCreationData": false,
"debugOnRightClick": false,
"renderModeChoice": "NORMAL",
"vanillaModelHologramRenderMode_2": "OFF",
"modelExportMode": "NONE",
"automaticModelExporting": false,
"attemptPhysicsModPatch_2": "CUSTOM",
"modelUpdateFrequency": "Slow",
"entityRenderModeOverrides": {},
"entityPhysicsModPatchOverrides": {},
"entityVanillaHologramOverrides": {},
"modelsNamesDisabled": [],
"allowEBEModConfigModify": true,
"animationLODDistance": 8,
"retainDetailOnLowFps": false,
"retainDetailOnLargerMobs": true,
"animationFrameSkipDuringIrisShadowPass": true,
"preventFirstPersonHandAnimating": false,
"onlyClientPlayerModel": false,
"doubleChestAnimFix": true,
"enforceOptifineVariationRequiresDefaultModel": false,
"enforceOptifineVariationRequiresDefaultModel_v2": false,
"resetPlayerModelEachRender": true,
"resetPlayerModelEachRender_v2": true,
"onlyDebugRenderOnHover": false,
"enforceOptifineSubFoldersVariantOnly": false,
"enforceOptiFineAnimSyntaxLimits": true,
"allowOptifineFallbackProperties": true,
"showReloadErrorToast": true,
"exportRotations": false,
"asmMaths": true,
"logASM": false,
"parrotShoulderPositionAnimatesByDefault": true
}
@@ -0,0 +1,14 @@
#Select the backend to use. Set to "DEFAULT" to let Flywheel decide.
backend = "DEFAULT"
#Enable or disable instance update limiting with distance.
limitUpdates = true
#The number of worker threads to use. Set to -1 to let Flywheel decide. Set to 0 to disable parallelism. Requires a game restart to take effect.
# Default: -1
# Range: -1 ~ 4
workerThreads = -1
#Config options for Flywheel's built-in backends.
[flw_backends]
#How smooth Flywheel's shader-based lighting should be. May have a large performance impact.
#Allowed Values: FLAT, TRI_LINEAR, SMOOTH, SMOOTH_INNER_FACE_CORRECTED
lightSmoothness = "FLAT"
@@ -0,0 +1,16 @@
mode = "fastest"
chunk_rebuild_scheduler = "culling"
[adaptive_ticking]
slow = 5
slower = 8
background_sleep = true
[light_sources]
entities = false
self = true
water_sensitive_check = true
beam = true
guardian_laser = true
sonic_boom = true
glowing_effect = true
@@ -0,0 +1,30 @@
{
"quality": {
"weather_quality": "FAST",
"leaves_quality": "FAST",
"enable_vignette": true,
"use_closest_point_entity_sort": false
},
"advanced": {
"enable_memory_tracing": false,
"use_advanced_staging_buffers": true,
"cpu_render_ahead_limit": 3
},
"performance": {
"chunk_builder_threads": 0,
"chunk_build_defer_mode": "ALWAYS",
"animate_only_visible_textures": true,
"use_entity_culling": true,
"use_fog_occlusion": true,
"use_block_face_culling": true,
"use_no_error_g_l_context": true,
"quad_splitting_mode": "SAFE"
},
"notifications": {
"has_cleared_donation_button": false,
"has_seen_donation_prompt": false
},
"debug": {
"terrain_sorting_enabled": true
}
}
@@ -0,0 +1,20 @@
# Partial file: SPR merges these over its generated per-sound defaults (-1 = unlimited).
create\:cogs=4
create\:mixing=4
create\:mixing_compounded_1=4
create\:mechanical_press_activation=4
create\:mechanical_press_activation_compounded_1=4
create\:mechanical_press_activation_belt=4
create\:mechanical_press_activation_belt_compounded_1=4
create\:crushing_1=4
create\:crushing_2=4
create\:crushing_3=4
create\:saw_activate_stone=4
create\:saw_activate_wood=4
create\:funnel_flap=4
create\:funnel_flap_compounded_1=4
create\:depot_slide=4
create\:depot_plop=4
create\:steam=4
create\:sanding_short=4
create\:sanding_long=4
@@ -0,0 +1,99 @@
# Enables/Disables all sound effects
enabled=true
# Affects how quiet a sound gets based on distance
# Lower values mean distant sounds are louder
# This setting requires you to be in singleplayer or having the mod installed on the server
# 1.0 is the physically correct value
attenuation_factor=1.0
# The ray distance at which reverb starts
# 0.0 disables reverb attenuation
reverb_attenuation_distance=0.0
# The volume of simulated reverberations
reverb_gain=1.0
# The brightness of reverberation
# Higher values result in more high frequencies in reverberation
# Lower values give a more muffled sound to the reverb
reverb_brightness=1.0
# The distance of reverb relative to the sound distance
reverb_distance=1.5
# The amount of sound that will be absorbed when traveling through blocks
block_absorption=1.0
# Higher values mean smaller objects won't be considered as occluding
occlusion_variation=0.35
# The default amount of sound reflectance energy for all blocks
# Lower values result in more conservative reverb simulation with shorter reverb tails
# Higher values result in more generous reverb simulation with higher reverb tails
default_block_reflectivity=0.5
# The default amount of occlusion for all blocks
# Lower values will result in sounds being less muffled through walls
# Higher values mean sounds will be not audible though thicker walls
default_block_occlusion_factor=1.0
# Minecraft won't allow sounds to play past a certain distance
# This parameter is a multiplier for how far away a sound source is allowed to be in order for it to actually play
# This setting only takes affect in singleplayer worlds and when installed on the server
sound_distance_allowance=4.0
# A value controlling the amount that air absorbs high frequencies with distance
# A value of 1.0 is physically correct for air with normal humidity and temperature
# Higher values mean air will absorb more high frequencies with distance
# 0 disables this effect
air_absorption=1.0
# How much sound is filtered when the player is underwater
# 0.0 means no filter
# 1.0 means fully filtered
underwater_filter=0.9
# Whether sounds like cave, nether or underwater ambient sounds should have sound physics
evaluate_ambient_sounds=false
# The number of rays to trace to determine reverberation for each sound source
# More rays provides more consistent tracing results but takes more time to calculate
# Decrease this value if you experience lag spikes when sounds play
environment_evaluation_ray_count=16
# The number of rays bounces to trace to determine reverberation for each sound source
# More bounces provides more echo and sound ducting but takes more time to calculate
# Decrease this value if you experience lag spikes when sounds play
environment_evaluation_ray_bounces=2
# If sound hits a non-full-square side, block occlusion is multiplied by this
non_full_block_occlusion_factor=0.25
# The maximum amount of rays to determine occlusion
# Directly correlates to the amount of blocks between walls that are considered
max_occlusion_rays=16
# The amount at which occlusion is capped
max_occlusion=64.0
# If enabled, the occlusion calculation only uses one path between the sound source and the listener instead of 9
strict_occlusion=true
# Whether to try calculating where the sound should come from based on reflections
sound_direction_evaluation=true
# Skip redirecting non-occluded sounds (the ones you can see directly)
redirect_non_occluded_sounds=true
# If music discs or other longer sounds should be frequently reevaluated
update_moving_sounds=false
# The interval in ticks that moving sounds are reevaluated
# Lower values mean more frequent reevaluation but also more lag
# This option only takes effect if update_moving_sounds is enabled
sound_update_interval=5
# The maximum distance a sound can be processed
max_sound_processing_distance=512.0
# Disable level clone and cache. This will fall back to original main thread access.
# WARNING! Enabling this will cause instability and issues with other mods.
unsafe_level_access=false
# The radius of chunks to clone for level access
level_clone_range=4
# The maximum number of ticks to retain the cloned level in the cache
level_clone_max_retain_ticks=20
# The maximum distance a player can move from the cloned origin before invalidation
level_clone_max_retain_block_distance=16
# Enables debug logging
debug_logging=false
# Provides more information about occlusion in the logs
occlusion_logging=false
# Provides more information about the environment calculation in the logs
environment_logging=false
# Provides more information about how long computations take
performance_logging=false
# If enabled, the path of the sound will be rendered in game
render_sound_bounces=false
# If enabled, occlusion will be visualized in game
render_occlusion=false
# Enables/Disables sound effects for Simple Voice Chat audio
simple_voice_chat_integration=true
# Enables/Disables hearing your own echo with Simple Voice Chat
simple_voice_chat_hear_self=false
@@ -0,0 +1,12 @@
#The render distance for physics objects (SubLevels) in chunks.
# Default: 128
# Range: 1 ~ 1000000
physicsRenderDistance = 16
#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
+11
View File
@@ -0,0 +1,11 @@
version:3955
renderDistance:8
simulationDistance:8
biomeBlendRadius:0
mipmapLevels:2
entityDistanceScaling:0.75
particles:1
entityShadows:false
graphicsMode:1
maxFps:120
enableVsync:true
@@ -0,0 +1,980 @@
_version = 4
[server]
#
# Defines the distance the player will receive updates around.
realTimeUpdateDistanceRadiusInChunks = 256
#
# Prefix of the level keys sent to the clients.
# If the mod is running behind a proxy, each backend should use a unique value.
# If this value is empty, level key will be based on the server's seed hash.
levelKeyPrefix = ""
#
# Defines the distance allowed to be synchronized around the player.
# Should be the same or larger than maxGenerationRequestDistance in most cases.
maxSyncOnLoadRequestDistance = 4096
#
# If true, clients will receive updated LODs when joining or loading new LODs.
synchronizeOnLoad = true
#
# Custom server key used which can be used to always reuse the same LOD data folder,
# for cases when the server doesn't have a static IP for some reason.
# If this value is empty, the client itself decides which folder name to use.
# Requires rejoining the server to apply after changing.
serverKey = ""
#
# Defines the distance allowed to generate around the player.
maxGenerationRequestDistance = 4096
#
# Makes the server send level keys for each world.
# Disable this if you use alternative ways to send level keys.
sendLevelKeys = true
#
# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING.
# Autogenerated ID used to prevent multiple independent servers from accidentally
# writing over each other's LODs when the same serverKey is set on both.
#
# How many LOD generation requests per second should a client send?
# Also limits the number of client requests allowed to stay in the server's queue.
generationRequestRateLimit = 20
#
# How many LOD sync requests per second should a client send?
# Also limits the amount of player's requests allowed to stay in the server's queue.
syncOnLoadRateLimit = 50
#
# If true, clients will receive real-time LOD updates for chunks outside the client's render distance.
enableRealTimeUpdates = true
#
# Maximum global speed for uploading LODs to the clients, in KB/s.
# Value of 0 disables the limit.
globalBandwidthLimit = 0
#
# Maximum per-player speed for uploading LODs to the clients, in KB/s.
# Value of 0 disables the limit.
playerBandwidthLimit = 500
#
# Enables adaptive transfer speed based on client performance.
# If true, DH will automatically adjust transfer rate to minimize connection lag.
# If false, transfer speed will remain fixed.
enableAdaptiveTransferSpeed = false
[common.lodBuilding]
#
# How should block data be compressed when creating LOD data?
# This setting will only affect new or updated LOD data,
# any data already generated when this setting is changed will be
# unaffected until it is modified or re-loaded.
#
# MERGE_SAME_BLOCKS
# Every block/biome change is recorded in the database.
# This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data.
# Expected Compression Ratio: 1.0
#
# VISUALLY_EQUAL
# Only visible block/biome changes are recorded in the database.
# Hidden blocks (IE ores) are ignored.
# Expected Compression Ratio: 0.7
worldCompression = "VISUALLY_EQUAL"
#
# Requires a level restart to apply.
#
# Setting this to a less strict mode increases DB read/write
# speed at the cost of potential data loss/corruption if
# the computer looses power
# (unexpected program shutdown should be safe for all options).
#
# FULL
# ACID - data will never be lost or corrupted
# Slow
# At max throughput: ~2k saves per sec, ~70% disk usage
#
# NORMAL
# May not be durable - some data may be lost on power loss, but DB should be safe from corruption
# Fast
# At max throughput: ~14k saves per sec, ~40% disk usage
#
# OFF
# Not consistent - database may be corrupted during power loss
# Very Fast
# At max throughput: ~16k saves per sec, ~6% disk usage
databaseSyncMode = "NORMAL"
dataCompression = "Z_STD_BLOCK"
#
# Enabling this will drastically speed up chunk processing time
# if you are CPU limited.
# Disabling it will drastically speed up chunk processing time
# if you are disk speed/IO limited.
#
# Normally DH will attempt to skip creating LODs for chunks it's already seen
# and that haven't changed.
#
# Disabling this option can also fix a rare bug where
# modified chunks are skipped due to being too similar to
# their saved hash.
disableUnchangedChunkCheck = false
[common.multiThreading]
#
# How many threads should be used by Distant Horizons?
numberOfThreads = 2
#
# A value between 1.0 and 0.0 that represents the percentage
# of time each thread can run before going idle.
#
# This can be used to reduce CPU usage if the thread count
# is already set to 1 for the given option, or more finely
# tune CPU performance.
threadRunTimeRatio = "1.0"
#
# What Java thread priority should DH's primary thread pools run with?
#
# You probably don't need to change this unless you are also
# running C2ME and are seeing thread starvation in either C2ME or DH.
threadPriority = 5
[common.logging]
#
# If enabled, the mod will log information about the renderer setup, cleanup, and any issues it may encounter.
# This can be useful for debugging.
logRendererEventToFile = "INFO"
#
# If enabled, the mod will log information about the world generation process.
# This can be useful for debugging.
logWorldGenEventToFile = "INFO"
#
# If enabled, the mod will log information about the renderer OpenGL process.
# This can be useful for debugging.
logRendererGLEventToChat = "ERROR"
#
#
globalChatMaxLevel = "ERROR"
#
# If enabled, the mod will log information about the world generation process.
# This can be useful for debugging.
logWorldGenChunkLoadEventToFile = "INFO"
#
# If enabled, the mod will log information about the renderer OpenGL process.
# This can be useful for debugging.
logRendererGLEventToFile = "INFO"
#
# If enabled, the mod will log information about network operations.
# This can be useful for debugging.
logNetworkEventToFile = "INFO"
#
# If enabled, config changes sent by the server will be logged.
logConnectionConfigChangesToFile = "WARN"
#
#
globalFileMaxLevel = "INFO"
[common.logging.warning]
#
# If enabled, a message will be displayed in chat if explicit garbage collection
# is disabled.
# This is known to cause out-of-memory issues
# and is better solved with a concurrent garbage collector.
showExplicitGcDisabledWarning = false
#
# If enabled, a chat message will be displayed when DH has too many chunks
# queued for updating.
showSlowWorldGenSettingWarnings = true
#
# If enabled, a chat message will be displayed when DH has too many chunks
# queued for updating.
showUpdateQueueOverloadedChatWarning = false
#
# If enabled, a message will be logged if explicit garbage collection
# is disabled.
# This is known to cause out-of-memory issues
# and is better solved with a concurrent garbage collector.
logExplicitGcDisabledWarning = false
#
# If enabled, a chat message will be displayed if Java doesn't have enough
# memory allocated to run DH well.
showLowMemoryWarningOnStartup = true
#
# If enabled, a chat message will be displayed when a replay is started
# giving some basic information about how DH will function.
showReplayWarningOnStartup = true
#
# If enabled, a chat message will be displayed when a potentially problematic
# mod is installed alongside DH.
showModCompatibilityWarningsOnStartup = true
#
# If enabled, a chat message will be displayed if vanilla MC's
# render distance is higher than the recommended amount.
showHighVanillaRenderDistanceWarning = true
#
# If enabled, a chat message will be displayed if DH detects
# that any pooled objects have been garbage collected.
showPoolInsufficientMemoryWarning = true
#
# If enabled, a message will be logged if the garbage
# collector Java is currently using is known
# to cause frame stuttering and/or other issues.
logGarbageCollectorWarning = true
#
# If enabled, a chat message will be displayed in chat if the garbage
# collector Java is currently using is known
# to cause frame stuttering and/or other issues.
showGarbageCollectorWarning = true
[common.worldGenerator]
#
# How should distant generator progress be displayed?
#
# OVERLAY: may be the same as CHAT for some Minecraft versions
# CHAT
# LOG
# DISABLED
showGenerationProgress = "DISABLED"
#
# If the SURFACE_THEN_CHUNKS generator plan is
# in use, this percentage can be defined to reduce how far the
# second high-quality generation pass will go
# as a percentage of your total render distance:
# -1.0 = Infinity
# 0.0 = 0%
# 0.5 = 50%
# 1.0 = 100%
# 2.0 = 200%
#
# Does nothing on a dedicated server.
surfaceRegenMaxDistancePercent = "-1.0"
#
# The max radius in chunks around the central point where world generation is allowed.
# If this value is set to 0, generation bounds are disabled and the render distance will be used.
#
# This should only be set if you have a pre-generated world that has a very limited size.
# Setting this on a normal MC world will prevent the world generator from filling
# out your render distance.
generationMaxChunkRadius = 0
#
# If the Minecraft server thread is taking too long
# Distant Horizons will pause world gen to reduce server stuttering/lag.
limitIfServerUnhealthy = true
#
# How often should the distant generator progress be displayed?
generationProgressDisplayIntervalInSeconds = 2
#
# When logging generation progress also include the rate at which chunks
# are being generated.
# This can be useful for troubleshooting performance.
generationProgressIncludeChunksPerSecond = true
#
# How detailed should LODs be generated outside the vanilla render distance?
#
# PRE_EXISTING_ONLY
# Only create LOD data for already generated chunks.
#
# FEATURES
# Generate everything except structures.
# Note: This may cause world generator bugs or instability when paired with certain world generator mods.
#
# INTERNAL_SERVER
# Ask the local server to generate/load each chunk.
# This is the most compatible and will generate structures correctly,
# but may cause server/simulation lag.
# Note: unlike other modes this option DOES save generated chunks to
# Minecraft's region files.
chunkGeneratorMode = "FEATURES"
#
# Defines how LODs will be generated
# outside the vanilla render distance.
# In multiplayer, this may be overridden by the server.
#
# SURFACE_THEN_CHUNKS
# The rough surface will be generated first
# then chunks will be generated to fill in
# missing features (ie trees and villages).
#
# Recommended for modded or vanilla worlds.
#
# SURFACE_ONLY
# Only the rough surface will be generated.
# Chunk features like trees and villages
# won't be generated.
#
# Recommended for computers that don't have
# enough CPU power or RAM space to handle
# the chunk generator.
#
# CHUNKS_ONLY
# Slower than either surface options,
# but provides more accurate terrain.
#
# Recommended for custom worlds
# where the surface generator would
# generate incorrect terrain.
#
# DISABLED
# No distant terrain will be generated.
generatorPlan = "SURFACE_THEN_CHUNKS"
#
# If true and the player/camera is moving quickly
# Distant Horizons will pause world gen to improve loading speed
# and reduce CPU load.
pauseIfMovingQuickly = true
#
# For how many seconds should instructions for disabling the distant generator progress be displayed?
# Setting this to 0 hides the instructional message so the world gen progress is shown immediately when it starts.
generationProgressDisableMessageDisplayTimeInSeconds = 20
#
# The center Z chunk position that the world gen max radius is centered around.
generationCenterChunkZ = 0
#
# The center X chunk position that the world gen max radius is centered around.
generationCenterChunkX = 0
[client]
#
# Should Distant Horizon's config button appear in Minecraft's options screen next to the fov slider?
showDhOptionsButtonInMinecraftUi = true
[client.advanced.autoUpdater]
#
# If DH should use the nightly (provided by Gitlab), or stable (provided by Modrinth) build.
# If [AUTO] is selected DH will update to new stable releases if the current jar is a stable jar
# and will update to new nightly builds if the current jar is a nightly jar (IE the version number ends in '-dev').
updateBranch = "AUTO"
#
# Automatically check for updates on game launch?
enableAutoUpdater = true
#
# Should Distant Horizons silently, automatically download and install new versions?
# This setting is force disabled on dedicated servers for stability reasons.
enableSilentUpdates = false
[client.advanced.debugging]
#
# If enabled this will disable (most) vanilla Minecraft rendering.
#
# NOTE: Do not report any issues when this mode is on!
# This setting is only for fun and debugging.
# Mod compatibility is not guaranteed.
lodOnlyMode = false
#
# What renderer is active?
#
# DEFAULT: Default lod renderer
# DEBUG_TRIANGLE: Debug testing renderer
# DISABLED: Disable rendering
rendererMode = "DEFAULT"
#
# If true overlapping quads will be rendered as bright red for easy identification.
# If false the quads will be rendered normally.
showOverlappingQuadErrors = false
#
# If true OpenGL Buffer garbage collection will be logged
# this also includes the number of live buffers.
logBufferGarbageCollection = false
#
# Should specialized colors/rendering modes be used?
#
# OFF: LODs will be drawn with their normal colors.
# SHOW_DETAIL: LODs' color will be based on their detail level.
# SHOW_BLOCK_MATERIAL: LODs' color will be based on their material.
# SHOW_OVERLAPPING_QUADS: LODs will be drawn with total white, but overlapping quads will be drawn with red.
debugRenderingColors = "OFF"
#
# Stops vertex colors from being passed.
# Useful for debugging shaders
enableWhiteWorld = false
#
# If enabled the LODs will render as wireframe.
renderWireframe = false
#
# If true several keys can be used to toggle debug states.
# F6 - enable/disable LOD rendering
# F7 - enable/disable LOD only rendering
# F8 - cycle through the different debug rendering modes
enableDebugKeybindings = false
[client.advanced.debugging.debugWireframe]
#
# Render LOD section status?
showRenderSectionStatus = false
#
# Render queued network sync on load tasks?
showNetworkSyncOnLoadQueue = false
#
# Render full data update/lock status?
showFullDataUpdateStatus = false
#
# Render queued world gen tasks?
showWorldGenQueue = false
#
# Render Quad Tree Rendering status?
showQuadTreeRenderStatus = false
#
# If enabled, various wireframes for debugging internal functions will be drawn.
#
# NOTE: There WILL be performance hit!
# Additionally, only stuff that's loaded after you enable this
# will render their debug wireframes.
enableRendering = false
[client.advanced.debugging.openGl]
#
# Defines how OpenGL errors are handled.
# Requires rebooting Minecraft to change.
# Will catch OpenGL errors thrown by other mods.
overrideVanillaGLLogger = true
#
# Defines how OpenGL errors are handled.
# May incorrectly catch OpenGL errors thrown by other mods.
#
# IGNORE: Do nothing.
# LOG: write an error to the log.
# LOG_THROW: write to the log and throw an exception.
# Warning: this should only be enabled when debugging the LOD renderer
# as it may break Minecraft's renderer when an exception is thrown.
glErrorHandlingMode = "IGNORE"
#
# If true each Open GL error will only be logged once.
# Enabling this may cause some error logs to be missed.
# Does nothing if overrideVanillaGLLogger is set to false.
#
# Generally this can be kept as 'true' to prevent log spam.
# However, Please set this to 'false' if a developer needs your log to debug a GL issue.
onlyLogGlErrorsOnce = true
[client.advanced.debugging.exampleConfigScreen]
shortTest = "69"
mapTest = "{}"
byteTest = "8"
longTest = "42069"
listTest = ["option 1", "option 2", "option 3"]
boolTest = false
doubleTest = "420.69"
floatTest = "0.42069"
linkableTest = 420
intTest = 69420
stringTest = "Test input box"
[client.advanced.debugging.f3Screen]
#
# Shows info about the render thread tasks.
showRenderThreadTasks = false
#
# Shows how many chunks are queued for processing and the max count that can be queued.
showQueuedChunkUpdateCount = true
#
# Shows the memory use and array counts for each DH object pool.
showSeparatedObjectPools = false
#
# Shows the player's LOD position.
showPlayerPos = true
#
# Shows the combined memory use and array counts for all DH pooled objects.
showCombinedObjectPools = false
#
# Defines what internal detail level the player position will be shown as.
# Internal detail level means: 6 = 1x1 block, 7 = 2x2 blocks, etc.
playerPosSectionDetailLevel = 6
#
# Only show levels that DH is actively rendering.
onlyShowRenderingLevels = true
#
# Shows info about each thread pool.
showThreadPools = true
#
# Shows what levels are loaded and world gen/rendering info about those levels.
showLevelStatus = true
[client.advanced.debugging.positionFinderDebugging]
positionFinderEnable = false
positionFinderMinBlockY = -64
positionFinderZPos = 0
positionFinderXPos = 0
positionFinderMaxBlockY = 125
positionFinderDetailLevel = 6
positionFinderMarginPercent = "0.0"
[client.advanced.graphics]
#
# Enabling Anti-Aliasing smooths edges and reduces flickering when moving.
enableAntiAliasing = true
#
# Enable Screen Space Ambient Occlusion
enableSsao = true
#
# If true some vanilla graphics settings will be automatically changed
# during DH setup to provide a better experience.
#
# IE disabling vanilla clouds (which render on top of DH LODs),
# and chunk fading (DH already fades MC chunks)
overrideVanillaGraphicsSettings = true
[client.advanced.graphics.culling]
#
# Determines how far from the camera Distant Horizons will start rendering.
# Measured as a percentage of the vanilla render distance.
#
# -1 = auto, overdraw will change based on the vanilla render distance.
#
# Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,
# but may cause holes in the world.
# Holes are most likely to appear when flying through unloaded terrain.
#
# Increasing the vanilla render distance increases the effectiveness of this setting.
overdrawPrevention = "-1.0"
#
# Identical to the other frustum culling option
# only used when a shader mod is present using the DH API
# and the shadow pass is being rendered.
#
# Disable this if shadows render incorrectly.
disableShadowPassFrustumCulling = false
#
# A comma separated list of block resource locations that will be replaced by water
# if they're visible on the water's surface.
waterSubSurfaceBlockReplacementCsv = "minecraft:kelp,minecraft:tall_seagrass,minecraft:seagrass"
#
# A comma separated list of block resource locations that won't be rendered by DH.
# Air is always included in this list.
#
# Note:
# If you see gaps, or holes you may have to change
# worldCompression to [MERGE_SAME_BLOCKS] and re-generate the LODs.
ignoredRenderBlockCsv = "minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom"
#
# A comma separated list of block resource locations that will be removed
# when on top of water.
waterSurfaceBlockReplacementCsv = "minecraft:lily_pad"
#
# Defines what blocks should be rendered as LODs.
#
# NONE: Include all blocks in the LODs
# NON_COLLIDING: Only render solid blocks in the LODs (tall grass, torches, etc. will be ignored)
blocksToIgnore = "NON_COLLIDING"
#
# If true LODs outside the player's camera
# aren't drawn, increasing GPU performance.
#
# If false all LODs are drawn, even those behind
# the player's camera, decreasing GPU performance.
#
# Disable this if you see LODs disappearing at the corners of your vision.
disableFrustumCulling = false
#
# If false all beacons near the camera won't be drawn to prevent vanilla overdraw.
# If true all beacons will be rendered.
#
# Generally this should be left as true. It's main purpose is for debugging
# beacon updating/rendering.
disableBeaconDistanceCulling = true
#
# Should the blocks underneath avoided blocks gain the color of the avoided block?
#
# True: a red flower will tint the grass below it red.
# False: skipped blocks will not change color of surface below them.
tintWithAvoidedBlocks = true
#
# If enabled caves won't be rendered.
#
# Note: for some world types this can cause
# overhangs or walls for floating objects.
# Tweaking the caveCullingHeight, can resolve some
# of those issues.
enableCaveCulling = true
#
# At what Y value should cave culling start?
# Lower this value if you get walls for areas with 0 light.
caveCullingHeight = 60
#
# A comma separated list of block resource locations that shouldn't be rendered
# if they are in a 0 sky light underground area.
# Air is always included in this list.
#
# Defaults to an empty list since most cave blocks will be automatically ignored due to being:
# transparent, non-solid, or liquids, but new blocks can be added here if needed.
ignoredRenderCaveBlockCsv = ""
#
# If set to true the overdraw prevention radius will get closer
# to the camera when flying/moving quickly.
#
# This helps reduce issues where Minecraft can't load or
# generate chunks fast enough to keep up with DH.
reduceOverdrawWithFastMovement = true
[client.advanced.graphics.texture]
#
# A comma separated list of block resource locations
# that DH will render their sides using the bottom texture.
# Partial matches/incomplete resource locations will also match.
#
# Example: "minecraft:grass_block,nylium"
#
# Changes require a restart.
blocksDontUseSideTextureCsv = "grass_block,mycelium,nylium,dirt_path,grass"
#
# The highest detail level number that can render with block textures.
# At higher detail levels each LOD covers multiple blocks,
# which can essentially make textures smaller than a pixel and therefore invisible.
#
# Larger numbers texture more distant LODs
# but increase memory usage.
maxTexturedLodDetailLevel = 2
#
# If true nearby and zoomed-in LODs will render with their block's
# actual texture instead of a single flat color.
#
# Only applies to high detail LODs where the texture is actually visible,
# distant LODs always use flat colors.
# Some shader packs nay not have an affect.
enableTexturedLods = true
#
# A comma separated list of block resource locations
# that DH won't render textures on.
# Partial matches/incomplete resource locations will also match.
#
# Example: "minecraft:grass_block,nylium"
#
# Changes require a restart.
blocksDontRenderTextureCsv = "minecraft:bamboo"
#
# A comma separated list of tag names
# that DH will render their sides using the bottom texture.
#
# Example: "grass_blocks,nylium"
#
# Changes require a restart.
blockTagsDontUseSideTextureCsv = "grass_blocks,nylium"
#
# A comma separated list of block resource locations
# that DH will use rasterization to determine
# the face textures.
#
# Can be used to fix issues with mods/blocks where
# a side uses just part of a modeled block's texture.
#
# Example: "minecraft:beacon"
#
# Changes require a restart.
blocksAlwaysRasterizeTextureCsv = "minecraft:beacon"
[client.advanced.graphics.noiseTexture]
#
# Should a noise texture be applied to LODs?
#
# This is done to simulate textures and make the LODs appear more detailed.
enableNoiseTexture = true
#
# Defines how far should the noise texture render before it fades away. (in blocks)
# Set to 0 to disable noise from fading away
noiseDropoff = 1024
#
# How many steps of noise should be applied to LODs?
noiseSteps = 4
#
# How intense should the noise should be?
noiseIntensity = "0.05"
[client.advanced.graphics.experimental]
#
# A comma separated list of dimension resource locations where DH won't render.
#
# Example: "minecraft:the_nether,minecraft:the_end"
# For Minecraft 1.12.2 and older:
# Open the F3 screen and copy the dimension_name:ID shown in yellow. Example: "overworld:0"
#
# Note:
# Some DH settings will be disabled and/or changed to improve
# visuals when DH rendering is disabled.
ignoredDimensionCsv = ""
#
# Requires a restart to change.
#
# Options:
# AUTO - changes based on the most likely API for that MC version
# OPEN_GL - The Default for MC 1.21.11 and older (supports Iris shaders)
# BLAZE_3D - The Default for MC 26.1.2 and newer (supports Vulkan)
renderingEngine = "AUTO"
#
# This is the earth size ratio when applying the curvature shader effect.
# Note: Enabling this feature may cause rendering bugs.
#
# 0 = flat/disabled
# 1 = 1 to 1 (6,371,000 blocks)
# 100 = 1 to 100 (63,710 blocks)
# 10000 = 1 to 10000 (637.1 blocks)
#
# Note: Due to current limitations, the min value is [50]
# and the max value is 5000. Any values outside this range
# will be set to 0 (disabled).
earthCurveRatio = 0
[client.advanced.graphics.genericRendering]
#
# A comma separated separated list of dimension resource locations where DH clouds will render.
#
# Example: "minecraft:overworld,minecraft:the_end"
#
# Changes require a world re-load.
dimensionEnabledCloudRenderingCsv = "minecraft:overworld"
#
# If true LOD clouds will be rendered.
enableCloudRendering = true
#
# If true LOD beacon beams will be rendered wider at extreme distances,
# making them easier to see.
# If false all LOD beacon beams will only ever be 1 block wide.
expandDistantBeacons = true
#
# Sets the maximum height beacons will render up to.
#
# Requires a world re-load to take affect.
beaconRenderHeight = 6000
#
# If true LOD beacon beams will be rendered.
enableBeaconRendering = true
#
# If true non terrain objects will be rendered by DH.
# i.e. beacon beams and clouds.
enableGenericRendering = true
#
# False = DH will render a single layer of clouds, like vanilla Minecraft.
# True = DH will render 3 layers of clouds at different heights.
enableMultiLayerClouds = true
[client.advanced.graphics.quality]
#
# What is the maximum detail LODs can render at?
# Higher settings will increase memory and GPU usage.
#
# CHUNK: render 1 LOD for each Chunk.
# HALF_CHUNK: render 4 LODs for each Chunk.
# FOUR_BLOCKS: render 16 LODs for each Chunk.
# TWO_BLOCKS: render 64 LODs for each Chunk.
# BLOCK: render 256 LODs for each Chunk (width of one block).
#
# Fastest: CHUNK
# Fanciest: BLOCK
maxHorizontalResolution = "BLOCK"
#
# If true LODs will fade away as you get closer to them.
# If false LODs will cut off abruptly at a set distance from the camera.
# This setting is affected by the vanilla overdraw prevention config.
ditherDhFade = true
#
# If true DH will try to use the camera position when
# determining LOD quality drop-off.
# If false DH will use the player's position.
#
# Enabling helps free-cam mods render correctly.
# Disabling helps multi-camera mods render correctly (ie Immersive Portals or camera mods).
useCameraPositionForQualityDropOff = true
#
# How bright LOD colors are.
#
# 0 = black
# 1 = normal
# 2 = near white
brightnessMultiplier = "1.0"
#
# How many detail levels zooming in can increase LOD quality by.
# Higher numbers allow stronger zooms to render crisper terrain,
# but will increase memory and GPU usage while zoomed in.
#
# A vanilla spyglass needs 4 detail levels to reach its full quality.
maxZoomQualityIncrease = 4
#
# How should LODs be shaded?
#
# AUTO: Uses the same side shading as vanilla Minecraft blocks.
# ENABLED: Simulates Minecraft's block shading for LODs.
# Can be used to force LOD shading when using some shaders.
# DISABLED: All LOD sides will be rendered with the same brightness.
lodShading = "AUTO"
#
# How saturated LOD colors are.
#
# 0 = black and white
# 1 = normal
# 2 = very saturated
saturationMultiplier = "1.0"
#
# This indicates how well LODs will represent
# overhangs, caves, floating islands, etc.
# Higher options will make the world more accurate, butwill increase memory and GPU usage.
#
# Lowest Quality: HEIGHT_MAP
# Highest Quality: PIXEL_ART
verticalQuality = "MEDIUM"
#
# The radius of the mod's render distance. (measured in chunks)
#
# Note: this is a best effort number.
# The actual render distance may be above or below this number
# depending on your other graphic settings.
lodChunkRenderDistanceRadius = 128
#
# How should the sides and bottom of grass block LODs render?
#
# AS_GRASS: all sides of dirt LOD's render using the top (green) color.
# FADE_TO_DIRT: sides fade from grass to dirt.
# AS_DIRT: sides render entirely as dirt.
grassSideRendering = "FADE_TO_DIRT"
#
# This indicates how far apart drops in LOD quality are.
#
# Higher settings will increase the distance between drops
# but will increase memory and GPU usage.
horizontalQuality = "MEDIUM"
#
# How should LOD transparency be handled.
#
# COMPLETE: LODs will render transparent.
# DISABLED: LODs will be opaque.
transparency = "COMPLETE"
#
# This is the same as vanilla Biome Blending settings for Lod area.
# Note: anything above '0' will slow down LOD loading time.
#
# '0' equals to Vanilla Biome Blending of '1x1' or 'OFF',
# '1' equals to Vanilla Biome Blending of '3x3',
# '2' equals to Vanilla Biome Blending of '5x5'...
lodBiomeBlending = 3
#
# How should vanilla Minecraft fade into Distant Horizons LODs?
#
# NONE: Fastest, there will be a pronounced border between DH and MC rendering.
# SINGLE_PASS: Fades after MC's transparent pass, opaque blocks underwater won't be faded.
# DOUBLE_PASS: Slowest, fades after both MC's opaque and transparent passes, provides the smoothest transition.
vanillaFadeMode = "DOUBLE_PASS"
#
# If true LOD quality will increase when the camera is zoomed in,
# IE when using a spyglass or zoom mod.
#
# Only LODs visible through the camera view are affected.
#
# When zoomed in LODs will load to the same detail level
# they would have if you were close to them.
increaseQualityWhenZoomedIn = true
[client.advanced.graphics.fog]
#
# Should Minecraft's fog render?
# Note: Other mods may conflict with this setting.
enableVanillaFog = false
#
# What is the maximum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
farFogMax = "1.0"
#
# Determines if fog is drawn on DH LODs.
enableDhFog = true
#
# At what distance should the far fog start?
#
# 0.0: Fog starts at the player's position.
# 1.0: Fog starts at the closest edge of the vanilla render distance.
# 1.414: Fog starts at the corner of the vanilla render distance.
farFogStart = "0.4"
#
# What is the minimum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
farFogMin = "0.0"
#
# What color should fog use?
#
# USE_WORLD_FOG_COLOR: Use the world's fog color.
# USE_SKY_COLOR: Use the sky's color.
colorMode = "USE_WORLD_FOG_COLOR"
#
# How should the fog thickness should be calculated?
#
# LINEAR: Linear based on distance (will ignore 'density')
# EXPONENTIAL: 1/(e^(distance*density))
# EXPONENTIAL_SQUARED: 1/(e^((distance*density)^2)
farFogFalloff = "EXPONENTIAL_SQUARED"
#
# Used in conjunction with the Fog Falloff.
farFogDensity = "2.5"
#
# Where should the far fog end?
#
# 0.0: Fog ends at player's position.
# 1.0: Fog ends at the closest edge of the vanilla render distance.
# 1.414: Fog ends at the corner of the vanilla render distance.
farFogEnd = "1.0"
[client.advanced.graphics.fog.heightFog]
#
# Where should the height fog start?
#
# ABOVE_CAMERA: Height fog starts at the camera and goes towards the sky
# BELOW_CAMERA: Height fog starts at the camera and goes towards the void
# ABOVE_AND_BELOW_CAMERA: Height fog starts from the camera to goes towards both the sky and void
# ABOVE_SET_HEIGHT: Height fog starts from a set height and goes towards the sky
# BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards the void
# ABOVE_AND_BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards both the sky and void
heightFogDirection = "BELOW_SET_HEIGHT"
#
# What is the minimum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
heightFogMin = "0.0"
#
# If the height fog is calculated around a set height, what is that height position?
heightFogBaseHeight = "80.0"
#
# What is the maximum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
heightFogMax = "1.0"
#
# How should the height fog thickness should be calculated?
#
# LINEAR: Linear based on height (will ignore 'density')
# EXPONENTIAL: 1/(e^(height*density))
# EXPONENTIAL_SQUARED: 1/(e^((height*density)^2)
heightFogFalloff = "EXPONENTIAL_SQUARED"
#
# What is the height fog's density?
heightFogDensity = "20.0"
#
# How should height effect the fog thickness?
# Note: height fog is combined with the other fog settings.
#
# SPHERICAL: Fog is calculated based on camera distance.
# CYLINDRICAL: Ignore height, fog is calculated based on horizontal distance.
#
# MAX: max(heightFog, farFog)
# ADDITION: heightFog + farFog
# MULTIPLY: heightFog * farFog
# INVERSE_MULTIPLY: 1 - (1-heightFog) * (1-farFog)
# LIMITED_ADDITION: farFog + max(farFog, heightFog)
# MULTIPLY_ADDITION: farFog + farFog * heightFog
# INVERSE_MULTIPLY_ADDITION: farFog + 1 - (1-heightFog) * (1-farFog)
# AVERAGE: farFog*0.5 + heightFog*0.5
heightFogMixMode = "SPHERICAL"
#
# Should the start of the height fog be offset?
#
# 0.0: Fog start with no offset.
# 1.0: Fog start with offset of the entire world's height. (Includes depth)
heightFogStart = "0.0"
#
# Should the end of the height fog be offset?
#
# 0.0: Fog end with no offset.
# 1.0: Fog end with offset of the entire world's height. (Include depth)
heightFogEnd = "0.6"
[client.advanced.multiplayer]
#
# How should multiplayer save folders should be named?
#
# NAME_ONLY: Example: "Minecraft Server"
# IP_ONLY: Example: "192.168.1.40"
# NAME_IP: Example: "Minecraft Server IP 192.168.1.40"
# NAME_IP_PORT: Example: "Minecraft Server IP 192.168.1.40:25565"NAME_IP_PORT_MC_VERSION: Example: "Minecraft Server IP 192.168.1.40:25565 GameVersion 1.16.5"
serverFolderNameMode = "NAME_ONLY"
@@ -0,0 +1,36 @@
{
"allowedCEM": "ALL",
"logModelCreationData": false,
"debugOnRightClick": false,
"renderModeChoice": "NORMAL",
"vanillaModelHologramRenderMode_2": "OFF",
"modelExportMode": "NONE",
"automaticModelExporting": false,
"attemptPhysicsModPatch_2": "CUSTOM",
"modelUpdateFrequency": "Slow",
"entityRenderModeOverrides": {},
"entityPhysicsModPatchOverrides": {},
"entityVanillaHologramOverrides": {},
"modelsNamesDisabled": [],
"allowEBEModConfigModify": true,
"animationLODDistance": 12,
"retainDetailOnLowFps": true,
"retainDetailOnLargerMobs": true,
"animationFrameSkipDuringIrisShadowPass": true,
"preventFirstPersonHandAnimating": false,
"onlyClientPlayerModel": false,
"doubleChestAnimFix": true,
"enforceOptifineVariationRequiresDefaultModel": false,
"enforceOptifineVariationRequiresDefaultModel_v2": false,
"resetPlayerModelEachRender": true,
"resetPlayerModelEachRender_v2": true,
"onlyDebugRenderOnHover": false,
"enforceOptifineSubFoldersVariantOnly": false,
"enforceOptiFineAnimSyntaxLimits": true,
"allowOptifineFallbackProperties": true,
"showReloadErrorToast": true,
"exportRotations": false,
"asmMaths": true,
"logASM": false,
"parrotShoulderPositionAnimatesByDefault": true
}
@@ -0,0 +1,14 @@
#Select the backend to use. Set to "DEFAULT" to let Flywheel decide.
backend = "DEFAULT"
#Enable or disable instance update limiting with distance.
limitUpdates = true
#The number of worker threads to use. Set to -1 to let Flywheel decide. Set to 0 to disable parallelism. Requires a game restart to take effect.
# Default: -1
# Range: -1 ~ 4
workerThreads = -1
#Config options for Flywheel's built-in backends.
[flw_backends]
#How smooth Flywheel's shader-based lighting should be. May have a large performance impact.
#Allowed Values: FLAT, TRI_LINEAR, SMOOTH, SMOOTH_INNER_FACE_CORRECTED
lightSmoothness = "SMOOTH"
@@ -0,0 +1,16 @@
mode = "fast"
chunk_rebuild_scheduler = "culling"
[adaptive_ticking]
slow = 5
slower = 8
background_sleep = true
[light_sources]
entities = false
self = true
water_sensitive_check = true
beam = true
guardian_laser = true
sonic_boom = true
glowing_effect = true
@@ -0,0 +1,20 @@
# Partial file: SPR merges these over its generated per-sound defaults (-1 = unlimited).
create\:cogs=4
create\:mixing=4
create\:mixing_compounded_1=4
create\:mechanical_press_activation=4
create\:mechanical_press_activation_compounded_1=4
create\:mechanical_press_activation_belt=4
create\:mechanical_press_activation_belt_compounded_1=4
create\:crushing_1=4
create\:crushing_2=4
create\:crushing_3=4
create\:saw_activate_stone=4
create\:saw_activate_wood=4
create\:funnel_flap=4
create\:funnel_flap_compounded_1=4
create\:depot_slide=4
create\:depot_plop=4
create\:steam=4
create\:sanding_short=4
create\:sanding_long=4
@@ -0,0 +1,99 @@
# Enables/Disables all sound effects
enabled=true
# Affects how quiet a sound gets based on distance
# Lower values mean distant sounds are louder
# This setting requires you to be in singleplayer or having the mod installed on the server
# 1.0 is the physically correct value
attenuation_factor=1.0
# The ray distance at which reverb starts
# 0.0 disables reverb attenuation
reverb_attenuation_distance=0.0
# The volume of simulated reverberations
reverb_gain=1.0
# The brightness of reverberation
# Higher values result in more high frequencies in reverberation
# Lower values give a more muffled sound to the reverb
reverb_brightness=1.0
# The distance of reverb relative to the sound distance
reverb_distance=1.5
# The amount of sound that will be absorbed when traveling through blocks
block_absorption=1.0
# Higher values mean smaller objects won't be considered as occluding
occlusion_variation=0.35
# The default amount of sound reflectance energy for all blocks
# Lower values result in more conservative reverb simulation with shorter reverb tails
# Higher values result in more generous reverb simulation with higher reverb tails
default_block_reflectivity=0.5
# The default amount of occlusion for all blocks
# Lower values will result in sounds being less muffled through walls
# Higher values mean sounds will be not audible though thicker walls
default_block_occlusion_factor=1.0
# Minecraft won't allow sounds to play past a certain distance
# This parameter is a multiplier for how far away a sound source is allowed to be in order for it to actually play
# This setting only takes affect in singleplayer worlds and when installed on the server
sound_distance_allowance=4.0
# A value controlling the amount that air absorbs high frequencies with distance
# A value of 1.0 is physically correct for air with normal humidity and temperature
# Higher values mean air will absorb more high frequencies with distance
# 0 disables this effect
air_absorption=1.0
# How much sound is filtered when the player is underwater
# 0.0 means no filter
# 1.0 means fully filtered
underwater_filter=0.9
# Whether sounds like cave, nether or underwater ambient sounds should have sound physics
evaluate_ambient_sounds=false
# The number of rays to trace to determine reverberation for each sound source
# More rays provides more consistent tracing results but takes more time to calculate
# Decrease this value if you experience lag spikes when sounds play
environment_evaluation_ray_count=16
# The number of rays bounces to trace to determine reverberation for each sound source
# More bounces provides more echo and sound ducting but takes more time to calculate
# Decrease this value if you experience lag spikes when sounds play
environment_evaluation_ray_bounces=2
# If sound hits a non-full-square side, block occlusion is multiplied by this
non_full_block_occlusion_factor=0.25
# The maximum amount of rays to determine occlusion
# Directly correlates to the amount of blocks between walls that are considered
max_occlusion_rays=16
# The amount at which occlusion is capped
max_occlusion=64.0
# If enabled, the occlusion calculation only uses one path between the sound source and the listener instead of 9
strict_occlusion=true
# Whether to try calculating where the sound should come from based on reflections
sound_direction_evaluation=true
# Skip redirecting non-occluded sounds (the ones you can see directly)
redirect_non_occluded_sounds=true
# If music discs or other longer sounds should be frequently reevaluated
update_moving_sounds=false
# The interval in ticks that moving sounds are reevaluated
# Lower values mean more frequent reevaluation but also more lag
# This option only takes effect if update_moving_sounds is enabled
sound_update_interval=5
# The maximum distance a sound can be processed
max_sound_processing_distance=512.0
# Disable level clone and cache. This will fall back to original main thread access.
# WARNING! Enabling this will cause instability and issues with other mods.
unsafe_level_access=false
# The radius of chunks to clone for level access
level_clone_range=4
# The maximum number of ticks to retain the cloned level in the cache
level_clone_max_retain_ticks=20
# The maximum distance a player can move from the cloned origin before invalidation
level_clone_max_retain_block_distance=16
# Enables debug logging
debug_logging=false
# Provides more information about occlusion in the logs
occlusion_logging=false
# Provides more information about the environment calculation in the logs
environment_logging=false
# Provides more information about how long computations take
performance_logging=false
# If enabled, the path of the sound will be rendered in game
render_sound_bounces=false
# If enabled, occlusion will be visualized in game
render_occlusion=false
# Enables/Disables sound effects for Simple Voice Chat audio
simple_voice_chat_integration=true
# Enables/Disables hearing your own echo with Simple Voice Chat
simple_voice_chat_hear_self=false
@@ -0,0 +1,12 @@
#The render distance for physics objects (SubLevels) in chunks.
# Default: 128
# Range: 1 ~ 1000000
physicsRenderDistance = 32
#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
+11
View File
@@ -0,0 +1,11 @@
version:3955
renderDistance:8
simulationDistance:8
biomeBlendRadius:2
mipmapLevels:4
entityDistanceScaling:1.0
particles:0
entityShadows:true
graphicsMode:1
maxFps:120
enableVsync:true
@@ -0,0 +1,980 @@
_version = 4
[server]
#
# Defines the distance the player will receive updates around.
realTimeUpdateDistanceRadiusInChunks = 256
#
# Prefix of the level keys sent to the clients.
# If the mod is running behind a proxy, each backend should use a unique value.
# If this value is empty, level key will be based on the server's seed hash.
levelKeyPrefix = ""
#
# Defines the distance allowed to be synchronized around the player.
# Should be the same or larger than maxGenerationRequestDistance in most cases.
maxSyncOnLoadRequestDistance = 4096
#
# If true, clients will receive updated LODs when joining or loading new LODs.
synchronizeOnLoad = true
#
# Custom server key used which can be used to always reuse the same LOD data folder,
# for cases when the server doesn't have a static IP for some reason.
# If this value is empty, the client itself decides which folder name to use.
# Requires rejoining the server to apply after changing.
serverKey = ""
#
# Defines the distance allowed to generate around the player.
maxGenerationRequestDistance = 4096
#
# Makes the server send level keys for each world.
# Disable this if you use alternative ways to send level keys.
sendLevelKeys = true
#
# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING.
# Autogenerated ID used to prevent multiple independent servers from accidentally
# writing over each other's LODs when the same serverKey is set on both.
#
# How many LOD generation requests per second should a client send?
# Also limits the number of client requests allowed to stay in the server's queue.
generationRequestRateLimit = 20
#
# How many LOD sync requests per second should a client send?
# Also limits the amount of player's requests allowed to stay in the server's queue.
syncOnLoadRateLimit = 50
#
# If true, clients will receive real-time LOD updates for chunks outside the client's render distance.
enableRealTimeUpdates = true
#
# Maximum global speed for uploading LODs to the clients, in KB/s.
# Value of 0 disables the limit.
globalBandwidthLimit = 0
#
# Maximum per-player speed for uploading LODs to the clients, in KB/s.
# Value of 0 disables the limit.
playerBandwidthLimit = 500
#
# Enables adaptive transfer speed based on client performance.
# If true, DH will automatically adjust transfer rate to minimize connection lag.
# If false, transfer speed will remain fixed.
enableAdaptiveTransferSpeed = false
[common.lodBuilding]
#
# How should block data be compressed when creating LOD data?
# This setting will only affect new or updated LOD data,
# any data already generated when this setting is changed will be
# unaffected until it is modified or re-loaded.
#
# MERGE_SAME_BLOCKS
# Every block/biome change is recorded in the database.
# This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data.
# Expected Compression Ratio: 1.0
#
# VISUALLY_EQUAL
# Only visible block/biome changes are recorded in the database.
# Hidden blocks (IE ores) are ignored.
# Expected Compression Ratio: 0.7
worldCompression = "VISUALLY_EQUAL"
#
# Requires a level restart to apply.
#
# Setting this to a less strict mode increases DB read/write
# speed at the cost of potential data loss/corruption if
# the computer looses power
# (unexpected program shutdown should be safe for all options).
#
# FULL
# ACID - data will never be lost or corrupted
# Slow
# At max throughput: ~2k saves per sec, ~70% disk usage
#
# NORMAL
# May not be durable - some data may be lost on power loss, but DB should be safe from corruption
# Fast
# At max throughput: ~14k saves per sec, ~40% disk usage
#
# OFF
# Not consistent - database may be corrupted during power loss
# Very Fast
# At max throughput: ~16k saves per sec, ~6% disk usage
databaseSyncMode = "NORMAL"
dataCompression = "Z_STD_BLOCK"
#
# Enabling this will drastically speed up chunk processing time
# if you are CPU limited.
# Disabling it will drastically speed up chunk processing time
# if you are disk speed/IO limited.
#
# Normally DH will attempt to skip creating LODs for chunks it's already seen
# and that haven't changed.
#
# Disabling this option can also fix a rare bug where
# modified chunks are skipped due to being too similar to
# their saved hash.
disableUnchangedChunkCheck = false
[common.multiThreading]
#
# How many threads should be used by Distant Horizons?
numberOfThreads = 2
#
# A value between 1.0 and 0.0 that represents the percentage
# of time each thread can run before going idle.
#
# This can be used to reduce CPU usage if the thread count
# is already set to 1 for the given option, or more finely
# tune CPU performance.
threadRunTimeRatio = "1.0"
#
# What Java thread priority should DH's primary thread pools run with?
#
# You probably don't need to change this unless you are also
# running C2ME and are seeing thread starvation in either C2ME or DH.
threadPriority = 5
[common.logging]
#
# If enabled, the mod will log information about the renderer setup, cleanup, and any issues it may encounter.
# This can be useful for debugging.
logRendererEventToFile = "INFO"
#
# If enabled, the mod will log information about the world generation process.
# This can be useful for debugging.
logWorldGenEventToFile = "INFO"
#
# If enabled, the mod will log information about the renderer OpenGL process.
# This can be useful for debugging.
logRendererGLEventToChat = "ERROR"
#
#
globalChatMaxLevel = "ERROR"
#
# If enabled, the mod will log information about the world generation process.
# This can be useful for debugging.
logWorldGenChunkLoadEventToFile = "INFO"
#
# If enabled, the mod will log information about the renderer OpenGL process.
# This can be useful for debugging.
logRendererGLEventToFile = "INFO"
#
# If enabled, the mod will log information about network operations.
# This can be useful for debugging.
logNetworkEventToFile = "INFO"
#
# If enabled, config changes sent by the server will be logged.
logConnectionConfigChangesToFile = "WARN"
#
#
globalFileMaxLevel = "INFO"
[common.logging.warning]
#
# If enabled, a message will be displayed in chat if explicit garbage collection
# is disabled.
# This is known to cause out-of-memory issues
# and is better solved with a concurrent garbage collector.
showExplicitGcDisabledWarning = false
#
# If enabled, a chat message will be displayed when DH has too many chunks
# queued for updating.
showSlowWorldGenSettingWarnings = true
#
# If enabled, a chat message will be displayed when DH has too many chunks
# queued for updating.
showUpdateQueueOverloadedChatWarning = false
#
# If enabled, a message will be logged if explicit garbage collection
# is disabled.
# This is known to cause out-of-memory issues
# and is better solved with a concurrent garbage collector.
logExplicitGcDisabledWarning = false
#
# If enabled, a chat message will be displayed if Java doesn't have enough
# memory allocated to run DH well.
showLowMemoryWarningOnStartup = true
#
# If enabled, a chat message will be displayed when a replay is started
# giving some basic information about how DH will function.
showReplayWarningOnStartup = true
#
# If enabled, a chat message will be displayed when a potentially problematic
# mod is installed alongside DH.
showModCompatibilityWarningsOnStartup = true
#
# If enabled, a chat message will be displayed if vanilla MC's
# render distance is higher than the recommended amount.
showHighVanillaRenderDistanceWarning = true
#
# If enabled, a chat message will be displayed if DH detects
# that any pooled objects have been garbage collected.
showPoolInsufficientMemoryWarning = true
#
# If enabled, a message will be logged if the garbage
# collector Java is currently using is known
# to cause frame stuttering and/or other issues.
logGarbageCollectorWarning = true
#
# If enabled, a chat message will be displayed in chat if the garbage
# collector Java is currently using is known
# to cause frame stuttering and/or other issues.
showGarbageCollectorWarning = true
[common.worldGenerator]
#
# How should distant generator progress be displayed?
#
# OVERLAY: may be the same as CHAT for some Minecraft versions
# CHAT
# LOG
# DISABLED
showGenerationProgress = "DISABLED"
#
# If the SURFACE_THEN_CHUNKS generator plan is
# in use, this percentage can be defined to reduce how far the
# second high-quality generation pass will go
# as a percentage of your total render distance:
# -1.0 = Infinity
# 0.0 = 0%
# 0.5 = 50%
# 1.0 = 100%
# 2.0 = 200%
#
# Does nothing on a dedicated server.
surfaceRegenMaxDistancePercent = "-1.0"
#
# The max radius in chunks around the central point where world generation is allowed.
# If this value is set to 0, generation bounds are disabled and the render distance will be used.
#
# This should only be set if you have a pre-generated world that has a very limited size.
# Setting this on a normal MC world will prevent the world generator from filling
# out your render distance.
generationMaxChunkRadius = 0
#
# If the Minecraft server thread is taking too long
# Distant Horizons will pause world gen to reduce server stuttering/lag.
limitIfServerUnhealthy = true
#
# How often should the distant generator progress be displayed?
generationProgressDisplayIntervalInSeconds = 2
#
# When logging generation progress also include the rate at which chunks
# are being generated.
# This can be useful for troubleshooting performance.
generationProgressIncludeChunksPerSecond = true
#
# How detailed should LODs be generated outside the vanilla render distance?
#
# PRE_EXISTING_ONLY
# Only create LOD data for already generated chunks.
#
# FEATURES
# Generate everything except structures.
# Note: This may cause world generator bugs or instability when paired with certain world generator mods.
#
# INTERNAL_SERVER
# Ask the local server to generate/load each chunk.
# This is the most compatible and will generate structures correctly,
# but may cause server/simulation lag.
# Note: unlike other modes this option DOES save generated chunks to
# Minecraft's region files.
chunkGeneratorMode = "FEATURES"
#
# Defines how LODs will be generated
# outside the vanilla render distance.
# In multiplayer, this may be overridden by the server.
#
# SURFACE_THEN_CHUNKS
# The rough surface will be generated first
# then chunks will be generated to fill in
# missing features (ie trees and villages).
#
# Recommended for modded or vanilla worlds.
#
# SURFACE_ONLY
# Only the rough surface will be generated.
# Chunk features like trees and villages
# won't be generated.
#
# Recommended for computers that don't have
# enough CPU power or RAM space to handle
# the chunk generator.
#
# CHUNKS_ONLY
# Slower than either surface options,
# but provides more accurate terrain.
#
# Recommended for custom worlds
# where the surface generator would
# generate incorrect terrain.
#
# DISABLED
# No distant terrain will be generated.
generatorPlan = "SURFACE_THEN_CHUNKS"
#
# If true and the player/camera is moving quickly
# Distant Horizons will pause world gen to improve loading speed
# and reduce CPU load.
pauseIfMovingQuickly = true
#
# For how many seconds should instructions for disabling the distant generator progress be displayed?
# Setting this to 0 hides the instructional message so the world gen progress is shown immediately when it starts.
generationProgressDisableMessageDisplayTimeInSeconds = 20
#
# The center Z chunk position that the world gen max radius is centered around.
generationCenterChunkZ = 0
#
# The center X chunk position that the world gen max radius is centered around.
generationCenterChunkX = 0
[client]
#
# Should Distant Horizon's config button appear in Minecraft's options screen next to the fov slider?
showDhOptionsButtonInMinecraftUi = true
[client.advanced.autoUpdater]
#
# If DH should use the nightly (provided by Gitlab), or stable (provided by Modrinth) build.
# If [AUTO] is selected DH will update to new stable releases if the current jar is a stable jar
# and will update to new nightly builds if the current jar is a nightly jar (IE the version number ends in '-dev').
updateBranch = "AUTO"
#
# Automatically check for updates on game launch?
enableAutoUpdater = true
#
# Should Distant Horizons silently, automatically download and install new versions?
# This setting is force disabled on dedicated servers for stability reasons.
enableSilentUpdates = false
[client.advanced.debugging]
#
# If enabled this will disable (most) vanilla Minecraft rendering.
#
# NOTE: Do not report any issues when this mode is on!
# This setting is only for fun and debugging.
# Mod compatibility is not guaranteed.
lodOnlyMode = false
#
# What renderer is active?
#
# DEFAULT: Default lod renderer
# DEBUG_TRIANGLE: Debug testing renderer
# DISABLED: Disable rendering
rendererMode = "DEFAULT"
#
# If true overlapping quads will be rendered as bright red for easy identification.
# If false the quads will be rendered normally.
showOverlappingQuadErrors = false
#
# If true OpenGL Buffer garbage collection will be logged
# this also includes the number of live buffers.
logBufferGarbageCollection = false
#
# Should specialized colors/rendering modes be used?
#
# OFF: LODs will be drawn with their normal colors.
# SHOW_DETAIL: LODs' color will be based on their detail level.
# SHOW_BLOCK_MATERIAL: LODs' color will be based on their material.
# SHOW_OVERLAPPING_QUADS: LODs will be drawn with total white, but overlapping quads will be drawn with red.
debugRenderingColors = "OFF"
#
# Stops vertex colors from being passed.
# Useful for debugging shaders
enableWhiteWorld = false
#
# If enabled the LODs will render as wireframe.
renderWireframe = false
#
# If true several keys can be used to toggle debug states.
# F6 - enable/disable LOD rendering
# F7 - enable/disable LOD only rendering
# F8 - cycle through the different debug rendering modes
enableDebugKeybindings = false
[client.advanced.debugging.debugWireframe]
#
# Render LOD section status?
showRenderSectionStatus = false
#
# Render queued network sync on load tasks?
showNetworkSyncOnLoadQueue = false
#
# Render full data update/lock status?
showFullDataUpdateStatus = false
#
# Render queued world gen tasks?
showWorldGenQueue = false
#
# Render Quad Tree Rendering status?
showQuadTreeRenderStatus = false
#
# If enabled, various wireframes for debugging internal functions will be drawn.
#
# NOTE: There WILL be performance hit!
# Additionally, only stuff that's loaded after you enable this
# will render their debug wireframes.
enableRendering = false
[client.advanced.debugging.openGl]
#
# Defines how OpenGL errors are handled.
# Requires rebooting Minecraft to change.
# Will catch OpenGL errors thrown by other mods.
overrideVanillaGLLogger = true
#
# Defines how OpenGL errors are handled.
# May incorrectly catch OpenGL errors thrown by other mods.
#
# IGNORE: Do nothing.
# LOG: write an error to the log.
# LOG_THROW: write to the log and throw an exception.
# Warning: this should only be enabled when debugging the LOD renderer
# as it may break Minecraft's renderer when an exception is thrown.
glErrorHandlingMode = "IGNORE"
#
# If true each Open GL error will only be logged once.
# Enabling this may cause some error logs to be missed.
# Does nothing if overrideVanillaGLLogger is set to false.
#
# Generally this can be kept as 'true' to prevent log spam.
# However, Please set this to 'false' if a developer needs your log to debug a GL issue.
onlyLogGlErrorsOnce = true
[client.advanced.debugging.exampleConfigScreen]
shortTest = "69"
mapTest = "{}"
byteTest = "8"
longTest = "42069"
listTest = ["option 1", "option 2", "option 3"]
boolTest = false
doubleTest = "420.69"
floatTest = "0.42069"
linkableTest = 420
intTest = 69420
stringTest = "Test input box"
[client.advanced.debugging.f3Screen]
#
# Shows info about the render thread tasks.
showRenderThreadTasks = false
#
# Shows how many chunks are queued for processing and the max count that can be queued.
showQueuedChunkUpdateCount = true
#
# Shows the memory use and array counts for each DH object pool.
showSeparatedObjectPools = false
#
# Shows the player's LOD position.
showPlayerPos = true
#
# Shows the combined memory use and array counts for all DH pooled objects.
showCombinedObjectPools = false
#
# Defines what internal detail level the player position will be shown as.
# Internal detail level means: 6 = 1x1 block, 7 = 2x2 blocks, etc.
playerPosSectionDetailLevel = 6
#
# Only show levels that DH is actively rendering.
onlyShowRenderingLevels = true
#
# Shows info about each thread pool.
showThreadPools = true
#
# Shows what levels are loaded and world gen/rendering info about those levels.
showLevelStatus = true
[client.advanced.debugging.positionFinderDebugging]
positionFinderEnable = false
positionFinderMinBlockY = -64
positionFinderZPos = 0
positionFinderXPos = 0
positionFinderMaxBlockY = 125
positionFinderDetailLevel = 6
positionFinderMarginPercent = "0.0"
[client.advanced.graphics]
#
# Enabling Anti-Aliasing smooths edges and reduces flickering when moving.
enableAntiAliasing = true
#
# Enable Screen Space Ambient Occlusion
enableSsao = false
#
# If true some vanilla graphics settings will be automatically changed
# during DH setup to provide a better experience.
#
# IE disabling vanilla clouds (which render on top of DH LODs),
# and chunk fading (DH already fades MC chunks)
overrideVanillaGraphicsSettings = true
[client.advanced.graphics.culling]
#
# Determines how far from the camera Distant Horizons will start rendering.
# Measured as a percentage of the vanilla render distance.
#
# -1 = auto, overdraw will change based on the vanilla render distance.
#
# Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,
# but may cause holes in the world.
# Holes are most likely to appear when flying through unloaded terrain.
#
# Increasing the vanilla render distance increases the effectiveness of this setting.
overdrawPrevention = "-1.0"
#
# Identical to the other frustum culling option
# only used when a shader mod is present using the DH API
# and the shadow pass is being rendered.
#
# Disable this if shadows render incorrectly.
disableShadowPassFrustumCulling = false
#
# A comma separated list of block resource locations that will be replaced by water
# if they're visible on the water's surface.
waterSubSurfaceBlockReplacementCsv = "minecraft:kelp,minecraft:tall_seagrass,minecraft:seagrass"
#
# A comma separated list of block resource locations that won't be rendered by DH.
# Air is always included in this list.
#
# Note:
# If you see gaps, or holes you may have to change
# worldCompression to [MERGE_SAME_BLOCKS] and re-generate the LODs.
ignoredRenderBlockCsv = "minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom"
#
# A comma separated list of block resource locations that will be removed
# when on top of water.
waterSurfaceBlockReplacementCsv = "minecraft:lily_pad"
#
# Defines what blocks should be rendered as LODs.
#
# NONE: Include all blocks in the LODs
# NON_COLLIDING: Only render solid blocks in the LODs (tall grass, torches, etc. will be ignored)
blocksToIgnore = "NON_COLLIDING"
#
# If true LODs outside the player's camera
# aren't drawn, increasing GPU performance.
#
# If false all LODs are drawn, even those behind
# the player's camera, decreasing GPU performance.
#
# Disable this if you see LODs disappearing at the corners of your vision.
disableFrustumCulling = false
#
# If false all beacons near the camera won't be drawn to prevent vanilla overdraw.
# If true all beacons will be rendered.
#
# Generally this should be left as true. It's main purpose is for debugging
# beacon updating/rendering.
disableBeaconDistanceCulling = true
#
# Should the blocks underneath avoided blocks gain the color of the avoided block?
#
# True: a red flower will tint the grass below it red.
# False: skipped blocks will not change color of surface below them.
tintWithAvoidedBlocks = true
#
# If enabled caves won't be rendered.
#
# Note: for some world types this can cause
# overhangs or walls for floating objects.
# Tweaking the caveCullingHeight, can resolve some
# of those issues.
enableCaveCulling = true
#
# At what Y value should cave culling start?
# Lower this value if you get walls for areas with 0 light.
caveCullingHeight = 60
#
# A comma separated list of block resource locations that shouldn't be rendered
# if they are in a 0 sky light underground area.
# Air is always included in this list.
#
# Defaults to an empty list since most cave blocks will be automatically ignored due to being:
# transparent, non-solid, or liquids, but new blocks can be added here if needed.
ignoredRenderCaveBlockCsv = ""
#
# If set to true the overdraw prevention radius will get closer
# to the camera when flying/moving quickly.
#
# This helps reduce issues where Minecraft can't load or
# generate chunks fast enough to keep up with DH.
reduceOverdrawWithFastMovement = true
[client.advanced.graphics.texture]
#
# A comma separated list of block resource locations
# that DH will render their sides using the bottom texture.
# Partial matches/incomplete resource locations will also match.
#
# Example: "minecraft:grass_block,nylium"
#
# Changes require a restart.
blocksDontUseSideTextureCsv = "grass_block,mycelium,nylium,dirt_path,grass"
#
# The highest detail level number that can render with block textures.
# At higher detail levels each LOD covers multiple blocks,
# which can essentially make textures smaller than a pixel and therefore invisible.
#
# Larger numbers texture more distant LODs
# but increase memory usage.
maxTexturedLodDetailLevel = 2
#
# If true nearby and zoomed-in LODs will render with their block's
# actual texture instead of a single flat color.
#
# Only applies to high detail LODs where the texture is actually visible,
# distant LODs always use flat colors.
# Some shader packs nay not have an affect.
enableTexturedLods = true
#
# A comma separated list of block resource locations
# that DH won't render textures on.
# Partial matches/incomplete resource locations will also match.
#
# Example: "minecraft:grass_block,nylium"
#
# Changes require a restart.
blocksDontRenderTextureCsv = "minecraft:bamboo"
#
# A comma separated list of tag names
# that DH will render their sides using the bottom texture.
#
# Example: "grass_blocks,nylium"
#
# Changes require a restart.
blockTagsDontUseSideTextureCsv = "grass_blocks,nylium"
#
# A comma separated list of block resource locations
# that DH will use rasterization to determine
# the face textures.
#
# Can be used to fix issues with mods/blocks where
# a side uses just part of a modeled block's texture.
#
# Example: "minecraft:beacon"
#
# Changes require a restart.
blocksAlwaysRasterizeTextureCsv = "minecraft:beacon"
[client.advanced.graphics.noiseTexture]
#
# Should a noise texture be applied to LODs?
#
# This is done to simulate textures and make the LODs appear more detailed.
enableNoiseTexture = true
#
# Defines how far should the noise texture render before it fades away. (in blocks)
# Set to 0 to disable noise from fading away
noiseDropoff = 1024
#
# How many steps of noise should be applied to LODs?
noiseSteps = 4
#
# How intense should the noise should be?
noiseIntensity = "0.05"
[client.advanced.graphics.experimental]
#
# A comma separated list of dimension resource locations where DH won't render.
#
# Example: "minecraft:the_nether,minecraft:the_end"
# For Minecraft 1.12.2 and older:
# Open the F3 screen and copy the dimension_name:ID shown in yellow. Example: "overworld:0"
#
# Note:
# Some DH settings will be disabled and/or changed to improve
# visuals when DH rendering is disabled.
ignoredDimensionCsv = ""
#
# Requires a restart to change.
#
# Options:
# AUTO - changes based on the most likely API for that MC version
# OPEN_GL - The Default for MC 1.21.11 and older (supports Iris shaders)
# BLAZE_3D - The Default for MC 26.1.2 and newer (supports Vulkan)
renderingEngine = "AUTO"
#
# This is the earth size ratio when applying the curvature shader effect.
# Note: Enabling this feature may cause rendering bugs.
#
# 0 = flat/disabled
# 1 = 1 to 1 (6,371,000 blocks)
# 100 = 1 to 100 (63,710 blocks)
# 10000 = 1 to 10000 (637.1 blocks)
#
# Note: Due to current limitations, the min value is [50]
# and the max value is 5000. Any values outside this range
# will be set to 0 (disabled).
earthCurveRatio = 0
[client.advanced.graphics.genericRendering]
#
# A comma separated separated list of dimension resource locations where DH clouds will render.
#
# Example: "minecraft:overworld,minecraft:the_end"
#
# Changes require a world re-load.
dimensionEnabledCloudRenderingCsv = "minecraft:overworld"
#
# If true LOD clouds will be rendered.
enableCloudRendering = true
#
# If true LOD beacon beams will be rendered wider at extreme distances,
# making them easier to see.
# If false all LOD beacon beams will only ever be 1 block wide.
expandDistantBeacons = true
#
# Sets the maximum height beacons will render up to.
#
# Requires a world re-load to take affect.
beaconRenderHeight = 6000
#
# If true LOD beacon beams will be rendered.
enableBeaconRendering = true
#
# If true non terrain objects will be rendered by DH.
# i.e. beacon beams and clouds.
enableGenericRendering = true
#
# False = DH will render a single layer of clouds, like vanilla Minecraft.
# True = DH will render 3 layers of clouds at different heights.
enableMultiLayerClouds = true
[client.advanced.graphics.quality]
#
# What is the maximum detail LODs can render at?
# Higher settings will increase memory and GPU usage.
#
# CHUNK: render 1 LOD for each Chunk.
# HALF_CHUNK: render 4 LODs for each Chunk.
# FOUR_BLOCKS: render 16 LODs for each Chunk.
# TWO_BLOCKS: render 64 LODs for each Chunk.
# BLOCK: render 256 LODs for each Chunk (width of one block).
#
# Fastest: CHUNK
# Fanciest: BLOCK
maxHorizontalResolution = "BLOCK"
#
# If true LODs will fade away as you get closer to them.
# If false LODs will cut off abruptly at a set distance from the camera.
# This setting is affected by the vanilla overdraw prevention config.
ditherDhFade = true
#
# If true DH will try to use the camera position when
# determining LOD quality drop-off.
# If false DH will use the player's position.
#
# Enabling helps free-cam mods render correctly.
# Disabling helps multi-camera mods render correctly (ie Immersive Portals or camera mods).
useCameraPositionForQualityDropOff = true
#
# How bright LOD colors are.
#
# 0 = black
# 1 = normal
# 2 = near white
brightnessMultiplier = "1.0"
#
# How many detail levels zooming in can increase LOD quality by.
# Higher numbers allow stronger zooms to render crisper terrain,
# but will increase memory and GPU usage while zoomed in.
#
# A vanilla spyglass needs 4 detail levels to reach its full quality.
maxZoomQualityIncrease = 4
#
# How should LODs be shaded?
#
# AUTO: Uses the same side shading as vanilla Minecraft blocks.
# ENABLED: Simulates Minecraft's block shading for LODs.
# Can be used to force LOD shading when using some shaders.
# DISABLED: All LOD sides will be rendered with the same brightness.
lodShading = "AUTO"
#
# How saturated LOD colors are.
#
# 0 = black and white
# 1 = normal
# 2 = very saturated
saturationMultiplier = "1.0"
#
# This indicates how well LODs will represent
# overhangs, caves, floating islands, etc.
# Higher options will make the world more accurate, butwill increase memory and GPU usage.
#
# Lowest Quality: HEIGHT_MAP
# Highest Quality: PIXEL_ART
verticalQuality = "MEDIUM"
#
# The radius of the mod's render distance. (measured in chunks)
#
# Note: this is a best effort number.
# The actual render distance may be above or below this number
# depending on your other graphic settings.
lodChunkRenderDistanceRadius = 128
#
# How should the sides and bottom of grass block LODs render?
#
# AS_GRASS: all sides of dirt LOD's render using the top (green) color.
# FADE_TO_DIRT: sides fade from grass to dirt.
# AS_DIRT: sides render entirely as dirt.
grassSideRendering = "FADE_TO_DIRT"
#
# This indicates how far apart drops in LOD quality are.
#
# Higher settings will increase the distance between drops
# but will increase memory and GPU usage.
horizontalQuality = "MEDIUM"
#
# How should LOD transparency be handled.
#
# COMPLETE: LODs will render transparent.
# DISABLED: LODs will be opaque.
transparency = "COMPLETE"
#
# This is the same as vanilla Biome Blending settings for Lod area.
# Note: anything above '0' will slow down LOD loading time.
#
# '0' equals to Vanilla Biome Blending of '1x1' or 'OFF',
# '1' equals to Vanilla Biome Blending of '3x3',
# '2' equals to Vanilla Biome Blending of '5x5'...
lodBiomeBlending = 3
#
# How should vanilla Minecraft fade into Distant Horizons LODs?
#
# NONE: Fastest, there will be a pronounced border between DH and MC rendering.
# SINGLE_PASS: Fades after MC's transparent pass, opaque blocks underwater won't be faded.
# DOUBLE_PASS: Slowest, fades after both MC's opaque and transparent passes, provides the smoothest transition.
vanillaFadeMode = "DOUBLE_PASS"
#
# If true LOD quality will increase when the camera is zoomed in,
# IE when using a spyglass or zoom mod.
#
# Only LODs visible through the camera view are affected.
#
# When zoomed in LODs will load to the same detail level
# they would have if you were close to them.
increaseQualityWhenZoomedIn = true
[client.advanced.graphics.fog]
#
# Should Minecraft's fog render?
# Note: Other mods may conflict with this setting.
enableVanillaFog = false
#
# What is the maximum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
farFogMax = "1.0"
#
# Determines if fog is drawn on DH LODs.
enableDhFog = true
#
# At what distance should the far fog start?
#
# 0.0: Fog starts at the player's position.
# 1.0: Fog starts at the closest edge of the vanilla render distance.
# 1.414: Fog starts at the corner of the vanilla render distance.
farFogStart = "0.4"
#
# What is the minimum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
farFogMin = "0.0"
#
# What color should fog use?
#
# USE_WORLD_FOG_COLOR: Use the world's fog color.
# USE_SKY_COLOR: Use the sky's color.
colorMode = "USE_WORLD_FOG_COLOR"
#
# How should the fog thickness should be calculated?
#
# LINEAR: Linear based on distance (will ignore 'density')
# EXPONENTIAL: 1/(e^(distance*density))
# EXPONENTIAL_SQUARED: 1/(e^((distance*density)^2)
farFogFalloff = "EXPONENTIAL_SQUARED"
#
# Used in conjunction with the Fog Falloff.
farFogDensity = "2.5"
#
# Where should the far fog end?
#
# 0.0: Fog ends at player's position.
# 1.0: Fog ends at the closest edge of the vanilla render distance.
# 1.414: Fog ends at the corner of the vanilla render distance.
farFogEnd = "1.0"
[client.advanced.graphics.fog.heightFog]
#
# Where should the height fog start?
#
# ABOVE_CAMERA: Height fog starts at the camera and goes towards the sky
# BELOW_CAMERA: Height fog starts at the camera and goes towards the void
# ABOVE_AND_BELOW_CAMERA: Height fog starts from the camera to goes towards both the sky and void
# ABOVE_SET_HEIGHT: Height fog starts from a set height and goes towards the sky
# BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards the void
# ABOVE_AND_BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards both the sky and void
heightFogDirection = "BELOW_SET_HEIGHT"
#
# What is the minimum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
heightFogMin = "0.0"
#
# If the height fog is calculated around a set height, what is that height position?
heightFogBaseHeight = "80.0"
#
# What is the maximum fog thickness?
#
# 0.0: No fog.
# 1.0: Fully opaque fog.
heightFogMax = "1.0"
#
# How should the height fog thickness should be calculated?
#
# LINEAR: Linear based on height (will ignore 'density')
# EXPONENTIAL: 1/(e^(height*density))
# EXPONENTIAL_SQUARED: 1/(e^((height*density)^2)
heightFogFalloff = "EXPONENTIAL_SQUARED"
#
# What is the height fog's density?
heightFogDensity = "20.0"
#
# How should height effect the fog thickness?
# Note: height fog is combined with the other fog settings.
#
# SPHERICAL: Fog is calculated based on camera distance.
# CYLINDRICAL: Ignore height, fog is calculated based on horizontal distance.
#
# MAX: max(heightFog, farFog)
# ADDITION: heightFog + farFog
# MULTIPLY: heightFog * farFog
# INVERSE_MULTIPLY: 1 - (1-heightFog) * (1-farFog)
# LIMITED_ADDITION: farFog + max(farFog, heightFog)
# MULTIPLY_ADDITION: farFog + farFog * heightFog
# INVERSE_MULTIPLY_ADDITION: farFog + 1 - (1-heightFog) * (1-farFog)
# AVERAGE: farFog*0.5 + heightFog*0.5
heightFogMixMode = "SPHERICAL"
#
# Should the start of the height fog be offset?
#
# 0.0: Fog start with no offset.
# 1.0: Fog start with offset of the entire world's height. (Includes depth)
heightFogStart = "0.0"
#
# Should the end of the height fog be offset?
#
# 0.0: Fog end with no offset.
# 1.0: Fog end with offset of the entire world's height. (Include depth)
heightFogEnd = "0.6"
[client.advanced.multiplayer]
#
# How should multiplayer save folders should be named?
#
# NAME_ONLY: Example: "Minecraft Server"
# IP_ONLY: Example: "192.168.1.40"
# NAME_IP: Example: "Minecraft Server IP 192.168.1.40"
# NAME_IP_PORT: Example: "Minecraft Server IP 192.168.1.40:25565"NAME_IP_PORT_MC_VERSION: Example: "Minecraft Server IP 192.168.1.40:25565 GameVersion 1.16.5"
serverFolderNameMode = "NAME_ONLY"
@@ -0,0 +1,36 @@
{
"allowedCEM": "ALL",
"logModelCreationData": false,
"debugOnRightClick": false,
"renderModeChoice": "NORMAL",
"vanillaModelHologramRenderMode_2": "OFF",
"modelExportMode": "NONE",
"automaticModelExporting": false,
"attemptPhysicsModPatch_2": "CUSTOM",
"modelUpdateFrequency": "Slow",
"entityRenderModeOverrides": {},
"entityPhysicsModPatchOverrides": {},
"entityVanillaHologramOverrides": {},
"modelsNamesDisabled": [],
"allowEBEModConfigModify": true,
"animationLODDistance": 12,
"retainDetailOnLowFps": true,
"retainDetailOnLargerMobs": true,
"animationFrameSkipDuringIrisShadowPass": true,
"preventFirstPersonHandAnimating": false,
"onlyClientPlayerModel": false,
"doubleChestAnimFix": true,
"enforceOptifineVariationRequiresDefaultModel": false,
"enforceOptifineVariationRequiresDefaultModel_v2": false,
"resetPlayerModelEachRender": true,
"resetPlayerModelEachRender_v2": true,
"onlyDebugRenderOnHover": false,
"enforceOptifineSubFoldersVariantOnly": false,
"enforceOptiFineAnimSyntaxLimits": true,
"allowOptifineFallbackProperties": true,
"showReloadErrorToast": true,
"exportRotations": false,
"asmMaths": true,
"logASM": false,
"parrotShoulderPositionAnimatesByDefault": true
}
@@ -0,0 +1,14 @@
#Select the backend to use. Set to "DEFAULT" to let Flywheel decide.
backend = "DEFAULT"
#Enable or disable instance update limiting with distance.
limitUpdates = true
#The number of worker threads to use. Set to -1 to let Flywheel decide. Set to 0 to disable parallelism. Requires a game restart to take effect.
# Default: -1
# Range: -1 ~ 4
workerThreads = -1
#Config options for Flywheel's built-in backends.
[flw_backends]
#How smooth Flywheel's shader-based lighting should be. May have a large performance impact.
#Allowed Values: FLAT, TRI_LINEAR, SMOOTH, SMOOTH_INNER_FACE_CORRECTED
lightSmoothness = "TRI_LINEAR"
@@ -0,0 +1,16 @@
mode = "fast"
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,20 @@
# Partial file: SPR merges these over its generated per-sound defaults (-1 = unlimited).
create\:cogs=4
create\:mixing=4
create\:mixing_compounded_1=4
create\:mechanical_press_activation=4
create\:mechanical_press_activation_compounded_1=4
create\:mechanical_press_activation_belt=4
create\:mechanical_press_activation_belt_compounded_1=4
create\:crushing_1=4
create\:crushing_2=4
create\:crushing_3=4
create\:saw_activate_stone=4
create\:saw_activate_wood=4
create\:funnel_flap=4
create\:funnel_flap_compounded_1=4
create\:depot_slide=4
create\:depot_plop=4
create\:steam=4
create\:sanding_short=4
create\:sanding_long=4
@@ -0,0 +1,99 @@
# Enables/Disables all sound effects
enabled=true
# Affects how quiet a sound gets based on distance
# Lower values mean distant sounds are louder
# This setting requires you to be in singleplayer or having the mod installed on the server
# 1.0 is the physically correct value
attenuation_factor=1.0
# The ray distance at which reverb starts
# 0.0 disables reverb attenuation
reverb_attenuation_distance=0.0
# The volume of simulated reverberations
reverb_gain=1.0
# The brightness of reverberation
# Higher values result in more high frequencies in reverberation
# Lower values give a more muffled sound to the reverb
reverb_brightness=1.0
# The distance of reverb relative to the sound distance
reverb_distance=1.5
# The amount of sound that will be absorbed when traveling through blocks
block_absorption=1.0
# Higher values mean smaller objects won't be considered as occluding
occlusion_variation=0.35
# The default amount of sound reflectance energy for all blocks
# Lower values result in more conservative reverb simulation with shorter reverb tails
# Higher values result in more generous reverb simulation with higher reverb tails
default_block_reflectivity=0.5
# The default amount of occlusion for all blocks
# Lower values will result in sounds being less muffled through walls
# Higher values mean sounds will be not audible though thicker walls
default_block_occlusion_factor=1.0
# Minecraft won't allow sounds to play past a certain distance
# This parameter is a multiplier for how far away a sound source is allowed to be in order for it to actually play
# This setting only takes affect in singleplayer worlds and when installed on the server
sound_distance_allowance=4.0
# A value controlling the amount that air absorbs high frequencies with distance
# A value of 1.0 is physically correct for air with normal humidity and temperature
# Higher values mean air will absorb more high frequencies with distance
# 0 disables this effect
air_absorption=1.0
# How much sound is filtered when the player is underwater
# 0.0 means no filter
# 1.0 means fully filtered
underwater_filter=0.9
# Whether sounds like cave, nether or underwater ambient sounds should have sound physics
evaluate_ambient_sounds=false
# The number of rays to trace to determine reverberation for each sound source
# More rays provides more consistent tracing results but takes more time to calculate
# Decrease this value if you experience lag spikes when sounds play
environment_evaluation_ray_count=16
# The number of rays bounces to trace to determine reverberation for each sound source
# More bounces provides more echo and sound ducting but takes more time to calculate
# Decrease this value if you experience lag spikes when sounds play
environment_evaluation_ray_bounces=2
# If sound hits a non-full-square side, block occlusion is multiplied by this
non_full_block_occlusion_factor=0.25
# The maximum amount of rays to determine occlusion
# Directly correlates to the amount of blocks between walls that are considered
max_occlusion_rays=16
# The amount at which occlusion is capped
max_occlusion=64.0
# If enabled, the occlusion calculation only uses one path between the sound source and the listener instead of 9
strict_occlusion=false
# Whether to try calculating where the sound should come from based on reflections
sound_direction_evaluation=true
# Skip redirecting non-occluded sounds (the ones you can see directly)
redirect_non_occluded_sounds=true
# If music discs or other longer sounds should be frequently reevaluated
update_moving_sounds=false
# The interval in ticks that moving sounds are reevaluated
# Lower values mean more frequent reevaluation but also more lag
# This option only takes effect if update_moving_sounds is enabled
sound_update_interval=5
# The maximum distance a sound can be processed
max_sound_processing_distance=512.0
# Disable level clone and cache. This will fall back to original main thread access.
# WARNING! Enabling this will cause instability and issues with other mods.
unsafe_level_access=false
# The radius of chunks to clone for level access
level_clone_range=4
# The maximum number of ticks to retain the cloned level in the cache
level_clone_max_retain_ticks=20
# The maximum distance a player can move from the cloned origin before invalidation
level_clone_max_retain_block_distance=16
# Enables debug logging
debug_logging=false
# Provides more information about occlusion in the logs
occlusion_logging=false
# Provides more information about the environment calculation in the logs
environment_logging=false
# Provides more information about how long computations take
performance_logging=false
# If enabled, the path of the sound will be rendered in game
render_sound_bounces=false
# If enabled, occlusion will be visualized in game
render_occlusion=false
# Enables/Disables sound effects for Simple Voice Chat audio
simple_voice_chat_integration=true
# Enables/Disables hearing your own echo with Simple Voice Chat
simple_voice_chat_hear_self=false
@@ -0,0 +1,12 @@
#The render distance for physics objects (SubLevels) in chunks.
# Default: 128
# Range: 1 ~ 1000000
physicsRenderDistance = 16
#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
+11
View File
@@ -0,0 +1,11 @@
version:3955
renderDistance:8
simulationDistance:8
biomeBlendRadius:1
mipmapLevels:2
entityDistanceScaling:0.75
particles:0
entityShadows:true
graphicsMode:1
maxFps:120
enableVsync:true
+124 -10
View File
@@ -4,14 +4,6 @@ hash-format = "sha256"
file = "config/akashictome-common.toml"
hash = "4e479c86ddfec0c5e28f07034f033a10abae404309f38ccc4bfc15da9de2dfa2"
[[files]]
file = "config/alexsmobs-common.toml"
hash = "bc47ee67032ceab8b2b408819ce9b268812f37da8f13b9b7efeae0deed607c4b"
[[files]]
file = "config/crittersandcompanions-common.json5"
hash = "a00a9fe09ce5b469247f757e780b5faf74191f5cfef158d999c4d7a1e203f420"
[[files]]
file = "config/defaultoptions/extra/config/DistantHorizons.toml"
hash = "8949bf3c7a43df7a7ae384213db0985425db3559219c29cb63eaef45c7962dcc"
@@ -45,8 +37,108 @@ file = "config/defaultoptions/options.txt"
hash = "62034e0589087dab02d1a15192ad313a57f907d5ee9ebbf8853151f7831851a1"
[[files]]
file = "config/fowlplay.json5"
hash = "09ad767a358310e9d38e632ba0e670b0afd1554c162eadb6928d106df17b66c3"
file = "config/modernfix-mixins.properties"
hash = "ca92e91a2d856d42e4a5b1ba1900fbed0e942bcbeddd75c0195a2375ade75695"
[[files]]
file = "config/onion-perf/deck/config/DistantHorizons.toml"
hash = "46910495d79b540beed8137765fff4e14be3fd1860a5cadc73b38a6fbc0f3984"
[[files]]
file = "config/onion-perf/deck/config/entity_model_features.json"
hash = "89a8e21f43f512d78bfbc3ffb97b6d65cedc732a65ee565aed46499aa79fd57d"
[[files]]
file = "config/onion-perf/deck/config/flywheel-client.toml"
hash = "e10963bf43711e1d326e076a5b5baa5654b73d1a3878da6aa11c373b47c9e3ac"
[[files]]
file = "config/onion-perf/deck/config/lambdynlights.toml"
hash = "53a0c285438775ce29b8d44180feba1fd17b496eb0bbe5cb18684f2af1fdd3c9"
[[files]]
file = "config/onion-perf/deck/config/sodium-options.json"
hash = "c8c04fb322fc59c73e446f665522c3f0cdaafe6143b94b5895bb07f267489235"
[[files]]
file = "config/onion-perf/deck/config/sound_physics_remastered/sound_rates.properties"
hash = "c4676fcc924cc4bacc187d5364831aac1be170b93a2dc8e11119bff0235353e9"
[[files]]
file = "config/onion-perf/deck/config/sound_physics_remastered/soundphysics.properties"
hash = "912706e845015c30604db7ad0020a4ca95cdf4a94f7cad9eb8a166eab83f8279"
[[files]]
file = "config/onion-perf/deck/config/ssrd-common.toml"
hash = "9b75828543ed2b8cc5804eabecdc28262297a90d6f0e8e771d64ff779a69e3c9"
[[files]]
file = "config/onion-perf/deck/options.txt"
hash = "c021331ed6ac5150025ccf8f61fc96793fdf2d2950617a85b68b977f7510ee47"
[[files]]
file = "config/onion-perf/high/config/DistantHorizons.toml"
hash = "b4070cb07cf2fb9c7b14a2c49bc6b45b6aeb26d650fe8de80e4f9cbe67cb3ff5"
[[files]]
file = "config/onion-perf/high/config/entity_model_features.json"
hash = "7650058e21d1eaafc14cc1879bc20c2b510eb9f3095914322b8b08fc31025437"
[[files]]
file = "config/onion-perf/high/config/flywheel-client.toml"
hash = "8171fd8500c7bbd52df9fe4f8ea1b7abdc6a24868a9e7e54fa908b4725382bde"
[[files]]
file = "config/onion-perf/high/config/lambdynlights.toml"
hash = "0406650cc9d47011360284e0019f237ba3c94fc3067dc2bb4171f9a5fbea8367"
[[files]]
file = "config/onion-perf/high/config/sound_physics_remastered/sound_rates.properties"
hash = "c4676fcc924cc4bacc187d5364831aac1be170b93a2dc8e11119bff0235353e9"
[[files]]
file = "config/onion-perf/high/config/sound_physics_remastered/soundphysics.properties"
hash = "912706e845015c30604db7ad0020a4ca95cdf4a94f7cad9eb8a166eab83f8279"
[[files]]
file = "config/onion-perf/high/config/ssrd-common.toml"
hash = "9b744451175d440c4f2567d1fdd9799c2dc434012f5b1edb33cc7550193c4cef"
[[files]]
file = "config/onion-perf/high/options.txt"
hash = "22900375cf1d9a608d9d19c8bdbd5bd497d47f66a11a57182de576e09497fd7d"
[[files]]
file = "config/onion-perf/mid/config/DistantHorizons.toml"
hash = "8949bf3c7a43df7a7ae384213db0985425db3559219c29cb63eaef45c7962dcc"
[[files]]
file = "config/onion-perf/mid/config/entity_model_features.json"
hash = "7650058e21d1eaafc14cc1879bc20c2b510eb9f3095914322b8b08fc31025437"
[[files]]
file = "config/onion-perf/mid/config/flywheel-client.toml"
hash = "517e16b3dc86f15b5dccd9ff4f70f767b73ebc2166f6ea2891565417d930772a"
[[files]]
file = "config/onion-perf/mid/config/lambdynlights.toml"
hash = "3876c8b0c49cc8c2a7f500bc5d77429035e4edb25e16d27434e1dd4d546ae510"
[[files]]
file = "config/onion-perf/mid/config/sound_physics_remastered/sound_rates.properties"
hash = "c4676fcc924cc4bacc187d5364831aac1be170b93a2dc8e11119bff0235353e9"
[[files]]
file = "config/onion-perf/mid/config/sound_physics_remastered/soundphysics.properties"
hash = "d93803ffc12f4b55f9668dc2b9a4823d733b77f02dfee0879d6507f262ad34c6"
[[files]]
file = "config/onion-perf/mid/config/ssrd-common.toml"
hash = "9b75828543ed2b8cc5804eabecdc28262297a90d6f0e8e771d64ff779a69e3c9"
[[files]]
file = "config/onion-perf/mid/options.txt"
hash = "62034e0589087dab02d1a15192ad313a57f907d5ee9ebbf8853151f7831851a1"
[[files]]
file = "config/paxi/datapacks/onion-fixes/data/adorablehamsterpets/recipe/cucumber_crate.json"
@@ -729,6 +821,11 @@ file = "mods/inventory-particles.pw.toml"
hash = "7ff791a8a6074a5f580a0a1899de69cff45ccf6db0a99d83494d57ff1a4cee43"
metafile = true
[[files]]
file = "mods/iris-flw-compat.pw.toml"
hash = "ed85b2ed42beb6523223eef6f0ae397ed29f8059b2bf5aa08d8598de04865614"
metafile = true
[[files]]
file = "mods/iris-veil-compat.pw.toml"
hash = "69f9051c61c05546a2bf008654149a101814842bc2bf384df5c0ee0d64301ccd"
@@ -844,6 +941,11 @@ file = "mods/mezzconfig.pw.toml"
hash = "efad386063cb79013d7522fcc5b5879c5c3cd556e83bd41a56980632827b45b2"
metafile = true
[[files]]
file = "mods/midnightlib.pw.toml"
hash = "f7198ef16f4eefbe80ffd1d8ac7c2d323da4e68faf916a8b4b9c3e40f0913b80"
metafile = true
[[files]]
file = "mods/miners-delight.pw.toml"
hash = "13481133d139f134bd3407983e29f86df835ff66bacb013d9d43e14f155241f4"
@@ -1309,6 +1411,18 @@ file = "mods/zume.pw.toml"
hash = "165466fe8e528309d95875396f2a5e13c0bab7bcb0c1134d1fd5f3a935d488f5"
metafile = true
[[files]]
file = "onion-perf-setup.bat"
hash = "8e8e613429e5a2243829fa023c1dcc36b874ce42176bfa55dfa3c8a73122ccfd"
[[files]]
file = "onion-perf-setup.ps1"
hash = "86e12ff56f13229c3b0159eb2ea0cba403f35c6221be67037ea8481d370fa30d"
[[files]]
file = "onion-perf-setup.sh"
hash = "ecd5dfd5e135db6e9a98df65a7176cc2a9b3e14d7a140a3dcce90aae64d06519"
[[files]]
file = "resourcepacks/better-leaves.pw.toml"
hash = "c26f8da8b6f013fb7e917e29fc6ae783eb379c539da12d9d61196bc762fd26e1"
+13
View File
@@ -0,0 +1,13 @@
name = "Iris & Oculus Flywheel Compat"
filename = "iris-flywheel-compat-NeoForge-2.4.0.jar"
side = "client"
[download]
url = "https://cdn.modrinth.com/data/ndHYMY2K/versions/6ZQz5Ow2/iris-flywheel-compat-NeoForge-2.4.0.jar"
hash-format = "sha512"
hash = "192e5f00f2c146d2920867d7cd691ecec7ddd2acfb95880cc59084e8949225db92ecfb9221ba7223fc924ec43f2ca861115f83f9dc3f79e14a2f2a8db5287070"
[update]
[update.modrinth]
mod-id = "ndHYMY2K"
version = "6ZQz5Ow2"
+13
View File
@@ -0,0 +1,13 @@
name = "MidnightLib"
filename = "midnightlib-neoforge-1.10.0+1.21.1.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/codAaoxh/versions/vcVnfiCM/midnightlib-neoforge-1.10.0%2B1.21.1.jar"
hash-format = "sha512"
hash = "9586940ec144452577455b61fd7c9f7ee123044a47255f9425be1bfcd4e842d5902b66c7f5546540281cad8c12e30d38fc29e8fb3aa3cb592ed13ff430316e68"
[update]
[update.modrinth]
mod-id = "codAaoxh"
version = "vcVnfiCM"
+7
View File
@@ -0,0 +1,7 @@
@echo off
rem Onion MC: apply a performance tier (deck / mid / high) to this instance.
rem Double-click it in the instance folder, or run: onion-perf-setup.bat high --yes
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0onion-perf-setup.ps1" %*
set ONION_RC=%ERRORLEVEL%
pause
exit /b %ONION_RC%
+183
View File
@@ -0,0 +1,183 @@
# Onion MC: apply a performance tier's tuned settings to this instance.
#
# onion-perf-setup.bat [deck|mid|high] [--yes] (or run this .ps1 directly)
#
# Lives in the instance's game folder (the one that holds config\ and options.txt)
# after a pack update. Close Minecraft first. Every file it changes is backed up to
# onion-perf-backup\<date-time>\ first. options.txt is merged key by key: only the
# tier's video keys change; keybinds, audio, resource packs etc. are left alone.
# No network access. Windows PowerShell 5.1 compatible.
$ErrorActionPreference = 'Stop'
function Fail([string]$msg) {
[Console]::Error.WriteLine("error: $msg")
exit 1
}
$Game = $PSScriptRoot
if (-not $Game) { $Game = Split-Path -Parent $MyInvocation.MyCommand.Path }
$Tier = ''
$Yes = $false
foreach ($a in $args) {
$s = ([string]$a).ToLowerInvariant()
if (@('deck', 'mid', 'high') -contains $s) {
if ($Tier) { Fail 'more than one tier given' }
$Tier = $s
} elseif (@('-y', '--yes', '-yes', '/y') -contains $s) {
$Yes = $true
} else {
Fail "unknown argument '$a' (use: deck | mid | high, optionally --yes)"
}
}
$Perf = Join-Path $Game 'config\onion-perf'
if (-not (Test-Path -LiteralPath (Join-Path $Perf 'mid') -PathType Container)) {
Fail "$Perf not found. Update the modpack first (it ships these settings), and run this script from the instance's game folder."
}
$Interactive = [Environment]::UserInteractive -and -not [Console]::IsInputRedirected
if (-not $Tier) {
if ($Interactive) {
Write-Host 'Which tier fits this machine?'
Write-Host ' deck Steam Deck, handheld, laptop/integrated graphics'
Write-Host ' mid GTX 1060 / RX 580 class (~6 GB VRAM), 4-6 core CPU [pack default]'
Write-Host ' high 8 GB+ VRAM graphics card (GTX 1080 Ti, RTX 3060 and up)'
$Tier = (Read-Host 'Tier [mid]').Trim().ToLowerInvariant()
if (-not $Tier) { $Tier = 'mid' }
if (@('deck', 'mid', 'high') -notcontains $Tier) { Fail "unknown tier '$Tier'" }
} else {
$Tier = 'mid'
}
}
$Src = Join-Path $Perf $Tier
if (-not (Test-Path -LiteralPath $Src -PathType Container)) { Fail "$Src not found. Update the modpack first." }
if (-not $Yes) {
if (-not $Interactive) { Fail 'not running interactively: close Minecraft, then re-run with --yes' }
$ans = Read-Host 'Is Minecraft closed? [y/N]'
if (@('y', 'yes') -notcontains $ans.Trim().ToLowerInvariant()) {
Write-Host 'Close Minecraft and run this again.'
exit 1
}
}
$Stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$Backup = Join-Path $Game (Join-Path 'onion-perf-backup' $Stamp)
$n = 1
while (Test-Path -LiteralPath $Backup) {
$Backup = Join-Path $Game (Join-Path 'onion-perf-backup' "$Stamp-$n")
$n++
}
# .NET file calls, not New-Item/Copy-Item: instance folders often contain '[' (Prism
# names the game dir after the instance) and -Path/Copy-Item -Destination glob those.
[IO.Directory]::CreateDirectory($Backup) | Out-Null
function Save-Backup([string]$rel) {
$from = Join-Path $Game $rel
if (-not (Test-Path -LiteralPath $from -PathType Leaf)) { return }
$to = Join-Path $Backup $rel
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($to)) | Out-Null
[IO.File]::Copy($from, $to, $true)
}
function Test-SameFile([string]$a, [string]$b) {
if (-not (Test-Path -LiteralPath $b -PathType Leaf)) { return $false }
$x = [IO.File]::ReadAllBytes($a)
$y = [IO.File]::ReadAllBytes($b)
if ($x.Length -ne $y.Length) { return $false }
return [Convert]::ToBase64String($x) -ceq [Convert]::ToBase64String($y)
}
$replaced = 0
$unchanged = 0
Write-Host "Applying tier '$Tier' to $Game"
$srcConfig = Join-Path $Src 'config'
$files = @()
if (Test-Path -LiteralPath $srcConfig) {
$srcConfig = (Get-Item -LiteralPath $srcConfig).FullName.TrimEnd('\', '/')
$files = @(Get-ChildItem -LiteralPath $srcConfig -Recurse -File | Sort-Object FullName)
}
foreach ($f in $files) {
$rel = 'config' + $f.FullName.Substring($srcConfig.Length)
$dst = Join-Path $Game $rel
if (Test-SameFile $f.FullName $dst) { $unchanged++; continue }
Save-Backup $rel
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($dst)) | Out-Null
[IO.File]::Copy($f.FullName, $dst, $true)
Write-Host " replaced $($rel -replace '\\', '/')"
$replaced++
}
$Opt = Join-Path $Game 'options.txt'
$SrcOpt = Join-Path $Src 'options.txt'
$Utf8 = New-Object Text.UTF8Encoding $false
if (-not (Test-Path -LiteralPath $SrcOpt -PathType Leaf)) {
Write-Host ' (tier has no options.txt)'
} elseif (-not (Test-Path -LiteralPath $Opt -PathType Leaf)) {
Write-Host ' options.txt not found: skipped (Default Options seeds it on first launch)'
} else {
$skip = @('version', 'maxFps', 'enableVsync')
$want = @{}
$order = New-Object System.Collections.Generic.List[string]
foreach ($line in [IO.File]::ReadAllLines($SrcOpt, $Utf8)) {
$i = $line.IndexOf(':')
if ($i -lt 0) { continue }
$k = $line.Substring(0, $i)
if ($skip -ccontains $k) { continue }
if (-not $want.ContainsKey($k)) { $order.Add($k) }
$want[$k] = $line.Substring($i + 1)
}
$raw = [IO.File]::ReadAllText($Opt, $Utf8)
$nl = "`n"
if ($raw.Contains("`r`n")) { $nl = "`r`n" }
$lines = $raw -split "`r?`n"
$trailing = $raw.EndsWith("`n")
if ($trailing) { $lines = $lines[0..($lines.Length - 2)] }
if ($raw.Length -eq 0) { $lines = @() }
$out = New-Object System.Collections.Generic.List[string]
$done = @{}
$changes = New-Object System.Collections.Generic.List[string]
foreach ($line in $lines) {
$i = $line.IndexOf(':')
if ($i -gt 0) {
$k = $line.Substring(0, $i)
if ($want.ContainsKey($k) -and -not $done.ContainsKey($k)) {
$v = $line.Substring($i + 1)
if ($v -cne $want[$k]) { $changes.Add(" ${k}: $v -> $($want[$k])") }
$out.Add("${k}:$($want[$k])")
$done[$k] = $true
continue
}
}
$out.Add($line)
}
foreach ($k in $order) {
if (-not $done.ContainsKey($k)) {
$changes.Add(" ${k}: (unset) -> $($want[$k])")
$out.Add("${k}:$($want[$k])")
}
}
if ($changes.Count -gt 0) {
Save-Backup 'options.txt'
# WriteAllText (not WriteAllLines): same UTF-8-without-BOM bytes, but joins with
# the file's own line endings - WriteAllLines always uses Environment.NewLine.
$text = [string]::Join($nl, $out.ToArray()) + $nl
[IO.File]::WriteAllText($Opt, $text, $Utf8)
Write-Host ' options.txt keys changed:'
foreach ($c in $changes) { Write-Host $c }
} else {
Write-Host ' options.txt: already matches the tier'
}
}
if (@(Get-ChildItem -LiteralPath $Backup -Force).Count -eq 0) {
Remove-Item -LiteralPath $Backup -Force
$Backup = '(nothing changed, no backup needed)'
}
Write-Host "Done: tier '$Tier'; $replaced file(s) replaced, $unchanged already up to date."
Write-Host "Backup: $Backup"
Write-Host 'Also set Java memory in your launcher (5 GB is enough): see PLAYER-TUNING.md section 2.'
exit 0
+157
View File
@@ -0,0 +1,157 @@
#!/bin/sh
# Onion MC: apply a performance tier's tuned settings to this instance.
#
# sh onion-perf-setup.sh [deck|mid|high] [--yes]
#
# Run it from (or point it at) the instance's game folder - the one that holds
# config/ and options.txt; this script lives there after a pack update.
# Close Minecraft first. Every file it changes is backed up to
# onion-perf-backup/<date-time>/ first. options.txt is merged key by key: only the
# tier's video keys change; keybinds, audio, resource packs etc. are left alone.
# No network access. Windows: double-click onion-perf-setup.bat instead.
die() { echo "error: $*" >&2; exit 1; }
case "$0" in
*/*) GAME=$(cd "${0%/*}" && pwd) || die "cannot find the script's folder" ;;
*) GAME=$(pwd) ;;
esac
TIER=""
YES=0
for a in "$@"; do
case "$a" in
deck|mid|high) [ -z "$TIER" ] || die "more than one tier given"; TIER=$a ;;
-y|--yes) YES=1 ;;
-h|--help) sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "unknown argument '$a' (use: deck | mid | high, optionally --yes)" ;;
esac
done
PERF="$GAME/config/onion-perf"
[ -d "$PERF/mid" ] || die "$PERF not found. Update the modpack first (it ships these
settings), and run this script from the instance's game folder."
INTERACTIVE=0
[ -t 0 ] && INTERACTIVE=1
if [ -z "$TIER" ]; then
if [ "$INTERACTIVE" = 1 ]; then
cat <<'EOF'
Which tier fits this machine?
deck Steam Deck, handheld, laptop/integrated graphics
mid GTX 1060 / RX 580 class (~6 GB VRAM), 4-6 core CPU [pack default]
high 8 GB+ VRAM graphics card (GTX 1080 Ti, RTX 3060 and up)
EOF
printf 'Tier [mid]: '
read -r TIER || TIER=""
[ -n "$TIER" ] || TIER=mid
case "$TIER" in deck|mid|high) ;; *) die "unknown tier '$TIER'" ;; esac
else
TIER=mid
fi
fi
SRC="$PERF/$TIER"
[ -d "$SRC" ] || die "$SRC not found. Update the modpack first."
if [ "$YES" != 1 ]; then
[ "$INTERACTIVE" = 1 ] || die "not running interactively: close Minecraft, then re-run with --yes"
printf 'Is Minecraft closed? [y/N]: '
read -r ans || ans=""
case "$ans" in y|Y|yes|YES|Yes) ;; *) echo "Close Minecraft and run this again."; exit 1 ;; esac
fi
STAMP=$(date +%Y%m%d-%H%M%S)
BACKUP="$GAME/onion-perf-backup/$STAMP"
n=1
while [ -e "$BACKUP" ]; do BACKUP="$GAME/onion-perf-backup/$STAMP-$n"; n=$((n + 1)); done
mkdir -p "$BACKUP" || die "cannot create $BACKUP"
backup() { # backup <path relative to the game dir>, if it exists
[ -f "$GAME/$1" ] || return 0
case "$1" in */*) mkdir -p "$BACKUP/${1%/*}" || die "cannot create backup folder" ;; esac
cp -p "$GAME/$1" "$BACKUP/$1" || die "cannot back up $1"
}
LIST=$(mktemp "${TMPDIR:-/tmp}/onion-perf.XXXXXX") || die "mktemp failed"
REPORT=$(mktemp "${TMPDIR:-/tmp}/onion-perf.XXXXXX") || die "mktemp failed"
trap 'rm -f "$LIST" "$REPORT"' EXIT
(cd "$SRC" && find config -type f) | sort > "$LIST" || die "cannot list $SRC"
replaced=0
unchanged=0
echo "Applying tier '$TIER' to $GAME"
while IFS= read -r rel; do
if [ -f "$GAME/$rel" ] && cmp -s "$SRC/$rel" "$GAME/$rel"; then
unchanged=$((unchanged + 1))
continue
fi
backup "$rel"
case "$rel" in */*) mkdir -p "$GAME/${rel%/*}" || die "cannot create folder for $rel" ;; esac
cp "$SRC/$rel" "$GAME/$rel" || die "cannot write $rel"
echo " replaced $rel"
replaced=$((replaced + 1))
done < "$LIST"
OPT="$GAME/options.txt"
if [ ! -f "$SRC/options.txt" ]; then
echo " (tier has no options.txt)"
elif [ ! -f "$OPT" ]; then
echo " options.txt not found: skipped (Default Options seeds it on first launch)"
else
TMP="$OPT.onion-perf.tmp"
awk -v rep="$REPORT" '
NR == FNR {
sub(/\r$/, "")
i = index($0, ":")
if (i == 0) next
k = substr($0, 1, i - 1)
if (k == "version" || k == "maxFps" || k == "enableVsync") next
if (!(k in want)) order[++n] = k
want[k] = substr($0, i + 1)
next
}
{
cr = ""
if (sub(/\r$/, "")) { cr = "\r"; crlf = 1 }
i = index($0, ":")
if (i > 0) {
k = substr($0, 1, i - 1)
if ((k in want) && !(k in done)) {
v = substr($0, i + 1)
if (v != want[k]) print " " k ": " v " -> " want[k] > rep
print k ":" want[k] cr
done[k] = 1
next
}
}
print $0 cr
}
END {
for (j = 1; j <= n; j++) {
k = order[j]
if (!(k in done)) {
print " " k ": (unset) -> " want[k] > rep
print k ":" want[k] (crlf ? "\r" : "")
}
}
}
' "$SRC/options.txt" "$OPT" > "$TMP" || { rm -f "$TMP"; die "cannot merge options.txt"; }
if [ -s "$REPORT" ]; then
backup options.txt
mv "$TMP" "$OPT" || die "cannot write options.txt"
echo " options.txt keys changed:"
cat "$REPORT"
else
rm -f "$TMP"
echo " options.txt: already matches the tier"
fi
fi
if [ -z "$(ls -A "$BACKUP")" ]; then
rmdir "$BACKUP"
BACKUP="(nothing changed, no backup needed)"
fi
echo "Done: tier '$TIER'; $replaced file(s) replaced, $unchanged already up to date."
echo "Backup: $BACKUP"
echo "Also set Java memory in your launcher (5 GB is enough): see PLAYER-TUNING.md section 2."
+1 -1
View File
@@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "3c17e58506c00f8c5aba5151afa765fdd68245c384293a017a5e2062ac35afbe"
hash = "f4b8ec15830662e033bd5c7723357a58aeeaf9dad1c5957f103961fb83ee0fd1"
[versions]
minecraft = "1.21.1"