rapier2d/pipeline/physics_pipeline/
quarantine.rs1use 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#[derive(Default)]
20pub struct Quarantine {
21 bodies: Vec<RigidBodyHandle>,
23 colliders: Vec<ColliderHandle>,
25 pub(super) body_scratch: Vec<(RigidBodyHandle, Pose)>,
28 pub(super) collider_scratch: Vec<ColliderHandle>,
31}
32
33impl Quarantine {
34 pub fn bodies(&self) -> &[RigidBodyHandle] {
36 &self.bodies
37 }
38
39 pub fn colliders(&self) -> &[ColliderHandle] {
41 &self.colliders
42 }
43
44 pub fn is_empty(&self) -> bool {
46 self.bodies.is_empty() && self.colliders.is_empty()
47 }
48
49 pub(super) fn clear(&mut self) {
51 self.bodies.clear();
52 self.colliders.clear();
53 }
54
55 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}