rapier2d/pipeline/physics_pipeline/
quarantine.rs1use crate::alloc_prelude::*;
9use crate::dynamics::{
10 RigidBody, RigidBodyHandle, RigidBodySet, RigidBodyVelocity, SoftBodyHandle,
11};
12use crate::geometry::{ColliderHandle, ColliderSet};
13use crate::math::{Pose, Vector};
14use crate::pipeline::PhysicsPipeline;
15
16#[derive(Default)]
20pub struct Quarantine {
21 bodies: Vec<RigidBodyHandle>,
23 colliders: Vec<ColliderHandle>,
25 pub(super) soft_bodies: Vec<SoftBodyHandle>,
27 pub(super) body_workspace: Vec<(RigidBodyHandle, Pose)>,
30 pub(super) collider_workspace: Vec<ColliderHandle>,
33}
34
35impl Quarantine {
36 pub fn bodies(&self) -> &[RigidBodyHandle] {
38 &self.bodies
39 }
40
41 pub fn colliders(&self) -> &[ColliderHandle] {
43 &self.colliders
44 }
45
46 pub fn soft_bodies(&self) -> &[SoftBodyHandle] {
50 &self.soft_bodies
51 }
52
53 pub fn is_empty(&self) -> bool {
55 self.bodies.is_empty() && self.colliders.is_empty() && self.soft_bodies.is_empty()
56 }
57
58 pub(super) fn clear(&mut self) {
60 self.bodies.clear();
61 self.colliders.clear();
62 self.soft_bodies.clear();
63 }
64
65 fn sanitize_body_dynamics(rb: &mut RigidBody) {
67 rb.vels = RigidBodyVelocity::zero();
68 rb.ccd_vels = RigidBodyVelocity::zero();
69 rb.forces.force = Vector::ZERO;
70 rb.forces.torque = Default::default();
71 rb.forces.user_force = Vector::ZERO;
72 rb.forces.user_torque = Default::default();
73 }
74
75 pub(super) fn detect_user_changes(
79 &mut self,
80 bodies: &mut RigidBodySet,
81 colliders: &mut ColliderSet,
82 ) {
83 for i in 0..bodies.modified_bodies.len() {
84 let handle = bodies.modified_bodies[i];
85 let Some(rb) = bodies.get_mut_internal(handle) else {
86 continue;
87 };
88 if !rb.is_enabled() {
89 continue;
90 }
91
92 let position_ok = rb.pos.position.is_finite();
93 let next_position_ok = rb.pos.next_position.is_finite();
94 if position_ok && next_position_ok && rb.vels.is_finite() {
95 continue;
96 }
97
98 if position_ok && !next_position_ok {
101 rb.pos.next_position = rb.pos.position;
102 } else if !position_ok && next_position_ok {
103 rb.pos.position = rb.pos.next_position;
104 }
105 Self::sanitize_body_dynamics(rb);
106 rb.set_enabled(false);
107 self.bodies.push(handle);
108 }
109
110 for i in 0..colliders.modified_colliders.len() {
111 let handle = colliders.modified_colliders[i];
112 let Some(co) = colliders.get_mut_internal(handle) else {
113 continue;
114 };
115 if !co.is_enabled() {
116 continue;
117 }
118
119 let local_aabb = co.shape.compute_local_aabb();
120 if co.pos.0.is_finite()
121 && local_aabb.mins.is_finite()
122 && local_aabb.maxs.is_finite()
123 && co
124 .parent
125 .as_ref()
126 .is_none_or(|p| p.pos_wrt_parent.is_finite())
127 {
128 continue;
129 }
130
131 co.set_enabled(false);
132 self.colliders.push(handle);
133 }
134 }
135
136 pub(super) fn apply_end_step(
142 &mut self,
143 bodies: &mut RigidBodySet,
144 colliders: &mut ColliderSet,
145 ) {
146 if self.body_workspace.is_empty() && self.collider_workspace.is_empty() {
147 return;
148 }
149
150 let mut body_workspace = core::mem::take(&mut self.body_workspace);
151 for (handle, prev_pose) in body_workspace.drain(..) {
152 let Some(rb) = bodies.get_mut_internal_with_modification_tracking(handle) else {
153 continue;
154 };
155
156 if prev_pose.is_finite() {
157 rb.pos.position = prev_pose;
158 rb.pos.next_position = prev_pose;
159 } else if rb.pos.position.is_finite() {
160 rb.pos.next_position = rb.pos.position;
161 }
162 Self::sanitize_body_dynamics(rb);
163
164 if rb.is_enabled() {
166 rb.set_enabled(false);
167 self.bodies.push(handle);
168 }
169 }
170 self.body_workspace = body_workspace;
171
172 let mut collider_workspace = core::mem::take(&mut self.collider_workspace);
173 for handle in collider_workspace.drain(..) {
174 let Some(co) = colliders.get_mut_internal_with_modification_tracking(handle) else {
175 continue;
176 };
177 if co.is_enabled() {
178 co.set_enabled(false);
179 self.colliders.push(handle);
180 }
181 }
182 self.collider_workspace = collider_workspace;
183 }
184}
185
186impl PhysicsPipeline {
187 pub fn quarantine(&self) -> &Quarantine {
190 &self.quarantine
191 }
192}
193
194#[cfg(test)]
195mod test {
196 use crate::math::{Real, Vector};
197 use crate::prelude::{ColliderBuilder, FixedJointBuilder, PhysicsWorld, RigidBodyBuilder};
198
199 fn world_with_ground() -> PhysicsWorld {
200 let mut world = PhysicsWorld::new();
201 world.insert(
203 RigidBodyBuilder::fixed().translation(Vector::Y * -10.0),
204 ColliderBuilder::ball(10.0),
205 );
206 world
207 }
208
209 fn assert_enabled_bodies_are_finite(world: &PhysicsWorld) {
210 for (handle, rb) in world.rigid_bodies() {
211 if rb.is_enabled() {
212 assert!(
213 rb.position().is_finite() && rb.vels().is_finite(),
214 "enabled body {handle:?} has non-finite state"
215 );
216 }
217 }
218 }
219
220 #[test]
221 fn healthy_sim_never_quarantines() {
222 let mut world = world_with_ground();
223 let (handle, _) = world.insert(
224 RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
225 ColliderBuilder::ball(0.5),
226 );
227
228 for _ in 0..60 {
229 world.step();
230 assert!(world.quarantine().bodies().is_empty());
231 assert!(world.quarantine().colliders().is_empty());
232 }
233
234 assert!(world.bodies[handle].position().is_finite());
235 }
236
237 #[test]
238 fn user_set_nan_position_is_quarantined() {
239 let mut world = world_with_ground();
240 let (poisoned, _) = world.insert(
241 RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
242 ColliderBuilder::ball(0.5),
243 );
244 let (healthy, _) = world.insert(
245 RigidBodyBuilder::dynamic().translation(Vector::X * 5.0 + Vector::Y * 3.0),
246 ColliderBuilder::ball(0.5),
247 );
248 world.step();
249
250 world.bodies[poisoned].set_translation(Vector::NAN, true);
251 world.step();
252
253 assert_eq!(world.quarantine().bodies(), &[poisoned]);
254 assert!(!world.bodies[poisoned].is_enabled());
255 assert!(world.bodies[healthy].is_enabled());
256
257 for _ in 0..10 {
259 world.step();
260 assert!(world.quarantine().bodies().is_empty());
261 assert_enabled_bodies_are_finite(&world);
262 }
263 }
264
265 #[test]
266 fn user_set_infinite_position_is_quarantined() {
267 let mut world = world_with_ground();
268 let (poisoned, _) = world.insert(
269 RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
270 ColliderBuilder::ball(0.5),
271 );
272 world.step();
273
274 world.bodies[poisoned].set_translation(Vector::Y * Real::INFINITY, true);
275 world.step();
276
277 assert_eq!(world.quarantine().bodies(), &[poisoned]);
278 assert!(!world.bodies[poisoned].is_enabled());
279 for _ in 0..10 {
280 world.step();
281 assert_enabled_bodies_are_finite(&world);
282 }
283 }
284
285 #[test]
286 fn user_set_nan_linvel_is_quarantined() {
287 let mut world = world_with_ground();
288 let (poisoned, _) = world.insert(
289 RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
290 ColliderBuilder::ball(0.5),
291 );
292 world.step();
293 let pose_before = *world.bodies[poisoned].position();
294
295 world.bodies[poisoned].set_linvel(Vector::NAN, true);
296 world.step();
297
298 assert_eq!(world.quarantine().bodies(), &[poisoned]);
299 let rb = &world.bodies[poisoned];
300 assert!(!rb.is_enabled());
301 assert_eq!(rb.linvel(), Vector::ZERO);
303 assert_eq!(*rb.position(), pose_before);
304 }
305
306 #[test]
307 fn nan_force_is_quarantined_at_end_of_step() {
308 let mut world = world_with_ground();
309 let (poisoned, _) = world.insert(
310 RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
311 ColliderBuilder::ball(0.5),
312 );
313 world.step();
314 let pose_before = *world.bodies[poisoned].position();
315
316 world.bodies[poisoned].add_force(Vector::NAN, true);
319 world.step();
320
321 assert_eq!(world.quarantine().bodies(), &[poisoned]);
322 let rb = &world.bodies[poisoned];
323 assert!(!rb.is_enabled());
324 assert_eq!(rb.linvel(), Vector::ZERO);
325 assert_eq!(*rb.position(), pose_before);
327
328 for _ in 0..10 {
329 world.step();
330 assert_enabled_bodies_are_finite(&world);
331 }
332 }
333
334 #[test]
335 fn nan_spread_through_contacts_is_contained() {
336 let mut world = world_with_ground();
337 let (bottom, _) = world.insert(
338 RigidBodyBuilder::dynamic().translation(Vector::Y * 0.5),
339 ColliderBuilder::ball(0.5),
340 );
341 let (_top, _) = world.insert(
342 RigidBodyBuilder::dynamic().translation(Vector::Y * 1.5),
343 ColliderBuilder::ball(0.5),
344 );
345 for _ in 0..30 {
347 world.step();
348 assert!(world.quarantine().bodies().is_empty());
349 }
350
351 world.bodies[bottom].add_force(Vector::NAN, true);
352 world.step();
353
354 assert!(world.quarantine().bodies().contains(&bottom));
358 assert_enabled_bodies_are_finite(&world);
359
360 for _ in 0..10 {
361 world.step();
362 assert!(world.quarantine().bodies().is_empty());
363 assert_enabled_bodies_are_finite(&world);
364 }
365 }
366
367 #[test]
368 fn joint_partner_survives_user_set_nan_pose() {
369 let mut world = world_with_ground();
370 let (poisoned, _) = world.insert(
371 RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
372 ColliderBuilder::ball(0.5),
373 );
374 let (partner, _) = world.insert(
375 RigidBodyBuilder::dynamic().translation(Vector::Y * 4.0),
376 ColliderBuilder::ball(0.5),
377 );
378 world.insert_impulse_joint(poisoned, partner, FixedJointBuilder::new());
379 world.step();
380
381 world.bodies[poisoned].set_translation(Vector::NAN, true);
384 world.step();
385
386 assert_eq!(world.quarantine().bodies(), &[poisoned]);
387 assert!(world.bodies[partner].is_enabled());
388 for _ in 0..10 {
389 world.step();
390 assert!(world.quarantine().bodies().is_empty());
391 assert_enabled_bodies_are_finite(&world);
392 }
393 }
394
395 #[test]
396 fn nan_kinematic_target_is_quarantined() {
397 let mut world = world_with_ground();
398 let (poisoned, _) = world.insert(
399 RigidBodyBuilder::kinematic_position_based().translation(Vector::Y * 3.0),
400 ColliderBuilder::ball(0.5),
401 );
402 world.step();
403 let pose_before = *world.bodies[poisoned].position();
404
405 world.bodies[poisoned].set_next_kinematic_translation(Vector::NAN);
406 world.step();
407
408 assert_eq!(world.quarantine().bodies(), &[poisoned]);
409 let rb = &world.bodies[poisoned];
410 assert!(!rb.is_enabled());
411 assert_eq!(*rb.position(), pose_before);
413 }
414
415 #[test]
416 fn nan_shape_standalone_collider_is_quarantined() {
417 let mut world = world_with_ground();
418 let poisoned = world.insert_collider(ColliderBuilder::ball(Real::NAN), None);
419 let (body, _) = world.insert(
420 RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
421 ColliderBuilder::ball(0.5),
422 );
423
424 for _ in 0..10 {
425 world.step();
426 assert_enabled_bodies_are_finite(&world);
427 }
428 assert!(!world.colliders[poisoned].is_enabled());
429 assert!(world.bodies[body].is_enabled());
430 }
431
432 #[test]
433 fn nan_shape_attached_collider_is_quarantined_but_body_survives() {
434 let mut world = world_with_ground();
435 let (body, poisoned) = world.insert(
436 RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
437 ColliderBuilder::ball(Real::NAN),
438 );
439
440 for _ in 0..10 {
441 world.step();
442 assert_enabled_bodies_are_finite(&world);
443 }
444 assert!(!world.colliders[poisoned].is_enabled());
447 let rb = &world.bodies[body];
448 assert!(rb.is_enabled());
449 assert!(rb.position().is_finite());
450 }
451
452 #[test]
453 fn quarantined_body_can_be_resurrected() {
454 let mut world = world_with_ground();
455 let (poisoned, _) = world.insert(
456 RigidBodyBuilder::dynamic().translation(Vector::Y * 3.0),
457 ColliderBuilder::ball(0.5),
458 );
459 world.step();
460 world.bodies[poisoned].add_force(Vector::NAN, true);
461 world.step();
462 assert_eq!(world.quarantine().bodies(), &[poisoned]);
463
464 let rb = &mut world.bodies[poisoned];
467 rb.set_translation(Vector::Y * 3.0, true);
468 rb.set_enabled(true);
469
470 for _ in 0..30 {
471 world.step();
472 assert!(world.quarantine().bodies().is_empty());
473 assert_enabled_bodies_are_finite(&world);
474 }
475 assert!(world.bodies[poisoned].is_enabled());
476 }
477}