1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87#[non_exhaustive]
88pub enum WorldSnapshotError {
89 Truncated,
91 BadMagic,
93 UnsupportedVersion {
96 found: u16,
98 supported: u16,
100 },
101 ReservedNotZero,
103 TrailingBytes {
105 extra: usize,
107 },
108 ChecksumMismatch {
110 stored: u64,
112 computed: u64,
114 },
115 InvalidValue {
120 section: &'static str,
122 },
123 DanglingIndex {
126 section: &'static str,
128 index: usize,
130 len: usize,
132 },
133 SdfFieldCountMismatch {
136 snapshot: usize,
138 world: usize,
140 },
141 CallbackCountMismatch {
144 section: &'static str,
146 snapshot: usize,
148 world: usize,
150 },
151 ParticipantMismatch(crate::world_participant::ParticipantMismatch),
155 ParticipantState {
159 index: usize,
161 error: crate::world_participant::StateError,
163 },
164 FieldState(crate::world_participant::StateError),
168 SemanticsMismatch {
174 stored: [u8; 32],
176 expected: [u8; 32],
178 },
179 LawIdMismatch {
182 stored: [u8; 32],
184 expected: [u8; 32],
186 },
187}
188
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192#[non_exhaustive]
193pub enum LawCheck {
194 Verified,
198 SemanticsVerified,
202 Unpinned,
206 VerifiedUnderPrevious,
214 SemanticsPrevious,
219}
220
221pub type SnapshotIds = ([u8; 32], [u8; 32]);
224
225struct 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
306const HEADER_LEN: usize = 16;
308const HEADER_LEN_V4: usize = HEADER_LEN + 64;
311const SEMANTICS_ID_AT: usize = HEADER_LEN;
313const LAW_ID_AT: usize = HEADER_LEN + 32;
315
316fn 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
326struct 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
385struct 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 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
486fn 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 #[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
1332fn r_material_table(r: &mut R<'_>) -> Res<MaterialTable> {
1338 const S: &str = "material_table";
1339 let n = r.len(S, 52)?;
1340 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
1532struct Decoded {
1536 config: SolverConfig,
1537 bodies: Vec<RigidBody>,
1538 distance_constraints: Vec<DistanceConstraint>,
1539 contact_constraints: Vec<ContactConstraint>,
1540 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 tgs_cache: (Vec<(u64, [Fix128; 3])>, u64, u64),
1567 participants: Vec<(u32, Vec<u8>)>,
1569 fault: Option<WorldFault>,
1571 fields: Vec<u8>,
1574 ccd: super::WorldCcdConfig,
1576 ids: Option<([u8; 32], [u8; 32])>,
1578}
1579
1580const SDF_STATIC_TAG: u8 = 0;
1583
1584impl PhysicsWorld {
1585 pub const WORLD_SNAPSHOT_MAGIC: [u8; 4] = *b"APWS";
1588
1589 pub const WORLD_SNAPSHOT_VERSION: u16 = 4;
1599
1600 #[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); 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 #[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 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 pub fn restore_world(&mut self, data: &[u8]) -> Result<(), WorldSnapshotError> {
1973 self.restore_world_checked(data, None).map(|_| ())
1974 }
1975
1976 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 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 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 cache.hits = hits;
2186 cache.misses = misses;
2187 self.tgs_impulse_cache = cache;
2188 }
2189 #[cfg(not(feature = "std"))]
2190 let _ = d.tgs_cache; 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
2200fn 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
2246fn 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
2627fn 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
2664fn 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 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 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 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 fn target_world() -> PhysicsWorld {
2918 let mut t = PhysicsWorld::new(SolverConfig::default());
2919 t.add_sdf_collider(SdfCollider::new_dynamic(ball_sdf(), 0));
2922 t
2923 }
2924
2925 #[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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 a.step_n(5, Fix128::ZERO);
3283 assert_eq!(a.snapshot_world(), before);
3284 }
3285
3286 #[test]
3288 fn restore_requires_the_same_number_of_non_data_parts() {
3289 let mut a = scene(SolverBackend::Xpbd, Broadphase::Bvh);
3290 a.config.substeps = 3;
3292 let blob = a.snapshot_world();
3293
3294 assert!(matches!(
3296 PhysicsWorld::from_world_snapshot(&blob),
3297 Err(WorldSnapshotError::SdfFieldCountMismatch {
3298 snapshot: 1,
3299 world: 0
3300 })
3301 ));
3302
3303 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 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 #[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 #[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 #[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 #[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 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 #[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 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 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 assert_eq!(
3638 super::r_shape(&mut reader(&[])).err(),
3639 Some(WorldSnapshotError::Truncated)
3640 );
3641 }
3642
3643 #[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 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 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 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 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 assert_eq!(
3764 law_check(Some((p, then)), Some(&other), target),
3765 Err(WorldSnapshotError::LawIdMismatch {
3766 stored: then,
3767 expected: other
3768 })
3769 );
3770 assert_eq!(
3772 law_check(Some((p, other)), Some(&now), target),
3773 Err(WorldSnapshotError::LawIdMismatch {
3774 stored: other,
3775 expected: then
3776 })
3777 );
3778 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 #[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 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 #[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 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 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 #[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 let mut c = scene(SolverBackend::Tgs, Broadphase::Bvh);
3951 c.restore_world(&blob).expect("restore");
3952 assert_eq!(c.snapshot_world(), blob);
3953 }
3954}