Files
onion-mc/analysis/client-perf/research/r7-sable-create-client.md
T
logan 611a1a5952 docs: client-perf analysis, bench harness and work journal
Everything the next agent needs to run the Phase 2 optimisation loop on a
machine that can launch the client:

- analysis/client-perf/: LEVERS (ranked levers + corrections), BENCH-MATRIX
  (world, scenes, config IDs, blocks B0-B14 with decision rules),
  PLAYER-TUNING, SPAWN-TUNING, research briefs, the jar scanner and its
  output, observed default configs, and proposed-configs/ (build.py
  generates tiers and bench variants and fails on unknown keys;
  apply_variant.py applies a variant and merges options.txt).
- analysis/client-perf/bench/onion-bench/: bench-world datapack (scene
  teleports, Create grid, mob pen, sound storm, 120 s teleport tour).
- analysis/audit/: the static recipe/dependency audit, including gen_fixes.py,
  which generates the shipped Paxi datapack and was never tracked.
- phase0/: per-machine measurement harness (Steam Deck, 7700K/1060,
  6700K/1080 Ti).
- .pi/journal.md: running work log; read it first.

All of it is packwizignored, so nothing here ships to players.
2026-10-14 18:15:32 -04:00

24 KiB
Raw Blame History

Research: Client rendering cost of the Create/Sable stack (r7)

Scope: Sable 2.0.5, Create Aeronautics bundled 1.3.2 (aeronautics 1.3.2 + simulated 1.3.2 + offroad 1.3.2), SSRD 1.8.7, Sable: Physics Compat 1.3.0, Create 6.0.10 + Flywheel 1.0.6 (jar-in-jar), Slice & Dice, Dragons Plus, Enchantment Industry — on 1.21.1 NeoForge, Sodium 0.8.13, Iris 1.8.14-beta.1, DH.

Tag legend: VERIFIED = confirmed against the exact pinned jars in this repo (bytecode/config inspection) or an official spec; DOCUMENTED = official issue tracker/changelog/mod page; COMMUNITY = reddit/user comments; INFERENCE = my reasoning.

Summary

Sable does not have its own renderer — its only sub-level renderer (SODIUM_REACHAROUND) re-drives Sodium's own chunk renderer once per sub-level (own viewport, frustum, section meshing, layered draws), so each airship is effectively a small second terrain pass, and cost scales with number of sub-levels × their meshed section count, not with world render distance. The two biggest client levers are (1) shaderpacks: Flywheel's indirect and instancing backends both self-disable while a shaderpack is active (and this pack ships no Iris Flywheel Compat), dropping Create contraption rendering onto the vanilla block-entity path, and (2) per-frame pathological interactions (log spam on the render thread, per-tick sub-level-aware sound distance math) that produce the "FPS decays over 30–60 min" reports. Known FPS bugs in the trackers are mostly either fixed by our pinned versions (burner-sound degradation fixed in Aeronautics 1.3.0) or still open and unconfirmed on 2.0.5/1.3.2 (gradual decay while sub-levels exist).

Findings

