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 % 2 == 0 {
17            RootOptimizationMode::Skip
18        } else if (frame_index / 2) % 16 == 0 {
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    /// Performs one step of incremental optimization of the tree.
46    ///
47    /// Incremental optimization of the tree ensures that after modification of the tree’s geometry
48    /// (after changes of its leaves AABBs) doesn’t lead to a poorly shaped tree, or tree with poor
49    /// spatial culling capabilities.
50    ///
51    /// This optimization is incremental because it only optimizes a subset of the tree in order
52    /// to reduce its computational cost. It is indented to be called multiple times across
53    /// multiple frames.
54    ///
55    /// For example, if the BVH is used for a physics engine’s broad-phase, this method would
56    /// typically be called once by simulation step.
57    pub fn optimize_incremental(&mut self, workspace: &mut BvhWorkspace) {
58        if self.nodes.is_empty() {
59            return;
60        }
61
62        workspace.rebuild_leaves.clear();
63        workspace.rebuild_frame_index = workspace.rebuild_frame_index.overflowing_add(1).0;
64        let config = self.optimization_config(workspace.rebuild_frame_index);
65
66        /*
67         * Subtree optimizations.
68         */
69        // let t0 = core::time::Instant::now();
70        let num_leaves = self.nodes[0].leaf_count();
71        let mut start_index = workspace.rebuild_start_index;
72
73        // println!("Max candidate leaf count = {}", max_candidate_leaf_count);
74        self.find_optimization_roots(
75            workspace,
76            0,
77            &mut start_index,
78            config.target_optimized_subtree_count as u32,
79            0,
80            config.target_subtree_leaf_count as u32,
81        );
82
83        if start_index >= num_leaves {
84            start_index = 0;
85            // TODO: if we hit the end of the tree, wrap back at the beginning
86            //       to reach the target subtree count.
87        }
88
89        workspace.rebuild_start_index = start_index;
90
91        // println!(
92        //     "Num refinement candidates: {}, list: {:?}",
93        //     workspace.optimization_roots.len(),
94        //     workspace.optimization_roots
95        // );
96
97        /*
98         * Root optimization.
99         */
100        workspace.rebuild_leaves.clear();
101
102        // let t0 = core::time::Instant::now();
103        match config.root_mode {
104            RootOptimizationMode::BreadthFirst => self
105                .find_root_optimization_pseudo_leaves_breadth_first(
106                    workspace,
107                    config.target_root_node_count,
108                ),
109            RootOptimizationMode::PriorityQueue => self
110                .find_root_optimization_pseudo_leaves_pqueue(
111                    workspace,
112                    config.target_root_node_count,
113                ),
114            RootOptimizationMode::Skip => {}
115        }
116
117        if !workspace.rebuild_leaves.is_empty() {
118            self.rebuild_range_binned(0, &mut workspace.rebuild_leaves);
119        }
120        // println!("Root optimization: {}", t0.elapsed().as_secs_f32() * 1000.0);
121
122        /*
123         * Subtree leaf optimizations.
124         */
125        for i in 0..workspace.optimization_roots.len() {
126            let subtree_root_id = workspace.optimization_roots[i];
127            workspace.rebuild_leaves.clear();
128            self.collect_leaves(workspace, subtree_root_id);
129
130            // let t1 = core::time::Instant::now();
131            self.rebuild_range_binned(subtree_root_id, &mut workspace.rebuild_leaves);
132            // println!(
133            //     "Optimized leaves: {}, time: {}",
134            //     workspace.rebuild_leaves.len(),
135            //     t1.elapsed().as_secs_f32() * 1000.0
136            // );
137        }
138
139        // println!(
140        //     "Leaf optimization: {}, optimization roots: {}, config: {:?}",
141        //     t0.elapsed().as_secs_f32() * 1000.0,
142        //     workspace.optimization_roots.len(),
143        //     config
144        // );
145        workspace.optimization_roots.clear();
146    }
147
148    fn collect_leaves(&self, workspace: &mut BvhWorkspace, subtree_root: u32) {
149        let node = &self.nodes[subtree_root as usize];
150        let left = &node.left;
151        let right = &node.right;
152
153        if left.is_leaf() {
154            workspace.rebuild_leaves.push(*left);
155        } else {
156            self.collect_leaves(workspace, left.children);
157        }
158
159        if right.is_leaf() {
160            workspace.rebuild_leaves.push(*right);
161        } else {
162            self.collect_leaves(workspace, right.children);
163        }
164    }
165
166    fn find_root_optimization_pseudo_leaves_breadth_first(
167        &mut self,
168        workspace: &mut BvhWorkspace,
169        target_count: usize,
170    ) {
171        if self.nodes.len() < 2 {
172            return;
173        }
174
175        workspace.dequeue.push_back(0);
176
177        while workspace.dequeue.len() + workspace.rebuild_leaves.len() < target_count {
178            let Some(curr_node) = workspace.dequeue.pop_front() else {
179                break;
180            };
181
182            let node = &self.nodes[curr_node as usize];
183            let left = &node.left;
184            let right = &node.right;
185
186            if left.is_leaf() || left.data.is_change_pending() {
187                workspace.rebuild_leaves.push(*left);
188            } else {
189                workspace.dequeue.push_back(left.children);
190            }
191
192            if right.is_leaf() || right.data.is_change_pending() {
193                workspace.rebuild_leaves.push(*right);
194            } else {
195                workspace.dequeue.push_back(right.children);
196            }
197        }
198
199        for id in workspace.dequeue.drain(..) {
200            let node = &self.nodes[id as usize];
201            workspace.rebuild_leaves.push(node.left);
202            workspace.rebuild_leaves.push(node.right);
203        }
204    }
205
206    fn find_root_optimization_pseudo_leaves_pqueue(
207        &mut self,
208        workspace: &mut BvhWorkspace,
209        target_count: usize,
210    ) {
211        if self.nodes.len() < 2 {
212            return;
213        }
214
215        workspace.queue.push(BvhOptimizationHeapEntry {
216            score: OrderedFloat(Real::MAX),
217            id: 0,
218        });
219
220        while workspace.queue.len() + workspace.rebuild_leaves.len() < target_count {
221            let Some(curr_node) = workspace.queue.pop() else {
222                break;
223            };
224
225            let node = &self.nodes[curr_node.id as usize];
226            let left = &node.left;
227            let right = &node.right;
228
229            if left.is_leaf() || left.data.is_change_pending() {
230                workspace.rebuild_leaves.push(*left);
231            } else {
232                let children = self.nodes[left.children as usize];
233                let left_score = children.left.volume() * children.left.leaf_count() as Real
234                    + children.right.volume() * children.right.leaf_count() as Real;
235                workspace.queue.push(BvhOptimizationHeapEntry {
236                    score: OrderedFloat(left_score),
237                    id: left.children,
238                });
239            }
240
241            if right.is_leaf() || right.data.is_change_pending() {
242                workspace.rebuild_leaves.push(*right);
243            } else {
244                let children = self.nodes[right.children as usize];
245                let right_score = children.left.volume() * children.left.leaf_count() as Real
246                    + children.right.volume() * children.right.leaf_count() as Real;
247                workspace.queue.push(BvhOptimizationHeapEntry {
248                    score: OrderedFloat(right_score),
249                    id: right.children,
250                });
251            }
252        }
253
254        for id in workspace.queue.as_slice() {
255            let node = &self.nodes[id.id as usize];
256            workspace.rebuild_leaves.push(node.left);
257            workspace.rebuild_leaves.push(node.right);
258        }
259        workspace.queue.clear();
260    }
261
262    fn find_optimization_roots(
263        &mut self,
264        workspace: &mut BvhWorkspace,
265        curr_node: u32,
266        start_index: &mut u32,
267        max_optimization_roots: u32,
268        mut leaf_count_before: u32,
269        max_candidate_leaf_count: u32,
270    ) {
271        if workspace.optimization_roots.len() == max_optimization_roots as usize {
272            // We reached the desired number of collected leaves. Just exit.
273            return;
274        }
275
276        let node = &mut self.nodes[curr_node as usize];
277        let left = &mut node.left;
278        let left_leaf_count = left.leaf_count();
279        let left_children = left.children;
280
281        if leaf_count_before + left_leaf_count > *start_index {
282            // Traverse the left children.
283            if left_leaf_count < max_candidate_leaf_count {
284                // If the node doesn’t have at least the leaves, it can’t be rebalanced.
285                if left_leaf_count > 2 {
286                    // Mark the optimization root so that the root pseudo-leaf
287                    // extraction knows not to cross this node.
288                    // This won’t disturb bvtt traversal because refit will get
289                    // rid of this flag.
290                    left.data.set_change_pending();
291                    workspace.optimization_roots.push(left_children);
292                    *start_index += left_leaf_count;
293                }
294            } else {
295                // This node has too many leaves. Recurse.
296                self.find_optimization_roots(
297                    workspace,
298                    left_children,
299                    start_index,
300                    max_optimization_roots,
301                    leaf_count_before,
302                    max_candidate_leaf_count,
303                );
304            }
305        }
306
307        leaf_count_before += left_leaf_count;
308
309        if workspace.optimization_roots.len() == max_optimization_roots as usize {
310            // We reached the desired number of collected leaves. Just exit.
311            return;
312        }
313
314        let node = &mut self.nodes[curr_node as usize];
315        let right = &mut node.right;
316        let right_leaf_count = right.leaf_count();
317        let right_children = right.children;
318
319        if leaf_count_before + right_leaf_count > *start_index {
320            // Traverse the right children.
321            if right_leaf_count < max_candidate_leaf_count {
322                if right_leaf_count > 2 {
323                    // Mark the optimization root so that the root pseudo-leaf
324                    // extraction knows not to cross this node.
325                    // This won’t disturb bvtt traversal because refit will get
326                    // rid of this flag.
327                    right.data.set_change_pending();
328                    workspace.optimization_roots.push(right_children);
329                    *start_index += right_leaf_count;
330                }
331            } else {
332                self.find_optimization_roots(
333                    workspace,
334                    right_children,
335                    start_index,
336                    max_optimization_roots,
337                    leaf_count_before,
338                    max_candidate_leaf_count,
339                );
340            }
341        }
342    }
343}
344
345#[derive(Copy, Clone, PartialEq, Eq, Debug)]
346enum RootOptimizationMode {
347    PriorityQueue,
348    BreadthFirst,
349    Skip,
350}
351
352#[derive(Copy, Clone, PartialEq, Eq, Debug)]
353struct OptimizationConfig {
354    target_root_node_count: usize,
355    target_subtree_leaf_count: usize,
356    target_optimized_subtree_count: usize,
357    root_mode: RootOptimizationMode,
358}
359
360#[derive(Copy, Clone, Debug)]
361pub struct BvhOptimizationHeapEntry {
362    score: OrderedFloat<Real>,
363    id: u32,
364}
365
366impl PartialOrd for BvhOptimizationHeapEntry {
367    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
368        Some(self.score.cmp(&other.score))
369    }
370}
371
372impl Ord for BvhOptimizationHeapEntry {
373    fn cmp(&self, other: &Self) -> Ordering {
374        self.score.cmp(&other.score)
375    }
376}
377
378impl PartialEq for BvhOptimizationHeapEntry {
379    fn eq(&self, other: &Self) -> bool {
380        self.score == other.score
381    }
382}
383
384impl Eq for BvhOptimizationHeapEntry {}