Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions crates/rapier2d/tests/ccd_fixed_target_cache_invalidation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! The CCD fixed-target cache must not outlive the colliders it points at.
//!
//! The non-bullet continuous pass sweeps against a cached list of fixed targets, held as
//! `ColliderHandle`s from one step to the next. The scene-change signal that invalidates it
//! is rebuilt from the per-step user-change lists, which are drained whether or not the CCD
//! pass runs — so a removal on a step where nothing moved fast enough used to be forgotten,
//! leaving the cache holding a dead handle for the next fast body to resolve.

use rapier2d::prelude::*;

/// A fixed collider removed on a step without any CCD-active body must not linger in the
/// cached fixed-target list.
#[test]
fn fixed_collider_removed_on_a_ccd_less_step_leaves_no_stale_target() {
let mut world = PhysicsWorld::new();

// Two parentless (hence fixed-target) colliders. `stale` sits on the path `faller`
// takes down to the ground, so a leftover entry is guaranteed to be resolved rather
// than skipped by the swept-AABB test.
world.insert_collider(
ColliderBuilder::cuboid(100.0, 0.5).translation(Vector::new(0.0, 0.0)),
None,
);
let stale = world.insert_collider(
ColliderBuilder::cuboid(2.0, 2.0).translation(Vector::new(10.0, 25.0)),
None,
);

// Moving fast from the first step: forces a continuous pass, which is what populates
// the fixed-target cache.
let fast = world.insert_body(
RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 30.0))
.linvel(Vector::new(0.0, -400.0)),
);
world.insert_collider(ColliderBuilder::ball(0.2), Some(fast));

// Starts at rest and only becomes CCD-active later, from gravity alone — no user change
// marks the step it wakes the continuous pass back up.
let faller =
world.insert_body(RigidBodyBuilder::dynamic().translation(Vector::new(10.0, 60.0)));
world.insert_collider(ColliderBuilder::ball(0.05), Some(faller));

world.step();

// Take the fast body out so the next few steps skip the continuous pass entirely.
world.remove_body(fast);
world.step();

// The removal whose invalidation signal used to be dropped: `faller` is still slow, so
// no CCD pass runs this step to notice it.
world.remove_collider(stale);
world.step();

// Nothing is touched from here on: `faller` accelerates until it is CCD-active, and the
// pass it triggers resolves the cached fixed targets.
for _ in 0..400 {
world.step();
}

// It fell all the way through where `stale` used to be and settled on the ground.
let y = world.bodies[faller].translation().y;
assert!(
(y - 0.55).abs() < 0.1,
"faller should rest on the ground at y ≈ 0.55, got {y}"
);
}
23 changes: 12 additions & 11 deletions src/dynamics/ccd/ccd_solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ impl CCDSolver {
Self::default()
}

/// Discards the cached fixed-target list used by the non-bullet continuous pass.
/// This must be called any time the fixed targets change. If using the physics pipeline,
/// it is called for you. If not, it must be called manually.
pub fn invalidate_fixed_targets_cache(&mut self) {
self.fixed_targets_cache = None;
}

/// Updates the set of bodies that needs CCD to be resolved.
///
/// Returns `true` if any rigid-body must have CCD resolved.
Expand Down Expand Up @@ -165,10 +172,6 @@ impl CCDSolver {
narrow_phase: &NarrowPhase,
hooks: &dyn PhysicsHooks,
events: &dyn EventHandler,
// `true` when colliders/bodies were added, removed or modified by the
// user since the last step: the only ways a fixed target can appear,
// vanish or move, hence the fixed-target cache invalidation signal.
scene_changed: bool,
) {
let dt = params.dt;
let linear_slop = params.allowed_linear_error();
Expand All @@ -194,14 +197,12 @@ impl CCDSolver {
);
let (bvh, dispatcher) = (query_pipeline.bvh, query_pipeline.dispatcher);
// Non-bullet fast bodies only hit fixed targets: sweep against the (small) cached
// fixed-collider list instead of the full BVH. Rebuilt — a full collider scan —
// only on scene changes, since fixed targets can't move otherwise.
// fixed-collider list instead of the full BVH.
let prediction = params.prediction_distance();
let cache_valid = !scene_changed
&& self
.fixed_targets_cache
.as_ref()
.is_some_and(|(p, _)| *p == prediction);
let cache_valid = self
.fixed_targets_cache
.as_ref()
.is_some_and(|(p, _)| *p == prediction);
if !cache_valid {
self.fixed_targets_cache = Some((
prediction,
Expand Down
14 changes: 7 additions & 7 deletions src/pipeline/physics_pipeline/substep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ impl PhysicsPipeline {
ccd_solver: &mut CCDSolver,
hooks: &dyn PhysicsHooks,
events: &dyn EventHandler,
scene_changed: bool,
) {
self.counters.ccd.toi_computation_time.start();
// Handle CCD: sweep the fast bodies and clamp their `next_position` to their
Expand All @@ -76,7 +75,6 @@ impl PhysicsPipeline {
narrow_phase,
hooks,
events,
scene_changed,
);
self.counters.ccd.toi_computation_time.pause();
}
Expand Down Expand Up @@ -333,12 +331,15 @@ impl PhysicsPipeline {
.filter(|h| colliders.get(*h).map(|c| !c.is_enabled()).unwrap_or(false)),
);

// Whether any user change could have added, removed or moved a FIXED
// collider this step the CCD fixed-target cache invalidation signal
// If a user change could have added, removed, or moved a FIXED
// collider this step, invalidate the cache.
// (internal motion never touches fixed colliders nor these lists).
let ccd_scene_changed = !modified_colliders.is_empty()
if !modified_colliders.is_empty()
|| !removed_colliders.is_empty()
|| !modified_bodies.is_empty();
|| !modified_bodies.is_empty()
{
ccd_solver.invalidate_fixed_targets_cache();
}

// Join islands based on new joints.
#[cfg(feature = "enhanced-determinism")]
Expand Down Expand Up @@ -514,7 +515,6 @@ impl PhysicsPipeline {
ccd_solver,
hooks,
events,
ccd_scene_changed,
);
}
}
Expand Down
Loading