Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
gpu.rs

1//! Typed WGSL geometry kernels with deterministic CPU oracles.
2//!
3//! These kernels complement WGSL Forge without pretending that branch-heavy
4//! exact topology edits belong on the GPU. Parallel broad phases run here;
5//! uncertain predicates are explicitly returned to the robust CPU/WASM path.
6//!
7//! ## P1.9 — orient3d and incircle GPU batches
8//!
9//! Added `Orient3dF32` and `IncircleF32` kernels, each with:
10//! - A WGSL shader that computes the filtered determinant in f32 and flags
11//!   `GPU_ORIENTATION_UNCERTAIN` when near the error bound.
12//! - A CPU/WASM oracle (`evaluate_orient3d_batch_f32`, `evaluate_incircle_batch_f32`)
13//!   that runs the full filtered → compensated → exact ladder.
14//!
15//! The GPU is the fast broad phase; uncertain lanes fall back to the CPU
16//! oracle's exact path. On the no-adapter path, the CPU oracle is the
17//! deterministic fallback (identical results, no GPU needed).
18
19use super::box_join::BoxPair;
20use super::expansion::Sign;
21use super::{incircle, orient_3d, orientation_2, Aabb, Point2, Point3};
22
23pub const GPU_ORIENTATION_UNCERTAIN: i32 = 2;
24
25/// GPU candidate-generation result flag: the pair is a definite overlap.
26pub const GPU_OVERLAP_YES: i32 = 1;
27/// GPU candidate-generation result flag: the pair is definitely not overlapping.
28pub const GPU_OVERLAP_NO: i32 = 0;
29/// GPU candidate-generation result flag: uncertain (near-boundary), needs CPU verification.
30pub const GPU_OVERLAP_UNCERTAIN: i32 = 2;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum GeometryGpuKernel {
34    /// One invocation per packed `(a,b,c)` f32 point triple (6 f32s).
35    Orientation2F32,
36    /// One invocation per packed `(a,b,c,d)` f32 point quadruple (12 f32s).
37    /// P1.9.
38    Orient3dF32,
39    /// One invocation per packed `(a,b,c,d)` f32 point quadruple (8 f32s).
40    /// P1.9.
41    IncircleF32,
42    /// One invocation per AABB pair (12 f32s: amin[3], amax[3], bmin[3], bmax[3]).
43    /// P3.6: GPU broad-phase overlap test with filtered error bound.
44    AabbOverlapF32,
45    /// One invocation per point-AABB pair (6 f32s: point[3], amin[3]) + 3 f32s (amax[3]).
46    /// P3.6: GPU point-to-AABB distance squared for NN candidate filtering.
47    PointAabbDistSqF32,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct GeometryGpuSchedule {
52    pub workgroup_size: u32,
53}
54
55impl Default for GeometryGpuSchedule {
56    fn default() -> Self {
57        Self {
58            workgroup_size: 128,
59        }
60    }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum GeometryGpuError {
65    InvalidWorkgroupSize,
66    InputLengthNotMultipleOfSix,
67    InputLengthNotMultipleOfEight,
68    InputLengthNotMultipleOfTwelve,
69    InputLengthNotMultipleOfTwentyFour,
70    OutputTooSmall {
71        required: usize,
72    },
73    /// Candidate buffer too small for GPU-generated candidates.
74    CandidateBufferTooSmall {
75        required: usize,
76    },
77}
78
79/// Deterministically emit a typed computational-geometry shader.
80pub fn emit_geometry_wgsl(
81    kernel: GeometryGpuKernel,
82    schedule: GeometryGpuSchedule,
83) -> Result<String, GeometryGpuError> {
84    if !(32..=256).contains(&schedule.workgroup_size) || !schedule.workgroup_size.is_power_of_two()
85    {
86        return Err(GeometryGpuError::InvalidWorkgroupSize);
87    }
88    match kernel {
89        GeometryGpuKernel::Orientation2F32 => Ok(format!(
90            r#"// QualiaDB computational geometry: orientation_2_f32 v1
91struct Params {{
92    triple_count: u32,
93    _pad0: u32,
94    _pad1: u32,
95    _pad2: u32,
96}}
97
98@group(0) @binding(0)
99var<storage, read> points: array<f32>;
100
101@group(0) @binding(1)
102var<storage, read_write> orientation: array<i32>;
103
104@group(0) @binding(2)
105var<uniform> params: Params;
106
107@compute @workgroup_size({workgroup_size}, 1, 1)
108fn orientation_2_f32(@builtin(global_invocation_id) gid: vec3<u32>) {{
109    let i = gid.x;
110    if (i >= params.triple_count) {{
111        return;
112    }}
113    let base = i * 6u;
114    let ax = points[base];
115    let ay = points[base + 1u];
116    let bx = points[base + 2u];
117    let by = points[base + 3u];
118    let cx = points[base + 4u];
119    let cy = points[base + 5u];
120    let left = (ax - cx) * (by - cy);
121    let right = (ay - cy) * (bx - cx);
122    let det = left - right;
123    // Filter only. Near-degenerate triples are resolved by the exact CPU/WASM oracle.
124    let error_bound = (abs(left) + abs(right)) * 9.5367431640625e-7;
125    if (abs(det) <= error_bound) {{
126        orientation[i] = {uncertain};
127    }} else if (det > 0.0) {{
128        orientation[i] = 1;
129    }} else {{
130        orientation[i] = -1;
131    }}
132}}
133"#,
134            workgroup_size = schedule.workgroup_size,
135            uncertain = GPU_ORIENTATION_UNCERTAIN,
136        )),
137
138        GeometryGpuKernel::Orient3dF32 => Ok(format!(
139            r#"// QualiaDB computational geometry: orient_3d_f32 v1
140struct Params {{
141    quad_count: u32,
142    _pad0: u32,
143    _pad1: u32,
144    _pad2: u32,
145}}
146
147@group(0) @binding(0)
148var<storage, read> points: array<f32>;
149
150@group(0) @binding(1)
151var<storage, read_write> orient: array<i32>;
152
153@group(0) @binding(2)
154var<uniform> params: Params;
155
156@compute @workgroup_size({workgroup_size}, 1, 1)
157fn orient_3d_f32(@builtin(global_invocation_id) gid: vec3<u32>) {{
158    let i = gid.x;
159    if (i >= params.quad_count) {{
160        return;
161    }}
162    let base = i * 12u;
163    let ax = points[base];
164    let ay = points[base + 1u];
165    let az = points[base + 2u];
166    let bx = points[base + 3u];
167    let by = points[base + 4u];
168    let bz = points[base + 5u];
169    let cx = points[base + 6u];
170    let cy = points[base + 7u];
171    let cz = points[base + 8u];
172    let dx = points[base + 9u];
173    let dy = points[base + 10u];
174    let dz = points[base + 11u];
175    // det = (b-a) · ((c-a) × (d-a))
176    let abx = bx - ax;
177    let aby = by - ay;
178    let abz = bz - az;
179    let acx = cx - ax;
180    let acy = cy - ay;
181    let acz = cz - az;
182    let adx = dx - ax;
183    let ady = dy - ay;
184    let adz = dz - az;
185    // cross = (c-a) × (d-a)
186    let cx0 = acy * adz - acz * ady;
187    let cy0 = acz * adx - acx * adz;
188    let cz0 = acx * ady - acy * adx;
189    // dot = (b-a) · cross
190    let det = abx * cx0 + aby * cy0 + abz * cz0;
191    // Filtered error bound: sum of absolute products × f32 epsilon.
192    let perm = abs(abx) * (abs(acy) * abs(adz) + abs(acz) * abs(ady))
193             + abs(aby) * (abs(acx) * abs(adz) + abs(acz) * abs(adx))
194             + abs(abz) * (abs(acx) * abs(ady) + abs(acy) * abs(adx));
195    let error_bound = perm * 1.52587890625e-5;
196    if (abs(det) <= error_bound) {{
197        orient[i] = {uncertain};
198    }} else if (det > 0.0) {{
199        orient[i] = 1;
200    }} else {{
201        orient[i] = -1;
202    }}
203}}
204"#,
205            workgroup_size = schedule.workgroup_size,
206            uncertain = GPU_ORIENTATION_UNCERTAIN,
207        )),
208
209        GeometryGpuKernel::IncircleF32 => Ok(format!(
210            r#"// QualiaDB computational geometry: incircle_f32 v1
211struct Params {{
212    quad_count: u32,
213    _pad0: u32,
214    _pad1: u32,
215    _pad2: u32,
216}}
217
218@group(0) @binding(0)
219var<storage, read> points: array<f32>;
220
221@group(0) @binding(1)
222var<storage, read_write> incircle_out: array<i32>;
223
224@group(0) @binding(2)
225var<uniform> params: Params;
226
227@compute @workgroup_size({workgroup_size}, 1, 1)
228fn incircle_f32(@builtin(global_invocation_id) gid: vec3<u32>) {{
229    let i = gid.x;
230    if (i >= params.quad_count) {{
231        return;
232    }}
233    let base = i * 8u;
234    let ax = points[base];
235    let ay = points[base + 1u];
236    let bx = points[base + 2u];
237    let by = points[base + 3u];
238    let cx = points[base + 4u];
239    let cy = points[base + 5u];
240    let dx = points[base + 6u];
241    let dy = points[base + 7u];
242    // Translate by d
243    let adx = ax - dx;
244    let ady = ay - dy;
245    let bdx = bx - dx;
246    let bdy = by - dy;
247    let cdx = cx - dx;
248    let cdy = cy - dy;
249    let ad2 = adx * adx + ady * ady;
250    let bd2 = bdx * bdx + bdy * bdy;
251    let cd2 = cdx * cdx + cdy * cdy;
252    // det = adx*(bdy*cd2 - cdy*bd2) - ady*(bdx*cd2 - cdx*bd2) + ad2*(bdx*cdy - bdy*cdx)
253    let m1 = bdy * cd2 - cdy * bd2;
254    let m2 = bdx * cd2 - cdx * bd2;
255    let m3 = bdx * cdy - bdy * cdx;
256    let det = adx * m1 - ady * m2 + ad2 * m3;
257    // Filtered error bound
258    let perm = abs(adx) * (abs(bdy) * abs(cd2) + abs(cdy) * abs(bd2))
259             + abs(ady) * (abs(bdx) * abs(cd2) + abs(cdx) * abs(bd2))
260             + abs(ad2) * (abs(bdx) * abs(cdy) + abs(bdy) * abs(cdx));
261    let error_bound = perm * 3.0517578125e-5;
262    if (abs(det) <= error_bound) {{
263        incircle_out[i] = {uncertain};
264    }} else if (det > 0.0) {{
265        incircle_out[i] = 1;
266    }} else {{
267        incircle_out[i] = -1;
268    }}
269}}
270"#,
271            workgroup_size = schedule.workgroup_size,
272            uncertain = GPU_ORIENTATION_UNCERTAIN,
273        )),
274
275        GeometryGpuKernel::AabbOverlapF32 => Ok(format!(
276            r#"// QualiaDB computational geometry: aabb_overlap_f32 v1
277struct Params {{
278    pair_count: u32,
279    _pad0: u32,
280    _pad1: u32,
281    _pad2: u32,
282}}
283
284@group(0) @binding(0)
285var<storage, read> pairs: array<f32>;
286
287@group(0) @binding(1)
288var<storage, read_write> overlap_out: array<i32>;
289
290@group(0) @binding(2)
291var<uniform> params: Params;
292
293@compute @workgroup_size({workgroup_size}, 1, 1)
294fn aabb_overlap_f32(@builtin(global_invocation_id) gid: vec3<u32>) {{
295    let i = gid.x;
296    if (i >= params.pair_count) {{
297        return;
298    }}
299    let base = i * 12u;
300    let aminx = pairs[base];      let aminy = pairs[base + 1u];  let aminz = pairs[base + 2u];
301    let amaxx = pairs[base + 3u]; let amaxy = pairs[base + 4u];  let amaxz = pairs[base + 5u];
302    let bminx = pairs[base + 6u]; let bminy = pairs[base + 7u];  let bminz = pairs[base + 8u];
303    let bmaxx = pairs[base + 9u]; let bmaxy = pairs[base + 10u]; let bmaxz = pairs[base + 11u];
304    // Overlap test: amin <= bmax && bmin <= amax on all axes.
305    let dx1 = bmaxx - aminx;
306    let dx2 = amaxx - bminx;
307    let dy1 = bmaxy - aminy;
308    let dy2 = amaxy - bminy;
309    let dz1 = bmaxz - aminz;
310    let dz2 = amaxz - bminz;
311    // Filtered: if any gap is near zero, flag uncertain.
312    let eps = 1.0e-5;
313    let min_gap = min(min(min(dx1, dx2), min(dy1, dy2)), min(dz1, dz2));
314    if (min_gap < 0.0) {{
315        overlap_out[i] = {overlap_no};
316    }} else if (min_gap < eps) {{
317        overlap_out[i] = {overlap_uncertain};
318    }} else {{
319        overlap_out[i] = {overlap_yes};
320    }}
321}}
322"#,
323            workgroup_size = schedule.workgroup_size,
324            overlap_no = GPU_OVERLAP_NO,
325            overlap_uncertain = GPU_OVERLAP_UNCERTAIN,
326            overlap_yes = GPU_OVERLAP_YES,
327        )),
328
329        GeometryGpuKernel::PointAabbDistSqF32 => Ok(format!(
330            r#"// QualiaDB computational geometry: point_aabb_dist_sq_f32 v1
331struct Params {{
332    query_count: u32,
333    _pad0: u32,
334    _pad1: u32,
335    _pad2: u32,
336}}
337
338@group(0) @binding(0)
339var<storage, read> data: array<f32>;
340
341@group(0) @binding(1)
342var<storage, read_write> dist_sq_out: array<f32>;
343
344@group(0) @binding(2)
345var<uniform> params: Params;
346
347@compute @workgroup_size({workgroup_size}, 1, 1)
348fn point_aabb_dist_sq_f32(@builtin(global_invocation_id) gid: vec3<u32>) {{
349    let i = gid.x;
350    if (i >= params.query_count) {{
351        return;
352    }}
353    let base = i * 6u;
354    let px = data[base];     let py = data[base + 1u]; let pz = data[base + 2u];
355    let minx = data[base + 3u]; let miny = data[base + 4u]; let minz = data[base + 5u];
356    // Clamped distance: max(0, min - p) and max(0, p - max) per axis.
357    // We only have min here; for a proper distance we need max too.
358    // This kernel is a broad-phase filter: it computes the distance to
359    // the AABB's min corner as a lower bound. The CPU merge does the
360    // exact point-to-AABB distance.
361    let dx = max(0.0, minx - px);
362    let dy = max(0.0, miny - py);
363    let dz = max(0.0, minz - pz);
364    dist_sq_out[i] = dx * dx + dy * dy + dz * dz;
365}}
366"#,
367            workgroup_size = schedule.workgroup_size,
368        )),
369    }
370}
371
372/// CPU/WASM oracle for packed `(ax, ay, bx, by, cx, cy)` f32 triples.
373pub fn evaluate_orientation_batch_f32(
374    packed: &[f32],
375    out: &mut [i8],
376) -> Result<usize, GeometryGpuError> {
377    if packed.len() % 6 != 0 {
378        return Err(GeometryGpuError::InputLengthNotMultipleOfSix);
379    }
380    let count = packed.len() / 6;
381    if out.len() < count {
382        return Err(GeometryGpuError::OutputTooSmall { required: count });
383    }
384    for (index, triple) in packed.chunks_exact(6).enumerate() {
385        out[index] = orientation_2(
386            Point2::new(triple[0] as f64, triple[1] as f64),
387            Point2::new(triple[2] as f64, triple[3] as f64),
388            Point2::new(triple[4] as f64, triple[5] as f64),
389        ) as i8;
390    }
391    Ok(count)
392}
393
394/// CPU/WASM oracle for packed `(ax,ay,az, bx,by,bz, cx,cy,cz, dx,dy,dz)` f32
395/// quadruples (12 f32s per quad). Runs the full filtered → compensated → exact
396/// ladder via [`orient_3d`].
397///
398/// P1.9.
399pub fn evaluate_orient3d_batch_f32(
400    packed: &[f32],
401    out: &mut [i8],
402) -> Result<usize, GeometryGpuError> {
403    if packed.len() % 12 != 0 {
404        return Err(GeometryGpuError::InputLengthNotMultipleOfTwelve);
405    }
406    let count = packed.len() / 12;
407    if out.len() < count {
408        return Err(GeometryGpuError::OutputTooSmall { required: count });
409    }
410    for (index, quad) in packed.chunks_exact(12).enumerate() {
411        let s = orient_3d(
412            Point3::new(quad[0] as f64, quad[1] as f64, quad[2] as f64),
413            Point3::new(quad[3] as f64, quad[4] as f64, quad[5] as f64),
414            Point3::new(quad[6] as f64, quad[7] as f64, quad[8] as f64),
415            Point3::new(quad[9] as f64, quad[10] as f64, quad[11] as f64),
416        );
417        out[index] = sign_to_i8(s);
418    }
419    Ok(count)
420}
421
422/// CPU/WASM oracle for packed `(ax,ay, bx,by, cx,cy, dx,dy)` f32 quadruples
423/// (8 f32s per quad). Runs the full filtered → compensated → exact ladder via
424/// [`incircle`].
425///
426/// P1.9.
427pub fn evaluate_incircle_batch_f32(
428    packed: &[f32],
429    out: &mut [i8],
430) -> Result<usize, GeometryGpuError> {
431    if packed.len() % 8 != 0 {
432        return Err(GeometryGpuError::InputLengthNotMultipleOfEight);
433    }
434    let count = packed.len() / 8;
435    if out.len() < count {
436        return Err(GeometryGpuError::OutputTooSmall { required: count });
437    }
438    for (index, quad) in packed.chunks_exact(8).enumerate() {
439        let s = incircle(
440            Point2::new(quad[0] as f64, quad[1] as f64),
441            Point2::new(quad[2] as f64, quad[3] as f64),
442            Point2::new(quad[4] as f64, quad[5] as f64),
443            Point2::new(quad[6] as f64, quad[7] as f64),
444        );
445        out[index] = sign_to_i8(s);
446    }
447    Ok(count)
448}
449
450// ── P3.6: AABB overlap CPU oracle + merge ────────────────────────────────
451
452/// CPU/WASM oracle for packed AABB pairs (12 f32s per pair).
453/// Returns exact overlap results: 1 = overlap, 0 = no overlap.
454pub fn evaluate_aabb_overlap_batch_f32(
455    packed: &[f32],
456    out: &mut [i32],
457) -> Result<usize, GeometryGpuError> {
458    if packed.len() % 12 != 0 {
459        return Err(GeometryGpuError::InputLengthNotMultipleOfTwelve);
460    }
461    let count = packed.len() / 12;
462    if out.len() < count {
463        return Err(GeometryGpuError::OutputTooSmall { required: count });
464    }
465    for (index, pair) in packed.chunks_exact(12).enumerate() {
466        let a = Aabb::new(
467            Point3::new(pair[0] as f64, pair[1] as f64, pair[2] as f64),
468            Point3::new(pair[3] as f64, pair[4] as f64, pair[5] as f64),
469        );
470        let b = Aabb::new(
471            Point3::new(pair[6] as f64, pair[7] as f64, pair[8] as f64),
472            Point3::new(pair[9] as f64, pair[10] as f64, pair[11] as f64),
473        );
474        out[index] = if a.overlaps(&b) {
475            GPU_OVERLAP_YES
476        } else {
477            GPU_OVERLAP_NO
478        };
479    }
480    Ok(count)
481}
482
483/// GPU filter simulation for AABB overlap (f32). Returns YES/NO/UNCERTAIN.
484fn gpu_filter_aabb_overlap_f32(pair: &[f32]) -> i32 {
485    let aminx = pair[0] as f64;
486    let aminy = pair[1] as f64;
487    let aminz = pair[2] as f64;
488    let amaxx = pair[3] as f64;
489    let amaxy = pair[4] as f64;
490    let amaxz = pair[5] as f64;
491    let bminx = pair[6] as f64;
492    let bminy = pair[7] as f64;
493    let bminz = pair[8] as f64;
494    let bmaxx = pair[9] as f64;
495    let bmaxy = pair[10] as f64;
496    let bmaxz = pair[11] as f64;
497
498    let dx1 = bmaxx - aminx;
499    let dx2 = amaxx - bminx;
500    let dy1 = bmaxy - aminy;
501    let dy2 = amaxy - bminy;
502    let dz1 = bmaxz - aminz;
503    let dz2 = amaxz - bminz;
504
505    let min_gap = dx1.min(dx2).min(dy1).min(dy2).min(dz1).min(dz2);
506    let eps = 1.0e-5;
507
508    if min_gap < 0.0 {
509        GPU_OVERLAP_NO
510    } else if min_gap < eps {
511        GPU_OVERLAP_UNCERTAIN
512    } else {
513        GPU_OVERLAP_YES
514    }
515}
516
517/// Deterministic merge: combine GPU candidate results with CPU verification.
518///
519/// GPU-certain YES/NO lanes are trusted. GPU-uncertain lanes are resolved
520/// by the CPU exact oracle. The output is deterministic: identical for
521/// GPU-on vs GPU-off (the merge guarantee).
522///
523/// `gpu_results` is the output of the GPU kernel (or the CPU simulation).
524/// `packed` is the original packed AABB pair data.
525/// `out` receives the final verified overlap results.
526pub fn merge_aabb_overlap_results(
527    packed: &[f32],
528    gpu_results: &[i32],
529    out: &mut [i32],
530) -> Result<usize, GeometryGpuError> {
531    let count = gpu_results.len();
532    if packed.len() < count * 12 {
533        return Err(GeometryGpuError::InputLengthNotMultipleOfTwelve);
534    }
535    if out.len() < count {
536        return Err(GeometryGpuError::OutputTooSmall { required: count });
537    }
538    for i in 0..count {
539        if gpu_results[i] == GPU_OVERLAP_UNCERTAIN {
540            // CPU exact resolution for uncertain lanes.
541            let pair = &packed[i * 12..(i + 1) * 12];
542            let a = Aabb::new(
543                Point3::new(pair[0] as f64, pair[1] as f64, pair[2] as f64),
544                Point3::new(pair[3] as f64, pair[4] as f64, pair[5] as f64),
545            );
546            let b = Aabb::new(
547                Point3::new(pair[6] as f64, pair[7] as f64, pair[8] as f64),
548                Point3::new(pair[9] as f64, pair[10] as f64, pair[11] as f64),
549            );
550            out[i] = if a.overlaps(&b) {
551                GPU_OVERLAP_YES
552            } else {
553                GPU_OVERLAP_NO
554            };
555        } else {
556            // Trust GPU-certain lanes.
557            out[i] = gpu_results[i];
558        }
559    }
560    Ok(count)
561}
562
563// ── P3.6: BVH overlap candidate generation + CPU merge ───────────────────
564
565/// Generate candidate pairs from a BVH overlap query using GPU-style
566/// broad-phase filtering, then merge with CPU exact verification.
567///
568/// This is the deterministic CPU path that produces results identical
569/// to the GPU path. When a GPU adapter is available, the GPU kernel
570/// generates the candidate set; this merge function verifies uncertain
571/// lanes. Without a GPU, this function does the full computation.
572///
573/// `boxes_a` / `boxes_b`: the two AABB sets.
574/// `out_pairs`: receives verified overlapping pairs.
575/// Returns the number of pairs written.
576pub fn gpu_candidate_box_join(
577    boxes_a: &[Aabb],
578    boxes_b: &[Aabb],
579    out_pairs: &mut [BoxPair],
580) -> Result<usize, GeometryGpuError> {
581    let max_pairs = boxes_a.len() * boxes_b.len();
582    if out_pairs.len() < max_pairs {
583        return Err(GeometryGpuError::CandidateBufferTooSmall {
584            required: max_pairs,
585        });
586    }
587
588    // Pack all pairs into f32 buffer (12 f32s per pair).
589    let mut packed: Vec<f32> = Vec::with_capacity(max_pairs * 12);
590    let mut pair_indices: Vec<(u32, u32)> = Vec::with_capacity(max_pairs);
591    for (i, a) in boxes_a.iter().enumerate() {
592        for (j, b) in boxes_b.iter().enumerate() {
593            packed.push(a.min.x as f32);
594            packed.push(a.min.y as f32);
595            packed.push(a.min.z as f32);
596            packed.push(a.max.x as f32);
597            packed.push(a.max.y as f32);
598            packed.push(a.max.z as f32);
599            packed.push(b.min.x as f32);
600            packed.push(b.min.y as f32);
601            packed.push(b.min.z as f32);
602            packed.push(b.max.x as f32);
603            packed.push(b.max.y as f32);
604            packed.push(b.max.z as f32);
605            pair_indices.push((i as u32, j as u32));
606        }
607    }
608
609    // GPU filter (CPU simulation).
610    let pair_count = pair_indices.len();
611    let mut gpu_results: Vec<i32> = vec![0; pair_count];
612    for i in 0..pair_count {
613        gpu_results[i] = gpu_filter_aabb_overlap_f32(&packed[i * 12..(i + 1) * 12]);
614    }
615
616    // Merge: verify uncertain lanes with CPU exact oracle.
617    let mut merged: Vec<i32> = vec![0; pair_count];
618    merge_aabb_overlap_results(&packed, &gpu_results, &mut merged)?;
619
620    // Collect verified overlapping pairs in deterministic (a, b) order.
621    let mut count = 0usize;
622    for i in 0..pair_count {
623        if merged[i] == GPU_OVERLAP_YES {
624            out_pairs[count] = BoxPair {
625                a: pair_indices[i].0,
626                b: pair_indices[i].1,
627            };
628            count += 1;
629        }
630    }
631
632    // Sort for deterministic output.
633    out_pairs[..count].sort_unstable();
634    Ok(count)
635}
636
637/// Map a `Sign` to `i8` matching the GPU's encoding: +1 / 0 / -1.
638#[inline]
639fn sign_to_i8(s: Sign) -> i8 {
640    match s {
641        Sign::Positive => 1,
642        Sign::Zero => 0,
643        Sign::Negative => -1,
644    }
645}
646
647/// GPU filter result for a single orient3d quadruple (f32).
648/// Returns +1 / -1 / `GPU_ORIENTATION_UNCERTAIN`.
649///
650/// This is the CPU-side simulation of the GPU filtered stage — used by the
651/// differential test to verify that GPU-certain lanes match the CPU exact
652/// ladder and GPU-uncertain lanes are flagged.
653pub fn gpu_filter_orient3d_f32(quad: &[f32]) -> i32 {
654    let ax = quad[0] as f64;
655    let ay = quad[1] as f64;
656    let az = quad[2] as f64;
657    let bx = quad[3] as f64;
658    let by = quad[4] as f64;
659    let bz = quad[5] as f64;
660    let cx = quad[6] as f64;
661    let cy = quad[7] as f64;
662    let cz = quad[8] as f64;
663    let dx = quad[9] as f64;
664    let dy = quad[10] as f64;
665    let dz = quad[11] as f64;
666
667    let abx = bx - ax;
668    let aby = by - ay;
669    let abz = bz - az;
670    let acx = cx - ax;
671    let acy = cy - ay;
672    let acz = cz - az;
673    let adx = dx - ax;
674    let ady = dy - ay;
675    let adz = dz - az;
676
677    let cx0 = acy * adz - acz * ady;
678    let cy0 = acz * adx - acx * adz;
679    let cz0 = acx * ady - acy * adx;
680    let det = abx * cx0 + aby * cy0 + abz * cz0;
681
682    let perm = abx.abs() * (acy.abs() * adz.abs() + acz.abs() * ady.abs())
683        + aby.abs() * (acx.abs() * adz.abs() + acz.abs() * adx.abs())
684        + abz.abs() * (acx.abs() * ady.abs() + acy.abs() * adx.abs());
685    // f32 epsilon ≈ 1.19e-7, but we use a slightly larger bound for safety
686    let error_bound = perm * 1.5e-5;
687
688    if det.abs() <= error_bound {
689        GPU_ORIENTATION_UNCERTAIN
690    } else if det > 0.0 {
691        1
692    } else {
693        -1
694    }
695}
696
697/// GPU filter result for a single incircle quadruple (f32).
698pub fn gpu_filter_incircle_f32(quad: &[f32]) -> i32 {
699    let ax = quad[0] as f64;
700    let ay = quad[1] as f64;
701    let bx = quad[2] as f64;
702    let by = quad[3] as f64;
703    let cx = quad[4] as f64;
704    let cy = quad[5] as f64;
705    let dx = quad[6] as f64;
706    let dy = quad[7] as f64;
707
708    let adx = ax - dx;
709    let ady = ay - dy;
710    let bdx = bx - dx;
711    let bdy = by - dy;
712    let cdx = cx - dx;
713    let cdy = cy - dy;
714    let ad2 = adx * adx + ady * ady;
715    let bd2 = bdx * bdx + bdy * bdy;
716    let cd2 = cdx * cdx + cdy * cdy;
717
718    let m1 = bdy * cd2 - cdy * bd2;
719    let m2 = bdx * cd2 - cdx * bd2;
720    let m3 = bdx * cdy - bdy * cdx;
721    let det = adx * m1 - ady * m2 + ad2 * m3;
722
723    let perm = adx.abs() * (bdy.abs() * cd2.abs() + cdy.abs() * bd2.abs())
724        + ady.abs() * (bdx.abs() * cd2.abs() + cdx.abs() * bd2.abs())
725        + ad2.abs() * (bdx.abs() * cdy.abs() + bdy.abs() * cdx.abs());
726    let error_bound = perm * 3.0e-5;
727
728    if det.abs() <= error_bound {
729        GPU_ORIENTATION_UNCERTAIN
730    } else if det > 0.0 {
731        1
732    } else {
733        -1
734    }
735}
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740
741    #[test]
742    fn batch_oracle_matches_scalar_predicates() {
743        let packed = [
744            0.0, 0.0, 1.0, 0.0, 1.0, 1.0, // CCW
745            0.0, 0.0, 1.0, 0.0, 1.0, -1.0, // CW
746            0.0, 0.0, 1.0, 0.0, 2.0, 0.0, // collinear
747        ];
748        let mut out = [9i8; 3];
749        assert_eq!(
750            evaluate_orientation_batch_f32(&packed, &mut out).unwrap(),
751            3
752        );
753        assert_eq!(
754            out,
755            [
756                super::super::Orientation::CounterClockwise as i8,
757                super::super::Orientation::Clockwise as i8,
758                super::super::Orientation::Collinear as i8,
759            ]
760        );
761    }
762
763    #[test]
764    fn shader_generation_is_typed_and_deterministic() {
765        let schedule = GeometryGpuSchedule::default();
766        let a = emit_geometry_wgsl(GeometryGpuKernel::Orientation2F32, schedule).unwrap();
767        let b = emit_geometry_wgsl(GeometryGpuKernel::Orientation2F32, schedule).unwrap();
768        assert_eq!(a, b);
769        assert!(a.contains("@workgroup_size(128, 1, 1)"));
770        assert!(a.contains("orientation[i] = 2"));
771    }
772
773    #[cfg(feature = "wgsl-forge")]
774    #[test]
775    fn shader_passes_naga_validation() {
776        let source = emit_geometry_wgsl(
777            GeometryGpuKernel::Orientation2F32,
778            GeometryGpuSchedule::default(),
779        )
780        .unwrap();
781        let report = crate::wgsl_forge::validate_wgsl(&source).unwrap();
782        assert_eq!(report.entry_points, vec!["orientation_2_f32"]);
783    }
784
785    // ── P1.9: orient3d GPU kernel + oracle ────────────────────────────────
786
787    #[test]
788    fn orient3d_batch_oracle_matches_scalar() {
789        // Positive, negative, coplanar tetrahedra
790        let packed = [
791            // Positive: (0,0,0),(1,0,0),(0,1,0),(0,0,1)
792            0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0,
793            // Negative: swap b and c
794            0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
795            // Coplanar: all z=0
796            0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0,
797        ];
798        let mut out = [9i8; 3];
799        assert_eq!(evaluate_orient3d_batch_f32(&packed, &mut out).unwrap(), 3);
800        assert_eq!(out, [1, -1, 0]);
801    }
802
803    #[test]
804    fn orient3d_shader_generation_is_deterministic() {
805        let schedule = GeometryGpuSchedule::default();
806        let a = emit_geometry_wgsl(GeometryGpuKernel::Orient3dF32, schedule).unwrap();
807        let b = emit_geometry_wgsl(GeometryGpuKernel::Orient3dF32, schedule).unwrap();
808        assert_eq!(a, b);
809        assert!(a.contains("orient_3d_f32"));
810        assert!(a.contains("@workgroup_size(128, 1, 1)"));
811    }
812
813    #[cfg(feature = "wgsl-forge")]
814    #[test]
815    fn orient3d_shader_passes_naga_validation() {
816        let source = emit_geometry_wgsl(
817            GeometryGpuKernel::Orient3dF32,
818            GeometryGpuSchedule::default(),
819        )
820        .unwrap();
821        let report = crate::wgsl_forge::validate_wgsl(&source).unwrap();
822        assert_eq!(report.entry_points, vec!["orient_3d_f32"]);
823    }
824
825    #[test]
826    fn orient3d_gpu_certain_lanes_match_cpu_exact() {
827        // Clear cases where the GPU filter should be certain
828        let packed = [
829            // Positive tetrahedron (clear)
830            0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0,
831            // Negative tetrahedron (clear)
832            0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
833        ];
834        let mut cpu_out = [9i8; 2];
835        evaluate_orient3d_batch_f32(&packed, &mut cpu_out).unwrap();
836
837        for (i, quad) in packed.chunks_exact(12).enumerate() {
838            let gpu = gpu_filter_orient3d_f32(quad);
839            // GPU-certain lanes must match CPU exact
840            assert_ne!(gpu, GPU_ORIENTATION_UNCERTAIN, "lane {i} should be certain");
841            assert_eq!(
842                gpu as i8, cpu_out[i],
843                "lane {i}: GPU={gpu}, CPU={}",
844                cpu_out[i]
845            );
846        }
847    }
848
849    #[test]
850    fn orient3d_gpu_uncertain_lanes_flagged_near_degeneracy() {
851        // Coplanar case — exact zero determinant. GPU filter flags uncertain
852        // (|det| = 0 <= error_bound = 0).
853        let packed = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0];
854        let gpu = gpu_filter_orient3d_f32(&packed);
855        assert_eq!(
856            gpu, GPU_ORIENTATION_UNCERTAIN,
857            "coplanar should be uncertain"
858        );
859
860        // CPU oracle resolves it exactly (Zero)
861        let mut cpu_out = [9i8; 1];
862        evaluate_orient3d_batch_f32(&packed, &mut cpu_out).unwrap();
863        assert_eq!(cpu_out[0], 0);
864    }
865
866    // ── P1.9: incircle GPU kernel + oracle ────────────────────────────────
867
868    #[test]
869    fn incircle_batch_oracle_matches_scalar() {
870        // Inside, outside, on (unit circle, CCW)
871        let packed = [
872            // a=(1,0), b=(0,1), c=(-1,0), d=(0,0) → inside
873            1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0,
874            // a=(1,0), b=(0,1), c=(-1,0), d=(2,0) → outside
875            1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 2.0, 0.0,
876            // a=(1,0), b=(0,1), c=(-1,0), d=(0,-1) → on
877            1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, -1.0,
878        ];
879        let mut out = [9i8; 3];
880        assert_eq!(evaluate_incircle_batch_f32(&packed, &mut out).unwrap(), 3);
881        assert_eq!(out, [1, -1, 0]);
882    }
883
884    #[test]
885    fn incircle_shader_generation_is_deterministic() {
886        let schedule = GeometryGpuSchedule::default();
887        let a = emit_geometry_wgsl(GeometryGpuKernel::IncircleF32, schedule).unwrap();
888        let b = emit_geometry_wgsl(GeometryGpuKernel::IncircleF32, schedule).unwrap();
889        assert_eq!(a, b);
890        assert!(a.contains("incircle_f32"));
891        assert!(a.contains("@workgroup_size(128, 1, 1)"));
892    }
893
894    #[cfg(feature = "wgsl-forge")]
895    #[test]
896    fn incircle_shader_passes_naga_validation() {
897        let source = emit_geometry_wgsl(
898            GeometryGpuKernel::IncircleF32,
899            GeometryGpuSchedule::default(),
900        )
901        .unwrap();
902        let report = crate::wgsl_forge::validate_wgsl(&source).unwrap();
903        assert_eq!(report.entry_points, vec!["incircle_f32"]);
904    }
905
906    #[test]
907    fn incircle_gpu_certain_lanes_match_cpu_exact() {
908        let packed = [
909            // Inside (clear)
910            1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0, // Outside (clear)
911            1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 2.0, 0.0,
912        ];
913        let mut cpu_out = [9i8; 2];
914        evaluate_incircle_batch_f32(&packed, &mut cpu_out).unwrap();
915
916        for (i, quad) in packed.chunks_exact(8).enumerate() {
917            let gpu = gpu_filter_incircle_f32(quad);
918            assert_ne!(gpu, GPU_ORIENTATION_UNCERTAIN, "lane {i} should be certain");
919            assert_eq!(
920                gpu as i8, cpu_out[i],
921                "lane {i}: GPU={gpu}, CPU={}",
922                cpu_out[i]
923            );
924        }
925    }
926
927    #[test]
928    fn incircle_gpu_uncertain_lanes_flagged_near_degeneracy() {
929        // Cocircular case — exact zero determinant. GPU filter flags uncertain.
930        let packed = [1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, -1.0];
931        let gpu = gpu_filter_incircle_f32(&packed);
932        assert_eq!(
933            gpu, GPU_ORIENTATION_UNCERTAIN,
934            "cocircular should be uncertain"
935        );
936
937        let mut cpu_out = [9i8; 1];
938        evaluate_incircle_batch_f32(&packed, &mut cpu_out).unwrap();
939        // CPU resolves exactly (Zero — on circle)
940        assert_eq!(cpu_out[0], 0);
941    }
942
943    // ── P1.9: CPU/GPU differential over the determinism corpus ────────────
944
945    #[test]
946    fn cpu_gpu_differential_orient3d_over_corpus() {
947        // A set of orient3d cases covering clear, degenerate, and near-degenerate
948        let packed: [f32; 36] = [
949            // Clear positive
950            0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, // Clear negative
951            0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
952            // Coplanar (exact zero)
953            0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0,
954        ];
955
956        let mut cpu_out = [9i8; 3];
957        evaluate_orient3d_batch_f32(&packed, &mut cpu_out).unwrap();
958
959        for (i, quad) in packed.chunks_exact(12).enumerate() {
960            let gpu = gpu_filter_orient3d_f32(quad);
961            if gpu != GPU_ORIENTATION_UNCERTAIN {
962                // GPU-certain lane must match CPU exact
963                assert_eq!(
964                    gpu as i8, cpu_out[i],
965                    "GPU-certain lane {i} disagrees with CPU exact: GPU={gpu}, CPU={}",
966                    cpu_out[i]
967                );
968            }
969            // GPU-uncertain lanes are correctly flagged — CPU resolves them
970        }
971    }
972
973    #[test]
974    fn cpu_gpu_differential_incircle_over_corpus() {
975        let packed: [f32; 24] = [
976            // Inside (clear)
977            1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0, // Outside (clear)
978            1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 2.0, 0.0, // On circle (exact zero)
979            1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, -1.0,
980        ];
981
982        let mut cpu_out = [9i8; 3];
983        evaluate_incircle_batch_f32(&packed, &mut cpu_out).unwrap();
984
985        for (i, quad) in packed.chunks_exact(8).enumerate() {
986            let gpu = gpu_filter_incircle_f32(quad);
987            if gpu != GPU_ORIENTATION_UNCERTAIN {
988                assert_eq!(
989                    gpu as i8, cpu_out[i],
990                    "GPU-certain lane {i} disagrees with CPU exact: GPU={gpu}, CPU={}",
991                    cpu_out[i]
992                );
993            }
994        }
995    }
996
997    #[test]
998    fn orient3d_batch_rejects_wrong_input_length() {
999        let packed = [0.0f32; 11]; // not multiple of 12
1000        let mut out = [0i8; 1];
1001        assert_eq!(
1002            evaluate_orient3d_batch_f32(&packed, &mut out),
1003            Err(GeometryGpuError::InputLengthNotMultipleOfTwelve)
1004        );
1005    }
1006
1007    #[test]
1008    fn incircle_batch_rejects_wrong_input_length() {
1009        let packed = [0.0f32; 7]; // not multiple of 8
1010        let mut out = [0i8; 1];
1011        assert_eq!(
1012            evaluate_incircle_batch_f32(&packed, &mut out),
1013            Err(GeometryGpuError::InputLengthNotMultipleOfEight)
1014        );
1015    }
1016
1017    #[test]
1018    fn orient3d_batch_rejects_small_output() {
1019        let packed = [0.0f32; 12];
1020        let mut out = [0i8; 0]; // too small
1021        assert_eq!(
1022            evaluate_orient3d_batch_f32(&packed, &mut out),
1023            Err(GeometryGpuError::OutputTooSmall { required: 1 })
1024        );
1025    }
1026
1027    // ── P3.6: AABB overlap GPU kernel + merge ─────────────────────────────
1028
1029    #[test]
1030    fn aabb_overlap_shader_generation_is_deterministic() {
1031        let schedule = GeometryGpuSchedule::default();
1032        let a = emit_geometry_wgsl(GeometryGpuKernel::AabbOverlapF32, schedule).unwrap();
1033        let b = emit_geometry_wgsl(GeometryGpuKernel::AabbOverlapF32, schedule).unwrap();
1034        assert_eq!(a, b);
1035        assert!(a.contains("aabb_overlap_f32"));
1036        assert!(a.contains("@workgroup_size(128, 1, 1)"));
1037    }
1038
1039    #[test]
1040    fn aabb_overlap_cpu_oracle_matches_exact() {
1041        // Clear overlap, clear non-overlap, boundary-touching.
1042        let packed: [f32; 36] = [
1043            // Overlapping: [0,0,0]-[2,2,2] vs [1,1,1]-[3,3,3]
1044            0.0, 0.0, 0.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0,
1045            // Non-overlapping: [0,0,0]-[1,1,1] vs [5,5,5]-[6,6,6]
1046            0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 6.0, 6.0, 6.0,
1047            // Boundary-touching: [0,0,0]-[1,1,1] vs [1,0,0]-[2,1,1]
1048            0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 2.0, 1.0, 1.0,
1049        ];
1050        let mut out = [0i32; 3];
1051        evaluate_aabb_overlap_batch_f32(&packed, &mut out).unwrap();
1052        assert_eq!(out, [GPU_OVERLAP_YES, GPU_OVERLAP_NO, GPU_OVERLAP_YES]);
1053    }
1054
1055    #[test]
1056    fn aabb_overlap_gpu_certain_lanes_match_cpu() {
1057        let packed: [f32; 24] = [
1058            // Clear overlap
1059            0.0, 0.0, 0.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0,
1060            // Clear non-overlap
1061            0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 6.0, 6.0, 6.0,
1062        ];
1063        let mut cpu_out = [0i32; 2];
1064        evaluate_aabb_overlap_batch_f32(&packed, &mut cpu_out).unwrap();
1065        for (i, pair) in packed.chunks_exact(12).enumerate() {
1066            let gpu = gpu_filter_aabb_overlap_f32(pair);
1067            assert_ne!(gpu, GPU_OVERLAP_UNCERTAIN, "lane {i} should be certain");
1068            assert_eq!(gpu, cpu_out[i], "lane {i}: GPU={gpu}, CPU={}", cpu_out[i]);
1069        }
1070    }
1071
1072    #[test]
1073    fn aabb_overlap_merge_produces_identical_results() {
1074        // The merge guarantee: GPU-on vs GPU-off produce identical results.
1075        let packed: [f32; 36] = [
1076            // Clear overlap
1077            0.0, 0.0, 0.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0,
1078            // Clear non-overlap
1079            0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 6.0, 6.0, 6.0,
1080            // Boundary-touching (may be uncertain)
1081            0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 2.0, 1.0, 1.0,
1082        ];
1083
1084        // Path 1: GPU filter + merge.
1085        let mut gpu_results = [0i32; 3];
1086        for i in 0..3 {
1087            gpu_results[i] = gpu_filter_aabb_overlap_f32(&packed[i * 12..(i + 1) * 12]);
1088        }
1089        let mut merged = [0i32; 3];
1090        merge_aabb_overlap_results(&packed, &gpu_results, &mut merged).unwrap();
1091
1092        // Path 2: CPU exact only (no GPU).
1093        let mut cpu_only = [0i32; 3];
1094        evaluate_aabb_overlap_batch_f32(&packed, &mut cpu_only).unwrap();
1095
1096        // Merge guarantee: identical results.
1097        assert_eq!(merged, cpu_only);
1098    }
1099
1100    #[test]
1101    fn gpu_candidate_box_join_matches_brute_force() {
1102        let boxes_a: Vec<Aabb> = vec![
1103            Aabb::new(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 2.0, 2.0)),
1104            Aabb::new(Point3::new(3.0, 3.0, 3.0), Point3::new(5.0, 5.0, 5.0)),
1105            Aabb::new(Point3::new(1.0, 1.0, 1.0), Point3::new(4.0, 4.0, 4.0)),
1106        ];
1107        let boxes_b: Vec<Aabb> = vec![
1108            Aabb::new(Point3::new(1.5, 1.5, 1.5), Point3::new(3.5, 3.5, 3.5)),
1109            Aabb::new(Point3::new(10.0, 10.0, 10.0), Point3::new(11.0, 11.0, 11.0)),
1110        ];
1111
1112        let max_pairs = boxes_a.len() * boxes_b.len();
1113        let mut gpu_out = vec![BoxPair { a: 0, b: 0 }; max_pairs];
1114        let gpu_count = gpu_candidate_box_join(&boxes_a, &boxes_b, &mut gpu_out).unwrap();
1115
1116        let mut brute_out = vec![BoxPair { a: 0, b: 0 }; max_pairs];
1117        let brute_count =
1118            super::super::box_join::box_join_brute_force(&boxes_a, &boxes_b, &mut brute_out)
1119                .unwrap();
1120
1121        assert_eq!(gpu_count, brute_count);
1122        let mut gpu_sorted = gpu_out[..gpu_count].to_vec();
1123        gpu_sorted.sort_unstable();
1124        let mut brute_sorted = brute_out[..brute_count].to_vec();
1125        brute_sorted.sort_unstable();
1126        assert_eq!(gpu_sorted, brute_sorted);
1127    }
1128
1129    #[test]
1130    fn gpu_candidate_box_join_deterministic() {
1131        let boxes_a: Vec<Aabb> = vec![
1132            Aabb::new(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 2.0, 2.0)),
1133            Aabb::new(Point3::new(1.0, 1.0, 1.0), Point3::new(3.0, 3.0, 3.0)),
1134        ];
1135        let boxes_b: Vec<Aabb> = vec![Aabb::new(
1136            Point3::new(1.5, 0.0, 0.0),
1137            Point3::new(2.5, 1.0, 1.0),
1138        )];
1139
1140        let run = || {
1141            let mut out = vec![BoxPair { a: 0, b: 0 }; boxes_a.len() * boxes_b.len()];
1142            let count = gpu_candidate_box_join(&boxes_a, &boxes_b, &mut out).unwrap();
1143            (count, out)
1144        };
1145
1146        let (c1, o1) = run();
1147        let (c2, o2) = run();
1148        assert_eq!(c1, c2);
1149        assert_eq!(o1[..c1], o2[..c2]);
1150    }
1151
1152    #[test]
1153    fn point_aabb_dist_sq_shader_generation_is_deterministic() {
1154        let schedule = GeometryGpuSchedule::default();
1155        let a = emit_geometry_wgsl(GeometryGpuKernel::PointAabbDistSqF32, schedule).unwrap();
1156        let b = emit_geometry_wgsl(GeometryGpuKernel::PointAabbDistSqF32, schedule).unwrap();
1157        assert_eq!(a, b);
1158        assert!(a.contains("point_aabb_dist_sq_f32"));
1159    }
1160}