Skip to main content

rapier2d/pipeline/physics_pipeline/
quarantine.rs

1//! Containment of non-finite (NaN or infinite) simulation state.
2//!
3//! Non-finite state is detected at two chokepoints (user modifications at step start, the
4//! position advance at step end) before it can corrupt the broad-phase or spread to other
5//! bodies; the affected body or collider is disabled, rolled back to its last valid pose when
6//! one is known, and reported through [`Quarantine`].
7
8use crate::alloc_prelude::*;
9use crate::dynamics::{RigidBody, RigidBodyHandle, RigidBodySet, RigidBodyVelocity};
10use crate::geometry::{ColliderHandle, ColliderSet};
11use crate::math::{Pose, Vector};
12use crate::pipeline::PhysicsPipeline;
13
14/// Non-finite (NaN or infinite) state detected and neutralized during the last step.
15///
16/// Reported bodies got rolled back to their last valid pose (when known), their velocities and
17/// user forces zeroed, and disabled; they can be re-enabled with `set_enabled(true)`. Reports
18/// are cleared each step.
19#[derive(Default)]
20pub struct Quarantine {
21    /// Bodies disabled because their pose or velocity went non-finite.
22    bodies: Vec<RigidBodyHandle>,
23    /// Colliders disabled because their own geometry went non-finite.
24    colliders: Vec<ColliderHandle>,
25    /// Bodies flagged by the end-of-step advance with their last valid pose;
26    /// consumed by `apply_end_step`.
27    pub(super) body_scratch: Vec<(RigidBodyHandle, Pose)>,
28    /// Colliders flagged by the end-of-step advance with a non-finite AABB despite a finite
29    /// body pose; consumed by `apply_end_step`.
30    pub(super) collider_scratch: Vec<ColliderHandle>,
31}
32
33impl Quarantine {
34    /// The rigid-bodies quarantined during the last step.
35    pub fn bodies(&self) -> &[RigidBodyHandle] {
36        &self.bodies
37    }
38
39    /// The colliders quarantined during the last step.
40    pub fn colliders(&self) -> &[ColliderHandle] {
41        &self.colliders
42    }
43
44    /// Was nothing quarantined during the last step?
45    pub fn is_empty(&self) -> bool {
46        self.bodies.is_empty() && self.colliders.is_empty()
47    }
48
49    /// Clears the reports at the beginning of a step.
50    pub(super) fn clear(&mut self) {
51        self.bodies.clear();
52        self.colliders.clear();
53    }
54
55    /// Neutralizes every non-finite value found in `rb`'s velocities and user forces.
56    fn sanitize_body_dynamics(rb: &mut RigidBody) {
57        rb.vels = RigidBodyVelocity::zero();
58        rb.ccd_vels = RigidBodyVelocity::zero();
59        rb.forces.force = Vector::ZERO;
60        rb.forces.torque = Default::default();
61        rb.forces.user_force = Vector::ZERO;
62        rb.forces.user_torque = Default::default();
63    }
64
65    /// Step-start chokepoint: quarantines user-modified bodies and colliders whose new state is
66    /// non-finite, before it reaches the broad-phase. Quarantined objects are already in the
67    /// modified lists, so their disable is processed by the same step's user-changes handling.
68    pub(super) fn detect_user_changes(
69        &mut self,
70        bodies: &mut RigidBodySet,
71        colliders: &mut ColliderSet,
72    ) {
73        for i in 0..bodies.modified_bodies.len() {
74            let handle = bodies.modified_bodies[i];
75            let Some(rb) = bodies.get_mut_internal(handle) else {
76                continue;
77            };
78            if !rb.is_enabled() {
79                continue;
80            }
81
82            let position_ok = rb.pos.position.is_finite();
83            let next_position_ok = rb.pos.next_position.is_finite();
84            if position_ok && next_position_ok && rb.vels.is_finite() {
85                continue;
86            }
87
88            // Repair the pose from its surviving finite half, if any (e.g. only the kinematic
89            // target was invalid).
90            if position_ok && !next_position_ok {
91                rb.pos.next_position = rb.pos.position;
92            } else if !position_ok && next_position_ok {
93                rb.pos.position = rb.pos.next_position;
94            }
95            Self::sanitize_body_dynamics(rb);
96            rb.set_enabled(false);
97            self.bodies.push(handle);
98        }
99
100        for i in 0..colliders.modified_colliders.len() {
101            let handle = colliders.modified_colliders[i];
102            let Some(co) = colliders.get_mut_internal(handle) else {
103                continue;
104            };
105            if !co.is_enabled() {
106                continue;
107            }
108
109            let local_aabb = co.shape.compute_local_aabb();
110            if co.pos.0.is_finite()
111                && local_aabb.mins.is_finite()
112                && local_aabb.maxs.is_finite()
113                && co
114                    .parent
115                    .as_ref()
116                    .is_none_or(|p| p.pos_wrt_parent.is_finite())
117            {
118                continue;
119            }
120
121            co.set_enabled(false);
122            self.colliders.push(handle);
123        }
124    }
125
126    /// Applies the quarantines detected by the last `advance_to_final_positions` call, which
127    /// left the flagged bodies un-advanced (pose, collider positions and mass-properties still
128    /// broad-phase-consistent), so rolling back only means discarding `next_position`.
129    /// Velocities are zeroed immediately so remaining CCD substeps can't spread them; the
130    /// disable is processed next step, like a user `set_enabled(false)` between steps.
131    pub(super) fn apply_end_step(
132        &mut self,
133        bodies: &mut RigidBodySet,
134        colliders: &mut ColliderSet,
135    ) {
136        if self.body_scratch.is_empty() && self.collider_scratch.is_empty() {
137            return;
138        }
139
140        let mut body_scratch = core::mem::take(&mut self.body_scratch);
141        for (handle, prev_pose) in body_scratch.drain(..) {
142            let Some(rb) = bodies.get_mut_internal_with_modification_tracking(handle) else {
143                continue;
144            };
145
146            if prev_pose.is_finite() {
147                rb.pos.position = prev_pose;
148                rb.pos.next_position = prev_pose;
149            } else if rb.pos.position.is_finite() {
150                rb.pos.next_position = rb.pos.position;
151            }
152            Self::sanitize_body_dynamics(rb);
153
154            // Later CCD substeps can re-detect the same body; only the first detection reports.
155            if rb.is_enabled() {
156                rb.set_enabled(false);
157                self.bodies.push(handle);
158            }
159        }
160        self.body_scratch = body_scratch;
161
162        let mut collider_scratch = core::mem::take(&mut self.collider_scratch);
163        for handle in collider_scratch.drain(..) {
164            let Some(co) = colliders.get_mut_internal_with_modification_tracking(handle) else {
165                continue;
166            };
167            if co.is_enabled() {
168                co.set_enabled(false);
169                self.colliders.push(handle);
170            }
171        }
172        self.collider_scratch = collider_scratch;
173    }
174}
175
176impl PhysicsPipeline {
177    /// The non-finite state detected and neutralized during the most recent call to
178    /// [`Self::step`].
179    pub fn quarantine(&self) -> &Quarantine {
180        &self.quarantine
181    }
182}
183
184#[cfg(test)]
185mod test {
186    use crate::math::{Real, Vector};
187    use crate::prelude::{ColliderBuilder, FixedJointBuilder, PhysicsWorld, RigidBodyBuilder};
188
189    fn world_with_ground() -> PhysicsWorld {
190        let mut world = PhysicsWorld::new();
191        // A big fixed ball whose top surface is at y = 0 (shape choice is dimension-agnostic).
192        world.insert(
193            RigidBodyBuilder::fixed().translation(Vector::Y * -10.0),
194            ColliderBuilder::ball(10.0),
195        );
196        world
197    }
198
199    fn assert_enabled_bodies_are_finite(world: &PhysicsWorld) {
200        for (handle, rb) in world.rigid_bodies() {
201            if rb.is_enabled() {
202                assert!(
203                    rb.position().is_finite() && rb.vels().is_finite(),
204                    "enabled body {handle:?} has non-finite state"
205                );
206            }
207        }
208    }
209
210    #[test]
211    fn healthy_sim_never_quarantines() {
212        let mut world = world_with_ground();
213        let (handle, _) = world.insert(
214            RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
215            ColliderBuilder::ball(0.5),
216        );
217
218        for _ in 0..60 {
219            world.step();
220            assert!(world.quarantine().bodies().is_empty());
221            assert!(world.quarantine().colliders().is_empty());
222        }
223
224        assert!(world.bodies[handle].position().is_finite());
225    }
226
227    #[test]
228    fn user_set_nan_position_is_quarantined() {
229        let mut world = world_with_ground();
230        let (poisoned, _) = world.insert(
231            RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
232            ColliderBuilder::ball(0.5),
233        );
234        let (healthy, _) = world.insert(
235            RigidBodyBuilder::dynamic().translation(Vector::X * 5.0 + Vector::Y * 3.0),
236            ColliderBuilder::ball(0.5),
237        );
238        world.step();
239
240        world.bodies[poisoned].set_translation(Vector::NAN, true);
241        world.step();
242
243        assert_eq!(world.quarantine().bodies(), &[poisoned]);
244        assert!(!world.bodies[poisoned].is_enabled());
245        assert!(world.bodies[healthy].is_enabled());
246
247        // The report only covers the last step, and the simulation keeps running.
248        for _ in 0..10 {
249            world.step();
250            assert!(world.quarantine().bodies().is_empty());
251            assert_enabled_bodies_are_finite(&world);
252        }
253    }
254
255    #[test]
256    fn user_set_infinite_position_is_quarantined() {
257        let mut world = world_with_ground();
258        let (poisoned, _) = world.insert(
259            RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
260            ColliderBuilder::ball(0.5),
261        );
262        world.step();
263
264        world.bodies[poisoned].set_translation(Vector::Y * Real::INFINITY, true);
265        world.step();
266
267        assert_eq!(world.quarantine().bodies(), &[poisoned]);
268        assert!(!world.bodies[poisoned].is_enabled());
269        for _ in 0..10 {
270            world.step();
271            assert_enabled_bodies_are_finite(&world);
272        }
273    }
274
275    #[test]
276    fn user_set_nan_linvel_is_quarantined() {
277        let mut world = world_with_ground();
278        let (poisoned, _) = world.insert(
279            RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
280            ColliderBuilder::ball(0.5),
281        );
282        world.step();
283        let pose_before = *world.bodies[poisoned].position();
284
285        world.bodies[poisoned].set_linvel(Vector::NAN, true);
286        world.step();
287
288        assert_eq!(world.quarantine().bodies(), &[poisoned]);
289        let rb = &world.bodies[poisoned];
290        assert!(!rb.is_enabled());
291        // The velocity was neutralized before it could corrupt the pose.
292        assert_eq!(rb.linvel(), Vector::ZERO);
293        assert_eq!(*rb.position(), pose_before);
294    }
295
296    #[test]
297    fn nan_force_is_quarantined_at_end_of_step() {
298        let mut world = world_with_ground();
299        let (poisoned, _) = world.insert(
300            RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
301            ColliderBuilder::ball(0.5),
302        );
303        world.step();
304        let pose_before = *world.bodies[poisoned].position();
305
306        // A NaN force is invisible at the start of the step; integration turns it into a NaN
307        // velocity and pose mid-step, which the end-of-step chokepoint must catch and roll back.
308        world.bodies[poisoned].add_force(Vector::NAN, true);
309        world.step();
310
311        assert_eq!(world.quarantine().bodies(), &[poisoned]);
312        let rb = &world.bodies[poisoned];
313        assert!(!rb.is_enabled());
314        assert_eq!(rb.linvel(), Vector::ZERO);
315        // Rolled back to the pose it had at the beginning of the poisoned step.
316        assert_eq!(*rb.position(), pose_before);
317
318        for _ in 0..10 {
319            world.step();
320            assert_enabled_bodies_are_finite(&world);
321        }
322    }
323
324    #[test]
325    fn nan_spread_through_contacts_is_contained() {
326        let mut world = world_with_ground();
327        let (bottom, _) = world.insert(
328            RigidBodyBuilder::dynamic().translation(Vector::Y * 0.5),
329            ColliderBuilder::ball(0.5),
330        );
331        let (_top, _) = world.insert(
332            RigidBodyBuilder::dynamic().translation(Vector::Y * 1.5),
333            ColliderBuilder::ball(0.5),
334        );
335        // Let the stack settle into persistent contacts.
336        for _ in 0..30 {
337            world.step();
338            assert!(world.quarantine().bodies().is_empty());
339        }
340
341        world.bodies[bottom].add_force(Vector::NAN, true);
342        world.step();
343
344        // The invalid value may legitimately infect the whole island through the contact solver
345        // before the end-of-step catch; containment means every infected body is quarantined and
346        // nothing else is corrupted.
347        assert!(world.quarantine().bodies().contains(&bottom));
348        assert_enabled_bodies_are_finite(&world);
349
350        for _ in 0..10 {
351            world.step();
352            assert!(world.quarantine().bodies().is_empty());
353            assert_enabled_bodies_are_finite(&world);
354        }
355    }
356
357    #[test]
358    fn joint_partner_survives_user_set_nan_pose() {
359        let mut world = world_with_ground();
360        let (poisoned, _) = world.insert(
361            RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
362            ColliderBuilder::ball(0.5),
363        );
364        let (partner, _) = world.insert(
365            RigidBodyBuilder::dynamic().translation(Vector::Y * 4.0),
366            ColliderBuilder::ball(0.5),
367        );
368        world.insert_impulse_joint(poisoned, partner, FixedJointBuilder::new());
369        world.step();
370
371        // Set between steps: the step-start chokepoint catches it before the joint solver can
372        // spread it to the partner.
373        world.bodies[poisoned].set_translation(Vector::NAN, true);
374        world.step();
375
376        assert_eq!(world.quarantine().bodies(), &[poisoned]);
377        assert!(world.bodies[partner].is_enabled());
378        for _ in 0..10 {
379            world.step();
380            assert!(world.quarantine().bodies().is_empty());
381            assert_enabled_bodies_are_finite(&world);
382        }
383    }
384
385    #[test]
386    fn nan_kinematic_target_is_quarantined() {
387        let mut world = world_with_ground();
388        let (poisoned, _) = world.insert(
389            RigidBodyBuilder::kinematic_position_based().translation(Vector::Y * 3.0),
390            ColliderBuilder::ball(0.5),
391        );
392        world.step();
393        let pose_before = *world.bodies[poisoned].position();
394
395        world.bodies[poisoned].set_next_kinematic_translation(Vector::NAN);
396        world.step();
397
398        assert_eq!(world.quarantine().bodies(), &[poisoned]);
399        let rb = &world.bodies[poisoned];
400        assert!(!rb.is_enabled());
401        // Only the kinematic target was invalid; the pose was repaired from its valid half.
402        assert_eq!(*rb.position(), pose_before);
403    }
404
405    #[test]
406    fn nan_shape_standalone_collider_is_quarantined() {
407        let mut world = world_with_ground();
408        let poisoned = world.insert_collider(ColliderBuilder::ball(Real::NAN), None);
409        let (body, _) = world.insert(
410            RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
411            ColliderBuilder::ball(0.5),
412        );
413
414        for _ in 0..10 {
415            world.step();
416            assert_enabled_bodies_are_finite(&world);
417        }
418        assert!(!world.colliders[poisoned].is_enabled());
419        assert!(world.bodies[body].is_enabled());
420    }
421
422    #[test]
423    fn nan_shape_attached_collider_is_quarantined_but_body_survives() {
424        let mut world = world_with_ground();
425        let (body, poisoned) = world.insert(
426            RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
427            ColliderBuilder::ball(Real::NAN),
428        );
429
430        for _ in 0..10 {
431            world.step();
432            assert_enabled_bodies_are_finite(&world);
433        }
434        // The collider is neutralized; the body itself keeps simulating (in free fall,
435        // since it lost its only collider).
436        assert!(!world.colliders[poisoned].is_enabled());
437        let rb = &world.bodies[body];
438        assert!(rb.is_enabled());
439        assert!(rb.position().is_finite());
440    }
441
442    #[test]
443    fn quarantined_body_can_be_resurrected() {
444        let mut world = world_with_ground();
445        let (poisoned, _) = world.insert(
446            RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
447            ColliderBuilder::ball(0.5),
448        );
449        world.step();
450        world.bodies[poisoned].add_force(Vector::NAN, true);
451        world.step();
452        assert_eq!(world.quarantine().bodies(), &[poisoned]);
453
454        // The quarantine left the body with a finite pose and cleared the poisoned force, so
455        // re-enabling it resumes a healthy simulation.
456        let rb = &mut world.bodies[poisoned];
457        rb.set_translation(Vector::Y * 3.0, true);
458        rb.set_enabled(true);
459
460        for _ in 0..30 {
461            world.step();
462            assert!(world.quarantine().bodies().is_empty());
463            assert_enabled_bodies_are_finite(&world);
464        }
465        assert!(world.bodies[poisoned].is_enabled());
466    }
467}