| name | titan-rendering-3d |
| description | Use when working on 3D-specific rendering — deferred chain, G-buffer, clustered shading, Stratum GI, ReGIR, SVGF/A-SVGF, TAA, shadow techniques, depth/HZB, anything 3D-specific. ALWAYS load titan-rendering-core first; this skill assumes core conventions. Loads Titan-specific overrides for realtime-rendering. NOT for 2D rendering or editor panels. |
Titan 3D Rendering Specialty
Prerequisite
This skill assumes titan-rendering-core conventions are already loaded. Invoke
titan-rendering-core BEFORE this skill — its frame-graph, DevicePool,
RenderTarget, HDR, and tonemap conventions are foundational and not restated here.
When to invoke
- Working in
crates/titan-rendering-3d/, crates/titan-rendering-post-process-3d/, crates/titan-rendering-editor-3d/, crates/titan-stratum-lighting/, crates/titan-bedrock-lighting/, crates/titan-lattice-lighting/
- Writing or modifying shaders, passes, denoiser code, shadow code
- Diagnosing GI / shadow / post artifacts (fireflies, banding, ghosting, dark voids, light leaks)
- NOT for the foundational layer →
titan-rendering-core
- NOT for 2D rendering (future)
- NOT for editor panels →
titan-editor
Generic skill prerequisites
realtime-rendering-3d — must be loaded before reading further. Clean-room provenance rule applies.
Locked Titan docs (read on demand)
docs/design/stratum_lighting.md — Stratum GI architecture.
docs/design/stratum_lighting_quality.md — Stratum quality knobs.
docs/design/stratum_lighting_regir.md — ReGIR cell-grid spatial reservoir design.
docs/design/stratum_sampler_architecture_audit.md — sampler / reservoir audit findings.
docs/design/stratum_shadow_world_space_tier.md — world-space shadow tier (m49-m).
docs/design/stratum_lighting_high_tier_denoiser_reference.md — high-tier denoiser clean-room reference.
docs/design/virtualized_geometry.md — virtualized geometry direction.
docs/design/render_bench_probe.md — the titan-render-bench-probe diagnosis instrument: scene layout, CLI, debug env overrides, stage dumps.
Crate map
crates/titan-rendering-3d/ — Renderer3d, deferred chain, G-buffer, clustered shading dispatch, mesh.wgsl forward+ pre-pass.
crates/titan-rendering-post-process-3d/ — Pass::Taa (jitter, reproject, YCoCg AABB clamp), post chain.
crates/titan-rendering-editor-3d/ — editor-only rendering: grid, outline, pick, cluster_debug, depth_readback.
crates/titan-stratum-lighting/ — pure ReSTIR/ReGIR reservoir-based many-lights path (Stratum
tier card): flat K-slot WRS, spatial reuse (Algorithm-7), cell-grid ReGIR, SVGF/A-SVGF, Reservoir
bit-31-packed shadow-visibility consumer. Post-P2-carve (2026-07-02) this crate no longer owns
VSM, shadow-importance scoring, occupancy/sun-visibility denoise, or the DDGI screen-probe field
— see titan-bedrock-lighting below. Depends on titan-bedrock-lighting, never the reverse.
crates/titan-bedrock-lighting/ — model-agnostic lighting infra shared by every current/future
lighting model (VSM stack incl. vsm_cache/vsm_invalidation, shadow-importance scoring,
occupancy/sun-visibility denoise, BedrockQualityConfig, DDGI screen-probe field with full
state+dispatch ownership, model-agnostic shadow-visibility march primitives
BedrockShadowVisibility{Screen,Sdf,Rt} bound via a stride-parameter array<u32> contract — no
model-specific struct crosses this boundary). Dispatched through BedrockRenderHook /
DispatchKind::BedrockHook (mirrors the pre-existing StratumRenderHook/StratumHook pattern).
Visibility ladder now complete (burn-down Phase 4, PR #39, 2026-07-17): HWRT(excluded/parked)
→ SDF/compute → BedrockShadowVisibilityCpuRt → BedrockShadowVisibilityClustered. Both new
rungs are DEVICE-CAPABILITY FALLBACKS, not quality tiers — select_shadow_visibility_rung is
evaluated with REAL per-frame capability data every call from the SAME production dispatch arm
(Pass::ShadowVisibilityClustered, scheduled in all 4 Stratum RONs + both Lattice-Mid RONs) and
both hook methods early-return with ZERO GPU work (and zero CPU allocation on the CpuRt path)
unless their rung is genuinely selected — currently dormant everywhere in production (SDF cascades
are unconditionally scheduled on every shipping RON, so the fallback trigger never fires today; this
is intentional, matches the ladder's canon role as a correctness fallback). CpuRt is a CPU-side
median-split BVH (Kay & Kajiya 1986 slab test) over MeshSdfInstanceCpu, driven by an async
G-buffer→CPU staging readback (CpuRtReadback, mirrors the existing VSM overflow-tally
map_async+AtomicBool-in-flight pattern, one-frame-plus latent, drained every frame by the
existing post_submit_poll hook — it deliberately has NO separate Pass:: entry since it schedules
no rasterized GPU work, only a CPU readback side-channel; it piggybacks the Clustered arm's
per-frame call site). Clustered samples the SAME cascade atlas the primary CSM path rasterizes (no
second raster path) via per-pixel depth-band cascade selection against the real cascade_splits the
primary path already computes (Engel 2009) — NOT a re-derived AABB representation. Both rungs ship
with a forced-capability-gap real-device test that drives the actual production dispatch method
end-to-end (the class of test this phase's review process treats as load-bearing: two real bugs —
a BVH leaf-offset bug and a built-but-never-called CpuRt dispatch gap — were caught ONLY because a
standalone unit-test harness was not trusted as sufficient proof).
Screen/Sdf also carry an opt-in per-slot HitDistancePlane (burn-down Phase 3, PR #38):
a WRITE_HIT_DISTANCE: bool pipeline-override constant (default false), pack2x16float
two-slots-per-u32, written at the SAME site as the bit-31 visibility fold. Stratum's callers pass
false + a shared 4-byte dummy binding (byte-identical, MEASURED with a base-vs-base determinism
control); Lattice passes true + a real per-pixel buffer, consumed by lattice_trace_resolve into
its own combined resolved_distance_plane (min(near, far), KEEP-IN-SYNC sentinel 65504.0) —
currently produced, no consumer wired yet (docs/TODO.md). Any new caller of these primitives must
preserve the false-path byte-identity guarantee; records::RayRecord is NOT this plane's producer
(CPU-side decode contract only, no WGSL packs one — see docs/agent-kb/titan-rendering-3d-agent/ p3-1-hit-distance-plane.md).
Cannot depend on titan-stratum-lighting or titan-lattice-lighting (dependency runs the other
way) — any type that must appear in BOTH model hook trait signatures (e.g.
ProbeTraceLightBuffers) lives in titan-rendering-3d/src/bedrock_hook.rs instead, since neither
impl crate can depend on the other.
crates/titan-lattice-lighting/ — non-reservoir per-frame WRS lighting model (Lattice tier card).
FULLY WIRED as of the P3 Lattice-Mid campaign (tile classify → power delta → sample gen → trace →
shade → hash update → temporal → spatial → composite, plus probe-gather GI + volume samples;
pipeline_deferred_lattice_mid.ron runnable, LATTICE_MID_IMPL_READY=true). Light-model Phase 2
(burn-down PR #37, 2026-07-16) added: area-light kind (position_kind.w=2.0, LightKind enum,
AreaLight authoring), LTC rect area-light shading (Heitz 2016) in lattice_shade.wgsl +
LTC-evaluated (identity-M) WRS candidate target in lattice_sample_gen.wgsl, transmission ray-type
generation, identical-ray-merge never-merge exceptions (area-in-penumbra + transmission),
bPenumbra production (bit 31 = weight-f16 sign lane — EVERY weight reader must mask & 0x7FFFu),
and the oct_decode fold-sign fix across all 5 crate copies. See "Area lights (Phase 2)" below for
the load-bearing conventions.
Cargo features: stratum-lighting (implies bedrock-lighting) and lighting-lattice gate the
two models independently; both default-on. titan-host is a transparent full-bundle passthrough
for both (crates/titan-host/tests/feature_passthrough_guard.rs proves genuine dep-graph exclusion
in either direction). titan-editor's own --no-default-features does NOT exercise this exclusion
— titan-editor depends on titan-host with titan-host's own defaults (no
default-features = false on that edge), so an editor-level lattice-only/stratum-only smoke needs
a titan-editor Cargo.toml change to disable titan-host's defaults, not yet done — the guard test
at the titan-host layer is today's exclusion ground truth.
Area lights (Phase 2, PR #37, 2026-07-16) — load-bearing conventions
- Per-kind lane map (authoritative doc:
GpuPunctualLight in titan-rendering-3d/src/lib.rs):
Area packs half-extents in direction_range.xy, ROLL angle in direction_range.z, oct-encoded
plane normal in spot_cones.zw; spot_cones.xy carry the omnidirectional sentinels
(OMNIDIRECTIONAL_CONE_INNER/OUTER_COS = -2.0/-3.0, same as Point — the smoothstep cone term
saturates to 1.0 for any valid cosine). Kind gates are > 1.5 && < 2.5; spot-only reads gate
> 0.5 && < 1.5. NEVER read direction_range.xyz as a direction without a kind gate.
- Intensity = emitted radiance (
L_e = color·intensity, the Heitz reference's own convention):
NO 1/d² and NO cone term at shading — the LTC polygon integral carries geometric falloff. The
Karis range window applies WITHOUT its 1/d² numerator (prevents a pop at culling range). One-sided
emission from the authored entity −Z face. Total flux grows with rect area (Φ = π·A·L).
- CORNER-WINDING TRAP (MC-furnace-verified twice): the reference
InitRectPoints's literal
order integrates NEGATIVE on the side its cross(dirx,diry) normal faces (its own clipless
behind test defines the emitting face as MINUS that cross) — with Titan's packed-normal-is-
emission-direction convention, max(0,sum) silently zeroes the light. light_pool.rs:: area_rect_corners and both WGSL consumers use the REVERSED (emission-side-positive) order;
KEEP-IN-SYNC.
- LTC LUTs:
titan_bedrock_lighting::ltc (vendored Heitz 2016 fit tables, BSD-3, exhaustively
transcription-verified; VENDORING.md entry). Two 64×64 Rgba16Float SAMPLED textures (hardware
bilinear = the technique's own reconstruction — the one genuinely-filtered LUT case, u32-pack
doctrine exempt), loaded once per LatticeDispatch. UV = (perceptual_roughness, sqrt(1−n·v))
clamped [0.01, 0.99]; ltc_2.w is the clipless-branch horizon form factor (unused by Titan's
full-clip path); sub-f16-normal fit cells flush to zero (unreachable behind MIN_ROUGHNESS).
Sampler-side candidate target uses the identity-M diffuse integral — NO LUT fetch (matches the
point/spot targets' diffuse-proxy family); its early-cull estimate scales by max(4·hx·hy, 1)
(the point model is not an upper bound for A>1 rects).
- Oct decode: all 5 lattice WGSL copies now carry the CORRECT Cigolle fold (subtract
t from
components >= 0); the old inverted fold was a MEASURED live artifact (17.99% of pixels,
RENDERING_BUG_HISTORY 2026-07-16), not dead code. duff_onb pins select(-1.0, 1.0, z >= 0.0)
— never the WGSL sign() builtin (returns 0.0 at 0.0, diverges from Rust at z==0). Drift guards:
shade.rs::oct_fold_and_sign_conventions_pinned_across_all_wgsl_copies (per-file code-line
counts + LTC helper body pins) — extend it when adding any new copy.
- A/B instruments: probe bench
--area-light (rect, radiance 30, 2×1 m) vs --area-flux-point
(equal-total-flux point, I = A·L/4) on --cell deferred-lattice.
Component-placement litmus (2026-07-02, P2 Task 6 STOP-AND-ASK resolution;
docs/design/lattice_stratum_split_audit.md §9 fork 7): does BOTH Lattice and Stratum need it (or
could either use it) → titan-bedrock-lighting (full ownership — state AND dispatch, not
source-relocation-only; established across 4 instances: VSM, shadow-importance, occupancy,
probe-field); does every 3D renderer need it regardless of lighting model → titan-rendering-3d;
does every renderer (2D included, eventually) need it → titan-rendering-core. Apply this litmus to
any future P3-P7 component-placement question before asking — it resolves the "which crate"
question, not data-flow-shape questions (those are still their own STOP-AND-ASK, e.g. Task 7's
screen/SDF/RT shadow-visibility stride-vs-copy fork).
Storage-texture formats — narrowest-that-holds; u32-pack is the DEFAULT mechanism (perf-paramount)
For every new storage AOV / render target, use the NARROWEST format that holds the
signal — never a wide default. As of the 2026-07-03 audit
(docs/design/wgpu_packed_values_audit.md, executed as P3 Task 7.6) the DEFAULT
mechanism is WGSL pack/unpack built-ins into baseline R32Uint/storage-buffer —
NOT the vendored compact-storage patch. The patch (docs/VENDORING.md §Wgpu;
vendor/wgpu-types format.rs allowed_usages + flags) exposes R8Unorm / R16Float /
Rg16Float / Rg11b10Ufloat as STORAGE_BINDING on native desktop, gated on the runtime
probes in lib.rs (compact_storage_supported, r8unorm_storage_supported) with
WebGPU-baseline fallbacks (R32Float ← R16Float, Rg32Float ← Rg16Float) — but the
audit found zero hardware-filtered narrow-storage consumers in the entire codebase
(every read of every narrow storage AOV is textureLoad at exact integer coords,
including both former precedent cases: the slot-5 contact AOV is min()-composited
point-read BY DESIGN for hard edges, and the slot-8.A.2 glossy hit-info BGL says
"Non-filtering: textureLoad at exact half-res coordinates"). All current patch sites were AUDITED to migrate to u32-pack, but execution (2026-07-04)
found 4 sites where a genuine shared-binding constraint blocks packing (stratum_shading.wgsl's
shading_confidence_out, stratum_specular_temporal.wgsl's specular_variance_out,
svgf_temporal.wgsl's variance_out/history_conf_curr — a hybrid pass shares one
Float-typed binding across diffuse+specular channels). The patch stays actively in
use, not dormant, for these 4 real tenants — verify with a fresh grep for
texture_storage_2d<r16float before assuming zero remaining sites.
NEVER widen to Rgba16Float / R32Float to dodge a "format not writable as
STORAGE_BINDING" error or a reviewer's not-baseline-writable flag — that defeats the
patch's entire purpose. Pick by signal: r8unorm = [0,1] scalar; R16Float = HDR /
distance scalar; Rg16Float = two [0,1]/UV channels (f16 UV ≈ 2 px @ 4K — fine for
blurred / denoised data); Rgba16Float only when HDR rgb is genuinely the minimum. Prefer
recomputing a derivable value (e.g. hit distance from a stored hit UV) over storing an extra
channel. Mark VENDORING.md if the patch must grow a new format. Spec-reviews / buddy-checks
MUST flag over-wide formats. (User-corrected TWICE — standing default; auto-memory
feedback_narrow_storage_formats_vendored_patch, reference_wgpu_storage_format_baseline.)
Vendored compact-storage TEXTURES vs WGSL pack/unpack built-ins — when to use which (2026-06-25)
GPU shader cores do math ONLY in f32/i32/u32/f16. unorm/snorm are storage
encodings, not math types — a unorm16 texel is a 16-bit int the texture unit unpacks to
f32 automatically on read. That gives TWO ways to get narrow-storage memory savings, and
they are complementary, not interchangeable:
| Mechanism | What it is | Needs the vendored patch? | Hardware filtering? |
|---|
| Compact-storage TEXTURE (above) | texture_storage_2d<r8unorm/r16float/rg16float, write> | YES (non-baseline; the patch + probe + fallback) | YES — sampled/textureSample gets hw bilinear |
WGSL pack/unpack built-ins | pack into u32 in a baseline R32Uint texture or storage buffer; unpack in-shader | NO — 100% WebGPU baseline | NO — point textureLoad/buffer read only |
Decide by whether the consumer needs hardware filtering:
- AOV sampled with hardware bilinear downstream (e.g. a denoiser history bilinearly
reprojected, a blurred signal
textureSampled) → packing breaks hw filtering → use the
compact-storage TEXTURE via the vendored patch. This is the patch's real and only
justified case (currently ZERO such sites exist — verify with a filtering-sampler read
before invoking it).
- AOV / payload read at EXACT integer coords (reservoirs, IDs, masks, packed metadata,
anything
textureLoaded at the same pixel) → do NOT reach for the patch. Pack into
u32 and use the core WGSL built-ins on a baseline R32Uint / storage buffer:
pack4x8unorm/unpack4x8unorm (4×[0,1] ≤8-bit), pack2x16unorm/unpack2x16unorm,
pack2x16float/unpack2x16float, or raw <</& bit-shifts for integers. Same
bandwidth, zero vendoring, portable, no probe/fallback. The hardware does the f32 math
after unpack; the pack is pure bandwidth.
- LONE-SCALAR refinement (2026-07-03): "point-read → pack" assumes ≥2 narrow
scalars from the same producer available to co-pack at zero net cost. A genuinely LONE
point-read scalar with no same-pass packing partner packed alone into
R32Uint COSTS
memory vs a patch R16Float/R8Unorm texture — for a confirmed-lone scalar the patch
narrow texture remains memory-optimal. First try to create a partner by re-pairing
sibling AOVs from the same producer pass; only then fall back to the patch.
- Concurrent-scatter exception (2026-07-03): NEVER byte-pack values that multiple
threads scatter-write concurrently into a shared
u32 — WGSL has no sub-word atomics,
so packing forces a CAS retry loop per contended byte (false sharing). Per-value u32
atomicAdd lanes are CORRECT even though "wider" (the DDGI feed_accum/
depth_feed_accum fixed-point lanes in probe_field.rs are the reference pattern —
audited correct, do not "optimize" them into packed bytes).
- naga toolchain limit (verified 2026-07-02 via real pipeline-build failure): naga
rejects
rg11b10float as a WGSL storage-texel format ("unknown storage format")
regardless of the wgpu-types patch — and naga is NOT vendored (only wgpu/-core/-hal/
-types are). Canon R11G11B10F storage output = manual 11-11-10 bitfield pack into
baseline R32Uint (no WGSL built-in exists for this layout). Extending vendoring into
naga was evaluated 2026-07-03 and declined — nothing hw-filters a narrow storage AOV.
Exact integers need u32, NOT f16/unorm. f16 is exact only to 2048; an index up
to MAX_PUNCTUAL_LIGHTS = 4096 (e.g. a stored NEE light-id for ReSTIR sample validation) MUST
be a bit-packed u32 — storing it in an rgba16float channel is silently lossy. This bit
the slot-11 D1 same-RNG re-trace: bitcast<f32>(seed) written into an f16 res_d channel was
truncated, so the "stored seed" was unrecoverable. Reservoir/ID data is point-read → it should
be u32-packed, not narrow-float-textured.
Framing correction (common trap): "narrow textures need no vendoring" is true only for
sampled textures (r16unorm via TEXTURE_FORMAT_16BIT_NORM, hw auto-convert on
textureSample). Using a compact format as a compute-WRITE storage texture is exactly the
non-baseline gap the patch fills — so the patch is not redundant, only over-applied when
reached for on point-read data that should be u32-packed. The 2026-06-25 deferred sweep
item is EXECUTED (2026-07-03): full classification of every narrow-format and
narrow-signal-in-wide-format site is in docs/design/wgpu_packed_values_audit.md — 9
prioritized migration work-packets, landing as P3 Task 7.6 of the Lattice/Stratum split plan.
Headline: gbuffer_motion_dilated was widened to Rgba16Float specifically to dodge the patch
and packs to R32Uint at half the memory; DDGI irradiance atlas → manual R11G11B10F bitfield
pack (canon restoration, closes the naga block). **Static-mesh vertex layout is now PACKED
(2026-07-04, packet 10, commit cf6ce7c7): 24 B total — position Float32x3 (world-scale,
also required unpacked by RT/BLAS build), normal+tangent Unorm10_10_10_2 bias-scale (pack
n*0.5+0.5/decode v*2-1 — the standard technique for signed unit vectors in an unsigned
packed format; wgpu::VertexFormat::Snorm10_10_10_2 does NOT exist in wgpu or WebGPU, verify
before ever reaching for a signed 10-10-10-2 variant), uv hand-rolled full-range Float16x2
(the existing f32_to_f16_bits_positive helper forces sign=0 and is unusable for UVs, which can
be negative). Repacking happens at mesh-upload time (was zero-copy before) via
pack_static_mesh_vertices, called from both upload_mesh and the morph-target
update_mesh_vertices path — do not add a new mesh-upload call site without routing through it.
Skinned meshes stay on the old unpacked layout (compute-skinning reads SkinnedMeshVertex as a
storage buffer with a hardcoded raw-f32 stride decode; packing it is a separately-scoped
subsystem, tracked in docs/TODO.md). The packed-vertex stride constant lives in titan-asset
(not titan-rendering-3d) specifically so titan-bedrock-lighting's VSM raster layout can
reference the same value without a circular crate dependency — tied together with a genuine
compile-time size assertion. KB: docs/agent-kb/titan-rendering-3d-agent/p3-7-6-static-mesh-vertex-pack.md.
Correction to the audit's own prediction (found during execution, not predicted by the
audit's static analysis): 4 sites turned out to have a genuine shared-binding constraint
blocking packing (SurfacePlanePoisson shares one Float-typed binding across diffuse+specular
variance/confidence channels) — the runtime probes did NOT retire; compact_storage_supported
stays load-bearing. Full narrative: docs/design/wgpu_packed_values_audit.md §7 (updated) +
.superpowers/sdd/task-7.6-packets-5b-6-report.md.
SP-A pass manifest (2026-06-02) — first stop for per-pass I/O
crates/titan-rendering-3d/src/manifest.rs — debug-dumps-gated, never in editor/game builds.
Exhaustive match &Pass → PassManifestEntry (compiler-enforced; 32 arms). Schema: ResourceId,
PassManifestEntry { id, inputs, outputs, dispatch, provenance }, DispatchKind (13 InlineRenderer /
17 StratumHook / 2 PostProcessHook), Provenance (28 Unaudited / 4 Audited at close).
Golden files: docs/rendering/pass_manifest.ron + docs/rendering/PASS_CATALOGUE.md.
Regenerate: UPDATE_GOLDEN=1 cargo nextest run -p titan-rendering-3d --features debug-dumps export.
CI: 15 GPU-free cross-checks (exhaustiveness, RON drift, ordering, hook-method strings) + editor no-compile guard.
Before diagnosing any artifact: open PASS_CATALOGUE.md to confirm what a pass reads/writes.
Full invariants and the verified deferred-mid pass order: KB sp-a-pass-manifest.md.
SP-C/D FrameResourceRegistry taps — inspect any buffer or texture edge
FrameResourceRegistry (titan-rendering-core/src/frame_resource_registry.rs,
#[cfg(feature="debug-dumps")]) is the SOLE tap mechanism. All scattered DumpSlot /
TextureDumpSlot / TextureDumpSlotR32F structs and per-resource TITAN_DUMP_<NAME> env vars
(HALF_RES_RESERVOIR, FULL_RES_RESERVOIR, SPATIAL_RESERVOIR, CELL_RESERVOIR, CELL_CANDIDATES,
VISIBLE_LIGHT_HASH, SHADING_CONFIDENCE, NUM_FRAMES, WY_SLOT0) were REMOVED in SP-C Task 12.
dump_stratum_stages and the --dump-stages bench arg and DumpKind::Stages probe path
were REMOVED in SP-D Phase 4 — use --tap / TITAN_TAP instead.
Trigger (env): TITAN_TAP=<comma,sep,ids> or TITAN_TAP=all. Windowing (SP-D):
TITAN_TAP_WINDOW=start..end (half-open), TITAN_TAP_ONCE=1 (single-shot per id),
TITAN_DUMP_SUBDIR=<name> (output subdir). Gate: TITAN_DUMP_AT_FRAME=N (legacy lower-bound).
Sink: TITAN_DUMP_DIR (default ./titan-dumps/). Modifier: TITAN_DUMP_RESERVOIR_ALL_SLOTS=1
for per-slot reservoir PGMs. TITAN_DUMP_LINEAR for Rgba16Float → extra .bin sidecar.
Trigger (bench CLI — SP-D): --tap <ids>, --dump-buffers, --dump-window <s..e>,
--dump-once, --dump-label <name> via titan-render-bench-common::TapArgs flattened into
all 3 benches. Apply: renderer.configure_taps(tap.to_tap_config()) + configure_tap_selection(&ids).
30 tappable declared edges (TAPPABLE_RESOURCES, verified 2026-06-06 post-Phase-2): 11 structured
buffers + 19 texture edges (G-buffer NORMAL/METALLIC_ROUGHNESS/MOTION/MOTION_DILATED/DEPTH,
RADIANCE_HDR, RADIANCE_RAW, DIFFUSE_RAW, SPECULAR_RAW, DIFFUSE_PREBLUR (2026-06-05),
SPECULAR_PREBLUR (2026-06-06 Phase 2), TAA_HISTORY, PROBE_DIFFUSE, GLOBAL_SDF_CASCADE,
PROBE_ATLAS, SDF_MARCH_DEBUG + 3 internal diagnostics SHADING_CONFIDENCE/NUM_FRAMES/WY_SLOT0).
15 UNTAPPABLE-with-reason. 45 total DECLARED_RESOURCES. New resource unclassified = CI-red.
SP-D HOOK_INTERNAL_TAPPABLE: 16 hook-internal taps (NOT declared edges; full list in the
titan-rendering-debugging instrument-catalog). 8 denoiser/diagnostic: radiance_pre_firefly,
probe_field_viz (VIZ-gated), diffuse_history/diffuse_moments/diffuse_variance, AND
specular_history/specular_moments/specular_variance — SVGF temporal now covers BOTH the
diffuse AND the specular channel (the Mid-tier specular temporal denoiser shipped 2026-06-04, Pass
StratumSpecularTemporal; the old "diffuse-only" note is retired). Mid/High only; None at Low/Potato.
8 SP-D B per-writer reservoir snapshots: cell_reservoir_{a,b}_postshadow,
full_res_reservoir_{a,b}_post{sdf,screen}, spatial_reservoir_post{sdf,screen} — isolate each shadow
writer's in-place reservoir edit (the shadow passes write post_upsample_reservoir = the SPATIAL
reservoir when reuse is on, NOT reservoir_a/b — KB sp-d-bfc-isolation-taps.md). Same TITAN_TAP id
namespace; CI test hook_internal_tappable_is_disjoint_from_declared enforces no collision.
diffuse_history/specular_history taps are path-aware: feedback-ON = tapped in
record_svgf_atrous_chain at pass-0; feedback-OFF = tapped in record_stratum_{,specular_}temporal.
à-trous per-iteration dumps: already present under diffuse_raw/specular_raw (SP-C Task 10) —
no separate *_atrous_iter{k} ids. SP-D F1: probe_diffuse is now a declared edge
(ProbeGather → Composite; was mislabeled DIFFUSE_RAW). SP-D C: global_sdf_cascade
(Rgba16Sint seed-coord D3) + probe_atlas (D2Array) are now tappable via tap_texture_layered.
TextureKind variants and B/px: Rgba16Float=8, R32Float=4, R32Uint=4, Rg16Float=4,
Rg8Unorm=2, Rg32Float=8 (SP-D, moments). B/px match is EXHAUSTIVE in tap_texture.
Depth uses tap_depth_texture (TextureAspect::DepthOnly); tap_texture uses All (color only).
NUM_FRAMES is R32Uint (packed round(8*n) — NOT R32Float; KB sp-c-c3-comment-staleness-learnings.md).
Inline vs hook taps: inline-renderer edges (G-buffer, RADIANCE_HDR, TAA_HISTORY) tap in
Renderer3d::render at Pass::* arms. Stratum-hook edges tap via ctx.tap_buffer /
ctx.tap_texture (PassCtx wrappers). TAA_HISTORY requires gated PostProcessHook::taa_history_texture().
Feature-forwarding footgun: bench debug-dumps must forward titan-rendering-post-process-3d/debug-dumps
via bench-common explicitly — transitive-only chains can silently break (SP-C lesson).
N+1 drain + schedule-frame invariant: copies scheduled at frame N decoded at N+1. sched_frame=N
passed to BufferAnalyser = Box<dyn FnOnce(&[u8], u64) + Send + Sync + 'static>.
RADIANCE_RAW dual-snap: radiance_raw_shaded (after StratumShading) + radiance_raw_denoised
(after final SvgfAtrous). One TITAN_TAP=radiance_raw id selects both.
Probe capture-window → registry frame-window: capture_frame_window(fps, start, end) returns the
ABSOLUTE-offset window (round(start×fps), round(start×fps) + ceil((end-start)×fps)) (commit 0c1bc4e7,
review-caught HIGH). frame_index is absolute and the probe renders round(start×fps) non-capturing
frames before the window, so the window must START at round(start×fps), not 0 (a zero-based formula
fires taps on the pre-capture frames). EMPIRICALLY PROVEN: capture-start=1.0, fps=60 → dumps at frame
60. Auto-wired in on_start when --dump-window absent + --capture-fps > 0.
Full env table, output formats, numpy decode recipes:
titan-rendering-debugging skill references/instrument-catalog.md.
Consolidated KB: docs/agent-kb/titan-rendering-3d-agent/sp-c-resource-registry.md (SP-C) +
sp-d-registry-unification.md (SP-D consolidated).
Recent locked decisions (until landed in docs/design/)
- ⭐ TONEMAP OUTPUT DITHER (2026-07-15, branch
tonemap-dither-srgb, PR #31 — do NOT re-break):
the final-encode dither in titan-rendering-post-process-core/src/shaders/tonemap.wgsl applies in
the sRGB-ENCODED space (exact IEC 61966-2-1 OETF → dither in 1/255 units → EOTF back; the
Bgra8UnormSrgb swapchain hardware-encodes after the shader returns). Applying it to the LINEAR
value lets the encode slope (~12.9 near black) amplify it ~13× — was the CONFIRMED "speckled dots
of light in floor shadows" artifact. Distribution: zero-mean TPDF h1+h2−1 over [−1,+1) LSB (two
INDEPENDENT hashes: IGN + Vlachos; a permuted-weight second IGN collapses on the px==py diagonal).
UNORM stores round-to-nearest ⇒ dither must be ZERO-MEAN — Gjøl's asymmetric [−0.5,+1.0) interval
is a floor-quantizer recipe; transplanting it bakes a +0.25 LSB whole-image brightening (DC —
invisible to HF/FFT metrics, check full-frame MEAN in any dither A/B; measured +0.242→+0.018 luma).
STATIC pattern is correct here (no downstream temporal integrator at the display write; community
norm). Survey + canon: docs/design/output_dither_survey.md, canon card tonemapping.md (extended
2026-07-15, kept Titan-free). Full record: RENDERING_BUG_HISTORY.md 2026-07-15.
- Froxel-fog scattering albedo (2026-07-15,
fa2a1924, PR #32): sigma_s = sigma_t × FOG_SCATTERING_ALBEDO (quality knob fog_scattering_albedo, default 1.0 = prior behavior
bit-identical; clamp [0,1]). Transmittance/extinction is albedo-INVARIANT by construction —
current.a stores un-scaled sigma_t; only the four in-scatter source terms scale. Canon:
extinction = scattering + absorption, ω = σ_s/σ_t scene-authored. Halo-salience context: at
ω=1.0 the free-space fog halo (peak luma 214) outshines the brightest lit surface (193) — the
user-owned tuning fork after the perceptual gate.
- ⭐ RENDER-SPACE RE-ANCHOR INVARIANT (2026-07-15, fog-trail root cause — applies to EVERY
temporal/reprojection consumer): Titan render space is camera-relative and RE-ANCHORED to the
camera's world position every frame (
extract.rs render_relative(cam_global, camera_world_pos).inverse(); camera uniform camera_pos = ZERO). Therefore view_proj_prev (and
ANY prev-frame matrix/buffer) lives in the PREVIOUS frame's render space: a position computed
this frame MUST be rebased p_prev_render = p_curr_render + (C_curr − C_prev) before crossing
into it (and prev-stored positions rebased with the NEGATED delta before use this frame — the
titan_stratum_lighting::gradient origin_shift precedent). Pure rotation masks the bug
(origins coincident); camera translation frustum-locks the effect — that fingerprint = this
defect. Omission in froxel_fog_inject.wgsl caused the 33–72 px fog-halo trail (FIXED
6178ce31, FogFrameParams.origin_shift; RENDERING_BUG_HISTORY.md 2026-07-14/15). When
adding any new history/reprojection pass, audit for this rebase FIRST. Snapshot-timing rule:
prev_camera_world_pos and view_proj_prev are both written at the TAIL of render() — same
frame-(N−1) provenance; read them before that point.
- ⭐ SCREEN-SPACE TRACE THICKNESS GATE (2026-07-15,
f825f17a, PR #35 — do NOT re-break):
EVERY screen-space HZB march that treats ndc.z >= scene_depth as a hit MUST bound how far
behind the on-screen surface the ray has traveled — the view-Z RELATIVE thickness gate
(RELATIVE_DEPTH_THICKNESS_VIEW_Z = 0.01, reject ⇒ continue the march; Klehm 2016 §4 /
Kasyan 2011 §3; relative/depth-invariant form is the Titan convention, never an absolute band).
lattice_probe_trace.wgsl::screen_trace() shipped without it while its sibling
shadow_visibility_screen.wgsl had it — unbounded behind-surface false hits were the CONFIRMED
root cause of the TL/TR probe-atlas magnitude asymmetry (13/16 winner directions reading exactly
0.0; mirror pair 8×→0.3% post-fix). Gap class: "shared-infra technique ported for one lighting
model, sibling copy missed" — when auditing any screen-trace variant, diff it against
shadow_visibility_screen.wgsl's gate/self-skip structure explicitly. Same session closed:
floor_shadow_mid FFT residual = real scene light-falloff gradient (NOT a defect; present in
pre-tonemap linear HDR — FFT-peak magnitude alone cannot distinguish a smooth gradient from a
periodic defect, check the argmax bin); ShadingConfidence exact-0.5 = canon merged-pair
semantics (K=2 everywhere, one surviving slot ⇒ saturate(1.0/2)); at-rest blobby flicker =
Stratum-only (validated null: same detector reads 20-67× on deferred-stratum, flat on Lattice).
- ⭐ LATTICE CANON-CONFORMANCE CAMPAIGN (2026-07-13, branch
lattice-flicker-parity, PR #28, user gate APPROVED). Do NOT re-break: (1) TAA blend = canon F5 adaptive α∈[0.03,0.12] (INSIDE form, YCoCg-Y luma-diff) + explicit defensive clamp; the old 1/(1+max luma) α=0.5 ceiling was an uncited mis-transplant (Karis §3.5 cites the EMA only). (2) TAA jitter = titan_rendering_core::r2_sequence (Roberts 2018, s0=0.5, UNWRAPPED raw u64 frame index, f64 internal) — canon pairing rule: F5's 8-33-frame window exceeds 8-tap Halton's period; halton_2_3 retained for other consumers. (3) Screen-probe placement jitter also R2 via LatticeProbePlaceFrameUbo.jitter_offset (in-shader Hammersley(8) deleted — period-2 oscillation carrier). (4) F1 ray reassignment = canon SUPERSAMPLE semantics: low-p̂ lanes trace sub-texel jitters of their winner's OWN footprint, equal-weight mean into the winner's texel; victim selection BRDF-only (card F1 "*" rule), winner ranking product-p̂; culled texels = zero + down-weight, filled by the cross-probe filter with self-weight EXCLUDED (energy dilution otherwise); placeholder dist=0 must NEVER enter any hit-distance consumer (temporal hit-velocity + filter clamp both gate on it). (5) ShadingConfidence = effective-sample-count coverage Σ num_merged/w_unclamped over visible-AND-guided slots /K (primary-verified; NOT clamp-survival); grazing history threshold DIVIDES by lerp(0.1,1,saturate(NoV)) (widens ≤10× at grazing — the multiplying form was inverted); F3 denom floor HISTORY_CONFIDENCE_DENOM_FLOOR=0.00625. (6) TAA confidence port REMOVED — its founding clean-room citation was falsified (primary's ShadingConfidence is denoiser-internal, zero TSR bindings); never wire shading confidence into a TAA/upscaler history blend (measured 7.7× junction regression). (7) identical_ray_merge PRESERVES weight: same-light K-slots merge by SUMMING weights into the survivor + SampleRecord.num_merged (bit 14); zeroing a slot's weight HALVED direct light in few-light scenes (live from sampler ship until 2026-07-13; all pre-fix absolute luma baselines are at the OLD half-energy level — re-baseline entry in RENDERING_BUG_HISTORY has raw + luma-normalized tables). (8) Full-res counter-capped GI temporal accumulator SHIPPED (2026-07-14, branch lattice-gi-temporal, 72da7e6c..ae0f3efa) — do NOT re-break: canon's MAIN GI accumulator (previously ABSENT; Titan had inverted the architecture by shipping only the probe-space EMA canon defaults OFF). Lives in Pass::LatticeComposite (gi_temporal_blend) on the PRE-modulation interpolated irradiance: alpha = 1/(1+N), per-pixel counter clamped to GI_TEMPORAL_MAX_FRAMES=10, relative view-Z depth reject 0.01, opt-in 45° history-normal reject (default off). Merged Rgba32Uint history (R11G11B10F irradiance / view-Z / counter+fast-amount state / oct-packed normal), tap lattice_gi_temporal_history. Fast-update: per-ray GEOMETRIC moving test (|probe speed − hit speed|/max(depth,floor) > 0.005, DE-JITTERED gbuffer_motion reads — a raw motion read re-saturates the signal with jitter noise; at-rest fraction must measure exactly 0.0) → per-probe moving-ray fraction → two-stage ramp CAPS the counter to (1−amount)·10 (alpha ≥ 0.5 at ceiling, NOT an alpha of 0.9); history stores the PRE-max raw amount (strict one-frame lookback — writing the post-max value creates a permanent latch, measured and fixed); cold-start = per-dispatch gi_temporal_history_invalid lifecycle flag (mirrors cut_taa_history; per-pixel disocclusion zeros ONLY the counter). probe_temporal_blend_enabled default FALSE per canon (the pass still writes reprojection history for pdf_Lighting). Probe-space spatial filter = 3 passes of 4-neighbour, widening to 8 + angle-error-weight skip only in fast-update regions. Envs (debug): TITAN_LATTICE_GI_TEMPORAL=0 kill, TITAN_LATTICE_PROBE_TEMPORAL_BLEND=1 legacy A/B. Validated: probe-space lag amplification eliminated (1.00× everywhere vs 0.92–12× OLD), static noise 0.97×/0.77×, attenuation trail bit-identical. Canon sources: updated screen-probe-gi.md card ("Main temporal accumulation (full-res)" row) + relay §6.3–§6.6 (source-verified 2026-07-13/14). NOTE: the reference is detection-BLIND to a moving light over static geometry (geometric signal only, verified at source) — the counted window is the only bound for that case; never "fix" a moving-light GI lag by adding a radiance-delta trigger without express user authorization. Open: moving-light trail = canon-correct attenuation physics (content/tuning fork user-owned), TL/TR steady-state residual (winner-direction trace variance, per-texel rank tap next), Stratum's pre-correction confidence formula echo (P6).
- ⭐ P3.5 PER-MESH (NEAR-FIELD) SDF TIER — IMPLEMENTED (2026-07-11/12, commits
cb06b842 Tasks 2-8 + 29ac1c68 Task 5c fix-round). Design docs/design/mesh_sdf_near_field_tier.md; plan docs/superpowers/plans/2026-07-10-mesh-sdf-near-field-tier.md. Wright 2015 G-F5 field-switch: cook-time per-mesh SDF bake (titan-asset::mesh_sdf_bake::bake_mesh_sdf, brute-force ray-sample + point-triangle magnitude, >50%-backface sign, thin-surface negative-texel guarantee, MeshTassetPayload.sdf: Option<MeshSdfPayload> schema bump) → shared runtime atlas (titan-bedrock-lighting::mesh_sdf_atlas::MeshSdfAtlas, single R16Float 3D shelf-packed atlas + region index buffer, sampled not storage — no vendored-patch dependency) → per-frame instance upload + world-grid cull (Pass::MeshSdfInstanceCull, camera-centered 8×8×8 grid over ±R_GRID_M=8.0, K_MAX_CANDIDATES=8/cell; skinned + non-uniform-scale instances excluded at upload) → a shared near_field_distance(pos_ws, t) -> vec2<f32> (distance, governing voxel size) snippet march consumers min-composite against the global cascade for t < R_NEAR_M=4.0. KEEP-IN-SYNC across all 5 march consumers (shadow_visibility_sdf.wgsl, probe_trace.wgsl, rt_reflection_trace.wgsl, restir_gi_initial.wgsl, lattice_world_cache_trace.wgsl) — verified byte-identical (MD5-pinned in-file #[cfg(test)] guards); the shared cascade-voxel-size helper is named cascade_voxel_size_m in all 5 (a pre-existing 1-token naming divergence was unified during Task 6, not left "byte-identical modulo naming"). lattice_world_cache_trace.wgsl lands at exactly 8/8 storage buffers post-merge (zero headroom — any future storage-buffer addition there needs its own slot-freeing pass first); probe_trace.wgsl's feed_accum+depth_feed_accum were concatenated into one array<atomic<u32>> with an index-range split to free the 9th slot (NOT byte-packing — each atomic lane keeps its own dedicated u32 word, safe under the concurrent-scatter rule). Three self-occlusion/eligibility gates, do NOT re-break: NEAR_SELF_SKIP_VOXELS=2.0 (near-field candidate skip while t < 2.0 × region.voxel_size_m, 1 voxel RTSDF ε-bound + 1 voxel trilinear support) and GLOBAL_SELF_SKIP_VOXELS=4.0 (global-cascade march start-gate, same RTSDF §2.5 citation applied to the far field — raised from an initially-proven 2.0 after round 22 found the false-hit population re-clusters at whatever the current gate threshold is, a re-arming artifact rather than a fixed-radius contamination footprint; the mid-march cascade-transition re-arm risk this implies is disclosed, unmeasured, watch-listed) are DISTINCT constants at DISTINCT sites (do not conflate near vs global gates when reading a KEEP-IN-SYNC diff) — plus a near-field candidacy eligibility rule (continue when the candidate's own voxel size exceeds the local global tier's, since a coarser field can never refine a finer one; continue when FLAG_UNIFORM_SCALE bit 0 is unset). Voxel-scaled hit-threshold floor (RTSDF §2.5.3) in all 5 marches: if d < max(HIT_THRESHOLD_M, hit_eps_for_step()) where hit_eps_for_step() is the governing tier's voxel size (near: region.voxel_size_m from the snippet's .y; far: cascade_voxel_size_m(cascade_idx)) — replaces a fixed threshold that produced thin-occluder holes. In-band step-clamp invariant (Task 7 fix round, found by dual review, not the implementer): a self-occlusion gate that forces d_global=1e30 inside its skip band MUST keep the march's step advance CLAMPED (clamp(d, MIN_STEP_M, voxel*0.5)) while still inside that band, and only go unclamped once past it — an unclamped step during the gated band lets the very first post-gate sample jump past t_max in one stride, silently skipping real occluders inside the skip radius (this bit rt_reflection_trace.wgsl/restir_gi_initial.wgsl when the gate was first added there; shadow_visibility_sdf.wgsl's original clamped-step shape was the correct reference to mirror, not a "verbatim already-clamped" assumption). Voxel-index formula unified write/read (Task 8, commit cb06b842): both the write side (global_sdf_seed_surface.wgsl) and all read sides now use floor((pos - origin) / voxel_size) — the prior read-side round(x - 0.5) ties-to-even at exact odd/even voxel-size multiples, a 1-voxel divergence CPU-proven at k ∈ [-8,8]; unified across 6 sites plus 4 additional cascade_voxel_coord sites in the SAME 4 files (a second, independent index-computation site beyond the march's own sample_cascade/sample_sdf, easy to miss when auditing only the primary formula) — a workspace-wide grep for round(x-0.5) voxel-index sites now returns zero. Concave-corner escape loop RETIRED (Task 5, commit dddbc81e) — the 12-iteration wall-normal/dir-direction escape walk (2026-07-10 rounds 1-4) is deleted, not kept; Task 5c (rounds 18-22) found it was masking the GLOBAL_SELF_SKIP_VOXELS mechanism above, not a separate defect. Escape-loop retirement reopened junction false hits at the wall-floor corner; the K=4.0 gate restores measured behavior to statistical-baseline parity (Task 9 battery: junction-flicker ON/OFF ratio 0.9996-1.0003) — a regression FIX, not a net improvement over the pre-campaign baseline; final #59/#60 disposition awaits the user perceptual gate (docs/OPEN_BUGS.md). Open, unresolved: far-field mesh-boundary behavior (a far-segment march near a mesh boundary mid-ray still reads the global cascade, not the near-field tier — docs/POST_WORK_FINDINGS.md); atlas eviction/streaming for world-streaming scenarios (docs/TODO.md).
- ⭐ LATTICE/STRATUM LIGHTING-MODEL SPLIT — THE ACTIVE CAMPAIGN, MERGED TO MASTER
536f8d0d (2026-07-01/02). The 2026-06-25 canon reconcile proved MegaLights-style stochastic direct is NON-reservoir; the tier checklists define two co-equal never-mix packages. titan-stratum-lighting's hybrid splits into Lattice (non-reservoir per-frame WRS + bundled confidence denoiser, new titan-lattice-lighting crate, skeleton only until P3+, all four tiers, new-project default), Stratum (pure ReSTIR/ReGIR reservoir path, purified in place, Mid+High), Bedrock (shared lighting infra — new titan-bedrock-lighting crate EXISTS and owns VSM, shadow-importance, occupancy/sun-vis denoise, quality config carve, and a new BedrockRenderHook trait dispatched via DispatchKind::BedrockHook in titan-rendering-3d/src/manifest.rs, mirroring the pre-existing StratumRenderHook/DispatchKind::StratumHook pattern; Renderer3d orchestrates live per-frame data between the two boxed hook trait objects at Pass::StratumComposite/StratumShading/StratumRtShadowRay/DeferredLighting — read crates/titan-rendering-3d/src/bedrock_hook.rs before touching any of these dispatch arms). titan-host's feature-forwarding gap is CLOSED (2026-07-02): titan-host is now a transparent full-bundle cargo-feature passthrough (default-features = false on its titan-plugins-default-3d dep + 9 mirrored forward arms including stratum-lighting/lighting-lattice); titan-bedrock-lighting (bedrock-lighting feature) is genuinely excludable end-to-end from any titan-host consumer now, proven by a cargo-tree exclusion guard (crates/titan-host/tests/feature_passthrough_guard.rs). Bedrock stays standalone shared infra — Stratum/Lattice imply it, never the reverse; titan-editor's Bedrock hook now sources titan_bedrock_lighting::BedrockQualityConfig directly (not via Stratum). No runtime switch; lighting-lattice/stratum-lighting cargo features. P2 structural carve is COMPLETE (2026-07-02, 10 tasks; behavior-frozen — bit-exact static-regime tap equivalence + moving-lights deltas inside the same-commit noise floor, RENDERING_BUG_HISTORY.md 2026-07-02 entry). The crate map above (§ "Crate map") and the component-placement litmus are the landed, current state — this paragraph is now historical/orientation context, not a pending-work marker. READ BEFORE ANY LIGHTING WORK: spec docs/superpowers/specs/2026-07-01-lattice-stratum-lighting-split-design.md (tier cards ×3 layers) + ledger docs/design/lattice_stratum_split_audit.md (classification of EVERY component; §9 forks ALL DECIDED) + the six plans docs/superpowers/plans/2026-07-01-lattice-stratum-split-p*.md. Load-bearing P1 findings: probe field = Bedrock DDGI far-field store (not parked — tier-ON at Mid/High; 4/5 legacy bugs already fixed, Chebyshev still inert); W3 ReBLUR is MERGED (canon values match; enable_anti_firefly = dead knob); occupancy/sun-vis denoiser = canon SIGMA Blur→PostBlur→Stabilize (shipped P3.7.5 O1-O5, commit d1861ed5; per-light-vs-aggregate penumbra approximation via W_c-weighted mean; occupancy_penumbra_view() producer contract; history packed R32Uint; KB sigma-occupancy-denoiser-rebuild.md; perceptual A/B still open, POST_WORK_FINDINGS.md); shipped ReGIR deviations (log-perceptual cell target, M-cap-20 merge, ±½-cell jitter, S→K=4 reduction) → canon S-resident rebuild in P6; slot 11 SUPERSEDED (one-accumulator question dissolves per-model). Motion blur: owned by future m51 post pass, mutually exclusive with time-augmented splatting (frame-graph guard, P7). P3 Lattice-Mid Task 8 shipped (2026-07-08, commit 958e57b4): Pass::LatticeComposite (titan-lattice-lighting::composite/shaders/lattice_composite.wgsl) upsamples the spatial pass's half-res filtered diffuse/specular, re-modulates by G-buffer albedo/env-BRDF (exact inverse of Task 4's lattice_shade.wgsl demod — MIN_ROUGHNESS=0.089/SPEC_DEMOD_EPS/env_brdf_approx KEEP-IN-SYNC), sums the near-field GI band (single composited band via a 1x1 zero dummy until T10b lands a producer — no additive far-field/DDGI branch at composite, per canon's handoff-not-additive contract), writes into radiance_raw. TAA confidence port — REMOVED (2026-07-13, canon correction; was @group(0) @binding(9) in titan-rendering-post-process-3d's taa.rs/shaders/taa.wgsl). A 2026-07-13 exhaustive primary-source verification (clean-room reference §9.7 CORRECTED + second verification pass) found ShadingConfidence has ZERO TSR-side bindings in the primary — it is denoiser-internal only (reciprocal max-frames map, clamp-relax, σ_c spatial gate); the earlier "emitted to TSR as a reactive/history-trust signal" claim was a relay error. Empirically, wiring a canon-conformant confidence formula through the port produced a bisected, isolation-proven 7.7× junction-noise regression (mean|ΔY| 0.2138→1.6388; TITAN_TAA_CONFIDENCE_STRENGTH=0 collapsed it to 0.1327) — low confidence forced TAA history discard exactly where the denoiser needed TAA's accumulation. The port (BGL binding 9, the confidence_strength/TITAN_TAA_CONFIDENCE_STRENGTH plumbing, the shading_confidence_view() trait method, the Lattice producer-side wiring, taa_confidence_port.rs) is fully removed, not softened — see the removal entry in RENDERING_BUG_HISTORY.md and stochastic-direct-denoiser.md's Output-AOV row (now marked DENOISER-INTERNAL ONLY). The ShadingConfidence AOV ITSELF and its denoiser-internal consumers (reciprocal map, k_relax, σ_c gate) are UNTOUCHED and remain canon: the formula correction landed 2026-07-13 (clean-room primary verification) — the original clamp-survival formula was built from a relay error; lattice_shade.wgsl now accumulates 1/w_unclamped over visible-AND-guided slots normalized by K — effective-sample-count coverage, provably [0,1] (each term 1/W_Y ∈ (0,1] since w_running ≥ picked_w). Paired corrections in lattice_temporal.wgsl: grazing history threshold WIDENS at grazing (/lerp(0.1,1.0,saturate(NoV)), primary-verified — the tightening ·saturate(NoV) form was inverted) and the F3 confidence-denominator floor is HISTORY_CONFIDENCE_DENOM_FLOOR=0.00625 (primary's 0.1/LIGHTING_EXTRA_EXPOSURE, domain-verified to transfer). See stochastic-direct-denoiser.md card (corrected rows) + KB confidence-chain-canon-correction-2026-07-13.md. P3 Lattice-Mid Task 9 shipped (2026-07-08, commit 849fd391): Pass::LatticeVolumeSamples (titan-lattice-lighting::volume/shaders/lattice_volume_samples.wgsl) — per-froxel-voxel K=2 stratified WRS over the clustered light list replaces froxel_fog_inject.wgsl's full punctual loop when Lattice is active (FogFrameParams.lattice_active branch, byte-identical original loop when 0); phase-weighted target PDF (HG substitutes the screen path's n·l — no surface normal at a froxel); MinSampleWeight=0.1; §16.3 R2-sequence per-Z-slice blue-noise offset. Occlusion cull is a per-voxel depth-compare, NOT canon's HZB test — accepted deviation (no general-purpose HZB exposed to hook passes today; the substitute is strictly more accurate than a coarse-tile HZB, logged POST_WORK_FINDINGS.md). Combined VisibleLightHash+HiddenLightHash guide-by-history buffer (6 words/voxel) — HiddenLightHash built canon-complete (real XXHash32, mirrors lattice_hash_update.wgsl) but currently unwritten/unread (no volume shadow-trace producer/consumer exists yet), same "reserved but unpopulated" precedent as RayRecord.distance. NOT wired into pipeline_deferred_lattice_mid.ron (that scaffold has no RADIANCE_HDR producer before FogApply yet — a pre-existing gap, left for the later tier-wiring task). Spec review's fix round also caught and fixed a pre-existing, unrelated bug: froxel_fog_inject.wgsl's PunctualLight struct read range from the wrong field (.x instead of direction_range.w) — the new WRS branch newly depended on cross-pass consistency with the sampler (which read it correctly), making it a real regression risk worth fixing now rather than deferring (RENDERING_BUG_HISTORY.md). P3 Lattice-Mid Task 10e shipped (2026-07-08, commit b381fb22): world-space directional radiance cache (titan-lattice-lighting::probe_gather::world_cache/shaders/lattice_world_cache_{place,trace,filter}.wgsl, Pass::LatticeWorldCache{Place,Trace,Filter}) — screen-probe GI's own disocclusion-fallback store (Lattice-owned per the component-placement litmus, Stratum's ReSTIR-GI+DDGI never reads it). PRODUCER ONLY — no consumer until T10a/T10b land (built to full canon completeness anyway per the standing directive). Per-probe 32×32 octahedral atlas: Radiance manually bitpacked R11G11B10F into baseline R32Uint (naga rejects rg11b10float as a storage format, so the vendored compact-storage patch doesn't apply here — a genuine plan-text correction, not a code divergence) + TraceDistance R16Float/R32Float fallback. Camera-centered 3D clipmap + ProbeIndex indirection + a lock-free free-list stack (place/free split across a compute-pass barrier prevents push/pop interleave) for persistent whole-probe reuse; toroidal origin-snap matches titan_bedrock_lighting::clipmap_shared's canonical formula. 2-bucket GPU priority histogram for the fixed per-frame trace budget (cache-miss HIGH over-budget → drops to 16×16; aged lighting-update LOW over-budget → skipped). SDF sphere-march trace mirrors probe_trace.wgsl's rung ladder; raw-E=π store proven by furnace test. 1 Critical fix required and applied: the F4 spatial filter initially mixed voxel-index-unit probe positions with metre-unit hit distances (hardcoded spacing=1.0), silently neutering the angle/occlusion leak-prevention tests — caught independently by BOTH spec-compliance and code-quality review, fixed by threading cascade0_spacing_m into the filter's UBO. 1 Important fix also applied: the trace's punctual NEE initially omitted range-window + spot-cone attenuation; fixed by mirroring lattice_shade.wgsl's convention (verified MORE complete than probe_trace.wgsl's own NEE, which is itself missing range/spot/the .a intensity factor — matching the more-canon-correct target over the literally-named reference is the right call under "skill canon supersedes Titan canon"). 2 Minor items carried forward to T10a/b (non-blocking): placement predicate is dense-fill until T10a defines an interpolation footprint (the free-list mechanism itself IS sparse-capable); octahedral atlas border gutter reserved but unpopulated (only needed once a future bilinear SH-gather consumer exists). Real-GPU pipeline-build test caught a genuine 8-storage-buffer-per-shader-stage WebGPU limit overflow that cargo check cannot see (fixed by merging trace_control+indirect into one buffer) — see titan-rendering-core's "WebGPU hard limits" note for this constraint. P3 Lattice-Mid Task 10a shipped (2026-07-08, commit 4bb4d421): screen-probe placement + trace (titan-lattice-lighting::probe_gather::{placement,trace}/shaders/lattice_probe_{place,trace,trace_hwrt}.wgsl, Pass::LatticeProbePlace/LatticeProbeTrace/optional LatticeProbeTraceHwrt). Uniform 16px grid + one quantized-density adaptive mark pass (4×2 layout; canon's "16→8→4" framing is conceptual, confirmed NOT to under-refine); 8×8 octahedral atlas WITH a populated gutter border (unlike T10e's world cache, which needs none yet). Product-IS ray-gen: groupshared bitonic sort of p̂=pdf_BRDF·pdf_Lighting + 3-low:1-high reassignment. pdf_Lighting sources the T10e world-space radiance cache 100% of the time this task (no screen-probe temporal history exists yet — correct for this phase; T10c must wire the last-frame-reprojection primary, carried forward, not a T10a blocker). Trace rung: screen march → SDF sphere-march → optional HWRT (Option<Self>/None on non-ray-query, mirrors BedrockShadowVisibilityRt). 1 Critical fix required: the proprietary product name from a cited clean-room relay leaked into a manifest Citation + 2 comment sites — fixed by removing the Tier-2 citation entirely (reframed as this pass's own producer defaults, Tier-1-only, mirroring world_cache.rs's shape) rather than obfuscating it; both reviewers independently re-verified zero remaining occurrences via their own greps. Also fixed: a dead WGSL override paired with a doc comment falsely claiming a WGSL assert existed (replaced with a real debug_assert_eq!); an undisclosed HWRT border-staleness gap (now disclosed); 8 dead voxel-material bindings in the HWRT bind-group (removed with their unused helpers); an imprecise cull-gating comment (tightened to the actual p̂ product). Cross-reviewer convergence pattern worth noting: neither this task nor Task 10e's Critical bugs were caught by only one review lens — both were independently found by BOTH spec-compliance and code-quality review, a strong signal for treating same-finding convergence as high-confidence. P3 Lattice-Mid Task 10b shipped (2026-07-08, commit f183b24e): probe filter + SH gather + F3 distance bands (titan-lattice-lighting::probe_gather::{filter,gather}/shaders/lattice_probe_{filter,sh,gather}.wgsl, Pass::LatticeProbeFilter/LatticeProbeSh; per-pixel gather folded into Pass::LatticeComposite, not a separate pass). Probe-space 3×3 depth-weight-only filter (F4-style clamp-neighbour-before-reprojection, byte-identical constants to Task 10e's world-cache filter). SH3 projection reuses the engine's existing Ramamoorthi/A_l convention, solid-angle-weighted octahedral quadrature (verified against the oct-decode Jacobian, not just plausible-sounding); furnace-proven E≈π. F3 distance-band handoff added to the trace: screen+SDF rungs march only [bias, gi_near_band_m=2.0m], T10e's world-space radiance cache fills the far field on near-miss (XOR by construction), DDGI beyond 16m left for tier-wiring. 1 canon-adjudication resolved (directive-owner call, not a bug): the in-plane interp jitter was initially a soft plane-weight instead of canon's literal hard accept/reject gate — rebuilt to the literal gate per the standing no-divergence directive; the SEPARATE soft plane-distance weight (a genuinely distinct canon mechanism, confirmed against two separate Wright-2021 slides, S38 vs S39) correctly stays soft. Also fixed: a stale docstring describing a dead no-op gate instead of the real zero-init-buffer mechanism; a dead UBO field repurposed as the now-tunable gi_near_band_m; wasted GPU work computing SH for unreachable adaptive probes (dispatch scoped to uniform probes only). Dispatched directly at the opus tier per its "reasoning-heavy" flag — came back DONE in one pass. Both reviewers cleared after one fix round. P3 Lattice-Mid Task 10c shipped (2026-07-08, commit a070ca50): screen-probe temporal filter + fast-update mode (titan-lattice-lighting::probe_gather::temporal/shaders/lattice_probe_temporal.wgsl, Pass::LatticeProbeTemporal). Per-probe depth+normal reprojection-rejection filter — a hard accept/reject gate (matching Bedrock's occupancy_temporal/svgf_temporal reprojection-filter convention), explicitly NOT a neighborhood/AABB clamp (jittered probe placement needs stable history; a clamp would fight the jitter). Closed the Task 10a carry-forward: LatticeProbeTrace's pdf_Lighting now sources the PRIMARY last-frame-reprojected temporal history, falling back to the Task 10e world-space radiance cache only on genuine disocclusion/cold-start. 1 canon-adjudication required (not a bug): fast-update's "raise spatial" was initially built as an intra-probe angular box-blur inside the temporal pass itself — spec review correctly identified this as the wrong mechanism (canon pairs fast-update with the CROSS-probe filter, Pass::LatticeProbeFilter from Task 10b, which runs BEFORE Temporal in the pipeline, so "raising spatial" for THIS frame's output can only mean widening Filter's kernel, not adding a new directional blur downstream). Fixed by computing the per-texel hit-velocity signal at trace time, max-reducing to one per-probe factor (LATTICE_SCREEN_PROBE_FAST_UPDATE, workgroup-uniform barrier, single-writer reduce), and having LatticeProbeFilter relax its own angle-error threshold (cos25°→cos45°) and depth-similarity sigma (0.5→1.5) as fast-update→1 — LatticeProbeTemporal keeps only "lower temporal" (continuous smoothstep alpha blend, not a hard switch). Also fixed: a FOV divergence between trace's and temporal's disocclusion gates (trace hardcoded 60°, temporal used the real per-frame value, undermining the manifest's "identical by construction" claim) — threaded ctx.fov_y_radians into LatticeProbeTraceFrameUbo.grid.w via bitcast so both passes share the identical value. A real-GPU pipeline-build test caught a WebGPU 16-sampled-textures-per-stage limit overflow (adding the fast-update binding pushed trace's aggregate sampled-texture count to 17) — fixed by merging the history radiance+distance textures into one Rg32Uint atlas (.r=packed R11G11B10F radiance, .g=bitcast distance — MORE precise than the prior f16 distance), simplifying ProbeTemporalResources from 4 ping-pong textures to 2. Both reviewers independently confirmed all fixes in a second pass, converging on the same "genuinely fixed, mechanism now correct" verdict from two different lenses. P3 Lattice-Mid Task 10d shipped (2026-07-08, commit d96610c1): bent-normal contact AO (HBIL) (titan-lattice-lighting::probe_gather::bent_normal/shaders/lattice_bent_normal.wgsl, Pass::LatticeBentNormal). Full Mayaux 2018 HBIL derivation, not a GTAO-style AO-only stub: per-pixel horizon-angle recurrence (2 slices × front/back march, 8 steps/direction, 16px trace distance canon-pinned to PROBE_DOWNSAMPLE), normal-tangent-plane horizon init, bent normal (eq 5/6) + radiometric AO (eq 11), and an incremental near-field bounce (eq 18/19) sampling last frame's reprojected diffuse-only radiance through a new persistent (no ping-pong) diffuse_history texture that LatticeComposite writes each frame. LatticeComposite redirects the far-field SH sample to the bent normal (not the geometric normal) and scales it by the closed-form ℱ0(AO) energy-compensation factor (Mayaux §2.2.3's own disclosed simplification) rather than re-deriving cone-reduced SH integration; near-field adds directly (HBIL composition: screen-probe GI = far-field, bent normal = near-field). Three literature substitutions, each verified against the actual cited source rather than accepted on the implementer's word: horizon angle computed from reconstructed 3D positions instead of the paper's screen-space height-field trig-shortcut (mathematically the same angle, standard GTAO-family practice); last frame's diffuse radiance read via a view_proj_prev world-position gather instead of the paper's scatter+push/pull (the paper's own disclosed §3.2 alternative; disocclusion holes read as zero for one frame, self-healing next frame — a disclosed quality gap, not a correctness one); the interleaved quarter-res split-buffer optimization deliberately NOT implemented since canon row 85 says "full-resolution" verbatim. Pass::LatticeBentNormal ordering rule requires a preceding Pass::LatticeProbeTemporal when present; LatticeComposite does NOT hard-require a preceding LatticeBentNormal, mirroring the existing LatticeProbeSh-optional precedent (both producer-optional, zero-init-dummy no-op). 2 must-fix code-quality findings, both closed in one round: (1) 14 "T10d" task-ID tags leaked into shipped comments — the SAME class of mistake as Task 10c's "T10c" leak, now the second recurrence this campaign; watch for this pattern continuing into future tasks. (2) The new diffuse_history prev-frame read was missing from manifest.rs's HISTORY_RESOURCES list — a latent landmine that would only surface as a false "producer-after-consumer reordering" CI failure once Task 11 actually wires LatticeBentNormal before LatticeComposite into a real RON; fixed proactively rather than left for Task 11 to hit. Both reviewers converged on a clean PASS after the fix round. P3 Lattice-Mid Task 11 shipped (2026-07-08, commits 6a5ca46a+7b8179ed): tier wiring + Mid preset lock — the final integration task, wiring every T0-10d producer pass into a real, runnable pipeline for the first time. New LatticeQualityConfig (titan-lattice-lighting/src/quality.rs) mirrors the Bedrock/Stratum override-contract convention; High/Low/Potato honestly resolve identically to Mid today (P4/P5 stubs, disclosed not silently claimed). Both pipeline_deferred_lattice_mid.ron and the forward variant finalized with the full pass chain; the plan's one explicitly-named hard ordering requirement (LatticePowerDelta before LatticeSampleGen, canon §4.1 steps 5→6 — violating it reintroduces a 1-frame flashlight delay) is genuinely encoded as a validate_pass_ordering rule, not just a comment. LATTICE_MID_IMPL_READY flipped true; resolve_pipeline_name and the separate RenderQuality::pipeline_filename mapping both gained the (Lattice, Mid, _) arms, drift-guarded by a test that iterates every (LightingModel, DenoiserTier, ShadingMode) tuple. LatticeVolumeSamples (Task 9) was Audited but missing from manifest.rs::ALL_PASSES — added, audit-count pin bumped 73→74. 1 real bug found via editor smoke, invisible to every existing test: LatticeProbeTrace's own HZB binding (Task 10a) declared TextureSampleType::Depth against a self-consistent Depth32Float dummy, but the real closest-HZB pyramid is a compute-built R32Float texture — wrong-but-internally-consistent, so no pipeline-build test caught it until this task's RON finally scheduled the pass in a real dispatch loop for the first time. Fixed: WGSL texture_2d<f32>+.r, BGL entry Float{filterable:false}, dummy rebuilt as 1×1 R32Float. 1 Critical spec-review finding, required a second fix round: the implementer correctly rebuilt the model-selection tie-break in installed_model_output() (arbitrate by active frame-graph pass membership — Pass::StratumShading/Pass::LatticeComposite — instead of an unconditional Stratum-first preference) as a pre-flip prerequisite the code already had a TODO for, but left TWO OTHER inlined copies of the OLD install-order tie-break unfixed: FogInject's lattice_active gate (silently forced LatticeVolumeSamples' output to never be consumed, falling back to the non-canon exhaustive per-cluster loop) and the resize path's gbuffer_bind_group rebuild (silently bound Stratum's unwritten radiance_raw_view() after any window resize in a Lattice project, losing all direct lighting). Both were genuinely invisible to CI/nextest/smoke (boot succeeds, lighting is just silently wrong) — closed by replicating the exact same pass-membership arbitration inline at both sites (kept inlined for legitimate borrow-checker reasons), plus a debug_assert! at all three sites guarding the "both models scheduled" case that should never arise. Manually verified via temporary logging (removed before commit) that Lattice's fog genuinely consumes LatticeVolumeSamples and radiance survives a resize. Both models (Stratum default + Lattice) smoke-tested clean after the fix. Testing-gap note (both this task's bug classes): a BGL-type mismatch against a self-consistent-but-wrong dummy, and a hook-selection tie-break that silently picks the wrong model, are BOTH invisible to cargo check/clippy/nextest/editor-smoke — the former needs a real pipeline-build test that constructs an actual bind group against the real resource (not a hand-rolled dummy of the wrong type), the latter needs a real-RON-scheduled + both-hooks-installed check that inspects which hook actually won, not just that the editor boots. Neither class of bug had a test until this task's real RON scheduling exposed them for the first time — worth remembering that "producer ships ahead of RON wiring" (this campaign's established pattern for Tasks 8-10e) defers not just integration risk but this entire class of latent, self-consistent-looking bugs until the wiring task.
- Mid-tier canon campaign — Slot 10 Temporal-reuse (2026-06-25). Audit
docs/design/mid_tier_canon_audit.md §10; spec 2026-06-25-slot10-temporal-reuse-design.md. Commits 85600d86+b0eeac44 (depth-convention), d4033a9a (D2/D3). Reservoir temporal-reuse audit: ReGIR-cell + ReSTIR-GI merges MATCH on mechanism (gather-back, M-cap 20/30, Jacobian=1). LOAD-BEARING (do NOT re-break):
- Depth convention is FORWARD-Z (near→0, far→1, sky=depth 1.0) —
perspective_rh + Less + depth-clear 1.0; sky/miss sentinel = depth >= 0.999999 (stratum crate) / >= 1.0 (rendering-3d crate). NEVER depth == 0.0 for sky. See the dedicated DEPTH CONVENTION note above + ENGINE_PRINCIPLES.md. (Fixed 4 backwards GI/RT sites this slot.)
- ReSTIR-GI temporal (
restir_gi_temporal.wgsl) D2/D3 canon corrections (Ouyang 2021 §4.2/§4.3): D3 = disocclusion reset clamped_prev_M = 0.0 (NOT a phantom 1.0; the init M=1 sample is the fresh survivor). D2 = depth-AND-normal biased-variant gate: 5% relative view-Z disocclusion (view_z_at_temporal, Schied 2017 SVGF §4.1) reading gbuffer_depth at prev_coord (no new binding) + forward-Z sky reject + unchanged normal cos25°. Initial pass stores x_v view-Z in res_d.g (forward-prep for D1; written-but-unread until D1 lands).
- D1 (periodic sample-validation re-trace, Ouyang §4.3) is NOT implemented — stochastic clear remains, a logged divergence. Clean canon impl needs a dedicated
Pass::ReSTIRGiValidate (the split initial/temporal passes don't share the SDF march helpers; 4-group limit saturated in the initial pass). Design note: KB restir-gi-temporal-canon-d2-d3.md. SCHEDULED (POST_WORK/TODO), not kept-as-fine.
- One-accumulator rule (reservoir M-cap vs SVGF temporal EMA) = DIVERGE, deferred to slot 11 (Denoising) — the production RELAX-with-ReSTIR pattern (A-SVGF λ = the anti-lag) is the canon-sanctioned target; slot 11 owns the collapse decision.
- Multi-light at-rest FLICKER = deferred to slot 11. TAA jitter is INVOLVED (proven:
--taa false 15→3); the precise mechanism is UNPROVEN (the 2×2 half-res-anchor fix that would validate it was byte-identical; ReGIR-cell-merge hypothesis REFUTED). REJECTED on canon grounds: unjitter the G-buffer (breaks TAA — taa.md:301 jitter firewall). Candidate (unproven): edge-triggered full-res refinement. Do NOT record the mechanism as confirmed ([[feedback_bug_cause_unproven_until_fix_improves]]).
- Mid-tier canon campaign — Slot 9 Volumetrics (2026-06-24/25). Audit
docs/design/mid_tier_canon_audit.md §9; specs 2026-06-24-froxel-volumetric-fog-design.md + 2026-06-25-atmosphere-lut-design.md + 2026-06-25-aerial-perspective-design.md. ALL LANDED + energy-validated. Commits: 9.A 646a49a8/204c62b9/e4376c0b; 9.B b3df50cf/f361b7ff/6237a9fe/273d8a23/b49e3755/f1e3068a; 9.C cf715e38/a56b4d42. LOAD-BEARING (do NOT re-break):
- 9.A froxel fog:
Pass::FogInject(per-frame compute, 160×90×{64/128} fixed grid, exp-Z, HG travel-dir phase, CSM sun + clustered punctual + SH-ambient inject, ping-pong scatter + 5% temporal) → FogIntegrate(Hillaire analytic in-slice integral, front-to-back) → FogApply(fragment {One,SrcAlpha,Add} = scene·T+L into RADIANCE_HDR, AFTER Transparent). Resolve the INTEGRATED grid, not injected. Travel-dir cosθ for HG (deferred_lighting.wgsl:189 l=normalize(-light.direction)).
- 9.B PB atmosphere (Hillaire 2020, USER chose 4-LUT dual-scattering, FULL scope): renderer-side regen order T→MS→Sky-View→cube→SH→prefilter (each reads the prior).
atmosphere_common.wgsl = Table-1 medium model VERBATIM. Pass::SkyViewDome = visible dome (REPLACE-blend fragment, depth>=1.0 gated, sun-aligned right=cross(up,sun_h) + √-lat UV, camera in DomeParams = vanilla WGSL no naga_oil) + sun disk E/Ω_sun·T. atmosphere_multiscatter.wgsl:229 f_ms_sum += contrib.f_ms * P_ISOTROPIC is CORRECT (paper Eq.7 + canon F6 carry p_u; furnace A/B bit-identical — do NOT "fix"). IBL (USER Option 2): EnvironmentLight::Procedural.sun_illuminance: Vec3 (sky brightness decoupled from the DirectionalLight lamp; Renderer3d.env_sun_direction/env_sun_illuminance). sky_from_skyview.wgsl (env-cube mip0) + sky_sh_project.wgsl (GPU SH-9 diffuse, replaces cpu_project_sh9) both sample the Sky-View LUT × env_sun_illuminance; the dome shares the same source. SH math = Ramamoorthi A_l(π,2π/3,π/4)+4π/N, raw-E convention (downstream /π); furnace = uniform L=1 → E=π. Below-horizon reads the REAL dim LUT rows via sign(lat) in BOTH cube+SH+dome — NOT clamp-to-horizon (over-brightens the IBL lower hemisphere ~10-30×). sky_sh_project workgroup = 64 threads (partials 9,216 B ≤ the 16,384 wgpu Limits::default() baseline; NOT the DX12-only 32,768). analytic sky.wgsl/cpu_project_sh9/sky_evaluate_cpu RETIRED.
- 9.C aerial perspective:
Pass::AerialPerspectiveInject (per-frame compute — view-dependent volume, MUST update on camera motion; 32×32×32 RGBA16Float, RGB=L_ap/A=mean-T_ap; one thread/(ix,iy) column reconstructs the live view ray, front-to-back ONE analytic step per Z-slice over LINEAR [0,ap_far_km]; march VERBATIM the sky-view kernel × env_sun_illuminance; near-gated) → Pass::AerialPerspectiveApply (fullscreen {One,SrcAlpha,Add} = scene·T_ap+L_ap, AFTER FogApply/BEFORE Taa; sky-gate depth>=1.0 = no-op = dome owns sky; near-gate; linear AP-Z t=vz/ap_far_m). The step count IS the volume depth AP_DIM_Z (no separate ap_steps knob). Knobs ap_volume_dim/ap_far_km/ap_near_gate_m.
- Method lessons: CPU-port the exact shader math for any energy/colour claim (tonemap+fog+exposure confound screenshots);
BenchSun ships intensity: 0.0 (temp-bump for sky/IBL furnaces); diagnostics were STALE 12× this slot (fresh cargo check the only verdict). Deferred (POST_WORK, user PR gate): perceptual A/Bs (fog, PB-IBL, AP km-vista) + the PB-IBL exposure re-tune.
- Mid-tier canon campaign — Slot 8 Reflections (2026-06-24, commits
2b518238 8.A.1, 52a4da96 8.A.2, a717ef40 8.C.1, 323837f5+d4704dde 8.B, ceb0d652 8.D, 5a24471b 8.C.2-A, 6241d6c3 8.C.2-B/C). Whole-slot canon review ✅ spec-compliant + energy-consistent. Canon 4-way reflection chain, audit docs/design/mid_tier_canon_audit.md §8. LOAD-BEARING (do NOT re-break):
Pass::ReflectionComposite is the SOLE owner of the specular-reflection term + is a FRAGMENT additive-blend pass (ColorTargetState blend One/One/Add, LoadOp::Load into RADIANCE_HDR). deferred_lighting.wgsl + _compute.wgsl do NOT compute ibl_specular (only ibl_diffuse·occlusion). A COMPUTE read-write of RADIANCE_HDR is IMPOSSIBLE: Rgba16Float read-write storage is not wgpu-baseline AND sampled+STORAGE_WRITE_ONLY on the same texture is an exclusive-usage conflict — use a fragment additive-blend pass (like Pass::Transparent). naga_oil-composed with the normal frame_bgl (fragment) so Frame::/Brdf::/GBuffer:: give byte-identical IBL.
- 4-way fallback chain, priority-arbitrated (NEVER summed):
ssr.a>0 (confidence-blend) → rt.a>0 (binary) → (REFLECT_PROBE_ENABLED && roughness≥REFLECT_PROBE_MIN_ROUGHNESS=0.3 → probe E_R/π) → IBL floor, each L_refl × fss_ess × occlusion. The four L_refl are mutually-consistent RADIANCE: SSR scene-HDR, RT (APPROX_ALBEDO/π)·(L_bounce+L_nee), probe E_R/π (irradiance→radiance, NO albedo — env reflection not a diffuse bounce), floor prefiltered_env(R). White-furnace: unit env → probe E_R=π → E_R/π=1 = prefiltered_env(R). Byte-identity on all-miss = old (ibl_diffuse+ibl_specular)·occ.
- SSR owns
reflection_radiance; RT owns a SEPARATE rt_reflection buffer (writing RT into the shared buffer after SSR CLOBBERS SSR's on-screen hits). RT = off-screen solver (frustum-exit-gated, perf only), SSR = on-screen, composite arbitrates. RT miss/SSR miss = 0/no-sky (env owned only by the floor).
- Probe atlas stores cosine-weighted irradiance E (albedo-free, no ÷π); receivers apply the Lambertian
(albedo/π)·E (Majercik DDGI). The probe GATHER was the lone bug (albedo·E, π× too bright; fixed 8.C.2-A). The probe reflection (c) rung at the PRIMARY pixel uses E_R/π (NO albedo — reflection).
- Tier knobs (8.D): 6 RANGE consts (ssr/glossy/rt max_steps, distances, z_thickness, glossy preblur) are WGSL
override from RenderQuality *_resolved() via PipelineCompilationOptions::constants. SSR_SHARP_MAX_ROUGHNESS=0.25 (sharp↔glossy split) + SSR_STRIDE/LOBE_BIAS/FRUSTUM_MARGIN/SDF-march stay fixed const, byte-identical ×4 shaders. Reflections Mid/High only (Low/Potato REFLECTION_TIER=0 = floor only).
- Debug:
TITAN_STRATUM_REFLECT_BACKEND=software|hardware (reflect ray backend → GI backend), TITAN_STRATUM_REFLECT_PROBE=0|1 (toggle (c) rung), TITAN_STRATUM_REFLECT_DEBUG_SOURCE=1|2|3|4 (isolate SSR/RT/probe/floor; debug-dumps only, const-folds to 0 in prod). LESSONS: narrow storage formats via the vendored patch ([[feedback_narrow_storage_formats_vendored_patch]]); re-check git log after every dispatch; smoke is the ONLY WGSL/real-adapter validator; bare-fmt the agents.
- Mid-tier canon campaign — W6.0 RT acceleration-structure foundation +
ray-tracing default-ON (2026-06-24, commits 8c17bd52→b93cb552 T1-T5, f7539945 T6, 8bf189ab T7, d0c3b133 flip+buddy-fixes, 9aa464e4 gate fix). The HW ray-tracing path now runs end-to-end. LOAD-BEARING (do NOT re-break):
ray-tracing is a DEFAULT feature of titan-rendering-3d. Device creation (Renderer3d::new) requests EXPERIMENTAL_RAY_QUERY + experimental_features (via titan_rendering_rt::experimental_features_enabled() — workspace forbids unsafe in titan-rendering-3d) + AS limits (Limits::using_minimum_supported_acceleration_structure_values()) ADAPTER-CONDITIONALLY (rt_requested = adapter.features().contains(EXPERIMENTAL_RAY_QUERY)). Non-RT GPUs boot + run the software path (GiRayBackend::Auto → software when rt_available false). NEVER make EXPERIMENTAL_RAY_QUERY an unconditional required_features — that bricks boot on non-RT hardware.
- An end-to-end RT smoke is MANDATORY for RT device/limit/BGL changes:
cargo check + unit tests build their own correctly-configured devices, so they CANNOT catch a missing experimental_features opt-in, zero AS limits, or a BGL/bind-group count mismatch. The flip surfaced all three only at editor boot. Smoke: CARGO_FEATURES=ray-tracing bash scripts/smoke-editor.sh; A/B TITAN_STRATUM_GI_BACKEND=software|hardware (debug-dumps) via the bench.
- HW BGLs bake in a TLAS binding at construction → a TLAS must be bound EVERY frame.
frame_tlas is None with no casters, so a persistent zero-instance empty TLAS (Renderer3d.empty_tlas, built once) is the per-frame binding fallback (tlas_for_ctx = frame_tlas.or(empty_tlas)); it traces all-miss = correct. Inline it as two disjoint field borrows at the PassCtx sites (NOT a &self method — borrow conflict with stratum_hook.as_mut()).
- Per-mesh BLAS keyed by a stable
rt_id: AtomicU64 on GpuMesh/GpuSkinnedDraw (upload paths are &self, so a handle can't live on GpuMesh); renderer-side maps rt_static_blas/rt_skinned built lazily in render(). These maps + BlasPool.blases are insert-only — a known leak across streaming (OPEN_BUGS; add eviction on unload). Skinned BLAS is fully REBUILT each frame, NOT refit — the vendored wgpu-core implements only Build (BlasBuildEntry has no update_mode); ALLOW_UPDATE is forward-compat only. Frame schedule: skinned-pose → compute-skin (skinned_skin_positions.wgsl, raw-f32 stride-18 decode) → ONE build_acceleration_structures (static first-build + skinned rebuild + TLAS) → traces.