Skip to main content

rapier2d/geometry/narrow_phase/
pair_management.rs

1//! Contact/intersection pair lifecycle: collider removal, user-change handling,
2//! pair insertion/removal in the interaction graphs (with persistent
3//! solver-structure mirroring), and broad-phase event registration.
4
5use super::{
6    ColliderGraphIndices, NarrowPhase, PairRemovalMode, assign_pair_solver_color,
7    clear_pair_solver_color,
8};
9use crate::alloc_prelude::*;
10use crate::dynamics::solver::solver_contact_graph::GraphPos;
11use crate::dynamics::{IslandManager, RigidBodySet};
12use crate::geometry::{
13    BroadPhasePairEvent, ColliderChanges, ColliderGraphIndex, ColliderHandle, ColliderPair,
14    ColliderSet, CollisionEvent, ContactManifoldData, ContactPair, InteractionGraph,
15    IntersectionPair, PairEventStatus,
16};
17use crate::pipeline::{ActiveEvents, EventHandler};
18use crate::prelude::CollisionEventFlags;
19use parry::utils::hashmap::HashMap;
20
21impl NarrowPhase {
22    /// Maintain the narrow-phase internal state by taking collider removal into account.
23    #[profiling::function]
24    pub fn handle_user_changes(
25        &mut self,
26        mut islands: Option<&mut IslandManager>,
27        modified_colliders: &[ColliderHandle],
28        removed_colliders: &[ColliderHandle],
29        colliders: &mut ColliderSet,
30        bodies: &mut RigidBodySet,
31        events: &dyn EventHandler,
32    ) {
33        // TODO: avoid these hash-maps.
34        // They are necessary to handle the swap-remove done internally
35        // by the contact/intersection graphs when a node is removed.
36        let mut prox_id_remap = HashMap::default();
37        let mut contact_id_remap = HashMap::default();
38
39        for collider in removed_colliders {
40            // NOTE: if the collider does not have any graph indices currently, there is nothing
41            // to remove in the narrow-phase for this collider.
42            if let Some(graph_idx) = self
43                .graph_indices
44                .remove(collider.0, ColliderGraphIndices::invalid())
45            {
46                let intersection_graph_id = prox_id_remap
47                    .get(collider)
48                    .copied()
49                    .unwrap_or(graph_idx.intersection_graph_index);
50                let contact_graph_id = contact_id_remap
51                    .get(collider)
52                    .copied()
53                    .unwrap_or(graph_idx.contact_graph_index);
54
55                self.remove_collider(
56                    intersection_graph_id,
57                    contact_graph_id,
58                    islands.as_deref_mut(),
59                    colliders,
60                    bodies,
61                    &mut prox_id_remap,
62                    &mut contact_id_remap,
63                    events,
64                );
65            }
66        }
67
68        self.handle_user_changes_on_colliders(
69            islands,
70            modified_colliders,
71            colliders,
72            bodies,
73            events,
74        );
75    }
76
77    #[profiling::function]
78    pub(crate) fn remove_collider(
79        &mut self,
80        intersection_graph_id: ColliderGraphIndex,
81        contact_graph_id: ColliderGraphIndex,
82        mut islands: Option<&mut IslandManager>,
83        colliders: &mut ColliderSet,
84        bodies: &mut RigidBodySet,
85        prox_id_remap: &mut HashMap<ColliderHandle, ColliderGraphIndex>,
86        contact_id_remap: &mut HashMap<ColliderHandle, ColliderGraphIndex>,
87        events: &dyn EventHandler,
88    ) {
89        // Wake up every body in contact with the deleted collider and generate Stopped collision events.
90        if let Some(islands) = islands.as_deref_mut() {
91            for (a, b, pair) in self.contact_graph.interactions_with(contact_graph_id) {
92                if let Some(parent) = colliders.get(a).and_then(|c| c.parent.as_ref()) {
93                    islands.wake_up(bodies, parent.handle, true)
94                }
95
96                if let Some(parent) = colliders.get(b).and_then(|c| c.parent.as_ref()) {
97                    islands.wake_up(bodies, parent.handle, true)
98                }
99
100                if pair
101                    .event_status
102                    .contains(PairEventStatus::START_EVENT_EMITTED)
103                {
104                    events.handle_collision_event(
105                        bodies,
106                        colliders,
107                        CollisionEvent::Stopped(a, b, CollisionEventFlags::REMOVED),
108                        Some(pair),
109                    );
110                }
111            }
112        } else {
113            // If there is no island, don’t wake-up bodies, but do send the Stopped collision event.
114            for (a, b, pair) in self.contact_graph.interactions_with(contact_graph_id) {
115                if pair
116                    .event_status
117                    .contains(PairEventStatus::START_EVENT_EMITTED)
118                {
119                    events.handle_collision_event(
120                        bodies,
121                        colliders,
122                        CollisionEvent::Stopped(a, b, CollisionEventFlags::REMOVED),
123                        Some(pair),
124                    );
125                }
126            }
127        }
128
129        // Generate Stopped collision events for intersections.
130        for (a, b, pair) in self
131            .intersection_graph
132            .interactions_with(intersection_graph_id)
133        {
134            if pair.start_event_emitted {
135                events.handle_collision_event(
136                    bodies,
137                    colliders,
138                    CollisionEvent::Stopped(
139                        a,
140                        b,
141                        CollisionEventFlags::REMOVED | CollisionEventFlags::SENSOR,
142                    ),
143                    None,
144                );
145            }
146        }
147
148        // We have to manage the fact that one other collider will
149        // have its graph index changed because of the node's swap-remove.
150        if let Some(replacement) = self.intersection_graph.remove_node(intersection_graph_id) {
151            if let Some(replacement) = self.graph_indices.get_mut(replacement.0) {
152                replacement.intersection_graph_index = intersection_graph_id;
153            } else {
154                prox_id_remap.insert(replacement, intersection_graph_id);
155                // I feel like this should never happen now that the narrow-phase is the one owning
156                // the graph_indices. Let's put an unreachable in there and see if anybody still manages
157                // to reach it. If nobody does, we will remove this.
158                unreachable!();
159            }
160        }
161
162        // Node removal swap-removes contact-graph edges, shifting the edge indices the
163        // solver graph's `ContactRef`s and the force-event list are keyed by (neither is
164        // mirrored through node removal), so both must be rebuilt from scratch next step.
165        self.solver_graph_valid = false;
166        self.force_list_valid = false;
167        let pair_solver_hints = &mut self.pair_solver_hints;
168        // Persistent islands can't be rebuilt lazily like the tables above:
169        // unlink each removed edge and mirror each swap-remove exactly.
170        let mut edges_len = self.contact_graph.graph.edges.len();
171        let mut pi = islands.map(|i| &mut i.persistent);
172        if let Some(replacement) = self
173            .contact_graph
174            .remove_node_with(contact_graph_id, &mut |e| {
175                // Mirror the edges vec's swap_remove on the solver hints (on length
176                // mismatch — deserialized state — drop them; they rebuild lazily).
177                if e.index() < pair_solver_hints.len() {
178                    pair_solver_hints.swap_remove(e.index());
179                } else {
180                    pair_solver_hints.clear();
181                }
182
183                edges_len -= 1;
184                if let Some(pi) = pi.as_deref_mut() {
185                    pi.unlink_contact(e.index() as u32);
186                    pi.contact_edge_removed(e.index() as u32, edges_len as u32);
187                }
188            })
189        {
190            if let Some(replacement) = self.graph_indices.get_mut(replacement.0) {
191                replacement.contact_graph_index = contact_graph_id;
192            } else {
193                contact_id_remap.insert(replacement, contact_graph_id);
194                // I feel like this should never happen now that the narrow-phase is the one owning
195                // the graph_indices. Let's put an unreachable in there and see if anybody still manages
196                // to reach it. If nobody does, we will remove this.
197                unreachable!();
198            }
199        }
200    }
201
202    #[profiling::function]
203    pub(crate) fn handle_user_changes_on_colliders(
204        &mut self,
205        mut islands: Option<&mut IslandManager>,
206        modified_colliders: &[ColliderHandle],
207        colliders: &ColliderSet,
208        bodies: &mut RigidBodySet,
209        events: &dyn EventHandler,
210    ) {
211        let mut pairs_to_remove = vec![];
212
213        for handle in modified_colliders {
214            // NOTE: we use `get` because the collider may no longer
215            //       exist if it has been removed.
216            if let Some(co) = colliders.get(*handle) {
217                // Any user modification can flip the contact-force-event config
218                // (`ActiveEvents`/threshold have no change flag), so flag its pairs for
219                // force-event membership reconciliation at the next graph maintenance.
220                if let Some(gid) = self.graph_indices.get(handle.0) {
221                    if InteractionGraph::<ColliderHandle, ContactPair>::is_graph_index_valid(
222                        gid.contact_graph_index,
223                    ) {
224                        for edge in self.contact_graph.graph.edges(gid.contact_graph_index) {
225                            self.force_event_flagged.push(edge.id().index() as u32);
226                        }
227                    }
228                }
229
230                if !co.changes.needs_narrow_phase_update() {
231                    // No flag relevant to the narrow-phase is enabled for this collider.
232                    continue;
233                }
234
235                if let Some(gid) = self.graph_indices.get(handle.0) {
236                    // For each modified colliders, we need to wake-up the bodies it is in contact with
237                    // so that the narrow-phase properly takes into account the change in, e.g.,
238                    // collision groups. Waking up the modified collider's parent isn't enough because
239                    // it could be a fixed or kinematic body which don't propagate the wake-up state.
240                    // A pure in-place deformation (soft-body surface) is simulation-driven, not a
241                    // user change: it must not keep its island awake.
242                    let deformed_only = (co.changes
243                        & !(ColliderChanges::IN_MODIFIED_SET | ColliderChanges::DEFORMED))
244                        .is_empty();
245                    if let Some(islands) = islands.as_deref_mut().filter(|_| !deformed_only) {
246                        if let Some(co_parent) = &co.parent {
247                            islands.wake_up(bodies, co_parent.handle, true);
248                        }
249
250                        for inter in self
251                            .contact_graph
252                            .interactions_with(gid.contact_graph_index)
253                        {
254                            let other_handle = if *handle == inter.0 { inter.1 } else { inter.0 };
255                            let other_parent = colliders
256                                .get(other_handle)
257                                .and_then(|co| co.parent.as_ref());
258
259                            if let Some(other_parent) = other_parent {
260                                islands.wake_up(bodies, other_parent.handle, true);
261                            }
262                        }
263                    }
264
265                    // A parent or effective-dominance change (re-parenting, body type
266                    // change) invalidates the solver graph colors of this collider's
267                    // pairs: release and re-assign them with the current bodies.
268                    if co.changes.intersects(
269                        ColliderChanges::PARENT | ColliderChanges::PARENT_EFFECTIVE_DOMINANCE,
270                    ) {
271                        let mut edges_to_recolor = alloc::vec::Vec::new();
272                        for edge in self.contact_graph.graph.edges(gid.contact_graph_index) {
273                            edges_to_recolor.push(edge.id().index());
274                        }
275
276                        for edge_id in edges_to_recolor {
277                            let pair = &mut self.contact_graph.graph.edges[edge_id].weight;
278                            clear_pair_solver_color(&mut self.body_solver_color_masks, pair);
279
280                            let touching = pair.has_any_active_contact();
281                            if touching {
282                                let body_info = |co: ColliderHandle| {
283                                    colliders
284                                        .get(co)
285                                        .and_then(|co| co.parent.as_ref())
286                                        .map(|p| {
287                                            let rb = &bodies[p.handle];
288                                            (p.handle.into_raw_parts().0, rb.is_fixed())
289                                        })
290                                };
291                                let info1 = body_info(pair.collider1);
292                                let info2 = body_info(pair.collider2);
293                                assign_pair_solver_color(
294                                    &mut self.body_solver_color_masks,
295                                    pair,
296                                    info1,
297                                    info2,
298                                );
299                            }
300
301                            // Persistent islands: after re-parenting or a body type change,
302                            // a link recorded with the old endpoints may no longer describe
303                            // connectivity (a link to a fixed body doesn't connect). Update it.
304                            if let Some(islands) = islands.as_deref_mut() {
305                                let parent = |co: ColliderHandle| {
306                                    colliders.get(co).and_then(|c| c.parent.map(|p| p.handle))
307                                };
308                                let pair = &self.contact_graph.graph.edges[edge_id].weight;
309                                let (co1, co2) = (pair.collider1, pair.collider2);
310                                islands.persistent.unlink_contact(edge_id as u32);
311                                if touching {
312                                    islands.persistent.link_contact(
313                                        bodies,
314                                        edge_id as u32,
315                                        parent(co1),
316                                        parent(co2),
317                                    );
318                                }
319                            }
320                        }
321                    }
322
323                    // For each collider which had their sensor status modified, we need
324                    // to transfer their contact/intersection graph edges to the intersection/contact graph.
325                    // To achieve this we will remove the relevant contact/intersection pairs form the
326                    // contact/intersection graphs, and then add them into the other graph.
327                    if co.changes.intersects(ColliderChanges::TYPE) {
328                        if co.is_sensor() {
329                            // Find the contact pairs for this collider and
330                            // push them to `pairs_to_remove`.
331                            for inter in self
332                                .contact_graph
333                                .interactions_with(gid.contact_graph_index)
334                            {
335                                pairs_to_remove.push((
336                                    ColliderPair::new(inter.0, inter.1),
337                                    PairRemovalMode::FromContactGraph,
338                                ));
339                            }
340                        } else {
341                            // Find the contact pairs for this collider and
342                            // push them to `pairs_to_remove` if both involved
343                            // colliders are not sensors.
344                            for inter in self
345                                .intersection_graph
346                                .interactions_with(gid.intersection_graph_index)
347                                .filter(|(h1, h2, _)| {
348                                    !colliders[*h1].is_sensor() && !colliders[*h2].is_sensor()
349                                })
350                            {
351                                pairs_to_remove.push((
352                                    ColliderPair::new(inter.0, inter.1),
353                                    PairRemovalMode::FromIntersectionGraph,
354                                ));
355                            }
356                        }
357                    }
358
359                    // NOTE: if a collider only changed parent, we don’t need to remove it from any
360                    //       of the graphs as re-parenting doesn’t change the sensor status of a
361                    //       collider. If needed, their collision/intersection data will be
362                    //       updated/removed automatically in the contact or intersection update
363                    //       functions.
364                }
365            }
366        }
367
368        // Remove the pair from the relevant graph.
369        for pair in &pairs_to_remove {
370            self.remove_pair(
371                islands.as_deref_mut(),
372                colliders,
373                bodies,
374                &pair.0,
375                events,
376                pair.1,
377            );
378        }
379
380        // Add the removed pair to the relevant graph.
381        for pair in pairs_to_remove {
382            self.add_pair(colliders, &pair.0);
383        }
384    }
385
386    #[profiling::function]
387    fn remove_pair(
388        &mut self,
389        mut islands: Option<&mut IslandManager>,
390        colliders: &ColliderSet,
391        bodies: &mut RigidBodySet,
392        pair: &ColliderPair,
393        events: &dyn EventHandler,
394        mode: PairRemovalMode,
395    ) {
396        if let (Some(co1), Some(co2)) =
397            (colliders.get(pair.collider1), colliders.get(pair.collider2))
398        {
399            // TODO: could we just unwrap here?
400            // Don't we have the guarantee that we will get a `AddPair` before a `DeletePair`?
401            if let (Some(gid1), Some(gid2)) = (
402                self.graph_indices.get(pair.collider1.0),
403                self.graph_indices.get(pair.collider2.0),
404            ) {
405                if mode == PairRemovalMode::FromIntersectionGraph
406                    || (mode == PairRemovalMode::Auto && (co1.is_sensor() || co2.is_sensor()))
407                {
408                    let intersection = self
409                        .intersection_graph
410                        .remove_edge(gid1.intersection_graph_index, gid2.intersection_graph_index);
411
412                    // Emit an intersection lost event if we had an intersection before removing the edge.
413                    if let Some(mut intersection) = intersection {
414                        if intersection.intersecting
415                            && (co1.flags.active_events | co2.flags.active_events)
416                                .contains(ActiveEvents::COLLISION_EVENTS)
417                        {
418                            intersection.emit_stop_event(
419                                bodies,
420                                colliders,
421                                pair.collider1,
422                                pair.collider2,
423                                events,
424                            )
425                        }
426                    }
427                } else {
428                    // O(1) maintenance of the persistent solver structures through the edges vec's
429                    // swap-remove (with stored-position fixup) — a global rebuild
430                    // is O(all pairs). If a rebuild is pending, `ContactRef`/`graph_pos` are garbage: skip it.
431                    let solver_graph_valid = self.solver_graph_valid;
432                    let graph = &mut self.solver_contact_graph;
433                    let force_list = &mut self.force_event_pairs;
434                    let force_pos = &mut self.force_event_pos;
435                    let pair_solver_hints = &mut self.pair_solver_hints;
436                    let num_edges = self.contact_graph.graph.edges.len();
437                    let edges_ptr = self.contact_graph.graph.edges.as_mut_ptr();
438                    let mut removed_index = usize::MAX;
439                    let mut consistent = true;
440                    let contact_pair = self.contact_graph.remove_edge_with(
441                        gid1.contact_graph_index,
442                        gid2.contact_graph_index,
443                        &mut |e| {
444                            removed_index = e.index();
445                            // Mirror the edges vec's swap_remove on the solver hints
446                            // (on length mismatch — deserialized state — drop them
447                            // and fall back to a full solver-graph rebuild).
448                            if e.index() < pair_solver_hints.len() {
449                                pair_solver_hints.swap_remove(e.index());
450                            } else {
451                                pair_solver_hints.clear();
452                                consistent = false;
453                            }
454
455                            if e.index() >= num_edges {
456                                consistent = false;
457                                return;
458                            }
459
460                            // Pull the removed pair's manifolds out of the buckets. Raw ordinal
461                            // access: the fixup may rewrite another manifold of this same pair via
462                            // `edges_ptr`. SAFETY: runs before the swap_remove (indices live); single-threaded.
463                            if solver_graph_valid {
464                                unsafe {
465                                    let pair: *mut ContactPair =
466                                        &mut (*edges_ptr.add(e.index())).weight;
467                                    let sm = (*pair).solver_manifolds_mut();
468                                    let (sm_ptr, num) = (sm.as_mut_ptr(), sm.len());
469                                    for ordinal in 0..num {
470                                        let mdata: *mut ContactManifoldData =
471                                            &mut (*sm_ptr.add(ordinal)).data;
472                                        let pos = (*mdata).graph_pos;
473                                        if pos.is_some() {
474                                            Self::remove_and_fixup(graph, edges_ptr, pos);
475                                            (*mdata).graph_pos = GraphPos::NONE;
476                                        }
477                                    }
478                                }
479                            }
480
481                            // Force-event list: drop the removed pair's membership, then
482                            // mirror the swap-remove on the back-ref array (grown at pair
483                            // add so it matches the edges vec; anything else = deserialized).
484                            if force_pos.len() == num_edges {
485                                let cur = force_pos[e.index()];
486                                if cur != u32::MAX {
487                                    force_list.swap_remove(cur as usize);
488                                    if (cur as usize) < force_list.len() {
489                                        force_pos[force_list[cur as usize] as usize] = cur;
490                                    }
491                                    force_pos[e.index()] = u32::MAX;
492                                }
493                                force_pos.swap_remove(e.index());
494                            } else {
495                                force_pos.clear();
496                                force_list.clear();
497                                consistent = false;
498                            }
499                        },
500                    );
501
502                    if !consistent {
503                        // Deserialized/degenerate bookkeeping: rebuild from scratch.
504                        self.solver_graph_valid = false;
505                        self.force_list_valid = false;
506                    } else if removed_index < self.contact_graph.graph.edges.len() {
507                        // A pair was swap-moved into the removed slot: rewrite the
508                        // edge index its persistent entries are keyed by.
509                        let new_edge = removed_index as u32;
510                        let moved = &mut self.contact_graph.graph.edges[removed_index].weight;
511                        let graph = &mut self.solver_contact_graph;
512                        // Same rebuild-pending gate as the removal loop above:
513                        // stale `graph_pos` back-refs must not drive bucket writes.
514                        if solver_graph_valid {
515                            for m in moved.solver_manifolds_mut() {
516                                if m.data.graph_pos.is_some() {
517                                    graph.rewrite_edge(m.data.graph_pos, new_edge);
518                                }
519                            }
520                        }
521                        let fpos = self
522                            .force_event_pos
523                            .get(removed_index)
524                            .copied()
525                            .unwrap_or(u32::MAX);
526                        if fpos != u32::MAX {
527                            self.force_event_pairs[fpos as usize] = new_edge;
528                        }
529                    }
530
531                    // Persistent islands: unlink the removed pair (if it was
532                    // touching) and mirror the edges-vec swap-remove on the
533                    // link-location table.
534                    if removed_index != usize::MAX && num_edges > 0 {
535                        if let Some(islands) = islands.as_deref_mut() {
536                            islands.persistent.unlink_contact(removed_index as u32);
537                            islands
538                                .persistent
539                                .contact_edge_removed(removed_index as u32, num_edges as u32 - 1);
540                        }
541                    }
542
543                    // Emit a contact stopped event if we had a contact before removing the edge.
544                    // Also wake up the dynamic bodies that were in contact.
545                    if let Some(mut ctct) = contact_pair {
546                        clear_pair_solver_color(&mut self.body_solver_color_masks, &mut ctct);
547
548                        if ctct.has_any_active_contact() {
549                            if let Some(islands) = islands {
550                                if let Some(co_parent1) = &co1.parent {
551                                    islands.wake_up(bodies, co_parent1.handle, true);
552                                }
553
554                                if let Some(co_parent2) = co2.parent {
555                                    islands.wake_up(bodies, co_parent2.handle, true);
556                                }
557                            }
558
559                            if (co1.flags.active_events | co2.flags.active_events)
560                                .contains(ActiveEvents::COLLISION_EVENTS)
561                            {
562                                ctct.emit_stop_event(bodies, colliders, events);
563                            }
564                        }
565
566                        // Retire the pair for reuse by `add_pair` (bounded pool).
567                        if self.retired_pairs.len() < 2048 {
568                            self.retired_pairs.push(ctct);
569                        }
570                    }
571                }
572            }
573        }
574    }
575
576    #[profiling::function]
577    fn add_pair(&mut self, colliders: &ColliderSet, pair: &ColliderPair) {
578        if let (Some(co1), Some(co2)) =
579            (colliders.get(pair.collider1), colliders.get(pair.collider2))
580        {
581            // These colliders have no parents - continue.
582
583            let (gid1, gid2) = self.graph_indices.ensure_pair_exists(
584                pair.collider1.0,
585                pair.collider2.0,
586                ColliderGraphIndices::invalid(),
587            );
588
589            if co1.is_sensor() || co2.is_sensor() {
590                // NOTE: the collider won't have a graph index as long
591                // as it does not interact with anything.
592                if !InteractionGraph::<(), ()>::is_graph_index_valid(gid1.intersection_graph_index)
593                {
594                    gid1.intersection_graph_index =
595                        self.intersection_graph.graph.add_node(pair.collider1);
596                }
597
598                if !InteractionGraph::<(), ()>::is_graph_index_valid(gid2.intersection_graph_index)
599                {
600                    gid2.intersection_graph_index =
601                        self.intersection_graph.graph.add_node(pair.collider2);
602                }
603
604                if self
605                    .intersection_graph
606                    .graph
607                    .find_edge(gid1.intersection_graph_index, gid2.intersection_graph_index)
608                    .is_none()
609                {
610                    let _ = self.intersection_graph.add_edge(
611                        gid1.intersection_graph_index,
612                        gid2.intersection_graph_index,
613                        IntersectionPair::new(),
614                    );
615                }
616            } else {
617                // NOTE: same code as above, but for the contact graph.
618                // TODO: refactor both pieces of code somehow?
619
620                // NOTE: the collider won't have a graph index as long
621                // as it does not interact with anything.
622                if !InteractionGraph::<(), ()>::is_graph_index_valid(gid1.contact_graph_index) {
623                    gid1.contact_graph_index = self.contact_graph.graph.add_node(pair.collider1);
624                }
625
626                if !InteractionGraph::<(), ()>::is_graph_index_valid(gid2.contact_graph_index) {
627                    gid2.contact_graph_index = self.contact_graph.graph.add_node(pair.collider2);
628                }
629
630                if self
631                    .contact_graph
632                    .graph
633                    .find_edge(gid1.contact_graph_index, gid2.contact_graph_index)
634                    .is_none()
635                {
636                    let interaction = if let Some(mut retired) = self.retired_pairs.pop() {
637                        retired.reset_for_reuse(pair.collider1, pair.collider2);
638                        retired
639                    } else {
640                        ContactPair::new(pair.collider1, pair.collider2)
641                    };
642                    // Keep the solver hints aligned with the edges vec at all times: add/remove
643                    // events interleave within a step, so deferred growth would desync the swap_remove
644                    // mirroring. On mismatch (deserialized), drop them — they rebuild lazily.
645                    if self.pair_solver_hints.len() == self.contact_graph.graph.edges.len() {
646                        self.pair_solver_hints.push(0);
647                    } else {
648                        self.pair_solver_hints.clear();
649                    }
650                    // Same for the force-event back-ref array: the O(1) removal maintenance
651                    // mirrors the edges vec's swap_remove on it, so it must track the edge
652                    // count exactly (not lazily at the next maintenance).
653                    if self.force_event_pos.len() == self.contact_graph.graph.edges.len() {
654                        self.force_event_pos.push(u32::MAX);
655                    } else {
656                        self.force_event_pos.clear();
657                        self.force_event_pairs.clear();
658                        self.force_list_valid = false;
659                    }
660                    let _ = self.contact_graph.add_edge(
661                        gid1.contact_graph_index,
662                        gid2.contact_graph_index,
663                        interaction,
664                    );
665                }
666            }
667        }
668    }
669
670    pub(crate) fn register_pairs(
671        &mut self,
672        mut islands: Option<&mut IslandManager>,
673        colliders: &ColliderSet,
674        bodies: &mut RigidBodySet,
675        broad_phase_events: &[BroadPhasePairEvent],
676        events: &dyn EventHandler,
677    ) {
678        for event in broad_phase_events {
679            match event {
680                BroadPhasePairEvent::AddPair(pair) => {
681                    self.add_pair(colliders, pair);
682                }
683                BroadPhasePairEvent::DeletePair(pair) => {
684                    self.remove_pair(
685                        islands.as_deref_mut(),
686                        colliders,
687                        bodies,
688                        pair,
689                        events,
690                        PairRemovalMode::Auto,
691                    );
692                }
693            }
694        }
695    }
696}