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