# Renderer architecture map

Map of how Hikari owns rendering: layers, RHI vocabulary, render graph, passes, and sync points. Behavior details and pass gates live in [Rendering](/docs/systems/rendering.md); this page is the structural overview.

**Active backends:** Metal (macOS) and D3D12 (Windows) share one graph path. Linux Vulkan is scaffolding only.

---

## Layer stack

```mermaid
flowchart TB
  subgraph game_thread [Game thread]
    World[World / entities]
    RF[RenderFrame publish]
    RCQ[RenderCommandQueue]
    World --> RF
    World --> RCQ
  end

  subgraph render_thread [Render thread]
    RT[renderer/render_thread.zig]
    Res[renderer_residency / consume]
    Core[renderer/renderer_core.executeFrame]
    Prep[prepareFrame — clusters, shadows, RT, cull]
    Assemble[pipeline.assemble]
    Compile[GraphBuilder.compile]
    Exec[pipeline.execute]
    Present[Adapter.finishOutput]
    RT --> Res --> Core
    Core --> Prep --> Assemble --> Compile --> Exec --> Present
  end

  subgraph shared_policy [Shared graphics — policy]
    Graph[rendergraph/*]
    RHI[rhi/types.zig]
    Drivers[rendergraph/drivers/*]
    Pipelines[renderer_pipelines / gbuffer / shadows]
  end

  subgraph platform [Platform — native only]
    Adapter[renderer_frame Adapter]
    Enc[pass_encoder]
    Native[Metal / D3D12 native]
    Adapter --> Enc --> Native
  end

  RF --> RT
  RCQ --> Res
  Assemble --> Graph
  Compile --> Graph
  Exec --> Drivers
  Drivers --> Enc
  Graph --> RHI
  Compile --> RHI
  Core --> Adapter
```

| Layer | Owns | Must not own |
|---|---|---|
| Game / World | Visibility, materials, lights, publish snapshot | Native GPU objects |
| `renderer_core` | Frame algorithm: prepare → assemble → compile → execute → present | API-specific barriers |
| `rendergraph` | Pass order, resource decls, liveness, barriers, queue acquires | Native command lists |
| `rhi/` | States, queues, formats, barrier POD, capabilities | Allocation / encoding |
| Platform `Adapter` | Resource tables, encode, GPU signal/wait, present | Pass ordering / feature gates |
| Native Metal/D3D12 | Device, heaps, PSO bytes, swapchain | Graph policy |

Invariant: **policy above RHI; platforms only lower**. Do not fork frame orchestration per API. Shared-type renderer policy state is `renderer.common` (`CommonState`).

Host capability is **declared, never probed**: every renderer states `pub const surface_support: renderer_contract.SurfaceSupport` (`.ui_only` for Class 1 test/headless hosts, `.scene_composite` for the full pipeline). `validateCommonState` checks the claim against the machinery scene assemble dispatches through, and `encodeSurfaceAssemble` returns `error.SurfaceClassUnsupported` rather than downgrading a scene surface to a UI-only graph. Inferring support from incidental fields is what let that downgrade happen silently in release.

### Shared code never asks what a backend has

The same rule applies to every structural question. Shared render code never branches on `@hasDecl` / `@hasField`; a probe compiles clean on both hosts and on every stub, so a rename would delete a code path on one backend instead of failing a build. Three replacements, in order of preference:

1. **Require the decl and let the backend with nothing to do write an explicit no-op** (`MetalRenderer.waitBeforeNextFrame`, `MetalResources.endUpload`) whose surrounding contract says why it is empty.
2. **Make a genuine capability difference a declared constant both sides give a value to** (`BackendOps.raytraced_light_clusters_slot`).
3. **When the question is unavoidable, give it one named predicate** with its backstop contract written next to it (`rendergraph/backend.supportsPluginPostProcess`, `dispatchesByKind`).

`renderer_shape.zig` states the shape shared code requires of a renderer, because Zig only analyses the branches it reaches. `zig build check-probes` (a dependency of `zig build test`) fails the build on a new branch probe; a probe used as a `@compileError` assertion is the fix and is not counted, and the remaining allowlisted branches carry a reason in `src/hikari/src/tools/checks/structural_probes.zig`.