A. How a Sable sub-level renders, and how it scales

  1. Sable's sub-level renderer is a Sodium "reach-around", not a separate engine. sable-client.toml exposes exactly one renderer: sub_level_renderer = "SODIUM_REACHAROUND" (sole allowed value). Bytecode of mixin/sublevel_render/impl/sodium/SodiumWorldRendererMixin shows per sub-level: build a Sodium Viewport from the camera + sub-level container, compileSections (chunk mesh builds) honoring Sodium's performance.chunkBuildDeferMode/PrioritizeChunkUpdates, then draw via ChunkRenderMatrices + layered render types (VeilRenderType.LayeredRenderType) with ShaderInstance.setDefaultUniforms. Sub-levels also have their own frustum (cullingFrustum) and per-frame culling update. — VERIFIED (jar inspection; keys cross-checked in analysis/client-perf/data/observed-default-configs/sable-client.toml; Sodium integration also described in Sable's own README: https://github.com/ryanhcode/sable)
  2. Cost model. Per frame each visible sub-level costs: one frustum+cull update, draws for all compiled sections in it (≈ ceil(blocks/16)³ worst case, typically the hull), plus matrix-only transform (movement itself is cheap — it's a pose, not a re-mesh). Re-meshing happens when blocks on the sub-level change (also piston/contraption edits). Scaling is roughly linear in count of rendered sub-levels and in meshed section count per sub-level, additive to normal world terrain cost; SSRD then decides how far away this pipeline runs. — VERIFIED (mechanism) / INFERENCE (exact per-sub-level ms; no public benchmark found)
  3. Sable patches the shader stack it runs on. Sable 2.0.5 ships overrides of Flywheel's internal shaders (assets/flywheel/flywheel/internal/*, plus light/flat.glsl, light/smooth.glsl, light/smooth_when_embedded.glsl) so Create Flywheel visuals can be embedded in sub-levels; it has a sublevel_render.fancy.ProgramMixin and compatibility.flywheel.ShaderSourcesMixin. The vanilla core-shader override in the Sable stack is only worldborder.* from the nested Veil 4.3.2 jar (4 files) — the static scan's "sable overrides vanilla core shaders" is real but tiny in file count; the runtime shader patching (Veil/Flywheel) is the substantive interference surface. Iris compat exists explicitly (SableIrisCompat, compatibility.iris.ExtendedShaderMixin), and the pack additionally ships irisveil-0.3.0 (Iris Veil Compat). — VERIFIED (jar inspection; static scan in analysis/client-perf/data/jarscan-report.md)
  4. Sable does not disable Sodium optimizations — the opposite: it requires Sodium (SSRD hard-requires it too) and reads Sodium settings (PerformanceSettings.chunkBuildDeferMode, PrioritizeChunkUpdates) to decide how sub-level rebuilds are queued; it changes no sodium-options.json keys. Known Sodium visual conflicts with Sable were fixed over time ("Fix some visual issues with Sodium on AMD machines" in 2.0.0; "Update Sodium compatibility to 0.8.13 alpha" in 2.0.0). — VERIFIED (jar inspection) + DOCUMENTED (https://api.modrinth.com/v2/project/T9PomCSv/version)
  5. Client-cost toggles that exist in Sable 2.0.5 (all keys verified in SableClientConfig / observed defaults):
    key default perf relevance
    sub_level_dynamic_shading true adds dynamic_directional_shading mixins into ModelBlockRenderer/AmbientOcclusionFace/SectionCompiler (meshing-side cost for sub-levels)
    sub_level_skylight_shadows false if enabled, sub-levels cast world shadows — leave off
    sub_level_water_occlusion true hooks FogRenderer/GameRenderer/Camera
    sub_level_snapshot_interpolation_delay_ticks 1.5 (0–100) interpolation buffer, not render cost
    sub_level_renderer SODIUM_REACHAROUND only value; no vanilla fallback to pick
    attempt_udp_networking true client side of UDP movement channel (disabling = "worse performance and latency" per common-config comment)
    Common config (SableConfig): sub_level_tracking_range (network distance of sub-levels to players), sub_levels_with_players_cannot_unload, sub_level_splitting*, disable_udp_pipeline, verbose_serialization_logging. — VERIFIED (jar inspection). Community advice to raise tracking range to 1280 (default 320) is about server→client streaming of sub-levels, not client frame cost: COMMUNITY (https://www.reddit.com/r/CreateMod/comments/1tdil1p/limiting_sublevel_render_distance_in_sable/ — key name sub_level_tracking_range verified in jar)

B. Known FPS issues / fixes in the Sable & Aeronautics trackers

  1. Render-thread log spam can collapse FPS near any sub-level (Sable #881, open). A mod calling getEntityLookedAt() every frame produced huge AABBs against Sable's entity getter; Sable logged Aborting entity get for abnormally large AABB hundreds of times/sec, log4j ate ~75% of the render thread, 30→9–12 FPS. Root cause is the triggering mod (LegendarySurvivalOverhaul — not in our pack; only legendary-tooltips is, which does no entity lookups). Lesson for us: any per-frame entity/clip query mod can detonate on sub-levels; watch client logs for repeated Sable errors. — DOCUMENTED (https://github.com/ryanhcode/sable/issues/881)
  2. "FPS decays over 20–60 min while a sub-level exists" (Sable #1011, open; Simulated-Project #746/#1204). Two distinct reported causes: (a) #1011 — gradual decay mitigated temporarily by F3+T (texture reload) or world rejoin, smells like a render-resource leak (Sable 1.2.2 era, unconfirmed on 2.0.5); (b) #746/#1204 — BalloonBurnerSoundInstance (aeronautics:block.hot_air_burner.head, aeronautics:block.steam_vent.head) re-played ~20×/s, each tick doing distanceSquaredWithSubLevels → projectOutOfSubLevel → getContaining → getLocalPlot → getChunk/getBlockEntity — hundreds of× vanilla cost; master volume 0 or blocking the two sound IDs stabilizes FPS. Fixed upstream: Aeronautics 1.3.0 changelog has "Fix performance degradation with Adjustable Burner sounds" → our 1.3.2 pin includes the fix. Caveat: 1.3.2 still contains BalloonBurnerSoundInstance with distanceSquaredWithSubLevels and AeroSoundDistUtil.tickGlobalBurnerSound — the sub-level-aware per-tick distance math is structurally still there, so treat "burners on many airships" as a known stressor. — DOCUMENTED (https://github.com/ryanhcode/sable/issues/1011, https://github.com/Creators-of-Aeronautics/Simulated-Project/issues/746, https://github.com/Creators-of-Aeronautics/Simulated-Project/issues/1204, https://api.modrinth.com/v2/project/oWaK0Q19/version) + VERIFIED (1.3.2 jar inspection)
  3. Iris shader artifacts with sub-levels — affects our exact stack (Iris #3164, open). With Iris 1.8.14-beta.1 + Sable 2.0.x + Aeronautics 1.3.x + Sodium 0.8.13, physics structures render incorrectly with shaders enabled; artifact scales with structure size; any shaderpack; goes away with shaders off; specifically triggered by shader real-time shadows (Complementary/Miniature: disable shadows; Solas: disable SSAO). Root cause attributed to Veil; a community fix exists but is unmerged. Our shaderpacks/ = Complementary Reimagined + Makeup–Ultra-Fast, i.e. the affected families (Makeup–Ultra-Fast is also named in SSRD #50 "SSRD not working with Shaders"). — DOCUMENTED (https://github.com/IrisShaders/Iris/issues/3164, https://github.com/RanoldStranold/SSRD/issues/50)
  4. Other tracker items (context, mostly visual or fixed): sub-levels invisible behind DH LODs at distance when tracking range > view distance (Sable #376); "Visual bugs with Sodium" world-invisible report (Sable #331, older Sable); Iris tessellation-shader geometry invisible with Aeronautics (Simulated-Project #308); IFC + flywheel:DEFAULT → invisible/transparent cogs & shafts (iris-flw-compat #244); resource leak of Contraption Diagram fixed in Aeronautics 1.1.0. — DOCUMENTED (https://github.com/ryanhcode/sable/issues/376, https://github.com/ryanhcode/sable/issues/331, https://github.com/Creators-of-Aeronautics/Simulated-Project/issues/308, https://github.com/leon-o/iris-flw-compat/issues/244, https://api.modrinth.com/v2/project/oWaK0Q19/version)

C. SSRD (1.8.7) — what it changes on the client

  1. Config surface: a "Sub-Level Render Distance" slider injected into Sodium's video settings (below Simulation Distance); slider max is dynamically capped by your DH/Voxy LOD distance or the server maximum; server caps via /gamerule ssrdMaxRenderDistance (default 128 chunks) and /gamerule ssrdForceloadLimit (default 2 per player). SSRD 1.8+ requires Sodium 0.8 (we have 0.8.13 ✓) and DH or Voxy on the client. — DOCUMENTED (https://modrinth.com/project/fJ183v3D)
  2. Cost & bugs: raising the slider = more sub-levels rendered by the full Sable/Sodium pipeline at distance (frame cost scales with visible sections, culling still applies), plus server-side force-loading pressure. Known issues: sub-levels visible through LODs with certain shaderpacks (Modrinth warning), DH fade-out also fades sub-levels (#64), zoom glitching (#66), Colorwheel incompatibility (#62, closed), various forceload/unload bugs (mostly closed as of 1.8.7). Mixins confirm the hooks: RenderSectionManagerMixin, SodiumConfigBuilderMixin (the slider), VanillaFrustumMixin/SodiumViewportMixin, RenderSystemFogMixin, DH DHShaderSourceMixin/FrameUniformsMixin, plus visual-distance mixins on contraption/entity/block-entity visuals. — DOCUMENTED (https://modrinth.com/project/fJ183v3D, https://github.com/RanoldStranold/SSRD/issues?q=is%3Aissue) + VERIFIED (SSRD-1.8.7-1.21.1.jar mixin list)
  3. sablecompat (Sable: Physics Compat 1.3.0) only provides tags for modded blocks — no rendering behavior. Create Aeronautics: Compatibility 1.1.3 likewise compat glue. Neither is a perf surface. — DOCUMENTED (https://modrinth.com/project/sZbcIrJb; jar metadata)

D. Flywheel backend choice (Pascal NVIDIA vs Mesa RADV, with Iris)

  1. Backend IDs & requirements (Flywheel 1.0.6, verified in source + jar): flywheel:off, flywheel:instancing (GL 3.3 + ARB_shader_bit_encoding; priority 500), flywheel:indirect (GL 4.6 + compute/DSA/ARB_multi_draw_indirect/SSBO/etc.; priority 1000 — GPU-driven culling with a depth pyramid + MDI; uses GL_KHR_shader_subgroup). DEFAULT picks the highest-priority supported backend, with a fallback log "Flywheel backend fell back from '{}' to '{}'". Driver special-case: Driver.INTEL gets indirect priority 1 ("Intel has very poor performance with indirect rendering, and on top of that has graphics bugs") — irrelevant to our fleet (NVIDIA Pascal, AMD/Mesa RADV). Both Pascal (GTX 10xx) and Steam Deck RADV advertise GL 4.6 + the required ARBs, so DEFAULT → indirect on all three machines when no shaders. — VERIFIED (Flywheel Backends.java @ Engine-Room/Flywheel branch 1.21.1/dev, and bytecode in create-1.21.1-6.0.10.jar → nested flywheel-neoforge-1.21.1-1.0.6.jar)
  2. With a shaderpack active, both backends self-disable: supported() = … && !ShadersModHelper.isShaderPackInUse() on both instancing and indirect → DEFAULT falls back to flywheel:off (vanilla block-entity path for all Create visuals). This is why the pack has no working Create-instancing under Iris. The documented fix is Iris & Oculus Flywheel Compat (IFC): "Flywheel will disable its optimizations when you are using shaderpacks. This mod forces Flywheel's optimizations to be enabled… render Create's entities using Flywheel Instancing with shaderpack enabled… significantly improve fps" — IFC 2.3.x supports Create 6.0.9 + Flywheel 1.0.6, needs Iris ≥ 1.8.1 (we have 1.8.14-beta.1 ✓); Create 6.0.10 is newer than the last tested 6.0.9 → verify before shipping. Our pack does not include IFC. — VERIFIED (Flywheel source) + DOCUMENTED (https://modrinth.com/mod/iris-flw-compat, https://github.com/leon-o/iris-flw-compat/issues)
  3. Driver risk notes for indirect: AMD Windows has a confirmed driver crash in Flywheel's indirect path (atio6axx.dll, glBindBuffersRange/MDI; GPUOpen-Drivers #89, acknowledged in Create/Flywheel repos) — does not apply to our fleet (no AMD Windows machine; Steam Deck is Mesa RADV, which Flywheel explicitly detects as Driver.MESA and supports); Create #10191 reports indirect crashes with elevator pulleys on one user setup (fix: instancing), and some users report indirect crashes with Create 6 + certain setups. Pascal is generally the safest indirect platform; treat any "Create block crash" on a client as "switch backend to flywheel:instancing or flywheel:off first". — DOCUMENTED (https://github.com/GPUOpen-Drivers/AMD-Gfx-Drivers/issues/89, https://github.com/Creators-of-Create/Create/issues/10191, https://www.reddit.com/r/CreateMod/comments/1pogo4t/create_6_crashing_when_placing_a_block_other/)
  4. flywheel-client.toml (verified defaults in pack): backend = "DEFAULT", limitUpdates = true ("instance update limiting with distance" — keeps distant contraptions from updating instances; leave on), workerThreads = -1 (0–4; parallelizes instance updates, needs restart; 4-core Zen2 Deck vs 8-thread 7700K/6700K → explicit 2 on Deck if update-thread contention shows up is a testable knob), [flw_backends] lightSmoothness = "SMOOTH" (values FLAT, TRI_LINEAR, SMOOTH, SMOOTH_INNER_FACE_CORRECTED; config comment: "May have a large performance impact" — SMOOTH adds per-vertex light interpolation in Flywheel's uber-shaders; FLAT is the cheap end; each value changes compiled shader permutations). — VERIFIED (observed-default-configs/flywheel-client.toml, LightSmoothness.class bytecode)

E. Create contraption rendering cost & the addons

  1. Create 6 = Flywheel 1.0 (official changelog: "Now using Flywheel 1.0"); contraption/kinetic visuals are instances batched per material; indirect adds GPU culling+MDI so cost scales with visible instances, instancing costs CPU draw calls per material (fine for tens of contraptions, and it's what IFC re-enables under shaders). Without Flywheel (backend off, e.g. under shaders without IFC) every kinetic/contraption visual becomes a vanilla block-entity render — the classic "Create + shaders = CPU death" mode. — DOCUMENTED (https://wiki.createmod.net/users/changelogs/6.0.0) + VERIFIED (backend mechanics, above)
  2. Sable × Create integration adds its own frame mixins: Sable 2.0.5 has 30+ compatibility.create.* mixins (ContraptionVisual, CarriageContraptionVisual, VisualizationEventHandler, flywheel Engine/RenderDispatcher/EmbeddedEnvironment/VisualManager mixins) so contraptions riding sub-levels render inside the sub-level pass with embedded lighting — contraptions on airships are therefore both Flywheel instances and embedded-environment draws; a big airship factory is the worst-case render target. — VERIFIED (sable-neoforge.mixins.json, jarscan: create-aeronautics 20 frame mixins, sable 127)
  3. Slice & Dice, Dragons Plus, Enchantment Industry: no client-perf issue reports found in their trackers for 1.21.1/Create 6 (S&D mostly touches RenderLevelStage/Gui events per jarscan; no lag/fps issues surfaced). They add ordinary instance/BER content; cost scales with placed blocks like any Create addon. — DOCUMENTED (absence of evidence) — treat as "no known client perf issues found" for these three.
  4. Sound-adjacent sanity note: Create 6.0.0 already caps boiler sounds ("Play at most four steam engine sounds at once per side"), but Aeronautics adds its own global sound instances; combined with Sable's sub-level-aware distance math (Finding 7) the sound engine is a real client-tick cost center on all machines — an easy benchmark axis (master volume 0 vs 100). — DOCUMENTED (https://wiki.createmod.net/users/changelogs/6.0.0) + Finding 7

Top levers (ranked)

# Lever Impact Machine(s) Risk Confidence
1 Add Iris Flywheel Compat 2.3.x (test vs Create 6.0.10) or accept flywheel:off under shaders knowingly H — restores GPU instancing for all Create visuals under shaders; without it shader users pay vanilla-BER cost for every cog/contraption All, biggest on 7700K+1060 (CPU-bound) and Deck (4-core Zen2) M — IFC lists Create 6.0.9 as last tested; visual glitches possible per iris-flw-compat #244 High (Flywheel source-verified behavior + IFC docs)
2 In shaderpack settings, disable real-time shadows/SSAO (Complementary Reimagined, Makeup–Ultra-Fast) or accept #3164 artifacts H — removes documented sub-level rendering corruption and shadow-pass cost; shadows are the confirmed trigger All shader users L — pure shader setting High (Iris #3164 reproduces on our exact versions)
3 Keep SSRD slider modest (e.g. start at 8–16 chunks, not the 128-chunk server cap) and don't forceload broadly M–H — directly bounds how many sub-level sections the per-sub-level Sodium pass draws; also server load Deck (GPU/VRAM-limited) & 1060 6GB first L — slider is the intended knob; gamerule cap on server Medium-High (mechanism verified; magnitude unbenchmarked)
4 Prefer flywheel:indirect (DEFAULT) without shaders; only drop to flywheel:instancing on crash; consider lightSmoothness = "FLAT"/TRI_LINEAR on Deck if Flywheel shader cost shows in F1 M — indirect = GPU-culled MDI; FLAT removes per-vertex light smoothing cost ("large performance impact" per config) Deck (RADV) and 1060 for lightSmoothness; indirect OK on Pascal & RADV L–M — changing lightSmoothness alters visuals (block light smoothing) Medium-High (config semantics verified; perf delta unbenchmarked)
5 Keep sub_level_skylight_shadows=false, keep limitUpdates=true, keep UDP networking on; watch client logs for Sable "Aborting entity get" spam (Finding 6 class) M — avoids enabling the shadow pass; avoids the documented render-thread log-stall class All L High (defaults verified; #881 mechanism documented)
6 If gradual FPS decay reproduces on 2.0.5: test F3+T / master-volume-0 as diagnostics (sound-engine vs render-resource); keep an eye on Sable #1011 M — separates the two documented decay causes cheaply before deeper profiling All; 8GB-RAM 1080 Ti machine most sensitive to leaks L — diagnostics only Medium (both causes documented on adjacent versions; 1.3.2 fixed the burner one)

Open questions to verify by benchmark

  1. ms/frame cost of one idle sub-level vs an identical static structure in the world (isolates the per-sub-level Sodium re-drive overhead), and the marginal cost of the 2nd, 3rd, 4th sub-level (linearity of Finding 2).
  2. Cost of a moving + rotating sub-level with sub_level_dynamic_shading=true vs false (does rotation re-mesh? how often?) — deck vs 1060.
  3. Frame cost of the same airship under shaders with IFC on vs off (instancing-merged vs vanilla-BER fallback) on 7700K/1060 and Deck.
  4. flywheel:indirect vs flywheel:instancing vs off on RADV/Steam Deck specifically (Mesa GL4.6 MDI perf is the wildcard; also VRAM headroom vs the 1080 Ti's 8 GB-free-RAM host).
  5. SSRD slider sweep (4/16/32/64 chunks) with DH LODs: frame-time and chunk-build queue behavior (Sable reads chunkBuildDeferMode=ALWAYS — does sub-level remeshing starve behind deferred world builds?).
  6. Sound-engine axis: master volume 0 vs 100 near N burners/steam vents on 1.3.2 (residual per-tick distanceSquaredWithSubLevels cost, Finding 7).
  7. Long-run decay test (45+ min, sub-levels present, #1011 class) on Sable 2.0.5 — does it still exist? Track client RAM/VRAM and F3+T recovery.
  8. IFC 2.3.x × Create 6.0.10 × Iris 1.8.14-beta.1 compatibility spot-check (invisible cogs #244 class) before adding it to the pack.

Sources

Kept:

Dropped:

  • Modders-of-Create/Create-Aeronautics #14 "Gradual drop in fps" (404 — repo/issue no longer resolvable; superseded by Simulated-Project #746/#1204)
  • r/CreateMod "Limiting sublevel render distance in Sable" full thread (login-walled; snippet retained only as COMMUNITY corroboration; its ticking_range key name is stale — actual key verified as sub_level_tracking_range)
  • mcmod.cn Flywheel page (secondary aggregator; its AMD-Windows advice superseded by GPUOpen issue #89)
  • 9minecraft/deepwiki Flywheel articles (SEO/aggregator content, no unique facts)

Gaps

  • No public benchmark quantifies per-sub-level frame cost or scaling with count/size — the cost model in Finding 2 is mechanism-derived, not measured.
  • Could not confirm whether Sable #1011 (gradual decay, F3+T-reset) still reproduces on Sable 2.0.5 — issue is open but filed against 1.2.2; no maintainer root-cause posted.
  • IFC compatibility with Create 6.0.10 specifically (last tested 6.0.9) is unverified; nor is IFC × Sable 2.0.5 embedding (Sable overrides Flywheel shaders, IFC also patches them — interaction untested publicly).
  • Shaderpack-specific behavior of Makeup–Ultra-Fast with SSRD (#50) and Complementary Reimagined with #3164 is reported for sibling packs (Makeup–Ultra-Fast plain, Complementary Unbound); Reimagined itself untested in those threads.
  • Steam Deck (RADV) Flywheel-indirect reports are anecdotal in either direction; no RADV-specific issue was found, which is weak evidence of health, not proof.