Skip to main content

threecrate_gpu/
nearest_neighbor.rs

1//! GPU-accelerated nearest neighbor search
2
3use crate::spatial::{vec4s, GpuKdTree, KD_TREE_WGSL, MAX_K};
4use crate::GpuContext;
5use bytemuck::{Pod, Zeroable};
6use threecrate_core::{Point3f, Result};
7
8/// Parameters for nearest neighbor search
9#[repr(C)]
10#[derive(Copy, Clone, Pod, Zeroable)]
11pub struct NearestNeighborParams {
12    pub num_points: u32,
13    pub k_neighbors: u32,
14    pub max_distance: f32,
15    pub _padding: u32,
16}
17
18/// GPU representation of a point for nearest neighbor search
19#[repr(C)]
20#[derive(Copy, Clone, Pod, Zeroable)]
21#[repr(align(16))]
22pub struct GpuPoint {
23    pub position: [f32; 3],
24    pub _padding: f32,
25}
26
27/// Result of nearest neighbor search
28#[repr(C)]
29#[derive(Copy, Clone, Pod, Zeroable)]
30pub struct NeighborResult {
31    pub index: u32,
32    pub distance: f32,
33    pub _padding: [u32; 2],
34}
35
36/// k-NN kernel: one invocation per query point, walking the GPU kd-tree.
37fn knn_shader() -> String {
38    format!(
39        "{}{}",
40        KD_TREE_WGSL,
41        r#"
42struct KnnParams {
43    num_queries: u32,
44    k: u32,
45    limit_sq: f32,
46    _pad: u32,
47}
48
49@group(0) @binding(1) var<storage, read> queries: array<vec4<f32>>;
50// k slots per query: (original point index, distance bits). Unused slots have
51// index NO_NODE.
52@group(0) @binding(2) var<storage, read_write> results: array<vec2<u32>>;
53@group(0) @binding(3) var<uniform> params: KnnParams;
54
55@compute @workgroup_size(64)
56fn knn(@builtin(global_invocation_id) gid: vec3<u32>) {
57    let q = gid.x;
58    if (q >= params.num_queries) {
59        return;
60    }
61    find_k_nearest(queries[q].xyz, params.k, params.limit_sq);
62    let base = q * params.k;
63    for (var i = 0u; i < params.k; i++) {
64        if (i < knn_count) {
65            results[base + i] = vec2<u32>(nodes[knn_node[i]].index, bitcast<u32>(sqrt(knn_dist[i])));
66        } else {
67            results[base + i] = vec2<u32>(NO_NODE, 0u);
68        }
69    }
70}
71"#
72    )
73}
74
75impl GpuContext {
76    /// GPU-accelerated k-nearest neighbor search.
77    ///
78    /// Builds a kd-tree over `points` once, uploads it, and answers every query
79    /// on the GPU by walking the tree. Returns, for each query, up to `k`
80    /// `(point index, distance)` pairs closer than `max_distance`, closest
81    /// first. `k` is clamped to 1..=32.
82    pub async fn find_k_nearest_neighbors(
83        &self,
84        points: &[Point3f],
85        query_points: &[Point3f],
86        k: usize,
87        max_distance: f32,
88    ) -> Result<Vec<Vec<(usize, f32)>>> {
89        if points.is_empty() || query_points.is_empty() {
90            return Ok(vec![Vec::new(); query_points.len()]);
91        }
92        let k = k.clamp(1, MAX_K);
93
94        let tree = GpuKdTree::new(self, points)?;
95        if tree.is_empty() {
96            return Ok(vec![Vec::new(); query_points.len()]);
97        }
98
99        let queries = self.create_buffer_init(
100            "kNN Queries",
101            &vec4s(&tree.frame.points_to_local(query_points)),
102            wgpu::BufferUsages::STORAGE,
103        );
104        let results = self.create_buffer(
105            "kNN Results",
106            (query_points.len() * k * std::mem::size_of::<[u32; 2]>()) as u64,
107            wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
108        );
109        let params = KnnParams {
110            num_queries: query_points.len() as u32,
111            k: k as u32,
112            // Negative means nothing matches, as with the old brute-force
113            // search (squaring it would turn it into a valid range).
114            limit_sq: if max_distance < 0.0 {
115                0.0
116            } else {
117                max_distance * max_distance
118            },
119            _pad: 0,
120        };
121        let params_buffer =
122            self.create_buffer_init("kNN Params", &[params], wgpu::BufferUsages::UNIFORM);
123
124        let pipeline = self.cached_pipeline(&self.pipelines.knn, "kNN", knn_shader, "knn");
125        let bind_group = self.create_bind_group(
126            "kNN",
127            &pipeline.get_bind_group_layout(0),
128            &[
129                wgpu::BindGroupEntry {
130                    binding: 0,
131                    resource: tree.nodes.as_entire_binding(),
132                },
133                wgpu::BindGroupEntry {
134                    binding: 1,
135                    resource: queries.as_entire_binding(),
136                },
137                wgpu::BindGroupEntry {
138                    binding: 2,
139                    resource: results.as_entire_binding(),
140                },
141                wgpu::BindGroupEntry {
142                    binding: 3,
143                    resource: params_buffer.as_entire_binding(),
144                },
145            ],
146        );
147
148        let mut encoder = self
149            .device
150            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("kNN") });
151        {
152            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
153                label: Some("kNN"),
154                timestamp_writes: None,
155            });
156            pass.set_pipeline(pipeline);
157            pass.set_bind_group(0, &bind_group, &[]);
158            pass.dispatch_workgroups(query_points.len().div_ceil(64) as u32, 1, 1);
159        }
160        self.queue.submit(std::iter::once(encoder.finish()));
161
162        let raw: Vec<[u32; 2]> = self.read_buffer(&results)?;
163        Ok(raw
164            .chunks_exact(k)
165            .map(|slots| {
166                slots
167                    .iter()
168                    .take_while(|slot| slot[0] != u32::MAX)
169                    .map(|slot| (slot[0] as usize, f32::from_bits(slot[1])))
170                    .collect()
171            })
172            .collect())
173    }
174}
175
176/// Uniform block of the k-NN kernel (`KnnParams` in WGSL).
177#[repr(C)]
178#[derive(Copy, Clone, Pod, Zeroable)]
179struct KnnParams {
180    num_queries: u32,
181    k: u32,
182    limit_sq: f32,
183    _pad: u32,
184}
185
186/// GPU-accelerated nearest neighbor search for single query point
187pub async fn gpu_find_k_nearest(
188    gpu_context: &GpuContext,
189    points: &[Point3f],
190    query: &Point3f,
191    k: usize,
192) -> Result<Vec<(usize, f32)>> {
193    let results = gpu_context
194        .find_k_nearest_neighbors(points, &[*query], k, f32::INFINITY)
195        .await?;
196    Ok(results.into_iter().next().unwrap_or_default())
197}
198
199/// GPU-accelerated nearest neighbor search for multiple query points
200pub async fn gpu_find_k_nearest_batch(
201    gpu_context: &GpuContext,
202    points: &[Point3f],
203    query_points: &[Point3f],
204    k: usize,
205) -> Result<Vec<Vec<(usize, f32)>>> {
206    gpu_context
207        .find_k_nearest_neighbors(points, query_points, k, f32::INFINITY)
208        .await
209}
210
211/// GPU-accelerated radius-based nearest neighbor search
212pub async fn gpu_find_radius_neighbors(
213    gpu_context: &GpuContext,
214    points: &[Point3f],
215    query: &Point3f,
216    radius: f32,
217) -> Result<Vec<(usize, f32)>> {
218    let results = gpu_context
219        .find_k_nearest_neighbors(points, &[*query], 32, radius)
220        .await?;
221    Ok(results.into_iter().next().unwrap_or_default())
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use crate::device::GpuContext;
228    use approx::assert_relative_eq;
229    use threecrate_core::Point3f;
230
231    /// Try to create a GPU context, return None if not available
232    async fn try_create_gpu_context() -> Option<GpuContext> {
233        match GpuContext::new().await {
234            Ok(gpu) => Some(gpu),
235            Err(_) => {
236                println!("⚠️  GPU not available, skipping GPU-dependent test");
237                None
238            }
239        }
240    }
241
242    /// Create a simple test point cloud
243    fn create_test_points() -> Vec<Point3f> {
244        vec![
245            Point3f::new(0.0, 0.0, 0.0),
246            Point3f::new(1.0, 0.0, 0.0),
247            Point3f::new(0.0, 1.0, 0.0),
248            Point3f::new(0.0, 0.0, 1.0),
249            Point3f::new(1.0, 1.0, 1.0),
250        ]
251    }
252
253    #[test]
254    fn test_gpu_nearest_neighbor_single() {
255        pollster::block_on(async {
256            let Some(gpu) = try_create_gpu_context().await else {
257                return;
258            };
259
260            let points = create_test_points();
261            let query = Point3f::new(0.1, 0.1, 0.1);
262
263            let neighbors = gpu_find_k_nearest(&gpu, &points, &query, 3).await.unwrap();
264
265            assert_eq!(neighbors.len(), 3);
266            assert_eq!(neighbors[0].0, 0); // Closest should be origin
267            assert!(neighbors[0].1 < 0.2); // Distance should be small
268
269            println!("✓ GPU single nearest neighbor test passed");
270        });
271    }
272
273    #[test]
274    fn test_gpu_nearest_neighbor_batch() {
275        pollster::block_on(async {
276            let Some(gpu) = try_create_gpu_context().await else {
277                return;
278            };
279
280            let points = create_test_points();
281            let queries = vec![Point3f::new(0.1, 0.1, 0.1), Point3f::new(0.9, 0.1, 0.1)];
282
283            let results = gpu_find_k_nearest_batch(&gpu, &points, &queries, 2)
284                .await
285                .unwrap();
286
287            assert_eq!(results.len(), 2);
288            assert_eq!(results[0].len(), 2);
289            assert_eq!(results[1].len(), 2);
290
291            // First query should find origin as closest
292            assert_eq!(results[0][0].0, 0);
293
294            // Second query should find (1,0,0) as closest
295            assert_eq!(results[1][0].0, 1);
296
297            println!("✓ GPU batch nearest neighbor test passed");
298        });
299    }
300
301    #[test]
302    fn test_gpu_radius_neighbors() {
303        pollster::block_on(async {
304            let Some(gpu) = try_create_gpu_context().await else {
305                return;
306            };
307
308            let points = create_test_points();
309            let query = Point3f::new(0.0, 0.0, 0.0);
310            let radius = 1.5;
311
312            let neighbors = gpu_find_radius_neighbors(&gpu, &points, &query, radius)
313                .await
314                .unwrap();
315
316            // Should find points within radius
317            assert!(!neighbors.is_empty());
318
319            // All distances should be within radius
320            for (_, distance) in &neighbors {
321                assert!(*distance <= radius);
322            }
323
324            println!(
325                "✓ GPU radius neighbors test passed: {} neighbors found",
326                neighbors.len()
327            );
328        });
329    }
330
331    #[test]
332    fn test_gpu_knn_matches_cpu_kd_tree() {
333        use threecrate_algorithms::KdTree;
334        use threecrate_core::NearestNeighborSearch;
335
336        pollster::block_on(async {
337            let Some(gpu) = try_create_gpu_context().await else {
338                return;
339            };
340
341            // Deterministic pseudo-random cloud, offset from the origin
342            let mut state = 12345u32;
343            let mut next = || {
344                state = state.wrapping_mul(1664525).wrapping_add(1013904223);
345                (state >> 8) as f32 / (1u32 << 24) as f32
346            };
347            let points: Vec<Point3f> = (0..20_000)
348                .map(|_| Point3f::new(100.0 + 20.0 * next(), -50.0 + 20.0 * next(), 5.0 * next()))
349                .collect();
350            let queries = &points[..2_000];
351            let k = 10;
352
353            let gpu_results = gpu_find_k_nearest_batch(&gpu, &points, queries, k)
354                .await
355                .unwrap();
356            let tree = KdTree::new(&points).unwrap();
357
358            for (query, gpu_neighbors) in queries.iter().zip(&gpu_results) {
359                let cpu_neighbors = tree.find_k_nearest(query, k);
360                assert_eq!(gpu_neighbors.len(), k);
361                for (g, c) in gpu_neighbors.iter().zip(&cpu_neighbors) {
362                    assert!((g.1 - c.1).abs() < 1e-3, "gpu {:?} vs cpu {:?}", g, c);
363                }
364            }
365        });
366    }
367
368    #[test]
369    fn test_gpu_negative_max_distance_matches_nothing() {
370        pollster::block_on(async {
371            let Some(gpu) = try_create_gpu_context().await else {
372                return;
373            };
374            let points = create_test_points();
375            let results = gpu
376                .find_k_nearest_neighbors(&points, &[Point3f::origin()], 3, -1.0)
377                .await
378                .unwrap();
379            assert!(results[0].is_empty());
380        });
381    }
382
383    #[test]
384    fn test_gpu_nearest_neighbor_accuracy() {
385        pollster::block_on(async {
386            let Some(gpu) = try_create_gpu_context().await else {
387                return;
388            };
389
390            let points = create_test_points();
391            let query = Point3f::new(0.5, 0.5, 0.5);
392
393            let neighbors = gpu_find_k_nearest(&gpu, &points, &query, 1).await.unwrap();
394
395            assert_eq!(neighbors.len(), 1);
396
397            // Manually verify the nearest neighbor
398            let min_dist = points
399                .iter()
400                .map(|p| (query - *p).magnitude())
401                .fold(f32::INFINITY, f32::min);
402
403            // Every point here is equally far from the query, so any of them is
404            // a correct answer; check the returned one is at the minimum.
405            let (index, distance) = neighbors[0];
406            assert_relative_eq!(
407                (query - points[index]).magnitude(),
408                min_dist,
409                epsilon = 0.001
410            );
411            assert_relative_eq!(distance, min_dist, epsilon = 0.001);
412
413            println!("✓ GPU nearest neighbor accuracy test passed");
414        });
415    }
416}