---

## Ownership and threading

```mermaid
sequenceDiagram
  participant GT as Game thread
  participant Lock as world.lock
  participant Pub as RenderFrame triple-buffer
  participant RT as Render thread
  participant GPU as GPU queues

  GT->>Lock: fill write RenderFrame
  GT->>Lock: unlock
  GT->>Pub: publish()
  Note over RT: does not take world.lock
  RT->>Pub: consume latest
  RT->>RT: applyRenderCommands / residency
  RT->>RT: prepareFrame
  RT->>RT: assemble + compile graph
  RT->>GPU: execute passes + barriers
  RT->>GPU: present / finishOutput
```

- **Frames in flight:** `graphics/renderer/frames.zig` → `frames_in_flight = 3`. Ringed uniforms/object data; **both** Metal and D3D12 wait per ring slot before rewrite (Metal last CB on slot; D3D12 fence token). Present/swapchain pacing is orthogonal.
- **Two handle dialects** (do not merge):
  - Residency: `RenderHandle` / `TypedHandle` (`native/handle.zig`)
  - Graph/RHI: `BufferHandle` / `TextureHandle` / graph handles (`rhi/types.zig`, `rendergraph/types.zig`)

---

## Module map

```text
src/hikari/src/graphics/
├── api.zig                  Public barrel
├── rhi/                     Backend-neutral vocabulary (types, barriers, queues)
├── renderer/                Frame policy: core, prepare, FIF, residency consume, pass encode
├── rendergraph/             Graph builder, compile, schedule, passes/, drivers/
├── shadow/                  Atlas policy, matrices, per-face culling
├── raytracing/              TLAS policy, bindings, geometry update rules
├── material/                PSO kinds, shader artifacts, uniforms
├── particles/               GPU particle runtime, emitter commands, live-bound
├── residency/               CPU/GPU residency, bindless heap, geometry_residency, telemetry
├── effects/                 G-buffer, content compute, GPU cull, visual look, lighting helpers
├── gpu/                     Vertex buffers, object data, material table, staging
└── debug/                   Visualizers, viewport pick/state

src/hikari/src/platform/{macOS,Windows}/graphics/
├── renderer.zig             Facade + EngineHandles
├── renderer_frame.zig       Graph backend + queue prepare/acquire/release/abort hooks
├── queue_timeline.zig       Native event/fence Signal·Wait for graph timeline tokens
├── pass_encoder.zig         begin / barrier / bind / finish (typed compute lists when async)
├── content_compute.zig    Content-compute ring dispatch (uses graph release for cross-queue tokens)
└── passes/backend_ops.zig   Draw/dispatch emission table

src/hikari/sdk/src/render_config.zig   Feature gates (POD)
```

---

## RHI vocabulary

`graphics/rhi/types.zig` is the only place to extend graph sync language.

```mermaid
flowchart LR
  subgraph resources [Resources]
    Tex[TextureHandle]
    Buf[BufferHandle]
    AS[AccelerationStructureHandle]
  end

  subgraph queues [QueueClass]
    G[graphics]
    C[compute]
    Cop[copy]
  end

  subgraph domain [PipelineDomain]
    Pg[graphics]
    Pc[compute]
    Pcop[copy]
    Prt[ray_tracing]
  end

  subgraph sync [Sync POD]
    RS[ResourceState]
    PS[PipelineStage]
    AM[AccessMask]
    RB[ResourceBarrier]
    ST[SubmissionToken]
  end

  Tex --> RB
  Buf --> RB
  AS --> RB
  RS --> RB
  PS --> RB
  AM --> RB
  G --> ST
  C --> ST
```

| Concept | Role |
|---|---|
| `ResourceState` | Logical layout: color/depth attach, shader read/write, copy, AS build/read, present, … |
| `PipelineStage` / `AccessMask` | Hazard granularity for barriers |
| `QueueClass` | graphics / compute / copy — independent of `PipelineDomain` |
| `SubmissionToken` | `{ queue, value }` for timeline waits |
| `Capabilities` | What the device actually supports (async compute, copy queue, bindless, RT, …) |

