---
name: SRE unified scene
overview: Replace SRE's three scene sources, ISceneSource contract and per-group GpuScene with one entity hierarchy (generational handles, a required transform, typed components), an asset library whose glTF prefabs are instantiated without copying GPU data, and a RenderScene that syncs the hierarchy into one geometry pool, one instance/material/joint buffer set and draw lists usable by Direct, batched-instanced and Indirect strategies. Vessels own the per-step script. All 12 gfx.sre goldens are regenerated (spirv and elf); legacy libraries and tests are untouched.
todos:
  - id: cpu-scene
    content: "Add CPU scene core: Entity handles, node table with Transform + hierarchy, component pools, views, dirty tracking, scene_systems (animate, bounds) + unit tests"
    status: pending
  - id: assets
    content: Add scene_types.h (contract v2), AssetLibrary, Prefab-as-Scene, instantiate(), cloneMaterial, unified gltf_loader on gltf_accessors.h + tests
    status: pending
  - id: render-scene
    content: "Add RenderScene: global geometry pool, scene sets (View/Instances/Joints/Materials/Textures/Lights), slot allocator, DrawList (Hierarchy/ByMesh), stats; BatchedDraw, ShellDraw, IndirectDraw(DrawList); Recipe records RenderScene updates"
    status: pending
  - id: indirect-port
    content: Add sre_scene.glsl + sre_pbr_indirect shaders/.pipe/CMake/device lists; port IndirectPbrTechnique and food store/mountain range indirect vessels; new goldens gfx.sre.110/111/120/121
    status: pending
  - id: stress3d-port
    content: Port Stress3D techniques vessel by vessel (20, 1, 2, 11, 12, 5, 3, 4) with sre_* shaders, .pipe, elf .sp3+TOML for gfx11/11_5/12/13, and new goldens
    status: pending
  - id: vessel-api
    content: "SreVessel: Scene holds AssetLibrary/Scene/RenderScene, onStep hook, loadPrefab, addDefaultCameraAndLight; add prefab-hierarchy vessel and gfx.sre.30 strategy-equivalence/dedup vessel"
    status: pending
  - id: delete-old
    content: Delete GltfSceneSource, IndirectSceneSource, ProceduralSceneSource, GpuScene, scene_conformance, NodeUbo path, InstancedDraw, ISceneSource; replace scene_contract_test; update sre.rst and sre.mdc
    status: pending
isProject: false
---

# Unified SRE scene and hierarchy-driven Vessel API

## 0. Decisions taken and conflicts found

Your decisions:
- **Full unification.** All 12 `gfx.sre.*` cases get new goldens. Legacy `gfx.stress3d.*`, `gfx.indirect.*` and their CSV files stay as they are.
- **Replace the scene contract.** `ISceneSource`, `FrameState` and `k_SceneContractVersion = 1` go. The new `Scene` is the only representation, with the contract bumped to v2.
- **Regenerate ELF shaders.** Every shader change ships its `.sp3` and TOML files for gfx11, gfx11_5, gfx12 and gfx13 in the same change.

Conflicts between the prompt and the code, and how the plan resolves each:
1. **"SRE creates separate mesh geometry per instance" is only partly true.**
   - The per-instance copies are in legacy `gfx_scene`: `loadNodeVertices`/`loadNodeIndices` in `engine/gfx/support/scene/src/scene_renderer_scene.cpp:195-207` append the mesh again for every node. Three 100k-vertex cars take about 25 MB instead of 8.4 MB.
   - SRE already reuses meshes by glTF mesh index ([gltf_scene_source.cpp](engine/gfx/support/sre/src/scene/gltf_scene_source.cpp) `loadMesh`, line 433) and packs each mesh once per group ([gpu_scene.cpp](engine/gfx/support/sre/src/scene/gpu_scene.cpp) lines 194-222).
   - The waste that SRE does have is listed in section 1.3. The plan targets those items.
2. **[.cursor/rules/sre.mdc](.cursor/rules/sre.mdc) says the scene contract is frozen and shared with an ECS source's owner.** No ECS source exists anywhere in the repo; it appears only in comments in `scene_contract.h`, `sre.rst` and `scene_contract_test.cpp`. Per your decision the contract is replaced, and `sre.mdc` and [sre.rst](engine/gfx/support/sre/doc/sre.rst) change in the same commits.
3. **`sre.mdc` forbids changing `engine/gfx/support/scene`, `scene_indirect` and `app/scene_viewer`.** The plan respects this. SRE keeps using `gfx_scene` only for `SceneAsset` (reading the `.scene` file) and `Camera` (projection math).
4. **The shader sources are shared with the legacy renderers.**
   - `pbr.vert.spv`, `pbr_amdllpc.*.bin`, `fur.*`, `hair.*` and `meshlet.*` are referenced by `engine/gfx/support/scene/asset/*_pipeline.json`, and `pbr_indirect.*` is used by `IndirectVessel`.
   - Editing them would break the legacy goldens. All new shaders are therefore new `sre_*` files under `engine/gfx/graphics_core/test/shaders/`.
5. **`InstancedDraw` is not object instancing.**
   - Furry orb uses its instance count for fur shells (`max(stepIndex % 150, 1)`, [sre_furry_orb_vessel.cpp](engine/gfx/graphics_core/test/vessel/sre/sre_furry_orb_vessel.cpp) lines 109-111). Hairmark uses it for 500 hair shells (`gl_InstanceIndex` in `hair.vert:38-41`, `fur.vert:51-55`).
   - The plan renames it to `ShellDraw` and adds a separate `BatchedDraw` for real object instancing.
6. **Runtime mutation runs into one-shot GPU setup.**
   - `FrameGraph` compiles and realizes once, with no reset ([frame_graph.h](engine/gfx/support/sre/include/ip/gfx/sre/frame/frame_graph.h) lines 407-420). `SreVessel::setUp` does this once ([sre_vessel.cpp](engine/gfx/graphics_core/test/vessel/sre/sre_vessel.cpp) lines 119-124).
   - `SreVessel` records step N+1 while step N is still on the GPU (lines 200-207).
   - The plan therefore uses fixed-capacity GPU buffers updated with `vkCmdUpdateBuffer`. The data is captured into the command buffer at record time, so recording ahead stays safe.
   - Assets (meshes and textures) are frozen once the `RenderScene` exists. Entities, components and materials stay mutable within the configured capacities.
