Skip to main content

forge_audio/
diff_pool.rs

1//! WavDiff + DiffPool + RollbackBuffer — 18-byte diff protocol with
2//! pre-allocated ring buffers for deterministic rollback.
3//!
4//! All world mutations flow through `WavDiff`. The `DiffPool` is a 1.1MB
5//! pre-allocated ring buffer that never grows. The `RollbackBuffer` stores
6//! exactly 120 frames (1.0 second at 120Hz) of `FrameSnapshot` metadata
7//! for prediction/correction rollback.
8//!
9//! Zero heap allocation. Integer-only. Bitwise deterministic.
10
11/// Pre-allocated DiffPool capacity.
12/// 18 bytes × 64000 ≈ 1.125 MB — fits the 1.1MB budget with margin.
13pub const POOL_CAPACITY: usize = 64000;
14
15/// Rollback window: exactly 120 frames = 1.0 second at 120Hz.
16/// Architecturally locked — do not change.
17pub const ROLLBACK_FRAMES: usize = 120;
18
19/// 18-byte diff record for a single state mutation.
20///
21/// Every state change produces
22/// one `WavDiff`. The engine is the sole authority that evaluates diffs.
23///
24/// Layout: `#[repr(C, packed)]` guarantees exactly 18 bytes with zero padding.
25/// Fields are laid out as 3×i32 + 3×u16 = 12 + 6 = 18 bytes.
26#[repr(C, packed)]
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct WavDiff {
29    /// Channel index.
30    pub channel: i32,             // 4 bytes
31    /// Frame offset in buffer.
32    pub frame_offset: i32,             // 4 bytes
33    /// Parameter identifier.
34    pub param_id: i32,             // 4 bytes
35    /// Index within the channel's state array.
36    pub index: u16,               // 2 bytes
37    /// State value before the mutation.
38    pub old_val: u16,             // 2 bytes
39    /// State value after the mutation.
40    pub new_val: u16,             // 2 bytes
41}
42// Compile-time size assertion: WavDiff must be exactly 18 bytes.
43const _: () = assert!(core::mem::size_of::<WavDiff>() == 18);
44
45impl WavDiff {
46    /// Convenience accessor for coordinate tuple as a tuple.
47    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
65/// Pre-allocated 1.1MB ring buffer of `WavDiff` entries.
66///
67/// Never grows. When full, oldest entries are overwritten.
68/// At 120Hz with ~100 diffs/frame, this holds ~640 frames (~5.3 seconds)
69/// of history — well beyond the 120-frame rollback window.
70pub struct DiffPool {
71    buffer: Box<[WavDiff; POOL_CAPACITY]>, // @forge:allow_alloc — one-time boot allocation
72    /// Write cursor — next slot to write into.
73    head: usize,
74    /// Total number of valid entries (capped at POOL_CAPACITY).
75    count: usize,
76}
77
78impl DiffPool {
79    /// Create a new pre-allocated DiffPool. All slots zeroed.
80    /// Single heap allocation at boot — never grows.
81    pub fn new() -> Self {
82        // alloc-ok: one-time boot allocation, pre-allocated ring buffer
83        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    /// Append a diff to the ring buffer. Returns the absolute index.
95    /// O(1), no allocation.
96    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    /// Get a diff by absolute index. Returns `None` if index is out of range.
107    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    /// Current write head position.
117    pub fn head(&self) -> usize {
118        self.head
119    }
120
121    /// Number of valid entries in the pool.
122    pub fn count(&self) -> usize {
123        self.count
124    }
125
126    /// Iterate diffs for a frame given `start` index and `count`.
127    /// Handles ring buffer wrap-around.
128    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    /// Iterate diffs for a frame in reverse order (for rewind).
137    /// Traverses from `start + count - 1` back to `start`.
138    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
153/// Forward iterator over a frame's diffs in the DiffPool.
154pub 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
178/// Reverse iterator over a frame's diffs in the DiffPool.
179pub 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/// Metadata snapshot for a single simulation frame.
208///
209/// Stored in the `RollbackBuffer`. Points into the `DiffPool` via
210/// `diff_start` and `diff_count` — no data duplication.
211#[derive(Clone, Copy, Debug)]
212pub struct FrameSnapshot {
213    /// Monotonic tick counter.
214    pub tick: u64,
215    /// 10-bit packed InputBits (u16 for alignment).
216    pub inputs: u16,
217    /// Index into DiffPool where this frame's diffs begin.
218    pub diff_start: u32,
219    /// Number of 18-byte diffs produced this frame.
220    pub diff_count: u16,
221    /// Blake3 checksum of physical state after this frame.
222    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
237/// 120-frame ring buffer for prediction/correction rollback.
238///
239/// Exactly 1.0 second at 120Hz. Architecturally locked — never grows.
240/// Each frame stores a `FrameSnapshot` that references diffs in the `DiffPool`.
241pub struct RollbackBuffer {
242    frames: [FrameSnapshot; ROLLBACK_FRAMES],
243    /// Write cursor — next slot to write into.
244    head: usize,
245    /// Number of valid frames stored (capped at ROLLBACK_FRAMES).
246    count: usize,
247}
248
249impl RollbackBuffer {
250    /// Create a new RollbackBuffer. All 120 slots zeroed.
251    pub fn new() -> Self {
252        Self {
253            frames: [FrameSnapshot::default(); ROLLBACK_FRAMES],
254            head: 0,
255            count: 0,
256        }
257    }
258
259    /// Record a frame snapshot. Overwrites oldest if buffer is full.
260    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    /// Find a frame by tick number. Returns `None` if not in the buffer.
269    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    /// Get the most recent frame snapshot.
284    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    /// Number of valid frames stored.
297    pub fn count(&self) -> usize {
298        self.count
299    }
300
301    /// Iterate frames from `start_tick` to `end_tick` (inclusive) in forward order.
302    /// Used for replay (old→new) after rewind.
303    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    /// Iterate frames from `end_tick` back to `start_tick` (inclusive) in reverse order.
319    /// Used for rewind (new→old).
320    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
330/// Forward iterator over frames in a tick range.
331pub 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
355/// Reverse iterator over frames in a tick range.
356pub 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
383/// Rewind state by applying diffs in reverse (new→old).
384///
385/// For each diff in the frame range (newest to oldest), swaps `new_val` back
386/// to `old_val` in the provided state.
387///
388/// `apply_fn` receives `(coords, index, value_to_set)` for each rewind step.
389pub 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        // Traverse diffs in reverse: undo new→old
398        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
404/// Replay diffs forward (old→new) to restore state after rewind.
405///
406/// For each diff in the frame range (oldest to newest), applies `new_val`.
407///
408/// `apply_fn` receives `(coords, index, value_to_set)` for each replay step.
409pub 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        // Traverse diffs forward: apply old→new
418        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
424/// Compute a Blake3 checksum of a frame's diffs in the DiffPool.
425///
426/// Hashes each 18-byte WavDiff in the frame's range sequentially.
427/// The resulting 32-byte hash is stored in `FrameSnapshot.checksum` for
428/// bitwise reproducibility verification across edge devices.
429///
430/// Zero allocation — reads directly from the pre-allocated ring buffer.
431pub 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        // Hash the raw 18 bytes of each WavDiff
435        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// ── Property-based tests ─────────────────────────────────────────────────────
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use proptest::prelude::*;
453    use std::collections::HashMap;
454
455    /// Strategy to generate a valid WavDiff with constrained ranges.
456    fn arb_wav_diff() -> impl Strategy<Value = WavDiff> {
457        (
458            // Coords: small range to increase collision likelihood
459            -8i32..=8i32,
460            -8i32..=8i32,
461            -8i32..=8i32,
462            // Index within block (0..32768 but u16 max is fine)
463            0u16..=255u16,
464            // State IDs: small range to increase old/new overlap
465            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    /// Strategy to generate a sequence of diffs (1..=120 per frame, up to 120 frames).
479    fn arb_diff_sequence() -> impl Strategy<Value = Vec<Vec<WavDiff>>> {
480        // 1..=20 frames, each with 1..=10 diffs
481        proptest::collection::vec(
482            proptest::collection::vec(arb_wav_diff(), 1..=10),
483            1..=20,
484        )
485    }
486
487    // ── CP-6: Rollback Equivalence ───────────────────────────────────────
488    //
489    // For any sequence of WavDiff entries, rewinding (new→old) then
490    // replaying (old→new) produces the original state.
491    //
492    // **Validates: Requirements CP-6**
493    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            // Build a simple state map
501            // Start with all zeros (air)
502            let mut grid: HashMap<((i32, i32, i32), u16), u16> = HashMap::new();
503
504            let start_tick = 1u64;
505
506            // 1. Apply all diffs forward, recording into pool + rollback
507            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                    // Record current state as old_val for this diff
515                    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            // Capture the final state
539            let final_state = grid.clone();
540            let end_tick = start_tick + frames_diffs.len() as u64 - 1;
541
542            // 2. Rewind: apply diffs in reverse (new→old)
543            rewind(&pool, &rollback, start_tick, end_tick, |coords, index, val| {
544                grid.insert((coords, index), val);
545            });
546
547            // Capture the rewound state (should be all zeros / initial)
548            let rewound_state = grid.clone();
549
550            // Verify rewound state: all touched keys should be back to 0 (initial)
551            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            // 3. Replay: apply diffs forward (old→new)
558            replay(&pool, &rollback, start_tick, end_tick, |coords, index, val| {
559                grid.insert((coords, index), val);
560            });
561
562            // 4. Verify: replayed state matches the original final state
563            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    // ── Unit test: WavDiff size ────────────────────────────────────────
572    #[test]
573    fn test_wav_diff_is_18_bytes() {
574        assert_eq!(core::mem::size_of::<WavDiff>(), 18);
575    }
576
577    // ── Unit test: DiffPool ring buffer wrap-around ──────────────────────
578    #[test]
579    fn test_diffpool_wraparound() {
580        let mut pool = DiffPool::new();
581        // Fill to capacity
582        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); // wrapped around
594
595        // One more push overwrites slot 0
596        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); // still capped
605        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    // ── Unit test: RollbackBuffer 120-frame cap ──────────────────────────
613    #[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        // Oldest tick should be 30 (150 - 120)
627        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}