Affected versions: 0.32.0, 0.33.0, current master (7e72d13). 3D, f32 (2D presumably identical — same code path).
Summary
With the parallel feature enabled, the first contact involving a multibody
link panics:
thread '<unnamed>' panicked at nalgebra-0.35.0/src/base/matrix_view.rs:310:9:
Matrix slicing out of bounds.
...
7: rapier3d::dynamics::joint::multibody_joint::multibody::Multibody::fill_jacobians
8: rapier3d::dynamics::solver::contact_constraint::generic_contact_constraint::GenericContactConstraintBuilder::generate
9: rapier3d::dynamics::solver::contact_constraint::contact_constraints_set::ContactConstraintsSet::init
10: rapier3d::dynamics::solver::velocity_solver::VelocitySolver::init_constraints
11: rapier3d::dynamics::solver::island_solver::IslandSolver::init_and_solve
12-19: rayon ...
Serial builds are unaffected.
Root cause
generic_contact_constraint.rs (and the same pattern in
generic_joint_constraint_builder.rs) gates the on-demand jacobian-buffer
resize on the feature flag:
if jacobians.nrows() < required_jacobian_len && !cfg!(feature = "parallel") {
jacobians.resize_vertically_mut(required_jacobian_len, 0.0);
}
This guard appears to be a leftover from the old intra-island parallelism
(where the buffer was shared across tasks and could not be resized inside
generate). The current solver parallelizes across islands only — the
comment in physics_pipeline.rs says intra-island parallelism "hasn't been
ported to the new solver yet" — and each rayon task owns its IslandSolver
(and therefore its ContactConstraintsSet::generic_jacobians) exclusively via
par_iter_mut. Nothing else ever sizes generic_jacobians (it is created as
DVector::zeros(0) and only fill(0.0) is called on it), so under
feature = "parallel" it stays 0-length forever and the first
fill_jacobians slices out of bounds.
Since the buffer is task-exclusive under the current design, the resize is
race-free and the guard can simply be removed (see attached patch/PR, which
also adds a regression test).
Reproduction
Any scene where a multibody link touches anything, built with
--features parallel:
let mut world = PhysicsWorld::new();
// Free-standing fixed ground (NOT part of any multibody).
world.insert(
RigidBodyBuilder::fixed(),
ColliderBuilder::cuboid(50.0, 0.5, 50.0),
);
// A 4-link dynamic chain on spherical joints, starting above the ground.
let mut prev = None;
for i in 0..4 {
let (link, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(i as f32 * 1.2, 4.0, 0.0)),
ColliderBuilder::cuboid(0.5, 0.5, 0.5),
);
if let Some(p) = prev {
world.insert_multibody_joint(
p,
link,
SphericalJointBuilder::new().local_anchor1(Vector::new(1.2, 0.0, 0.0)),
);
}
prev = Some(link);
}
for _ in 0..400 {
world.step(); // panics when the chain reaches the ground
}
Passes without parallel; panics with it. With the guard removed it passes in
both modes, and the full test suite stays green in both modes.
Found in a Bevy/bevy_rapier3d simulation (articulated entities with limbs controlled by neural networks under constant
spawn/despawn churn) where it crashed long-running sessions the first time a
swimmer link collided with terrain.
Affected versions: 0.32.0, 0.33.0, current master (
7e72d13). 3D,f32(2D presumably identical — same code path).Summary
With the
parallelfeature enabled, the first contact involving a multibodylink panics:
Serial builds are unaffected.
Root cause
generic_contact_constraint.rs(and the same pattern ingeneric_joint_constraint_builder.rs) gates the on-demand jacobian-bufferresize on the feature flag:
if jacobians.nrows() < required_jacobian_len && !cfg!(feature = "parallel") {
jacobians.resize_vertically_mut(required_jacobian_len, 0.0);
}
This guard appears to be a leftover from the old intra-island parallelism
(where the buffer was shared across tasks and could not be resized inside
generate). The current solver parallelizes across islands only — thecomment in
physics_pipeline.rssays intra-island parallelism "hasn't beenported to the new solver yet" — and each rayon task owns its
IslandSolver(and therefore its
ContactConstraintsSet::generic_jacobians) exclusively viapar_iter_mut. Nothing else ever sizesgeneric_jacobians(it is created asDVector::zeros(0)and onlyfill(0.0)is called on it), so underfeature = "parallel"it stays 0-length forever and the firstfill_jacobiansslices out of bounds.Since the buffer is task-exclusive under the current design, the resize is
race-free and the guard can simply be removed (see attached patch/PR, which
also adds a regression test).
Reproduction
Any scene where a multibody link touches anything, built with
--features parallel:let mut world = PhysicsWorld::new();
// Free-standing fixed ground (NOT part of any multibody).
world.insert(
RigidBodyBuilder::fixed(),
ColliderBuilder::cuboid(50.0, 0.5, 50.0),
);
// A 4-link dynamic chain on spherical joints, starting above the ground.
let mut prev = None;
for i in 0..4 {
let (link, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(i as f32 * 1.2, 4.0, 0.0)),
ColliderBuilder::cuboid(0.5, 0.5, 0.5),
);
if let Some(p) = prev {
world.insert_multibody_joint(
p,
link,
SphericalJointBuilder::new().local_anchor1(Vector::new(1.2, 0.0, 0.0)),
);
}
prev = Some(link);
}
for _ in 0..400 {
world.step(); // panics when the chain reaches the ground
}
Passes without
parallel; panics with it. With the guard removed it passes inboth modes, and the full test suite stays green in both modes.
Found in a Bevy/bevy_rapier3d simulation (articulated entities with limbs controlled by neural networks under constant
spawn/despawn churn) where it crashed long-running sessions the first time a
swimmer link collided with terrain.