Skip to main content

concinnity_core/render/
shadow_schedule.rs

1//! Cross-backend cascade re-render scheduling for the cascaded shadow map. The
2//! shadow pass re-rasterizes all scene geometry into every cascade slice, so it
3//! is one of the heaviest passes; `ShadowUpdate::Hybrid` amortizes the far
4//! cascades across frames (near cascade every frame, one far cascade round-robin)
5//! while keeping each slice primed before it is sampled. Shared by all three
6//! backends so the policy lives once next to the CSM math in `csm.rs`.
7
8use crate::components::ShadowUpdate;
9use crate::gfx::render_types::NUM_SHADOW_CASCADES;
10
11/// Round-robin clock + primed-set state for the cascade re-render schedule. One
12/// per renderer; `next_mask` advances it once per frame and returns which
13/// cascades to re-render. The caller refreshes only those cascades' light VPs and
14/// re-rasterizes only those slices, so skipped cascades keep the depth + VP they
15/// were last rendered with.
16#[derive(Debug, Default)]
17pub struct ShadowCascadeScheduler {
18    // Advances once per shadow update; selects which far cascade Hybrid mode
19    // refreshes this frame.
20    clock: u32,
21    // Bit `i` set once cascade `i` has been rendered since the scheduler was
22    // created. Unprimed cascades are force-rendered so a slice is never sampled
23    // before it holds valid depth.
24    primed_mask: u32,
25}
26
27impl ShadowCascadeScheduler {
28    /// Choose which cascades to re-render this frame and advance the round-robin
29    /// clock. Delegates the selection to the pure `select_cascade_mask` and
30    /// applies its side effects: advance the clock and record the newly primed
31    /// set. Bit `i` set in the result means cascade `i` re-renders this frame.
32    pub fn next_mask(&mut self, update: ShadowUpdate, active_cascades: u32) -> u32 {
33        let (mask, primed) =
34            select_cascade_mask(update, self.clock, self.primed_mask, active_cascades);
35        self.clock = self.clock.wrapping_add(1);
36        self.primed_mask = primed;
37        mask
38    }
39}
40
41// Pure cascade-selection step, split out of `next_mask` so the priming +
42// round-robin policy is unit-testable without renderer state.
43//
44// Given the update policy, the current round-robin clock, and which cascades
45// have already been primed, returns `(render_mask, new_primed_mask)`:
46//
47//   - `scheduled` is the policy's steady-state set (EveryFrame = all; Hybrid =
48//     the near cascade plus one round-robin far cascade).
49//   - any cascade not yet primed is force-rendered this frame so its slice is
50//     never sampled before it holds valid depth. Because a cascade's bit is set
51//     in `primed` the first frame it renders and never cleared, priming renders
52//     each cascade exactly once on first access; it is never re-primed.
53fn select_cascade_mask(
54    update: ShadowUpdate,
55    clock: u32,
56    primed: u32,
57    active_cascades: u32,
58) -> (u32, u32) {
59    // Only the first `active` cascades exist this frame; the rest are never
60    // scheduled (and never sampled -- the shader bounds its lookup by the same
61    // count). `primed` may carry bits for cascades that were active under a
62    // higher count; `& all` masks them off so they never force a render.
63    let active = active_cascades.clamp(1, NUM_SHADOW_CASCADES as u32);
64    let all = (1u32 << active) - 1;
65    let scheduled = match update {
66        ShadowUpdate::EveryFrame => all,
67        ShadowUpdate::Hybrid => {
68            let far_count = (active - 1).max(1);
69            let far = 1 + (clock % far_count);
70            (1u32 | (1u32 << far)) & all
71        }
72    };
73    let unprimed = all & !primed;
74    let mask = (scheduled | unprimed) & all;
75    (mask, primed | mask)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::{ShadowCascadeScheduler, select_cascade_mask};
81    use crate::components::ShadowUpdate;
82    use crate::gfx::render_types::NUM_SHADOW_CASCADES;
83
84    const ALL: u32 = (1u32 << NUM_SHADOW_CASCADES) - 1;
85
86    #[test]
87    fn first_access_primes_every_cascade_at_once() {
88        // From the unprimed state both policies render all cascades on the
89        // very first frame, so no slice is sampled before it holds depth.
90        for update in [ShadowUpdate::Hybrid, ShadowUpdate::EveryFrame] {
91            let (mask, primed) = select_cascade_mask(update, 0, 0, NUM_SHADOW_CASCADES as u32);
92            assert_eq!(mask, ALL, "{update:?} should prime all cascades on frame 0");
93            assert_eq!(primed, ALL, "every cascade is primed after frame 0");
94        }
95    }
96
97    #[test]
98    fn fewer_active_cascades_only_schedule_the_live_slots() {
99        // With active_cascades = 2, only cascades 0 and 1 are ever scheduled --
100        // the inactive 2,3 never render even if their primed bits are stale from
101        // a higher count. EveryFrame renders both; Hybrid (far_count = 1) also
102        // renders both (no far cascade to amortize with only one far slot).
103        let live = 0b11u32;
104        for update in [ShadowUpdate::Hybrid, ShadowUpdate::EveryFrame] {
105            // Stale primed bits for the now-inactive cascades must not leak in.
106            let (mask, primed) = select_cascade_mask(update, 0, ALL, 2);
107            assert_eq!(mask, live, "{update:?} scheduled an inactive cascade");
108            assert_eq!(
109                primed & !live,
110                ALL & !live,
111                "inactive primed bits untouched"
112            );
113        }
114        // active = 1: only the near cascade ever renders.
115        let (mask, _) = select_cascade_mask(ShadowUpdate::Hybrid, 3, 0, 1);
116        assert_eq!(mask, 0b1);
117    }
118
119    #[test]
120    fn priming_never_re_renders_a_cascade() {
121        // Once primed, Hybrid drops back to its steady-state set: the near
122        // cascade plus exactly one far cascade. The full-prime render only
123        // happens while a cascade is still unprimed, never again.
124        let far_count = (NUM_SHADOW_CASCADES as u32 - 1).max(1);
125        for clock in 0..(far_count * 3) {
126            let (mask, primed) =
127                select_cascade_mask(ShadowUpdate::Hybrid, clock, ALL, NUM_SHADOW_CASCADES as u32);
128            assert_eq!(primed, ALL, "already-primed set is unchanged");
129            assert_eq!(mask & 1, 1, "near cascade refreshes every frame");
130            let extra = (mask & !1u32).count_ones();
131            assert_eq!(extra, 1, "exactly one far cascade refreshes per frame");
132        }
133    }
134
135    #[test]
136    fn hybrid_round_robins_every_far_cascade() {
137        // Over `far_count` consecutive frames the steady-state Hybrid mask
138        // touches each far cascade exactly once.
139        let far_count = (NUM_SHADOW_CASCADES as u32 - 1).max(1);
140        let mut union = 0u32;
141        for clock in 0..far_count {
142            let (mask, _) =
143                select_cascade_mask(ShadowUpdate::Hybrid, clock, ALL, NUM_SHADOW_CASCADES as u32);
144            union |= mask;
145        }
146        assert_eq!(union, ALL, "every cascade is refreshed within one round");
147    }
148
149    #[test]
150    fn every_frame_always_renders_all() {
151        for clock in 0..5 {
152            let (mask, primed) = select_cascade_mask(
153                ShadowUpdate::EveryFrame,
154                clock,
155                ALL,
156                NUM_SHADOW_CASCADES as u32,
157            );
158            assert_eq!(mask, ALL);
159            assert_eq!(primed, ALL);
160        }
161    }
162
163    #[test]
164    fn priming_is_monotonic_and_one_shot_per_cascade() {
165        // Drive the policy frame by frame from the unprimed state and assert
166        // the primed set only grows, and any cascade rendered purely for
167        // priming (in the mask but not in that frame's scheduled set) is
168        // rendered at most once across the whole run.
169        let mut primed = 0u32;
170        let mut prime_renders = [0u32; NUM_SHADOW_CASCADES];
171        for clock in 0..(NUM_SHADOW_CASCADES as u32 + 4) {
172            let far_count = (NUM_SHADOW_CASCADES as u32 - 1).max(1);
173            let scheduled_near_far = 1u32 | (1u32 << (1 + clock % far_count));
174            let before = primed;
175            let (mask, next) = select_cascade_mask(
176                ShadowUpdate::Hybrid,
177                clock,
178                primed,
179                NUM_SHADOW_CASCADES as u32,
180            );
181            assert_eq!(next & before, before, "primed set must never lose a bit");
182            for (c, count) in prime_renders.iter_mut().enumerate() {
183                let bit = 1u32 << c;
184                let primed_only = (mask & bit != 0) && (scheduled_near_far & bit == 0);
185                if primed_only {
186                    *count += 1;
187                }
188            }
189            primed = next;
190        }
191        for (c, count) in prime_renders.iter().enumerate() {
192            assert!(
193                *count <= 1,
194                "cascade {c} was force-primed {count} times (>1)"
195            );
196        }
197        assert_eq!(primed, ALL, "all cascades primed after the run");
198    }
199
200    #[test]
201    fn scheduler_advances_clock_and_accumulates_primes() {
202        // The struct wrapper primes everything on frame 0, then steady-states
203        // to near + one far cascade and rotates the far cascade each frame.
204        let mut sched = ShadowCascadeScheduler::default();
205        assert_eq!(
206            sched.next_mask(ShadowUpdate::Hybrid, NUM_SHADOW_CASCADES as u32),
207            ALL,
208            "frame 0 primes all"
209        );
210        let far_count = (NUM_SHADOW_CASCADES as u32 - 1).max(1);
211        let mut union = 0u32;
212        for _ in 0..far_count {
213            let mask = sched.next_mask(ShadowUpdate::Hybrid, NUM_SHADOW_CASCADES as u32);
214            assert_eq!(mask & 1, 1, "near cascade refreshes every frame");
215            assert_eq!((mask & !1u32).count_ones(), 1, "one far cascade per frame");
216            union |= mask;
217        }
218        assert_eq!(union, ALL, "every cascade refreshed within one round");
219    }
220}