Skip to main content

box3d_rust/recording/
player.rs

1//! Recording player and validate-replay. Port of recording_replay.c player API.
2//!
3//! SPDX-FileCopyrightText: 2026 Erin Catto
4//! SPDX-License-Identifier: MIT
5
6use crate::core::{get_length_units_per_meter, set_length_units_per_meter};
7use crate::id::BodyId;
8use crate::math_functions::{Aabb, Vec3};
9use crate::recording::dispatch::{dispatch_one, RecReader};
10use crate::recording::ops::RecOp;
11use crate::recording::registry::{GeometryKind, RegistrySlot};
12use crate::recording::session::{RecHeader, RecTag, REC_MAGIC, REC_VERSION_MAJOR};
13use crate::recording::snapshot::deserialize_into_shell;
14use crate::types::default_world_def;
15use crate::world::World;
16use std::collections::HashMap;
17
18const KEYFRAME_INTERVAL_DEFAULT: i32 = 16;
19
20/// Interactive / headless recording player. (b3RecPlayer)
21pub struct RecPlayer {
22    pub data: Vec<u8>,
23    pub header_end: i32,
24    pub registry_end: i32,
25    pub length_scale: f32,
26    pub previous_length_scale: f32,
27    pub frame: i32,
28    pub frame_count: i32,
29    pub recorded_dt: f32,
30    pub recorded_sub_step_count: i32,
31    pub bounds: Aabb,
32    pub at_end: bool,
33    pub at_pre_step: bool,
34    pub diverge_frame: i32,
35    pub body_ids: Vec<BodyId>,
36    pub frame0_body_ids: Vec<BodyId>,
37    pub keyframe_interval: i32,
38    pub keyframe_min_interval: i32,
39    pub keyframe_budget: usize,
40    pub keyframe_bytes: usize,
41    pub last_keyframe_frame: i32,
42    pub world: World,
43    pub frame_queries: Vec<crate::recording::query_replay::FrameQuery>,
44    /// Reader borrows `data` via raw pointer for self-referential layout.
45    reader_cursor: i32,
46    reader_ok: bool,
47    reader_diverged: bool,
48    reader_slots: Vec<RegistrySlot>,
49    reader_tags: Vec<RecTag>,
50    reader_pending_query_key: u64,
51    frame0_image_start: usize,
52    frame0_image_size: usize,
53}
54
55impl RecPlayer {
56    /// (b3RecPlayer_Create)
57    pub fn create(data: &[u8], _worker_count: i32) -> Option<Box<Self>> {
58        let hdr = RecHeader::from_bytes(data)?;
59        if hdr.magic != REC_MAGIC {
60            eprintln!("b3RecPlayer_Create: bad magic 0x{:08X}", hdr.magic);
61            return None;
62        }
63        if hdr.version_major != REC_VERSION_MAJOR {
64            eprintln!(
65                "b3RecPlayer_Create: version mismatch {}.{} vs {}.x",
66                hdr.version_major, hdr.version_minor, REC_VERSION_MAJOR
67            );
68            return None;
69        }
70        if hdr.pointer_width != std::mem::size_of::<*const ()>() as u8 {
71            eprintln!(
72                "b3RecPlayer_Create: pointer width mismatch {} vs {}",
73                hdr.pointer_width,
74                std::mem::size_of::<*const ()>()
75            );
76            return None;
77        }
78        if hdr.big_endian != 0 {
79            eprintln!("b3RecPlayer_Create: big-endian recording not supported");
80            return None;
81        }
82        if hdr.snapshot_size == 0 {
83            eprintln!("b3RecPlayer_Create: missing snapshot seed");
84            return None;
85        }
86
87        let header_end64 = 48u64 + hdr.snapshot_size;
88        let registry_end64 = if hdr.registry_offset != 0 {
89            hdr.registry_offset
90        } else {
91            data.len() as u64
92        };
93        if header_end64 < 48 || header_end64 > registry_end64 || registry_end64 > data.len() as u64
94        {
95            eprintln!("b3RecPlayer_Create: corrupt offsets");
96            return None;
97        }
98        let header_end = header_end64 as i32;
99        let registry_end = registry_end64 as i32;
100
101        let previous_length_scale = get_length_units_per_meter();
102        if hdr.length_scale > 0.0 {
103            set_length_units_per_meter(hdr.length_scale);
104        }
105
106        let copy = data.to_vec();
107        let snap_start = 48usize;
108        let snap_size = hdr.snapshot_size as usize;
109
110        let (mut slots, tags) =
111            load_registry_block(&copy, hdr.registry_offset as usize, copy.len());
112
113        let mut world = World::new(&default_world_def());
114        if !deserialize_into_shell(
115            &copy[snap_start..snap_start + snap_size],
116            &mut world,
117            &mut slots,
118        ) {
119            eprintln!("b3RecPlayer_Create: snapshot deserialization failed");
120            set_length_units_per_meter(previous_length_scale);
121            return None;
122        }
123
124        let mut player = Box::new(Self {
125            data: copy,
126            header_end,
127            registry_end,
128            length_scale: hdr.length_scale,
129            previous_length_scale,
130            frame: 0,
131            frame_count: 0,
132            recorded_dt: 0.0,
133            recorded_sub_step_count: 0,
134            bounds: Aabb::default(),
135            at_end: false,
136            at_pre_step: false,
137            diverge_frame: -1,
138            body_ids: Vec::new(),
139            frame0_body_ids: Vec::new(),
140            keyframe_interval: KEYFRAME_INTERVAL_DEFAULT,
141            keyframe_min_interval: KEYFRAME_INTERVAL_DEFAULT,
142            keyframe_budget: 0,
143            keyframe_bytes: 0,
144            last_keyframe_frame: 0,
145            world,
146            frame_queries: Vec::new(),
147            reader_cursor: header_end,
148            reader_ok: true,
149            reader_diverged: false,
150            reader_slots: slots,
151            reader_tags: tags,
152            reader_pending_query_key: 0,
153            frame0_image_start: snap_start,
154            frame0_image_size: snap_size,
155        });
156
157        player.scan_file();
158        player.seed_frame0_body_ids();
159        Some(player)
160    }
161
162    fn with_reader<R>(&mut self, f: impl FnOnce(&mut RecReader<'_>) -> R) -> R {
163        let data_ptr = self.data.as_ptr();
164        let data_len = self.data.len();
165        let owner = self as *mut RecPlayer;
166        let mut rdr = RecReader {
167            // SAFETY: data Vec is not resized while the reader lives.
168            data: unsafe { std::slice::from_raw_parts(data_ptr, data_len) },
169            size: self.registry_end,
170            cursor: self.reader_cursor,
171            ok: self.reader_ok,
172            diverged: self.reader_diverged,
173            world: std::ptr::null_mut(),
174            owner: Some(owner),
175            slots: std::mem::take(&mut self.reader_slots),
176            tags: std::mem::take(&mut self.reader_tags),
177            pending_query_key: self.reader_pending_query_key,
178            pending_body_create: None,
179            pending_body_destroy: None,
180        };
181        rdr.world = &mut self.world as *mut World;
182        let result = f(&mut rdr);
183        let created = rdr.pending_body_create.take();
184        let destroyed = rdr.pending_body_destroy.take();
185        self.reader_cursor = rdr.cursor;
186        self.reader_ok = rdr.ok;
187        self.reader_diverged = rdr.diverged;
188        self.reader_slots = rdr.slots;
189        self.reader_tags = rdr.tags;
190        self.reader_pending_query_key = rdr.pending_query_key;
191        if let Some(id) = created {
192            self.track_body_create(id);
193        }
194        if let Some(id) = destroyed {
195            self.track_body_destroy(id);
196        }
197        result
198    }
199
200    /// Count Step ops and first dt. (b3RecScanFile)
201    fn scan_file(&mut self) {
202        let mut cursor = self.header_end as usize;
203        let end = self.registry_end as usize;
204        let data = &self.data;
205        let mut first_step = true;
206        while cursor + 4 <= end {
207            let opcode = data[cursor];
208            let payload = data[cursor + 1] as u32
209                | ((data[cursor + 2] as u32) << 8)
210                | ((data[cursor + 3] as u32) << 16);
211            cursor += 4;
212            if cursor + payload as usize > end {
213                break;
214            }
215            if opcode == RecOp::Step as u8 {
216                self.frame_count += 1;
217                if first_step && payload >= 4 + 4 {
218                    // WORLDID u32 + F32 dt + I32 subStep
219                    let base = cursor + 4; // skip world id
220                    if base + 8 <= cursor + payload as usize {
221                        self.recorded_dt =
222                            f32::from_le_bytes(data[base..base + 4].try_into().unwrap());
223                        self.recorded_sub_step_count =
224                            i32::from_le_bytes(data[base + 4..base + 8].try_into().unwrap());
225                    }
226                    first_step = false;
227                }
228            } else if opcode == RecOp::RecordingBounds as u8 && payload >= 24 {
229                // AABB: lower xyz + upper xyz as f32
230                let b = cursor;
231                self.bounds = Aabb {
232                    lower_bound: Vec3 {
233                        x: f32::from_le_bytes(data[b..b + 4].try_into().unwrap()),
234                        y: f32::from_le_bytes(data[b + 4..b + 8].try_into().unwrap()),
235                        z: f32::from_le_bytes(data[b + 8..b + 12].try_into().unwrap()),
236                    },
237                    upper_bound: Vec3 {
238                        x: f32::from_le_bytes(data[b + 12..b + 16].try_into().unwrap()),
239                        y: f32::from_le_bytes(data[b + 16..b + 20].try_into().unwrap()),
240                        z: f32::from_le_bytes(data[b + 20..b + 24].try_into().unwrap()),
241                    },
242                };
243            }
244            if opcode == RecOp::DestroyWorld as u8 {
245                break;
246            }
247            cursor += payload as usize;
248        }
249    }
250
251    fn seed_frame0_body_ids(&mut self) {
252        self.body_ids.clear();
253        for (i, body) in self.world.bodies.iter().enumerate() {
254            if body.id == i as i32 && body.set_index != crate::core::NULL_INDEX {
255                let id = BodyId {
256                    index1: i as i32 + 1,
257                    world0: self.world.world_id,
258                    generation: body.generation,
259                };
260                self.body_ids.push(id);
261            }
262        }
263        self.frame0_body_ids = self.body_ids.clone();
264    }
265
266    pub fn track_body_create(&mut self, id: BodyId) {
267        self.body_ids.push(id);
268    }
269
270    pub fn track_body_destroy(&mut self, id: BodyId) {
271        for slot in &mut self.body_ids {
272            if slot.index1 == id.index1 && slot.generation == id.generation {
273                *slot = BodyId::default();
274                break;
275            }
276        }
277    }
278
279    /// (b3RecPlayer_StepFrame)
280    pub fn step_frame(&mut self) -> bool {
281        self.at_pre_step = false;
282        self.frame_queries.clear();
283        if self.at_end {
284            return false;
285        }
286
287        let mut stepped = false;
288        loop {
289            if self.reader_cursor >= self.registry_end || !self.reader_ok {
290                self.at_end = true;
291                return stepped;
292            }
293
294            if stepped {
295                let next = self.data[self.reader_cursor as usize];
296                if next != RecOp::StateHash as u8 {
297                    return true;
298                }
299            }
300
301            let op = self.with_reader(dispatch_one);
302            if op < 0 {
303                self.at_end = true;
304                return stepped;
305            }
306            if op == RecOp::DestroyWorld as i32 {
307                self.at_end = true;
308                return stepped;
309            }
310            if op == RecOp::Step as i32 {
311                self.frame += 1;
312                stepped = true;
313            } else if op == RecOp::StateHash as i32 {
314                if self.diverge_frame < 0 && self.reader_diverged {
315                    self.diverge_frame = self.frame;
316                }
317            }
318        }
319    }
320
321    /// Park before Step when a CreateBody led the frame. (b3RecPlayer_SubStepFrame)
322    pub fn sub_step_frame(&mut self) {
323        if self.at_end {
324            return;
325        }
326
327        if !self.at_pre_step {
328            self.frame_queries.clear();
329        }
330
331        let mut stepped = false;
332        let mut have_create_body = false;
333        loop {
334            if self.reader_cursor >= self.registry_end || !self.reader_ok {
335                self.at_end = true;
336                self.at_pre_step = false;
337                return;
338            }
339
340            let current = self.data[self.reader_cursor as usize];
341            if stepped && current != RecOp::StateHash as u8 {
342                return;
343            }
344
345            if !self.at_pre_step && have_create_body && current == RecOp::Step as u8 {
346                self.at_pre_step = true;
347                return;
348            }
349
350            let op = self.with_reader(dispatch_one);
351            if op < 0 {
352                self.at_end = true;
353                self.at_pre_step = false;
354                return;
355            }
356            if op == RecOp::DestroyWorld as i32 {
357                self.at_end = true;
358                self.at_pre_step = false;
359                return;
360            }
361            if op == RecOp::CreateBody as i32 {
362                have_create_body = true;
363            }
364            if op == RecOp::Step as i32 {
365                self.frame += 1;
366                stepped = true;
367                self.at_pre_step = false;
368            } else if op == RecOp::StateHash as i32 {
369                if self.diverge_frame < 0 && self.reader_diverged {
370                    self.diverge_frame = self.frame;
371                }
372            }
373        }
374    }
375
376    /// (b3RecPlayer_Restart)
377    pub fn restart(&mut self) {
378        let snap = self.data
379            [self.frame0_image_start..self.frame0_image_start + self.frame0_image_size]
380            .to_vec();
381        let mut slots = self.reader_slots.clone();
382        // Clear live compounds so deserialize rebuilds.
383        for s in &mut slots {
384            s.live_compound = None;
385        }
386        if !deserialize_into_shell(&snap, &mut self.world, &mut slots) {
387            self.reader_ok = false;
388            return;
389        }
390        self.reader_slots = slots;
391        self.reader_cursor = self.header_end;
392        self.reader_ok = true;
393        self.reader_diverged = false;
394        self.reader_pending_query_key = 0;
395        self.frame = 0;
396        self.at_end = false;
397        self.at_pre_step = false;
398        self.diverge_frame = -1;
399        self.body_ids = self.frame0_body_ids.clone();
400        self.last_keyframe_frame = 0;
401    }
402
403    /// (b3RecPlayer_SeekFrame)
404    pub fn seek_frame(&mut self, target_frame: i32) {
405        if target_frame <= self.frame {
406            self.restart();
407        }
408        while self.frame < target_frame && self.step_frame() {}
409    }
410
411    pub fn has_diverged(&self) -> bool {
412        self.reader_diverged
413    }
414
415    pub fn get_diverge_frame(&self) -> i32 {
416        self.diverge_frame
417    }
418
419    pub fn get_frame(&self) -> i32 {
420        self.frame
421    }
422
423    pub fn get_frame_count(&self) -> i32 {
424        self.frame_count
425    }
426
427    pub fn is_at_end(&self) -> bool {
428        self.at_end
429    }
430
431    pub fn is_at_pre_step(&self) -> bool {
432        self.at_pre_step
433    }
434
435    pub fn get_body_count(&self) -> i32 {
436        self.body_ids.len() as i32
437    }
438
439    pub fn get_body_id(&self, index: i32) -> BodyId {
440        if index < 0 || index as usize >= self.body_ids.len() {
441            return BodyId::default();
442        }
443        self.body_ids[index as usize]
444    }
445
446    pub fn world(&self) -> &World {
447        &self.world
448    }
449
450    pub fn world_mut(&mut self) -> &mut World {
451        &mut self.world
452    }
453
454    /// (b3RecPlayer_GetInfo)
455    pub fn get_info(&self) -> RecPlayerInfo {
456        RecPlayerInfo {
457            frame_count: self.frame_count,
458            time_step: self.recorded_dt,
459            sub_step_count: self.recorded_sub_step_count,
460            bounds: self.bounds,
461        }
462    }
463
464    /// (b3RecPlayer_SetKeyframePolicy)
465    pub fn set_keyframe_policy(&mut self, budget_bytes: usize, min_interval_frames: i32) {
466        self.keyframe_budget = budget_bytes;
467        self.keyframe_min_interval = min_interval_frames.max(1);
468        self.keyframe_interval = self.keyframe_min_interval;
469        self.keyframe_bytes = 0;
470    }
471
472    pub fn get_keyframe_min_interval(&self) -> i32 {
473        self.keyframe_min_interval
474    }
475
476    pub fn get_keyframe_interval(&self) -> i32 {
477        self.keyframe_interval
478    }
479
480    pub fn get_keyframe_budget(&self) -> usize {
481        self.keyframe_budget
482    }
483
484    pub fn get_keyframe_bytes(&self) -> usize {
485        self.keyframe_bytes
486    }
487
488    pub fn get_frame_query_count(&self) -> i32 {
489        self.frame_queries.len() as i32
490    }
491
492    pub fn get_frame_query(
493        &self,
494        index: i32,
495    ) -> Option<&crate::recording::query_replay::FrameQuery> {
496        self.frame_queries.get(index as usize)
497    }
498
499    pub fn resolve_tag(&self, key: u64) -> Option<&crate::recording::session::RecTag> {
500        self.reader_tags.iter().find(|t| t.key == key)
501    }
502}
503
504/// Viewer info for a recording. (b3RecPlayerInfo)
505#[derive(Debug, Clone, Copy)]
506pub struct RecPlayerInfo {
507    pub frame_count: i32,
508    pub time_step: f32,
509    pub sub_step_count: i32,
510    pub bounds: Aabb,
511}
512
513impl Drop for RecPlayer {
514    fn drop(&mut self) {
515        set_length_units_per_meter(self.previous_length_scale);
516    }
517}
518
519/// (b3ValidateReplay)
520pub fn validate_replay(data: &[u8], worker_count: i32) -> bool {
521    let Some(mut player) = RecPlayer::create(data, worker_count) else {
522        return false;
523    };
524    while player.step_frame() {
525        if player.reader_diverged {
526            break;
527        }
528    }
529    player.reader_ok && !player.reader_diverged
530}
531
532fn load_registry_block(data: &[u8], offset: usize, end: usize) -> (Vec<RegistrySlot>, Vec<RecTag>) {
533    if offset == 0 || offset + 4 > end {
534        return (Vec::new(), Vec::new());
535    }
536    let mut cursor = offset;
537    let entry_count = u32::from_le_bytes(data[cursor..cursor + 4].try_into().unwrap()) as usize;
538    cursor += 4;
539    let mut slots = Vec::with_capacity(entry_count);
540    for _ in 0..entry_count {
541        if cursor + 5 > end {
542            break;
543        }
544        let kind = match data[cursor] {
545            0 => GeometryKind::Hull,
546            1 => GeometryKind::Mesh,
547            2 => GeometryKind::HeightField,
548            _ => GeometryKind::Compound,
549        };
550        cursor += 1;
551        let byte_count = u32::from_le_bytes(data[cursor..cursor + 4].try_into().unwrap()) as usize;
552        cursor += 4;
553        if cursor + byte_count > end {
554            break;
555        }
556        let bytes = data[cursor..cursor + byte_count].to_vec();
557        cursor += byte_count;
558        slots.push(RegistrySlot {
559            kind,
560            bytes,
561            live_compound: None,
562        });
563    }
564
565    let mut tags = Vec::new();
566    if cursor + 4 <= end {
567        let tag_count = u32::from_le_bytes(data[cursor..cursor + 4].try_into().unwrap()) as usize;
568        cursor += 4;
569        for _ in 0..tag_count {
570            if cursor + 16 > end {
571                break;
572            }
573            let key = u64::from_le_bytes(data[cursor..cursor + 8].try_into().unwrap());
574            cursor += 8;
575            let id = u64::from_le_bytes(data[cursor..cursor + 8].try_into().unwrap());
576            cursor += 8;
577            if cursor + 2 > end {
578                break;
579            }
580            let len = u16::from_le_bytes(data[cursor..cursor + 2].try_into().unwrap());
581            cursor += 2;
582            let name = if len == 0xFFFF {
583                String::new()
584            } else {
585                let n = len as usize;
586                if cursor + n > end {
587                    break;
588                }
589                let s = String::from_utf8_lossy(&data[cursor..cursor + n]).into_owned();
590                cursor += n;
591                s
592            };
593            tags.push(RecTag { key, id, name });
594        }
595    }
596    let _ = cursor;
597    let _map: HashMap<u64, u32> = tags
598        .iter()
599        .enumerate()
600        .map(|(i, t)| (t.key, i as u32))
601        .collect();
602    let _ = _map;
603    (slots, tags)
604}