Backends map these to Metal/D3D12 transitions; they must not invent parallel state enums for the graph.

---

## Render-graph lifecycle (one frame)

```mermaid
flowchart TB
  A[Adapter.prepareFrame] --> B[GraphBuilder.init]
  B --> C[pipeline.assemble]
  C --> D[declare textures / FrameTargets]
  C --> E[add conditional passes + accesses]
  E --> F[graph_builder.compile]
  F --> G[liveness cull dead passes]
  F --> H[emit CompiledBarrier list]
  F --> I[emit QueueSyncEdge acquires]
  G --> J[RenderGraphSchedule]
  H --> J
  I --> J
  J --> K[pipeline.execute]
  K --> L[for each live pass: apply barriers → driver.run]
  L --> M[Adapter.finishOutput / present]
```

### Assemble vs execute

| Phase | File | Job |
|---|---|---|
| **Assemble** | `pipeline.assemble` | Given `RenderPipelineConfig` + `FrameCtx`, create targets and register passes in order |
| **Compile** | `compile.zig` | Cull unreachable work; derive barriers from access transitions; record cross-queue **releases** (producer) and **acquires** (consumer) for every graphics↔compute edge |
| **Execute** | `pipeline.execute` + drivers | prepare producer token → `queueAcquire` → barriers/work → `flushPassBatch` (when `batch_boundary`) → publish `queueRelease`; abort clears staged state on error |

Submit batching: `CompiledPass.batch_boundary` forces a flush only on structural boundaries: queue switch, non-empty acquires/releases, and the last pass. GPU profiling does not participate in scheduling; Metal writes pass timestamp pairs into the active `MTL4CommandBuffer`, while D3D12 writes queries into the active command list. D3D12 keeps one closed command list per pass and batches `ExecuteCommandLists`; Metal shares one open command buffer per `QueueClass` (`frame_submission.zig`) and commits at the structural boundary. Graph flush is `batch_boundary` / `flushPassBatch`; encoder finish only ends the pass. Present stays frame-owned in `finishOutput`.

Graph texture allocation distinguishes scratch from history. Scratch textures with exact matching descriptors may share a physical pool entry when their compiled lifetimes are strictly disjoint, all accesses use the graphics queue, and the first logical texture's terminal state restores the next one's initial state. Cross-queue, overlapping, storage-only, or descriptor-mismatched textures remain separate. Persistent histories are declared per successfully encoded surface: an omitted history enters the same GPU-safe retirement window as cached scratch, while a skipped surface or failed encode leaves its prior declaration intact. Residency reports `transient` and `persistent` rows separately; `retiring` persistent rows are awaiting that safety window. Scratch colour attachments that no surviving pass reads — every access a plain attachment write — are discarded: the schedule rewrites their descriptor to `TextureUsage.memoryless`, compiles no barriers or queue edges for them, and the backends give them no memory (Metal memoryless storage with `DontCare` load/store; D3D12 a null RTV). The decision sees declared accesses only, so every reader, bindless included, must be declared, and a texture the host samples after the graph runs is declared with `GraphBuilder.markReadAfterGraph`. Both backends panic on a bindless or SRV view of a discarded attachment.

Pass kinds are the closed set in `pass_registry.Kind`. Platform registers callbacks once; assemble only *schedules* which kinds run this frame.

---

## Pass inventory and data flow

Structural order (conditional rows omitted when gated off):

```mermaid
flowchart TB
  Prep["Pre-graph: clusters · shadow pack · TLAS · GPU cull · content ensure"]

  Prep --> SD[shadow_depth]
  Prep --> CC[content_compute*]
  SD --> GB[gbuffer_geometry]
  CC --> GB
  GB --> DP[depth_pyramid]
  GB --> Late["occlusion_cull + gbuffer_geometry_late"]
  Late --> DP
  GB --> VTM[velocity_tile_max]
  SD --> VolD[volumetric_density]
  DP --> VolD
  GB --> VolD
  VolD --> VolI[volumetric_inject]
  VolI --> VolG[volumetric_integrate]
  GB --> AO[ambient_occlusion ± temporal]
  DP --> AO
  AO --> GI[global_illumination ± temporal]
  GI --> Lit{deferred_lighting / raytraced_direct+light}
  AO --> Lit
  GB --> Lit
  Lit --> Bounce[gi_bounce_capture]
  Bounce --> Ref[reflections ± temporal]
  Lit --> Ref
  Ref --> VolC[volumetric_composite]
  VolC --> TC[transmission_capture]
  Ref --> TC
  TC --> FT[forward_transparent]
  VolG --> FT
  FT --> TAA[temporal_reconstruction TAA/TAAU]
  TAA --> Post["exposure → auto_focus / DOF → motion_blur"]
  FT --> Post
  VTM --> Post
  Post --> Bloom[bloom_extract → blur ×2]
  Bloom --> TM["tonemap (+bloom) / debug_visualizers"]
  TM --> UI[ui]
  UI --> BB[(backbuffer / present)]
```

