Skip to main content

box2d_rust/recording/
write.rs

1// Wire write primitives from recording.c. Everything is little-endian and
2// byte-identical to the C recording format: geometry PODs are written field
3// by field in declaration order (the C memcpy layouts have no padding), and
4// world positions widen to two doubles only in the double-precision build.
5//
6// SPDX-FileCopyrightText: 2026 Erin Catto
7// SPDX-License-Identifier: MIT
8
9use crate::collision::{
10    Capsule, ChainSegment, Circle, MassData, Polygon, Segment, WorldCastOutput,
11};
12use crate::distance::ShapeProxy;
13use crate::dynamic_tree::TreeStats;
14use crate::hull::MAX_POLYGON_VERTICES;
15use crate::id::{BodyId, ChainId, JointId, ShapeId, WorldId};
16use crate::math_functions::{Aabb, Pos, Rot, Transform, Vec2, WorldTransform};
17use crate::types::{
18    BodyDef, ChainDef, DistanceJointDef, ExplosionDef, Filter, FilterJointDef, JointDef,
19    MotionLocks, MotorJointDef, PrismaticJointDef, QueryFilter, RayResult, RevoluteJointDef,
20    ShapeDef, SurfaceMaterial, WeldJointDef, WheelJointDef,
21};
22
23pub fn rec_w_u8(buf: &mut Vec<u8>, v: u8) {
24    buf.push(v);
25}
26
27pub fn rec_w_u16(buf: &mut Vec<u8>, v: u16) {
28    buf.extend_from_slice(&v.to_le_bytes());
29}
30
31pub fn rec_w_u32(buf: &mut Vec<u8>, v: u32) {
32    buf.extend_from_slice(&v.to_le_bytes());
33}
34
35pub fn rec_w_u64(buf: &mut Vec<u8>, v: u64) {
36    buf.extend_from_slice(&v.to_le_bytes());
37}
38
39pub fn rec_w_i32(buf: &mut Vec<u8>, v: i32) {
40    rec_w_u32(buf, v as u32);
41}
42
43pub fn rec_w_f32(buf: &mut Vec<u8>, v: f32) {
44    rec_w_u32(buf, v.to_bits());
45}
46
47pub fn rec_w_f64(buf: &mut Vec<u8>, v: f64) {
48    rec_w_u64(buf, v.to_bits());
49}
50
51pub fn rec_w_bool(buf: &mut Vec<u8>, v: bool) {
52    rec_w_u8(buf, if v { 1 } else { 0 });
53}
54
55pub fn rec_w_vec2(buf: &mut Vec<u8>, v: Vec2) {
56    rec_w_f32(buf, v.x);
57    rec_w_f32(buf, v.y);
58}
59
60pub fn rec_w_rot(buf: &mut Vec<u8>, v: Rot) {
61    rec_w_f32(buf, v.c);
62    rec_w_f32(buf, v.s);
63}
64
65pub fn rec_w_xf(buf: &mut Vec<u8>, v: Transform) {
66    rec_w_vec2(buf, v.p);
67    rec_w_rot(buf, v.q);
68}
69
70/// A world position keeps full precision on the wire so a recording
71/// reproduces the simulation far from the origin. In the float build this is
72/// two floats, identical to VEC2. (b2RecW_POSITION)
73#[cfg(feature = "double-precision")]
74pub fn rec_w_position(buf: &mut Vec<u8>, v: Pos) {
75    rec_w_f64(buf, v.x);
76    rec_w_f64(buf, v.y);
77}
78
79#[cfg(not(feature = "double-precision"))]
80pub fn rec_w_position(buf: &mut Vec<u8>, v: Pos) {
81    rec_w_f32(buf, v.x);
82    rec_w_f32(buf, v.y);
83}
84
85pub fn rec_w_worldxf(buf: &mut Vec<u8>, v: WorldTransform) {
86    rec_w_position(buf, v.p);
87    rec_w_rot(buf, v.q);
88}
89
90pub fn rec_w_worldid(buf: &mut Vec<u8>, v: WorldId) {
91    rec_w_u32(buf, v.store());
92}
93
94pub fn rec_w_bodyid(buf: &mut Vec<u8>, v: BodyId) {
95    rec_w_u64(buf, v.store());
96}
97
98pub fn rec_w_shapeid(buf: &mut Vec<u8>, v: ShapeId) {
99    rec_w_u64(buf, v.store());
100}
101
102pub fn rec_w_chainid(buf: &mut Vec<u8>, v: ChainId) {
103    rec_w_u64(buf, v.store());
104}
105
106pub fn rec_w_jointid(buf: &mut Vec<u8>, v: JointId) {
107    rec_w_u64(buf, v.store());
108}
109
110// Geometry is pointer-free POD; C memcpys the structs, which have no padding,
111// so field-order writes are byte-identical.
112
113pub fn rec_w_circle(buf: &mut Vec<u8>, v: Circle) {
114    rec_w_vec2(buf, v.center);
115    rec_w_f32(buf, v.radius);
116}
117
118pub fn rec_w_capsule(buf: &mut Vec<u8>, v: Capsule) {
119    rec_w_vec2(buf, v.center1);
120    rec_w_vec2(buf, v.center2);
121    rec_w_f32(buf, v.radius);
122}
123
124pub fn rec_w_segment(buf: &mut Vec<u8>, v: Segment) {
125    rec_w_vec2(buf, v.point1);
126    rec_w_vec2(buf, v.point2);
127}
128
129pub fn rec_w_polygon(buf: &mut Vec<u8>, v: &Polygon) {
130    for vertex in v.vertices.iter() {
131        rec_w_vec2(buf, *vertex);
132    }
133    for normal in v.normals.iter() {
134        rec_w_vec2(buf, *normal);
135    }
136    rec_w_vec2(buf, v.centroid);
137    rec_w_f32(buf, v.radius);
138    rec_w_i32(buf, v.count);
139}
140
141pub fn rec_w_chainseg(buf: &mut Vec<u8>, v: ChainSegment) {
142    rec_w_vec2(buf, v.ghost1);
143    rec_w_segment(buf, v.segment);
144    rec_w_vec2(buf, v.ghost2);
145    rec_w_i32(buf, v.chain_id);
146}
147
148pub fn rec_w_filter(buf: &mut Vec<u8>, v: Filter) {
149    rec_w_u64(buf, v.category_bits);
150    rec_w_u64(buf, v.mask_bits);
151    rec_w_i32(buf, v.group_index);
152}
153
154pub fn rec_w_material(buf: &mut Vec<u8>, v: SurfaceMaterial) {
155    rec_w_f32(buf, v.friction);
156    rec_w_f32(buf, v.restitution);
157    rec_w_f32(buf, v.rolling_resistance);
158    rec_w_f32(buf, v.tangent_speed);
159    rec_w_u64(buf, v.user_material_id);
160    rec_w_u32(buf, v.custom_color);
161}
162
163pub fn rec_w_massdata(buf: &mut Vec<u8>, v: MassData) {
164    rec_w_f32(buf, v.mass);
165    rec_w_vec2(buf, v.center);
166    rec_w_f32(buf, v.rotational_inertia);
167}
168
169pub fn rec_w_locks(buf: &mut Vec<u8>, v: MotionLocks) {
170    rec_w_bool(buf, v.linear_x);
171    rec_w_bool(buf, v.linear_y);
172    rec_w_bool(buf, v.angular_z);
173}
174
175/// Length-prefixed string; 0xFFFF marks NULL in C. Rust strings are never
176/// null, so that sentinel only appears when reading C recordings.
177/// (b2RecW_STR)
178pub fn rec_w_str(buf: &mut Vec<u8>, s: &str) {
179    let bytes = s.as_bytes();
180    let len = bytes.len().min(65534);
181    rec_w_u16(buf, len as u16);
182    buf.extend_from_slice(&bytes[..len]);
183}
184
185pub fn rec_w_explosiondef(buf: &mut Vec<u8>, v: &ExplosionDef) {
186    rec_w_u64(buf, v.mask_bits);
187    rec_w_position(buf, v.position);
188    rec_w_f32(buf, v.radius);
189    rec_w_f32(buf, v.falloff);
190    rec_w_f32(buf, v.impulse_per_length);
191}
192
193// Hand-written def helpers. userData and the validity cookie are not
194// serialized; readers start from b2Default*Def() and overwrite fields.
195
196pub fn rec_w_bodydef(buf: &mut Vec<u8>, v: &BodyDef) {
197    rec_w_i32(buf, v.type_ as i32);
198    rec_w_position(buf, v.position);
199    rec_w_rot(buf, v.rotation);
200    rec_w_vec2(buf, v.linear_velocity);
201    rec_w_f32(buf, v.angular_velocity);
202    rec_w_f32(buf, v.linear_damping);
203    rec_w_f32(buf, v.angular_damping);
204    rec_w_f32(buf, v.gravity_scale);
205    rec_w_f32(buf, v.sleep_threshold);
206    rec_w_str(buf, &v.name);
207    // userData: not preserved
208    rec_w_u64(buf, 0);
209    rec_w_locks(buf, v.motion_locks);
210    rec_w_bool(buf, v.enable_sleep);
211    rec_w_bool(buf, v.is_awake);
212    rec_w_bool(buf, v.is_bullet);
213    rec_w_bool(buf, v.is_enabled);
214    rec_w_bool(buf, v.allow_fast_rotation);
215    rec_w_bool(buf, v.enable_contact_recycling);
216}
217
218pub fn rec_w_shapedef(buf: &mut Vec<u8>, v: &ShapeDef) {
219    // userData: not preserved
220    rec_w_u64(buf, 0);
221    rec_w_material(buf, v.material);
222    rec_w_f32(buf, v.density);
223    rec_w_filter(buf, v.filter);
224    rec_w_bool(buf, v.enable_custom_filtering);
225    rec_w_bool(buf, v.is_sensor);
226    rec_w_bool(buf, v.enable_sensor_events);
227    rec_w_bool(buf, v.enable_contact_events);
228    rec_w_bool(buf, v.enable_hit_events);
229    rec_w_bool(buf, v.enable_pre_solve_events);
230    rec_w_bool(buf, v.invoke_contact_creation);
231    rec_w_bool(buf, v.update_body_mass);
232}
233
234/// Variable-length def: point and material arrays are length-prefixed and
235/// inlined. (b2RecW_CHAINDEF)
236pub fn rec_w_chaindef(buf: &mut Vec<u8>, v: &ChainDef) {
237    // userData: not preserved
238    rec_w_u64(buf, 0);
239    rec_w_i32(buf, v.points.len() as i32);
240    for point in v.points.iter() {
241        rec_w_vec2(buf, *point);
242    }
243    rec_w_i32(buf, v.materials.len() as i32);
244    for material in v.materials.iter() {
245        rec_w_material(buf, *material);
246    }
247    rec_w_filter(buf, v.filter);
248    rec_w_bool(buf, v.is_loop);
249    rec_w_bool(buf, v.enable_sensor_events);
250}
251
252/// Joint defs share a base. Body ids are written as ids and remapped to the
253/// replay world on read. (static b2RecW_JointBase)
254fn rec_w_joint_base(buf: &mut Vec<u8>, base: &JointDef) {
255    rec_w_u64(buf, 0); // userData
256    rec_w_bodyid(buf, base.body_id_a);
257    rec_w_bodyid(buf, base.body_id_b);
258    rec_w_xf(buf, base.local_frame_a);
259    rec_w_xf(buf, base.local_frame_b);
260    rec_w_f32(buf, base.force_threshold);
261    rec_w_f32(buf, base.torque_threshold);
262    rec_w_f32(buf, base.constraint_hertz);
263    rec_w_f32(buf, base.constraint_damping_ratio);
264    rec_w_f32(buf, base.draw_scale);
265    rec_w_bool(buf, base.collide_connected);
266}
267
268pub fn rec_w_distancejointdef(buf: &mut Vec<u8>, v: &DistanceJointDef) {
269    rec_w_joint_base(buf, &v.base);
270    rec_w_f32(buf, v.length);
271    rec_w_bool(buf, v.enable_spring);
272    rec_w_f32(buf, v.lower_spring_force);
273    rec_w_f32(buf, v.upper_spring_force);
274    rec_w_f32(buf, v.hertz);
275    rec_w_f32(buf, v.damping_ratio);
276    rec_w_bool(buf, v.enable_limit);
277    rec_w_f32(buf, v.min_length);
278    rec_w_f32(buf, v.max_length);
279    rec_w_bool(buf, v.enable_motor);
280    rec_w_f32(buf, v.max_motor_force);
281    rec_w_f32(buf, v.motor_speed);
282}
283
284pub fn rec_w_motorjointdef(buf: &mut Vec<u8>, v: &MotorJointDef) {
285    rec_w_joint_base(buf, &v.base);
286    rec_w_vec2(buf, v.linear_velocity);
287    rec_w_f32(buf, v.max_velocity_force);
288    rec_w_f32(buf, v.angular_velocity);
289    rec_w_f32(buf, v.max_velocity_torque);
290    rec_w_f32(buf, v.linear_hertz);
291    rec_w_f32(buf, v.linear_damping_ratio);
292    rec_w_f32(buf, v.max_spring_force);
293    rec_w_f32(buf, v.angular_hertz);
294    rec_w_f32(buf, v.angular_damping_ratio);
295    rec_w_f32(buf, v.max_spring_torque);
296}
297
298pub fn rec_w_filterjointdef(buf: &mut Vec<u8>, v: &FilterJointDef) {
299    rec_w_joint_base(buf, &v.base);
300}
301
302pub fn rec_w_prismaticjointdef(buf: &mut Vec<u8>, v: &PrismaticJointDef) {
303    rec_w_joint_base(buf, &v.base);
304    rec_w_bool(buf, v.enable_spring);
305    rec_w_f32(buf, v.hertz);
306    rec_w_f32(buf, v.damping_ratio);
307    rec_w_f32(buf, v.target_translation);
308    rec_w_bool(buf, v.enable_limit);
309    rec_w_f32(buf, v.lower_translation);
310    rec_w_f32(buf, v.upper_translation);
311    rec_w_bool(buf, v.enable_motor);
312    rec_w_f32(buf, v.max_motor_force);
313    rec_w_f32(buf, v.motor_speed);
314}
315
316pub fn rec_w_revolutejointdef(buf: &mut Vec<u8>, v: &RevoluteJointDef) {
317    rec_w_joint_base(buf, &v.base);
318    rec_w_f32(buf, v.target_angle);
319    rec_w_bool(buf, v.enable_spring);
320    rec_w_f32(buf, v.hertz);
321    rec_w_f32(buf, v.damping_ratio);
322    rec_w_bool(buf, v.enable_limit);
323    rec_w_f32(buf, v.lower_angle);
324    rec_w_f32(buf, v.upper_angle);
325    rec_w_bool(buf, v.enable_motor);
326    rec_w_f32(buf, v.max_motor_torque);
327    rec_w_f32(buf, v.motor_speed);
328}
329
330pub fn rec_w_weldjointdef(buf: &mut Vec<u8>, v: &WeldJointDef) {
331    rec_w_joint_base(buf, &v.base);
332    rec_w_f32(buf, v.linear_hertz);
333    rec_w_f32(buf, v.angular_hertz);
334    rec_w_f32(buf, v.linear_damping_ratio);
335    rec_w_f32(buf, v.angular_damping_ratio);
336}
337
338pub fn rec_w_wheeljointdef(buf: &mut Vec<u8>, v: &WheelJointDef) {
339    rec_w_joint_base(buf, &v.base);
340    rec_w_bool(buf, v.enable_spring);
341    rec_w_f32(buf, v.hertz);
342    rec_w_f32(buf, v.damping_ratio);
343    rec_w_bool(buf, v.enable_limit);
344    rec_w_f32(buf, v.lower_translation);
345    rec_w_f32(buf, v.upper_translation);
346    rec_w_bool(buf, v.enable_motor);
347    rec_w_f32(buf, v.max_motor_torque);
348    rec_w_f32(buf, v.motor_speed);
349}
350
351pub fn rec_w_aabb(buf: &mut Vec<u8>, v: Aabb) {
352    rec_w_vec2(buf, v.lower_bound);
353    rec_w_vec2(buf, v.upper_bound);
354}
355
356pub fn rec_w_queryfilter(buf: &mut Vec<u8>, v: QueryFilter) {
357    rec_w_u64(buf, v.category_bits);
358    rec_w_u64(buf, v.mask_bits);
359}
360
361pub fn rec_w_shapeproxy(buf: &mut Vec<u8>, v: &ShapeProxy) {
362    let count = v.count.clamp(0, MAX_POLYGON_VERTICES as i32);
363    rec_w_i32(buf, count);
364    for point in v.points[..count as usize].iter() {
365        rec_w_vec2(buf, *point);
366    }
367    rec_w_f32(buf, v.radius);
368}
369
370pub fn rec_w_worldcastoutput(buf: &mut Vec<u8>, v: WorldCastOutput) {
371    rec_w_vec2(buf, v.normal);
372    rec_w_position(buf, v.point);
373    rec_w_f32(buf, v.fraction);
374    rec_w_i32(buf, v.iterations);
375    rec_w_bool(buf, v.hit);
376}
377
378pub fn rec_w_rayresult(buf: &mut Vec<u8>, v: &RayResult) {
379    rec_w_shapeid(buf, v.shape_id);
380    rec_w_position(buf, v.point);
381    rec_w_vec2(buf, v.normal);
382    rec_w_f32(buf, v.fraction);
383    rec_w_i32(buf, v.node_visits);
384    rec_w_i32(buf, v.leaf_visits);
385    rec_w_bool(buf, v.hit);
386}
387
388pub fn rec_w_planeresult(buf: &mut Vec<u8>, v: &crate::collision::PlaneResult) {
389    rec_w_vec2(buf, v.plane.normal);
390    rec_w_f32(buf, v.plane.offset);
391    rec_w_vec2(buf, v.point);
392    rec_w_bool(buf, v.hit);
393}
394
395pub fn rec_w_treestats(buf: &mut Vec<u8>, v: TreeStats) {
396    rec_w_i32(buf, v.node_visits);
397    rec_w_i32(buf, v.leaf_visits);
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::recording::{rec_patch_u32, rec_reserve_u32, RecHeader, Recording};
404
405    // Wire layout: primitives are little-endian and framing backpatches the
406    // 24-bit payload size, matching the C format byte for byte.
407    #[test]
408    fn wire_primitives_and_framing() {
409        let mut buf = Vec::new();
410        rec_w_u16(&mut buf, 0x1234);
411        rec_w_u32(&mut buf, 0xAABBCCDD);
412        rec_w_f32(&mut buf, 1.0);
413        rec_w_bool(&mut buf, true);
414        assert_eq!(
415            buf,
416            [0x34, 0x12, 0xDD, 0xCC, 0xBB, 0xAA, 0x00, 0x00, 0x80, 0x3F, 0x01]
417        );
418
419        // Framing: opcode, u24 size, payload.
420        let mut rec = Recording::new(64);
421        rec.begin_record(0x80);
422        rec_w_f32(&mut rec.buffer, 1.0 / 60.0);
423        rec_w_i32(&mut rec.buffer, 4);
424        rec.end_record();
425        assert_eq!(rec.buffer[0], 0x80);
426        assert_eq!(&rec.buffer[1..4], &[8, 0, 0]); // 8-byte payload
427        assert_eq!(rec.buffer.len(), 12);
428
429        // commit_record writes the same framing in one shot.
430        let mut rec2 = Recording::new(64);
431        rec2.commit_record(0x80, &rec.buffer[4..12]);
432        assert_eq!(rec2.buffer, rec.buffer);
433
434        // Reserve/patch round trip.
435        let mut qbuf = Vec::new();
436        let offset = rec_reserve_u32(&mut qbuf);
437        rec_w_u8(&mut qbuf, 7);
438        rec_patch_u32(&mut qbuf, offset, 3);
439        assert_eq!(qbuf, [3, 0, 0, 0, 7]);
440    }
441
442    // The 32-byte header round trips through its exact C layout.
443    #[test]
444    fn header_round_trip() {
445        let header = RecHeader {
446            magic: crate::recording::REC_MAGIC,
447            version_major: crate::recording::REC_VERSION_MAJOR,
448            version_minor: crate::recording::REC_VERSION_MINOR,
449            length_scale: 1.0,
450            pointer_width: 8,
451            big_endian: 0,
452            validation_enabled: 1,
453            snapshot_size: 1234,
454        };
455        let mut buf = Vec::new();
456        header.write(&mut buf);
457        assert_eq!(buf.len(), RecHeader::SIZE);
458        assert_eq!(&buf[0..4], b"B2RC");
459        assert_eq!(RecHeader::read(&buf), Some(header));
460    }
461
462    // The string writer length-prefixes and truncates like C.
463    #[test]
464    fn string_writer() {
465        let mut buf = Vec::new();
466        rec_w_str(&mut buf, "box");
467        assert_eq!(buf, [3, 0, b'b', b'o', b'x']);
468
469        let mut empty = Vec::new();
470        rec_w_str(&mut empty, "");
471        assert_eq!(empty, [0, 0]);
472    }
473}