Skip to main content

oxiphysics_gpu/collision_gpu/
types.rs

1use super::functions::*;
2// Auto-generated module
3//
4// 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
5
6/// Broadphase collision detection kernel (CPU mock of a GPU dispatch).
7///
8/// Detects all overlapping AABB pairs from a flat list of `AabbGpu`.
9#[derive(Debug, Default)]
10pub struct BroadphaseGpuKernel {
11    /// Inflation margin added to each AABB before testing.
12    pub margin: f32,
13}
14impl BroadphaseGpuKernel {
15    /// Create a new `BroadphaseGpuKernel` with the given inflation margin.
16    pub fn new(margin: f32) -> Self {
17        Self { margin }
18    }
19    /// Dispatch the broadphase over `aabbs`.
20    ///
21    /// Returns all overlapping pairs (canonical form, `a < b`).
22    pub fn dispatch(&self, aabbs: &[AabbGpu]) -> Vec<CollisionPair> {
23        let mut pairs = Vec::new();
24        for (i, aabb_i) in aabbs.iter().enumerate() {
25            let expanded_i = aabb_i.expanded(self.margin);
26            for (j, aabb_j) in aabbs.iter().enumerate().skip(i + 1) {
27                if expanded_i.overlaps(aabb_j) {
28                    pairs.push(CollisionPair::new(i as u32, j as u32));
29                }
30            }
31        }
32        pairs
33    }
34    /// BVH-accelerated broadphase dispatch.
35    pub fn dispatch_bvh(&self, aabbs: &[AabbGpu]) -> Vec<CollisionPair> {
36        let mut builder = GpuBvhBuilder::new();
37        let root = builder.build(aabbs);
38        let mut pairs = Vec::new();
39        for (i, aabb) in aabbs.iter().enumerate() {
40            let query = aabb.expanded(self.margin);
41            let hits = builder.query_overlaps(root, &query);
42            for j in hits {
43                if j as usize > i {
44                    pairs.push(CollisionPair::new(i as u32, j));
45                }
46            }
47        }
48        pairs
49    }
50}
51/// Axis-aligned bounding box for GPU collision passes.
52///
53/// Corners are stored as `[f32; 3]` to match typical GPU buffer layouts.
54#[derive(Debug, Clone, PartialEq)]
55pub struct AabbGpu {
56    /// Component-wise minimum corner.
57    pub min: [f32; 3],
58    /// Component-wise maximum corner.
59    pub max: [f32; 3],
60}
61impl AabbGpu {
62    /// Construct an `AabbGpu` from explicit min/max corners.
63    pub fn new(min: [f32; 3], max: [f32; 3]) -> Self {
64        Self { min, max }
65    }
66    /// Construct a degenerate `AabbGpu` that contains exactly one point.
67    pub fn from_point(p: [f32; 3]) -> Self {
68        Self { min: p, max: p }
69    }
70    /// Return the smallest `AabbGpu` that contains both `self` and `other`.
71    pub fn merge(&self, other: &AabbGpu) -> AabbGpu {
72        AabbGpu {
73            min: [
74                self.min[0].min(other.min[0]),
75                self.min[1].min(other.min[1]),
76                self.min[2].min(other.min[2]),
77            ],
78            max: [
79                self.max[0].max(other.max[0]),
80                self.max[1].max(other.max[1]),
81                self.max[2].max(other.max[2]),
82            ],
83        }
84    }
85    /// Returns `true` if this box overlaps `other` (touching counts as overlap).
86    pub fn overlaps(&self, other: &AabbGpu) -> bool {
87        self.min[0] <= other.max[0]
88            && self.max[0] >= other.min[0]
89            && self.min[1] <= other.max[1]
90            && self.max[1] >= other.min[1]
91            && self.min[2] <= other.max[2]
92            && self.max[2] >= other.min[2]
93    }
94    /// Returns `true` if the point `p` lies inside or on the surface.
95    pub fn contains_point(&self, p: [f32; 3]) -> bool {
96        p[0] >= self.min[0]
97            && p[0] <= self.max[0]
98            && p[1] >= self.min[1]
99            && p[1] <= self.max[1]
100            && p[2] >= self.min[2]
101            && p[2] <= self.max[2]
102    }
103    /// Surface area of the AABB (sum of the six face areas).
104    pub fn surface_area(&self) -> f32 {
105        let dx = (self.max[0] - self.min[0]).max(0.0);
106        let dy = (self.max[1] - self.min[1]).max(0.0);
107        let dz = (self.max[2] - self.min[2]).max(0.0);
108        2.0 * (dx * dy + dy * dz + dz * dx)
109    }
110    /// Volume of the AABB.
111    pub fn volume(&self) -> f32 {
112        let dx = (self.max[0] - self.min[0]).max(0.0);
113        let dy = (self.max[1] - self.min[1]).max(0.0);
114        let dz = (self.max[2] - self.min[2]).max(0.0);
115        dx * dy * dz
116    }
117    /// Centre of the AABB.
118    pub fn centre(&self) -> [f32; 3] {
119        [
120            (self.min[0] + self.max[0]) * 0.5,
121            (self.min[1] + self.max[1]) * 0.5,
122            (self.min[2] + self.max[2]) * 0.5,
123        ]
124    }
125    /// Half-extents of the AABB.
126    pub fn half_extents(&self) -> [f32; 3] {
127        [
128            (self.max[0] - self.min[0]) * 0.5,
129            (self.max[1] - self.min[1]) * 0.5,
130            (self.max[2] - self.min[2]) * 0.5,
131        ]
132    }
133    /// Expand the AABB by `margin` in every direction.
134    pub fn expanded(&self, margin: f32) -> AabbGpu {
135        AabbGpu {
136            min: [
137                self.min[0] - margin,
138                self.min[1] - margin,
139                self.min[2] - margin,
140            ],
141            max: [
142                self.max[0] + margin,
143                self.max[1] + margin,
144                self.max[2] + margin,
145            ],
146        }
147    }
148}
149/// Persistent 4-point contact manifold with warm-start impulse data.
150///
151/// Retains up to 4 contact points between a pair of bodies across frames,
152/// selecting the deepest/most-separated subset for stability.
153#[derive(Debug, Clone)]
154pub struct PersistentManifoldGpu {
155    /// Index of body A.
156    pub body_a: u32,
157    /// Index of body B.
158    pub body_b: u32,
159    /// Retained contact points (up to 4).
160    pub points: [Option<ManifoldPoint>; 4],
161    /// Number of active contact points.
162    pub num_points: usize,
163}
164impl PersistentManifoldGpu {
165    /// Create an empty manifold for the given body pair.
166    pub fn new(body_a: u32, body_b: u32) -> Self {
167        Self {
168            body_a,
169            body_b,
170            points: [None, None, None, None],
171            num_points: 0,
172        }
173    }
174    /// Add or replace a contact point.
175    ///
176    /// If there are fewer than 4 points the new point is simply appended.
177    /// Otherwise the shallowest existing point is replaced if it is shallower
178    /// than the new one.
179    pub fn add_contact(&mut self, new_pt: ManifoldPoint) {
180        if self.num_points < 4 {
181            self.points[self.num_points] = Some(new_pt);
182            self.num_points += 1;
183            return;
184        }
185        let mut shallowest_idx = 0;
186        let mut shallowest_depth = f32::INFINITY;
187        for (i, opt) in self.points.iter().enumerate() {
188            if let Some(p) = opt
189                && p.depth < shallowest_depth
190            {
191                shallowest_depth = p.depth;
192                shallowest_idx = i;
193            }
194        }
195        if new_pt.depth > shallowest_depth {
196            self.points[shallowest_idx] = Some(new_pt);
197        }
198    }
199    /// Remove stale contact points whose depth has become positive (separated).
200    pub fn remove_stale(&mut self, threshold: f32) {
201        for slot in self.points.iter_mut() {
202            if let Some(p) = slot
203                && p.depth < threshold
204            {
205                *slot = None;
206            }
207        }
208        self.num_points = self.points.iter().filter(|s| s.is_some()).count();
209    }
210    /// Reset warm-start impulses to zero.
211    pub fn reset_warm_start(&mut self) {
212        for slot in self.points.iter_mut().flatten() {
213            slot.warm_impulse_normal = 0.0;
214            slot.warm_impulse_t1 = 0.0;
215            slot.warm_impulse_t2 = 0.0;
216        }
217    }
218    /// Return an iterator over active contact points.
219    pub fn active_points(&self) -> impl Iterator<Item = &ManifoldPoint> {
220        self.points.iter().flatten()
221    }
222}
223/// Builds a flat BVH from a list of AABBs using Morton-code sorting.
224///
225/// The builder sorts primitives by their Morton code (computed from the centroid
226/// of each AABB), then constructs the BVH bottom-up.
227#[derive(Debug, Default)]
228pub struct GpuBvhBuilder {
229    pub(super) nodes: Vec<BvhNodeGpu>,
230}
231impl GpuBvhBuilder {
232    /// Create a new, empty `GpuBvhBuilder`.
233    pub fn new() -> Self {
234        Self { nodes: Vec::new() }
235    }
236    /// Build a BVH from a slice of `AabbGpu` primitives.
237    ///
238    /// Returns the index of the root node in the returned flat node array.
239    pub fn build(&mut self, aabbs: &[AabbGpu]) -> usize {
240        self.nodes.clear();
241        if aabbs.is_empty() {
242            return 0;
243        }
244        let scene_min = [
245            aabbs.iter().map(|a| a.min[0]).fold(f32::INFINITY, f32::min),
246            aabbs.iter().map(|a| a.min[1]).fold(f32::INFINITY, f32::min),
247            aabbs.iter().map(|a| a.min[2]).fold(f32::INFINITY, f32::min),
248        ];
249        let scene_max = [
250            aabbs
251                .iter()
252                .map(|a| a.max[0])
253                .fold(f32::NEG_INFINITY, f32::max),
254            aabbs
255                .iter()
256                .map(|a| a.max[1])
257                .fold(f32::NEG_INFINITY, f32::max),
258            aabbs
259                .iter()
260                .map(|a| a.max[2])
261                .fold(f32::NEG_INFINITY, f32::max),
262        ];
263        let extent = [
264            (scene_max[0] - scene_min[0]).max(1e-6),
265            (scene_max[1] - scene_min[1]).max(1e-6),
266            (scene_max[2] - scene_min[2]).max(1e-6),
267        ];
268        let mut indexed: Vec<(u32, usize)> = aabbs
269            .iter()
270            .enumerate()
271            .map(|(i, a)| {
272                let c = a.centre();
273                let nx = (c[0] - scene_min[0]) / extent[0];
274                let ny = (c[1] - scene_min[1]) / extent[1];
275                let nz = (c[2] - scene_min[2]) / extent[2];
276                (morton_code(nx, ny, nz), i)
277            })
278            .collect();
279        indexed.sort_by_key(|&(code, _)| code);
280        let leaf_base = 0usize;
281        for &(_, prim_idx) in &indexed {
282            self.nodes
283                .push(BvhNodeGpu::leaf(aabbs[prim_idx].clone(), prim_idx as u32));
284        }
285        let mut current_level: Vec<usize> = (leaf_base..leaf_base + indexed.len()).collect();
286        while current_level.len() > 1 {
287            let mut next_level = Vec::new();
288            let mut i = 0;
289            while i < current_level.len() {
290                if i + 1 < current_level.len() {
291                    let l = current_level[i];
292                    let r = current_level[i + 1];
293                    let merged = self.nodes[l].aabb.merge(&self.nodes[r].aabb);
294                    let internal = BvhNodeGpu::internal(merged, l as u32, r as u32);
295                    let idx = self.nodes.len();
296                    self.nodes.push(internal);
297                    next_level.push(idx);
298                    i += 2;
299                } else {
300                    next_level.push(current_level[i]);
301                    i += 1;
302                }
303            }
304            current_level = next_level;
305        }
306        current_level[0]
307    }
308    /// Return a reference to the built flat node array.
309    pub fn nodes(&self) -> &[BvhNodeGpu] {
310        &self.nodes
311    }
312    /// Query all leaf primitives whose AABB overlaps `query`.
313    pub fn query_overlaps(&self, root: usize, query: &AabbGpu) -> Vec<u32> {
314        if self.nodes.is_empty() {
315            return Vec::new();
316        }
317        let mut result = Vec::new();
318        let mut stack = vec![root];
319        while let Some(idx) = stack.pop() {
320            let node = &self.nodes[idx];
321            if !node.aabb.overlaps(query) {
322                continue;
323            }
324            if node.is_leaf {
325                result.push(node.primitive_index);
326            } else {
327                stack.push(node.left_child as usize);
328                stack.push(node.right_child as usize);
329            }
330        }
331        result
332    }
333}
334/// A single contact point in the manifold.
335#[derive(Debug, Clone)]
336pub struct ManifoldPoint {
337    /// Contact point in world space.
338    pub position: [f32; 3],
339    /// Contact normal (pointing from B to A).
340    pub normal: [f32; 3],
341    /// Penetration depth.
342    pub depth: f32,
343    /// Accumulated impulse for warm starting (normal direction).
344    pub warm_impulse_normal: f32,
345    /// Accumulated impulse for warm starting (tangent 1).
346    pub warm_impulse_t1: f32,
347    /// Accumulated impulse for warm starting (tangent 2).
348    pub warm_impulse_t2: f32,
349}
350impl ManifoldPoint {
351    /// Create a new manifold point from a contact result.
352    pub fn from_contact(c: &ContactResult) -> Self {
353        Self {
354            position: c.contact_point,
355            normal: c.normal,
356            depth: c.depth,
357            warm_impulse_normal: 0.0,
358            warm_impulse_t1: 0.0,
359            warm_impulse_t2: 0.0,
360        }
361    }
362}
363/// Narrowphase collision detection kernel (CPU mock with simplified GJK/SAT).
364///
365/// For AABB vs AABB pairs the kernel uses SAT (Separating Axis Theorem) which
366/// is exact.  For more general shapes a GJK iteration would be used.
367#[derive(Debug, Default)]
368pub struct NarrowphaseGpuKernel;
369impl NarrowphaseGpuKernel {
370    /// Create a new `NarrowphaseGpuKernel`.
371    pub fn new() -> Self {
372        Self
373    }
374    /// Test a single AABB pair using SAT; returns `Some(ContactResult)` on
375    /// contact, `None` otherwise.
376    pub fn test_aabb_pair(
377        &self,
378        prim_a: u32,
379        aabb_a: &AabbGpu,
380        prim_b: u32,
381        aabb_b: &AabbGpu,
382    ) -> Option<ContactResult> {
383        let axes: [[f32; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
384        let mut min_depth = f32::INFINITY;
385        let mut min_axis = [1.0f32, 0.0, 0.0];
386        for axis in &axes {
387            let a_min = dot3f(aabb_a.min, *axis);
388            let a_max = dot3f(aabb_a.max, *axis);
389            let b_min = dot3f(aabb_b.min, *axis);
390            let b_max = dot3f(aabb_b.max, *axis);
391            let overlap = a_max.min(b_max) - a_min.max(b_min);
392            if overlap <= 0.0 {
393                return None;
394            }
395            if overlap < min_depth {
396                min_depth = overlap;
397                let ca = dot3f(aabb_a.centre(), *axis);
398                let cb = dot3f(aabb_b.centre(), *axis);
399                let sign = if ca > cb { 1.0 } else { -1.0 };
400                min_axis = scale3f(*axis, sign);
401            }
402        }
403        let ca = aabb_a.centre();
404        let cb = aabb_b.centre();
405        let contact_point = [
406            (ca[0] + cb[0]) * 0.5,
407            (ca[1] + cb[1]) * 0.5,
408            (ca[2] + cb[2]) * 0.5,
409        ];
410        Some(ContactResult {
411            prim_a,
412            prim_b,
413            contact_point,
414            normal: min_axis,
415            depth: min_depth,
416        })
417    }
418    /// Run GJK for two AABB shapes (CPU mock).
419    ///
420    /// Returns `true` if the shapes intersect, along with the final simplex
421    /// distance (0.0 on intersection).
422    pub fn gjk_intersect(&self, a: &AabbGpu, b: &AabbGpu) -> (bool, f32) {
423        let ca = a.centre();
424        let cb = b.centre();
425        let mut d = sub3f(ca, cb);
426        if len_sq3f(d) < 1e-9 {
427            d = [1.0, 0.0, 0.0];
428        }
429        let s0 = minkowski_support(a, b, d);
430        let mut simplex = vec![s0];
431        d = [-s0.v[0], -s0.v[1], -s0.v[2]];
432        for _ in 0..32 {
433            if len_sq3f(d) < 1e-12 {
434                return (true, 0.0);
435            }
436            let a_sup = minkowski_support(a, b, d);
437            if dot3f(a_sup.v, d) < 0.0 {
438                return (false, len3f(d));
439            }
440            simplex.push(a_sup);
441            let (intersecting, new_d) = update_simplex(&mut simplex);
442            if intersecting {
443                return (true, 0.0);
444            }
445            d = new_d;
446        }
447        (true, 0.0)
448    }
449    /// Dispatch narrowphase over a list of broadphase pairs and AABBs.
450    pub fn dispatch(&self, pairs: &[CollisionPair], aabbs: &[AabbGpu]) -> Vec<ContactResult> {
451        let mut contacts = Vec::new();
452        for pair in pairs {
453            let a = pair.a as usize;
454            let b = pair.b as usize;
455            if a < aabbs.len()
456                && b < aabbs.len()
457                && let Some(c) = self.test_aabb_pair(pair.a, &aabbs[a], pair.b, &aabbs[b])
458            {
459                contacts.push(c);
460            }
461        }
462        contacts
463    }
464}
465/// A single BVH node in GPU-friendly flat representation.
466///
467/// Leaf nodes have `is_leaf == true` and `primitive_index` set.
468/// Internal nodes use `left_child` / `right_child` as indices into the node
469/// array.
470#[derive(Debug, Clone)]
471pub struct BvhNodeGpu {
472    /// Bounding box for this node.
473    pub aabb: AabbGpu,
474    /// Index of the left child node (internal node only).
475    pub left_child: u32,
476    /// Index of the right child node (internal node only).
477    pub right_child: u32,
478    /// Whether this node is a leaf.
479    pub is_leaf: bool,
480    /// Index of the primitive stored in this leaf.
481    pub primitive_index: u32,
482}
483impl BvhNodeGpu {
484    /// Create a new internal BVH node.
485    pub fn internal(aabb: AabbGpu, left_child: u32, right_child: u32) -> Self {
486        Self {
487            aabb,
488            left_child,
489            right_child,
490            is_leaf: false,
491            primitive_index: u32::MAX,
492        }
493    }
494    /// Create a new leaf BVH node.
495    pub fn leaf(aabb: AabbGpu, primitive_index: u32) -> Self {
496        Self {
497            aabb,
498            left_child: u32::MAX,
499            right_child: u32::MAX,
500            is_leaf: true,
501            primitive_index,
502        }
503    }
504}
505/// Combined broadphase + narrowphase collision pipeline.
506///
507/// Holds the configuration for both stages and maintains a list of persistent
508/// manifolds from the previous frame for warm starting.
509#[derive(Debug)]
510pub struct CollisionGpuPipeline {
511    /// Broadphase kernel configuration.
512    pub broadphase: BroadphaseGpuKernel,
513    /// Narrowphase kernel.
514    pub narrowphase: NarrowphaseGpuKernel,
515    /// Persistent manifolds from the previous timestep.
516    pub manifolds: Vec<PersistentManifoldGpu>,
517    /// Contact list from the most recent dispatch.
518    pub contacts: Vec<ContactResult>,
519}
520impl CollisionGpuPipeline {
521    /// Create a new pipeline with default broadphase margin.
522    pub fn new(margin: f32) -> Self {
523        Self {
524            broadphase: BroadphaseGpuKernel::new(margin),
525            narrowphase: NarrowphaseGpuKernel::new(),
526            manifolds: Vec::new(),
527            contacts: Vec::new(),
528        }
529    }
530    /// Run one full collision detection pass over `aabbs`.
531    ///
532    /// 1. Broadphase: find overlapping AABB pairs.
533    /// 2. Narrowphase: compute contact points for each pair.
534    /// 3. Update persistent manifolds.
535    pub fn dispatch(&mut self, aabbs: &[AabbGpu]) {
536        let pairs = self.broadphase.dispatch(aabbs);
537        self.contacts = self.narrowphase.dispatch(&pairs, aabbs);
538        self.update_manifolds();
539    }
540    /// Run the full pipeline using BVH-accelerated broadphase.
541    pub fn dispatch_bvh(&mut self, aabbs: &[AabbGpu]) {
542        let pairs = self.broadphase.dispatch_bvh(aabbs);
543        self.contacts = self.narrowphase.dispatch(&pairs, aabbs);
544        self.update_manifolds();
545    }
546    /// Update the persistent manifold list from the current contact set.
547    fn update_manifolds(&mut self) {
548        self.manifolds.retain(|m| m.num_points > 0);
549        for contact in &self.contacts {
550            let existing = self.manifolds.iter_mut().find(|m| {
551                (m.body_a == contact.prim_a && m.body_b == contact.prim_b)
552                    || (m.body_a == contact.prim_b && m.body_b == contact.prim_a)
553            });
554            let pt = ManifoldPoint::from_contact(contact);
555            if let Some(manifold) = existing {
556                manifold.add_contact(pt);
557            } else {
558                let mut new_m = PersistentManifoldGpu::new(contact.prim_a, contact.prim_b);
559                new_m.add_contact(pt);
560                self.manifolds.push(new_m);
561            }
562        }
563    }
564    /// Clear all manifolds and contact data.
565    pub fn reset(&mut self) {
566        self.manifolds.clear();
567        self.contacts.clear();
568    }
569    /// Return the total number of contact points across all manifolds.
570    pub fn total_contact_points(&self) -> usize {
571        self.manifolds.iter().map(|m| m.num_points).sum()
572    }
573}
574/// Result of a GJK distance query.
575#[derive(Debug, Clone)]
576pub struct GjkResult {
577    /// Whether the shapes are intersecting (distance = 0).
578    pub intersecting: bool,
579    /// Minimum distance between the shapes (0 if intersecting).
580    pub distance: f32,
581    /// Closest point on shape A.
582    pub closest_a: [f32; 3],
583    /// Closest point on shape B.
584    pub closest_b: [f32; 3],
585    /// Separation axis (unit vector from B to A).
586    pub axis: [f32; 3],
587}
588/// Full GPU collision pipeline: broadphase (SAP) + narrowphase (GJK) +
589/// persistent contact cache with warmstarting.
590///
591/// Intended as the primary collision API. Configure once, call `step` each
592/// physics tick to get a fresh contact list.
593#[derive(Debug)]
594pub struct GpuCollisionPipeline {
595    /// SAP broadphase.
596    pub broadphase: GpuBroadphase,
597    /// GJK narrowphase.
598    pub narrowphase: GpuNarrowphase,
599    /// Persistent contact cache.
600    pub contact_cache: GpuContactCache,
601    /// AABB tree (rebuilt each frame or on-demand).
602    pub aabb_tree: GpuAabbTree,
603    /// Contact list from the most recent step.
604    pub contacts: Vec<ContactResult>,
605    /// Persistent manifolds.
606    pub manifolds: Vec<PersistentManifoldGpu>,
607    /// Cumulative stats since pipeline creation.
608    pub cumulative_stats: CollisionKernelStats,
609}
610impl GpuCollisionPipeline {
611    /// Create a new `GpuCollisionPipeline` with sensible defaults.
612    pub fn new(margin: f32, max_cache_age: u32) -> Self {
613        Self {
614            broadphase: GpuBroadphase::new(0, margin),
615            narrowphase: GpuNarrowphase::new(32),
616            contact_cache: GpuContactCache::new(max_cache_age),
617            aabb_tree: GpuAabbTree::new(),
618            contacts: Vec::new(),
619            manifolds: Vec::new(),
620            cumulative_stats: CollisionKernelStats::new(),
621        }
622    }
623    /// Run one full collision step using SAP broadphase + GJK narrowphase.
624    ///
625    /// Steps:
626    /// 1. Rebuild the AABB tree.
627    /// 2. Run SAP broadphase.
628    /// 3. Run GJK narrowphase on each pair.
629    /// 4. Refresh contact cache; apply warmstart data.
630    /// 5. Update persistent manifolds.
631    /// 6. Age out stale cache entries.
632    pub fn step(&mut self, aabbs: &[AabbGpu]) {
633        self.aabb_tree.build(aabbs);
634        self.broadphase.sweep_axis = GpuBroadphase::choose_best_axis(aabbs);
635        let pairs = self.broadphase.dispatch(aabbs);
636        self.contacts = self.narrowphase.dispatch(&pairs, aabbs);
637        for (pair, contact) in pairs.iter().zip(self.contacts.iter()) {
638            self.contact_cache.insert(pair, contact);
639        }
640        self.update_manifolds();
641        self.contact_cache.advance_frame();
642        let mut step_stats = self.broadphase.stats.clone();
643        step_stats.accumulate(&self.narrowphase.stats);
644        self.cumulative_stats.accumulate(&step_stats);
645    }
646    fn update_manifolds(&mut self) {
647        self.manifolds.retain(|m| m.num_points > 0);
648        for contact in &self.contacts {
649            let existing = self.manifolds.iter_mut().find(|m| {
650                (m.body_a == contact.prim_a && m.body_b == contact.prim_b)
651                    || (m.body_a == contact.prim_b && m.body_b == contact.prim_a)
652            });
653            let mut pt = ManifoldPoint::from_contact(contact);
654            if let Some(entry) = self.contact_cache.find(contact.prim_a, contact.prim_b) {
655                entry.apply_warm_start(&mut pt);
656            }
657            if let Some(manifold) = existing {
658                manifold.add_contact(pt);
659            } else {
660                let mut new_m = PersistentManifoldGpu::new(contact.prim_a, contact.prim_b);
661                new_m.add_contact(pt);
662                self.manifolds.push(new_m);
663            }
664        }
665    }
666    /// Clear all contacts, manifolds, and the contact cache.
667    pub fn reset(&mut self) {
668        self.contacts.clear();
669        self.manifolds.clear();
670        self.contact_cache.clear();
671    }
672    /// Total active contact points across all manifolds.
673    pub fn total_contact_points(&self) -> usize {
674        self.manifolds.iter().map(|m| m.num_points).sum()
675    }
676    /// Get a snapshot of per-frame broadphase + narrowphase stats.
677    pub fn frame_stats(&self) -> CollisionKernelStats {
678        let mut s = self.broadphase.stats.clone();
679        s.accumulate(&self.narrowphase.stats);
680        s
681    }
682}
683/// Broadphase entry used in Sort-and-Sweep.
684#[derive(Debug, Clone)]
685pub(super) struct SapEntry {
686    /// Minimum along the sweep axis.
687    pub(super) lo: f32,
688    /// Maximum along the sweep axis.
689    pub(super) hi: f32,
690    /// Primitive index.
691    pub(super) idx: u32,
692}
693/// A pair of primitive indices that may be in contact.
694#[derive(Debug, Clone, PartialEq, Eq, Hash)]
695pub struct CollisionPair {
696    /// Index of the first primitive.
697    pub a: u32,
698    /// Index of the second primitive.
699    pub b: u32,
700}
701impl CollisionPair {
702    /// Construct a canonical pair with `a <= b`.
703    pub fn new(a: u32, b: u32) -> Self {
704        if a <= b {
705            Self { a, b }
706        } else {
707            Self { a: b, b: a }
708        }
709    }
710}
711/// Contact information produced by the narrowphase kernel.
712#[derive(Debug, Clone)]
713pub struct ContactResult {
714    /// Index of the first primitive.
715    pub prim_a: u32,
716    /// Index of the second primitive.
717    pub prim_b: u32,
718    /// Contact point in world space (midpoint approximation).
719    pub contact_point: [f32; 3],
720    /// Contact normal pointing from B towards A.
721    pub normal: [f32; 3],
722    /// Penetration depth (positive = overlap).
723    pub depth: f32,
724}
725/// Support point difference used internally by GJK.
726#[derive(Debug, Clone, Copy)]
727pub(super) struct SupportPoint {
728    /// The point in the Minkowski difference.
729    pub(super) v: [f32; 3],
730}
731impl SupportPoint {
732    pub(super) fn new(v: [f32; 3]) -> Self {
733        Self { v }
734    }
735}
736/// Statistics for a single collision kernel dispatch.
737#[derive(Debug, Clone, Default)]
738pub struct CollisionKernelStats {
739    /// Total bytes transferred (read + write) during the dispatch.
740    pub bytes_transferred: u64,
741    /// Number of primitive pairs tested in the broadphase.
742    pub broadphase_pairs_tested: u64,
743    /// Number of pairs that passed the broadphase (potential contacts).
744    pub broadphase_hits: u64,
745    /// Number of narrowphase GJK/SAT queries executed.
746    pub narrowphase_queries: u64,
747    /// Number of narrowphase queries that resulted in a contact.
748    pub narrowphase_contacts: u64,
749    /// Elapsed wall-clock time for this dispatch in seconds.
750    pub elapsed_secs: f64,
751}
752impl CollisionKernelStats {
753    /// Create a zero-initialised stats record.
754    pub fn new() -> Self {
755        Self::default()
756    }
757    /// Broadphase hit-rate in `[0, 1]`.
758    ///
759    /// Returns `0.0` when no pairs were tested.
760    pub fn broadphase_hit_rate(&self) -> f64 {
761        if self.broadphase_pairs_tested == 0 {
762            return 0.0_f64;
763        }
764        self.broadphase_hits as f64 / self.broadphase_pairs_tested as f64
765    }
766    /// Narrowphase contact-rate in `[0, 1]`.
767    ///
768    /// Returns `0.0` when no narrowphase queries were executed.
769    pub fn narrowphase_contact_rate(&self) -> f64 {
770        if self.narrowphase_queries == 0 {
771            return 0.0_f64;
772        }
773        self.narrowphase_contacts as f64 / self.narrowphase_queries as f64
774    }
775    /// Estimated memory bandwidth in GB/s.
776    ///
777    /// Returns `0.0` when elapsed time is zero.
778    pub fn bandwidth_gb_s(&self) -> f64 {
779        if self.elapsed_secs <= 0.0_f64 {
780            return 0.0_f64;
781        }
782        self.bytes_transferred as f64 / self.elapsed_secs / 1.0e9_f64
783    }
784    /// Pair throughput in pairs-per-second.
785    ///
786    /// Returns `0.0` when elapsed time is zero.
787    pub fn pair_throughput(&self) -> f64 {
788        if self.elapsed_secs <= 0.0_f64 {
789            return 0.0_f64;
790        }
791        self.broadphase_pairs_tested as f64 / self.elapsed_secs
792    }
793    /// Accumulate another stats record into this one.
794    pub fn accumulate(&mut self, other: &CollisionKernelStats) {
795        self.bytes_transferred += other.bytes_transferred;
796        self.broadphase_pairs_tested += other.broadphase_pairs_tested;
797        self.broadphase_hits += other.broadphase_hits;
798        self.narrowphase_queries += other.narrowphase_queries;
799        self.narrowphase_contacts += other.narrowphase_contacts;
800        self.elapsed_secs += other.elapsed_secs;
801    }
802}
803/// A single entry in the contact cache for warmstarting.
804#[derive(Debug, Clone)]
805pub struct ContactCacheEntry {
806    /// Body pair key (a, b) with a < b.
807    pub key: (u32, u32),
808    /// Accumulated normal impulse from the previous frame.
809    pub accumulated_normal: f32,
810    /// Accumulated tangent impulse (direction 1) from the previous frame.
811    pub accumulated_tangent_1: f32,
812    /// Accumulated tangent impulse (direction 2) from the previous frame.
813    pub accumulated_tangent_2: f32,
814    /// Contact point used for positional warmstart.
815    pub contact_point: [f32; 3],
816    /// Age in frames since this entry was last refreshed.
817    pub age_frames: u32,
818}
819impl ContactCacheEntry {
820    /// Create a new cache entry for the given pair and contact.
821    pub fn new(pair: &CollisionPair, contact: &ContactResult) -> Self {
822        let (a, b) = if pair.a <= pair.b {
823            (pair.a, pair.b)
824        } else {
825            (pair.b, pair.a)
826        };
827        Self {
828            key: (a, b),
829            accumulated_normal: 0.0_f32,
830            accumulated_tangent_1: 0.0_f32,
831            accumulated_tangent_2: 0.0_f32,
832            contact_point: contact.contact_point,
833            age_frames: 0,
834        }
835    }
836    /// Apply warmstart data to a manifold point.
837    pub fn apply_warm_start(&self, pt: &mut ManifoldPoint) {
838        pt.warm_impulse_normal = self.accumulated_normal;
839        pt.warm_impulse_t1 = self.accumulated_tangent_1;
840        pt.warm_impulse_t2 = self.accumulated_tangent_2;
841    }
842    /// Update from a resolved contact (call after solver).
843    pub fn update(&mut self, normal_impulse: f32, t1: f32, t2: f32) {
844        self.accumulated_normal = normal_impulse;
845        self.accumulated_tangent_1 = t1;
846        self.accumulated_tangent_2 = t2;
847        self.age_frames = 0;
848    }
849}
850/// Flat AABB tree (BVH) built with parallel bottom-up Morton-code construction.
851///
852/// The tree stores nodes in a flat `Vec`BvhNodeGpu` for cache-friendly GPU
853/// traversal.  Construction uses a parallel (CPU-mock) bottom-up merge pass
854/// after sorting leaf centres by Morton code.
855#[derive(Debug, Default)]
856pub struct GpuAabbTree {
857    /// Flat array of BVH nodes.
858    pub nodes: Vec<BvhNodeGpu>,
859    /// Index of the root node.
860    pub root: usize,
861    /// Number of leaf primitives.
862    pub num_primitives: usize,
863    /// Morton codes computed during last build.
864    pub morton_codes: Vec<u32>,
865}
866impl GpuAabbTree {
867    /// Create a new empty `GpuAabbTree`.
868    pub fn new() -> Self {
869        Self::default()
870    }
871    /// Build the tree from a slice of `AabbGpu` primitives.
872    ///
873    /// Sorts primitives by Morton code, inserts leaf nodes, then performs
874    /// a CPU-mock parallel bottom-up merge to form internal nodes.
875    pub fn build(&mut self, aabbs: &[AabbGpu]) {
876        self.nodes.clear();
877        self.morton_codes.clear();
878        self.num_primitives = aabbs.len();
879        if aabbs.is_empty() {
880            self.root = 0;
881            return;
882        }
883        let mut scene_min = [f32::INFINITY; 3];
884        let mut scene_max = [f32::NEG_INFINITY; 3];
885        for a in aabbs {
886            for d in 0..3 {
887                scene_min[d] = scene_min[d].min(a.min[d]);
888                scene_max[d] = scene_max[d].max(a.max[d]);
889            }
890        }
891        let extent = [
892            (scene_max[0] - scene_min[0]).max(1.0e-6_f32),
893            (scene_max[1] - scene_min[1]).max(1.0e-6_f32),
894            (scene_max[2] - scene_min[2]).max(1.0e-6_f32),
895        ];
896        let mut order: Vec<(u32, usize)> = aabbs
897            .iter()
898            .enumerate()
899            .map(|(i, a)| {
900                let c = a.centre();
901                let nx = (c[0] - scene_min[0]) / extent[0];
902                let ny = (c[1] - scene_min[1]) / extent[1];
903                let nz = (c[2] - scene_min[2]) / extent[2];
904                let code = morton_code(nx, ny, nz);
905                self.morton_codes.push(code);
906                (code, i)
907            })
908            .collect();
909        order.sort_by_key(|&(code, _)| code);
910        for &(_, prim_idx) in &order {
911            self.nodes
912                .push(BvhNodeGpu::leaf(aabbs[prim_idx].clone(), prim_idx as u32));
913        }
914        let mut level: Vec<usize> = (0..aabbs.len()).collect();
915        while level.len() > 1 {
916            let mut next = Vec::with_capacity(level.len().div_ceil(2));
917            let mut i = 0;
918            while i < level.len() {
919                if i + 1 < level.len() {
920                    let l = level[i];
921                    let r = level[i + 1];
922                    let merged = self.nodes[l].aabb.merge(&self.nodes[r].aabb);
923                    let parent = BvhNodeGpu::internal(merged, l as u32, r as u32);
924                    let idx = self.nodes.len();
925                    self.nodes.push(parent);
926                    next.push(idx);
927                    i += 2;
928                } else {
929                    next.push(level[i]);
930                    i += 1;
931                }
932            }
933            level = next;
934        }
935        self.root = level[0];
936    }
937    /// Query all leaf primitives whose AABB overlaps `query`.
938    pub fn query(&self, query: &AabbGpu) -> Vec<u32> {
939        if self.nodes.is_empty() {
940            return Vec::new();
941        }
942        let mut result = Vec::new();
943        let mut stack = vec![self.root];
944        while let Some(idx) = stack.pop() {
945            let node = &self.nodes[idx];
946            if !node.aabb.overlaps(query) {
947                continue;
948            }
949            if node.is_leaf {
950                result.push(node.primitive_index);
951            } else {
952                stack.push(node.left_child as usize);
953                stack.push(node.right_child as usize);
954            }
955        }
956        result
957    }
958    /// Tree depth (longest path from root to leaf).
959    pub fn depth(&self) -> usize {
960        if self.nodes.is_empty() {
961            return 0;
962        }
963        self.depth_from(self.root)
964    }
965    fn depth_from(&self, idx: usize) -> usize {
966        let node = &self.nodes[idx];
967        if node.is_leaf {
968            1
969        } else {
970            let ld = self.depth_from(node.left_child as usize);
971            let rd = self.depth_from(node.right_child as usize);
972            1 + ld.max(rd)
973        }
974    }
975    /// Count the number of leaf nodes.
976    pub fn leaf_count(&self) -> usize {
977        self.nodes.iter().filter(|n| n.is_leaf).count()
978    }
979    /// Surface Area Heuristic cost of the tree.
980    ///
981    /// SAH cost = sum over all internal nodes of `surface_area(node)`.
982    pub fn sah_cost(&self) -> f32 {
983        self.nodes
984            .iter()
985            .filter(|n| !n.is_leaf)
986            .map(|n| n.aabb.surface_area())
987            .sum()
988    }
989}
990/// GPU narrowphase kernel performing parallel GJK distance queries.
991///
992/// Each pair from the broadphase is resolved with a dedicated GJK iteration
993/// to compute exact contact information.
994#[derive(Debug, Default)]
995pub struct GpuNarrowphase {
996    /// Maximum GJK iterations before declaring intersection.
997    pub max_iterations: usize,
998    /// Stats from the most recent dispatch.
999    pub stats: CollisionKernelStats,
1000}
1001impl GpuNarrowphase {
1002    /// Create a new `GpuNarrowphase` with a given iteration limit.
1003    pub fn new(max_iterations: usize) -> Self {
1004        Self {
1005            max_iterations: max_iterations.max(4),
1006            stats: CollisionKernelStats::new(),
1007        }
1008    }
1009    /// Run a GJK distance query for two `AabbGpu` shapes.
1010    pub fn gjk_distance(&self, a: &AabbGpu, b: &AabbGpu) -> GjkResult {
1011        let ca = a.centre();
1012        let cb = b.centre();
1013        let mut d = sub3f(ca, cb);
1014        if len_sq3f(d) < 1.0e-9_f32 {
1015            d = [1.0_f32, 0.0_f32, 0.0_f32];
1016        }
1017        let s0 = minkowski_support(a, b, d);
1018        let mut simplex: Vec<SupportPoint> = vec![s0];
1019        d = [-s0.v[0], -s0.v[1], -s0.v[2]];
1020        for _ in 0..self.max_iterations {
1021            if len_sq3f(d) < 1.0e-12_f32 {
1022                return GjkResult {
1023                    intersecting: true,
1024                    distance: 0.0_f32,
1025                    closest_a: ca,
1026                    closest_b: cb,
1027                    axis: norm3f(sub3f(ca, cb)),
1028                };
1029            }
1030            let sup = minkowski_support(a, b, d);
1031            if dot3f(sup.v, d) < 0.0_f32 {
1032                let dist = len3f(d);
1033                let axis = norm3f(d);
1034                return GjkResult {
1035                    intersecting: false,
1036                    distance: dist,
1037                    closest_a: aabb_support(a, axis),
1038                    closest_b: aabb_support(b, [-axis[0], -axis[1], -axis[2]]),
1039                    axis,
1040                };
1041            }
1042            simplex.push(sup);
1043            let (inter, new_d) = update_simplex(&mut simplex);
1044            if inter {
1045                return GjkResult {
1046                    intersecting: true,
1047                    distance: 0.0_f32,
1048                    closest_a: ca,
1049                    closest_b: cb,
1050                    axis: norm3f(sub3f(ca, cb)),
1051                };
1052            }
1053            d = new_d;
1054        }
1055        GjkResult {
1056            intersecting: true,
1057            distance: 0.0_f32,
1058            closest_a: ca,
1059            closest_b: cb,
1060            axis: norm3f(sub3f(ca, cb)),
1061        }
1062    }
1063    /// Compute penetration depth from an overlapping AABB pair using SAT.
1064    ///
1065    /// Returns `None` if the shapes do not overlap.
1066    pub fn sat_depth(&self, a: &AabbGpu, b: &AabbGpu) -> Option<(f32, [f32; 3])> {
1067        let mut min_depth = f32::INFINITY;
1068        let mut min_axis = [1.0_f32, 0.0_f32, 0.0_f32];
1069        for d in 0..3usize {
1070            let mut axis = [0.0_f32; 3];
1071            axis[d] = 1.0_f32;
1072            let a_min = a.min[d];
1073            let a_max = a.max[d];
1074            let b_min = b.min[d];
1075            let b_max = b.max[d];
1076            let overlap = a_max.min(b_max) - a_min.max(b_min);
1077            if overlap <= 0.0_f32 {
1078                return None;
1079            }
1080            if overlap < min_depth {
1081                min_depth = overlap;
1082                let sign = if (a.min[d] + a.max[d]) > (b.min[d] + b.max[d]) {
1083                    1.0_f32
1084                } else {
1085                    -1.0_f32
1086                };
1087                axis[d] = sign;
1088                min_axis = axis;
1089            }
1090        }
1091        Some((min_depth, min_axis))
1092    }
1093    /// Dispatch the narrowphase over a list of broadphase pairs.
1094    ///
1095    /// Returns full `ContactResult` list with GJK/SAT contact data.
1096    pub fn dispatch(&mut self, pairs: &[CollisionPair], aabbs: &[AabbGpu]) -> Vec<ContactResult> {
1097        let mut contacts = Vec::new();
1098        self.stats.narrowphase_queries += pairs.len() as u64;
1099        for pair in pairs {
1100            let ai = pair.a as usize;
1101            let bi = pair.b as usize;
1102            if ai >= aabbs.len() || bi >= aabbs.len() {
1103                continue;
1104            }
1105            let gjk = self.gjk_distance(&aabbs[ai], &aabbs[bi]);
1106            if gjk.intersecting {
1107                let (depth, normal) = self
1108                    .sat_depth(&aabbs[ai], &aabbs[bi])
1109                    .unwrap_or((0.0_f32, gjk.axis));
1110                let ca = aabbs[ai].centre();
1111                let cb = aabbs[bi].centre();
1112                let contact_point = [
1113                    (ca[0] + cb[0]) * 0.5_f32,
1114                    (ca[1] + cb[1]) * 0.5_f32,
1115                    (ca[2] + cb[2]) * 0.5_f32,
1116                ];
1117                contacts.push(ContactResult {
1118                    prim_a: pair.a,
1119                    prim_b: pair.b,
1120                    contact_point,
1121                    normal,
1122                    depth,
1123                });
1124                self.stats.narrowphase_contacts += 1;
1125            }
1126        }
1127        contacts
1128    }
1129}
1130/// Persistent contact cache for GPU broadphase/narrowphase warmstarting.
1131///
1132/// Maps body-pair keys to cached impulse data from the previous frame.
1133/// Entries are aged out after `max_age_frames` frames without a refresh.
1134#[derive(Debug, Default)]
1135pub struct GpuContactCache {
1136    /// Stored entries.
1137    pub entries: Vec<ContactCacheEntry>,
1138    /// Maximum age before an entry is evicted.
1139    pub max_age_frames: u32,
1140}
1141impl GpuContactCache {
1142    /// Create a new contact cache with the given maximum age.
1143    pub fn new(max_age_frames: u32) -> Self {
1144        Self {
1145            entries: Vec::new(),
1146            max_age_frames: max_age_frames.max(1),
1147        }
1148    }
1149    /// Look up a cached entry for the given pair key.
1150    pub fn find(&self, a: u32, b: u32) -> Option<&ContactCacheEntry> {
1151        let key = if a <= b { (a, b) } else { (b, a) };
1152        self.entries.iter().find(|e| e.key == key)
1153    }
1154    /// Look up a cached entry mutably.
1155    pub fn find_mut(&mut self, a: u32, b: u32) -> Option<&mut ContactCacheEntry> {
1156        let key = if a <= b { (a, b) } else { (b, a) };
1157        self.entries.iter_mut().find(|e| e.key == key)
1158    }
1159    /// Insert or refresh a contact cache entry.
1160    pub fn insert(&mut self, pair: &CollisionPair, contact: &ContactResult) {
1161        let a = pair.a;
1162        let b = pair.b;
1163        let key = if a <= b { (a, b) } else { (b, a) };
1164        if let Some(e) = self.entries.iter_mut().find(|e| e.key == key) {
1165            e.contact_point = contact.contact_point;
1166            e.age_frames = 0;
1167        } else {
1168            self.entries.push(ContactCacheEntry::new(pair, contact));
1169        }
1170    }
1171    /// Advance the cache by one frame: age entries and evict stale ones.
1172    pub fn advance_frame(&mut self) {
1173        for e in self.entries.iter_mut() {
1174            e.age_frames += 1;
1175        }
1176        let max_age = self.max_age_frames;
1177        self.entries.retain(|e| e.age_frames <= max_age);
1178    }
1179    /// Number of entries currently in the cache.
1180    pub fn len(&self) -> usize {
1181        self.entries.len()
1182    }
1183    /// Returns `true` if the cache is empty.
1184    pub fn is_empty(&self) -> bool {
1185        self.entries.is_empty()
1186    }
1187    /// Clear all entries.
1188    pub fn clear(&mut self) {
1189        self.entries.clear();
1190    }
1191}
1192/// GPU-accelerated broadphase using Sort-and-Sweep (SAP).
1193///
1194/// Sorts AABB projections along a chosen axis (default X), sweeps for
1195/// overlapping intervals, then validates the remaining axes and records
1196/// broadphase pairs.
1197#[derive(Debug, Default)]
1198pub struct GpuBroadphase {
1199    /// Axis along which to sort/sweep: 0=X, 1=Y, 2=Z.
1200    pub sweep_axis: usize,
1201    /// Inflation margin applied to each AABB before testing.
1202    pub margin: f32,
1203    /// Stats from the most recent dispatch.
1204    pub stats: CollisionKernelStats,
1205}
1206impl GpuBroadphase {
1207    /// Create a new `GpuBroadphase` with the given sweep axis and margin.
1208    pub fn new(sweep_axis: usize, margin: f32) -> Self {
1209        Self {
1210            sweep_axis: sweep_axis % 3,
1211            margin,
1212            stats: CollisionKernelStats::new(),
1213        }
1214    }
1215    /// Choose the axis with the widest spread for better SAP performance.
1216    ///
1217    /// Returns the axis index (0, 1, or 2).
1218    pub fn choose_best_axis(aabbs: &[AabbGpu]) -> usize {
1219        if aabbs.is_empty() {
1220            return 0;
1221        }
1222        let mut spread = [0.0_f32; 3];
1223        for (d, s) in spread.iter_mut().enumerate() {
1224            let lo = aabbs.iter().map(|a| a.min[d]).fold(f32::INFINITY, f32::min);
1225            let hi = aabbs
1226                .iter()
1227                .map(|a| a.max[d])
1228                .fold(f32::NEG_INFINITY, f32::max);
1229            *s = hi - lo;
1230        }
1231        if spread[0] >= spread[1] && spread[0] >= spread[2] {
1232            0
1233        } else if spread[1] >= spread[2] {
1234            1
1235        } else {
1236            2
1237        }
1238    }
1239    /// Run the SAP broadphase and return overlapping pairs.
1240    ///
1241    /// Complexity: O(n log n + k) where k is the number of pairs found.
1242    pub fn dispatch(&mut self, aabbs: &[AabbGpu]) -> Vec<CollisionPair> {
1243        let axis = self.sweep_axis;
1244        let _n = aabbs.len();
1245        let mut pairs = Vec::new();
1246        let mut entries: Vec<SapEntry> = aabbs
1247            .iter()
1248            .enumerate()
1249            .map(|(i, a)| SapEntry {
1250                lo: a.min[axis] - self.margin,
1251                hi: a.max[axis] + self.margin,
1252                idx: i as u32,
1253            })
1254            .collect();
1255        entries.sort_by(|a, b| a.lo.partial_cmp(&b.lo).unwrap_or(std::cmp::Ordering::Equal));
1256        let mut tests = 0u64;
1257        let mut hits = 0u64;
1258        for (i, ei) in entries.iter().enumerate() {
1259            for ej in entries.iter().skip(i + 1) {
1260                if ej.lo > ei.hi {
1261                    break;
1262                }
1263                tests += 1;
1264                let pi = ei.idx as usize;
1265                let pj = ej.idx as usize;
1266                let expanded_i = aabbs[pi].expanded(self.margin);
1267                if expanded_i.overlaps(&aabbs[pj]) {
1268                    hits += 1;
1269                    pairs.push(CollisionPair::new(ei.idx, ej.idx));
1270                }
1271            }
1272        }
1273        self.stats.broadphase_pairs_tested += tests;
1274        self.stats.broadphase_hits += hits;
1275        self.stats.bytes_transferred += std::mem::size_of_val(aabbs) as u64;
1276        pairs
1277    }
1278}