Skip to main content

box2d_rust/recording/
snapshot.rs

1// World snapshot foundation from world_snapshot.c: the image header, the
2// bounds-checked reader, and serialize/deserialize for the engine containers
3// (id pools, bitsets, the pair hash set, and the broad-phase trees).
4//
5// Format note: the C snapshot memcpys internal structs and gates loading on a
6// layout hash of their sizes — C refuses images from any other C build. The
7// Rust port keeps that design (snapshots are build-specific) but writes
8// fields explicitly in declaration order; the layout hash mixes Rust struct
9// sizes, so Rust images are only accepted by a matching Rust build. The
10// recording OP STREAM stays byte-compatible with C; the seed snapshot blob is
11// inherently implementation-specific on both sides.
12//
13// SPDX-FileCopyrightText: 2026 Erin Catto
14// SPDX-License-Identifier: MIT
15
16use super::write::{rec_w_i32, rec_w_u16, rec_w_u32, rec_w_u64};
17use crate::bitset::BitSet;
18use crate::dynamic_tree::{DynamicTree, TreeNode};
19use crate::id_pool::IdPool;
20use crate::math_functions::{Aabb, Vec2};
21use crate::table::HashSet;
22
23/// Snapshot image magic, 'BNS2'. (B2_SNAP_MAGIC)
24pub const SNAP_MAGIC: u32 = 0x32534E42;
25
26/// Bump this if any serialized data structure changes. (B2_SNAP_VERSION —
27/// restarted at 1 for the Rust field-order format)
28pub const SNAP_VERSION: u32 = 1;
29
30/// Image was built with validation (debug assertions). (B2_SNAP_FLAG_VALIDATION)
31pub const SNAP_FLAG_VALIDATION: u32 = 0x1;
32/// Image was built with double-precision world positions.
33/// (B2_SNAP_FLAG_DOUBLE_PRECISION)
34pub const SNAP_FLAG_DOUBLE_PRECISION: u32 = 0x2;
35
36/// Layout hash seeds from all serialized structs. Changing any struct size
37/// updates the hash and refuses older images, same role as C's
38/// b2ComputeLayoutHash over its memcpy'd structs.
39pub fn compute_layout_hash() -> u32 {
40    let mut h: u32 = 2166136261;
41    let mut mix = |x: usize| {
42        h ^= x as u32;
43        h = h.wrapping_mul(16777619);
44    };
45    mix(std::mem::size_of::<crate::body::Body>());
46    mix(std::mem::size_of::<crate::body::BodySim>());
47    mix(std::mem::size_of::<crate::body::BodyState>());
48    mix(std::mem::size_of::<crate::shape::Shape>());
49    mix(std::mem::size_of::<crate::shape::ChainShape>());
50    mix(std::mem::size_of::<crate::contact::Contact>());
51    mix(std::mem::size_of::<crate::contact::ContactSim>());
52    mix(std::mem::size_of::<crate::joint::Joint>());
53    mix(std::mem::size_of::<crate::joint::JointSim>());
54    mix(std::mem::size_of::<crate::island::Island>());
55    mix(std::mem::size_of::<crate::island::IslandSim>());
56    mix(std::mem::size_of::<TreeNode>());
57    mix(std::mem::size_of::<crate::sensor::Sensor>());
58    mix(crate::constants::GRAPH_COLOR_COUNT as usize);
59    h
60}
61
62/// Snapshot image header. (b2SnapHeader)
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct SnapHeader {
65    pub magic: u32,
66    pub version: u32,
67    pub layout_hash: u32,
68    pub flags: u32,
69}
70
71impl SnapHeader {
72    pub const SIZE: usize = 16;
73
74    /// Header for an image produced by this build.
75    pub fn current() -> SnapHeader {
76        let mut flags = 0;
77        if cfg!(debug_assertions) {
78            flags |= SNAP_FLAG_VALIDATION;
79        }
80        if crate::core::is_double_precision() {
81            flags |= SNAP_FLAG_DOUBLE_PRECISION;
82        }
83        SnapHeader {
84            magic: SNAP_MAGIC,
85            version: SNAP_VERSION,
86            layout_hash: compute_layout_hash(),
87            flags,
88        }
89    }
90
91    pub fn write(&self, buf: &mut Vec<u8>) {
92        rec_w_u32(buf, self.magic);
93        rec_w_u32(buf, self.version);
94        rec_w_u32(buf, self.layout_hash);
95        rec_w_u32(buf, self.flags);
96    }
97
98    /// True when an image with this header can be restored by this build.
99    /// The validation flag is diagnostic only, exactly like C.
100    pub fn is_compatible(&self) -> bool {
101        let current = SnapHeader::current();
102        self.magic == SNAP_MAGIC
103            && self.version == SNAP_VERSION
104            && self.layout_hash == current.layout_hash
105            && (self.flags & SNAP_FLAG_DOUBLE_PRECISION)
106                == (current.flags & SNAP_FLAG_DOUBLE_PRECISION)
107    }
108}
109
110/// Bounds-checked little-endian reader over a snapshot image. A short or
111/// corrupt image trips `ok` instead of panicking, mirroring the C
112/// b2SnapReader contract. (b2SnapReader)
113#[derive(Debug)]
114pub struct SnapReader<'a> {
115    pub data: &'a [u8],
116    pub cursor: usize,
117    pub ok: bool,
118}
119
120impl<'a> SnapReader<'a> {
121    pub fn new(data: &'a [u8]) -> SnapReader<'a> {
122        SnapReader {
123            data,
124            cursor: 0,
125            ok: true,
126        }
127    }
128
129    fn take(&mut self, n: usize) -> &'a [u8] {
130        if !self.ok || self.cursor + n > self.data.len() {
131            self.ok = false;
132            return &[];
133        }
134        let slice = &self.data[self.cursor..self.cursor + n];
135        self.cursor += n;
136        slice
137    }
138
139    pub fn r_u16(&mut self) -> u16 {
140        let b = self.take(2);
141        if b.len() < 2 {
142            return 0;
143        }
144        u16::from_le_bytes([b[0], b[1]])
145    }
146
147    pub fn r_u32(&mut self) -> u32 {
148        let b = self.take(4);
149        if b.len() < 4 {
150            return 0;
151        }
152        u32::from_le_bytes([b[0], b[1], b[2], b[3]])
153    }
154
155    pub fn r_i32(&mut self) -> i32 {
156        self.r_u32() as i32
157    }
158
159    pub fn r_u64(&mut self) -> u64 {
160        let b = self.take(8);
161        if b.len() < 8 {
162            return 0;
163        }
164        u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
165    }
166
167    pub fn r_f32(&mut self) -> f32 {
168        f32::from_bits(self.r_u32())
169    }
170
171    pub fn r_f64(&mut self) -> f64 {
172        f64::from_bits(self.r_u64())
173    }
174
175    pub fn r_bool(&mut self) -> bool {
176        let b = self.take(1);
177        !b.is_empty() && b[0] != 0
178    }
179
180    pub fn r_u8(&mut self) -> u8 {
181        let b = self.take(1);
182        if b.is_empty() {
183            0
184        } else {
185            b[0]
186        }
187    }
188
189    /// Guard an element count against the remaining stream so a corrupt
190    /// count cannot force a huge allocation. (b2SnapCheckCount)
191    pub fn check_count(&mut self, count: i32, min_stream_bytes: usize) -> bool {
192        if count < 0
193            || (count as usize) * min_stream_bytes
194                > self.data.len() - self.cursor.min(self.data.len())
195        {
196            self.ok = false;
197            return false;
198        }
199        true
200    }
201
202    pub fn r_header(&mut self) -> SnapHeader {
203        SnapHeader {
204            magic: self.r_u32(),
205            version: self.r_u32(),
206            layout_hash: self.r_u32(),
207            flags: self.r_u32(),
208        }
209    }
210}
211
212// Container serialize/deserialize. (b2SerIdPool/b2DesIdPool, b2SerBitSet/
213// b2DesBitSet, b2SerHashSet/b2DesHashSet, b2SerTree/b2DesTree)
214
215pub(crate) fn ser_id_pool(buf: &mut Vec<u8>, pool: &IdPool) {
216    rec_w_i32(buf, pool.free_array.len() as i32);
217    for &id in pool.free_array.iter() {
218        rec_w_i32(buf, id);
219    }
220    rec_w_i32(buf, pool.next_index);
221}
222
223pub(crate) fn des_id_pool(r: &mut SnapReader) -> IdPool {
224    let count = r.r_i32();
225    if !r.check_count(count, 4) {
226        return IdPool::new();
227    }
228    let mut pool = IdPool::new();
229    pool.free_array = (0..count).map(|_| r.r_i32()).collect();
230    pool.next_index = r.r_i32();
231    pool
232}
233
234pub(crate) fn ser_bitset(buf: &mut Vec<u8>, bs: &BitSet) {
235    rec_w_u32(buf, bs.block_count);
236    rec_w_i32(buf, bs.blocks.len() as i32);
237    for &block in bs.blocks.iter() {
238        rec_w_u64(buf, block);
239    }
240}
241
242pub(crate) fn des_bitset(r: &mut SnapReader) -> BitSet {
243    let block_count = r.r_u32();
244    let capacity = r.r_i32();
245    if !r.check_count(capacity, 8) {
246        return BitSet::new(0);
247    }
248    let mut bs = BitSet::new(0);
249    bs.block_count = block_count;
250    bs.blocks = (0..capacity).map(|_| r.r_u64()).collect();
251    bs
252}
253
254pub(crate) fn ser_hashset(buf: &mut Vec<u8>, hs: &HashSet) {
255    rec_w_u32(buf, hs.count);
256    rec_w_i32(buf, hs.items.len() as i32);
257    for &item in hs.items.iter() {
258        rec_w_u64(buf, item);
259    }
260}
261
262pub(crate) fn des_hashset(r: &mut SnapReader) -> HashSet {
263    let count = r.r_u32();
264    let capacity = r.r_i32();
265    if !r.check_count(capacity, 8) {
266        return HashSet::new(16);
267    }
268    let mut hs = HashSet::new(16);
269    hs.count = count;
270    hs.items = (0..capacity).map(|_| r.r_u64()).collect();
271    hs
272}
273
274fn ser_aabb(buf: &mut Vec<u8>, aabb: Aabb) {
275    super::write::rec_w_f32(buf, aabb.lower_bound.x);
276    super::write::rec_w_f32(buf, aabb.lower_bound.y);
277    super::write::rec_w_f32(buf, aabb.upper_bound.x);
278    super::write::rec_w_f32(buf, aabb.upper_bound.y);
279}
280
281fn des_aabb(r: &mut SnapReader) -> Aabb {
282    Aabb {
283        lower_bound: Vec2 {
284            x: r.r_f32(),
285            y: r.r_f32(),
286        },
287        upper_bound: Vec2 {
288            x: r.r_f32(),
289            y: r.r_f32(),
290        },
291    }
292}
293
294pub(crate) fn ser_tree(buf: &mut Vec<u8>, tree: &DynamicTree) {
295    rec_w_i32(buf, tree.root);
296    rec_w_i32(buf, tree.node_count);
297    rec_w_i32(buf, tree.free_list);
298    rec_w_i32(buf, tree.proxy_count);
299    rec_w_i32(buf, tree.nodes.len() as i32);
300    for node in tree.nodes.iter() {
301        ser_aabb(buf, node.aabb);
302        rec_w_u64(buf, node.category_bits);
303        rec_w_i32(buf, node.child1);
304        rec_w_i32(buf, node.child2);
305        rec_w_u64(buf, node.user_data);
306        rec_w_i32(buf, node.parent);
307        rec_w_i32(buf, node.next);
308        rec_w_u16(buf, node.height);
309        rec_w_u16(buf, node.flags);
310    }
311    // Rebuild scratch (leaf_indices/leaf_centers/rebuild_capacity) is
312    // per-step transient and rebuilt on demand; C serializes the node pool
313    // and scalars the same way via memcpy.
314}
315
316pub(crate) fn des_tree(r: &mut SnapReader) -> DynamicTree {
317    let mut tree = DynamicTree::new(0);
318    tree.root = r.r_i32();
319    tree.node_count = r.r_i32();
320    tree.free_list = r.r_i32();
321    tree.proxy_count = r.r_i32();
322    let capacity = r.r_i32();
323    // Each node is at least 48 bytes on the wire.
324    if !r.check_count(capacity, 48) {
325        return DynamicTree::new(0);
326    }
327    tree.nodes = (0..capacity)
328        .map(|_| {
329            let mut node = TreeNode::default_node();
330            node.aabb = des_aabb(r);
331            node.category_bits = r.r_u64();
332            node.child1 = r.r_i32();
333            node.child2 = r.r_i32();
334            node.user_data = r.r_u64();
335            node.parent = r.r_i32();
336            node.next = r.r_i32();
337            node.height = r.r_u16();
338            node.flags = r.r_u16();
339            node
340        })
341        .collect();
342    tree
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    // Every container round trips exactly, and the header refuses foreign
350    // images.
351    #[test]
352    fn container_round_trips() {
353        // Id pool with a used range and free holes.
354        let mut pool = IdPool::new();
355        for _ in 0..6 {
356            pool.alloc_id();
357        }
358        pool.free_id(2);
359        pool.free_id(4);
360        let mut buf = Vec::new();
361        ser_id_pool(&mut buf, &pool);
362        let mut r = SnapReader::new(&buf);
363        let restored = des_id_pool(&mut r);
364        assert!(r.ok);
365        assert_eq!(restored.id_count(), pool.id_count());
366        assert_eq!(restored.id_capacity(), pool.id_capacity());
367
368        // Bit set.
369        let mut bs = BitSet::new(200);
370        bs.set_bit_count_and_clear(200);
371        bs.set_bit(3);
372        bs.set_bit(130);
373        let mut buf = Vec::new();
374        ser_bitset(&mut buf, &bs);
375        let mut r = SnapReader::new(&buf);
376        let restored = des_bitset(&mut r);
377        assert!(r.ok);
378        assert!(restored.get_bit(3) && restored.get_bit(130) && !restored.get_bit(64));
379
380        // Pair hash set.
381        let mut hs = HashSet::new(16);
382        hs.add_key(crate::table::shape_pair_key(3, 9));
383        hs.add_key(crate::table::shape_pair_key(1, 2));
384        let mut buf = Vec::new();
385        ser_hashset(&mut buf, &hs);
386        let mut r = SnapReader::new(&buf);
387        let restored = des_hashset(&mut r);
388        assert!(r.ok);
389        assert!(restored.contains_key(crate::table::shape_pair_key(3, 9)));
390        assert!(!restored.contains_key(crate::table::shape_pair_key(3, 8)));
391
392        // Dynamic tree with a few proxies.
393        let mut tree = DynamicTree::new(4);
394        for i in 0..5 {
395            let x = i as f32;
396            tree.create_proxy(
397                Aabb {
398                    lower_bound: Vec2 { x, y: 0.0 },
399                    upper_bound: Vec2 { x: x + 1.0, y: 1.0 },
400                },
401                1,
402                i as u64,
403            );
404        }
405        let mut buf = Vec::new();
406        ser_tree(&mut buf, &tree);
407        let mut r = SnapReader::new(&buf);
408        let restored = des_tree(&mut r);
409        assert!(r.ok);
410        assert_eq!(restored.proxy_count(), 5);
411        assert_eq!(restored.height(), tree.height());
412        restored.validate();
413
414        // A truncated image trips ok instead of panicking.
415        let mut short = SnapReader::new(&buf[..10]);
416        let _ = des_tree(&mut short);
417        assert!(!short.ok);
418    }
419
420    #[test]
421    fn header_compatibility() {
422        let header = SnapHeader::current();
423        let mut buf = Vec::new();
424        header.write(&mut buf);
425        assert_eq!(buf.len(), SnapHeader::SIZE);
426
427        let mut r = SnapReader::new(&buf);
428        let read = r.r_header();
429        assert!(r.ok);
430        assert_eq!(read, header);
431        assert!(read.is_compatible());
432
433        // Wrong layout hash refuses to load.
434        let foreign = SnapHeader {
435            layout_hash: header.layout_hash ^ 1,
436            ..header
437        };
438        assert!(!foreign.is_compatible());
439    }
440}