Skip to main content

box3d_rust/recording/
session.rs

1//! Recording session lifecycle: create, start/stop, framing, registry tail.
2//! Port of recording.c lifecycle + framing (b3Recording, b3RecBegin/EndRecord).
3//!
4//! SPDX-FileCopyrightText: 2026 Erin Catto
5//! SPDX-License-Identifier: MIT
6
7use crate::constants::BODY_NAME_LENGTH;
8use crate::core::get_length_units_per_meter;
9use crate::id::WorldId;
10use crate::math_functions::{aabb_union, Aabb};
11use crate::recording::buffer::RecBuffer;
12use crate::recording::hash::hash_world_state;
13use crate::recording::registry::GeometryRegistry;
14use crate::recording::snapshot::serialize_world;
15use crate::world::World;
16use std::collections::HashMap;
17
18/// Magic 'B3RC' little-endian. (B3_REC_MAGIC)
19pub const REC_MAGIC: u32 = 0x4352_3342;
20/// Major recording version. (B3_REC_VERSION_MAJOR)
21pub const REC_VERSION_MAJOR: u16 = 3;
22/// Minor recording version. (B3_REC_VERSION_MINOR)
23pub const REC_VERSION_MINOR: u16 = 2;
24
25/// Fixed 48-byte recording header. (b3RecHeader)
26#[repr(C)]
27#[derive(Debug, Clone, Copy)]
28pub struct RecHeader {
29    pub magic: u32,
30    pub version_major: u16,
31    pub version_minor: u16,
32    pub pointer_width: u8,
33    pub big_endian: u8,
34    pub validation_enabled: u8,
35    pub reserved: u8,
36    pub length_scale: f32,
37    pub reserved2: u32,
38    pub reserved3: u32,
39    pub snapshot_size: u64,
40    pub registry_offset: u64,
41    pub registry_byte_count: u64,
42}
43
44const _: () = assert!(std::mem::size_of::<RecHeader>() == 48);
45
46impl RecHeader {
47    pub fn to_bytes(self) -> [u8; 48] {
48        let mut b = [0u8; 48];
49        b[0..4].copy_from_slice(&self.magic.to_le_bytes());
50        b[4..6].copy_from_slice(&self.version_major.to_le_bytes());
51        b[6..8].copy_from_slice(&self.version_minor.to_le_bytes());
52        b[8] = self.pointer_width;
53        b[9] = self.big_endian;
54        b[10] = self.validation_enabled;
55        b[11] = self.reserved;
56        b[12..16].copy_from_slice(&self.length_scale.to_le_bytes());
57        b[16..20].copy_from_slice(&self.reserved2.to_le_bytes());
58        b[20..24].copy_from_slice(&self.reserved3.to_le_bytes());
59        b[24..32].copy_from_slice(&self.snapshot_size.to_le_bytes());
60        b[32..40].copy_from_slice(&self.registry_offset.to_le_bytes());
61        b[40..48].copy_from_slice(&self.registry_byte_count.to_le_bytes());
62        b
63    }
64
65    pub fn from_bytes(data: &[u8]) -> Option<Self> {
66        if data.len() < 48 {
67            return None;
68        }
69        Some(Self {
70            magic: u32::from_le_bytes(data[0..4].try_into().ok()?),
71            version_major: u16::from_le_bytes(data[4..6].try_into().ok()?),
72            version_minor: u16::from_le_bytes(data[6..8].try_into().ok()?),
73            pointer_width: data[8],
74            big_endian: data[9],
75            validation_enabled: data[10],
76            reserved: data[11],
77            length_scale: f32::from_le_bytes(data[12..16].try_into().ok()?),
78            reserved2: u32::from_le_bytes(data[16..20].try_into().ok()?),
79            reserved3: u32::from_le_bytes(data[20..24].try_into().ok()?),
80            snapshot_size: u64::from_le_bytes(data[24..32].try_into().ok()?),
81            registry_offset: u64::from_le_bytes(data[32..40].try_into().ok()?),
82            registry_byte_count: u64::from_le_bytes(data[40..48].try_into().ok()?),
83        })
84    }
85}
86
87/// Interned query tag. (b3RecTag)
88#[derive(Debug, Clone)]
89pub struct RecTag {
90    pub key: u64,
91    pub id: u64,
92    pub name: String,
93}
94
95/// User-owned recording buffer. (b3Recording)
96#[derive(Debug)]
97pub struct Recording {
98    pub buffer: RecBuffer,
99    /// Offset of the 3-byte size field for u24 backpatch.
100    pub record_start: i32,
101    pub registry: GeometryRegistry,
102    pub tags: Vec<RecTag>,
103    tag_map: HashMap<u64, u32>,
104    pub accumulated_bounds: Aabb,
105    pub have_bounds: bool,
106}
107
108impl Recording {
109    /// (b3CreateRecording)
110    pub fn new(byte_capacity: i32) -> Self {
111        let init_cap = if byte_capacity > 0 {
112            byte_capacity as usize
113        } else {
114            65536
115        };
116        Self {
117            buffer: RecBuffer::with_capacity(init_cap),
118            record_start: 0,
119            registry: GeometryRegistry::new(),
120            tags: Vec::new(),
121            tag_map: HashMap::new(),
122            accumulated_bounds: Aabb::default(),
123            have_bounds: false,
124        }
125    }
126
127    pub fn data(&self) -> &[u8] {
128        &self.buffer.data
129    }
130
131    pub fn size(&self) -> i32 {
132        self.buffer.size()
133    }
134
135    /// (b3RecBeginRecord)
136    pub fn begin_record(&mut self, opcode: u8) {
137        self.buffer.append_u8(opcode);
138        self.record_start = self.buffer.size();
139        self.buffer.append(&[0, 0, 0]);
140    }
141
142    /// (b3RecEndRecord)
143    pub fn end_record(&mut self) {
144        let payload_size = self.buffer.size() - self.record_start - 3;
145        debug_assert!(payload_size >= 0 && payload_size < (1 << 24));
146        let p = self.record_start as usize;
147        let sz = payload_size as u32;
148        self.buffer.data[p] = sz as u8;
149        self.buffer.data[p + 1] = (sz >> 8) as u8;
150        self.buffer.data[p + 2] = (sz >> 16) as u8;
151    }
152
153    /// (b3RecCommitRecord)
154    pub fn commit_record(&mut self, opcode: u8, payload: &[u8]) {
155        debug_assert!(payload.len() < (1 << 24));
156        self.buffer.append_u8(opcode);
157        let sz = payload.len() as u32;
158        self.buffer
159            .append(&[sz as u8, (sz >> 8) as u8, (sz >> 16) as u8]);
160        self.buffer.append(payload);
161    }
162
163    /// (b3RecAccumulateBounds)
164    pub fn accumulate_bounds(&mut self, bounds: Aabb) {
165        self.accumulated_bounds = if self.have_bounds {
166            aabb_union(self.accumulated_bounds, bounds)
167        } else {
168            bounds
169        };
170        self.have_bounds = true;
171    }
172
173    /// (b3HashQueryTag)
174    pub fn hash_query_tag(id: u64, name: &str) -> u64 {
175        use crate::recording::hash::{SNAP_FNV_INIT, SNAP_FNV_PRIME};
176        let mut h = SNAP_FNV_INIT;
177        for i in 0..8 {
178            h = (h ^ ((id >> (8 * i)) & 0xFF)).wrapping_mul(SNAP_FNV_PRIME);
179        }
180        for b in name.bytes() {
181            h = (h ^ (b as u64)).wrapping_mul(SNAP_FNV_PRIME);
182        }
183        if h != 0 {
184            h
185        } else {
186            1
187        }
188    }
189
190    /// (b3RecInternTag)
191    pub fn intern_tag(&mut self, key: u64, id: u64, name: &str) {
192        if self.tag_map.contains_key(&key) {
193            return;
194        }
195        let index = self.tags.len() as u32;
196        let mut clamped = String::new();
197        for (i, ch) in name.chars().enumerate() {
198            if i >= BODY_NAME_LENGTH {
199                break;
200            }
201            clamped.push(ch);
202        }
203        self.tags.push(RecTag {
204            key,
205            id,
206            name: clamped,
207        });
208        self.tag_map.insert(key, index);
209    }
210
211    /// (b3RecWriteRegistry)
212    pub fn write_registry(&mut self) {
213        self.buffer.append_u32(self.registry.entries.len() as u32);
214        for e in &self.registry.entries {
215            self.buffer.append_u8(e.kind as u8);
216            self.buffer.append_u32(e.bytes.len() as u32);
217            self.buffer.append(&e.bytes);
218        }
219        self.buffer.append_u32(self.tags.len() as u32);
220        for t in &self.tags {
221            self.buffer.append_u64(t.key);
222            self.buffer.append_u64(t.id);
223            self.buffer.append_str(&t.name);
224        }
225    }
226
227    fn reset_for_session(&mut self) {
228        self.buffer.data.clear();
229        self.record_start = 0;
230        self.have_bounds = false;
231        self.registry = GeometryRegistry::new();
232        self.tags.clear();
233        self.tag_map.clear();
234    }
235}
236
237/// Public world id matching C's `{ worldId+1, generation }`.
238pub fn world_public_id(world: &World) -> WorldId {
239    WorldId {
240        index1: world.world_id.wrapping_add(1),
241        generation: world.generation,
242    }
243}
244
245/// Start recording into a user-owned buffer. (b3StartRecordingIntoBuffer /
246/// b3World_StartRecording)
247pub fn start_recording(world: &mut World, recording: &mut Recording) {
248    recording.reset_for_session();
249
250    let mut hdr = RecHeader {
251        magic: REC_MAGIC,
252        version_major: REC_VERSION_MAJOR,
253        version_minor: REC_VERSION_MINOR,
254        pointer_width: std::mem::size_of::<*const ()>() as u8,
255        big_endian: 0,
256        validation_enabled: if cfg!(debug_assertions) { 1 } else { 0 },
257        reserved: 0,
258        length_scale: get_length_units_per_meter(),
259        reserved2: 0,
260        reserved3: 0,
261        snapshot_size: 0,
262        registry_offset: 0,
263        registry_byte_count: 0,
264    };
265
266    // SAFETY: recording outlives the session; cleared in stop_recording.
267    world.recording = Some(recording as *mut Recording);
268
269    let mut snap_buf = RecBuffer::new();
270    serialize_world(world, &mut snap_buf, &mut recording.registry);
271    hdr.snapshot_size = snap_buf.size() as u64;
272
273    recording.buffer.append(&hdr.to_bytes());
274    recording.buffer.append(&snap_buf.data);
275
276    let wid = world_public_id(world);
277    let hash = hash_world_state(world);
278    recording.write_state_hash(wid, hash);
279}
280
281/// Stop recording and finalize the buffer. (b3StopRecordingInternal /
282/// b3World_StopRecording)
283pub fn stop_recording(world: &mut World) {
284    let Some(rec_ptr) = world.recording.take() else {
285        return;
286    };
287    // SAFETY: pointer set by start_recording; exclusive while session active.
288    let rec = unsafe { &mut *rec_ptr };
289
290    let bounds = if rec.have_bounds {
291        rec.accumulated_bounds
292    } else {
293        Aabb::default()
294    };
295    rec.write_recording_bounds(bounds);
296
297    let wid = world_public_id(world);
298    rec.write_destroy_world(wid);
299
300    let registry_offset = rec.buffer.size();
301    rec.write_registry();
302    let registry_byte_count = rec.buffer.size() - registry_offset;
303
304    // Backpatch registryOffset / registryByteCount at header offsets 32 / 40.
305    let data = &mut rec.buffer.data;
306    debug_assert!(data.len() >= 48);
307    data[32..40].copy_from_slice(&(registry_offset as u64).to_le_bytes());
308    data[40..48].copy_from_slice(&(registry_byte_count as u64).to_le_bytes());
309}
310
311/// Invoke `f` on the active recording, if any.
312#[inline]
313pub fn with_recording(world: &mut World, f: impl FnOnce(&mut Recording)) {
314    if let Some(p) = world.recording {
315        // SAFETY: valid for the session; no concurrent access in serial port.
316        unsafe { f(&mut *p) }
317    }
318}