Skip to main content

oxiphysics_gpu/collision_gpu/
types.rs

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