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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ jobs:
args: -p spatialrust-filtering --features filter-mls
- name: spatialrust-filter-mls-meta
args: -p spatialrust --features filter-mls
- name: spatialrust-filter-mls-gpu
args: -p spatialrust-filtering --features filter-mls-gpu
- name: spatialrust-filter-mls-gpu-meta
args: -p spatialrust --features filter-mls-gpu
- name: spatialrust-filter-fps
args: -p spatialrust-filtering --features filter-fps
- name: spatialrust-filter-fps-meta
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ removed no sooner than the next major (see `docs/API_STABILITY.md`).

### Changed

- Add opt-in `filter-mls-gpu` smoothing for constant, linear and quadratic
surfaces. Reusable input-bound radius rows feed GPU weighted frames and
polynomial fits in three passes; explicit upload/download receipts and
per-point unchanged/plane/polynomial outcomes accompany the result. CPU tree
construction and schema/attribute reconstruction remain. Normalized f32
Cholesky uses a different rank test from CPU f64; nearly singular fits can
produce large coordinate differences, documented with adverse fixtures.
Incomplete/malformed rows or numerical errors invalidate all device output.

- Add opt-in `feature-iss-gpu` keypoint detection: shared uploaded positions feed
GPU radius graphs, compensated covariance, eigenvalue saliency and local
maxima. Explicit payload receipts cover upload and flag/status download;
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ FPFH descriptors have an explicit GPU path under `register-fpfh-gpu`:
search, SPFH and weighted FPFH on device and reports all payload transfers.
Resident APIs reuse uploaded input and neighborhoods. Capacity overflow returns
an error; GPU rounding can change angular bins near boundaries. CPU matching/
RANSAC and ordinary descriptor defaults are retained; MLS remains CPU.
RANSAC and ordinary descriptor defaults are retained.
See [FPFH measurements](notes/2026-10-12_gpu_fpfh_descriptors.md).

ISS keypoints have an explicit `feature-iss-gpu` path:
Expand All @@ -672,6 +672,16 @@ remain host stages. Capacity includes self; incomplete graphs reject.
GPU f32 arithmetic can change selection near thresholds and NMS ties compared
with CPU f64. See [ISS validation and measurements](notes/2026-10-13_gpu_iss_keypoints.md).

MLS smoothing has an explicit `filter-mls-gpu` path:
`smoother.filter_with_gpu(&runtime, &cloud, neighbor_capacity)` runs radius search,
weighted local frames and order-0/1/2 surface fits on GPU. The result preserves
schema, metadata and attributes and reports transfers, per-point modes and
unchanged/plane/polynomial counts. Resident APIs reuse uploaded input and radius
rows. CPU tree construction and output reconstruction remain host stages.
GPU f32 rank tests can choose a different fallback from CPU f64, with large
differences on nearly singular fits. See
[MLS precision and measurements](notes/2026-10-13_gpu_mls_smoothing.md).

### Library

Load or save by file extension:
Expand Down
1 change: 1 addition & 0 deletions crates/spatialrust-filtering/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ filter-outlier = ["dep:spatialrust-search"]
filter-crop = []
filter-fps = []
filter-mls = ["dep:spatialrust-search"]
filter-mls-gpu = ["filter-mls", "dep:spatialrust-gpu", "spatialrust-gpu/gpu-wgpu"]

[dependencies]
spatialrust-core = { workspace = true }
Expand Down
3 changes: 3 additions & 0 deletions crates/spatialrust-filtering/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ pub use fps::{FarthestPointSampling, FarthestPointSamplingConfig};
#[cfg(feature = "filter-mls")]
pub use mls::{MlsConfig, MlsSmoothing};

#[cfg(feature = "filter-mls-gpu")]
pub use mls::{GpuMlsFilterResult, GpuMlsPositionResult};

#[cfg(feature = "filter-outlier")]
pub use outlier::{
RadiusOutlierConfig, RadiusOutlierRemoval, StatisticalOutlierConfig, StatisticalOutlierRemoval,
Expand Down
104 changes: 104 additions & 0 deletions crates/spatialrust-filtering/src/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ impl MlsConfig {
}
}

/// Explicit GPU MLS positions, device outcomes and payload receipt.
#[cfg(feature = "filter-mls-gpu")]
#[derive(Clone, Debug, PartialEq)]
pub struct GpuMlsPositionResult {
/// Original-order explicitly downloaded GPU coordinates.
pub positions: Vec<Vec3<f32>>,
/// Per-point device outcomes aligned with positions.
pub modes: Vec<spatialrust_gpu::GpuMlsMode>,
/// Device fitting/fallback counts.
pub diagnostics: spatialrust_gpu::GpuMlsDiagnostics,
/// Tree/query upload and padded position/status download payload.
pub transfers: spatialrust_core::TransferStats,
}

