Skip to main content

concinnity_core/render/
hiz_spd.rs

1//! Dispatch geometry for the single-pass Hi-Z downsampler.
2//!
3//! One workgroup of the `hiz_spd_*` kernels reduces a [`TILE`]x[`TILE`] tile of
4//! its base level through [`LEVELS`] levels, so a pyramid is built in two
5//! dispatches instead of one per mip: phase 1 reads the main depth and writes
6//! mips 0..5, and the tail continues from mip 5 to write mips 6..10. Vulkan and
7//! DirectX share this plan; Metal still runs the per-mip chain.
8
9use crate::render::uniforms::HizSpdParams;
10
11/// Levels one SPD dispatch produces, counting its base level.
12pub const LEVELS: u32 = 6;
13
14/// Base-level texels one workgroup reduces, per axis.
15pub const TILE: u32 = 32;
16
17/// Deepest pyramid two dispatches can produce: phase 1 covers levels 0..5 and
18/// the tail adds another five on top of the mip 5 it starts from.
19pub const MAX_MIPS: u32 = LEVELS + LEVELS - 1;
20
21/// One dispatch of the plan.
22#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23pub struct Dispatch {
24    /// Pyramid mip this dispatch's base level is, and the first of the
25    /// [`LEVELS`] mips its descriptors bind.
26    pub base_mip: u32,
27    /// Push / root constants for the kernel.
28    pub params: HizSpdParams,
29    /// Workgroups to dispatch, X and Y.
30    pub groups: (u32, u32),
31}
32
33/// The dispatches that build a `mip_count`-deep pyramid over a `width` x
34/// `height` depth source.
35#[derive(Copy, Clone, Debug, PartialEq, Eq)]
36pub struct Plan {
37    /// Depth source to mips 0..6. Always present.
38    pub phase1: Dispatch,
39    /// Mip 6 to mips 7..12. Absent when the pyramid stops at mip 6 or shallower.
40    pub tail: Option<Dispatch>,
41}
42
43/// Size of mip `level` of a `base`-sized image, floored at 1.
44pub fn level_size(base: (u32, u32), level: u32) -> (u32, u32) {
45    ((base.0 >> level).max(1), (base.1 >> level).max(1))
46}
47
48impl Plan {
49    /// Plan the two dispatches. `mip_count` is clamped to [`MAX_MIPS`]; a
50    /// shallower pyramid than requested only costs the cull a coarser level to
51    /// pick from, which makes it more permissive rather than wrong.
52    pub fn new(width: u32, height: u32, mip_count: u32, sample_count: u32) -> Self {
53        let base = (width.max(1), height.max(1));
54        let mips = mip_count.clamp(1, MAX_MIPS);
55        let phase1 = Dispatch {
56            base_mip: 0,
57            params: HizSpdParams {
58                base_width: base.0,
59                base_height: base.1,
60                level_count: mips.min(LEVELS),
61                sample_count,
62            },
63            groups: (base.0.div_ceil(TILE), base.1.div_ceil(TILE)),
64        };
65        // The tail's own base is mip `LEVELS - 1`, which it reads and does not
66        // rewrite, so it earns its dispatch only once a deeper mip exists.
67        let tail = (mips > LEVELS).then(|| {
68            let tail_base = level_size(base, LEVELS - 1);
69            let groups = (tail_base.0.div_ceil(TILE), tail_base.1.div_ceil(TILE));
70            Dispatch {
71                base_mip: LEVELS - 1,
72                params: HizSpdParams {
73                    base_width: tail_base.0,
74                    base_height: tail_base.1,
75                    level_count: tail_level_count(tail_base, groups, mips - (LEVELS - 1)),
76                    sample_count,
77                },
78                groups,
79            }
80        });
81        Self { phase1, tail }
82    }
83
84    /// Pyramid depth this plan actually writes. The image and the cull's mip
85    /// count must both use it: a mip the plan skipped is never written, and
86    /// sampling one would feed the cull uninitialised memory.
87    pub fn mip_count(&self) -> u32 {
88        match self.tail {
89            Some(t) => t.base_mip + t.params.level_count,
90            None => self.phase1.params.level_count,
91        }
92    }
93
94    /// Mips the dispatch starting at `base_mip` binds, clamped to the pyramid's
95    /// actual depth. Descriptors past the end repeat the last live mip so the
96    /// array is fully populated; the kernel's `level_count` keeps it from
97    /// writing them.
98    pub fn bound_mips(base_mip: u32, mip_count: u32) -> impl Iterator<Item = u32> {
99        (0..LEVELS).map(move |i| (base_mip + i).min(mip_count.saturating_sub(1)))
100    }
101}
102
103// Levels the tail may write before its groups start colliding. A level with
104// fewer texts per axis than there are groups would have several groups target
105// the same texel, so the plan stops one level short instead; the pyramid ends
106// shallower, which only makes the cull more permissive.
107fn tail_level_count(base: (u32, u32), groups: (u32, u32), requested: u32) -> u32 {
108    let mut count = 1;
109    for level in 1..requested.min(LEVELS) {
110        let size = level_size(base, level);
111        if size.0 < groups.0 || size.1 < groups.1 {
112            break;
113        }
114        count = level + 1;
115    }
116    count
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    // 1024x768 is 11 mips, exactly what two dispatches reach: phase 1 takes
124    // 0..5 over a 32x24 grid, the tail takes 6..10 from the 32x24 mip 5 in a
125    // single workgroup.
126    #[test]
127    fn plan_at_1024x768() {
128        let p = Plan::new(1024, 768, 11, 1);
129        assert_eq!(p.phase1.groups, (32, 24));
130        assert_eq!(p.phase1.params.level_count, LEVELS);
131        let tail = p.tail.expect("11 mips needs a tail");
132        assert_eq!(tail.base_mip, LEVELS - 1);
133        assert_eq!((tail.params.base_width, tail.params.base_height), (32, 24));
134        assert_eq!(tail.params.level_count, 6);
135        assert_eq!(tail.groups, (1, 1));
136        assert_eq!(p.mip_count(), 11);
137    }
138
139    // A pyramid that stops inside phase 1's reach spends one dispatch.
140    #[test]
141    fn shallow_pyramid_has_no_tail() {
142        let p = Plan::new(32, 32, LEVELS, 1);
143        assert!(p.tail.is_none());
144        assert_eq!(p.phase1.groups, (1, 1));
145        assert_eq!(p.phase1.params.level_count, LEVELS);
146    }
147
148    #[test]
149    fn one_mip_writes_only_the_base() {
150        let p = Plan::new(1920, 1080, 1, 1);
151        assert_eq!(p.phase1.params.level_count, 1);
152        assert!(p.tail.is_none());
153    }
154
155    // Every level the tail writes is owned by exactly one workgroup: its
156    // coarsest level has at least as many texels as there are groups, so no two
157    // groups target the same texel.
158    #[test]
159    fn tail_levels_are_never_shared_between_groups() {
160        for (w, h) in [
161            (1024u32, 768u32),
162            (1920, 1080),
163            (2560, 1440),
164            (3840, 2160),
165            (7680, 4320),
166        ] {
167            let mips = 32 - w.max(h).leading_zeros();
168            let Some(tail) = Plan::new(w, h, mips, 1).tail else {
169                continue;
170            };
171            let base = (tail.params.base_width, tail.params.base_height);
172            let coarsest = tail.params.level_count - 1;
173            let size = level_size(base, coarsest);
174            assert!(
175                size.0 >= tail.groups.0 && size.1 >= tail.groups.1,
176                "{w}x{h}: level {coarsest} is {size:?} for {:?} groups",
177                tail.groups
178            );
179        }
180    }
181
182    // Beyond MAX_MIPS the plan clamps rather than leaving levels unwritten.
183    // 8192 divides into tiles exactly, so every level still has a sole owner
184    // and the tail keeps its full reach.
185    #[test]
186    fn deep_pyramid_clamps_to_what_two_dispatches_reach() {
187        assert_eq!(MAX_MIPS, 11);
188        let p = Plan::new(8192, 8192, 14, 1);
189        let tail = p.tail.expect("deep pyramid needs a tail");
190        assert_eq!(tail.params.level_count, LEVELS);
191        assert_eq!(p.mip_count(), MAX_MIPS);
192    }
193
194    // 7680 does not: mip 5 is 240, which needs eight tiles across but falls to
195    // seven texels by the tail's last level, so the plan gives that level up.
196    #[test]
197    fn tail_gives_up_a_level_rather_than_let_groups_collide() {
198        let p = Plan::new(7680, 4320, 13, 1);
199        let tail = p.tail.expect("deep pyramid needs a tail");
200        assert_eq!(tail.groups, (8, 5));
201        assert_eq!(tail.params.level_count, 5);
202        assert_eq!(p.mip_count(), 10);
203    }
204
205    // The pyramid depth a backend allocates and tells the cull about is the
206    // depth the plan writes, never the depth that was asked for.
207    #[test]
208    fn mip_count_reports_what_is_written() {
209        assert_eq!(Plan::new(1024, 768, 11, 1).mip_count(), 11);
210        assert_eq!(Plan::new(32, 32, LEVELS, 1).mip_count(), LEVELS);
211        assert_eq!(Plan::new(1920, 1080, 1, 1).mip_count(), 1);
212        assert!(Plan::new(3840, 2160, 12, 1).mip_count() <= 12);
213    }
214
215    #[test]
216    fn bound_mips_repeats_the_last_live_mip() {
217        assert!(Plan::bound_mips(5, 9).eq([5, 6, 7, 8, 8, 8]));
218        assert!(Plan::bound_mips(0, 11).eq([0, 1, 2, 3, 4, 5]));
219    }
220}