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
.wgsl file per sub-pass in crates/titan-stratum-lighting/src/shaders/
- Self-contained — no imports (see
wgsl-no-imports skill). Inline shared
helpers (edge-stop weights, pack/unpack, luminance) in each shader.
- Keep a
_common.wgsl reference 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_bgl fields
fn new(device) -> Self that 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: bool and 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 (one dv() 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
Pass enum in crates/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
StratumRenderHook impl in
crates/titan-stratum-lighting/src/render_hook.rs
- Add the dispatcher as an
Option<MyDispatcher> field on StratumDispatch
- Create lazily with
get_or_insert_with on first frame
- Set
None in the constructor literal
8. Quality knobs
- Add fields to
RenderQuality in crates/titan-project/src/quality.rs
with #[serde(default = "...")] attributes
- Resolve into a config struct in
StratumQualityConfig via
from_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],
}
Correct:
struct TemporalUbo {
jitter_delta: [f32; 2],
_pad: [f32; 2],
}
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) {
}
This allows the module to be checked in and the Pass enum variants to exist
in the RON without runtime panics.