Skip to main content

alice_physics/solver/
world_snapshot.rs

1//! Whole-world snapshot: every piece of [`PhysicsWorld`] state that
2//! [`PhysicsWorld::step`] reads, in one versioned blob with one checksum.
3//!
4// LIMITATION(COV-ENGINE-011): keeps only the per-body motion and sleep state and expects the caller to rebuild joints, colliders, force fields, materials and so on by replay.
5//! [`PhysicsWorld::serialize_state`] (rollback netcode) keeps only the
6//! per-body motion and sleep state and expects the caller to rebuild
7//! joints, colliders, force fields, materials and so on by replay. This
8//! module is the other end: [`PhysicsWorld::snapshot_world`] writes the whole
9//! world, and [`PhysicsWorld::from_world_snapshot`] /
10//! [`PhysicsWorld::restore_world`] bring it back so that every later `step`
11//! is bit-identical to the original's (branching search, rollback without a
12//! replay log, persistence).
13//!
14//! Since version 4 the header also carries two content hashes: the
15//! stepping-semantics identifier of the binary that wrote the blob
16//! ([`crate::PHYSICS_SEMANTICS_ID`]) and the world's rule identifier
17//! ([`PhysicsWorld::law_id`] of that semantics). A reader refuses a version 4
18//! blob written under other stepping semantics
19//! ([`WorldSnapshotError::SemanticsMismatch`]), except one written under an
20//! earlier identifier listed in [`crate::PREVIOUS_PHYSICS_SEMANTICS_IDS`]
21//! (same stepping bits), and
22//! [`PhysicsWorld::restore_world_checked`] also compares the rule identifier
23//! with one the caller expects and reports what it could verify
24//! ([`LawCheck`]). Version 1 to 3 blobs carry neither hash; they are still
25//! read, and reported as [`LawCheck::Unpinned`].
26//!
27//! The field-by-field coverage, the format and the errors are documented on
28//! [`PhysicsWorld::snapshot_world`] and [`WorldSnapshotError`].
29
30use super::{fnv1a_fold, ConstraintBatch, ContactConstraint, DistanceConstraint, PhysicsWorld};
31use super::{BodyType, Broadphase, RigidBody, SolverBackend, SolverConfig};
32use crate::body_collider::BodyCollider;
33use crate::collider::{Capsule, Contact, ConvexHull, Sphere, AABB};
34use crate::compound::{CompoundChild, CompoundShape, ShapeRef};
35use crate::contact_cache::{BodyPairKey, CachedContactPoint, ContactCache, ContactManifold};
36use crate::event::{ContactEvent, ContactEventType, EventCollector, TriggerEvent};
37use crate::filter::CollisionFilter;
38use crate::force::{ForceField, ForceFieldInstance};
39use crate::joint::{
40    BallJoint, ConeTwistJoint, D6Joint, D6Motion, FixedJoint, HingeJoint, Joint, SliderJoint,
41    SpringJoint,
42};
43use crate::material::{
44    CombineRule, MaterialTable, PairOverride, PhysicsMaterial, MATERIAL_ID_CAPACITY,
45};
46use crate::math::{Fix128, QuatFix, Vec3Fix};
47use crate::semantics::{PHYSICS_SEMANTICS_ID, PREVIOUS_PHYSICS_SEMANTICS_IDS};
48use crate::shape::Shape;
49use crate::sleeping::{IslandManager, SleepConfig, SleepData, SleepState};
50use crate::static_collider::StaticCollider;
51use crate::world_participant::WorldFault;
52
53#[cfg(not(feature = "std"))]
54use alloc::collections::{BTreeMap, BTreeSet};
55#[cfg(not(feature = "std"))]
56use alloc::vec::Vec;
57#[cfg(feature = "std")]
58use std::collections::{BTreeMap, BTreeSet};
59
60/// Why [`PhysicsWorld::restore_world`] / [`PhysicsWorld::from_world_snapshot`]
61/// rejected a blob. The target world is unchanged in every case.
62///
63/// # Which input gives which error
64///
65/// | input | error |
66/// |---|---|
67/// | fewer than 24 bytes, a version 4 blob of fewer than 88 bytes, or shorter than the declared payload | [`Self::Truncated`] |
68/// | wrong magic | [`Self::BadMagic`] |
69/// | version 0 or above [`PhysicsWorld::WORLD_SNAPSHOT_VERSION`] | [`Self::UnsupportedVersion`] |
70/// | reserved bytes not 0 | [`Self::ReservedNotZero`] |
71/// | longer than the declared payload | [`Self::TrailingBytes`] |
72/// | checksum differs | [`Self::ChecksumMismatch`] |
73/// | version 4 header `semantics_id` differs from this binary's [`crate::PHYSICS_SEMANTICS_ID`] | [`Self::SemanticsMismatch`] |
74/// | version 4 header `law_id` differs from the one passed to [`PhysicsWorld::restore_world_checked`] | [`Self::LawIdMismatch`] |
75/// | unknown enum tag / non-boolean byte / unrepresentable value inside the payload | [`Self::InvalidValue`] |
76/// | material table with 0 or more than 65,536 materials, or a material whose id is not its index | [`Self::InvalidValue`] (`section: "material_table"`) |
77/// | a joint / constraint / contact / SDF collider / batch / proxy index past its target | [`Self::DanglingIndex`] |
78/// | target world holds a different number of SDF fields | [`Self::SdfFieldCountMismatch`] |
79/// | target world holds a different number of hooks / modifiers / bridges | [`Self::CallbackCountMismatch`] |
80/// | target world holds other participants (count, kinds, order) | [`Self::ParticipantMismatch`] |
81/// | a participant of the target world refuses its payload | [`Self::ParticipantState`] |
82/// | the target world declares other shared fields | [`Self::FieldState`] |
83/// | unknown fault code / participant fault tag, field mode or layout tag | [`Self::InvalidValue`] |
84///
85/// On every error the target world is left untouched.
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87#[non_exhaustive]
88pub enum WorldSnapshotError {
89    /// The blob ends before the header or the declared payload does.
90    Truncated,
91    /// The first 4 bytes are not [`PhysicsWorld::WORLD_SNAPSHOT_MAGIC`].
92    BadMagic,
93    /// The version is not one this build reads (1 to
94    /// [`PhysicsWorld::WORLD_SNAPSHOT_VERSION`]).
95    UnsupportedVersion {
96        /// Version stored in the blob.
97        found: u16,
98        /// Version this build reads.
99        supported: u16,
100    },
101    /// The reserved header bytes are not 0.
102    ReservedNotZero,
103    /// Bytes remain after the checksum, or after the last payload section.
104    TrailingBytes {
105        /// Number of unread bytes.
106        extra: usize,
107    },
108    /// The stored checksum differs from the one computed over the blob.
109    ChecksumMismatch {
110        /// Checksum stored in the blob.
111        stored: u64,
112        /// Checksum of the bytes as received.
113        computed: u64,
114    },
115    /// A value inside the payload is not valid for its field (unknown enum
116    /// tag, a boolean byte other than 0 / 1, a count that does not fit
117    /// `usize`, invalid metric weights, a material table holding 0 or more
118    /// than 65,536 materials or a material whose id is not its index).
119    InvalidValue {
120        /// Section the value was read in.
121        section: &'static str,
122    },
123    /// An index refers past the end of what it indexes (for example a joint
124    /// naming a body the snapshot does not hold).
125    DanglingIndex {
126        /// Section holding the index.
127        section: &'static str,
128        /// The index.
129        index: usize,
130        /// Length of what it indexes.
131        len: usize,
132    },
133    /// The snapshot holds a different number of SDF colliders than the target
134    /// world holds SDF fields (fields are code and are not in the blob).
135    SdfFieldCountMismatch {
136        /// SDF colliders in the snapshot.
137        snapshot: usize,
138        /// SDF colliders in the target world.
139        world: usize,
140    },
141    /// The snapshot was taken with a different number of pre-solve hooks,
142    /// contact modifiers or installed GPU bridges than the target world has.
143    CallbackCountMismatch {
144        /// `"pre_solve_hooks"`, `"contact_modifiers"` or `"gpu_solver_bridge"`.
145        section: &'static str,
146        /// Count in the snapshot.
147        snapshot: usize,
148        /// Count in the target world.
149        world: usize,
150    },
151    /// The target world's participants differ from the snapshot's in number,
152    /// kinds or order (participants are code and are not in the blob; the
153    /// target world must already hold the same ones).
154    ParticipantMismatch(crate::world_participant::ParticipantMismatch),
155    /// Participant `index` of the target world refused its payload
156    /// ([`crate::world_participant::Participant::check_state`]). Every
157    /// payload is checked before any is read.
158    ParticipantState {
159        /// Registration index of the participant.
160        index: usize,
161        /// Why it refused.
162        error: crate::world_participant::StateError,
163    },
164    /// The snapshot's shared fields do not match the target world's
165    /// declared fields (ids, modes, layouts;
166    /// [`crate::world_participant::FieldBoard::check_values`]).
167    FieldState(crate::world_participant::StateError),
168    /// A version 4 blob was written under other stepping semantics: its
169    /// header `semantics_id` is neither this binary's
170    /// [`crate::PHYSICS_SEMANTICS_ID`] nor one of
171    /// [`crate::PREVIOUS_PHYSICS_SEMANTICS_IDS`], so a restored world would
172    /// not step the way the writer's did. Checked after the checksum.
173    SemanticsMismatch {
174        /// `semantics_id` stored in the blob's header.
175        stored: [u8; 32],
176        /// [`crate::PHYSICS_SEMANTICS_ID`] of this binary.
177        expected: [u8; 32],
178    },
179    /// The version 4 blob's header `law_id` differs from the identifier
180    /// passed to [`PhysicsWorld::restore_world_checked`].
181    LawIdMismatch {
182        /// `law_id` stored in the blob's header.
183        stored: [u8; 32],
184        /// The identifier the caller expected.
185        expected: [u8; 32],
186    },
187}
188
189/// What [`PhysicsWorld::restore_world_checked`] could verify about the rule
190/// identifiers of a blob it accepted.
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192#[non_exhaustive]
193pub enum LawCheck {
194    /// Version 4: the header `semantics_id` equals this binary's
195    /// [`crate::PHYSICS_SEMANTICS_ID`], and the header `law_id` equals the
196    /// identifier the caller passed.
197    Verified,
198    /// Version 4: the header `semantics_id` equals this binary's
199    /// [`crate::PHYSICS_SEMANTICS_ID`]; the caller passed no identifier, so
200    /// the header `law_id` was not compared with anything.
201    SemanticsVerified,
202    /// Version 1 to 3: the blob carries no identifiers, so neither could be
203    /// verified. The blob is accepted; a caller that requires verification
204    /// can refuse it on this value.
205    Unpinned,
206    /// Version 4 written under an earlier identifier listed in
207    /// [`crate::PREVIOUS_PHYSICS_SEMANTICS_IDS`] (the stepping bits are the
208    /// same): the identifier the caller passed is this target's own
209    /// [`PhysicsWorld::law_id`] under [`crate::PHYSICS_SEMANTICS_ID`], and
210    /// the header `law_id` equals the target's `law_id` under the earlier
211    /// identifier. [`PhysicsWorld::world_snapshot_ids`] gives that
212    /// identifier.
213    VerifiedUnderPrevious,
214    /// Version 4 written under an earlier identifier listed in
215    /// [`crate::PREVIOUS_PHYSICS_SEMANTICS_IDS`]; the caller passed no
216    /// identifier, so the header `law_id` was not compared with anything.
217    /// [`PhysicsWorld::world_snapshot_ids`] gives the identifier.
218    SemanticsPrevious,
219}
220
221/// The `(semantics_id, law_id)` in the header of a version 4 world snapshot
222/// ([`PhysicsWorld::world_snapshot_ids`]).
223pub type SnapshotIds = ([u8; 32], [u8; 32]);
224
225/// Lower-case hex of a 32-byte identifier, for error messages.
226struct Hex<'a>(&'a [u8; 32]);
227
228impl core::fmt::Display for Hex<'_> {
229    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
230        for b in self.0 {
231            write!(f, "{b:02x}")?;
232        }
233        Ok(())
234    }
235}
236
237impl core::fmt::Display for WorldSnapshotError {
238    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
239        match self {
240            Self::Truncated => write!(f, "world snapshot is truncated"),
241            Self::BadMagic => write!(f, "not a world snapshot (bad magic)"),
242            Self::UnsupportedVersion { found, supported } => write!(
243                f,
244                "world snapshot version {found} is not supported (expected {supported})"
245            ),
246            Self::ReservedNotZero => write!(f, "world snapshot reserved header bytes are not 0"),
247            Self::TrailingBytes { extra } => {
248                write!(f, "world snapshot has {extra} trailing bytes")
249            }
250            Self::ChecksumMismatch { stored, computed } => write!(
251                f,
252                "world snapshot checksum mismatch (stored {stored:#018x}, computed {computed:#018x})"
253            ),
254            Self::InvalidValue { section } => {
255                write!(f, "world snapshot has an invalid value in {section}")
256            }
257            Self::DanglingIndex { section, index, len } => write!(
258                f,
259                "world snapshot {section} refers to index {index} of {len}"
260            ),
261            Self::SdfFieldCountMismatch { snapshot, world } => write!(
262                f,
263                "world snapshot has {snapshot} SDF colliders, target world holds {world} SDF fields"
264            ),
265            Self::CallbackCountMismatch {
266                section,
267                snapshot,
268                world,
269            } => write!(
270                f,
271                "world snapshot was taken with {snapshot} {section}, target world has {world}"
272            ),
273            Self::ParticipantMismatch(m) => write!(
274                f,
275                "world snapshot participants differ from the target world's: {m:?}"
276            ),
277            Self::ParticipantState { index, error } => write!(
278                f,
279                "participant {index} of the target world refused its snapshot payload: {error:?}"
280            ),
281            Self::FieldState(error) => write!(
282                f,
283                "world snapshot fields differ from the target world's: {error:?}"
284            ),
285            Self::SemanticsMismatch { stored, expected } => write!(
286                f,
287                "world snapshot was written under semantics {}, this binary steps under {}",
288                Hex(stored),
289                Hex(expected)
290            ),
291            Self::LawIdMismatch { stored, expected } => write!(
292                f,
293                "world snapshot law id {} differs from the expected {}",
294                Hex(stored),
295                Hex(expected)
296            ),
297        }
298    }
299}
300
301#[cfg(feature = "std")]
302impl std::error::Error for WorldSnapshotError {}
303
304type Res<T> = Result<T, WorldSnapshotError>;
305
306/// Header length of version 1 to 3 blobs, and the part every version shares.
307const HEADER_LEN: usize = 16;
308/// Header length of version 4 blobs: the shared 16 bytes, then
309/// `semantics_id` and `law_id` (32 bytes each).
310const HEADER_LEN_V4: usize = HEADER_LEN + 64;
311/// Offset of `semantics_id` in a version 4 header.
312const SEMANTICS_ID_AT: usize = HEADER_LEN;
313/// Offset of `law_id` in a version 4 header.
314const LAW_ID_AT: usize = HEADER_LEN + 32;
315
316/// Header length of a blob of `version` (already checked to be supported).
317fn header_len(version: u16) -> usize {
318    if version >= 4 {
319        HEADER_LEN_V4
320    } else {
321        HEADER_LEN
322    }
323}
324const CHECKSUM_LEN: usize = 8;
325
326// ── Writer ────────────────────────────────────────────────────────────────
327
328struct W(Vec<u8>);
329
330impl W {
331    fn u8(&mut self, v: u8) {
332        self.0.push(v);
333    }
334    fn bool(&mut self, v: bool) {
335        self.0.push(u8::from(v));
336    }
337    fn u16(&mut self, v: u16) {
338        self.0.extend_from_slice(&v.to_le_bytes());
339    }
340    fn u32(&mut self, v: u32) {
341        self.0.extend_from_slice(&v.to_le_bytes());
342    }
343    fn i32(&mut self, v: i32) {
344        self.0.extend_from_slice(&v.to_le_bytes());
345    }
346    fn u64(&mut self, v: u64) {
347        self.0.extend_from_slice(&v.to_le_bytes());
348    }
349    fn usize(&mut self, v: usize) {
350        self.u64(v as u64);
351    }
352    fn f32(&mut self, v: f32) {
353        self.u32(v.to_bits());
354    }
355    fn fix(&mut self, v: Fix128) {
356        self.0.extend_from_slice(&v.hi.to_le_bytes());
357        self.0.extend_from_slice(&v.lo.to_le_bytes());
358    }
359    fn vec3(&mut self, v: Vec3Fix) {
360        self.fix(v.x);
361        self.fix(v.y);
362        self.fix(v.z);
363    }
364    fn quat(&mut self, q: QuatFix) {
365        self.fix(q.x);
366        self.fix(q.y);
367        self.fix(q.z);
368        self.fix(q.w);
369    }
370    fn opt_fix(&mut self, v: Option<Fix128>) {
371        match v {
372            Some(x) => {
373                self.u8(1);
374                self.fix(x);
375            }
376            None => self.u8(0),
377        }
378    }
379    fn aabb(&mut self, b: AABB) {
380        self.vec3(b.min);
381        self.vec3(b.max);
382    }
383}
384
385// ── Reader ────────────────────────────────────────────────────────────────
386
387struct R<'a> {
388    data: &'a [u8],
389    pos: usize,
390}
391
392impl<'a> R<'a> {
393    fn take(&mut self, n: usize) -> Res<&'a [u8]> {
394        let end = self
395            .pos
396            .checked_add(n)
397            .ok_or(WorldSnapshotError::Truncated)?;
398        if end > self.data.len() {
399            return Err(WorldSnapshotError::Truncated);
400        }
401        let s = &self.data[self.pos..end];
402        self.pos = end;
403        Ok(s)
404    }
405    fn arr<const N: usize>(&mut self) -> Res<[u8; N]> {
406        let mut a = [0u8; N];
407        a.copy_from_slice(self.take(N)?);
408        Ok(a)
409    }
410    fn u8(&mut self) -> Res<u8> {
411        Ok(self.take(1)?[0])
412    }
413    fn bool(&mut self, section: &'static str) -> Res<bool> {
414        match self.u8()? {
415            0 => Ok(false),
416            1 => Ok(true),
417            _ => Err(WorldSnapshotError::InvalidValue { section }),
418        }
419    }
420    fn u16(&mut self) -> Res<u16> {
421        Ok(u16::from_le_bytes(self.arr()?))
422    }
423    fn u32(&mut self) -> Res<u32> {
424        Ok(u32::from_le_bytes(self.arr()?))
425    }
426    fn i32(&mut self) -> Res<i32> {
427        Ok(i32::from_le_bytes(self.arr()?))
428    }
429    fn u64(&mut self) -> Res<u64> {
430        Ok(u64::from_le_bytes(self.arr()?))
431    }
432    fn usize(&mut self, section: &'static str) -> Res<usize> {
433        usize::try_from(self.u64()?).map_err(|_| WorldSnapshotError::InvalidValue { section })
434    }
435    /// A element count: each element takes at least `min_size` bytes, so a
436    /// count larger than the rest of the blob allows is truncation (and is
437    /// rejected before anything is allocated for it).
438    fn len(&mut self, section: &'static str, min_size: usize) -> Res<usize> {
439        let n = self.usize(section)?;
440        let remaining = self.data.len() - self.pos;
441        if n.checked_mul(min_size.max(1))
442            .is_none_or(|need| need > remaining)
443        {
444            return Err(WorldSnapshotError::Truncated);
445        }
446        Ok(n)
447    }
448    fn f32(&mut self) -> Res<f32> {
449        Ok(f32::from_bits(self.u32()?))
450    }
451    fn fix(&mut self) -> Res<Fix128> {
452        let hi = i64::from_le_bytes(self.arr()?);
453        let lo = u64::from_le_bytes(self.arr()?);
454        Ok(Fix128 { hi, lo })
455    }
456    fn vec3(&mut self) -> Res<Vec3Fix> {
457        Ok(Vec3Fix::new(self.fix()?, self.fix()?, self.fix()?))
458    }
459    fn quat(&mut self) -> Res<QuatFix> {
460        Ok(QuatFix {
461            x: self.fix()?,
462            y: self.fix()?,
463            z: self.fix()?,
464            w: self.fix()?,
465        })
466    }
467    fn opt_fix(&mut self, section: &'static str) -> Res<Option<Fix128>> {
468        Ok(if self.bool(section)? {
469            Some(self.fix()?)
470        } else {
471            None
472        })
473    }
474    fn aabb(&mut self) -> Res<AABB> {
475        Ok(AABB {
476            min: self.vec3()?,
477            max: self.vec3()?,
478        })
479    }
480}
481
482const fn invalid(section: &'static str) -> WorldSnapshotError {
483    WorldSnapshotError::InvalidValue { section }
484}
485
486// ── Leaf types ────────────────────────────────────────────────────────────
487
488fn w_config(w: &mut W, c: &SolverConfig) {
489    w.usize(c.substeps);
490    w.usize(c.iterations);
491    w.vec3(c.gravity);
492    w.fix(c.damping);
493    w.fix(c.warm_start_factor);
494    w.u8(match c.solver_backend {
495        SolverBackend::Xpbd => 0,
496        SolverBackend::Tgs => 1,
497    });
498}
499
500fn r_config(r: &mut R<'_>) -> Res<SolverConfig> {
501    const S: &str = "config";
502    Ok(SolverConfig {
503        substeps: r.usize(S)?,
504        iterations: r.usize(S)?,
505        gravity: r.vec3()?,
506        damping: r.fix()?,
507        warm_start_factor: r.fix()?,
508        solver_backend: match r.u8()? {
509            0 => SolverBackend::Xpbd,
510            1 => SolverBackend::Tgs,
511            _ => return Err(invalid(S)),
512        },
513    })
514}
515
516fn w_body(w: &mut W, b: &RigidBody) {
517    w.vec3(b.position);
518    w.vec3(b.velocity);
519    w.fix(b.inv_mass);
520    w.vec3(b.inv_inertia);
521    w.vec3(b.prev_position);
522    w.quat(b.rotation);
523    w.vec3(b.angular_velocity);
524    w.quat(b.prev_rotation);
525    w.fix(b.restitution);
526    w.fix(b.friction);
527    w.fix(b.gravity_scale);
528    w.fix(b.linear_damping);
529    w.fix(b.angular_damping);
530    w.bool(b.is_sensor);
531    w.u8(b.body_type as u8);
532    match b.kinematic_target {
533        Some((p, q)) => {
534            w.u8(1);
535            w.vec3(p);
536            w.quat(q);
537        }
538        None => w.u8(0),
539    }
540}
541
542fn r_body(r: &mut R<'_>) -> Res<RigidBody> {
543    const S: &str = "bodies";
544    Ok(RigidBody {
545        position: r.vec3()?,
546        velocity: r.vec3()?,
547        inv_mass: r.fix()?,
548        inv_inertia: r.vec3()?,
549        prev_position: r.vec3()?,
550        rotation: r.quat()?,
551        angular_velocity: r.vec3()?,
552        prev_rotation: r.quat()?,
553        restitution: r.fix()?,
554        friction: r.fix()?,
555        gravity_scale: r.fix()?,
556        linear_damping: r.fix()?,
557        angular_damping: r.fix()?,
558        is_sensor: r.bool(S)?,
559        body_type: match r.u8()? {
560            0 => BodyType::Dynamic,
561            1 => BodyType::Static,
562            2 => BodyType::Kinematic,
563            _ => return Err(invalid(S)),
564        },
565        kinematic_target: if r.bool(S)? {
566            Some((r.vec3()?, r.quat()?))
567        } else {
568            None
569        },
570    })
571}
572
573fn w_contact(w: &mut W, c: &Contact) {
574    w.fix(c.depth);
575    w.vec3(c.normal);
576    w.vec3(c.point_a);
577    w.vec3(c.point_b);
578}
579
580fn r_contact(r: &mut R<'_>) -> Res<Contact> {
581    Ok(Contact {
582        depth: r.fix()?,
583        normal: r.vec3()?,
584        point_a: r.vec3()?,
585        point_b: r.vec3()?,
586    })
587}
588
589fn w_joint(w: &mut W, j: &Joint) {
590    match j {
591        Joint::Ball(j) => {
592            w.u8(0);
593            w.usize(j.body_a);
594            w.usize(j.body_b);
595            w.vec3(j.local_anchor_a);
596            w.vec3(j.local_anchor_b);
597            w.fix(j.compliance);
598            w.opt_fix(j.break_force);
599        }
600        Joint::Hinge(j) => {
601            w.u8(1);
602            w.usize(j.body_a);
603            w.usize(j.body_b);
604            w.vec3(j.local_anchor_a);
605            w.vec3(j.local_anchor_b);
606            w.vec3(j.local_axis_a);
607            w.vec3(j.local_axis_b);
608            w.opt_fix(j.angle_min);
609            w.opt_fix(j.angle_max);
610            w.fix(j.compliance);
611            w.fix(j.angular_compliance);
612            w.opt_fix(j.break_force);
613        }
614        Joint::Fixed(j) => {
615            w.u8(2);
616            w.usize(j.body_a);
617            w.usize(j.body_b);
618            w.vec3(j.local_anchor_a);
619            w.vec3(j.local_anchor_b);
620            w.quat(j.relative_rotation);
621            w.fix(j.compliance);
622            w.fix(j.angular_compliance);
623            w.opt_fix(j.break_force);
624        }
625        Joint::Slider(j) => {
626            w.u8(3);
627            w.usize(j.body_a);
628            w.usize(j.body_b);
629            w.vec3(j.local_axis);
630            w.vec3(j.local_anchor_a);
631            w.vec3(j.local_anchor_b);
632            w.opt_fix(j.limit_min);
633            w.opt_fix(j.limit_max);
634            w.fix(j.compliance);
635            w.opt_fix(j.break_force);
636        }
637        Joint::Spring(j) => {
638            w.u8(4);
639            w.usize(j.body_a);
640            w.usize(j.body_b);
641            w.vec3(j.local_anchor_a);
642            w.vec3(j.local_anchor_b);
643            w.fix(j.rest_length);
644            w.fix(j.stiffness);
645            w.fix(j.damping);
646            w.opt_fix(j.break_force);
647        }
648        Joint::D6(j) => {
649            w.u8(5);
650            w.usize(j.body_a);
651            w.usize(j.body_b);
652            w.vec3(j.local_anchor_a);
653            w.vec3(j.local_anchor_b);
654            w.quat(j.local_frame_a);
655            w.quat(j.local_frame_b);
656            for m in [
657                j.linear_x,
658                j.linear_y,
659                j.linear_z,
660                j.angular_x,
661                j.angular_y,
662                j.angular_z,
663            ] {
664                w.u8(match m {
665                    D6Motion::Locked => 0,
666                    D6Motion::Free => 1,
667                    D6Motion::Limited => 2,
668                });
669            }
670            w.vec3(j.linear_limit_min);
671            w.vec3(j.linear_limit_max);
672            w.vec3(j.angular_limit_min);
673            w.vec3(j.angular_limit_max);
674            w.fix(j.compliance);
675            w.fix(j.angular_compliance);
676            w.opt_fix(j.break_force);
677        }
678        Joint::ConeTwist(j) => {
679            w.u8(6);
680            w.usize(j.body_a);
681            w.usize(j.body_b);
682            w.vec3(j.local_anchor_a);
683            w.vec3(j.local_anchor_b);
684            w.vec3(j.twist_axis_a);
685            w.vec3(j.twist_axis_b);
686            w.fix(j.cone_limit);
687            w.fix(j.twist_limit);
688            w.fix(j.compliance);
689            w.fix(j.angular_compliance);
690            w.opt_fix(j.break_force);
691        }
692    }
693}
694
695fn r_d6_motion(r: &mut R<'_>) -> Res<D6Motion> {
696    match r.u8()? {
697        0 => Ok(D6Motion::Locked),
698        1 => Ok(D6Motion::Free),
699        2 => Ok(D6Motion::Limited),
700        _ => Err(invalid("joints")),
701    }
702}
703
704fn r_joint(r: &mut R<'_>) -> Res<Joint> {
705    const S: &str = "joints";
706    Ok(match r.u8()? {
707        0 => Joint::Ball(BallJoint {
708            body_a: r.usize(S)?,
709            body_b: r.usize(S)?,
710            local_anchor_a: r.vec3()?,
711            local_anchor_b: r.vec3()?,
712            compliance: r.fix()?,
713            break_force: r.opt_fix(S)?,
714        }),
715        1 => Joint::Hinge(HingeJoint {
716            body_a: r.usize(S)?,
717            body_b: r.usize(S)?,
718            local_anchor_a: r.vec3()?,
719            local_anchor_b: r.vec3()?,
720            local_axis_a: r.vec3()?,
721            local_axis_b: r.vec3()?,
722            angle_min: r.opt_fix(S)?,
723            angle_max: r.opt_fix(S)?,
724            compliance: r.fix()?,
725            angular_compliance: r.fix()?,
726            break_force: r.opt_fix(S)?,
727        }),
728        2 => Joint::Fixed(FixedJoint {
729            body_a: r.usize(S)?,
730            body_b: r.usize(S)?,
731            local_anchor_a: r.vec3()?,
732            local_anchor_b: r.vec3()?,
733            relative_rotation: r.quat()?,
734            compliance: r.fix()?,
735            angular_compliance: r.fix()?,
736            break_force: r.opt_fix(S)?,
737        }),
738        3 => Joint::Slider(SliderJoint {
739            body_a: r.usize(S)?,
740            body_b: r.usize(S)?,
741            local_axis: r.vec3()?,
742            local_anchor_a: r.vec3()?,
743            local_anchor_b: r.vec3()?,
744            limit_min: r.opt_fix(S)?,
745            limit_max: r.opt_fix(S)?,
746            compliance: r.fix()?,
747            break_force: r.opt_fix(S)?,
748        }),
749        4 => Joint::Spring(SpringJoint {
750            body_a: r.usize(S)?,
751            body_b: r.usize(S)?,
752            local_anchor_a: r.vec3()?,
753            local_anchor_b: r.vec3()?,
754            rest_length: r.fix()?,
755            stiffness: r.fix()?,
756            damping: r.fix()?,
757            break_force: r.opt_fix(S)?,
758        }),
759        5 => Joint::D6(D6Joint {
760            body_a: r.usize(S)?,
761            body_b: r.usize(S)?,
762            local_anchor_a: r.vec3()?,
763            local_anchor_b: r.vec3()?,
764            local_frame_a: r.quat()?,
765            local_frame_b: r.quat()?,
766            linear_x: r_d6_motion(r)?,
767            linear_y: r_d6_motion(r)?,
768            linear_z: r_d6_motion(r)?,
769            angular_x: r_d6_motion(r)?,
770            angular_y: r_d6_motion(r)?,
771            angular_z: r_d6_motion(r)?,
772            linear_limit_min: r.vec3()?,
773            linear_limit_max: r.vec3()?,
774            angular_limit_min: r.vec3()?,
775            angular_limit_max: r.vec3()?,
776            compliance: r.fix()?,
777            angular_compliance: r.fix()?,
778            break_force: r.opt_fix(S)?,
779        }),
780        6 => Joint::ConeTwist(ConeTwistJoint {
781            body_a: r.usize(S)?,
782            body_b: r.usize(S)?,
783            local_anchor_a: r.vec3()?,
784            local_anchor_b: r.vec3()?,
785            twist_axis_a: r.vec3()?,
786            twist_axis_b: r.vec3()?,
787            cone_limit: r.fix()?,
788            twist_limit: r.fix()?,
789            compliance: r.fix()?,
790            angular_compliance: r.fix()?,
791            break_force: r.opt_fix(S)?,
792        }),
793        _ => return Err(invalid(S)),
794    })
795}
796
797fn w_force_field(w: &mut W, f: &ForceFieldInstance) {
798    match f.field {
799        ForceField::Directional {
800            direction,
801            strength,
802        } => {
803            w.u8(0);
804            w.vec3(direction);
805            w.fix(strength);
806        }
807        ForceField::Point {
808            center,
809            strength,
810            repulsive,
811            max_force,
812        } => {
813            w.u8(1);
814            w.vec3(center);
815            w.fix(strength);
816            w.bool(repulsive);
817            w.fix(max_force);
818        }
819        ForceField::Drag { coefficient } => {
820            w.u8(2);
821            w.fix(coefficient);
822        }
823        ForceField::Buoyancy {
824            surface_y,
825            density,
826            drag,
827        } => {
828            w.u8(3);
829            w.fix(surface_y);
830            w.fix(density);
831            w.fix(drag);
832        }
833        ForceField::Vortex {
834            center,
835            axis,
836            strength,
837            falloff_radius,
838        } => {
839            w.u8(4);
840            w.vec3(center);
841            w.vec3(axis);
842            w.fix(strength);
843            w.fix(falloff_radius);
844        }
845        ForceField::Explosion {
846            center,
847            strength,
848            radius,
849            falloff_power,
850        } => {
851            w.u8(5);
852            w.vec3(center);
853            w.fix(strength);
854            w.fix(radius);
855            w.fix(falloff_power);
856        }
857        ForceField::Magnetic {
858            position,
859            moment,
860            strength,
861        } => {
862            w.u8(6);
863            w.vec3(position);
864            w.vec3(moment);
865            w.fix(strength);
866        }
867    }
868    match &f.affected_bodies {
869        Some(list) => {
870            w.u8(1);
871            w.usize(list.len());
872            for &i in list {
873                w.usize(i);
874            }
875        }
876        None => w.u8(0),
877    }
878    w.bool(f.enabled);
879}
880
881fn r_force_field(r: &mut R<'_>) -> Res<ForceFieldInstance> {
882    const S: &str = "force_fields";
883    let field = match r.u8()? {
884        0 => ForceField::Directional {
885            direction: r.vec3()?,
886            strength: r.fix()?,
887        },
888        1 => ForceField::Point {
889            center: r.vec3()?,
890            strength: r.fix()?,
891            repulsive: r.bool(S)?,
892            max_force: r.fix()?,
893        },
894        2 => ForceField::Drag {
895            coefficient: r.fix()?,
896        },
897        3 => ForceField::Buoyancy {
898            surface_y: r.fix()?,
899            density: r.fix()?,
900            drag: r.fix()?,
901        },
902        4 => ForceField::Vortex {
903            center: r.vec3()?,
904            axis: r.vec3()?,
905            strength: r.fix()?,
906            falloff_radius: r.fix()?,
907        },
908        5 => ForceField::Explosion {
909            center: r.vec3()?,
910            strength: r.fix()?,
911            radius: r.fix()?,
912            falloff_power: r.fix()?,
913        },
914        6 => ForceField::Magnetic {
915            position: r.vec3()?,
916            moment: r.vec3()?,
917            strength: r.fix()?,
918        },
919        _ => return Err(invalid(S)),
920    };
921    let affected_bodies = if r.bool(S)? {
922        let n = r.len(S, 8)?;
923        let mut v = Vec::with_capacity(n);
924        for _ in 0..n {
925            v.push(r.usize(S)?);
926        }
927        Some(v)
928    } else {
929        None
930    };
931    Ok(ForceFieldInstance {
932        field,
933        affected_bodies,
934        enabled: r.bool(S)?,
935    })
936}
937
938fn w_static_collider(w: &mut W, c: &StaticCollider) {
939    match c {
940        StaticCollider::Plane(p) => {
941            w.u8(0);
942            w.vec3(p.normal);
943            w.fix(p.offset);
944        }
945        StaticCollider::HeightField(h) => {
946            w.u8(1);
947            w.usize(h.heights.len());
948            for &v in &h.heights {
949                w.fix(v);
950            }
951            w.u32(h.width);
952            w.u32(h.depth);
953            w.fix(h.spacing);
954            w.vec3(h.origin);
955        }
956        StaticCollider::TriMesh(m) => {
957            w.u8(2);
958            w.usize(m.triangles.len());
959            for t in &m.triangles {
960                w.vec3(t.v0);
961                w.vec3(t.v1);
962                w.vec3(t.v2);
963            }
964            w.usize(m.bvh.nodes.len());
965            for n in &m.bvh.nodes {
966                for v in n.aabb_min {
967                    w.i32(v);
968                }
969                w.u32(n.first_child_or_prim);
970                for v in n.aabb_max {
971                    w.i32(v);
972                }
973                w.u32(n.prim_count_escape);
974            }
975            w.usize(m.bvh.primitives.len());
976            for &p in &m.bvh.primitives {
977                w.u32(p);
978            }
979            w.aabb(m.bvh.bounds);
980            w.aabb(m.bounds);
981        }
982    }
983}
984
985fn r_static_collider(r: &mut R<'_>) -> Res<StaticCollider> {
986    const S: &str = "static_colliders";
987    Ok(match r.u8()? {
988        0 => StaticCollider::Plane(crate::plane_collider::PlaneCollider {
989            normal: r.vec3()?,
990            offset: r.fix()?,
991        }),
992        1 => {
993            let n = r.len(S, 16)?;
994            let mut heights = Vec::with_capacity(n);
995            for _ in 0..n {
996                heights.push(r.fix()?);
997            }
998            StaticCollider::HeightField(crate::heightfield::HeightField {
999                heights,
1000                width: r.u32()?,
1001                depth: r.u32()?,
1002                spacing: r.fix()?,
1003                origin: r.vec3()?,
1004            })
1005        }
1006        2 => {
1007            let n = r.len(S, 144)?;
1008            let mut triangles = Vec::with_capacity(n);
1009            for _ in 0..n {
1010                triangles.push(crate::trimesh::Triangle {
1011                    v0: r.vec3()?,
1012                    v1: r.vec3()?,
1013                    v2: r.vec3()?,
1014                });
1015            }
1016            let n = r.len(S, 32)?;
1017            let mut nodes = Vec::with_capacity(n);
1018            for _ in 0..n {
1019                let aabb_min = [r.i32()?, r.i32()?, r.i32()?];
1020                let first_child_or_prim = r.u32()?;
1021                let aabb_max = [r.i32()?, r.i32()?, r.i32()?];
1022                let prim_count_escape = r.u32()?;
1023                nodes.push(crate::bvh::BvhNode {
1024                    aabb_min,
1025                    first_child_or_prim,
1026                    aabb_max,
1027                    prim_count_escape,
1028                });
1029            }
1030            let n = r.len(S, 4)?;
1031            let mut primitives = Vec::with_capacity(n);
1032            for _ in 0..n {
1033                primitives.push(r.u32()?);
1034            }
1035            let bvh = crate::bvh::LinearBvh {
1036                nodes,
1037                primitives,
1038                bounds: r.aabb()?,
1039            };
1040            StaticCollider::TriMesh(crate::trimesh::TriMesh {
1041                triangles,
1042                bvh,
1043                bounds: r.aabb()?,
1044            })
1045        }
1046        _ => return Err(invalid(S)),
1047    })
1048}
1049
1050fn w_shape(w: &mut W, s: &Shape) {
1051    match *s {
1052        Shape::Box { half_extents } => {
1053            w.u8(0);
1054            w.vec3(half_extents);
1055        }
1056        Shape::Cylinder {
1057            radius,
1058            half_height,
1059        } => {
1060            w.u8(1);
1061            w.fix(radius);
1062            w.fix(half_height);
1063        }
1064        Shape::Cone {
1065            radius,
1066            half_height,
1067        } => {
1068            w.u8(2);
1069            w.fix(radius);
1070            w.fix(half_height);
1071        }
1072        Shape::Ellipsoid { radii } => {
1073            w.u8(3);
1074            w.vec3(radii);
1075        }
1076        Shape::Wedge {
1077            width,
1078            height,
1079            depth,
1080        } => {
1081            w.u8(4);
1082            w.fix(width);
1083            w.fix(height);
1084            w.fix(depth);
1085        }
1086        Shape::Torus {
1087            major_radius,
1088            minor_radius,
1089        } => {
1090            w.u8(5);
1091            w.fix(major_radius);
1092            w.fix(minor_radius);
1093        }
1094    }
1095}
1096
1097fn r_shape(r: &mut R<'_>) -> Res<Shape> {
1098    Ok(match r.u8()? {
1099        0 => Shape::Box {
1100            half_extents: r.vec3()?,
1101        },
1102        1 => Shape::Cylinder {
1103            radius: r.fix()?,
1104            half_height: r.fix()?,
1105        },
1106        2 => Shape::Cone {
1107            radius: r.fix()?,
1108            half_height: r.fix()?,
1109        },
1110        3 => Shape::Ellipsoid { radii: r.vec3()? },
1111        4 => Shape::Wedge {
1112            width: r.fix()?,
1113            height: r.fix()?,
1114            depth: r.fix()?,
1115        },
1116        5 => Shape::Torus {
1117            major_radius: r.fix()?,
1118            minor_radius: r.fix()?,
1119        },
1120        _ => return Err(invalid("body_colliders")),
1121    })
1122}
1123
1124fn w_body_collider(w: &mut W, c: Option<&BodyCollider>) {
1125    match c {
1126        None => w.u8(0),
1127        Some(BodyCollider::Shape(s)) => {
1128            w.u8(1);
1129            w_shape(w, s);
1130        }
1131        Some(BodyCollider::Compound(c)) => {
1132            w.u8(2);
1133            w.usize(c.children.len());
1134            for ch in &c.children {
1135                match &ch.shape {
1136                    ShapeRef::Sphere(s) => {
1137                        w.u8(0);
1138                        w.vec3(s.center);
1139                        w.fix(s.radius);
1140                    }
1141                    ShapeRef::Capsule(s) => {
1142                        w.u8(1);
1143                        w.vec3(s.a);
1144                        w.vec3(s.b);
1145                        w.fix(s.radius);
1146                    }
1147                    ShapeRef::Box(b) => {
1148                        w.u8(2);
1149                        w.vec3(b.center);
1150                        w.vec3(b.half_extents);
1151                        w.quat(b.rotation);
1152                    }
1153                    ShapeRef::ConvexHull(h) => {
1154                        w.u8(3);
1155                        w.usize(h.vertices.len());
1156                        for &v in &h.vertices {
1157                            w.vec3(v);
1158                        }
1159                    }
1160                }
1161                w.vec3(ch.local_position);
1162                w.quat(ch.local_rotation);
1163            }
1164            w.aabb(c.cached_aabb);
1165            w.bool(c.dirty);
1166        }
1167    }
1168}
1169
1170fn r_body_collider(r: &mut R<'_>) -> Res<Option<BodyCollider>> {
1171    const S: &str = "body_colliders";
1172    Ok(match r.u8()? {
1173        0 => None,
1174        1 => Some(BodyCollider::Shape(r_shape(r)?)),
1175        2 => {
1176            let n = r.len(S, 1)?;
1177            let mut children = Vec::with_capacity(n);
1178            for _ in 0..n {
1179                let shape = match r.u8()? {
1180                    0 => ShapeRef::Sphere(Sphere {
1181                        center: r.vec3()?,
1182                        radius: r.fix()?,
1183                    }),
1184                    1 => ShapeRef::Capsule(Capsule {
1185                        a: r.vec3()?,
1186                        b: r.vec3()?,
1187                        radius: r.fix()?,
1188                    }),
1189                    2 => ShapeRef::Box(crate::box_collider::OrientedBox {
1190                        center: r.vec3()?,
1191                        half_extents: r.vec3()?,
1192                        rotation: r.quat()?,
1193                    }),
1194                    3 => {
1195                        let m = r.len(S, 48)?;
1196                        let mut vertices = Vec::with_capacity(m);
1197                        for _ in 0..m {
1198                            vertices.push(r.vec3()?);
1199                        }
1200                        ShapeRef::ConvexHull(ConvexHull { vertices })
1201                    }
1202                    _ => return Err(invalid(S)),
1203                };
1204                children.push(CompoundChild {
1205                    shape,
1206                    local_position: r.vec3()?,
1207                    local_rotation: r.quat()?,
1208                });
1209            }
1210            let mut c = CompoundShape::new();
1211            c.children = children;
1212            c.cached_aabb = r.aabb()?;
1213            c.dirty = r.bool(S)?;
1214            Some(BodyCollider::Compound(c))
1215        }
1216        _ => return Err(invalid(S)),
1217    })
1218}
1219
1220fn w_contact_cache(w: &mut W, c: &ContactCache) {
1221    w.usize(c.manifolds.len());
1222    for m in &c.manifolds {
1223        w.u32(m.pair.body_a);
1224        w.u32(m.pair.body_b);
1225        w.usize(m.points.len());
1226        for p in &m.points {
1227            w.vec3(p.local_point_a);
1228            w.vec3(p.local_point_b);
1229            w.vec3(p.normal);
1230            w.fix(p.depth);
1231            w.fix(p.lambda_n);
1232            w.fix(p.lambda_t1);
1233            w.fix(p.lambda_t2);
1234            w.u32(p.age);
1235        }
1236        w.vec3(m.normal);
1237        w.fix(m.friction);
1238        w.fix(m.restitution);
1239        w.u32(m.stale_frames);
1240    }
1241    w.u32(c.max_stale_frames);
1242    w.fix(c.warm_start_factor);
1243}
1244
1245fn r_contact_cache(r: &mut R<'_>) -> Res<ContactCache> {
1246    const S: &str = "contact_cache";
1247    let n = r.len(S, 8)?;
1248    let mut manifolds = Vec::with_capacity(n);
1249    for _ in 0..n {
1250        let pair = BodyPairKey {
1251            body_a: r.u32()?,
1252            body_b: r.u32()?,
1253        };
1254        let k = r.len(S, 212)?;
1255        let mut points = Vec::with_capacity(k);
1256        for _ in 0..k {
1257            points.push(CachedContactPoint {
1258                local_point_a: r.vec3()?,
1259                local_point_b: r.vec3()?,
1260                normal: r.vec3()?,
1261                depth: r.fix()?,
1262                lambda_n: r.fix()?,
1263                lambda_t1: r.fix()?,
1264                lambda_t2: r.fix()?,
1265                age: r.u32()?,
1266            });
1267        }
1268        manifolds.push(ContactManifold {
1269            pair,
1270            points,
1271            normal: r.vec3()?,
1272            friction: r.fix()?,
1273            restitution: r.fix()?,
1274            stale_frames: r.u32()?,
1275        });
1276    }
1277    let mut cache = ContactCache::new();
1278    cache.manifolds = manifolds;
1279    cache.max_stale_frames = r.u32()?;
1280    cache.warm_start_factor = r.fix()?;
1281    // Rebuilt (not saved): the same index `ContactCache::end_frame` builds.
1282    #[cfg(feature = "std")]
1283    {
1284        cache.pair_index.clear();
1285        for (idx, m) in cache.manifolds.iter().enumerate() {
1286            cache.pair_index.insert(m.pair, idx);
1287        }
1288    }
1289    Ok(cache)
1290}
1291
1292const fn combine_tag(c: CombineRule) -> u8 {
1293    match c {
1294        CombineRule::Average => 0,
1295        CombineRule::Min => 1,
1296        CombineRule::Max => 2,
1297        CombineRule::Multiply => 3,
1298    }
1299}
1300
1301fn r_combine(r: &mut R<'_>) -> Res<CombineRule> {
1302    match r.u8()? {
1303        0 => Ok(CombineRule::Average),
1304        1 => Ok(CombineRule::Min),
1305        2 => Ok(CombineRule::Max),
1306        3 => Ok(CombineRule::Multiply),
1307        _ => Err(invalid("material_table")),
1308    }
1309}
1310
1311fn w_material_table(w: &mut W, t: &MaterialTable) {
1312    w.usize(t.materials.len());
1313    for m in &t.materials {
1314        w.u16(m.id);
1315        w.fix(m.static_friction);
1316        w.fix(m.dynamic_friction);
1317        w.fix(m.restitution);
1318        w.u8(combine_tag(m.friction_combine));
1319        w.u8(combine_tag(m.restitution_combine));
1320    }
1321    w.usize(t.pair_overrides.len());
1322    for p in &t.pair_overrides {
1323        w.u16(p.mat_a);
1324        w.u16(p.mat_b);
1325        w.fix(p.friction);
1326        w.fix(p.restitution);
1327    }
1328    w.u8(combine_tag(t.default_friction_combine));
1329    w.u8(combine_tag(t.default_restitution_combine));
1330}
1331
1332/// Reads the material table and rejects any table the `MaterialTable` API
1333/// cannot build: `MaterialTable::new` puts the default material at id 0 and
1334/// `try_register` assigns `id = len` up to 65,536 ids, so a valid table holds
1335/// `1..=65_536` materials with `materials[i].id == i` (`get` indexes by
1336/// position and falls back to entry 0, so an empty table panics there).
1337fn r_material_table(r: &mut R<'_>) -> Res<MaterialTable> {
1338    const S: &str = "material_table";
1339    let n = r.len(S, 52)?;
1340    // `n > 65_536` is also caught by the id check below (an id is a `u16`);
1341    // checking it here rejects before reading the entries.
1342    if n == 0 || n > MATERIAL_ID_CAPACITY {
1343        return Err(invalid(S));
1344    }
1345    let mut materials = Vec::with_capacity(n);
1346    for i in 0..n {
1347        let id = r.u16()?;
1348        if usize::from(id) != i {
1349            return Err(invalid(S));
1350        }
1351        materials.push(PhysicsMaterial {
1352            id,
1353            static_friction: r.fix()?,
1354            dynamic_friction: r.fix()?,
1355            restitution: r.fix()?,
1356            friction_combine: r_combine(r)?,
1357            restitution_combine: r_combine(r)?,
1358        });
1359    }
1360    let n = r.len(S, 36)?;
1361    let mut pair_overrides = Vec::with_capacity(n);
1362    for _ in 0..n {
1363        pair_overrides.push(PairOverride {
1364            mat_a: r.u16()?,
1365            mat_b: r.u16()?,
1366            friction: r.fix()?,
1367            restitution: r.fix()?,
1368        });
1369    }
1370    let mut t = MaterialTable::new();
1371    t.materials = materials;
1372    t.pair_overrides = pair_overrides;
1373    t.default_friction_combine = r_combine(r)?;
1374    t.default_restitution_combine = r_combine(r)?;
1375    Ok(t)
1376}
1377
1378fn w_pair(w: &mut W, p: (usize, usize)) {
1379    w.usize(p.0);
1380    w.usize(p.1);
1381}
1382
1383fn r_pair(r: &mut R<'_>) -> Res<(usize, usize)> {
1384    Ok((r.usize("events")?, r.usize("events")?))
1385}
1386
1387fn w_events(w: &mut W, e: &EventCollector) {
1388    w.usize(e.contact_events.len());
1389    for c in &e.contact_events {
1390        w.usize(c.body_a);
1391        w.usize(c.body_b);
1392        w.u8(match c.event_type {
1393            ContactEventType::Begin => 0,
1394            ContactEventType::Persist => 1,
1395            ContactEventType::End => 2,
1396        });
1397        w.vec3(c.normal);
1398        w.vec3(c.point);
1399        w.fix(c.depth);
1400        w.fix(c.relative_velocity);
1401    }
1402    w.usize(e.trigger_events.len());
1403    for t in &e.trigger_events {
1404        w.usize(t.trigger_body);
1405        w.usize(t.other_body);
1406        w.bool(t.entered);
1407    }
1408    w.usize(e.prev_pairs.len());
1409    for &p in &e.prev_pairs {
1410        w_pair(w, p);
1411    }
1412    w.usize(e.curr_pairs.len());
1413    for &p in &e.curr_pairs {
1414        w_pair(w, p);
1415    }
1416    w.usize(e.prev_triggers.len());
1417    for &(k, v) in &e.prev_triggers {
1418        w_pair(w, k);
1419        w_pair(w, v);
1420    }
1421    w.usize(e.curr_triggers.len());
1422    for (&k, &v) in &e.curr_triggers {
1423        w_pair(w, k);
1424        w_pair(w, v);
1425    }
1426}
1427
1428fn r_events(r: &mut R<'_>) -> Res<EventCollector> {
1429    const S: &str = "events";
1430    let mut e = EventCollector::new();
1431    let n = r.len(S, 145)?;
1432    for _ in 0..n {
1433        e.contact_events.push(ContactEvent {
1434            body_a: r.usize(S)?,
1435            body_b: r.usize(S)?,
1436            event_type: match r.u8()? {
1437                0 => ContactEventType::Begin,
1438                1 => ContactEventType::Persist,
1439                2 => ContactEventType::End,
1440                _ => return Err(invalid(S)),
1441            },
1442            normal: r.vec3()?,
1443            point: r.vec3()?,
1444            depth: r.fix()?,
1445            relative_velocity: r.fix()?,
1446        });
1447    }
1448    let n = r.len(S, 17)?;
1449    for _ in 0..n {
1450        e.trigger_events.push(TriggerEvent {
1451            trigger_body: r.usize(S)?,
1452            other_body: r.usize(S)?,
1453            entered: r.bool(S)?,
1454        });
1455    }
1456    let n = r.len(S, 16)?;
1457    for _ in 0..n {
1458        e.prev_pairs.push(r_pair(r)?);
1459    }
1460    let n = r.len(S, 16)?;
1461    let mut curr_pairs = BTreeSet::new();
1462    for _ in 0..n {
1463        curr_pairs.insert(r_pair(r)?);
1464    }
1465    e.curr_pairs = curr_pairs;
1466    let n = r.len(S, 32)?;
1467    for _ in 0..n {
1468        let k = r_pair(r)?;
1469        let v = r_pair(r)?;
1470        e.prev_triggers.push((k, v));
1471    }
1472    let n = r.len(S, 32)?;
1473    let mut curr_triggers = BTreeMap::new();
1474    for _ in 0..n {
1475        let k = r_pair(r)?;
1476        let v = r_pair(r)?;
1477        curr_triggers.insert(k, v);
1478    }
1479    e.curr_triggers = curr_triggers;
1480    Ok(e)
1481}
1482
1483fn w_tree(w: &mut W, t: &crate::dynamic_bvh::DynamicAabbTree) {
1484    w.usize(t.nodes.len());
1485    for n in &t.nodes {
1486        w.aabb(n.aabb);
1487        w.u32(n.parent);
1488        w.u32(n.left);
1489        w.u32(n.right);
1490        w.i32(n.height);
1491        w.u32(n.user_data);
1492        w.bool(n.is_leaf);
1493    }
1494    w.usize(t.free_list.len());
1495    for &f in &t.free_list {
1496        w.u32(f);
1497    }
1498    w.u32(t.root);
1499    w.fix(t.margin);
1500    let (l1, l2, linf) = t.metric.weights();
1501    w.fix(l1);
1502    w.fix(l2);
1503    w.fix(linf);
1504}
1505
1506fn r_tree(r: &mut R<'_>) -> Res<crate::dynamic_bvh::DynamicAabbTree> {
1507    const S: &str = "broadphase_tree";
1508    let mut t = crate::dynamic_bvh::DynamicAabbTree::new();
1509    let n = r.len(S, 117)?;
1510    for _ in 0..n {
1511        t.nodes.push(crate::dynamic_bvh::DynamicNode {
1512            aabb: r.aabb()?,
1513            parent: r.u32()?,
1514            left: r.u32()?,
1515            right: r.u32()?,
1516            height: r.i32()?,
1517            user_data: r.u32()?,
1518            is_leaf: r.bool(S)?,
1519        });
1520    }
1521    let n = r.len(S, 4)?;
1522    for _ in 0..n {
1523        t.free_list.push(r.u32()?);
1524    }
1525    t.root = r.u32()?;
1526    t.margin = r.fix()?;
1527    let (l1, l2, linf) = (r.fix()?, r.fix()?, r.fix()?);
1528    t.metric = crate::metric::MetricWeights::new(l1, l2, linf).map_err(|_| invalid(S))?;
1529    Ok(t)
1530}
1531
1532// ── Whole world ───────────────────────────────────────────────────────────
1533
1534/// Everything decoded from a blob, before it is moved into a world.
1535struct Decoded {
1536    config: SolverConfig,
1537    bodies: Vec<RigidBody>,
1538    distance_constraints: Vec<DistanceConstraint>,
1539    contact_constraints: Vec<ContactConstraint>,
1540    /// position, rotation, scale, body index, inv_rotation, scale_f32, inv_scale_f32
1541    sdf_poses: Vec<(Vec3Fix, QuatFix, Fix128, usize, QuatFix, f32, f32)>,
1542    sdf_collision_radius: Fix128,
1543    static_colliders: Vec<StaticCollider>,
1544    constraint_batches: Vec<ConstraintBatch>,
1545    batches_dirty: bool,
1546    batch_static_bodies: Vec<bool>,
1547    contact_cache: ContactCache,
1548    material_table: MaterialTable,
1549    body_materials: Vec<u16>,
1550    pre_solve_hooks: usize,
1551    contact_modifiers: usize,
1552    gpu_solver_bridge: usize,
1553    joints: Vec<Joint>,
1554    force_fields: Vec<ForceFieldInstance>,
1555    events: EventCollector,
1556    sleep_config: SleepConfig,
1557    sleep_data: Vec<SleepData>,
1558    body_collision_radii: Vec<Option<Fix128>>,
1559    broadphase: Broadphase,
1560    broadphase_tree: crate::dynamic_bvh::DynamicAabbTree,
1561    broadphase_proxies: Vec<Option<u32>>,
1562    body_colliders: Vec<Option<BodyCollider>>,
1563    body_filters: Vec<CollisionFilter>,
1564    overflow_detected: bool,
1565    /// (entries sorted by id, hits, misses)
1566    tgs_cache: (Vec<(u64, [Fix128; 3])>, u64, u64),
1567    /// (kind, payload) per participant, registration order (version 2)
1568    participants: Vec<(u32, Vec<u8>)>,
1569    /// The recorded fault (version 2)
1570    fault: Option<WorldFault>,
1571    /// The bytes of `FieldBoard::write_values` (version 2; an empty board for
1572    /// version 1)
1573    fields: Vec<u8>,
1574    /// The continuous collision setting (version 3; off for versions 1 and 2)
1575    ccd: super::WorldCcdConfig,
1576    /// The header's `(semantics_id, law_id)` (version 4; `None` before)
1577    ids: Option<([u8; 32], [u8; 32])>,
1578}
1579
1580/// Encoded tag of a static SDF collider's body index ([`crate::sdf_collider::SDF_STATIC`]),
1581/// kept apart from body indices so the blob is the same on 32- and 64-bit targets.
1582const SDF_STATIC_TAG: u8 = 0;
1583
1584impl PhysicsWorld {
1585    /// Magic of a [`Self::snapshot_world`] blob (`b"APWS"`, distinct from
1586    /// [`Self::STATE_MAGIC`] of the rollback blob).
1587    pub const WORLD_SNAPSHOT_MAGIC: [u8; 4] = *b"APWS";
1588
1589    /// Format version of a [`Self::snapshot_world`] blob. Version 1 blobs
1590    /// (no `participants`, `fault` or `fields` section) are still read, as a
1591    /// world without participants, fault or fields, version 1 and 2 blobs
1592    /// (no `continuous_collision` section) as a world with continuous
1593    /// collision off, and version 1 to 3 blobs (no `semantics_id` / `law_id`
1594    /// in the header) without the semantics check, reported as
1595    /// [`LawCheck::Unpinned`] by [`Self::restore_world_checked`]; a blob of
1596    /// any other version is rejected with
1597    /// [`WorldSnapshotError::UnsupportedVersion`].
1598    pub const WORLD_SNAPSHOT_VERSION: u16 = 4;
1599
1600    /// Write every piece of state [`Self::step`] reads into one versioned blob
1601    /// with a checksum.
1602    ///
1603    /// Restore it with [`Self::from_world_snapshot`] (into a new world) or
1604    /// [`Self::restore_world`] (into an existing one, keeping its SDF fields
1605    /// and callbacks). Every later `step` of the restored world is
1606    /// bit-identical to the original's.
1607    ///
1608    /// # Examples
1609    ///
1610    /// ```
1611    /// use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
1612    ///
1613    /// let mut world = PhysicsWorld::new(PhysicsConfig::default());
1614    /// world.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
1615    /// let dt = Fix128::from_ratio(1, 60);
1616    /// world.step_n(10, dt);
1617    ///
1618    /// let blob = world.snapshot_world();
1619    /// let mut branch = PhysicsWorld::from_world_snapshot(&blob).unwrap();
1620    /// world.step_n(30, dt);
1621    /// branch.step_n(30, dt);
1622    /// assert_eq!(world.snapshot_world(), branch.snapshot_world());
1623    /// ```
1624    ///
1625    /// # Field coverage
1626    ///
1627    /// Every field of [`PhysicsWorld`], classified. "Saved" fields are written
1628    /// byte-exact; "rebuilt" fields are derived on restore from saved ones;
1629    /// "not covered" fields cannot be written as data, and restore instead
1630    /// requires the target world to already hold the same number of them.
1631    ///
1632    /// | field | class | how / why |
1633    /// |---|---|---|
1634    /// | `config` | saved | all of [`SolverConfig`] |
1635    /// | `bodies` | saved | every [`RigidBody`] field, including `prev_*`, damping, `kinematic_target` |
1636    /// | `distance_constraints` | saved | including `cached_lambda` |
1637    /// | `contact_constraints` | saved | including `cached_lambda` |
1638    /// | `sdf_colliders` (pose) | saved | `position` / `rotation` / `scale` / `body_index` and the cached `inv_rotation` / `scale_f32` / `inv_scale_f32` as stored (not recomputed) |
1639    /// | `sdf_colliders` (`field`) | not covered | `Box<dyn SdfField>` is code; the target world keeps its own fields, and the count must match ([`WorldSnapshotError::SdfFieldCountMismatch`]) |
1640    /// | `sdf_collision_radius` | saved | |
1641    /// | `static_colliders` | saved | plane / height field / triangle mesh including its BVH nodes as stored |
1642    /// | `constraint_batches` / `batches_dirty` / `batch_static_bodies` | saved | as stored, so a stale-but-clean batch set is reproduced rather than recomputed |
1643    /// | `contact_cache` (manifolds, `max_stale_frames`, `warm_start_factor`) | saved | |
1644    /// | `contact_cache` (`pair_index`, std) | rebuilt | from the manifolds in order, the same way `ContactCache::end_frame` rebuilds it |
1645    /// | `material_table` / `body_materials` | saved | materials, pair overrides, default combine rules |
1646    /// | `pre_solve_hooks` / `contact_modifiers` (std) | not covered | closures / trait objects; the count is recorded and must match ([`WorldSnapshotError::CallbackCountMismatch`]) |
1647    /// | `gpu_solver_bridge` (feature `gpu-solver-bridge`) | not covered | trait object; "installed or not" is recorded and must match |
1648    /// | `joints` | saved | all seven kinds, every field |
1649    /// | `joint_motors` / `joint_motors_3d` | not covered | the restore target keeps its own motors (add them with [`PhysicsWorld::add_joint_motor`] / [`PhysicsWorld::add_joint_motor_3d`] before restoring), as the format predates them; a motor's gains and targets that change between frames are saved next to the snapshot |
1650    /// | `force_fields` | saved | field, `affected_bodies`, `enabled` |
1651    /// | `events` | saved | this frame's events and the previous / current pair and trigger sets that decide begin / persist / end |
1652    /// | `islands` (`sleep_data`, `config`) | saved | |
1653    /// | `islands` (union-find `parent` / `rank`) | rebuilt | `IslandManager::new` plus a union of every joint's bodies, the same as the start of every `step` |
1654    /// | `body_collision_radii` / `body_filters` | saved | as stored, including their own lengths |
1655    /// | `broadphase` / `broadphase_tree` / `broadphase_proxies` | saved | the persistent tree node by node (pair order follows the tree layout) |
1656    /// | `broadphase_hybrid` | rebuilt | empty; its pairs are a pure function of the bodies staged each substep, so a fresh one gives the same pairs |
1657    /// | `body_colliders` | saved | shape or compound, including the compound's cached AABB and dirty flag |
1658    /// | `overflow_detected` | saved | sticky flag |
1659    /// | `kinematic_substeps_left` | rebuilt | always 0 outside `step` (reset at the end of every step), and a snapshot is only taken between steps |
1660    /// | `tgs_impulse_cache` (std) | saved | entries sorted by id, hit / miss counters; the live set is rebuilt empty (the `sweep` at the end of every step empties it) |
1661    /// | `participants` (std) | saved / not covered | each participant's kind and its [`crate::world_participant::Participant::write_state`] payload are saved; the participant itself is code, so the target world must hold the same kinds in the same order ([`WorldSnapshotError::ParticipantMismatch`]), and every payload is checked before any is read ([`WorldSnapshotError::ParticipantState`]) |
1662    /// | `participant_plan` (std) | not covered | derived from the target world's own participants and fields |
1663    /// | `fault` | saved | the recorded fault, so a restored branch is still faulted |
1664    /// | `fields` | saved | the committed values once ([`crate::world_participant::FieldBoard::write_values`]); the target world must declare the same fields ([`WorldSnapshotError::FieldState`]) |
1665    /// | `ccd` | saved | [`PhysicsWorld::continuous_collision`] (version 3) |
1666    ///
1667    /// Not in the table because they are not [`PhysicsWorld`] fields:
1668    /// motors the caller applies itself with [`crate::motor::apply_motors`]
1669    /// and [`crate::character::CharacterController`] are owned by the caller and
1670    /// are applied from outside `step`, and cloth / fluid / FEM / vehicle state
1671    /// lives in its own types. Save them next to the world snapshot.
1672    ///
1673    // LIMITATION(COV-ENGINE-012): Known difference: the union-find of `islands` is rebuilt from the joints.
1674    /// Known difference: the union-find of `islands` is rebuilt from the joints. After a
1675    /// `remove_joint` between two steps, the original still carries that union
1676    /// until the next `step` resets it, while the restored world does not; the
1677    /// two differ only for [`PhysicsWorld::wake_body`] called before that next
1678    /// step. The structure is private to [`crate::sleeping`] and has no
1679    /// non-mutating accessor, so it cannot be copied from `&self`.
1680    ///
1681    /// # Format (version 4)
1682    ///
1683    /// | range | content |
1684    /// |---|---|
1685    /// | `[0..4)` | magic [`PhysicsWorld::WORLD_SNAPSHOT_MAGIC`] (`b"APWS"`) |
1686    /// | `[4..6)` | version u16 = [`PhysicsWorld::WORLD_SNAPSHOT_VERSION`] |
1687    /// | `[6..8)` | reserved u16 = 0 |
1688    /// | `[8..16)` | payload length u64 |
1689    /// | `[16..48)` | `semantics_id`: [`crate::PHYSICS_SEMANTICS_ID`] of the writing binary |
1690    /// | `[48..80)` | `law_id`: [`PhysicsWorld::law_id`] of this world under that `semantics_id` |
1691    /// | `[80..80+len)` | payload (the sections in the table above, little endian) |
1692    /// | last 8 | FNV-1a 64 of every preceding byte (header included) |
1693    ///
1694    /// Version 1 to 3 blobs have the 16-byte header only (no `semantics_id`
1695    /// or `law_id`) and their payload starts at byte 16; the payload itself
1696    /// is the same in version 3 and 4. Version 4 grows the header rather than
1697    /// using the reserved bytes, which stay 0.
1698    ///
1699    /// The header `law_id` is the rule of the world when it was written. It
1700    /// is not guaranteed to equal [`PhysicsWorld::law_id`] of a world after
1701    /// restore: the restored world keeps what the blob does not hold (for
1702    /// example its own participants), so whatever [`PhysicsWorld::law_id`]
1703    /// reads from those comes from the target world. The reader does not
1704    /// recompute it; [`Self::restore_world_checked`] compares the header
1705    /// value with the one the caller expects.
1706    ///
1707    /// Version 2 appends three sections to the version 1 payload, after the
1708    /// TGS cache, in this order:
1709    ///
1710    /// | section | content |
1711    /// |---|---|
1712    /// | `participants` | `count: u64`, then per participant `kind: u32`, `payload_len: u64`, payload |
1713    /// | `fault` | `u8` code: `0` none, `1` participant (`index: u64`, `kind: u32`, fault tag `u8`), `2` rigid overflow, `3` force out of range (`body: u64`), `4` field out of range (`field: u32`, `index: u64`) |
1714    /// | `fields` | [`crate::world_participant::FieldBoard::write_values`] |
1715    ///
1716    /// Version 3 appends one section after `fields`:
1717    ///
1718    /// | section | content |
1719    /// |---|---|
1720    /// | `continuous_collision` | `enabled: u8` (`0` / `1`), `motion_threshold` ([`PhysicsWorld::continuous_collision`]) |
1721    ///
1722    /// The version is raised for it because the reader rejects trailing
1723    /// bytes: a section appended to a version 2 blob could not be told apart
1724    /// from a corrupt one.
1725    #[must_use]
1726    pub fn snapshot_world(&self) -> Vec<u8> {
1727        let mut w = W(Vec::new());
1728        w.0.extend_from_slice(&Self::WORLD_SNAPSHOT_MAGIC);
1729        w.u16(Self::WORLD_SNAPSHOT_VERSION);
1730        w.u16(0);
1731        w.u64(0); // payload length, patched below
1732        w.0.extend_from_slice(&PHYSICS_SEMANTICS_ID);
1733        w.0.extend_from_slice(&self.law_id(&PHYSICS_SEMANTICS_ID));
1734
1735        w_config(&mut w, &self.config);
1736
1737        w.usize(self.bodies.len());
1738        for b in &self.bodies {
1739            w_body(&mut w, b);
1740        }
1741
1742        w.usize(self.distance_constraints.len());
1743        for c in &self.distance_constraints {
1744            w.usize(c.body_a);
1745            w.usize(c.body_b);
1746            w.vec3(c.local_anchor_a);
1747            w.vec3(c.local_anchor_b);
1748            w.fix(c.target_distance);
1749            w.fix(c.compliance);
1750            w.fix(c.cached_lambda);
1751        }
1752
1753        w.usize(self.contact_constraints.len());
1754        for c in &self.contact_constraints {
1755            w.usize(c.body_a);
1756            w.usize(c.body_b);
1757            w_contact(&mut w, &c.contact);
1758            w.fix(c.friction);
1759            w.fix(c.restitution);
1760            w.fix(c.cached_lambda);
1761        }
1762
1763        w.usize(self.sdf_colliders.len());
1764        for s in &self.sdf_colliders {
1765            w.vec3(s.position);
1766            w.quat(s.rotation);
1767            w.fix(s.scale);
1768            if s.body_index == crate::sdf_collider::SDF_STATIC {
1769                w.u8(SDF_STATIC_TAG);
1770            } else {
1771                w.u8(1);
1772                w.usize(s.body_index);
1773            }
1774            w.quat(s.inv_rotation);
1775            w.f32(s.scale_f32);
1776            w.f32(s.inv_scale_f32);
1777        }
1778        w.fix(self.sdf_collision_radius);
1779
1780        w.usize(self.static_colliders.len());
1781        for c in &self.static_colliders {
1782            w_static_collider(&mut w, c);
1783        }
1784
1785        w.usize(self.constraint_batches.len());
1786        for b in &self.constraint_batches {
1787            w.usize(b.distance_indices.len());
1788            for &i in &b.distance_indices {
1789                w.usize(i);
1790            }
1791            w.usize(b.contact_indices.len());
1792            for &i in &b.contact_indices {
1793                w.usize(i);
1794            }
1795        }
1796        w.bool(self.batches_dirty);
1797        w.usize(self.batch_static_bodies.len());
1798        for &s in &self.batch_static_bodies {
1799            w.bool(s);
1800        }
1801
1802        w_contact_cache(&mut w, &self.contact_cache);
1803        w_material_table(&mut w, &self.material_table);
1804        w.usize(self.body_materials.len());
1805        for &m in &self.body_materials {
1806            w.u16(m);
1807        }
1808
1809        // Callbacks are code: only their count is recorded.
1810        #[cfg(feature = "std")]
1811        {
1812            w.usize(self.pre_solve_hooks.len());
1813            w.usize(self.contact_modifiers.len());
1814        }
1815        #[cfg(not(feature = "std"))]
1816        {
1817            w.usize(0);
1818            w.usize(0);
1819        }
1820        #[cfg(feature = "gpu-solver-bridge")]
1821        w.usize(usize::from(self.gpu_solver_bridge.is_some()));
1822        #[cfg(not(feature = "gpu-solver-bridge"))]
1823        w.usize(0);
1824
1825        w.usize(self.joints.len());
1826        for j in &self.joints {
1827            w_joint(&mut w, j);
1828        }
1829
1830        w.usize(self.force_fields.len());
1831        for f in &self.force_fields {
1832            w_force_field(&mut w, f);
1833        }
1834
1835        w_events(&mut w, &self.events);
1836
1837        w.fix(self.islands.config.linear_threshold);
1838        w.fix(self.islands.config.angular_threshold);
1839        w.u32(self.islands.config.frames_to_sleep);
1840        w.usize(self.islands.sleep_data.len());
1841        for sd in &self.islands.sleep_data {
1842            w.u8(match sd.state {
1843                SleepState::Awake => 0,
1844                SleepState::Sleeping => 1,
1845            });
1846            w.u32(sd.idle_frames);
1847        }
1848
1849        w.usize(self.body_collision_radii.len());
1850        for &r in &self.body_collision_radii {
1851            w.opt_fix(r);
1852        }
1853
1854        w.u8(match self.broadphase {
1855            Broadphase::Bvh => 0,
1856            Broadphase::DynamicTree => 1,
1857            Broadphase::Hybrid => 2,
1858        });
1859        w_tree(&mut w, &self.broadphase_tree);
1860        w.usize(self.broadphase_proxies.len());
1861        for &p in &self.broadphase_proxies {
1862            match p {
1863                Some(id) => {
1864                    w.u8(1);
1865                    w.u32(id);
1866                }
1867                None => w.u8(0),
1868            }
1869        }
1870
1871        w.usize(self.body_colliders.len());
1872        for c in &self.body_colliders {
1873            w_body_collider(&mut w, c.as_ref());
1874        }
1875
1876        w.usize(self.body_filters.len());
1877        for f in &self.body_filters {
1878            w.u32(f.layer);
1879            w.u32(f.mask);
1880            w.u32(f.group);
1881        }
1882
1883        w.bool(self.overflow_detected);
1884
1885        #[cfg(feature = "std")]
1886        {
1887            let cache = &self.tgs_impulse_cache;
1888            let mut entries: Vec<_> = cache.entries.iter().map(|(&k, &v)| (k, v)).collect();
1889            entries.sort_unstable_by_key(|e| e.0);
1890            w.usize(entries.len());
1891            for (id, imp) in entries {
1892                w.u64(id);
1893                w.fix(imp.normal);
1894                w.fix(imp.tangent1);
1895                w.fix(imp.tangent2);
1896            }
1897            w.u64(cache.hits);
1898            w.u64(cache.misses);
1899        }
1900        #[cfg(not(feature = "std"))]
1901        {
1902            w.usize(0);
1903            w.u64(0);
1904            w.u64(0);
1905        }
1906
1907        #[cfg(feature = "std")]
1908        let participants = {
1909            let mut out = Vec::new();
1910            self.write_participants(&mut out);
1911            out
1912        };
1913        #[cfg(not(feature = "std"))]
1914        let participants: Vec<(u32, Vec<u8>)> = Vec::new();
1915        w.usize(participants.len());
1916        for (kind, payload) in &participants {
1917            w.u32(*kind);
1918            w.usize(payload.len());
1919            w.0.extend_from_slice(payload);
1920        }
1921        w_fault(&mut w, self.fault());
1922        self.fields().write_values(&mut w.0);
1923        w.bool(self.ccd.is_enabled());
1924        w.fix(self.ccd.motion_threshold());
1925
1926        let mut data = w.0;
1927        let payload_len = (data.len() - HEADER_LEN_V4) as u64;
1928        data[8..16].copy_from_slice(&payload_len.to_le_bytes());
1929        let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
1930        fnv1a_fold(&mut hash, &data);
1931        data.extend_from_slice(&hash.to_le_bytes());
1932        data
1933    }
1934
1935    /// Build a new world from a [`Self::snapshot_world`] blob.
1936    ///
1937    /// The new world holds no SDF fields, hooks, contact modifiers or GPU
1938    /// bridge, so a snapshot taken with any of those is rejected
1939    /// ([`WorldSnapshotError::SdfFieldCountMismatch`] /
1940    /// [`WorldSnapshotError::CallbackCountMismatch`]); use
1941    /// [`Self::restore_world`] on a world that holds them instead.
1942    ///
1943    /// Rotations are restored as stored, as in [`Self::restore_world`].
1944    ///
1945    /// # Errors
1946    ///
1947    /// See [`WorldSnapshotError`] for which input gives which error.
1948    pub fn from_world_snapshot(data: &[u8]) -> Result<Self, WorldSnapshotError> {
1949        let mut world = Self::new(SolverConfig::default());
1950        world.restore_world(data)?;
1951        Ok(world)
1952    }
1953
1954    /// Replace this world's state with a [`Self::snapshot_world`] blob.
1955    ///
1956    /// Everything in the blob replaces the current state, including the
1957    /// [`SolverConfig`] and the body population. What is not data stays:
1958    /// the SDF fields of `self.sdf_colliders` (paired with the blob's poses in
1959    /// order), the pre-solve hooks, contact modifiers and the installed GPU
1960    /// bridge; their counts must equal the snapshot's.
1961    ///
1962    /// Body and SDF collider rotations are restored as stored and brought to
1963    /// unit length by the next step, so a non-unit rotation in `data` scales
1964    /// what it rotates by `|q|^2` in the queries made before that step (see
1965    /// [`RigidBody::rotation`]). The next step also rebuilds each collider's
1966    /// cached inverse rotation and scale from its restored fields
1967    /// ([`crate::sdf_collider::SdfCollider::update_cache`]).
1968    ///
1969    /// # Errors
1970    ///
1971    /// See [`WorldSnapshotError`] for which input gives which error. On error `self` is unchanged.
1972    pub fn restore_world(&mut self, data: &[u8]) -> Result<(), WorldSnapshotError> {
1973        self.restore_world_checked(data, None).map(|_| ())
1974    }
1975
1976    /// [`Self::restore_world`], also comparing the blob's rule identifier
1977    /// with `expected_law_id` and reporting what could be verified.
1978    ///
1979    /// | blob | `expected_law_id` | result |
1980    /// |---|---|---|
1981    /// | version 4, header `semantics_id` is neither [`crate::PHYSICS_SEMANTICS_ID`] nor in [`crate::PREVIOUS_PHYSICS_SEMANTICS_IDS`] | any | [`WorldSnapshotError::SemanticsMismatch`] |
1982    /// | version 4 | `Some(id)`, header `law_id == id` | [`LawCheck::Verified`] |
1983    /// | version 4 | `Some(id)`, header `law_id != id` | [`WorldSnapshotError::LawIdMismatch`] |
1984    /// | version 4 | `None` | [`LawCheck::SemanticsVerified`] |
1985    /// | version 4, earlier `semantics_id` `s` | `Some(id)`, `id == self.law_id(&PHYSICS_SEMANTICS_ID)` and header `law_id == self.law_id(&s)` | [`LawCheck::VerifiedUnderPrevious`] |
1986    /// | version 4, earlier `semantics_id` | `Some(id)`, otherwise | [`WorldSnapshotError::LawIdMismatch`] |
1987    /// | version 4, earlier `semantics_id` | `None` | [`LawCheck::SemanticsPrevious`] |
1988    /// | version 1 to 3 | any (not compared) | [`LawCheck::Unpinned`] |
1989    ///
1990    /// `expected_law_id` is compared with the header as written, not with
1991    /// [`Self::law_id`] of the restored world (see the format notes on
1992    /// [`Self::snapshot_world`]). To accept only verified blobs, refuse
1993    /// anything but [`LawCheck::Verified`].
1994    ///
1995    /// # Intended use
1996    ///
1997    /// Pass the target world's own `self.law_id(&PHYSICS_SEMANTICS_ID)`,
1998    /// computed **before** the restore, as `expected_law_id`: the blob is then
1999    /// accepted only if it was written under the rule this world already
2000    /// runs. Passing the blob header's own `law_id` compares the blob with
2001    /// itself and always gives [`LawCheck::Verified`], which checks nothing.
2002    /// The target's participants are kept by the restore (they are code, not
2003    /// data), so a target whose participant has the same kind but another
2004    /// step rule is refused this way, while a plain [`Self::restore_world`]
2005    /// accepts it.
2006    ///
2007    /// [`LawCheck::Verified`] then means that the target was already built
2008    /// with the same rules as the blob before the restore. The restore
2009    /// overwrites the configuration, but the target's own `law_id` is taken
2010    /// before that and covers the whole configuration, so a target that
2011    /// differs only in configuration (gravity, materials, force fields, and
2012    /// so on) gets [`WorldSnapshotError::LawIdMismatch`]: the check is
2013    /// strict and never accepts a blob written under other rules.
2014    ///
2015    /// # Examples
2016    ///
2017    /// ```
2018    /// use alice_physics::{
2019    ///     Fix128, LawCheck, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix,
2020    ///     WorldSnapshotError, PHYSICS_SEMANTICS_ID,
2021    /// };
2022    ///
2023    /// let mut world = PhysicsWorld::new(PhysicsConfig::default());
2024    /// world.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
2025    /// let blob = world.snapshot_world();
2026    ///
2027    /// // A target running the same rule: its own law id, taken before the restore
2028    /// let mut same = PhysicsWorld::new(PhysicsConfig::default());
2029    /// let expected = same.law_id(&PHYSICS_SEMANTICS_ID);
2030    /// assert_eq!(same.restore_world_checked(&blob, Some(&expected)), Ok(LawCheck::Verified));
2031    ///
2032    /// // A target running another rule refuses the blob and keeps its state
2033    /// let mut other = PhysicsWorld::new(PhysicsConfig {
2034    ///     gravity: Vec3Fix::from_int(0, -3, 0),
2035    ///     ..PhysicsConfig::default()
2036    /// });
2037    /// let expected = other.law_id(&PHYSICS_SEMANTICS_ID);
2038    /// let before = other.snapshot_world();
2039    /// assert!(matches!(
2040    ///     other.restore_world_checked(&blob, Some(&expected)),
2041    ///     Err(WorldSnapshotError::LawIdMismatch { .. })
2042    /// ));
2043    /// assert_eq!(other.snapshot_world(), before);
2044    ///
2045    /// // Without an expected id only the stepping semantics is checked
2046    /// assert_eq!(other.restore_world_checked(&blob, None), Ok(LawCheck::SemanticsVerified));
2047    /// ```
2048    ///
2049    /// # Errors
2050    ///
2051    /// Every error of [`Self::restore_world`], and
2052    /// [`WorldSnapshotError::LawIdMismatch`]. On error `self` is unchanged.
2053    pub fn restore_world_checked(
2054        &mut self,
2055        data: &[u8],
2056        expected_law_id: Option<&[u8; 32]>,
2057    ) -> Result<LawCheck, WorldSnapshotError> {
2058        let d = decode(data)?;
2059        let check = law_check(d.ids, expected_law_id, |s| self.law_id(s))?;
2060        self.check_attachable(&d)?;
2061        self.check_participants(&d.participants)?;
2062        self.fields()
2063            .check_values(&d.fields)
2064            .map_err(WorldSnapshotError::FieldState)?;
2065        self.install(d);
2066        Ok(check)
2067    }
2068
2069    /// The `(semantics_id, law_id)` in the header of a version 4
2070    /// [`Self::snapshot_world`] blob, or `None` for a version 1 to 3 blob
2071    /// (which carries no identifiers).
2072    ///
2073    /// The blob gets the structural checks of [`Self::restore_world`] (length,
2074    /// magic, version, reserved bytes, payload length, checksum), but the
2075    /// identifiers are not judged: a blob written under another stepping
2076    /// semantics, which a restore refuses with
2077    /// [`WorldSnapshotError::SemanticsMismatch`], still gives its
2078    /// identifiers here. After [`Self::restore_world_checked`] reports
2079    /// [`LawCheck::VerifiedUnderPrevious`] or [`LawCheck::SemanticsPrevious`],
2080    /// this names the earlier identifier the blob was written under.
2081    ///
2082    /// # Errors
2083    ///
2084    /// The structural errors of [`Self::restore_world`]: [`WorldSnapshotError::Truncated`],
2085    /// [`WorldSnapshotError::BadMagic`], [`WorldSnapshotError::UnsupportedVersion`],
2086    /// [`WorldSnapshotError::ReservedNotZero`], [`WorldSnapshotError::TrailingBytes`]
2087    /// and [`WorldSnapshotError::ChecksumMismatch`].
2088    pub fn world_snapshot_ids(data: &[u8]) -> Result<Option<SnapshotIds>, WorldSnapshotError> {
2089        decode_header(data).map(|(_, _, _, ids)| ids)
2090    }
2091
2092    fn check_attachable(&self, d: &Decoded) -> Res<()> {
2093        if d.sdf_poses.len() != self.sdf_colliders.len() {
2094            return Err(WorldSnapshotError::SdfFieldCountMismatch {
2095                snapshot: d.sdf_poses.len(),
2096                world: self.sdf_colliders.len(),
2097            });
2098        }
2099        #[cfg(feature = "std")]
2100        let (hooks, modifiers) = (self.pre_solve_hooks.len(), self.contact_modifiers.len());
2101        #[cfg(not(feature = "std"))]
2102        let (hooks, modifiers) = (0usize, 0usize);
2103        #[cfg(feature = "gpu-solver-bridge")]
2104        let bridge = usize::from(self.gpu_solver_bridge.is_some());
2105        #[cfg(not(feature = "gpu-solver-bridge"))]
2106        let bridge = 0usize;
2107        for (section, snapshot, world) in [
2108            ("pre_solve_hooks", d.pre_solve_hooks, hooks),
2109            ("contact_modifiers", d.contact_modifiers, modifiers),
2110            ("gpu_solver_bridge", d.gpu_solver_bridge, bridge),
2111        ] {
2112            if snapshot != world {
2113                return Err(WorldSnapshotError::CallbackCountMismatch {
2114                    section,
2115                    snapshot,
2116                    world,
2117                });
2118            }
2119        }
2120        Ok(())
2121    }
2122
2123    fn install(&mut self, d: Decoded) {
2124        self.config = d.config;
2125        self.bodies = d.bodies;
2126        self.distance_constraints = d.distance_constraints;
2127        self.contact_constraints = d.contact_constraints;
2128        for (s, p) in self.sdf_colliders.iter_mut().zip(d.sdf_poses) {
2129            s.position = p.0;
2130            s.rotation = p.1;
2131            s.scale = p.2;
2132            s.body_index = p.3;
2133            s.inv_rotation = p.4;
2134            s.scale_f32 = p.5;
2135            s.inv_scale_f32 = p.6;
2136        }
2137        self.sdf_collision_radius = d.sdf_collision_radius;
2138        self.static_colliders = d.static_colliders;
2139        self.constraint_batches = d.constraint_batches;
2140        self.batches_dirty = d.batches_dirty;
2141        self.batch_static_bodies = d.batch_static_bodies;
2142        self.contact_cache = d.contact_cache;
2143        self.material_table = d.material_table;
2144        self.body_materials = d.body_materials;
2145        self.joints = d.joints;
2146        self.force_fields = d.force_fields;
2147        self.events = d.events;
2148
2149        // Rebuilt: the union-find, as at the start of every `step`.
2150        let mut islands = IslandManager::new(d.sleep_data.len(), d.sleep_config);
2151        for j in &self.joints {
2152            let (a, b) = j.bodies();
2153            if a < d.sleep_data.len() && b < d.sleep_data.len() {
2154                islands.union(a, b);
2155            }
2156        }
2157        islands.sleep_data = d.sleep_data;
2158        self.islands = islands;
2159
2160        self.body_collision_radii = d.body_collision_radii;
2161        self.broadphase = d.broadphase;
2162        self.broadphase_tree = d.broadphase_tree;
2163        self.broadphase_proxies = d.broadphase_proxies;
2164        self.broadphase_hybrid = crate::bvh::BroadphaseHybrid::new();
2165        self.body_colliders = d.body_colliders;
2166        self.body_filters = d.body_filters;
2167        self.overflow_detected = d.overflow_detected;
2168        self.kinematic_substeps_left = 0;
2169
2170        #[cfg(feature = "std")]
2171        {
2172            let (entries, hits, misses) = d.tgs_cache;
2173            let mut cache = crate::solver_tgs::ImpulseCache::new();
2174            for (id, [n, t1, t2]) in entries {
2175                cache.entries.insert(
2176                    id,
2177                    crate::solver_tgs::CachedImpulse {
2178                        normal: n,
2179                        tangent1: t1,
2180                        tangent2: t2,
2181                    },
2182                );
2183            }
2184            // Rebuilt: the live set is empty between steps (`sweep` clears it).
2185            cache.hits = hits;
2186            cache.misses = misses;
2187            self.tgs_impulse_cache = cache;
2188        }
2189        #[cfg(not(feature = "std"))]
2190        let _ = d.tgs_cache; // the TGS backend falls back to XPBD without std
2191
2192        // Checked by `restore_world` before anything was installed.
2193        self.read_participants(&d.participants);
2194        self.set_fault(d.fault);
2195        self.fields_mut().read_values(&d.fields);
2196        self.ccd = d.ccd;
2197    }
2198}
2199
2200/// What a decoded blob's header identifiers verify against `expected`.
2201///
2202/// `law` is the target's [`PhysicsWorld::law_id`] (taken before the
2203/// restore). It is used only for a blob written under an identifier of
2204/// [`PREVIOUS_PHYSICS_SEMANTICS_IDS`] (`decode` has already refused any
2205/// other one): its header `law_id` was hashed with that earlier identifier,
2206/// so `expected` (an identifier under [`PHYSICS_SEMANTICS_ID`]) is checked
2207/// to be the target's own, and the header against the target's identifier
2208/// under the earlier semantics.
2209fn law_check(
2210    ids: Option<([u8; 32], [u8; 32])>,
2211    expected: Option<&[u8; 32]>,
2212    law: impl Fn(&[u8; 32]) -> [u8; 32],
2213) -> Res<LawCheck> {
2214    let Some((semantics_id, stored)) = ids else {
2215        return Ok(LawCheck::Unpinned);
2216    };
2217    if semantics_id == PHYSICS_SEMANTICS_ID {
2218        return match expected {
2219            None => Ok(LawCheck::SemanticsVerified),
2220            Some(e) if *e == stored => Ok(LawCheck::Verified),
2221            Some(e) => Err(WorldSnapshotError::LawIdMismatch {
2222                stored,
2223                expected: *e,
2224            }),
2225        };
2226    }
2227    let Some(e) = expected else {
2228        return Ok(LawCheck::SemanticsPrevious);
2229    };
2230    if *e != law(&PHYSICS_SEMANTICS_ID) {
2231        return Err(WorldSnapshotError::LawIdMismatch {
2232            stored,
2233            expected: *e,
2234        });
2235    }
2236    let under_previous = law(&semantics_id);
2237    if stored != under_previous {
2238        return Err(WorldSnapshotError::LawIdMismatch {
2239            stored,
2240            expected: under_previous,
2241        });
2242    }
2243    Ok(LawCheck::VerifiedUnderPrevious)
2244}
2245
2246/// The structural checks of a blob (length, magic, version, reserved bytes,
2247/// payload length, checksum) and its header: `(version, header length, end of
2248/// the body, (semantics_id, law_id) for version 4)`. The identifiers are read,
2249/// not judged; [`decode`] checks the semantics.
2250fn decode_header(data: &[u8]) -> Res<(u16, usize, usize, Option<SnapshotIds>)> {
2251    if data.len() < HEADER_LEN + CHECKSUM_LEN {
2252        return Err(WorldSnapshotError::Truncated);
2253    }
2254    if data[0..4] != PhysicsWorld::WORLD_SNAPSHOT_MAGIC {
2255        return Err(WorldSnapshotError::BadMagic);
2256    }
2257    let version = u16::from_le_bytes([data[4], data[5]]);
2258    if !(1..=PhysicsWorld::WORLD_SNAPSHOT_VERSION).contains(&version) {
2259        return Err(WorldSnapshotError::UnsupportedVersion {
2260            found: version,
2261            supported: PhysicsWorld::WORLD_SNAPSHOT_VERSION,
2262        });
2263    }
2264    if data[6] != 0 || data[7] != 0 {
2265        return Err(WorldSnapshotError::ReservedNotZero);
2266    }
2267    let header = header_len(version);
2268    if data.len() < header + CHECKSUM_LEN {
2269        return Err(WorldSnapshotError::Truncated);
2270    }
2271    let mut len_bytes = [0u8; 8];
2272    len_bytes.copy_from_slice(&data[8..16]);
2273    let payload_len = u64::from_le_bytes(len_bytes);
2274    let available = (data.len() - header - CHECKSUM_LEN) as u64;
2275    if payload_len > available {
2276        return Err(WorldSnapshotError::Truncated);
2277    }
2278    if payload_len < available {
2279        return Err(WorldSnapshotError::TrailingBytes {
2280            extra: (available - payload_len) as usize,
2281        });
2282    }
2283    let body_end = data.len() - CHECKSUM_LEN;
2284    let mut stored = [0u8; 8];
2285    stored.copy_from_slice(&data[body_end..]);
2286    let stored = u64::from_le_bytes(stored);
2287    let mut computed: u64 = 0xcbf2_9ce4_8422_2325;
2288    fnv1a_fold(&mut computed, &data[..body_end]);
2289    if stored != computed {
2290        return Err(WorldSnapshotError::ChecksumMismatch { stored, computed });
2291    }
2292    let ids = if version >= 4 {
2293        let mut semantics_id = [0u8; 32];
2294        semantics_id.copy_from_slice(&data[SEMANTICS_ID_AT..SEMANTICS_ID_AT + 32]);
2295        let mut law_id = [0u8; 32];
2296        law_id.copy_from_slice(&data[LAW_ID_AT..LAW_ID_AT + 32]);
2297        Some((semantics_id, law_id))
2298    } else {
2299        None
2300    };
2301    Ok((version, header, body_end, ids))
2302}
2303
2304fn decode(data: &[u8]) -> Res<Decoded> {
2305    let (version, header, body_end, ids) = decode_header(data)?;
2306    if let Some((semantics_id, _)) = ids {
2307        if semantics_id != PHYSICS_SEMANTICS_ID
2308            && !PREVIOUS_PHYSICS_SEMANTICS_IDS.contains(&semantics_id)
2309        {
2310            return Err(WorldSnapshotError::SemanticsMismatch {
2311                stored: semantics_id,
2312                expected: PHYSICS_SEMANTICS_ID,
2313            });
2314        }
2315    }
2316
2317    let mut r = R {
2318        data: &data[..body_end],
2319        pos: header,
2320    };
2321    let mut d = decode_payload(&mut r, version)?;
2322    d.ids = ids;
2323    if r.pos != body_end {
2324        return Err(WorldSnapshotError::TrailingBytes {
2325            extra: body_end - r.pos,
2326        });
2327    }
2328    validate(&d)?;
2329    Ok(d)
2330}
2331
2332fn decode_payload(r: &mut R<'_>, version: u16) -> Res<Decoded> {
2333    let config = r_config(r)?;
2334
2335    let n = r.len("bodies", 1)?;
2336    let mut bodies = Vec::with_capacity(n);
2337    for _ in 0..n {
2338        bodies.push(r_body(r)?);
2339    }
2340
2341    let n = r.len("distance_constraints", 160)?;
2342    let mut distance_constraints = Vec::with_capacity(n);
2343    for _ in 0..n {
2344        const S: &str = "distance_constraints";
2345        distance_constraints.push(DistanceConstraint {
2346            body_a: r.usize(S)?,
2347            body_b: r.usize(S)?,
2348            local_anchor_a: r.vec3()?,
2349            local_anchor_b: r.vec3()?,
2350            target_distance: r.fix()?,
2351            compliance: r.fix()?,
2352            cached_lambda: r.fix()?,
2353        });
2354    }
2355
2356    let n = r.len("contact_constraints", 208)?;
2357    let mut contact_constraints = Vec::with_capacity(n);
2358    for _ in 0..n {
2359        const S: &str = "contact_constraints";
2360        contact_constraints.push(ContactConstraint {
2361            body_a: r.usize(S)?,
2362            body_b: r.usize(S)?,
2363            contact: r_contact(r)?,
2364            friction: r.fix()?,
2365            restitution: r.fix()?,
2366            cached_lambda: r.fix()?,
2367        });
2368    }
2369
2370    let n = r.len("sdf_colliders", 193)?;
2371    let mut sdf_poses = Vec::with_capacity(n);
2372    for _ in 0..n {
2373        const S: &str = "sdf_colliders";
2374        let position = r.vec3()?;
2375        let rotation = r.quat()?;
2376        let scale = r.fix()?;
2377        let body_index = match r.u8()? {
2378            SDF_STATIC_TAG => crate::sdf_collider::SDF_STATIC,
2379            1 => r.usize(S)?,
2380            _ => return Err(invalid(S)),
2381        };
2382        let inv_rotation = r.quat()?;
2383        let scale_f32 = r.f32()?;
2384        let inv_scale_f32 = r.f32()?;
2385        sdf_poses.push((
2386            position,
2387            rotation,
2388            scale,
2389            body_index,
2390            inv_rotation,
2391            scale_f32,
2392            inv_scale_f32,
2393        ));
2394    }
2395    let sdf_collision_radius = r.fix()?;
2396
2397    let n = r.len("static_colliders", 1)?;
2398    let mut static_colliders = Vec::with_capacity(n);
2399    for _ in 0..n {
2400        static_colliders.push(r_static_collider(r)?);
2401    }
2402
2403    let n = r.len("constraint_batches", 16)?;
2404    let mut constraint_batches = Vec::with_capacity(n);
2405    for _ in 0..n {
2406        const S: &str = "constraint_batches";
2407        let k = r.len(S, 8)?;
2408        let mut distance_indices = Vec::with_capacity(k);
2409        for _ in 0..k {
2410            distance_indices.push(r.usize(S)?);
2411        }
2412        let k = r.len(S, 8)?;
2413        let mut contact_indices = Vec::with_capacity(k);
2414        for _ in 0..k {
2415            contact_indices.push(r.usize(S)?);
2416        }
2417        constraint_batches.push(ConstraintBatch {
2418            distance_indices,
2419            contact_indices,
2420        });
2421    }
2422    let batches_dirty = r.bool("constraint_batches")?;
2423    let n = r.len("batch_static_bodies", 1)?;
2424    let mut batch_static_bodies = Vec::with_capacity(n);
2425    for _ in 0..n {
2426        batch_static_bodies.push(r.bool("batch_static_bodies")?);
2427    }
2428
2429    let contact_cache = r_contact_cache(r)?;
2430    let material_table = r_material_table(r)?;
2431    let n = r.len("body_materials", 2)?;
2432    let mut body_materials = Vec::with_capacity(n);
2433    for _ in 0..n {
2434        body_materials.push(r.u16()?);
2435    }
2436
2437    let pre_solve_hooks = r.usize("pre_solve_hooks")?;
2438    let contact_modifiers = r.usize("contact_modifiers")?;
2439    let gpu_solver_bridge = r.usize("gpu_solver_bridge")?;
2440
2441    let n = r.len("joints", 1)?;
2442    let mut joints = Vec::with_capacity(n);
2443    for _ in 0..n {
2444        joints.push(r_joint(r)?);
2445    }
2446
2447    let n = r.len("force_fields", 1)?;
2448    let mut force_fields = Vec::with_capacity(n);
2449    for _ in 0..n {
2450        force_fields.push(r_force_field(r)?);
2451    }
2452
2453    let events = r_events(r)?;
2454
2455    let sleep_config = SleepConfig {
2456        linear_threshold: r.fix()?,
2457        angular_threshold: r.fix()?,
2458        frames_to_sleep: r.u32()?,
2459    };
2460    let n = r.len("islands", 5)?;
2461    let mut sleep_data = Vec::with_capacity(n);
2462    for _ in 0..n {
2463        let state = match r.u8()? {
2464            0 => SleepState::Awake,
2465            1 => SleepState::Sleeping,
2466            _ => return Err(invalid("islands")),
2467        };
2468        sleep_data.push(SleepData {
2469            state,
2470            idle_frames: r.u32()?,
2471        });
2472    }
2473
2474    let n = r.len("body_collision_radii", 1)?;
2475    let mut body_collision_radii = Vec::with_capacity(n);
2476    for _ in 0..n {
2477        body_collision_radii.push(r.opt_fix("body_collision_radii")?);
2478    }
2479
2480    let broadphase = match r.u8()? {
2481        0 => Broadphase::Bvh,
2482        1 => Broadphase::DynamicTree,
2483        2 => Broadphase::Hybrid,
2484        _ => return Err(invalid("broadphase")),
2485    };
2486    let broadphase_tree = r_tree(r)?;
2487    let n = r.len("broadphase_proxies", 1)?;
2488    let mut broadphase_proxies = Vec::with_capacity(n);
2489    for _ in 0..n {
2490        broadphase_proxies.push(if r.bool("broadphase_proxies")? {
2491            Some(r.u32()?)
2492        } else {
2493            None
2494        });
2495    }
2496
2497    let n = r.len("body_colliders", 1)?;
2498    let mut body_colliders = Vec::with_capacity(n);
2499    for _ in 0..n {
2500        body_colliders.push(r_body_collider(r)?);
2501    }
2502
2503    let n = r.len("body_filters", 12)?;
2504    let mut body_filters = Vec::with_capacity(n);
2505    for _ in 0..n {
2506        body_filters.push(CollisionFilter {
2507            layer: r.u32()?,
2508            mask: r.u32()?,
2509            group: r.u32()?,
2510        });
2511    }
2512
2513    let overflow_detected = r.bool("overflow_detected")?;
2514
2515    let n = r.len("tgs_impulse_cache", 56)?;
2516    let mut entries = Vec::with_capacity(n);
2517    for _ in 0..n {
2518        entries.push((r.u64()?, [r.fix()?, r.fix()?, r.fix()?]));
2519    }
2520    let tgs_cache = (entries, r.u64()?, r.u64()?);
2521
2522    let (participants, fault, fields) = if version >= 2 {
2523        let n = r.len("participants", 12)?;
2524        let mut participants = Vec::with_capacity(n);
2525        for _ in 0..n {
2526            let kind = r.u32()?;
2527            let len = r.len("participants", 1)?;
2528            participants.push((kind, r.take(len)?.to_vec()));
2529        }
2530        (participants, r_fault(r)?, r_fields(r)?)
2531    } else {
2532        let mut empty = Vec::new();
2533        crate::world_participant::FieldBoard::new().write_values(&mut empty);
2534        (Vec::new(), None, empty)
2535    };
2536    let ccd = if version >= 3 {
2537        super::WorldCcdConfig::new()
2538            .with_enabled(r.bool("continuous_collision")?)
2539            .with_motion_threshold(r.fix()?)
2540    } else {
2541        super::WorldCcdConfig::new()
2542    };
2543
2544    Ok(Decoded {
2545        config,
2546        bodies,
2547        distance_constraints,
2548        contact_constraints,
2549        sdf_poses,
2550        sdf_collision_radius,
2551        static_colliders,
2552        constraint_batches,
2553        batches_dirty,
2554        batch_static_bodies,
2555        contact_cache,
2556        material_table,
2557        body_materials,
2558        pre_solve_hooks,
2559        contact_modifiers,
2560        gpu_solver_bridge,
2561        joints,
2562        force_fields,
2563        events,
2564        sleep_config,
2565        sleep_data,
2566        body_collision_radii,
2567        broadphase,
2568        broadphase_tree,
2569        broadphase_proxies,
2570        body_colliders,
2571        body_filters,
2572        overflow_detected,
2573        tgs_cache,
2574        participants,
2575        fault,
2576        fields,
2577        ccd,
2578        ids: None,
2579    })
2580}
2581
2582fn w_fault(w: &mut W, fault: Option<WorldFault>) {
2583    match fault {
2584        None => w.u8(0),
2585        Some(WorldFault::Participant { index, kind, fault }) => {
2586            w.u8(1);
2587            w.usize(index);
2588            w.u32(kind.get());
2589            w.u8(fault.tag());
2590        }
2591        Some(WorldFault::RigidOverflow) => w.u8(2),
2592        Some(WorldFault::ForceOutOfRange { body }) => {
2593            w.u8(3);
2594            w.usize(body);
2595        }
2596        Some(WorldFault::FieldOutOfRange { field, index }) => {
2597            w.u8(4);
2598            w.u32(field.get());
2599            w.usize(index);
2600        }
2601    }
2602}
2603
2604fn r_fault(r: &mut R<'_>) -> Res<Option<WorldFault>> {
2605    const SECTION: &str = "fault";
2606    Ok(match r.u8()? {
2607        0 => None,
2608        1 => {
2609            let index = r.usize(SECTION)?;
2610            let kind = crate::world_participant::ParticipantKind::new(r.u32()?);
2611            let fault = crate::world_participant::ParticipantFault::from_tag(r.u8()?)
2612                .ok_or(WorldSnapshotError::InvalidValue { section: SECTION })?;
2613            Some(WorldFault::Participant { index, kind, fault })
2614        }
2615        2 => Some(WorldFault::RigidOverflow),
2616        3 => Some(WorldFault::ForceOutOfRange {
2617            body: r.usize(SECTION)?,
2618        }),
2619        4 => Some(WorldFault::FieldOutOfRange {
2620            field: crate::world_participant::PortId::new(r.u32()?),
2621            index: r.usize(SECTION)?,
2622        }),
2623        _ => return Err(WorldSnapshotError::InvalidValue { section: SECTION }),
2624    })
2625}
2626
2627/// The bytes of the `fields` section (`FieldBoard::write_values`), walked to
2628/// find where it ends; whether they fit the target world is checked on
2629/// restore (`FieldBoard::check_values`).
2630fn r_fields(r: &mut R<'_>) -> Res<Vec<u8>> {
2631    const SECTION: &str = "fields";
2632    let start = r.pos;
2633    let n = r.len(SECTION, 4 + 1 + 1 + 8)?;
2634    for _ in 0..n {
2635        r.u32()?;
2636        if r.u8()? > 1 {
2637            return Err(WorldSnapshotError::InvalidValue { section: SECTION });
2638        }
2639        let samples = match r.u8()? {
2640            0 => r.usize(SECTION)?,
2641            1 => {
2642                let origin = r.vec3()?;
2643                let cell = r.fix()?;
2644                let dims = [r.usize(SECTION)?, r.usize(SECTION)?, r.usize(SECTION)?];
2645                crate::world_participant::FieldLayout::Grid { origin, cell, dims }
2646                    .samples()
2647                    .ok_or(WorldSnapshotError::InvalidValue { section: SECTION })?
2648            }
2649            2 => crate::world_participant::FieldLayout::Channels {
2650                count: r.usize(SECTION)?,
2651            }
2652            .samples()
2653            .ok_or(WorldSnapshotError::InvalidValue { section: SECTION })?,
2654            _ => return Err(WorldSnapshotError::InvalidValue { section: SECTION }),
2655        };
2656        let bytes = samples
2657            .checked_mul(16)
2658            .ok_or(WorldSnapshotError::Truncated)?;
2659        r.take(bytes)?;
2660    }
2661    Ok(r.data[start..r.pos].to_vec())
2662}
2663
2664/// Reject indices that `step` would follow past the end of a `Vec`.
2665fn validate(d: &Decoded) -> Res<()> {
2666    let n = d.bodies.len();
2667    let check = |section: &'static str, index: usize, len: usize| -> Res<()> {
2668        if index < len {
2669            Ok(())
2670        } else {
2671            Err(WorldSnapshotError::DanglingIndex {
2672                section,
2673                index,
2674                len,
2675            })
2676        }
2677    };
2678    for j in &d.joints {
2679        let (a, b) = j.bodies();
2680        check("joints", a, n)?;
2681        check("joints", b, n)?;
2682    }
2683    for c in &d.distance_constraints {
2684        check("distance_constraints", c.body_a, n)?;
2685        check("distance_constraints", c.body_b, n)?;
2686    }
2687    for c in &d.contact_constraints {
2688        check("contact_constraints", c.body_a, n)?;
2689        check("contact_constraints", c.body_b, n)?;
2690    }
2691    for p in &d.sdf_poses {
2692        if p.3 != crate::sdf_collider::SDF_STATIC {
2693            check("sdf_colliders", p.3, n)?;
2694        }
2695    }
2696    for b in &d.constraint_batches {
2697        for &i in &b.distance_indices {
2698            check("constraint_batches", i, d.distance_constraints.len())?;
2699        }
2700        for &i in &b.contact_indices {
2701            check("constraint_batches", i, d.contact_constraints.len())?;
2702        }
2703    }
2704    for &p in d.broadphase_proxies.iter().flatten() {
2705        check(
2706            "broadphase_proxies",
2707            p as usize,
2708            d.broadphase_tree.nodes.len(),
2709        )?;
2710    }
2711    Ok(())
2712}
2713
2714#[cfg(all(test, feature = "std"))]
2715mod tests {
2716    // Oracles for the whole-world snapshot.
2717    //
2718    // The comparator ([`digest`]) reads every [`PhysicsWorld`] field directly
2719    // (through `Debug` or field by field) and never goes through the snapshot
2720    // encoder, so a field the encoder drops on both sides still shows up as a
2721    // difference between the original and the restored world.
2722
2723    use super::super::*;
2724    use super::WorldSnapshotError;
2725    use crate::character::CharacterController;
2726    use crate::collider::Contact;
2727    use crate::compound::CompoundShape;
2728    use crate::force::{ForceField, ForceFieldInstance};
2729    use crate::heightfield::HeightField;
2730    use crate::joint::{
2731        BallJoint, ConeTwistJoint, D6Joint, FixedJoint, HingeJoint, Joint, SliderJoint, SpringJoint,
2732    };
2733    use crate::motor::{apply_motors, JointMotor, PdController};
2734    use crate::plane_collider::PlaneCollider;
2735    use crate::sdf_collider::{ClosureSdf, SdfCollider};
2736    use crate::shape::Shape;
2737    use crate::static_collider::StaticCollider;
2738    use crate::trimesh::TriMesh;
2739
2740    fn fx(n: i64, d: i64) -> Fix128 {
2741        Fix128::from_ratio(n, d)
2742    }
2743
2744    fn v(x: i64, y: i64, z: i64) -> Vec3Fix {
2745        Vec3Fix::from_int(x, y, z)
2746    }
2747
2748    /// Every field of `w`, rendered without the snapshot encoder.
2749    fn digest(w: &mut PhysicsWorld) -> Vec<(&'static str, String)> {
2750        let mut d = Vec::new();
2751        d.push(("config", format!("{:?}", w.config)));
2752        d.push(("bodies", format!("{:?}", w.bodies)));
2753        d.push((
2754            "distance_constraints",
2755            format!("{:?}", w.distance_constraints),
2756        ));
2757        d.push((
2758            "contact_constraints",
2759            format!("{:?}", w.contact_constraints),
2760        ));
2761        let sdf: Vec<String> = w
2762            .sdf_colliders
2763            .iter()
2764            .map(|s| {
2765                format!(
2766                    "{:?} {:?} {:?} {} {:?} {} {} field@probe={}",
2767                    s.position,
2768                    s.rotation,
2769                    s.scale,
2770                    s.body_index,
2771                    s.inv_rotation,
2772                    s.scale_f32.to_bits(),
2773                    s.inv_scale_f32.to_bits(),
2774                    s.field.distance(0.3, 0.2, 0.1).to_bits()
2775                )
2776            })
2777            .collect();
2778        d.push(("sdf_colliders", format!("{sdf:?}")));
2779        d.push((
2780            "sdf_collision_radius",
2781            format!("{:?}", w.sdf_collision_radius),
2782        ));
2783        let statics: Vec<String> = w
2784            .static_colliders
2785            .iter()
2786            .map(|c| match c {
2787                StaticCollider::Plane(p) => format!("plane {p:?}"),
2788                StaticCollider::HeightField(h) => format!("hf {h:?}"),
2789                StaticCollider::TriMesh(m) => format!(
2790                    "mesh {:?} {:?} {:?} {:?} {:?}",
2791                    m.triangles, m.bvh.nodes, m.bvh.primitives, m.bvh.bounds, m.bounds
2792                ),
2793            })
2794            .collect();
2795        d.push(("static_colliders", format!("{statics:?}")));
2796        let batches: Vec<_> = w
2797            .constraint_batches
2798            .iter()
2799            .map(|b| (b.distance_indices.clone(), b.contact_indices.clone()))
2800            .collect();
2801        d.push(("constraint_batches", format!("{batches:?}")));
2802        d.push(("batches_dirty", format!("{}", w.batches_dirty)));
2803        d.push((
2804            "batch_static_bodies",
2805            format!("{:?}", w.batch_static_bodies),
2806        ));
2807        let index_probe: Vec<_> = w
2808            .contact_cache
2809            .manifolds
2810            .iter()
2811            .map(|m| w.contact_cache.find(&m.pair).map(|f| f.stale_frames))
2812            .collect();
2813        d.push((
2814            "contact_cache",
2815            format!(
2816                "{:?} {} {:?}",
2817                w.contact_cache.manifolds,
2818                w.contact_cache.max_stale_frames,
2819                w.contact_cache.warm_start_factor
2820            ),
2821        ));
2822        d.push(("contact_cache.pair_index", format!("{index_probe:?}")));
2823        d.push((
2824            "material_table",
2825            format!(
2826                "{:?} {:?} {:?} {:?}",
2827                w.material_table.materials,
2828                w.material_table.pair_overrides,
2829                w.material_table.default_friction_combine,
2830                w.material_table.default_restitution_combine
2831            ),
2832        ));
2833        d.push(("body_materials", format!("{:?}", w.body_materials)));
2834        d.push((
2835            "callbacks",
2836            format!("{} {}", w.pre_solve_hooks.len(), w.contact_modifiers.len()),
2837        ));
2838        d.push(("joints", format!("{:?}", w.joints)));
2839        d.push(("force_fields", format!("{:?}", w.force_fields)));
2840        let e = &w.events;
2841        d.push((
2842            "events",
2843            format!(
2844                "{:?} {:?} {:?} {:?} {:?} {:?}",
2845                e.contact_events,
2846                e.trigger_events,
2847                e.prev_pairs,
2848                e.curr_pairs,
2849                e.prev_triggers,
2850                e.curr_triggers
2851            ),
2852        ));
2853        d.push(("islands.config", format!("{:?}", w.islands.config)));
2854        d.push(("islands.sleep_data", format!("{:?}", w.islands.sleep_data)));
2855        let n = w.islands.sleep_data.len();
2856        let mut partition = Vec::new();
2857        for i in 0..n {
2858            for j in 0..n {
2859                partition.push(w.islands.find(i) == w.islands.find(j));
2860            }
2861        }
2862        d.push(("islands.partition", format!("{partition:?}")));
2863        d.push((
2864            "body_collision_radii",
2865            format!("{:?}", w.body_collision_radii),
2866        ));
2867        d.push(("broadphase", format!("{:?}", w.broadphase)));
2868        let t = &w.broadphase_tree;
2869        d.push((
2870            "broadphase_tree",
2871            format!(
2872                "{:?} {:?} {} {:?} {:?}",
2873                t.nodes, t.free_list, t.root, t.margin, t.metric
2874            ),
2875        ));
2876        d.push(("broadphase_proxies", format!("{:?}", w.broadphase_proxies)));
2877        d.push(("body_colliders", format!("{:?}", w.body_colliders)));
2878        d.push(("body_filters", format!("{:?}", w.body_filters)));
2879        d.push(("overflow_detected", format!("{}", w.overflow_detected)));
2880        d.push((
2881            "kinematic_substeps_left",
2882            format!("{}", w.kinematic_substeps_left),
2883        ));
2884        let c = &w.tgs_impulse_cache;
2885        let mut entries: Vec<_> = c.entries.iter().map(|(k, v)| (*k, *v)).collect();
2886        entries.sort_unstable_by_key(|e| e.0);
2887        let mut live: Vec<_> = c.live.keys().copied().collect();
2888        live.sort_unstable();
2889        d.push((
2890            "tgs_impulse_cache",
2891            format!("{entries:?} {live:?} {} {}", c.hits, c.misses),
2892        ));
2893        d
2894    }
2895
2896    /// Fields whose digest differs between `a` and `b`.
2897    fn differing(a: &mut PhysicsWorld, b: &mut PhysicsWorld) -> Vec<&'static str> {
2898        digest(a)
2899            .into_iter()
2900            .zip(digest(b))
2901            .filter(|(x, y)| x.1 != y.1)
2902            .map(|(x, _)| x.0)
2903            .collect()
2904    }
2905
2906    fn ball_sdf() -> Box<dyn crate::sdf_collider::SdfField> {
2907        Box::new(ClosureSdf::new(
2908            |x, y, z| (x * x + y * y + z * z).sqrt() - 1.0,
2909            |x, y, z| {
2910                let l = (x * x + y * y + z * z).sqrt().max(1e-6);
2911                (x / l, y / l, z / l)
2912            },
2913        ))
2914    }
2915
2916    /// The SDF fields a restore target must already hold (code, not data).
2917    fn target_world() -> PhysicsWorld {
2918        let mut t = PhysicsWorld::new(SolverConfig::default());
2919        // attached to body 0 at the default pose: every pose field differs from
2920        // the scene's static, rotated, scaled collider
2921        t.add_sdf_collider(SdfCollider::new_dynamic(ball_sdf(), 0));
2922        t
2923    }
2924
2925    /// Caller-owned state stepped next to the world (not part of the snapshot).
2926    #[derive(Clone)]
2927    struct Driver {
2928        motors: Vec<JointMotor>,
2929        character: (Vec3Fix, Vec3Fix, bool, Option<usize>, Vec3Fix),
2930    }
2931
2932    impl Driver {
2933        fn new() -> Self {
2934            let mut pd = PdController::new(fx(20, 1), fx(2, 1), fx(50, 1));
2935            pd.set_position_target(fx(3, 2));
2936            Self {
2937                motors: vec![JointMotor::new(1, pd)],
2938                character: (v(1, 1, 3), Vec3Fix::ZERO, false, None, Vec3Fix::ZERO),
2939            }
2940        }
2941
2942        /// One frame: motors, a character that pushes bodies, then `step`.
2943        fn frame(&mut self, w: &mut PhysicsWorld, dt: Fix128) {
2944            apply_motors(&self.motors, &w.joints, &mut w.bodies, dt);
2945            let (p, vel, g, gb, pv) = self.character;
2946            let mut ch = CharacterController::new_default(p);
2947            ch.velocity = vel;
2948            ch.grounded = g;
2949            ch.ground_body_index = gb;
2950            ch.platform_velocity = pv;
2951            ch.move_and_slide(
2952                Vec3Fix::new(fx(1, 20), Fix128::ZERO, Fix128::ZERO),
2953                &w.bodies,
2954                &w.sdf_colliders,
2955            );
2956            for push in ch.compute_push_impulses(&w.bodies, fx(1, 2)) {
2957                let b = &mut w.bodies[push.body_index];
2958                if !b.is_static() {
2959                    b.velocity = b.velocity + push.impulse * b.inv_mass;
2960                }
2961            }
2962            self.character = (
2963                ch.position,
2964                ch.velocity,
2965                ch.grounded,
2966                ch.ground_body_index,
2967                ch.platform_velocity,
2968            );
2969            w.step(dt);
2970        }
2971    }
2972
2973    /// A scene that exercises every saved field: joints of all seven kinds,
2974    /// distance constraint, force fields with an affected-body list, static
2975    /// plane / height field / triangle mesh, SDF collider, shaped and compound
2976    /// bodies, materials with a pair override, filters, a sensor (trigger
2977    /// events), a kinematic body with a target, and a resting body that falls
2978    /// asleep.
2979    fn scene(backend: SolverBackend, broadphase: Broadphase) -> PhysicsWorld {
2980        let config = SolverConfig {
2981            solver_backend: backend,
2982            ..SolverConfig::default()
2983        };
2984        let mut w = PhysicsWorld::new(config);
2985        w.set_broadphase(broadphase);
2986        w.set_sdf_collision_radius(fx(3, 5));
2987        // a non-default fat margin, so the restored tree must carry it
2988        w.broadphase_tree.margin = fx(3, 8);
2989        w.set_sleep_config(SleepConfig {
2990            linear_threshold: fx(1, 2),
2991            angular_threshold: fx(1, 2),
2992            frames_to_sleep: 3,
2993        });
2994
2995        w.add_static_collider(StaticCollider::Plane(PlaneCollider::new(
2996            v(0, 1, 0),
2997            Fix128::ZERO,
2998        )));
2999        let heights: Vec<Fix128> = (0..16).map(|i| fx(i % 4, 8)).collect();
3000        w.add_static_collider(StaticCollider::HeightField(HeightField::new(
3001            heights,
3002            4,
3003            4,
3004            Fix128::ONE,
3005            v(6, 0, -2),
3006        )));
3007        w.add_static_collider(StaticCollider::TriMesh(TriMesh::from_indexed(
3008            &[v(-10, 0, -10), v(-6, 1, -10), v(-10, 0, -6), v(-6, 1, -6)],
3009            &[0, 1, 2, 1, 3, 2],
3010        )));
3011        w.add_sdf_collider(
3012            SdfCollider::new_static(
3013                ball_sdf(),
3014                v(-4, 0, 0),
3015                QuatFix::from_axis_angle(v(0, 1, 0), fx(1, 3)),
3016            )
3017            .with_scale(fx(3, 2)),
3018        );
3019
3020        // 0: rests on the plane and falls asleep
3021        w.add_body_with_radius(
3022            RigidBody::new_dynamic(
3023                Vec3Fix::new(Fix128::ZERO, fx(1, 2), Fix128::ZERO),
3024                Fix128::ONE,
3025            ),
3026            fx(1, 2),
3027        );
3028        // 1: anchor, 2..=8: one joint of each kind to the anchor
3029        let anchor = w.add_body(RigidBody::new_static(v(3, 6, 0)));
3030        let mut ids = Vec::new();
3031        for k in 0..7 {
3032            let id = w.add_body_with_radius(
3033                RigidBody::new_dynamic(v(3 + k, 5, 2 * k), Fix128::ONE),
3034                fx(1, 4),
3035            );
3036            ids.push(id);
3037        }
3038        let up = v(0, 1, 0);
3039        let z = Vec3Fix::ZERO;
3040        let mut d6 = D6Joint::new(anchor, ids[5], z, z);
3041        d6.linear_x = crate::joint::D6Motion::Limited;
3042        w.add_joint(Joint::Ball(BallJoint::new(anchor, ids[0], z, v(-1, 1, 0))));
3043        let mut hinge = HingeJoint::new(anchor, ids[1], z, v(-1, 0, 0), up, up);
3044        hinge.angle_min = Some(fx(-1, 2));
3045        hinge.angle_max = Some(fx(1, 2));
3046        w.add_joint(Joint::Hinge(hinge));
3047        w.add_joint(Joint::Fixed(FixedJoint::new(
3048            anchor,
3049            ids[2],
3050            z,
3051            v(0, 1, 0),
3052            QuatFix::IDENTITY,
3053        )));
3054        let mut slider = SliderJoint::new(anchor, ids[3], v(1, 0, 0), z, z);
3055        slider.limit_min = Some(fx(-1, 1));
3056        slider.break_force = Some(fx(1000, 1));
3057        w.add_joint(Joint::Slider(slider));
3058        w.add_joint(Joint::Spring(SpringJoint::new(
3059            anchor,
3060            ids[4],
3061            z,
3062            z,
3063            fx(3, 2),
3064            fx(40, 1),
3065            fx(1, 2),
3066        )));
3067        w.add_joint(Joint::D6(d6));
3068        w.add_joint(Joint::ConeTwist(ConeTwistJoint::new(
3069            anchor, ids[6], z, z, up, up,
3070        )));
3071        w.add_distance_constraint(DistanceConstraint {
3072            body_a: ids[0],
3073            body_b: ids[1],
3074            local_anchor_a: z,
3075            local_anchor_b: z,
3076            target_distance: fx(3, 2),
3077            compliance: fx(1, 1000),
3078            cached_lambda: Fix128::ZERO,
3079        });
3080
3081        // shaped + compound bodies above the height field, a body above the SDF
3082        let boxed = w
3083            .add_shaped_body(
3084                &Shape::Box {
3085                    half_extents: Vec3Fix::new(fx(1, 2), fx(1, 4), fx(1, 2)),
3086                },
3087                Fix128::ONE,
3088                v(7, 2, 0),
3089            )
3090            .unwrap();
3091        let mut compound = CompoundShape::new();
3092        compound.add_sphere(
3093            crate::collider::Sphere::new(Vec3Fix::ZERO, fx(1, 2)),
3094            v(-1, 0, 0) * fx(1, 2),
3095            QuatFix::IDENTITY,
3096        );
3097        compound.add_sphere(
3098            crate::collider::Sphere::new(Vec3Fix::ZERO, fx(1, 2)),
3099            v(1, 0, 0) * fx(1, 2),
3100            QuatFix::IDENTITY,
3101        );
3102        let comp = w
3103            .add_compound_body(&compound, Fix128::ONE, v(8, 3, 1))
3104            .unwrap();
3105        // compute the cached AABB, so `dirty` is false and the cache is non-zero
3106        if let Some(crate::body_collider::BodyCollider::Compound(c)) = &mut w.body_colliders[comp] {
3107            let _ = c.compute_aabb();
3108            assert!(!c.dirty);
3109        }
3110        let over_sdf =
3111            w.add_body_with_radius(RigidBody::new_dynamic(v(-4, 3, 0), Fix128::ONE), fx(1, 2));
3112        // sensor around the swinging bodies, kinematic body with a target
3113        let sensor = w.add_body_with_radius(RigidBody::new_sensor(v(3, 5, 0)), fx(2, 1));
3114        let mut kin = RigidBody::new_kinematic(v(0, 2, -3));
3115        kin.kinematic_target = Some((v(2, 2, -3), QuatFix::IDENTITY));
3116        let kin = w.add_body_with_radius(kin, fx(1, 2));
3117
3118        let rubber = w.material_table.register_rubber();
3119        let ice = w.material_table.register_ice();
3120        w.material_table
3121            .set_pair_override(rubber, ice, fx(1, 7), fx(2, 7));
3122        w.set_body_material(boxed, rubber);
3123        w.set_body_material(comp, ice);
3124        w.set_body_filter(over_sdf, CollisionFilter::new(2, 0xFFFF_FFFF));
3125
3126        w.add_force_field(
3127            ForceFieldInstance::new(ForceField::Directional {
3128                direction: v(1, 0, 0),
3129                strength: fx(3, 1),
3130            })
3131            .with_affected_bodies(vec![boxed, comp]),
3132        );
3133        w.add_force_field(ForceFieldInstance::new(ForceField::Drag {
3134            coefficient: fx(1, 10),
3135        }));
3136        w.add_force_field(ForceFieldInstance::new(ForceField::Vortex {
3137            center: v(3, 5, 0),
3138            axis: up,
3139            strength: fx(1, 2),
3140            falloff_radius: fx(20, 1),
3141        }));
3142        let mut blast = ForceFieldInstance::new(ForceField::Explosion {
3143            center: v(0, 0, 0),
3144            strength: fx(500, 1),
3145            radius: fx(50, 1),
3146            falloff_power: fx(2, 1),
3147        });
3148        blast.enabled = false;
3149        w.add_force_field(blast);
3150        let _ = (sensor, kin);
3151        w
3152    }
3153
3154    fn dt() -> Fix128 {
3155        fx(1, 60)
3156    }
3157
3158    /// Run `k` frames, add a manual contact (so `contact_constraints` and
3159    /// `contact_cache` are non-empty in the snapshot), snapshot, restore into a
3160    /// target holding the same SDF field, then run `m` more frames on both.
3161    /// Returns the field names that differed at any checkpoint.
3162    fn round_trip(
3163        backend: SolverBackend,
3164        broadphase: Broadphase,
3165        k: usize,
3166        m: usize,
3167        overflow: bool,
3168    ) -> Vec<&'static str> {
3169        let mut a = scene(backend, broadphase);
3170        let mut drv = Driver::new();
3171        for _ in 0..k {
3172            drv.frame(&mut a, dt());
3173        }
3174        a.add_contact(ContactConstraint::new(
3175            2,
3176            3,
3177            Contact {
3178                depth: fx(1, 100),
3179                normal: v(0, 1, 0),
3180                point_a: v(3, 5, 0),
3181                point_b: v(4, 5, 2),
3182            },
3183        ));
3184        a.overflow_detected = overflow;
3185        // populate the colour batches (dirty = false) so they differ from a
3186        // fresh world's (empty, dirty = true)
3187        a.rebuild_batches();
3188        assert!(!a.constraint_batches.is_empty() && !a.batches_dirty);
3189        assert!(!a.contact_constraints.is_empty() && !a.contact_cache.manifolds.is_empty());
3190        let blob = a.snapshot_world();
3191        let mut b = target_world();
3192        b.restore_world(&blob).expect("restore");
3193        let mut drv_b = drv.clone();
3194
3195        let mut diff = differing(&mut a, &mut b);
3196        for _ in 0..m {
3197            drv.frame(&mut a, dt());
3198            drv_b.frame(&mut b, dt());
3199            for f in differing(&mut a, &mut b) {
3200                if !diff.contains(&f) {
3201                    diff.push(f);
3202                }
3203            }
3204        }
3205        if a.snapshot_world() != b.snapshot_world() && !diff.contains(&"blob") {
3206            diff.push("blob");
3207        }
3208        diff
3209    }
3210
3211    #[test]
3212    fn round_trip_is_bit_identical_for_every_field_and_every_later_step() {
3213        for backend in [SolverBackend::Xpbd, SolverBackend::Tgs] {
3214            for bp in [Broadphase::Bvh, Broadphase::DynamicTree] {
3215                for (k, m) in [(0, 0), (0, 5), (1, 1), (7, 3), (20, 15)] {
3216                    let diff = round_trip(backend, bp, k, m, k % 2 == 1);
3217                    assert!(diff.is_empty(), "{backend:?} {bp:?} k={k} m={m}: {diff:?}");
3218                }
3219            }
3220        }
3221    }
3222
3223    /// Controls: the round trip above is only meaningful if the scene really
3224    /// holds a sleeping body, events, contact-cache entries, tree nodes, TGS
3225    /// warm-start entries and a non-trivial island partition at snapshot time.
3226    #[test]
3227    fn round_trip_scene_exercises_the_covered_state() {
3228        let mut a = scene(SolverBackend::Xpbd, Broadphase::DynamicTree);
3229        let mut drv = Driver::new();
3230        for _ in 0..20 {
3231            drv.frame(&mut a, dt());
3232        }
3233        assert!(a
3234            .islands
3235            .sleep_data
3236            .iter()
3237            .any(|s| s.state == SleepState::Sleeping && a.bodies.len() > 1));
3238        assert!(a.is_sleeping(0), "body 0 rests on the plane and sleeps");
3239        assert!(!a.events.curr_pairs.is_empty() || !a.events.prev_pairs.is_empty());
3240        assert!(
3241            !a.events.curr_triggers.is_empty(),
3242            "sensor overlaps the swinging bodies"
3243        );
3244        assert!(!a.broadphase_tree.nodes.is_empty());
3245        assert!(a.broadphase_proxies.iter().any(Option::is_some));
3246        assert_ne!(a.islands.find(1), a.islands.find(0));
3247        assert_eq!(a.islands.find(1), a.islands.find(2));
3248        assert!(a
3249            .body_colliders
3250            .iter()
3251            .any(|c| matches!(c, Some(crate::body_collider::BodyCollider::Compound(_)))));
3252
3253        let mut t = scene(SolverBackend::Tgs, Broadphase::Bvh);
3254        let mut drv = Driver::new();
3255        for _ in 0..7 {
3256            drv.frame(&mut t, dt());
3257        }
3258        assert!(!t.tgs_impulse_cache.entries.is_empty());
3259        assert!(t.tgs_impulse_cache.hits + t.tgs_impulse_cache.misses > 0);
3260        // the live set is empty between steps, which is why it is rebuilt empty
3261        assert!(t.tgs_impulse_cache.live.is_empty());
3262    }
3263
3264    #[test]
3265    fn step_n_equals_step_called_n_times() {
3266        for n in [0usize, 1, 2, 9] {
3267            let mut a = scene(SolverBackend::Xpbd, Broadphase::Bvh);
3268            let mut b = scene(SolverBackend::Xpbd, Broadphase::Bvh);
3269            a.step_n(n, dt());
3270            for _ in 0..n {
3271                b.step(dt());
3272            }
3273            assert!(differing(&mut a, &mut b).is_empty(), "n={n}");
3274            assert_eq!(a.snapshot_world(), b.snapshot_world());
3275        }
3276        // n = 0 leaves the world unchanged
3277        let mut a = scene(SolverBackend::Xpbd, Broadphase::Bvh);
3278        let before = a.snapshot_world();
3279        a.step_n(0, dt());
3280        assert_eq!(a.snapshot_world(), before);
3281        // and so does a non-positive dt for any n
3282        a.step_n(5, Fix128::ZERO);
3283        assert_eq!(a.snapshot_world(), before);
3284    }
3285
3286    /// The target keeps its SDF fields and callbacks; the counts must match.
3287    #[test]
3288    fn restore_requires_the_same_number_of_non_data_parts() {
3289        let mut a = scene(SolverBackend::Xpbd, Broadphase::Bvh);
3290        // differs from the target's config, so a partial install would show
3291        a.config.substeps = 3;
3292        let blob = a.snapshot_world();
3293
3294        // a fresh world has no SDF field
3295        assert!(matches!(
3296            PhysicsWorld::from_world_snapshot(&blob),
3297            Err(WorldSnapshotError::SdfFieldCountMismatch {
3298                snapshot: 1,
3299                world: 0
3300            })
3301        ));
3302
3303        // a pre-solve hook on the target that the source did not have
3304        let mut t = target_world();
3305        t.add_pre_solve_hook(Box::new(|_, _, _| true));
3306        let before = digest(&mut t);
3307        assert_eq!(
3308            t.restore_world(&blob),
3309            Err(WorldSnapshotError::CallbackCountMismatch {
3310                section: "pre_solve_hooks",
3311                snapshot: 0,
3312                world: 1,
3313            })
3314        );
3315        assert_eq!(
3316            digest(&mut t),
3317            before,
3318            "rejected restore leaves the target untouched"
3319        );
3320    }
3321
3322    // ---- coverage: codec variants and reader boundaries (2026-10-07) ----
3323
3324    /// A world holding every `Shape` variant, a compound with every child
3325    /// kind, every force field kind, a `Multiply` material and the hybrid
3326    /// broadphase (none of which the round-trip scene uses).
3327    fn variant_world() -> PhysicsWorld {
3328        use crate::body_collider::BodyCollider;
3329        use crate::collider::{Capsule, ConvexHull, Sphere};
3330        use crate::material::{CombineRule, PhysicsMaterial};
3331        let mut w = PhysicsWorld::new(SolverConfig::default());
3332        w.set_broadphase(Broadphase::Hybrid);
3333        let shapes = [
3334            Shape::Box {
3335                half_extents: Vec3Fix::new(fx(1, 2), fx(1, 4), fx(1, 2)),
3336            },
3337            Shape::Cylinder {
3338                radius: fx(1, 2),
3339                half_height: fx(3, 4),
3340            },
3341            Shape::Cone {
3342                radius: fx(1, 2),
3343                half_height: fx(1, 1),
3344            },
3345            Shape::Ellipsoid {
3346                radii: Vec3Fix::new(fx(1, 2), fx(3, 4), fx(1, 4)),
3347            },
3348            Shape::Wedge {
3349                width: fx(1, 1),
3350                height: fx(1, 2),
3351                depth: fx(3, 2),
3352            },
3353            Shape::Torus {
3354                major_radius: fx(1, 1),
3355                minor_radius: fx(1, 4),
3356            },
3357        ];
3358        for (k, shape) in shapes.iter().enumerate() {
3359            let i = w.add_body_with_radius(
3360                RigidBody::new_dynamic(v(3 * k as i64, 4, 0), Fix128::ONE),
3361                fx(3, 2),
3362            );
3363            w.body_colliders[i] = Some(BodyCollider::Shape(*shape));
3364        }
3365        let mut c = CompoundShape::new();
3366        c.add_sphere(
3367            Sphere::new(Vec3Fix::ZERO, fx(1, 4)),
3368            v(1, 0, 0),
3369            QuatFix::IDENTITY,
3370        );
3371        c.add_capsule(
3372            Capsule::new(v(0, -1, 0), v(0, 1, 0), fx(1, 4)),
3373            v(-1, 0, 0),
3374            QuatFix::IDENTITY,
3375        );
3376        c.add_box(
3377            crate::box_collider::OrientedBox {
3378                center: Vec3Fix::ZERO,
3379                half_extents: Vec3Fix::new(fx(1, 4), fx(1, 4), fx(1, 4)),
3380                rotation: QuatFix::IDENTITY,
3381            },
3382            v(0, 0, 1),
3383            QuatFix::from_axis_angle(v(0, 1, 0), fx(1, 3)),
3384        );
3385        c.add_convex_hull(
3386            ConvexHull::new(vec![v(0, 0, 0), v(1, 0, 0), v(0, 1, 0), v(0, 0, 1)]),
3387            v(0, 0, -1),
3388            QuatFix::IDENTITY,
3389        );
3390        let comp =
3391            w.add_body_with_radius(RigidBody::new_dynamic(v(0, 8, 4), Fix128::ONE), fx(2, 1));
3392        w.body_colliders[comp] = Some(BodyCollider::Compound(c));
3393
3394        let mut mat = PhysicsMaterial::new(0, fx(1, 3), fx(1, 5));
3395        mat.friction_combine = CombineRule::Multiply;
3396        mat.restitution_combine = CombineRule::Multiply;
3397        let id = w.material_table.register(mat);
3398        w.material_table.default_friction_combine = CombineRule::Multiply;
3399        w.set_body_material(comp, id);
3400
3401        w.add_force_field(
3402            ForceFieldInstance::new(ForceField::Point {
3403                center: v(0, 10, 0),
3404                strength: fx(5, 1),
3405                repulsive: true,
3406                max_force: fx(50, 1),
3407            })
3408            .with_affected_bodies(vec![0, comp]),
3409        );
3410        w.add_force_field(ForceFieldInstance::new(ForceField::Buoyancy {
3411            surface_y: fx(2, 1),
3412            density: fx(1000, 1),
3413            drag: fx(1, 2),
3414        }));
3415        w.add_force_field(ForceFieldInstance::new(ForceField::Magnetic {
3416            position: v(0, 0, 0),
3417            moment: v(0, 1, 0),
3418            strength: fx(1, 10),
3419        }));
3420        w
3421    }
3422
3423    /// Every codec variant survives a round trip: the restored world equals
3424    /// the original field by field, now and after the same later steps.
3425    #[test]
3426    fn round_trip_covers_every_shape_child_and_force_field_kind() {
3427        let mut a = variant_world();
3428        let blob = a.snapshot_world();
3429        let mut b = PhysicsWorld::from_world_snapshot(&blob).expect("restore");
3430        assert!(differing(&mut a, &mut b).is_empty());
3431        for _ in 0..5 {
3432            a.step(dt());
3433            b.step(dt());
3434        }
3435        assert_eq!(differing(&mut a, &mut b), Vec::<&str>::new());
3436        assert_eq!(a.snapshot_world(), b.snapshot_world());
3437    }
3438
3439    /// A dynamic SDF collider (attached to a body, not static) round-trips
3440    /// into a target holding the same field.
3441    #[test]
3442    fn round_trip_keeps_a_body_attached_sdf_collider() {
3443        let mut a = target_world();
3444        a.add_body_with_radius(RigidBody::new_dynamic(v(0, 3, 0), Fix128::ONE), fx(1, 2));
3445        let blob = a.snapshot_world();
3446        let mut b = target_world();
3447        b.restore_world(&blob).expect("restore");
3448        assert!(differing(&mut a, &mut b).is_empty());
3449    }
3450
3451    /// Every error renders a message naming its values.
3452    #[test]
3453    fn error_display_names_the_values() {
3454        use crate::world_participant::{ParticipantMismatch, StateError};
3455        let cases: [(WorldSnapshotError, &[&str]); 15] = [
3456            (WorldSnapshotError::Truncated, &["truncated"]),
3457            (WorldSnapshotError::BadMagic, &["magic"]),
3458            (
3459                WorldSnapshotError::UnsupportedVersion {
3460                    found: 77,
3461                    supported: 88,
3462                },
3463                &["77", "88"],
3464            ),
3465            (WorldSnapshotError::ReservedNotZero, &["reserved"]),
3466            (WorldSnapshotError::TrailingBytes { extra: 5 }, &["5"]),
3467            (
3468                WorldSnapshotError::ChecksumMismatch {
3469                    stored: 0x10,
3470                    computed: 0x20,
3471                },
3472                &["0x0000000000000010", "0x0000000000000020"],
3473            ),
3474            (
3475                WorldSnapshotError::InvalidValue { section: "joints" },
3476                &["joints"],
3477            ),
3478            (
3479                WorldSnapshotError::DanglingIndex {
3480                    section: "contacts",
3481                    index: 9,
3482                    len: 4,
3483                },
3484                &["contacts", "9", "4"],
3485            ),
3486            (
3487                WorldSnapshotError::SdfFieldCountMismatch {
3488                    snapshot: 2,
3489                    world: 3,
3490                },
3491                &["2", "3"],
3492            ),
3493            (
3494                WorldSnapshotError::CallbackCountMismatch {
3495                    section: "contact_modifiers",
3496                    snapshot: 6,
3497                    world: 7,
3498                },
3499                &["contact_modifiers", "6", "7"],
3500            ),
3501            (
3502                WorldSnapshotError::ParticipantMismatch(ParticipantMismatch::Count {
3503                    snapshot: 11,
3504                    world: 12,
3505                }),
3506                &["11", "12"],
3507            ),
3508            (
3509                WorldSnapshotError::ParticipantState {
3510                    index: 13,
3511                    error: StateError::InvalidValue,
3512                },
3513                &["13", "InvalidValue"],
3514            ),
3515            (
3516                WorldSnapshotError::FieldState(StateError::Length {
3517                    expected: 14,
3518                    found: 15,
3519                }),
3520                &["14", "15"],
3521            ),
3522            (
3523                WorldSnapshotError::SemanticsMismatch {
3524                    stored: [0xAB; 32],
3525                    expected: [0x01; 32],
3526                },
3527                &["semantics", &"ab".repeat(32), &"01".repeat(32)],
3528            ),
3529            (
3530                WorldSnapshotError::LawIdMismatch {
3531                    stored: [0x0F; 32],
3532                    expected: [0xF0; 32],
3533                },
3534                &["law id", &"0f".repeat(32), &"f0".repeat(32)],
3535            ),
3536        ];
3537        for (e, parts) in cases {
3538            let text = e.to_string();
3539            for p in parts {
3540                assert!(text.contains(p), "{e:?} -> {text:?} lacks {p:?}");
3541            }
3542        }
3543    }
3544
3545    /// Header checks that do not depend on the format version: a blob
3546    /// shorter than a header, a wrong magic, non-zero reserved bytes, a
3547    /// declared payload longer than the blob, extra bytes after the
3548    /// checksum and a corrupted payload byte are all rejected, and the
3549    /// target world is left untouched each time.
3550    #[test]
3551    fn restore_rejects_malformed_headers_and_corruption() {
3552        let src = variant_world();
3553        let blob = src.snapshot_world();
3554        let mut t = PhysicsWorld::new(SolverConfig::default());
3555        let before = digest(&mut t);
3556
3557        assert_eq!(
3558            t.restore_world(&blob[..10]),
3559            Err(WorldSnapshotError::Truncated)
3560        );
3561
3562        let mut bad = blob.clone();
3563        bad[0] ^= 0xFF;
3564        assert_eq!(t.restore_world(&bad), Err(WorldSnapshotError::BadMagic));
3565
3566        let mut bad = blob.clone();
3567        bad[6] = 1;
3568        assert_eq!(
3569            t.restore_world(&bad),
3570            Err(WorldSnapshotError::ReservedNotZero)
3571        );
3572
3573        let mut bad = blob.clone();
3574        bad[8..16].copy_from_slice(&u64::MAX.to_le_bytes());
3575        assert_eq!(t.restore_world(&bad), Err(WorldSnapshotError::Truncated));
3576
3577        let mut bad = blob.clone();
3578        bad.extend_from_slice(&[0, 0, 0]);
3579        assert_eq!(
3580            t.restore_world(&bad),
3581            Err(WorldSnapshotError::TrailingBytes { extra: 3 })
3582        );
3583
3584        let mut bad = blob.clone();
3585        bad[20] ^= 0x01;
3586        assert!(matches!(
3587            t.restore_world(&bad),
3588            Err(WorldSnapshotError::ChecksumMismatch { stored, computed }) if stored != computed
3589        ));
3590
3591        assert_eq!(digest(&mut t), before);
3592        // control: the untouched blob restores
3593        assert!(t.restore_world(&blob).is_ok());
3594    }
3595
3596    fn reader(data: &[u8]) -> super::R<'_> {
3597        super::R { data, pos: 0 }
3598    }
3599
3600    /// The leaf readers reject an unknown tag in their own section.
3601    #[test]
3602    fn leaf_readers_reject_unknown_tags() {
3603        let inv = |section| Some(WorldSnapshotError::InvalidValue { section });
3604        assert_eq!(
3605            super::r_shape(&mut reader(&[6])).err(),
3606            inv("body_colliders")
3607        );
3608        assert_eq!(
3609            super::r_body_collider(&mut reader(&[3])).err(),
3610            inv("body_colliders")
3611        );
3612        // a compound with one child of unknown kind 4
3613        let mut w = super::W(Vec::new());
3614        w.u8(2);
3615        w.usize(1);
3616        w.u8(4);
3617        assert_eq!(
3618            super::r_body_collider(&mut reader(&w.0)).err(),
3619            inv("body_colliders")
3620        );
3621        assert_eq!(
3622            super::r_force_field(&mut reader(&[7])).err(),
3623            inv("force_fields")
3624        );
3625        assert_eq!(super::r_d6_motion(&mut reader(&[3])).err(), inv("joints"));
3626        assert_eq!(
3627            super::r_combine(&mut reader(&[4])).err(),
3628            inv("material_table")
3629        );
3630        assert_eq!(super::r_fault(&mut reader(&[5])).err(), inv("fault"));
3631        // config: a valid encoding with its backend tag replaced
3632        let mut w = super::W(Vec::new());
3633        super::w_config(&mut w, &SolverConfig::default());
3634        *w.0.last_mut().expect("backend tag") = 2;
3635        assert_eq!(super::r_config(&mut reader(&w.0)).err(), inv("config"));
3636        // empty input is truncation, not a value error
3637        assert_eq!(
3638            super::r_shape(&mut reader(&[])).err(),
3639            Some(WorldSnapshotError::Truncated)
3640        );
3641    }
3642
3643    /// The fault codec round-trips every variant.
3644    #[test]
3645    fn fault_codec_round_trips_every_variant() {
3646        use crate::world_participant::{ParticipantFault, ParticipantKind, PortId, WorldFault};
3647        let faults = [
3648            None,
3649            Some(WorldFault::Participant {
3650                index: 3,
3651                kind: ParticipantKind::new(0xABCD),
3652                fault: ParticipantFault::InvalidState,
3653            }),
3654            Some(WorldFault::RigidOverflow),
3655            Some(WorldFault::ForceOutOfRange { body: 17 }),
3656            Some(WorldFault::FieldOutOfRange {
3657                field: PortId::new(9),
3658                index: 21,
3659            }),
3660        ];
3661        for f in faults {
3662            let mut w = super::W(Vec::new());
3663            super::w_fault(&mut w, f);
3664            let mut r = reader(&w.0);
3665            assert_eq!(super::r_fault(&mut r), Ok(f));
3666            assert_eq!(r.pos, w.0.len(), "{f:?} reads exactly what it wrote");
3667        }
3668        // a participant fault with an unknown fault tag
3669        let mut w = super::W(Vec::new());
3670        w.u8(1);
3671        w.usize(0);
3672        w.u32(1);
3673        w.u8(0xEE);
3674        assert_eq!(
3675            super::r_fault(&mut reader(&w.0)),
3676            Err(WorldSnapshotError::InvalidValue { section: "fault" })
3677        );
3678    }
3679
3680    // Version 4 header identifiers. oracle: the identifiers computed apart
3681    // from the blob (`PHYSICS_SEMANTICS_ID`, `law_id` of the written world)
3682    // and the documented offsets.
3683
3684    /// Recompute the trailing checksum so only the targeted check can fire.
3685    fn reseal(mut b: Vec<u8>) -> Vec<u8> {
3686        let n = b.len() - super::CHECKSUM_LEN;
3687        let mut h: u64 = 0xcbf2_9ce4_8422_2325;
3688        super::super::fnv1a_fold(&mut h, &b[..n]);
3689        b[n..].copy_from_slice(&h.to_le_bytes());
3690        b
3691    }
3692
3693    fn header_id(b: &[u8], at: usize) -> [u8; 32] {
3694        b[at..at + 32].try_into().expect("32 bytes")
3695    }
3696
3697    #[test]
3698    fn version_4_header_layout() {
3699        use crate::semantics::PHYSICS_SEMANTICS_ID;
3700        assert_eq!(super::HEADER_LEN_V4, 80);
3701        assert_eq!((super::SEMANTICS_ID_AT, super::LAW_ID_AT), (16, 48));
3702        assert_eq!(super::header_len(1), 16);
3703        assert_eq!(super::header_len(3), 16);
3704        assert_eq!(super::header_len(4), 80);
3705        let w = variant_world();
3706        let b = w.snapshot_world();
3707        assert_eq!(&b[4..6], &4u16.to_le_bytes());
3708        assert_eq!(header_id(&b, 16), PHYSICS_SEMANTICS_ID);
3709        assert_eq!(header_id(&b, 48), w.law_id(&PHYSICS_SEMANTICS_ID));
3710        let len = u64::from_le_bytes(b[8..16].try_into().expect("8 bytes")) as usize;
3711        assert_eq!(len, b.len() - 80 - 8);
3712    }
3713
3714    #[test]
3715    fn law_check_covers_every_case() {
3716        use super::{law_check, LawCheck, PHYSICS_SEMANTICS_ID};
3717        let (s, l) = (PHYSICS_SEMANTICS_ID, [2u8; 32]);
3718        let other = [3u8; 32];
3719        // the current semantics never asks the target for its law id
3720        let unused = |_: &[u8; 32]| -> [u8; 32] { panic!("law id asked for") };
3721        assert_eq!(law_check(None, None, unused), Ok(LawCheck::Unpinned));
3722        assert_eq!(
3723            law_check(None, Some(&other), unused),
3724            Ok(LawCheck::Unpinned)
3725        );
3726        assert_eq!(
3727            law_check(Some((s, l)), None, unused),
3728            Ok(LawCheck::SemanticsVerified)
3729        );
3730        assert_eq!(
3731            law_check(Some((s, l)), Some(&l), unused),
3732            Ok(LawCheck::Verified)
3733        );
3734        assert_eq!(
3735            law_check(Some((s, l)), Some(&other), unused),
3736            Err(WorldSnapshotError::LawIdMismatch {
3737                stored: l,
3738                expected: other
3739            })
3740        );
3741
3742        // an earlier semantics `p`: the target's law id is `[4]` under the
3743        // current semantics and `[5]` under `p`
3744        let p = [9u8; 32];
3745        let (now, then) = ([4u8; 32], [5u8; 32]);
3746        let target = |id: &[u8; 32]| {
3747            if *id == PHYSICS_SEMANTICS_ID {
3748                now
3749            } else {
3750                assert_eq!(*id, p);
3751                then
3752            }
3753        };
3754        assert_eq!(
3755            law_check(Some((p, then)), None, target),
3756            Ok(LawCheck::SemanticsPrevious)
3757        );
3758        assert_eq!(
3759            law_check(Some((p, then)), Some(&now), target),
3760            Ok(LawCheck::VerifiedUnderPrevious)
3761        );
3762        // `expected` is not the target's own current law id
3763        assert_eq!(
3764            law_check(Some((p, then)), Some(&other), target),
3765            Err(WorldSnapshotError::LawIdMismatch {
3766                stored: then,
3767                expected: other
3768            })
3769        );
3770        // the header is not the target's law id under `p`
3771        assert_eq!(
3772            law_check(Some((p, other)), Some(&now), target),
3773            Err(WorldSnapshotError::LawIdMismatch {
3774                stored: other,
3775                expected: then
3776            })
3777        );
3778        // the header holding the current-semantics id is not accepted either
3779        assert_eq!(
3780            law_check(Some((p, now)), Some(&now), target),
3781            Err(WorldSnapshotError::LawIdMismatch {
3782                stored: now,
3783                expected: then
3784            })
3785        );
3786    }
3787
3788    /// A foreign `semantics_id` (checksum recomputed) is refused after the
3789    /// checksum, before anything is read; a changed `law_id` is refused only
3790    /// against an expected one; the next version is unsupported; the target
3791    /// is untouched in every case.
3792    #[test]
3793    fn version_4_identifier_checks() {
3794        use super::LawCheck;
3795        use crate::semantics::PHYSICS_SEMANTICS_ID;
3796        let src = variant_world();
3797        let blob = src.snapshot_world();
3798        let law = src.law_id(&PHYSICS_SEMANTICS_ID);
3799        let mut t = PhysicsWorld::new(SolverConfig::default());
3800        let before = digest(&mut t);
3801
3802        let mut x = blob.clone();
3803        x[16 + 7] ^= 0x40;
3804        let x = reseal(x);
3805        let foreign = WorldSnapshotError::SemanticsMismatch {
3806            stored: header_id(&x, 16),
3807            expected: PHYSICS_SEMANTICS_ID,
3808        };
3809        assert_eq!(t.restore_world(&x), Err(foreign));
3810        assert_eq!(t.restore_world_checked(&x, Some(&law)), Err(foreign));
3811
3812        let mut x = blob.clone();
3813        x[48 + 3] ^= 0x02;
3814        let x = reseal(x);
3815        assert_eq!(
3816            t.restore_world_checked(&x, Some(&law)),
3817            Err(WorldSnapshotError::LawIdMismatch {
3818                stored: header_id(&x, 48),
3819                expected: law
3820            })
3821        );
3822
3823        let mut x = blob.clone();
3824        x[4..6].copy_from_slice(&5u16.to_le_bytes());
3825        assert_eq!(
3826            t.restore_world(&reseal(x)),
3827            Err(WorldSnapshotError::UnsupportedVersion {
3828                found: 5,
3829                supported: 4
3830            })
3831        );
3832        // a version 4 header cut inside its identifiers
3833        assert_eq!(
3834            t.restore_world(&blob[..60]),
3835            Err(WorldSnapshotError::Truncated)
3836        );
3837        assert_eq!(digest(&mut t), before);
3838
3839        assert_eq!(
3840            t.restore_world_checked(&blob, None),
3841            Ok(LawCheck::SemanticsVerified)
3842        );
3843        let mut t = PhysicsWorld::new(SolverConfig::default());
3844        assert_eq!(
3845            t.restore_world_checked(&blob, Some(&law)),
3846            Ok(LawCheck::Verified)
3847        );
3848    }
3849
3850    /// The version 3 form of a version 4 blob (same payload, 16-byte header)
3851    /// restores to the same world and reports `Unpinned`.
3852    #[test]
3853    fn version_3_form_is_unpinned() {
3854        use super::LawCheck;
3855        let src = variant_world();
3856        let blob = src.snapshot_world();
3857        let mut v3 = blob[..16].to_vec();
3858        v3[4..6].copy_from_slice(&3u16.to_le_bytes());
3859        v3.extend_from_slice(&blob[80..]);
3860        let v3 = reseal(v3);
3861        let mut t = PhysicsWorld::new(SolverConfig::default());
3862        assert_eq!(
3863            t.restore_world_checked(&v3, Some(&[9; 32])),
3864            Ok(LawCheck::Unpinned)
3865        );
3866        assert_eq!(t.snapshot_world(), blob);
3867    }
3868
3869    /// Ids of the impulse-cache test: distinctive byte patterns (so their
3870    /// position in a blob can be found) chosen so that the map's own
3871    /// iteration order is not ascending (checked in the test).
3872    fn cache_ids() -> Vec<u64> {
3873        (1..=24u64)
3874            .map(|k| 0x5a00_0000_0000_0000 | (k * 0x0001_0203_0405_0607))
3875            .collect()
3876    }
3877
3878    /// An impulse that depends on the id (through its low byte, so the
3879    /// impulse's own bytes never repeat the id's pattern).
3880    fn cache_impulse(id: u64) -> crate::solver_tgs::CachedImpulse {
3881        let k = (id & 0xff) as i64;
3882        crate::solver_tgs::CachedImpulse {
3883            normal: Fix128::from_int(k),
3884            tangent1: Fix128::from_ratio(k, 3),
3885            tangent2: -Fix128::from_ratio(k, 7),
3886        }
3887    }
3888
3889    /// oracle (format, "entries sorted by id"): the TGS impulse cache is
3890    /// written in ascending id order whatever order the map iterates in.
3891    /// The same entries inserted in two orders, one into a map grown to a
3892    /// larger capacity first, give the same blob; every id appears in the
3893    /// blob at offsets that increase with the id; and the map's own
3894    /// iteration order is checked to differ from the ascending order, so a
3895    /// writer that iterated the map without sorting could not pass.
3896    #[cfg(feature = "std")]
3897    #[test]
3898    fn impulse_cache_is_written_in_ascending_id_order() {
3899        let ids = cache_ids();
3900        let mut a = scene(SolverBackend::Tgs, Broadphase::Bvh);
3901        let mut b = scene(SolverBackend::Tgs, Broadphase::Bvh);
3902        for &id in &ids {
3903            a.tgs_impulse_cache.entries.insert(id, cache_impulse(id));
3904        }
3905        b.tgs_impulse_cache.entries.reserve(4096);
3906        for &id in ids.iter().rev() {
3907            b.tgs_impulse_cache.entries.insert(id, cache_impulse(id));
3908        }
3909        let iterated: Vec<u64> = a.tgs_impulse_cache.entries.keys().copied().collect();
3910        let mut ascending = iterated.clone();
3911        ascending.sort_unstable();
3912        assert_ne!(
3913            iterated, ascending,
3914            "the map must not already iterate in id order"
3915        );
3916
3917        let blob = a.snapshot_world();
3918        assert_eq!(
3919            b.snapshot_world(),
3920            blob,
3921            "insertion order and capacity leak into the blob"
3922        );
3923
3924        let mut sorted = ids.clone();
3925        sorted.sort_unstable();
3926        let offsets: Vec<usize> = sorted
3927            .iter()
3928            .map(|id| {
3929                let needle = id.to_le_bytes();
3930                let hits: Vec<usize> = blob
3931                    .windows(8)
3932                    .enumerate()
3933                    .filter(|(_, w)| *w == needle)
3934                    .map(|(i, _)| i)
3935                    .collect();
3936                assert_eq!(
3937                    hits.len(),
3938                    1,
3939                    "id {id:#x} must appear once in the blob: {hits:?}"
3940                );
3941                hits[0]
3942            })
3943            .collect();
3944        assert!(
3945            offsets.windows(2).all(|w| w[0] < w[1]),
3946            "ids are not written in ascending order: {offsets:?}"
3947        );
3948
3949        // and the order survives a round trip (the restored world writes the same blob)
3950        let mut c = scene(SolverBackend::Tgs, Broadphase::Bvh);
3951        c.restore_world(&blob).expect("restore");
3952        assert_eq!(c.snapshot_world(), blob);
3953    }
3954}