1pub const POOL_CAPACITY: usize = 64000;
14
15pub const ROLLBACK_FRAMES: usize = 120;
18
19#[repr(C, packed)]
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct WavDiff {
29 pub channel: i32, pub frame_offset: i32, pub param_id: i32, pub index: u16, pub old_val: u16, pub new_val: u16, }
42const _: () = assert!(core::mem::size_of::<WavDiff>() == 18);
44
45impl WavDiff {
46 pub fn coords(&self) -> (i32, i32, i32) {
48 (self.channel, self.frame_offset, self.param_id)
49 }
50}
51
52impl Default for WavDiff {
53 fn default() -> Self {
54 Self {
55 channel: 0,
56 frame_offset: 0,
57 param_id: 0,
58 index: 0,
59 old_val: 0,
60 new_val: 0,
61 }
62 }
63}
64
65pub struct DiffPool {
71 buffer: Box<[WavDiff; POOL_CAPACITY]>, head: usize,
74 count: usize,
76}
77
78impl DiffPool {
79 pub fn new() -> Self {
82 let buffer = vec![WavDiff::default(); POOL_CAPACITY]
84 .into_boxed_slice()
85 .try_into()
86 .unwrap_or_else(|_| unreachable!());
87 Self {
88 buffer,
89 head: 0,
90 count: 0,
91 }
92 }
93
94 pub fn push(&mut self, diff: WavDiff) -> u32 {
97 let idx = self.head;
98 self.buffer[idx] = diff;
99 self.head = (self.head + 1) % POOL_CAPACITY;
100 if self.count < POOL_CAPACITY {
101 self.count += 1;
102 }
103 idx as u32
104 }
105
106 pub fn get(&self, index: u32) -> Option<&WavDiff> {
108 let idx = index as usize;
109 if idx < POOL_CAPACITY {
110 Some(&self.buffer[idx])
111 } else {
112 None
113 }
114 }
115
116 pub fn head(&self) -> usize {
118 self.head
119 }
120
121 pub fn count(&self) -> usize {
123 self.count
124 }
125
126 pub fn frame_diffs(&self, start: u32, count: u16) -> FrameDiffIter<'_> {
129 FrameDiffIter {
130 pool: self,
131 current: start as usize,
132 remaining: count as usize,
133 }
134 }
135
136 pub fn frame_diffs_reverse(&self, start: u32, count: u16) -> FrameDiffReverseIter<'_> {
139 let c = count as usize;
140 let last = if c == 0 {
141 start as usize
142 } else {
143 (start as usize + c - 1) % POOL_CAPACITY
144 };
145 FrameDiffReverseIter {
146 pool: self,
147 current: last,
148 remaining: c,
149 }
150 }
151}
152
153pub struct FrameDiffIter<'a> {
155 pool: &'a DiffPool,
156 current: usize,
157 remaining: usize,
158}
159
160impl<'a> Iterator for FrameDiffIter<'a> {
161 type Item = &'a WavDiff;
162
163 fn next(&mut self) -> Option<Self::Item> {
164 if self.remaining == 0 {
165 return None;
166 }
167 let diff = &self.pool.buffer[self.current];
168 self.current = (self.current + 1) % POOL_CAPACITY;
169 self.remaining -= 1;
170 Some(diff)
171 }
172
173 fn size_hint(&self) -> (usize, Option<usize>) {
174 (self.remaining, Some(self.remaining))
175 }
176}
177
178pub struct FrameDiffReverseIter<'a> {
180 pool: &'a DiffPool,
181 current: usize,
182 remaining: usize,
183}
184
185impl<'a> Iterator for FrameDiffReverseIter<'a> {
186 type Item = &'a WavDiff;
187
188 fn next(&mut self) -> Option<Self::Item> {
189 if self.remaining == 0 {
190 return None;
191 }
192 let diff = &self.pool.buffer[self.current];
193 if self.current == 0 {
194 self.current = POOL_CAPACITY - 1;
195 } else {
196 self.current -= 1;
197 }
198 self.remaining -= 1;
199 Some(diff)
200 }
201
202 fn size_hint(&self) -> (usize, Option<usize>) {
203 (self.remaining, Some(self.remaining))
204 }
205}
206
207#[derive(Clone, Copy, Debug)]
212pub struct FrameSnapshot {
213 pub tick: u64,
215 pub inputs: u16,
217 pub diff_start: u32,
219 pub diff_count: u16,
221 pub checksum: [u8; 32],
223}
224
225impl Default for FrameSnapshot {
226 fn default() -> Self {
227 Self {
228 tick: 0,
229 inputs: 0,
230 diff_start: 0,
231 diff_count: 0,
232 checksum: [0u8; 32],
233 }
234 }
235}
236
237pub struct RollbackBuffer {
242 frames: [FrameSnapshot; ROLLBACK_FRAMES],
243 head: usize,
245 count: usize,
247}
248
249impl RollbackBuffer {
250 pub fn new() -> Self {
252 Self {
253 frames: [FrameSnapshot::default(); ROLLBACK_FRAMES],
254 head: 0,
255 count: 0,
256 }
257 }
258
259 pub fn push(&mut self, snapshot: FrameSnapshot) {
261 self.frames[self.head] = snapshot;
262 self.head = (self.head + 1) % ROLLBACK_FRAMES;
263 if self.count < ROLLBACK_FRAMES {
264 self.count += 1;
265 }
266 }
267
268 pub fn find_by_tick(&self, tick: u64) -> Option<&FrameSnapshot> {
270 for i in 0..self.count {
271 let idx = if self.head == 0 {
272 ROLLBACK_FRAMES - 1 - i
273 } else {
274 (self.head + ROLLBACK_FRAMES - 1 - i) % ROLLBACK_FRAMES
275 };
276 if self.frames[idx].tick == tick {
277 return Some(&self.frames[idx]);
278 }
279 }
280 None
281 }
282
283 pub fn latest(&self) -> Option<&FrameSnapshot> {
285 if self.count == 0 {
286 return None;
287 }
288 let idx = if self.head == 0 {
289 ROLLBACK_FRAMES - 1
290 } else {
291 self.head - 1
292 };
293 Some(&self.frames[idx])
294 }
295
296 pub fn count(&self) -> usize {
298 self.count
299 }
300
301 pub fn frames_forward(&self, start_tick: u64, end_tick: u64) -> FrameRangeIter<'_> {
304 let oldest_idx = if self.count < ROLLBACK_FRAMES {
305 0
306 } else {
307 self.head
308 };
309 FrameRangeIter {
310 buffer: self,
311 pos: 0,
312 oldest_idx,
313 start_tick,
314 end_tick,
315 }
316 }
317
318 pub fn frames_reverse(&self, start_tick: u64, end_tick: u64) -> FrameRangeReverseIter<'_> {
321 FrameRangeReverseIter {
322 buffer: self,
323 pos: 0,
324 start_tick,
325 end_tick,
326 }
327 }
328}
329
330pub struct FrameRangeIter<'a> {
332 buffer: &'a RollbackBuffer,
333 pos: usize,
334 oldest_idx: usize,
335 start_tick: u64,
336 end_tick: u64,
337}
338
339impl<'a> Iterator for FrameRangeIter<'a> {
340 type Item = &'a FrameSnapshot;
341
342 fn next(&mut self) -> Option<Self::Item> {
343 while self.pos < self.buffer.count {
344 let idx = (self.oldest_idx + self.pos) % ROLLBACK_FRAMES;
345 self.pos += 1;
346 let frame = &self.buffer.frames[idx];
347 if frame.tick >= self.start_tick && frame.tick <= self.end_tick {
348 return Some(frame);
349 }
350 }
351 None
352 }
353}
354
355pub struct FrameRangeReverseIter<'a> {
357 buffer: &'a RollbackBuffer,
358 pos: usize,
359 start_tick: u64,
360 end_tick: u64,
361}
362
363impl<'a> Iterator for FrameRangeReverseIter<'a> {
364 type Item = &'a FrameSnapshot;
365
366 fn next(&mut self) -> Option<Self::Item> {
367 while self.pos < self.buffer.count {
368 let idx = if self.buffer.head == 0 {
369 ROLLBACK_FRAMES - 1 - self.pos
370 } else {
371 (self.buffer.head + ROLLBACK_FRAMES - 1 - self.pos) % ROLLBACK_FRAMES
372 };
373 self.pos += 1;
374 let frame = &self.buffer.frames[idx];
375 if frame.tick >= self.start_tick && frame.tick <= self.end_tick {
376 return Some(frame);
377 }
378 }
379 None
380 }
381}
382
383pub fn rewind(
390 pool: &DiffPool,
391 rollback: &RollbackBuffer,
392 start_tick: u64,
393 end_tick: u64,
394 mut apply_fn: impl FnMut((i32, i32, i32), u16, u16),
395) {
396 for frame in rollback.frames_reverse(start_tick, end_tick) {
397 for diff in pool.frame_diffs_reverse(frame.diff_start, frame.diff_count) {
399 apply_fn(diff.coords(), diff.index, diff.old_val);
400 }
401 }
402}
403
404pub fn replay(
410 pool: &DiffPool,
411 rollback: &RollbackBuffer,
412 start_tick: u64,
413 end_tick: u64,
414 mut apply_fn: impl FnMut((i32, i32, i32), u16, u16),
415) {
416 for frame in rollback.frames_forward(start_tick, end_tick) {
417 for diff in pool.frame_diffs(frame.diff_start, frame.diff_count) {
419 apply_fn(diff.coords(), diff.index, diff.new_val);
420 }
421 }
422}
423
424pub fn compute_tick_checksum(pool: &DiffPool, diff_start: u32, diff_count: u16) -> [u8; 32] {
432 let mut hasher = blake3::Hasher::new();
433 for diff in pool.frame_diffs(diff_start, diff_count) {
434 let bytes: &[u8] = unsafe {
436 core::slice::from_raw_parts(
437 diff as *const WavDiff as *const u8,
438 core::mem::size_of::<WavDiff>(),
439 )
440 };
441 hasher.update(bytes);
442 }
443 *hasher.finalize().as_bytes()
444}
445
446
447#[cfg(test)]
450mod tests {
451 use super::*;
452 use proptest::prelude::*;
453 use std::collections::HashMap;
454
455 fn arb_wav_diff() -> impl Strategy<Value = WavDiff> {
457 (
458 -8i32..=8i32,
460 -8i32..=8i32,
461 -8i32..=8i32,
462 0u16..=255u16,
464 0u16..=31u16,
466 0u16..=31u16,
467 )
468 .prop_map(|(cx, cy, cz, idx, old, new)| WavDiff {
469 channel: cx,
470 frame_offset: cy,
471 param_id: cz,
472 index: idx,
473 old_val: old,
474 new_val: new,
475 })
476 }
477
478 fn arb_diff_sequence() -> impl Strategy<Value = Vec<Vec<WavDiff>>> {
480 proptest::collection::vec(
482 proptest::collection::vec(arb_wav_diff(), 1..=10),
483 1..=20,
484 )
485 }
486
487 proptest! {
494 #![proptest_config(ProptestConfig::with_cases(64))]
495 #[test]
496 fn prop_cp6_rollback_equivalence(frames_diffs in arb_diff_sequence()) {
497 let mut pool = DiffPool::new();
498 let mut rollback = RollbackBuffer::new();
499
500 let mut grid: HashMap<((i32, i32, i32), u16), u16> = HashMap::new();
503
504 let start_tick = 1u64;
505
506 for (frame_idx, frame_diffs) in frames_diffs.iter().enumerate() {
508 let tick = start_tick + frame_idx as u64;
509 let diff_start = pool.head() as u32;
510 let mut diff_count = 0u16;
511
512 for diff in frame_diffs {
513 let key = (diff.coords(), diff.index);
514 let current = grid.get(&key).copied().unwrap_or(0);
516 let actual_diff = WavDiff {
517 channel: diff.channel,
518 frame_offset: diff.frame_offset,
519 param_id: diff.param_id,
520 index: diff.index,
521 old_val: current,
522 new_val: diff.new_val,
523 };
524 pool.push(actual_diff);
525 grid.insert(key, diff.new_val);
526 diff_count += 1;
527 }
528
529 rollback.push(FrameSnapshot {
530 tick,
531 inputs: 0,
532 diff_start,
533 diff_count,
534 checksum: [0u8; 32],
535 });
536 }
537
538 let final_state = grid.clone();
540 let end_tick = start_tick + frames_diffs.len() as u64 - 1;
541
542 rewind(&pool, &rollback, start_tick, end_tick, |coords, index, val| {
544 grid.insert((coords, index), val);
545 });
546
547 let rewound_state = grid.clone();
549
550 for key in final_state.keys() {
552 let val = rewound_state.get(key).copied().unwrap_or(0);
553 prop_assert_eq!(val, 0,
554 "After rewind, key {:?} should be 0 (initial), got {}", key, val);
555 }
556
557 replay(&pool, &rollback, start_tick, end_tick, |coords, index, val| {
559 grid.insert((coords, index), val);
560 });
561
562 for (key, expected_mat) in &final_state {
564 let actual = grid.get(key).copied().unwrap_or(0);
565 prop_assert_eq!(actual, *expected_mat,
566 "After replay, key {:?} should be {}, got {}", key, expected_mat, actual);
567 }
568 }
569 }
570
571 #[test]
573 fn test_wav_diff_is_18_bytes() {
574 assert_eq!(core::mem::size_of::<WavDiff>(), 18);
575 }
576
577 #[test]
579 fn test_diffpool_wraparound() {
580 let mut pool = DiffPool::new();
581 for i in 0..POOL_CAPACITY {
583 pool.push(WavDiff {
584 channel: 0,
585 frame_offset: 0,
586 param_id: 0,
587 index: (i % 65536) as u16,
588 old_val: 0,
589 new_val: 1,
590 });
591 }
592 assert_eq!(pool.count(), POOL_CAPACITY);
593 assert_eq!(pool.head(), 0); pool.push(WavDiff {
597 channel: 99,
598 frame_offset: 0,
599 param_id: 0,
600 index: 42,
601 old_val: 5,
602 new_val: 10,
603 });
604 assert_eq!(pool.count(), POOL_CAPACITY); let d = pool.get(0).unwrap();
606 let cx = { d.channel };
607 let idx = { d.index };
608 assert_eq!(cx, 99);
609 assert_eq!(idx, 42);
610 }
611
612 #[test]
614 fn test_rollback_buffer_cap() {
615 let mut rb = RollbackBuffer::new();
616 for i in 0..150u64 {
617 rb.push(FrameSnapshot {
618 tick: i,
619 inputs: 0,
620 diff_start: 0,
621 diff_count: 0,
622 checksum: [0u8; 32],
623 });
624 }
625 assert_eq!(rb.count(), ROLLBACK_FRAMES);
626 assert!(rb.find_by_tick(29).is_none());
628 assert!(rb.find_by_tick(30).is_some());
629 assert!(rb.find_by_tick(149).is_some());
630 }
631}