Skip to main content

parry3d/partitioning/bvh/
bvh_optimize.rs

1use super::{Bvh, BvhWorkspace};
2use crate::math::Real;
3use core::cmp::Ordering;
4use ordered_float::OrderedFloat;
5
6#[cfg(not(feature = "std"))]
7use crate::math::ComplexField; // For `round` and `sqrt` in no-std+alloc builds.
8
9impl Bvh {
10    fn optimization_config(&self, frame_index: u32) -> OptimizationConfig {
11        const TARGET_REBUILD_NODE_PERCENTAGE: u32 = 5;
12        let num_leaves = self.nodes[0].leaf_count();
13        let num_optimized_leaves = (num_leaves * TARGET_REBUILD_NODE_PERCENTAGE).div_ceil(100);
14
15        let num_leaves_sqrt = (num_leaves as Real).sqrt();
16        let root_mode = if frame_index.is_multiple_of(2) {
17            RootOptimizationMode::Skip
18        } else if (frame_index / 2).is_multiple_of(16) {
19            RootOptimizationMode::BreadthFirst
20        } else {
21            RootOptimizationMode::PriorityQueue
22        };
23
24        let target_root_node_count = num_leaves_sqrt.ceil();
25        let target_subtree_leaf_count = (num_leaves_sqrt * 4.0).ceil();
26        let root_refinement_cost = target_root_node_count * target_root_node_count.log2()
27            / (target_subtree_leaf_count * target_subtree_leaf_count.log2());
28        let mut target_optimized_subtree_count =
29            (num_optimized_leaves as Real / target_subtree_leaf_count - root_refinement_cost)
30                .round()
31                .max(0.0) as usize;
32
33        if root_mode == RootOptimizationMode::Skip {
34            target_optimized_subtree_count = target_optimized_subtree_count.max(1)
35        }
36
37        OptimizationConfig {
38            target_root_node_count: target_root_node_count as usize,
39            target_subtree_leaf_count: target_subtree_leaf_count as usize,
40            target_optimized_subtree_count,
41            root_mode,
42        }
43    }
44
45    /// Incrementally improves tree quality after dynamic updates.
46    ///
47    /// This method performs one step of tree optimization by rebuilding small portions of
48    /// the BVH to maintain good query performance. As objects move around, the tree's structure
49    /// can become suboptimal even after refitting. This method gradually fixes these issues
50    /// without the cost of rebuilding the entire tree.
51    ///
52    /// # What It Does
53    ///
54    /// Each call optimizes:
55    /// - A small percentage of the tree (typically ~5% of leaves)
56    /// - The root region (periodically)
57    /// - Subtrees that have degraded the most
58    ///
59    /// The optimization is **incremental** - it's designed to be called repeatedly (e.g.,
60    /// every few frames) to continuously maintain tree quality without large frame-time spikes.
61    ///
62    /// # When to Use
63    ///
64    /// Call `optimize_incremental`:
65    /// - Every 5-10 frames for highly dynamic scenes
66    /// - After many [`insert`] and [`remove`] operations
67    /// - When query performance degrades over time
68    /// - In physics engines, once per simulation step
69    ///
70    /// **Don't call this**:
71    /// - Every frame (too frequent - diminishing returns)
72    /// - For static scenes (not needed)
73    /// - Immediately after [`from_leaves`] (tree is already optimal)
74    ///
75    /// # Arguments
76    ///
77    /// * `workspace` - A reusable workspace to avoid allocations. The same workspace
78    ///   can be shared across multiple BVH operations.
79    ///
80    /// # Performance
81    ///
82    /// - **Time**: O(k log k) where k is the number of leaves optimized (~5% of total)
83    /// - **Target**: <1ms for trees with 10,000 leaves (on modern hardware)
84    /// - Spreads optimization cost over many frames
85    /// - Much cheaper than full rebuild (O(n log n))
86    ///
87    /// # Examples
88    ///
89    /// ## In a game loop
90    ///
91    /// ```
92    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
93    /// use parry3d::partitioning::{Bvh, BvhWorkspace};
94    /// use parry3d::bounding_volume::Aabb;
95    /// use parry3d::math::Vector;
96    ///
97    /// let mut bvh = Bvh::new();
98    /// let mut workspace = BvhWorkspace::default();
99    ///
100    /// // Add initial objects
101    /// for i in 0..1000 {
102    ///     let aabb = Aabb::new(
103    ///         Vector::new(i as f32, 0.0, 0.0),
104    ///         Vector::new(i as f32 + 1.0, 1.0, 1.0)
105    ///     );
106    ///     bvh.insert(aabb, i);
107    /// }
108    ///
109    /// // Game loop
110    /// for frame in 0..1000 {
111    ///     // Update object positions every frame
112    ///     for i in 0..1000 {
113    ///         let time = frame as f32 * 0.016;
114    ///         let offset = (time * (i as f32 + 1.0)).sin() * 5.0;
115    ///         let aabb = Aabb::new(
116    ///             Vector::new(i as f32 + offset, 0.0, 0.0),
117    ///             Vector::new(i as f32 + offset + 1.0, 1.0, 1.0)
118    ///         );
119    ///         bvh.insert_or_update_partially(aabb, i, 0.0);
120    ///     }
121    ///
122    ///     // Update AABBs every frame (fast)
123    ///     bvh.refit(&mut workspace);
124    ///
125    ///     // Optimize tree quality every 5 frames (slower but important)
126    ///     if frame % 5 == 0 {
127    ///         bvh.optimize_incremental(&mut workspace);
128    ///     }
129    ///
130    ///     // Perform queries (ray casts, collision detection, etc.)
131    /// }
132    /// # }
133    /// ```
134    ///
135    /// ## Physics engine broad-phase
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    /// // Simulation loop
147    /// for step in 0..100 {
148    ///     // Physics update: integrate velocities, forces, etc.
149    ///     // Update BVH with new rigid body positions
150    ///     for body_id in 0..100 {
151    ///         let aabb = get_body_aabb(body_id);
152    ///         bvh.insert_or_update_partially(aabb, body_id, 0.0);
153    ///     }
154    ///
155    ///     // Refit tree for new positions
156    ///     bvh.refit(&mut workspace);
157    ///
158    ///     // Optimize tree quality once per step
159    ///     bvh.optimize_incremental(&mut workspace);
160    ///
161    ///     // Broad-phase collision detection
162    ///     // let pairs = find_overlapping_pairs(&bvh);
163    ///     // ...
164    /// }
165    ///
166    /// # fn get_body_aabb(id: u32) -> Aabb {
167    /// #     Aabb::new(Vector::ZERO, Vector::new(1.0, 1.0, 1.0))
168    /// # }
169    /// # }
170    /// ```
171    ///
172    /// ## After many insertions/removals
173    ///
174    /// ```
175    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
176    /// use parry3d::partitioning::{Bvh, BvhWorkspace};
177    /// use parry3d::bounding_volume::Aabb;
178    /// use parry3d::math::Vector;
179    ///
180    /// let mut bvh = Bvh::new();
181    /// let mut workspace = BvhWorkspace::default();
182    ///
183    /// // Dynamically add many objects over time
184    /// for i in 0..1000 {
185    ///     let aabb = Aabb::new(
186    ///         Vector::new(i as f32, 0.0, 0.0),
187    ///         Vector::new(i as f32 + 1.0, 1.0, 1.0)
188    ///     );
189    ///     bvh.insert(aabb, i);
190    ///
191    ///     // Periodically optimize while building
192    ///     if i % 100 == 0 && i > 0 {
193    ///         bvh.optimize_incremental(&mut workspace);
194    ///     }
195    /// }
196    ///
197    /// // Tree is now well-optimized despite incremental construction
198    /// # }
199    /// ```
200    ///
201    /// # How It Works
202    ///
203    /// The method uses a sophisticated strategy:
204    ///
205    /// 1. **Subtree Selection**: Identifies small subtrees that would benefit most from rebuilding
206    /// 2. **Local Rebuild**: Rebuilds selected subtrees using the binned construction algorithm
207    /// 3. **Root Optimization**: Periodically optimizes the top levels of the tree
208    /// 4. **Rotation**: Tracks changes and rotates subtrees incrementally
209    ///
210    /// The optimization rotates through different parts of the tree on successive calls,
211    /// ensuring the entire tree is eventually optimized.
212    ///
213    /// # Comparison with Full Rebuild
214    ///
215    /// | Operation | Time | When to Use |
216    /// |-----------|------|-------------|
217    /// | `optimize_incremental` | O(k log k), k ≈ 5% of n | Every few frames in dynamic scenes |
218    /// | `from_leaves` (rebuild) | O(n log n) | When tree is very poor or scene is static |
219    ///
220    /// # Notes
221    ///
222    /// - State persists across calls - each call continues from where the last left off
223    /// - Call frequency can be adjusted based on performance budget
224    /// - Complementary to [`refit`] - use both for best results
225    /// - The workspace stores optimization state between calls
226    ///
227    /// # See Also
228    ///
229    /// - [`refit`](Bvh::refit) - Update AABBs after leaf movements (call more frequently)
230    /// - [`from_leaves`](Bvh::from_leaves) - Full rebuild (for major scene changes)
231    /// - [`BvhWorkspace`] - Reusable workspace for operations
232    ///
233    /// [`insert`]: Bvh::insert
234    /// [`remove`]: Bvh::remove
235    /// [`from_leaves`]: Bvh::from_leaves
236    /// [`refit`]: Bvh::refit
237    pub fn optimize_incremental(&mut self, workspace: &mut BvhWorkspace) {
238        if self.nodes.is_empty() {
239            return;
240        }
241
242        workspace.rebuild_leaves.clear();
243        self.optimization.rebuild_frame_index =
244            self.optimization.rebuild_frame_index.overflowing_add(1).0;
245        let config = self.optimization_config(self.optimization.rebuild_frame_index);
246
247        /*
248         * Subtree optimizations.
249         */
250        // let t0 = core::time::Instant::now();
251        let num_leaves = self.nodes[0].leaf_count();
252        let mut start_index = self.optimization.rebuild_start_index;
253
254        // println!("Max candidate leaf count = {}", max_candidate_leaf_count);
255        self.find_optimization_roots(
256            workspace,
257            0,
258            &mut start_index,
259            config.target_optimized_subtree_count as u32,
260            0,
261            config.target_subtree_leaf_count as u32,
262        );
263
264        if start_index >= num_leaves {
265            start_index = 0;
266            // TODO: if we hit the end of the tree, wrap back at the beginning
267            //       to reach the target subtree count.
268        }
269
270        self.optimization.rebuild_start_index = start_index;
271
272        // println!(
273        //     "Num refinement candidates: {}, list: {:?}",
274        //     workspace.optimization_roots.len(),
275        //     workspace.optimization_roots
276        // );
277
278        /*
279         * Root optimization.
280         */
281        workspace.rebuild_leaves.clear();
282
283        // let t0 = core::time::Instant::now();
284        match config.root_mode {
285            RootOptimizationMode::BreadthFirst => self
286                .find_root_optimization_pseudo_leaves_breadth_first(
287                    workspace,
288                    config.target_root_node_count,
289                ),
290            RootOptimizationMode::PriorityQueue => self
291                .find_root_optimization_pseudo_leaves_pqueue(
292                    workspace,
293                    config.target_root_node_count,
294                ),
295            RootOptimizationMode::Skip => {}
296        }
297
298        if !workspace.rebuild_leaves.is_empty() {
299            self.rebuild_range_binned(0, &mut workspace.rebuild_leaves);
300        }
301        // println!("Root optimization: {}", t0.elapsed().as_secs_f32() * 1000.0);
302
303        /*
304         * Subtree leaf optimizations.
305         */
306        for i in 0..workspace.optimization_roots.len() {
307            let subtree_root_id = workspace.optimization_roots[i];
308            workspace.rebuild_leaves.clear();
309            self.collect_leaves(workspace, subtree_root_id);
310
311            // let t1 = core::time::Instant::now();
312            self.rebuild_range_binned(subtree_root_id, &mut workspace.rebuild_leaves);
313            // println!(
314            //     "Optimized leaves: {}, time: {}",
315            //     workspace.rebuild_leaves.len(),
316            //     t1.elapsed().as_secs_f32() * 1000.0
317            // );
318        }
319
320        // println!(
321        //     "Leaf optimization: {}, optimization roots: {}, config: {:?}",
322        //     t0.elapsed().as_secs_f32() * 1000.0,
323        //     workspace.optimization_roots.len(),
324        //     config
325        // );
326        workspace.optimization_roots.clear();
327    }
328
329    fn collect_leaves(&self, workspace: &mut BvhWorkspace, subtree_root: u32) {
330        let node = &self.nodes[subtree_root as usize];
331        let left = &node.left;
332        let right = &node.right;
333
334        if left.is_leaf() {
335            workspace.rebuild_leaves.push(*left);
336        } else {
337            self.collect_leaves(workspace, left.children);
338        }
339
340        if right.is_leaf() {
341            workspace.rebuild_leaves.push(*right);
342        } else {
343            self.collect_leaves(workspace, right.children);
344        }
345    }
346
347    fn find_root_optimization_pseudo_leaves_breadth_first(
348        &mut self,
349        workspace: &mut BvhWorkspace,
350        target_count: usize,
351    ) {
352        if self.nodes.len() < 2 {
353            return;
354        }
355
356        workspace.dequeue.push_back(0);
357
358        while workspace.dequeue.len() + workspace.rebuild_leaves.len() < target_count {
359            let Some(curr_node) = workspace.dequeue.pop_front() else {
360                break;
361            };
362
363            let node = &self.nodes[curr_node as usize];
364            let left = &node.left;
365            let right = &node.right;
366
367            if left.is_leaf() || left.data.is_change_pending() {
368                workspace.rebuild_leaves.push(*left);
369            } else {
370                workspace.dequeue.push_back(left.children);
371            }
372
373            if right.is_leaf() || right.data.is_change_pending() {
374                workspace.rebuild_leaves.push(*right);
375            } else {
376                workspace.dequeue.push_back(right.children);
377            }
378        }
379
380        for id in workspace.dequeue.drain(..) {
381            let node = &self.nodes[id as usize];
382            workspace.rebuild_leaves.push(node.left);
383            workspace.rebuild_leaves.push(node.right);
384        }
385    }
386
387    fn find_root_optimization_pseudo_leaves_pqueue(
388        &mut self,
389        workspace: &mut BvhWorkspace,
390        target_count: usize,
391    ) {
392        if self.nodes.len() < 2 {
393            return;
394        }
395
396        workspace.queue.push(BvhOptimizationHeapEntry {
397            score: OrderedFloat(Real::MAX),
398            id: 0,
399        });
400
401        while workspace.queue.len() + workspace.rebuild_leaves.len() < target_count {
402            let Some(curr_node) = workspace.queue.pop() else {
403                break;
404            };
405
406            let node = &self.nodes[curr_node.id as usize];
407            let left = &node.left;
408            let right = &node.right;
409
410            if left.is_leaf() || left.data.is_change_pending() {
411                workspace.rebuild_leaves.push(*left);
412            } else {
413                let children = self.nodes[left.children as usize];
414                let left_score = children.left.volume() * children.left.leaf_count() as Real
415                    + children.right.volume() * children.right.leaf_count() as Real;
416                workspace.queue.push(BvhOptimizationHeapEntry {
417                    score: OrderedFloat(left_score),
418                    id: left.children,
419                });
420            }
421
422            if right.is_leaf() || right.data.is_change_pending() {
423                workspace.rebuild_leaves.push(*right);
424            } else {
425                let children = self.nodes[right.children as usize];
426                let right_score = children.left.volume() * children.left.leaf_count() as Real
427                    + children.right.volume() * children.right.leaf_count() as Real;
428                workspace.queue.push(BvhOptimizationHeapEntry {
429                    score: OrderedFloat(right_score),
430                    id: right.children,
431                });
432            }
433        }
434
435        for id in workspace.queue.as_slice() {
436            let node = &self.nodes[id.id as usize];
437            workspace.rebuild_leaves.push(node.left);
438            workspace.rebuild_leaves.push(node.right);
439        }
440        workspace.queue.clear();
441    }
442
443    fn find_optimization_roots(
444        &mut self,
445        workspace: &mut BvhWorkspace,
446        curr_node: u32,
447        start_index: &mut u32,
448        max_optimization_roots: u32,
449        mut leaf_count_before: u32,
450        max_candidate_leaf_count: u32,
451    ) {
452        if workspace.optimization_roots.len() == max_optimization_roots as usize {
453            // We reached the desired number of collected leaves. Just exit.
454            return;
455        }
456
457        let node = &mut self.nodes[curr_node as usize];
458        let left = &mut node.left;
459        let left_leaf_count = left.leaf_count();
460        let left_children = left.children;
461
462        if leaf_count_before + left_leaf_count > *start_index {
463            // Traverse the left children.
464            if left_leaf_count < max_candidate_leaf_count {
465                // If the node doesn’t have at least the leaves, it can’t be rebalanced.
466                if left_leaf_count > 2 {
467                    // Mark the optimization root so that the root pseudo-leaf
468                    // extraction knows not to cross this node.
469                    // This won’t disturb bvtt traversal because refit will get
470                    // rid of this flag.
471                    left.data.set_change_pending();
472                    workspace.optimization_roots.push(left_children);
473                    *start_index += left_leaf_count;
474                }
475            } else {
476                // This node has too many leaves. Recurse.
477                self.find_optimization_roots(
478                    workspace,
479                    left_children,
480                    start_index,
481                    max_optimization_roots,
482                    leaf_count_before,
483                    max_candidate_leaf_count,
484                );
485            }
486        }
487
488        leaf_count_before += left_leaf_count;
489
490        if workspace.optimization_roots.len() == max_optimization_roots as usize {
491            // We reached the desired number of collected leaves. Just exit.
492            return;
493        }
494
495        let node = &mut self.nodes[curr_node as usize];
496        let right = &mut node.right;
497        let right_leaf_count = right.leaf_count();
498        let right_children = right.children;
499
500        if leaf_count_before + right_leaf_count > *start_index {
501            // Traverse the right children.
502            if right_leaf_count < max_candidate_leaf_count {
503                if right_leaf_count > 2 {
504                    // Mark the optimization root so that the root pseudo-leaf
505                    // extraction knows not to cross this node.
506                    // This won’t disturb bvtt traversal because refit will get
507                    // rid of this flag.
508                    right.data.set_change_pending();
509                    workspace.optimization_roots.push(right_children);
510                    *start_index += right_leaf_count;
511                }
512            } else {
513                self.find_optimization_roots(
514                    workspace,
515                    right_children,
516                    start_index,
517                    max_optimization_roots,
518                    leaf_count_before,
519                    max_candidate_leaf_count,
520                );
521            }
522        }
523    }
524}
525
526/// The optimization state used by `Bvh::optimize_incremental`.
527/// This allows each call to `optimize_incremental` to continue from where the last one left off.
528#[derive(Clone, Debug, Default)]
529#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
530#[cfg_attr(
531    feature = "rkyv",
532    derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
533)]
534pub(super) struct BvhIncrementalOptimizationState {
535    pub(super) rebuild_frame_index: u32,
536    pub(super) rebuild_start_index: u32,
537}
538
539#[derive(Copy, Clone, PartialEq, Eq, Debug)]
540enum RootOptimizationMode {
541    PriorityQueue,
542    BreadthFirst,
543    Skip,
544}
545
546#[derive(Copy, Clone, PartialEq, Eq, Debug)]
547struct OptimizationConfig {
548    target_root_node_count: usize,
549    target_subtree_leaf_count: usize,
550    target_optimized_subtree_count: usize,
551    root_mode: RootOptimizationMode,
552}
553
554#[derive(Copy, Clone, Debug)]
555pub struct BvhOptimizationHeapEntry {
556    score: OrderedFloat<Real>,
557    id: u32,
558}
559
560impl PartialOrd for BvhOptimizationHeapEntry {
561    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
562        Some(self.cmp(other))
563    }
564}
565
566impl Ord for BvhOptimizationHeapEntry {
567    fn cmp(&self, other: &Self) -> Ordering {
568        self.score.cmp(&other.score)
569    }
570}
571
572impl PartialEq for BvhOptimizationHeapEntry {
573    fn eq(&self, other: &Self) -> bool {
574        self.score == other.score
575    }
576}
577
578impl Eq for BvhOptimizationHeapEntry {}