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