Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Design: section meshing

Proposal for #1508, requiring maintainer approval before code. Implementation follows R2 GPU instancing (#808); this page does not claim that prerequisite is complete. No Rust API is implemented here.

Scope and reference

Extend terrain geometry preparation, not the renderer. Full mesher replacement is out of scope: replacing scheduling, visibility, buffers and upload ownership would defeat the frozen-plan and shader contracts. Renderer ports can use this contribution seam but cannot install an arbitrary section compiler through it. Fluids retain their existing mesher; replacing block geometry does not remove fluid geometry. Block entities drawing themselves use the R2 lifecycle decision, not a second geometry hook at the same moment.

Vanilla 26.3 SectionCompiler.compile traverses the section, collects block entities separately, emits fluid and model geometry into material layers, and sorts translucent quads. Verified with python3 tools/rea_vanilla.py method net.minecraft.client.renderer.chunk.SectionCompiler compile from /workspace/horizon; its callers are queried with the corresponding callers command. The proposed frozen plan is Horizon policy, not a vanilla hook. The issue identifies only a partial NeoForge counterpart: model data and model quad hooks, not whole-section replacement. Public model documentation is contextual reference, not a claim of an identical event contract. No external project implementation is a design input.

Owning thread and frozen decisions

Register subscriptions by block or exact state during client registration; resolve keys to dense state slots once per resource epoch. Exact-state rules follow block rules, listeners in dependency order. One client-side preparation moment receives a ClientHost and immutable section input on the world-owning thread. Client-lib decides, game fires, client-lib validates and applies in one transaction; if section or resource preconditions changed, refuse and reschedule, never silently recompute inside apply. No new broad Host service is needed.

Freeze a section plan with shared state recipes and sparse position overrides for neighbour-dependent choices. Resolve model selection and culling there. Workers consume immutable arrays and prepared geometry references, never a live world, game handle or ordinary mod callback. A state recipe selects a prepared model, appends quads, or replaces its block-model quads (including with zero). Culling is a six-face mask plus explicit full-face occlusion declarations; unculled decorative quads are permitted. Replacement clears the original geometry before append. Multiple appenders compose; reject competing replacements for the same state at registration with both owners named. Do not let load order silently choose a replacement.

Snapshot the 16-cubed section plus a one-block halo on all six sides, including edges and corners: 18 cubed = 5832 state cells, with immutable light, biome and availability inputs. Model selection may inspect only that radius. Missing neighbours are explicitly Missing, never a fabricated state id; for selection and culling treat them as nonmatching, nonoccluding air. Outside world height is known air. Arrival or disappearance invalidates every dependent border section. Larger-radius connected textures are out of scope. Freeze deterministic position seeds; worker order cannot change model choice. Biome blending may use the engine's separately captured blend footprint; it does not enlarge mod geometry neighbour access.

API-owned data and worker execution

Proposed API-owned Wire representations are SectionMeshPlanWire, SectionMeshInputWire, StateMeshRecipeWire, MeshQuadWire, MeshVertexWire, MeshFaceMaskWire, PreparedModelKeyWire, MeshMaterialKeyWire, and WorkerMeshRegistrationWire. These are bounded values with explicit integer widths, enum tags and lengths; no game structs, GPU layouts, pointers or borrowed references cross native/wasm boundaries. Model and material keys carry resource generation and resolved slots; unknown or stale keys refuse that contribution. Wire here is the mod boundary, not a multiplayer packet.

A declared worker-safe registration is a separate execution capability within this preparation contract, not another all-mod event bus. Give it a restricted read-only worker Host over captured input and registries, with no world writes, attachments mutation, scheduling, drawing or live Game access. Native captures must be immutable and thread safe; wasm uses isolated worker instances with the same imports, budgets and dependency ordering. Bind callbacks once into batches of subscribed states/positions. Geometry callbacks emit bounded section batches before the ordinary block loop; that loop reads concrete data only, with no per-block trait objects, wasm calls or dynamic listener lookup.

