Skip to main content

symtropy_physics/
replay.rs

1// Copyright (C) 2024-2026 Tristan Stoltz / Luminous Dynamics
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// Commercial licensing: see COMMERCIAL_LICENSE.md at repository root
4//! Determinism helpers: record/replay command streams and bitwise snapshots.
5//!
6//! This module is intentionally minimal: it provides
7//! - a small `WorldCommand` vocabulary for driving a `PhysicsWorld`
8//! - a `ReplayTape` of per-tick frames (dt + commands)
9//! - `WorldSnapshot`/`BodySnapshot` that capture simulation state as raw `f64` bits
10//!
11//! The goal is to make it easy to build a replay harness that asserts
12//! **bitwise-equal state per tick** across record/replay passes.
13
14use nalgebra::SVector;
15use symtropy_math::Bivector;
16
17use crate::body::{BodyHandle, BodyType, RigidBody};
18use crate::integrator;
19use crate::world::PhysicsWorld;
20
21/// Commands that mutate a physics world at a tick boundary.
22#[derive(Clone, Debug)]
23pub enum WorldCommand<const D: usize> {
24    ApplyForce {
25        body: BodyHandle,
26        force: Box<SVector<f64, D>>,
27    },
28    ApplyImpulse {
29        body: BodyHandle,
30        impulse: Box<SVector<f64, D>>,
31    },
32    SetLinearVelocity {
33        body: BodyHandle,
34        velocity: Box<SVector<f64, D>>,
35    },
36    SetAngularVelocity {
37        body: BodyHandle,
38        velocity: Box<Bivector<D>>,
39    },
40    Wake {
41        body: BodyHandle,
42    },
43}
44
45/// A single replay frame: `dt` + ordered list of commands to apply before stepping.
46#[derive(Clone, Debug)]
47pub struct ReplayFrame<const D: usize> {
48    pub dt: f64,
49    pub commands: Vec<WorldCommand<D>>,
50}
51
52/// A full replay tape: ordered frames.
53#[derive(Clone, Debug, Default)]
54pub struct ReplayTape<const D: usize> {
55    pub frames: Vec<ReplayFrame<D>>,
56}
57
58impl<const D: usize> ReplayTape<D> {
59    pub fn push_frame(&mut self, dt: f64, commands: Vec<WorldCommand<D>>) {
60        self.frames.push(ReplayFrame { dt, commands });
61    }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum ApplyCommandError {
66    MissingBody(BodyHandle),
67}
68
69/// Apply a list of commands to a world.
70pub fn apply_commands<const D: usize>(
71    world: &mut PhysicsWorld<D>,
72    commands: &[WorldCommand<D>],
73) -> Result<(), ApplyCommandError> {
74    for cmd in commands {
75        match cmd {
76            WorldCommand::ApplyForce { body, force } => {
77                let Some(b) = world.body_mut(*body) else {
78                    return Err(ApplyCommandError::MissingBody(*body));
79                };
80                b.apply_force(**force);
81            }
82            WorldCommand::ApplyImpulse { body, impulse } => {
83                let Some(b) = world.body_mut(*body) else {
84                    return Err(ApplyCommandError::MissingBody(*body));
85                };
86                integrator::apply_impulse(b, &**impulse);
87            }
88            WorldCommand::SetLinearVelocity { body, velocity } => {
89                let Some(b) = world.body_mut(*body) else {
90                    return Err(ApplyCommandError::MissingBody(*body));
91                };
92                b.linear_velocity = **velocity;
93            }
94            WorldCommand::SetAngularVelocity { body, velocity } => {
95                let Some(b) = world.body_mut(*body) else {
96                    return Err(ApplyCommandError::MissingBody(*body));
97                };
98                b.angular_velocity = **velocity;
99            }
100            WorldCommand::Wake { body } => {
101                let Some(b) = world.body_mut(*body) else {
102                    return Err(ApplyCommandError::MissingBody(*body));
103                };
104                b.wake();
105            }
106        }
107    }
108    Ok(())
109}
110
111/// Bitwise snapshot of a rigid body.
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct BodySnapshot<const D: usize> {
114    pub handle: BodyHandle,
115    pub body_type: BodyType,
116    pub translation: [u64; D],
117    pub rotation: [[u64; D]; D],
118    pub linear_velocity: [u64; D],
119    pub angular_velocity: [[u64; D]; D],
120    pub sleeping: bool,
121    pub sleep_counter: u32,
122}
123
124impl<const D: usize> BodySnapshot<D> {
125    pub fn from_body(body: &RigidBody<D>) -> Self {
126        let translation = std::array::from_fn(|i| body.transform.translation.0[i].to_bits());
127
128        let rot = body.transform.rotation.to_matrix();
129        let rotation = std::array::from_fn(|r| std::array::from_fn(|c| rot[(r, c)].to_bits()));
130
131        let linear_velocity = std::array::from_fn(|i| body.linear_velocity[i].to_bits());
132
133        let ang = body.angular_velocity.to_matrix();
134        let angular_velocity =
135            std::array::from_fn(|r| std::array::from_fn(|c| ang[(r, c)].to_bits()));
136
137        Self {
138            handle: body.handle,
139            body_type: body.body_type,
140            translation,
141            rotation,
142            linear_velocity,
143            angular_velocity,
144            sleeping: body.sleeping,
145            sleep_counter: body.sleep_counter,
146        }
147    }
148}
149
150/// Bitwise snapshot of a collision event.
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct CollisionEventSnapshot<const D: usize> {
153    pub body_a: BodyHandle,
154    pub body_b: BodyHandle,
155    pub impulse: u64,
156    pub normal: [u64; D],
157    pub depth: u64,
158}
159
160impl<const D: usize> CollisionEventSnapshot<D> {
161    pub fn from_event(event: &crate::contact::CollisionEvent<D>) -> Self {
162        Self {
163            body_a: event.body_a,
164            body_b: event.body_b,
165            impulse: event.impulse.to_bits(),
166            normal: std::array::from_fn(|i| event.normal[i].to_bits()),
167            depth: event.depth.to_bits(),
168        }
169    }
170}
171
172/// Bitwise snapshot of a physics world (bodies + last-step collision events).
173#[derive(Clone, Debug, PartialEq, Eq)]
174pub struct WorldSnapshot<const D: usize> {
175    pub bodies: Vec<BodySnapshot<D>>,
176    pub collision_events: Vec<CollisionEventSnapshot<D>>,
177}
178
179impl<const D: usize> WorldSnapshot<D> {
180    pub fn capture(world: &PhysicsWorld<D>) -> Self {
181        let mut bodies: Vec<_> = world.bodies.iter().map(BodySnapshot::from_body).collect();
182        bodies.sort_by_key(|b| b.handle);
183
184        let mut collision_events: Vec<_> = world
185            .collision_events
186            .iter()
187            .map(CollisionEventSnapshot::from_event)
188            .collect();
189        collision_events.sort_by_key(|e| (e.body_a, e.body_b, e.impulse, e.depth));
190
191        Self {
192            bodies,
193            collision_events,
194        }
195    }
196}