7. **The Stress3D design docs are stale.** `scene/doc/stress3d_vessel.rst` and `scene_rendering_engine.rst` describe a `RenderEngine`/`AutoConstantDataSource` design that does not exist in code. The plan is based on the code.
8. **Driver limits** ([sre_driver_constraints.rst](engine/gfx/support/sre/doc/sre_driver_constraints.rst)):
   - Every descriptor set has a single binding; a single binding holding an array works.
   - No buffer device address.
   - Dynamic UBO offsets are dropped by the driver (`vk_command_buffer.cpp:1383-1397`).
   - `firstInstance` with instance-rate attributes is proven by `IndirectPbrTechnique`.
   - `gl_DrawID` and `gl_BaseInstance` are unverified on the TNG driver, so the design does not use them.

## 1. Current state

### 1.1 How each implementation works

**Legacy Stress3D (`gfx_scene`)**
- **Loading:** reads the glTF protobuf from `.scene`.
- **GPU buffers:** 7 merged buffers per glTF scene. Each mesh is copied again for every node that references it.
- **Instances and transforms:**
  - The glTF node tree is kept, and world transforms are recomputed by walking parent pointers.
  - Each node has its own 8260-byte UBO and descriptor set.
  - A scene-wide UBO carries a zoom-to-fit model matrix.
- **Draws:** one `drawIndexed` per primitive per node, rebinding 8 descriptor sets and a 28-byte push constant each time (`scene_renderer_scene.cpp:1463-1515`).

**Legacy indirect (`gfx_scene_indirect`)**
- **Loading:** the same protobuf, through its own `indirect3d::SceneAsset`.
- **GPU buffers:** one global vertex and index pool with rebased indices.
- **Instances and transforms:**
  - The node tree's world transforms are uploaded every frame, for every node, into a node-matrix buffer.
  - Per-instance node, material and skin indices are instance-rate attributes located through `firstInstance`.
- **Draws:** CPU-built `DrawIndexedIndirectCommand`s, one per primitive with `instanceCount = N`, issued as a single `drawIndexedIndirect` (`scene_indirect_renderer.cpp:246-290, 607-660`).

**SRE** (`GltfSceneSource`, `IndirectSceneSource` and `ProceduralSceneSource`, feeding `GpuScene`)
- **Loading:** two separate glTF loaders: [gltf_scene_source.cpp](engine/gfx/support/sre/src/scene/gltf_scene_source.cpp) (1024 lines) and [indirect_scene_source.cpp](engine/gfx/support/sre/src/scene/indirect_scene_source.cpp) (742 lines). Each has its own "Golden parity" quirks.
- **GPU buffers:** `GpuGroup` stores each mesh once per group. A mesh used in two groups is stored twice.
- **Instances and transforms:**
  - The `Instance` list is fixed: count, order, mesh and group never change ([scene_contract.h](engine/gfx/support/sre/include/ip/gfx/sre/scene/scene_contract.h) line 292).
  - The hierarchy is private to each source.
  - Stress3D techniques give each instance its own `NodeUbo` ([stress3d_uniforms.h](engine/gfx/support/sre/include/ip/gfx/sre/techniques/common/stress3d_uniforms.h) lines 48-55).
  - The indirect technique uses an SSBO plus instance streams.
- **Draws:**
  - `DirectDraw` issues one draw per `DrawItem`, which is one primitive of one instance.
  - `IndirectDraw` uses commands built inside `IndirectPbrTechnique` ([indirect_pbr_technique.cpp](engine/gfx/support/sre/src/techniques/indirect/indirect_pbr_technique.cpp) lines 91-150).

### 1.2 Per-instance data path in SRE today
- **Pbr, Caustic, Tessellation:** set 1 is a per-instance `NodeUbo` descriptor set, bound in every `bindDrawState`.
  - Unskinned matrices are stored column-major and skinned ones row-major (a Golden parity quirk in `packNodeMatrix`).
  - Pbr's material goes through 5 single-sampler sets (sets 3-7) plus a fragment push constant ([pbr_technique.cpp](engine/gfx/support/sre/src/techniques/stress3d/pbr_technique.cpp) lines 350-379).
- **Hair:** the world matrix is written into `SceneUbo.model`, taken from the group's first instance. `gl_InstanceIndex` is the shell index.
- **Meshlet:** no per-instance transform; vertices sit pre-baked in a group-wide pool.
- **IndirectPbr:** instance-rate `R32_SINT` attributes for node slot, material and skin offset (bindings 6-8), read from a node-matrix SSBO. The material comes from a material SSBO and textures from `sampler2D[128]`.

### 1.3 Concrete duplication and waste in SRE
1. **Per-instance node UBOs:** `NodeUbo` is 8260 bytes, with one buffer and one descriptor set per instance per technique. Each `Stress3dUniforms` holds its own copy (`stress3d_uniforms.h:130-133`).
   - Example: pet_cabaret's 4 techniques each allocate their own copies, and 1000 instances cost 8.26 MB per technique.
   - The new design costs 80 bytes per instance, shared by all techniques.
2. **Geometry duplicated per group:** a mesh referenced from two glTF scenes is uploaded twice, because `uploadGroups` packs per group.
3. **Meshlet pool parity quirk:** every meshlet primitive re-appends all positions and normals met so far in the group (`gpu_scene.h:61`). Storage grows quadratically with the number of meshlet primitives.
4. **Two loaders** for the same `.scene` format, plus the separate asset sets `scenes/` and `scenes_indirect/` built from the same glTF files.
5. **Duplicated light, camera and view uniform buffers** in every technique.
6. **Scenario logic inside techniques and sources rather than the script:**
   - zoom-to-fit spin (`computeZoomToFitModel`);
   - `cameras.back()` and `lights.back()` selection;
   - the camera Y-flip in the indirect loader;
   - the hairmark torus animation callback.

### 1.4 Dependency map

```mermaid
flowchart LR
  subgraph legacy [Legacy, untouched]
    gfx_scene
    gfx_scene_indirect
    Stress3dVessel --> gfx_scene
    FurryOrbVessel --> gfx_scene
    PetCabaretVessel --> gfx_scene
    HairmarkVessel --> gfx_scene
    IndirectVessel --> gfx_scene_indirect
    SceneRenderVessel --> gfx_scene
  end
  subgraph sre [gfx_sre]
    scene_contract --> GltfSceneSource
    scene_contract --> IndirectSceneSource
    scene_contract --> ProceduralSceneSource
    GpuScene --> techniques
    techniques --> draw_strategy
    draw_strategy --> recipe
  end
  GltfSceneSource -->|"SceneAsset"| gfx_scene
  IndirectSceneSource -->|"SceneAsset"| gfx_scene
  SreVessel --> sre
  SreVessels["10 Sre*Vessel classes"] --> SreVessel
  gfx_sre_test --> sre
  composite["vessel_sre_stress3d_test.cpp: 12 tests"] --> SreVessels
```