/// Explicit GPU MLS filtered cloud, outcome counts and payload receipt.
#[cfg(feature = "filter-mls-gpu")]
#[derive(Clone, Debug, PartialEq)]
pub struct GpuMlsFilterResult {
/// Smoothed cloud preserving schema, metadata and non-position fields.
pub cloud: PointCloud,
/// Per-point device outcomes aligned with cloud rows.
pub modes: Vec<spatialrust_gpu::GpuMlsMode>,
/// Device fitting/fallback counts.
pub diagnostics: spatialrust_gpu::GpuMlsDiagnostics,
/// Input upload and position/status download payload.
pub transfers: spatialrust_core::TransferStats,
}

/// Moving Least Squares smoothing filter.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MlsSmoothing {
Expand Down Expand Up @@ -69,6 +97,82 @@ impl MlsSmoothing {
self.smoothed_positions_with_workers(input, parallel_worker_count(input.len()))
}

/// Explicitly uploads input, computes GPU radius neighborhoods, weighted
/// frames and order-0/1/2 polynomial fits, and downloads positions/status.
/// CPU tree construction remains; no automatic CPU fallback. Capacity includes
/// self and must fit all radius matches. GPU configuration requires finite
/// XYZ/extents and a positive finite nonzero f32 radius square, stricter than
/// CPU MLS. GPU normalized f32 math/rank tests can change coordinates and
/// fit/fallback decisions compared with CPU f64. Numerical underflow errors
/// propagate; insufficient support and singular fits use device fallbacks.
#[cfg(feature = "filter-mls-gpu")]
pub fn smoothed_positions_with_gpu(
&self,
runtime: &spatialrust_gpu::WgpuRuntime,
input: &PointCloud,
neighbor_capacity: u32,
) -> SpatialResult<GpuMlsPositionResult> {
let radius = self.config.search_radius;
if !radius.is_finite()
|| radius <= 0.0
|| !(radius * radius).is_finite()
|| radius * radius == 0.0
|| self.config.polynomial_order > 2
{
return Err(SpatialError::InvalidArgument(
"GPU MLS requires a positive finite nonzero radius square and order 0..=2"
.to_owned(),
));
}
let (x, y, z) = input.positions3()?;
if x.iter().chain(y).chain(z).any(|v| !v.is_finite()) {
return Err(SpatialError::InvalidArgument("GPU MLS requires finite XYZ".to_owned()));
}
let tree = KdTree::from_slices(x, y, z);
let device_input = spatialrust_gpu::GpuMlsInput::upload(runtime, &tree)?;
let rows = device_input.neighborhoods_resident(runtime, radius, neighbor_capacity)?;
let output = device_input.smooth_resident(
runtime,
&rows,
self.config.polynomial_order,
u32::try_from(self.config.min_neighbors).unwrap_or(u32::MAX),
)?;
let readback = output.download(runtime)?;
let mut transfers = device_input.upload_transfers();
transfers.record(
spatialrust_core::TransferDirection::DeviceToHost,
readback.transfers.device_to_host_bytes(),
);
let positions =
readback.positions.into_iter().map(|p| Vec3::new(p[0], p[1], p[2])).collect();
Ok(GpuMlsPositionResult {
positions,
modes: readback.modes,
diagnostics: readback.diagnostics,
transfers,
})
}

/// Explicit GPU MLS filtering with CPU schema/attribute reconstruction. The
/// ordinary filter trait retains CPU execution. See the position API for
/// capacity, precision and numeric-error contracts; all payload is reported.
#[cfg(feature = "filter-mls-gpu")]
pub fn filter_with_gpu(
&self,
runtime: &spatialrust_gpu::WgpuRuntime,
input: &PointCloud,
neighbor_capacity: u32,
) -> SpatialResult<GpuMlsFilterResult> {
let result = self.smoothed_positions_with_gpu(runtime, input, neighbor_capacity)?;
let cloud = build_output(input, &result.positions)?;
Ok(GpuMlsFilterResult {
cloud,
modes: result.modes,
diagnostics: result.diagnostics,
transfers: result.transfers,
})
}

fn smoothed_positions_with_workers(
&self,
input: &PointCloud,
Expand Down
Loading
Loading