Skip to main content

obvhs/bvh2/
reinsertion.rs

1// Reinsertion optimizer based on "Parallel Reinsertion for Bounding Volume Hierarchy Optimization", by D. Meister and J. Bittner:
2// https://meistdan.github.io/publications/prbvh/paper.pdf
3// https://jcgt.org/published/0011/04/01/paper.pdf
4// Reference: https://github.com/madmann91/bvh/blob/3490634ae822e5081e41f09498fcce03bc1419e3/src/bvh/v2/reinsertion_optimizer.h
5
6// Note: Most asserts exist to try to elide bounds checks
7
8#[cfg(feature = "parallel")]
9use rayon::{
10    iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator},
11    slice::ParallelSliceMut,
12};
13
14use rdst::{RadixKey, RadixSort};
15
16use crate::{
17    bvh2::{Bvh2, Bvh2Node},
18    fast_stack,
19    faststack::FastStack,
20};
21
22/// Restructures the BVH, optimizing node locations within the BVH hierarchy per SAH cost.
23#[derive(Default)]
24pub struct ReinsertionOptimizer {
25    candidates: Vec<Candidate>,
26    reinsertions: Vec<Reinsertion>,
27    touched: Vec<bool>,
28    batch_size_ratio: f32,
29}
30
31impl ReinsertionOptimizer {
32    /// Restructures the BVH, optimizing node locations within the BVH hierarchy per SAH cost.
33    /// batch_size_ratio: Fraction of the number of nodes to optimize per iteration.
34    /// ratio_sequence: A sequence of ratios to preform reinsertion at. These are as a
35    /// proportion of the batch_size_ratio. If None, the following sequence is used:
36    /// (1..32).step_by(2).map(|n| 1.0 / n as f32) or
37    /// 1/1, 1/3, 1/5, 1/7, 1/9, 1/11, 1/13, 1/15, 1/17, 1/19, 1/21, 1/23, 1/25, 1/27, 1/29, 1/31
38    pub fn run(&mut self, bvh: &mut Bvh2, batch_size_ratio: f32, ratio_sequence: Option<Vec<f32>>) {
39        crate::scope!("reinsertion_optimize");
40
41        if bvh.nodes.is_empty() || bvh.nodes[0].is_leaf() || batch_size_ratio <= 0.0 {
42            return;
43        }
44
45        bvh.init_parents_if_uninit();
46
47        let cap = (bvh.nodes.len() as f32 * batch_size_ratio.min(1.0)).ceil() as usize;
48
49        self.candidates.reserve(cap);
50        self.reinsertions.reserve(cap);
51        self.touched.clear();
52        self.touched.resize(bvh.nodes.len(), false);
53        self.batch_size_ratio = batch_size_ratio;
54        self.optimize_impl(bvh, ratio_sequence);
55    }
56
57    /// Restructures the BVH, optimizing given node locations within the BVH hierarchy per SAH cost.
58    ///
59    /// # Arguments
60    /// * `candidates` - A list of ids for nodes that need to be re-inserted.
61    /// * `iterations` - The number of times reinsertion is run. Parallel reinsertion passes can result in conflicts
62    ///   that potentially limit the proportion of reinsertions in a single pass.
63    pub fn run_with_candidates(&mut self, bvh: &mut Bvh2, candidates: &[u32], iterations: u32) {
64        crate::scope!("reinsertion_optimize_candidates");
65
66        if bvh.nodes.is_empty() || bvh.nodes[0].is_leaf() {
67            return;
68        }
69
70        bvh.init_parents_if_uninit();
71
72        let cap = candidates.len();
73
74        self.candidates = candidates
75            .iter()
76            .map(|node_id| {
77                let cost = bvh.nodes[*node_id as usize].aabb().half_area();
78                Candidate {
79                    cost,
80                    node_id: *node_id,
81                }
82            })
83            .collect::<Vec<_>>();
84        self.reinsertions.reserve(cap);
85        self.touched.clear();
86        self.touched.resize(bvh.nodes.len(), false);
87        self.optimize_specific_candidates(bvh, iterations);
88    }
89
90    pub fn optimize_impl(&mut self, bvh: &mut Bvh2, ratio_sequence: Option<Vec<f32>>) {
91        bvh.children_are_ordered_after_parents = false;
92        // This initially preforms reinsertion at the specified ratio, then at progressively smaller ratios,
93        // focusing more reinsertion time at the top of the bvh. The original method would perform reinsertion
94        // for a fixed ratio a fixed number of times.
95        let ratio_sequence = ratio_sequence.unwrap_or(
96            (1..32)
97                .step_by(2)
98                .map(|n| 1.0 / n as f32)
99                .collect::<Vec<_>>(),
100        );
101
102        ratio_sequence.iter().for_each(|ratio| {
103            let batch_size =
104                (((bvh.nodes.len() as f32 * self.batch_size_ratio) * ratio) as usize).max(1);
105            let node_count = bvh.nodes.len().min(batch_size + 1);
106            self.find_candidates(bvh, node_count);
107            self.optimize_candidates(bvh, node_count - 1);
108        });
109    }
110
111    pub fn optimize_specific_candidates(&mut self, bvh: &mut Bvh2, iterations: u32) {
112        bvh.children_are_ordered_after_parents = false;
113        for _ in 0..iterations {
114            self.optimize_candidates(bvh, self.candidates.len());
115        }
116    }
117
118    /// Find potential candidates for reinsertion
119    fn find_candidates(&mut self, bvh: &mut Bvh2, node_count: usize) {
120        // This method just takes the first node_count*2 nodes in the bvh and sorts them by their half area
121        // This seemed to find candidates much faster while resulting in similar bvh traversal performance vs the original method
122        // https://github.com/madmann91/bvh/blob/3490634ae822e5081e41f09498fcce03bc1419e3/src/bvh/v2/reinsertion_optimizer.h#L88
123        // Taking the first node_count * 2 seemed to work nearly as well as sorting all the nodes, but builds much faster.
124        self.candidates.clear();
125        bvh.nodes
126            .iter()
127            .take(node_count * 2)
128            .enumerate()
129            .skip(1)
130            .for_each(|(i, node)| {
131                self.candidates.push(Candidate {
132                    cost: node.aabb().half_area(),
133                    node_id: i as u32,
134                });
135            });
136        self.candidates.radix_sort_unstable();
137    }
138
139    #[allow(unused_variables)]
140    fn optimize_candidates(&mut self, bvh: &mut Bvh2, count: usize) {
141        self.touched.fill(false);
142
143        #[cfg(feature = "parallel")]
144        {
145            self.reinsertions.resize(count, Default::default());
146            self.reinsertions
147                .par_iter_mut()
148                .enumerate()
149                .for_each(|(i, reinsertion)| {
150                    *reinsertion = find_reinsertion(bvh, self.candidates[i].node_id as usize)
151                });
152        }
153        #[cfg(not(feature = "parallel"))]
154        {
155            self.reinsertions.clear();
156            assert!(count <= self.candidates.len());
157            (0..count).for_each(|i| {
158                let r = find_reinsertion(bvh, self.candidates[i].node_id as usize);
159                if r.area_diff > 0.0 {
160                    self.reinsertions.push(r)
161                }
162            });
163        }
164
165        #[cfg(feature = "parallel")]
166        self.reinsertions
167            .par_sort_unstable_by(|a, b| b.area_diff.partial_cmp(&a.area_diff).unwrap());
168
169        #[cfg(not(feature = "parallel"))]
170        self.reinsertions
171            .sort_unstable_by(|a, b| b.area_diff.partial_cmp(&a.area_diff).unwrap());
172
173        assert!(self.reinsertions.len() <= self.touched.len());
174        (0..self.reinsertions.len()).for_each(|i| {
175            let reinsertion = &self.reinsertions[i];
176
177            #[cfg(feature = "parallel")]
178            if reinsertion.area_diff <= 0.0 {
179                return;
180            }
181
182            let conflicts = self.get_conflicts(bvh, reinsertion.from, reinsertion.to);
183
184            if conflicts.iter().any(|&i| self.touched[i]) {
185                return;
186            }
187
188            conflicts.iter().for_each(|&conflict| {
189                self.touched[conflict] = true;
190            });
191
192            reinsert_node(bvh, reinsertion.from as usize, reinsertion.to as usize);
193        });
194    }
195
196    #[inline(always)]
197    fn get_conflicts(&self, bvh: &mut Bvh2, from: u32, to: u32) -> [usize; 5] {
198        [
199            to as usize,
200            from as usize,
201            Bvh2Node::get_sibling_id(from as usize),
202            // SAFETY: Caller asserts self.bvh.parents is Some outside of hot loop
203            bvh.parents[to as usize] as usize,
204            bvh.parents[from as usize] as usize,
205        ]
206    }
207}
208
209#[derive(Default, Clone, Copy)]
210pub struct Reinsertion {
211    pub from: u32,
212    pub to: u32,
213    pub area_diff: f32,
214}
215
216#[derive(Clone, Copy, Debug)]
217struct Candidate {
218    node_id: u32,
219    cost: f32,
220}
221
222impl RadixKey for Candidate {
223    const LEVELS: usize = 4;
224
225    #[inline]
226    fn get_level(&self, level: usize) -> u8 {
227        (-self.cost).get_level(level)
228    }
229}
230
231pub fn find_reinsertion(bvh: &Bvh2, node_id: usize) -> Reinsertion {
232    if bvh.parents.is_empty() {
233        panic!("Parents mapping required. Please run Bvh2::init_parents() before reinsert_node()")
234    }
235
236    debug_assert_ne!(node_id, 0);
237    // Try to elide bounds checks
238    assert!(node_id < bvh.nodes.len());
239    assert!(node_id < bvh.parents.len());
240
241    /*
242     * Here is an example that explains how the cost of a reinsertion is computed. For the
243     * reinsertion from A to C, in the figure below, we need to remove P1, replace it by B,
244     * and create a node that holds A and C and place it where C was.
245     *
246     *             R
247     *            / \
248     *          Pn   Q1
249     *          /     \
250     *        ...     ...
251     *        /         \
252     *       P1          C
253     *      / \
254     *     A   B
255     *
256     * The resulting area *decrease* is (SA(x) means the surface area of x):
257     *
258     *     SA(P1) +                                                : P1 was removed
259     *     SA(P2) - SA(B) +                                        : P2 now only contains B
260     *     SA(P3) - SA(B U sibling(P2)) +                          : Same but for P3
261     *     ... +
262     *     SA(Pn) - SA(B U sibling(P2) U ... U sibling(P(n - 1)) + : Same but for Pn
263     *     0 +                                                     : R does not change
264     *     SA(Q1) - SA(Q1 U A) +                                   : Q1 now contains A
265     *     SA(Q2) - SA(Q2 U A) +                                   : Q2 now contains A
266     *     ... +
267     *     -SA(A U C)                                              : For the parent of A and C
268     */
269    let mut best_reinsertion = Reinsertion {
270        from: node_id as u32,
271        to: 0,
272        area_diff: 0.0,
273    };
274    let node_area = bvh.nodes[node_id].aabb().half_area();
275
276    let parent_area = bvh.nodes[bvh.parents[node_id] as usize].aabb().half_area();
277    let mut area_diff = parent_area;
278    let mut sibling_id = Bvh2Node::get_sibling_id(node_id);
279    let mut pivot_bbox = *bvh.nodes[sibling_id].aabb();
280    let parent_id = bvh.parents[node_id] as usize;
281    let mut pivot_id = parent_id;
282    let aabb = bvh.nodes[node_id].aabb();
283    let mut longest = 0;
284    // TODO is it possible to push only the left pair and reduce the stack size?
285    fast_stack!((f32, u32), (96, 192), bvh.max_depth * 2, stack, {
286        stack.clear();
287        loop {
288            stack.push((area_diff, sibling_id as u32));
289            while !stack.is_empty() {
290                longest = stack.len().max(longest);
291                let (top_area_diff, top_sibling_id) = stack.pop_fast();
292                if top_area_diff - node_area <= best_reinsertion.area_diff {
293                    continue;
294                }
295
296                let dst_node = &bvh.nodes[top_sibling_id as usize];
297                let merged_area = dst_node.aabb().union(aabb).half_area();
298                let reinsert_area = top_area_diff - merged_area;
299                if reinsert_area > best_reinsertion.area_diff {
300                    best_reinsertion.to = top_sibling_id;
301                    best_reinsertion.area_diff = reinsert_area;
302                }
303
304                if !dst_node.is_leaf() {
305                    let child_area = reinsert_area + dst_node.aabb().half_area();
306                    stack.push((child_area, dst_node.first_index));
307                    stack.push((child_area, dst_node.first_index + 1));
308                }
309            }
310
311            if pivot_id != parent_id {
312                pivot_bbox = pivot_bbox.union(bvh.nodes[sibling_id].aabb());
313                area_diff += bvh.nodes[pivot_id].aabb().half_area() - pivot_bbox.half_area();
314            }
315
316            if pivot_id == 0 {
317                break;
318            }
319
320            sibling_id = Bvh2Node::get_sibling_id(pivot_id);
321            pivot_id = bvh.parents[pivot_id] as usize;
322        }
323    });
324
325    if best_reinsertion.to == Bvh2Node::get_sibling_id32(best_reinsertion.from)
326        || best_reinsertion.to == bvh.parents[best_reinsertion.from as usize]
327    {
328        best_reinsertion = Reinsertion::default();
329    }
330
331    best_reinsertion
332}
333
334pub fn reinsert_node(bvh: &mut Bvh2, from: usize, to: usize) {
335    if bvh.parents.is_empty() {
336        panic!("Parents mapping required. Please run Bvh2::init_parents() before reinsert_node()")
337    }
338
339    let sibling_id = Bvh2Node::get_sibling_id(from);
340    let parent_id = bvh.parents[from] as usize;
341    let sibling_node = bvh.nodes[sibling_id];
342    let dst_node = bvh.nodes[to];
343
344    bvh.nodes[to].make_inner(Bvh2Node::get_left_sibling_id(from) as u32);
345    bvh.nodes[sibling_id] = dst_node;
346    bvh.nodes[parent_id] = sibling_node;
347
348    // Tell the children (or primitives) of the two moved nodes where they went.
349    bvh.relink_node(&dst_node, sibling_id);
350    bvh.relink_node(&sibling_node, parent_id);
351
352    bvh.parents[sibling_id] = to as u32;
353    bvh.parents[from] = to as u32;
354    bvh.refit_from_fast(to);
355    bvh.refit_from_fast(parent_id);
356}
357
358#[cfg(test)]
359mod tests {
360
361    use crate::{
362        ploc::{PlocBuilder, PlocSearchDistance, SortPrecision},
363        test_util::geometry::demoscene,
364    };
365
366    use super::*;
367
368    #[test]
369    fn test_reinsertion() {
370        let tris = demoscene(32, 0);
371        let mut aabbs = Vec::with_capacity(tris.len());
372        let mut indices = Vec::with_capacity(tris.len());
373        for (i, primitive) in tris.iter().enumerate() {
374            indices.push(i as u32);
375            aabbs.push(primitive.aabb());
376        }
377        {
378            // Test without init_primitives_to_nodes & init_parents
379            let mut bvh = PlocBuilder::new().build(
380                PlocSearchDistance::VeryLow,
381                &aabbs,
382                indices.clone(),
383                SortPrecision::U64,
384                1,
385            );
386            bvh.validate(&tris, false, false);
387            ReinsertionOptimizer::default().run(&mut bvh, 0.25, None);
388            bvh.validate(&tris, false, false);
389            bvh.reorder_in_stack_traversal_order();
390            ReinsertionOptimizer::default().run(&mut bvh, 0.5, None);
391            bvh.validate(&tris, false, false);
392        }
393        {
394            // Test with init_primitives_to_nodes & init_parents
395            let mut bvh = PlocBuilder::new().build(
396                PlocSearchDistance::VeryLow,
397                &aabbs,
398                indices,
399                SortPrecision::U64,
400                1,
401            );
402            bvh.validate(&tris, false, false);
403            bvh.init_primitives_to_nodes_if_uninit();
404            bvh.init_parents_if_uninit();
405            bvh.validate(&tris, false, false);
406            ReinsertionOptimizer::default().run(&mut bvh, 0.25, None);
407            bvh.validate(&tris, false, false);
408            bvh.reorder_in_stack_traversal_order();
409            ReinsertionOptimizer::default().run(&mut bvh, 0.5, None);
410            bvh.validate(&tris, false, false);
411        }
412    }
413}