Consumers of `ip/gfx/sre/scene/*`: only `gfx_sre` itself, `gfx_sre_test`, and the `vessel/sre/` and `vessel/include/test/gfx/sre/` files. `vessel_viewer` and `scene_viewer` do not use SRE.

## 2. Proposed architecture

```mermaid
flowchart TD
  VesselScript["SreVessel subclass: start and onStep"] -->|"create, clone, reparent, edit"| Scene
  AssetLibrary -->|"Prefab is a template Scene"| Scene
  GltfLoader --> AssetLibrary
  Scene -->|"sync once per step"| RenderScene
  AssetLibrary -->|"upload once"| RenderScene
  RenderScene --> GeometryPool
  RenderScene --> SceneBuffers["Instances, Joints, Materials, Lights, View, Textures"]
  RenderScene --> DrawLists["DrawList per filter: batches, records, indirect commands"]
  DrawLists --> Direct[DirectDraw]
  DrawLists --> Batched[BatchedDraw]
  DrawLists --> Indirect[IndirectDraw]
  DrawLists --> Shell[ShellDraw]
  DrawLists --> Meshlet[MeshletDraw]
```

### 2.1 Layering (sre.rst "Layers" stays true)
- `scene/` (may include vkutil):
  - `entity.h`, `components.h`, `scene.h` and `scene_systems.h` (animation and bounds): pure CPU code, no Vulkan.
  - `asset_library.h` and `gltf_loader.h`: CPU code, using `gfx_scene`'s `SceneAsset`.
  - `render_scene.h`: GPU sync, using vkutil.
- `frame/` stays below `scene/`. Because `scene/` may not include `frame/`, `RenderScene` exposes its buffers and the recipe/technique layers import them into the frame graph. This mirrors how techniques import `GpuGroup` buffers today.
- Deleted at the end: `gltf_scene_source.*`, `indirect_scene_source.*`, `procedural_scene_source.*`, `gpu_scene.*`, `scene_conformance.*`, and the `ISceneSource`/`FrameState` part of `scene_contract.h`. That file is renamed `scene_types.h`; it keeps `MeshData`, `MaterialData`, `TextureData` and `LightData`, adds `SkinData` and `AnimationClip`, and sets the version to 2.

### 2.2 Core data model: a lightweight ECS with the hierarchy in the node table

The design is entity handles plus one dense node table holding the required transform and the hierarchy links, plus one typed component pool per component type.

Why this rather than a pointer-based node tree like `GltfSceneSource::Node` (unique_ptr nodes with parent and child pointers):
- Handles survive clones and destruction safely: stale handles are detected rather than dangling.
- Cloning becomes a table copy plus a handle remap.
- GPU sync can walk dense arrays.
- "Query components by type" becomes a pool lookup instead of a scan of each node's list.

Why not a full ECS library: the repo has no entt or flecs in its Conan dependencies, archetype storage is unnecessary at test scale, and determinism is easier with simple ordered storage.

- **`Entity`:** `{uint32_t index; uint32_t generation;}`, following the style of `frame::ResourceHandle`.
  - Destroying an entity bumps its generation, so stale handles are detected: `isAlive()` returns false and accessors throw `std::invalid_argument`.
  - Freed indices are reused lowest first, so allocation is deterministic.
- **Required `Transform` per node:** local TRS plus an optional glTF `matrix` (local = TRS × matrix, as both loaders compute today). The node also caches `world` and a `dirty` flag.
- **Hierarchy:** each node stores `parent`, `firstChild`, `lastChild`, `prevSibling` and `nextSibling` as entity indices, so child order is explicit and sibling order is the order children were added.
  - Roots form an ordered list of their own.
  - Draw order and every traversal are pre-order: roots in order, children in order. This matches `GltfSceneSource::buildInstances` today.
- **Components:** `ComponentPool<T>` is a sparse-set pool (dense values plus an entity-to-slot map), type-erased behind `IComponentPool`, with a per-type id from `componentTypeId<T>()`.
  - Built-in components: `Name`, `MeshRenderer`, `SkinnedMesh`, `Light`, `CameraComponent`, `AnimationPlayer`.
  - Users may define their own component types; no registration call is needed.
  - Pools iterate in ascending entity index order (the dense array is kept sorted on insert), so views are deterministic.
- **Dirty tracking:**
  - Mutable access (`editTransform()`, `edit<T>()`) sets a per-entity dirty bit; const `get` does not.
  - Structural changes (create, destroy, reparent, add or remove a renderable component) bump `structureVersion`, which tells `RenderScene` to rebuild its draw lists.

### 2.3 Asset and resource layer
- **`AssetLibrary` owns resources:** `MeshData`, `MaterialData`, `TextureData`, `SkinData`, `AnimationClip` and `Prefab`, addressed by the existing ids (`MeshId`, `MaterialId`, `TextureId`) plus new `SkinId`, `AnimationId` and `PrefabId`.
- **Deduplication:**
  - Within an asset, by glTF mesh, material, texture and skin index.
  - Across loads, by asset path: loading the same `.scene` twice returns the same `PrefabId`.
- **A prefab is itself a `Scene`** (the template) owned by the library, with a root entity holding one child root per glTF scene.
  - `instantiate()` is a cross-scene subtree clone. Resource ids are copied, never the data, so there is one clone implementation shared by `instantiate` and `clone`.
  - glTF scenes become layer bits (`GltfLoadOptions::layerPerScene`, default true): scene i gets `MeshRenderer::layers = 1 << i`. This replaces the old "group" concept; furry orb uses groups 0-3.
- **One glTF loader** (`gltf_loader.cpp`) built on the existing [gltf_accessors.h](engine/gfx/support/sre/src/scene/gltf_accessors.h):
  - Applies glTF-correct defaults: normalized normals, missing streams zero-filled rather than reading accessor 0, primitives without a material use a default material, and animations use linear, step or slerp sampling over glTF key times.
  - The camera Y-flip and the legacy left-handed light and camera fixes become a load option, `GltfLoadOptions::legacyLeftHanded`, applied to camera and light entities. They do not live in technique code.
