Skip to main content

rapier2d/geometry/narrow_phase/
mod.rs

1//! Narrow-phase collision detection: contact and intersection pair management
2//! between colliders whose broad-phase AABBs overlap, plus the persistent
3//! solver-facing bookkeeping maintained across steps.
4
5mod contacts;
6mod intersections;
7mod pair_management;
8mod pair_update;
9mod queries;
10pub(crate) mod soft_contacts;
11mod solver_graph;
12#[cfg(test)]
13#[cfg(feature = "f32")]
14#[cfg(feature = "dim3")]
15mod test;
16
17use crate::alloc_prelude::*;
18use crate::data::Coarena;
19use crate::dynamics::solver::solver_contact_graph::{
20    GENERIC_BUCKET, SolverContactGraph, bucket_id,
21};
22use crate::dynamics::{IslandManager, RigidBodySet};
23use crate::geometry::{
24    ColliderGraphIndex, ColliderHandle, ColliderSet, ContactData, ContactManifoldData, ContactPair,
25    InteractionGraph, IntersectionPair, SolverFlags,
26};
27use alloc::sync::Arc;
28use parry::query::{DefaultQueryDispatcher, PersistentQueryDispatcher};
29
30#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
31#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
32struct ColliderGraphIndices {
33    contact_graph_index: ColliderGraphIndex,
34    intersection_graph_index: ColliderGraphIndex,
35}
36
37impl ColliderGraphIndices {
38    fn invalid() -> Self {
39        Self {
40            contact_graph_index: InteractionGraph::<(), ()>::invalid_graph_index(),
41            intersection_graph_index: InteractionGraph::<(), ()>::invalid_graph_index(),
42        }
43    }
44}
45
46#[derive(Copy, Clone, PartialEq, Eq)]
47enum PairRemovalMode {
48    FromContactGraph,
49    FromIntersectionGraph,
50    Auto,
51}
52
53/// Strong-wakes whichever of the two bodies is a sleeping dynamic body.
54fn strong_wake_sleeping_side(
55    islands: &mut IslandManager,
56    bodies: &mut RigidBodySet,
57    h1: Option<crate::dynamics::RigidBodyHandle>,
58    h2: Option<crate::dynamics::RigidBodyHandle>,
59) {
60    for h in [h1, h2].into_iter().flatten() {
61        let sleeping_dyn = bodies
62            .get(h)
63            .is_some_and(|rb| rb.is_dynamic() && rb.activation.sleeping);
64        if sleeping_dyn {
65            islands.wake_up(bodies, h, true);
66        }
67    }
68}
69
70/// Packs a coloring body descriptor `(arena index, is_fixed)` into a `u32` for the
71/// deferred-coloring workspace list: `u32::MAX` = no body, else `(id << 1) | is_fixed`.
72fn pack_color_body_info(info: Option<(u32, bool)>) -> u32 {
73    match info {
74        None => u32::MAX,
75        Some((id, fixed)) => (id << 1) | fixed as u32,
76    }
77}
78
79/// Inverse of [`pack_color_body_info`].
80fn unpack_color_body_info(packed: u32) -> Option<(u32, bool)> {
81    if packed == u32::MAX {
82        None
83    } else {
84        Some((packed >> 1, packed & 1 != 0))
85    }
86}
87
88/// Assigns a persistent solver graph color to a newly-active pair: first color used by
89/// neither body. Dynamic/dynamic pairs search from color 0 up, pairs with a non-dynamic
90/// body from 127 down; when no color is free, the pair takes the sequential overflow color.
91fn assign_pair_solver_color(
92    masks: &mut Vec<u128>,
93    pair: &mut ContactPair,
94    body1: Option<(u32, bool)>, // (arena index, is_fixed)
95    body2: Option<(u32, bool)>,
96) {
97    use crate::geometry::contact_pair::{
98        SOLVER_COLOR_OVERFLOW, SOLVER_COLOR_UNCOLORED, SOLVER_DYNAMIC_COLOR_COUNT,
99    };
100
101    // A pair of two soft surfaces never reaches the rigid solver: no color.
102    let Some(pair) = pair.rigid_mut() else {
103        return;
104    };
105    if pair.solver_color != SOLVER_COLOR_UNCOLORED {
106        return;
107    }
108
109    let conflicting1 = body1.filter(|(_, fixed)| !fixed).map(|(id, _)| id);
110    let conflicting2 = body2.filter(|(_, fixed)| !fixed).map(|(id, _)| id);
111    let max_id = conflicting1.max(conflicting2).map(|id| id as usize);
112
113    if let Some(max_id) = max_id {
114        if masks.len() <= max_id {
115            masks.resize(max_id + 1, 0);
116        }
117    }
118
119    let (color, bodies) = match (conflicting1, conflicting2) {
120        (Some(i1), Some(i2)) => {
121            // Dynamic-vs-dynamic: pack from the low colors, but never into the top band reserved
122            // for dynamic-vs-fixed contacts (so those stay strictly last). If the low colors are
123            // exhausted the pair overflows (solved sequentially) rather than encroaching.
124            let mask = masks[i1 as usize] | masks[i2 as usize];
125            let dynamic_free = !mask & ((1u128 << SOLVER_DYNAMIC_COLOR_COUNT) - 1);
126            (dynamic_free.trailing_zeros(), [i1, i2])
127        }
128        (Some(i1), None) => {
129            let mask = masks[i1 as usize];
130            (127u32.wrapping_sub((!mask).leading_zeros()), [i1, u32::MAX])
131        }
132        (None, Some(i2)) => {
133            let mask = masks[i2 as usize];
134            (127u32.wrapping_sub((!mask).leading_zeros()), [i2, u32::MAX])
135        }
136        (None, None) => {
137            // No conflicting body: this pair never reaches the parallel solver.
138            pair.solver_color = SOLVER_COLOR_OVERFLOW;
139            pair.solver_color_bodies = [u32::MAX; 2];
140            return;
141        }
142    };
143
144    if color >= 128 {
145        // The color space of at least one body is saturated.
146        pair.solver_color = SOLVER_COLOR_OVERFLOW;
147        pair.solver_color_bodies = [u32::MAX; 2];
148        return;
149    }
150
151    for id in bodies {
152        if id != u32::MAX {
153            masks[id as usize] |= 1 << color;
154        }
155    }
156
157    pair.solver_color = color as u8;
158    pair.solver_color_bodies = bodies;
159}
160
161/// Releases the solver graph color held by a contact pair (no-op if it holds none).
162fn clear_pair_solver_color(masks: &mut [u128], pair: &mut ContactPair) {
163    use crate::geometry::contact_pair::{SOLVER_COLOR_OVERFLOW, SOLVER_COLOR_UNCOLORED};
164
165    let Some(pair) = pair.rigid_mut() else {
166        return;
167    };
168    if pair.solver_color < SOLVER_COLOR_OVERFLOW {
169        for id in pair.solver_color_bodies {
170            if id != u32::MAX {
171                if let Some(mask) = masks.get_mut(id as usize) {
172                    *mask &= !(1u128 << pair.solver_color);
173                }
174            }
175        }
176    }
177
178    pair.solver_color = SOLVER_COLOR_UNCOLORED;
179    pair.solver_color_bodies = [u32::MAX; 2];
180}
181
182/// Clears a filtered-out pair's contacts, reporting whether a solver manifold still held
183/// a live solver-graph entry: clearing destroys the `graph_pos` back-references the
184/// incremental maintenance needs, so `true` must force a full rebuild (`OUTCOME_CLEARED_IN_GRAPH`).
185fn clear_filtered_pair(pair: &mut ContactPair) -> bool {
186    let in_graph = pair
187        .solver_manifolds()
188        .iter()
189        .any(|m| m.data.graph_pos.is_some());
190    pair.clear();
191    in_graph
192}
193
194/// Bit of a pair's solver hint: at least one body is a dynamic *awake* body (no dynamic
195/// awake side means the pair never reaches the solver — the hint must predict solver
196/// qualification exactly). Repaired by the pair update when a sleeping side wakes.
197const PAIR_HINT_DYN_BIT: u16 = 1 << 15;
198/// Mask of a pair's solver hint holding its qualified solver-manifold count.
199const PAIR_HINT_COUNT_MASK: u16 = PAIR_HINT_DYN_BIT - 1;
200
201/// Whether a *single-manifold* pair's bucket membership drifted from its stored
202/// `graph_pos` — the event-driven dirty predicate (bucket entries move only on
203/// begin/end-touch). Only exact for single-manifold pairs; others always reconcile.
204fn single_manifold_bucket_drift(pair: &ContactPair, selectable: bool) -> bool {
205    use crate::geometry::contact_pair::{SOLVER_COLOR_OVERFLOW, SOLVER_COLOR_UNCOLORED};
206
207    let manifold = &pair.solver_manifolds()[0];
208    let qualifies = selectable
209        && manifold
210            .data
211            .solver_flags
212            .contains(SolverFlags::COMPUTE_RIGID_IMPULSES)
213        && manifold.data.num_active_contacts() != 0;
214    let pos = manifold.data.graph_pos;
215    if !qualifies {
216        return pos.is_some();
217    }
218    if !pos.is_some() {
219        return true;
220    }
221    // Generic (multibody) membership only changes with the multibody topology
222    // epoch, which forces a full rebuild — count/color drift doesn't move it.
223    if pos.bucket() == GENERIC_BUCKET {
224        return false;
225    }
226    let mut color = pair.solver_color();
227    if color == SOLVER_COLOR_UNCOLORED {
228        color = SOLVER_COLOR_OVERFLOW;
229    }
230    pos.bucket() != bucket_id(color)
231}
232
233/// The number of this pair's solver manifolds that the constraint solver must see
234/// (impulses to compute and at least one active contact).
235fn pair_qualified_manifold_count(pair: &ContactPair) -> u16 {
236    let mut count: u16 = 0;
237    for manifold in pair.solver_manifolds() {
238        if manifold
239            .data
240            .solver_flags
241            .contains(SolverFlags::COMPUTE_RIGID_IMPULSES)
242            && manifold.data.num_active_contacts() != 0
243        {
244            count = count.saturating_add(1);
245        }
246    }
247    count.min(PAIR_HINT_COUNT_MASK)
248}
249
250/// Collects into `candidates` the sorted, deduplicated indices of the graph edges adjacent
251/// to a collider needing a narrow-phase update. Seeding from `modified_colliders` + active
252/// bodies' colliders is exhaustive (pipeline-moved colliders leave the set; body is active).
253fn collect_pairs_to_update<E>(
254    candidates: &mut Vec<u32>,
255    graph_indices: &Coarena<ColliderGraphIndices>,
256    graph: &crate::data::graph::Graph<ColliderHandle, E>,
257    islands: &IslandManager,
258    bodies: &RigidBodySet,
259    colliders: &ColliderSet,
260    modified_colliders: &[ColliderHandle],
261    select_graph_id: impl Fn(&ColliderGraphIndices) -> ColliderGraphIndex,
262) {
263    candidates.clear();
264
265    if graph.edges.is_empty() {
266        return;
267    }
268
269    // When most bodies are awake, walking the graph adjacency (pointer-chasing) and
270    // sorting costs more than the linear edge scan it replaces: visit every edge and
271    // let the per-edge change-flags check skip the few unchanged ones.
272    let num_active = islands.active_bodies().count();
273    if num_active * 2 >= bodies.len() {
274        candidates.extend(0..graph.edges.len() as u32);
275        return;
276    }
277
278    let mut push_edges_of = |handle: ColliderHandle, require_change_flags: bool| {
279        let Some(co) = colliders.get(handle) else {
280            return;
281        };
282        if require_change_flags && !co.changes.needs_narrow_phase_update() {
283            return;
284        }
285        let Some(gid) = graph_indices.get(handle.0) else {
286            return;
287        };
288        for edge in graph.edges(select_graph_id(gid)) {
289            candidates.push(edge.id().index() as u32);
290        }
291    };
292
293    for handle in modified_colliders {
294        push_edges_of(*handle, true);
295    }
296
297    // Active bodies' colliders may have moved this step without having any
298    // change flag (internal motion doesn't go through the user-modification
299    // tracking), so their pairs are always candidates.
300    for body_handle in islands.active_bodies() {
301        if let Some(rb) = bodies.get(body_handle) {
302            for co_handle in rb.colliders() {
303                push_edges_of(*co_handle, false);
304            }
305        }
306    }
307
308    // Sort + dedup: each edge visited exactly once (it can be pushed once per seeded
309    // collider), and the deterministic edge-index order of a full graph scan is preserved.
310    candidates.sort_unstable();
311    candidates.dedup();
312}
313
314/// The narrow-phase collision detector that computes precise contact points between colliders.
315///
316/// After the broad-phase quickly filters out distant object pairs, the narrow-phase performs
317/// detailed geometric computations to find exact:
318/// - Contact points (where surfaces touch)
319/// - Contact normals (which direction surfaces face)
320/// - Penetration depths (how much objects overlap)
321///
322/// You typically don't interact with this directly - it's managed by [`PhysicsPipeline::step`](crate::pipeline::PhysicsPipeline::step).
323/// However, you can access it to query contact information or intersection state between specific colliders.
324///
325/// **For spatial queries** (raycasts, shape casts), use [`QueryPipeline`](crate::pipeline::QueryPipeline) instead.
326#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
327#[derive(Clone)]
328pub struct NarrowPhase {
329    #[cfg_attr(
330        feature = "serde-serialize",
331        serde(skip, default = "crate::geometry::default_persistent_query_dispatcher")
332    )]
333    query_dispatcher: Arc<dyn PersistentQueryDispatcher<ContactManifoldData, ContactData>>,
334    contact_graph: InteractionGraph<ColliderHandle, ContactPair>,
335    intersection_graph: InteractionGraph<ColliderHandle, IntersectionPair>,
336    graph_indices: Coarena<ColliderGraphIndices>,
337    /// Workspace buffer holding the edge indices of pairs to process during a step, so
338    /// the per-step loops don’t have to iterate on the whole interaction graphs.
339    #[cfg_attr(feature = "serde-serialize", serde(skip))]
340    update_candidates: Vec<u32>,
341    /// Solver graph coloring masks: per rigid-body (arena index), the set of solver colors
342    /// used by its active contact pairs. Maintained incrementally on contact start/stop,
343    /// so the solver never recolors its constraint graph from scratch.
344    #[cfg_attr(feature = "serde-serialize", serde(default))]
345    body_solver_color_masks: Vec<u128>,
346    /// Workspace: per-body packed qualification info (rigid-body arena index), rebuilt during
347    /// solver-graph maintenance. `u64::MAX` = missing/fixed/kinematic-or-sleeping;
348    /// else `(active_set_id << 32) | is_dynamic`.
349    #[cfg_attr(feature = "serde-serialize", serde(skip))]
350    body_qualify_info: Vec<u64>,
351    /// Workspace: per-body awake bit (arena index), rebuilt each narrow-phase update.
352    /// Internal motion sets no change flags, so "the parent body is awake" is the
353    /// narrow-phase's it-may-have-moved signal for pair updates.
354    #[cfg_attr(feature = "serde-serialize", serde(skip))]
355    awake_body_mask: Vec<bool>,
356    /// Per-pair solver-qualification hints (contact-graph edge index): bit 15 = has a
357    /// dynamic body, low bits = qualified solver-manifold count. Maintained incrementally
358    /// (count-cleared on sleep, mirrored on removals) so selection never re-walks every pair.
359    pair_solver_hints: Vec<u16>,
360    /// Persistent per-color buckets of the solver-active contact manifolds,
361    /// maintained by [`Self::maintain_solver_contact_graph`];
362    /// the solver consumes them directly — no re-selection, re-qualification, or counting sort.
363    solver_contact_graph: SolverContactGraph,
364    /// Whether [`Self::solver_contact_graph`] currently reflects the live contact
365    /// set. `false` forces a full rebuild on the next maintenance pass (the very first
366    /// step, or after a change that invalidates the whole graph).
367    solver_graph_valid: bool,
368    /// The [`IslandManager::active_set_epoch`] the solver contact graph was last
369    /// (re)built at. A mismatch means the awake set / solver-body indices shifted
370    /// (sleep, wake, body add/remove), so the graph is fully rebuilt.
371    solver_graph_epoch: u32,
372    /// The [`MultibodyJointSet::topology_epoch`] the solver contact graph was last built at.
373    /// A mismatch means bodies may have joined/left a multibody (manifolds can switch
374    /// between color buckets and the generic list), forcing a full rebuild.
375    solver_graph_mb_epoch: u32,
376    /// Workspace: edge indices fully updated this step (`OUTCOME_FULL`): possible bucket
377    /// membership change. Consumed by the incremental maintenance in
378    /// [`Self::maintain_solver_contact_graph`] and the force-event list reconciliation.
379    #[cfg_attr(feature = "serde-serialize", serde(skip))]
380    solver_graph_dirty: Vec<u32>,
381    /// Persistent list of solver-active pairs (edge indices) with contact-force events
382    /// enabled — exactly what the post-solve force-event pass must inspect. Maintained
383    /// incrementally with the solver graph, so no-force-events scenes pay nothing per step.
384    force_event_pairs: Vec<u32>,
385    /// Per-edge back-reference into [`Self::force_event_pairs`] (`u32::MAX` = not a member):
386    /// O(1) membership reconciliation. Edge-index shifts (pair/collider removal) are covered
387    /// by the full rebuild those removals already force via `solver_graph_valid`.
388    force_event_pos: Vec<u32>,
389    /// Workspace: edges flagged for force-event membership reconciliation because a collider
390    /// was user-modified this step (an `ActiveEvents`/threshold flip has no change flag
391    /// and need not trigger a contact update, so it would otherwise go unnoticed mid-epoch).
392    force_event_flagged: Vec<u32>,
393    /// Whether the force-event pair list is intact. It is maintained incrementally
394    /// through every transition, so it does NOT need epoch full rebuilds; `false`
395    /// (a degenerate state) triggers the from-scratch scan.
396    force_list_valid: bool,
397    /// Workspace: begin-touch pairs deferred for greedy coloring in canonical
398    /// `(min, max body id)` order (discovery-order independent: ≈ Δ colors instead of ≈ 2Δ).
399    /// Entries are `(edge id, packed body infos)`, see [`pack_color_body_info`].
400    #[cfg_attr(feature = "serde-serialize", serde(skip))]
401    solver_color_todo: Vec<(u32, u32, u32)>,
402    /// The self contact detection of the soft collision meshes (by collider), rebuilt by
403    /// every update for the awake bodies (see `soft_contacts`).
404    #[cfg_attr(feature = "serde-serialize", serde(skip))]
405    soft_self: parry::utils::hashmap::HashMap<ColliderHandle, soft_contacts::SoftSelfContacts>,
406    /// Pool of retired [`ContactPair`]s, reused by [`Self::add_pair`] so
407    /// pair-churn-heavy scenes (hundreds of broad-phase add/delete events per
408    /// step) skip the buffer reallocation of freshly constructed pairs.
409    #[cfg_attr(feature = "serde-serialize", serde(skip))]
410    retired_pairs: Vec<ContactPair>,
411}
412
413pub(crate) type ContactManifoldIndex = usize;
414
415impl Default for NarrowPhase {
416    fn default() -> Self {
417        Self::new()
418    }
419}
420
421impl NarrowPhase {
422    /// Creates a new empty narrow-phase.
423    pub fn new() -> Self {
424        Self::with_query_dispatcher(DefaultQueryDispatcher)
425    }
426
427    /// Creates a new empty narrow-phase with a custom query dispatcher.
428    pub fn with_query_dispatcher<D>(d: D) -> Self
429    where
430        D: 'static + PersistentQueryDispatcher<ContactManifoldData, ContactData>,
431    {
432        Self {
433            query_dispatcher: Arc::new(d),
434            contact_graph: InteractionGraph::new(),
435            intersection_graph: InteractionGraph::new(),
436            graph_indices: Coarena::new(),
437            update_candidates: Vec::new(),
438            retired_pairs: Vec::new(),
439            body_solver_color_masks: Vec::new(),
440            body_qualify_info: Vec::new(),
441            awake_body_mask: Vec::new(),
442            pair_solver_hints: Vec::new(),
443            solver_contact_graph: SolverContactGraph::new(),
444            solver_graph_valid: false,
445            solver_graph_epoch: 0,
446            solver_graph_mb_epoch: 0,
447            solver_graph_dirty: Vec::new(),
448            force_event_pairs: Vec::new(),
449            force_event_pos: Vec::new(),
450            force_event_flagged: Vec::new(),
451            force_list_valid: false,
452            solver_color_todo: Vec::new(),
453            soft_self: Default::default(),
454        }
455    }
456
457    /// Replaces the query dispatcher used by this narrow-phase.
458    pub fn set_query_dispatcher<D>(&mut self, d: D)
459    where
460        D: 'static + PersistentQueryDispatcher<ContactManifoldData, ContactData>,
461    {
462        self.query_dispatcher = Arc::new(d);
463    }
464
465    fn update_awake_body_mask(&mut self, islands: &IslandManager) {
466        self.awake_body_mask.clear();
467        let len = islands
468            .active_bodies()
469            .map(|h| h.into_raw_parts().0 as usize)
470            .max()
471            .map(|m| m + 1)
472            .unwrap_or(0);
473        self.awake_body_mask.resize(len, false);
474        for handle in islands.active_bodies() {
475            self.awake_body_mask[handle.into_raw_parts().0 as usize] = true;
476        }
477    }
478}