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
//! Cross-backend cascade re-render scheduling for the cascaded shadow map. The
//! shadow pass re-rasterizes all scene geometry into every cascade slice, so it
//! is one of the heaviest passes; `ShadowUpdate::Hybrid` amortizes the far
//! cascades across frames (near cascade every frame, one far cascade round-robin)
//! while keeping each slice primed before it is sampled. Shared by all three
//! backends so the policy lives once next to the CSM math in `csm.rs`.
use crate::components::ShadowUpdate;
use crate::gfx::render_types::NUM_SHADOW_CASCADES;
/// Round-robin clock + primed-set state for the cascade re-render schedule. One
/// per renderer; `next_mask` advances it once per frame and returns which
/// cascades to re-render. The caller refreshes only those cascades' light VPs and
/// re-rasterizes only those slices, so skipped cascades keep the depth + VP they
/// were last rendered with.
#[derive(Debug, Default)]
pub struct ShadowCascadeScheduler {
// Advances once per shadow update; selects which far cascade Hybrid mode
// refreshes this frame.
clock: u32,
// Bit `i` set once cascade `i` has been rendered since the scheduler was
// created. Unprimed cascades are force-rendered so a slice is never sampled
// before it holds valid depth.
primed_mask: u32,
}
impl ShadowCascadeScheduler {
/// Choose which cascades to re-render this frame and advance the round-robin
/// clock. Delegates the selection to the pure `select_cascade_mask` and
/// applies its side effects: advance the clock and record the newly primed
/// set. Bit `i` set in the result means cascade `i` re-renders this frame.
pub fn next_mask(&mut self, update: ShadowUpdate, active_cascades: u32) -> u32 {
let (mask, primed) =
select_cascade_mask(update, self.clock, self.primed_mask, active_cascades);
self.clock = self.clock.wrapping_add(1);
self.primed_mask = primed;
mask
}
}
// Pure cascade-selection step, split out of `next_mask` so the priming +
// round-robin policy is unit-testable without renderer state.
//
// Given the update policy, the current round-robin clock, and which cascades
// have already been primed, returns `(render_mask, new_primed_mask)`:
//
// - `scheduled` is the policy's steady-state set (EveryFrame = all; Hybrid =
// the near cascade plus one round-robin far cascade).
// - any cascade not yet primed is force-rendered this frame so its slice is
// never sampled before it holds valid depth. Because a cascade's bit is set
// in `primed` the first frame it renders and never cleared, priming renders
// each cascade exactly once on first access; it is never re-primed.
fn select_cascade_mask(
update: ShadowUpdate,
clock: u32,
primed: u32,
active_cascades: u32,
) -> (u32, u32) {
// Only the first `active` cascades exist this frame; the rest are never
// scheduled (and never sampled -- the shader bounds its lookup by the same
// count). `primed` may carry bits for cascades that were active under a
// higher count; `& all` masks them off so they never force a render.
let active = active_cascades.clamp(1, NUM_SHADOW_CASCADES as u32);
let all = (1u32 << active) - 1;
let scheduled = match update {
ShadowUpdate::EveryFrame => all,
ShadowUpdate::Hybrid => {
let far_count = (active - 1).max(1);
let far = 1 + (clock % far_count);
(1u32 | (1u32 << far)) & all
}
};
let unprimed = all & !primed;
let mask = (scheduled | unprimed) & all;
(mask, primed | mask)
}
#[cfg(test)]
mod tests {
use super::{ShadowCascadeScheduler, select_cascade_mask};
use crate::components::ShadowUpdate;
use crate::gfx::render_types::NUM_SHADOW_CASCADES;
const ALL: u32 = (1u32 << NUM_SHADOW_CASCADES) - 1;
#[test]
fn first_access_primes_every_cascade_at_once() {
// From the unprimed state both policies render all cascades on the
// very first frame, so no slice is sampled before it holds depth.
for update in [ShadowUpdate::Hybrid, ShadowUpdate::EveryFrame] {
let (mask, primed) = select_cascade_mask(update, 0, 0, NUM_SHADOW_CASCADES as u32);
assert_eq!(mask, ALL, "{update:?} should prime all cascades on frame 0");
assert_eq!(primed, ALL, "every cascade is primed after frame 0");
}
}
#[test]
fn fewer_active_cascades_only_schedule_the_live_slots() {
// With active_cascades = 2, only cascades 0 and 1 are ever scheduled --
// the inactive 2,3 never render even if their primed bits are stale from
// a higher count. EveryFrame renders both; Hybrid (far_count = 1) also
// renders both (no far cascade to amortize with only one far slot).
let live = 0b11u32;
for update in [ShadowUpdate::Hybrid, ShadowUpdate::EveryFrame] {
// Stale primed bits for the now-inactive cascades must not leak in.
let (mask, primed) = select_cascade_mask(update, 0, ALL, 2);
assert_eq!(mask, live, "{update:?} scheduled an inactive cascade");
assert_eq!(
primed & !live,
ALL & !live,
"inactive primed bits untouched"
);
}
// active = 1: only the near cascade ever renders.
let (mask, _) = select_cascade_mask(ShadowUpdate::Hybrid, 3, 0, 1);
assert_eq!(mask, 0b1);
}
#[test]
fn priming_never_re_renders_a_cascade() {
// Once primed, Hybrid drops back to its steady-state set: the near
// cascade plus exactly one far cascade. The full-prime render only
// happens while a cascade is still unprimed, never again.
let far_count = (NUM_SHADOW_CASCADES as u32 - 1).max(1);
for clock in 0..(far_count * 3) {
let (mask, primed) =
select_cascade_mask(ShadowUpdate::Hybrid, clock, ALL, NUM_SHADOW_CASCADES as u32);
assert_eq!(primed, ALL, "already-primed set is unchanged");
assert_eq!(mask & 1, 1, "near cascade refreshes every frame");
let extra = (mask & !1u32).count_ones();
assert_eq!(extra, 1, "exactly one far cascade refreshes per frame");
}
}
#[test]
fn hybrid_round_robins_every_far_cascade() {
// Over `far_count` consecutive frames the steady-state Hybrid mask
// touches each far cascade exactly once.
let far_count = (NUM_SHADOW_CASCADES as u32 - 1).max(1);
let mut union = 0u32;
for clock in 0..far_count {
let (mask, _) =
select_cascade_mask(ShadowUpdate::Hybrid, clock, ALL, NUM_SHADOW_CASCADES as u32);
union |= mask;
}
assert_eq!(union, ALL, "every cascade is refreshed within one round");
}
#[test]
fn every_frame_always_renders_all() {
for clock in 0..5 {
let (mask, primed) = select_cascade_mask(
ShadowUpdate::EveryFrame,
clock,
ALL,
NUM_SHADOW_CASCADES as u32,
);
assert_eq!(mask, ALL);
assert_eq!(primed, ALL);
}
}
#[test]
fn priming_is_monotonic_and_one_shot_per_cascade() {
// Drive the policy frame by frame from the unprimed state and assert
// the primed set only grows, and any cascade rendered purely for
// priming (in the mask but not in that frame's scheduled set) is
// rendered at most once across the whole run.
let mut primed = 0u32;
let mut prime_renders = [0u32; NUM_SHADOW_CASCADES];
for clock in 0..(NUM_SHADOW_CASCADES as u32 + 4) {
let far_count = (NUM_SHADOW_CASCADES as u32 - 1).max(1);
let scheduled_near_far = 1u32 | (1u32 << (1 + clock % far_count));
let before = primed;
let (mask, next) = select_cascade_mask(
ShadowUpdate::Hybrid,
clock,
primed,
NUM_SHADOW_CASCADES as u32,
);
assert_eq!(next & before, before, "primed set must never lose a bit");
for (c, count) in prime_renders.iter_mut().enumerate() {
let bit = 1u32 << c;
let primed_only = (mask & bit != 0) && (scheduled_near_far & bit == 0);
if primed_only {
*count += 1;
}
}
primed = next;
}
for (c, count) in prime_renders.iter().enumerate() {
assert!(
*count <= 1,
"cascade {c} was force-primed {count} times (>1)"
);
}
assert_eq!(primed, ALL, "all cascades primed after the run");
}
#[test]
fn scheduler_advances_clock_and_accumulates_primes() {
// The struct wrapper primes everything on frame 0, then steady-states
// to near + one far cascade and rotates the far cascade each frame.
let mut sched = ShadowCascadeScheduler::default();
assert_eq!(
sched.next_mask(ShadowUpdate::Hybrid, NUM_SHADOW_CASCADES as u32),
ALL,
"frame 0 primes all"
);
let far_count = (NUM_SHADOW_CASCADES as u32 - 1).max(1);
let mut union = 0u32;
for _ in 0..far_count {
let mask = sched.next_mask(ShadowUpdate::Hybrid, NUM_SHADOW_CASCADES as u32);
assert_eq!(mask & 1, 1, "near cascade refreshes every frame");
assert_eq!((mask & !1u32).count_ones(), 1, "one far cascade per frame");
union |= mask;
}
assert_eq!(union, ALL, "every cascade refreshed within one round");
}
}