Skip to main content

parry2d/partitioning/bvh/
bvh_refit.rs

1use super::bvh_tree::{BvhNodeIndex, BvhNodeVec, BvhNodeWide};
2use super::{Bvh, BvhNode, BvhWorkspace};
3use crate::utils::VecMap;
4use alloc::vec::Vec;
5
6/// Raw pointers to the refit buffers, shared across parallel refit tasks.
7///
8/// Safety: tasks write disjoint index ranges (see `refit_recurse_parallel`).
9#[cfg(feature = "parallel")]
10#[derive(Copy, Clone)]
11struct RefitPtrs {
12    target: *mut BvhNodeVec,
13    leaf_data: *mut VecMap<BvhNodeIndex>,
14    parents: *mut Vec<BvhNodeIndex>,
15}
16
17#[cfg(feature = "parallel")]
18unsafe impl Send for RefitPtrs {}
19#[cfg(feature = "parallel")]
20unsafe impl Sync for RefitPtrs {}
21
22impl Bvh {
23    /// Updates the BVH's internal node AABBs after leaf changes.
24    ///
25    /// Refitting ensures that every internal node's AABB tightly encloses the AABBs of its
26    /// children. This operation is essential after updating leaf positions with
27    /// [`insert_or_update_partially`] and is much faster than rebuilding the entire tree.
28    ///
29    /// In addition to updating AABBs, this method:
30    /// - Reorders nodes in depth-first order for better cache locality during queries
31    /// - Ensures leaf counts on each node are correct
32    /// - Propagates change flags from leaves to ancestors (for change detection)
33    ///
34    /// # When to Use
35    ///
36    /// Call `refit` after:
37    /// - Bulk updates with [`insert_or_update_partially`]
38    /// - Any operation that modifies leaf AABBs without updating ancestor nodes
39    /// - When you want to optimize tree layout for better query performance
40    ///
41    /// **Don't call `refit` after**:
42    /// - Regular [`insert`] calls (they already update ancestors)
43    /// - [`remove`] calls (they already maintain tree validity)
44    ///
45    /// # Arguments
46    ///
47    /// * `workspace` - A reusable workspace to avoid allocations. Can be shared across
48    ///   multiple BVH operations for better performance.
49    ///
50    /// # Performance
51    ///
52    /// - **Time**: O(n) where n is the number of nodes
53    /// - **Space**: O(n) temporary storage in workspace
54    /// - Much faster than rebuilding the tree from scratch
55    /// - Essential for maintaining good query performance in dynamic scenes
56    ///
57    /// # Examples
58    ///
59    /// ## After bulk updates
60    ///
61    /// ```
62    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
63    /// use parry3d::partitioning::{Bvh, BvhWorkspace};
64    /// use parry3d::bounding_volume::Aabb;
65    /// use parry3d::math::Vector;
66    ///
67    /// let mut bvh = Bvh::new();
68    /// let mut workspace = BvhWorkspace::default();
69    ///
70    /// // Insert initial objects
71    /// for i in 0..100 {
72    ///     let aabb = Aabb::new(
73    ///         Vector::new(i as f32, 0.0, 0.0),
74    ///         Vector::new(i as f32 + 1.0, 1.0, 1.0)
75    ///     );
76    ///     bvh.insert(aabb, i);
77    /// }
78    ///
79    /// // Update all objects without tree propagation (faster)
80    /// for i in 0..100 {
81    ///     let offset = 0.1;
82    ///     let aabb = Aabb::new(
83    ///         Vector::new(i as f32 + offset, 0.0, 0.0),
84    ///         Vector::new(i as f32 + 1.0 + offset, 1.0, 1.0)
85    ///     );
86    ///     bvh.insert_or_update_partially(aabb, i, 0.0);
87    /// }
88    ///
89    /// // Now update the tree in one efficient pass
90    /// bvh.refit(&mut workspace);
91    /// # }
92    /// ```
93    ///
94    /// ## In a game loop
95    ///
96    /// ```
97    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
98    /// use parry3d::partitioning::{Bvh, BvhWorkspace};
99    /// use parry3d::bounding_volume::Aabb;
100    /// use parry3d::math::Vector;
101    ///
102    /// let mut bvh = Bvh::new();
103    /// let mut workspace = BvhWorkspace::default();
104    ///
105    /// // Game initialization - add objects
106    /// for i in 0..1000 {
107    ///     let aabb = Aabb::new(
108    ///         Vector::new(i as f32, 0.0, 0.0),
109    ///         Vector::new(i as f32 + 1.0, 1.0, 1.0)
110    ///     );
111    ///     bvh.insert(aabb, i);
112    /// }
113    ///
114    /// // Game loop - update objects each frame
115    /// for frame in 0..100 {
116    ///     // Update physics, AI, etc.
117    ///     for i in 0..1000 {
118    ///         let time = frame as f32 * 0.016; // ~60 FPS
119    ///         let pos = time.sin() * 10.0;
120    ///         let aabb = Aabb::new(
121    ///             Vector::new(i as f32 + pos, 0.0, 0.0),
122    ///             Vector::new(i as f32 + pos + 1.0, 1.0, 1.0)
123    ///         );
124    ///         bvh.insert_or_update_partially(aabb, i, 0.0);
125    ///     }
126    ///
127    ///     // Refit once per frame for all updates
128    ///     bvh.refit(&mut workspace);
129    ///
130    ///     // Now perform collision detection queries...
131    /// }
132    /// # }
133    /// ```
134    ///
135    /// ## With change detection margin
136    ///
137    /// ```
138    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
139    /// use parry3d::partitioning::{Bvh, BvhWorkspace};
140    /// use parry3d::bounding_volume::Aabb;
141    /// use parry3d::math::Vector;
142    ///
143    /// let mut bvh = Bvh::new();
144    /// let mut workspace = BvhWorkspace::default();
145    ///
146    /// // Add an object
147    /// let aabb = Aabb::new(Vector::ZERO, Vector::new(1.0, 1.0, 1.0));
148    /// bvh.insert(aabb, 0);
149    ///
150    /// // Update with a margin - tree won't update if movement is small
151    /// let margin = 0.5;
152    /// let new_aabb = Aabb::new(Vector::new(0.1, 0.0, 0.0), Vector::new(1.1, 1.0, 1.0));
153    /// bvh.insert_or_update_partially(new_aabb, 0, margin);
154    ///
155    /// // Refit propagates the change detection flags
156    /// bvh.refit(&mut workspace);
157    /// # }
158    /// ```
159    ///
160    /// # Comparison with `refit_without_opt`
161    ///
162    /// This method reorganizes the tree in memory for better cache performance.
163    /// If you only need to update AABBs without reordering, use [`refit_without_opt`](Self::refit_without_opt)
164    /// which is faster but doesn't improve memory layout.
165    ///
166    /// # Notes
167    ///
168    /// - Reuses the provided `workspace` to avoid allocations
169    /// - Safe to call even if no leaves were modified (just reorganizes tree)
170    /// - Does not change the tree's topology, only AABBs and layout
171    /// - Call this before [`optimize_incremental`] for best results
172    ///
173    /// # See Also
174    ///
175    /// - [`insert_or_update_partially`](Bvh::insert_or_update_partially) - Update leaves
176    ///   without propagation
177    /// - [`refit_without_opt`](Self::refit_without_opt) - Faster refit without memory
178    ///   reorganization
179    /// - [`optimize_incremental`](Bvh::optimize_incremental) - Improve tree quality
180    /// - [`BvhWorkspace`] - Reusable workspace for operations
181    ///
182    /// [`insert_or_update_partially`]: Bvh::insert_or_update_partially
183    /// [`insert`]: Bvh::insert
184    /// [`remove`]: Bvh::remove
185    /// [`optimize_incremental`]: Bvh::optimize_incremental
186    pub fn refit(&mut self, workspace: &mut BvhWorkspace) {
187        Self::refit_buffers::<true>(
188            &mut self.nodes,
189            &mut workspace.refit_tmp,
190            &mut self.leaf_node_indices,
191            &mut self.parents,
192        );
193
194        // Swap the old nodes with the refitted ones.
195        core::mem::swap(&mut self.nodes, &mut workspace.refit_tmp);
196        // The refit rebuilt the node array in depth-first order, dropping the
197        // orphaned slots the free list pointed to.
198        self.free_wide_nodes.clear();
199    }
200
201    /// Same as [`Self::refit`], but processes independent subtrees in parallel.
202    ///
203    /// The result is identical to [`Self::refit`] (same node layout, same flags);
204    /// only the work distribution differs.
205    #[cfg(feature = "parallel")]
206    pub fn refit_parallel(&mut self, workspace: &mut BvhWorkspace) {
207        Self::refit_buffers_parallel::<true>(
208            &mut self.nodes,
209            &mut workspace.refit_tmp,
210            &mut self.leaf_node_indices,
211            &mut self.parents,
212        );
213
214        // Swap the old nodes with the refitted ones.
215        core::mem::swap(&mut self.nodes, &mut workspace.refit_tmp);
216        // The refit rebuilt the node array in depth-first order, dropping the
217        // orphaned slots the free list pointed to.
218        self.free_wide_nodes.clear();
219    }
220
221    #[cfg(feature = "parallel")]
222    fn refit_buffers_parallel<const RESOLVE: bool>(
223        source: &mut BvhNodeVec,
224        target: &mut BvhNodeVec,
225        leaf_data: &mut VecMap<BvhNodeIndex>,
226        parents: &mut Vec<BvhNodeIndex>,
227    ) {
228        // Subtrees below this leaf count are refitted sequentially.
229        const SEQ_LEAF_THRESHOLD: u32 = 2048;
230        // Bounds the number of spawned tasks to 2^MAX_SPLIT_DEPTH.
231        const MAX_SPLIT_DEPTH: u32 = 6;
232
233        if source.is_empty() || source[0].leaf_count() <= SEQ_LEAF_THRESHOLD.max(2) {
234            return Self::refit_buffers::<RESOLVE>(source, target, leaf_data, parents);
235        }
236
237        target.resize(
238            source.len(),
239            BvhNodeWide {
240                left: BvhNode::zeros(),
241                right: BvhNode::zeros(),
242            },
243        );
244        parents.resize(source.len(), BvhNodeIndex::default());
245
246        let ptrs = RefitPtrs {
247            target: target as *mut BvhNodeVec,
248            leaf_data: leaf_data as *mut VecMap<BvhNodeIndex>,
249            parents: parents as *mut Vec<BvhNodeIndex>,
250        };
251
252        // Mirror of `refit_buffers`' root special case, with the sequential target-id
253        // counter replaced by offsets computed from the subtree leaf counts: the
254        // depth-first layout of a subtree with `n` leaves spans exactly `n - 1` wide
255        // nodes.
256        let root = source[0];
257        let left_size = if root.left.is_leaf() {
258            0
259        } else {
260            root.left.leaf_count() - 1
261        };
262        let right_size = if root.right.is_leaf() {
263            0
264        } else {
265            root.right.leaf_count() - 1
266        };
267        let final_len = (1 + left_size + right_size) as usize;
268
269        let _ = rayon::join(
270            || {
271                // SAFETY: this task writes only to target slots [1, 1 + left_size),
272                //         entry target[0].left, and the leaf data of leaves of the
273                //         left subtree — all disjoint from the other task.
274                let target = unsafe { &mut *ptrs.target };
275                if !root.left.is_leaf() {
276                    Self::refit_recurse_parallel::<RESOLVE>(
277                        source,
278                        ptrs,
279                        root.left.children,
280                        1,
281                        BvhNodeIndex::left(0),
282                        MAX_SPLIT_DEPTH,
283                        SEQ_LEAF_THRESHOLD,
284                    );
285                } else {
286                    target[0].left = root.left;
287                    if RESOLVE {
288                        target[0].left.data.resolve_pending_change();
289                    }
290                }
291            },
292            || {
293                // SAFETY: see the other task; slots [1 + left_size, final_len) and
294                //         entry target[0].right.
295                let target = unsafe { &mut *ptrs.target };
296                if !root.right.is_leaf() {
297                    Self::refit_recurse_parallel::<RESOLVE>(
298                        source,
299                        ptrs,
300                        root.right.children,
301                        1 + left_size,
302                        BvhNodeIndex::right(0),
303                        MAX_SPLIT_DEPTH,
304                        SEQ_LEAF_THRESHOLD,
305                    );
306                } else {
307                    target[0].right = root.right;
308                    if RESOLVE {
309                        target[0].right.data.resolve_pending_change();
310                    }
311                }
312            },
313        );
314
315        source.truncate(final_len);
316        target.truncate(final_len);
317        parents.truncate(final_len);
318    }
319
320    /// Recursive parallel counterpart of `refit_recurse`.
321    ///
322    /// `target_id` is the depth-first slot this subtree's root occupies (its left
323    /// child subtree starts at `target_id + 1`, its right child subtree right after
324    /// the left one, whose extent is known from its leaf count).
325    #[cfg(feature = "parallel")]
326    fn refit_recurse_parallel<const RESOLVE: bool>(
327        source: &BvhNodeVec,
328        ptrs: RefitPtrs,
329        source_id: u32,
330        target_id: u32,
331        parent: BvhNodeIndex,
332        depth: u32,
333        seq_leaf_threshold: u32,
334    ) {
335        let node = source[source_id as usize];
336        let leaf_count = node.left.leaf_count() + node.right.leaf_count();
337
338        if depth == 0 || leaf_count <= seq_leaf_threshold {
339            //
340
341            // SAFETY: the sequential refit of this subtree only touches the target
342            //         slots [target_id, target_id + leaf_count - 1), its parent entry,
343            //         and its own leaves' data: all disjoint from the other tasks.
344            let target = unsafe { &mut *ptrs.target };
345            let leaf_data = unsafe { &mut *ptrs.leaf_data };
346            let parents = unsafe { &mut *ptrs.parents };
347            let mut counter = target_id;
348            Self::refit_recurse::<RESOLVE>(
349                source,
350                target,
351                leaf_data,
352                parents,
353                source_id,
354                &mut counter,
355                parent,
356            );
357            debug_assert_eq!(counter, target_id + leaf_count - 1);
358            return;
359        }
360
361        let left_size = if node.left.is_leaf() {
362            0
363        } else {
364            node.left.leaf_count() - 1
365        };
366
367        let _ = rayon::join(
368            || {
369                let target = unsafe { &mut *ptrs.target };
370                let leaf_data = unsafe { &mut *ptrs.leaf_data };
371                if !node.left.is_leaf() {
372                    Self::refit_recurse_parallel::<RESOLVE>(
373                        source,
374                        ptrs,
375                        node.left.children,
376                        target_id + 1,
377                        BvhNodeIndex::left(target_id),
378                        depth - 1,
379                        seq_leaf_threshold,
380                    );
381                } else {
382                    target[target_id as usize].left = node.left;
383                    if RESOLVE {
384                        target[target_id as usize]
385                            .left
386                            .data
387                            .resolve_pending_change();
388                    }
389                    leaf_data[node.left.children as usize] = BvhNodeIndex::left(target_id);
390                }
391            },
392            || {
393                let target = unsafe { &mut *ptrs.target };
394                let leaf_data = unsafe { &mut *ptrs.leaf_data };
395                if !node.right.is_leaf() {
396                    Self::refit_recurse_parallel::<RESOLVE>(
397                        source,
398                        ptrs,
399                        node.right.children,
400                        target_id + 1 + left_size,
401                        BvhNodeIndex::right(target_id),
402                        depth - 1,
403                        seq_leaf_threshold,
404                    );
405                } else {
406                    target[target_id as usize].right = node.right;
407                    if RESOLVE {
408                        target[target_id as usize]
409                            .right
410                            .data
411                            .resolve_pending_change();
412                    }
413                    leaf_data[node.right.children as usize] = BvhNodeIndex::right(target_id);
414                }
415            },
416        );
417
418        // Both children of this wide node are now written: compute the summary entry
419        // in the parent, like the tail of `refit_recurse`.
420        let target = unsafe { &mut *ptrs.target };
421        let parents = unsafe { &mut *ptrs.parents };
422        let merged = target[target_id as usize]
423            .left
424            .merged(&target[target_id as usize].right, target_id);
425        target[parent] = merged;
426        parents[target_id as usize] = parent;
427    }
428
429    /// Same as [`Self::refit`], but leaves every change-detection flag untouched.
430    ///
431    /// Use this to make the tree valid for queries after a batch of
432    /// [`Self::insert_or_update_partially`] without consuming the pending change
433    /// flags: a later flag-resolving [`Self::refit`] (or
434    /// [`Self::refit_partial`]) will promote them as if this call never happened.
435    pub fn refit_without_resolve(&mut self, workspace: &mut BvhWorkspace) {
436        Self::refit_buffers::<false>(
437            &mut self.nodes,
438            &mut workspace.refit_tmp,
439            &mut self.leaf_node_indices,
440            &mut self.parents,
441        );
442        core::mem::swap(&mut self.nodes, &mut workspace.refit_tmp);
443        // The refit rebuilt the node array in depth-first order, dropping the
444        // orphaned slots the free list pointed to.
445        self.free_wide_nodes.clear();
446    }
447
448    /// Parallel version of [`Self::refit_without_resolve`].
449    #[cfg(feature = "parallel")]
450    pub fn refit_without_resolve_parallel(&mut self, workspace: &mut BvhWorkspace) {
451        Self::refit_buffers_parallel::<false>(
452            &mut self.nodes,
453            &mut workspace.refit_tmp,
454            &mut self.leaf_node_indices,
455            &mut self.parents,
456        );
457        core::mem::swap(&mut self.nodes, &mut workspace.refit_tmp);
458        // The refit rebuilt the node array in depth-first order, dropping the
459        // orphaned slots the free list pointed to.
460        self.free_wide_nodes.clear();
461    }
462
463    pub(super) fn refit_buffers<const RESOLVE: bool>(
464        source: &mut BvhNodeVec,
465        target: &mut BvhNodeVec,
466        leaf_data: &mut VecMap<BvhNodeIndex>,
467        parents: &mut Vec<BvhNodeIndex>,
468    ) {
469        if source.is_empty() {
470            target.clear();
471            parents.clear();
472        } else if source[0].leaf_count() <= 2 {
473            // No actual refit to apply, just copy the root wide node.
474            target.clear();
475            parents.clear();
476            target.push(source[0]);
477            if RESOLVE {
478                target[0].left.data.resolve_pending_change();
479                if target[0].right.leaf_count() > 0 {
480                    target[0].right.data.resolve_pending_change();
481                }
482            }
483            parents.push(BvhNodeIndex::default());
484        } else if !source.is_empty() && source[0].leaf_count() > 2 {
485            target.resize(
486                source.len(),
487                BvhNodeWide {
488                    left: BvhNode::zeros(),
489                    right: BvhNode::zeros(),
490                },
491            );
492
493            let mut len = 1;
494
495            // Start with a special case for the root then recurse.
496            let left_child_id = source[0].left.children;
497            let right_child_id = source[0].right.children;
498
499            if !source[0].left.is_leaf() {
500                Self::refit_recurse::<RESOLVE>(
501                    source,
502                    target,
503                    leaf_data,
504                    parents,
505                    left_child_id,
506                    &mut len,
507                    BvhNodeIndex::left(0),
508                );
509            } else {
510                target[0].left = source[0].left;
511                if RESOLVE {
512                    target[0].left.data.resolve_pending_change();
513                }
514
515                // NOTE: updating the leaf_data shouldn’t be needed here since the root
516                //       is always at 0.
517                // *self.leaf_data.get_mut_unknown_gen(left_child_id).unwrap() = BvhNodeIndex::left(0);
518            }
519
520            if !source[0].right.is_leaf() {
521                Self::refit_recurse::<RESOLVE>(
522                    source,
523                    target,
524                    leaf_data,
525                    parents,
526                    right_child_id,
527                    &mut len,
528                    BvhNodeIndex::right(0),
529                );
530            } else {
531                target[0].right = source[0].right;
532                if RESOLVE {
533                    target[0].right.data.resolve_pending_change();
534                }
535                // NOTE: updating the leaf_data shouldn’t be needed here since the root
536                //       is always at 0.
537                // *self.leaf_data.get_mut_unknown_gen(right_child_id).unwrap() = BvhNodeIndex::right(0);
538            }
539
540            source.truncate(len as usize);
541            target.truncate(len as usize);
542            parents.truncate(len as usize);
543        }
544    }
545
546    fn refit_recurse<const RESOLVE: bool>(
547        source: &BvhNodeVec,
548        target: &mut BvhNodeVec,
549        leaf_data: &mut VecMap<BvhNodeIndex>,
550        parents: &mut [BvhNodeIndex],
551        source_id: u32,
552        target_id_mut: &mut u32,
553        parent: BvhNodeIndex,
554    ) {
555        let target_id = *target_id_mut;
556        *target_id_mut += 1;
557
558        let node = &source[source_id as usize];
559        let left_is_leaf = node.left.is_leaf();
560        let right_is_leaf = node.right.is_leaf();
561        let left_source_id = node.left.children;
562        let right_source_id = node.right.children;
563
564        if !left_is_leaf {
565            Self::refit_recurse::<RESOLVE>(
566                source,
567                target,
568                leaf_data,
569                parents,
570                left_source_id,
571                target_id_mut,
572                BvhNodeIndex::left(target_id),
573            );
574        } else {
575            let node = &source[source_id as usize];
576            target[target_id as usize].left = node.left;
577            if RESOLVE {
578                target[target_id as usize]
579                    .left
580                    .data
581                    .resolve_pending_change();
582            }
583            leaf_data[node.left.children as usize] = BvhNodeIndex::left(target_id);
584        }
585
586        if !right_is_leaf {
587            Self::refit_recurse::<RESOLVE>(
588                source,
589                target,
590                leaf_data,
591                parents,
592                right_source_id,
593                target_id_mut,
594                BvhNodeIndex::right(target_id),
595            );
596        } else {
597            let node = &source[source_id as usize];
598            target[target_id as usize].right = node.right;
599            if RESOLVE {
600                target[target_id as usize]
601                    .right
602                    .data
603                    .resolve_pending_change();
604            }
605            leaf_data[node.right.children as usize] = BvhNodeIndex::right(target_id);
606        }
607
608        let node = &target[target_id as usize];
609        target[parent] = node.left.merged(&node.right, target_id);
610        parents[target_id as usize] = parent;
611    }
612
613    /// Incrementally refits the tree after a small number of leaf updates or
614    /// insertions.
615    ///
616    /// This is a faster alternative to [`Self::refit`] valid only if, since the last
617    /// refit, the only tree modifications were calls to
618    /// [`Self::insert_or_update_partially`], [`Self::insert`],
619    /// [`Self::insert_with_change_detection`], or
620    /// [`Self::reinsert_or_update_with_change_detection`] (in-place updates,
621    /// insertions, or removal-based relocations of leaves). In particular, no leaf
622    /// was removed without being re-inserted in the same batch, and no optimization
623    /// ran. Otherwise, call [`Self::refit`] instead.
624    ///
625    /// Insertions are safe here because `insert_new_unchecked` keeps the tree
626    /// geometrically valid on its own (it enlarges the ancestor AABBs and increments
627    /// their leaf counts during its descent) and creates the new leaf with a pending
628    /// change flag. Any transient change-flag state it leaves behind (the pending
629    /// flag inherited by the wide node that used to hold the insertion sibling, the
630    /// raw-merged flags written by its SAH rotations) lies on the inserted leaf's
631    /// ancestor path, which the walk below rewrites all the way to the root — see
632    /// `refit_path`.
633    ///
634    /// [`BvhLeafUpdateStatus::Inserted`]: super::BvhLeafUpdateStatus::Inserted
635    ///
636    /// `previously_changed` must contain (a superset of) the leaves whose change flag
637    /// was set by the previous refit: their change flag gets cleared. `newly_changed`
638    /// must contain (a superset of) the leaves updated in-place or inserted since the
639    /// last refit: their change flag gets set if their fat AABB actually changed
640    /// (inserted leaves always count as changed).
641    ///
642    /// Unlike [`Self::refit`], this runs in `O(changed * tree_height)` instead of
643    /// `O(node_count)`, but doesn't reorder nodes in memory.
644    pub fn refit_partial(&mut self, previously_changed: &[u32], newly_changed: &[u32]) {
645        // First resolve the change flags of every impacted leaf, and only then walk
646        // their ancestor paths. Walking while some leaves still hold an unresolved
647        // pending flag would propagate that transient state into internal nodes
648        // (a pending internal node reads as "unchanged" during traversals).
649        for leaf in previously_changed {
650            let Some(leaf_node_id) = self.leaf_node_indices.get(*leaf as usize).copied() else {
651                continue;
652            };
653            let data = &mut self.nodes[leaf_node_id].data;
654
655            // Clear the flag of leaves that were changed at the previous refit and
656            // didn't move since. Leaves that moved again (pending) are promoted by
657            // the next loop instead.
658            if !data.is_change_pending() && data.is_changed() {
659                data.resolve_pending_change();
660            }
661        }
662
663        for leaf in newly_changed {
664            let Some(leaf_node_id) = self.leaf_node_indices.get(*leaf as usize).copied() else {
665                continue;
666            };
667            let data = &mut self.nodes[leaf_node_id].data;
668
669            // Promote pending leaves to CHANGED. Leaves whose update stayed within
670            // the change-detection margin have no flag to update.
671            if data.is_change_pending() {
672                data.resolve_pending_change();
673            }
674        }
675
676        for leaf in previously_changed.iter().chain(newly_changed) {
677            let Some(leaf_node_id) = self.leaf_node_indices.get(*leaf as usize).copied() else {
678                continue;
679            };
680            self.refit_path(leaf_node_id);
681        }
682    }
683
684    /// Propagates AABB and change-flag updates from the given node up to the root.
685    ///
686    /// The walk deliberately does NOT stop early when an ancestor's recomputed value
687    /// matches its stored one: leaf insertions can leave transient change-flag state
688    /// (pending flags inherited by the wide node that used to hold the insertion
689    /// sibling, raw-merged flags written by the insertion's SAH rotations) anywhere
690    /// on the inserted leaf's ancestor path. Walking the whole path rewrites every
691    /// ancestor with an exact, normalized recomputation, which is what keeps the
692    /// internal change flags exactly equal to the OR of their descendant leaves'
693    /// flags. An early-out could strand a stale pending flag above the break point,
694    /// and a pending internal node reads as "unchanged" during change-detection
695    /// traversals — hiding every changed leaf underneath. The full walk costs
696    /// O(tree height) per changed leaf, which is fine in the small-change regime
697    /// partial refits are meant for.
698    fn refit_path(&mut self, node: BvhNodeIndex) {
699        let (mut wide_id, _) = node.decompose();
700
701        while wide_id != 0 {
702            let parent = self.parents[wide_id];
703            let wide = &self.nodes[wide_id];
704            let mut recomputed = wide.left.merged(&wide.right, wide_id as u32);
705            recomputed.data.normalize_change_flag();
706
707            self.nodes[parent] = recomputed;
708            (wide_id, _) = parent.decompose();
709        }
710    }
711
712    /// Similar to [`Self::refit`] but without any optimization of the internal node storage layout.
713    ///
714    /// This can be faster than [`Self::refit`] but doesn’t reorder node to be more cache-efficient
715    /// on tree traversals.
716    pub fn refit_without_opt(&mut self) {
717        if self.leaf_count() > 2 {
718            let root = &self.nodes[0];
719            let left = root.left.children;
720            let right = root.right.children;
721            let left_is_leaf = root.left.is_leaf();
722            let right_is_leaf = root.right.is_leaf();
723
724            if !left_is_leaf {
725                self.recurse_refit_without_opt(left, BvhNodeIndex::left(0));
726            }
727            if !right_is_leaf {
728                self.recurse_refit_without_opt(right, BvhNodeIndex::right(0));
729            }
730        }
731    }
732
733    fn recurse_refit_without_opt(&mut self, node_id: u32, parent: BvhNodeIndex) {
734        let node = &self.nodes[node_id as usize];
735        let left = &node.left;
736        let right = &node.right;
737        let left_is_leaf = left.is_leaf();
738        let right_is_leaf = right.is_leaf();
739        let left_children = left.children;
740        let right_children = right.children;
741
742        if !left_is_leaf {
743            self.recurse_refit_without_opt(left_children, BvhNodeIndex::left(node_id));
744        } else {
745            self.nodes[node_id as usize]
746                .left
747                .data
748                .resolve_pending_change();
749        }
750        if !right_is_leaf {
751            self.recurse_refit_without_opt(right_children, BvhNodeIndex::right(node_id));
752        } else {
753            self.nodes[node_id as usize]
754                .right
755                .data
756                .resolve_pending_change();
757        }
758
759        let node = &self.nodes[node_id as usize];
760        let left = &node.left;
761        let right = &node.right;
762        let merged = left.merged(right, node_id);
763
764        self.nodes[parent] = merged;
765    }
766}