1use crate::spatial::{vec4s, GpuKdTree, KD_TREE_WGSL, MAX_K};
4use crate::GpuContext;
5use bytemuck::{Pod, Zeroable};
6use threecrate_core::{Point3f, Result};
7
8#[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#[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#[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
36fn 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 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 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#[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
186pub 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
199pub 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
211pub 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 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 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); assert!(neighbors[0].1 < 0.2); 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 assert_eq!(results[0][0].0, 0);
293
294 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 assert!(!neighbors.is_empty());
318
319 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 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 let min_dist = points
399 .iter()
400 .map(|p| (query - *p).magnitude())
401 .fold(f32::INFINITY, f32::min);
402
403 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}