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 {}