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: level extracted state

Provisional answer to #866: use a typed, frame-owned extension store, freeze it after base extraction and extract_level_render_state, and lend immutable values to drawing through a snapshot-only DrawContext. This is a design proposal, not an implemented API or a claim that the issue is complete.

Vanilla and NeoForge 26.3

Verified with the pinned vanilla oracle from /workspace/horizon: python3 tools/rea_vanilla.py class net.minecraft.client.renderer.LevelRenderer and python3 tools/rea_vanilla.py method net.minecraft.client.renderer.LevelRenderer render. render passes levelRenderState to submitFeatures, then calls FeatureRenderDispatcher.prepareFrame before constructing the frame graph. This verifies a state-to-submission boundary, not arbitrary mod storage or a particular extraction thread. A query for extractLevel failed because that method is absent; the complete extraction caller/thread census remains open. Vanilla has no mod registration contract.

The issue names NeoForge 26.3 ExtractLevelRenderStateEvent as the counterpart. The public 26.3 Javadoc was inaccessible during this investigation. Its exact fields, cancellation, thread guarantees, ordering and attachment limits are unverified. The issue's description is intent, not verification of NeoForge behavior. Require reachable public documentation before adopting the counterpart's full contract; do not infer it from older versions, source or patches. All limits below are proposed Horizon limits, not NeoForge limits.

Verified Horizon owners and missing seam

Paths below are relative to the repository at 731b0cae68.

OwnerEvidenceImplication
Frame preparationcrates/horizon-client/src/game.rs:15048, render_withBuilds frame before consumer; proposed extraction/freeze integration belongs here
Renderer consumercrates/horizon-client/src/game.rs:15040, render_frameCalls renderer with prepared frame
Deferred submissioncrates/horizon-client/src/game.rs:15029Submit thread may retain a frame after presentation
Frame fieldscrates/horizon-client-render/src/gfx.rs:1104, FrameExisting borrowed frame is not proof of owned extension storage
Renderer entry and resizecrates/horizon-client-render/src/gfx.rs:8024, :5640Submission and target replacement need explicit epoch checks
Renderer replacementcrates/horizon-client/src/game.rs:14236, adopt_rendererRebuild must retire old extension/resource generations
Fog extractioncrates/horizon-client/src/game/interface/render_tick.rs:133Existing base preparation must precede extension freeze
Block-screen visibilitycrates/horizon-client/src/game/interface/render_tick.rs:529Keep existing extraction/cancellation decisions; do not rerun in draw
Draw adaptercrates/horizon-client/src/game/host/draw.rs:6Currently contains &Game; cannot certify snapshot-only access

This is a verified integration census, not a complete proof of every submission, reload or early-return route. Implementation must audit normal world rendering, screenshot sequences, split views, loading/title frames, device loss and deferred submission. Early loading returns inside render_with must not fire a world hook.

Proposed contract

Register LevelStateKey<T> at mod load, resolve it to a dense slot and retain schema and mod-generation identity. T must use an API-owned LevelStateWire schema: fixed-width scalars, bounded sequences/strings, API enums, vectors and resolved resource keys. The API owns schema validation and generated native/wasm encoding. No Any, Rust pointers, engine structs, closures, GPU handles or live entity/world references cross this boundary. Custom records compose supported Wire fields; a type parameter is not permission to serialize arbitrary memory.

Registration returns the named refusal UnsupportedLevelStateCapability for unsupported field kinds, live references, custom destructors or requested mutable draw access; duplicate keys return DuplicateLevelStateKey, invalid bounds return InvalidLevelStateSchema. Refuse before enabling the listener. These refusals apply only to this registrar's declarations, not unrelated unknown identifiers.

On the client owner thread: capture base camera, fog and existing visibility results; decide the world/view and epoch; fire one dependency-ordered extract_level_render_state with ClientHost and a mutable typed frame writer; validate and atomically publish each listener's contribution; freeze; submit. Host writes and apply use the existing transaction discipline. Recheck world, resource and mod generations after callbacks; refuse stale publication without recomputing or firing twice. Extraction receives no DrawContext.

The frame packet owns the frozen store, identified by frame sequence, view, world lifetime, resource generation, target generation and mod generation. Values default to absent every frame; no inheritance from frame N to N+1. A listener may write only its registered slots; declared dependent readers may read those slots. Missing values return None. Later listeners see only committed prior contributions. No draw callback mutates the store.

