Skip to main content

concinnity_render/render_graph/
passes.rs

1// src/render_graph/passes.rs
2//
3// Stable identity for every render-graph pass. Used by:
4//
5//   - The graph itself, as the dispatch key the executor matches on.
6//   - The per-pass GPU timer (`crate::pass_timing`), which keys its
7//     sample-buffer slots off the same integer.
8//
9// The `pass_ids!` invocation below is the single registration point: one line
10// per pass names the variant and its stable timing name, and the macro derives
11// the enum, [`PASS_NAMES`], [`PASS_COUNT`], and [`PassId::ALL`] from it. A pass
12// therefore cannot exist without a timing name (which would otherwise report
13// zero GPU time), and the name table cannot drift out of index order.
14//
15// Variants are `#[repr(u32)]` so a `PassId` round-trips through `as usize` into
16// [`PASS_NAMES`] and any `[T; PASS_COUNT]` companion array. The list is
17// append-only: inserting in the middle renumbers later variants and silently
18// shifts every timing slot.
19
20/// Declare the pass vocabulary. Each entry is `Variant => "timing_name"`.
21macro_rules! pass_ids {
22    ($($(#[$doc:meta])* $variant:ident => $name:literal,)*) => {
23        /// One per-pass identity. Cast to `usize` to index [`PASS_NAMES`] or any
24        /// `[T; PASS_COUNT]` companion array.
25        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26        #[repr(u32)]
27        pub enum PassId {
28            $($(#[$doc])* $variant,)*
29        }
30
31        /// Stable display name for each pass. Index = `PassId as usize`. Used by
32        /// the WS `profile.passes` reply and the per-pass timing readback.
33        pub const PASS_NAMES: [&str; PASS_COUNT] = [$($name,)*];
34
35        /// Number of distinct passes the engine times. The per-pass timing array
36        /// in [`crate::profile::RenderStats`] is sized to at least this many slots.
37        pub const PASS_COUNT: usize = [$(PassId::$variant,)*].len();
38
39        impl PassId {
40            /// Every variant, in declaration (index) order.
41            pub const ALL: [PassId; PASS_COUNT] = [$(PassId::$variant,)*];
42        }
43    };
44}
45
46pass_ids! {
47    /// GPU visibility cull; produces the indirect draw arguments.
48    Cull => "cull",
49    /// Directional shadow cascades and spot shadow slices.
50    Shadow => "shadow",
51    /// Depth / normal prepass feeding screen-space reflections.
52    SsrPrepass => "ssr_prepass",
53    /// Depth / normal prepass feeding ambient occlusion.
54    SsaoPrepass => "ssao_prepass",
55    /// The ambient-occlusion gather.
56    SsaoKernel => "ssao_kernel",
57    /// Bilateral blur over the occlusion buffer.
58    SsaoBlur => "ssao_blur",
59    /// The lit forward pass.
60    Main => "main",
61    /// Luminance reduction driving auto-exposure.
62    AutoExposure => "auto_exposure",
63    /// Projected decals.
64    Decals => "decals",
65    /// Volumetric fog composite.
66    Fog => "fog",
67    /// Particle simulation compute.
68    ParticlesSim => "particles_sim",
69    /// Particle draw.
70    ParticlesDraw => "particles_draw",
71    /// Reflection trace and composite.
72    SsrResolve => "ssr_resolve",
73    /// Screen-space velocity, for TAA and motion blur.
74    Velocity => "velocity",
75    /// Temporal anti-aliasing resolve.
76    TaaResolve => "taa_resolve",
77    /// Bloom down/upsample chain.
78    Bloom => "bloom",
79    /// Tonemap, grade, and present.
80    Composite => "composite",
81    /// Volumetric-fog froxel-volume compute pass. Populates a 3D
82    /// `(scattered, transmittance)` texture once per frame, sampled by the
83    /// fullscreen `Fog` render pass instead of an inline ray-march. Every
84    /// backend implements the path; the `Fog` pass trilinear-samples the
85    /// volume by (screen_uv, view_z).
86    FogFroxel => "fog_froxel",
87    /// Temporal upscaling pass. When the world's `PostProcessConfig`
88    /// enables `temporal_upscaling`, the renderer draws the 3D scene at a
89    /// fraction of drawable size and inserts this pass between the post-SSR
90    /// scene and the Bloom + Composite stack. The backend runs its
91    /// platform-native temporal upscaler (MetalFX on macOS; FSR / DLSS /
92    /// XeSS slots on the Windows backends are placeholders today) to
93    /// reconstruct a drawable-resolution image. Replaces `TaaResolve`:
94    /// the upscaler does temporal accumulation itself, so adding both
95    /// would double-temporal the scene.
96    Upscale => "upscale",
97    /// Transparent / translucent geometry pass. Runs after `SsrResolve`
98    /// (so water + glass see opaque reflections) and before
99    /// `TaaResolve` / `Upscale` (so translucents pick up temporal
100    /// accumulation). Reads the latest scene-pre-taa colour + main
101    /// depth as sampled textures; writes scene-pre-taa blended
102    /// (SRC_ALPHA / ONE_MINUS_SRC_ALPHA). Each transparent draw owns
103    /// its own pipeline + descriptor set; the pass aggregates them as
104    /// a back-to-front sorted list at encode time. Gated on
105    /// `FrameGraphInputs::transparent_enabled`; when no consumer is
106    /// in the world, the slot is omitted entirely.
107    Transparent => "transparent",
108    /// Raymarched SDF volume pass. Rasterises the back faces of each
109    /// `SdfVolume`'s world-space bounding box and runs the user-authored
110    /// fragment shader, which sphere-traces a signed distance field
111    /// inside the box. Hit fragments write opaque colour into
112    /// `hdr_resolve` (RMW between `AutoExposure` and `Decals`) and
113    /// update the main depth attachment so the raymarched surface
114    /// composites with rasterised geometry naturally: decals, fog,
115    /// SSR-resolve, and TAA all consume the post-Raymarch depth and
116    /// colour. Gated on `FrameGraphInputs::raymarch_enabled`; when no
117    /// `SdfVolume` is in the world the slot is omitted entirely.
118    Raymarch => "raymarch",
119    /// Mid-frame Hi-Z (depth-mip pyramid) rebuild for two-pass occlusion
120    /// culling. Inserted only when `FrameGraphInputs::two_pass_occlusion_enabled`
121    /// is on: after `Main` (phase 1) has written this frame's depth, this
122    /// compute pass reduces it into the Hi-Z pyramid so `Cull2` can re-test
123    /// the objects phase 1 occluded against up-to-date depth. Distinct from
124    /// the end-of-frame Hi-Z build (which feeds the *next* frame's phase-1
125    /// cull and stays an inline action, not a graph node). Every backend
126    /// implements the node; whether it appears is `two_pass_occlusion_enabled`,
127    /// which each seeds from its own two-pass state.
128    HizBuild => "hiz_build",
129    /// Phase-2 GPU cull for two-pass occlusion. Re-tests the objects `Cull`
130    /// (phase 1) marked Hi-Z-occluded against the freshly rebuilt pyramid
131    /// (`HizBuild`) and encodes a draw for any that turn out visible into a
132    /// second indirect command buffer `Main2` consumes. Reads the per-object
133    /// status buffer phase-1 cull wrote + the `draw_args2` buffer it writes.
134    /// Gated on `FrameGraphInputs::two_pass_occlusion_enabled`.
135    Cull2 => "cull2",
136    /// Phase-2 main pass for two-pass occlusion. Loads (does not clear) the
137    /// HDR colour + depth `Main` wrote and re-runs only the bindless-static
138    /// indirect draw through `Cull2`'s command buffer, depth-compositing the
139    /// disoccluded geometry with phase 1. Instanced + skinned geometry is not
140    /// Hi-Z-culled, so it is fully drawn in phase 1 and not repeated here.
141    /// Becomes the new head of the hdr_resolve post-decoration chain (so
142    /// AutoExposure / Decals / Fog / SSR see the combined result). Gated on
143    /// `FrameGraphInputs::two_pass_occlusion_enabled`.
144    Main2 => "main2",
145    /// Screen-space global illumination. A refinement of SSR: it reuses the
146    /// SSR depth + normal pre-pass G-buffer and screen-space ray-march, but
147    /// integrates bounced radiance over a cosine-weighted hemisphere instead
148    /// of along one reflection vector. Sits on the hdr_resolve RMW chain (after
149    /// `Raymarch`, before `Decals`): it reads the lit scene as the bounce
150    /// radiance source and additively composites the gathered + denoised
151    /// indirect term back into it, so the near-field colour bleed layers on top
152    /// of the IBL ambient. Gated on `FrameGraphInputs::ssgi_enabled`; when
153    /// `indirect_lighting` is IBL-only the slot is omitted entirely.
154    Ssgi => "ssgi",
155    /// Hardware ray-traced reflections. Occupies the same scene-pre-taa slot as
156    /// `SsrResolve` (reads the post-decoration `hdr_resolve`, writes
157    /// `scene_pre_taa`) and takes precedence over it: when this pass is live the
158    /// builder inserts it and omits `SsrResolve` (a world may author both; RT
159    /// runs where available, SSR is the fallback). It still relies on the SSR
160    /// depth + normal + roughness
161    /// pre-pass (so `SsrPrepass` is forced on), but instead of a screen-space
162    /// march it traces a world-space reflection ray against an acceleration
163    /// structure built over the static scene geometry, so off-screen reflected
164    /// geometry appears. Gated on `FrameGraphInputs::rt_reflections_enabled`,
165    /// and so only on GPUs that report ray-tracing support.
166    RtReflections => "rt_reflections",
167    /// Unified geometry G-buffer pre-pass. One jittered traversal of the visible
168    /// set writes view-space normal + linear depth, perceptual roughness, and
169    /// screen-space motion into a single MRT (plus a sampleable depth), replacing
170    /// the separate `SsrPrepass` + `Velocity` (and the SSAO-owned prepass): every
171    /// consumer (SSR, SSAO, SSGI, RT, TAA, upscaler) reads this one output. Gated
172    /// on `FrameGraphInputs::unified_gbuffer_prepass`.
173    GBufferPrepass => "gbuffer_prepass",
174    /// Roughness-aware reflection composite. Not a standalone graph node: it is
175    /// encoded inline at the tail of the `SsrResolve` / `RtReflections` pass
176    /// (both write a reflection target, then blur it by roughness and composite
177    /// it over the scene). It carries its own timing slot so its cost is visible
178    /// separately from the trace/march that precedes it. Inline on every
179    /// backend, so no backend's graph executor ever dispatches this id: each
180    /// treats it as a programming error the way it treats the bundled SSAO
181    /// sub-passes.
182    ReflectionComposite => "reflection_composite",
183    /// Clustered light-binning compute pass. Once per frame, before Main: bins the
184    /// scene's local lights (the GpuLight buffer) into a per-cluster index list
185    /// over a screen-tiled, exponential-depth froxel grid, which the forward pass
186    /// reads to shade each fragment from only its cluster's lights instead of
187    /// iterating every light. Runs when the world has local lights. Writes a
188    /// storage buffer Main reads (RAW edge).
189    LightCull => "light_cull",
190    /// Depth-only render of each shadowed spot light's cone into one slice of the
191    /// spot shadow map array. Local lights are static, so the projections are
192    /// built once; only the depth contents refresh, one slice per frame under
193    /// `ShadowUpdate::Hybrid`. Runs when the world has a shadow-casting spot.
194    SpotShadow => "spot_shadow",
195    /// World-space line geometry (trajectories, tethers, path previews, the
196    /// editor's origin axes). Blend-writes the resolved scene colour after the
197    /// world decorations, sampling the resolved scene depth so a line behind
198    /// geometry is occluded by it. Gated on `FrameGraphInputs::lines_enabled`:
199    /// a frame that submits no lines omits the node entirely, so a frame that
200    /// draws none never pays for it.
201    Lines => "lines",
202    /// Terminal Hi-Z (depth-mip pyramid) build. Reduces the frame's final main
203    /// depth into the pyramid the *next* frame's phase-1 `Cull` tests against,
204    /// so it is declared last and reads the depth every decoration pass has
205    /// finished with. Distinct from `HizBuild`, which rebuilds the same pyramid
206    /// mid-frame from phase-1 depth for `Cull2`; when two-pass occlusion is on
207    /// both run and this one supersedes it for the next frame. Present whenever
208    /// the GPU-cull path built a pyramid (`FrameGraphInputs::hiz_build_enabled`).
209    HizFinal => "hiz_final",
210}
211
212impl PassId {
213    /// Stable display name, looked up in [`PASS_NAMES`]. `'static` since
214    /// the table is `const`.
215    pub fn name(self) -> &'static str {
216        PASS_NAMES[self as usize]
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn every_pass_id_indexes_its_own_name() {
226        // The macro pairs each variant with its name, so this asserts the
227        // derivation rather than a hand-maintained mirror: indices stay dense
228        // and in declaration order, and no pass ships nameless.
229        for (i, &pass) in PassId::ALL.iter().enumerate() {
230            assert_eq!(pass as usize, i, "{pass:?} index out of order");
231            assert_eq!(pass.name(), PASS_NAMES[i], "{pass:?} name table mismatch");
232            assert!(!pass.name().is_empty(), "{pass:?} has an empty name");
233        }
234        assert_eq!(PASS_NAMES.len(), PASS_COUNT);
235    }
236
237    #[test]
238    fn pass_names_are_unique() {
239        // A copy-pasted name would silently merge two passes in the profiler.
240        let mut seen = hashbrown::HashSet::new();
241        for &name in PASS_NAMES.iter() {
242            assert!(seen.insert(name), "duplicate pass name {name:?}");
243        }
244    }
245}