Skip to main content

concinnity_core/render/uniforms/
probe.rs

1//! The reflection-probe set the forward, SSR and ray-traced resolves all read.
2//! Matches `ProbeUniforms` / `ProbeSet` in `shaders/probe_types.slang`, whose
3//! `MAX_PROBES` is baked in from the constant below.
4
5/// Maximum reflection probes a frame can bind. The shader's `MAX_PROBES` define
6/// is injected from this value, so the two cannot drift.
7pub const MAX_PROBES: usize = 8;
8
9// Every automatically seeded probe has to fit in the bound set.
10const _: () = assert!(crate::render::reflection_probe::AUTO_SEED_BUDGET <= MAX_PROBES);
11
12/// One reflection probe's parallax box. The specular IBL term box-projects the
13/// reflection vector against [box_min, box_max] (the probe's influence volume)
14/// and re-anchors the cube sample at the box hit relative to `probe_pos` (the
15/// capture point), so a static captured cube tracks a moving first-person
16/// camera. Three float4s keep every field 16-byte aligned. `box_min.w` is the
17/// enabled flag: 0 disables parallax (and signals no baked probe), so the shader
18/// samples the raw reflection vector.
19#[derive(Copy, Clone, bytemuck::Zeroable, bytemuck::Pod)]
20#[repr(C)]
21pub struct ProbeUniforms {
22    /// xyz = influence-box min; w = enabled (1.0 = parallax on, 0.0 = off).
23    pub box_min: [f32; 4],
24    /// xyz = influence-box max; w unused.
25    pub box_max: [f32; 4],
26    /// xyz = probe capture position; w unused.
27    pub probe_pos: [f32; 4],
28}
29
30impl ProbeUniforms {
31    /// The "no probe" value: parallax disabled, so the shader samples the raw
32    /// reflection vector (which, with the probe cube slot aliasing the sky until
33    /// a bake, reproduces the pre-probe reflection exactly).
34    pub const DISABLED: ProbeUniforms = ProbeUniforms {
35        box_min: [0.0; 4],
36        box_max: [0.0; 4],
37        probe_pos: [0.0; 4],
38    };
39}
40
41/// The full set of reflection probes. `count` is how many of `probes` are live;
42/// the fragment shader blends every probe whose influence box covers the surface
43/// (a partition-of-unity weight by signed box distance), falling back to the
44/// nearest when the surface is outside all boxes, and samples those slices of
45/// the probe cube array. Slices beyond `count` hold the sky fallback cube and a
46/// `DISABLED` box.
47#[derive(Copy, Clone, bytemuck::NoUninit)]
48#[repr(C)]
49pub struct ProbeSet {
50    /// Live entries in `probes`.
51    pub count: u32,
52    /// Padding so the field layout matches the shader-side struct.
53    /// Padding so the field layout matches the shader-side struct.
54    pub _pad: [u32; 3],
55    /// Probe entries; the first `count` are live.
56    pub probes: [ProbeUniforms; MAX_PROBES],
57}
58
59impl ProbeSet {
60    /// An empty set: no probes, so the shader falls back to the sky.
61    pub const EMPTY: ProbeSet = ProbeSet {
62        count: 0,
63        _pad: [0; 3],
64        probes: [ProbeUniforms::DISABLED; MAX_PROBES],
65    };
66}
67
68/// Per-dispatch params for the runtime reflection-probe prefilter kernels.
69/// Matches `ProbePrefilterParams` in `shaders/probe_prefilter.slang`. 32 bytes.
70///
71/// Built by [`crate::render::reflection_probe::PrefilterPlan`], which is what
72/// decides the sizes, the roughness per mip and the firefly clamp so the three
73/// backends dispatch identical work.
74#[derive(Copy, Clone, bytemuck::NoUninit)]
75#[repr(C)]
76pub struct ProbePrefilterParams {
77    /// Destination cube-face edge in texels.
78    pub dst_size: u32,
79    /// Source cube-face edge at mip 0, in texels.
80    pub src_size: u32,
81    /// GGX samples per output texel.
82    pub sample_count: u32,
83    /// Source mip the downsample kernel reduces; it writes `src_mip + 1`.
84    pub src_mip: u32,
85    /// GGX roughness of the destination mip.
86    pub roughness: f32,
87    /// Firefly clamp luminance; `<= 0` disables the cap.
88    pub clamp_lum: f32,
89    /// Mip levels the source pyramid has, bounding the solid-angle lod.
90    pub src_mip_count: f32,
91    /// Padding so the field layout matches the shader-side struct.
92    pub _pad: f32,
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use core::mem::{offset_of, size_of};
99
100    // Eight tightly-packed 4-byte scalars, the layout
101    // `ProbePrefilterParams` in `shaders/probe_prefilter.slang` declares. Every
102    // field is a scalar, so no target 16-aligns one and shifts the rest; a
103    // vector added here would, and would silently feed each kernel garbage.
104    #[test]
105    fn probe_prefilter_params_layout_matches_the_shader() {
106        assert_eq!(size_of::<ProbePrefilterParams>(), 32);
107        assert_eq!(offset_of!(ProbePrefilterParams, dst_size), 0);
108        assert_eq!(offset_of!(ProbePrefilterParams, src_size), 4);
109        assert_eq!(offset_of!(ProbePrefilterParams, sample_count), 8);
110        assert_eq!(offset_of!(ProbePrefilterParams, src_mip), 12);
111        assert_eq!(offset_of!(ProbePrefilterParams, roughness), 16);
112        assert_eq!(offset_of!(ProbePrefilterParams, clamp_lum), 20);
113        assert_eq!(offset_of!(ProbePrefilterParams, src_mip_count), 24);
114        assert_eq!(offset_of!(ProbePrefilterParams, _pad), 28);
115    }
116}