Skip to main content

threecrate_gpu/
filtering.rs

1//! GPU-accelerated filtering
2
3use crate::spatial::{GpuKdTree, KD_TREE_WGSL};
4use crate::GpuContext;
5use threecrate_core::{Point3f, PointCloud, Result};
6
7const STATISTICAL_OUTLIER_SHADER: &str = r#"
8struct GpuPoint {
9    position: vec3<f32>,
10    _padding: f32,  // Ensure 16-byte alignment
11}
12
13@group(0) @binding(0) var<storage, read> input_points: array<GpuPoint>;
14@group(0) @binding(1) var<storage, read> neighbors: array<array<u32, MAX_NEIGHBORS>>;
15@group(0) @binding(2) var<storage, read_write> is_outlier: array<u32>;
16@group(0) @binding(3) var<uniform> params: FilterParams;
17
18struct FilterParams {
19    num_points: u32,
20    k_neighbors: u32,
21    std_dev_threshold: f32,
22    mean_distance: f32,
23}
24
25@compute @workgroup_size(64)
26fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
27    let index = global_id.x;
28    if (index >= params.num_points) {
29        return;
30    }
31    
32    let center_point = input_points[index].position;
33    
34    // Compute mean distance to k-nearest neighbors
35    var total_distance = 0.0;
36    var count = 0u;
37    
38    for (var i = 0u; i < params.k_neighbors; i++) {
39        let neighbor_idx = neighbors[index][i];
40        if (neighbor_idx < params.num_points) {
41            let neighbor_point = input_points[neighbor_idx].position;
42            let distance = length(center_point - neighbor_point);
43            total_distance += distance;
44            count++;
45        }
46    }
47    
48    if (count == 0u) {
49        is_outlier[index] = 0u;
50        return;
51    }
52    
53    let mean_distance = total_distance / f32(count);
54    
55    // Mark as outlier if distance is beyond threshold
56    let deviation = abs(mean_distance - params.mean_distance);
57    is_outlier[index] = select(0u, 1u, deviation > params.std_dev_threshold);
58}
59"#;
60
61/// Radius outlier kernel: one thread per kd-tree node. Counts the other points
62/// within `radius` with the shared tree walk, stopping as soon as it has
63/// `min_neighbors`, since the point is then known to stay.
64fn radius_outlier_shader() -> String {
65    format!(
66        "{}{}",
67        KD_TREE_WGSL,
68        r#"
69struct RadiusOutlierParams {
70    num_nodes: u32,
71    radius_sq: f32,
72    min_neighbors: u32,
73    _padding: u32,
74}
75
76@group(0) @binding(1) var<storage, read_write> is_outlier: array<u32>;
77@group(0) @binding(2) var<uniform> params: RadiusOutlierParams;
78
79@compute @workgroup_size(64)
80fn radius_outlier(@builtin(global_invocation_id) gid: vec3<u32>) {
81    let node_id = gid.x;
82    if (node_id >= params.num_nodes) {
83        return;
84    }
85    let count = count_within(
86        nodes[node_id].pos,
87        params.radius_sq,
88        node_id,
89        params.min_neighbors,
90    );
91    is_outlier[nodes[node_id].index] = select(0u, 1u, count < params.min_neighbors);
92}
93"#
94    )
95}
96
97const VOXEL_GRID_SHADER: &str = r#"
98struct GpuPoint {
99    position: vec3<f32>,
100    _padding: f32,  // Ensure 16-byte alignment
101}
102
103@group(0) @binding(0) var<storage, read> input_points: array<GpuPoint>;
104@group(0) @binding(1) var<storage, read_write> voxel_indices: array<u32>;
105@group(0) @binding(2) var<uniform> params: VoxelGridParams;
106
107struct VoxelGridParams {
108    num_points: u32,
109    voxel_size: f32,
110    min_x: f32,
111    min_y: f32,
112    min_z: f32,
113    max_x: f32,
114    max_y: f32,
115    max_z: f32,
116}
117
118fn get_voxel_index(point: vec3<f32>, params: VoxelGridParams) -> u32 {
119    // Simple coordinate-based voxel index (similar to CPU version)
120    let voxel_x = i32(floor((point.x - params.min_x) / params.voxel_size));
121    let voxel_y = i32(floor((point.y - params.min_y) / params.voxel_size));
122    let voxel_z = i32(floor((point.z - params.min_z) / params.voxel_size));
123    
124    // Use a simple hash based on coordinates
125    let hash_x = u32(voxel_x * voxel_x + voxel_x);
126    let hash_y = u32(voxel_y * voxel_y + voxel_y * 7);
127    let hash_z = u32(voxel_z * voxel_z + voxel_z * 13);
128    
129    return (hash_x + hash_y * 31u + hash_z * 97u) % 100000u;
130}
131
132@compute @workgroup_size(64)
133fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
134    let index = global_id.x;
135    if (index >= params.num_points) {
136        return;
137    }
138    
139    let point = input_points[index].position;
140    let voxel_idx = get_voxel_index(point, params);
141    
142    // Store voxel index for this point
143    voxel_indices[index] = voxel_idx;
144}
145"#;
146
147impl GpuContext {
148    /// Remove statistical outliers from point cloud using GPU acceleration
149    pub async fn remove_statistical_outliers(
150        &self,
151        points: &[Point3f],
152        k_neighbors: usize,
153        std_dev_multiplier: f32,
154    ) -> Result<Vec<Point3f>> {
155        if points.is_empty() {
156            return Ok(Vec::new());
157        }
158
159        // Convert points to GPU format with proper alignment
160        #[repr(C)]
161        #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
162        struct GpuPoint {
163            position: [f32; 3],
164            _padding: f32,
165        }
166
167        let point_data: Vec<GpuPoint> = points
168            .iter()
169            .map(|p| GpuPoint {
170                position: [p.x, p.y, p.z],
171                _padding: 0.0,
172            })
173            .collect();
174
175        // Convert to the format expected by helper functions
176        let point_data_flat: Vec<[f32; 3]> = point_data.iter().map(|p| p.position).collect();
177
178        // Compute neighbors (reuse from normals computation)
179        let neighbors = self.compute_neighbors_simple(&point_data_flat, k_neighbors);
180
181        // Compute global statistics on CPU first (could be moved to GPU)
182        let global_mean =
183            self.compute_global_mean_distance(&point_data_flat, &neighbors, k_neighbors);
184
185        // Create buffers
186        let input_buffer =
187            self.create_buffer_init("Input Points", &point_data, wgpu::BufferUsages::STORAGE);
188
189        let neighbors_buffer =
190            self.create_buffer_init("Neighbors", &neighbors, wgpu::BufferUsages::STORAGE);
191
192        let outlier_buffer = self.create_buffer(
193            "Outlier Flags",
194            (point_data.len() * std::mem::size_of::<u32>()) as u64,
195            wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
196        );
197
198        #[repr(C)]
199        #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
200        struct FilterParams {
201            num_points: u32,
202            k_neighbors: u32,
203            std_dev_threshold: f32,
204            mean_distance: f32,
205        }
206
207        let params = FilterParams {
208            num_points: points.len() as u32,
209            k_neighbors: k_neighbors as u32,
210            std_dev_threshold: global_mean * std_dev_multiplier,
211            mean_distance: global_mean,
212        };
213
214        let params_buffer =
215            self.create_buffer_init("Filter Params", &[params], wgpu::BufferUsages::UNIFORM);
216
217        // Create shader with MAX_NEIGHBORS constant
218        let shader_source =
219            STATISTICAL_OUTLIER_SHADER.replace("MAX_NEIGHBORS", &k_neighbors.to_string());
220        let shader = self.create_shader_module("Statistical Outlier Filter", &shader_source);
221
222        // Create bind group layout
223        let bind_group_layout = self.create_bind_group_layout(
224            "Outlier Filter",
225            &[
226                wgpu::BindGroupLayoutEntry {
227                    binding: 0,
228                    visibility: wgpu::ShaderStages::COMPUTE,
229                    ty: wgpu::BindingType::Buffer {
230                        ty: wgpu::BufferBindingType::Storage { read_only: true },
231                        has_dynamic_offset: false,
232                        min_binding_size: None,
233                    },
234                    count: None,
235                },
236                wgpu::BindGroupLayoutEntry {
237                    binding: 1,
238                    visibility: wgpu::ShaderStages::COMPUTE,
239                    ty: wgpu::BindingType::Buffer {
240                        ty: wgpu::BufferBindingType::Storage { read_only: true },
241                        has_dynamic_offset: false,
242                        min_binding_size: None,
243                    },
244                    count: None,
245                },
246                wgpu::BindGroupLayoutEntry {
247                    binding: 2,
248                    visibility: wgpu::ShaderStages::COMPUTE,
249                    ty: wgpu::BindingType::Buffer {
250                        ty: wgpu::BufferBindingType::Storage { read_only: false },
251                        has_dynamic_offset: false,
252                        min_binding_size: None,
253                    },
254                    count: None,
255                },
256                wgpu::BindGroupLayoutEntry {
257                    binding: 3,
258                    visibility: wgpu::ShaderStages::COMPUTE,
259                    ty: wgpu::BindingType::Buffer {
260                        ty: wgpu::BufferBindingType::Uniform,
261                        has_dynamic_offset: false,
262                        min_binding_size: None,
263                    },
264                    count: None,
265                },
266            ],
267        );
268
269        // Create compute pipeline
270        let pipeline = self
271            .device
272            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
273                label: Some("Outlier Filter Pipeline"),
274                layout: Some(&self.device.create_pipeline_layout(
275                    &wgpu::PipelineLayoutDescriptor {
276                        label: Some("Outlier Filter Layout"),
277                        bind_group_layouts: &[Some(&bind_group_layout)],
278                        immediate_size: 0,
279                    },
280                )),
281                module: &shader,
282                entry_point: Some("main"),
283                compilation_options: wgpu::PipelineCompilationOptions::default(),
284                cache: None,
285            });
286
287        // Create bind group
288        let bind_group = self.create_bind_group(
289            "Outlier Filter",
290            &bind_group_layout,
291            &[
292                wgpu::BindGroupEntry {
293                    binding: 0,
294                    resource: input_buffer.as_entire_binding(),
295                },
296                wgpu::BindGroupEntry {
297                    binding: 1,
298                    resource: neighbors_buffer.as_entire_binding(),
299                },
300                wgpu::BindGroupEntry {
301                    binding: 2,
302                    resource: outlier_buffer.as_entire_binding(),
303                },
304                wgpu::BindGroupEntry {
305                    binding: 3,
306                    resource: params_buffer.as_entire_binding(),
307                },
308            ],
309        );
310
311        // Execute compute shader
312        let mut encoder = self
313            .device
314            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
315                label: Some("Outlier Filter"),
316            });
317
318        {
319            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
320                label: Some("Outlier Filter Pass"),
321                timestamp_writes: None,
322            });
323            compute_pass.set_pipeline(&pipeline);
324            compute_pass.set_bind_group(0, &bind_group, &[]);
325            let workgroup_count = (points.len() + 63) / 64;
326            compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1);
327        }
328
329        // Read back results
330        let staging_buffer = self.create_buffer(
331            "Outlier Staging",
332            (point_data.len() * std::mem::size_of::<u32>()) as u64,
333            wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
334        );
335
336        encoder.copy_buffer_to_buffer(
337            &outlier_buffer,
338            0,
339            &staging_buffer,
340            0,
341            (point_data.len() * std::mem::size_of::<u32>()) as u64,
342        );
343
344        self.queue.submit(std::iter::once(encoder.finish()));
345
346        // Map and read results
347        let buffer_slice = staging_buffer.slice(..);
348        let (sender, receiver) = futures_intrusive::channel::shared::oneshot_channel();
349        buffer_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap());
350
351        self.device.poll(wgpu::PollType::Wait {
352            submission_index: None,
353            timeout: None,
354        });
355
356        if let Some(Ok(())) = receiver.receive().await {
357            let data = buffer_slice.get_mapped_range();
358            let outlier_flags: Vec<u32> = bytemuck::cast_slice(&data).to_vec();
359
360            let filtered_points: Vec<Point3f> = points
361                .iter()
362                .zip(outlier_flags.iter())
363                .filter(|&(_, &is_outlier)| is_outlier == 0)
364                .map(|(point, _)| *point)
365                .collect();
366
367            drop(data);
368            staging_buffer.unmap();
369
370            Ok(filtered_points)
371        } else {
372            Err(threecrate_core::Error::Gpu(
373                "Failed to read GPU filtering results".to_string(),
374            ))
375        }
376    }
377
378    /// Compute global mean distance for statistical filtering
379    fn compute_global_mean_distance(
380        &self,
381        points: &[[f32; 3]],
382        neighbors: &[[u32; 64]],
383        k: usize,
384    ) -> f32 {
385        let k = k.min(64).min(points.len());
386        let mut total_distance = 0.0;
387        let mut count = 0;
388
389        for (i, point) in points.iter().enumerate() {
390            for j in 0..k {
391                let neighbor_idx = neighbors[i][j] as usize;
392                if neighbor_idx < points.len() {
393                    let neighbor_point = &points[neighbor_idx];
394                    let dx = point[0] - neighbor_point[0];
395                    let dy = point[1] - neighbor_point[1];
396                    let dz = point[2] - neighbor_point[2];
397                    let distance = (dx * dx + dy * dy + dz * dz).sqrt();
398                    total_distance += distance;
399                    count += 1;
400                }
401            }
402        }
403
404        if count > 0 {
405            total_distance / count as f32
406        } else {
407            0.0
408        }
409    }
410
411    /// Remove radius outliers from point cloud using GPU acceleration.
412    ///
413    /// A point is kept when at least `min_neighbors` other points lie within
414    /// `radius` of it. Points with a NaN or infinite coordinate have no
415    /// neighbors, so they are removed whenever `min_neighbors > 0`.
416    pub async fn remove_radius_outliers(
417        &self,
418        points: &[Point3f],
419        radius: f32,
420        min_neighbors: usize,
421    ) -> Result<Vec<Point3f>> {
422        if points.is_empty() {
423            return Ok(Vec::new());
424        }
425
426        let tree = GpuKdTree::new(self, points)?;
427        // Every point starts flagged as an outlier unless no neighbors are
428        // required; the kernel then decides for each finite point.
429        let initial_flag = (min_neighbors > 0) as u32;
430        let outlier_buffer = self.create_buffer_init(
431            "Outlier Flags",
432            &vec![initial_flag; points.len()],
433            wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
434        );
435
436        if !tree.is_empty() {
437            #[repr(C)]
438            #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
439            struct RadiusOutlierParams {
440                num_nodes: u32,
441                radius_sq: f32,
442                min_neighbors: u32,
443                _padding: u32,
444            }
445            let params = RadiusOutlierParams {
446                num_nodes: tree.node_to_index.len() as u32,
447                // A negative radius matches nothing (as the old brute-force
448                // kernel did); squaring it would make it a valid range.
449                radius_sq: if radius < 0.0 { -1.0 } else { radius * radius },
450                min_neighbors: min_neighbors.min(u32::MAX as usize) as u32,
451                _padding: 0,
452            };
453            let params_buffer = self.create_buffer_init(
454                "Radius Outlier Params",
455                &[params],
456                wgpu::BufferUsages::UNIFORM,
457            );
458
459            let pipeline = self.cached_pipeline(
460                &self.pipelines.radius_outlier,
461                "Radius Outlier Filter",
462                radius_outlier_shader,
463                "radius_outlier",
464            );
465            let bind_group = self.create_bind_group(
466                "Radius Outlier Filter",
467                &pipeline.get_bind_group_layout(0),
468                &[
469                    wgpu::BindGroupEntry {
470                        binding: 0,
471                        resource: tree.nodes.as_entire_binding(),
472                    },
473                    wgpu::BindGroupEntry {
474                        binding: 1,
475                        resource: outlier_buffer.as_entire_binding(),
476                    },
477                    wgpu::BindGroupEntry {
478                        binding: 2,
479                        resource: params_buffer.as_entire_binding(),
480                    },
481                ],
482            );
483
484            let mut encoder = self
485                .device
486                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
487                    label: Some("Radius Outlier Filter"),
488                });
489            {
490                let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
491                    label: Some("Radius Outlier Filter"),
492                    timestamp_writes: None,
493                });
494                pass.set_pipeline(pipeline);
495                pass.set_bind_group(0, &bind_group, &[]);
496                pass.dispatch_workgroups(tree.node_to_index.len().div_ceil(64) as u32, 1, 1);
497            }
498            self.queue.submit(std::iter::once(encoder.finish()));
499        }
500
501        let outlier_flags: Vec<u32> = self.read_buffer(&outlier_buffer)?;
502        Ok(points
503            .iter()
504            .zip(&outlier_flags)
505            .filter(|&(_, &is_outlier)| is_outlier == 0)
506            .map(|(point, _)| *point)
507            .collect())
508    }
509
510    /// Voxel grid filtering using GPU acceleration
511    pub async fn voxel_grid_filter(
512        &self,
513        points: &[Point3f],
514        voxel_size: f32,
515    ) -> Result<Vec<Point3f>> {
516        if points.is_empty() {
517            return Ok(Vec::new());
518        }
519
520        if voxel_size <= 0.0 {
521            return Err(threecrate_core::Error::InvalidData(
522                "Voxel size must be positive".to_string(),
523            ));
524        }
525
526        // Convert points to GPU format
527        #[repr(C)]
528        #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
529        struct GpuPoint {
530            position: [f32; 3],
531            _padding: f32,
532        }
533
534        let point_data: Vec<GpuPoint> = points
535            .iter()
536            .map(|p| GpuPoint {
537                position: [p.x, p.y, p.z],
538                _padding: 0.0,
539            })
540            .collect();
541
542        // Compute grid bounds
543        let min_x = points
544            .iter()
545            .map(|p| p.x)
546            .min_by(|a, b| a.partial_cmp(b).unwrap())
547            .unwrap();
548        let min_y = points
549            .iter()
550            .map(|p| p.y)
551            .min_by(|a, b| a.partial_cmp(b).unwrap())
552            .unwrap();
553        let min_z = points
554            .iter()
555            .map(|p| p.z)
556            .min_by(|a, b| a.partial_cmp(b).unwrap())
557            .unwrap();
558        let max_x = points
559            .iter()
560            .map(|p| p.x)
561            .max_by(|a, b| a.partial_cmp(b).unwrap())
562            .unwrap();
563        let max_y = points
564            .iter()
565            .map(|p| p.y)
566            .max_by(|a, b| a.partial_cmp(b).unwrap())
567            .unwrap();
568        let max_z = points
569            .iter()
570            .map(|p| p.z)
571            .max_by(|a, b| a.partial_cmp(b).unwrap())
572            .unwrap();
573
574        // Estimate total voxels for buffer allocation (we'll use a hash-based approach)
575        let total_voxels = points.len() * 2; // Conservative estimate
576
577        // Create buffers
578        let input_buffer =
579            self.create_buffer_init("Input Points", &point_data, wgpu::BufferUsages::STORAGE);
580
581        let voxel_indices_buffer = self.create_buffer(
582            "Voxel Indices",
583            (point_data.len() * std::mem::size_of::<u32>()) as u64,
584            wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
585        );
586
587        #[repr(C)]
588        #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
589        struct VoxelGridParams {
590            num_points: u32,
591            voxel_size: f32,
592            min_x: f32,
593            min_y: f32,
594            min_z: f32,
595            max_x: f32,
596            max_y: f32,
597            max_z: f32,
598        }
599
600        let params = VoxelGridParams {
601            num_points: points.len() as u32,
602            voxel_size,
603            min_x,
604            min_y,
605            min_z,
606            max_x,
607            max_y,
608            max_z,
609        };
610
611        let params_buffer =
612            self.create_buffer_init("Voxel Grid Params", &[params], wgpu::BufferUsages::UNIFORM);
613
614        // Create shader
615        let shader = self.create_shader_module("Voxel Grid Filter", VOXEL_GRID_SHADER);
616
617        // Create bind group layout
618        let bind_group_layout = self.create_bind_group_layout(
619            "Voxel Grid Filter",
620            &[
621                wgpu::BindGroupLayoutEntry {
622                    binding: 0,
623                    visibility: wgpu::ShaderStages::COMPUTE,
624                    ty: wgpu::BindingType::Buffer {
625                        ty: wgpu::BufferBindingType::Storage { read_only: true },
626                        has_dynamic_offset: false,
627                        min_binding_size: None,
628                    },
629                    count: None,
630                },
631                wgpu::BindGroupLayoutEntry {
632                    binding: 1,
633                    visibility: wgpu::ShaderStages::COMPUTE,
634                    ty: wgpu::BindingType::Buffer {
635                        ty: wgpu::BufferBindingType::Storage { read_only: false },
636                        has_dynamic_offset: false,
637                        min_binding_size: None,
638                    },
639                    count: None,
640                },
641                wgpu::BindGroupLayoutEntry {
642                    binding: 2,
643                    visibility: wgpu::ShaderStages::COMPUTE,
644                    ty: wgpu::BindingType::Buffer {
645                        ty: wgpu::BufferBindingType::Uniform,
646                        has_dynamic_offset: false,
647                        min_binding_size: None,
648                    },
649                    count: None,
650                },
651            ],
652        );
653
654        // Create compute pipeline
655        let pipeline = self
656            .device
657            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
658                label: Some("Voxel Grid Filter Pipeline"),
659                layout: Some(&self.device.create_pipeline_layout(
660                    &wgpu::PipelineLayoutDescriptor {
661                        label: Some("Voxel Grid Filter Layout"),
662                        bind_group_layouts: &[Some(&bind_group_layout)],
663                        immediate_size: 0,
664                    },
665                )),
666                module: &shader,
667                entry_point: Some("main"),
668                compilation_options: wgpu::PipelineCompilationOptions::default(),
669                cache: None,
670            });
671
672        // Create bind group
673        let bind_group = self.create_bind_group(
674            "Voxel Grid Filter",
675            &bind_group_layout,
676            &[
677                wgpu::BindGroupEntry {
678                    binding: 0,
679                    resource: input_buffer.as_entire_binding(),
680                },
681                wgpu::BindGroupEntry {
682                    binding: 1,
683                    resource: voxel_indices_buffer.as_entire_binding(),
684                },
685                wgpu::BindGroupEntry {
686                    binding: 2,
687                    resource: params_buffer.as_entire_binding(),
688                },
689            ],
690        );
691
692        // Execute compute shader
693        let mut encoder = self
694            .device
695            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
696                label: Some("Voxel Grid Filter"),
697            });
698
699        {
700            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
701                label: Some("Voxel Grid Filter Pass"),
702                timestamp_writes: None,
703            });
704            compute_pass.set_pipeline(&pipeline);
705            compute_pass.set_bind_group(0, &bind_group, &[]);
706            let workgroup_count = (points.len() + 63) / 64;
707            compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1);
708        }
709
710        // Read back voxel indices
711        let indices_staging = self.create_buffer(
712            "Voxel Indices Staging",
713            (point_data.len() * std::mem::size_of::<u32>()) as u64,
714            wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
715        );
716
717        encoder.copy_buffer_to_buffer(
718            &voxel_indices_buffer,
719            0,
720            &indices_staging,
721            0,
722            (point_data.len() * std::mem::size_of::<u32>()) as u64,
723        );
724
725        self.queue.submit(std::iter::once(encoder.finish()));
726
727        // Map and read results
728        let indices_slice = indices_staging.slice(..);
729
730        let (indices_sender, indices_receiver) =
731            futures_intrusive::channel::shared::oneshot_channel();
732
733        indices_slice.map_async(wgpu::MapMode::Read, move |v| {
734            indices_sender.send(v).unwrap()
735        });
736
737        self.device.poll(wgpu::PollType::Wait {
738            submission_index: None,
739            timeout: None,
740        });
741
742        if let Some(Ok(())) = indices_receiver.receive().await {
743            let indices_data = indices_slice.get_mapped_range();
744
745            let voxel_indices: Vec<u32> = bytemuck::cast_slice(&indices_data).to_vec();
746
747            // Post-process on CPU: keep one point per voxel (the first one)
748            let mut voxel_used = vec![false; total_voxels];
749            let mut filtered_points = Vec::new();
750
751            for (point_idx, &voxel_idx) in voxel_indices.iter().enumerate() {
752                if voxel_idx < total_voxels as u32 && !voxel_used[voxel_idx as usize] {
753                    filtered_points.push(points[point_idx]);
754                    voxel_used[voxel_idx as usize] = true;
755                }
756            }
757
758            drop(indices_data);
759            indices_staging.unmap();
760
761            Ok(filtered_points)
762        } else {
763            Err(threecrate_core::Error::Gpu(
764                "Failed to read GPU voxel grid filtering results".to_string(),
765            ))
766        }
767    }
768}
769
770/// GPU-accelerated statistical outlier removal
771pub async fn gpu_remove_statistical_outliers(
772    gpu_context: &GpuContext,
773    cloud: &PointCloud<Point3f>,
774    k_neighbors: usize,
775    std_dev_multiplier: f32,
776) -> Result<PointCloud<Point3f>> {
777    let filtered_points = gpu_context
778        .remove_statistical_outliers(&cloud.points, k_neighbors, std_dev_multiplier)
779        .await?;
780    Ok(PointCloud::from_points(filtered_points))
781}
782
783/// GPU-accelerated radius outlier removal
784pub async fn gpu_radius_outlier_removal(
785    gpu_context: &GpuContext,
786    cloud: &PointCloud<Point3f>,
787    radius: f32,
788    min_neighbors: usize,
789) -> Result<PointCloud<Point3f>> {
790    let filtered_points = gpu_context
791        .remove_radius_outliers(&cloud.points, radius, min_neighbors)
792        .await?;
793    Ok(PointCloud::from_points(filtered_points))
794}
795
796/// GPU-accelerated voxel grid filtering
797pub async fn gpu_voxel_grid_filter(
798    gpu_context: &GpuContext,
799    cloud: &PointCloud<Point3f>,
800    voxel_size: f32,
801) -> Result<PointCloud<Point3f>> {
802    let filtered_points = gpu_context
803        .voxel_grid_filter(&cloud.points, voxel_size)
804        .await?;
805    Ok(PointCloud::from_points(filtered_points))
806}
807
808#[cfg(test)]
809mod tests {
810    use super::*;
811    use std::time::Instant;
812    use threecrate_core::{Point3f, PointCloud};
813
814    async fn try_create_gpu_context() -> Option<GpuContext> {
815        GpuContext::new().await.ok()
816    }
817
818    fn create_test_point_cloud_with_outliers() -> PointCloud<Point3f> {
819        let mut points = Vec::new();
820
821        // Create a cluster of normal points
822        for i in 0..50 {
823            for j in 0..50 {
824                let x = (i as f32 - 25.0) * 0.1;
825                let y = (j as f32 - 25.0) * 0.1;
826                let z = 0.0;
827                points.push(Point3f::new(x, y, z));
828            }
829        }
830
831        // Add some outliers far from the cluster
832        for i in 0..20 {
833            let x = 50.0 + (i as f32 * 2.0);
834            let y = 50.0 + (i as f32 * 2.0);
835            let z = 50.0 + (i as f32 * 2.0);
836            points.push(Point3f::new(x, y, z));
837        }
838
839        PointCloud::from_points(points)
840    }
841
842    fn create_test_point_cloud_no_outliers() -> PointCloud<Point3f> {
843        let mut points = Vec::new();
844
845        // Create a uniform grid of points
846        for i in 0..25 {
847            for j in 0..25 {
848                for k in 0..5 {
849                    let x = (i as f32 - 12.5) * 0.1;
850                    let y = (j as f32 - 12.5) * 0.1;
851                    let z = (k as f32 - 2.5) * 0.1;
852                    points.push(Point3f::new(x, y, z));
853                }
854            }
855        }
856
857        PointCloud::from_points(points)
858    }
859
860    #[tokio::test]
861    async fn test_gpu_statistical_outlier_removal_basic() {
862        let gpu_context = match try_create_gpu_context().await {
863            Some(ctx) => ctx,
864            None => {
865                println!("Skipping GPU test - no GPU available");
866                return;
867            }
868        };
869
870        let cloud = create_test_point_cloud_with_outliers();
871        let original_count = cloud.len();
872
873        let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, 1.0)
874            .await
875            .unwrap();
876        let filtered_count = filtered.len();
877
878        // Should remove some outliers
879        assert!(filtered_count < original_count);
880        assert!(filtered_count > 0);
881
882        println!(
883            "GPU outlier removal: {} -> {} points",
884            original_count, filtered_count
885        );
886    }
887
888    #[tokio::test]
889    async fn test_gpu_statistical_outlier_removal_no_outliers() {
890        let gpu_context = match try_create_gpu_context().await {
891            Some(ctx) => ctx,
892            None => {
893                println!("Skipping GPU test - no GPU available");
894                return;
895            }
896        };
897
898        let cloud = create_test_point_cloud_no_outliers();
899        let original_count = cloud.len();
900
901        let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, 1.0)
902            .await
903            .unwrap();
904        let filtered_count = filtered.len();
905
906        println!(
907            "GPU outlier removal (no outliers): {} -> {} points",
908            original_count, filtered_count
909        );
910
911        // The algorithm should remove some points but not all
912        // Even with no obvious outliers, the algorithm may remove points based on statistical analysis
913        assert!(filtered_count > 0);
914        assert!(filtered_count < original_count);
915    }
916
917    #[tokio::test]
918    async fn test_gpu_statistical_outlier_removal_empty_cloud() {
919        let gpu_context = match try_create_gpu_context().await {
920            Some(ctx) => ctx,
921            None => {
922                println!("Skipping GPU test - no GPU available");
923                return;
924            }
925        };
926
927        let cloud = PointCloud::<Point3f>::new();
928        let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, 1.0)
929            .await
930            .unwrap();
931
932        assert_eq!(filtered.len(), 0);
933    }
934
935    #[tokio::test]
936    async fn test_gpu_statistical_outlier_removal_single_point() {
937        let gpu_context = match try_create_gpu_context().await {
938            Some(ctx) => ctx,
939            None => {
940                println!("Skipping GPU test - no GPU available");
941                return;
942            }
943        };
944
945        let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
946        let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 1, 1.0)
947            .await
948            .unwrap();
949
950        // Should keep the single point
951        assert_eq!(filtered.len(), 1);
952    }
953
954    #[tokio::test]
955    async fn test_gpu_vs_cpu_statistical_outlier_removal() {
956        let gpu_context = match try_create_gpu_context().await {
957            Some(ctx) => ctx,
958            None => {
959                println!("Skipping GPU vs CPU test - no GPU available");
960                return;
961            }
962        };
963
964        let cloud = create_test_point_cloud_with_outliers();
965
966        // CPU version - use the same algorithm logic but avoid type conflicts
967        let cpu_start = Instant::now();
968        let cpu_filtered_count = {
969            // Simple CPU implementation for comparison
970            let points = &cloud.points;
971            let k = 10;
972            let std_dev_multiplier = 1.0;
973
974            if points.is_empty() {
975                0
976            } else {
977                // Simple distance-based outlier detection
978                let mut mean_distances = Vec::new();
979                for point in points {
980                    let mut distances = Vec::new();
981                    for other_point in points {
982                        if other_point != point {
983                            let dx = point.x - other_point.x;
984                            let dy = point.y - other_point.y;
985                            let dz = point.z - other_point.z;
986                            let distance = (dx * dx + dy * dy + dz * dz).sqrt();
987                            distances.push(distance);
988                        }
989                    }
990                    distances.sort_by(|a, b| a.partial_cmp(b).unwrap());
991                    let k_neighbors = k.min(distances.len());
992                    if k_neighbors > 0 {
993                        let mean =
994                            distances[..k_neighbors].iter().sum::<f32>() / k_neighbors as f32;
995                        mean_distances.push(mean);
996                    } else {
997                        mean_distances.push(0.0);
998                    }
999                }
1000
1001                let global_mean = mean_distances.iter().sum::<f32>() / mean_distances.len() as f32;
1002                let variance = mean_distances
1003                    .iter()
1004                    .map(|&d| (d - global_mean).powi(2))
1005                    .sum::<f32>()
1006                    / mean_distances.len() as f32;
1007                let global_std_dev = variance.sqrt();
1008                let threshold = global_mean + std_dev_multiplier * global_std_dev;
1009
1010                mean_distances.iter().filter(|&&d| d <= threshold).count()
1011            }
1012        };
1013        let cpu_time = cpu_start.elapsed();
1014
1015        // GPU version
1016        let gpu_start = Instant::now();
1017        let gpu_filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, 1.0)
1018            .await
1019            .unwrap();
1020        let gpu_time = gpu_start.elapsed();
1021
1022        println!(
1023            "CPU outlier removal: {} -> {} points in {:?}",
1024            cloud.len(),
1025            cpu_filtered_count,
1026            cpu_time
1027        );
1028        println!(
1029            "GPU outlier removal: {} -> {} points in {:?}",
1030            cloud.len(),
1031            gpu_filtered.len(),
1032            gpu_time
1033        );
1034
1035        // Both should remove outliers (exact counts may vary due to different algorithms)
1036        assert!(cpu_filtered_count < cloud.len());
1037        assert!(gpu_filtered.len() < cloud.len());
1038
1039        // Performance comparison (GPU should be faster for larger datasets)
1040        if cloud.len() > 1000 {
1041            println!(
1042                "GPU speedup: {:.2}x",
1043                cpu_time.as_secs_f32() / gpu_time.as_secs_f32()
1044            );
1045        }
1046    }
1047
1048    #[tokio::test]
1049    async fn test_gpu_statistical_outlier_removal_different_parameters() {
1050        let gpu_context = match try_create_gpu_context().await {
1051            Some(ctx) => ctx,
1052            None => {
1053                println!("Skipping GPU test - no GPU available");
1054                return;
1055            }
1056        };
1057
1058        let cloud = create_test_point_cloud_with_outliers();
1059        let original_count = cloud.len();
1060
1061        // Test different k values
1062        for k in [5, 10, 20] {
1063            let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, k, 1.0)
1064                .await
1065                .unwrap();
1066            println!("k={}: {} -> {} points", k, original_count, filtered.len());
1067            assert!(filtered.len() > 0);
1068        }
1069
1070        // Test different std_dev_multiplier values
1071        for std_dev in [0.5, 1.0, 2.0] {
1072            let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 10, std_dev)
1073                .await
1074                .unwrap();
1075            println!(
1076                "std_dev={}: {} -> {} points",
1077                std_dev,
1078                original_count,
1079                filtered.len()
1080            );
1081            assert!(filtered.len() > 0);
1082        }
1083    }
1084
1085    #[tokio::test]
1086    async fn test_gpu_statistical_outlier_removal_large_dataset() {
1087        let gpu_context = match try_create_gpu_context().await {
1088            Some(ctx) => ctx,
1089            None => {
1090                println!("Skipping GPU test - no GPU available");
1091                return;
1092            }
1093        };
1094
1095        // Create a smaller dataset for faster testing
1096        let mut points = Vec::new();
1097        for i in 0..50 {
1098            for j in 0..50 {
1099                let x = (i as f32 - 25.0) * 0.1;
1100                let y = (j as f32 - 25.0) * 0.1;
1101                let z = 0.0;
1102                points.push(Point3f::new(x, y, z));
1103            }
1104        }
1105
1106        // Add some outliers
1107        for i in 0..10 {
1108            let x = 25.0 + (i as f32 * 2.0);
1109            let y = 25.0 + (i as f32 * 2.0);
1110            let z = 25.0 + (i as f32 * 2.0);
1111            points.push(Point3f::new(x, y, z));
1112        }
1113
1114        let cloud = PointCloud::from_points(points);
1115        let original_count = cloud.len();
1116
1117        let start = Instant::now();
1118        let filtered = gpu_remove_statistical_outliers(&gpu_context, &cloud, 5, 1.0)
1119            .await
1120            .unwrap();
1121        let elapsed = start.elapsed();
1122
1123        println!(
1124            "Large dataset GPU outlier removal: {} -> {} points in {:?}",
1125            original_count,
1126            filtered.len(),
1127            elapsed
1128        );
1129
1130        assert!(filtered.len() < original_count);
1131        assert!(filtered.len() > 0);
1132
1133        // Should complete in reasonable time
1134        assert!(elapsed.as_secs() < 10);
1135    }
1136
1137    fn create_test_point_cloud_for_radius_outlier() -> PointCloud<Point3f> {
1138        let mut points = Vec::new();
1139
1140        // Create a dense cluster
1141        for i in 0..20 {
1142            for j in 0..20 {
1143                let x = (i as f32 - 10.0) * 0.1;
1144                let y = (j as f32 - 10.0) * 0.1;
1145                let z = 0.0;
1146                points.push(Point3f::new(x, y, z));
1147            }
1148        }
1149
1150        // Add some isolated points (outliers)
1151        for i in 0..5 {
1152            let x = 10.0 + (i as f32 * 2.0);
1153            let y = 10.0 + (i as f32 * 2.0);
1154            let z = 10.0 + (i as f32 * 2.0);
1155            points.push(Point3f::new(x, y, z));
1156        }
1157
1158        PointCloud::from_points(points)
1159    }
1160
1161    #[tokio::test]
1162    async fn test_gpu_radius_outlier_removal_matches_cpu() {
1163        let Some(gpu_context) = try_create_gpu_context().await else {
1164            return;
1165        };
1166
1167        // Uneven density: a dense patch, a sparse patch, and scattered points
1168        let mut state = 7u32;
1169        let mut next = || {
1170            state = state.wrapping_mul(1664525).wrapping_add(1013904223);
1171            (state >> 8) as f32 / (1u32 << 24) as f32
1172        };
1173        let mut cloud = PointCloud::new();
1174        for _ in 0..15_000 {
1175            cloud.push(Point3f::new(next(), next(), 0.1 * next()));
1176        }
1177        for _ in 0..3_000 {
1178            cloud.push(Point3f::new(2.0 + 3.0 * next(), 3.0 * next(), next()));
1179        }
1180        cloud.push(Point3f::new(f32::NAN, 0.0, 0.0));
1181
1182        let gpu = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.05, 4)
1183            .await
1184            .unwrap();
1185        let finite = PointCloud::from_points(
1186            cloud
1187                .points
1188                .iter()
1189                .copied()
1190                .filter(|p| p.coords.iter().all(|c| c.is_finite()))
1191                .collect(),
1192        );
1193        let cpu = threecrate_algorithms::radius_outlier_removal(&finite, 0.05, 4).unwrap();
1194
1195        assert_eq!(gpu.len(), cpu.len());
1196        assert!(gpu.points.iter().zip(&cpu.points).all(|(g, c)| g == c));
1197    }
1198
1199    #[tokio::test]
1200    async fn test_gpu_radius_outlier_removal_negative_radius() {
1201        let Some(gpu_context) = try_create_gpu_context().await else {
1202            return;
1203        };
1204        let cloud = PointCloud::from_points(
1205            (0..50)
1206                .map(|i| Point3f::new(i as f32 * 0.01, 0.0, 0.0))
1207                .collect(),
1208        );
1209        // A negative radius matches nothing, so every point is an outlier
1210        let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, -0.2, 3)
1211            .await
1212            .unwrap();
1213        assert!(filtered.is_empty());
1214    }
1215
1216    #[tokio::test]
1217    async fn test_gpu_radius_outlier_removal_basic() {
1218        let gpu_context = match try_create_gpu_context().await {
1219            Some(ctx) => ctx,
1220            None => {
1221                println!("Skipping GPU test - no GPU available");
1222                return;
1223            }
1224        };
1225
1226        let cloud = create_test_point_cloud_for_radius_outlier();
1227        let original_count = cloud.len();
1228
1229        let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, 3)
1230            .await
1231            .unwrap();
1232        let filtered_count = filtered.len();
1233
1234        // Should remove some outliers
1235        assert!(filtered_count < original_count);
1236        assert!(filtered_count > 0);
1237
1238        println!(
1239            "GPU radius outlier removal: {} -> {} points",
1240            original_count, filtered_count
1241        );
1242    }
1243
1244    #[tokio::test]
1245    async fn test_gpu_radius_outlier_removal_empty_cloud() {
1246        let gpu_context = match try_create_gpu_context().await {
1247            Some(ctx) => ctx,
1248            None => {
1249                println!("Skipping GPU test - no GPU available");
1250                return;
1251            }
1252        };
1253
1254        let cloud = PointCloud::<Point3f>::new();
1255        let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, 3)
1256            .await
1257            .unwrap();
1258
1259        assert_eq!(filtered.len(), 0);
1260    }
1261
1262    #[tokio::test]
1263    async fn test_gpu_radius_outlier_removal_single_point() {
1264        let gpu_context = match try_create_gpu_context().await {
1265            Some(ctx) => ctx,
1266            None => {
1267                println!("Skipping GPU test - no GPU available");
1268                return;
1269            }
1270        };
1271
1272        let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
1273        let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, 1)
1274            .await
1275            .unwrap();
1276
1277        // Should remove the single point since it has no neighbors
1278        assert_eq!(filtered.len(), 0);
1279    }
1280
1281    #[tokio::test]
1282    async fn test_gpu_radius_outlier_removal_different_parameters() {
1283        let gpu_context = match try_create_gpu_context().await {
1284            Some(ctx) => ctx,
1285            None => {
1286                println!("Skipping GPU test - no GPU available");
1287                return;
1288            }
1289        };
1290
1291        let cloud = create_test_point_cloud_for_radius_outlier();
1292        let original_count = cloud.len();
1293
1294        // Test different radius values
1295        for radius in [0.2, 0.5, 1.0] {
1296            let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, radius, 3)
1297                .await
1298                .unwrap();
1299            println!(
1300                "radius={}: {} -> {} points",
1301                radius,
1302                original_count,
1303                filtered.len()
1304            );
1305            assert!(filtered.len() > 0);
1306        }
1307
1308        // Test different min_neighbors values
1309        for min_neighbors in [1, 3, 5] {
1310            let filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, min_neighbors)
1311                .await
1312                .unwrap();
1313            println!(
1314                "min_neighbors={}: {} -> {} points",
1315                min_neighbors,
1316                original_count,
1317                filtered.len()
1318            );
1319            assert!(filtered.len() > 0);
1320        }
1321    }
1322
1323    fn create_test_point_cloud_for_voxel_grid() -> PointCloud<Point3f> {
1324        let mut points = Vec::new();
1325
1326        // Create a dense grid of points
1327        for i in 0..10 {
1328            for j in 0..10 {
1329                for k in 0..5 {
1330                    let x = (i as f32 - 5.0) * 0.1;
1331                    let y = (j as f32 - 5.0) * 0.1;
1332                    let z = (k as f32 - 2.5) * 0.1;
1333                    points.push(Point3f::new(x, y, z));
1334                }
1335            }
1336        }
1337
1338        // Add some duplicate points in the same voxels
1339        for i in 0..5 {
1340            for j in 0..5 {
1341                let x = (i as f32 - 2.5) * 0.1;
1342                let y = (j as f32 - 2.5) * 0.1;
1343                let z = 0.0;
1344                points.push(Point3f::new(x, y, z));
1345                points.push(Point3f::new(x, y, z)); // Duplicate
1346            }
1347        }
1348
1349        PointCloud::from_points(points)
1350    }
1351
1352    #[tokio::test]
1353    async fn test_gpu_voxel_grid_filter_basic() {
1354        let gpu_context = match try_create_gpu_context().await {
1355            Some(ctx) => ctx,
1356            None => {
1357                println!("Skipping GPU test - no GPU available");
1358                return;
1359            }
1360        };
1361
1362        let cloud = create_test_point_cloud_for_voxel_grid();
1363        let original_count = cloud.len();
1364
1365        let filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.1)
1366            .await
1367            .unwrap();
1368        let filtered_count = filtered.len();
1369
1370        // Should reduce the number of points by removing duplicates
1371        assert!(filtered_count < original_count);
1372        assert!(filtered_count > 0);
1373
1374        println!(
1375            "GPU voxel grid filter: {} -> {} points",
1376            original_count, filtered_count
1377        );
1378    }
1379
1380    #[tokio::test]
1381    async fn test_gpu_voxel_grid_filter_empty_cloud() {
1382        let gpu_context = match try_create_gpu_context().await {
1383            Some(ctx) => ctx,
1384            None => {
1385                println!("Skipping GPU test - no GPU available");
1386                return;
1387            }
1388        };
1389
1390        let cloud = PointCloud::<Point3f>::new();
1391        let filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.1)
1392            .await
1393            .unwrap();
1394
1395        assert_eq!(filtered.len(), 0);
1396    }
1397
1398    #[tokio::test]
1399    async fn test_gpu_voxel_grid_filter_single_point() {
1400        let gpu_context = match try_create_gpu_context().await {
1401            Some(ctx) => ctx,
1402            None => {
1403                println!("Skipping GPU test - no GPU available");
1404                return;
1405            }
1406        };
1407
1408        let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
1409        let filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.1)
1410            .await
1411            .unwrap();
1412
1413        // Should keep the single point
1414        assert_eq!(filtered.len(), 1);
1415    }
1416
1417    #[tokio::test]
1418    async fn test_gpu_voxel_grid_filter_different_voxel_sizes() {
1419        let gpu_context = match try_create_gpu_context().await {
1420            Some(ctx) => ctx,
1421            None => {
1422                println!("Skipping GPU test - no GPU available");
1423                return;
1424            }
1425        };
1426
1427        let cloud = create_test_point_cloud_for_voxel_grid();
1428        let original_count = cloud.len();
1429
1430        // Test different voxel sizes
1431        for voxel_size in [0.05, 0.1, 0.2] {
1432            let filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, voxel_size)
1433                .await
1434                .unwrap();
1435            println!(
1436                "voxel_size={}: {} -> {} points",
1437                voxel_size,
1438                original_count,
1439                filtered.len()
1440            );
1441            assert!(filtered.len() > 0);
1442            assert!(filtered.len() <= original_count);
1443        }
1444    }
1445
1446    #[tokio::test]
1447    async fn test_gpu_voxel_grid_filter_invalid_voxel_size() {
1448        let gpu_context = match try_create_gpu_context().await {
1449            Some(ctx) => ctx,
1450            None => {
1451                println!("Skipping GPU test - no GPU available");
1452                return;
1453            }
1454        };
1455
1456        let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
1457        let result = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.0).await;
1458        assert!(result.is_err());
1459
1460        let result = gpu_voxel_grid_filter(&gpu_context, &cloud, -1.0).await;
1461        assert!(result.is_err());
1462    }
1463
1464    #[tokio::test]
1465    async fn test_gpu_vs_cpu_performance_comparison() {
1466        let gpu_context = match try_create_gpu_context().await {
1467            Some(ctx) => ctx,
1468            None => {
1469                println!("Skipping GPU vs CPU test - no GPU available");
1470                return;
1471            }
1472        };
1473
1474        let cloud = create_test_point_cloud_for_radius_outlier();
1475
1476        // GPU radius outlier removal
1477        let gpu_start = Instant::now();
1478        let gpu_filtered = gpu_radius_outlier_removal(&gpu_context, &cloud, 0.5, 3)
1479            .await
1480            .unwrap();
1481        let gpu_time = gpu_start.elapsed();
1482
1483        // GPU voxel grid filter
1484        let gpu_voxel_start = Instant::now();
1485        let gpu_voxel_filtered = gpu_voxel_grid_filter(&gpu_context, &cloud, 0.1)
1486            .await
1487            .unwrap();
1488        let gpu_voxel_time = gpu_voxel_start.elapsed();
1489
1490        println!(
1491            "GPU radius outlier removal: {} -> {} points in {:?}",
1492            cloud.len(),
1493            gpu_filtered.len(),
1494            gpu_time
1495        );
1496        println!(
1497            "GPU voxel grid filter: {} -> {} points in {:?}",
1498            cloud.len(),
1499            gpu_voxel_filtered.len(),
1500            gpu_voxel_time
1501        );
1502
1503        // Both should complete successfully
1504        assert!(gpu_filtered.len() > 0);
1505        assert!(gpu_voxel_filtered.len() > 0);
1506
1507        // Should complete in reasonable time
1508        assert!(gpu_time.as_secs() < 10);
1509        assert!(gpu_voxel_time.as_secs() < 10);
1510    }
1511}