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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
//! Decides, per cull record, whether the model-history ring holds a usable
//! previous-frame transform for the G-buffer pre-pass's motion vectors.
//!
//! The history ring is filled on the GPU by `model_history.slang`, which copies
//! this frame's model matrices straight out of the bindless object buffer. That
//! makes a history entry meaningful only while its record keeps its occupant: a
//! recycled draw slot, a runtime reserve that repacked around a
//! streamed-in chunk, or the frames before the ring has been written at all
//! would otherwise reproject through a stranger's transform and smear under TAA.
//!
//! Each backend runs [`ModelHistory::begin`] once per frame and then asks for
//! every record's flag bits while it builds the draw-args buffer. A record this
//! reports stale carries [`crate::gfx::render_types::draw_args_no_history`], and
//! the pre-pass reprojects it through its own current model, which is a zero
//! model-delta rather than a wrong one.
use alloc::vec::Vec;
use crate::gfx::render_types::draw_args_no_history;
/// How a frame's draw-args build treats the model-history ring.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum HistoryMode {
/// The pre-pass fills and reads the ring this frame: flag only the records
/// whose occupant moved since the snapshot was taken.
Track,
/// The ring is not being filled this frame -- the pre-pass is off, or no
/// consumer reads motion -- so nothing in it can be trusted when it returns:
/// flag every record and re-prime.
#[default]
Stale,
/// A reflection-probe bake, building its own records into its own buffers:
/// flag every record and leave the frame's tracker alone.
Untracked,
}
// Occupant kinds, in the token's top bits, so a draw slot and a skinned slot
// that share an index never compare equal.
const KIND_DRAW: u64 = 0;
const KIND_SKINNED: u64 = 1;
// The token stored for a record no frame has observed yet. Distinct from every
// real token, whose kind occupies only the low two bits of the top byte.
const UNOBSERVED: u64 = u64::MAX;
/// Per-record previous-frame-transform validity for the GPU-filled model
/// history ring.
#[derive(Debug, Default)]
pub struct ModelHistory {
// Bumped whenever a draw slot takes a new occupant, so a record that keeps
// its index still reads as changed.
draw_gen: Vec<u32>,
// The same for the skinned tail's instance pool.
skinned_gen: Vec<u32>,
// Occupant token per cull record, as of the frame that last observed it.
observed: Vec<u64>,
// Set by `reset`: the ring holds nothing this frame's records were written
// for, so every slot wants filling before it is read.
prime: bool,
// This frame's mode, from `begin`.
mode: HistoryMode,
}
// `kind`'s occupant at `index`, at generation `generation`.
fn token(kind: u64, generation: u32, index: usize) -> u64 {
(kind << 62) | ((generation as u64 & 0x3FFF_FFFF) << 32) | (index as u64 & 0xFFFF_FFFF)
}
impl ModelHistory {
/// An empty tracker; [`Self::reset`] sizes it to the world.
pub fn new() -> Self {
Self::default()
}
/// Size the tracker to a world of `n_cull` records and invalidate every
/// one: the history ring's buffers have just been (re)allocated, so nothing
/// in them was written for these records. Occupant generations survive, so
/// a slot reused across the rebuild still reads as changed.
pub fn reset(&mut self, n_cull: usize) {
self.observed.clear();
self.observed.resize(n_cull, UNOBSERVED);
self.prime = true;
}
/// Open a draw-args build over `n_cull` records. Call once per build, before
/// any of the flag queries; a `Track` build in steady state is a no-op.
pub fn begin(&mut self, mode: HistoryMode, n_cull: usize) {
self.mode = mode;
match mode {
HistoryMode::Track if self.observed.len() == n_cull => {}
HistoryMode::Track | HistoryMode::Stale => self.reset(n_cull),
HistoryMode::Untracked => {}
}
}
/// The `GpuDrawArgs::flags` bits cull record `record` needs for the draw
/// slot `draw_idx` now filling it: `NO_HISTORY` when the ring's entry was
/// written for a different occupant, nothing when it can be trusted. Call
/// exactly once per record per build.
pub fn draw_flags(&mut self, record: usize, draw_idx: usize) -> u32 {
if self.mode != HistoryMode::Track {
return draw_args_no_history();
}
let generation = self.draw_gen.get(draw_idx).copied().unwrap_or(0);
self.flags(record, token(KIND_DRAW, generation, draw_idx))
}
/// [`Self::draw_flags`] for a record in the skinned tail.
pub fn skinned_flags(&mut self, record: usize, skinned_idx: usize) -> u32 {
if self.mode != HistoryMode::Track {
return draw_args_no_history();
}
let generation = self.skinned_gen.get(skinned_idx).copied().unwrap_or(0);
self.flags(record, token(KIND_SKINNED, generation, skinned_idx))
}
fn flags(&mut self, record: usize, token: u64) -> u32 {
match self.observe(record, token) {
true => draw_args_no_history(),
false => 0,
}
}
/// Whether the ring's slots must all be filled before this frame reads one.
/// True once per [`Self::reset`]; the caller dispatches the history kernel
/// into every slot on that frame instead of only its own.
pub fn take_prime(&mut self) -> bool {
core::mem::take(&mut self.prime)
}
/// Note that draw slot `draw_idx` now holds a different object. Call
/// wherever a slot is written for a new occupant -- a reused or appended
/// draw slot, a streamed chunk moving in, a spawned clone.
pub fn reoccupy_draw(&mut self, draw_idx: usize) {
bump(&mut self.draw_gen, draw_idx);
}
/// Note that skinned instance `skinned_idx` now holds a different object,
/// as a revealed instance-pool slot does.
pub fn reoccupy_skinned(&mut self, skinned_idx: usize) {
bump(&mut self.skinned_gen, skinned_idx);
}
fn observe(&mut self, record: usize, token: u64) -> bool {
let Some(slot) = self.observed.get_mut(record) else {
// A record past the tracked range has no history to trust.
return true;
};
let stale = *slot != token;
*slot = token;
stale
}
}
// Bump `slots[index]`, growing the vec to reach it.
fn bump(slots: &mut Vec<u32>, index: usize) {
if index >= slots.len() {
slots.resize(index + 1, 0);
}
slots[index] = slots[index].wrapping_add(1);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gfx::render_types::draw_args_no_history;
const KEEP: u32 = 0;
// A record keeping its occupant is stale on the first observation (nothing
// was ever written for it) and trusted from the second frame on.
#[test]
fn a_settled_record_is_stale_once_then_trusted() {
let mut h = ModelHistory::new();
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(2, 2), draw_args_no_history());
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(2, 2), KEEP);
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(2, 2), KEEP);
}
// Reusing a draw slot in place keeps the record index but changes the
// occupant, which is exactly the ghosting case the flag exists for.
#[test]
fn reoccupying_a_slot_invalidates_its_record_for_one_frame() {
let mut h = ModelHistory::new();
h.begin(HistoryMode::Track, 4);
h.draw_flags(1, 1);
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(1, 1), KEEP);
h.reoccupy_draw(1);
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(1, 1), draw_args_no_history());
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(1, 1), KEEP);
}
// The runtime reserve repacks when a chunk streams in or out, so a record
// can be handed a different draw slot without either slot being reused.
#[test]
fn a_repacked_record_is_stale_even_though_neither_slot_changed() {
let mut h = ModelHistory::new();
h.begin(HistoryMode::Track, 8);
h.draw_flags(5, 40);
h.begin(HistoryMode::Track, 8);
assert_eq!(h.draw_flags(5, 40), KEEP);
// The reserve shifted: record 5 now carries draw slot 41.
h.begin(HistoryMode::Track, 8);
assert_eq!(h.draw_flags(5, 41), draw_args_no_history());
h.begin(HistoryMode::Track, 8);
assert_eq!(h.draw_flags(5, 41), KEEP);
}
// The two occupant pools index from zero independently, so a skinned tail
// record must not be settled by the static prefix's observation.
#[test]
fn draw_and_skinned_occupants_never_alias() {
let mut h = ModelHistory::new();
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(0, 3), draw_args_no_history());
assert_eq!(h.skinned_flags(1, 3), draw_args_no_history());
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(0, 3), KEEP);
assert_eq!(h.skinned_flags(1, 3), KEEP);
h.reoccupy_skinned(3);
h.begin(HistoryMode::Track, 4);
// Only the skinned pool moved.
assert_eq!(h.draw_flags(0, 3), KEEP);
assert_eq!(h.skinned_flags(1, 3), draw_args_no_history());
}
// A record count change reallocates the ring, so every record is stale
// again and every slot wants filling before it is read.
#[test]
fn a_record_count_change_invalidates_everything_and_asks_for_a_prime() {
let mut h = ModelHistory::new();
h.begin(HistoryMode::Track, 3);
assert!(h.take_prime());
for r in 0..3 {
assert_eq!(h.draw_flags(r, r), draw_args_no_history());
}
h.begin(HistoryMode::Track, 3);
assert!(!h.take_prime());
for r in 0..3 {
assert_eq!(h.draw_flags(r, r), KEEP);
}
h.begin(HistoryMode::Track, 5);
assert!(h.take_prime());
for r in 0..3 {
assert_eq!(h.draw_flags(r, r), draw_args_no_history());
}
}
// A frame the pre-pass sits out leaves the ring stale, so every record is
// flagged and the next tracked frame starts over from a prime.
#[test]
fn a_stale_frame_flags_everything_and_re_primes() {
let mut h = ModelHistory::new();
h.begin(HistoryMode::Track, 2);
h.draw_flags(0, 0);
h.begin(HistoryMode::Track, 2);
assert_eq!(h.draw_flags(0, 0), KEEP);
h.take_prime();
h.begin(HistoryMode::Stale, 2);
assert_eq!(h.draw_flags(0, 0), draw_args_no_history());
assert!(h.take_prime());
h.begin(HistoryMode::Track, 2);
assert_eq!(h.draw_flags(0, 0), draw_args_no_history());
}
// A probe bake builds its own records into its own buffers: it flags
// everything but must not disturb the frame's own tracking.
#[test]
fn an_untracked_build_leaves_the_frames_tracker_alone() {
let mut h = ModelHistory::new();
h.begin(HistoryMode::Track, 2);
h.draw_flags(0, 0);
assert!(h.take_prime());
h.begin(HistoryMode::Untracked, 2);
assert_eq!(h.draw_flags(0, 0), draw_args_no_history());
assert!(!h.take_prime());
h.begin(HistoryMode::Track, 2);
assert_eq!(h.draw_flags(0, 0), KEEP);
}
// A record past the tracked range (a world that grew its reserve without a
// rebuild) never claims a history it does not have.
#[test]
fn a_record_past_the_tracked_range_is_always_stale() {
let mut h = ModelHistory::new();
h.begin(HistoryMode::Track, 2);
assert_eq!(h.draw_flags(7, 7), draw_args_no_history());
h.begin(HistoryMode::Track, 2);
assert_eq!(h.draw_flags(7, 7), draw_args_no_history());
}
// Generations are bumped for slots the tracker has never sized for, so a
// reoccupy that arrives before the first observation still counts.
#[test]
fn reoccupying_an_untracked_slot_grows_the_generation_table() {
let mut h = ModelHistory::new();
h.begin(HistoryMode::Track, 4);
h.reoccupy_draw(9);
assert_eq!(h.draw_flags(0, 9), draw_args_no_history());
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(0, 9), KEEP);
h.reoccupy_draw(9);
h.begin(HistoryMode::Track, 4);
assert_eq!(h.draw_flags(0, 9), draw_args_no_history());
}
}