1use std::cmp::Ordering;
35use threecrate_core::{NearestNeighborSearch, Point3f};
36
37#[derive(Debug, Clone)]
51pub struct SoaPoints {
52 xs: Vec<f32>,
53 ys: Vec<f32>,
54 zs: Vec<f32>,
55}
56
57impl SoaPoints {
58 pub fn from_points(points: &[Point3f]) -> Self {
60 let mut xs = Vec::with_capacity(points.len());
61 let mut ys = Vec::with_capacity(points.len());
62 let mut zs = Vec::with_capacity(points.len());
63 for p in points {
64 xs.push(p.x);
65 ys.push(p.y);
66 zs.push(p.z);
67 }
68 Self { xs, ys, zs }
69 }
70
71 #[inline]
73 pub fn len(&self) -> usize {
74 self.xs.len()
75 }
76
77 #[inline]
79 pub fn is_empty(&self) -> bool {
80 self.xs.is_empty()
81 }
82
83 #[inline]
85 pub fn xs(&self) -> &[f32] {
86 &self.xs
87 }
88
89 #[inline]
91 pub fn ys(&self) -> &[f32] {
92 &self.ys
93 }
94
95 #[inline]
97 pub fn zs(&self) -> &[f32] {
98 &self.zs
99 }
100}
101
102pub fn batch_distances_squared(query: &Point3f, pts: &SoaPoints, out: &mut [f32]) {
111 debug_assert_eq!(out.len(), pts.len());
112
113 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
114 {
115 if is_x86_feature_detected!("avx2") {
116 return unsafe { avx2_distances_squared(query, pts, out) };
118 }
119 if is_x86_feature_detected!("sse2") {
120 return unsafe { sse2_distances_squared(query, pts, out) };
121 }
122 }
123
124 scalar_distances_squared(query, pts, out);
125}
126
127#[inline]
131pub fn scalar_distances_squared(query: &Point3f, pts: &SoaPoints, out: &mut [f32]) {
132 let (qx, qy, qz) = (query.x, query.y, query.z);
133 let n = pts.len();
134 let xs = pts.xs();
135 let ys = pts.ys();
136 let zs = pts.zs();
137 for i in 0..n {
138 let dx = xs[i] - qx;
139 let dy = ys[i] - qy;
140 let dz = zs[i] - qz;
141 out[i] = dx * dx + dy * dy + dz * dz;
142 }
143}
144
145#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
154#[target_feature(enable = "sse2")]
155unsafe fn sse2_distances_squared(query: &Point3f, pts: &SoaPoints, out: &mut [f32]) {
156 #[cfg(target_arch = "x86")]
157 use std::arch::x86::*;
158 #[cfg(target_arch = "x86_64")]
159 use std::arch::x86_64::*;
160
161 let n = pts.len();
162 let xs = pts.xs();
163 let ys = pts.ys();
164 let zs = pts.zs();
165
166 let qx_v = _mm_set1_ps(query.x);
167 let qy_v = _mm_set1_ps(query.y);
168 let qz_v = _mm_set1_ps(query.z);
169
170 let chunks = n / 4;
171 let remainder = n % 4;
172
173 for c in 0..chunks {
174 let base = c * 4;
175 let xs_v = _mm_loadu_ps(xs.as_ptr().add(base));
176 let ys_v = _mm_loadu_ps(ys.as_ptr().add(base));
177 let zs_v = _mm_loadu_ps(zs.as_ptr().add(base));
178
179 let dx = _mm_sub_ps(xs_v, qx_v);
180 let dy = _mm_sub_ps(ys_v, qy_v);
181 let dz = _mm_sub_ps(zs_v, qz_v);
182
183 let d2 = _mm_add_ps(
184 _mm_add_ps(_mm_mul_ps(dx, dx), _mm_mul_ps(dy, dy)),
185 _mm_mul_ps(dz, dz),
186 );
187
188 _mm_storeu_ps(out.as_mut_ptr().add(base), d2);
189 }
190
191 let rem_start = chunks * 4;
193 scalar_remainder(query, xs, ys, zs, out, rem_start, remainder);
194}
195
196#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
205#[target_feature(enable = "avx2")]
206unsafe fn avx2_distances_squared(query: &Point3f, pts: &SoaPoints, out: &mut [f32]) {
207 #[cfg(target_arch = "x86")]
208 use std::arch::x86::*;
209 #[cfg(target_arch = "x86_64")]
210 use std::arch::x86_64::*;
211
212 let n = pts.len();
213 let xs = pts.xs();
214 let ys = pts.ys();
215 let zs = pts.zs();
216
217 let qx_v = _mm256_set1_ps(query.x);
218 let qy_v = _mm256_set1_ps(query.y);
219 let qz_v = _mm256_set1_ps(query.z);
220
221 let chunks = n / 8;
222 let remainder_start = chunks * 8;
223 let remainder = n - remainder_start;
224
225 for c in 0..chunks {
226 let base = c * 8;
227 let xs_v = _mm256_loadu_ps(xs.as_ptr().add(base));
228 let ys_v = _mm256_loadu_ps(ys.as_ptr().add(base));
229 let zs_v = _mm256_loadu_ps(zs.as_ptr().add(base));
230
231 let dx = _mm256_sub_ps(xs_v, qx_v);
232 let dy = _mm256_sub_ps(ys_v, qy_v);
233 let dz = _mm256_sub_ps(zs_v, qz_v);
234
235 let d2 = _mm256_add_ps(
236 _mm256_add_ps(_mm256_mul_ps(dx, dx), _mm256_mul_ps(dy, dy)),
237 _mm256_mul_ps(dz, dz),
238 );
239
240 _mm256_storeu_ps(out.as_mut_ptr().add(base), d2);
241 }
242
243 let mut rem = remainder;
245 let mut rem_base = remainder_start;
246
247 if rem >= 4 {
248 #[cfg(target_arch = "x86_64")]
250 use std::arch::x86_64::*;
251 let qx_s = _mm_set1_ps(query.x);
252 let qy_s = _mm_set1_ps(query.y);
253 let qz_s = _mm_set1_ps(query.z);
254
255 let xs_v = _mm_loadu_ps(xs.as_ptr().add(rem_base));
256 let ys_v = _mm_loadu_ps(ys.as_ptr().add(rem_base));
257 let zs_v = _mm_loadu_ps(zs.as_ptr().add(rem_base));
258
259 let dx = _mm_sub_ps(xs_v, qx_s);
260 let dy = _mm_sub_ps(ys_v, qy_s);
261 let dz = _mm_sub_ps(zs_v, qz_s);
262
263 let d2 = _mm_add_ps(
264 _mm_add_ps(_mm_mul_ps(dx, dx), _mm_mul_ps(dy, dy)),
265 _mm_mul_ps(dz, dz),
266 );
267 _mm_storeu_ps(out.as_mut_ptr().add(rem_base), d2);
268
269 rem_base += 4;
270 rem -= 4;
271 }
272
273 scalar_remainder(query, xs, ys, zs, out, rem_base, rem);
274}
275
276#[cfg_attr(
278 not(any(target_arch = "x86", target_arch = "x86_64")),
279 allow(dead_code)
280)]
281#[inline(always)]
282fn scalar_remainder(
283 query: &Point3f,
284 xs: &[f32],
285 ys: &[f32],
286 zs: &[f32],
287 out: &mut [f32],
288 start: usize,
289 count: usize,
290) {
291 let (qx, qy, qz) = (query.x, query.y, query.z);
292 for i in 0..count {
293 let idx = start + i;
294 let dx = xs[idx] - qx;
295 let dy = ys[idx] - qy;
296 let dz = zs[idx] - qz;
297 out[idx] = dx * dx + dy * dy + dz * dz;
298 }
299}
300
301pub struct SimdBruteForceSearch {
326 soa: SoaPoints,
327}
328
329impl SimdBruteForceSearch {
330 pub fn new(points: &[Point3f]) -> Self {
332 Self {
333 soa: SoaPoints::from_points(points),
334 }
335 }
336
337 pub fn len(&self) -> usize {
339 self.soa.len()
340 }
341
342 pub fn is_empty(&self) -> bool {
344 self.soa.is_empty()
345 }
346
347 pub fn soa(&self) -> &SoaPoints {
349 &self.soa
350 }
351}
352
353impl NearestNeighborSearch for SimdBruteForceSearch {
354 fn find_k_nearest(&self, query: &Point3f, k: usize) -> Vec<(usize, f32)> {
355 if k == 0 || self.soa.is_empty() {
356 return Vec::new();
357 }
358
359 let n = self.soa.len();
360 let k = k.min(n);
361
362 let mut dist_sq = vec![0.0f32; n];
364 batch_distances_squared(query, &self.soa, &mut dist_sq);
365
366 let mut heap: std::collections::BinaryHeap<DistEntry> =
368 std::collections::BinaryHeap::with_capacity(k + 1);
369
370 for (idx, &d2) in dist_sq.iter().enumerate() {
371 if heap.len() < k {
372 heap.push(DistEntry {
373 dist_sq: d2,
374 index: idx,
375 });
376 } else if let Some(farthest) = heap.peek() {
377 if d2 < farthest.dist_sq {
378 heap.pop();
379 heap.push(DistEntry {
380 dist_sq: d2,
381 index: idx,
382 });
383 }
384 }
385 }
386
387 let mut result: Vec<(usize, f32)> = heap
389 .into_iter()
390 .map(|e| (e.index, e.dist_sq.sqrt()))
391 .collect();
392 result.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
393 result
394 }
395
396 fn find_radius_neighbors(&self, query: &Point3f, radius: f32) -> Vec<(usize, f32)> {
397 if radius <= 0.0 || self.soa.is_empty() {
398 return Vec::new();
399 }
400
401 let n = self.soa.len();
402 let radius_sq = radius * radius;
403
404 let mut dist_sq = vec![0.0f32; n];
406 batch_distances_squared(query, &self.soa, &mut dist_sq);
407
408 let mut result: Vec<(usize, f32)> = dist_sq
410 .iter()
411 .enumerate()
412 .filter_map(|(idx, &d2)| {
413 if d2 <= radius_sq {
414 Some((idx, d2.sqrt()))
415 } else {
416 None
417 }
418 })
419 .collect();
420
421 result.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
422 result
423 }
424}
425
426#[derive(Debug, Clone, Copy, PartialEq)]
431struct DistEntry {
432 dist_sq: f32,
433 index: usize,
434}
435
436impl Eq for DistEntry {}
437
438impl PartialOrd for DistEntry {
439 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
440 Some(self.cmp(other))
441 }
442}
443
444impl Ord for DistEntry {
445 fn cmp(&self, other: &Self) -> Ordering {
446 self.dist_sq
449 .total_cmp(&other.dist_sq)
450 .then(self.index.cmp(&other.index))
451 }
452}
453
454#[cfg(test)]
459mod tests {
460 use super::*;
461 use threecrate_core::Point3f;
462
463 fn cube_points() -> Vec<Point3f> {
464 vec![
465 Point3f::new(0.0, 0.0, 0.0),
466 Point3f::new(1.0, 0.0, 0.0),
467 Point3f::new(0.0, 1.0, 0.0),
468 Point3f::new(0.0, 0.0, 1.0),
469 Point3f::new(1.0, 1.0, 0.0),
470 Point3f::new(1.0, 0.0, 1.0),
471 Point3f::new(0.0, 1.0, 1.0),
472 Point3f::new(1.0, 1.0, 1.0),
473 ]
474 }
475
476 #[test]
479 fn test_soa_layout() {
480 let pts = cube_points();
481 let soa = SoaPoints::from_points(&pts);
482 assert_eq!(soa.len(), pts.len());
483 for (i, p) in pts.iter().enumerate() {
484 assert_eq!(soa.xs()[i], p.x);
485 assert_eq!(soa.ys()[i], p.y);
486 assert_eq!(soa.zs()[i], p.z);
487 }
488 }
489
490 fn reference_dist_sq(query: &Point3f, pts: &[Point3f]) -> Vec<f32> {
493 pts.iter()
494 .map(|p| {
495 let dx = p.x - query.x;
496 let dy = p.y - query.y;
497 let dz = p.z - query.z;
498 dx * dx + dy * dy + dz * dz
499 })
500 .collect()
501 }
502
503 #[test]
504 fn test_scalar_distances_match_reference() {
505 let pts = cube_points();
506 let soa = SoaPoints::from_points(&pts);
507 let query = Point3f::new(0.5, 0.5, 0.5);
508 let reference = reference_dist_sq(&query, &pts);
509 let mut out = vec![0.0f32; pts.len()];
510 scalar_distances_squared(&query, &soa, &mut out);
511 for (got, expected) in out.iter().zip(reference.iter()) {
512 assert!(
513 (got - expected).abs() < 1e-6,
514 "got={got}, expected={expected}"
515 );
516 }
517 }
518
519 #[test]
520 fn test_batch_distances_match_scalar() {
521 let pts = cube_points();
522 let soa = SoaPoints::from_points(&pts);
523 let query = Point3f::new(0.3, 0.7, 0.2);
524
525 let mut scalar_out = vec![0.0f32; pts.len()];
526 scalar_distances_squared(&query, &soa, &mut scalar_out);
527
528 let mut simd_out = vec![0.0f32; pts.len()];
529 batch_distances_squared(&query, &soa, &mut simd_out);
530
531 for (got, expected) in simd_out.iter().zip(scalar_out.iter()) {
532 assert!(
533 (got - expected).abs() < 1e-5,
534 "SIMD={got}, scalar={expected}"
535 );
536 }
537 }
538
539 #[test]
542 fn test_batch_distances_various_sizes() {
543 for n in [1, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 100] {
544 let pts: Vec<Point3f> = (0..n)
545 .map(|i| Point3f::new(i as f32, (i * 2) as f32, (i * 3) as f32))
546 .collect();
547 let soa = SoaPoints::from_points(&pts);
548 let query = Point3f::new(5.0, 10.0, 15.0);
549 let reference = reference_dist_sq(&query, &pts);
550
551 let mut simd_out = vec![0.0f32; n];
552 batch_distances_squared(&query, &soa, &mut simd_out);
553
554 for (i, (got, expected)) in simd_out.iter().zip(reference.iter()).enumerate() {
555 assert!(
556 (got - expected).abs() < 1e-4,
557 "n={n} i={i}: SIMD={got}, ref={expected}"
558 );
559 }
560 }
561 }
562
563 #[test]
566 fn test_simd_knn_matches_brute_force() {
567 use crate::nearest_neighbor::BruteForceSearch;
568 let pts = cube_points();
569 let query = Point3f::new(0.5, 0.5, 0.5);
570 let k = 3;
571
572 let simd = SimdBruteForceSearch::new(&pts);
573 let scalar = BruteForceSearch::new(&pts);
574
575 let mut simd_res = simd.find_k_nearest(&query, k);
576 let mut scalar_res = scalar.find_k_nearest(&query, k);
577
578 simd_res.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().then(a.0.cmp(&b.0)));
579 scalar_res.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().then(a.0.cmp(&b.0)));
580
581 assert_eq!(simd_res.len(), k);
582 for ((si, sd), (_, bd)) in simd_res.iter().zip(scalar_res.iter()) {
583 assert!(
584 (sd - bd).abs() < 1e-5,
585 "dist mismatch: simd={sd} scalar={bd}"
586 );
587 let _ = si; }
589 }
590
591 #[test]
592 fn test_simd_radius_matches_brute_force() {
593 use crate::nearest_neighbor::BruteForceSearch;
594 let pts = cube_points();
595 let query = Point3f::new(0.5, 0.5, 0.5);
596 let radius = 1.0;
597
598 let simd = SimdBruteForceSearch::new(&pts);
599 let scalar = BruteForceSearch::new(&pts);
600
601 let mut simd_res = simd.find_radius_neighbors(&query, radius);
602 let mut scalar_res = scalar.find_radius_neighbors(&query, radius);
603
604 simd_res.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().then(a.0.cmp(&b.0)));
605 scalar_res.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().then(a.0.cmp(&b.0)));
606
607 assert_eq!(simd_res.len(), scalar_res.len(), "result count mismatch");
608 for ((_, sd), (_, bd)) in simd_res.iter().zip(scalar_res.iter()) {
609 assert!((sd - bd).abs() < 1e-5);
610 }
611 }
612
613 #[test]
614 fn test_empty_cloud() {
615 let simd = SimdBruteForceSearch::new(&[]);
616 let q = Point3f::new(0.0, 0.0, 0.0);
617 assert!(simd.find_k_nearest(&q, 5).is_empty());
618 assert!(simd.find_radius_neighbors(&q, 10.0).is_empty());
619 }
620
621 #[test]
622 fn test_k_larger_than_cloud() {
623 let pts = cube_points();
624 let simd = SimdBruteForceSearch::new(&pts);
625 let q = Point3f::new(0.0, 0.0, 0.0);
626 let result = simd.find_k_nearest(&q, 100);
627 assert_eq!(result.len(), pts.len());
628 }
629
630 #[test]
631 fn test_exact_origin_distance() {
632 let pts = vec![Point3f::new(3.0, 4.0, 0.0)]; let soa = SoaPoints::from_points(&pts);
634 let query = Point3f::origin();
635 let mut out = vec![0.0f32; 1];
636 batch_distances_squared(&query, &soa, &mut out);
637 assert!((out[0] - 25.0).abs() < 1e-6);
638 }
639}