Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
bvh.rs

1//! Static BVH (Bounding Volume Hierarchy) / AABB-tree builder and traversal.
2//!
3//! Provides a deterministic, caller-buffered BVH over 3D AABBs built by
4//! Morton-code spatial sorting followed by a median split. (This is not a
5//! Surface Area Heuristic build — no per-split surface-area cost is evaluated;
6//! see `build_recursive`, which splits each range at its Morton-sorted median.
7//! A true SAH build remains a possible future improvement.)
8//! The tree is built once and queried many times — suitable for static scenes.
9//!
10//! ## Layout
11//!
12//! The BVH is stored as a flat array of nodes. Internal nodes store two child
13//! indices; leaf nodes store a range of primitive indices. The node array is
14//! `repr(C)` and POD for direct GPU staging.
15//!
16//! ## Determinism
17//!
18//! The builder sorts primitives by Morton code, then recursively partitions
19//! each range at its median. Two builds from the same input produce
20//! byte-identical node arrays.
21
22use bytemuck::{Pod, Zeroable};
23
24use super::distance::Aabb;
25use super::spatial_order::sort_by_morton_3d;
26
27// ---------------------------------------------------------------------------
28// Errors
29// ---------------------------------------------------------------------------
30
31/// Errors raised by the BVH builder.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum BvhError {
34    /// `nodes` buffer too small.
35    NodeBufferTooSmall { required: usize },
36    /// `prim_indices` buffer too small (needs `primitive_count` entries).
37    IndexBufferTooSmall { required: usize },
38    /// `morton_codes` buffer too small.
39    CodeBufferTooSmall { required: usize },
40    /// `sort_indices` buffer too small.
41    SortBufferTooSmall { required: usize },
42    /// A bounding box is invalid (min > max).
43    InvalidAabb { index: usize },
44}
45
46// ---------------------------------------------------------------------------
47// POD node
48// ---------------------------------------------------------------------------
49
50/// BVH node: 48 bytes, `repr(C)`, naturally aligned.
51///
52/// ```text
53/// offset  size  field
54/// 0       12    bbox_min:[f32;3]
55/// 12      12    bbox_max:[f32;3]
56/// 24      4     left_or_first:u32    (internal: left child index; leaf: first prim index)
57/// 28      4     right_or_count:u32  (internal: right child index; leaf: prim count)
58/// 32      4     parent:u32          (parent node index, or INVALID_INDEX)
59/// 36      4     node_type:u32       (0 = internal, 1 = leaf)
60/// ```
61#[repr(C)]
62#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
63pub struct BvhNode {
64    pub bbox_min: [f32; 3],
65    pub bbox_max: [f32; 3],
66    pub left_or_first: u32,
67    pub right_or_count: u32,
68    pub parent: u32,
69    pub node_type: u32,
70}
71
72impl Default for BvhNode {
73    fn default() -> Self {
74        Self {
75            bbox_min: [0.0; 3],
76            bbox_max: [0.0; 3],
77            left_or_first: 0,
78            right_or_count: 0,
79            parent: super::topology::INVALID_INDEX,
80            node_type: 0,
81        }
82    }
83}
84
85/// Node size in bytes.
86pub const BVH_NODE_SIZE: usize = 40;
87
88/// Maximum BVH depth (bounded by 2^21 primitives → 21 levels).
89pub const MAX_BVH_DEPTH: usize = 32;
90
91/// Leaf size: maximum primitives per leaf before splitting.
92const LEAF_SIZE: usize = 4;
93
94impl From<super::spatial_order::SpatialOrderError> for BvhError {
95    fn from(err: super::spatial_order::SpatialOrderError) -> Self {
96        match err {
97            super::spatial_order::SpatialOrderError::CodeBufferTooSmall { required } => {
98                BvhError::CodeBufferTooSmall { required }
99            }
100            super::spatial_order::SpatialOrderError::IndexBufferTooSmall { required } => {
101                BvhError::SortBufferTooSmall { required }
102            }
103            super::spatial_order::SpatialOrderError::NonFiniteCoordinate { index } => {
104                BvhError::InvalidAabb { index }
105            }
106        }
107    }
108}
109
110/// Recursive BVH build (simpler and correct).
111///
112/// Returns the node index and the number of nodes used.
113fn build_recursive(
114    primitives: &[Aabb],
115    prim_indices: &mut [u32],
116    start: usize,
117    count: usize,
118    nodes: &mut [BvhNode],
119    next_node: &mut usize,
120    parent: u32,
121) -> usize {
122    let node_idx = *next_node;
123    *next_node += 1;
124
125    // Compute bounding box.
126    let mut bbox = Aabb::new(
127        primitives[prim_indices[start] as usize].min,
128        primitives[prim_indices[start] as usize].max,
129    );
130    for i in 1..count {
131        let p_idx = prim_indices[start + i] as usize;
132        bbox = bbox.union(&primitives[p_idx]);
133    }
134
135    if count <= LEAF_SIZE {
136        nodes[node_idx] = BvhNode {
137            bbox_min: [bbox.min.x as f32, bbox.min.y as f32, bbox.min.z as f32],
138            bbox_max: [bbox.max.x as f32, bbox.max.y as f32, bbox.max.z as f32],
139            left_or_first: start as u32,
140            right_or_count: count as u32,
141            parent,
142            node_type: 1,
143        };
144        return 1;
145    }
146
147    // Internal node: split at median (Morton-sorted).
148    let mid = start + count / 2;
149
150    // Build left subtree first (gets node_idx + 1).
151    let left_child = *next_node;
152    let left_size = build_recursive(
153        primitives,
154        prim_indices,
155        start,
156        count / 2,
157        nodes,
158        next_node,
159        node_idx as u32,
160    );
161
162    // Build right subtree.
163    let right_child = *next_node;
164    let right_size = build_recursive(
165        primitives,
166        prim_indices,
167        mid,
168        count - count / 2,
169        nodes,
170        next_node,
171        node_idx as u32,
172    );
173
174    nodes[node_idx] = BvhNode {
175        bbox_min: [bbox.min.x as f32, bbox.min.y as f32, bbox.min.z as f32],
176        bbox_max: [bbox.max.x as f32, bbox.max.y as f32, bbox.max.z as f32],
177        left_or_first: left_child as u32,
178        right_or_count: right_child as u32,
179        parent,
180        node_type: 0,
181    };
182
183    1 + left_size + right_size
184}
185
186/// Build a BVH using the recursive builder (correct, deterministic).
187///
188/// Caller-supplied buffers:
189/// - `nodes`: needs up to `2 * primitives.len()` entries.
190/// - `prim_indices`: needs `primitives.len()` entries (reordered by Morton code).
191/// - `morton_codes`: needs `primitives.len()` entries (scratch).
192/// - `sort_indices`: needs `primitives.len()` entries (scratch).
193///
194/// Returns (node_count, root_index). Root is always 0.
195pub fn build_bvh_recursive(
196    primitives: &[Aabb],
197    nodes: &mut [BvhNode],
198    prim_indices: &mut [u32],
199    morton_codes: &mut [u64],
200    sort_indices: &mut [u32],
201) -> Result<(usize, usize), BvhError> {
202    let n = primitives.len();
203    if n == 0 {
204        return Ok((0, 0));
205    }
206
207    // Validate AABBs.
208    for (i, aabb) in primitives.iter().enumerate() {
209        if aabb.min.x > aabb.max.x || aabb.min.y > aabb.max.y || aabb.min.z > aabb.max.z {
210            return Err(BvhError::InvalidAabb { index: i });
211        }
212    }
213
214    if morton_codes.len() < n {
215        return Err(BvhError::CodeBufferTooSmall { required: n });
216    }
217    if sort_indices.len() < n {
218        return Err(BvhError::SortBufferTooSmall { required: n });
219    }
220    if prim_indices.len() < n {
221        return Err(BvhError::IndexBufferTooSmall { required: n });
222    }
223
224    let max_nodes = 2 * n - 1;
225    if nodes.len() < max_nodes {
226        return Err(BvhError::NodeBufferTooSmall {
227            required: max_nodes,
228        });
229    }
230
231    // Compute centroids for Morton sorting.
232    let centroids: Vec<[f64; 3]> = primitives
233        .iter()
234        .map(|aabb| {
235            [
236                0.5 * (aabb.min.x + aabb.max.x) as f64,
237                0.5 * (aabb.min.y + aabb.max.y) as f64,
238                0.5 * (aabb.min.z + aabb.max.z) as f64,
239            ]
240        })
241        .collect();
242
243    // Sort by Morton code.
244    sort_by_morton_3d(&centroids, morton_codes, sort_indices)?;
245    prim_indices[..n].copy_from_slice(&sort_indices[..n]);
246
247    // Build recursively.
248    let mut next_node = 0usize;
249    let node_count = build_recursive(
250        primitives,
251        prim_indices,
252        0,
253        n,
254        nodes,
255        &mut next_node,
256        super::topology::INVALID_INDEX,
257    );
258
259    Ok((node_count, 0))
260}
261
262// ---------------------------------------------------------------------------
263// Traversal: query AABB overlap
264// ---------------------------------------------------------------------------
265
266/// Query the BVH for all primitives whose AABB overlaps `query_bbox`.
267///
268/// `out_indices` receives the primitive indices. Returns the count found.
269/// `stack` is scratch space (needs `MAX_BVH_DEPTH` entries).
270///
271/// Zero-heap. Deterministic (traversal order is left-to-right).
272pub fn query_overlap(
273    nodes: &[BvhNode],
274    primitives: &[Aabb],
275    prim_indices: &[u32],
276    root: usize,
277    node_count: usize,
278    query_bbox: &Aabb,
279    out_indices: &mut [u32],
280    stack: &mut [u32],
281) -> Result<usize, BvhError> {
282    if node_count == 0 {
283        return Ok(0);
284    }
285    if stack.len() < MAX_BVH_DEPTH * 2 {
286        return Err(BvhError::SortBufferTooSmall {
287            required: MAX_BVH_DEPTH * 2,
288        });
289    }
290
291    let mut out_count = 0;
292    let mut stack_top = 0usize;
293    stack[stack_top] = root as u32;
294    stack_top += 1;
295
296    while stack_top > 0 {
297        stack_top -= 1;
298        let node_idx = stack[stack_top] as usize;
299
300        let node = &nodes[node_idx];
301        let node_bbox = Aabb::new(
302            crate::specialized_libs::computational_geometry::Point3::new(
303                node.bbox_min[0] as f64,
304                node.bbox_min[1] as f64,
305                node.bbox_min[2] as f64,
306            ),
307            crate::specialized_libs::computational_geometry::Point3::new(
308                node.bbox_max[0] as f64,
309                node.bbox_max[1] as f64,
310                node.bbox_max[2] as f64,
311            ),
312        );
313
314        if !node_bbox.overlaps(query_bbox) {
315            continue;
316        }
317
318        // Leaf: node_type == 1. Internal: node_type == 0.
319        if node.node_type == 1 {
320            // Leaf node: check each primitive's AABB.
321            let start = node.left_or_first as usize;
322            let count = node.right_or_count as usize;
323            for i in 0..count {
324                if out_count >= out_indices.len() {
325                    return Ok(out_count);
326                }
327                let p_idx = prim_indices[start + i] as usize;
328                if primitives[p_idx].overlaps(query_bbox) {
329                    out_indices[out_count] = prim_indices[start + i];
330                    out_count += 1;
331                }
332            }
333        } else {
334            // Internal node: push children.
335            if stack_top + 2 > stack.len() {
336                break;
337            }
338            stack[stack_top] = node.right_or_count; // right child
339            stack_top += 1;
340            stack[stack_top] = node.left_or_first; // left child
341            stack_top += 1;
342        }
343    }
344
345    Ok(out_count)
346}
347
348/// Query the BVH for the closest primitive to a point.
349///
350/// Uses distance pruning. Returns the primitive index and squared distance,
351/// or `None` if the tree is empty.
352///
353/// `stack` needs `MAX_BVH_DEPTH * 2` entries.
354pub fn query_closest(
355    nodes: &[BvhNode],
356    primitives: &[Aabb],
357    prim_indices: &[u32],
358    root: usize,
359    node_count: usize,
360    point: super::primitives::Point3,
361    stack: &mut [u32],
362) -> Result<Option<(u32, f64)>, BvhError> {
363    if node_count == 0 {
364        return Ok(None);
365    }
366    if stack.len() < MAX_BVH_DEPTH * 2 {
367        return Err(BvhError::SortBufferTooSmall {
368            required: MAX_BVH_DEPTH * 2,
369        });
370    }
371
372    let mut best_idx: Option<u32> = None;
373    let mut best_dist_sq = f64::INFINITY;
374
375    let mut stack_top = 0usize;
376    stack[stack_top] = root as u32;
377    stack_top += 1;
378
379    while stack_top > 0 {
380        stack_top -= 1;
381        let node_idx = stack[stack_top] as usize;
382        let node = &nodes[node_idx];
383
384        let node_bbox = Aabb::new(
385            super::primitives::Point3::new(
386                node.bbox_min[0] as f64,
387                node.bbox_min[1] as f64,
388                node.bbox_min[2] as f64,
389            ),
390            super::primitives::Point3::new(
391                node.bbox_max[0] as f64,
392                node.bbox_max[1] as f64,
393                node.bbox_max[2] as f64,
394            ),
395        );
396
397        // Prune: if node bbox is farther than best, skip.
398        let node_dist_sq = node_bbox.distance_sq_to_point(point);
399        if node_dist_sq > best_dist_sq {
400            continue;
401        }
402
403        if node.node_type == 1 {
404            // Leaf node.
405            let start = node.left_or_first as usize;
406            let count = node.right_or_count as usize;
407            for i in 0..count {
408                let p_idx = prim_indices[start + i] as usize;
409                let dist_sq = primitives[p_idx].distance_sq_to_point(point);
410                if dist_sq < best_dist_sq {
411                    best_dist_sq = dist_sq;
412                    best_idx = Some(prim_indices[start + i]);
413                }
414            }
415        } else {
416            // Internal: push both children.
417            if stack_top + 2 > stack.len() {
418                break;
419            }
420            stack[stack_top] = node.right_or_count;
421            stack_top += 1;
422            stack[stack_top] = node.left_or_first;
423            stack_top += 1;
424        }
425    }
426
427    Ok(best_idx.map(|idx| (idx, best_dist_sq)))
428}
429
430// ---------------------------------------------------------------------------
431// Tests
432// ---------------------------------------------------------------------------
433
434#[cfg(test)]
435mod tests {
436    use super::super::primitives::Point3;
437    use super::*;
438
439    fn make_aabb(min: [f64; 3], max: [f64; 3]) -> Aabb {
440        Aabb::new(
441            Point3::new(min[0], min[1], min[2]),
442            Point3::new(max[0], max[1], max[2]),
443        )
444    }
445
446    fn unit_cubes() -> Vec<Aabb> {
447        // 8 unit cubes in a 2×2×2 grid.
448        let mut prims = Vec::new();
449        for x in 0..2 {
450            for y in 0..2 {
451                for z in 0..2 {
452                    let min = [x as f64, y as f64, z as f64];
453                    let max = [(x + 1) as f64, (y + 1) as f64, (z + 1) as f64];
454                    prims.push(make_aabb(min, max));
455                }
456            }
457        }
458        prims
459    }
460
461    #[test]
462    fn bvh_node_is_pod_with_exact_size() {
463        assert_eq!(std::mem::size_of::<BvhNode>(), BVH_NODE_SIZE);
464        assert_eq!(std::mem::align_of::<BvhNode>(), 4);
465    }
466
467    #[test]
468    fn build_bvh_8_cubes() {
469        let prims = unit_cubes();
470        let n = prims.len();
471        let mut nodes = vec![BvhNode::default(); 2 * n];
472        let mut prim_indices = vec![0u32; n];
473        let mut morton_codes = vec![0u64; n];
474        let mut sort_indices = vec![0u32; n];
475
476        let (node_count, root) = build_bvh_recursive(
477            &prims,
478            &mut nodes,
479            &mut prim_indices,
480            &mut morton_codes,
481            &mut sort_indices,
482        )
483        .unwrap();
484
485        assert!(node_count > 0);
486        assert_eq!(root, 0);
487        // 8 prims with LEAF_SIZE=4: root + 2 leaves = 3 nodes.
488        assert_eq!(node_count, 3);
489    }
490
491    #[test]
492    fn build_bvh_deterministic() {
493        let prims = unit_cubes();
494        let n = prims.len();
495
496        let mut nodes_a = vec![BvhNode::default(); 2 * n];
497        let mut indices_a = vec![0u32; n];
498        let mut codes_a = vec![0u64; n];
499        let mut sort_a = vec![0u32; n];
500        let (count_a, _) = build_bvh_recursive(
501            &prims,
502            &mut nodes_a,
503            &mut indices_a,
504            &mut codes_a,
505            &mut sort_a,
506        )
507        .unwrap();
508
509        let mut nodes_b = vec![BvhNode::default(); 2 * n];
510        let mut indices_b = vec![0u32; n];
511        let mut codes_b = vec![0u64; n];
512        let mut sort_b = vec![0u32; n];
513        let (count_b, _) = build_bvh_recursive(
514            &prims,
515            &mut nodes_b,
516            &mut indices_b,
517            &mut codes_b,
518            &mut sort_b,
519        )
520        .unwrap();
521
522        assert_eq!(count_a, count_b);
523        assert_eq!(nodes_a[..count_a], nodes_b[..count_b]);
524        assert_eq!(indices_a, indices_b);
525    }
526
527    #[test]
528    fn query_overlap_finds_correct_primitives() {
529        let prims = unit_cubes();
530        let n = prims.len();
531        let mut nodes = vec![BvhNode::default(); 2 * n];
532        let mut prim_indices = vec![0u32; n];
533        let mut morton_codes = vec![0u64; n];
534        let mut sort_indices = vec![0u32; n];
535
536        let (node_count, root) = build_bvh_recursive(
537            &prims,
538            &mut nodes,
539            &mut prim_indices,
540            &mut morton_codes,
541            &mut sort_indices,
542        )
543        .unwrap();
544
545        // Query: a box that covers only the (0,0,0) cube.
546        let query = make_aabb([-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]);
547        let mut out = vec![0u32; n];
548        let mut stack = vec![0u32; MAX_BVH_DEPTH * 2];
549        let count = query_overlap(
550            &nodes,
551            &prims,
552            &prim_indices,
553            root,
554            node_count,
555            &query,
556            &mut out,
557            &mut stack,
558        )
559        .unwrap();
560
561        assert_eq!(count, 1);
562        // The found primitive should be the one at (0,0,0).
563        let found_prim = prims[out[0] as usize];
564        assert_eq!(found_prim.min, Point3::new(0.0, 0.0, 0.0));
565    }
566
567    #[test]
568    fn query_overlap_all() {
569        let prims = unit_cubes();
570        let n = prims.len();
571        let mut nodes = vec![BvhNode::default(); 2 * n];
572        let mut prim_indices = vec![0u32; n];
573        let mut morton_codes = vec![0u64; n];
574        let mut sort_indices = vec![0u32; n];
575
576        let (node_count, root) = build_bvh_recursive(
577            &prims,
578            &mut nodes,
579            &mut prim_indices,
580            &mut morton_codes,
581            &mut sort_indices,
582        )
583        .unwrap();
584
585        // Query: a box that covers everything.
586        let query = make_aabb([-10.0, -10.0, -10.0], [10.0, 10.0, 10.0]);
587        let mut out = vec![0u32; n];
588        let mut stack = vec![0u32; MAX_BVH_DEPTH * 2];
589        let count = query_overlap(
590            &nodes,
591            &prims,
592            &prim_indices,
593            root,
594            node_count,
595            &query,
596            &mut out,
597            &mut stack,
598        )
599        .unwrap();
600
601        assert_eq!(count, n);
602    }
603
604    #[test]
605    fn query_closest_finds_nearest() {
606        let prims = unit_cubes();
607        let n = prims.len();
608        let mut nodes = vec![BvhNode::default(); 2 * n];
609        let mut prim_indices = vec![0u32; n];
610        let mut morton_codes = vec![0u64; n];
611        let mut sort_indices = vec![0u32; n];
612
613        let (node_count, root) = build_bvh_recursive(
614            &prims,
615            &mut nodes,
616            &mut prim_indices,
617            &mut morton_codes,
618            &mut sort_indices,
619        )
620        .unwrap();
621
622        let point = Point3::new(0.1, 0.1, 0.1);
623        let mut stack = vec![0u32; MAX_BVH_DEPTH * 2];
624        let result = query_closest(
625            &nodes,
626            &prims,
627            &prim_indices,
628            root,
629            node_count,
630            point,
631            &mut stack,
632        )
633        .unwrap();
634
635        assert!(result.is_some());
636        let (idx, dist_sq) = result.unwrap();
637        // The closest cube should be at (0,0,0).
638        let prim = &prims[idx as usize];
639        assert_eq!(prim.min, Point3::new(0.0, 0.0, 0.0));
640        // Point is inside the cube, so distance is 0.
641        assert!(dist_sq < 1e-12);
642    }
643
644    #[test]
645    fn query_closest_outside() {
646        let prims = unit_cubes();
647        let n = prims.len();
648        let mut nodes = vec![BvhNode::default(); 2 * n];
649        let mut prim_indices = vec![0u32; n];
650        let mut morton_codes = vec![0u64; n];
651        let mut sort_indices = vec![0u32; n];
652
653        let (node_count, root) = build_bvh_recursive(
654            &prims,
655            &mut nodes,
656            &mut prim_indices,
657            &mut morton_codes,
658            &mut sort_indices,
659        )
660        .unwrap();
661
662        // Point far from all cubes.
663        let point = Point3::new(5.0, 5.0, 5.0);
664        let mut stack = vec![0u32; MAX_BVH_DEPTH * 2];
665        let result = query_closest(
666            &nodes,
667            &prims,
668            &prim_indices,
669            root,
670            node_count,
671            point,
672            &mut stack,
673        )
674        .unwrap();
675
676        assert!(result.is_some());
677        let (_idx, dist_sq) = result.unwrap();
678        // Closest cube is at (1,1,1), distance from (5,5,5) to (2,2,2) corner = sqrt(3*9)=sqrt(27)
679        assert!((dist_sq - 27.0).abs() < 1e-10, "dist_sq={dist_sq}");
680    }
681
682    #[test]
683    fn differential_vs_brute_force_overlap() {
684        // Random-ish AABBs.
685        let prims: Vec<Aabb> = (0..20)
686            .map(|i| {
687                let x = (i % 5) as f64;
688                let y = ((i / 5) % 4) as f64;
689                let z = (i / 20) as f64;
690                make_aabb([x, y, z], [x + 0.8, y + 0.8, z + 0.8])
691            })
692            .collect();
693        let n = prims.len();
694        let mut nodes = vec![BvhNode::default(); 2 * n];
695        let mut prim_indices = vec![0u32; n];
696        let mut morton_codes = vec![0u64; n];
697        let mut sort_indices = vec![0u32; n];
698
699        let (node_count, root) = build_bvh_recursive(
700            &prims,
701            &mut nodes,
702            &mut prim_indices,
703            &mut morton_codes,
704            &mut sort_indices,
705        )
706        .unwrap();
707
708        let query = make_aabb([1.5, 1.5, 0.0], [3.5, 2.5, 1.0]);
709        let mut bvh_out = vec![0u32; n];
710        let mut stack = vec![0u32; MAX_BVH_DEPTH * 2];
711        let bvh_count = query_overlap(
712            &nodes,
713            &prims,
714            &prim_indices,
715            root,
716            node_count,
717            &query,
718            &mut bvh_out,
719            &mut stack,
720        )
721        .unwrap();
722
723        // Brute force.
724        let mut brute_out: Vec<u32> = Vec::new();
725        for (i, p) in prims.iter().enumerate() {
726            if p.overlaps(&query) {
727                brute_out.push(i as u32);
728            }
729        }
730
731        // Compare as sets.
732        let mut bvh_sorted: Vec<u32> = bvh_out[..bvh_count].to_vec();
733        bvh_sorted.sort_unstable();
734        let mut brute_sorted = brute_out.clone();
735        brute_sorted.sort_unstable();
736        assert_eq!(bvh_sorted, brute_sorted, "BVH and brute force must agree");
737    }
738
739    #[test]
740    fn empty_bvh_returns_empty() {
741        let prims: Vec<Aabb> = vec![];
742        let mut nodes = vec![BvhNode::default(); 1];
743        let mut prim_indices = vec![0u32; 1];
744        let mut morton_codes = vec![0u64; 1];
745        let mut sort_indices = vec![0u32; 1];
746
747        let (node_count, _) = build_bvh_recursive(
748            &prims,
749            &mut nodes,
750            &mut prim_indices,
751            &mut morton_codes,
752            &mut sort_indices,
753        )
754        .unwrap();
755
756        assert_eq!(node_count, 0);
757    }
758
759    #[test]
760    fn single_primitive_bvh() {
761        let prims = vec![make_aabb([0.0, 0.0, 0.0], [1.0, 1.0, 1.0])];
762        let n = prims.len();
763        let mut nodes = vec![BvhNode::default(); 2 * n];
764        let mut prim_indices = vec![0u32; n];
765        let mut morton_codes = vec![0u64; n];
766        let mut sort_indices = vec![0u32; n];
767
768        let (node_count, root) = build_bvh_recursive(
769            &prims,
770            &mut nodes,
771            &mut prim_indices,
772            &mut morton_codes,
773            &mut sort_indices,
774        )
775        .unwrap();
776
777        assert_eq!(node_count, 1);
778        let node = &nodes[root];
779        assert_eq!(node.right_or_count, 1); // leaf with 1 primitive
780    }
781}