Skip to main content

threecrate_gpu/
tsdf.rs

1use crate::device::GpuContext;
2use bytemuck::{Pod, Zeroable};
3use nalgebra::{Matrix4, Point3};
4use threecrate_core::{ColoredPoint3f, Error, PointCloud, Result};
5use wgpu::util::DeviceExt;
6
7/// TSDF voxel data for GPU processing
8#[repr(C)]
9#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
10#[repr(align(16))] // Ensure 16-byte alignment for GPU
11pub struct TsdfVoxel {
12    pub tsdf_value: f32,
13    pub weight: f32,
14    pub color_r: u32,
15    pub color_g: u32,
16    pub color_b: u32,
17    pub _padding1: u32,
18    pub _padding2: u32,
19    pub _padding3: u32,
20}
21
22/// TSDF volume parameters
23#[derive(Debug, Clone)]
24pub struct TsdfVolume {
25    pub voxel_size: f32,
26    pub truncation_distance: f32,
27    pub resolution: [u32; 3], // [width, height, depth]
28    pub origin: Point3<f32>,
29}
30
31/// Represents a TSDF volume stored on the GPU.
32pub struct TsdfVolumeGpu {
33    pub volume: TsdfVolume,
34    pub voxel_buffer: wgpu::Buffer,
35}
36
37/// Camera intrinsic parameters
38#[repr(C)]
39#[derive(Copy, Clone, Pod, Zeroable)]
40#[repr(align(16))] // Ensure 16-byte alignment for GPU
41pub struct CameraIntrinsics {
42    pub fx: f32,
43    pub fy: f32,
44    pub cx: f32,
45    pub cy: f32,
46    pub width: u32,
47    pub height: u32,
48    pub depth_scale: f32,
49    pub _padding: f32,
50}
51
52/// TSDF integration parameters
53#[repr(C)]
54#[derive(Copy, Clone, Pod, Zeroable)]
55#[repr(align(16))]
56pub struct TsdfParams {
57    pub voxel_size: f32,
58    pub truncation_distance: f32,
59    pub max_weight: f32,
60    pub iso_value: f32,
61    pub resolution: [u32; 3],
62    pub _padding2: u32,
63    pub origin: [f32; 3],
64    pub _padding3: f32,
65}
66
67#[repr(C)]
68#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
69#[repr(align(16))] // Ensure 16-byte alignment for GPU
70pub struct GpuPoint3f {
71    pub x: f32,
72    pub y: f32,
73    pub z: f32,
74    pub r: u32,
75    pub g: u32,
76    pub b: u32,
77    pub _padding1: u32,
78    pub _padding2: u32,
79}
80
81impl GpuContext {
82    /// Integrate depth image into TSDF volume
83    pub async fn tsdf_integrate(
84        &self,
85        volume: &mut TsdfVolume,
86        depth_image: &[f32],
87        color_image: Option<&[u8]>, // RGB color data
88        camera_pose: &Matrix4<f32>,
89        intrinsics: &CameraIntrinsics,
90    ) -> Result<Vec<TsdfVoxel>> {
91        let total_voxels =
92            (volume.resolution[0] * volume.resolution[1] * volume.resolution[2]) as usize;
93
94        // Create buffers
95        let depth_buffer = self
96            .device
97            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
98                label: Some("TSDF Depth Buffer"),
99                contents: bytemuck::cast_slice(depth_image),
100                usage: wgpu::BufferUsages::STORAGE,
101            });
102
103        let color_buffer = if let Some(color_data) = color_image {
104            // Convert RGB u8 data to packed u32 RGB values
105            let mut packed_colors = Vec::with_capacity(color_data.len() / 3);
106            for chunk in color_data.chunks_exact(3) {
107                let r = chunk[0] as u32;
108                let g = chunk[1] as u32;
109                let b = chunk[2] as u32;
110                let packed = (r << 16) | (g << 8) | b;
111                packed_colors.push(packed);
112            }
113
114            self.device
115                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
116                    label: Some("TSDF Color Buffer"),
117                    contents: bytemuck::cast_slice(&packed_colors),
118                    usage: wgpu::BufferUsages::STORAGE,
119                })
120        } else {
121            // Create empty buffer if no color data
122            self.device
123                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
124                    label: Some("TSDF Empty Color Buffer"),
125                    contents: bytemuck::cast_slice(&[0u32; 4]), // Small dummy buffer
126                    usage: wgpu::BufferUsages::STORAGE,
127                })
128        };
129
130        // Initialize TSDF volume if needed
131        let initial_voxels = vec![
132            TsdfVoxel {
133                tsdf_value: 1.0,
134                weight: 0.0,
135                color_r: 0,
136                color_g: 0,
137                color_b: 0,
138                _padding1: 0,
139                _padding2: 0,
140                _padding3: 0,
141            };
142            total_voxels
143        ];
144
145        let voxel_buffer = self
146            .device
147            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
148                label: Some("TSDF Voxel Buffer"),
149                contents: bytemuck::cast_slice(&initial_voxels),
150                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
151            });
152
153        // Convert camera transform to world-to-camera matrix (inverse of camera pose)
154        let world_to_camera = camera_pose
155            .try_inverse()
156            .ok_or_else(|| Error::Gpu("Failed to invert camera pose matrix".into()))?;
157
158        let mut camera_transform = [[0.0f32; 4]; 4];
159        for i in 0..4 {
160            for j in 0..4 {
161                camera_transform[i][j] = world_to_camera[(i, j)];
162            }
163        }
164
165        let transform_buffer = self
166            .device
167            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
168                label: Some("TSDF Transform Buffer"),
169                contents: bytemuck::cast_slice(&[camera_transform]),
170                usage: wgpu::BufferUsages::UNIFORM,
171            });
172
173        let intrinsics_buffer = self
174            .device
175            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
176                label: Some("TSDF Intrinsics Buffer"),
177                contents: bytemuck::bytes_of(intrinsics),
178                usage: wgpu::BufferUsages::UNIFORM,
179            });
180
181        let params = TsdfParams {
182            voxel_size: volume.voxel_size,
183            truncation_distance: volume.truncation_distance,
184            max_weight: 100.0,
185            iso_value: 0.0,
186            resolution: volume.resolution,
187            _padding2: 0,
188            origin: [volume.origin.x, volume.origin.y, volume.origin.z],
189            _padding3: 0.0,
190        };
191
192        let params_buffer = self
193            .device
194            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
195                label: Some("TSDF Params Buffer"),
196                contents: bytemuck::bytes_of(&params),
197                usage: wgpu::BufferUsages::UNIFORM,
198            });
199
200        // Create compute pipeline
201        let shader = self
202            .device
203            .create_shader_module(wgpu::ShaderModuleDescriptor {
204                label: Some("TSDF Integration Shader"),
205                source: wgpu::ShaderSource::Wgsl(
206                    include_str!("shaders/tsdf_integration.wgsl").into(),
207                ),
208            });
209
210        let pipeline = self
211            .device
212            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
213                label: Some("TSDF Integration Pipeline"),
214                layout: None,
215                module: &shader,
216                entry_point: Some("main"),
217                compilation_options: wgpu::PipelineCompilationOptions::default(),
218                cache: None,
219            });
220
221        // Create bind group
222        let bind_group_entries = vec![
223            wgpu::BindGroupEntry {
224                binding: 0,
225                resource: voxel_buffer.as_entire_binding(),
226            },
227            wgpu::BindGroupEntry {
228                binding: 1,
229                resource: depth_buffer.as_entire_binding(),
230            },
231            wgpu::BindGroupEntry {
232                binding: 2,
233                resource: transform_buffer.as_entire_binding(),
234            },
235            wgpu::BindGroupEntry {
236                binding: 3,
237                resource: intrinsics_buffer.as_entire_binding(),
238            },
239            wgpu::BindGroupEntry {
240                binding: 4,
241                resource: params_buffer.as_entire_binding(),
242            },
243            wgpu::BindGroupEntry {
244                binding: 5,
245                resource: color_buffer.as_entire_binding(),
246            },
247        ];
248
249        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
250            label: Some("TSDF Integration Bind Group"),
251            layout: &pipeline.get_bind_group_layout(0),
252            entries: &bind_group_entries,
253        });
254
255        // Dispatch compute shader
256        let mut encoder = self
257            .device
258            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
259                label: Some("TSDF Integration Encoder"),
260            });
261
262        {
263            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
264                label: Some("TSDF Integration Pass"),
265                timestamp_writes: None,
266            });
267
268            compute_pass.set_pipeline(&pipeline);
269            compute_pass.set_bind_group(0, &bind_group, &[]);
270
271            // Dispatch with 4x4x4 workgroups
272            let workgroup_size = 4;
273            let dispatch_x = (volume.resolution[0] + workgroup_size - 1) / workgroup_size;
274            let dispatch_y = (volume.resolution[1] + workgroup_size - 1) / workgroup_size;
275            let dispatch_z = (volume.resolution[2] + workgroup_size - 1) / workgroup_size;
276
277            compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, dispatch_z);
278        }
279
280        // Read back results
281        let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
282            label: Some("TSDF Staging Buffer"),
283            size: (total_voxels * std::mem::size_of::<TsdfVoxel>()) as u64,
284            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
285            mapped_at_creation: false,
286        });
287
288        encoder.copy_buffer_to_buffer(&voxel_buffer, 0, &staging_buffer, 0, staging_buffer.size());
289
290        self.queue.submit(std::iter::once(encoder.finish()));
291
292        let buffer_slice = staging_buffer.slice(..);
293        let (sender, receiver) = flume::unbounded();
294        buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
295            sender.send(result).unwrap();
296        });
297
298        self.device.poll(wgpu::PollType::Wait {
299            submission_index: None,
300            timeout: None,
301        });
302        receiver
303            .recv_async()
304            .await
305            .map_err(|_| Error::Gpu("Failed to receive mapping result".into()))?
306            .map_err(|e| Error::Gpu(format!("Buffer mapping failed: {:?}", e)))?;
307
308        let data = buffer_slice.get_mapped_range();
309        // `get_mapped_range` only guarantees 8-byte alignment, but `TsdfVoxel` is
310        // `repr(align(16))`, so `cast_slice` would panic with
311        // `TargetAlignmentGreaterAndInputNotAligned`. Copy the bytes into a
312        // correctly aligned Vec instead of reinterpreting the mapped range.
313        let voxel_size = std::mem::size_of::<TsdfVoxel>();
314        let count = data.len() / voxel_size;
315        let mut result = vec![TsdfVoxel::zeroed(); count];
316        bytemuck::cast_slice_mut(&mut result).copy_from_slice(&data[..count * voxel_size]);
317
318        drop(data);
319        staging_buffer.unmap();
320
321        Ok(result)
322    }
323
324    /// Extract point cloud from TSDF volume using marching cubes
325    pub async fn tsdf_extract_surface(
326        &self,
327        volume: &TsdfVolume,
328        voxels: &[TsdfVoxel],
329        iso_value: f32,
330    ) -> Result<PointCloud<ColoredPoint3f>> {
331        let total_voxels =
332            (volume.resolution[0] * volume.resolution[1] * volume.resolution[2]) as usize;
333        let max_points = std::cmp::min(total_voxels, 1_000_000); // Limit to reasonable size
334
335        // Create voxel buffer
336        let voxel_buffer = self
337            .device
338            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
339                label: Some("TSDF Voxel Buffer"),
340                contents: bytemuck::cast_slice(voxels),
341                usage: wgpu::BufferUsages::STORAGE,
342            });
343
344        // Create output buffers
345        let points_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
346            label: Some("Surface Points Buffer"),
347            size: (max_points * std::mem::size_of::<GpuPoint3f>()) as u64,
348            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
349            mapped_at_creation: false,
350        });
351
352        let point_count_buffer =
353            self.device
354                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
355                    label: Some("Point Count Buffer"),
356                    contents: bytemuck::bytes_of(&0u32),
357                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
358                });
359
360        let params = TsdfParams {
361            voxel_size: volume.voxel_size,
362            truncation_distance: volume.truncation_distance,
363            max_weight: 100.0,
364            iso_value,
365            resolution: volume.resolution,
366            _padding2: 0,
367            origin: [volume.origin.x, volume.origin.y, volume.origin.z],
368            _padding3: 0.0,
369        };
370
371        let params_buffer = self
372            .device
373            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
374                label: Some("Surface Extraction Params Buffer"),
375                contents: bytemuck::bytes_of(&params),
376                usage: wgpu::BufferUsages::UNIFORM,
377            });
378
379        // Create compute pipeline
380        let shader = self
381            .device
382            .create_shader_module(wgpu::ShaderModuleDescriptor {
383                label: Some("Surface Extraction Shader"),
384                source: wgpu::ShaderSource::Wgsl(
385                    include_str!("shaders/surface_extraction.wgsl").into(),
386                ),
387            });
388
389        let pipeline = self
390            .device
391            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
392                label: Some("Surface Extraction Pipeline"),
393                layout: None,
394                module: &shader,
395                entry_point: Some("main"),
396                compilation_options: wgpu::PipelineCompilationOptions::default(),
397                cache: None,
398            });
399
400        // Create bind group
401        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
402            label: Some("Surface Extraction Bind Group"),
403            layout: &pipeline.get_bind_group_layout(0),
404            entries: &[
405                wgpu::BindGroupEntry {
406                    binding: 0,
407                    resource: voxel_buffer.as_entire_binding(),
408                },
409                wgpu::BindGroupEntry {
410                    binding: 1,
411                    resource: points_buffer.as_entire_binding(),
412                },
413                wgpu::BindGroupEntry {
414                    binding: 2,
415                    resource: params_buffer.as_entire_binding(),
416                },
417                wgpu::BindGroupEntry {
418                    binding: 3,
419                    resource: point_count_buffer.as_entire_binding(),
420                },
421            ],
422        });
423
424        // Create staging buffer for reading back results
425        let point_count_staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
426            label: Some("Point Count Staging Buffer"),
427            size: std::mem::size_of::<u32>() as u64,
428            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
429            mapped_at_creation: false,
430        });
431
432        // Dispatch compute shader
433        let mut encoder = self
434            .device
435            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
436                label: Some("Surface Extraction Encoder"),
437            });
438
439        {
440            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
441                label: Some("Surface Extraction Pass"),
442                timestamp_writes: None,
443            });
444
445            compute_pass.set_pipeline(&pipeline);
446            compute_pass.set_bind_group(0, &bind_group, &[]);
447            compute_pass.dispatch_workgroups(
448                (volume.resolution[0] + 3) / 4,
449                (volume.resolution[1] + 3) / 4,
450                (volume.resolution[2] + 3) / 4,
451            );
452        }
453
454        // Copy point count to staging buffer
455        encoder.copy_buffer_to_buffer(
456            &point_count_buffer,
457            0,
458            &point_count_staging_buffer,
459            0,
460            std::mem::size_of::<u32>() as u64,
461        );
462
463        self.queue.submit(Some(encoder.finish()));
464
465        // Read point count
466        let point_count_slice = point_count_staging_buffer.slice(..);
467        let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
468        point_count_slice.map_async(wgpu::MapMode::Read, move |result| {
469            tx.send(result).unwrap();
470        });
471        self.device.poll(wgpu::PollType::Wait {
472            submission_index: None,
473            timeout: None,
474        });
475        rx.receive().await.unwrap()?;
476
477        let mapped_range = point_count_slice.get_mapped_range();
478        let point_count = bytemuck::cast_slice::<u8, u32>(mapped_range.as_ref())[0] as usize;
479        drop(mapped_range);
480        point_count_staging_buffer.unmap();
481
482        if point_count == 0 {
483            return Ok(PointCloud { points: Vec::new() });
484        }
485
486        // Create staging buffer for points
487        let points_staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
488            label: Some("Points Staging Buffer"),
489            size: (point_count * std::mem::size_of::<GpuPoint3f>()) as u64,
490            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
491            mapped_at_creation: false,
492        });
493
494        // Copy points to staging buffer
495        let mut encoder = self
496            .device
497            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
498                label: Some("Points Copy Encoder"),
499            });
500
501        encoder.copy_buffer_to_buffer(
502            &points_buffer,
503            0,
504            &points_staging_buffer,
505            0,
506            (point_count * std::mem::size_of::<GpuPoint3f>()) as u64,
507        );
508
509        self.queue.submit(Some(encoder.finish()));
510
511        // Read points
512        let points_slice = points_staging_buffer.slice(..);
513        let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
514        points_slice.map_async(wgpu::MapMode::Read, move |result| {
515            tx.send(result).unwrap();
516        });
517        self.device.poll(wgpu::PollType::Wait {
518            submission_index: None,
519            timeout: None,
520        });
521        rx.receive().await.unwrap()?;
522
523        let mapped_range = points_slice.get_mapped_range();
524        // `GpuPoint3f` is `repr(align(16))` but `get_mapped_range` only guarantees
525        // 8-byte alignment, so copy into an aligned Vec rather than casting the
526        // mapped bytes in place (which panics with
527        // `TargetAlignmentGreaterAndInputNotAligned`).
528        let point_size = std::mem::size_of::<GpuPoint3f>();
529        let available = mapped_range.len() / point_size;
530        let mut gpu_points = vec![GpuPoint3f::zeroed(); available];
531        bytemuck::cast_slice_mut(&mut gpu_points)
532            .copy_from_slice(&mapped_range[..available * point_size]);
533        let mut points = Vec::with_capacity(point_count);
534
535        for gpu_point in gpu_points.iter().take(point_count) {
536            points.push(ColoredPoint3f {
537                position: Point3::new(gpu_point.x, gpu_point.y, gpu_point.z),
538                color: [gpu_point.r as u8, gpu_point.g as u8, gpu_point.b as u8],
539            });
540        }
541
542        drop(mapped_range); // Explicitly drop the mapped range before unmapping
543        points_staging_buffer.unmap();
544
545        Ok(PointCloud { points })
546    }
547}
548
549impl TsdfVolumeGpu {
550    /// Creates a new TSDF volume on the GPU.
551    pub fn new(gpu: &GpuContext, volume_params: TsdfVolume) -> Self {
552        let total_voxels = (volume_params.resolution[0]
553            * volume_params.resolution[1]
554            * volume_params.resolution[2]) as usize;
555
556        // Initialize voxels with default values
557        let initial_voxels = vec![
558            TsdfVoxel {
559                tsdf_value: 1.0,
560                weight: 0.0,
561                color_r: 0,
562                color_g: 0,
563                color_b: 0,
564                _padding1: 0,
565                _padding2: 0,
566                _padding3: 0,
567            };
568            total_voxels
569        ];
570
571        let voxel_buffer = gpu
572            .device
573            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
574                label: Some("TSDF Voxel Buffer"),
575                contents: bytemuck::cast_slice(&initial_voxels),
576                usage: wgpu::BufferUsages::STORAGE
577                    | wgpu::BufferUsages::COPY_SRC
578                    | wgpu::BufferUsages::COPY_DST,
579            });
580
581        Self {
582            volume: volume_params,
583            voxel_buffer,
584        }
585    }
586
587    /// Integrates a depth image into the TSDF volume.
588    pub async fn integrate(
589        &self,
590        gpu: &GpuContext,
591        depth_image: &[f32],
592        color_image: Option<&[u8]>, // RGB color data
593        camera_pose: &Matrix4<f32>,
594        intrinsics: &CameraIntrinsics,
595    ) -> Result<()> {
596        // Create buffers for depth, color, transform, and parameters
597        let depth_buffer = gpu.create_buffer_init(
598            "TSDF Depth Buffer",
599            depth_image,
600            wgpu::BufferUsages::STORAGE,
601        );
602
603        let color_buffer = if let Some(data) = color_image {
604            gpu.create_buffer_init("TSDF Color Buffer", data, wgpu::BufferUsages::STORAGE)
605        } else {
606            // Create a dummy buffer if no color image is provided
607            gpu.create_buffer_init(
608                "TSDF Dummy Color Buffer",
609                &[0u32; 4],
610                wgpu::BufferUsages::STORAGE,
611            )
612        };
613
614        // Convert camera transform to world-to-camera matrix (inverse of camera pose)
615        let world_to_camera = camera_pose
616            .try_inverse()
617            .ok_or_else(|| Error::Gpu("Failed to invert camera pose matrix".into()))?;
618
619        let mut camera_transform = [[0.0f32; 4]; 4];
620        for i in 0..4 {
621            for j in 0..4 {
622                camera_transform[i][j] = world_to_camera[(i, j)];
623            }
624        }
625
626        let transform_buffer = gpu.create_buffer_init(
627            "TSDF Transform Buffer",
628            &[camera_transform],
629            wgpu::BufferUsages::UNIFORM,
630        );
631
632        let intrinsics_buffer = gpu.create_buffer_init(
633            "TSDF Intrinsics Buffer",
634            &[*intrinsics],
635            wgpu::BufferUsages::UNIFORM,
636        );
637
638        let params = TsdfParams {
639            voxel_size: self.volume.voxel_size,
640            truncation_distance: self.volume.truncation_distance,
641            max_weight: 100.0,
642            iso_value: 0.0,
643            resolution: self.volume.resolution,
644            _padding2: 0,
645            origin: [
646                self.volume.origin.x,
647                self.volume.origin.y,
648                self.volume.origin.z,
649            ],
650            _padding3: 0.0,
651        };
652        let params_buffer =
653            gpu.create_buffer_init("TSDF Params Buffer", &[params], wgpu::BufferUsages::UNIFORM);
654
655        // Create compute pipeline
656        let shader = gpu.create_shader_module(
657            "TSDF Integration Shader",
658            include_str!("shaders/tsdf_integration.wgsl"),
659        );
660        let pipeline = gpu
661            .device
662            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
663                label: Some("TSDF Integration Pipeline"),
664                layout: None,
665                module: &shader,
666                entry_point: Some("main"),
667                compilation_options: wgpu::PipelineCompilationOptions::default(),
668                cache: None,
669            });
670
671        // Create bind group
672        let bind_group_entries = vec![
673            wgpu::BindGroupEntry {
674                binding: 0,
675                resource: self.voxel_buffer.as_entire_binding(),
676            },
677            wgpu::BindGroupEntry {
678                binding: 1,
679                resource: depth_buffer.as_entire_binding(),
680            },
681            wgpu::BindGroupEntry {
682                binding: 2,
683                resource: transform_buffer.as_entire_binding(),
684            },
685            wgpu::BindGroupEntry {
686                binding: 3,
687                resource: intrinsics_buffer.as_entire_binding(),
688            },
689            wgpu::BindGroupEntry {
690                binding: 4,
691                resource: params_buffer.as_entire_binding(),
692            },
693            wgpu::BindGroupEntry {
694                binding: 5,
695                resource: color_buffer.as_entire_binding(),
696            },
697        ];
698
699        let bind_group = gpu.create_bind_group(
700            "TSDF Integration Bind Group",
701            &pipeline.get_bind_group_layout(0),
702            &bind_group_entries,
703        );
704
705        // Dispatch compute shader
706        let mut encoder = gpu
707            .device
708            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
709                label: Some("TSDF Integration Encoder"),
710            });
711
712        {
713            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
714                label: Some("TSDF Integration Pass"),
715                timestamp_writes: None,
716            });
717
718            compute_pass.set_pipeline(&pipeline);
719            compute_pass.set_bind_group(0, &bind_group, &[]);
720
721            // Dispatch with 4x4x4 workgroups
722            let workgroup_size = 4;
723            let dispatch_x = (self.volume.resolution[0] + workgroup_size - 1) / workgroup_size;
724            let dispatch_y = (self.volume.resolution[1] + workgroup_size - 1) / workgroup_size;
725            let dispatch_z = (self.volume.resolution[2] + workgroup_size - 1) / workgroup_size;
726
727            println!(
728                "Dispatching compute shader with {} x {} x {} workgroups",
729                dispatch_x, dispatch_y, dispatch_z
730            );
731            compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, dispatch_z);
732        }
733
734        gpu.queue.submit(std::iter::once(encoder.finish()));
735        Ok(())
736    }
737
738    /// Downloads the TSDF voxel data from the GPU.
739    pub async fn download_voxels(&self, gpu: &GpuContext) -> Result<Vec<TsdfVoxel>> {
740        let total_voxels = (self.volume.resolution[0]
741            * self.volume.resolution[1]
742            * self.volume.resolution[2]) as usize;
743        let buffer_size = (total_voxels * std::mem::size_of::<TsdfVoxel>()) as u64;
744
745        let staging_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
746            label: Some("TSDF Staging Buffer"),
747            size: buffer_size,
748            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
749            mapped_at_creation: false,
750        });
751
752        let mut encoder = gpu
753            .device
754            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
755                label: Some("TSDF Download Encoder"),
756            });
757
758        encoder.copy_buffer_to_buffer(&self.voxel_buffer, 0, &staging_buffer, 0, buffer_size);
759
760        gpu.queue.submit(std::iter::once(encoder.finish()));
761
762        let buffer_slice = staging_buffer.slice(..);
763        let (sender, receiver) = flume::unbounded();
764        buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
765            sender.send(result).unwrap();
766        });
767
768        gpu.device.poll(wgpu::PollType::Wait {
769            submission_index: None,
770            timeout: None,
771        });
772        receiver
773            .recv_async()
774            .await
775            .map_err(|_| Error::Gpu("Failed to receive mapping result".into()))??;
776
777        let data = buffer_slice.get_mapped_range();
778        // `get_mapped_range` only guarantees 8-byte alignment, but `TsdfVoxel` is
779        // `repr(align(16))`, so `cast_slice` would panic with
780        // `TargetAlignmentGreaterAndInputNotAligned`. Copy the bytes into a
781        // correctly aligned Vec instead of reinterpreting the mapped range.
782        let voxel_size = std::mem::size_of::<TsdfVoxel>();
783        let count = data.len() / voxel_size;
784        let mut result = vec![TsdfVoxel::zeroed(); count];
785        bytemuck::cast_slice_mut(&mut result).copy_from_slice(&data[..count * voxel_size]);
786
787        drop(data);
788        staging_buffer.unmap();
789
790        Ok(result)
791    }
792
793    /// Extract point cloud from TSDF volume using marching cubes
794    pub async fn extract_surface(
795        &self,
796        gpu: &GpuContext,
797        iso_value: f32,
798    ) -> Result<PointCloud<ColoredPoint3f>> {
799        let voxels = self.download_voxels(gpu).await?;
800        gpu.tsdf_extract_surface(&self.volume, &voxels, iso_value)
801            .await
802    }
803}
804
805/// Create a new TSDF volume with specified parameters
806pub fn create_tsdf_volume(
807    voxel_size: f32,
808    truncation_distance: f32,
809    resolution: [u32; 3],
810    origin: Point3<f32>,
811) -> TsdfVolume {
812    TsdfVolume {
813        voxel_size,
814        truncation_distance,
815        resolution,
816        origin,
817    }
818}
819
820/// GPU-accelerated TSDF integration from depth image
821pub async fn gpu_tsdf_integrate(
822    gpu_context: &GpuContext,
823    volume: &mut TsdfVolume,
824    depth_image: &[f32],
825    color_image: Option<&[u8]>,
826    camera_pose: &Matrix4<f32>,
827    intrinsics: &CameraIntrinsics,
828) -> Result<Vec<TsdfVoxel>> {
829    gpu_context
830        .tsdf_integrate(volume, depth_image, color_image, camera_pose, intrinsics)
831        .await
832}
833
834/// GPU-accelerated surface extraction from TSDF volume
835pub async fn gpu_tsdf_extract_surface(
836    gpu_context: &GpuContext,
837    volume: &TsdfVolume,
838    voxels: &[TsdfVoxel],
839    iso_value: f32,
840) -> Result<PointCloud<ColoredPoint3f>> {
841    gpu_context
842        .tsdf_extract_surface(volume, voxels, iso_value)
843        .await
844}
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849    use crate::device::GpuContext;
850    use approx::assert_relative_eq;
851    use nalgebra::{Matrix4, Point3};
852
853    /// Try to create a GPU context, return None if not available
854    async fn try_create_gpu_context() -> Option<GpuContext> {
855        match GpuContext::new().await {
856            Ok(gpu) => Some(gpu),
857            Err(_) => {
858                println!("⚠️  GPU not available, skipping GPU-dependent test");
859                None
860            }
861        }
862    }
863
864    /// Create simple depth image for basic testing
865    fn create_simple_depth_image(width: u32, height: u32, depth: f32) -> Vec<f32> {
866        vec![depth; (width * height) as usize]
867    }
868
869    /// Create a basic camera setup for testing
870    fn create_test_camera() -> CameraIntrinsics {
871        CameraIntrinsics {
872            fx: 525.0,
873            fy: 525.0,
874            cx: 319.5,
875            cy: 239.5,
876            width: 640,
877            height: 480,
878            depth_scale: 1.0,
879            _padding: 0.0,
880        }
881    }
882
883    /// Create identity camera pose
884    fn create_identity_pose() -> Matrix4<f32> {
885        Matrix4::new(
886            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
887        )
888    }
889
890    #[test]
891    fn test_tsdf_basic_integration() {
892        pollster::block_on(async {
893            let Some(gpu) = try_create_gpu_context().await else {
894                return;
895            };
896
897            // Create a simple TSDF volume
898            let voxel_size = 0.02; // 2cm voxels for faster processing
899            let truncation_distance = 0.1;
900            let resolution = [32, 32, 32]; // Smaller resolution for speed
901            let origin = Point3::new(-0.32, -0.32, 0.0);
902
903            let volume_params =
904                create_tsdf_volume(voxel_size, truncation_distance, resolution, origin);
905            let tsdf_volume_gpu = TsdfVolumeGpu::new(&gpu, volume_params);
906
907            // Create simple depth image with constant depth
908            let intrinsics = create_test_camera();
909            let depth_image = create_simple_depth_image(intrinsics.width, intrinsics.height, 0.5);
910            let camera_pose = create_identity_pose();
911
912            // Test integration
913            let result = tsdf_volume_gpu
914                .integrate(&gpu, &depth_image, None, &camera_pose, &intrinsics)
915                .await;
916            assert!(result.is_ok(), "TSDF integration should succeed");
917
918            // Test voxel download
919            let voxels = tsdf_volume_gpu.download_voxels(&gpu).await.unwrap();
920            assert_eq!(
921                voxels.len(),
922                (32 * 32 * 32) as usize,
923                "Should have correct number of voxels"
924            );
925
926            // Check that some voxels have been updated
927            let updated_voxels = voxels.iter().filter(|v| v.weight > 0.0).count();
928            assert!(updated_voxels > 0, "Some voxels should have been updated");
929
930            println!(
931                "✓ Basic integration test passed: {} voxels updated",
932                updated_voxels
933            );
934        });
935    }
936
937    #[test]
938    fn test_tsdf_surface_extraction() {
939        pollster::block_on(async {
940            let Some(gpu) = try_create_gpu_context().await else {
941                return;
942            };
943
944            // Create TSDF volume
945            let voxel_size = 0.02;
946            let truncation_distance = 0.1;
947            let resolution = [32, 32, 32];
948            let origin = Point3::new(-0.32, -0.32, 0.0);
949
950            let volume_params =
951                create_tsdf_volume(voxel_size, truncation_distance, resolution, origin);
952            let tsdf_volume_gpu = TsdfVolumeGpu::new(&gpu, volume_params);
953
954            // Integrate a simple depth image
955            let intrinsics = create_test_camera();
956            let depth_image = create_simple_depth_image(intrinsics.width, intrinsics.height, 0.3);
957            let camera_pose = create_identity_pose();
958
959            tsdf_volume_gpu
960                .integrate(&gpu, &depth_image, None, &camera_pose, &intrinsics)
961                .await
962                .unwrap();
963
964            // Extract surface
965            let point_cloud = tsdf_volume_gpu.extract_surface(&gpu, 0.0).await.unwrap();
966
967            // Should extract some points
968            assert!(
969                !point_cloud.points.is_empty(),
970                "Should extract surface points"
971            );
972
973            // Points should be in reasonable Z range around the depth value
974            let avg_z = point_cloud.points.iter().map(|p| p.position.z).sum::<f32>()
975                / point_cloud.points.len() as f32;
976
977            assert!(
978                avg_z > 0.2 && avg_z < 0.4,
979                "Average Z should be near depth value of 0.3"
980            );
981
982            println!(
983                "✓ Surface extraction test passed: {} points extracted, avg Z: {:.3}",
984                point_cloud.points.len(),
985                avg_z
986            );
987        });
988    }
989
990    #[test]
991    fn test_tsdf_multiple_integrations() {
992        pollster::block_on(async {
993            let Some(gpu) = try_create_gpu_context().await else {
994                return;
995            };
996
997            // Create TSDF volume
998            let voxel_size = 0.02;
999            let truncation_distance = 0.1;
1000            let resolution = [32, 32, 32];
1001            let origin = Point3::new(-0.32, -0.32, 0.0);
1002
1003            let volume_params =
1004                create_tsdf_volume(voxel_size, truncation_distance, resolution, origin);
1005            let tsdf_volume_gpu = TsdfVolumeGpu::new(&gpu, volume_params);
1006
1007            let intrinsics = create_test_camera();
1008            let camera_pose = create_identity_pose();
1009
1010            // Integrate multiple depth images
1011            let depths = [0.25, 0.3, 0.35];
1012            for &depth in &depths {
1013                let depth_image =
1014                    create_simple_depth_image(intrinsics.width, intrinsics.height, depth);
1015                tsdf_volume_gpu
1016                    .integrate(&gpu, &depth_image, None, &camera_pose, &intrinsics)
1017                    .await
1018                    .unwrap();
1019            }
1020
1021            // Check voxel weights have increased
1022            let voxels = tsdf_volume_gpu.download_voxels(&gpu).await.unwrap();
1023            let max_weight = voxels.iter().map(|v| v.weight).fold(0.0, f32::max);
1024            assert!(
1025                max_weight > 1.0,
1026                "Multiple integrations should increase voxel weights"
1027            );
1028
1029            // Extract surface
1030            let point_cloud = tsdf_volume_gpu.extract_surface(&gpu, 0.0).await.unwrap();
1031            assert!(
1032                !point_cloud.points.is_empty(),
1033                "Should extract surface after multiple integrations"
1034            );
1035
1036            println!(
1037                "✓ Multiple integration test passed: max weight {:.1}, {} points extracted",
1038                max_weight,
1039                point_cloud.points.len()
1040            );
1041        });
1042    }
1043
1044    #[test]
1045    fn test_tsdf_coordinate_system() {
1046        pollster::block_on(async {
1047            let Some(_gpu) = try_create_gpu_context().await else {
1048                return;
1049            };
1050
1051            // Test basic coordinate system consistency
1052            let voxel_size = 0.02;
1053            let resolution = [32, 32, 32];
1054            let origin = Point3::new(-0.32, -0.32, 0.0);
1055
1056            // Check volume bounds
1057            let max_coord = Point3::new(
1058                origin.x + (resolution[0] as f32) * voxel_size,
1059                origin.y + (resolution[1] as f32) * voxel_size,
1060                origin.z + (resolution[2] as f32) * voxel_size,
1061            );
1062
1063            assert_relative_eq!(max_coord.x, 0.32, epsilon = 0.01);
1064            assert_relative_eq!(max_coord.y, 0.32, epsilon = 0.01);
1065            assert_relative_eq!(max_coord.z, 0.64, epsilon = 0.01);
1066
1067            // Test camera transforms
1068            let camera_pose = create_identity_pose();
1069            let world_to_camera = camera_pose.try_inverse().unwrap();
1070
1071            let test_point = Point3::new(0.1, 0.2, 0.3);
1072            let camera_point = world_to_camera.transform_point(&test_point);
1073
1074            // For identity transform, should be the same
1075            assert_relative_eq!(test_point.x, camera_point.x, epsilon = 0.001);
1076            assert_relative_eq!(test_point.y, camera_point.y, epsilon = 0.001);
1077            assert_relative_eq!(test_point.z, camera_point.z, epsilon = 0.001);
1078
1079            println!("✓ Coordinate system test passed");
1080        });
1081    }
1082
1083    #[test]
1084    fn test_tsdf_color_integration() {
1085        pollster::block_on(async {
1086            let Some(gpu) = try_create_gpu_context().await else {
1087                return;
1088            };
1089
1090            // Create TSDF volume
1091            let voxel_size = 0.02;
1092            let truncation_distance = 0.1;
1093            let resolution = [32, 32, 32];
1094            let origin = Point3::new(-0.32, -0.32, 0.0);
1095
1096            let volume_params =
1097                create_tsdf_volume(voxel_size, truncation_distance, resolution, origin);
1098            let tsdf_volume_gpu = TsdfVolumeGpu::new(&gpu, volume_params);
1099
1100            // Create depth and color images
1101            let intrinsics = create_test_camera();
1102            let depth_image = create_simple_depth_image(intrinsics.width, intrinsics.height, 0.3);
1103
1104            // Simple red color image
1105            let pixel_count = (intrinsics.width * intrinsics.height) as usize;
1106            let mut color_image = Vec::with_capacity(pixel_count * 3);
1107            for _ in 0..pixel_count {
1108                color_image.extend_from_slice(&[255u8, 0u8, 0u8]); // RGB: red
1109            }
1110
1111            let camera_pose = create_identity_pose();
1112
1113            // Integrate with color
1114            tsdf_volume_gpu
1115                .integrate(
1116                    &gpu,
1117                    &depth_image,
1118                    Some(&color_image),
1119                    &camera_pose,
1120                    &intrinsics,
1121                )
1122                .await
1123                .unwrap();
1124
1125            // Extract surface
1126            let point_cloud = tsdf_volume_gpu.extract_surface(&gpu, 0.0).await.unwrap();
1127
1128            assert!(
1129                !point_cloud.points.is_empty(),
1130                "Should extract colored surface points"
1131            );
1132
1133            // Check that some points have red color
1134            let red_points = point_cloud
1135                .points
1136                .iter()
1137                .filter(|p| p.color[0] > 200)
1138                .count();
1139
1140            assert!(red_points > 0, "Some points should have red color");
1141
1142            println!("✓ Color integration test passed: {} red points", red_points);
1143        });
1144    }
1145}