Drawing receives a frame-scoped read view through DrawContext and reads state(key) -> Option<&T> (equivalent checked decoded view on wasm). The context exposes captured camera/resources and approved draw commands, never a route to mutable live game state. Existing adapters holding Game require refactoring before this promise can ship. Draw callbacks run during command preparation; submission workers consume immutable commands and leases, and never call mods or carry a ClientHost. This permits separate extraction/submission threads without requiring native or wasm callbacks to be worker-safe.

A frame and every deferred consumer share an immutable lease. Release only after the final CPU consumer completes; GPU-referenced resources retain their existing fence lifetime separately. Never clear or reuse storage at flip/present alone. Bound in-flight packets to 3; backpressure before extraction when all are leased. Suspended/zero-size/no-world frames create no level store and fire no level hook. Failed or cancelled frames release their lease and cannot publish commands.

Resize increments target generation: refuse old target-dependent submission and extract afresh; never patch frozen dimensions. Resource reload and dimension switch increment their respective generations, retire old packets and prevent new publication from them. Existing accepted GPU work may finish with leased old resources. Teardown stops callbacks, drains/cancels CPU submissions, waits for resource users and then releases stores before unloading native code/wasm instances.

Untrusted input and unused cost

Proposed ceilings: 256 slots per mod, 64 KiB encoded and decoded per slot, 1 MiB per mod/frame, 8 MiB total/frame, sequence length 4096, nesting depth 8. Use checked arithmetic and validate lengths before allocating. Floats must be finite; camera-relative positions have magnitude at most 1,000,000, alpha is [0,1], and resource keys must match the captured generation. Reject invalid tags, NaN, infinity, stale keys and oversized values rather than clamp silently.

Invalid output rolls back that listener's entire contribution; built-in state and earlier valid contributions survive. Catch native unwinds at the adapter; wasm traps/fuel exhaustion have the same policy. Disable the faulty listener for its mod generation and emit one diagnostic. Native process aborts are not recoverable; native callbacks cannot be safely forcibly preempted. Wasm has a finite callback fuel budget. No arbitrary mod destructor executes on submission.

Resolve subscriptions and slots at registration. With no extraction listeners and no state consumers, one cached enabled-bit branch selects the existing path: 0 extension allocations, 0 store clears, 0 serialization, 0 callbacks and 0 scans per frame. With listeners, reuse bounded arenas only after leases expire; dispatch only subscribed listeners. Verify allocation counters and benchmark the callee path before claiming a measured performance result.

Acceptance matrix

Future implementation tests; these are expected values, not passing results.

ScenarioConcrete expected result
Extract A=7; live value changes to B=9 before drawN draws 7; N+1 extracts/draws 9
N writes 7; listener omits slot in N+1N+1 reads None
Two views, contributions 3 and 5Each draws its own value, never the other's
Listener order dependency A then BB reads A's committed 7 exactly once
Base overlay cancelled before level hook0 built-in overlay draws after freeze
NaN, infinity, 65537-byte slot, 4097 elements0 offending listener values; built-in state preserved
Native unwind / wasm trap after writing 7Slot absent; 1 diagnostic; 0 later callbacks that generation
Reload epoch 4 to 5 before publication0 epoch-4 publications into epoch 5
Resize 800x600 to 1024x768New snapshot reports 1024x768; 0 old-target submissions
No world / zero-size surface0 level callbacks and stores
Three retained packets; request fourth3 live stores; fourth waits before extraction
Present N while deferred consumer holds NN still reads 7; arena reuse count 0 until release
Same Wire input native/wasmSame slot values, refusal and ordering results
No subscriptions for 100 frames0 extension allocations, scans and callbacks

Proposed stages

Implementation file: none in this change. First approve ownership, Wire schema, limits and public counterpart evidence. Next audit and refactor the complete frame/draw boundary, with lifetime and early-return tests. Then add the catalog hook, generated native/wasm adapters and typed storage together. Finally wire a sample contribution into a real drawing consumer and run the matrix plus empty path allocation and throughput measurements. No Rust API is added by this note.

Open questions

  • Exact NeoForge fields, ordering, cancellation and limits are unverified. Recommend requiring public 26.3 Javadoc before final hook adoption; keep the proposed store as Horizon design, not a claim of counterpart equivalence.
  • Complete extraction/caller and async owner census remains unfinished. Recommend proving all world/view entry and early-return routes before coding dispatch, using vanilla oracle callers and Horizon consumer tests.
  • How broad should snapshot-only DrawContext migration be? Recommend migrating every consumer of this store and refusing registrations requiring live reads; do not expose the store through the current Game-backed adapter unchanged.
  • Are budgets sufficient for real mods? Recommend the explicit ceilings above initially, measure representative native/wasm scenes, and change them only through reviewed schema policy. Do not offer an unbounded native escape hatch.