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: resource-backed special item renderers

Provisional answer to #876: what resource-backed baked geometry and typed stack-derived state may a special renderer submit? Root must review this design before code. This page implements no API. Geometry belongs in resources; callbacks select and pose baked parts only where ordinary item-model properties cannot express the behavior.

Reference and boundary

Vanilla 26.3 SpecialModelRenderers.bootstrap registers type identifiers and unbaked codecs. SpecialModelWrapper.update extracts an argument from the stack, creates a layer, records extents, transform and foil, and installs the special renderer plus argument. Verified using tools/rea_vanilla.py method for both methods from /workspace/horizon. The callers query for the wrapper's update returned zero direct references; interface dispatch means that is not a complete context census. Horizon routes are listed below instead.

The named NeoForge counterpart is RegisterSpecialModelRendererEvent. Its 26.3 public Javadoc was inaccessible during this investigation. Public client-item documentation describes stack state preparation and submission, but currently labels itself 26.1. Exact 26.3 registration timing, cancellation, codec shape and capability limits remain unverified. Require public 26.3 documentation before adopting those details; the contract below is proposed Horizon policy, not asserted parity. No foreign renderer source was read.

level-extracted-state.md is absent from arismoko/main at this worktree's base ff35cdee95. PR #2357 is an adjacent proposal only; this design neither copies it nor requires its contents.

Proposed registration, bake and preparation

One register_special_model_renderer client registration moment takes a ClientHost and a typed registrar. Dependency-ordered listeners register a namespaced type, bounded resource schema, declared stack-component inputs and one preparation callback. Duplicate type ownership refuses registration with both owners named; vanilla built-in identifiers cannot be replaced. Resolve identifiers and callback slots at load/bake, not on every draw. No second event bus or compatibility alias is introduced.

API-owned SpecialRendererRegistrationWire, SpecialResourceSchemaWire, SpecialBakeRecipeWire, SpecialStackInputWire, SpecialDrawContextWire, SpecialDrawListWire and SpecialPartInstanceWire define explicit integer widths, enum tags and lengths. Generated native and wasm adapters share these representations and validation; game JSON values, Rust engine structs, GPU pointers and borrowed live stacks never cross the boundary. Wire is an API boundary, not a network synchronization format.

Resources declare named geometry parts, texture/material references, local transforms and conservative extents. Reuse existing model loading/baking and prepared geometry ownership. Bake once per resource reload; callbacks cannot create geometry, compile shaders, read arbitrary files or recursively render items. UnsupportedSpecialRendererCapability refuses registration requesting raw pipelines, live world access, arbitrary codecs, recursive item draws or undeclared stack inputs. Unknown special types produce UnknownSpecialRendererType with resource path and type, replacing the foreign base-model workaround. This refusal applies only to special-model type decoding; unrelated unknown identifiers keep their existing behavior. Built-in specials retain their existing renderer paths. A failed candidate resource reload leaves the previous complete resource set active; initial failure uses the ordinary missing-model diagnostic rather than silently drawing base.

Client-lib decides preparation input; game fires the registered callback with ClientHost and a typed drawing context; client-lib validates and applies in one transaction. Input is an immutable copy of declared component values, display context, owner snapshot, deterministic seed, pose, packed light and foil policy. The drawing context records instances of this recipe's baked parts with local poses and vanilla-space tint. It exposes no device, frame format or live entity. The output is an immutable draw list stored in the ordinary item render state; GUI and world consumers execute it without calling the mod again. Engine display transforms, clipping, lighting, glint, sorting and bounds remain authoritative. Selected parts' transformed bounds must fit declared conservative extents.

Apply rechecks the captured stack revision, resource holder and mod lifetime; changed preconditions refuse the prepared result, never recompute within apply. A later ordinary preparation may retry. Preparation writes and staged draws roll back together. Inside lifted passes, publication, retirement and journal emission wait for root commit; rollback publishes nothing. Saves are refused during an open action savepoint under the existing transaction rule, even though rendering itself adds no persistent state.

Lifetime and colour

Use the existing resource lifecycle holder and its identity for baked references and leases, with resolved part slots. Do not add a special-renderer generation counter. Reload builds a complete candidate, validates/bakes it, then publishes atomically at root commit. Old queued states hold leases until submission ends; they cannot publish into the replacement holder. Retire resources after leases drain. Unload stops new callbacks, drains outstanding callback/draw leases, then unloads native code or wasm stores. Device recreation rebuilds engine GPU objects from resource data without exposing their identity to mods.

Follow mod render colour. Packed ARGB tint and light 0..=15 remain vanilla-space inputs, converted once by the engine. Shader colours are linear BT.709, paper white 1.0, values above 1 allowed. Generated entry/exit boundaries own storage conversion. Texture data channels stay data; coverage does not grant ownership of frame alpha. Shader-pack and SDR/HDR changes preserve this contract and cannot make a callback depend on the frame format.

Current route owners

Verified against base ff35cdee95; paths are relative to crates/. These are integration owners, not claims that the proposed callback already exists.

