#![cfg(feature = "parallel")]
use rapier3d::geometry::ContactPair;
use rapier3d::pipeline::EventHandler;
use rapier3d::prelude::*;
use std::sync::Mutex;
#[derive(Default)]
struct EventLog(Mutex<Vec<String>>);
impl EventHandler for EventLog {
fn handle_collision_event(
&self,
_bodies: &RigidBodySet,
_colliders: &ColliderSet,
event: CollisionEvent,
_contact_pair: Option<&ContactPair>,
) {
self.0.lock().unwrap().push(format!("{event:?}"));
}
fn handle_contact_force_event(
&self,
_dt: Real,
_bodies: &RigidBodySet,
_colliders: &ColliderSet,
_contact_pair: &ContactPair,
_total_force_magnitude: Real,
) {
}
fn handle_soft_body_tear_event(&self, _soft_bodies: &SoftBodySet, _event: &SoftBodyTearEvent) {}
}
fn snapshot(bodies: &RigidBodySet) -> Vec<(u32, String)> {
let mut out: Vec<_> = bodies
.iter()
.map(|(h, rb)| {
(
h.into_raw_parts().0,
format!(
"{:?} {:?} {:?} {:?} {}",
rb.translation(),
rb.rotation(),
rb.linvel(),
rb.angvel(),
rb.is_sleeping()
),
)
})
.collect();
out.sort_by_key(|e| e.0);
out
}
fn soft_snapshot(soft_bodies: &SoftBodySet) -> Vec<String> {
let mut out = Vec::new();
for (h, sb) in soft_bodies.iter() {
for (i, p) in sb.particles().iter().enumerate() {
out.push(format!(
"{:?}/{i} {:?} {:?}",
h.into_raw_parts().0,
p.position(),
p.velocity()
));
}
}
out
}
#[cfg(feature = "serde-serialize")]
fn phase_checksum(bf: &BroadPhaseBvh, nf: &NarrowPhase) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
let mut eat = |bytes: &[u8]| {
for b in bytes {
h ^= *b as u64;
h = h.wrapping_mul(0x100000001b3);
}
};
eat(&bincode::serialize(bf).expect("broad-phase serialization"));
eat(&bincode::serialize(nf).expect("narrow-phase serialization"));
h
}
#[cfg(not(feature = "serde-serialize"))]
fn phase_checksum(_bf: &BroadPhaseBvh, _nf: &NarrowPhase) -> u64 {
0
}
type StepRecord = (Vec<(u32, String)>, Vec<String>, u64, Vec<String>);
fn run_sim(num_threads: usize, num_steps: usize) -> Vec<StepRecord> {
let pool = rapier3d::rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build()
.unwrap();
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let mut impulse_joints = ImpulseJointSet::new();
let mut multibody_joints = MultibodyJointSet::new();
let mut soft_bodies = SoftBodySet::new();
let mut pipeline = PhysicsPipeline::new();
let mut bf = BroadPhaseBvh::new();
let mut nf = NarrowPhase::new();
let mut islands = IslandManager::new();
let mut ccd = CCDSolver::new();
let params = IntegrationParameters::default();
let gravity = Vector::Y * -9.81;
let ground = bodies.insert(RigidBodyBuilder::fixed().translation(Vector::new(0.0, -0.5, 0.0)));
colliders.insert_with_parent(
ColliderBuilder::cuboid(20.0, 0.5, 20.0),
ground,
&mut bodies,
);
let mut cubes = Vec::new();
for i in 0..12 {
for j in 0..3 {
for k in 0..12 {
let jitter = (i as f32 * 0.013 + k as f32 * 0.017) % 0.05;
let pos = Vector::new(
i as f32 * 1.05 - 6.0 + jitter,
j as f32 * 1.05 + 0.55,
k as f32 * 1.05 - 6.0 - jitter,
);
let rb = bodies.insert(RigidBodyBuilder::dynamic().translation(pos));
colliders.insert_with_parent(
ColliderBuilder::cuboid(0.5, 0.5, 0.5)
.active_events(ActiveEvents::COLLISION_EVENTS),
rb,
&mut bodies,
);
cubes.push(rb);
}
}
}
let anchor = bodies.insert(RigidBodyBuilder::fixed().translation(Vector::new(0.0, 8.0, 0.0)));
let mut prev = anchor;
for i in 0..4 {
let rb = bodies.insert(RigidBodyBuilder::dynamic().translation(Vector::new(
0.6 * (i + 1) as f32,
8.0,
0.0,
)));
colliders.insert_with_parent(ColliderBuilder::ball(0.25), rb, &mut bodies);
let joint = SphericalJointBuilder::new()
.local_anchor1(Vector::X * 0.3)
.local_anchor2(Vector::X * -0.3);
impulse_joints.insert(prev, rb, joint, true);
prev = rb;
}
colliders.insert(
ColliderBuilder::cuboid(3.0, 0.5, 3.0)
.translation(Vector::new(0.0, 1.0, 0.0))
.sensor(true)
.active_events(ActiveEvents::COLLISION_EVENTS),
);
let cloth = SoftBodyBuilder::cloth(
Vector::new(-2.0, 4.0, -2.0),
Vector::X * 0.2,
Vector::Z * 0.2,
20,
20,
)
.particle_mass(0.05);
soft_bodies.insert(cloth, &mut bodies, &mut colliders);
let jelly = SoftBodyBuilder::cuboid(Vector::new(4.0, 5.0, 4.0), Vector::splat(0.6), 4, 4, 4)
.cell_model(SoftBodyCellModel::Corotational)
.particle_mass(0.2);
soft_bodies.insert(jelly, &mut bodies, &mut colliders);
let balloon = SoftBodyBuilder::sphere(Vector::new(-4.0, 5.0, 4.0), 0.8, 2)
.shape_matching(true)
.particle_mass(0.05);
soft_bodies.insert(balloon, &mut bodies, &mut colliders);
#[cfg(feature = "fem")]
{
let fem_jelly =
SoftBodyBuilder::cuboid(Vector::new(4.0, 5.0, -4.0), Vector::splat(0.6), 4, 4, 4)
.cell_model(SoftBodyCellModel::Corotational)
.solver(SoftBodySolver::Fem)
.particle_mass(0.2);
soft_bodies.insert(fem_jelly, &mut bodies, &mut colliders);
}
let events = EventLog::default();
let mut snapshots = Vec::new();
for step in 0..num_steps {
if step == 60 {
bodies.remove(
cubes[7],
&mut islands,
&mut colliders,
&mut impulse_joints,
&mut multibody_joints,
&mut soft_bodies,
true,
);
}
if step == 100 {
bodies[cubes[20]].apply_impulse(Vector::new(2.0, 3.0, 1.0), true);
}
pool.install(|| {
pipeline.step(
gravity,
¶ms,
&mut islands,
&mut bf,
&mut nf,
&mut bodies,
&mut colliders,
&mut impulse_joints,
&mut multibody_joints,
&mut soft_bodies,
&mut ccd,
&(),
&events,
);
});
let step_events = core::mem::take(&mut *events.0.lock().unwrap());
let checksum = if step % 10 == 0 || step + 1 == num_steps {
phase_checksum(&bf, &nf)
} else {
0
};
snapshots.push((
snapshot(&bodies),
step_events,
checksum,
soft_snapshot(&soft_bodies),
));
}
snapshots
}
#[test]
fn identical_results_for_any_worker_count() {
const STEPS: usize = 160;
let base = run_sim(1, STEPS);
for num_threads in [2, 8] {
let run = run_sim(num_threads, STEPS);
for step in 0..STEPS {
let (
(base_state, base_events, base_sum, base_soft),
(run_state, run_events, run_sum, run_soft),
) = (&base[step], &run[step]);
assert_eq!(
base_soft, run_soft,
"soft-body particle divergence at step {step}, {num_threads} threads vs 1"
);
assert_eq!(
base_events, run_events,
"event-sequence divergence at step {step}, {num_threads} threads vs 1"
);
assert_eq!(
base_sum, run_sum,
"broad/narrow-phase checksum divergence at step {step}, {num_threads} threads \
vs 1: the two pool sizes stored the same simulation in a different layout"
);
if base_state != run_state {
let (h, s) = base_state
.iter()
.zip(run_state.iter())
.find(|(a, b)| a != b)
.map(|(a, b)| {
(
a.0,
format!("1 thread: {}\n{num_threads} threads: {}", a.1, b.1),
)
})
.unwrap();
panic!(
"state divergence at step {step}, body {h}, {num_threads} threads vs 1:\n{s}"
);
}
}
}
}