Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

Expand All @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down
31 changes: 25 additions & 6 deletions threecrate-gpu/src/tsdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TsdfVoxel> = 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::<TsdfVoxel>();
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();
Expand Down Expand Up @@ -514,7 +521,15 @@ impl GpuContext {
rx.receive().await.unwrap()?;

let mapped_range = points_slice.get_mapped_range();
let gpu_points = bytemuck::cast_slice::<u8, GpuPoint3f>(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::<GpuPoint3f>();
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) {
Expand Down Expand Up @@ -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<TsdfVoxel> = 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::<TsdfVoxel>();
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();
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading