| name | titan-rendering-core |
| description | Use when working in titan-rendering-frame-graph, titan-rendering-core, or titan-rendering-post-process-core — frame graph DAG, RenderTarget, DevicePool, PostProcessHook, CameraFrameData, HDR intermediate, tonemap, color management. Foundational layer shared between current 3D and future 2D rendering. Loads Titan-specific overrides for realtime-rendering. NOT for 3D-specific concerns (deferred chain, GI, shadows) — load titan-rendering-3d after this. |
Titan Rendering Core Specialty
When to invoke
- Working in
crates/titan-rendering-frame-graph/, crates/titan-rendering-core/, or crates/titan-rendering-post-process-core/
- Touching the Pass enum, DAG topology, RenderTarget allocation, DevicePool, PostProcessHook trait, CameraFrameData
- Color management: sRGB / linear-space / HDR Rgba16Float intermediate / tonemap (Reinhard)
- NOT for 3D-specific deferred chain / G-buffer / Stratum / shadows → load
titan-rendering-3d as well
- NOT for 2D-specific (future) → load
titan-rendering-2d as well (when it exists)
Generic skill prerequisites
Invoke first via the Skill tool:
realtime-rendering-3d — clean-room rendering knowledge + provenance rule (load-bearing for IP posture).
Titan-specific notes below OVERRIDE the generic skill where they speak.
Locked Titan docs (read on demand)
- TODO:
docs/design/frame_graph.md does not yet exist as a consolidated doc. Cite the MEMORY.md entries below until it is authored — see docs/TODO.md.
docs/RENDER_BUNDLES.md — wgpu render bundle conventions.
Crate map
crates/titan-rendering-frame-graph/ — DAG types, Pass enum, data-only.
crates/titan-rendering-core/ — DevicePool, RenderTarget, PostProcessHook trait, CameraFrameData, shared resource allocation.
crates/titan-rendering-post-process-core/ — Pass::Tonemap (Reinhard), color-management passes shared between 2D and 3D.
Verified engine defaults (RenderQuality, 2026-06-02)
- SVGF
atrous_passes default = 4 (NOT 5; changed at Artifact-A fix 2f19515b; quality.rs:1003).
- SVGF σ_L default = 6.0 (NOT 4.0; raised at m49-l-cellflicker-fix-g;
quality.rs:1011).
- SVGF σ_L_specular default = 2.5 (NOT 1.5;
quality.rs:1012).
These values appear in bench-tool CLI flags and bench-catalog.md (now corrected). Trust quality.rs.
WebGPU hard limits
max_bind_groups = 4 on the WebGPU baseline — a pass needing resources beyond its 4th group must pack them into an EXISTING group's free binding slots, never add a 5th (caught in practice by P3 Task 12's DDGI-atlas wiring into lattice_probe_trace: the atlas + origin UBO went into the existing light bind group's spare slots). Independent of the sampled-texture and storage-buffer limits below; like them, invisible to cargo check — only a real-device pipeline-build test catches it.
max_sampled_textures_per_shader_stage = 16 — the GI/reflection trace family (restir_gi_initial, rt_reflection_trace, probe_trace, lattice_world_cache_trace, lattice_probe_trace) sat at exactly 16 until P3 Task 12 channel-merged the per-cascade voxel albedo+emissive textures into one Rg32Uint each (u32-pack doctrine: .r=packed albedo, .g=manual R11G11B10F emissive), freeing 4 slots family-wide. Consolidate via u32 channel-packing when at the ceiling; NEVER widen an unrelated texture's format to dodge a binding limit.
max_storage_buffers_per_shader_stage = 8 on the WebGPU baseline (no required_limits override in use). cargo check/clippy CANNOT see this — it's a real-device pipeline-creation-time failure only (naga/wgpu validates bind-group-layout entry counts against the adapter's actual limits, not at Rust compile time). A compute pass needing a 9th storage buffer must merge two logically-separate buffers into one (e.g. a small control/indirect-args struct folded into an existing buffer) rather than requesting a raised limit — caught in practice by P3 Task 10e's world-cache place pass (real-GPU pipeline-build test failed at 9 buffers; fixed by merging trace_control+indirect into one). Always have at least one real-device pipeline-build test per new bind-group layout — cargo check passing is not proof the layout is valid.
Test-coverage gaps that ship latent, self-consistent-looking bugs
Two bug classes have now bitten this project (P3 Lattice-Mid Task 11, 2026-07-08) and share
one root cause: they're invisible to cargo check/clippy/nextest/editor-smoke because each
half of the bug is internally self-consistent — nothing actually contradicts until the OTHER
half (a real RON scheduling the pass, or both models' hooks being live at once) exists.
- BGL type vs. real resource type mismatch, masked by a self-consistent dummy. A pass
authored before its RON wiring exists can declare a
BindGroupLayoutEntry type that matches
its OWN placeholder dummy texture but not the real resource it will eventually bind (e.g.
TextureSampleType::Depth against a Depth32Float dummy, when the real producer is actually
a compute-built R32Float). No unit test catches this because no test constructs a REAL bind
group against the real resource — only a real pipeline-build test that binds the actual
producer's output (not a hand-authored dummy) will. Prefer a real-GPU pipeline-build test
that exercises the true resource chain over one that only proves the BGL is internally
consistent with its own dummy.
- Hook-selection tie-break that silently picks the wrong model. When two mutually-exclusive
systems (e.g. two lighting models) can both have their trait objects installed at once (even
if only one is ever actually scheduled by the active RON), any code that arbitrates between
them by hook-presence alone (
.or_else()-style chains) rather than by which one's passes are
ACTUALLY scheduled in the current frame graph will silently pick the wrong one the moment both
hooks are installed but only one is active. This passes every existing test because the wrong
choice still "boots" and still returns a valid (but wrong) value. Catching it requires a check
that inspects which candidate actually WON the arbitration, not just that boot succeeded.
Both classes share the same defense: don't trust "it type-checks and the editor boots" as proof
of correctness for any codepath gated on a real RON/real-resource-graph existing — verify against
the actual scheduled passes / actual bound resource, not a self-consistent placeholder.
Recent locked decisions (until landed in docs/design/)
- Frame graph FULLY AUDITED (2026-06-10, GI-readiness Phase 2): all 35 passes carry
Provenance::Audited in manifest.rs (Tier-1 citations + why-chosen; PASS_CATALOGUE.md header = 0/35 Unaudited; audit_debt_report pins the count at 35). New passes MUST ship with an Audited record — the catalogue is provenance ground truth. The audit declared the previously-untracked GBufferFill→DeferredLighting emissive edge (GBUFFER_EMISSIVE, tappable, TITAN_TAP=gbuffer_emissive). The "35" figure is now historical (stale — this section was never updated incrementally across the many campaigns since); as of P3.5 (2026-07-12) audit_debt_report's live pin is 74 audited passes, including Pass::MeshSdfInstanceCull (the near-field SDF tier's world-grid cull pass, manifest.rs:3379, Provenance::Audited citing Wright 2015 slide 52). Always trust the live audit_debt_report assertion in manifest.rs, not this count. GLOBAL_SDF_CASCADE_AGE is a layered-tap PARTITION EXCEPTION: it appears in TAPPABLE_RESOURCES (via tap_texture_layered, 4× D3 R32Uint per-voxel age volumes) WITHOUT being a DECLARED_RESOURCES id — declaring it would require inventing a spurious pass output for what is really a debug readback of an internal volume alongside GLOBAL_SDF_CASCADE's own seed-coord tap, not a frame-graph pass edge. The every_manifest_resource_is_tappable_or_listed_untappable coverage test's partition guarantee covers DECLARED ids only — a layered-tap-only id like this one is a documented, compiler-unenforced exception to "every id is in exactly one list," not a bug if you find it missing from DECLARED_RESOURCES.
[[project_rendering_crate_split_2026_05_22]] — render crate split: 6-crate structure, HDR Rgba16Float intermediate always-on, linear-space alpha blend, Pass enum data-only, editor cargo feature narrowed to Category A.
[[project_render_audit_phase2_2026_05_23]] — 13-subsystem audit findings, 8 [H] inline fixes + 17 [M] TODO + 16 [L] noted.
KB
Read docs/agent-kb/titan-rendering-3d-agent/INDEX.md (the rendering agent
owns both core and 3d learnings in one KB — there is no separate -core-agent).
SP-C/D FrameResourceRegistry (2026-06-03)
FrameResourceRegistry in titan-rendering-core/src/frame_resource_registry.rs
(#[cfg(feature="debug-dumps")]) is the SOLE tap mechanism.
TapConfig (SP-D): TapConfig { at_frame: Option<u64>, window: Option<(u64, u64)>, once: bool, subdir: Option<String> }.
Read ONCE at from_env() construction — NOT re-read per tap per frame (closes the deferred
SP-C per-tap-env-reread LOW). Programmatic override: set_config(TapConfig) /
Renderer3d::configure_taps(TapConfig).
Gate method: should_fire(id: ResourceId, frame: u64) -> bool — single dispatch point for all
3 tap methods: selection check AND frame_in_gate(frame) AND (!once OR !already_fired).
once-fired set keyed by id (not frame) — fires exactly once per run per id.
TextureKind variants (exhaustive B/px match — no wildcard):
| variant | B/px | output |
|---|
Rgba16Float | 8 | .ppm (Reinhard+gamma) + .bin if TITAN_DUMP_LINEAR |
R32Float | 4 | .bin raw float32 |
R32Uint | 4 | .bin raw uint32 |
Rg16Float | 4 | .bin raw f16 pairs reshape(H,W,2) |
Rg8Unorm | 2 | .bin raw uint8 pairs reshape(H,W,2) |
Rg32Float | 8 | .bin raw f32 pairs reshape(H,W,2) — SP-D, moments format |
Adding a new variant: (1) enum; (2) B/px arm in tap_texture; (3) decode arm in decode_texture;
(4) doc comment B/px list in tap_texture; (5) manifest ResourceId doc for format.
Subdir-aware output path: dump_dir().join(config.subdir?).join("{basename}-frame{frame}.{ext}");
create_dir_all called on first write. Buffer analysers write inside their own closures (subdir
applies to texture dumps only).
Full env table, output formats, numpy recipes: titan-rendering-debugging skill → instrument-catalog.md.
Consolidated KB: docs/agent-kb/titan-rendering-3d-agent/sp-c-resource-registry.md.
SP-D specifics: docs/agent-kb/titan-rendering-3d-agent/sp-d-registry-unification.md.
Surface gaps
If you learn an invariant not in the cited docs (a new RenderTarget allocation
rule, a new PostProcessHook ordering constraint, a new format invariant), flag
it for the main chain to land in the consolidated docs/design/frame_graph.md
once that doc is authored, or in docs/POST_WORK_FINDINGS.md if it's a one-off
finding.
Anti-scope
- 3D-specific (deferred chain, G-buffer, Stratum, shadows, TAA reproject) →
titan-rendering-3d
- 2D-specific (future) →
titan-rendering-2d
- IP infringement: never cite proprietary engine source per realtime-rendering provenance rule.