#![allow(dead_code)]
#![allow(rustdoc::broken_intra_doc_links)]
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 {
entries: HashMap<u64, CachedImpulse>,
live: HashMap<u64, ()>,
hits: u64,
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;
}
#[must_use]
pub(crate) fn peek(&self, contact_id: u64) -> CachedImpulse {
self.entries.get(&contact_id).copied().unwrap_or_default()
}
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();
}
#[must_use]
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub(crate) fn clear(&mut self) {
self.entries.clear();
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
}
#[must_use]
pub(crate) fn len(&self) -> usize {
self.parent.len()
}
#[must_use]
pub(crate) fn is_empty(&self) -> bool {
self.parent.is_empty()
}
}
#[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>,
}
#[must_use]
pub(crate) fn build_islands<B: BodyLike, C: ContactLike, J: JointLike>(
bodies: &[B],
contacts: &[C],
joints: &[J],
) -> Vec<Island> {
let n = bodies.len();
let mut uf = UnionFind::new(n);
for c in contacts {
let (a, b) = (c.body_a(), c.body_b());
assert!(a < n && b < n, "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());
assert!(a < n && b < n, "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_key(|i| i.bodies.first().copied().unwrap_or(usize::MAX));
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) {
assert!(cfg.substeps > 0, "TgsConfig::substeps must be positive");
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::{BodyType, ContactConstraint, DistanceConstraint, RigidBody};
pub(crate) struct BodyRef<'a> {
pub(crate) body: &'a RigidBody,
pub(crate) id: u64,
}
impl BodyLike for BodyRef<'_> {
fn stable_id(&self) -> u64 {
self.id
}
fn is_dynamic(&self) -> bool {
matches!(self.body.body_type, BodyType::Dynamic)
}
}
pub(crate) struct ContactRef<'a> {
pub(crate) contact: &'a ContactConstraint,
pub(crate) id: u64,
}
impl ContactLike for ContactRef<'_> {
fn body_a(&self) -> usize {
self.contact.body_a
}
fn body_b(&self) -> usize {
self.contact.body_b
}
fn stable_id(&self) -> u64 {
self.id
}
}
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<F>(islands: &[Island], f: F)
where
F: Fn(&Island) + Send + Sync,
{
use rayon::prelude::*;
islands.par_iter().for_each(f);
}
#[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_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.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.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)
}
}
#[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).is_empty());
}
#[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]);
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);
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]);
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]);
let b = build_islands(&bodies, &contacts, &[] as &[MockJoint]);
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 body_ref_reports_dynamic_state() {
let mut body = RigidBody::default();
body.body_type = BodyType::Dynamic;
let dyn_ref = BodyRef {
body: &body,
id: 42,
};
assert!(dyn_ref.is_dynamic());
assert_eq!(dyn_ref.stable_id(), 42);
let mut sbody = RigidBody::default();
sbody.body_type = BodyType::Static;
let stat_ref = BodyRef {
body: &sbody,
id: 7,
};
assert!(!stat_ref.is_dynamic());
let mut kbody = RigidBody::default();
kbody.body_type = BodyType::Kinematic;
let kin_ref = BodyRef {
body: &kbody,
id: 3,
};
assert!(!kin_ref.is_dynamic());
}
#[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]);
}
#[cfg(feature = "parallel")]
#[test]
fn par_dispatch_islands_visits_every_island() {
use std::sync::atomic::{AtomicUsize, Ordering};
let islands: Vec<Island> = (0..16)
.map(|i| Island {
bodies: vec![i, i + 100],
contacts: vec![],
joints: vec![],
})
.collect();
let counter = AtomicUsize::new(0);
par_dispatch_islands(&islands, |_| {
counter.fetch_add(1, Ordering::Relaxed);
});
assert_eq!(counter.load(Ordering::Relaxed), 16);
}
#[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);
}
}