name: clean-room-dispatch-implementation description: Workflow for implementing a Tier-2 rendering technique in Titan: read the clean-room reference doc, write self-contained WGSL shaders with inlined helpers, follow the existing pass pattern (preblur.rs template), create pipeline structs + constructors + dispatch methods, wire into the frame-graph RON and StratumRenderHook trait source: auto-skill extracted_at: '2026-06-15T04:29:18.101Z'
Clean-room technique implementation workflow
When implementing a Tier-2 rendering technique (ReBLUR, ReSTIR PT, etc.) from a clean-room reference doc in Titan:
1. Read the reference doc
The reference lives in docs/design/ (e.g.
stratum_lighting_high_tier_denoiser_reference.md). It contains the
algorithm breakdown, per-pass formulas, data structure inventory, and knob
defaults — all restated from public papers/slides.
2. Write WGSL shaders
- One
.wgslfile per sub-pass incrates/titan-stratum-lighting/src/shaders/ - Self-contained — no imports (see
wgsl-no-importsskill). Inline shared helpers (edge-stop weights, pack/unpack, luminance) in each shader. - Keep a
_common.wgslreference file that is never compiled, serving as the canonical formula source. - WGSL structs mirror the Rust UBO structs. Use
@group(0)for IO bindings,@group(1)for uniform params.
3. Create host-side Rust module
In crates/titan-stratum-lighting/src/, follow the preblur.rs pattern
(see titan-compute-pipeline-pattern skill for exact pipeline creation):
- One struct per pass with
pipeline,io_bgl,param_bglfields fn new(device) -> Selfthat compiles the shader and creates BGLs- UBO structs with
#[repr(C)]+bytemuck::Pod + Zeroable - A top-level dispatcher struct that owns all passes, history textures, scratch textures, and per-pass UBO buffers
4. History texture management
For temporal techniques needing ping-pong history:
- Store 14 persistent textures per channel (slow, fast, view_z, nr, packed, moments, stabilization × 2 sides each)
- Use
create_history_texture(device, label, format, resolution)helper - Provide
ping: booland accessor pairs (slow_curr()/slow_prev()) that select the correct side - Call
history.swap()at end of each frame's dispatch
5. Dispatch method
The dispatch_diffuse (or equivalent) method:
- Creates
TextureViews for all scratch + history textures (onedv()closure) - For each sub-pass in order: write UBO via
queue.write_buffer, create bind groups (io + param), begin compute pass, set pipeline + bind groups, dispatch workgroups at 8×8, drop pass - Call
history.swap()after all sub-passes
6. Frame graph wiring
- Add the pass variant to
Passenum incrates/titan-rendering-frame-graph/src/lib.rs - Add match arms at the renderer dispatch site in
crates/titan-rendering-3d/src/lib.rs - Update the RON configs in
crates/titan-rendering-frame-graph/configs/ - Flip any feature gate constants (e.g.
HIGH_TIER_DENOISER_IMPL_READY)
7. Hook wiring
- Override the trait method on
StratumRenderHookimpl incrates/titan-stratum-lighting/src/render_hook.rs - Add the dispatcher as an
Option<MyDispatcher>field onStratumDispatch - Create lazily with
get_or_insert_withon first frame - Set
Nonein the constructor literal
8. Quality knobs
- Add fields to
RenderQualityincrates/titan-project/src/quality.rswith#[serde(default = "...")]attributes - Resolve into a config struct in
StratumQualityConfigviafrom_render_quality - Wire into the dispatcher's
new()and dispatch methods
Reference implementation
The ReBLUR denoiser in crates/titan-stratum-lighting/src/reblur.rs is the
canonical example of this workflow applied end-to-end.
Common review gotchas
These mistakes were caught during code review of the ReBLUR implementation and are easy to repeat when wiring any new denoiser chain.
Wrong radiance input — using G-buffer albedo instead of shading output
The denoiser reads lit radiance (post-shading HDR output), not unlit
base color. Always pass the shading output texture (e.g. diffuse_raw_view
from StratumDispatch), never gbuffer_albedo_view. The G-buffer albedo
contains only material base color — the denoiser would filter material
properties instead of lighting.
Correct: &self.diffuse_raw_view — the storage texture StratumShading
writes diffuse radiance into.
Output not reaching the frame graph — final pass must write to radiance_raw
Every pass in the chain writes to private scratch or history textures by
default. The LAST pass in the chain (stabilization in ReBLUR) must write
its output to the frame graph's shared output slot (radiance_raw_view)
so that DeferredLighting (which is downstream in the RON) reads the
denoised result.
Fix: Pass radiance_out: &wgpu::TextureView as a parameter and use it
as the out_stabilised storage texture binding for the final pass.
TemporalUbo — Rust struct must match WGSL vec2 alignment
When the WGSL shader declares jitter_delta: vec2<f32>, the Rust UBO
struct must use jitter_delta: [f32; 2] (not _pad: [f32; 4]). A
vec2<f32> in WGSL is 8 bytes with 8-byte alignment; the Rust struct
needs to place it at the correct offset with matching size.
Incorrect:
struct TemporalUbo {
// ...
_pad: [f32; 4], // WGSL reads vec2<f32> here — jitter_delta lost
}
Correct:
struct TemporalUbo {
// ...
jitter_delta: [f32; 2],
_pad: [f32; 2], // alignment tail
}
todo!() panics in checked-in code
Stub modules with todo!() compile but panic at runtime if the dispatch
arm is ever wired before the implementation. Use no-op bodies instead:
pub fn dispatch(&self, _encoder: &mut wgpu::CommandEncoder, _resolution: glam::UVec2) {
// TODO: implement
}
This allows the module to be checked in and the Pass enum variants to exist in the RON without runtime panics.