Skip to main content

concinnity_core/render/
frame_dirty.rs

1//! Write tracking for a uniform block a backend rings over its frames in
2//! flight. The CPU owns the values; every slot is marked when they change and
3//! one slot is cleared per frame, so a world whose values are steady writes
4//! nothing after the ring has caught up.
5
6/// Which frame-in-flight copies of a ringed uniform block still need a write.
7///
8/// ```rust
9/// # use concinnity_core::render::frame_dirty::FrameDirty;
10/// let mut dirty = FrameDirty::new(2);
11/// assert!(dirty.take(0));
12/// assert!(dirty.take(1));
13/// assert!(!dirty.take(0));
14/// ```
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub struct FrameDirty {
17    all: u32,
18    pending: u32,
19}
20
21/// Frames in flight a single [`FrameDirty`] can track.
22pub const MAX_TRACKED_FRAMES: usize = 32;
23
24impl FrameDirty {
25    /// Track `frames` slots, every one pending so the ring seeds itself.
26    /// Frames beyond [`MAX_TRACKED_FRAMES`] are not tracked.
27    pub const fn new(frames: usize) -> Self {
28        let all = if frames >= MAX_TRACKED_FRAMES {
29            u32::MAX
30        } else {
31            (1u32 << frames) - 1
32        };
33        Self { all, pending: all }
34    }
35
36    /// Mark every slot pending, after a change to the CPU-side values.
37    pub fn mark_all(&mut self) {
38        self.pending = self.all;
39    }
40
41    /// Whether `frame` still needs a write, clearing it if so.
42    pub fn take(&mut self, frame: usize) -> bool {
43        if frame >= MAX_TRACKED_FRAMES {
44            return true;
45        }
46        let bit = 1u32 << frame;
47        let pending = self.pending & bit != 0;
48        self.pending &= !bit;
49        pending
50    }
51
52    /// Whether any slot is still pending.
53    pub const fn any_pending(&self) -> bool {
54        self.pending != 0
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn every_slot_is_pending_until_it_is_taken_once() {
64        let mut dirty = FrameDirty::new(3);
65        assert!(dirty.any_pending());
66        for frame in 0..3 {
67            assert!(dirty.take(frame), "slot {frame} seeds pending");
68        }
69        assert!(!dirty.any_pending());
70        for frame in 0..3 {
71            assert!(!dirty.take(frame), "slot {frame} stays clear");
72        }
73    }
74
75    #[test]
76    fn a_change_re_arms_every_slot() {
77        let mut dirty = FrameDirty::new(2);
78        dirty.take(0);
79        dirty.take(1);
80        dirty.mark_all();
81        assert!(dirty.take(1));
82        assert!(dirty.take(0));
83        assert!(!dirty.any_pending());
84    }
85
86    #[test]
87    fn taking_one_slot_leaves_the_others_alone() {
88        let mut dirty = FrameDirty::new(3);
89        assert!(dirty.take(1));
90        assert!(dirty.any_pending());
91        assert!(dirty.take(0));
92        assert!(dirty.take(2));
93        assert!(!dirty.any_pending());
94    }
95
96    #[test]
97    fn an_untracked_slot_always_reports_pending() {
98        let mut dirty = FrameDirty::new(2);
99        assert!(dirty.take(MAX_TRACKED_FRAMES));
100        assert!(dirty.take(MAX_TRACKED_FRAMES));
101    }
102
103    #[test]
104    fn a_ring_wider_than_the_mask_tracks_every_bit_it_has() {
105        let dirty = FrameDirty::new(MAX_TRACKED_FRAMES);
106        assert_eq!(dirty, FrameDirty::new(MAX_TRACKED_FRAMES + 4));
107        assert!(dirty.any_pending());
108    }
109}