- **Material instances (Unity `renderer.material` semantics):** `AssetLibrary::cloneMaterial(id)` returns a new `MaterialId`. `MeshRenderer::materials[i]` overrides the mesh primitive's default material. Materials may be added or edited after the GPU upload, up to `RenderSceneCapacity::materials`.
- **Procedural meshes** (hairmark, Donut's cube) use `AssetLibrary::addMesh(makeMeshData(mesh))`, with `makeMeshData` kept from [procedural_scene_source.h](engine/gfx/support/sre/include/ip/gfx/sre/scene/procedural_scene_source.h).
- **External textures:** `RenderScene::registerImageTexture(name)` reserves a texture-array slot whose descriptor is written once the frame graph is realized. Furry orb's render-to-texture floor then becomes a cloned material referencing that slot, replacing the current `PbrOptions::baseColorImage` special case.

### 2.4 Render-sync layer (`RenderScene`)
- **Built once from the frozen `AssetLibrary`:**
  - One global geometry pool: six vertex streams plus u32 indices rebased per mesh, with `vertexOffset = 0`, as in the indirect path. Each mesh is stored exactly once whatever its layers.
  - Per-mesh meshlet ranges, with no cumulative re-append.
  - The material SSBO and the texture array.
- **Fixed-capacity buffers** (`RenderSceneCapacity`), each a single-binding descriptor set shared by every technique:
  - `View` UBO: view, projection, view-projection, camera position;
  - `Instances` SSBO: `InstanceData` = world mat4 (column-major, one convention), joint offset, joint count, flags; 80 bytes;
  - `Joints` SSBO;
  - `Materials` SSBO;
  - `Lights`;
  - `Textures`: `sampler2D[k_MaxTextureCount]`, one binding holding an array.
- **`sync(Scene&, StepTime)`, once per step on the CPU:**
  1. Propagate world transforms in pre-order, only for subtrees with dirty nodes.
  2. Allocate a stable instance slot per renderable entity (lowest free slot first) and free it on destroy.
  3. Collect dirty slots, material edits, joints (computed relative to the instance, as the contract does today) and lights.
  4. If `structureVersion` changed, rebuild every requested `DrawList`.
- **`recordUpdates(cmd)`:** `vkCmdUpdateBuffer` in chunks of at most 64 KiB (as `indirect_pbr_technique.cpp:470-475` does), for dirty ranges only. The first step writes everything.
  - `Recipe::build()` records the `RenderScene` updates first in its `update` pass, then the techniques' updates.
  - Draw-list record and command buffers are updated the same way, so recording ahead with `SreVessel` stays correct.
- **`DrawList`, requested by a technique before `importResources`:**
  - Requested with `DrawFilter{layers, order}`, where `order` is `Hierarchy` (pre-order, required for blending) or `ByMesh` (mesh, then primitive, then instance; the batching order).
  - Output:
    - `DrawBatch`es, each `{mesh, primitive, firstIndex, indexCount, vertexCount, material, firstRecord, recordCount, hasMeshlets}`;
    - a `DrawRecord` stream `{instanceSlot, material}`, an instance-rate vertex buffer of `R32G32_UINT`;
    - an indirect command buffer with one `DrawIndexedIndirectCommand` per indexed batch, where `firstInstance = firstRecord`.
  - In `Hierarchy` order each batch has `recordCount = 1`; in `ByMesh` order it covers every instance of the mesh primitive.
- **Draw strategies** (one shared data source; [draw_strategy.h](engine/gfx/support/sre/include/ip/gfx/sre/draw/draw_strategy.h)):
  - `DirectDraw`: for each batch and record, `bindDrawState`, then `drawIndexed(count, 1, first, 0, firstInstance = record)`.
  - `BatchedDraw` (new): one `drawIndexed(count, recordCount, first, 0, firstRecord)` per batch.
  - `IndirectDraw`: `drawIndexedIndirect` over the list's command buffer. It now takes a `DrawList` rather than a technique-built buffer.
  - `ShellDraw` (renamed `InstancedDraw`): per record, push `{instance, material}`, then `drawIndexed(count, shellCount(), first, 0, 0)`. `gl_InstanceIndex` stays the shell index.
  - `MeshletDraw`: per record, push `{instance, meshletOffset, meshletCount}`, then `drawMeshTasksEXT(meshletCount)`. Only this mesh's meshlets are dispatched.

### 2.5 Shader changes (new `sre_*` files; legacy files untouched)
- **New shared include** `shaders/glsl/sre_scene.glsl` declares:
  - set 0 View, set 1 Instances, set 2 Joints, set 3 Materials, set 4 Textures, set 5 Lights;
  - sets 6 and up for technique-specific bindings (caustic `sampler2DArray`, hair texture array, meshlet SSBOs).
- **Vertex-input techniques** (`sre_pbr`, `sre_fur`, `sre_caustic`, `sre_pn_tess`, `sre_stress_tess`, `sre_spotlights`):
  - Bindings 0-5 are the per-vertex streams. Binding 6 is the per-instance `layout(location = 6) in uvec2 inDrawRecord;` read through `firstInstance`.
  - The world matrix is `instances[inDrawRecord.x].world` and the material is `materials[inDrawRecord.y]`.
- **Shell and mesh techniques** (`sre_fur` under `ShellDraw`, `sre_hair`, `sre_meshlet`): `{uint instance; uint material; ...}` comes from a vertex/mesh-stage push constant, so `gl_InstanceIndex` stays the shell index.
  - Fur is drawn by `ShellDraw` only; the Pbr technique picks the push-constant path when its program is Fur.
- **PBR materials move to the material SSBO plus the texture array**, replacing per-material sampler sets 3-7 and the 28-byte push constant.
  - Stress3D PBR shading and indirect PBR shading stay as two technique families (two fragment shaders) unless you choose to merge them (see open questions). Both read the same scene sets.
- **Skinning:** joint matrices relative to the instance come from the Joints SSBO at `jointOffset`, with no 128-joint UBO cap.
- **The zoom-to-fit spin leaves the shader UBO.** The vessel script spins a turntable entity instead (section 3).
- **Per shader family, each change includes:**
  - the GLSL;
  - a new `.pipe` file (`userDataNode` per set, `INPUT_RATE_INSTANCE` binding 6, push-constant dwords);
  - `add_shader_executable` targets in [graphics_core/test/CMakeLists.txt](engine/gfx/graphics_core/test/CMakeLists.txt);
  - entries in the 7 device lists `device/{navi48,strix1_b0,medusa1,medusa2,krackan2e,alphatrion2,phoenix}/CMakeLists.txt`;
  - amdllpc, then `disassemble_elf`, producing `shaders/{gfx11,gfx11_5,gfx12,gfx13}/sre_*.sp3` plus `pipeline_metadata/<gen>/sre_*.toml`;
  - `add_elf_shader_pipeline` entries.

### 2.6 Public API sketch (signatures only)

```cpp
namespace diag::ip::gfx::sre::scene
{
struct Entity
{
    uint32_t index      = std::numeric_limits<uint32_t>::max();
    uint32_t generation = 0U;
    [[nodiscard]] bool isValid() const noexcept;
    friend bool operator==(const Entity&, const Entity&) = default;
};

struct Transform
{
    Vector3    translation{Vector3::k_Zero};
    Quaternion rotation{Quaternion::k_Identity};
    Vector3    scale{Vector3::k_UnitScale};
    // NOLINTBEGIN(readability-redundant-member-init)
    std::optional<Matrix4> matrix{};  ///< glTF node matrix, applied after TRS
    // NOLINTEND(readability-redundant-member-init)
    [[nodiscard]] Matrix4 getLocalMatrix() const;
};

enum class ParentMode : uint8_t { KeepWorld, KeepLocal };
enum class DrawOrder : uint8_t { Hierarchy, ByMesh };

struct Name           { std::string value{}; };
struct MeshRenderer   { MeshId mesh = 0U; std::vector<MaterialId> materials{}; uint32_t layers = 1U;
                        bool visible = true; std::optional<vk::Extent2D> fragmentShadingRate{}; };
struct SkinnedMesh    { SkinId skin = 0U; std::vector<Entity> joints{}; };
struct Light          { LightData data{}; };          ///< pose from the entity's world transform
struct CameraComponent{ Camera camera{}; };           ///< pose from the entity's world transform
struct AnimationPlayer{ AnimationId clip = 0U; std::vector<Entity> targets{}; bool loop = true; };

template<typename... Ts> class View;  ///< ascending entity order; begin()/end() yield (Entity, Ts&...)

class Scene final
{
public:
    Scene();
    ~Scene();
    TNG_UNCOPYABLE_MOVABLE(Scene)

    // Entities
    Entity createEntity(std::string_view name = {}, Entity parent = {}, const Transform& local = {});
    void   destroy(Entity entity);                            ///< entity and all descendants
    [[nodiscard]] bool   isAlive(Entity entity) const noexcept;
    Entity clone(Entity source, Entity parent = {});          ///< deep subtree clone, resources shared
    Entity cloneFrom(const Scene& source, Entity sourceRoot, Entity parent = {}, const Transform& local = {});

    // Hierarchy
    void setParent(Entity child, Entity parent, ParentMode mode = ParentMode::KeepWorld);
    void setSiblingIndex(Entity entity, uint32_t index);
    [[nodiscard]] Entity              getParent(Entity entity) const;
    [[nodiscard]] std::vector<Entity> getChildren(Entity entity) const;
    [[nodiscard]] std::span<const Entity> getRoots() const noexcept;
    [[nodiscard]] Entity find(Entity root, std::string_view path) const;  ///< "a/b/c" by Name
    template<typename F> void forEachInSubtree(Entity root, F&& visit) const;  ///< pre-order

    // Transform
    [[nodiscard]] const Transform& getLocalTransform(Entity entity) const;
    Transform& editTransform(Entity entity);                  ///< marks the subtree dirty
    void setLocalTransform(Entity entity, const Transform& local);
    void setLocalPosition(Entity entity, const Vector3& position);
    void setLocalRotation(Entity entity, const Quaternion& rotation);
    void setLocalScale(Entity entity, const Vector3& scale);
    [[nodiscard]] Matrix4 getWorldMatrix(Entity entity);      ///< propagates lazily
    void setWorldMatrix(Entity entity, const Matrix4& world);
    void updateWorldTransforms();

    // Components
    template<typename T> T& add(Entity entity, T component = {});
    template<typename T> void remove(Entity entity);
    template<typename T> [[nodiscard]] bool has(Entity entity) const;
    template<typename T> [[nodiscard]] const T& get(Entity entity) const;
    template<typename T> [[nodiscard]] const T* tryGet(Entity entity) const;
    template<typename T> T& edit(Entity entity);              ///< marks the component dirty
    template<typename T> T* tryEdit(Entity entity);
    template<typename T> [[nodiscard]] T* findInChildren(Entity root);  ///< first in pre-order
    template<typename... Ts> [[nodiscard]] View<Ts...> view();

    [[nodiscard]] uint64_t getStructureVersion() const noexcept;
};

struct GltfLoadOptions { bool layerPerScene = true; bool legacyLeftHanded = true; float defaultCameraAspectRatio = 1.0F; };

class AssetLibrary final
{
public:
    PrefabId   loadGltf(std::string_view assetPath, const vkutil::DeviceContext& device, const GltfLoadOptions& options);
    MeshId     addMesh(MeshData mesh);
    MaterialId addMaterial(MaterialData material);
    TextureId  addTexture(TextureData texture);
    MaterialId cloneMaterial(MaterialId source);
    MaterialData& editMaterial(MaterialId material);          ///< marks it dirty for RenderScene
    [[nodiscard]] const Prefab&   getPrefab(PrefabId prefab) const;
    [[nodiscard]] const MeshData& getMesh(MeshId mesh) const;
    [[nodiscard]] std::span<const MaterialData> getMaterials() const noexcept;
    void freezeResources();                                   ///< called by RenderScene; later mesh/texture adds throw
};

Entity instantiate(Scene& scene, const AssetLibrary& assets, PrefabId prefab,
                   Entity parent = {}, const Transform& local = {});
void animate(Scene& scene, const AssetLibrary& assets, const StepTime& time);  ///< every AnimationPlayer
[[nodiscard]] BoundingBox computeWorldBounds(Scene& scene, const AssetLibrary& assets, Entity root);

struct RenderSceneCapacity { uint32_t instances = 4096U; uint32_t drawRecords = 16384U; uint32_t materials = 1024U;
                             uint32_t joints = 8192U; uint32_t lights = 16U; };
struct DrawFilter { uint32_t layers = ~0U; DrawOrder order = DrawOrder::Hierarchy; };
struct DrawListHandle { uint32_t index = std::numeric_limits<uint32_t>::max(); };
struct RenderStats { size_t geometryBytes; size_t instanceBytes; size_t materialBytes; uint32_t instances;
                     uint32_t batches; uint32_t records; };

class RenderScene final
{
public:
    RenderScene(const vkutil::DeviceContext& context, AssetLibrary& assets, const RenderSceneCapacity& capacity);
    DrawListHandle requestDrawList(const DrawFilter& filter);
    TextureId      registerImageTexture(std::string name);
    void bindImageTexture(TextureId texture, vk::ImageView view);
    void setActiveCamera(Entity camera);
    void sync(Scene& scene, const StepTime& time);
    void recordUpdates(vk::raii::CommandBuffer& commandBuffer);
    [[nodiscard]] const GpuGeometry& getGeometry() const noexcept;
    [[nodiscard]] const SceneSets&   getSets() const noexcept;      ///< View..Lights sets and layouts
    [[nodiscard]] std::span<const vkutil::GpuBuffer* const> getUpdatedBuffers() const;  ///< for frame-graph import
    [[nodiscard]] const DrawList& getDrawList(DrawListHandle handle) const;
    [[nodiscard]] const StepTime& getTime() const noexcept;
    [[nodiscard]] RenderStats getStats() const noexcept;
};
}  // namespace diag::ip::gfx::sre::scene
```

**Technique interface changes** ([technique.h](engine/gfx/support/sre/include/ip/gfx/sre/techniques/technique.h)):
- `getGroup()` becomes `getDrawList()`.
- `bindDrawState(cmd, const scene::DrawBatch&, uint32_t record)`.
- `getDrawRepeatCount()`/`bindRepeatState()` stay; caustic draws each primitive twice.
- `importResources` imports the `RenderScene` buffers once, through a helper in `techniques/common/scene_resources.h`.

## 3. Vessel integration

**`SreVessel` gains a script hook** ([sre_vessel.h](engine/gfx/graphics_core/test/vessel/include/test/gfx/sre/sre_vessel.h)):
- `Scene` becomes `{AssetLibrary assets; scene::Scene world; unique_ptr<RenderScene> renderScene; techniques}`.
- New `virtual void onStep(scene::Scene&, scene::AssetLibrary&, const StepTime&) {}`.
- `recordStep()` runs `onStep()`, then `scene::animate()`, then `renderScene->sync()`, then `frameGraph->record()`.
- `buildRecipe()` is the "Start" phase: load prefabs, instantiate, create techniques and passes.
- `loadGltfSource` and `loadIndirectSource` become a single `loadPrefab(context, options)`.

**Unity example in Vessel pseudo-C++:**

```cpp
SreVessel::Scene SrePrefabHierarchyVessel::buildRecipe(RecipeContext& context)
{
    Scene s = makeScene(context);  // assets, world, renderScene (capacity defaults)
    const auto prefab = s.assets.loadGltf(context.asset, context.device, {});

    const auto parent = instantiate(s.world, s.assets, prefab, {}, {.translation = {0, 0, 0}});
    s.world.edit<sre::scene::Name>(parent).value = "Parent_Instance";
    if (auto* renderer = s.world.findInChildren<sre::scene::MeshRenderer>(parent))
    {
        const auto material = s.assets.cloneMaterial(s.assets.getMesh(renderer->mesh).primitives[0].material);
        s.assets.editMaterial(material).baseColorFactor = {0, 0, 1, 1};
        renderer->materials = {material};  // like Unity's renderer.material: a per-instance copy
    }

    const auto child = instantiate(s.world, s.assets, prefab, {}, {.translation = {2, 0, 0}});
    s.world.edit<sre::scene::Name>(child).value = "Child_Instance";
    s.world.setLocalScale(child, {0.5F, 0.5F, 0.5F});
    s.world.setParent(child, parent, ParentMode::KeepWorld);  // Unity SetParent default
    s.world.setLocalPosition(child, Vector3::k_Zero);

    auto& pbr = s.addTechnique<PbrTechnique>(context.device, *s.renderScene, PbrOptions{.flavor = context.shaderFlavor,
                                             .drawList = {.order = DrawOrder::ByMesh}, .extent = context.extent});
    context.recipe.addRasterPass("forward").colorTarget(context.swapchainColor, getSceneClearColor())
        .depthTarget(context.swapchainDepth, sre::frame::clearDepth(1.0F)).draw(pbr, sre::draw::BatchedDraw{});
    return s;
}
```

**Existing vessels rewritten.** None needs scene-specific engine code.
- **Water bottle (`gfx.sre.20`):**
  - `buildRecipe` instantiates the prefab under a `turntable` entity.
  - The zoom-to-fit scale and offset come from `computeWorldBounds`, computed once.
  - `onStep` sets the turntable rotation to 2π × fraction.
  - A default camera entity and a default directional light entity are created by `SreVessel::addDefaultCameraAndLight()`.
- **Food store and mountain range, direct and indirect (`gfx.sre.1`, `.2`, `.110`, `.111`, `.120`, `.121`):**
  - Both load the same prefab: the indirect TCDs move to `media/common/gfx/scenes/*.scene` (open question 3).
  - The direct variant uses `PbrTechnique` with `DirectDraw`; the indirect variant uses `IndirectPbrTechnique` with `IndirectDraw`.
  - glTF animations run through `AnimationPlayer` with no per-test code.
  - The camera is the glTF camera entity, chosen by `setActiveCamera(world.find(root, "..."))`, or by the loader's ordered camera list `prefab.cameras[i]` for index-based TCDs.
- **Hairmark (`gfx.sre.5`):** `addMesh(makeMeshData(asset.makeMesh(0)))`, then one entity with a `MeshRenderer`. `onStep` writes the torus transform. The camera entity is created in `buildRecipe`. The technique uses `ShellDraw{500}`.
- **Furry orb (`gfx.sre.3`):**
  - Layers 0-3 come from the glTF scenes.
  - The floor entities get a cloned material whose base color is `registerImageTexture("furryOrbTexture")`.
  - The orb uses `ShellDraw{step % 150}`.
- **Pet cabaret, stress tess, dragon taskmesh (`gfx.sre.4`, `.11`, `.12`):** a technique per layer with matching filters. Meshlets come from per-mesh ranges.

**Donut generality check:**
- **`deferred_shading`:**
  - Procedural cube: `addMesh` and `addMaterial` with a texture.
  - Entities `CubeNode` (with `MeshRenderer`) and its child `Sun` (with `Light`, directional).
  - A G-buffer technique (multiple render targets) with `DirectDraw`, then a lighting compute pass added to the frame graph directly, which SRE already supports.
  - The final blit is not implemented on the TNG driver and becomes a fullscreen raster pass.
  - No scene-specific code is needed. The G-buffer and lighting techniques would be new techniques, not part of this plan.
- **`bindless_rendering`:**
  - Load Sponza and instantiate it once.
  - Donut's instance, geometry and material structured buffers map to set 1 (Instances), the `DrawBatch`/`DrawRecord` stream and set 3 (Materials). Its push constant `int2(instance, geometry)` is exactly `DrawRecord`.
  - Differences: SRE has a 128-texture array versus Donut's 1024, and uses vertex attributes instead of raw-buffer vertex pulling, since there is no buffer device address. Pulling would still be possible with SSBO arrays.

## 4. Semantics and edge cases
- **Reparenting:**
  - `KeepWorld` (the default, as in Unity) recomputes local = inverse(parentWorld) × world and decomposes it into TRS. If the result is not representable as TRS (shear from non-uniform scale under rotation), the local is stored in `Transform::matrix` with TRS reset, and a debug log is written.
  - `KeepLocal` leaves the local untouched.
  - Parenting to self or to a descendant throws. An empty `parent` makes the entity a root, appended last.
- **Destroying a node with children** destroys the whole subtree (Unity semantics), in post-order. Their slots and handles are freed, `structureVersion` is bumped, and there is no orphaning option. To keep children, reparent them first.
- **Cloning:**
  - Deep copy of nodes and all components. Resource ids (meshes, materials including cloned ones, skins and clips) are shared.
  - Entity references inside the subtree are remapped: `SkinnedMesh::joints` and `AnimationPlayer::targets`. References to entities outside the subtree are kept as they are.
  - `Name` is copied unchanged; names are not unique.
  - User component types are copied with their copy constructor, so a user component holding an `Entity` gets no automatic remapping. A `remapEntities` hook is optional and documented.
- **Handle invalidation:** generational handles never dangle. Pointers or references returned by `get`/`edit`/`tryGet` are valid only until the next structural change to that component pool; this is documented, and debug builds check it with the guard below.
- **Mutation during iteration:**
  - A `View` or `forEachInSubtree` holds an iteration guard. Structural changes while one is live trigger a `TNG_ASSERT` in debug builds and are undefined in release.
  - Value edits through `edit` are allowed.
  - Scripts that spawn or destroy while iterating collect handles first, a pattern shown in the doc.
- **Determinism for CRC tests:**
  - No `unordered_map` iteration feeds output.
  - Draw order follows the pre-order hierarchy, or a stable mesh, primitive, entity order.
  - Slots are allocated lowest first; transforms are propagated in a fixed order.
  - Animation uses `StepTime` only; there are no wall-clock or thread-pool-dependent results (texture transcoding stays parallel, but each result lands in its own fixed slot).
  - `RenderScene` writes its full state on the first step.
- **Capacity overflow:** `sync()` throws `std::length_error` naming the exceeded capacity. Asset additions after `freezeResources()` throw `std::logic_error`.
- **Mesh, primitive and material validity:** the asset checks from `scene_conformance` move to `checkAssets(const AssetLibrary&)`. `RenderScene` runs it at construction, and a debug-only `checkScene(const Scene&)` covers hierarchy invariants.

## 5. Migration plan (each step builds and passes `gfx_sre_test`; old and new coexist until step 7)
1. **CPU scene core:** `entity.h`, `components.h`, `scene.h`/`.cpp`, `scene_systems` (animation, bounds), with unit tests. No render changes.
2. **Asset library and unified glTF loader:**
   - Add `scene_types.h`, a copy of the contract types plus `SkinData`/`AnimationClip`, at version 2.
   - `scene_contract.h` stays temporarily and aliases the shared types.
   - Tests use procedural assets plus a small glTF fixture.
3. **`RenderScene`:**
   - Geometry pool, scene sets, slots, draw lists, stats.
   - New draw strategies `BatchedDraw`, `ShellDraw`, and an `IndirectDraw(DrawList)` overload.
   - `Recipe::build()` records `RenderScene` updates.
   - Old `GpuScene` and techniques stay.
4. **Shared shader include plus `sre_pbr_indirect` (SPIR-V only, as today):**
   - Port `IndirectPbrTechnique` to `RenderScene` and switch the food-store and mountain-range indirect vessels.
   - New goldens for `gfx.sre.110`, `.111`, `.120` and `.121`.
5. **Stress3D family, one vessel per change**, each with its shaders, `.pipe` files, CMake and device entries, regenerated `.sp3`/TOML for 4 generations, and new goldens:
   - water bottle (`gfx.sre.20`; Pbr);
   - food store (`gfx.sre.1`);
   - mountain range (`gfx.sre.2`);
   - stress tess (`gfx.sre.11`; Pbr plus PnTriangles);
   - dragon (`gfx.sre.12`; Meshlet);
   - hairmark (`gfx.sre.5`; Hair, `ShellDraw`);
   - furry orb (`gfx.sre.3`; Pbr, Fur, render-to-texture);
   - pet cabaret (`gfx.sre.4`; Caustic, StressTess, Spotlights, 4× MSAA).
6. **New coverage:** the prefab-hierarchy vessel (the Unity example), the dedup/equivalence vessel (section 6), and their TCDs and goldens.
7. **Delete the old path:**
   - `GltfSceneSource`, `IndirectSceneSource`, `ProceduralSceneSource`, `GpuScene`/`GpuGroup`/`DrawItem`, `scene_conformance`, `Stress3dUniforms`'s `NodeUbo` path, `InstancedDraw`, the `ISceneSource`/`FrameState` part of `scene_contract.h`, and `SreVessel::loadGltfSource`/`loadIndirectSource`.
   - Replace `scene_contract_test.cpp` with `scene_types_test.cpp`, which guards v2 the same way.
   - Delete `procedural_scene_source_test.cpp` and `scene_conformance_test.cpp`, replaced by the step 1-3 tests.
   - Update [sre.rst](engine/gfx/support/sre/doc/sre.rst) (layers, scene section, the "Golden parity" section narrowed to legacy goldens no longer applying, test numbering, how to extend) and [.cursor/rules/sre.mdc](.cursor/rules/sre.mdc), removing the frozen-contract and Golden parity rules.

**Goldens and CRCs expected to change (all `gfx.sre.*`; new CSVs under `engine/gfx/graphics_core/test/composite/reference/`, e.g. `gfx-sre.csv`, `gfx-sre-hairmark.csv`, `gfx-sre-hairmark_prbs.csv`, `gfx-sre-indirect-crc32.csv`, `gfx-sre-indirect-hhash.csv`; TCDs repointed):**
- **`gfx.sre.1` and `.2`** (food store, mountain range; spirv and elf, 4K and 1080p):
  - new vertex and fragment interface: instance SSBO plus `firstInstance`, material SSBO plus texture array;
  - one matrix convention, replacing the column-major/row-major quirk;
  - default-material and texture-coordinate fallbacks;
  - unified animation sampling.
- **`gfx.sre.3`** (furry orb): all of the above, plus the fur shell push-constant path and the render-to-texture floor as a material instance.
- **`gfx.sre.4`** (pet cabaret): the Caustic, Tessellation and Spotlights interfaces; joints move from a UBO to an SSBO.
- **`gfx.sre.5`** (hairmark, every resolution and PRBS case): hair moves from `SceneUbo.model` to the instance SSBO plus a push constant; the camera becomes an entity.
- **`gfx.sre.11`** (stress tess): Pbr and PnTriangles interfaces; the scene set's stage mask is fixed.
- **`gfx.sre.12`** (dragon): per-mesh meshlet ranges replace the cumulative pool, and the instance transform now applies.
- **`gfx.sre.20`** (water bottle): the spin moves from `SceneUbo.model` to a turntable entity, and the matrix order of operations changes.
- **`gfx.sre.110`, `.111`, `.120`, `.121`:**
  - the `.scene` file may move from `scenes_indirect/` to `scenes/` (open question 3);
  - the indirect loader quirks go: camera Y-flip, last-material default, accessor-0 fallback, "unset" material factors, the light position taken from the camera's unflipped Y;
  - node slot numbering and command order change.
- Unchanged: all `gfx.stress3d.*`, `gfx.indirect.*`, `gfx.gfxoff`, `gfx.harvest.3` and the cube tests, plus their CSV files.
- Approval process: images dumped with `dump_image.when = "Always"` on hardware are compared visually against the legacy dumps. Visual differences must be explained by the intentional fixes above.

## 6. Validation
- **Unit tests** (`engine/gfx/support/sre/test/unit/software/`, listed in its CMakeLists):
  - `scene_hierarchy_test`: create and destroy a subtree, sibling order, roots, pre-order, stale handle detection, deterministic index reuse.
  - `scene_transform_test`: world propagation, lazy `getWorldMatrix`, dirty subtree only, `setWorldMatrix`.
  - `scene_reparent_test`: `KeepWorld` and `KeepLocal`, the shear fallback, cycle rejection.
  - `scene_clone_test`: deep clone, shared resource ids, internal reference remap (joints, animation targets), outside references kept.
  - `scene_component_test`: add, remove, get, `tryGet`, edit dirty bits, views in ascending order, user component types, the iteration guard (death test in debug).
  - `asset_library_test`: dedup by glTF index and by path; `cloneMaterial`; `freezeResources` throwing.
  - `gltf_loader_test`: a fixture with two nodes sharing a mesh gives one `MeshData`; layers per scene; defaults.
  - `animation_test`: sampling at step fractions; bindings after clone.
  - `draw_list_test`: no Vulkan needed. Test `buildDrawLists()` (a pure function) for `Hierarchy` versus `ByMesh` order, batch merging, `firstRecord` offsets, indirect command contents, layer filters.
  - `render_scene_capacity_test`: overflow messages.
  - Port `recipe_test`, `pbr_technique_test` and `indirect_pbr_technique_test` to `RenderScene` fakes.
- **Deduplication metrics,** from `RenderScene::getStats()` and logged by the new dedup vessel:
  - instantiate the water bottle N = 1, 10, 100 and 1000 times;
  - `geometryBytes` must stay constant;
  - `instanceBytes` must equal 80 × N;
  - descriptor sets must stay constant (previously N per technique);
  - draw calls: N × primitives for `DirectDraw`, primitives for `BatchedDraw` and `IndirectDraw`.
  - The same counts are asserted in a unit test on `buildDrawLists`.
  - Comparison point: a legacy NodeUbo footprint of 8260 × N × techniques bytes.
- **Both strategies render identically:**
  - New test `gfx.sre.30`: one scene (food store plus 3 extra clones of a vehicle subtree) with four cases.
    - `DirectDraw` with `ByMesh` order;
    - `BatchedDraw`;
    - `IndirectDraw`;
    - `DirectDraw` with `Hierarchy` order (opaque only).
  - All four cases must match one golden column, so if each matches the golden they match each other. Same technique, same `DrawList` data, so the outputs must be byte-identical.
  - The `Hierarchy`-order case is allowed only on opaque content with `LessOrEqual` depth and no coplanar ties. If it differs, it gets its own column and is excluded from the equality claim.

## 7. Open questions and risks (please decide before implementation)
1. **Merge the two PBR families?** Stress3D `pbr.frag` and indirect `pbr_indirect.frag` shade differently. The plan keeps two techniques on one scene. Merging into one technique would remove another shader set but changes the look of one family.
2. **Runtime asset loading:** the plan freezes meshes and textures once `RenderScene` exists, because the frame graph is one-shot and uploads would overlap with in-flight steps. Is loading new glTF files mid-run needed? It would require `FrameGraph` reset and rebuild support plus a fence-safe upload path.
3. **One `.scene` per model:** move the `gfx.sre.11x` and `12x` TCDs from `scenes_indirect/` to `scenes/` (same glTF source; this halves asset loads). If the two generator outputs differ in content, keep both.
4. **Texture array size:** is 128 enough for food store, pet cabaret and furry orb under the unified path? This is checked in step 4; otherwise raise `k_MaxTextureCount`, together with the `.pipe` dword counts.
5. **Golden capture procedure and owners:** who records and approves the new CSV files on hardware, and which devices? The repo has no capture tool; only the CRC and HighwayHash dumpers that compare.
6. **ELF regeneration:** `disassemble_elf` is not in the repo. Please confirm the amdllpc version and flags that match the existing `.sp3` headers, so the 4 generations are reproducible.
7. **Default capacities** (4096 instances, 16384 records, 1024 materials, 8192 joints): accept these, or make them TCD parameters (`render.capacity.*`)?
8. **Notify the ECS owner** named in `scene_contract.h`, even though no code of theirs exists.
9. **Risk: GPU validation cost.** About 9 shader families × 4 generations, and 12 test families re-goldened. Mitigated by moving one vessel per change, and by keeping the legacy vessels as a visual reference.
