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}