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