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 na::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 nalgebra::Point3;
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    ///         Point3::new(i as f32, 0.0, 0.0),
104    ///         Point3::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    ///             Point3::new(i as f32 + offset, 0.0, 0.0),
117    ///             Point3::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 nalgebra::Point3;
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(Point3::origin(), Point3::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 nalgebra::Point3;
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    ///         Point3::new(i as f32, 0.0, 0.0),
187    ///         Point3::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        workspace.rebuild_frame_index = workspace.rebuild_frame_index.overflowing_add(1).0;
244        let config = self.optimization_config(workspace.rebuild_frame_index);
245
246        /*
247         * Subtree optimizations.
248         */
249        // let t0 = core::time::Instant::now();
250        let num_leaves = self.nodes[0].leaf_count();
251        let mut start_index = workspace.rebuild_start_index;
252
253        // println!("Max candidate leaf count = {}", max_candidate_leaf_count);
254        self.find_optimization_roots(
255            workspace,
256            0,
257            &mut start_index,
258            config.target_optimized_subtree_count as u32,
259            0,
260            config.target_subtree_leaf_count as u32,
261        );
262
263        if start_index >= num_leaves {
264            start_index = 0;
265            // TODO: if we hit the end of the tree, wrap back at the beginning
266            //       to reach the target subtree count.
267        }
268
269        workspace.rebuild_start_index = start_index;
270
271        // println!(
272        //     "Num refinement candidates: {}, list: {:?}",
273        //     workspace.optimization_roots.len(),
274        //     workspace.optimization_roots
275        // );
276
277        /*
278         * Root optimization.
279         */
280        workspace.rebuild_leaves.clear();
281
282        // let t0 = core::time::Instant::now();
283        match config.root_mode {
284            RootOptimizationMode::BreadthFirst => self
285                .find_root_optimization_pseudo_leaves_breadth_first(
286                    workspace,
287                    config.target_root_node_count,
288                ),
289            RootOptimizationMode::PriorityQueue => self
290                .find_root_optimization_pseudo_leaves_pqueue(
291                    workspace,
292                    config.target_root_node_count,
293                ),
294            RootOptimizationMode::Skip => {}
295        }
296
297        if !workspace.rebuild_leaves.is_empty() {
298            self.rebuild_range_binned(0, &mut workspace.rebuild_leaves);
299        }
300        // println!("Root optimization: {}", t0.elapsed().as_secs_f32() * 1000.0);
301
302        /*
303         * Subtree leaf optimizations.
304         */
305        for i in 0..workspace.optimization_roots.len() {
306            let subtree_root_id = workspace.optimization_roots[i];
307            workspace.rebuild_leaves.clear();
308            self.collect_leaves(workspace, subtree_root_id);
309
310            // let t1 = core::time::Instant::now();
311            self.rebuild_range_binned(subtree_root_id, &mut workspace.rebuild_leaves);
312            // println!(
313            //     "Optimized leaves: {}, time: {}",
314            //     workspace.rebuild_leaves.len(),
315            //     t1.elapsed().as_secs_f32() * 1000.0
316            // );
317        }
318
319        // println!(
320        //     "Leaf optimization: {}, optimization roots: {}, config: {:?}",
321        //     t0.elapsed().as_secs_f32() * 1000.0,
322        //     workspace.optimization_roots.len(),
323        //     config
324        // );
325        workspace.optimization_roots.clear();
326    }
327
328    fn collect_leaves(&self, workspace: &mut BvhWorkspace, subtree_root: u32) {
329        let node = &self.nodes[subtree_root as usize];
330        let left = &node.left;
331        let right = &node.right;
332
333        if left.is_leaf() {
334            workspace.rebuild_leaves.push(*left);
335        } else {
336            self.collect_leaves(workspace, left.children);
337        }
338
339        if right.is_leaf() {
340            workspace.rebuild_leaves.push(*right);
341        } else {
342            self.collect_leaves(workspace, right.children);
343        }
344    }
345
346    fn find_root_optimization_pseudo_leaves_breadth_first(
347        &mut self,
348        workspace: &mut BvhWorkspace,
349        target_count: usize,
350    ) {
351        if self.nodes.len() < 2 {
352            return;
353        }
354
355        workspace.dequeue.push_back(0);
356
357        while workspace.dequeue.len() + workspace.rebuild_leaves.len() < target_count {
358            let Some(curr_node) = workspace.dequeue.pop_front() else {
359                break;
360            };
361
362            let node = &self.nodes[curr_node as usize];
363            let left = &node.left;
364            let right = &node.right;
365
366            if left.is_leaf() || left.data.is_change_pending() {
367                workspace.rebuild_leaves.push(*left);
368            } else {
369                workspace.dequeue.push_back(left.children);
370            }
371
372            if right.is_leaf() || right.data.is_change_pending() {
373                workspace.rebuild_leaves.push(*right);
374            } else {
375                workspace.dequeue.push_back(right.children);
376            }
377        }
378
379        for id in workspace.dequeue.drain(..) {
380            let node = &self.nodes[id as usize];
381            workspace.rebuild_leaves.push(node.left);
382            workspace.rebuild_leaves.push(node.right);
383        }
384    }
385
386    fn find_root_optimization_pseudo_leaves_pqueue(
387        &mut self,
388        workspace: &mut BvhWorkspace,
389        target_count: usize,
390    ) {
391        if self.nodes.len() < 2 {
392            return;
393        }
394
395        workspace.queue.push(BvhOptimizationHeapEntry {
396            score: OrderedFloat(Real::MAX),
397            id: 0,
398        });
399
400        while workspace.queue.len() + workspace.rebuild_leaves.len() < target_count {
401            let Some(curr_node) = workspace.queue.pop() else {
402                break;
403            };
404
405            let node = &self.nodes[curr_node.id as usize];
406            let left = &node.left;
407            let right = &node.right;
408
409            if left.is_leaf() || left.data.is_change_pending() {
410                workspace.rebuild_leaves.push(*left);
411            } else {
412                let children = self.nodes[left.children as usize];
413                let left_score = children.left.volume() * children.left.leaf_count() as Real
414                    + children.right.volume() * children.right.leaf_count() as Real;
415                workspace.queue.push(BvhOptimizationHeapEntry {
416                    score: OrderedFloat(left_score),
417                    id: left.children,
418                });
419            }
420
421            if right.is_leaf() || right.data.is_change_pending() {
422                workspace.rebuild_leaves.push(*right);
423            } else {
424                let children = self.nodes[right.children as usize];
425                let right_score = children.left.volume() * children.left.leaf_count() as Real
426                    + children.right.volume() * children.right.leaf_count() as Real;
427                workspace.queue.push(BvhOptimizationHeapEntry {
428                    score: OrderedFloat(right_score),
429                    id: right.children,
430                });
431            }
432        }
433
434        for id in workspace.queue.as_slice() {
435            let node = &self.nodes[id.id as usize];
436            workspace.rebuild_leaves.push(node.left);
437            workspace.rebuild_leaves.push(node.right);
438        }
439        workspace.queue.clear();
440    }
441
442    fn find_optimization_roots(
443        &mut self,
444        workspace: &mut BvhWorkspace,
445        curr_node: u32,
446        start_index: &mut u32,
447        max_optimization_roots: u32,
448        mut leaf_count_before: u32,
449        max_candidate_leaf_count: u32,
450    ) {
451        if workspace.optimization_roots.len() == max_optimization_roots as usize {
452            // We reached the desired number of collected leaves. Just exit.
453            return;
454        }
455
456        let node = &mut self.nodes[curr_node as usize];
457        let left = &mut node.left;
458        let left_leaf_count = left.leaf_count();
459        let left_children = left.children;
460
461        if leaf_count_before + left_leaf_count > *start_index {
462            // Traverse the left children.
463            if left_leaf_count < max_candidate_leaf_count {
464                // If the node doesn’t have at least the leaves, it can’t be rebalanced.
465                if left_leaf_count > 2 {
466                    // Mark the optimization root so that the root pseudo-leaf
467                    // extraction knows not to cross this node.
468                    // This won’t disturb bvtt traversal because refit will get
469                    // rid of this flag.
470                    left.data.set_change_pending();
471                    workspace.optimization_roots.push(left_children);
472                    *start_index += left_leaf_count;
473                }
474            } else {
475                // This node has too many leaves. Recurse.
476                self.find_optimization_roots(
477                    workspace,
478                    left_children,
479                    start_index,
480                    max_optimization_roots,
481                    leaf_count_before,
482                    max_candidate_leaf_count,
483                );
484            }
485        }
486
487        leaf_count_before += left_leaf_count;
488
489        if workspace.optimization_roots.len() == max_optimization_roots as usize {
490            // We reached the desired number of collected leaves. Just exit.
491            return;
492        }
493
494        let node = &mut self.nodes[curr_node as usize];
495        let right = &mut node.right;
496        let right_leaf_count = right.leaf_count();
497        let right_children = right.children;
498
499        if leaf_count_before + right_leaf_count > *start_index {
500            // Traverse the right children.
501            if right_leaf_count < max_candidate_leaf_count {
502                if right_leaf_count > 2 {
503                    // Mark the optimization root so that the root pseudo-leaf
504                    // extraction knows not to cross this node.
505                    // This won’t disturb bvtt traversal because refit will get
506                    // rid of this flag.
507                    right.data.set_change_pending();
508                    workspace.optimization_roots.push(right_children);
509                    *start_index += right_leaf_count;
510                }
511            } else {
512                self.find_optimization_roots(
513                    workspace,
514                    right_children,
515                    start_index,
516                    max_optimization_roots,
517                    leaf_count_before,
518                    max_candidate_leaf_count,
519                );
520            }
521        }
522    }
523}
524
525#[derive(Copy, Clone, PartialEq, Eq, Debug)]
526enum RootOptimizationMode {
527    PriorityQueue,
528    BreadthFirst,
529    Skip,
530}
531
532#[derive(Copy, Clone, PartialEq, Eq, Debug)]
533struct OptimizationConfig {
534    target_root_node_count: usize,
535    target_subtree_leaf_count: usize,
536    target_optimized_subtree_count: usize,
537    root_mode: RootOptimizationMode,
538}
539
540#[derive(Copy, Clone, Debug)]
541pub struct BvhOptimizationHeapEntry {
542    score: OrderedFloat<Real>,
543    id: u32,
544}
545
546impl PartialOrd for BvhOptimizationHeapEntry {
547    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
548        Some(self.cmp(other))
549    }
550}
551
552impl Ord for BvhOptimizationHeapEntry {
553    fn cmp(&self, other: &Self) -> Ordering {
554        self.score.cmp(&other.score)
555    }
556}
557
558impl PartialEq for BvhOptimizationHeapEntry {
559    fn eq(&self, other: &Self) -> bool {
560        self.score == other.score
561    }
562}
563
564impl Eq for BvhOptimizationHeapEntry {}