diff --git a/README.md b/README.md index 6425714..1f9cfe4 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,8 @@ ratio above 1 means ThreeCrate is faster than Open3D. | Workload | How ThreeCrate compares | |---|---:| | Reading files (raw float parsing) | **1.8x–2.2x faster** | -| Voxel downsampling | **1.6x–1.8x faster** | +| Voxel downsampling (CPU) | **1.6x–1.8x faster** | +| Voxel downsampling (GPU, wgpu) | **1.8x–2.9x faster** *(vs our own CPU path, not Open3D)* | | Normal estimation | 0.57x–1.09x (falls behind on big clouds) | | Single-scale ICP | 0.71x–0.99x (falls behind on big clouds) | @@ -98,6 +99,13 @@ small and medium clouds it holds its own; on large clouds it still gives up some ground on normal estimation and dense ICP. We're being upfront about that — those are the two areas we're actively working on. +About the GPU row: the compute backend is [wgpu](https://wgpu.rs/), so it runs on +any GPU (NVIDIA/AMD/Intel/Apple) with no CUDA lock-in. But to be honest about it, +**only voxel downsampling and TSDF fusion are actually faster on the GPU today.** +Normal estimation and ICP are still quicker on CPU right now (per-call pipeline +rebuilds and blocking readbacks), so we don't list them as GPU wins — that work is +[tracked openly](https://github.com/rajgandhi1/threecrate/issues/178). + One thing we won't pretend about: **we haven't benchmarked PCL yet.** The harness to do it is written and ready in [`scripts/pcl_bench/`](scripts/pcl_bench), but until we've actually run it, there are no PCL numbers here to quote. diff --git a/ROADMAP.md b/ROADMAP.md index 26ed0fb..f22a7e7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -44,8 +44,10 @@ credibility story. In rough priority order: - **Competitive GPU compute** — cache pipelines, async readbacks, and a GPU-resident spatial index so GPU knn/normals/icp beat CPU (voxel and TSDF already do). → [#178](https://github.com/rajgandhi1/threecrate/issues/178) -- **Fix the GPU TSDF buffer-cast panic** on Windows (currently `#[ignore]`d). - → [#175](https://github.com/rajgandhi1/threecrate/issues/175) +- ~~Fix the GPU TSDF buffer-cast panic~~ — **done** ([#175](https://github.com/rajgandhi1/threecrate/issues/175)). + The readback cast a mapped GPU buffer (8-byte aligned) straight into + `repr(align(16))` structs; now it copies into a correctly aligned `Vec`. All + TSDF tests pass, no `#[ignore]`. - **Broader format coverage** and streaming improvements across `threecrate-io`. - **Python API parity** with the Rust surface (`threecrate-python`). diff --git a/threecrate-gpu/src/tsdf.rs b/threecrate-gpu/src/tsdf.rs index 9fefb18..5b1416f 100644 --- a/threecrate-gpu/src/tsdf.rs +++ b/threecrate-gpu/src/tsdf.rs @@ -306,7 +306,14 @@ impl GpuContext { .map_err(|e| Error::Gpu(format!("Buffer mapping failed: {:?}", e)))?; let data = buffer_slice.get_mapped_range(); - let result: Vec = bytemuck::cast_slice(&data).to_vec(); + // `get_mapped_range` only guarantees 8-byte alignment, but `TsdfVoxel` is + // `repr(align(16))`, so `cast_slice` would panic with + // `TargetAlignmentGreaterAndInputNotAligned`. Copy the bytes into a + // correctly aligned Vec instead of reinterpreting the mapped range. + let voxel_size = std::mem::size_of::(); + let count = data.len() / voxel_size; + let mut result = vec![TsdfVoxel::zeroed(); count]; + bytemuck::cast_slice_mut(&mut result).copy_from_slice(&data[..count * voxel_size]); drop(data); staging_buffer.unmap(); @@ -514,7 +521,15 @@ impl GpuContext { rx.receive().await.unwrap()?; let mapped_range = points_slice.get_mapped_range(); - let gpu_points = bytemuck::cast_slice::(mapped_range.as_ref()); + // `GpuPoint3f` is `repr(align(16))` but `get_mapped_range` only guarantees + // 8-byte alignment, so copy into an aligned Vec rather than casting the + // mapped bytes in place (which panics with + // `TargetAlignmentGreaterAndInputNotAligned`). + let point_size = std::mem::size_of::(); + let available = mapped_range.len() / point_size; + let mut gpu_points = vec![GpuPoint3f::zeroed(); available]; + bytemuck::cast_slice_mut(&mut gpu_points) + .copy_from_slice(&mapped_range[..available * point_size]); let mut points = Vec::with_capacity(point_count); for gpu_point in gpu_points.iter().take(point_count) { @@ -760,7 +775,14 @@ impl TsdfVolumeGpu { .map_err(|_| Error::Gpu("Failed to receive mapping result".into()))??; let data = buffer_slice.get_mapped_range(); - let result: Vec = bytemuck::cast_slice(&data).to_vec(); + // `get_mapped_range` only guarantees 8-byte alignment, but `TsdfVoxel` is + // `repr(align(16))`, so `cast_slice` would panic with + // `TargetAlignmentGreaterAndInputNotAligned`. Copy the bytes into a + // correctly aligned Vec instead of reinterpreting the mapped range. + let voxel_size = std::mem::size_of::(); + let count = data.len() / voxel_size; + let mut result = vec![TsdfVoxel::zeroed(); count]; + bytemuck::cast_slice_mut(&mut result).copy_from_slice(&data[..count * voxel_size]); drop(data); staging_buffer.unmap(); @@ -913,7 +935,6 @@ mod tests { } #[test] - #[ignore = "GPU TSDF bytemuck buffer-size panic on Windows; see #175"] fn test_tsdf_surface_extraction() { pollster::block_on(async { let Some(gpu) = try_create_gpu_context().await else { @@ -967,7 +988,6 @@ mod tests { } #[test] - #[ignore = "GPU TSDF bytemuck buffer-size panic on Windows; see #175"] fn test_tsdf_multiple_integrations() { pollster::block_on(async { let Some(gpu) = try_create_gpu_context().await else { @@ -1061,7 +1081,6 @@ mod tests { } #[test] - #[ignore = "GPU TSDF bytemuck buffer-size panic on Windows; see #175"] fn test_tsdf_color_integration() { pollster::block_on(async { let Some(gpu) = try_create_gpu_context().await else {