Skip to main content

concinnity_core/render/
model_history.rs

1//! Decides, per cull record, whether the model-history ring holds a usable
2//! previous-frame transform for the G-buffer pre-pass's motion vectors.
3//!
4//! The history ring is filled on the GPU by `model_history.slang`, which copies
5//! this frame's model matrices straight out of the bindless object buffer. That
6//! makes a history entry meaningful only while its record keeps its occupant: a
7//! recycled draw slot, a runtime reserve that repacked around a
8//! streamed-in chunk, or the frames before the ring has been written at all
9//! would otherwise reproject through a stranger's transform and smear under TAA.
10//!
11//! Each backend runs [`ModelHistory::begin`] once per frame and then asks for
12//! every record's flag bits while it builds the draw-args buffer. A record this
13//! reports stale carries [`crate::gfx::render_types::draw_args_no_history`], and
14//! the pre-pass reprojects it through its own current model, which is a zero
15//! model-delta rather than a wrong one.
16
17use alloc::vec::Vec;
18
19use crate::gfx::render_types::draw_args_no_history;
20
21/// How a frame's draw-args build treats the model-history ring.
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum HistoryMode {
24    /// The pre-pass fills and reads the ring this frame: flag only the records
25    /// whose occupant moved since the snapshot was taken.
26    Track,
27    /// The ring is not being filled this frame -- the pre-pass is off, or no
28    /// consumer reads motion -- so nothing in it can be trusted when it returns:
29    /// flag every record and re-prime.
30    #[default]
31    Stale,
32    /// A reflection-probe bake, building its own records into its own buffers:
33    /// flag every record and leave the frame's tracker alone.
34    Untracked,
35}
36
37// Occupant kinds, in the token's top bits, so a draw slot and a skinned slot
38// that share an index never compare equal.
39const KIND_DRAW: u64 = 0;
40const KIND_SKINNED: u64 = 1;
41
42// The token stored for a record no frame has observed yet. Distinct from every
43// real token, whose kind occupies only the low two bits of the top byte.
44const UNOBSERVED: u64 = u64::MAX;
45
46/// Per-record previous-frame-transform validity for the GPU-filled model
47/// history ring.
48#[derive(Debug, Default)]
49pub struct ModelHistory {
50    // Bumped whenever a draw slot takes a new occupant, so a record that keeps
51    // its index still reads as changed.
52    draw_gen: Vec<u32>,
53    // The same for the skinned tail's instance pool.
54    skinned_gen: Vec<u32>,
55    // Occupant token per cull record, as of the frame that last observed it.
56    observed: Vec<u64>,
57    // Set by `reset`: the ring holds nothing this frame's records were written
58    // for, so every slot wants filling before it is read.
59    prime: bool,
60    // This frame's mode, from `begin`.
61    mode: HistoryMode,
62}
63
64// `kind`'s occupant at `index`, at generation `generation`.
65fn token(kind: u64, generation: u32, index: usize) -> u64 {
66    (kind << 62) | ((generation as u64 & 0x3FFF_FFFF) << 32) | (index as u64 & 0xFFFF_FFFF)
67}
68
69impl ModelHistory {
70    /// An empty tracker; [`Self::reset`] sizes it to the world.
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Size the tracker to a world of `n_cull` records and invalidate every
76    /// one: the history ring's buffers have just been (re)allocated, so nothing
77    /// in them was written for these records. Occupant generations survive, so
78    /// a slot reused across the rebuild still reads as changed.
79    pub fn reset(&mut self, n_cull: usize) {
80        self.observed.clear();
81        self.observed.resize(n_cull, UNOBSERVED);
82        self.prime = true;
83    }
84
85    /// Open a draw-args build over `n_cull` records. Call once per build, before
86    /// any of the flag queries; a `Track` build in steady state is a no-op.
87    pub fn begin(&mut self, mode: HistoryMode, n_cull: usize) {
88        self.mode = mode;
89        match mode {
90            HistoryMode::Track if self.observed.len() == n_cull => {}
91            HistoryMode::Track | HistoryMode::Stale => self.reset(n_cull),
92            HistoryMode::Untracked => {}
93        }
94    }
95
96    /// The `GpuDrawArgs::flags` bits cull record `record` needs for the draw
97    /// slot `draw_idx` now filling it: `NO_HISTORY` when the ring's entry was
98    /// written for a different occupant, nothing when it can be trusted. Call
99    /// exactly once per record per build.
100    pub fn draw_flags(&mut self, record: usize, draw_idx: usize) -> u32 {
101        if self.mode != HistoryMode::Track {
102            return draw_args_no_history();
103        }
104        let generation = self.draw_gen.get(draw_idx).copied().unwrap_or(0);
105        self.flags(record, token(KIND_DRAW, generation, draw_idx))
106    }
107
108    /// [`Self::draw_flags`] for a record in the skinned tail.
109    pub fn skinned_flags(&mut self, record: usize, skinned_idx: usize) -> u32 {
110        if self.mode != HistoryMode::Track {
111            return draw_args_no_history();
112        }
113        let generation = self.skinned_gen.get(skinned_idx).copied().unwrap_or(0);
114        self.flags(record, token(KIND_SKINNED, generation, skinned_idx))
115    }
116
117    fn flags(&mut self, record: usize, token: u64) -> u32 {
118        match self.observe(record, token) {
119            true => draw_args_no_history(),
120            false => 0,
121        }
122    }
123
124    /// Whether the ring's slots must all be filled before this frame reads one.
125    /// True once per [`Self::reset`]; the caller dispatches the history kernel
126    /// into every slot on that frame instead of only its own.
127    pub fn take_prime(&mut self) -> bool {
128        core::mem::take(&mut self.prime)
129    }
130
131    /// Note that draw slot `draw_idx` now holds a different object. Call
132    /// wherever a slot is written for a new occupant -- a reused or appended
133    /// draw slot, a streamed chunk moving in, a spawned clone.
134    pub fn reoccupy_draw(&mut self, draw_idx: usize) {
135        bump(&mut self.draw_gen, draw_idx);
136    }
137
138    /// Note that skinned instance `skinned_idx` now holds a different object,
139    /// as a revealed instance-pool slot does.
140    pub fn reoccupy_skinned(&mut self, skinned_idx: usize) {
141        bump(&mut self.skinned_gen, skinned_idx);
142    }
143
144    fn observe(&mut self, record: usize, token: u64) -> bool {
145        let Some(slot) = self.observed.get_mut(record) else {
146            // A record past the tracked range has no history to trust.
147            return true;
148        };
149        let stale = *slot != token;
150        *slot = token;
151        stale
152    }
153}
154
155// Bump `slots[index]`, growing the vec to reach it.
156fn bump(slots: &mut Vec<u32>, index: usize) {
157    if index >= slots.len() {
158        slots.resize(index + 1, 0);
159    }
160    slots[index] = slots[index].wrapping_add(1);
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::gfx::render_types::draw_args_no_history;
167
168    const KEEP: u32 = 0;
169
170    // A record keeping its occupant is stale on the first observation (nothing
171    // was ever written for it) and trusted from the second frame on.
172    #[test]
173    fn a_settled_record_is_stale_once_then_trusted() {
174        let mut h = ModelHistory::new();
175        h.begin(HistoryMode::Track, 4);
176        assert_eq!(h.draw_flags(2, 2), draw_args_no_history());
177        h.begin(HistoryMode::Track, 4);
178        assert_eq!(h.draw_flags(2, 2), KEEP);
179        h.begin(HistoryMode::Track, 4);
180        assert_eq!(h.draw_flags(2, 2), KEEP);
181    }
182
183    // Reusing a draw slot in place keeps the record index but changes the
184    // occupant, which is exactly the ghosting case the flag exists for.
185    #[test]
186    fn reoccupying_a_slot_invalidates_its_record_for_one_frame() {
187        let mut h = ModelHistory::new();
188        h.begin(HistoryMode::Track, 4);
189        h.draw_flags(1, 1);
190        h.begin(HistoryMode::Track, 4);
191        assert_eq!(h.draw_flags(1, 1), KEEP);
192        h.reoccupy_draw(1);
193        h.begin(HistoryMode::Track, 4);
194        assert_eq!(h.draw_flags(1, 1), draw_args_no_history());
195        h.begin(HistoryMode::Track, 4);
196        assert_eq!(h.draw_flags(1, 1), KEEP);
197    }
198
199    // The runtime reserve repacks when a chunk streams in or out, so a record
200    // can be handed a different draw slot without either slot being reused.
201    #[test]
202    fn a_repacked_record_is_stale_even_though_neither_slot_changed() {
203        let mut h = ModelHistory::new();
204        h.begin(HistoryMode::Track, 8);
205        h.draw_flags(5, 40);
206        h.begin(HistoryMode::Track, 8);
207        assert_eq!(h.draw_flags(5, 40), KEEP);
208        // The reserve shifted: record 5 now carries draw slot 41.
209        h.begin(HistoryMode::Track, 8);
210        assert_eq!(h.draw_flags(5, 41), draw_args_no_history());
211        h.begin(HistoryMode::Track, 8);
212        assert_eq!(h.draw_flags(5, 41), KEEP);
213    }
214
215    // The two occupant pools index from zero independently, so a skinned tail
216    // record must not be settled by the static prefix's observation.
217    #[test]
218    fn draw_and_skinned_occupants_never_alias() {
219        let mut h = ModelHistory::new();
220        h.begin(HistoryMode::Track, 4);
221        assert_eq!(h.draw_flags(0, 3), draw_args_no_history());
222        assert_eq!(h.skinned_flags(1, 3), draw_args_no_history());
223        h.begin(HistoryMode::Track, 4);
224        assert_eq!(h.draw_flags(0, 3), KEEP);
225        assert_eq!(h.skinned_flags(1, 3), KEEP);
226        h.reoccupy_skinned(3);
227        h.begin(HistoryMode::Track, 4);
228        // Only the skinned pool moved.
229        assert_eq!(h.draw_flags(0, 3), KEEP);
230        assert_eq!(h.skinned_flags(1, 3), draw_args_no_history());
231    }
232
233    // A record count change reallocates the ring, so every record is stale
234    // again and every slot wants filling before it is read.
235    #[test]
236    fn a_record_count_change_invalidates_everything_and_asks_for_a_prime() {
237        let mut h = ModelHistory::new();
238        h.begin(HistoryMode::Track, 3);
239        assert!(h.take_prime());
240        for r in 0..3 {
241            assert_eq!(h.draw_flags(r, r), draw_args_no_history());
242        }
243        h.begin(HistoryMode::Track, 3);
244        assert!(!h.take_prime());
245        for r in 0..3 {
246            assert_eq!(h.draw_flags(r, r), KEEP);
247        }
248        h.begin(HistoryMode::Track, 5);
249        assert!(h.take_prime());
250        for r in 0..3 {
251            assert_eq!(h.draw_flags(r, r), draw_args_no_history());
252        }
253    }
254
255    // A frame the pre-pass sits out leaves the ring stale, so every record is
256    // flagged and the next tracked frame starts over from a prime.
257    #[test]
258    fn a_stale_frame_flags_everything_and_re_primes() {
259        let mut h = ModelHistory::new();
260        h.begin(HistoryMode::Track, 2);
261        h.draw_flags(0, 0);
262        h.begin(HistoryMode::Track, 2);
263        assert_eq!(h.draw_flags(0, 0), KEEP);
264        h.take_prime();
265        h.begin(HistoryMode::Stale, 2);
266        assert_eq!(h.draw_flags(0, 0), draw_args_no_history());
267        assert!(h.take_prime());
268        h.begin(HistoryMode::Track, 2);
269        assert_eq!(h.draw_flags(0, 0), draw_args_no_history());
270    }
271
272    // A probe bake builds its own records into its own buffers: it flags
273    // everything but must not disturb the frame's own tracking.
274    #[test]
275    fn an_untracked_build_leaves_the_frames_tracker_alone() {
276        let mut h = ModelHistory::new();
277        h.begin(HistoryMode::Track, 2);
278        h.draw_flags(0, 0);
279        assert!(h.take_prime());
280        h.begin(HistoryMode::Untracked, 2);
281        assert_eq!(h.draw_flags(0, 0), draw_args_no_history());
282        assert!(!h.take_prime());
283        h.begin(HistoryMode::Track, 2);
284        assert_eq!(h.draw_flags(0, 0), KEEP);
285    }
286
287    // A record past the tracked range (a world that grew its reserve without a
288    // rebuild) never claims a history it does not have.
289    #[test]
290    fn a_record_past_the_tracked_range_is_always_stale() {
291        let mut h = ModelHistory::new();
292        h.begin(HistoryMode::Track, 2);
293        assert_eq!(h.draw_flags(7, 7), draw_args_no_history());
294        h.begin(HistoryMode::Track, 2);
295        assert_eq!(h.draw_flags(7, 7), draw_args_no_history());
296    }
297
298    // Generations are bumped for slots the tracker has never sized for, so a
299    // reoccupy that arrives before the first observation still counts.
300    #[test]
301    fn reoccupying_an_untracked_slot_grows_the_generation_table() {
302        let mut h = ModelHistory::new();
303        h.begin(HistoryMode::Track, 4);
304        h.reoccupy_draw(9);
305        assert_eq!(h.draw_flags(0, 9), draw_args_no_history());
306        h.begin(HistoryMode::Track, 4);
307        assert_eq!(h.draw_flags(0, 9), KEEP);
308        h.reoccupy_draw(9);
309        h.begin(HistoryMode::Track, 4);
310        assert_eq!(h.draw_flags(0, 9), draw_args_no_history());
311    }
312}