Parent: #126
Related: #131, #137, #139
Problem
Bloom currently turns a glTF scene into a flat Vec and bakes each mesh-node world transform into copied vertices. The textured loader collects all transforms in native/shared/src/models_gltf.rs, then decodes and bakes every primitive once per placement. Other loader paths call bake_scene_mesh_instances in native/shared/src/models_gltf_bake.rs, which also clones and transforms geometry per placement. bloom_scene_attach_model then clones vertices, secondary UVs, and indices again into every SceneNode.
This is functionally correct for small files but makes heavily instanced production scenes unusable. The NVIDIA Bistro source has:
- 551 unique meshes
- 2,909 mesh-referencing nodes
- 5,910 total nodes
Literal loading expanded to roughly 19 GB RSS and exceeded practical GPU buffer limits. The current rich Bistro launch profile therefore keeps only one transform per unique mesh. That stays near 1 GB RSS, but intentionally omits repeated architecture and props, leaving some objects visibly floating.
This is an ownership/instancing problem, not a missing-texture problem.
Outcome
Decode and upload each immutable primitive geometry once, preserve every glTF scene/node placement as an instance, and render/ray-query all placements through shared geometry plus per-instance transforms/material bindings. The simple public loadModel workflow must remain sufficient; applications should not need to manually reconstruct glTF hierarchy.
Required architecture
Import representation
Introduce explicit canonical geometry and instance records rather than using a baked MeshData as both:
- Geometry/primitive ID: immutable positions, normals, tangents, UV sets, skin inputs, indices, bounds, topology, and default material reference.
- Instance ID: geometry ID, world transform, normal transform or derivation contract, effective material/variant, source scene/node identity, visibility flags, and optional skin/animation identity.
- Model scene: roots/hierarchy and the ordered list of instances reachable from the selected glTF scene.
Decode each accessor/primitive once. A glTF mesh with multiple primitives produces multiple geometry references per node placement, without duplicating the primitive payload.
Preserve the existing orphan-mesh behavior explicitly: meshes unreachable from the selected scene receive one identity instance only if that remains the documented compatibility behavior.
Scene ownership and API
Add an instance-aware attachment path used by loadModel and the examples. Avoid copying CPU vertex/index arrays in bloom_scene_attach_model for ordinary immutable model instances. Keep immediate/procedural geometry editing on an owned compatibility path.
The TypeScript API should remain simple. A preferred shape is an attachModelScene/model.instantiate operation that creates all imported placements; retain attachModelToNode for selecting a single primitive/instance where useful. Do not expose internal GPU offsets as public API.
Renderer integration
All paths must consume the same geometry/instance contract:
- main opaque, masked, double-sided, transparent, and refractive scene passes
- depth/Hi-Z and directional/point/spot shadow casters
- visibility-buffer and virtual-geometry paths
- SSR/SSGI scene representation, card/SDF generation, and bounds
- picking, frustum/occlusion culling, LOD selection, and debug views
- hardware ray queries/path tracing: build one reusable BLAS per compatible geometry and emit multiple TLAS instances where the backend supports it
- software/fallback ray paths: share source geometry and apply instance transforms without expanding vertex buffers
Existing GPU-driven geometry hashing may deduplicate some uploads after CPU cloning; that is not sufficient. CPU model/scene ownership, secondary passes, and acceleration structures must also avoid per-placement geometry copies.
Transform correctness
Cover:
- nested node hierarchy and glTF matrix/TRS composition order
- non-uniform scale using inverse-transpose normal transforms
- negative determinant/mirrored transforms and front-face/tangent handedness
- multiple glTF scenes and deterministic default-scene selection
- per-instance material variants/overrides without cloning geometry
- bounds transformed conservatively per instance
- static instances first; document and preserve the separate ownership needed for skinned/morphed/deforming geometry
Lifetime and mutation
Define model geometry lifetime independently from instance lifetime. Destroying an instance may not invalidate shared geometry used elsewhere. Editing procedural geometry or requesting a unique mutable copy must use explicit copy-on-write/owned behavior rather than silently modifying every instance.
Verification
Add focused fixtures/tests for:
- two nodes sharing one primitive at different transforms: one CPU decode and one GPU geometry allocation, two visible placements;
- nested TRS/matrix hierarchy with known world-space bounds;
- non-uniform and negative scale: correct lighting, culling, tangents, and two-sided behavior;
- one mesh with multiple primitives/materials instantiated by multiple nodes;
- opaque, MASK foliage, BLEND/transmission, and emissive repeated instances;
- directional shadows and ray-query/PT hits from every placement;
- picking returns the correct instance/node identity;
- model unload while another instance remains, then final resource retirement;
- capability fallback without indirect draw-count or hardware ray queries;
- existing one-instance models and procedural SceneNode geometry remain visually unchanged.
Add counters to quality telemetry for unique geometries, geometry bytes, instances, instance bytes, BLAS count/bytes, and TLAS instance count. A debug view must identify geometry ID and instance ID.
Acceptance criteria
Likely files
- native/shared/src/models.rs
- native/shared/src/models_gltf.rs
- native/shared/src/models_gltf_bake.rs
- native/shared/src/ffi_core/scene.rs
- native/shared/src/scene.rs and scene GPU-driven ownership
- native/shared/src/renderer scene/shadow/SSGI/PT/acceleration-structure paths
- src/models/index.ts and src/scene/index.ts
- examples/bistro and tools/quality/prepare_bistro.py
- native/shared model, scene, visibility, ray-query, and golden tests
Non-goals
Parent: #126
Related: #131, #137, #139
Problem
Bloom currently turns a glTF scene into a flat Vec and bakes each mesh-node world transform into copied vertices. The textured loader collects all transforms in native/shared/src/models_gltf.rs, then decodes and bakes every primitive once per placement. Other loader paths call bake_scene_mesh_instances in native/shared/src/models_gltf_bake.rs, which also clones and transforms geometry per placement. bloom_scene_attach_model then clones vertices, secondary UVs, and indices again into every SceneNode.
This is functionally correct for small files but makes heavily instanced production scenes unusable. The NVIDIA Bistro source has:
Literal loading expanded to roughly 19 GB RSS and exceeded practical GPU buffer limits. The current rich Bistro launch profile therefore keeps only one transform per unique mesh. That stays near 1 GB RSS, but intentionally omits repeated architecture and props, leaving some objects visibly floating.
This is an ownership/instancing problem, not a missing-texture problem.
Outcome
Decode and upload each immutable primitive geometry once, preserve every glTF scene/node placement as an instance, and render/ray-query all placements through shared geometry plus per-instance transforms/material bindings. The simple public loadModel workflow must remain sufficient; applications should not need to manually reconstruct glTF hierarchy.
Required architecture
Import representation
Introduce explicit canonical geometry and instance records rather than using a baked MeshData as both:
Decode each accessor/primitive once. A glTF mesh with multiple primitives produces multiple geometry references per node placement, without duplicating the primitive payload.
Preserve the existing orphan-mesh behavior explicitly: meshes unreachable from the selected scene receive one identity instance only if that remains the documented compatibility behavior.
Scene ownership and API
Add an instance-aware attachment path used by loadModel and the examples. Avoid copying CPU vertex/index arrays in bloom_scene_attach_model for ordinary immutable model instances. Keep immediate/procedural geometry editing on an owned compatibility path.
The TypeScript API should remain simple. A preferred shape is an attachModelScene/model.instantiate operation that creates all imported placements; retain attachModelToNode for selecting a single primitive/instance where useful. Do not expose internal GPU offsets as public API.
Renderer integration
All paths must consume the same geometry/instance contract:
Existing GPU-driven geometry hashing may deduplicate some uploads after CPU cloning; that is not sufficient. CPU model/scene ownership, secondary passes, and acceleration structures must also avoid per-placement geometry copies.
Transform correctness
Cover:
Lifetime and mutation
Define model geometry lifetime independently from instance lifetime. Destroying an instance may not invalidate shared geometry used elsewhere. Editing procedural geometry or requesting a unique mutable copy must use explicit copy-on-write/owned behavior rather than silently modifying every instance.
Verification
Add focused fixtures/tests for:
Add counters to quality telemetry for unique geometries, geometry bytes, instances, instance bytes, BLAS count/bytes, and TLAS instance count. A debug view must identify geometry ID and instance ID.
Acceptance criteria
Likely files
Non-goals