1use std::collections::{BTreeMap, HashMap};
2use std::ops::{Add, Mul};
3
4use loam_math::Bivector;
5use loam_time::par;
6use loam_time::StateHash;
7
8use crate::body::{BodyArena, BodyDef, BodyId, RigidBody};
9use crate::collider::Collider;
10use crate::collision::VectorOps;
11use crate::dirty::{DirtyBodies, DirtyDrain};
12use crate::edit::EditError;
13use crate::field_contact::FieldNarrowphase;
14use crate::geometry::GeometryStore;
15use crate::integrator::{integrate_body, BroadphaseBound, PhysicsSpace};
16use crate::manifold::{
17 ContactPoint, Manifold, BAUMGARTE_BETA, DEFAULT_PGS_ITERS, MAX_LINEAR_CORRECTION,
18 PENETRATION_SLOP, RESTITUTION_THRESHOLD,
19};
20use crate::narrowphase::Narrowphase;
21use crate::response::Contact;
22use crate::response::FRICTION_COEFF;
23use crate::state::WorldState;
24
25pub type PairKey = (BodyId, BodyId);
27
28fn canonical_pair(a: BodyId, b: BodyId) -> PairKey {
29 debug_assert_ne!(a, b, "a body cannot pair with itself");
30 if a < b {
31 (a, b)
32 } else {
33 (b, a)
34 }
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct Island {
40 pub id: BodyId,
42 pub bodies: Vec<BodyId>,
44 pub constraints: Vec<PairKey>,
46}
47
48pub const DEFAULT_SOLVER_TOLERANCE: f32 = 1e-3;
49
50const WAKE_APPROACH_SPEED: f32 = 0.01;
51
52pub const ISLANDS_PER_SOLVE_WORKER: usize = 256;
54
55const STALE_CONSTRAINT_KEY: &str = "constraint buffer outlived its manifold";
56const STALE_MANIFOLD_BODY: &str = "manifold key names a body that is gone";
57
58const BROADPHASE_TRIANGLE_SLACK: f32 = 4.0 * f32::EPSILON;
60
61#[derive(Clone, Copy)]
62struct RadialInterval {
63 lo: f32,
64 hi: f32,
65 radius: f32,
66 dense: u32,
67 id: BodyId,
68 dynamic: bool,
69 group: u32,
70 mask: u32,
71}
72
73fn bounding_radius(collider: Option<&Collider>) -> f32 {
74 let Some(collider) = collider else {
75 return 0.0;
76 };
77 match collider {
78 Collider::Sphere { radius, .. } | Collider::HyperSphere4D { radius, .. } => *radius,
79 Collider::Box3 { half_extents } => half_extents.length(),
80 Collider::Polygon2D { vertices } => max_norm(vertices.iter().map(|v| v.length_squared())),
81 Collider::ConvexPolytope3D { vertices } => {
82 max_norm(vertices.iter().map(|v| v.length_squared()))
83 }
84 Collider::ConvexPolytope4D { vertices } => {
85 max_norm(vertices.iter().map(|v| v.length_squared()))
86 }
87 Collider::HalfSpace { .. } | Collider::HalfSpace4D { .. } => f32::INFINITY,
88 }
89}
90
91fn max_norm(norms_squared: impl Iterator<Item = f32>) -> f32 {
92 norms_squared.fold(0.0_f32, f32::max).sqrt()
93}
94
95#[derive(Clone, Copy, Debug, PartialEq)]
97pub struct SolveReport {
98 pub residual: f32,
99 pub converged: bool,
100}
101
102impl Default for SolveReport {
103 fn default() -> Self {
104 Self {
105 residual: 0.0,
106 converged: true,
107 }
108 }
109}
110
111struct ScratchUnit {
112 key: PairKey,
113 a: u32,
114 b: u32,
115 first: u32,
116 count: u32,
117}
118
119struct IslandSolve<S: PhysicsSpace> {
120 bodies: Vec<RigidBody<S>>,
121 dense: Vec<u32>,
122 units: Vec<ScratchUnit>,
123 points: Vec<ContactPoint<S>>,
124 residual: f32,
125 push: Vec<f32>,
126 saved: Vec<RigidBody<S>>,
127}
128
129impl<S: PhysicsSpace> Default for IslandSolve<S> {
130 fn default() -> Self {
131 Self {
132 bodies: Vec::new(),
133 dense: Vec::new(),
134 units: Vec::new(),
135 points: Vec::new(),
136 residual: 0.0,
137 push: Vec::new(),
138 saved: Vec::new(),
139 }
140 }
141}
142
143const SCATTERED_NOWHERE: u32 = u32::MAX;
144
145#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
146pub struct StepCounters {
147 pub index_visits: u32,
148 pub distance_evals: u32,
149 pub candidates: u32,
150 pub contacts: u32,
151}
152
153#[derive(Clone, Copy)]
154struct ConstraintUnit {
155 island: BodyId,
156 key: PairKey,
157 dense: (usize, usize),
158}
159
160#[cfg_attr(feature = "persist", derive(serde::Serialize, serde::Deserialize))]
161#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
162pub struct FieldId(u32);
163
164struct FieldEntry {
165 anchor: BodyId,
166 field: Box<dyn loam_shape::field::DistanceField>,
167}
168
169pub struct World<S: PhysicsSpace> {
170 space: S,
171 pub(crate) bodies: BodyArena<S>,
172 gravity: Option<S::Vector>,
173 pub narrowphase: Narrowphase<S>,
174 pub field_narrowphase: FieldNarrowphase<S>,
175 fields: Vec<FieldEntry>,
176 field_bindings: Vec<(BodyId, FieldId)>,
177 geometry: GeometryStore,
178 pub(crate) manifolds: HashMap<PairKey, Manifold<S>>,
179 manifold_order: Vec<PairKey>,
180 manifold_pool: Vec<Manifold<S>>,
181 pending_contacts: Vec<(PairKey, Contact<S>)>,
182 pgs_iters: usize,
183 time: f32,
184 dirty: DirtyBodies,
185 pair_order: Vec<PairKey>,
186 constraints: Vec<ConstraintUnit>,
187 broadphase_intervals: Vec<RadialInterval>,
188 broadphase_active: Vec<u32>,
189 touched_pairs: Vec<PairKey>,
190 island_parent: Vec<u32>,
191 island_labels: Vec<BodyId>,
192 counters: StepCounters,
193 solver_tolerance: f32,
194 report: SolveReport,
195 scratch: Vec<IslandSolve<S>>,
196 scratch_islands: usize,
197 scratch_local: Vec<u32>,
198}
199
200const _: () = {
201 const fn assert_send_sync<T: Send + Sync>() {}
202 const fn assert_no_allocation<T: Copy>() {}
203 #[cfg(feature = "r2")]
204 {
205 assert_send_sync::<World<loam_math::EuclideanR2>>();
206 assert_no_allocation::<RigidBody<loam_math::EuclideanR2>>();
207 }
208 #[cfg(feature = "r3")]
209 {
210 assert_send_sync::<World<loam_math::EuclideanR3>>();
211 assert_no_allocation::<RigidBody<loam_math::EuclideanR3>>();
212 }
213 #[cfg(feature = "r4")]
214 {
215 assert_send_sync::<World<loam_math::EuclideanR4>>();
216 assert_no_allocation::<RigidBody<loam_math::EuclideanR4>>();
217 }
218};
219
220impl<S: PhysicsSpace> World<S> {
221 pub fn new(space: S) -> Self {
222 Self {
223 space,
224 bodies: BodyArena::new(),
225 gravity: None,
226 narrowphase: Narrowphase::new(),
227 field_narrowphase: FieldNarrowphase::new(),
228 fields: Vec::new(),
229 field_bindings: Vec::new(),
230 geometry: GeometryStore::default(),
231 manifolds: HashMap::new(),
232 manifold_order: Vec::new(),
233 manifold_pool: Vec::new(),
234 pending_contacts: Vec::new(),
235 pgs_iters: DEFAULT_PGS_ITERS,
236 time: 0.0,
237 dirty: DirtyBodies::default(),
238 pair_order: Vec::new(),
239 constraints: Vec::new(),
240 broadphase_intervals: Vec::new(),
241 broadphase_active: Vec::new(),
242 touched_pairs: Vec::new(),
243 island_parent: Vec::new(),
244 island_labels: Vec::new(),
245 counters: StepCounters::default(),
246 solver_tolerance: DEFAULT_SOLVER_TOLERANCE,
247 report: SolveReport::default(),
248 scratch: Vec::new(),
249 scratch_islands: 0,
250 scratch_local: Vec::new(),
251 }
252 }
253
254 pub fn counters(&self) -> StepCounters {
256 self.counters
257 }
258
259 pub fn bodies(&self) -> &BodyArena<S> {
260 &self.bodies
261 }
262
263 pub fn space(&self) -> &S {
264 &self.space
265 }
266
267 pub fn gravity(&self) -> Option<S::Vector> {
268 self.gravity
269 }
270
271 pub fn set_gravity(&mut self, gravity: Option<S::Vector>) -> Result<(), EditError> {
272 if gravity.is_some_and(|gravity| !self.space.valid_vector(gravity)) {
273 return Err(EditError::InvalidGravity);
274 }
275 self.gravity = gravity;
276 Ok(())
277 }
278
279 pub fn solver_iterations(&self) -> usize {
280 self.pgs_iters
281 }
282
283 pub fn set_solver_iterations(&mut self, iterations: usize) {
284 self.pgs_iters = iterations;
285 }
286
287 pub fn solver_tolerance(&self) -> f32 {
288 self.solver_tolerance
289 }
290
291 pub fn set_solver_tolerance(&mut self, tolerance: f32) -> Result<(), EditError> {
292 if tolerance.is_nan() || tolerance < 0.0 {
293 return Err(EditError::InvalidSolverTolerance);
294 }
295 self.solver_tolerance = tolerance;
296 Ok(())
297 }
298
299 pub fn time(&self) -> f32 {
300 self.time
301 }
302
303 pub fn body(&self, id: BodyId) -> Option<&RigidBody<S>> {
304 self.bodies.get(id)
305 }
306
307 pub fn manifolds(&self) -> impl ExactSizeIterator<Item = (&PairKey, &Manifold<S>)> {
308 self.manifold_order
309 .iter()
310 .map(|key| (key, &self.manifolds[key]))
311 }
312
313 pub fn manifold(&self, key: PairKey) -> Option<&Manifold<S>> {
314 self.manifolds.get(&key)
315 }
316
317 pub fn solve_report(&self) -> SolveReport {
318 self.report
319 }
320
321 pub fn push_body(&mut self, body: BodyDef<S>) -> BodyId {
322 let row = body.into_row(&mut self.geometry, &self.space);
323 let id = self.bodies.spawn(row);
324 self.dirty.mark(id);
325 id
326 }
327
328 pub fn insert_field(
330 &mut self,
331 anchor: BodyId,
332 field: Box<dyn loam_shape::field::DistanceField>,
333 ) -> Result<FieldId, EditError> {
334 let Some(body) = self.bodies.get_mut(anchor) else {
335 return Err(EditError::StaleHandle);
336 };
337 if !body.is_static() {
338 return Err(EditError::DynamicFieldAnchor);
339 }
340 body.collision_group = 0;
341 body.collision_mask = 0;
342 self.drop_contacts_of(anchor);
343 let id = FieldId(self.fields.len() as u32);
344 self.fields.push(FieldEntry { anchor, field });
345 Ok(id)
346 }
347
348 pub fn bind_field(&mut self, body: BodyId, field: FieldId) -> Result<(), EditError> {
350 let Some(entry) = self.fields.get(field.0 as usize) else {
351 return Err(EditError::StaleHandle);
352 };
353 if entry.anchor == body {
354 return Err(EditError::AnchorBindsOwnField);
355 }
356 if self.bodies.get(body).is_none() {
357 return Err(EditError::StaleHandle);
358 }
359 if let Err(at) = self.field_bindings.binary_search(&(body, field)) {
360 self.field_bindings.insert(at, (body, field));
361 }
362 Ok(())
363 }
364
365 pub fn field_bindings(&self) -> &[(BodyId, FieldId)] {
366 &self.field_bindings
367 }
368
369 pub fn geometry(&self) -> &GeometryStore {
370 &self.geometry
371 }
372
373 pub fn reclaim_geometry(&mut self) -> Option<Collider> {
375 self.geometry.take_released()
376 }
377
378 pub fn collider(&self, body: &RigidBody<S>) -> Option<&Collider> {
379 self.geometry.get(body.collider())
380 }
381
382 pub fn despawn_body(&mut self, id: BodyId) -> Result<(), EditError> {
384 if self.fields.iter().any(|field| field.anchor == id) {
385 return Err(EditError::FieldAnchorRemoval);
386 }
387 let Some(removed) = self.bodies.despawn(id) else {
388 return Err(EditError::StaleHandle);
389 };
390 self.field_bindings.retain(|&(body, _)| body != id);
391 self.geometry.release(removed.collider());
392 self.drop_contacts_of(id);
393 self.dirty.forget(id);
394 Ok(())
395 }
396
397 pub fn set_pose(
399 &mut self,
400 id: BodyId,
401 position: S::Point,
402 orientation: S::Iso,
403 ) -> Result<(), EditError> {
404 if self.bodies.get(id).is_none() {
405 return Err(EditError::StaleHandle);
406 }
407 if !self.space.valid_point(position) || !self.space.valid_orientation(orientation) {
408 return Err(EditError::NotFinite);
409 }
410 let body = &mut self.bodies[id];
411 body.position = position;
412 body.orientation = orientation;
413 body.wake();
414 self.drop_contacts_of(id);
415 self.dirty.mark(id);
416 Ok(())
417 }
418
419 pub fn set_velocity(
420 &mut self,
421 id: BodyId,
422 velocity: S::Vector,
423 angular_velocity: S::AngVel,
424 ) -> Result<(), EditError> {
425 if self.bodies.get(id).is_none() {
426 return Err(EditError::StaleHandle);
427 }
428 if !self.space.valid_vector(velocity)
429 || !self.space.valid_angular_velocity(angular_velocity)
430 {
431 return Err(EditError::NotFinite);
432 }
433 let body = &mut self.bodies[id];
434 body.velocity = velocity;
435 body.angular_velocity = angular_velocity;
436 body.wake();
437 self.dirty.mark(id);
438 Ok(())
439 }
440
441 pub fn set_restitution(&mut self, id: BodyId, restitution: f32) -> Result<(), EditError> {
442 let Some(body) = self.bodies.get_mut(id) else {
443 return Err(EditError::StaleHandle);
444 };
445 if !restitution.is_finite() || restitution < 0.0 {
446 return Err(EditError::InvalidRestitution);
447 }
448 body.restitution = restitution;
449 self.drop_contacts_of(id);
450 Ok(())
451 }
452
453 pub fn set_collision_filter(
454 &mut self,
455 id: BodyId,
456 group: u32,
457 mask: u32,
458 ) -> Result<(), EditError> {
459 if self.fields.iter().any(|field| field.anchor == id) && (group != 0 || mask != 0) {
460 return Err(EditError::InvalidFieldBinding);
461 }
462 let Some(body) = self.bodies.get_mut(id) else {
463 return Err(EditError::StaleHandle);
464 };
465 body.collision_group = group;
466 body.collision_mask = mask;
467 self.drop_contacts_of(id);
468 Ok(())
469 }
470
471 pub fn set_mass_properties(
473 &mut self,
474 id: BodyId,
475 mass: f32,
476 inertia: S::Inertia,
477 ) -> Result<(), EditError> {
478 if mass > 0.0 && self.fields.iter().any(|field| field.anchor == id) {
479 return Err(EditError::DynamicFieldAnchor);
480 }
481 if self.bodies.get(id).is_none() {
482 return Err(EditError::StaleHandle);
483 }
484 if !self.space.valid_inertia(inertia) {
485 return Err(EditError::InvalidInertia);
486 }
487 self.bodies[id].set_mass_properties(mass, inertia)?;
488 self.drop_contacts_of(id);
489 self.dirty.mark(id);
490 Ok(())
491 }
492
493 pub fn set_collider(
495 &mut self,
496 id: BodyId,
497 collider: Collider,
498 inertia: S::Inertia,
499 ) -> Result<(), EditError> {
500 if self.bodies.get(id).is_none() {
501 self.geometry.stash(collider);
502 return Err(EditError::StaleHandle);
503 }
504 if !self.space.valid_inertia(inertia) {
505 self.geometry.stash(collider);
506 return Err(EditError::InvalidInertia);
507 }
508 let Self {
509 space,
510 bodies,
511 geometry,
512 ..
513 } = self;
514 bodies[id].replace_collider(geometry, space, collider, inertia)?;
515 self.drop_contacts_of(id);
516 self.dirty.mark(id);
517 Ok(())
518 }
519
520 pub fn apply_impulse(&mut self, id: BodyId, impulse: S::Vector) -> Result<(), EditError>
521 where
522 S::Vector: Add<Output = S::Vector> + Mul<f32, Output = S::Vector>,
523 {
524 if self.bodies.get(id).is_none() {
525 return Err(EditError::StaleHandle);
526 }
527 if !self.space.valid_vector(impulse) {
528 return Err(EditError::NotFinite);
529 }
530 self.bodies[id].apply_impulse(impulse);
531 self.dirty.mark(id);
532 Ok(())
533 }
534
535 pub fn apply_impulse_at_point(
536 &mut self,
537 id: BodyId,
538 impulse: S::Vector,
539 point: S::Point,
540 ) -> Result<(), EditError>
541 where
542 S::Vector: Add<Output = S::Vector> + Mul<f32, Output = S::Vector>,
543 {
544 if self.bodies.get(id).is_none() {
545 return Err(EditError::StaleHandle);
546 }
547 if !self.space.valid_vector(impulse) || !self.space.valid_point(point) {
548 return Err(EditError::NotFinite);
549 }
550 let space = &self.space;
551 self.bodies[id].apply_impulse_at_point(space, impulse, point);
552 self.dirty.mark(id);
553 Ok(())
554 }
555
556 pub fn sleep_body(&mut self, id: BodyId) -> Result<(), EditError>
557 where
558 S::Vector: Default,
559 {
560 let Some(body) = self.bodies.get_mut(id) else {
561 return Err(EditError::StaleHandle);
562 };
563 body.sleep();
564 self.dirty.mark(id);
565 Ok(())
566 }
567
568 pub fn wake_body(&mut self, id: BodyId) -> Result<(), EditError> {
569 let Some(body) = self.bodies.get_mut(id) else {
570 return Err(EditError::StaleHandle);
571 };
572 body.wake();
573 self.dirty.mark(id);
574 Ok(())
575 }
576
577 fn drop_contacts_of(&mut self, id: BodyId) {
578 let mut write = 0;
579 for read in 0..self.manifold_order.len() {
580 let key = self.manifold_order[read];
581 if key.0 == id || key.1 == id {
582 if let Some(manifold) = self.manifolds.remove(&key) {
583 self.manifold_pool.push(manifold);
584 }
585 } else {
586 self.manifold_order[write] = key;
587 write += 1;
588 }
589 }
590 self.manifold_order.truncate(write);
591 }
592
593 pub fn drain_dirty(&mut self) -> DirtyDrain<'_, S> {
595 let Self { bodies, dirty, .. } = self;
596 dirty.drain(bodies)
597 }
598
599 pub fn snapshot(&self) -> WorldState<S> {
600 WorldState {
601 bodies: self.bodies.clone(),
602 geometry: self.geometry.clone(),
603 manifolds: self
604 .manifolds()
605 .map(|(&key, manifold)| (key, manifold.clone()))
606 .collect(),
607 time: self.time,
608 field_bindings: self.field_bindings.clone(),
609 field_anchors: self.fields.iter().map(|entry| entry.anchor).collect(),
610 registrations: self.narrowphase.registrations().to_vec(),
611 field_registrations: self.field_narrowphase.registrations().to_vec(),
612 }
613 }
614
615 pub fn check_restore(&self, state: &WorldState<S>) -> Result<(), EditError>
616 where
617 S::Vector: VectorOps,
618 S::AngVel: PartialEq,
619 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
620 {
621 if self
622 .gravity
623 .is_some_and(|gravity| !self.space.valid_vector(gravity))
624 {
625 return Err(EditError::InvalidGravity);
626 }
627 if self.solver_tolerance.is_nan() || self.solver_tolerance < 0.0 {
628 return Err(EditError::InvalidSolverTolerance);
629 }
630 let field_registrations = self.field_narrowphase.registrations();
631 if self.narrowphase.registrations() != state.registrations
632 || field_registrations.len() != state.field_registrations.len()
633 || state
634 .field_registrations
635 .iter()
636 .any(|kind| !field_registrations.contains(kind))
637 {
638 return Err(EditError::RegistrationMismatch);
639 }
640 if self.fields.len() != state.field_anchors.len()
641 || self
642 .fields
643 .iter()
644 .zip(&state.field_anchors)
645 .any(|(entry, &anchor)| entry.anchor != anchor)
646 {
647 return Err(EditError::FieldMismatch);
648 }
649 if !state.time.is_finite() || state.time < 0.0 {
650 return Err(EditError::InvalidTime);
651 }
652 state.bodies.validate(&self.space, &state.geometry)?;
653 for (&anchor, entry) in state.field_anchors.iter().zip(&self.fields) {
654 let Some(body) = state.bodies.get(anchor) else {
655 return Err(EditError::InvalidFieldBinding);
656 };
657 if entry.anchor != anchor
658 || !body.is_static()
659 || body.collision_group != 0
660 || body.collision_mask != 0
661 {
662 return Err(EditError::InvalidFieldBinding);
663 }
664 }
665 if state
666 .field_bindings
667 .windows(2)
668 .any(|pair| pair[0] >= pair[1])
669 {
670 return Err(EditError::InvalidFieldBinding);
671 }
672 for &(body, field) in &state.field_bindings {
673 let Some(entry) = self.fields.get(field.0 as usize) else {
674 return Err(EditError::InvalidFieldBinding);
675 };
676 if body == entry.anchor || state.bodies.get(body).is_none() {
677 return Err(EditError::InvalidFieldBinding);
678 }
679 }
680 for (&key, manifold) in &state.manifolds {
681 let (Some(body_a), Some(body_b)) = (state.bodies.get(key.0), state.bodies.get(key.1))
682 else {
683 return Err(EditError::InvalidManifold);
684 };
685 if key.0 >= key.1 || manifold.body_a != key.0 || manifold.body_b != key.1 {
686 return Err(EditError::InvalidManifold);
687 }
688 let field_pair = state.field_bindings.iter().any(|&(body, field)| {
689 let anchor = self.fields[field.0 as usize].anchor;
690 canonical_pair(body, anchor) == key
691 && state.bodies.get(body).is_some_and(|body| {
692 state.field_registrations.contains(&body.collider().kind())
693 })
694 });
695 let registered_pair = self.narrowphase.registrations().iter().any(|&(a, b)| {
696 let pair = (body_a.collider().kind(), body_b.collider().kind());
697 (a, b) == pair || (b, a) == pair
698 });
699 let body_pair = registered_pair
700 && !(body_a.is_static() && body_b.is_static())
701 && body_a.collision_mask & body_b.collision_group != 0
702 && body_b.collision_mask & body_a.collision_group != 0;
703 if (!field_pair && !body_pair) || !manifold.is_valid(&self.space, body_a, body_b) {
704 return Err(EditError::InvalidManifold);
705 }
706 }
707 Ok(())
708 }
709
710 pub fn restore(&mut self, state: &WorldState<S>) -> Result<(), EditError>
712 where
713 S::Vector: VectorOps,
714 S::AngVel: PartialEq,
715 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
716 {
717 self.check_restore(state)?;
718 self.bodies = state.bodies.clone();
719 self.geometry = state.geometry.clone();
720 let required_pool = state
721 .manifolds
722 .len()
723 .max(self.manifold_pool.len() + self.manifold_order.len());
724 if self.manifold_pool.capacity() < required_pool {
725 self.manifold_pool
726 .reserve_exact(required_pool - self.manifold_pool.len());
727 }
728 for key in self.manifold_order.drain(..) {
729 if let Some(manifold) = self.manifolds.remove(&key) {
730 self.manifold_pool.push(manifold);
731 }
732 }
733 for (&key, manifold) in &state.manifolds {
734 let restored = if let Some(mut restored) = self.manifold_pool.pop() {
735 restored.clone_from(manifold);
736 restored
737 } else {
738 manifold.clone()
739 };
740 self.manifolds.insert(key, restored);
741 self.manifold_order.push(key);
742 }
743 self.time = state.time;
744 self.field_bindings.clear();
745 self.field_bindings.extend_from_slice(&state.field_bindings);
746 self.dirty.mark_every(&self.bodies);
747 self.pair_order.clear();
748 self.constraints.clear();
749 self.touched_pairs.clear();
750 self.pending_contacts.clear();
751 self.broadphase_intervals.clear();
752 self.broadphase_active.clear();
753 self.island_parent.clear();
754 self.island_labels.clear();
755 self.scratch_islands = 0;
756 self.report = SolveReport::default();
757 Ok(())
758 }
759
760 pub fn step(&mut self, dt: f32) -> Result<(), EditError>
762 where
763 S::Vector: VectorOps,
764 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
765 {
766 if !dt.is_finite() || dt < 0.0 {
767 return Err(EditError::InvalidTimeStep);
768 }
769 let next_time = self.time + dt;
770 if !next_time.is_finite() {
771 return Err(EditError::InvalidTimeStep);
772 }
773 self.counters = StepCounters::default();
774 self.wake_approaching_contacts();
776 self.apply_forces(dt);
777 self.integrate(dt);
778 self.update_manifolds();
779 self.collect_constraints();
780 self.prepare_solve();
781 self.warm_start();
782 self.solve(dt);
783
784 self.time = next_time;
785 Ok(())
786 }
787
788 fn apply_forces(&mut self, dt: f32)
789 where
790 S::Vector: VectorOps,
791 {
792 let Some(acceleration) = self.gravity else {
793 return;
794 };
795 for body in self.bodies.iter_mut() {
796 if body.inv_mass() != 0.0 {
797 body.velocity = body.velocity + acceleration * dt;
798 }
799 }
800 }
801
802 fn integrate(&mut self, dt: f32)
803 where
804 S::Vector: VectorOps,
805 {
806 for i in 0..self.bodies.len() {
807 if self.bodies[i].inv_mass() == 0.0 {
808 continue;
809 }
810 integrate_body(&self.space, &mut self.bodies[i], dt);
811 let id = self.bodies.id_at(i);
812 self.dirty.mark(id);
813 }
814 }
815
816 fn update_manifolds(&mut self)
817 where
818 S::Vector: VectorOps,
819 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
820 {
821 let mut pairs = std::mem::take(&mut self.pair_order);
822 Self::fill_broadphase(
823 &self.bodies,
824 &self.geometry,
825 &self.space,
826 &mut self.broadphase_intervals,
827 &mut self.broadphase_active,
828 &mut pairs,
829 &mut self.counters,
830 );
831 let mut touched = std::mem::take(&mut self.touched_pairs);
832 touched.clear();
833 let mut pending = std::mem::take(&mut self.pending_contacts);
834 pending.clear();
835
836 for &key in &pairs {
837 let (i, j) = self.dense_pair(key);
838 let (a, b) = split_two_mut(self.bodies.dense_mut(), i, j);
839 if let Some(manifold) = self.manifolds.get_mut(&key) {
841 manifold.refresh(&self.space, a, b);
842 }
843 let Some(contact) = self.narrowphase.test(a, b, &self.geometry, &self.space) else {
844 continue;
845 };
846 if !self.manifolds.contains_key(&key)
847 && a.is_sleeping() != b.is_sleeping()
848 && !a.is_static()
849 && !b.is_static()
850 {
851 a.wake();
852 b.wake();
853 }
854 pending.push((key, contact));
855 }
856
857 self.field_manifolds(&mut pending);
858
859 touched.extend(pending.iter().map(|(key, _)| *key));
860 touched.sort_unstable();
861 touched.dedup();
862 self.counters.contacts = pending.len() as u32;
863 self.recycle_untouched_manifolds(&touched);
864 for &(key, contact) in &pending {
865 self.ensure_manifold(key, contact.restitution);
866 let Self {
867 bodies,
868 manifolds,
869 space,
870 ..
871 } = self;
872 let i = bodies
873 .dense_index(key.0)
874 .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}"));
875 let j = bodies
876 .dense_index(key.1)
877 .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}"));
878 let (a, b) = split_two_mut(bodies.dense_mut(), i, j);
879 manifolds
880 .get_mut(&key)
881 .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"))
882 .add_or_update(space, a, b, contact);
883 }
884 self.touched_pairs = touched;
885 self.pending_contacts = pending;
886 self.pair_order = pairs;
887 }
888
889 fn ensure_manifold(&mut self, key: PairKey, restitution: f32)
890 where
891 S::Vector: VectorOps,
892 {
893 if self.manifolds.contains_key(&key) {
894 return;
895 }
896 let manifold = if let Some(mut manifold) = self.manifold_pool.pop() {
897 manifold.reset(key.0, key.1, restitution);
898 manifold
899 } else {
900 let required = self.manifolds.len() + 1;
901 if self.manifold_pool.capacity() < required {
902 self.manifold_pool
903 .reserve_exact(required - self.manifold_pool.len());
904 }
905 Manifold::new(key.0, key.1, restitution)
906 };
907 self.manifolds.insert(key, manifold);
908 }
909
910 fn recycle_untouched_manifolds(&mut self, touched: &[PairKey]) {
911 for &key in &self.manifold_order {
912 if touched.binary_search(&key).is_err() {
913 if let Some(manifold) = self.manifolds.remove(&key) {
914 self.manifold_pool.push(manifold);
915 }
916 }
917 }
918 self.manifold_order.clear();
919 self.manifold_order.extend_from_slice(touched);
920 }
921
922 fn wake_approaching_contacts(&mut self)
923 where
924 S::Vector: VectorOps,
925 {
926 for key in &self.manifold_order {
927 let manifold = &self.manifolds[key];
928 let (Some(a), Some(b)) = (
929 self.bodies.get(manifold.body_a),
930 self.bodies.get(manifold.body_b),
931 ) else {
932 continue;
933 };
934 if a.is_static() || b.is_static() || a.is_sleeping() == b.is_sleeping() {
935 continue;
936 }
937 let approaching = manifold.points.iter().any(|point| {
938 let relative = self.space.velocity_at_point(b, point.world_point)
939 - self.space.velocity_at_point(a, point.world_point);
940 VectorOps::dot(relative, point.normal) < -WAKE_APPROACH_SPEED
941 });
942 if approaching {
943 let id = if a.is_sleeping() {
944 manifold.body_a
945 } else {
946 manifold.body_b
947 };
948 if let Some(body) = self.bodies.get_mut(id) {
949 body.wake();
950 }
951 }
952 }
953 }
954
955 fn field_manifolds(&mut self, pending: &mut Vec<(PairKey, Contact<S>)>)
956 where
957 S::Vector: VectorOps,
958 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
959 {
960 for index in 0..self.field_bindings.len() {
961 let (body, field) = self.field_bindings[index];
962 let Some(entry) = self.fields.get(field.0 as usize) else {
963 continue;
964 };
965 let anchor = entry.anchor;
966 let (Some(row), Some(_)) = (self.bodies.get(body), self.bodies.get(anchor)) else {
967 continue;
968 };
969 let Ok(found) =
970 self.field_narrowphase
971 .test(row, &self.geometry, entry.field.as_ref(), &self.space)
972 else {
973 continue;
974 };
975 if !found.separation.is_finite() || !found.error.is_finite() || found.error < 0.0 {
976 continue;
977 }
978 let certified_separation = (found.separation as f64 + found.error as f64).next_up();
979 if certified_separation >= 0.0 || -certified_separation > f32::MAX as f64 {
980 continue;
981 }
982 let mut penetration = (-certified_separation) as f32;
983 if penetration as f64 > -certified_separation {
984 penetration = penetration.next_down();
985 }
986 let key = canonical_pair(body, anchor);
987 let normal = if key.0 == anchor {
988 found.normal
989 } else {
990 -found.normal
991 };
992 let contact = Contact {
993 normal,
994 point: found.witness,
995 penetration,
996 restitution: (self.bodies[body].restitution + self.bodies[anchor].restitution)
997 * 0.5,
998 };
999 let (i, j) = self.dense_pair(key);
1000 let (a, b) = split_two_mut(self.bodies.dense_mut(), i, j);
1001 if let Some(manifold) = self.manifolds.get_mut(&key) {
1002 manifold.refresh(&self.space, a, b);
1003 }
1004 pending.push((key, contact));
1005 }
1006 }
1007
1008 fn collect_constraints(&mut self) {
1010 let mut parent = std::mem::take(&mut self.island_parent);
1011 let mut labels = std::mem::take(&mut self.island_labels);
1012 Self::fill_islands(
1013 &self.bodies,
1014 self.manifold_order.iter().copied(),
1015 &mut parent,
1016 &mut labels,
1017 );
1018 let mut units = std::mem::take(&mut self.constraints);
1019 units.clear();
1020 units.extend(self.manifold_order.iter().map(|&key| {
1021 let dense = self.dense_pair(key);
1022 ConstraintUnit {
1023 island: constraint_island(&self.bodies, &labels, dense),
1024 key,
1025 dense,
1026 }
1027 }));
1028 units.sort_unstable_by_key(|unit| (unit.island, unit.key));
1029 self.island_parent = parent;
1030 self.island_labels = labels;
1031 self.constraints = units;
1032 }
1033
1034 fn prepare_solve(&mut self)
1036 where
1037 S::Vector: VectorOps,
1038 {
1039 for unit in &self.constraints {
1040 debug_assert_eq!(unit.dense, self.dense_pair(unit.key));
1041 let (i, j) = unit.dense;
1042 let manifold = self
1043 .manifolds
1044 .get_mut(&unit.key)
1045 .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"));
1046 let (a, b) = split_two_mut(self.bodies.dense_mut(), i, j);
1047 for cp in &mut manifold.points {
1048 let v_rel = self.space.velocity_at_point(b, cp.world_point)
1049 - self.space.velocity_at_point(a, cp.world_point);
1050 let v_n = VectorOps::dot(v_rel, cp.normal);
1051
1052 cp.velocity_bias = if v_n < -RESTITUTION_THRESHOLD {
1053 manifold.restitution * v_n
1054 } else {
1055 0.0
1056 };
1057
1058 cp.tangent_impulse = 0.0;
1059 cp.tangent_dir = VectorOps::zero();
1060 }
1061 }
1062 }
1063
1064 fn warm_start(&mut self)
1065 where
1066 S::Vector: VectorOps,
1067 {
1068 for unit in &self.constraints {
1069 debug_assert_eq!(unit.dense, self.dense_pair(unit.key));
1070 let (i, j) = unit.dense;
1071 let manifold = self
1072 .manifolds
1073 .get(&unit.key)
1074 .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"));
1075 let (a, b) = split_two_mut(self.bodies.dense_mut(), i, j);
1076 for cp in &manifold.points {
1077 if cp.normal_impulse > 0.0 {
1078 self.space.apply_contact_impulse(
1079 a,
1080 b,
1081 cp.world_point,
1082 cp.normal,
1083 cp.normal_impulse,
1084 );
1085 }
1086 }
1087 }
1088 }
1089
1090 fn solve(&mut self, dt: f32)
1091 where
1092 S::Vector: VectorOps,
1093 {
1094 self.gather_islands();
1095 let Self {
1096 space,
1097 scratch,
1098 scratch_islands,
1099 pgs_iters,
1100 ..
1101 } = self;
1102 let iterations = *pgs_iters;
1103 let space = &*space;
1104 let islands = &mut scratch[..*scratch_islands];
1105 let workers = islands.len() / ISLANDS_PER_SOLVE_WORKER;
1106 if workers < 2 {
1107 for island in islands.iter_mut() {
1108 solve_island(space, iterations, dt, island);
1109 }
1110 } else {
1111 let chunk = islands.len().div_ceil(workers);
1112 par::for_each_chunk(islands, chunk, |chunk| {
1113 for island in chunk {
1114 solve_island(space, iterations, dt, island);
1115 }
1116 });
1117 }
1118 self.scatter_islands();
1119 }
1120
1121 fn gather_islands(&mut self) {
1122 self.scratch_local.clear();
1123 self.scratch_local
1124 .resize(self.bodies.len(), SCATTERED_NOWHERE);
1125 self.scratch_islands = 0;
1126 let mut current = None;
1127 for unit in &self.constraints {
1128 if current != Some(unit.island) {
1129 current = Some(unit.island);
1130 if self.scratch.len() == self.scratch_islands {
1131 self.scratch.push(IslandSolve::default());
1132 }
1133 let island = &mut self.scratch[self.scratch_islands];
1134 island.bodies.clear();
1135 island.dense.clear();
1136 island.units.clear();
1137 island.points.clear();
1138 self.scratch_islands += 1;
1139 }
1140 let island = &mut self.scratch[self.scratch_islands - 1];
1141 let (i, j) = unit.dense;
1142 let a = gather_body(island, &mut self.scratch_local, &self.bodies, i);
1143 let b = gather_body(island, &mut self.scratch_local, &self.bodies, j);
1144 let manifold = self
1145 .manifolds
1146 .get(&unit.key)
1147 .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"));
1148 let first = island.points.len() as u32;
1149 island.points.extend_from_slice(&manifold.points);
1150 island.units.push(ScratchUnit {
1151 key: unit.key,
1152 a,
1153 b,
1154 first,
1155 count: manifold.points.len() as u32,
1156 });
1157 }
1158 }
1159
1160 fn scatter_islands(&mut self) {
1161 let mut residual = 0.0_f32;
1162 for island in &self.scratch[..self.scratch_islands] {
1163 residual = residual.max(island.residual);
1164 for (local, &dense) in island.dense.iter().enumerate() {
1165 if dense != SCATTERED_NOWHERE {
1166 self.bodies[dense as usize] = island.bodies[local];
1167 }
1168 }
1169 for unit in &island.units {
1170 let manifold = self
1171 .manifolds
1172 .get_mut(&unit.key)
1173 .unwrap_or_else(|| panic!("{STALE_CONSTRAINT_KEY}"));
1174 let first = unit.first as usize;
1175 manifold
1176 .points
1177 .copy_from_slice(&island.points[first..first + unit.count as usize]);
1178 }
1179 }
1180 self.report = SolveReport {
1181 residual,
1182 converged: residual <= self.solver_tolerance,
1183 };
1184 }
1185
1186 pub fn broadphase_into(&mut self, pairs: &mut Vec<PairKey>) {
1188 self.counters = StepCounters::default();
1189 Self::fill_broadphase(
1190 &self.bodies,
1191 &self.geometry,
1192 &self.space,
1193 &mut self.broadphase_intervals,
1194 &mut self.broadphase_active,
1195 pairs,
1196 &mut self.counters,
1197 );
1198 }
1199
1200 fn fill_broadphase(
1202 bodies: &BodyArena<S>,
1203 geometry: &GeometryStore,
1204 space: &S,
1205 intervals: &mut Vec<RadialInterval>,
1206 active: &mut Vec<u32>,
1207 pairs: &mut Vec<PairKey>,
1208 counters: &mut StepCounters,
1209 ) {
1210 pairs.clear();
1211 intervals.clear();
1212 active.clear();
1213 let n = bodies.len();
1214 if n < 2 {
1215 return;
1216 }
1217 let certified = space.broadphase_bound() == BroadphaseBound::Certified;
1218
1219 let anchor = (1..n).fold(0, |lowest, dense| {
1220 if bodies.id_at(dense) < bodies.id_at(lowest) {
1221 dense
1222 } else {
1223 lowest
1224 }
1225 });
1226 let origin = bodies[anchor].position;
1227
1228 for dense in 0..n {
1229 let body = &bodies[dense];
1230 let radius = bounding_radius(geometry.get(body.collider()));
1231 let (lo, hi) = if certified {
1232 counters.distance_evals += 1;
1233 let d = space.distance(origin, body.position);
1234 let slack = d * BROADPHASE_TRIANGLE_SLACK;
1235 (d - radius - slack, d + radius + slack)
1236 } else {
1237 (f32::NEG_INFINITY, f32::INFINITY)
1238 };
1239 intervals.push(RadialInterval {
1240 lo,
1241 hi,
1242 radius,
1243 dense: dense as u32,
1244 id: bodies.id_at(dense),
1245 dynamic: body.inv_mass() != 0.0,
1246 group: body.collision_group,
1247 mask: body.collision_mask,
1248 });
1249 }
1250
1251 intervals.sort_unstable_by(|a, b| a.lo.total_cmp(&b.lo).then(a.id.cmp(&b.id)));
1252
1253 for i in 0..n {
1254 let entry = intervals[i];
1255
1256 let mut visits = counters.index_visits;
1257 active.retain(|&open| {
1258 visits += 1;
1259 intervals[open as usize].hi >= entry.lo
1260 });
1261 counters.index_visits = visits;
1262 for &open in active.iter() {
1263 counters.index_visits += 1;
1264 let other = intervals[open as usize];
1265 if !entry.dynamic && !other.dynamic {
1266 continue;
1267 }
1268
1269 if entry.group & other.mask == 0 || other.group & entry.mask == 0 {
1270 continue;
1271 }
1272 if certified {
1273 counters.distance_evals += 1;
1274 let gap = space.distance(
1275 bodies[other.dense as usize].position,
1276 bodies[entry.dense as usize].position,
1277 );
1278 if gap > other.radius + entry.radius {
1279 continue;
1280 }
1281 }
1282 pairs.push(canonical_pair(other.id, entry.id));
1283 }
1284 active.push(i as u32);
1285 }
1286
1287 pairs.sort_unstable();
1288 counters.candidates = pairs.len() as u32;
1289 }
1290
1291 pub fn hash_contacts(&self, hash: &mut StateHash) {
1293 for (key, manifold) in self.manifolds() {
1294 for id in [key.0, key.1] {
1295 hash.write_u32(id.slot());
1296 hash.write_u32(id.generation());
1297 }
1298 hash.write_u32(manifold.points.len() as u32);
1299 for point in &manifold.points {
1300 hash.write_f32(point.normal_impulse);
1301 }
1302 }
1303 }
1304
1305 pub fn state_hash(&self, sample_body: impl Fn(&RigidBody<S>, &mut Vec<u32>)) -> u64 {
1307 let mut order: Vec<BodyId> = (0..self.bodies.len())
1308 .map(|dense| self.bodies.id_at(dense))
1309 .collect();
1310 order.sort_unstable();
1311
1312 let mut words = Vec::new();
1313 for id in order {
1314 sample_body(&self.bodies[id], &mut words);
1315 }
1316 let mut hash = StateHash::new();
1317 hash.write_u32s(&words);
1318 self.hash_contacts(&mut hash);
1319 hash.finish()
1320 }
1321
1322 pub fn islands(&self) -> Vec<Island> {
1324 let mut parent = Vec::new();
1325 let mut labels = Vec::new();
1326 Self::fill_islands(
1327 &self.bodies,
1328 self.manifold_order.iter().copied(),
1329 &mut parent,
1330 &mut labels,
1331 );
1332
1333 let mut by_id: BTreeMap<BodyId, Island> = BTreeMap::new();
1334 for &key in &self.manifold_order {
1335 if self.bodies[key.0].inv_mass() == 0.0 && self.bodies[key.1].inv_mass() == 0.0 {
1336 continue;
1337 }
1338 let id = constraint_island(&self.bodies, &labels, self.dense_pair(key));
1339 let island = by_id.entry(id).or_insert_with(|| Island {
1340 id,
1341 bodies: Vec::new(),
1342 constraints: Vec::new(),
1343 });
1344 island.constraints.push(key);
1345 for member in [key.0, key.1] {
1346 if self.bodies[member].inv_mass() != 0.0 {
1347 island.bodies.push(member);
1348 }
1349 }
1350 }
1351
1352 let mut islands: Vec<Island> = by_id.into_values().collect();
1353 for island in &mut islands {
1354 island.bodies.sort_unstable();
1355 island.bodies.dedup();
1356 }
1357 islands
1358 }
1359
1360 fn fill_islands(
1361 bodies: &BodyArena<S>,
1362 touched: impl Iterator<Item = PairKey>,
1363 parent: &mut Vec<u32>,
1364 labels: &mut Vec<BodyId>,
1365 ) {
1366 let n = bodies.len();
1367 parent.clear();
1368 parent.extend(0..n as u32);
1369 labels.clear();
1370 labels.extend((0..n).map(|dense| bodies.id_at(dense)));
1371
1372 for key in touched {
1373 let (i, j) = (
1374 bodies
1375 .dense_index(key.0)
1376 .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}")),
1377 bodies
1378 .dense_index(key.1)
1379 .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}")),
1380 );
1381 if bodies[i].inv_mass() == 0.0 || bodies[j].inv_mass() == 0.0 {
1382 continue;
1383 }
1384 let (a, b) = (find_root(parent, i), find_root(parent, j));
1385 if a != b {
1386 parent[a.max(b)] = a.min(b) as u32;
1387 }
1388 }
1389
1390 for dense in 0..n {
1391 let root = find_root(parent, dense);
1392 labels[root] = labels[root].min(bodies.id_at(dense));
1393 }
1394 for dense in 0..n {
1395 let label = labels[find_root(parent, dense)];
1396 labels[dense] = label;
1397 }
1398 }
1399
1400 fn dense_pair(&self, key: PairKey) -> (usize, usize) {
1401 (
1402 self.bodies
1403 .dense_index(key.0)
1404 .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}")),
1405 self.bodies
1406 .dense_index(key.1)
1407 .unwrap_or_else(|| panic!("{STALE_MANIFOLD_BODY}")),
1408 )
1409 }
1410}
1411
1412fn find_root(parent: &mut [u32], mut dense: usize) -> usize {
1414 while parent[dense] as usize != dense {
1415 parent[dense] = parent[parent[dense] as usize];
1416 dense = parent[dense] as usize;
1417 }
1418 dense
1419}
1420
1421fn constraint_island<S: PhysicsSpace>(
1422 bodies: &BodyArena<S>,
1423 labels: &[BodyId],
1424 dense: (usize, usize),
1425) -> BodyId {
1426 let (i, j) = dense;
1427 debug_assert!(
1428 bodies[i].inv_mass() != 0.0 || bodies[j].inv_mass() != 0.0,
1429 "a contact between two static bodies has no island to solve in",
1430 );
1431 if bodies[i].inv_mass() != 0.0 {
1432 labels[i]
1433 } else {
1434 labels[j]
1435 }
1436}
1437
1438fn split_two_mut<T>(slice: &mut [T], i: usize, j: usize) -> (&mut T, &mut T) {
1440 debug_assert_ne!(i, j, "split_two_mut requires distinct indices");
1441 if i < j {
1442 let (left, right) = slice.split_at_mut(j);
1443 (&mut left[i], &mut right[0])
1444 } else {
1445 let (left, right) = slice.split_at_mut(i);
1446 (&mut right[0], &mut left[j])
1447 }
1448}
1449
1450fn gather_body<S: PhysicsSpace>(
1451 island: &mut IslandSolve<S>,
1452 local: &mut [u32],
1453 bodies: &BodyArena<S>,
1454 dense: usize,
1455) -> u32 {
1456 if bodies[dense].inv_mass() == 0.0 {
1457 island.bodies.push(bodies[dense]);
1458 island.dense.push(SCATTERED_NOWHERE);
1459 return island.bodies.len() as u32 - 1;
1460 }
1461 if local[dense] == SCATTERED_NOWHERE {
1462 local[dense] = island.bodies.len() as u32;
1463 island.bodies.push(bodies[dense]);
1464 island.dense.push(dense as u32);
1465 }
1466 local[dense]
1467}
1468
1469fn solve_island<S>(space: &S, iterations: usize, dt: f32, island: &mut IslandSolve<S>)
1471where
1472 S: PhysicsSpace,
1473 S::Vector: VectorOps,
1474{
1475 let IslandSolve {
1476 bodies,
1477 units,
1478 points,
1479 residual,
1480 push,
1481 saved,
1482 ..
1483 } = island;
1484 *residual = 0.0;
1485 for _ in 0..iterations {
1486 *residual = 0.0;
1487 for unit in units.iter() {
1488 let (a, b) = split_two_mut(bodies, unit.a as usize, unit.b as usize);
1489 let first = unit.first as usize;
1490 for cp in &mut points[first..first + unit.count as usize] {
1491 *residual = residual.max(solve_normal_then_tangent(space, a, b, cp));
1492 }
1493 }
1494 }
1495 if dt <= 0.0 {
1496 return;
1497 }
1498 saved.clear();
1500 saved.extend_from_slice(bodies);
1501 for body in bodies.iter_mut() {
1502 body.velocity = VectorOps::zero();
1503 body.angular_velocity = S::AngVel::zero();
1504 }
1505 push.clear();
1506 push.resize(points.len(), 0.0);
1507 for _ in 0..iterations {
1508 for unit in units.iter() {
1509 let (a, b) = split_two_mut(bodies, unit.a as usize, unit.b as usize);
1510 let range = unit.first as usize..(unit.first + unit.count) as usize;
1511 for (cp, accumulated) in points[range.clone()].iter().zip(&mut push[range]) {
1512 let depth = (cp.penetration - PENETRATION_SLOP).max(0.0);
1513 let target = (depth * BAUMGARTE_BETA).min(MAX_LINEAR_CORRECTION) / dt;
1514 let k_n = a.inv_mass() + b.inv_mass();
1515 if k_n <= 0.0 {
1516 continue;
1517 }
1518 let v_rel = b.velocity - a.velocity;
1519 let next =
1520 (*accumulated + (target - VectorOps::dot(v_rel, cp.normal)) / k_n).max(0.0);
1521 let impulse = next - *accumulated;
1522 *accumulated = next;
1523 if impulse != 0.0 {
1524 a.velocity = a.velocity - cp.normal * (impulse * a.inv_mass());
1525 b.velocity = b.velocity + cp.normal * (impulse * b.inv_mass());
1526 }
1527 }
1528 }
1529 }
1530 for (body, before) in bodies.iter_mut().zip(saved.iter()) {
1531 let from = body.position;
1532 integrate_body(space, body, dt);
1533 body.velocity = space.parallel_transport(from, body.position, before.velocity);
1534 body.angular_velocity = before.angular_velocity;
1535 }
1536}
1537
1538fn solve_normal_then_tangent<S>(
1539 space: &S,
1540 a: &mut RigidBody<S>,
1541 b: &mut RigidBody<S>,
1542 cp: &mut ContactPoint<S>,
1543) -> f32
1544where
1545 S: PhysicsSpace,
1546 S::Vector: VectorOps,
1547{
1548 debug_assert!(
1549 VectorOps::is_finite(cp.normal) && cp.penetration.is_finite(),
1550 "non-finite contact in solve_normal_then_tangent",
1551 );
1552
1553 let v_rel_n_vec =
1554 space.velocity_at_point(b, cp.world_point) - space.velocity_at_point(a, cp.world_point);
1555 let v_n = VectorOps::dot(v_rel_n_vec, cp.normal);
1556 let k_n = space.effective_mass_inv(a, b, cp.world_point, cp.normal);
1557
1558 let mut change = 0.0;
1559 if k_n > 0.0 {
1560 let dj = -(v_n + cp.velocity_bias) / k_n;
1561 let new_acc = (cp.normal_impulse + dj).max(0.0);
1562 let actual = new_acc - cp.normal_impulse;
1563 cp.normal_impulse = new_acc;
1564 change = actual.abs();
1565 if actual.abs() > 0.0 {
1566 space.apply_contact_impulse(a, b, cp.world_point, cp.normal, actual);
1567 }
1568 }
1569
1570 let previous = cp.tangent_dir * cp.tangent_impulse;
1571 let v_rel =
1572 space.velocity_at_point(b, cp.world_point) - space.velocity_at_point(a, cp.world_point);
1573 let slip = v_rel - cp.normal * VectorOps::dot(v_rel, cp.normal);
1574 let speed = VectorOps::length(slip);
1575 let mut impulse = previous;
1576 if speed > 1e-8 {
1577 let direction = slip * (1.0 / speed);
1578 let inverse_mass = space.effective_mass_inv(a, b, cp.world_point, direction);
1579 if inverse_mass > 0.0 {
1580 impulse = impulse + slip * (1.0 / inverse_mass);
1581 }
1582 }
1583 let magnitude = VectorOps::length(impulse);
1584 let limit = cp.normal_impulse * FRICTION_COEFF;
1585 if magnitude > limit {
1586 impulse = impulse * (limit / magnitude);
1587 }
1588 cp.tangent_impulse = VectorOps::length(impulse);
1589 cp.tangent_dir = if cp.tangent_impulse > 0.0 {
1590 impulse * (1.0 / cp.tangent_impulse)
1591 } else {
1592 VectorOps::zero()
1593 };
1594
1595 let delta = impulse - previous;
1596 let delta_magnitude = VectorOps::length(delta);
1597 if delta_magnitude > 0.0 {
1598 space.apply_contact_impulse(
1599 a,
1600 b,
1601 cp.world_point,
1602 delta * (1.0 / delta_magnitude),
1603 -delta_magnitude,
1604 );
1605 }
1606 change
1607}
1608
1609#[cfg(all(test, feature = "r2", feature = "r3", feature = "r4"))]
1610mod tests {
1611 use std::alloc::System;
1612 use std::collections::BTreeSet;
1613
1614 use super::*;
1615 use crate::determinism_fixture::{
1616 multi_island_world, record_flick_chamber_tape, replay_flick_chamber_tape, sample_body_r3,
1617 MULTI_ISLAND_DT, MULTI_ISLAND_STEPS, REPLAY_SEED,
1618 };
1619 use crate::euclidean_r3::{
1620 box_body, halfspace_body_r3, register_default_narrowphase, sphere_body_r3,
1621 };
1622 use glam::{Quat, Vec3};
1623 use loam_math::{Bivector3, EuclideanR3, Space};
1624 use loam_time::alloc::{bytes_allocated_by, CountingAllocator};
1625 use loam_time::Tape;
1626
1627 const PERMUTATION_SEEDS: [u64; 4] = [1, 0x9e37_79b9_7f4a_7c15, 0xdead_beef_cafe_f00d, 424_242];
1628
1629 fn constraint_order<S: PhysicsSpace>(world: &World<S>) -> Vec<PairKey> {
1630 world.constraints.iter().map(|unit| unit.key).collect()
1631 }
1632
1633 #[global_allocator]
1634 static COUNTING_ALLOCATOR: CountingAllocator<System> = CountingAllocator::new(System);
1635
1636 #[test]
1637 fn a_group_outside_the_others_mask_never_reaches_the_narrowphase() {
1638 let mut world = World::new(EuclideanR3);
1639 register_default_narrowphase(&mut world.narrowphase);
1640 let a = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 1.0, 1.0).unwrap());
1641 let b = world
1642 .push_body(sphere_body_r3(Vec3::new(0.5, 0.0, 0.0), Vec3::ZERO, 1.0, 1.0).unwrap());
1643 let mut pairs = Vec::new();
1644 world.broadphase_into(&mut pairs);
1645 assert_eq!(pairs.len(), 1, "the pair does not overlap to begin with");
1646
1647 world.bodies[a].collision_group = 0b01;
1648 world.bodies[a].collision_mask = 0b01;
1649 world.bodies[b].collision_group = 0b10;
1650 world.bodies[b].collision_mask = 0b10;
1651 world.broadphase_into(&mut pairs);
1652 assert!(
1653 pairs.is_empty(),
1654 "a filtered pair still reached the narrowphase"
1655 );
1656
1657 world.bodies[b].collision_mask = 0b11;
1658 world.broadphase_into(&mut pairs);
1659 assert!(
1660 pairs.is_empty(),
1661 "a one-sided mask edit produced a pair that collides in one direction"
1662 );
1663
1664 world.bodies[a].collision_mask = 0b11;
1665 world.broadphase_into(&mut pairs);
1666 assert_eq!(pairs.len(), 1, "agreement did not restore the pair");
1667 }
1668
1669 #[test]
1670 fn a_tape_replays_the_same_after_a_round_trip_through_its_byte_format() {
1671 let recorded = record_flick_chamber_tape(REPLAY_SEED);
1672 let decoded = Tape::decode(&recorded.encode()).expect("own encoding decodes");
1673 assert_eq!(decoded, recorded);
1674 assert_eq!(replay_flick_chamber_tape(&decoded), decoded.checkpoints());
1675 }
1676
1677 #[test]
1678 fn a_flipped_input_word_moves_the_replayed_state_hash() {
1679 let recorded = record_flick_chamber_tape(REPLAY_SEED);
1680 let throw = (0..recorded.ticks())
1681 .find(|&tick| recorded.input(tick).expect("inside the tape")[0] != u32::MAX)
1682 .expect("the scripted stream throws at least once");
1683
1684 let mut tampered = Tape::new(
1685 recorded.tick_hz(),
1686 recorded.seed(),
1687 recorded.words_per_tick(),
1688 );
1689 for tick in 0..recorded.ticks() {
1690 let mut frame: Vec<u32> = recorded.input(tick).expect("inside the tape").to_vec();
1691 if tick == throw {
1692 frame[2] ^= 1;
1693 }
1694 tampered.push_tick(&frame);
1695 }
1696
1697 assert_ne!(
1698 replay_flick_chamber_tape(&tampered),
1699 recorded.checkpoints(),
1700 "one ulp of input made no difference, so the tape is not driving \
1701 the simulation"
1702 );
1703 }
1704
1705 #[test]
1706 fn state_hash_is_invariant_under_arena_compaction() {
1707 let radii = [0.3_f32, 0.4, 0.5];
1708 let body = |i: usize| {
1709 sphere_body_r3(
1710 Vec3::new(i as f32, 2.0 * i as f32, 0.0),
1711 Vec3::new(0.0, -1.0, 0.5 * i as f32),
1712 radii[i],
1713 1.0 + i as f32,
1714 )
1715 .unwrap()
1716 };
1717
1718 let mut direct = World::new(EuclideanR3);
1719 for i in 0..3 {
1720 direct.push_body(body(i));
1721 }
1722
1723 let mut compacted = World::new(EuclideanR3);
1724 compacted.push_body(body(0));
1725 let doomed = compacted.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 9.0, 4.0).unwrap());
1726 compacted.push_body(body(1));
1727 compacted.push_body(body(2));
1728 assert!(compacted.despawn_body(doomed).is_ok());
1729
1730 let dense_order = |world: &World<EuclideanR3>| {
1731 world
1732 .bodies
1733 .iter()
1734 .map(|body| body.mass().to_bits())
1735 .collect::<Vec<_>>()
1736 };
1737 assert_ne!(
1738 dense_order(&direct),
1739 dense_order(&compacted),
1740 "the two worlds must differ in storage order or the pin is vacuous"
1741 );
1742 assert_eq!(
1743 direct.state_hash(sample_body_r3),
1744 compacted.state_hash(sample_body_r3),
1745 );
1746 }
1747
1748 #[test]
1749 fn state_hash_covers_carried_contact_impulses() {
1750 let mut world = multi_island_world();
1751 for _ in 0..MULTI_ISLAND_STEPS {
1752 world.step(MULTI_ISLAND_DT).unwrap();
1753 }
1754 let settled = world.state_hash(sample_body_r3);
1755
1756 let point = world
1757 .manifolds
1758 .values_mut()
1759 .flat_map(|manifold| manifold.points.iter_mut())
1760 .next()
1761 .expect("the fixture rests in contact");
1762 point.normal_impulse = f32::from_bits(point.normal_impulse.to_bits() ^ 1);
1763
1764 assert_ne!(
1765 settled,
1766 world.state_hash(sample_body_r3),
1767 "one ulp of warm-start impulse left the state hash unmoved"
1768 );
1769 }
1770
1771 const SPHERE_RADIUS: f32 = 0.5;
1772 const GRAVITY_Y: f32 = -9.8;
1773
1774 fn settled_sphere_stack(dt: f32, settle_steps: usize) -> World<EuclideanR3> {
1775 let mut world = World::new(EuclideanR3);
1776 register_default_narrowphase(&mut world.narrowphase);
1777 world
1778 .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
1779 .unwrap();
1780
1781 for level in 0..3 {
1782 let y = SPHERE_RADIUS + level as f32 * 2.0 * SPHERE_RADIUS;
1783 let id = world.push_body(
1784 sphere_body_r3(Vec3::new(0.0, y, 0.0), Vec3::ZERO, SPHERE_RADIUS, 1.0).unwrap(),
1785 );
1786 world.bodies[id].restitution = 0.0;
1787 }
1788 let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
1789 world.bodies[floor].restitution = 0.0;
1790
1791 for _ in 0..settle_steps {
1792 world.step(dt).unwrap();
1793 }
1794 world
1795 }
1796
1797 #[test]
1798 fn a_one_iteration_budget_separates_a_resting_contact_from_a_fast_impact() {
1799 const DT: f32 = 1.0 / 240.0;
1800 let mut resting = settled_sphere_stack(DT, 240);
1801 resting.set_solver_iterations(1);
1802 resting.step(DT).unwrap();
1803 let settled = resting.solve_report();
1804 assert!(
1805 resting.counters().contacts > 0,
1806 "the resting stack lost its contacts"
1807 );
1808 assert!(settled.converged, "resting residual {}", settled.residual);
1809
1810 let mut struck = settled_sphere_stack(DT, 240);
1811 struck.set_solver_iterations(1);
1812 let top = struck.bodies.id_at(2);
1813 struck
1814 .set_velocity(top, Vec3::new(0.0, -20.0, 0.0), Bivector3::ZERO)
1815 .unwrap();
1816 struck.step(DT).unwrap();
1817 let impact = struck.solve_report();
1818 assert!(!impact.converged, "impact residual {}", impact.residual);
1819 }
1820
1821 mod solver_contracts {
1822 use glam::{Vec2, Vec4};
1823 use loam_math::{EuclideanR2, EuclideanR4};
1824
1825 use super::*;
1826 use crate::euclidean_r2::{sphere_body, static_wall};
1827 use crate::euclidean_r4::{halfspace4_body_r4, sphere_body_r4};
1828
1829 fn kinetic_energy<S: PhysicsSpace<Inertia = f32>>(body: &RigidBody<S>, omega: f32) -> f32
1830 where
1831 S::Vector: VectorOps,
1832 {
1833 0.5 * body.mass() * VectorOps::length_squared(body.velocity)
1834 + 0.5 * body.inertia * omega * omega
1835 }
1836
1837 const FLOOR_HALF: Vec2 = Vec2::new(50.0, 1.0);
1838
1839 fn floor_r2() -> BodyDef<EuclideanR2> {
1840 static_wall(Vec2::new(0.0, -FLOOR_HALF.y), FLOOR_HALF).unwrap()
1841 }
1842
1843 const REBOUND_APPROACH: f32 = 2.0;
1844 const REBOUND_DT: f32 = 1.0 / 1000.0;
1845 const REBOUND_GAP: f32 = 0.01;
1846
1847 fn assert_elastic_rebound_conserves_kinetic_energy<S>(
1848 mut world: World<S>,
1849 faller: BodyId,
1850 up: S::Vector,
1851 angular_speed: impl Fn(S::AngVel) -> f32,
1852 ) where
1853 S: PhysicsSpace<Inertia = f32>,
1854 S::Vector: VectorOps,
1855 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
1856 {
1857 let energy_before = kinetic_energy(
1858 &world.bodies[faller],
1859 angular_speed(world.bodies[faller].angular_velocity),
1860 );
1861 let steps = (4.0 * REBOUND_GAP / (REBOUND_APPROACH * REBOUND_DT)).ceil() as usize;
1862 for _ in 0..steps {
1863 world.step(REBOUND_DT).unwrap();
1864 }
1865
1866 let body = &world.bodies[faller];
1867 let rebound = VectorOps::dot(body.velocity, up);
1868 assert!(rebound > 0.0, "body did not rebound: v·up = {rebound}");
1869 let spin = angular_speed(body.angular_velocity);
1870 assert!(spin < 1e-6, "central impact spun the body: |ω| = {spin}");
1871
1872 let energy_after = kinetic_energy(body, spin);
1873 let ratio = energy_after / energy_before;
1874 assert!(
1875 ratio <= 1.0 + 1e-4,
1876 "e = 1 impact added energy: {energy_before} -> {energy_after}"
1877 );
1878 assert!(
1879 ratio >= 1.0 - 1e-4,
1880 "e = 1 impact lost energy: {energy_before} -> {energy_after}"
1881 );
1882 }
1883
1884 #[test]
1885 fn perfectly_elastic_rebound_conserves_kinetic_energy_r2() {
1886 let mut world = World::new(EuclideanR2);
1887 crate::euclidean_r2::register_default_narrowphase(&mut world.narrowphase);
1888 let disk = world.push_body(
1889 sphere_body(
1890 Vec2::new(0.0, SPHERE_RADIUS + REBOUND_GAP),
1891 Vec2::new(0.0, -REBOUND_APPROACH),
1892 SPHERE_RADIUS,
1893 1.0,
1894 )
1895 .unwrap(),
1896 );
1897 let floor = world.push_body(floor_r2());
1898 world.bodies[disk].restitution = 1.0;
1899 world.bodies[floor].restitution = 1.0;
1900
1901 assert_elastic_rebound_conserves_kinetic_energy(world, disk, Vec2::Y, |omega| {
1902 omega.0.abs()
1903 });
1904 }
1905
1906 #[test]
1907 fn perfectly_elastic_rebound_conserves_kinetic_energy_r3() {
1908 let mut world = World::new(EuclideanR3);
1909 register_default_narrowphase(&mut world.narrowphase);
1910 let sphere = world.push_body(
1911 sphere_body_r3(
1912 Vec3::new(0.0, SPHERE_RADIUS + REBOUND_GAP, 0.0),
1913 Vec3::new(0.0, -REBOUND_APPROACH, 0.0),
1914 SPHERE_RADIUS,
1915 1.0,
1916 )
1917 .unwrap(),
1918 );
1919 let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
1920 world.bodies[sphere].restitution = 1.0;
1921 world.bodies[floor].restitution = 1.0;
1922
1923 assert_elastic_rebound_conserves_kinetic_energy(world, sphere, Vec3::Y, |omega| {
1924 omega.magnitude()
1925 });
1926 }
1927
1928 #[test]
1929 fn perfectly_elastic_rebound_conserves_kinetic_energy_r4() {
1930 let mut world = World::new(EuclideanR4);
1931 crate::euclidean_r4::register_default_narrowphase(&mut world.narrowphase);
1932 let sphere = world.push_body(
1933 sphere_body_r4(
1934 Vec4::new(0.0, SPHERE_RADIUS + REBOUND_GAP, 0.0, 0.0),
1935 Vec4::new(0.0, -REBOUND_APPROACH, 0.0, 0.0),
1936 SPHERE_RADIUS,
1937 1.0,
1938 )
1939 .unwrap(),
1940 );
1941 let floor = world.push_body(halfspace4_body_r4(Vec4::Y, 0.0).unwrap());
1942 world.bodies[sphere].restitution = 1.0;
1943 world.bodies[floor].restitution = 1.0;
1944
1945 assert_elastic_rebound_conserves_kinetic_energy(world, sphere, Vec4::Y, |omega| {
1946 omega.magnitude()
1947 });
1948 }
1949
1950 const SLIDE_SPEED: f32 = 5.0;
1951 const SLIDE_DT: f32 = 1.0 / 240.0;
1952 const SLIDE_STEPS: usize = 240;
1953
1954 fn assert_tangent_impulse_stays_inside_the_coulomb_cone<S>(
1955 mut world: World<S>,
1956 slider: BodyId,
1957 slide: S::Vector,
1958 up: S::Vector,
1959 ) where
1960 S: PhysicsSpace,
1961 S::Vector: VectorOps,
1962 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
1963 {
1964 let mut cone_ever_binds = false;
1965 for _ in 0..SLIDE_STEPS {
1966 world.step(SLIDE_DT).unwrap();
1967 for (_, manifold) in world.manifolds() {
1968 for cp in &manifold.points {
1969 let cap = cp.normal_impulse * FRICTION_COEFF;
1970 assert!(
1971 cp.tangent_impulse <= cap + 1e-6,
1972 "friction escaped the cone: jt = {}, μ·jn = {cap}",
1973 cp.tangent_impulse
1974 );
1975 assert!(
1976 cp.tangent_impulse >= 0.0,
1977 "tangent accumulator went negative: {}",
1978 cp.tangent_impulse
1979 );
1980 if cap > 1e-6 && cp.tangent_impulse >= cap - 1e-6 {
1981 cone_ever_binds = true;
1982 }
1983 }
1984 }
1985 }
1986
1987 assert!(
1988 cone_ever_binds,
1989 "friction never saturated, so the clamp was never exercised"
1990 );
1991 let body = &world.bodies[slider];
1992 let along = VectorOps::dot(body.velocity, slide);
1993 assert!(
1994 along < SLIDE_SPEED,
1995 "friction did not brake the slide: v·slide = {along}"
1996 );
1997
1998 let contact = world.space.exp(body.position, up * -SPHERE_RADIUS);
1999 let angular_at_contact = world.space.velocity_at_point(body, contact) - body.velocity;
2000 let backspin = VectorOps::dot(angular_at_contact, slide);
2001 assert!(
2002 backspin < -1e-3,
2003 "friction spun the body away from rolling: contact-point \
2004 angular velocity along the slide is {backspin}"
2005 );
2006 }
2007
2008 #[test]
2009 fn tangent_impulse_stays_inside_the_coulomb_cone_r2() {
2010 let mut world = World::new(EuclideanR2);
2011 crate::euclidean_r2::register_default_narrowphase(&mut world.narrowphase);
2012 world.set_gravity(Some(Vec2::new(0.0, GRAVITY_Y))).unwrap();
2013 let disk = world.push_body(
2014 sphere_body(
2015 Vec2::new(0.0, SPHERE_RADIUS),
2016 Vec2::new(SLIDE_SPEED, 0.0),
2017 SPHERE_RADIUS,
2018 1.0,
2019 )
2020 .unwrap(),
2021 );
2022 let floor = world.push_body(floor_r2());
2023 world.bodies[disk].restitution = 0.0;
2024 world.bodies[floor].restitution = 0.0;
2025
2026 assert_tangent_impulse_stays_inside_the_coulomb_cone(world, disk, Vec2::X, Vec2::Y);
2027 }
2028
2029 #[test]
2030 fn tangent_impulse_stays_inside_the_coulomb_cone_r3() {
2031 let mut world = World::new(EuclideanR3);
2032 register_default_narrowphase(&mut world.narrowphase);
2033 world
2034 .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
2035 .unwrap();
2036 let sphere = world.push_body(
2037 sphere_body_r3(
2038 Vec3::new(0.0, SPHERE_RADIUS, 0.0),
2039 Vec3::new(SLIDE_SPEED, 0.0, 0.0),
2040 SPHERE_RADIUS,
2041 1.0,
2042 )
2043 .unwrap(),
2044 );
2045 let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
2046 world.bodies[sphere].restitution = 0.0;
2047 world.bodies[floor].restitution = 0.0;
2048
2049 assert_tangent_impulse_stays_inside_the_coulomb_cone(world, sphere, Vec3::X, Vec3::Y);
2050 }
2051
2052 #[test]
2053 fn tangent_impulse_stays_inside_the_coulomb_cone_r4() {
2054 let mut world = World::new(EuclideanR4);
2055 crate::euclidean_r4::register_default_narrowphase(&mut world.narrowphase);
2056 world
2057 .set_gravity(Some(Vec4::new(0.0, GRAVITY_Y, 0.0, 0.0)))
2058 .unwrap();
2059 let sphere = world.push_body(
2060 sphere_body_r4(
2061 Vec4::new(0.0, SPHERE_RADIUS, 0.0, 0.0),
2062 Vec4::new(SLIDE_SPEED, 0.0, 0.0, 0.0),
2063 SPHERE_RADIUS,
2064 1.0,
2065 )
2066 .unwrap(),
2067 );
2068 let floor = world.push_body(halfspace4_body_r4(Vec4::Y, 0.0).unwrap());
2069 world.bodies[sphere].restitution = 0.0;
2070 world.bodies[floor].restitution = 0.0;
2071
2072 assert_tangent_impulse_stays_inside_the_coulomb_cone(world, sphere, Vec4::X, Vec4::Y);
2073 }
2074
2075 const STACK_DT: f32 = 1.0 / 240.0;
2076 const STACK_SETTLE_STEPS: usize = 400;
2077 const STACK_LEVELS: usize = 3;
2078
2079 fn clear_warm_start<S: PhysicsSpace>(world: &mut World<S>) {
2080 for manifold in world.manifolds.values_mut() {
2081 for cp in &mut manifold.points {
2082 cp.normal_impulse = 0.0;
2083 }
2084 }
2085 }
2086
2087 fn velocities<S: PhysicsSpace>(world: &World<S>) -> Vec<S::Vector>
2088 where
2089 S::Vector: VectorOps,
2090 {
2091 world.bodies.iter().map(|b| b.velocity).collect()
2092 }
2093
2094 fn normal_impulses<S: PhysicsSpace>(world: &World<S>) -> Vec<f32> {
2095 world
2096 .manifolds()
2097 .map(|(_, manifold)| manifold)
2098 .flat_map(|m| m.points.iter().map(|cp| cp.normal_impulse))
2099 .collect()
2100 }
2101
2102 fn max_velocity_gap<V: VectorOps>(a: &[V], b: &[V]) -> f32 {
2103 assert_eq!(a.len(), b.len(), "body layouts diverged");
2104 a.iter()
2105 .zip(b)
2106 .map(|(x, y)| VectorOps::length(*x - *y))
2107 .fold(0.0_f32, f32::max)
2108 }
2109
2110 fn max_scalar_gap(a: &[f32], b: &[f32]) -> f32 {
2111 assert_eq!(a.len(), b.len(), "manifold layouts diverged");
2112 a.iter()
2113 .zip(b)
2114 .map(|(x, y)| (x - y).abs())
2115 .fold(0.0_f32, f32::max)
2116 }
2117
2118 fn assert_warm_started_step_matches_cold_started_converged_step<S>(
2119 fixture: impl Fn() -> World<S>,
2120 ) where
2121 S: PhysicsSpace,
2122 S::Vector: VectorOps,
2123 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
2124 {
2125 let mut warm = fixture();
2126 let mut cold_converged = fixture();
2127 let mut cold_default = fixture();
2128
2129 assert!(
2130 warm.manifolds()
2131 .map(|(_, manifold)| manifold)
2132 .flat_map(|m| &m.points)
2133 .any(|cp| cp.normal_impulse > 0.0),
2134 "fixture carries no warm-start payload"
2135 );
2136
2137 clear_warm_start(&mut cold_converged);
2138 clear_warm_start(&mut cold_default);
2139 cold_converged.set_solver_iterations(400);
2140
2141 warm.step(STACK_DT).unwrap();
2142 cold_converged.step(STACK_DT).unwrap();
2143 cold_default.step(STACK_DT).unwrap();
2144
2145 let converged = velocities(&cold_converged);
2146 let warm_gap = max_velocity_gap(&velocities(&warm), &converged);
2147 let cold_gap = max_velocity_gap(&velocities(&cold_default), &converged);
2148
2149 assert!(
2150 warm_gap < 1e-5,
2151 "warm-started step diverged from the converged solve by {warm_gap} m/s"
2152 );
2153 assert!(
2154 warm_gap < cold_gap,
2155 "warm start bought no convergence: warm {warm_gap} vs cold {cold_gap}"
2156 );
2157
2158 let impulse_gap =
2159 max_scalar_gap(&normal_impulses(&warm), &normal_impulses(&cold_converged));
2160 let reference = normal_impulses(&cold_converged)
2161 .into_iter()
2162 .fold(0.0_f32, f32::max);
2163 assert!(
2164 impulse_gap < 1e-3 * reference,
2165 "warm-started accumulator diverged by {impulse_gap} against a peak impulse of {reference}"
2166 );
2167 }
2168
2169 fn settled_disk_stack_r2() -> World<EuclideanR2> {
2170 let mut world = World::new(EuclideanR2);
2171 crate::euclidean_r2::register_default_narrowphase(&mut world.narrowphase);
2172 world.set_gravity(Some(Vec2::new(0.0, GRAVITY_Y))).unwrap();
2173
2174 for level in 0..STACK_LEVELS {
2175 let y = SPHERE_RADIUS + level as f32 * 2.0 * SPHERE_RADIUS;
2176 let id = world.push_body(
2177 sphere_body(Vec2::new(0.0, y), Vec2::ZERO, SPHERE_RADIUS, 1.0).unwrap(),
2178 );
2179 world.bodies[id].restitution = 0.0;
2180 }
2181 let floor = world.push_body(floor_r2());
2182 world.bodies[floor].restitution = 0.0;
2183
2184 for _ in 0..STACK_SETTLE_STEPS {
2185 world.step(STACK_DT).unwrap();
2186 }
2187 world
2188 }
2189
2190 fn settled_sphere_stack_r4() -> World<EuclideanR4> {
2191 let mut world = World::new(EuclideanR4);
2192 crate::euclidean_r4::register_default_narrowphase(&mut world.narrowphase);
2193 world
2194 .set_gravity(Some(Vec4::new(0.0, GRAVITY_Y, 0.0, 0.0)))
2195 .unwrap();
2196
2197 for level in 0..STACK_LEVELS {
2198 let y = SPHERE_RADIUS + level as f32 * 2.0 * SPHERE_RADIUS;
2199 let id = world.push_body(
2200 sphere_body_r4(Vec4::new(0.0, y, 0.0, 0.0), Vec4::ZERO, SPHERE_RADIUS, 1.0)
2201 .unwrap(),
2202 );
2203 world.bodies[id].restitution = 0.0;
2204 }
2205 let floor = world.push_body(halfspace4_body_r4(Vec4::Y, 0.0).unwrap());
2206 world.bodies[floor].restitution = 0.0;
2207
2208 for _ in 0..STACK_SETTLE_STEPS {
2209 world.step(STACK_DT).unwrap();
2210 }
2211 world
2212 }
2213
2214 #[test]
2215 fn warm_started_step_matches_cold_started_converged_step_r2() {
2216 assert_warm_started_step_matches_cold_started_converged_step(settled_disk_stack_r2);
2217 }
2218
2219 #[test]
2220 fn warm_started_step_matches_cold_started_converged_step_r3() {
2221 assert_warm_started_step_matches_cold_started_converged_step(|| {
2222 settled_sphere_stack(STACK_DT, STACK_SETTLE_STEPS)
2223 });
2224 }
2225
2226 #[test]
2227 fn warm_started_step_matches_cold_started_converged_step_r4() {
2228 assert_warm_started_step_matches_cold_started_converged_step(settled_sphere_stack_r4);
2229 }
2230 }
2231
2232 #[test]
2233 fn shrinking_normal_impulse_releases_applied_friction_even_without_slip() {
2234 use crate::euclidean_r2::sphere_body;
2235 use glam::Vec2;
2236 use loam_math::EuclideanR2;
2237
2238 for slip in [0.0, 1.0] {
2239 let mut geometry = crate::geometry::GeometryStore::default();
2240 let mut a = sphere_body(Vec2::ZERO, Vec2::ZERO, 1.0, 0.0)
2241 .unwrap()
2242 .into_row(&mut geometry, &EuclideanR2);
2243 let mut b = sphere_body(Vec2::ZERO, Vec2::new(slip, 2.0), 1.0, 1.0)
2244 .unwrap()
2245 .into_row(&mut geometry, &EuclideanR2);
2246 let previous_friction = 2.0 * FRICTION_COEFF;
2247 let mut contact = ContactPoint {
2248 world_point: Vec2::ZERO,
2249 anchor_a: Vec2::ZERO,
2250 anchor_b: Vec2::ZERO,
2251 normal: Vec2::Y,
2252 penetration: 0.0,
2253 normal_impulse: 2.0,
2254 tangent_dir: Vec2::X,
2255 tangent_impulse: previous_friction,
2256 velocity_bias: 0.0,
2257 };
2258 solve_normal_then_tangent(&EuclideanR2, &mut a, &mut b, &mut contact);
2259 assert_eq!(contact.normal_impulse, 0.0);
2260 assert_eq!(contact.tangent_impulse, 0.0);
2261 assert!((b.velocity - Vec2::X * (slip + previous_friction)).length() < 1e-6);
2262 }
2263 }
2264
2265 const ISLAND_X: [f32; 4] = [-4.0, 0.0, 4.0, 8.0];
2266
2267 fn settled_islands(dt: f32, settle_steps: usize) -> (World<EuclideanR3>, BodyId, Vec<BodyId>) {
2268 let mut world = World::new(EuclideanR3);
2269 register_default_narrowphase(&mut world.narrowphase);
2270 world
2271 .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
2272 .unwrap();
2273
2274 let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
2275 world.bodies[floor].restitution = 0.0;
2276 let mut spheres = Vec::with_capacity(ISLAND_X.len());
2277 for x in ISLAND_X {
2278 let id = world.push_body(island_sphere(x));
2279 world.bodies[id].restitution = 0.0;
2280 spheres.push(id);
2281 }
2282
2283 for _ in 0..settle_steps {
2284 world.step(dt).unwrap();
2285 }
2286 (world, floor, spheres)
2287 }
2288
2289 fn island_sphere(x: f32) -> BodyDef<EuclideanR3> {
2290 sphere_body_r3(
2291 Vec3::new(x, SPHERE_RADIUS, 0.0),
2292 Vec3::ZERO,
2293 SPHERE_RADIUS,
2294 1.0,
2295 )
2296 .unwrap()
2297 }
2298
2299 fn body_state(world: &World<EuclideanR3>, id: BodyId) -> (Vec3, Vec3, Bivector3) {
2300 let body = &world.bodies[id];
2301 (body.position, body.velocity, body.angular_velocity)
2302 }
2303
2304 fn normal_impulses(world: &World<EuclideanR3>, key: PairKey) -> Vec<f32> {
2305 world.manifolds[&key]
2306 .points
2307 .iter()
2308 .map(|cp| cp.normal_impulse)
2309 .collect()
2310 }
2311
2312 #[test]
2313 fn sleeping_after_a_step_removes_inactive_islands_immediately() {
2314 let (mut world, floor, spheres) = settled_islands(1.0 / 240.0, 1);
2315 for id in &spheres {
2316 assert!(world.manifolds.contains_key(&(floor, *id)));
2317 world.bodies[*id].sleep();
2318 }
2319 assert!(world.islands().is_empty());
2320 world.bodies[spheres[0]].wake();
2321 let islands = world.islands();
2322 assert_eq!(islands.len(), 1);
2323 assert_eq!(islands[0].bodies, vec![spheres[0]]);
2324 }
2325
2326 #[test]
2327 fn despawn_preserves_surviving_manifold_keys_and_warm_start_impulses() {
2328 let dt = 1.0 / 240.0;
2329 let settle_steps = 400;
2330 let (mut world, floor, spheres) = settled_islands(dt, settle_steps);
2331 let (mut control, _, control_spheres) = settled_islands(dt, settle_steps);
2332
2333 let doomed = spheres[1];
2334 let keeper = spheres[3];
2335 let keeper_key = (floor, keeper);
2336 let control_key = (floor, control_spheres[3]);
2337
2338 let impulses_before = normal_impulses(&world, keeper_key);
2339 assert!(
2340 impulses_before.iter().any(|&jn| jn > 0.0),
2341 "fixture carries no warm-start payload, so nothing below is being preserved"
2342 );
2343 let keeper_position_before = world.bodies.dense_index(keeper).unwrap();
2344
2345 assert!(world.despawn_body(doomed).is_ok());
2346
2347 assert_ne!(
2348 world.bodies.dense_index(keeper).unwrap(),
2349 keeper_position_before,
2350 "despawn moved no surviving body, so this test never reached the compaction case"
2351 );
2352 assert!(
2353 !world
2354 .manifolds
2355 .keys()
2356 .any(|&(a, b)| a == doomed || b == doomed),
2357 "the removed body's manifolds outlived it"
2358 );
2359 assert_eq!(
2360 normal_impulses(&world, keeper_key),
2361 impulses_before,
2362 "compaction disturbed a surviving manifold's warm-start impulses"
2363 );
2364
2365 for _ in 0..60 {
2366 world.step(dt).unwrap();
2367 control.step(dt).unwrap();
2368 }
2369
2370 assert_eq!(
2371 body_state(&world, keeper),
2372 body_state(&control, control_spheres[3]),
2373 "an unrelated despawn perturbed a surviving island"
2374 );
2375 let impulses_after = normal_impulses(&world, keeper_key);
2376 assert_eq!(
2377 impulses_after,
2378 normal_impulses(&control, control_key),
2379 "an unrelated despawn moved a surviving manifold's impulses"
2380 );
2381 assert!(
2382 impulses_after.iter().any(|&jn| jn > 0.0),
2383 "the surviving contact stopped carrying an impulse"
2384 );
2385 }
2386
2387 #[test]
2388 fn spawn_mid_simulation_leaves_existing_islands_bit_identical() {
2389 let dt = 1.0 / 240.0;
2390 let settle_steps = 400;
2391 let (mut world, floor, spheres) = settled_islands(dt, settle_steps);
2392 let (mut control, _, control_spheres) = settled_islands(dt, settle_steps);
2393
2394 let keeper = spheres[0];
2395 let keeper_key = (floor, keeper);
2396 let newcomer = world.push_body(island_sphere(12.0));
2397 world.bodies[newcomer].restitution = 0.0;
2398
2399 for _ in 0..60 {
2400 world.step(dt).unwrap();
2401 control.step(dt).unwrap();
2402 }
2403
2404 assert!(
2405 world.manifolds.contains_key(&(floor, newcomer)),
2406 "the spawned body never made contact, so it exercised no solver state"
2407 );
2408 assert_eq!(
2409 body_state(&world, keeper),
2410 body_state(&control, control_spheres[0]),
2411 "a spawn perturbed an existing island"
2412 );
2413 assert_eq!(
2414 normal_impulses(&world, keeper_key),
2415 normal_impulses(&control, (floor, control_spheres[0])),
2416 "a spawn moved an existing manifold's warm-start impulses"
2417 );
2418 }
2419
2420 #[test]
2421 fn a_recycled_slot_inherits_no_manifold_from_the_previous_body() {
2422 let dt = 1.0 / 240.0;
2423 let (mut world, floor, spheres) = settled_islands(dt, 400);
2424 let doomed = spheres[1];
2425 assert!(world.manifolds.contains_key(&(floor, doomed)));
2426
2427 assert!(world.despawn_body(doomed).is_ok());
2428 assert_eq!(
2429 world.despawn_body(doomed),
2430 Err(EditError::StaleHandle),
2431 "a stale handle despawned a second body"
2432 );
2433 assert!(world.bodies.get(doomed).is_none());
2434
2435 let reborn = world.push_body(island_sphere(ISLAND_X[1]));
2436 assert_eq!(
2437 reborn.slot(),
2438 doomed.slot(),
2439 "the slot was not recycled, so this test is not exercising aliasing"
2440 );
2441 assert_ne!(reborn, doomed);
2442 assert!(
2443 world.bodies.get(doomed).is_none(),
2444 "the stale handle resolved to the body that took its slot"
2445 );
2446
2447 world.step(dt).unwrap();
2448 assert!(
2449 world.manifolds.contains_key(&(floor, reborn)),
2450 "the respawned body made no contact"
2451 );
2452 assert!(
2453 !world.manifolds.contains_key(&(floor, doomed)),
2454 "a manifold keyed on the despawned body came back with the slot"
2455 );
2456 }
2457
2458 const EDIT_DT: f32 = 1.0 / 240.0;
2459 const EDIT_SETTLE_STEPS: usize = 400;
2460
2461 #[test]
2462 fn a_rejected_edit_leaves_the_body_its_inertia_and_its_contacts_untouched() {
2463 let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2464 let id = spheres[0];
2465 let key = (floor, id);
2466 let before = body_state(&world, id);
2467 let mass = world.bodies[id].mass();
2468 let inv_mass = world.bodies[id].inv_mass();
2469 let inertia = world.bodies[id].inertia;
2470 let orientation = world.bodies[id].orientation;
2471 let stale = BodyId::forge(u32::MAX, 0);
2472 let impulses = normal_impulses(&world, key);
2473 let _ = world.drain_dirty().count();
2474 assert!(
2475 impulses.iter().any(|&jn| jn > 0.0),
2476 "the fixture carries no warm start, so a preserved contact proves nothing"
2477 );
2478
2479 assert_eq!(
2480 world.set_mass_properties(id, f32::NAN, 1.0),
2481 Err(EditError::InvalidMass)
2482 );
2483 assert_eq!(
2484 world.set_mass_properties(id, 3.0, f32::NAN),
2485 Err(EditError::InvalidInertia)
2486 );
2487 assert_eq!(
2488 world.set_restitution(id, -1.0),
2489 Err(EditError::InvalidRestitution)
2490 );
2491 assert_eq!(
2492 world.set_collider(
2493 id,
2494 Collider::Box3 {
2495 half_extents: Vec3::ONE
2496 },
2497 2.0
2498 ),
2499 Err(EditError::UnsupportedCollider)
2500 );
2501 while world.reclaim_geometry().is_some() {}
2502 assert_eq!(
2503 world.set_collider(id, Collider::sphere_at_origin(0.5), f32::INFINITY),
2504 Err(EditError::InvalidInertia)
2505 );
2506 assert!(
2507 world.reclaim_geometry().is_some(),
2508 "a rejected inertia swallowed the caller's collider"
2509 );
2510 assert_eq!(
2511 world.set_pose(id, Vec3::splat(f32::NAN), orientation),
2512 Err(EditError::NotFinite)
2513 );
2514 let spun = loam_math::Iso3 {
2515 rotation: glam::Quat::from_xyzw(f32::NAN, 0.0, 0.0, 1.0),
2516 translation: Vec3::ZERO,
2517 };
2518 assert_eq!(
2519 world.set_pose(id, Vec3::ZERO, spun),
2520 Err(EditError::NotFinite)
2521 );
2522 let zero_rotation = loam_math::Iso3 {
2523 rotation: glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
2524 translation: Vec3::ZERO,
2525 };
2526 assert_eq!(
2527 world.set_pose(id, Vec3::ZERO, zero_rotation),
2528 Err(EditError::NotFinite)
2529 );
2530 assert_eq!(
2531 world.set_velocity(id, Vec3::ZERO, Bivector3::new(f32::NAN, 0.0, 0.0)),
2532 Err(EditError::NotFinite)
2533 );
2534 assert_eq!(
2535 world.set_velocity(stale, Vec3::ZERO, Bivector3::ZERO),
2536 Err(EditError::StaleHandle)
2537 );
2538 assert_eq!(
2539 world.drain_dirty().count(),
2540 0,
2541 "a rejected edit published a pose change"
2542 );
2543
2544 assert_eq!(body_state(&world, id), before);
2545 assert_eq!(world.bodies[id].mass(), mass);
2546 assert_eq!(world.bodies[id].inv_mass(), inv_mass);
2547 assert_eq!(world.bodies[id].inertia, inertia);
2548 assert_eq!(normal_impulses(&world, key), impulses);
2549
2550 let mut world4 = World::new(loam_math::EuclideanR4);
2551 let id4 = world4.push_body(
2552 crate::euclidean_r4::sphere_body_r4(glam::Vec4::ZERO, glam::Vec4::ZERO, 0.5, 1.0)
2553 .unwrap(),
2554 );
2555 let before4 = world4.bodies[id4].orientation;
2556 let scale = std::f32::consts::FRAC_1_SQRT_2;
2557 let invalid4 = loam_math::Iso4Flat {
2558 rotation: loam_math::Rotor4 {
2559 s: scale,
2560 xy: 0.0,
2561 xz: 0.0,
2562 xw: 0.0,
2563 yz: 0.0,
2564 yw: 0.0,
2565 zw: 0.0,
2566 xyzw: scale,
2567 },
2568 translation: glam::Vec4::ZERO,
2569 };
2570 assert_eq!(
2571 world4.set_pose(id4, glam::Vec4::ZERO, invalid4),
2572 Err(EditError::NotFinite)
2573 );
2574 assert_eq!(world4.bodies[id4].orientation, before4);
2575 }
2576
2577 #[test]
2578 fn warm_start_impulses_survive_a_step_and_a_snapshot_restore() {
2579 let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2580 let key = (floor, spheres[0]);
2581 let settled = normal_impulses(&world, key);
2582 assert!(settled.iter().any(|&jn| jn > 0.0));
2583
2584 let state = world.snapshot();
2585 world.step(EDIT_DT).unwrap();
2586
2587 let stepped = normal_impulses(&world, key);
2588 assert_eq!(
2589 stepped.len(),
2590 settled.len(),
2591 "a step dropped a contact slot"
2592 );
2593 for (after, before) in stepped.iter().zip(&settled) {
2594 assert!(
2595 *after > 0.0 && (after - before).abs() < 1e-3,
2596 "a step reset the accumulator: {before} became {after}"
2597 );
2598 }
2599
2600 assert!(world
2601 .apply_impulse(spheres[0], Vec3::new(0.0, 20.0, 0.0))
2602 .is_ok());
2603 for _ in 0..30 {
2604 world.step(EDIT_DT).unwrap();
2605 }
2606 assert!(
2607 !world.manifolds.contains_key(&key),
2608 "the launched sphere kept its contact, so the restore has nothing to undo"
2609 );
2610
2611 assert!(world.restore(&state).is_ok());
2612 assert_eq!(normal_impulses(&world, key), settled);
2613 assert_eq!(world.time, state.time);
2614 }
2615
2616 #[test]
2617 fn invalid_snapshot_state_is_refused_before_the_world_changes() {
2618 let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2619 let key = (floor, spheres[0]);
2620 let before = world.state_hash(sample_body_r3);
2621 let time = world.time;
2622 let impulses = normal_impulses(&world, key);
2623 let unchanged = |world: &World<EuclideanR3>| {
2624 assert_eq!(world.state_hash(sample_body_r3), before);
2625 assert_eq!(world.time, time);
2626 assert_eq!(normal_impulses(world, key), impulses);
2627 };
2628
2629 let mut invalid_body = world.snapshot();
2630 invalid_body.bodies[spheres[0]].orientation.rotation = Quat::from_xyzw(0.0, 0.0, 0.0, 2.0);
2631 assert_eq!(world.restore(&invalid_body), Err(EditError::InvalidBody));
2632 unchanged(&world);
2633
2634 let mut invalid_time = world.snapshot();
2635 invalid_time.time = f32::NAN;
2636 assert_eq!(world.restore(&invalid_time), Err(EditError::InvalidTime));
2637 unchanged(&world);
2638
2639 let mut invalid_manifold = world.snapshot();
2640 invalid_manifold.manifolds.get_mut(&key).unwrap().points[0].normal = Vec3::splat(f32::NAN);
2641 assert_eq!(
2642 world.restore(&invalid_manifold),
2643 Err(EditError::InvalidManifold)
2644 );
2645 unchanged(&world);
2646 }
2647
2648 #[test]
2649 fn invalid_gravity_and_time_steps_leave_body_state_and_time_unchanged() {
2650 let mut world = World::new(EuclideanR3);
2651 let body = world.push_body(sphere_body_r3(Vec3::X, Vec3::Y, 0.5, 1.0).unwrap());
2652 let before = world.state_hash(sample_body_r3);
2653
2654 assert_eq!(
2655 world.set_gravity(Some(Vec3::splat(f32::NAN))),
2656 Err(EditError::InvalidGravity)
2657 );
2658 assert_eq!(world.gravity(), None);
2659 for dt in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -1.0] {
2660 assert_eq!(world.step(dt), Err(EditError::InvalidTimeStep));
2661 assert!(world.body(body).is_some());
2662 assert_eq!(world.state_hash(sample_body_r3), before);
2663 assert_eq!(world.time(), 0.0);
2664 }
2665
2666 world.step(0.0).unwrap();
2667 assert!(world.body(body).is_some());
2668 assert_eq!(world.state_hash(sample_body_r3), before);
2669 assert_eq!(world.time(), 0.0);
2670
2671 world.time = f32::MAX;
2672 assert_eq!(world.step(f32::MAX), Err(EditError::InvalidTimeStep));
2673 assert_eq!(world.state_hash(sample_body_r3), before);
2674 assert_eq!(world.time(), f32::MAX);
2675 }
2676
2677 #[test]
2678 fn a_teleported_sleeping_body_publishes_its_pose_change() {
2679 let (mut world, _, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2680 let id = spheres[0];
2681 assert!(world.sleep_body(id).is_ok());
2682 assert!(world.drain_dirty().count() > 0);
2683
2684 world.step(EDIT_DT).unwrap();
2685 assert!(
2686 !world.drain_dirty().any(|(dirty, _)| dirty == id),
2687 "a sleeping body was published as if it had moved"
2688 );
2689
2690 let orientation = world.bodies[id].orientation;
2691 let elsewhere = Vec3::new(40.0, 9.0, 0.0);
2692 assert!(world.set_pose(id, elsewhere, orientation).is_ok());
2693 assert!(
2694 world.drain_dirty().any(|(dirty, _)| dirty == id),
2695 "the teleport never reached the dirty set"
2696 );
2697 assert_eq!(world.bodies[id].position, elsewhere);
2698 }
2699
2700 #[test]
2701 fn a_restored_world_rebuilds_its_configuration_and_reproduces_the_snapshot() {
2702 let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2703 let key = (floor, spheres[0]);
2704 let state = world.snapshot();
2705
2706 let mut unregistered = World::new(EuclideanR3);
2707 assert_eq!(
2708 unregistered.restore(&state),
2709 Err(EditError::RegistrationMismatch),
2710 "restore accepted a world whose dispatch table cannot serve the snapshot"
2711 );
2712 assert_eq!(unregistered.bodies.len(), 0);
2713
2714 let mut rebuilt = World::new(EuclideanR3);
2715 register_default_narrowphase(&mut rebuilt.narrowphase);
2716 rebuilt.set_gravity(world.gravity()).unwrap();
2717 assert!(rebuilt.restore(&state).is_ok());
2718
2719 assert_eq!(
2720 rebuilt.state_hash(sample_body_r3),
2721 world.state_hash(sample_body_r3)
2722 );
2723 assert_eq!(normal_impulses(&rebuilt, key), normal_impulses(&world, key));
2724
2725 for _ in 0..60 {
2726 world.step(EDIT_DT).unwrap();
2727 rebuilt.step(EDIT_DT).unwrap();
2728 }
2729 assert_eq!(
2730 rebuilt.state_hash(sample_body_r3),
2731 world.state_hash(sample_body_r3),
2732 "the restored world solved with a different dispatch table"
2733 );
2734 }
2735
2736 #[test]
2737 fn a_collider_replacement_drops_the_previous_shapes_inertia() {
2738 let mut world = World::new(EuclideanR3);
2739 register_default_narrowphase(&mut world.narrowphase);
2740 let id = world.push_body(box_body(Vec3::ZERO, Vec3::ZERO, Vec3::ONE, 2.0).unwrap());
2741 let previous = world.bodies[id].inertia;
2742 let replacement = 0.5;
2743 assert_ne!(
2744 previous, replacement,
2745 "the two shapes share an inertia, so the swap cannot be seen"
2746 );
2747
2748 assert!(world
2749 .set_collider(id, Collider::sphere_at_origin(0.25), replacement)
2750 .is_ok());
2751 assert!(matches!(
2752 world.collider(&world.bodies[id]),
2753 Some(Collider::Sphere { .. })
2754 ));
2755 assert!(world
2756 .apply_impulse_at_point(id, Vec3::new(3.0, 0.0, 0.0), Vec3::new(0.0, 2.0, 0.0))
2757 .is_ok());
2758
2759 assert_eq!(world.bodies[id].velocity, Vec3::new(1.5, 0.0, 0.0));
2760 assert_eq!(
2761 world.bodies[id].angular_velocity,
2762 Bivector3::new(-12.0, 0.0, 0.0)
2763 );
2764 }
2765
2766 #[test]
2767 fn a_despawn_leaves_no_contact_or_dirty_row_naming_the_removed_body() {
2768 let (mut world, floor, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2769 let doomed = spheres[1];
2770 assert!(world.manifolds.contains_key(&(floor, doomed)));
2771 assert!(world
2772 .set_velocity(doomed, Vec3::new(0.0, 3.0, 0.0), Bivector3::ZERO)
2773 .is_ok());
2774
2775 assert!(world.despawn_body(doomed).is_ok());
2776
2777 assert!(
2778 !world
2779 .manifolds
2780 .keys()
2781 .any(|&(a, b)| a == doomed || b == doomed),
2782 "a contact outlived the body it names"
2783 );
2784 let reborn = world.push_body(island_sphere(ISLAND_X[1]));
2785 assert_eq!(
2786 reborn.slot(),
2787 doomed.slot(),
2788 "the slot was not recycled, so this test is not exercising aliasing"
2789 );
2790 let published: Vec<BodyId> = world.drain_dirty().map(|(id, _)| id).collect();
2791 assert!(
2792 !published.contains(&doomed),
2793 "a removed row was published for mirroring"
2794 );
2795 assert!(
2796 published.contains(&reborn),
2797 "the row that took the removed slot was never published"
2798 );
2799
2800 world.step(EDIT_DT).unwrap();
2801 assert!(!world
2802 .manifolds
2803 .keys()
2804 .any(|&(a, b)| a == doomed || b == doomed));
2805 }
2806
2807 #[test]
2808 fn bodies_that_share_a_hull_hold_one_copy_of_its_vertices() {
2809 use crate::euclidean_r4::{polytope_body_r4, tesseract_vertices};
2810 use glam::Vec4;
2811 use loam_math::EuclideanR4;
2812
2813 let hull = tesseract_vertices(1.0);
2814 let mut world = World::new(EuclideanR4);
2815 let a =
2816 world.push_body(polytope_body_r4(Vec4::ZERO, Vec4::ZERO, hull.clone(), 1.0).unwrap());
2817 let b = world.push_body(polytope_body_r4(Vec4::X * 8.0, Vec4::ZERO, hull, 1.0).unwrap());
2818
2819 assert_eq!(world.bodies[a].collider(), world.bodies[b].collider());
2820 assert_eq!(world.geometry().prepared(), 1);
2821
2822 let vertices = |id: BodyId| match world.collider(&world.bodies[id]) {
2823 Some(Collider::ConvexPolytope4D { vertices }) => vertices.as_ptr(),
2824 other => panic!("the hull is not prepared: {other:?}"),
2825 };
2826 assert_eq!(
2827 vertices(a),
2828 vertices(b),
2829 "each body kept its own copy of the hull"
2830 );
2831
2832 assert!(world.despawn_body(a).is_ok());
2833 assert_eq!(
2834 world.geometry().prepared(),
2835 1,
2836 "a shared hull was released while another body still used it"
2837 );
2838 assert!(world.despawn_body(b).is_ok());
2839 assert_eq!(
2840 world.geometry().prepared(),
2841 0,
2842 "the last body's hull outlived it"
2843 );
2844 }
2845
2846 #[test]
2847 fn a_restored_sleeping_body_publishes_the_pose_the_restore_gave_it() {
2848 let (mut world, _, spheres) = settled_islands(EDIT_DT, EDIT_SETTLE_STEPS);
2849 let id = spheres[0];
2850 assert!(world.sleep_body(id).is_ok());
2851 let settled = world.bodies[id].position;
2852
2853 let state = world.snapshot();
2854 let orientation = world.bodies[id].orientation;
2855 assert!(world
2856 .set_pose(id, Vec3::new(40.0, 9.0, 0.0), orientation)
2857 .is_ok());
2858 assert!(world.sleep_body(id).is_ok());
2859 let _ = world.drain_dirty().count();
2860
2861 assert!(world.restore(&state).is_ok());
2862 assert_eq!(world.bodies[id].position, settled);
2863 assert!(world.bodies[id].is_sleeping());
2864 assert!(
2865 world.drain_dirty().any(|(dirty, _)| dirty == id),
2866 "a restore moved a sleeping body without publishing it"
2867 );
2868 }
2869
2870 #[test]
2871 fn a_drained_row_is_readable_without_copying_the_ids_out_first() {
2872 let (mut world, _, spheres) = settled_islands(EDIT_DT, 4);
2873 let _ = world.drain_dirty().count();
2874 world.step(EDIT_DT).unwrap();
2875
2876 let mut mirror: BTreeMap<BodyId, Vec3> = BTreeMap::new();
2877 for (id, body) in world.drain_dirty() {
2878 mirror.insert(id, body.position);
2879 }
2880
2881 assert_eq!(mirror.len(), spheres.len());
2882 for id in &spheres {
2883 assert_eq!(mirror.get(id), Some(&world.bodies[*id].position));
2884 }
2885 }
2886
2887 #[test]
2888 fn a_half_space_on_a_body_with_mass_is_not_reported_as_an_unsupported_collider() {
2889 let mut world = World::new(EuclideanR3);
2890 let id = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 0.5, 2.0).unwrap());
2891 let floor = Collider::HalfSpace {
2892 normal: Vec3::Y,
2893 offset: 0.0,
2894 };
2895 assert!(
2896 world.space.supports_collider(floor.kind()),
2897 "the space rejects half-spaces outright, so this cannot tell the two apart"
2898 );
2899
2900 assert_eq!(
2901 world.set_collider(id, floor, 1.0),
2902 Err(EditError::DynamicHalfSpace)
2903 );
2904 assert_eq!(world.bodies[id].mass(), 2.0);
2905
2906 let ground = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
2907 assert_eq!(
2908 world.set_mass_properties(ground, 3.0, 1.0),
2909 Err(EditError::DynamicHalfSpace)
2910 );
2911 assert_eq!(world.bodies[ground].mass(), 0.0);
2912 }
2913
2914 #[test]
2915 fn registration_order_does_not_refuse_a_snapshot_of_the_same_pairs() {
2916 fn never(
2917 _a: &RigidBody<EuclideanR3>,
2918 _b: &RigidBody<EuclideanR3>,
2919 _geometry: &crate::geometry::GeometryStore,
2920 _space: &EuclideanR3,
2921 ) -> Option<crate::response::Contact<EuclideanR3>> {
2922 None
2923 }
2924
2925 use crate::collider::ColliderKind;
2926
2927 let pairs = [
2928 (ColliderKind::Sphere, ColliderKind::HalfSpace),
2929 (ColliderKind::Sphere, ColliderKind::ConvexPolytope3D),
2930 (ColliderKind::ConvexPolytope3D, ColliderKind::HalfSpace),
2931 ];
2932 assert_ne!(
2933 pairs.first(),
2934 pairs.last(),
2935 "the registration sequence is symmetric, so reversing it changes nothing"
2936 );
2937
2938 let mut forward = World::new(EuclideanR3);
2939 for &(a, b) in &pairs {
2940 forward.narrowphase.register(a, b, never);
2941 }
2942 let mut reversed = World::new(EuclideanR3);
2943 for &(a, b) in pairs.iter().rev() {
2944 reversed.narrowphase.register(a, b, never);
2945 }
2946 forward.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 0.5, 1.0).unwrap());
2947
2948 assert!(reversed.restore(&forward.snapshot()).is_ok());
2949 assert!(forward.restore(&reversed.snapshot()).is_ok());
2950 assert_eq!(reversed.bodies.len(), 1);
2951 }
2952
2953 #[test]
2954 fn dirty_publication_allocates_nothing_on_a_warmed_world() {
2955 let (mut world, _, spheres) = settled_islands(EDIT_DT, 4);
2956 for _ in 0..2 {
2957 world.step(EDIT_DT).unwrap();
2958 assert!(world.drain_dirty().count() > 0);
2959 }
2960
2961 let bytes = bytes_allocated_by(|| {
2962 for _ in 0..16 {
2963 for &id in &spheres {
2964 assert!(world.wake_body(id).is_ok());
2965 }
2966 assert_eq!(world.drain_dirty().count(), spheres.len());
2967 }
2968 })
2969 .expect("the counting allocator is installed");
2970 assert_eq!(
2971 bytes, 0,
2972 "16 dirty publications over a steady body set asked the allocator for {bytes} bytes"
2973 );
2974 }
2975
2976 #[test]
2977 fn split_two_mut_returns_borrows_in_argument_order() {
2978 let mut slice = [0u32, 1, 2, 3];
2979 for (i, j) in [(1usize, 3usize), (3, 1)] {
2980 let (a, b) = split_two_mut(&mut slice, i, j);
2981 assert_eq!((*a, *b), (i as u32, j as u32), "split_two_mut({i}, {j})");
2982 }
2983 }
2984
2985 fn stacked_pair_world(doomed_first: bool) -> (World<EuclideanR3>, BodyId, BodyId, BodyId) {
2986 const LOWER_HALF_EXTENT: f32 = 0.5;
2987 const UPPER_HALF_EXTENT: f32 = 0.35;
2988 const DOOMED_X: f32 = -6.0;
2989
2990 let mut world = World::new(EuclideanR3);
2991 register_default_narrowphase(&mut world.narrowphase);
2992 world
2993 .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
2994 .unwrap();
2995
2996 let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
2997 world.bodies[floor].restitution = 0.0;
2998
2999 let spawned_first = doomed_first.then(|| world.push_body(island_sphere(DOOMED_X)));
3000 let lower = world.push_body(
3001 box_body(
3002 Vec3::new(0.0, LOWER_HALF_EXTENT, 0.0),
3003 Vec3::ZERO,
3004 Vec3::splat(LOWER_HALF_EXTENT),
3005 1.0,
3006 )
3007 .unwrap(),
3008 );
3009 let upper = world.push_body(
3010 box_body(
3011 Vec3::new(0.0, 2.0 * LOWER_HALF_EXTENT + UPPER_HALF_EXTENT, 0.0),
3012 Vec3::ZERO,
3013 Vec3::splat(UPPER_HALF_EXTENT),
3014 3.0,
3015 )
3016 .unwrap(),
3017 );
3018 let doomed = spawned_first.unwrap_or_else(|| world.push_body(island_sphere(DOOMED_X)));
3019
3020 for id in [lower, upper, doomed] {
3021 world.bodies[id].restitution = 0.0;
3022 }
3023 (world, lower, upper, doomed)
3024 }
3025
3026 #[test]
3027 fn gravity_does_not_wake_a_sleeping_support_until_its_neighbor_is_driven() {
3028 let (mut world, lower, upper, distant) = stacked_pair_world(false);
3029 world.despawn_body(distant).unwrap();
3030 let dt = 1.0 / 240.0;
3031 for _ in 0..600 {
3032 world.step(dt).unwrap();
3033 }
3034 world.sleep_body(lower).unwrap();
3035 for _ in 0..120 {
3036 world.step(dt).unwrap();
3037 assert!(world.bodies[lower].is_sleeping());
3038 }
3039 assert!(world.manifolds.contains_key(&canonical_pair(lower, upper)));
3040 world
3041 .set_velocity(upper, -Vec3::Y, Bivector3::ZERO)
3042 .unwrap();
3043 world.step(dt).unwrap();
3044 assert!(!world.bodies[lower].is_sleeping());
3045 }
3046
3047 fn despawned_pair_world(
3048 doomed_first: bool,
3049 dt: f32,
3050 ) -> (World<EuclideanR3>, BodyId, BodyId, PairKey) {
3051 const SETTLE_STEPS: usize = 40;
3052
3053 let (mut world, lower, upper, doomed) = stacked_pair_world(doomed_first);
3054 for _ in 0..SETTLE_STEPS {
3055 world.step(dt).unwrap();
3056 }
3057
3058 let key = canonical_pair(lower, upper);
3059 assert!(
3060 world.manifolds.contains_key(&key),
3061 "the two dynamic bodies never settled into contact"
3062 );
3063 assert!(world.despawn_body(doomed).is_ok());
3064
3065 let (i, j) = world.dense_pair(key);
3066 assert_eq!(
3067 i > j,
3068 doomed_first,
3069 "the pair is stored at {i}, {j}, which is not the order this fixture \
3070 was built to produce"
3071 );
3072 (world, lower, upper, key)
3073 }
3074
3075 #[test]
3076 fn contact_normal_points_from_the_pair_key_low_body_to_the_high_one() {
3077 let dt = 1.0 / 240.0;
3078 for doomed_first in [false, true] {
3079 let (mut world, _, _, key) = despawned_pair_world(doomed_first, dt);
3080 world.step(dt).unwrap();
3081
3082 let manifold = world.manifolds.get(&key).expect("the pair separated");
3083 let key_axis = world.bodies[key.1].position - world.bodies[key.0].position;
3084 assert!(!manifold.points.is_empty(), "manifold carries no contact");
3085 for cp in &manifold.points {
3086 assert!(
3087 cp.normal.dot(key_axis) > 0.0,
3088 "normal {} points back toward the key's low body",
3089 cp.normal
3090 );
3091 }
3092 }
3093 }
3094
3095 #[test]
3096 fn storage_order_does_not_reach_a_contacting_pairs_trajectory() {
3097 let dt = 1.0 / 240.0;
3098 let steps = 120;
3099 let mut trajectories = Vec::new();
3100 for doomed_first in [false, true] {
3101 let (mut world, lower, upper, key) = despawned_pair_world(doomed_first, dt);
3102 let mut contact_steps = 0;
3103 let mut trajectory = Vec::with_capacity(steps);
3104 for _ in 0..steps {
3105 world.step(dt).unwrap();
3106 if world.manifolds.contains_key(&key) {
3107 contact_steps += 1;
3108 }
3109 trajectory.push((body_state(&world, lower), body_state(&world, upper)));
3110 }
3111 assert_eq!(
3112 contact_steps, steps,
3113 "the pair held contact for only {contact_steps} of {steps} steps"
3114 );
3115 trajectories.push(trajectory);
3116 }
3117
3118 let step = trajectories[0]
3119 .iter()
3120 .zip(&trajectories[1])
3121 .position(|(a, b)| a != b);
3122 assert!(
3123 step.is_none(),
3124 "storage order reached the solve: the pair diverged from the control \
3125 at step {step:?}"
3126 );
3127 }
3128
3129 struct Xorshift(u64);
3131
3132 impl Xorshift {
3133 fn new(seed: u64) -> Self {
3134 Self(seed | 1)
3135 }
3136
3137 fn next_u64(&mut self) -> u64 {
3138 self.0 ^= self.0 << 13;
3139 self.0 ^= self.0 >> 7;
3140 self.0 ^= self.0 << 17;
3141 self.0
3142 }
3143
3144 fn range(&mut self, lo: f32, hi: f32) -> f32 {
3145 let unit = (self.next_u64() >> 40) as f32 / (1u32 << 24) as f32;
3146 lo + (hi - lo) * unit
3147 }
3148 }
3149
3150 fn random_scene(seed: u64, count: usize, spread: f32) -> World<EuclideanR3> {
3151 let mut rng = Xorshift::new(seed);
3152 let mut world = World::new(EuclideanR3);
3153 register_default_narrowphase(&mut world.narrowphase);
3154 world
3155 .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
3156 .unwrap();
3157 world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
3158
3159 let mut spawned = Vec::with_capacity(count);
3160 for _ in 0..count {
3161 let position = Vec3::new(
3162 rng.range(-spread, spread),
3163 rng.range(0.5, spread + 0.5),
3164 rng.range(-spread, spread),
3165 );
3166 let id = if rng.next_u64() & 1 == 0 {
3167 world.push_body(
3168 sphere_body_r3(position, Vec3::ZERO, rng.range(0.2, 0.8), 1.0).unwrap(),
3169 )
3170 } else {
3171 world.push_body(
3172 box_body(position, Vec3::ZERO, Vec3::splat(rng.range(0.2, 0.6)), 1.0).unwrap(),
3173 )
3174 };
3175 if rng.next_u64().is_multiple_of(8) {
3176 assert!(world.bodies[id].set_mass(0.0));
3177 }
3178 world.bodies[id].restitution = 0.0;
3179 spawned.push(id);
3180 }
3181 for doomed in spawned.iter().step_by(5) {
3182 assert!(world.despawn_body(*doomed).is_ok());
3183 }
3184 world
3185 }
3186
3187 fn all_pairs_reference(world: &World<EuclideanR3>) -> Vec<PairKey> {
3188 let mut pairs = Vec::new();
3189 let n = world.bodies.len();
3190 for i in 0..n {
3191 for j in (i + 1)..n {
3192 let (a, b) = (&world.bodies[i], &world.bodies[j]);
3193 if a.inv_mass() == 0.0 && b.inv_mass() == 0.0 {
3194 continue;
3195 }
3196 let reach = bounding_radius(world.collider(a)) + bounding_radius(world.collider(b));
3197 if world.space.distance(a.position, b.position) <= reach {
3198 pairs.push(canonical_pair(world.bodies.id_at(i), world.bodies.id_at(j)));
3199 }
3200 }
3201 }
3202 pairs.sort_unstable();
3203 pairs
3204 }
3205
3206 fn dynamic_body_count(world: &World<EuclideanR3>) -> usize {
3207 world.bodies.iter().filter(|b| b.inv_mass() != 0.0).count()
3208 }
3209
3210 const RANDOM_SCENE_SHAPES: [(usize, f32); 3] = [(12, 2.0), (40, 6.0), (80, 3.0)];
3211
3212 #[test]
3213 fn sweep_broadphase_emits_exactly_the_all_pairs_candidate_set() {
3214 let mut ever_beyond_the_floor = false;
3215 for seed in PERMUTATION_SEEDS {
3216 for (count, spread) in RANDOM_SCENE_SHAPES {
3217 let mut world = random_scene(seed, count, spread);
3218 let mut emitted = Vec::new();
3219 for step in 0..8 {
3220 let expected = all_pairs_reference(&world);
3221 world.broadphase_into(&mut emitted);
3222 assert_eq!(
3223 emitted, expected,
3224 "seed {seed}, {count} bodies, spread {spread}, step {step}"
3225 );
3226 ever_beyond_the_floor |= expected.len() > dynamic_body_count(&world);
3227 world.step(1.0 / 240.0).unwrap();
3228 }
3229 }
3230 }
3231 assert!(
3232 ever_beyond_the_floor,
3233 "no scene ever produced a candidate pair between two finite colliders"
3234 );
3235 }
3236
3237 #[test]
3238 fn broadphase_culls_only_pairs_the_narrowphase_would_reject() {
3239 for seed in PERMUTATION_SEEDS {
3240 let mut world = random_scene(seed, 40, 3.0);
3241 let mut emitted = Vec::new();
3242 for step in 0..8 {
3243 world.broadphase_into(&mut emitted);
3244 let n = world.bodies.len();
3245 let mut culled = 0usize;
3246 for i in 0..n {
3247 for j in (i + 1)..n {
3248 if world.bodies[i].inv_mass() == 0.0 && world.bodies[j].inv_mass() == 0.0 {
3249 continue;
3250 }
3251 let key = canonical_pair(world.bodies.id_at(i), world.bodies.id_at(j));
3252 if emitted.binary_search(&key).is_ok() {
3253 continue;
3254 }
3255 culled += 1;
3256 let contact = world.narrowphase.test(
3257 &world.bodies[i],
3258 &world.bodies[j],
3259 world.geometry(),
3260 &world.space,
3261 );
3262 assert!(
3263 contact.is_none(),
3264 "seed {seed} step {step}: the sweep culled {key:?}, which the \
3265 narrowphase reports in contact"
3266 );
3267 }
3268 }
3269 assert!(
3270 culled > 0,
3271 "seed {seed} step {step}: nothing was culled, so this pass proved nothing"
3272 );
3273 world.step(1.0 / 240.0).unwrap();
3274 }
3275 }
3276 }
3277
3278 #[test]
3279 fn broadphase_emits_strictly_ascending_keys_under_disagreeing_storage_order() {
3280 for seed in PERMUTATION_SEEDS {
3281 let mut world = random_scene(seed, 40, 3.0);
3282 let disagrees = (1..world.bodies.len())
3283 .any(|dense| world.bodies.id_at(dense) < world.bodies.id_at(dense - 1));
3284 assert!(
3285 disagrees,
3286 "seed {seed}: storage order still agrees with handle order, so this \
3287 scene cannot tell the two apart"
3288 );
3289
3290 let mut pairs = Vec::new();
3291 world.broadphase_into(&mut pairs);
3292 assert!(pairs.len() > 1, "seed {seed}: too few pairs to be ordered");
3293 assert!(
3294 pairs.windows(2).all(|w| w[0] < w[1]),
3295 "seed {seed}: emission order is not strictly ascending in BodyId"
3296 );
3297 }
3298 }
3299
3300 #[test]
3301 fn broadphase_prunes_the_quadratic_pair_set_at_scale() {
3302 let mut world = random_scene(PERMUTATION_SEEDS[1], 200, 20.0);
3303 let n = world.bodies.len();
3304 assert!(
3305 n >= 100,
3306 "the scale case needs at least 100 bodies, got {n}"
3307 );
3308 let all_pairs = n * (n - 1) / 2;
3309 let mut pairs = Vec::new();
3310 world.broadphase_into(&mut pairs);
3311 let emitted = pairs.len();
3312 assert!(
3313 emitted * 10 < all_pairs,
3314 "the sweep emitted {emitted} of {all_pairs} pairs, which is no better than \
3315 a constant-factor cull"
3316 );
3317 }
3318
3319 #[test]
3320 fn broadphase_fill_allocates_nothing_after_the_first_pass() {
3321 let world = random_scene(PERMUTATION_SEEDS[0], 120, 8.0);
3322 let mut intervals = Vec::new();
3323 let mut active = Vec::new();
3324 let mut pairs = Vec::new();
3325
3326 for _ in 0..2 {
3327 World::fill_broadphase(
3328 &world.bodies,
3329 world.geometry(),
3330 &world.space,
3331 &mut intervals,
3332 &mut active,
3333 &mut pairs,
3334 &mut StepCounters::default(),
3335 );
3336 }
3337 assert!(!pairs.is_empty(), "the fixture produced no pairs to emit");
3338
3339 let bytes = bytes_allocated_by(|| {
3340 for _ in 0..16 {
3341 World::fill_broadphase(
3342 &world.bodies,
3343 world.geometry(),
3344 &world.space,
3345 &mut intervals,
3346 &mut active,
3347 &mut pairs,
3348 &mut StepCounters::default(),
3349 );
3350 }
3351 })
3352 .expect("the counting allocator is installed");
3353 assert_eq!(
3354 bytes, 0,
3355 "16 sweeps over a steady body set asked the allocator for {bytes} bytes"
3356 );
3357 }
3358
3359 #[test]
3360 fn step_counters_report_the_sweep_work_of_a_four_body_line() {
3361 let mut world = World::new(EuclideanR3);
3362 register_default_narrowphase(&mut world.narrowphase);
3363 for x in [0.0, 0.75, 1.75, 5.0] {
3364 world.push_body(sphere_body_r3(Vec3::new(x, 0.0, 0.0), Vec3::ZERO, 0.5, 1.0).unwrap());
3365 }
3366 world.step(1.0 / 240.0).unwrap();
3367 assert_eq!(
3368 world.counters(),
3369 StepCounters {
3370 index_visits: 7,
3371 distance_evals: 6,
3372 candidates: 2,
3373 contacts: 1,
3374 }
3375 );
3376 }
3377
3378 #[test]
3379 fn exactly_tangent_spheres_are_a_candidate_pair() {
3380 const RADIUS: f32 = 0.5;
3381 let mut world = World::new(EuclideanR3);
3382 let anchor = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, RADIUS, 1.0).unwrap());
3383 let tangent = world.push_body(sphere_body_r3(Vec3::X, Vec3::ZERO, RADIUS, 1.0).unwrap());
3384 let separated = world.push_body(
3385 sphere_body_r3(
3386 Vec3::new(-(1.0 + f32::EPSILON), 0.0, 0.0),
3387 Vec3::ZERO,
3388 RADIUS,
3389 1.0,
3390 )
3391 .unwrap(),
3392 );
3393
3394 let position = |id: BodyId| {
3395 let dense = world.bodies.dense_index(id).expect("nothing despawned");
3396 world.bodies[dense].position
3397 };
3398 assert_eq!(
3399 world.space.distance(position(anchor), position(tangent)),
3400 RADIUS + RADIUS,
3401 "the fixture is not exactly tangent, so it cannot state the boundary"
3402 );
3403 assert!(
3404 world.space.distance(position(anchor), position(separated)) > RADIUS + RADIUS,
3405 "the separated sphere is not past the boundary"
3406 );
3407
3408 let mut pairs = Vec::new();
3409 world.broadphase_into(&mut pairs);
3410 assert_eq!(pairs, vec![canonical_pair(anchor, tangent)]);
3411 }
3412
3413 #[test]
3414 fn coincident_point_colliders_are_a_candidate_pair() {
3415 let mut world = World::new(EuclideanR3);
3416 let a = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 0.0, 1.0).unwrap());
3417 let b = world.push_body(sphere_body_r3(Vec3::ZERO, Vec3::ZERO, 0.0, 1.0).unwrap());
3418 let mut pairs = Vec::new();
3419 world.broadphase_into(&mut pairs);
3420 assert_eq!(pairs, vec![canonical_pair(a, b)]);
3421 }
3422
3423 #[test]
3424 fn bounding_radius_contains_every_posed_vertex_of_its_collider() {
3425 let half_extents = Vec3::new(0.5, 1.25, 0.25);
3426 let vertices = crate::euclidean_r3::box_vertices(half_extents);
3427 let radius = bounding_radius(Some(&Collider::ConvexPolytope3D {
3428 vertices: vertices.clone(),
3429 }));
3430 assert_eq!(radius, half_extents.length());
3431
3432 let rotation = glam::Quat::from_axis_angle(Vec3::new(1.0, 2.0, 3.0).normalize(), 0.7);
3433 for v in &vertices {
3434 let posed = rotation * *v;
3435 assert!(
3436 posed.length() <= radius + 1e-6,
3437 "posed vertex {posed} escaped the bounding radius {radius}"
3438 );
3439 }
3440
3441 assert_eq!(
3442 bounding_radius(Some(&Collider::sphere_at_origin(0.75))),
3443 0.75
3444 );
3445 assert_eq!(
3446 bounding_radius(Some(&Collider::HalfSpace {
3447 normal: Vec3::Y,
3448 offset: 0.0
3449 })),
3450 f32::INFINITY,
3451 "a half-space is unbounded and must never be culled"
3452 );
3453 }
3454
3455 const ISLAND_SETTLE_STEPS: usize = 400;
3456 const ISLAND_COLUMN_PITCH: f32 = 4.0;
3457 const ISLAND_COLUMNS: usize = 6;
3458 const ISLAND_COLUMN_HEIGHT: usize = 3;
3459 const ISLAND_PINNED_COLUMN: usize = 2;
3460
3461 fn settled_columns(seed: u64) -> World<EuclideanR3> {
3462 const GAP: f32 = 0.05;
3463 const PINNED_TOUCH: f32 = 0.01;
3464
3465 let mut rng = Xorshift::new(seed);
3466 let mut world = World::new(EuclideanR3);
3467 register_default_narrowphase(&mut world.narrowphase);
3468 world
3469 .set_gravity(Some(Vec3::new(0.0, GRAVITY_Y, 0.0)))
3470 .unwrap();
3471 world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
3472
3473 let mut columns: Vec<Vec<BodyId>> = Vec::with_capacity(ISLAND_COLUMNS);
3474 for column in 0..ISLAND_COLUMNS {
3475 let x = column as f32 * ISLAND_COLUMN_PITCH + rng.range(-0.5, 0.5);
3476 let radius = rng.range(0.3, 0.6);
3477 let mut ids = Vec::with_capacity(ISLAND_COLUMN_HEIGHT);
3478 for level in 0..ISLAND_COLUMN_HEIGHT {
3479 let pinned = column == ISLAND_PINNED_COLUMN && level == 1;
3480 let y = if pinned {
3481 3.0 * radius - PINNED_TOUCH
3482 } else {
3483 radius + GAP + level as f32 * (2.0 * radius + GAP)
3484 };
3485 let id = world.push_body(
3486 sphere_body_r3(Vec3::new(x, y, 0.0), Vec3::ZERO, radius, 1.0).unwrap(),
3487 );
3488 world.bodies[id].restitution = 0.0;
3489 if pinned {
3490 assert!(world.bodies[id].set_mass(0.0));
3491 }
3492 ids.push(id);
3493 }
3494 columns.push(ids);
3495 }
3496
3497 for column in columns.iter().take(2) {
3498 assert!(world.despawn_body(column[0]).is_ok());
3499 }
3500 for _ in 0..ISLAND_SETTLE_STEPS {
3501 world.step(1.0 / 240.0).unwrap();
3502 }
3503 world
3504 }
3505
3506 #[test]
3507 fn island_ids_are_the_lowest_body_id_not_the_lowest_storage_position() {
3508 let mut orders_disagreed = 0usize;
3509 for seed in PERMUTATION_SEEDS {
3510 let mut world = settled_columns(seed);
3511 for step in 0..8 {
3512 world.step(1.0 / 240.0).unwrap();
3513 let islands = world.islands();
3514 for island in &islands {
3515 let lowest_handle = island.bodies.iter().copied().min();
3516 assert_eq!(
3517 Some(island.id),
3518 lowest_handle,
3519 "seed {seed} step {step}: island {:?} is not named by its \
3520 lowest handle",
3521 island.id
3522 );
3523 let lowest_stored =
3524 island.bodies.iter().copied().min_by_key(|&id| {
3525 world.bodies.dense_index(id).expect(STALE_MANIFOLD_BODY)
3526 });
3527 if lowest_stored != lowest_handle {
3528 orders_disagreed += 1;
3529 }
3530 }
3531 assert!(
3532 islands.windows(2).all(|w| w[0].id < w[1].id),
3533 "seed {seed} step {step}: islands are not strictly ascending in id"
3534 );
3535 }
3536 }
3537 assert!(
3538 orders_disagreed > 0,
3539 "no island ever held a body whose handle order disagreed with its \
3540 storage order, so the two labelling rules were never told apart"
3541 );
3542 }
3543
3544 #[test]
3545 fn a_static_body_joins_no_island_and_merges_none() {
3546 for seed in PERMUTATION_SEEDS {
3547 let world = settled_columns(seed);
3548 let pinned = world
3549 .bodies
3550 .iter()
3551 .position(|body| {
3552 body.inv_mass() == 0.0
3553 && matches!(world.collider(body), Some(Collider::Sphere { .. }))
3554 })
3555 .map(|dense| world.bodies.id_at(dense))
3556 .expect("the fixture lost its static sphere");
3557
3558 let neighbours: Vec<BodyId> = world
3559 .manifolds
3560 .keys()
3561 .filter_map(|&(a, b)| match (a == pinned, b == pinned) {
3562 (true, false) => Some(b),
3563 (false, true) => Some(a),
3564 _ => None,
3565 })
3566 .collect();
3567 assert_eq!(
3568 neighbours.len(),
3569 2,
3570 "seed {seed}: the static sphere touches {} bodies, so it is \
3571 not wedged between two",
3572 neighbours.len()
3573 );
3574
3575 let islands = world.islands();
3576 let island_of = |id: BodyId| {
3577 islands
3578 .iter()
3579 .find(|island| island.bodies.contains(&id))
3580 .map(|island| island.id)
3581 };
3582 assert!(
3583 island_of(pinned).is_none(),
3584 "seed {seed}: a static body joined an island"
3585 );
3586 assert_ne!(
3587 island_of(neighbours[0]),
3588 island_of(neighbours[1]),
3589 "seed {seed}: two bodies that meet only through a static one \
3590 were merged into one island"
3591 );
3592 }
3593 }
3594
3595 fn flood_fill_islands(world: &World<EuclideanR3>) -> Vec<Island> {
3596 let dynamic = |id: BodyId| world.bodies[id].inv_mass() != 0.0;
3597 let mut adjacency: BTreeMap<BodyId, Vec<BodyId>> = BTreeMap::new();
3598 for (&(a, b), _) in world.manifolds() {
3599 for id in [a, b].into_iter().filter(|&id| dynamic(id)) {
3600 adjacency.entry(id).or_default();
3601 }
3602 if dynamic(a) && dynamic(b) {
3603 adjacency.entry(a).or_default().push(b);
3604 adjacency.entry(b).or_default().push(a);
3605 }
3606 }
3607
3608 let mut seen: BTreeSet<BodyId> = BTreeSet::new();
3609 let mut islands = Vec::new();
3610 for &seed in adjacency.keys() {
3611 if !seen.insert(seed) {
3612 continue;
3613 }
3614 let mut bodies = vec![seed];
3615 let mut frontier = vec![seed];
3616 while let Some(body) = frontier.pop() {
3617 for &next in &adjacency[&body] {
3618 if seen.insert(next) {
3619 bodies.push(next);
3620 frontier.push(next);
3621 }
3622 }
3623 }
3624 bodies.sort_unstable();
3625 let constraints = world
3626 .manifolds()
3627 .map(|(&key, _)| key)
3628 .filter(|&(a, b)| {
3629 bodies.binary_search(&a).is_ok() || bodies.binary_search(&b).is_ok()
3630 })
3631 .collect();
3632 islands.push(Island {
3633 id: bodies[0],
3634 bodies,
3635 constraints,
3636 });
3637 }
3638 islands.sort_unstable_by_key(|island| island.id);
3639 islands
3640 }
3641
3642 #[test]
3643 fn islands_match_a_flood_fill_of_the_contact_graph() {
3644 let mut ever_multi_body = false;
3645 for seed in PERMUTATION_SEEDS {
3646 let mut world = settled_columns(seed);
3647 for step in 0..8 {
3648 world.step(1.0 / 240.0).unwrap();
3649 let islands = world.islands();
3650 ever_multi_body |= islands.iter().any(|island| island.bodies.len() > 1);
3651 assert_eq!(
3652 islands,
3653 flood_fill_islands(&world),
3654 "seed {seed} step {step}: union-find disagreed with the flood fill"
3655 );
3656 }
3657 }
3658 assert!(
3659 ever_multi_body,
3660 "no island ever held two bodies, so the comparison never covered a union"
3661 );
3662 }
3663
3664 #[test]
3665 fn a_single_island_solves_in_the_global_ascending_key_order() {
3666 let world = settled_sphere_stack(1.0 / 240.0, 200);
3667 let islands = world.islands();
3668 assert_eq!(
3669 islands.len(),
3670 1,
3671 "the stack is not one island, so this fixture cannot state the \
3672 single-island case"
3673 );
3674
3675 let ascending: Vec<PairKey> = world.manifolds().map(|(&key, _)| key).collect();
3676 assert!(ascending.len() > 1, "too few constraints to be ordered");
3677 assert_eq!(
3678 constraint_order(&world),
3679 ascending,
3680 "grouping moved a constraint in a world with a single island"
3681 );
3682 assert_eq!(islands[0].constraints, ascending);
3683 assert_eq!(
3684 islands[0].bodies.len(),
3685 3,
3686 "the island should hold the three spheres and not the static floor"
3687 );
3688 }
3689
3690 #[test]
3691 fn constraint_buffer_runs_island_by_island() {
3692 let mut world = multi_island_world();
3693 for _ in 0..MULTI_ISLAND_STEPS {
3694 world.step(MULTI_ISLAND_DT).unwrap();
3695 }
3696
3697 let islands = world.islands();
3698 assert_eq!(
3699 islands.len(),
3700 3,
3701 "the groups share only the static floor, so they are three islands"
3702 );
3703 let grouped: Vec<PairKey> = islands
3704 .iter()
3705 .flat_map(|island| island.constraints.iter().copied())
3706 .collect();
3707 assert_eq!(
3708 constraint_order(&world),
3709 grouped,
3710 "the solved buffer is not the islands in order"
3711 );
3712
3713 let ascending: Vec<PairKey> = world.manifolds().map(|(&key, _)| key).collect();
3714 assert_ne!(
3715 grouped, ascending,
3716 "the fixture's islands happen to be contiguous in ascending key \
3717 order, so it cannot show that grouping reorders anything"
3718 );
3719 }
3720
3721 #[test]
3722 fn island_scratch_allocates_nothing_after_the_first_solve() {
3723 const DT: f32 = 1.0 / 240.0;
3724 let (mut world, _, _) = settled_islands(DT, 120);
3725 world.step(DT).unwrap();
3726 assert!(!world.constraints.is_empty(), "the fixture solves nothing");
3727 world.solve(DT);
3728
3729 let bytes = bytes_allocated_by(|| {
3730 for _ in 0..16 {
3731 world.solve(DT);
3732 }
3733 })
3734 .expect("the counting allocator is installed");
3735 assert_eq!(
3736 bytes, 0,
3737 "16 solves over a steady island set asked the allocator for {bytes} bytes"
3738 );
3739 }
3740
3741 #[test]
3742 fn island_grouping_allocates_nothing_after_the_first_pass() {
3743 let mut world = settled_columns(PERMUTATION_SEEDS[0]);
3744 for _ in 0..2 {
3745 world.collect_constraints();
3746 }
3747 assert!(world.constraints.len() > 1);
3748
3749 let bytes = bytes_allocated_by(|| {
3750 for _ in 0..16 {
3751 world.collect_constraints();
3752 }
3753 })
3754 .expect("the counting allocator is installed");
3755 assert_eq!(
3756 bytes, 0,
3757 "16 island passes over a steady contact set asked the allocator for \
3758 {bytes} bytes"
3759 );
3760 }
3761
3762 #[test]
3763 fn bounded_contact_churn_allocates_nothing_after_warmup() {
3764 let mut world = World::new(EuclideanR3);
3765 register_default_narrowphase(&mut world.narrowphase);
3766 let floor = world.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
3767 let mut active =
3768 world.push_body(sphere_body_r3(Vec3::Y * 0.49, Vec3::ZERO, 0.5, 1.0).unwrap());
3769 let mut inactive =
3770 world.push_body(sphere_body_r3(Vec3::Y * 4.0, Vec3::ZERO, 0.5, 1.0).unwrap());
3771 world.update_manifolds();
3772 for _ in 0..4 {
3773 world.despawn_body(inactive).unwrap();
3774 let next =
3775 world.push_body(sphere_body_r3(Vec3::Y * 4.0, Vec3::ZERO, 0.5, 1.0).unwrap());
3776 let active_orientation = world.bodies[active].orientation;
3777 let next_orientation = world.bodies[next].orientation;
3778 world
3779 .set_pose(active, Vec3::Y * 4.0, active_orientation)
3780 .unwrap();
3781 world
3782 .set_pose(next, Vec3::Y * 0.49, next_orientation)
3783 .unwrap();
3784 world.update_manifolds();
3785 inactive = active;
3786 active = next;
3787 }
3788 assert!(world.manifolds.contains_key(&canonical_pair(floor, active)));
3789
3790 let state = world.snapshot();
3791 world.restore(&state).unwrap();
3792 let retained = world.manifold_pool.len();
3793 for _ in 0..4 {
3794 world.restore(&state).unwrap();
3795 assert_eq!(world.manifold_pool.len(), retained);
3796 }
3797 let first_measured_generation = active.generation();
3798
3799 let mut bytes = 0;
3800 for _ in 0..16 {
3801 world.despawn_body(inactive).unwrap();
3802 let next =
3803 world.push_body(sphere_body_r3(Vec3::Y * 4.0, Vec3::ZERO, 0.5, 1.0).unwrap());
3804 let active_orientation = world.bodies[active].orientation;
3805 let next_orientation = world.bodies[next].orientation;
3806 world
3807 .set_pose(active, Vec3::Y * 4.0, active_orientation)
3808 .unwrap();
3809 world
3810 .set_pose(next, Vec3::Y * 0.49, next_orientation)
3811 .unwrap();
3812 bytes += bytes_allocated_by(|| world.update_manifolds())
3813 .expect("the counting allocator is installed");
3814 inactive = active;
3815 active = next;
3816 }
3817 assert!(active.generation() > first_measured_generation);
3818 assert!(world.manifolds.contains_key(&canonical_pair(floor, active)));
3819 assert_eq!(
3820 bytes, 0,
3821 "16 new-generation recontacts asked the allocator for {bytes} bytes"
3822 );
3823 }
3824
3825 #[test]
3826 fn posed_halfspaces_move_sphere_and_hull_contacts_in_r3_and_r4() {
3827 let mut r3 = World::new(EuclideanR3);
3828 register_default_narrowphase(&mut r3.narrowphase);
3829 let wall3 = r3.push_body(halfspace_body_r3(Vec3::Y, 0.0).unwrap());
3830 let sphere3 =
3831 r3.push_body(sphere_body_r3(Vec3::new(2.25, 3.0, -2.0), Vec3::ZERO, 0.5, 1.0).unwrap());
3832 let hull3 = r3.push_body(
3833 box_body(Vec3::new(2.1, 3.0, 2.0), Vec3::ZERO, Vec3::splat(0.2), 1.0).unwrap(),
3834 );
3835 let pose3 = loam_math::Iso3 {
3836 rotation: glam::Quat::from_rotation_arc(Vec3::Y, Vec3::X),
3837 translation: Vec3::ZERO,
3838 };
3839 r3.set_pose(wall3, Vec3::X * 2.0, pose3).unwrap();
3840 r3.update_manifolds();
3841 for body in [sphere3, hull3] {
3842 let manifold = r3.manifold(canonical_pair(wall3, body)).unwrap();
3843 assert!(manifold.points.iter().any(|point| point.normal.x > 0.99));
3844 }
3845
3846 let mut r4 = World::new(loam_math::EuclideanR4);
3847 crate::euclidean_r4::register_default_narrowphase(&mut r4.narrowphase);
3848 let wall4 =
3849 r4.push_body(crate::euclidean_r4::halfspace4_body_r4(glam::Vec4::Y, 0.0).unwrap());
3850 let sphere4 = r4.push_body(
3851 crate::euclidean_r4::sphere_body_r4(
3852 glam::Vec4::new(2.25, 3.0, -2.0, 0.0),
3853 glam::Vec4::ZERO,
3854 0.5,
3855 1.0,
3856 )
3857 .unwrap(),
3858 );
3859 let hull4 = r4.push_body(
3860 crate::euclidean_r4::polytope_body_r4(
3861 glam::Vec4::new(2.1, 3.0, 2.0, 0.0),
3862 glam::Vec4::ZERO,
3863 crate::euclidean_r4::tesseract_vertices(0.2),
3864 1.0,
3865 )
3866 .unwrap(),
3867 );
3868 let pose4 = loam_math::Iso4Flat {
3869 rotation: loam_math::Rotor4::from_rotation_arc(glam::Vec4::Y, glam::Vec4::X),
3870 translation: glam::Vec4::ZERO,
3871 };
3872 r4.set_pose(wall4, glam::Vec4::X * 2.0, pose4).unwrap();
3873 r4.update_manifolds();
3874 for body in [sphere4, hull4] {
3875 let manifold = r4.manifold(canonical_pair(wall4, body)).unwrap();
3876 assert!(manifold.points.iter().any(|point| point.normal.x > 0.99));
3877 }
3878 }
3879
3880 #[test]
3881 fn r4_wall_correction_pushes_toward_the_near_face() {
3882 use crate::euclidean_r4::{sphere_body_r4, tesseract_vertices};
3883 use glam::Vec4;
3884 use loam_math::EuclideanR4;
3885 for x in [-0.092, -0.09, -0.088] {
3886 let mut world = World::new(EuclideanR4);
3887 crate::euclidean_r4::register_default_narrowphase(&mut world.narrowphase);
3888 world.push_body(
3889 BodyDef::fixed(
3890 Vec4::ZERO,
3891 Collider::ConvexPolytope4D {
3892 vertices: tesseract_vertices(2.0)
3893 .into_iter()
3894 .map(|v| v * Vec4::new(0.05, 2.0, 2.0, 2.0))
3895 .collect(),
3896 },
3897 1.0,
3898 &EuclideanR4,
3899 )
3900 .unwrap(),
3901 );
3902 let ball = world.push_body(
3903 sphere_body_r4(Vec4::new(x, 0.0, 0.0, 0.0), Vec4::ZERO, 0.1, 1.0).unwrap(),
3904 );
3905 world.step(1.0 / 240.0).unwrap();
3906 assert!(world.bodies[ball].position.x < x, "x={x}");
3907 }
3908 }
3909}