RouteFile:lineRequired behavior
Decode / foreign workaroundhorizon-assets/src/item_model/special.rs:82, :90, :103Resolve registered types; report unknown special types
Resource parts and baked extentshorizon-client/src/item_render/special.rs:51, :307; horizon-client/src/item_render.rs:748, :841Bake resource recipe and conservative bounds
Shared stack preparation, handshorizon-client/src/game/interface/item_draw.rs:376, :398Freeze declared inputs and prepare once per resolution
Inventory, cursor and other GUI iconshorizon-client/src/game/interface/inventory_slots.rs:6; horizon-client/src/game/interface/item_draw.rs:455; horizon-client/src/item_render/gui.rs:180Same prepared parts, GUI clipping and lighting
Dropped/ground itemshorizon-client/src/game/interface/item_draw.rs:705, :789, :793Copy poses reuse state on GPU and CPU paths
Frames and armor-stand equipmenthorizon-client/src/game/entities/frames.rs:749; horizon-client/src/game/entities/armor_stands.rs:1786, :1847Fixed/head/hand contexts use same preparation
Other world item consumershorizon-client/src/game/blocks/campfire.rs:172; blocks/vault.rs:250, blocks/shelf.rs:230, blocks/brush.rs:347 under the same game/ directoryPreserve context and engine lighting
Entity/effect and mod-submitted itemshorizon-client/src/game/items/potions.rs:2219, items/totem.rs:224, items/item_particles.rs:18; horizon-client/src/game/modding/native_block_entity_draw.rs:1237Share resolver, no recursive callback escape
CPU special submission / GPU submissionhorizon-client/src/item_render.rs:267, :299; horizon-client/src/item_render/special.rs:466; horizon-client/src/game/interface/item_draw.rs:556Consume identical frozen draw list
Reload and GPU resource ownershiphorizon-client/src/game.rs:14259; horizon-client-render/src/gfx/prepared_model.rs:256Rebuild affected item states and retain leased resources

Native and wasm need generated catalog registration, dispatch and callback exports in the eventual implementation; neither adapter exists for this feature today. Both must enter the same game preparation boundary and enforce identical failures. The implementation review must trace first/third-person callers through the shared hand resolver and reload publication through its full owner chain before claiming exhaustive integration.

Untrusted inputs and cost

Proposed extension budgets: 256 parts per recipe, 65536 vertices per part, 16 MiB decoded geometry per recipe, 64 KiB resource parameters and 16 KiB copied stack inputs, 256 instances and 64 KiB encoded draw output per preparation. Use checked arithmetic before allocation; bound encoded and decoded sizes, resource nesting to 32 and identifiers to 256 UTF-8 bytes. Reject invalid tags, indices, foreign/stale part slots and dependency cycles. Transforms and extents must be finite, local translation magnitude at most 1024 and scale magnitude at most 64; zero scale is valid. These extension budgets do not cap vanilla's own models, stack components or draw capacity. Oversized declared stack input refuses this extension preparation without truncating the actual stack.

Catch unwind-capable native callback panics; wasm traps and fuel exhaustion have the same policy: discard the entire draw list and callback transaction, diagnose once and disable the registration for the holder lifetime. Draw the missing-model marker for affected custom layers, retaining independent built-in layers. A later built-in item still renders normally. Native aborts cannot be caught and native callbacks cannot safely be forcibly preempted; wasm gets finite execution fuel. Reject an invalid command atomically, never retain a partially accepted list.

With no registrations, retain the existing built-in branch: one O(1) resolved variant test, zero extension allocations, serialization or listener scans. A registered draw dispatches directly by resolved slot, copies only declared inputs and records bounded commands. GPU/CPU submission has no callback or mod scan. These are performance acceptance requirements, not measured results.

Acceptance matrix

Future implementation tests use original assets, not third-party model files.

CaseConcrete expected result
Original cube A and two separated cubes B; boolean component false/trueExactly 6/12 resource quads selected in both inventory and hand
Same true stack in ground, frame, head and other-player handB selected in all four; engine context transforms differ
Resource reload changes B to three cubesTrue stack selects 18 quads after commit; 0 new draws reference old holder
Reload fails with unknown special typeNamed error; previous holder remains active; initial load displays missing model
Queue old state, reload, drainOld lease valid until drain; retirement occurs once after root commit
Rollback lifted preparation/reload0 published draws, retirements or journal entries
Save during action savepointRefused; no persisted partial state
NaN transform, 257 instances, stale part, oversized inputEntire custom layer refused; 0 partial instances
Native panic / wasm trap followed by vanilla cubeCustom layer missing marker; vanilla cube still 6 quads
Unsupported raw-pipeline capability / duplicate registrationNamed registration refusal; no partial type installation
White 1, linear RGB 2, encoded tint 0.5Shader values 1, 2, approximately 0.21763764; frame alpha engine-owned
Native and wasm, same inputsIdentical part slots, transforms, tint and refusal outcomes
No listener, repeated built-in scenesIdentical baseline output; 0 extension callbacks/allocations

Proposed stages (file none)

  1. Review this contract and obtain public NeoForge 26.3 registration evidence.
  2. Finalize API-owned Wire schemas, generated native/wasm boundary and refusal diagnostics; connect resource decode/bake to existing lifecycle ownership.
  3. Integrate shared stack preparation and both submission paths, transactional reload/teardown and colour rules; trace all caller routes above.
  4. Add original-asset acceptance scenes, failure injection and empty-registration counters, then measure cost. No implementation files are proposed in this note.

Open questions

  • Exact NeoForge 26.3 registration contract is unverified. Recommendation: require public 26.3 docs/Javadoc before locking the counterpart's signature and timing; do not infer it from older docs or source.
  • R2 prepared-part API and practical extension budgets need implementation review. Recommendation: use the existing lifecycle holder with the budgets above, test representative original scenes, and enlarge only extension limits with measured evidence; preserve vanilla capacity throughout.