#2305's biome colour resolvers already require worker colour execution. Preserve that declared worker-safe capability: capture resolver state and lifetime with the job, use the existing tint evaluation/cache path, and forbid live mutable world access. Its colour callbacks are not implicitly geometry callbacks. The no-per-block-dispatch requirement applies to this new geometry seam; it does not assert existing tint callbacks never execute. Owner-prepared tint values remain valid where supported. Teardown first stops scheduling and increments mod generation, then cancels jobs, drains callback instances and results, releases plans, and only then unloads native code or wasm stores. A job lease prevents calling unloaded code.

Rendering and colour ownership

Use R2b prepared model geometry as the canonical baked source, sharing immutable geometry and material identities instead of exposing atlas offsets or GPU device objects. Section emission expands that geometry through the terrain path; instanced self-drawing block entities remain on R2. Freeze the self-draw skip bit before terrain preparation: it suppresses that entity's ordinary block-model geometry once, without suppressing unrelated extra geometry or fluids. Never infer self-drawing from merely having a block entity.

Opaque and cutout output retain terrain depth/culling policy. Translucent output enters the existing translucent sorting and upload path, never the opaque buffer because a mod requested an arbitrary shader. Cull only declared full occluding faces; translucent neighbours do not acquire opaque occlusion automatically. Shader packs consume the same material attributes as vanilla terrain; a mod cannot choose a raw pipeline, blend mode or frame format here.

Follow #1498 and the colour design: shader API 0.2 colour is linear light with BT.709 primaries, paper white 1.0, and values above 1.0 retained. Existing packed ARGB tint/light inputs remain vanilla-space inputs; renderer conversion happens once. Encoded tint 0.5 becomes approximately 0.21763764 under power 2.2; linear shader output 2.0 stays 2.0 until output mapping. UV, normals, depth and masks remain data. Alpha and compositing belong to the renderer. Pack changes rebuild plans only when geometry/material/colour interpretation changes; render-only changes rebuild pipelines without needlessly re-running geometry callbacks.

Route ownership and invalidation

Verified owners at worktree base 5c2583c030; paths below are relative to crates/. These are current integration points, not claims that proposed epoch checks or callbacks already exist.

RouteCurrent file:line ownerRequired design behavior
Initial dispatch and worker meshhorizon-client-render/src/streaming.rs:2237, :1142; horizon-client-render/src/mesher.rs:1741, :1751Capture plan before dispatch; mesh immutable snapshot
Block/state write, neighbours, bordershorizon-client-render/src/world.rs:385Dirty all sections intersecting radius-one dependency, including vertical and diagonal borders
Chunk arrival, biome and light updateshorizon-client-render/src/streaming.rs:3455, :3466, :3476; :1302, :1341Invalidate dependent halo plans even if centre states did not change
Unloadhorizon-client-render/src/streaming.rs:516, :1394, :1961Invalidate lifetime generation, cancel jobs, remove uploads; dirty surviving neighbours
Resource/model replacementhorizon-client-render/src/streaming.rs:1277, :1283; horizon-client/src/game.rs:14161Atomically replace WorkerContext and prepared geometry epoch; rebuild loaded sections
Biome-tint refreshhorizon-client/src/game/modding/native_biome_colors.rs:16Capture new colours/resolvers and remesh through resource reload
Shader/renderer rebuild and pending uploadshorizon-client/src/game.rs:14161, :14178Clear pending old output; rebuild affected plans through shared invalidation
Cancelled/stale completionhorizon-client-render/src/streaming.rs:1887Refuse results with mismatching token or absent column; recycle buffers
Prepared geometry generationhorizon-client-render/src/gfx/prepared_model.rs:268, :273Keep old resources leased while jobs run; refuse old-generation publication
Block entities and layer emissionhorizon-client-render/src/mesher.rs:1751; vanilla SectionCompiler.compileIntegrate proposed R2 skip decision here; retain separate self-draw lifecycle and translucent sorting
Native/wasm tint and teardownhorizon-client-render/src/mesher/mod_tint.rs:20, :110Extend captured data/lifetime boundary; proposed geometry adapters belong in game, not render crate

