1use 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
25pub const GPU_OVERLAP_YES: i32 = 1;
27pub const GPU_OVERLAP_NO: i32 = 0;
29pub const GPU_OVERLAP_UNCERTAIN: i32 = 2;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum GeometryGpuKernel {
34 Orientation2F32,
36 Orient3dF32,
39 IncircleF32,
42 AabbOverlapF32,
45 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 CandidateBufferTooSmall {
75 required: usize,
76 },
77}
78
79pub 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
372pub 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
394pub 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
422pub 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
450pub 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
483fn 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
517pub 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 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 out[i] = gpu_results[i];
558 }
559 }
560 Ok(count)
561}
562
563pub 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 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 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 let mut merged: Vec<i32> = vec![0; pair_count];
618 merge_aabb_overlap_results(&packed, &gpu_results, &mut merged)?;
619
620 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 out_pairs[..count].sort_unstable();
634 Ok(count)
635}
636
637#[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
647pub 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 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
697pub 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, 0.0, 0.0, 1.0, 0.0, 1.0, -1.0, 0.0, 0.0, 1.0, 0.0, 2.0, 0.0, ];
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 #[test]
788 fn orient3d_batch_oracle_matches_scalar() {
789 let packed = [
791 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 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 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 let packed = [
829 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 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 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 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 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 #[test]
869 fn incircle_batch_oracle_matches_scalar() {
870 let packed = [
872 1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0,
874 1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 2.0, 0.0,
876 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 1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0, 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 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 assert_eq!(cpu_out[0], 0);
941 }
942
943 #[test]
946 fn cpu_gpu_differential_orient3d_over_corpus() {
947 let packed: [f32; 36] = [
949 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 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 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 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 }
971 }
972
973 #[test]
974 fn cpu_gpu_differential_incircle_over_corpus() {
975 let packed: [f32; 24] = [
976 1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 2.0, 0.0, 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]; 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]; 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]; assert_eq!(
1022 evaluate_orient3d_batch_f32(&packed, &mut out),
1023 Err(GeometryGpuError::OutputTooSmall { required: 1 })
1024 );
1025 }
1026
1027 #[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 let packed: [f32; 36] = [
1043 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 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 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 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 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 let packed: [f32; 36] = [
1076 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 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 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 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 let mut cpu_only = [0i32; 3];
1094 evaluate_aabb_overlap_batch_f32(&packed, &mut cpu_only).unwrap();
1095
1096 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}