Skip to main content

oxiphysics_gpu/kernels/
broadphase.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Broadphase AABB kernels for parallel overlap detection.
5
6use crate::compute::ComputeKernel;
7use std::collections::HashMap;
8
9// ---------------------------------------------------------------------------
10// Legacy f64 kernels (keep existing API)
11// ---------------------------------------------------------------------------
12
13/// Kernel that sorts AABBs along the X-axis and outputs sorted indices.
14///
15/// **Input layout** (flat f64 array, 6 values per object):
16///   `[min_x, max_x, min_y, max_y, min_z, max_z, ...]`
17///
18/// **Output\[0\]**: sorted indices (as f64, cast back to usize by caller).
19pub struct AabbSortKernel;
20
21impl ComputeKernel for AabbSortKernel {
22    fn name(&self) -> &str {
23        "AabbSortKernel"
24    }
25
26    fn execute(&self, inputs: &[&[f64]], outputs: &mut [Vec<f64>], _work_size: usize) {
27        if inputs.is_empty() || outputs.is_empty() {
28            return;
29        }
30        let aabbs = inputs[0];
31        let n = aabbs.len() / 6;
32        // Build (index, min_x) pairs and sort by min_x.
33        let mut indices: Vec<usize> = (0..n).collect();
34        indices.sort_by(|&a, &b| {
35            let ax = aabbs[a * 6];
36            let bx = aabbs[b * 6];
37            ax.partial_cmp(&bx).unwrap_or(std::cmp::Ordering::Equal)
38        });
39        outputs[0] = indices.iter().map(|&i| i as f64).collect();
40    }
41}
42
43/// Kernel that detects overlapping AABB pairs.
44///
45/// **Input layout** (flat f64 array, 6 values per object):
46///   `[min_x, max_x, min_y, max_y, min_z, max_z, ...]`
47///
48/// **Output\[0\]**: flat pairs `[i, j, i, j, ...]` of overlapping indices (as f64).
49pub struct AabbOverlapKernel;
50
51impl AabbOverlapKernel {
52    #[inline]
53    fn overlaps(a: &[f64], b: &[f64]) -> bool {
54        // a, b are slices of length 6: [min_x, max_x, min_y, max_y, min_z, max_z]
55        a[0] <= b[1] && a[1] >= b[0] // x overlap
56        && a[2] <= b[3] && a[3] >= b[2] // y overlap
57        && a[4] <= b[5] && a[5] >= b[4] // z overlap
58    }
59}
60
61impl ComputeKernel for AabbOverlapKernel {
62    fn name(&self) -> &str {
63        "AabbOverlapKernel"
64    }
65
66    fn execute(&self, inputs: &[&[f64]], outputs: &mut [Vec<f64>], _work_size: usize) {
67        if inputs.is_empty() || outputs.is_empty() {
68            return;
69        }
70        let aabbs = inputs[0];
71        let n = aabbs.len() / 6;
72        let mut pairs = Vec::new();
73        for i in 0..n {
74            for j in (i + 1)..n {
75                if Self::overlaps(&aabbs[i * 6..(i + 1) * 6], &aabbs[j * 6..(j + 1) * 6]) {
76                    pairs.push(i as f64);
77                    pairs.push(j as f64);
78                }
79            }
80        }
81        outputs[0] = pairs;
82    }
83}
84
85// ---------------------------------------------------------------------------
86// GPU-compatible f32 AABB
87// ---------------------------------------------------------------------------
88
89/// Axis-aligned bounding box using f32 coordinates for GPU compatibility.
90#[derive(Debug, Clone, Copy, PartialEq)]
91pub struct AabbGpu {
92    /// Minimum corner `[min_x, min_y, min_z]`.
93    pub min: [f32; 3],
94    /// Maximum corner `[max_x, max_y, max_z]`.
95    pub max: [f32; 3],
96    /// Index of the owning rigid body.
97    pub body_id: u32,
98}
99
100impl AabbGpu {
101    /// Create a new GPU AABB.
102    pub fn new(min: [f32; 3], max: [f32; 3], body_id: u32) -> Self {
103        Self { min, max, body_id }
104    }
105
106    /// Test whether two AABBs overlap in all three axes.
107    #[inline]
108    pub fn overlaps(&self, other: &AabbGpu) -> bool {
109        self.min[0] <= other.max[0]
110            && self.max[0] >= other.min[0]
111            && self.min[1] <= other.max[1]
112            && self.max[1] >= other.min[1]
113            && self.min[2] <= other.max[2]
114            && self.max[2] >= other.min[2]
115    }
116}
117
118// ---------------------------------------------------------------------------
119// Sort-and-Sweep (GPU)
120// ---------------------------------------------------------------------------
121
122/// Sort-and-sweep broadphase operating on `AabbGpu` values.
123///
124/// Bodies are sorted by their minimum X coordinate, then swept to find all
125/// overlapping pairs in O(n log n + k) time where k is the number of pairs.
126pub struct SortAndSweepGpu;
127
128impl SortAndSweepGpu {
129    /// Detect all overlapping AABB pairs.
130    ///
131    /// Returns a `Vec` of `(body_id_a, body_id_b)` pairs where the two AABBs
132    /// overlap in all three axes.  Each pair is reported exactly once with
133    /// `body_id_a < body_id_b`.
134    pub fn detect_pairs(aabbs: &[AabbGpu]) -> Vec<(u32, u32)> {
135        if aabbs.is_empty() {
136            return Vec::new();
137        }
138
139        // Sort by min_x.
140        let mut sorted: Vec<&AabbGpu> = aabbs.iter().collect();
141        sorted.sort_by(|a, b| {
142            a.min[0]
143                .partial_cmp(&b.min[0])
144                .unwrap_or(std::cmp::Ordering::Equal)
145        });
146
147        let n = sorted.len();
148        let mut pairs = Vec::new();
149
150        for i in 0..n {
151            for j in (i + 1)..n {
152                // Early-out: if the next AABB's min_x exceeds current max_x, no further overlaps
153                if sorted[j].min[0] > sorted[i].max[0] {
154                    break;
155                }
156                if sorted[i].overlaps(sorted[j]) {
157                    let a = sorted[i].body_id;
158                    let b = sorted[j].body_id;
159                    let pair = if a < b { (a, b) } else { (b, a) };
160                    pairs.push(pair);
161                }
162            }
163        }
164
165        pairs
166    }
167}
168
169// ---------------------------------------------------------------------------
170// Uniform Grid (GPU)
171// ---------------------------------------------------------------------------
172
173/// Uniform 3-D grid for GPU broadphase.
174#[derive(Debug, Clone)]
175pub struct UniformGridGpu {
176    /// Cell size (same in all dimensions).
177    pub cell_size: f32,
178    /// World-space origin of the grid `[ox, oy, oz]`.
179    pub origin: [f32; 3],
180    /// Number of cells in each dimension `[nx, ny, nz]`.
181    pub dims: [u32; 3],
182}
183
184impl UniformGridGpu {
185    /// Create a new uniform grid.
186    pub fn new(cell_size: f32, origin: [f32; 3], dims: [u32; 3]) -> Self {
187        Self {
188            cell_size,
189            origin,
190            dims,
191        }
192    }
193
194    /// Compute the cell index `[ix, iy, iz]` for a world-space position.
195    pub fn cell_of(&self, pos: [f32; 3]) -> [i32; 3] {
196        [
197            ((pos[0] - self.origin[0]) / self.cell_size).floor() as i32,
198            ((pos[1] - self.origin[1]) / self.cell_size).floor() as i32,
199            ((pos[2] - self.origin[2]) / self.cell_size).floor() as i32,
200        ]
201    }
202
203    /// Pack cell indices into a `u64` key for use in a HashMap.
204    fn cell_key(ix: i32, iy: i32, iz: i32) -> u64 {
205        // Pack three i16-range integers into a u64.
206        let x = (ix as i16) as u64 & 0xFFFF;
207        let y = (iy as i16) as u64 & 0xFFFF;
208        let z = (iz as i16) as u64 & 0xFFFF;
209        (z << 32) | (y << 16) | x
210    }
211
212    /// Insert all AABBs into the uniform grid.
213    ///
214    /// Each AABB is inserted into every cell it overlaps.  Returns a map from
215    /// packed cell key to the list of `body_id`s in that cell.
216    pub fn insert_aabbs(&self, aabbs: &[AabbGpu]) -> HashMap<u64, Vec<u32>> {
217        let mut map: HashMap<u64, Vec<u32>> = HashMap::new();
218
219        for aabb in aabbs {
220            let min_cell = self.cell_of(aabb.min);
221            let max_cell = self.cell_of(aabb.max);
222
223            for iz in min_cell[2]..=max_cell[2] {
224                for iy in min_cell[1]..=max_cell[1] {
225                    for ix in min_cell[0]..=max_cell[0] {
226                        let key = Self::cell_key(ix, iy, iz);
227                        map.entry(key).or_default().push(aabb.body_id);
228                    }
229                }
230            }
231        }
232
233        map
234    }
235
236    /// Query all overlapping AABB pairs using the uniform grid.
237    ///
238    /// Returns deduplicated pairs `(a, b)` with `a < b`.
239    pub fn query_pairs(&self, aabbs: &[AabbGpu]) -> Vec<(u32, u32)> {
240        let map = self.insert_aabbs(aabbs);
241
242        let mut seen = std::collections::HashSet::new();
243        let mut pairs = Vec::new();
244
245        for body_list in map.values() {
246            let n = body_list.len();
247            for i in 0..n {
248                for j in (i + 1)..n {
249                    let a = body_list[i];
250                    let b = body_list[j];
251                    let pair = if a < b { (a, b) } else { (b, a) };
252                    if seen.insert(pair) {
253                        // Verify actual AABB overlap
254                        if let (Some(aa), Some(bb)) = (
255                            aabbs.iter().find(|x| x.body_id == pair.0),
256                            aabbs.iter().find(|x| x.body_id == pair.1),
257                        ) && aa.overlaps(bb)
258                        {
259                            pairs.push(pair);
260                        }
261                    }
262                }
263            }
264        }
265
266        pairs.sort();
267        pairs
268    }
269}
270
271// ---------------------------------------------------------------------------
272// Morton code (Z-curve)
273// ---------------------------------------------------------------------------
274
275/// Compute the 3-D Morton code (Z-curve) for integer coordinates.
276///
277/// Interleaves the bits of `x`, `y`, and `z` to produce a single `u64`
278/// space-filling key.  Each coordinate may use up to 21 bits.
279pub fn morton_code(x: u32, y: u32, z: u32) -> u64 {
280    spread_bits(x as u64) | (spread_bits(y as u64) << 1) | (spread_bits(z as u64) << 2)
281}
282
283/// Spread the bits of a 21-bit integer, inserting two zero bits between each bit.
284#[inline]
285fn spread_bits(mut v: u64) -> u64 {
286    v &= 0x1fffff; // keep lower 21 bits
287    v = (v | (v << 32)) & 0x1f00000000ffff;
288    v = (v | (v << 16)) & 0x1f0000ff0000ff;
289    v = (v | (v << 8)) & 0x100f00f00f00f00f;
290    v = (v | (v << 4)) & 0x10c30c30c30c30c3;
291    v = (v | (v << 2)) & 0x1249249249249249;
292    v
293}
294
295// ---------------------------------------------------------------------------
296// Sort-and-Sweep (CPU-side mock for GPU-style parallel data)
297// ---------------------------------------------------------------------------
298
299/// Sort-and-sweep on a flat f64 AABB array (GPU-style parallel data layout).
300///
301/// Input layout per object: `[min_x, max_x, min_y, max_y, min_z, max_z]`.
302/// Returns pairs of indices `(i, j)` (sorted, deduped) that overlap.
303pub fn sort_and_sweep_flat(aabbs: &[f64]) -> Vec<(usize, usize)> {
304    let n = aabbs.len() / 6;
305    if n == 0 {
306        return Vec::new();
307    }
308
309    // Sort by min_x
310    let mut order: Vec<usize> = (0..n).collect();
311    order.sort_by(|&a, &b| {
312        let ax = aabbs[a * 6];
313        let bx = aabbs[b * 6];
314        ax.partial_cmp(&bx).unwrap_or(std::cmp::Ordering::Equal)
315    });
316
317    let mut pairs = Vec::new();
318    for (i, &si) in order.iter().enumerate() {
319        let max_x_i = aabbs[si * 6 + 1];
320        for &sj in order.iter().skip(i + 1) {
321            if aabbs[sj * 6] > max_x_i {
322                break; // Early out: rest cannot overlap in X
323            }
324            // Full 3-D overlap check
325            let ai = &aabbs[si * 6..(si + 1) * 6];
326            let aj = &aabbs[sj * 6..(sj + 1) * 6];
327            if ai[0] <= aj[1]
328                && ai[1] >= aj[0]
329                && ai[2] <= aj[3]
330                && ai[3] >= aj[2]
331                && ai[4] <= aj[5]
332                && ai[5] >= aj[4]
333            {
334                let pair = if si < sj { (si, sj) } else { (sj, si) };
335                pairs.push(pair);
336            }
337        }
338    }
339    pairs.sort();
340    pairs.dedup();
341    pairs
342}
343
344// ---------------------------------------------------------------------------
345// Uniform Grid (CPU-side parallel data helper)
346// ---------------------------------------------------------------------------
347
348/// Assigns each AABB in a flat array to uniform grid cells.
349///
350/// Returns a `Vec<(cell_key, body_idx)>` (unsorted, one entry per cell overlap).
351pub fn assign_to_grid_cells(aabbs: &[f64], cell_size: f64, origin: [f64; 3]) -> Vec<(u64, usize)> {
352    let n = aabbs.len() / 6;
353    let mut result = Vec::new();
354    let pack = |ix: i64, iy: i64, iz: i64| -> u64 {
355        let x = (ix as i16) as u64 & 0xFFFF;
356        let y = (iy as i16) as u64 & 0xFFFF;
357        let z = (iz as i16) as u64 & 0xFFFF;
358        (z << 32) | (y << 16) | x
359    };
360    for i in 0..n {
361        let a = &aabbs[i * 6..(i + 1) * 6];
362        let ix0 = ((a[0] - origin[0]) / cell_size).floor() as i64;
363        let ix1 = ((a[1] - origin[0]) / cell_size).floor() as i64;
364        let iy0 = ((a[2] - origin[1]) / cell_size).floor() as i64;
365        let iy1 = ((a[3] - origin[1]) / cell_size).floor() as i64;
366        let iz0 = ((a[4] - origin[2]) / cell_size).floor() as i64;
367        let iz1 = ((a[5] - origin[2]) / cell_size).floor() as i64;
368        for iz in iz0..=iz1 {
369            for iy in iy0..=iy1 {
370                for ix in ix0..=ix1 {
371                    result.push((pack(ix, iy, iz), i));
372                }
373            }
374        }
375    }
376    result
377}
378
379/// Extract candidate pairs from a cell-assignment list.
380///
381/// Groups entries by cell key and emits pairs for bodies sharing a cell.
382pub fn pairs_from_grid_assignments(assignments: &[(u64, usize)]) -> Vec<(usize, usize)> {
383    let mut by_cell: HashMap<u64, Vec<usize>> = HashMap::new();
384    for &(key, idx) in assignments {
385        by_cell.entry(key).or_default().push(idx);
386    }
387    let mut seen = std::collections::HashSet::new();
388    let mut pairs = Vec::new();
389    for bodies in by_cell.values() {
390        let n = bodies.len();
391        for i in 0..n {
392            for j in (i + 1)..n {
393                let a = bodies[i];
394                let b = bodies[j];
395                let p = if a < b { (a, b) } else { (b, a) };
396                if seen.insert(p) {
397                    pairs.push(p);
398                }
399            }
400        }
401    }
402    pairs.sort();
403    pairs
404}
405
406// ---------------------------------------------------------------------------
407// Morton code sort utilities
408// ---------------------------------------------------------------------------
409
410/// Compute a Morton key for a world-space AABB centroid.
411///
412/// Quantises each coordinate to a 21-bit integer using `cell_size` and `origin`.
413pub fn morton_key_for_aabb(aabb: &AabbGpu, cell_size: f32, origin: [f32; 3]) -> u64 {
414    let cx = ((aabb.min[0] + aabb.max[0]) * 0.5 - origin[0]) / cell_size;
415    let cy = ((aabb.min[1] + aabb.max[1]) * 0.5 - origin[1]) / cell_size;
416    let cz = ((aabb.min[2] + aabb.max[2]) * 0.5 - origin[2]) / cell_size;
417
418    let ix = (cx.max(0.0) as u32).min(0x1F_FFFF);
419    let iy = (cy.max(0.0) as u32).min(0x1F_FFFF);
420    let iz = (cz.max(0.0) as u32).min(0x1F_FFFF);
421    morton_code(ix, iy, iz)
422}
423
424/// Sort a slice of `AabbGpu` by Morton code (Z-order curve).
425///
426/// Returns a new `Vec`AabbGpu` sorted by `morton_key_for_aabb`.
427pub fn morton_sort(aabbs: &[AabbGpu], cell_size: f32, origin: [f32; 3]) -> Vec<AabbGpu> {
428    let mut keyed: Vec<(u64, AabbGpu)> = aabbs
429        .iter()
430        .map(|a| (morton_key_for_aabb(a, cell_size, origin), *a))
431        .collect();
432    keyed.sort_by_key(|&(k, _)| k);
433    keyed.into_iter().map(|(_, a)| a).collect()
434}
435
436// ---------------------------------------------------------------------------
437// Compact pairs list
438// ---------------------------------------------------------------------------
439
440/// A deduplicated, sorted list of overlapping body-ID pairs.
441#[derive(Debug, Clone, Default)]
442pub struct CompactPairList {
443    pairs: Vec<(u32, u32)>,
444}
445
446impl CompactPairList {
447    /// Create an empty list.
448    pub fn new() -> Self {
449        Self::default()
450    }
451
452    /// Insert a pair (order-normalised to a < b).
453    pub fn insert(&mut self, a: u32, b: u32) {
454        let pair = if a < b { (a, b) } else { (b, a) };
455        // Simple insert-if-not-present (small lists)
456        if !self.pairs.contains(&pair) {
457            self.pairs.push(pair);
458        }
459    }
460
461    /// Insert all pairs from a `Vec`.
462    pub fn insert_all(&mut self, pairs: &[(u32, u32)]) {
463        for &(a, b) in pairs {
464            self.insert(a, b);
465        }
466    }
467
468    /// Sort the pair list.
469    pub fn sort(&mut self) {
470        self.pairs.sort();
471    }
472
473    /// Return a reference to all pairs.
474    pub fn pairs(&self) -> &[(u32, u32)] {
475        &self.pairs
476    }
477
478    /// Number of pairs.
479    pub fn len(&self) -> usize {
480        self.pairs.len()
481    }
482
483    /// Returns true if the list is empty.
484    pub fn is_empty(&self) -> bool {
485        self.pairs.is_empty()
486    }
487
488    /// Remove pairs involving a specific body (e.g., after removal from simulation).
489    pub fn remove_body(&mut self, body_id: u32) {
490        self.pairs.retain(|&(a, b)| a != body_id && b != body_id);
491    }
492
493    /// Returns `true` if the given pair exists.
494    pub fn contains(&self, a: u32, b: u32) -> bool {
495        let p = if a < b { (a, b) } else { (b, a) };
496        self.pairs.contains(&p)
497    }
498}
499
500// ---------------------------------------------------------------------------
501// BVH (GPU)
502// ---------------------------------------------------------------------------
503
504/// A single node of a GPU-oriented BVH tree.
505#[derive(Debug, Clone, Copy)]
506pub struct BvhGpuNode {
507    /// Bounding box of this node.
508    pub aabb: AabbGpu,
509    /// Index of the left child node, or `< 0` for a leaf (then `-body_id - 1`).
510    pub left: i32,
511    /// Index of the right child node, or `< 0` for a leaf (then `-body_id - 1`).
512    pub right: i32,
513}
514
515impl BvhGpuNode {
516    /// Create an internal node.
517    pub fn internal(aabb: AabbGpu, left: i32, right: i32) -> Self {
518        Self { aabb, left, right }
519    }
520
521    /// Create a leaf node for a given body.
522    pub fn leaf(aabb: AabbGpu) -> Self {
523        let id = aabb.body_id as i32;
524        Self {
525            aabb,
526            left: -(id + 1),
527            right: -(id + 1),
528        }
529    }
530
531    /// Return true if this is a leaf node.
532    pub fn is_leaf(&self) -> bool {
533        self.left < 0
534    }
535}
536
537/// Build a BVH using a simple midpoint-split heuristic.
538///
539/// Returns a flat `Vec`BvhGpuNode` with node 0 as the root.
540/// Internal nodes reference their children by index into this vec.
541pub fn build_bvh(aabbs: &[AabbGpu]) -> Vec<BvhGpuNode> {
542    let mut nodes = Vec::new();
543    if aabbs.is_empty() {
544        return nodes;
545    }
546    let mut indices: Vec<usize> = (0..aabbs.len()).collect();
547    build_bvh_recursive(aabbs, &mut indices, &mut nodes);
548    nodes
549}
550
551fn merge_aabbs(aabbs: &[AabbGpu], indices: &[usize]) -> AabbGpu {
552    let mut min = aabbs[indices[0]].min;
553    let mut max = aabbs[indices[0]].max;
554    for &idx in &indices[1..] {
555        let a = &aabbs[idx];
556        for k in 0..3 {
557            if a.min[k] < min[k] {
558                min[k] = a.min[k];
559            }
560            if a.max[k] > max[k] {
561                max[k] = a.max[k];
562            }
563        }
564    }
565    AabbGpu {
566        min,
567        max,
568        body_id: 0,
569    }
570}
571
572fn build_bvh_recursive(
573    aabbs: &[AabbGpu],
574    indices: &mut [usize],
575    nodes: &mut Vec<BvhGpuNode>,
576) -> i32 {
577    let n = indices.len();
578    let merged = merge_aabbs(aabbs, indices);
579
580    if n == 1 {
581        let idx = nodes.len() as i32;
582        nodes.push(BvhGpuNode::leaf(aabbs[indices[0]]));
583        return idx;
584    }
585
586    // Find the longest axis of the merged AABB.
587    let extents = [
588        merged.max[0] - merged.min[0],
589        merged.max[1] - merged.min[1],
590        merged.max[2] - merged.min[2],
591    ];
592    let axis = if extents[0] >= extents[1] && extents[0] >= extents[2] {
593        0
594    } else if extents[1] >= extents[2] {
595        1
596    } else {
597        2
598    };
599
600    // Sort by centroid along longest axis.
601    indices.sort_by(|&a, &b| {
602        let ca = (aabbs[a].min[axis] + aabbs[a].max[axis]) * 0.5;
603        let cb = (aabbs[b].min[axis] + aabbs[b].max[axis]) * 0.5;
604        ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
605    });
606
607    let mid = n / 2;
608    let (left_idx, right_idx) = indices.split_at_mut(mid);
609
610    // Reserve a slot for the internal node before recursing.
611    let node_idx = nodes.len() as i32;
612    nodes.push(BvhGpuNode {
613        aabb: merged,
614        left: 0,
615        right: 0,
616    }); // placeholder
617
618    let mut left_indices = left_idx.to_vec();
619    let mut right_indices = right_idx.to_vec();
620
621    let left = build_bvh_recursive(aabbs, &mut left_indices, nodes);
622    let right = build_bvh_recursive(aabbs, &mut right_indices, nodes);
623
624    nodes[node_idx as usize].left = left;
625    nodes[node_idx as usize].right = right;
626
627    node_idx
628}
629
630// ---------------------------------------------------------------------------
631// LBVH — Linear BVH traversal
632// ---------------------------------------------------------------------------
633
634/// Query overlapping leaf pairs in an LBVH by traversal.
635///
636/// Traverses the BVH from the root to find all pairs of leaf nodes whose
637/// AABBs overlap.  Returns `Vec<(u32, u32)>` with `a < b` and deduplication.
638pub fn lbvh_query_pairs(nodes: &[BvhGpuNode]) -> Vec<(u32, u32)> {
639    if nodes.is_empty() {
640        return Vec::new();
641    }
642    let mut pairs = Vec::new();
643    // Stack-based traversal: (nodeA, nodeB)
644    let mut stack: Vec<(usize, usize)> = Vec::new();
645    // Self-collision query: test root against itself
646    stack.push((0, 0));
647
648    while let Some((a_idx, b_idx)) = stack.pop() {
649        let na = &nodes[a_idx];
650        let nb = &nodes[b_idx];
651
652        if !na.aabb.overlaps(&nb.aabb) {
653            continue;
654        }
655
656        if na.is_leaf() && nb.is_leaf() {
657            if a_idx != b_idx {
658                let id_a = na.aabb.body_id;
659                let id_b = nb.aabb.body_id;
660                let pair = if id_a < id_b {
661                    (id_a, id_b)
662                } else {
663                    (id_b, id_a)
664                };
665                pairs.push(pair);
666            }
667            continue;
668        }
669
670        // Descend into larger node first
671        if na.is_leaf() {
672            // descend b
673            if nb.left >= 0 {
674                stack.push((a_idx, nb.left as usize));
675            }
676            if nb.right >= 0 {
677                stack.push((a_idx, nb.right as usize));
678            }
679        } else if nb.is_leaf() {
680            // descend a
681            if na.left >= 0 {
682                stack.push((na.left as usize, b_idx));
683            }
684            if na.right >= 0 {
685                stack.push((na.right as usize, b_idx));
686            }
687        } else {
688            // Both internal — descend a
689            if na.left >= 0 {
690                stack.push((na.left as usize, b_idx));
691            }
692            if na.right >= 0 {
693                stack.push((na.right as usize, b_idx));
694            }
695        }
696    }
697    pairs.sort();
698    pairs.dedup();
699    pairs
700}
701
702// ---------------------------------------------------------------------------
703// BVH refitting
704// ---------------------------------------------------------------------------
705
706/// Refit BVH bounding boxes bottom-up after leaf AABBs have changed.
707///
708/// For each internal node, recomputes its AABB as the union of its children.
709/// Assumes a flat node array where children always have higher indices
710/// (which is guaranteed by `build_bvh_recursive`).
711pub fn refit_bvh(nodes: &mut [BvhGpuNode]) {
712    // Process nodes in reverse order (children before parents)
713    let n = nodes.len();
714    for i in (0..n).rev() {
715        if nodes[i].is_leaf() {
716            continue; // Leaves are driven by actual AABB data — no refit needed here
717        }
718        let left_idx = nodes[i].left;
719        let right_idx = nodes[i].right;
720        if left_idx < 0 || right_idx < 0 {
721            continue;
722        }
723        let l = &nodes[left_idx as usize];
724        let r = &nodes[right_idx as usize];
725        let mut min = l.aabb.min;
726        let mut max = l.aabb.max;
727        for k in 0..3 {
728            if r.aabb.min[k] < min[k] {
729                min[k] = r.aabb.min[k];
730            }
731            if r.aabb.max[k] > max[k] {
732                max[k] = r.aabb.max[k];
733            }
734        }
735        nodes[i].aabb = AabbGpu {
736            min,
737            max,
738            body_id: 0,
739        };
740    }
741}
742
743// ---------------------------------------------------------------------------
744// BVH quality metrics
745// ---------------------------------------------------------------------------
746
747/// Compute the surface area of an `AabbGpu`.
748pub fn aabb_surface_area(aabb: &AabbGpu) -> f32 {
749    let dx = aabb.max[0] - aabb.min[0];
750    let dy = aabb.max[1] - aabb.min[1];
751    let dz = aabb.max[2] - aabb.min[2];
752    2.0 * (dx * dy + dy * dz + dz * dx)
753}
754
755/// Surface Area Heuristic (SAH) cost of a BVH tree.
756///
757/// `SAH = sum_{internal nodes} SA(node) / SA(root) * cost_traversal`
758///       `+ sum_{leaf nodes} SA(leaf) / SA(root) * num_primitives`
759///
760/// A lower SAH cost indicates a better-quality BVH.
761pub fn bvh_sah_cost(nodes: &[BvhGpuNode], cost_traversal: f32, cost_primitive: f32) -> f32 {
762    if nodes.is_empty() {
763        return 0.0;
764    }
765    let root_sa = aabb_surface_area(&nodes[0].aabb);
766    if root_sa < 1e-20 {
767        return 0.0;
768    }
769    let mut cost = 0.0f32;
770    for node in nodes {
771        let sa = aabb_surface_area(&node.aabb);
772        if node.is_leaf() {
773            cost += sa / root_sa * cost_primitive;
774        } else {
775            cost += sa / root_sa * cost_traversal;
776        }
777    }
778    cost
779}
780
781/// Compute the depth of the BVH tree.
782///
783/// Returns the maximum node depth from root (depth 0) to the deepest leaf.
784pub fn bvh_depth(nodes: &[BvhGpuNode]) -> usize {
785    if nodes.is_empty() {
786        return 0;
787    }
788    let mut max_depth = 0usize;
789    // Stack: (node_idx, current_depth)
790    let mut stack: Vec<(usize, usize)> = vec![(0, 0)];
791    while let Some((idx, depth)) = stack.pop() {
792        if depth > max_depth {
793            max_depth = depth;
794        }
795        let node = &nodes[idx];
796        if !node.is_leaf() {
797            if node.left >= 0 {
798                stack.push((node.left as usize, depth + 1));
799            }
800            if node.right >= 0 {
801                stack.push((node.right as usize, depth + 1));
802            }
803        }
804    }
805    max_depth
806}
807
808/// Count the number of leaf nodes in the BVH.
809pub fn bvh_leaf_count(nodes: &[BvhGpuNode]) -> usize {
810    nodes.iter().filter(|n| n.is_leaf()).count()
811}
812
813// ---------------------------------------------------------------------------
814// Parallel SAP update (incremental update for moved bodies)
815// ---------------------------------------------------------------------------
816
817/// Update the sort-and-sweep pair list after a set of bodies have moved.
818///
819/// This is an incremental update: only re-run SAP for the bodies that moved
820/// and merge with the existing pair list.  All existing pairs involving moved
821/// bodies are removed and recomputed.
822///
823/// `moved_ids`: set of `body_id`s that have moved.
824/// `aabbs`: updated AABB array (all bodies).
825///
826/// Returns the updated pair list.
827pub fn sap_incremental_update(
828    existing: &CompactPairList,
829    aabbs: &[AabbGpu],
830    moved_ids: &[u32],
831) -> CompactPairList {
832    let mut new_list = existing.clone();
833
834    // Remove all pairs involving moved bodies
835    for &id in moved_ids {
836        new_list.remove_body(id);
837    }
838
839    // Re-detect pairs for moved bodies against all others
840    for &moved_id in moved_ids {
841        if let Some(moved_aabb) = aabbs.iter().find(|a| a.body_id == moved_id) {
842            for other in aabbs {
843                if other.body_id == moved_id {
844                    continue;
845                }
846                if moved_aabb.overlaps(other) {
847                    new_list.insert(moved_id, other.body_id);
848                }
849            }
850        }
851    }
852
853    new_list.sort();
854    new_list
855}
856
857// ---------------------------------------------------------------------------
858// SAH split heuristic
859// ---------------------------------------------------------------------------
860
861/// Surface Area Heuristic (SAH) split for BVH construction.
862///
863/// Evaluates `num_bins` candidate split planes along the given axis and
864/// returns the bin index (from 0 to `num_bins-1`) that minimises the SAH cost.
865///
866/// Returns `None` if all primitives have the same centroid along the axis.
867pub fn sah_best_split(
868    aabbs: &[AabbGpu],
869    indices: &[usize],
870    axis: usize,
871    num_bins: usize,
872) -> Option<usize> {
873    if indices.len() < 2 || num_bins < 2 {
874        return None;
875    }
876
877    // Centroid range
878    let min_c = indices
879        .iter()
880        .map(|&i| 0.5 * (aabbs[i].min[axis] + aabbs[i].max[axis]))
881        .fold(f32::INFINITY, f32::min);
882    let max_c = indices
883        .iter()
884        .map(|&i| 0.5 * (aabbs[i].min[axis] + aabbs[i].max[axis]))
885        .fold(f32::NEG_INFINITY, f32::max);
886
887    if (max_c - min_c).abs() < 1e-10 {
888        return None;
889    }
890
891    let bin_width = (max_c - min_c) / num_bins as f32;
892    let mut bin_counts = vec![0usize; num_bins];
893    let mut bin_aabbs: Vec<Option<AabbGpu>> = vec![None; num_bins];
894
895    for &i in indices {
896        let c = 0.5 * (aabbs[i].min[axis] + aabbs[i].max[axis]);
897        let bin = ((c - min_c) / bin_width).floor() as usize;
898        let bin = bin.min(num_bins - 1);
899        bin_counts[bin] += 1;
900        bin_aabbs[bin] = Some(match &bin_aabbs[bin] {
901            None => aabbs[i],
902            Some(prev) => {
903                let mut merged = *prev;
904                for k in 0..3 {
905                    if aabbs[i].min[k] < merged.min[k] {
906                        merged.min[k] = aabbs[i].min[k];
907                    }
908                    if aabbs[i].max[k] > merged.max[k] {
909                        merged.max[k] = aabbs[i].max[k];
910                    }
911                }
912                merged
913            }
914        });
915    }
916
917    let mut best_cost = f32::INFINITY;
918    let mut best_split = None;
919
920    for split in 1..num_bins {
921        let left_count: usize = bin_counts[..split].iter().sum();
922        let right_count: usize = bin_counts[split..].iter().sum();
923        if left_count == 0 || right_count == 0 {
924            continue;
925        }
926
927        // Compute left and right bounding boxes
928        let left_sa = bin_aabbs[..split]
929            .iter()
930            .flatten()
931            .fold(None::<AabbGpu>, |acc, a| {
932                Some(match acc {
933                    None => *a,
934                    Some(prev) => {
935                        let mut m = prev;
936                        for k in 0..3 {
937                            if a.min[k] < m.min[k] {
938                                m.min[k] = a.min[k];
939                            }
940                            if a.max[k] > m.max[k] {
941                                m.max[k] = a.max[k];
942                            }
943                        }
944                        m
945                    }
946                })
947            })
948            .map(|a| aabb_surface_area(&a))
949            .unwrap_or(0.0);
950
951        let right_sa = bin_aabbs[split..]
952            .iter()
953            .flatten()
954            .fold(None::<AabbGpu>, |acc, a| {
955                Some(match acc {
956                    None => *a,
957                    Some(prev) => {
958                        let mut m = prev;
959                        for k in 0..3 {
960                            if a.min[k] < m.min[k] {
961                                m.min[k] = a.min[k];
962                            }
963                            if a.max[k] > m.max[k] {
964                                m.max[k] = a.max[k];
965                            }
966                        }
967                        m
968                    }
969                })
970            })
971            .map(|a| aabb_surface_area(&a))
972            .unwrap_or(0.0);
973
974        let cost = left_sa * left_count as f32 + right_sa * right_count as f32;
975        if cost < best_cost {
976            best_cost = cost;
977            best_split = Some(split);
978        }
979    }
980
981    best_split
982}
983
984// ---------------------------------------------------------------------------
985// LBVH build using Morton codes
986// ---------------------------------------------------------------------------
987
988/// Build a Linear BVH (LBVH) by sorting AABBs on their Morton codes.
989///
990/// This is a GPU-style construction algorithm:
991/// 1. Compute Morton code for each AABB centroid.
992/// 2. Sort AABBs by Morton code.
993/// 3. Recursively build BVH on sorted order (binary radix tree).
994///
995/// Returns a flat node array with node 0 as the root.
996pub fn build_lbvh(aabbs: &[AabbGpu], cell_size: f32, origin: [f32; 3]) -> Vec<BvhGpuNode> {
997    if aabbs.is_empty() {
998        return Vec::new();
999    }
1000    // Sort by Morton code
1001    let sorted = morton_sort(aabbs, cell_size, origin);
1002    // Build BVH on sorted order
1003    build_bvh(&sorted)
1004}
1005
1006// ---------------------------------------------------------------------------
1007// Tests
1008// ---------------------------------------------------------------------------
1009
1010#[cfg(test)]
1011mod tests {
1012    use super::*;
1013
1014    #[test]
1015    fn aabb_overlap_detects_overlapping_boxes() {
1016        // Two boxes that clearly overlap
1017        #[rustfmt::skip]
1018        let aabbs: Vec<f64> = vec![
1019            0.0, 2.0, 0.0, 2.0, 0.0, 2.0, // box 0
1020            1.0, 3.0, 1.0, 3.0, 1.0, 3.0, // box 1 (overlaps box 0)
1021        ];
1022        let mut outputs = vec![Vec::new()];
1023        AabbOverlapKernel.execute(&[&aabbs], &mut outputs, 2);
1024        assert_eq!(outputs[0], vec![0.0, 1.0]);
1025    }
1026
1027    #[test]
1028    fn aabb_overlap_rejects_non_overlapping_boxes() {
1029        #[rustfmt::skip]
1030        let aabbs: Vec<f64> = vec![
1031            0.0, 1.0, 0.0, 1.0, 0.0, 1.0, // box 0
1032            5.0, 6.0, 5.0, 6.0, 5.0, 6.0, // box 1 (far away)
1033        ];
1034        let mut outputs = vec![Vec::new()];
1035        AabbOverlapKernel.execute(&[&aabbs], &mut outputs, 2);
1036        assert!(outputs[0].is_empty());
1037    }
1038
1039    #[test]
1040    fn test_broadphase_gpu_matches_cpu() {
1041        // Three AABBs: 0 overlaps 1, 1 overlaps 2, but 0 does NOT overlap 2.
1042        #[rustfmt::skip]
1043        let aabbs: Vec<f64> = vec![
1044            0.0, 1.5, 0.0, 1.5, 0.0, 1.5, // box 0
1045            1.0, 2.5, 0.0, 1.5, 0.0, 1.5, // box 1 (overlaps 0 in x)
1046            3.0, 4.0, 0.0, 1.5, 0.0, 1.5, // box 2 (overlaps 1 in x, not 0)
1047        ];
1048
1049        // GPU kernel output
1050        let mut gpu_outputs = vec![Vec::new()];
1051        AabbOverlapKernel.execute(&[&aabbs], &mut gpu_outputs, 3);
1052
1053        // CPU brute-force reference
1054        let n = aabbs.len() / 6;
1055        let mut cpu_pairs: Vec<(usize, usize)> = Vec::new();
1056        for i in 0..n {
1057            for j in (i + 1)..n {
1058                let a = &aabbs[i * 6..(i + 1) * 6];
1059                let b = &aabbs[j * 6..(j + 1) * 6];
1060                let overlaps = a[0] <= b[1]
1061                    && a[1] >= b[0]
1062                    && a[2] <= b[3]
1063                    && a[3] >= b[2]
1064                    && a[4] <= b[5]
1065                    && a[5] >= b[4];
1066                if overlaps {
1067                    cpu_pairs.push((i, j));
1068                }
1069            }
1070        }
1071
1072        // Convert GPU output to (usize, usize) pairs
1073        let raw = &gpu_outputs[0];
1074        assert_eq!(raw.len() % 2, 0, "GPU output length must be even");
1075        let gpu_pairs: Vec<(usize, usize)> = raw
1076            .chunks(2)
1077            .map(|c| (c[0] as usize, c[1] as usize))
1078            .collect();
1079
1080        assert_eq!(
1081            gpu_pairs, cpu_pairs,
1082            "GPU broadphase pairs do not match CPU brute-force pairs"
1083        );
1084    }
1085
1086    /// Morton code: (0,0,0) → 0, and each axis occupies its own bit lanes.
1087    #[test]
1088    fn test_morton_code_correctness() {
1089        assert_eq!(morton_code(0, 0, 0), 0);
1090        // x=1: only bit 0 of x set → Morton bit 0 (axis 0)
1091        assert_eq!(morton_code(1, 0, 0), 1);
1092        // y=1: Morton bit 1
1093        assert_eq!(morton_code(0, 1, 0), 2);
1094        // z=1: Morton bit 2
1095        assert_eq!(morton_code(0, 0, 1), 4);
1096        // x=1, y=1, z=1 → bits 0,1,2 all set
1097        assert_eq!(morton_code(1, 1, 1), 7);
1098    }
1099
1100    /// SAP detects overlapping GPU AABBs.
1101    #[test]
1102    fn test_sap_finds_overlap() {
1103        let a = AabbGpu::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0], 0);
1104        let b = AabbGpu::new([1.0, 1.0, 1.0], [3.0, 3.0, 3.0], 1);
1105        let c = AabbGpu::new([10.0, 10.0, 10.0], [12.0, 12.0, 12.0], 2);
1106
1107        let pairs = SortAndSweepGpu::detect_pairs(&[a, b, c]);
1108        assert!(
1109            pairs.contains(&(0, 1)),
1110            "SAP should find pair (0,1), got {pairs:?}"
1111        );
1112        assert!(!pairs.contains(&(0, 2)), "SAP should not find pair (0,2)");
1113        assert!(!pairs.contains(&(1, 2)), "SAP should not find pair (1,2)");
1114    }
1115
1116    /// SAP returns empty for non-overlapping AABBs.
1117    #[test]
1118    fn test_sap_no_overlap() {
1119        let a = AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0);
1120        let b = AabbGpu::new([5.0, 5.0, 5.0], [6.0, 6.0, 6.0], 1);
1121        let pairs = SortAndSweepGpu::detect_pairs(&[a, b]);
1122        assert!(
1123            pairs.is_empty(),
1124            "Should be no overlapping pairs, got {pairs:?}"
1125        );
1126    }
1127
1128    /// Uniform grid detects the same pairs as brute-force for 3 AABBs.
1129    #[test]
1130    fn test_uniform_grid_pair_detection() {
1131        let a = AabbGpu::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0], 0);
1132        let b = AabbGpu::new([1.5, 1.5, 1.5], [3.5, 3.5, 3.5], 1);
1133        let c = AabbGpu::new([10.0, 10.0, 10.0], [12.0, 12.0, 12.0], 2);
1134
1135        let grid = UniformGridGpu::new(5.0, [0.0, 0.0, 0.0], [10, 10, 10]);
1136        let pairs = grid.query_pairs(&[a, b, c]);
1137
1138        // Only a and b overlap.
1139        assert!(
1140            pairs.contains(&(0, 1)),
1141            "Grid should find pair (0,1), got {pairs:?}"
1142        );
1143        assert!(
1144            !pairs
1145                .iter()
1146                .any(|&(x, y)| x == 0 && y == 2 || x == 2 && y == 0)
1147        );
1148    }
1149
1150    /// BVH builds without panic and covers all body AABBs.
1151    #[test]
1152    fn test_bvh_depth() {
1153        let aabbs: Vec<AabbGpu> = (0..8)
1154            .map(|i| {
1155                let x = (i * 3) as f32;
1156                AabbGpu::new([x, 0.0, 0.0], [x + 1.0, 1.0, 1.0], i)
1157            })
1158            .collect();
1159
1160        let nodes = build_bvh(&aabbs);
1161
1162        // There should be at least 1 node, and at most 2*n-1 nodes.
1163        assert!(!nodes.is_empty(), "BVH should have at least one node");
1164        assert!(
1165            nodes.len() <= 2 * aabbs.len(),
1166            "BVH node count unexpected: {}",
1167            nodes.len()
1168        );
1169
1170        // The root node (index 0) should cover the entire range.
1171        let root = &nodes[0];
1172        assert!(root.aabb.min[0] <= 0.0 + 1e-5, "Root min_x too large");
1173        assert!(
1174            root.aabb.max[0] >= 22.0 - 1e-5,
1175            "Root max_x too small: {}",
1176            root.aabb.max[0]
1177        );
1178    }
1179
1180    /// AabbGpu::overlaps is symmetric.
1181    #[test]
1182    fn test_aabb_gpu_overlap_symmetric() {
1183        let a = AabbGpu::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0], 0);
1184        let b = AabbGpu::new([1.0, 1.0, 1.0], [3.0, 3.0, 3.0], 1);
1185        assert_eq!(a.overlaps(&b), b.overlaps(&a));
1186
1187        let c = AabbGpu::new([5.0, 5.0, 5.0], [6.0, 6.0, 6.0], 2);
1188        assert_eq!(a.overlaps(&c), c.overlaps(&a));
1189        assert!(!a.overlaps(&c));
1190    }
1191
1192    // ── New expanded tests ──
1193
1194    #[test]
1195    fn test_sort_and_sweep_flat_finds_pair() {
1196        #[rustfmt::skip]
1197        let aabbs: Vec<f64> = vec![
1198            0.0, 2.0, 0.0, 2.0, 0.0, 2.0, // box 0
1199            1.0, 3.0, 1.0, 3.0, 1.0, 3.0, // box 1 overlaps 0
1200            5.0, 6.0, 5.0, 6.0, 5.0, 6.0, // box 2 no overlap
1201        ];
1202        let pairs = sort_and_sweep_flat(&aabbs);
1203        assert!(pairs.contains(&(0, 1)), "should find (0,1)");
1204        assert!(!pairs.iter().any(|&(a, b)| (a == 0 || a == 1) && b == 2));
1205    }
1206
1207    #[test]
1208    fn test_sort_and_sweep_flat_empty() {
1209        let pairs = sort_and_sweep_flat(&[]);
1210        assert!(pairs.is_empty());
1211    }
1212
1213    #[test]
1214    fn test_sort_and_sweep_flat_no_overlap() {
1215        #[rustfmt::skip]
1216        let aabbs: Vec<f64> = vec![
1217            0.0, 1.0, 0.0, 1.0, 0.0, 1.0,
1218            2.0, 3.0, 2.0, 3.0, 2.0, 3.0,
1219        ];
1220        let pairs = sort_and_sweep_flat(&aabbs);
1221        assert!(pairs.is_empty());
1222    }
1223
1224    #[test]
1225    fn test_assign_to_grid_cells() {
1226        #[rustfmt::skip]
1227        let aabbs: Vec<f64> = vec![
1228            0.0, 1.0, 0.0, 1.0, 0.0, 1.0, // fits in cell (0,0,0)
1229        ];
1230        let cells = assign_to_grid_cells(&aabbs, 2.0, [0.0, 0.0, 0.0]);
1231        assert!(!cells.is_empty());
1232        // All entries should have body index 0
1233        assert!(cells.iter().all(|&(_, idx)| idx == 0));
1234    }
1235
1236    #[test]
1237    fn test_pairs_from_grid_assignments() {
1238        // Two bodies both assigned to the same cell
1239        let assignments = vec![(0u64, 0usize), (0u64, 1usize)];
1240        let pairs = pairs_from_grid_assignments(&assignments);
1241        assert!(pairs.contains(&(0, 1)));
1242    }
1243
1244    #[test]
1245    fn test_pairs_from_grid_assignments_no_dup() {
1246        // Same pair in multiple cells
1247        let assignments = vec![
1248            (0u64, 0usize),
1249            (0u64, 1usize),
1250            (1u64, 0usize),
1251            (1u64, 1usize),
1252        ];
1253        let pairs = pairs_from_grid_assignments(&assignments);
1254        // Pair (0,1) should appear exactly once
1255        let count = pairs.iter().filter(|&&p| p == (0, 1)).count();
1256        assert_eq!(count, 1);
1257    }
1258
1259    #[test]
1260    fn test_morton_sort_orders_aabbs() {
1261        let aabbs: Vec<AabbGpu> = vec![
1262            AabbGpu::new([4.0, 0.0, 0.0], [5.0, 1.0, 1.0], 0),
1263            AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 1),
1264            AabbGpu::new([2.0, 2.0, 2.0], [3.0, 3.0, 3.0], 2),
1265        ];
1266        let sorted = morton_sort(&aabbs, 1.0, [0.0, 0.0, 0.0]);
1267        // Body 1 (centroid near origin) should come first in Z-order
1268        assert_eq!(
1269            sorted[0].body_id, 1,
1270            "body at origin should be first in Morton order"
1271        );
1272    }
1273
1274    #[test]
1275    fn test_morton_key_reproducible() {
1276        let a = AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0);
1277        let k1 = morton_key_for_aabb(&a, 1.0, [0.0, 0.0, 0.0]);
1278        let k2 = morton_key_for_aabb(&a, 1.0, [0.0, 0.0, 0.0]);
1279        assert_eq!(k1, k2);
1280    }
1281
1282    #[test]
1283    fn test_compact_pair_list_insert_dedup() {
1284        let mut list = CompactPairList::new();
1285        list.insert(0, 1);
1286        list.insert(1, 0); // same pair, reversed
1287        list.insert(0, 1); // duplicate
1288        assert_eq!(list.len(), 1);
1289    }
1290
1291    #[test]
1292    fn test_compact_pair_list_contains() {
1293        let mut list = CompactPairList::new();
1294        list.insert(2, 5);
1295        assert!(list.contains(2, 5));
1296        assert!(list.contains(5, 2));
1297        assert!(!list.contains(0, 1));
1298    }
1299
1300    #[test]
1301    fn test_compact_pair_list_remove_body() {
1302        let mut list = CompactPairList::new();
1303        list.insert(0, 1);
1304        list.insert(0, 2);
1305        list.insert(1, 2);
1306        list.remove_body(0);
1307        assert!(!list.contains(0, 1));
1308        assert!(!list.contains(0, 2));
1309        assert!(list.contains(1, 2));
1310    }
1311
1312    #[test]
1313    fn test_compact_pair_list_insert_all() {
1314        let mut list = CompactPairList::new();
1315        list.insert_all(&[(0, 1), (1, 2), (0, 2)]);
1316        assert_eq!(list.len(), 3);
1317    }
1318
1319    #[test]
1320    fn test_compact_pair_list_sort() {
1321        let mut list = CompactPairList::new();
1322        list.insert(3, 4);
1323        list.insert(0, 1);
1324        list.insert(1, 2);
1325        list.sort();
1326        assert_eq!(list.pairs()[0], (0, 1));
1327    }
1328
1329    // ── LBVH traversal tests ─────────────────────────────────────────────────
1330
1331    #[test]
1332    fn test_lbvh_query_pairs_finds_overlap() {
1333        // Build BVH for two overlapping boxes
1334        let a = AabbGpu::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0], 0);
1335        let b = AabbGpu::new([1.0, 1.0, 1.0], [3.0, 3.0, 3.0], 1);
1336        let nodes = build_bvh(&[a, b]);
1337        let pairs = lbvh_query_pairs(&nodes);
1338        assert!(
1339            pairs.contains(&(0, 1)),
1340            "LBVH traversal should find (0,1): {pairs:?}"
1341        );
1342    }
1343
1344    #[test]
1345    fn test_lbvh_query_pairs_no_overlap() {
1346        let a = AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0);
1347        let b = AabbGpu::new([5.0, 5.0, 5.0], [6.0, 6.0, 6.0], 1);
1348        let nodes = build_bvh(&[a, b]);
1349        let pairs = lbvh_query_pairs(&nodes);
1350        assert!(
1351            pairs.is_empty(),
1352            "Non-overlapping: should have no pairs: {pairs:?}"
1353        );
1354    }
1355
1356    #[test]
1357    fn test_lbvh_query_empty_bvh() {
1358        let pairs = lbvh_query_pairs(&[]);
1359        assert!(pairs.is_empty());
1360    }
1361
1362    // ── BVH refitting tests ──────────────────────────────────────────────────
1363
1364    #[test]
1365    fn test_refit_bvh_no_panic() {
1366        let aabbs: Vec<AabbGpu> = (0..4)
1367            .map(|i| {
1368                let x = (i * 2) as f32;
1369                AabbGpu::new([x, 0.0, 0.0], [x + 1.0, 1.0, 1.0], i)
1370            })
1371            .collect();
1372        let mut nodes = build_bvh(&aabbs);
1373        // Simulate moving a leaf
1374        for node in nodes.iter_mut() {
1375            if node.is_leaf() {
1376                node.aabb.max[0] += 0.5;
1377            }
1378        }
1379        refit_bvh(&mut nodes);
1380        // After refitting, root should still cover all children
1381        assert!(!nodes.is_empty());
1382    }
1383
1384    #[test]
1385    fn test_refit_bvh_root_encompasses_leaves() {
1386        let aabbs: Vec<AabbGpu> = vec![
1387            AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0),
1388            AabbGpu::new([10.0, 0.0, 0.0], [11.0, 1.0, 1.0], 1),
1389        ];
1390        let mut nodes = build_bvh(&aabbs);
1391        refit_bvh(&mut nodes);
1392        // Root AABB should span at least [0..11]
1393        assert!(nodes[0].aabb.min[0] <= 0.0 + 1e-5);
1394        assert!(nodes[0].aabb.max[0] >= 11.0 - 1e-5);
1395    }
1396
1397    // ── BVH quality metrics tests ────────────────────────────────────────────
1398
1399    #[test]
1400    fn test_aabb_surface_area() {
1401        let a = AabbGpu::new([0.0, 0.0, 0.0], [1.0, 2.0, 3.0], 0);
1402        let sa = aabb_surface_area(&a);
1403        // SA = 2*(1*2 + 2*3 + 3*1) = 2*(2+6+3) = 22
1404        assert!((sa - 22.0).abs() < 1e-5, "SA = {sa}");
1405    }
1406
1407    #[test]
1408    fn test_aabb_surface_area_unit_cube() {
1409        let a = AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0);
1410        let sa = aabb_surface_area(&a);
1411        assert!((sa - 6.0).abs() < 1e-5, "unit cube SA = {sa}");
1412    }
1413
1414    #[test]
1415    fn test_bvh_sah_cost_positive() {
1416        let aabbs: Vec<AabbGpu> = (0..4)
1417            .map(|i| {
1418                let x = (i * 3) as f32;
1419                AabbGpu::new([x, 0.0, 0.0], [x + 2.0, 2.0, 2.0], i)
1420            })
1421            .collect();
1422        let nodes = build_bvh(&aabbs);
1423        let cost = bvh_sah_cost(&nodes, 1.0, 1.0);
1424        assert!(cost > 0.0, "SAH cost should be positive: {cost}");
1425        assert!(cost.is_finite(), "SAH cost should be finite");
1426    }
1427
1428    #[test]
1429    fn test_bvh_depth_single_leaf() {
1430        let aabbs = vec![AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0)];
1431        let nodes = build_bvh(&aabbs);
1432        let d = bvh_depth(&nodes);
1433        assert_eq!(d, 0, "single leaf depth = {d}");
1434    }
1435
1436    #[test]
1437    fn test_bvh_depth_two_leaves() {
1438        let aabbs = vec![
1439            AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0),
1440            AabbGpu::new([2.0, 0.0, 0.0], [3.0, 1.0, 1.0], 1),
1441        ];
1442        let nodes = build_bvh(&aabbs);
1443        let d = bvh_depth(&nodes);
1444        assert!(d >= 1, "two-leaf BVH depth >= 1, got {d}");
1445    }
1446
1447    #[test]
1448    fn test_bvh_leaf_count() {
1449        let aabbs: Vec<AabbGpu> = (0..8)
1450            .map(|i| {
1451                let x = (i * 2) as f32;
1452                AabbGpu::new([x, 0.0, 0.0], [x + 1.0, 1.0, 1.0], i)
1453            })
1454            .collect();
1455        let nodes = build_bvh(&aabbs);
1456        let leaves = bvh_leaf_count(&nodes);
1457        assert_eq!(leaves, 8, "Expected 8 leaves, got {leaves}");
1458    }
1459
1460    // ── SAP incremental update tests ─────────────────────────────────────────
1461
1462    #[test]
1463    fn test_sap_incremental_removes_moved() {
1464        let a = AabbGpu::new([0.0, 0.0, 0.0], [2.0, 2.0, 2.0], 0);
1465        let b = AabbGpu::new([1.0, 1.0, 1.0], [3.0, 3.0, 3.0], 1);
1466        let c = AabbGpu::new([10.0, 10.0, 10.0], [11.0, 11.0, 11.0], 2);
1467        let initial_pairs = SortAndSweepGpu::detect_pairs(&[a, b, c]);
1468        let mut existing = CompactPairList::new();
1469        existing.insert_all(&initial_pairs);
1470
1471        // Move body 0 far away
1472        let a_moved = AabbGpu::new([20.0, 20.0, 20.0], [21.0, 21.0, 21.0], 0);
1473        let updated = sap_incremental_update(&existing, &[a_moved, b, c], &[0]);
1474
1475        // After move, body 0 overlaps nothing
1476        assert!(
1477            !updated.contains(0, 1),
1478            "pair (0,1) should be removed after move"
1479        );
1480    }
1481
1482    #[test]
1483    fn test_sap_incremental_adds_new_overlap() {
1484        let _a = AabbGpu::new([10.0, 0.0, 0.0], [11.0, 1.0, 1.0], 0);
1485        let b = AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 1);
1486        let existing = CompactPairList::new(); // no initial pairs
1487
1488        // Move body 0 to overlap body 1
1489        let a_moved = AabbGpu::new([0.5, 0.5, 0.5], [1.5, 1.5, 1.5], 0);
1490        let updated = sap_incremental_update(&existing, &[a_moved, b], &[0]);
1491        assert!(
1492            updated.contains(0, 1),
1493            "pair (0,1) should be added after move: {:?}",
1494            updated.pairs()
1495        );
1496    }
1497
1498    // ── SAH split tests ──────────────────────────────────────────────────────
1499
1500    #[test]
1501    fn test_sah_best_split_basic() {
1502        let aabbs: Vec<AabbGpu> = (0..8)
1503            .map(|i| {
1504                let x = (i * 2) as f32;
1505                AabbGpu::new([x, 0.0, 0.0], [x + 1.5, 1.0, 1.0], i)
1506            })
1507            .collect();
1508        let indices: Vec<usize> = (0..8).collect();
1509        let result = sah_best_split(&aabbs, &indices, 0, 8);
1510        assert!(result.is_some(), "SAH split should find a valid split");
1511    }
1512
1513    #[test]
1514    fn test_sah_best_split_single_element() {
1515        let aabbs = vec![AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0)];
1516        let result = sah_best_split(&aabbs, &[0], 0, 4);
1517        assert!(result.is_none(), "Single element: no split possible");
1518    }
1519
1520    // ── LBVH build tests ─────────────────────────────────────────────────────
1521
1522    #[test]
1523    fn test_build_lbvh_nonempty() {
1524        let aabbs: Vec<AabbGpu> = (0..6)
1525            .map(|i| {
1526                let x = (i * 2) as f32;
1527                AabbGpu::new([x, 0.0, 0.0], [x + 1.0, 1.0, 1.0], i)
1528            })
1529            .collect();
1530        let nodes = build_lbvh(&aabbs, 1.0, [0.0, 0.0, 0.0]);
1531        assert!(!nodes.is_empty(), "LBVH should build non-empty node list");
1532        assert!(nodes.len() <= 2 * aabbs.len());
1533    }
1534
1535    #[test]
1536    fn test_build_lbvh_empty() {
1537        let nodes = build_lbvh(&[], 1.0, [0.0, 0.0, 0.0]);
1538        assert!(nodes.is_empty());
1539    }
1540
1541    #[test]
1542    fn test_build_lbvh_single() {
1543        let aabbs = vec![AabbGpu::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0)];
1544        let nodes = build_lbvh(&aabbs, 1.0, [0.0, 0.0, 0.0]);
1545        assert_eq!(nodes.len(), 1);
1546        assert!(nodes[0].is_leaf());
1547    }
1548
1549    #[test]
1550    fn test_lbvh_vs_brute_force_pairs() {
1551        // LBVH traversal should find same pairs as brute-force overlap check
1552        let aabbs: Vec<AabbGpu> = (0..6)
1553            .map(|i| {
1554                let x = (i as f32) * 0.8;
1555                AabbGpu::new([x, 0.0, 0.0], [x + 1.0, 1.0, 1.0], i as u32)
1556            })
1557            .collect();
1558
1559        let lbvh_nodes = build_lbvh(&aabbs, 0.5, [0.0, 0.0, 0.0]);
1560        let mut lbvh_pairs = lbvh_query_pairs(&lbvh_nodes);
1561        lbvh_pairs.sort();
1562        lbvh_pairs.dedup();
1563
1564        let mut brute: Vec<(u32, u32)> = Vec::new();
1565        for i in 0..aabbs.len() {
1566            for j in (i + 1)..aabbs.len() {
1567                if aabbs[i].overlaps(&aabbs[j]) {
1568                    brute.push((aabbs[i].body_id, aabbs[j].body_id));
1569                }
1570            }
1571        }
1572        brute.sort();
1573
1574        assert_eq!(
1575            lbvh_pairs, brute,
1576            "LBVH pairs {lbvh_pairs:?} != brute force {brute:?}"
1577        );
1578    }
1579}