Every job/result carries section lifetime, section revision, halo revisions, resource epoch, tint generation and mod generation. Publication checks all of these again immediately before upload. Unload/reload at identical coordinates creates a new lifetime. Cancellation is best effort; even a completed cancelled job cannot publish. Old epoch A resources stay alive for A jobs but their results cannot replace epoch B. Snapshot leases and result memory count against admission budgets until dropped; never reuse an old plan against newly baked geometry.

Bounds and performance

Proposed hard limits: 256 contributed quads per block position, 262144 contributed quads and 32 MiB encoded contribution/plan data per section, and 128 MiB total in-flight extension data. Count shared recipe storage once globally and expanded output per job; checked arithmetic precedes allocation. Pause new extension jobs at the global limit, rather than evicting live leases. Validate every vertex, UV, normal and colour for finiteness; positions must be in block-local [-1,2], UV magnitude at most 65536, normal components in [-1,1], and alpha in [0,1]. Geometry cannot extend beyond the halo. Reject invalid indices, material keys, lengths and enum tags before baking. Enforce both encoded and decoded budgets.

Refuse a failed mod's entire section contribution atomically and use original built-in geometry, including original culling, without partial replacement. Catch unwind-capable native panics at the adapter; wasm traps/fuel exhaustion have the same refusal. Disable the faulty registration for the generation with one diagnostic, invalidate affected sections and continue built-in rendering. Native aborts cannot be recovered; worker-safe native code must use the supported unwind boundary. Wasm batches receive a finite fuel quota; native callbacks have no safe forced preemption and remain subject to the declared worker-safe contract.

With no geometry subscriptions, use the existing mesher entry directly: no plan allocation, halo copy, listener scan or per-block branch added by this feature. Select the path once at dispatch. With subscriptions, share dense state recipes, reuse scratch buffers and precompute sparse overrides. Acceptance requires allocation/callback counters at zero for the empty extension path and measured no-mod meshing throughput within benchmark noise (target under 1% regression); this document makes no measured performance claim.

Acceptance cases

These are future implementation tests, not results of this documentation change.

InputExpected value
Isolated opaque unit cube6 quads
Two adjacent full opaque cubes10 exterior quads
Replace isolated cube with empty recipe0 model quads, no retained original six
Append one unculled quad to isolated cube7 quads
Two cubes across section border; neighbour unloadsCentre contribution changes from 5 faces to 6; reappearance returns to 5
Connected model on east/west matchesNeither: model 10; east only: 11; west only: 12; both: 13; Missing counts as false
Write at local (15,15,15) with surrounding sections loadedExactly 8 radius-one dependent sections dirtied
Finish A after resource B or unload/reload0 A uploads; B remains visible
Self-drawing block entity with six-face model0 ordinary model faces; one R2 draw; fluid retained
Two translucent quads at distances 2 and 5Both in translucent output, distance 5 ordered before 2
NaN vertex, 257 quads at one cell, panic or wasm trapWhole offending contribution refused; isolated built-in cube has 6 faces
Reload during worker colour callbackOld callback lease survives to completion; old result has 0 uploads
Native and wasm same bounded planIdentical model ids, face counts and materials
Repeated empty-listener runsIdentical baseline geometry; 0 extension allocations/callbacks

Open questions

  • Final R2b geometry identity and R2 self-draw extraction type are still moving. Recommend binding the proposed keys/skip bit to those approved types after #808, retaining the generation and lifetime rules above.
  • Limits need representative mod scenes before implementation approval. Recommend the explicit bounds above as initial ceilings, then measure dense connected geometry and wasm batches before increasing them; do not ship an unbounded escape hatch.