Gates, plugin post-process slots, and notes: [Rendering — Implemented pipeline](/docs/systems/rendering.md#implemented-pipeline). Volumetric density/inject/integrate are scheduled **before** AO/lighting/reflections so froxel work can overlap those graphics passes on the async compute queue (graphics-queue fallback when the device has no usable async compute). Composite is **always** in the render domain after lighting, **before glass and before TAA** — fog therefore enters colour history (motion-only trail accepted). Glass applies the same integrated volume in forward. Camera post (exposure, DOF, motion blur) meters the post-reconstruction image, then bloom/tonemap.

### Feature layers (orthogonal)

```mermaid
flowchart LR
  subgraph A [Contract A — bus]
    GBuf[G-buffer attachments]
  end
  subgraph B [Contract B — classical]
    Def[Deferred + virtual shadow maps]
  end
  subgraph C [Contract C — RT optional]
    RTS[RT scene TLAS]
    RTH[RT hit shading / bindless]
  end
  subgraph D [Contract D — GI]
    GI[Indirect diffuse SS / RT]
  end
  subgraph E [Contract E — content]
    AC[Async compute producers]
  end

  AC -.->|optional vertex texture| GBuf
  GBuf --> Def
  GBuf --> RTS
  RTS --> RTH
  Def --> Post[AO / GI / reflections / vol / TAA / bloom / tonemap / UI]
  RTS --> Post
  GBuf --> GI
  RTS --> GI
  GI --> Def
```

Master switches live in `RenderPipelineConfig` (`sdk/src/render_config.zig`). Disabled features skip assemble (no passes / no PSOs where documented) — see volumetrics zero-cost-off and RT prepare short-circuit in [Rendering](/docs/systems/rendering.md#rendering-contracts-composability).

---

## Sync points

### 1. CPU frames-in-flight (shared contract)

Canonical source: `graphics/renderer/frames.zig`.

**Protocol (Metal and D3D12 — same logic, different wait primitive):**

```text
BEGIN:
  slot = frame_counter % frames_in_flight   // default 3
  waitUntilSlotComplete(slot)               // Metal: last CB on slot; D3D12: fence token on slot
  write uniforms / lights / object data / per-slot heaps into ring[slot]
  advance orphan free lists (age ≥ frames_in_flight)

RECORD + SUBMIT (binds ring[slot])

END:
  markSlotSubmitted(slot, token)            // Metal: track last CB; D3D12: ring_timeline + fence signal
  present / swapchain pacing                // orthogonal — protects backbuffers only
```

| Rule | Detail |
|------|--------|
| SSOT depth | `frames.frames_in_flight`; native mirrors `HIKARI_MTL_FRAMES_IN_FLIGHT` / `HIKARI_D3D12_FRAME_POOL_COUNT` (compile-time assert) |
| Ring wait | **Always** required before rewriting a slot — never substitute present/swapchain depth |
| Present wait | Independent; bounds backbuffer reuse (`buffer_count`, often 2) |
| Deferred free | Orphan age `0` (idle only) or `≥ frames_in_flight` |
| Handle dialects | Residency `RenderHandle` vs graph RHI handles — do not merge |

Tests: `frames.zig` (slot math, `SlotTimeline`, `RingWriteTracker`, protocol simulation) run under `zig build test` / graphics tests.

### GPU pass timestamps

| Backend | Stamp | Resolve |
|---------|-------|---------|
| Metal | Per-slot `MTL4CounterHeap` timestamps (`writeTimestampIntoHeap` start/end pairs). Graph CBs stamp at create/commit; pre-graph producers sharing a CB stamp when the current pass changes. | After ring-slot wait via `resolveCounterRange` + `queryTimestampFrequency` |
| D3D12 | TIMESTAMP query heap pair per pass + `ResolveQueryData` on the list. Graph render/compute passes and pre-graph raw lists (`beginTimedPass`) both write. | After ring-slot fence wait + map readback |

GPU frame detail (`profiler/gpu_detail.zig`, profiler builds while Recording) rides the same passes: both resolves also return each pass's start relative to the frame's first pass; Metal adds three render-encoder stage timestamps per encoder (`writeTimestampWithGranularity:afterStage:`); D3D12 adds one `PIPELINE_STATISTICS` query per render pass. Stage heaps are created on first enable and resolve beside the pass timestamps. See [rendering](/docs/systems/rendering.md) for how to read them.

Shared publish: `profiler/gpu_timing.zig` → Profiler UI **GPU** rows. Gated by compile-time `-Dprofiler-timing` and runtime Recording. Independent of `--profiler-residency` (residency tabs). Pre-graph producers (`light_binning`, `gpu_culling.prepare`, `skinning_writeback`, `raytracing.acceleration_structures`) and editor `viewport_composite` / `editor_ui_layer` are timestamped the same way as graph passes.

### 2. Intra-queue barriers (same `QueueClass`)

Compiled from consecutive `PassAccess` transitions on a resource (mip/layer aware for textures):

```text
CompiledBarrier {
  before → after ResourceState
  source/destination stages + access
  optional texture_range
}
```

Applied by `pass_encoder` immediately before the consuming pass encodes.

### 3. Cross-queue sync (`QueueSync` + `QueueSyncEdge`)

Bidirectional: **graphics→compute** (e.g. depth/shadow → volumetric inject) and **compute→graphics** (content compute → G-buffer; integrated volumes → forward/composite). Same-queue edges are no-ops. Content compute and engine compute share one graph-owned path.

```mermaid
sequenceDiagram
  participant Prod as Producer queue
  participant QS as QueueSync map
  participant Cons as Consumer queue

  Prod->>QS: queuePrepareRelease → nextSignal
  Prod->>Prod: submit work + native Signal (Metal event / D3D12 fence)
  Note over Prod: After successful submit
  Prod->>QS: queueRelease → recordProduced
  Note over Cons: Before consumer encode
  Cons->>QS: producedToken(resource_id)
  Cons->>Cons: queueAcquire → native Wait
  Cons->>Cons: sample / read resource
```

Rules (`queue_sync.zig` + platform `queue_timeline.zig`):

- Graph owns timeline counters, produced-token map, and compiled release/acquire edges; backends only Signal/Wait.
- Resource ids cover graph textures, buffers, and acceleration structures (`crossQueueResourceId`), not only content-compute ring slots.
- Identity is stable across frames (`markCrossQueueProducer` for lag-1 reads with no in-frame writer).
- Resize / recreate → `QueueSync.clear()` so ids cannot alias stale tokens.
- Metal encodes waits when a shared command buffer opens and the prepared signal at flush; only a no-submission pass needs the checked signal-only fallback. Intra-queue hazards use per-resource `MTLFence`s (`platform/macOS/graphics/barriers.zig`).
- No water/volumetrics ⇒ no edges, no signals (empty-list checks only).
- Pre-graph jobs (light binning, GPU cull phase 1, skinning writeback) publish `queue_sync.pre_graph_resource_id` on the graphics timeline after prepare submits. The first graph pass on each `QueueClass` acquires that token. Metal `frame_fence` remains an extra safety net; it is not the Windows contract.

### 4. Texture upload (residency)

| Backend | Path | Sync |
|---|---|---|
| D3D12 | COPY queue upload → DIRECT promote | GPU fence wait (**no** CPU spin); render thread `pollPendingTextureUploads` |
| Metal | CPU `replaceRegion` | Completes before publish ready |

States: `pending` → `uploading` → `ready` / `failed`. Loading gates treat `uploading` as not ready.

### 5. Ray-tracing structures

- Shared `raytracing_scene.zig`: reuse / refit / rebuild TLAS; static BLAS compact; retire after `frames_in_flight`. Structures are recorded onto the pre-graph buffer and fence-ordered; **compaction is the only work allowed to drain the GPU**, once per batch rather than once per structure (see *Submission model* in `rendering.md`).
- Effective RT modes need `scene_ready` (non-empty opaque TLAS), not device support alone.
- The renderer-global bindless heap activates for raster material preparation; RT hit shading reuses the same stable indices.

### 6. Present / output

- Standalone: graph writes backbuffer → present.
- Editor inset viewport: scene finishes in graph; `_engine/composite` blit + chrome are **outside** the scene graph (`renderer_viewport.zig`).

---

## Lite vs full schedule (mental model)

```mermaid
flowchart LR
  subgraph lite [Typical small-title lite]
    S1[shadow_depth]
    S2[gbuffer_geometry]
    S3[depth_pyramid?]
    S4[deferred_lighting]
    S5[forward_transparent?]
    S6[tonemap?]
    S7[ui]
    S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7
  end

  subgraph full [Optional heavy stack]
    F1[+ AO / GI temporal]
    F2[+ reflections temporal]
    F3[+ volumetric inject/integrate/composite]
    F4[+ TAA]
    F5[+ exposure / DOF / motion blur]
    F6[+ bloom]
    F7[raytraced_direct + raytraced_lighting / RT scene]
  end

  lite -.->|feature gates| full
```

Turning RT and volumetrics off removes those passes and (for volumetrics) PSO/alloc work. Deferred G-buffer + virtual shadow maps remain the baseline cost.

---

## Where to change what

| Goal | Touch |
|---|---|
| New pass / reorder | `pass_registry.Kind` + `passes/` + `pipeline.assemble` + driver + platform callback |
| New resource state / queue hazard | `rhi/types.zig` then compile/encoder lowering |
| Feature toggle | `render_config.zig` + assemble gates (+ project settings schema if user-facing) |
| Draw/dispatch encoding | `platform/*/passes/backend_ops.zig` / `pass_encoder.zig` |
| G-buffer layout | `effects/gbuffer.zig` + geometry/lighting shaders (both backends) |
| Shadow policy / VSM | `shadow/shadow_types.zig` / `shadow/shadow_pass.zig` / `shadow/vsm/` |
| Cross-queue producer | Graph accesses on differing `QueueClass` → compile emits release+acquire; `queue_timeline` Signal/Wait; `markCrossQueueProducer` for cross-frame only |
| Async volumetric inject | `passes/volumetric.zig` queue param + `pipeline.assemble` earliest-after-depth placement + `supportsAsyncComputeQueue` |

---

## Code map

Shared modules (`src/hikari/src/graphics/`):

| File | Responsibility |
|------|----------------|
| `renderer/renderer_core.zig` | Frame/graph algorithm over a platform `Adapter`: prepare → assemble → compile → execute → present |
| `renderer/frame_epoch.zig` | Editor multi-surface protocol (present list, dual-publish alternate consume/prepare) |
| `renderer/renderer_shared.zig` | Facade; split under `renderer_shared/` (`frame`, `commands/` + facade, `consume`, `prepare`, `emit/` + facade) |
| `renderer/renderer_prepare.zig` | Facade; `prepareFrame` stages under `renderer_prepare/` (`temporal`, `objects`, `rt`, `uniforms`, `lighting`, …) |
| `renderer/renderer_pipelines.zig` | Fullscreen/effect PSOs, engine pipelines, UI atlas, optional RT |
| `effects/content_compute.zig` | Content-compute producers, lag policy, ring helpers |
| `effects/compute_passes.zig` | Facade; per-cmd binding contracts, params ABI, D3D12 root budget under `compute_passes/` (`types`, `layout`, `catalog`, family rows) |
| `effects/gpu_culling.zig` | Cull prep + slot capacity helpers + shared frustum-plane extraction |
| `shadow/shadow_culling.zig` | Per-face/class meshlet work staging for GPU visible-triangle expansion |
| `renderer/renderer_lifecycle.zig` | Teardown: residency, PSOs, materials, textures |
| `renderer/renderer_residency.zig` | Per-frame command drain + `consumeRenderFrame` |
| `renderer/renderer_common.zig` | Facade; `EngineHandles`, feature availability, reconstruction/governor, display, setters under `renderer_common/` |
| `renderer/dynamic_buffer_manifest.zig` | Compile-time shared frame-ring buffer membership and create/assign/destroy lifecycle; backend types come from `platform/*/graphics/dynamic_buffer_types.zig` |
| `renderer/pass_encoder.zig` / `renderer/pass_targets.zig` | PassEncoder contract + graph texture unwrap |
| `rendergraph/drivers/` | Shared pass `run` drivers |
| `rendergraph/graph.zig` | Facade; `types` / `schedule` / `compile` / `builder` |
| `rendergraph/pass_registry.zig` | Pass-kind registry + typed callbacks |
| `shadow/shadow_pass.zig` | Atlas size policy, face uniforms, per-face draws |
| `renderer/platform_resources.zig` | `PlatformResources(Backend)` tables |
| `material/shader_uniforms.zig` | Global `HikariUniforms` CPU mirror |
| `residency/bindless_heap.zig` | Renderer-global stable buffer/texture indices (raster + RT) |
| `gpu/geometry_table.zig` | Per-instance bindless geometry rows for meshlet attribute-pull + RT resolve |
| `gpu/material_table.zig` | Facade; surface-local raster/RT material records under `material_table/` (`record`, intern `table`, gated `build`, content-compute patch) |
| `residency/geometry_residency.zig` | CPU mesh residency ledger (`--profiler-residency=cpu`) |
| `residency/render_residency/telemetry.zig` | GPU ownership telemetry (`--profiler-residency=gpu`) |
| `raytracing/raytracing_scene.zig` / `raytracing/raytracing_bindings.zig` | TLAS policy + instance packing |
| `material/pipeline_kind.zig` | PSO kind tags + color/depth/blend policy |
| `effects/gbuffer.zig` | Attachment slots + `target_formats` |

Platform layout (Metal / D3D12 — native only):

| File | Responsibility |
|------|----------------|
| `platform/*/graphics/renderer.zig` | Device lifecycle, `handles`, tick |
| `renderer_frame.zig` | Render-graph backend + `FrameAdapter` |
| `pass_encoder.zig` | Native begin/barrier/bind/finish |
| `renderer_viewport.zig` | Editor insets and offscreen composite |
| `resources.zig` | Backend traits for `PlatformResources` |
| `passes/backend_ops.zig` | Geometry, lighting, shadow draw emission |
| `shadow_renderer.zig` | Atlas GPU resources + pass create |

Do not dump new policy into the platform facade. Shared behaviour goes under `graphics/`.

## Related docs

| Doc | Focus |
|---|---|
| [Rendering](/docs/systems/rendering.md) | Contracts, pass gates, lighting/RT/TAA behavior |
| [Shaders](/docs/guides/shaders.md) | Packages, `PipelineKind`, binding layouts |
| [Lifecycle](/docs/systems/lifecycle.md) | Session tick → publish → render |
| [Platforms](/docs/systems/platforms.md) | Metal / D3D12 / Linux status |
| [Volumetric media](/docs/systems/volumetric-fog.md) | Global fog and bounded smoke, froxel path, async compute queue contract, zero-cost-off |
