#![allow(dead_code)]
#![allow(rustdoc::broken_intra_doc_links)]
use crate::error::PhysicsError;
use crate::math::Fix128;
use std::collections::HashMap;
pub(crate) trait BodyLike {
fn stable_id(&self) -> u64;
fn is_dynamic(&self) -> bool;
}
pub(crate) trait ContactLike {
fn body_a(&self) -> usize;
fn body_b(&self) -> usize;
fn stable_id(&self) -> u64;
}
pub(crate) trait JointLike {
fn body_a(&self) -> usize;
fn body_b(&self) -> usize;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct CachedImpulse {
pub(crate) normal: Fix128,
pub(crate) tangent1: Fix128,
pub(crate) tangent2: Fix128,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct ImpulseCache {
pub(crate) entries: HashMap<u64, CachedImpulse>,
pub(crate) live: HashMap<u64, ()>,
pub(crate) hits: u64,
pub(crate) misses: u64,
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct ImpulseCacheStats {
pub(crate) hits: u64,
pub(crate) misses: u64,
}
impl ImpulseCacheStats {
#[must_use]
pub(crate) fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
impl ImpulseCache {
#[must_use]
pub(crate) fn new() -> Self {
Self::default()
}
#[must_use]
pub(crate) fn take(&mut self, contact_id: u64) -> CachedImpulse {
self.live.insert(contact_id, ());
match self.entries.get(&contact_id).copied() {
Some(v) => {
self.hits += 1;
v
}
None => {
self.misses += 1;
CachedImpulse::default()
}
}
}
#[must_use]
pub(crate) fn stats(&self) -> ImpulseCacheStats {
ImpulseCacheStats {
hits: self.hits,
misses: self.misses,
}
}
pub(crate) fn reset_stats(&mut self) {
self.hits = 0;
self.misses = 0;
}
pub(crate) fn set(&mut self, contact_id: u64, imp: CachedImpulse) {
self.entries.insert(contact_id, imp);
}
pub(crate) fn sweep(&mut self) {
self.entries.retain(|k, _| self.live.contains_key(k));
self.live.clear();
}
}
#[derive(Debug, Clone)]
pub(crate) struct UnionFind {
parent: Vec<usize>,
rank: Vec<u8>,
}
impl UnionFind {
#[must_use]
pub(crate) fn new(n: usize) -> Self {
Self {
parent: (0..n).collect(),
rank: vec![0; n],
}
}
pub(crate) fn find(&mut self, i: usize) -> usize {
let mut root = i;
while self.parent[root] != root {
root = self.parent[root];
}
let mut cur = i;
while self.parent[cur] != root {
let next = self.parent[cur];
self.parent[cur] = root;
cur = next;
}
root
}
pub(crate) fn union(&mut self, i: usize, j: usize) -> bool {
let ri = self.find(i);
let rj = self.find(j);
if ri == rj {
return false;
}
if self.rank[ri] < self.rank[rj] {
self.parent[ri] = rj;
} else if self.rank[ri] > self.rank[rj] {
self.parent[rj] = ri;
} else {
self.parent[rj] = ri;
self.rank[ri] += 1;
}
true
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct Island {
pub(crate) bodies: Vec<usize>,
pub(crate) contacts: Vec<usize>,
pub(crate) joints: Vec<usize>,
}
pub(crate) fn build_islands<B: BodyLike, C: ContactLike, J: JointLike>(
bodies: &[B],
contacts: &[C],
joints: &[J],
) -> Result<Vec<Island>, PhysicsError> {
let n = bodies.len();
let mut uf = UnionFind::new(n);
for c in contacts {
let (a, b) = (c.body_a(), c.body_b());
if a >= n || b >= n {
return Err(PhysicsError::InvalidConstraint {
reason: "contact references body out of bounds",
});
}
if bodies[a].is_dynamic() && bodies[b].is_dynamic() {
uf.union(a, b);
}
}
for j in joints {
let (a, b) = (j.body_a(), j.body_b());
if a >= n || b >= n {
return Err(PhysicsError::InvalidConstraint {
reason: "joint references body out of bounds",
});
}
if bodies[a].is_dynamic() && bodies[b].is_dynamic() {
uf.union(a, b);
}
}
let mut buckets: HashMap<usize, Island> = HashMap::new();
for (i, body) in bodies.iter().enumerate() {
if body.is_dynamic() {
let root = uf.find(i);
buckets.entry(root).or_default().bodies.push(i);
}
}
for (idx, c) in contacts.iter().enumerate() {
let (a, b) = (c.body_a(), c.body_b());
let root = if bodies[a].is_dynamic() {
uf.find(a)
} else if bodies[b].is_dynamic() {
uf.find(b)
} else {
continue; };
let island = buckets.entry(root).or_default();
island.contacts.push(idx);
if !bodies[a].is_dynamic() && !island.bodies.contains(&a) {
island.bodies.push(a);
}
if !bodies[b].is_dynamic() && !island.bodies.contains(&b) {
island.bodies.push(b);
}
}
for (idx, j) in joints.iter().enumerate() {
let (a, b) = (j.body_a(), j.body_b());
let root = if bodies[a].is_dynamic() {
uf.find(a)
} else if bodies[b].is_dynamic() {
uf.find(b)
} else {
continue;
};
let island = buckets.entry(root).or_default();
island.joints.push(idx);
if !bodies[a].is_dynamic() && !island.bodies.contains(&a) {
island.bodies.push(a);
}
if !bodies[b].is_dynamic() && !island.bodies.contains(&b) {
island.bodies.push(b);
}
}
let mut islands: Vec<Island> = buckets.into_values().collect();
for island in &mut islands {
island.bodies.sort_unstable();
island.contacts.sort_unstable();
island.joints.sort_unstable();
}
islands.sort_by(|x, y| {
x.bodies
.cmp(&y.bodies)
.then_with(|| x.contacts.cmp(&y.contacts))
.then_with(|| x.joints.cmp(&y.joints))
});
Ok(islands)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TgsConfig {
pub(crate) substeps: u32,
pub(crate) velocity_iters: u32,
pub(crate) position_iters: u32,
pub(crate) warmstart: bool,
}
impl Default for TgsConfig {
fn default() -> Self {
Self {
substeps: 4,
velocity_iters: 4,
position_iters: 2,
warmstart: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AdaptiveSubStepConfig {
pub(crate) max_translation_per_step: Fix128,
pub(crate) max_substeps: u32,
pub(crate) min_substeps: u32,
}
impl Default for AdaptiveSubStepConfig {
fn default() -> Self {
Self {
max_translation_per_step: Fix128::from_ratio(1, 10), max_substeps: 16,
min_substeps: 1,
}
}
}
pub(crate) trait HasVelocity {
fn velocity_l_inf(&self) -> Fix128;
}
#[must_use]
pub(crate) fn adaptive_substeps_for<B: HasVelocity>(
bodies: &[B],
dt: Fix128,
cfg: &AdaptiveSubStepConfig,
) -> u32 {
let mut v_max = Fix128::ZERO;
for b in bodies {
let v = b.velocity_l_inf();
if v > v_max {
v_max = v;
}
}
let travel = v_max * dt;
let min = cfg.min_substeps.max(1);
let max = cfg.max_substeps.max(min);
if travel <= cfg.max_translation_per_step {
return min;
}
let mut n: u32 = min;
while n < max && Fix128::from_int(i64::from(n)) * cfg.max_translation_per_step < travel {
n += 1;
}
n
}
#[must_use]
pub(crate) fn adaptive_substeps_for_ccd<B: HasVelocity>(
bodies: &[B],
dt: Fix128,
smallest_collider_radius: Fix128,
ccd_safety_factor: Fix128,
cfg: &AdaptiveSubStepConfig,
) -> u32 {
let ccd_step = smallest_collider_radius * ccd_safety_factor;
let effective_step = if ccd_step < cfg.max_translation_per_step {
ccd_step
} else {
cfg.max_translation_per_step
};
let effective_cfg = AdaptiveSubStepConfig {
max_translation_per_step: effective_step,
max_substeps: cfg.max_substeps,
min_substeps: cfg.min_substeps,
};
adaptive_substeps_for(bodies, dt, &effective_cfg)
}
#[cfg(test)]
mod adaptive_substeps_tests {
use super::*;
struct DummyBody(Fix128);
impl HasVelocity for DummyBody {
fn velocity_l_inf(&self) -> Fix128 {
self.0
}
}
fn cfg() -> AdaptiveSubStepConfig {
AdaptiveSubStepConfig {
max_translation_per_step: Fix128::from_ratio(1, 10),
max_substeps: 8,
min_substeps: 1,
}
}
#[test]
fn zero_velocity_returns_min_substeps() {
let bodies = [DummyBody(Fix128::ZERO), DummyBody(Fix128::ZERO)];
let dt = Fix128::from_ratio(1, 60);
assert_eq!(adaptive_substeps_for(&bodies, dt, &cfg()), 1);
}
#[test]
fn empty_body_slice_returns_min_substeps() {
let bodies: [DummyBody; 0] = [];
let dt = Fix128::from_ratio(1, 60);
assert_eq!(adaptive_substeps_for(&bodies, dt, &cfg()), 1);
}
#[test]
fn slow_body_below_threshold_returns_min_substeps() {
let bodies = [DummyBody(Fix128::from_int(1))];
let dt = Fix128::from_ratio(1, 60);
assert_eq!(adaptive_substeps_for(&bodies, dt, &cfg()), 1);
}
#[test]
fn fast_body_scales_substeps_up() {
let bodies = [DummyBody(Fix128::from_int(60))];
let dt = Fix128::from_ratio(1, 60);
let cfg = cfg();
let n = adaptive_substeps_for(&bodies, dt, &cfg);
assert!(
n >= 2 && n <= cfg.max_substeps,
"expected non-trivial substep count within cap, got {n}"
);
}
#[test]
fn monotonic_in_max_velocity() {
let slow = [DummyBody(Fix128::from_int(6))];
let fast = [DummyBody(Fix128::from_int(60))];
let dt = Fix128::from_ratio(1, 60);
let c = cfg();
let a = adaptive_substeps_for(&slow, dt, &c);
let b = adaptive_substeps_for(&fast, dt, &c);
assert!(
b >= a,
"faster body must not require fewer substeps (a={a}, b={b})"
);
}
#[test]
fn ccd_variant_dominates_vanilla_for_small_radius() {
let bodies = [DummyBody(Fix128::from_int(60))];
let dt = Fix128::from_ratio(1, 60);
let base = cfg();
let vanilla = adaptive_substeps_for(&bodies, dt, &base);
let ccd = adaptive_substeps_for_ccd(
&bodies,
dt,
Fix128::from_ratio(1, 100), Fix128::from_ratio(1, 2), &base,
);
assert!(
ccd >= vanilla,
"CCD variant with tighter cap must not return fewer substeps (vanilla={vanilla}, ccd={ccd})"
);
}
#[test]
fn ccd_variant_matches_vanilla_for_large_radius() {
let bodies = [DummyBody(Fix128::from_int(60))];
let dt = Fix128::from_ratio(1, 60);
let base = cfg();
let vanilla = adaptive_substeps_for(&bodies, dt, &base);
let ccd = adaptive_substeps_for_ccd(
&bodies,
dt,
Fix128::from_int(100), Fix128::from_ratio(1, 2), &base,
);
assert_eq!(
vanilla, ccd,
"large collider must not tighten the vanilla adaptive cap"
);
}
#[test]
fn ccd_variant_zero_velocity_returns_min() {
let bodies = [DummyBody(Fix128::ZERO), DummyBody(Fix128::ZERO)];
let dt = Fix128::from_ratio(1, 60);
let base = cfg();
let n = adaptive_substeps_for_ccd(
&bodies,
dt,
Fix128::from_ratio(1, 1000),
Fix128::from_ratio(1, 2),
&base,
);
assert_eq!(n, base.min_substeps.max(1));
}
}
pub(crate) trait TgsHooks {
fn begin_substep(&mut self, sub_dt: Fix128);
fn velocity_iteration(&mut self, sub_dt: Fix128);
fn position_iteration(&mut self, sub_dt: Fix128);
fn end_substep(&mut self, sub_dt: Fix128);
}
pub(crate) fn tgs_step<H: TgsHooks>(hooks: &mut H, cfg: &TgsConfig, dt: Fix128) {
if cfg.substeps == 0 {
return;
}
let inv = Fix128::from_f32(1.0 / cfg.substeps as f32);
let sub_dt = dt * inv;
for _ in 0..cfg.substeps {
hooks.begin_substep(sub_dt);
for _ in 0..cfg.velocity_iters {
hooks.velocity_iteration(sub_dt);
}
for _ in 0..cfg.position_iters {
hooks.position_iteration(sub_dt);
}
hooks.end_substep(sub_dt);
}
}
use crate::solver::DistanceConstraint;
pub(crate) struct DistanceRef<'a> {
pub(crate) joint: &'a DistanceConstraint,
}
impl JointLike for DistanceRef<'_> {
fn body_a(&self) -> usize {
self.joint.body_a
}
fn body_b(&self) -> usize {
self.joint.body_b
}
}
pub(crate) fn dispatch_islands<F>(islands: &[Island], mut f: F)
where
F: FnMut(&Island),
{
for island in islands {
f(island);
}
}
#[cfg(feature = "parallel")]
pub(crate) fn par_dispatch_islands<T, F, R>(islands: &[Island], aux: &mut [T], f: F) -> Vec<R>
where
F: Fn(&Island, &mut T) -> R + Send + Sync,
T: Send,
R: Send,
{
assert_eq!(
islands.len(),
aux.len(),
"one aux slot per island is required"
);
use rayon::prelude::*;
islands
.par_iter()
.zip(aux.par_iter_mut())
.map(|(isl, a)| f(isl, a))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
struct MockBody {
id: u64,
dynamic: bool,
}
impl BodyLike for MockBody {
fn stable_id(&self) -> u64 {
self.id
}
fn is_dynamic(&self) -> bool {
self.dynamic
}
}
struct MockContact {
id: u64,
a: usize,
b: usize,
}
impl ContactLike for MockContact {
fn body_a(&self) -> usize {
self.a
}
fn body_b(&self) -> usize {
self.b
}
fn stable_id(&self) -> u64 {
self.id
}
}
struct MockJoint {
a: usize,
b: usize,
}
impl JointLike for MockJoint {
fn body_a(&self) -> usize {
self.a
}
fn body_b(&self) -> usize {
self.b
}
}
#[test]
fn impulse_cache_returns_zero_for_new_contact() {
let mut cache = ImpulseCache::new();
let got = cache.take(42);
assert_eq!(got, CachedImpulse::default());
}
#[test]
fn impulse_cache_sweep_degenerate_empty_and_nothing_stale() {
let mut empty = ImpulseCache::new();
empty.sweep();
assert!(
empty.entries.is_empty(),
"sweeping an empty cache stays empty"
);
let mut all_live = ImpulseCache::new();
let imp = CachedImpulse {
normal: Fix128::from_int(2),
tangent1: Fix128::ZERO,
tangent2: Fix128::ZERO,
};
all_live.set(1, imp);
all_live.set(2, imp);
let _ = all_live.take(1);
let _ = all_live.take(2);
all_live.sweep();
assert_eq!(
all_live.entries.len(),
2,
"nothing stale: both entries survive"
);
assert_eq!(all_live.stored(1), imp);
assert_eq!(all_live.stored(2), imp);
}
#[test]
fn impulse_cache_round_trips_stored_impulse() {
let mut cache = ImpulseCache::new();
let imp = CachedImpulse {
normal: Fix128::from_f32(1.5),
tangent1: Fix128::from_f32(-0.25),
tangent2: Fix128::from_f32(0.0),
};
cache.set(7, imp);
assert_eq!(cache.take(7), imp);
assert_eq!(cache.entries.len(), 1);
}
#[test]
fn impulse_cache_sweep_drops_untouched_entries() {
let mut cache = ImpulseCache::new();
cache.set(1, CachedImpulse::default());
cache.set(2, CachedImpulse::default());
let _ = cache.take(1);
cache.sweep();
assert_eq!(cache.entries.len(), 1);
assert!(cache.entries_contains(1));
assert!(!cache.entries_contains(2));
}
impl ImpulseCache {
fn entries_contains(&self, id: u64) -> bool {
self.entries.contains_key(&id)
}
fn stored(&self, id: u64) -> CachedImpulse {
self.entries.get(&id).copied().unwrap_or_default()
}
}
#[test]
fn union_find_singletons_are_distinct() {
let mut uf = UnionFind::new(5);
for i in 0..5 {
assert_eq!(uf.find(i), i);
}
}
#[test]
fn union_find_merges_and_deduplicates() {
let mut uf = UnionFind::new(4);
assert!(uf.union(0, 1));
assert!(uf.union(1, 2));
assert!(!uf.union(0, 2), "already in same set");
assert_eq!(uf.find(0), uf.find(2));
assert_ne!(uf.find(0), uf.find(3));
}
#[test]
fn islands_empty_world_returns_no_islands() {
let bodies: [MockBody; 0] = [];
let contacts: [MockContact; 0] = [];
let joints: [MockJoint; 0] = [];
assert!(build_islands(&bodies, &contacts, &joints)
.expect("valid island inputs")
.is_empty());
}
#[test]
fn islands_out_of_bounds_contact_is_an_error_not_a_panic() {
let bodies = [MockBody {
id: 0,
dynamic: true,
}];
let contacts = [MockContact { id: 7, a: 0, b: 1 }];
let joints: [MockJoint; 0] = [];
let err = build_islands(&bodies, &contacts, &joints).unwrap_err();
assert_eq!(
err,
PhysicsError::InvalidConstraint {
reason: "contact references body out of bounds"
}
);
}
#[test]
fn islands_out_of_bounds_joint_is_an_error_not_a_panic() {
let bodies = [MockBody {
id: 0,
dynamic: true,
}];
let contacts: [MockContact; 0] = [];
let joints = [MockJoint { a: 5, b: 0 }];
let err = build_islands(&bodies, &contacts, &joints).unwrap_err();
assert_eq!(
err,
PhysicsError::InvalidConstraint {
reason: "joint references body out of bounds"
}
);
}
#[test]
fn islands_two_disjoint_pairs_form_two_groups() {
let bodies = [
MockBody {
id: 0,
dynamic: true,
},
MockBody {
id: 1,
dynamic: true,
},
MockBody {
id: 2,
dynamic: true,
},
MockBody {
id: 3,
dynamic: true,
},
];
let contacts = [
MockContact {
id: 100,
a: 0,
b: 1,
},
MockContact {
id: 101,
a: 2,
b: 3,
},
];
let islands =
build_islands(&bodies, &contacts, &[] as &[MockJoint]).expect("valid island inputs");
assert_eq!(islands.len(), 2);
assert_eq!(islands[0].bodies, vec![0, 1]);
assert_eq!(islands[1].bodies, vec![2, 3]);
}
#[test]
fn islands_joint_bridges_two_pairs() {
let bodies = [
MockBody {
id: 0,
dynamic: true,
},
MockBody {
id: 1,
dynamic: true,
},
MockBody {
id: 2,
dynamic: true,
},
];
let contacts = [MockContact {
id: 100,
a: 0,
b: 1,
}];
let joints = [MockJoint { a: 1, b: 2 }];
let islands = build_islands(&bodies, &contacts, &joints).expect("valid island inputs");
assert_eq!(islands.len(), 1);
assert_eq!(islands[0].bodies, vec![0, 1, 2]);
assert_eq!(islands[0].contacts, vec![0]);
assert_eq!(islands[0].joints, vec![0]);
}
#[test]
fn islands_static_body_does_not_fuse_two_stacks() {
let bodies = [
MockBody {
id: 0,
dynamic: false,
}, MockBody {
id: 1,
dynamic: true,
},
MockBody {
id: 2,
dynamic: true,
},
MockBody {
id: 3,
dynamic: true,
},
MockBody {
id: 4,
dynamic: true,
},
];
let contacts = [
MockContact {
id: 100,
a: 0,
b: 1,
}, MockContact {
id: 101,
a: 1,
b: 2,
}, MockContact {
id: 200,
a: 0,
b: 3,
}, MockContact {
id: 201,
a: 3,
b: 4,
}, ];
let islands =
build_islands(&bodies, &contacts, &[] as &[MockJoint]).expect("valid island inputs");
assert_eq!(islands.len(), 2, "static ground must not fuse islands");
assert!(islands[0].bodies.contains(&0));
assert!(islands[1].bodies.contains(&0));
}
#[test]
fn islands_are_deterministic_across_runs() {
let bodies = [
MockBody {
id: 0,
dynamic: true,
},
MockBody {
id: 1,
dynamic: true,
},
MockBody {
id: 2,
dynamic: true,
},
];
let contacts = [
MockContact {
id: 100,
a: 2,
b: 0,
},
MockContact {
id: 101,
a: 1,
b: 2,
},
];
let a =
build_islands(&bodies, &contacts, &[] as &[MockJoint]).expect("valid island inputs");
let b =
build_islands(&bodies, &contacts, &[] as &[MockJoint]).expect("valid island inputs");
assert_eq!(a, b);
}
#[test]
fn tgs_config_defaults_are_reasonable() {
let cfg = TgsConfig::default();
assert!(cfg.substeps >= 1);
assert!(cfg.velocity_iters >= 1);
assert!(cfg.position_iters >= 1);
assert!(cfg.warmstart);
}
struct HookCounter {
begin: u32,
vel: u32,
pos: u32,
end: u32,
}
impl TgsHooks for HookCounter {
fn begin_substep(&mut self, _sub_dt: Fix128) {
self.begin += 1;
}
fn velocity_iteration(&mut self, _sub_dt: Fix128) {
self.vel += 1;
}
fn position_iteration(&mut self, _sub_dt: Fix128) {
self.pos += 1;
}
fn end_substep(&mut self, _sub_dt: Fix128) {
self.end += 1;
}
}
#[test]
fn dispatch_islands_visits_every_island_once() {
let islands = vec![
Island {
bodies: vec![0, 1],
contacts: vec![0],
joints: vec![],
},
Island {
bodies: vec![2, 3],
contacts: vec![1],
joints: vec![],
},
];
let mut visited = Vec::new();
dispatch_islands(&islands, |isl| {
visited.push(isl.bodies[0]);
});
assert_eq!(visited, vec![0, 2]);
}
#[test]
fn dispatch_islands_degenerate_empty_and_single() {
let none: [Island; 0] = [];
let mut calls = 0u32;
dispatch_islands(&none, |_| calls += 1);
assert_eq!(calls, 0, "empty island list dispatches zero times");
let one = [Island {
bodies: vec![5],
contacts: vec![],
joints: vec![],
}];
let mut seen = Vec::new();
dispatch_islands(&one, |isl| seen.push(isl.bodies.clone()));
assert_eq!(seen, vec![vec![5]], "single island dispatches exactly once");
}
#[cfg(feature = "parallel")]
#[test]
fn par_dispatch_islands_visits_every_island() {
let islands: Vec<Island> = (0..16)
.map(|i| Island {
bodies: vec![i, i + 100],
contacts: vec![],
joints: vec![],
})
.collect();
let mut aux = vec![0u32; islands.len()];
let results: Vec<usize> = par_dispatch_islands(&islands, &mut aux, |isl, slot| {
*slot += 1;
isl.bodies[0]
});
assert_eq!(results, (0..16).collect::<Vec<_>>());
assert!(
aux.iter().all(|&v| v == 1),
"each aux slot touched exactly once"
);
}
#[cfg(feature = "parallel")]
#[test]
#[should_panic(expected = "one aux slot per island is required")]
fn par_dispatch_islands_panics_on_aux_length_mismatch() {
let islands = vec![Island::default(), Island::default()];
let mut aux = vec![0u32; 1];
let _: Vec<usize> = par_dispatch_islands(&islands, &mut aux, |_, _| 0);
}
#[test]
fn tgs_step_visits_hooks_the_expected_number_of_times() {
let cfg = TgsConfig {
substeps: 3,
velocity_iters: 2,
position_iters: 1,
warmstart: false,
};
let mut h = HookCounter {
begin: 0,
vel: 0,
pos: 0,
end: 0,
};
tgs_step(&mut h, &cfg, Fix128::from_f32(1.0 / 60.0));
assert_eq!(h.begin, 3);
assert_eq!(h.vel, 6); assert_eq!(h.pos, 3); assert_eq!(h.end, 3);
}
#[test]
fn union_find_union_reports_merge_and_uses_rank() {
let mut uf = UnionFind::new(6);
assert!(uf.union(0, 1), "初回 merge は true");
assert!(!uf.union(0, 1), "同一集合は false");
assert!(!uf.union(1, 0));
assert!(uf.union(2, 3));
assert!(uf.union(0, 2), "集合同士の merge");
assert_eq!(uf.find(3), uf.find(1));
assert_ne!(uf.find(4), uf.find(0));
assert!(uf.union(4, 5));
assert!(uf.union(5, 3));
let root = uf.find(0);
for i in 0..6 {
assert_eq!(uf.find(i), root, "{i}");
}
assert_eq!(uf.parent[5], root);
let mut r = UnionFind::new(3);
r.union(0, 1);
let root01 = r.find(0);
r.union(2, 0);
assert_eq!(r.find(2), root01, "rank の高い側が root のまま");
assert_eq!(r.rank[root01], 1);
let mut q = UnionFind::new(4);
q.union(0, 1);
q.union(2, 3);
q.union(0, 2);
let qr = q.find(0);
assert_eq!(q.rank[qr], 2);
}
#[test]
fn impulse_cache_counts_hits_and_misses_and_reports_hit_rate() {
let mut c = ImpulseCache::new();
assert!(c.entries.is_empty());
assert_eq!(c.take(7), CachedImpulse::default());
let s = c.stats();
assert_eq!((s.hits, s.misses), (0, 1));
assert!((s.hit_rate() - 0.0).abs() < 1e-12);
let imp = CachedImpulse {
normal: Fix128::from_int(3),
tangent1: Fix128::from_int(-1),
tangent2: Fix128::from_ratio(1, 2),
};
c.set(7, imp);
assert_eq!(c.entries.len(), 1);
assert_eq!(c.take(7), imp);
assert_eq!(c.take(7), imp);
assert_eq!(c.take(9), CachedImpulse::default());
let s = c.stats();
assert_eq!((s.hits, s.misses), (2, 2));
assert!((s.hit_rate() - 0.5).abs() < 1e-12);
c.reset_stats();
let s = c.stats();
assert_eq!((s.hits, s.misses), (0, 0));
assert_eq!(s.hit_rate(), 0.0, "total 0 は 0.0");
let st = ImpulseCacheStats { hits: 3, misses: 1 };
assert!((st.hit_rate() - 0.75).abs() < 1e-12);
c.set(100, imp);
c.set(200, imp);
let _ = c.take(100);
c.sweep();
assert_eq!(c.stored(100), imp);
assert_eq!(c.stored(200), CachedImpulse::default());
assert_eq!(
c.stored(7),
imp,
"7 is live in this tick (taken before sweep)"
);
c.sweep();
assert!(c.entries.is_empty());
}
#[test]
fn islands_attach_static_neighbours_and_sort_canonically() {
let bodies = [
MockBody {
id: 10,
dynamic: true,
},
MockBody {
id: 11,
dynamic: true,
},
MockBody {
id: 12,
dynamic: false,
},
MockBody {
id: 13,
dynamic: true,
},
MockBody {
id: 14,
dynamic: true,
},
];
let contacts = [
MockContact { id: 1, a: 1, b: 0 },
MockContact { id: 2, a: 4, b: 2 },
MockContact { id: 3, a: 2, b: 2 },
];
let joints: [MockJoint; 0] = [];
let islands = build_islands(&bodies, &contacts, &joints).expect("valid");
assert_eq!(islands.len(), 3);
assert_eq!(islands[0].bodies, vec![0, 1]);
assert_eq!(islands[0].contacts, vec![0]);
assert_eq!(
islands[1].bodies,
vec![2, 4],
"static 2 attached to island of 4, sorted"
);
assert_eq!(islands[1].contacts, vec![1]);
assert_eq!(islands[2].bodies, vec![3]);
assert!(islands[2].contacts.is_empty());
assert!(islands.iter().all(|i| !i.contacts.contains(&2)));
let joints2 = [MockJoint { a: 3, b: 1 }, MockJoint { a: 2, b: 3 }];
let with_joints = build_islands(&bodies, &contacts, &joints2).expect("valid");
assert_eq!(with_joints.len(), 2);
assert_eq!(with_joints[0].bodies, vec![0, 1, 2, 3]);
assert_eq!(with_joints[0].joints, vec![0, 1]);
assert_eq!(
with_joints[1].bodies,
vec![2, 4],
"static body is attached to both islands"
);
}
#[test]
fn adaptive_substeps_clamp_and_repeated_addition_search() {
struct V(Fix128);
impl HasVelocity for V {
fn velocity_l_inf(&self) -> Fix128 {
self.0
}
}
let cfg = AdaptiveSubStepConfig {
max_translation_per_step: Fix128::from_ratio(1, 4),
max_substeps: 6,
min_substeps: 2,
};
let dt = Fix128::ONE;
assert_eq!(
adaptive_substeps_for(&[V(Fix128::from_ratio(1, 4))], dt, &cfg),
2
);
assert_eq!(adaptive_substeps_for(&[V(Fix128::ONE)], dt, &cfg), 4);
assert_eq!(
adaptive_substeps_for(&[V(Fix128::from_ratio(11, 10))], dt, &cfg),
5
);
assert_eq!(
adaptive_substeps_for(&[V(Fix128::from_int(100))], dt, &cfg),
6
);
assert_eq!(
adaptive_substeps_for(
&[V(Fix128::ZERO), V(Fix128::ONE), V(Fix128::from_ratio(1, 2))],
dt,
&cfg
),
4
);
assert_eq!(
adaptive_substeps_for(&[V(Fix128::ONE), V(Fix128::ZERO)], dt, &cfg),
4
);
assert_eq!(
adaptive_substeps_for(&[V(Fix128::from_int(2))], Fix128::from_ratio(1, 2), &cfg),
4
);
let odd = AdaptiveSubStepConfig {
max_translation_per_step: Fix128::ONE,
max_substeps: 1,
min_substeps: 0,
};
assert_eq!(
adaptive_substeps_for(&[V(Fix128::from_int(50))], dt, &odd),
1
);
let inv = AdaptiveSubStepConfig {
max_translation_per_step: Fix128::ONE,
max_substeps: 2,
min_substeps: 5,
};
assert_eq!(
adaptive_substeps_for(&[V(Fix128::from_int(50))], dt, &inv),
5
);
assert_eq!(
adaptive_substeps_for_ccd(
&[V(Fix128::ONE)],
dt,
Fix128::from_ratio(1, 8),
Fix128::ONE,
&cfg
),
6
);
assert_eq!(
adaptive_substeps_for_ccd(&[V(Fix128::ONE)], dt, Fix128::ONE, Fix128::ONE, &cfg),
4
);
assert_eq!(
adaptive_substeps_for_ccd(
&[V(Fix128::from_ratio(1, 2))],
dt,
Fix128::ONE,
Fix128::from_ratio(1, 8),
&cfg
),
4
);
}
#[test]
fn union_find_higher_rank_root_absorbs_lower_without_rank_growth() {
let mut uf = UnionFind::new(4);
uf.union(0, 1); let root = uf.find(0);
assert!(uf.union(0, 3), "rank 1 側 (ri) が rank 0 (rj) を吸収");
assert_eq!(uf.find(3), root);
assert_eq!(uf.rank[root], 1, "rank は増えない");
assert!(uf.union(2, 1), "逆向き (ri rank 0、rj rank 1)");
assert_eq!(uf.find(2), root);
assert_eq!(uf.rank[root], 1);
}
#[test]
fn islands_static_body_touching_two_stacks_keeps_them_separate() {
let bodies = [
MockBody {
id: 1,
dynamic: true,
},
MockBody {
id: 2,
dynamic: false,
},
MockBody {
id: 3,
dynamic: true,
},
];
let contacts = [
MockContact { id: 1, a: 0, b: 1 },
MockContact { id: 2, a: 1, b: 2 },
];
let joints: [MockJoint; 0] = [];
let islands = build_islands(&bodies, &contacts, &joints).expect("valid");
assert_eq!(islands.len(), 2);
assert_eq!(islands[0].bodies, vec![0, 1]);
assert_eq!(islands[0].contacts, vec![0]);
assert_eq!(islands[1].bodies, vec![1, 2]);
assert_eq!(islands[1].contacts, vec![1]);
let j2 = [MockJoint { a: 0, b: 1 }, MockJoint { a: 2, b: 1 }];
let noc: [MockContact; 0] = [];
let via_joint = build_islands(&bodies, &noc, &j2).expect("valid");
assert_eq!(via_joint.len(), 2);
assert_eq!(via_joint[0].joints, vec![0]);
assert_eq!(via_joint[1].joints, vec![1]);
}
#[test]
fn union_find_deeper_left_tree_absorbs_without_rank_growth() {
let mut uf = UnionFind::new(5);
assert!(uf.union(0, 1)); assert_eq!(uf.find(0), 0);
assert_eq!(uf.rank[0], 1);
assert!(uf.union(0, 2));
assert_eq!(uf.parent[2], 0, "浅い木 2 が深い木 0 の直下");
assert_eq!(uf.rank[0], 1, "rank 不変");
assert_eq!(uf.rank[2], 0);
assert!(uf.union(3, 0));
assert_eq!(uf.parent[3], 0);
assert_eq!(uf.rank[0], 1);
let mut q = UnionFind::new(4);
q.union(0, 1);
q.union(2, 3);
assert!(q.union(1, 3));
let qr = q.find(0);
assert_eq!(q.rank[qr], 2);
}
#[test]
fn islands_static_body_listed_once_across_multiple_contacts() {
let bodies = [
MockBody {
id: 0,
dynamic: false,
},
MockBody {
id: 1,
dynamic: true,
},
MockBody {
id: 2,
dynamic: true,
},
];
let joints: [MockJoint; 0] = [];
let as_a = [
MockContact { id: 1, a: 0, b: 1 },
MockContact { id: 2, a: 0, b: 2 },
MockContact { id: 3, a: 1, b: 2 },
];
let islands = build_islands(&bodies, &as_a, &joints).expect("valid");
assert_eq!(islands.len(), 1);
assert_eq!(islands[0].bodies, vec![0, 1, 2]);
assert_eq!(islands[0].contacts, vec![0, 1, 2]);
let as_b = [
MockContact { id: 1, a: 1, b: 0 },
MockContact { id: 2, a: 2, b: 0 },
MockContact { id: 3, a: 1, b: 2 },
];
let islands = build_islands(&bodies, &as_b, &joints).expect("valid");
assert_eq!(islands.len(), 1);
assert_eq!(islands[0].bodies, vec![0, 1, 2]);
assert_eq!(islands[0].contacts, vec![0, 1, 2]);
}
#[test]
fn islands_sharing_a_static_first_body_are_ordered_lexicographically() {
let mut bodies = vec![MockBody {
id: 0,
dynamic: false,
}];
for id in 1..=8u64 {
bodies.push(MockBody { id, dynamic: true });
}
let joints: [MockJoint; 0] = [];
let contacts = [
MockContact { id: 1, a: 0, b: 8 },
MockContact { id: 2, a: 3, b: 0 },
MockContact { id: 3, a: 0, b: 5 },
];
for _ in 0..16 {
let islands = build_islands(&bodies, &contacts, &joints).expect("valid");
let order: Vec<Vec<usize>> = islands.iter().map(|i| i.bodies.clone()).collect();
assert_eq!(
order,
vec![
vec![0, 3],
vec![0, 5],
vec![0, 8],
vec![1],
vec![2],
vec![4],
vec![6],
vec![7]
]
);
assert_eq!(islands[0].contacts, vec![1]);
assert_eq!(islands[1].contacts, vec![2]);
assert_eq!(islands[2].contacts, vec![0]);
}
}
#[test]
fn islands_static_body_attached_once_via_joints() {
let bodies = [
MockBody {
id: 0,
dynamic: false,
},
MockBody {
id: 1,
dynamic: true,
},
MockBody {
id: 2,
dynamic: true,
},
];
let contacts: [MockContact; 0] = [];
let as_a = [
MockJoint { a: 0, b: 1 },
MockJoint { a: 0, b: 2 },
MockJoint { a: 1, b: 2 },
];
let islands = build_islands(&bodies, &contacts, &as_a).expect("valid");
assert_eq!(islands.len(), 1);
assert_eq!(islands[0].bodies, vec![0, 1, 2]);
assert_eq!(islands[0].joints, vec![0, 1, 2]);
let as_b = [
MockJoint { a: 1, b: 0 },
MockJoint { a: 2, b: 0 },
MockJoint { a: 1, b: 2 },
];
let islands = build_islands(&bodies, &contacts, &as_b).expect("valid");
assert_eq!(islands.len(), 1);
assert_eq!(islands[0].bodies, vec![0, 1, 2]);
assert_eq!(islands[0].joints, vec![0, 1, 2]);
let single = [MockJoint { a: 1, b: 0 }];
let islands = build_islands(&bodies, &contacts, &single).expect("valid");
assert_eq!(islands.len(), 2);
assert_eq!(islands[0].bodies, vec![0, 1]);
assert_eq!(islands[0].joints, vec![0]);
assert_eq!(islands[1].bodies, vec![2]);
assert!(islands[1].joints.is_empty());
let bridge = [MockJoint { a: 1, b: 0 }, MockJoint { a: 0, b: 2 }];
let mut islands = build_islands(&bodies, &contacts, &bridge).expect("valid");
islands.sort_by(|x, y| x.bodies.cmp(&y.bodies));
assert_eq!(islands.len(), 2);
assert_eq!(islands[0].bodies, vec![0, 1]);
assert_eq!(islands[0].joints, vec![0]);
assert_eq!(islands[1].bodies, vec![0, 2]);
assert_eq!(islands[1].joints, vec![1]);
}
#[test]
fn adapter_refs_forward_body_indices_and_ids() {
use crate::math::Vec3Fix;
let joint = DistanceConstraint::new(2, 3, Vec3Fix::ZERO, Vec3Fix::ZERO, Fix128::ONE);
let jref = DistanceRef { joint: &joint };
assert_eq!(JointLike::body_a(&jref), 2);
assert_eq!(JointLike::body_b(&jref), 3);
let joint2 = DistanceConstraint::new(6, 8, Vec3Fix::ZERO, Vec3Fix::ZERO, Fix128::ONE);
let jref2 = DistanceRef { joint: &joint2 };
assert_eq!(JointLike::body_a(&jref2), 6);
assert_eq!(JointLike::body_b(&jref2), 8);
let bodies: Vec<MockBody> = (0..4)
.map(|i| MockBody {
id: i,
dynamic: true,
})
.collect();
let contact = MockContact { id: 5, a: 2, b: 3 };
let islands = build_islands(&bodies, &[contact], &[jref]).expect("valid");
assert_eq!(islands.len(), 3);
assert_eq!(islands[2].bodies, vec![2, 3]);
assert_eq!(islands[2].contacts, vec![0]);
assert_eq!(islands[2].joints, vec![0]);
}
#[cfg(feature = "parallel")]
#[test]
fn par_dispatch_islands_runs_closure_for_each_island_exactly_once() {
let islands: Vec<Island> = (0..7)
.map(|i| Island {
bodies: vec![i * 2, i * 2 + 1],
contacts: vec![i],
joints: vec![],
})
.collect();
let mut aux = vec![0u32; islands.len()];
let results: Vec<usize> = par_dispatch_islands(&islands, &mut aux, |isl, slot| {
*slot += 1;
isl.contacts[0]
});
assert_eq!(
results,
(0..7).collect::<Vec<_>>(),
"order preserved, each island visited exactly once"
);
assert!(
aux.iter().all(|&v| v == 1),
"each aux slot mutated exactly once (no double-dispatch, no skip)"
);
let mut none_aux: Vec<u32> = Vec::new();
let none: Vec<usize> = par_dispatch_islands(&[], &mut none_aux, |_, slot| {
*slot += 1;
0
});
assert!(none.is_empty());
}
#[test]
fn adaptive_substep_default_gives_ceil_of_travel_over_tenth() {
let cfg = AdaptiveSubStepConfig::default();
assert_eq!((cfg.min_substeps, cfg.max_substeps), (1, 16));
assert!((cfg.max_translation_per_step.to_f64() - 0.1).abs() < 1e-12);
struct V(Fix128);
impl HasVelocity for V {
fn velocity_l_inf(&self) -> Fix128 {
self.0
}
}
let dt = Fix128::from_ratio(1, 60);
let n = |v: i64| adaptive_substeps_for(&[V(Fix128::from_int(v))], dt, &cfg);
assert_eq!(n(0), 1, "static scene");
assert_eq!(n(6), 1, "travel exactly 0.1");
assert_eq!(n(7), 2, "travel 0.1167 -> 2 sub-steps");
assert_eq!(n(60), 10, "travel 1.0 -> 10 sub-steps");
assert_eq!(n(96), 16, "travel 1.6 -> 16 (cap)");
assert_eq!(n(10_000), 16, "clamped at max_substeps");
}
}