# Onion MC — client performance levers (Phase 1, unmeasured) Fleet (from `phase0/`): **P0** Steam Deck (GPU/shared-RAM bound), **P1** 7700K + GTX 1060 6 GB (VRAM bound), **P2** 6700K + 1080 Ti with ~8 GB free RAM (main-thread + heap bound). Every machine is 4C/8T. That makes CPU-side render cost, chunk/LOD worker threads and GC the common denominator. Impact estimates are informed guesses until Phase 2 measures them. Tags: **V** verified in source or config, **D** documented upstream, **C** community, **I** inference. Sources are in `research/` and `data/`. --- ## 1. The shape of the problem From `data/jarscan-report.md`: | Surface | Size | Who dominates | | --- | --- | --- | | Mixin classes on per-frame paths | 951 (121 `@Overwrite` / `@Redirect`) | Sable 127, Iris 114, Controlify 84 (GUI), Sodium 48, EMF 42, Create 30, ETF 27, Subtle Effects 26 | | Mods subscribing to per-frame events | 58 | Puzzles Lib, Create, Alex's Caves, Mowzie's, Twilight Forest, Ars Nouveau, Balm, Xaero's | | Mods on client / player / entity tick events | 79 | — | | Block + item atlas | ~60k 16×16-sprite equivalents (≥4096²) | **Handcrafted 27 %, Chipped 21 %, Rechiseled 9 %** (58 % together) | | Baked models / blockstates | 69,442 / 16,361 | Chipped 16.8k, Rechiseled 13.9k, Handcrafted 12.4k models (62 %) | | Entity types | ~600 | Alex's Mobs 140, Alex's Caves 92, Twilight Forest 80, Ars Nouveau 51 | | Animated atlas textures | 312 | Frights' Delight 69, TF 44, Chipped 30, Ars 29 | | Post-processing / shader payloads | — | Lodestone 59 files, Alex's Caves 38 (7 post), Charta 31, Exposure 21 (19 post); vanilla core-shader overrides from Sable, Sodium, Xaero's ×2 | What a player pays **by default today**, before choosing any options: - **Shaders and resource packs are not on by default.** packwiz only downloads them; the last real launch had `resourcePacks:[]` and no shaderpack. So Fresh Animations, Better Leaves, Complementary and MakeUp cost nothing until a player enables them. **I** - **Distant Horizons is mandatory and its defaults are heavy:** 256-chunk LOD radius, SSAO on, AA on, MEDIUM/MEDIUM quality, transparency COMPLETE. **V** (observed `DistantHorizons.toml`) - **Every mod's own defaults ship unchanged.** The pack ships **no client performance config**. Its only configs are PetProtect, Akashic Tome and the Paxi datapack. - The JVM, heap and GC are whatever each player's Prism instance has. The old instance had `MaxMemAlloc=8192` and stock G1. That last bullet is the reason for §2. --- ## 2. Meta-levers (enablers; do these first) | # | Lever | Why | | --- | --- | --- | | M1 | **A way to ship tuned defaults without clobbering player changes.** **Resolved (2026-10-14, Phase 1b):** use Default Options (`config/defaultoptions/`, client-only, copy-if-absent), plus an enforced packwiz file for `modernfix-mixins.properties`, which is read at mixin time. `preserve` survives `packwiz refresh` but leaks into the server dir, which matters for DH `[server]`/`[common]` and SSRD COMMON. Evidence: `proposed-configs/README.md` §1. | Without this, every config lever below depends on 10 players hand-editing files. | | M2 | **Per-tier profiles** (Deck / mid / high). Ship one conservative default, plus a short "if your machine is X, change Y" doc. Optionally make heavy client-only cosmetics packwiz `optional` (today only Controlify is). | Fleet constraints differ in direction. P2 should lower DH *threads*; P1 should lower DH *distance and quality*. One global ordering won't fit (already noted in `phase0/P2`). | | M3 | **Profile with spark on the client** (`/sparkc profiler`; spark is already in the pack) in Phase 2, not only FPS percentiles. | 951 frame-path mixins means attribution must come from a profiler, not from guessing which mod is slow. | --- ## 3. Ranked levers ### Tier 1 — biggest expected wins, config or policy only | # | Lever | Mechanism | Helps | Risk | Conf. | | --- | --- | --- | --- | --- | --- | | **1** | **Distant Horizons defaults per tier.** `lodChunkRenderDistanceRadius` 256 → ~64–96 (P0), 128–192 (P1), ~128 (P2). Turn SSAO off on P0 and P1. Lower the CPU thread preset on P2. Keep cave culling on. LODs come from the server; do `/dh pregen` server-side, not Chunky. | DH cost scales with LOD distance in VRAM, GPU and CPU. It is the largest always-on renderer the pack adds. | all; P1 VRAM, P0 everything | visual only | V/D | | **2** | **Shader policy.** Default off, opt-in per machine. If on: MakeUp UF, or Complementary at ≤ MEDIUM profile, shadow distance ≤ 8 chunks, entity shadows off. | With any shaderpack active, **Flywheel marks both GPU backends unsupported** (`!ShadersModHelper.isShaderPackInUse()`, checked in source). Create then renders every kinetic block entity on the CPU, and again in the shadow pass. EMF/GeckoLib entities are also redrawn for shadows, and DH adds a shadow pass over all LODs. | all; P0 hardest | none (policy) | **V** | | **3** | **Entity count, set on the server.** *Shipped in `d80f85b`, see `SPAWN-TUNING.md`.* Tune Alex's Mobs spawn weights: the 1.21.1 port has an open issue about inflated vanilla spawn rates (#62). Cap Fowlplay: #214 reports ~400 birds even after tuning. Cap Critters & Companions spawns via datapack (it has no config). | Every entity pipeline (vanilla, EMF/FA, GeckoLib, ETF, LambDynamicLights, Perception trails) multiplies by visible entity count. | all clients + server tick | gameplay feel | D/C | | **4** | **EMF/ETF throttles.** `animationLODDistance` 20 → ~8–12. Consider `retainDetailOnLowFps=false` on P0. `modelUpdateFrequency` Average → Slow. Keep `animationFrameSkipDuringIrisShadowPass=true` (already the default, **V** observed). Treat the FA Extensions pack as opt-in. | FA turns every vanilla mob into an animated CEM model. EMF's frame-skip-by-distance is the throttle. | P0, P2 | animation smoothness at range | V | | **5** | **JVM heap and GC per machine** (Prism settings; packwiz can't ship them). Heap: P0 5–6 G, P1 6–8 G, P2 ≤ 5–6 G (not the old instance's 8 G). Benchmark three GCs: client-tuned G1 (brucethemoose set), Generational ZGC (`-XX:+UseZGC -XX:+ZGenerational`; plain `UseZGC` on JDK 21 is the *non*-generational one), and Shenandoah. | DH warns about G1 itself. GC pauses show up as 1 % lows. P2's 8 GB headroom makes heap size a hard constraint. | P2, P0 | low | D/V | | **6** | **ModernFix `mixin.perf.dynamic_resources=true`.** Off by default (**V** in the observed `modernfix-mixins.properties`). Also consider `dynamic_entity_renderers`. | 69k baked models and 16k blockstates sit in heap. The author documents ~−55 % memory even with FerriteCore. ModernFix has a CTM sub-option (`dynamic_resources.ctm`); test against Fusion and Athena. | P2, P0 (RAM) | med: missing textures or crashes with CTM/model mods | D/V | | **7** | **Flywheel `lightSmoothness` SMOOTH → FLAT or TRI_LINEAR** (`flywheel-client.toml`). | The config comment itself says "may have a large performance impact". It only matters when shaders are off, i.e. when Flywheel is actually used. | all, Create-heavy bases | visual | V | | **7b** | **Sable / SSRD sub-level budget.** Sable has no renderer of its own. Its `SODIUM_REACHAROUND` path runs Sodium's chunk renderer once per visible sub-level, so cost is roughly sub-levels × meshed sections, on top of terrain. Default the client SSRD distance low (8–16 chunks), not the 128-chunk server cap. Keep `sub_level_skylight_shadows=false` (the default, **V** observed). | Each airship is a second terrain pass. Under shaders, shadows also trigger open Iris #3164 sub-level corruption. | all; P0, P1 first | visual range of airships | V (config) / I (magnitude) | | **8** | **LambDynamicLights**: mode → fast or fastest. On P0/P2 also set `light_sources.entities=false` (keep self). | Each moving light source triggers Sodium chunk-section rebuilds. On 4-core CPUs those compete with chunk builders, which default to 2 threads. | P2, P0 | less responsive light | V | ### Tier 2 — solid, smaller or more situational | # | Lever | Helps | Conf. | | --- | --- | --- | --- | | 9 | **Sound Physics Remastered**: ray count 32 → 16, bounces 4 → 2, `strict_occlusion=true` (1 occlusion path instead of 9). Cap noisy Create and mob sounds in `sound_rates.properties`. Cost scales with concurrent sounds, so a Create floor is the worst case. | P2, P0 | V | | 10 | **Vanilla and Sodium options**: render distance 8 to match the server (the old client had 12, which loads nothing extra in multiplayer). Biome blend 2 → 1 or 0. Mipmap 4 → 2 on P1/P0 (the atlas is ≥4096²). Entity distance 75–80 %. Sodium chunk threads auto-resolve to **2** on every fleet machine; try 3 on P1/P2. | all | V/I | | 11 | **Better Leaves needs leaf culling** if anyone enables it. Use **Cull Leaves** (same author, made for it) or More Culling. **Do not use Sodium Leaf Culling**: it crashes on Sodium ≥ 0.8.11. | all, forests | D | | 12 | **Twilight Forest / Alex's Caves client effects.** TF `cloudBlockPrecipitationDistance` → 0 and aurora toggles. AC `biome_ambient_light_coloring=false` for shader users. DH flags AC as partially incompatible (**V**, log). | in those dimensions | C/D | | 13 | **Jade.** Pre-tune `hide-blocks.json` (player heads) and container-content display. Sophisticated Backpacks' "20–30 FPS when looking at a backpack" report was traced to Jade's content display. | all | C | | 14 | **Bump Puzzles Lib 21.1.62 → 21.1.64**, as a *bugfix*, not a perf lever. The 21.1.64 cache is in `FabricEventFactory`, which isn't in the NeoForge jar. The NeoForge 62 → 64 diff is access-transformer restorations (21.1.63) plus one item-properties class. Applied in `a01bc3b`. | — | **V** (jar diff) | | 15 | **Xaero's**: cap world-map loaded regions, keep the minimap modest. Radar as dots. | P2 heap | D | | 16 | **WATUT**: raise `tickReceiveAndRenderRateOfGUIUpdates`, or `dontSendDetailedGUIInfo=true`. Watch the open TF conflict (#106). **Perception**: remove XP-orb trails. **Subtle Effects**: dust clouds off on P0. **FWA**: chain/lantern animations off on P0. | P0, P2 | V/C | | 17 | **Platform**: `mesa_glthread=true` + gamemode on the Deck. NVCP "Prefer maximum performance" on P1. Leave threaded optimisation alone; Sodium manages it. | P0, P1 | D/C | ### Tier 3 — content weight (structural; decide after measuring) - **Handcrafted + Chipped + Rechiseled** = 58 % of the atlas and 62 % of all models. They cost startup time, heap (baked models) and some VRAM. Lever 6 mitigates the heap part. Removing any of them is a content decision, but they're the only lever that moves startup and model RAM materially. Chipped requires Athena, and Rechiseled requires Fusion. - **Fresh Animations (+ Extensions)**: the largest documented entity-render multiplier, especially with shaders (EMF #443, Oculus era: 165 → 40 FPS with 30+ mobs). It's already opt-in because it's a resource pack. Keep it that way and say so in the player doc. --- ## 4. Mods to add (all confirmed with NeoForge 1.21.1 builds via the Modrinth API) **Owner decisions, 2026-10-14:** - Adding any helpful mod is allowed. Default Options, Sodium Extra, Reese's Sodium Options, BadOptimizations and Particle Core were **added in `a01bc3b`**. - The trials are bench block B12b. - More Culling is out: its NeoForge build excludes 1.21.1. - Shaders stay **opt-in**. | Mod | Why | Verdict | | --- | --- | --- | | **Default Options** (`default-options`) | Lever M1 delivery mechanism; Balm already present | **Add** | | **Sodium Extra** + **Reese's Sodium Options** | Expose particle, animation and fog toggles per machine. RSO works with Controlify, which matters on the Deck. | **Add** | | **Particle Core** (`particle-core` 0.3.3) | Particle frustum culling, async ticking, per-type limits. Big particle sources here: Subtle Effects, Perception, Ars, Alex's Caves, Sooty Chimneys. | **Add, then verify** | | **BadOptimizations** (2.4.1) | Lightmap and sky caching; self-disables on conflicts | **Add** | | **Gnetum** (3.3.6) | Spreads HUD rendering over frames. This pack is HUD-heavy (Xaero's, Jade, AppleSkin, titles, plaques). The author says it's compatible with ImmediatelyFast. Replaces Exordium, which has no NF 1.21.1 build. | **Trial** | | **Cull Leaves** or **More Culling** | Only if Better Leaves stays (lever 11) | **Add one** | | **Better Block Entities** (1.3.4, built for Sodium 0.8.12–0.8.13) | Meshes static chests, signs and beds into terrain. Its EMF compat is only for 1.21.11+, so test it with FA. | **Trial** | | **Flerovium** | Item, particle and entity render culling. Higher visual-glitch risk with layered models. | **Trial** | | **Super Resolution** (alpha) | FSR upscaling. Potentially the biggest P0 lever, but alpha, and Complementary has no SR interface. | **Trial on P0 only** | | **Ixeris** | Moves event polling off the render thread. Big win only on Windows with high-poll mice. Has open crash reports on older NVIDIA cards. | **Trial on P1** | | **Iris Flywheel Compat** | Restores Flywheel instancing under shaders, which would neutralise most of lever 2's Create penalty. Unofficial and breaks on Create/Iris updates. | **Trial only if shaders are wanted** | **Skip**: Sodium Leaf Culling (crashes), ScalableLux (breaks DH), Moonrise and C2ME on the client (no client gain in multiplayer), Exordium / Krypton / Nvidium / VulkanMod / FastChest / YOSBR (no NeoForge 1.21.1 build), Memory Leak Fix (obsolete), Sodium Options API (superseded by Sodium 0.8), Sodium Dynamic Lights (duplicates LambDynamicLights). ## 5. Mods worth considering for removal **Owner decision, 2026-10-14: remove no mods.** Kept for reference only. Mitigate with config instead (spawn tuning: `analysis/client-perf/SPAWN-TUNING.md`). Ordered by cost to value. None is urgent; measure first. | Mod | Cost evidence | Note | | --- | --- | --- | | **Miner's Delight** | It's the *only* consumer of **Lodestone** (10 frame mixins incl. 3 overwrite/redirect, 59 shader files, post effects). **V** (dependency scan) | Removing one food addon drops a whole rendering library | | **Fowlplay** | Open 1.21.1 issue: hundreds of birds; per-tick flight pathfinding (server) | Remove if spawn caps don't hold | | **Charta** | 31 shader files registered at load; niche use | Only if nobody plays cards (both sides) | | **WATUT** | Streams other players' GUI state each tick; open TF conflict | Flavour only | | **Emojiful** | Font-render hooks; known chat freezes upstream | Low impact; social call | | **Fresh Animations Extensions** | Extra CEM models on top of FA | Keep opt-in, recommend against on P0 | | **AmbientSounds** | Largest jar (85 MB of ogg); RAM residency unmeasured | Measure heap first | **Not removable, contrary to r4's suggestion**: one of Fusion/Athena. Chipped *requires* Athena and Rechiseled *requires* Fusion (**V**, `neoforge.mods.toml` dependencies). --- ## 6. Phase 2 — test order **Superseded by [`BENCH-MATRIX.md`](BENCH-MATRIX.md)**: world, scenes, config IDs, blocks B0–B13 with decision rules. ## 7. Corrections to subagent research (verified by the orchestrator) Added in Phase 1b (2026-10-14): - **DH default threads is 4 on the fleet, not 2.** The default is `ceil(nproc × 0.5)` (`ThreadPresetConfigEventHandler` @ 3.3.2). The observed `numberOfThreads = 2` came from the owner's 4-thread machine (`nproc` = 4). Every other observed DH value matches the source defaults. - **Puzzles Lib 21.1.64 has no perf change on NeoForge** (see lever 14). r9's lead and this file's earlier "the fix *is* in 21.1.64 for 1.21.1" were wrong for our loader. - **r6's Shenandoah flags crash JDK 21.** `-XX:ShenandoahGCMode=iu` needs `-XX:+UnlockExperimentalVMOptions` (tested on OpenJDK 21.0.12). Use plain `-XX:+UseShenandoahGC`. - **SSRD's distance is a COMMON config** (`config/ssrd-common.toml`, `physicsRenderDistance`, default 128, slider floor 16), and the server reads it too. It is not stored in Sodium's options. - r4 "remove one CTM engine": wrong. Both are hard dependencies (see §5). - r9 "Puzzles Lib ~8 % render-thread overhead": the measurement was on MC 26.2/Fabric. The fix *is* in 21.1.64 for 1.21.1; the size of the gain there is unknown. - r3/r8 cite EMF #443 for magnitude. That report is from the Oculus/Forge era and is closed. Treat the 4× figure as directional only. - Static scan's "Sable overrides vanilla core shaders" (r7 checked it): 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 was checked in `Backends.java` on the 1.21.1 branch: confirmed.