From 4765ada076b92248fe1b5a69c6be708a49a8d4a7 Mon Sep 17 00:00:00 2001 From: rsasaki0109 Date: Thu, 16 Jul 2026 18:34:24 +0900 Subject: [PATCH] Restore CI compatibility gates --- .github/workflows/ci.yml | 4 +- crates/spatialrust-ai/src/mock.rs | 28 +- crates/spatialrust-arrow/src/cdata.rs | 51 ++-- crates/spatialrust-arrow/src/device.rs | 8 +- crates/spatialrust-arrow/src/lib.rs | 8 +- crates/spatialrust-arrow/src/stream.rs | 30 +- crates/spatialrust-camera/src/rgbd.rs | 67 ++--- .../src/backpressure.rs | 22 +- crates/spatialrust-distribute/src/error.rs | 4 +- crates/spatialrust-distribute/src/graph.rs | 31 +-- crates/spatialrust-distribute/src/lib.rs | 4 +- crates/spatialrust-distribute/src/transfer.rs | 13 +- crates/spatialrust-interchange/src/usd.rs | 29 +- crates/spatialrust-mapping/src/motion.rs | 7 +- crates/spatialrust-mapping/src/pose_graph.rs | 13 +- crates/spatialrust-platform/src/budget.rs | 5 +- .../spatialrust-platform/src/conformance.rs | 19 +- crates/spatialrust-platform/src/gate.rs | 14 +- crates/spatialrust-platform/src/lts.rs | 4 +- crates/spatialrust-platform/src/security.rs | 6 +- crates/spatialrust-py/src/dlpack_capsule.rs | 86 ++++-- crates/spatialrust-py/src/lib.rs | 5 +- crates/spatialrust-py/stubtest_allowlist.txt | 3 + crates/spatialrust-py/tests/test_bindings.py | 2 +- crates/spatialrust-records/src/lib.rs | 4 +- crates/spatialrust-records/src/migrate.rs | 6 +- crates/spatialrust-records/src/record.rs | 4 +- crates/spatialrust-records/src/schema.rs | 3 +- crates/spatialrust-records/src/stream.rs | 20 +- crates/spatialrust-runtime/src/diagnostics.rs | 6 +- crates/spatialrust-runtime/src/lib.rs | 8 +- crates/spatialrust-runtime/src/ros2.rs | 23 +- crates/spatialrust-scene/src/gaussian.rs | 35 +-- .../spatialrust-scene/src/marching_cubes.rs | 16 +- crates/spatialrust-scene/src/tsdf.rs | 22 +- crates/spatialrust-semantic/src/search.rs | 7 +- crates/spatialrust-sync/src/clock.rs | 6 +- crates/spatialrust-sync/src/frame_graph.rs | 15 +- crates/spatialrust-sync/src/lib.rs | 4 +- crates/spatialrust-sync/src/mcap_io.rs | 32 +-- crates/spatialrust-sync/src/replay.rs | 9 +- crates/spatialrust-tensor/src/dlpack.rs | 258 +++++++++++++++++- crates/spatialrust-tensor/src/lib.rs | 3 +- crates/spatialrust-vision/benches/geometry.rs | 14 +- crates/spatialrust-vision/src/adapters.rs | 11 +- crates/spatialrust-vision/src/analysis.rs | 8 +- crates/spatialrust-vision/src/filter.rs | 3 +- crates/spatialrust-vision/src/geometry.rs | 7 +- crates/spatialrust-vision/src/lib.rs | 32 +-- crates/spatialrust-vision/src/optical_flow.rs | 32 +-- crates/spatialrust-vision/src/pnp.rs | 52 ++-- crates/spatialrust-vision/src/stereo.rs | 49 +--- .../spatialrust/examples/north_star_demo.rs | 27 +- crates/spatialrust/src/lib.rs | 47 ++-- .../spatialrust/tests/north_star_pipeline.rs | 36 +-- .../spatialrust/tests/vision_ai_pipeline.rs | 14 +- 56 files changed, 639 insertions(+), 637 deletions(-) create mode 100644 crates/spatialrust-py/stubtest_allowlist.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7f3fc3..5644245 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -262,7 +262,9 @@ jobs: # intentionally not a production or default-CI dependency. python bench/opencv_comparison/test_report.py # Fail the build if the .pyi type stubs drift from the runtime API. - python -m mypy.stubtest spatialrust --ignore-missing-stub + python -m mypy.stubtest spatialrust --ignore-missing-stub \ + --allowlist crates/spatialrust-py/stubtest_allowlist.txt \ + --ignore-unused-allowlist vision-platform-conformance: name: Vision conformance (${{ matrix.os }}) diff --git a/crates/spatialrust-ai/src/mock.rs b/crates/spatialrust-ai/src/mock.rs index 80f6b09..cb091f8 100644 --- a/crates/spatialrust-ai/src/mock.rs +++ b/crates/spatialrust-ai/src/mock.rs @@ -67,15 +67,10 @@ impl ModelSession for MockSession { self.info.validate_inputs(&inputs)?; // Output is always a newly allocated host `TensorBuffer`. if options.output_copy == CopyPolicy::Forbid { - return Err(AiError::CopyRequired { - direction: "output host", - name: "depth".into(), - }); + return Err(AiError::CopyRequired { direction: "output host", name: "depth".into() }); } match self.profile { - MockProfile::SyntheticDepth => { - run_synthetic_depth(inputs, options.input_copy) - } + MockProfile::SyntheticDepth => run_synthetic_depth(inputs, options.input_copy), } } } @@ -110,10 +105,7 @@ impl MockProfile { } } -fn run_synthetic_depth( - inputs: NamedTensors, - input_copy: CopyPolicy, -) -> AiResult { +fn run_synthetic_depth(inputs: NamedTensors, input_copy: CopyPolicy) -> AiResult { let input = inputs.get("images").ok_or_else(|| AiError::MissingInput("images".into()))?; let descriptor = input.descriptor(); let shape = descriptor.shape(); @@ -131,10 +123,7 @@ fn run_synthetic_depth( } if !descriptor.is_c_contiguous() || descriptor.byte_offset() != 0 { if input_copy == CopyPolicy::Forbid { - return Err(AiError::CopyRequired { - direction: "input host", - name: "images".into(), - }); + return Err(AiError::CopyRequired { direction: "input host", name: "images".into() }); } return Err(AiError::Unsupported { backend: "mock".into(), @@ -192,8 +181,8 @@ fn f32_values(tensor: &TensorBuffer) -> AiResult> { mod tests { use super::{MockInferenceBackend, MockProfile}; use crate::{ - AiError, CopyPolicy, InferenceBackend, ModelSession as _, ModelSource, NamedTensors, - RunOptions, SessionOptions, + AiError, CopyPolicy, InferenceBackend, ModelSource, NamedTensors, RunOptions, + SessionOptions, }; use spatialrust_tensor::{DataType, Device, TensorBuffer, TensorDescriptor}; @@ -222,10 +211,7 @@ mod tests { let outputs = session .run_with_options( inputs, - RunOptions { - input_copy: CopyPolicy::Forbid, - output_copy: CopyPolicy::Allow, - }, + RunOptions { input_copy: CopyPolicy::Forbid, output_copy: CopyPolicy::Allow }, ) .unwrap(); let depth = outputs.get("depth").unwrap(); diff --git a/crates/spatialrust-arrow/src/cdata.rs b/crates/spatialrust-arrow/src/cdata.rs index 89d2da1..222a10c 100644 --- a/crates/spatialrust-arrow/src/cdata.rs +++ b/crates/spatialrust-arrow/src/cdata.rs @@ -149,9 +149,7 @@ pub unsafe fn import_point_cloud_c_data( ))); } if schema.n_children < 0 || array.n_children < 0 || schema.n_children != array.n_children { - return Err(ArrowBridgeError::SchemaMismatch( - "schema/array child counts disagree".into(), - )); + return Err(ArrowBridgeError::SchemaMismatch("schema/array child counts disagree".into())); } let n = schema.n_children as usize; let mut point_schema = PointSchema::new(); @@ -178,8 +176,7 @@ fn export_schema(point_schema: &PointSchema) -> ArrowBridgeResult>>()?; - let mut child_ptrs = - children.into_iter().map(Box::into_raw).collect::>(); + let mut child_ptrs = children.into_iter().map(Box::into_raw).collect::>(); let child_table = child_ptrs.as_mut_ptr(); let n_children = child_ptrs.len() as i64; // Leak the vec table into private data; release rebuilds and frees it. @@ -213,18 +210,12 @@ fn export_field_schema(field: &PointField) -> ArrowBridgeResult "Arrow C Data export currently supports scalar fields only".into(), )); } - let format = CString::new(dtype_format(field.dtype)?).map_err(|_| { - ArrowBridgeError::InvalidConfiguration("field format contained NUL".into()) - })?; - let name = CString::new(field.name.as_str()).map_err(|_| { - ArrowBridgeError::InvalidConfiguration("field name contained NUL".into()) - })?; - let private = Box::new(SchemaPrivate { - format, - name, - children_table: ptr::null_mut(), - n_children: 0, - }); + let format = CString::new(dtype_format(field.dtype)?) + .map_err(|_| ArrowBridgeError::InvalidConfiguration("field format contained NUL".into()))?; + let name = CString::new(field.name.as_str()) + .map_err(|_| ArrowBridgeError::InvalidConfiguration("field name contained NUL".into()))?; + let private = + Box::new(SchemaPrivate { format, name, children_table: ptr::null_mut(), n_children: 0 }); Ok(Box::new(ArrowSchema { format: private.format.as_ptr(), name: private.name.as_ptr(), @@ -429,9 +420,9 @@ fn format_dtype(format: &str) -> ArrowBridgeResult { "S" => Ok(DType::U16), "I" => Ok(DType::U32), "i" => Ok(DType::I32), - other => Err(ArrowBridgeError::SchemaMismatch(format!( - "unsupported Arrow format `{other}`" - ))), + other => { + Err(ArrowBridgeError::SchemaMismatch(format!("unsupported Arrow format `{other}`"))) + } } } @@ -511,11 +502,8 @@ unsafe extern "C" fn release_array(array: *mut ArrowArray) { if !array.private_data.is_null() { let private = Box::from_raw(array.private_data as *mut ArrayPrivate); if !private.children_table.is_null() && private.n_children > 0 { - let children = Vec::from_raw_parts( - private.children_table, - private.n_children, - private.n_children, - ); + let children = + Vec::from_raw_parts(private.children_table, private.n_children, private.n_children); for child in children { if !child.is_null() { if let Some(release) = (*child).release { @@ -526,11 +514,8 @@ unsafe extern "C" fn release_array(array: *mut ArrowArray) { } } if !private.buffers_table.is_null() && private.n_buffers > 0 { - let _ = Vec::from_raw_parts( - private.buffers_table as *mut *const c_void, - private.n_buffers, - private.n_buffers, - ); + let _ = + Vec::from_raw_parts(private.buffers_table, private.n_buffers, private.n_buffers); } drop(private); } @@ -561,7 +546,11 @@ mod tests { .unwrap(); let (mut schema, mut array) = export_point_cloud_c_data(&cloud).unwrap(); let imported = unsafe { - import_point_cloud_c_data(schema.as_mut_ptr(), array.as_mut_ptr(), SpatialMetadata::default()) + import_point_cloud_c_data( + schema.as_mut_ptr(), + array.as_mut_ptr(), + SpatialMetadata::default(), + ) } .unwrap(); assert_eq!(imported.len(), 2); diff --git a/crates/spatialrust-arrow/src/device.rs b/crates/spatialrust-arrow/src/device.rs index 4af4afd..29e4c73 100644 --- a/crates/spatialrust-arrow/src/device.rs +++ b/crates/spatialrust-arrow/src/device.rs @@ -5,7 +5,9 @@ use std::ptr; use spatialrust_core::{PointCloud, SpatialMetadata}; use crate::{ - cdata::{export_point_cloud_c_data, import_point_cloud_c_data, ArrowArray, ExportedArrowSchema}, + cdata::{ + export_point_cloud_c_data, import_point_cloud_c_data, ArrowArray, ExportedArrowSchema, + }, ArrowBridgeError, ArrowBridgeResult, }; @@ -112,7 +114,9 @@ pub unsafe fn import_point_cloud_device_array( #[cfg(test)] mod tests { - use super::{export_point_cloud_device_array, import_point_cloud_device_array, ArrowDeviceType}; + use super::{ + export_point_cloud_device_array, import_point_cloud_device_array, ArrowDeviceType, + }; use spatialrust_core::{ PointBuffer, PointBufferSet, PointCloud, SpatialMetadata, StandardSchemas, }; diff --git a/crates/spatialrust-arrow/src/lib.rs b/crates/spatialrust-arrow/src/lib.rs index ddccace..1814163 100644 --- a/crates/spatialrust-arrow/src/lib.rs +++ b/crates/spatialrust-arrow/src/lib.rs @@ -7,10 +7,10 @@ #[cfg(feature = "arrow-c-data")] mod cdata; -#[cfg(feature = "arrow-c-data")] -mod error; #[cfg(feature = "arrow-c-device")] mod device; +#[cfg(feature = "arrow-c-data")] +mod error; #[cfg(feature = "arrow-c-stream")] mod stream; @@ -19,12 +19,12 @@ pub use cdata::{ export_point_cloud_c_data, import_point_cloud_c_data, ArrowArray, ArrowSchema, ExportedArrowArray, ExportedArrowSchema, }; -#[cfg(feature = "arrow-c-data")] -pub use error::{ArrowBridgeError, ArrowBridgeResult}; #[cfg(feature = "arrow-c-device")] pub use device::{ export_point_cloud_device_array, import_point_cloud_device_array, ArrowDeviceArray, ArrowDeviceType, ExportedArrowDeviceArray, }; +#[cfg(feature = "arrow-c-data")] +pub use error::{ArrowBridgeError, ArrowBridgeResult}; #[cfg(feature = "arrow-c-stream")] pub use stream::{export_record_source_c_stream, ArrowArrayStream, ExportedArrowArrayStream}; diff --git a/crates/spatialrust-arrow/src/stream.rs b/crates/spatialrust-arrow/src/stream.rs index 1e4549d..485ca8c 100644 --- a/crates/spatialrust-arrow/src/stream.rs +++ b/crates/spatialrust-arrow/src/stream.rs @@ -7,17 +7,23 @@ use std::{ use spatialrust_records::SpatialRecordSource; -use crate::{cdata::{export_point_cloud_c_data, ArrowArray, ArrowSchema}, ArrowBridgeResult}; +use crate::{ + cdata::{export_point_cloud_c_data, ArrowArray, ArrowSchema}, + ArrowBridgeResult, +}; /// Arrow C Stream Interface object. #[repr(C)] pub struct ArrowArrayStream { /// Fills `out` with the stream schema. - pub get_schema: Option i32>, + pub get_schema: + Option i32>, /// Fills `out` with the next array (`release=null` when exhausted). - pub get_next: Option i32>, + pub get_next: + Option i32>, /// Optional last-error message. - pub get_last_error: Option *const c_char>, + pub get_last_error: + Option *const c_char>, /// Release callback. pub release: Option, /// Implementation private data. @@ -141,11 +147,9 @@ unsafe extern "C" fn stream_get_last_error(stream: *mut ArrowArrayStream) -> *co return ptr::null(); } match private_mut(stream) { - Ok(private) => private - .last_error - .as_ref() - .map(|value| value.as_ptr()) - .unwrap_or(ptr::null()), + Ok(private) => { + private.last_error.as_ref().map(|value| value.as_ptr()).unwrap_or(ptr::null()) + } Err(_) => ptr::null(), } } @@ -176,7 +180,9 @@ unsafe fn private_mut(stream: *mut ArrowArrayStream) -> Result<&'static mut Stre Ok(&mut *(stream.private_data as *mut StreamPrivate)) } -fn empty_cloud(schema: &spatialrust_core::PointSchema) -> ArrowBridgeResult { +fn empty_cloud( + schema: &spatialrust_core::PointSchema, +) -> ArrowBridgeResult { use spatialrust_core::{PointBuffer, PointBufferSet, PointCloud, SpatialMetadata}; let mut buffers = PointBufferSet::new(); for field in schema.fields() { @@ -214,9 +220,7 @@ mod tests { use spatialrust_core::{ PointBuffer, PointBufferSet, PointCloud, SpatialMetadata, StandardSchemas, }; - use spatialrust_records::{ - MemoryChunkSource, SchemaDescriptor, SchemaVersion, - }; + use spatialrust_records::{MemoryChunkSource, SchemaDescriptor, SchemaVersion}; #[test] fn stream_yields_chunked_clouds() { diff --git a/crates/spatialrust-camera/src/rgbd.rs b/crates/spatialrust-camera/src/rgbd.rs index 745596c..bd71bb3 100644 --- a/crates/spatialrust-camera/src/rgbd.rs +++ b/crates/spatialrust-camera/src/rgbd.rs @@ -56,11 +56,7 @@ pub struct DepthConversionOptions { impl Default for DepthConversionOptions { fn default() -> Self { - Self { - depth_scale: 1.0, - min_depth: f32::EPSILON, - max_depth: f32::INFINITY, - } + Self { depth_scale: 1.0, min_depth: f32::EPSILON, max_depth: f32::INFINITY } } } @@ -125,10 +121,7 @@ pub fn depth_to_xyz_dense( camera: &PinholeCamera, options: DepthConversionOptions, ) -> Result, RgbdError> { - let len = depth - .width() - .saturating_mul(depth.height()) - .saturating_mul(3); + let len = depth.width().saturating_mul(depth.height()).saturating_mul(3); let mut out = vec![0.0f32; len]; depth_to_xyz_dense_into(depth, camera, options, &mut out)?; Ok(out) @@ -143,10 +136,7 @@ pub fn depth_to_xyz_dense_into( ) -> Result<(), RgbdError> { validate_depth(depth, camera)?; options.validate()?; - let expected = depth - .width() - .saturating_mul(depth.height()) - .saturating_mul(3); + let expected = depth.width().saturating_mul(depth.height()).saturating_mul(3); if out.len() != expected { return Err(RgbdError::InvalidOptions(format!( "dense XYZ buffer length must be {expected}, found {}", @@ -176,13 +166,9 @@ fn fill_xyz_dense_identity( let scale_is_one = scale == 1.0; let max_is_inf = !max_d.is_finite(); // Per-column `(x - cx) / fx` factors so the inner loop is multiply-add only. - let x_mul: Vec = (0..width) - .map(|x| (x as f32 - pin.cx) * pin.inv_fx) - .collect(); + let x_mul: Vec = (0..width).map(|x| (x as f32 - pin.cx) * pin.inv_fx).collect(); let pixels = width.saturating_mul(height); - let threads = std::thread::available_parallelism() - .map(|n| n.get().clamp(1, 8)) - .unwrap_or(1); + let threads = std::thread::available_parallelism().map(|n| n.get().clamp(1, 8)).unwrap_or(1); // Thread spawn overhead dominates below ~2M pixels on typical hosts. if threads == 1 || pixels < 2_000_000 { fill_xyz_dense_identity_rows( @@ -203,7 +189,7 @@ fn fill_xyz_dense_identity( } let row_stride = width * 3; - let chunk = (height + threads - 1) / threads; + let chunk = height.div_ceil(threads); std::thread::scope(|scope| { let mut rest = out; let mut y0 = 0usize; @@ -254,9 +240,7 @@ fn fill_xyz_dense_identity_rows( if scale_is_one && max_is_inf && is_x86_feature_detected!("avx2") { // SAFETY: feature detection above; slices are row-validated. unsafe { - fill_xyz_dense_identity_rows_avx2( - depth, pin, x_mul, y0, y1, width, min_d, out, - ); + fill_xyz_dense_identity_rows_avx2(depth, pin, x_mul, y0, y1, width, min_d, out); } return; } @@ -302,10 +286,10 @@ unsafe fn fill_xyz_dense_identity_rows_avx2( min_d: f32, out: &mut [f32], ) { - #[cfg(target_arch = "x86_64")] - use std::arch::x86_64::*; #[cfg(target_arch = "x86")] use std::arch::x86::*; + #[cfg(target_arch = "x86_64")] + use std::arch::x86_64::*; let min_v = _mm256_set1_ps(min_d); let nan_v = _mm256_set1_ps(f32::NAN); @@ -340,11 +324,7 @@ unsafe fn fill_xyz_dense_identity_rows_avx2( let y_mul_s = (y as f32 - pin.cy) * pin.inv_fy; while x < width { let meters = *row.get_unchecked(x); - let z = if meters >= min_d { - meters - } else { - f32::NAN - }; + let z = if meters >= min_d { meters } else { f32::NAN }; *out.get_unchecked_mut(o) = *x_mul.get_unchecked(x) * z; *out.get_unchecked_mut(o + 1) = y_mul_s * z; *out.get_unchecked_mut(o + 2) = z; @@ -434,9 +414,7 @@ fn pack_xyz_identity( ) { let pin = FastPinholeF32::from_camera(camera); let width = depth.width(); - let x_mul: Vec = (0..width) - .map(|x| (x as f32 - pin.cx) * pin.inv_fx) - .collect(); + let x_mul: Vec = (0..width).map(|x| (x as f32 - pin.cx) * pin.inv_fx).collect(); let scale = options.depth_scale; let min_d = options.min_depth; let max_d = options.max_depth; @@ -459,11 +437,8 @@ fn pack_xyz_identity( } else { *row.get_unchecked(x) * scale }; - let valid = if max_is_inf { - meters >= min_d - } else { - meters >= min_d && meters <= max_d - }; + let valid = + if max_is_inf { meters >= min_d } else { meters >= min_d && meters <= max_d }; if !valid { continue; } @@ -559,9 +534,7 @@ fn pack_xyzrgb_identity( ) { let pin = FastPinholeF32::from_camera(camera); let width = depth.width(); - let x_mul: Vec = (0..width) - .map(|x| (x as f32 - pin.cx) * pin.inv_fx) - .collect(); + let x_mul: Vec = (0..width).map(|x| (x as f32 - pin.cx) * pin.inv_fx).collect(); let scale = options.depth_scale; let min_d = options.min_depth; let max_d = options.max_depth; @@ -637,11 +610,8 @@ fn pack_xyzrgb_identity_raw( } else { *depth_row.get_unchecked(x) * scale }; - let valid = if max_is_inf { - meters >= min_d - } else { - meters >= min_d && meters <= max_d - }; + let valid = + if max_is_inf { meters >= min_d } else { meters >= min_d && meters <= max_d }; if !valid { continue; } @@ -706,10 +676,7 @@ mod tests { let color = Image::::try_new(2, 2, vec![10, 11, 12, 20, 21, 22, 30, 31, 32, 40, 41, 42]) .unwrap(); - let options = DepthConversionOptions { - max_depth: 2.0, - ..Default::default() - }; + let options = DepthConversionOptions { max_depth: 2.0, ..Default::default() }; let cloud = rgbd_to_point_cloud(depth.view(), color.view(), &camera(), options).unwrap(); assert_eq!(cloud.len(), 2); assert_eq!(cloud.field("r").unwrap(), &PointBuffer::U8(vec![10, 40])); diff --git a/crates/spatialrust-distribute/src/backpressure.rs b/crates/spatialrust-distribute/src/backpressure.rs index 4366161..63f1f34 100644 --- a/crates/spatialrust-distribute/src/backpressure.rs +++ b/crates/spatialrust-distribute/src/backpressure.rs @@ -30,10 +30,7 @@ impl BackpressurePolicy { "require 0 < soft_limit <= hard_limit".into(), )); } - Ok(Self { - soft_limit, - hard_limit, - }) + Ok(Self { soft_limit, hard_limit }) } /// Evaluates queue depth against watermarks. @@ -62,12 +59,7 @@ impl BoundedTransferQueue { /// Creates an empty queue. #[must_use] pub fn new(policy: BackpressurePolicy) -> Self { - Self { - policy, - items: Vec::new(), - soft_trips: 0, - hard_rejects: 0, - } + Self { policy, items: Vec::new(), soft_trips: 0, hard_rejects: 0 } } /// Current depth. @@ -149,14 +141,8 @@ mod tests { fn soft_and_hard_limits() { let policy = BackpressurePolicy::try_new(1, 2).unwrap(); let mut queue = BoundedTransferQueue::new(policy); - assert_eq!( - queue.try_push(sample("t0")).unwrap(), - BackpressureSignal::SoftLimit - ); - assert_eq!( - queue.try_push(sample("t1")).unwrap(), - BackpressureSignal::SoftLimit - ); + assert_eq!(queue.try_push(sample("t0")).unwrap(), BackpressureSignal::SoftLimit); + assert_eq!(queue.try_push(sample("t1")).unwrap(), BackpressureSignal::SoftLimit); assert!(queue.try_push(sample("t2")).is_err()); assert_eq!(queue.hard_rejects(), 1); assert_eq!(queue.pop().unwrap().name, "t0"); diff --git a/crates/spatialrust-distribute/src/error.rs b/crates/spatialrust-distribute/src/error.rs index 815dd25..79034bd 100644 --- a/crates/spatialrust-distribute/src/error.rs +++ b/crates/spatialrust-distribute/src/error.rs @@ -16,9 +16,7 @@ pub enum DistributeError { #[error("partition graph contains a cycle")] CycleDetected, /// Transfer queue reached its hard backpressure limit. - #[error( - "transfer queue `{queue}` at capacity: depth {depth} >= hard_limit {hard_limit}" - )] + #[error("transfer queue `{queue}` at capacity: depth {depth} >= hard_limit {hard_limit}")] CapacityExceeded { /// Queue / transfer name used for diagnostics. queue: String, diff --git a/crates/spatialrust-distribute/src/graph.rs b/crates/spatialrust-distribute/src/graph.rs index 0400f2c..ee93a03 100644 --- a/crates/spatialrust-distribute/src/graph.rs +++ b/crates/spatialrust-distribute/src/graph.rs @@ -37,9 +37,7 @@ impl ExecutionPartition { )); } if nodes.iter().any(|n| n.is_empty()) { - return Err(DistributeError::InvalidConfiguration( - "node ids must be non-empty".into(), - )); + return Err(DistributeError::InvalidConfiguration("node ids must be non-empty".into())); } Ok(Self { id, nodes }) } @@ -82,16 +80,18 @@ impl PartitionGraph { } /// Connects two partitions with a directed edge. - pub fn connect(&mut self, from: impl Into, to: impl Into) -> DistributeResult<()> { + pub fn connect( + &mut self, + from: impl Into, + to: impl Into, + ) -> DistributeResult<()> { let from = from.into(); let to = to.into(); if !self.partitions.contains_key(&from) || !self.partitions.contains_key(&to) { return Err(DistributeError::Missing("partition endpoint".into())); } if from == to { - return Err(DistributeError::InvalidConfiguration( - "self-edges are not allowed".into(), - )); + return Err(DistributeError::InvalidConfiguration("self-edges are not allowed".into())); } if self.edges.iter().any(|(a, b)| a == &from && b == &to) { return Ok(()); @@ -121,18 +121,13 @@ impl PartitionGraph { /// Finds which partition owns a node id. #[must_use] pub fn partition_of_node(&self, node_id: &str) -> Option<&ExecutionPartition> { - self.partitions - .values() - .find(|partition| partition.contains(node_id)) + self.partitions.values().find(|partition| partition.contains(node_id)) } /// Returns outgoing neighbors of a partition. #[must_use] pub fn successors(&self, id: &str) -> Vec<&str> { - self.edges - .iter() - .filter_map(|(from, to)| (from == id).then_some(to.as_str())) - .collect() + self.edges.iter().filter_map(|(from, to)| (from == id).then_some(to.as_str())).collect() } /// Returns a topological order of partitions, or errors on cycles / missing nodes. @@ -148,7 +143,8 @@ impl PartitionGraph { let mut queue: VecDeque = indegree .iter() - .filter_map(|(id, deg)| (*deg == 0).then(|| (*id).to_string())) + .filter(|(_, deg)| **deg == 0) + .map(|(id, _)| (*id).to_string()) .collect(); // Stable order for deterministic schedules. let mut queued: HashSet = queue.iter().cloned().collect(); @@ -211,9 +207,6 @@ mod tests { .unwrap(); graph.connect("a", "b").unwrap(); graph.connect("b", "a").unwrap(); - assert!(matches!( - graph.topological_order(), - Err(crate::DistributeError::CycleDetected) - )); + assert!(matches!(graph.topological_order(), Err(crate::DistributeError::CycleDetected))); } } diff --git a/crates/spatialrust-distribute/src/lib.rs b/crates/spatialrust-distribute/src/lib.rs index 3f5fe58..b55e248 100644 --- a/crates/spatialrust-distribute/src/lib.rs +++ b/crates/spatialrust-distribute/src/lib.rs @@ -14,6 +14,4 @@ mod transfer; pub use backpressure::{BackpressurePolicy, BackpressureSignal, BoundedTransferQueue}; pub use error::{DistributeError, DistributeResult}; pub use graph::{ExecutionNode, ExecutionPartition, PartitionGraph}; -pub use transfer::{ - NamedTransfer, TransferDirection, TransferKind, TransferLedger, TransferPlan, -}; +pub use transfer::{NamedTransfer, TransferDirection, TransferKind, TransferLedger, TransferPlan}; diff --git a/crates/spatialrust-distribute/src/transfer.rs b/crates/spatialrust-distribute/src/transfer.rs index 31f8a2a..a0c83d1 100644 --- a/crates/spatialrust-distribute/src/transfer.rs +++ b/crates/spatialrust-distribute/src/transfer.rs @@ -64,14 +64,7 @@ impl NamedTransfer { "transfer endpoints must differ".into(), )); } - Ok(Self { - name, - direction, - kind, - from, - to, - bytes, - }) + Ok(Self { name, direction, kind, from, to, bytes }) } /// Bytes counted as measurable copies (zero-copy handoffs are 0). @@ -173,9 +166,7 @@ impl TransferLedger { #[cfg(test)] mod tests { - use super::{ - NamedTransfer, TransferDirection, TransferKind, TransferLedger, TransferPlan, - }; + use super::{NamedTransfer, TransferDirection, TransferKind, TransferLedger, TransferPlan}; use crate::{ExecutionPartition, PartitionGraph}; #[test] diff --git a/crates/spatialrust-interchange/src/usd.rs b/crates/spatialrust-interchange/src/usd.rs index c66bb71..634ebbb 100644 --- a/crates/spatialrust-interchange/src/usd.rs +++ b/crates/spatialrust-interchange/src/usd.rs @@ -110,7 +110,8 @@ pub fn export_stage_usda(stage: &MemoryUsdStageAdapter) -> InterchangeResult InterchangeResult InterchangeResult InterchangeResult<(UsdPrimPath, TriangleMesh)> { if !usda.contains("#usda") { - return Err(InterchangeError::InvalidConfiguration( - "missing USDA header".into(), - )); + return Err(InterchangeError::InvalidConfiguration("missing USDA header".into())); } - let path = extract_quoted_after(usda, "spatialrust:primPath = ") - .or_else(|_| { - let leaf = extract_mesh_leaf(usda)?; - UsdPrimPath::try_new(format!("/World/{leaf}")) - })?; + let path = extract_quoted_after(usda, "spatialrust:primPath = ").or_else(|_| { + let leaf = extract_mesh_leaf(usda)?; + UsdPrimPath::try_new(format!("/World/{leaf}")) + })?; let points_blob = extract_bracket_list(usda, "point3f[] points = ")?; let indices_blob = extract_bracket_list(usda, "int[] faceVertexIndices = ")?; let mut positions = Vec::new(); @@ -209,9 +204,7 @@ fn extract_quoted_after(usda: &str, marker: &str) -> InterchangeResult MappingResult; + fn estimate(&self, previous: &StampedPose, current: &StampedPose) + -> MappingResult; } /// Synthetic odometry that trusts successive pose stamps and emits their delta. diff --git a/crates/spatialrust-mapping/src/pose_graph.rs b/crates/spatialrust-mapping/src/pose_graph.rs index b2f195e..b80c06a 100644 --- a/crates/spatialrust-mapping/src/pose_graph.rs +++ b/crates/spatialrust-mapping/src/pose_graph.rs @@ -96,14 +96,14 @@ impl PoseGraph { let Some(from_pose) = self.nodes.get(&edge.from.0).cloned() else { continue; }; - let predicted = spatialrust_math::Pose3::new( - edge.to_t_from.compose(from_pose.pose.isometry), - ); + let predicted = + spatialrust_math::Pose3::new(edge.to_t_from.compose(from_pose.pose.isometry)); let Some(existing) = self.nodes.get_mut(&edge.to.0) else { continue; }; - let delta = (existing.pose.isometry.translation() - predicted.isometry.translation()) - .length(); + let delta = (existing.pose.isometry.translation() + - predicted.isometry.translation()) + .length(); if delta > 1e-4 { existing.pose = predicted; // Keep target stamp; overwrite pose only. @@ -125,7 +125,8 @@ impl PoseGraph { for j in (i + 1)..ids.len() { let a = &self.nodes[&ids[i]]; let b = &self.nodes[&ids[j]]; - let delta = (a.pose.isometry.translation() - b.pose.isometry.translation()).length(); + let delta = + (a.pose.isometry.translation() - b.pose.isometry.translation()).length(); if delta <= max_distance { out.push((PoseNodeId(ids[i].clone()), PoseNodeId(ids[j].clone()))); } diff --git a/crates/spatialrust-platform/src/budget.rs b/crates/spatialrust-platform/src/budget.rs index 0012fdf..865ca73 100644 --- a/crates/spatialrust-platform/src/budget.rs +++ b/crates/spatialrust-platform/src/budget.rs @@ -60,10 +60,7 @@ impl PerformanceBudgetReport { /// Records one measurement. pub fn sample(&mut self, budget_id: impl Into, observed: u64) { - self.samples.push(PerformanceSample { - budget_id: budget_id.into(), - observed, - }); + self.samples.push(PerformanceSample { budget_id: budget_id.into(), observed }); } /// Returns budgets. diff --git a/crates/spatialrust-platform/src/conformance.rs b/crates/spatialrust-platform/src/conformance.rs index d693516..23d94d5 100644 --- a/crates/spatialrust-platform/src/conformance.rs +++ b/crates/spatialrust-platform/src/conformance.rs @@ -44,11 +44,7 @@ impl ConformanceReport { status: ConformanceStatus, detail: Option, ) { - self.cases.push(ConformanceCase { - id: id.into(), - status, - detail, - }); + self.cases.push(ConformanceCase { id: id.into(), status, detail }); } /// Returns cases. @@ -82,12 +78,7 @@ impl ConformanceReport { /// Compact summary string for logs/docs. #[must_use] pub fn summary(&self) -> String { - format!( - "pass={} fail={} skip={}", - self.pass_count(), - self.fail_count(), - self.skip_count() - ) + format!("pass={} fail={} skip={}", self.pass_count(), self.fail_count(), self.skip_count()) } /// Fails if any case failed. @@ -109,11 +100,7 @@ mod tests { fn rejects_failures_and_summarizes() { let mut report = ConformanceReport::new(); report.record("arrow-roundtrip", ConformanceStatus::Pass, None); - report.record( - "mcap-optional", - ConformanceStatus::Skip, - Some("feature off".into()), - ); + report.record("mcap-optional", ConformanceStatus::Skip, Some("feature off".into())); assert!(report.assert_no_failures().is_ok()); assert_eq!(report.summary(), "pass=1 fail=0 skip=1"); report.record("bad", ConformanceStatus::Fail, None); diff --git a/crates/spatialrust-platform/src/gate.rs b/crates/spatialrust-platform/src/gate.rs index 506c7b1..48d3589 100644 --- a/crates/spatialrust-platform/src/gate.rs +++ b/crates/spatialrust-platform/src/gate.rs @@ -75,10 +75,7 @@ impl ReleaseGate { && self.budgets.is_none() { reasons.push("release gate has no configured surfaces".into()); - return ReleaseGateDecision { - allowed: false, - reasons, - }; + return ReleaseGateDecision { allowed: false, reasons }; } if let Some(stability) = &self.stability { @@ -129,10 +126,7 @@ impl ReleaseGate { } } - ReleaseGateDecision { - allowed: reasons.is_empty(), - reasons, - } + ReleaseGateDecision { allowed: reasons.is_empty(), reasons } } /// Convenience wrapper returning [`PlatformResult`]. @@ -141,9 +135,7 @@ impl ReleaseGate { if decision.allowed { Ok(()) } else { - Err(PlatformError::ReleaseGateDenied { - reasons: decision.reasons, - }) + Err(PlatformError::ReleaseGateDenied { reasons: decision.reasons }) } } } diff --git a/crates/spatialrust-platform/src/lts.rs b/crates/spatialrust-platform/src/lts.rs index 2c429e1..8058bea 100644 --- a/crates/spatialrust-platform/src/lts.rs +++ b/crates/spatialrust-platform/src/lts.rs @@ -46,9 +46,7 @@ impl LtsPolicy { /// Looks up a major line window. #[must_use] pub fn window_for(&self, major_line: &str) -> Option<&SupportWindow> { - self.windows - .iter() - .find(|window| window.major_line == major_line) + self.windows.iter().find(|window| window.major_line == major_line) } /// Default SpatialRust 1.x policy used by Epic 100. diff --git a/crates/spatialrust-platform/src/security.rs b/crates/spatialrust-platform/src/security.rs index 1c665b2..8365528 100644 --- a/crates/spatialrust-platform/src/security.rs +++ b/crates/spatialrust-platform/src/security.rs @@ -52,11 +52,7 @@ impl SecurityChecklist { /// Returns unsatisfied item ids. #[must_use] pub fn unsatisfied_ids(&self) -> Vec<&str> { - self.items - .iter() - .filter(|item| !item.satisfied) - .map(|item| item.id.as_str()) - .collect() + self.items.iter().filter(|item| !item.satisfied).map(|item| item.id.as_str()).collect() } /// Returns items. diff --git a/crates/spatialrust-py/src/dlpack_capsule.rs b/crates/spatialrust-py/src/dlpack_capsule.rs index d4c114e..a97911c 100644 --- a/crates/spatialrust-py/src/dlpack_capsule.rs +++ b/crates/spatialrust-py/src/dlpack_capsule.rs @@ -1,12 +1,17 @@ //! Audited CPython capsule boundary for DLPack ownership transfer. use pyo3::{ - exceptions::PyBufferError, + exceptions::{PyBufferError, PyTypeError}, prelude::*, types::{PyAny, PyDict}, }; -use spatialrust::tensor::{release_dlpack_raw, DlpackExport, DlpackImport, TensorBuffer}; +use spatialrust::tensor::{ + release_dlpack_legacy_raw, release_dlpack_raw, DlpackExport, DlpackImport, + DlpackLegacyExport, TensorBuffer, +}; +const LEGACY_NAME: &[u8] = b"dltensor\0"; +const USED_LEGACY_NAME: &[u8] = b"used_dltensor\0"; const VERSIONED_NAME: &[u8] = b"dltensor_versioned\0"; const USED_VERSIONED_NAME: &[u8] = b"used_dltensor_versioned\0"; @@ -21,20 +26,47 @@ unsafe extern "C" fn capsule_destructor(capsule: *mut pyo3::ffi::PyObject) { // SAFETY: the unconsumed capsule uniquely owns deleter responsibility. unsafe { release_dlpack_raw(raw) }; } + return; + } + let legacy_name = LEGACY_NAME.as_ptr().cast(); + // SAFETY: this only validates the legacy capsule/name pair. + if unsafe { pyo3::ffi::PyCapsule_IsValid(capsule, legacy_name) } == 1 { + // SAFETY: validity above proves the name and capsule pointer contract. + let raw = unsafe { pyo3::ffi::PyCapsule_GetPointer(capsule, legacy_name) }; + if !raw.is_null() { + // SAFETY: the unconsumed capsule uniquely owns deleter responsibility. + unsafe { release_dlpack_legacy_raw(raw) }; + } } } -pub(crate) fn export_tensor(py: Python<'_>, tensor: &TensorBuffer) -> PyResult> { - let export = DlpackExport::from_tensor(tensor) - .map_err(|error| PyBufferError::new_err(error.to_string()))?; - let raw = export.into_raw(); +pub(crate) fn export_tensor( + py: Python<'_>, + tensor: &TensorBuffer, + versioned: bool, +) -> PyResult> { + let (raw, name) = if versioned { + let export = DlpackExport::from_tensor(tensor) + .map_err(|error| PyBufferError::new_err(error.to_string()))?; + (export.into_raw(), VERSIONED_NAME) + } else { + let export = DlpackLegacyExport::from_tensor(tensor) + .map_err(|error| PyBufferError::new_err(error.to_string()))?; + (export.into_raw(), LEGACY_NAME) + }; // SAFETY: `raw` is live and the static nul-terminated name outlives the capsule. let capsule = unsafe { - pyo3::ffi::PyCapsule_New(raw, VERSIONED_NAME.as_ptr().cast(), Some(capsule_destructor)) + pyo3::ffi::PyCapsule_New(raw, name.as_ptr().cast(), Some(capsule_destructor)) }; if capsule.is_null() { // SAFETY: capsule construction failed, so ownership was not transferred. - unsafe { release_dlpack_raw(raw) }; + unsafe { + if versioned { + release_dlpack_raw(raw); + } else { + release_dlpack_legacy_raw(raw); + } + }; return Err(PyErr::fetch(py)); } // SAFETY: PyCapsule_New returned one new owned Python reference. @@ -45,22 +77,44 @@ pub(crate) fn import_tensor(producer: &Bound<'_, PyAny>) -> PyResult capsule, + Err(error) if error.is_instance_of::(producer.py()) => { + producer.call_method0("__dlpack__")? + } + Err(error) => return Err(error), + }; let name = VERSIONED_NAME.as_ptr().cast(); // SAFETY: this only asks CPython to validate the exact capsule/name pair. - if unsafe { pyo3::ffi::PyCapsule_IsValid(capsule.as_ptr(), name) } != 1 { - return Err(PyBufferError::new_err("producer did not return a dltensor_versioned capsule")); + if unsafe { pyo3::ffi::PyCapsule_IsValid(capsule.as_ptr(), name) } == 1 { + // SAFETY: capsule validity proves that the payload is non-null for this name. + let raw = unsafe { pyo3::ffi::PyCapsule_GetPointer(capsule.as_ptr(), name) }; + // SAFETY: both names are static nul-terminated strings and capsule is valid. + if unsafe { + pyo3::ffi::PyCapsule_SetName(capsule.as_ptr(), USED_VERSIONED_NAME.as_ptr().cast()) + } != 0 + { + return Err(PyErr::fetch(producer.py())); + } + // SAFETY: renaming transferred exclusive deleter ownership from the capsule. + return unsafe { DlpackImport::from_raw(raw) } + .map_err(|error| PyBufferError::new_err(error.to_string())); + } + let legacy_name = LEGACY_NAME.as_ptr().cast(); + if unsafe { pyo3::ffi::PyCapsule_IsValid(capsule.as_ptr(), legacy_name) } != 1 { + return Err(PyBufferError::new_err( + "producer did not return a dltensor or dltensor_versioned capsule", + )); } // SAFETY: capsule validity proves that the payload is non-null for this name. - let raw = unsafe { pyo3::ffi::PyCapsule_GetPointer(capsule.as_ptr(), name) }; + let raw = unsafe { pyo3::ffi::PyCapsule_GetPointer(capsule.as_ptr(), legacy_name) }; // SAFETY: both names are static nul-terminated strings and capsule is valid. - if unsafe { - pyo3::ffi::PyCapsule_SetName(capsule.as_ptr(), USED_VERSIONED_NAME.as_ptr().cast()) - } != 0 + if unsafe { pyo3::ffi::PyCapsule_SetName(capsule.as_ptr(), USED_LEGACY_NAME.as_ptr().cast()) } + != 0 { return Err(PyErr::fetch(producer.py())); } // SAFETY: renaming transferred exclusive deleter ownership from the capsule. - unsafe { DlpackImport::from_raw(raw) } + unsafe { DlpackImport::from_legacy_raw(raw) } .map_err(|error| PyBufferError::new_err(error.to_string())) } diff --git a/crates/spatialrust-py/src/lib.rs b/crates/spatialrust-py/src/lib.rs index f8f6227..7789242 100644 --- a/crates/spatialrust-py/src/lib.rs +++ b/crates/spatialrust-py/src/lib.rs @@ -433,7 +433,7 @@ impl PyTensor { "implicit DLPack copies are disabled; call Tensor.copy() explicitly", )); } - dlpack_capsule::export_tensor(py, &self.inner).map_err(to_py_err) + dlpack_capsule::export_tensor(py, &self.inner, max_version.is_some()).map_err(to_py_err) } /// Makes an explicit host-to-host allocation copy. @@ -2611,6 +2611,7 @@ struct PyMorphologyWorkspace { #[pymethods] impl PyMorphologyWorkspace { #[new] + #[pyo3(signature = ())] fn new() -> Self { Self { inner: RectMorphologyWorkspace::new(), element: None } } @@ -3033,6 +3034,7 @@ struct PyCannyWorkspace { #[pymethods] impl PyCannyWorkspace { #[new] + #[pyo3(signature = ())] fn new() -> Self { Self { inner: CannyWorkspace::new() } } @@ -3997,6 +3999,7 @@ struct PyDistanceTransformWorkspace { #[pymethods] impl PyDistanceTransformWorkspace { #[new] + #[pyo3(signature = ())] fn new() -> Self { Self { inner: DistanceTransformWorkspace::new() } } diff --git a/crates/spatialrust-py/stubtest_allowlist.txt b/crates/spatialrust-py/stubtest_allowlist.txt new file mode 100644 index 0000000..3b2ebba --- /dev/null +++ b/crates/spatialrust-py/stubtest_allowlist.txt @@ -0,0 +1,3 @@ +spatialrust\.CannyWorkspace\.__init__ +spatialrust\.DistanceTransformWorkspace\.__init__ +spatialrust\.MorphologyWorkspace\.__init__ diff --git a/crates/spatialrust-py/tests/test_bindings.py b/crates/spatialrust-py/tests/test_bindings.py index 0ec1af9..06f004c 100644 --- a/crates/spatialrust-py/tests/test_bindings.py +++ b/crates/spatialrust-py/tests/test_bindings.py @@ -984,7 +984,7 @@ def test_tensor_zero_copy_dlpack_import_retains_producer(dtype): imported = sr.tensor_view_from_dlpack(source) assert imported.shape == [3, 4] assert imported.dtype == np.dtype(dtype).name - assert imported.version[0] == 1 + assert imported.version in {(0, 0), (1, 0)} del source copied = np.from_dlpack(imported.copy()) np.testing.assert_array_equal(copied, np.arange(12, dtype=dtype).reshape(3, 4)) diff --git a/crates/spatialrust-records/src/lib.rs b/crates/spatialrust-records/src/lib.rs index 9c323f7..bc4a5a8 100644 --- a/crates/spatialrust-records/src/lib.rs +++ b/crates/spatialrust-records/src/lib.rs @@ -18,6 +18,4 @@ pub use record::SpatialRecord; pub use schema::{ compare_schemas, CompatVerdict, SchemaCompatReport, SchemaDescriptor, SchemaId, SchemaVersion, }; -pub use stream::{ - MemoryChunkSink, MemoryChunkSource, SpatialRecordSink, SpatialRecordSource, -}; +pub use stream::{MemoryChunkSink, MemoryChunkSource, SpatialRecordSink, SpatialRecordSource}; diff --git a/crates/spatialrust-records/src/migrate.rs b/crates/spatialrust-records/src/migrate.rs index e4d4d19..f69e2f6 100644 --- a/crates/spatialrust-records/src/migrate.rs +++ b/crates/spatialrust-records/src/migrate.rs @@ -68,9 +68,9 @@ pub fn migrate_record( if let Ok(source) = record.cloud().field(&field.name) { buffers.insert(field.name.clone(), clone_buffer(source)?); } else { - let fill = policy.fill_missing.ok_or_else(|| { - RecordsError::MissingField(field.name.clone()) - })?; + let fill = policy + .fill_missing + .ok_or_else(|| RecordsError::MissingField(field.name.clone()))?; buffers.insert(field.name.clone(), filled_buffer(field, len, fill)?); } } diff --git a/crates/spatialrust-records/src/record.rs b/crates/spatialrust-records/src/record.rs index 112a833..6f7a08d 100644 --- a/crates/spatialrust-records/src/record.rs +++ b/crates/spatialrust-records/src/record.rs @@ -62,7 +62,9 @@ impl SpatialRecord { mod tests { use super::SpatialRecord; use crate::SchemaVersion; - use spatialrust_core::{PointBuffer, PointBufferSet, PointCloud, SpatialMetadata, StandardSchemas}; + use spatialrust_core::{ + PointBuffer, PointBufferSet, PointCloud, SpatialMetadata, StandardSchemas, + }; #[test] fn record_rejects_schema_mismatch() { diff --git a/crates/spatialrust-records/src/schema.rs b/crates/spatialrust-records/src/schema.rs index 5822feb..b5cfe3c 100644 --- a/crates/spatialrust-records/src/schema.rs +++ b/crates/spatialrust-records/src/schema.rs @@ -237,8 +237,7 @@ mod tests { rebuilt = rebuilt.with_field(field); } } - let actual = - SchemaDescriptor::try_new("point", SchemaVersion::new(1, 0), rebuilt).unwrap(); + let actual = SchemaDescriptor::try_new("point", SchemaVersion::new(1, 0), rebuilt).unwrap(); assert_eq!(compare_schemas(&expected, &actual).verdict, CompatVerdict::Incompatible); } } diff --git a/crates/spatialrust-records/src/stream.rs b/crates/spatialrust-records/src/stream.rs index 22341ae..40a1dff 100644 --- a/crates/spatialrust-records/src/stream.rs +++ b/crates/spatialrust-records/src/stream.rs @@ -1,8 +1,6 @@ //! Chunked spatial-record sources and sinks. -use spatialrust_core::{ - PointBuffer, PointBufferSet, PointCloud, SpatialMetadata, SpatialTensor, -}; +use spatialrust_core::{PointBuffer, PointBufferSet, PointCloud, SpatialMetadata, SpatialTensor}; use crate::{RecordsError, RecordsResult, SchemaDescriptor, SpatialRecord}; @@ -51,13 +49,7 @@ impl MemoryChunkSource { )); } cloud.validate()?; - Ok(Self { - metadata: cloud.metadata().clone(), - schema, - cloud, - chunk_size, - offset: 0, - }) + Ok(Self { metadata: cloud.metadata().clone(), schema, cloud, chunk_size, offset: 0 }) } /// Creates a source using [`SpatialTensor`]’s default chunk size as a hint. @@ -160,11 +152,15 @@ fn slice_cloud( let source = cloud.field(&field.name)?; buffers.insert(field.name.clone(), slice_buffer(source, &range)?); } - let chunk = PointCloud::try_from_parts(schema.point_schema().clone(), buffers, metadata.clone())?; + let chunk = + PointCloud::try_from_parts(schema.point_schema().clone(), buffers, metadata.clone())?; SpatialRecord::try_new(schema.clone(), chunk) } -fn slice_buffer(buffer: &PointBuffer, range: &std::ops::Range) -> RecordsResult { +fn slice_buffer( + buffer: &PointBuffer, + range: &std::ops::Range, +) -> RecordsResult { Ok(match buffer { PointBuffer::F32(values) => PointBuffer::F32(values[range.clone()].to_vec()), PointBuffer::F64(values) => PointBuffer::F64(values[range.clone()].to_vec()), diff --git a/crates/spatialrust-runtime/src/diagnostics.rs b/crates/spatialrust-runtime/src/diagnostics.rs index b39566e..2332b92 100644 --- a/crates/spatialrust-runtime/src/diagnostics.rs +++ b/crates/spatialrust-runtime/src/diagnostics.rs @@ -23,10 +23,6 @@ impl FailureDiagnostic { summary: impl Into, remediation: Option, ) -> Self { - Self { - code: DiagnosticCode(code.into()), - summary: summary.into(), - remediation, - } + Self { code: DiagnosticCode(code.into()), summary: summary.into(), remediation } } } diff --git a/crates/spatialrust-runtime/src/lib.rs b/crates/spatialrust-runtime/src/lib.rs index 77e8616..12365b3 100644 --- a/crates/spatialrust-runtime/src/lib.rs +++ b/crates/spatialrust-runtime/src/lib.rs @@ -8,23 +8,23 @@ mod diagnostics; mod error; -mod pipeline; -mod trace; #[cfg(feature = "execution-graph")] mod graph; +mod pipeline; +mod trace; #[cfg(feature = "ros2")] mod ros2; pub use diagnostics::{DiagnosticCode, FailureDiagnostic}; pub use error::{RuntimeError, RuntimeResult}; -pub use pipeline::{BoundedPipeline, PipelineConfig, PipelineStage}; -pub use trace::{TraceEvent, TraceLevel, TraceLog}; #[cfg(feature = "execution-graph")] pub use graph::{ CompiledSpatialGraph, ExecutionReceipt, FnOperator, GraphNodeSpec, GraphOperator, SpatialExecutionGraph, }; +pub use pipeline::{BoundedPipeline, PipelineConfig, PipelineStage}; +pub use trace::{TraceEvent, TraceLevel, TraceLog}; #[cfg(feature = "ros2")] pub use ros2::{ diff --git a/crates/spatialrust-runtime/src/ros2.rs b/crates/spatialrust-runtime/src/ros2.rs index 7cea0e3..0c41388 100644 --- a/crates/spatialrust-runtime/src/ros2.rs +++ b/crates/spatialrust-runtime/src/ros2.rs @@ -89,12 +89,7 @@ impl PointCloud2Xyz { "xyz length must be a multiple of 3".into(), )); } - Ok(Self { - frame_id: frame_id.into(), - stamp_sec, - stamp_nanosec, - xyz, - }) + Ok(Self { frame_id: frame_id.into(), stamp_sec, stamp_nanosec, xyz }) } /// Returns point count. @@ -152,12 +147,7 @@ pub fn decode_point_cloud2_xyz(bytes: &[u8]) -> RuntimeResult { let data = r.read_bytes(data_len)?; let _is_dense = r.read_bool()?; if height == 0 || width == 0 { - return Ok(PointCloud2Xyz { - frame_id, - stamp_sec, - stamp_nanosec, - xyz: Vec::new(), - }); + return Ok(PointCloud2Xyz { frame_id, stamp_sec, stamp_nanosec, xyz: Vec::new() }); } if point_step < 12 { return Err(RuntimeError::InvalidConfiguration( @@ -379,16 +369,13 @@ mod tests { #[test] fn negotiates_point_cloud2() { let adapter = CatalogRos2Adapter::point_cloud2_xyz(); - assert_eq!( - adapter.negotiate(POINT_CLOUD2_TYPE).unwrap().spatial_topic, - "point/xyz" - ); + assert_eq!(adapter.negotiate(POINT_CLOUD2_TYPE).unwrap().spatial_topic, "point/xyz"); } #[test] fn roundtrips_xyz_cdr_and_loopback() { - let msg = PointCloud2Xyz::try_new("lidar", 1, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]) - .unwrap(); + let msg = + PointCloud2Xyz::try_new("lidar", 1, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(); let bytes = encode_point_cloud2_xyz(&msg).unwrap(); let mut node = LoopbackRos2Node::new(); node.publish("/points", bytes.clone()); diff --git a/crates/spatialrust-scene/src/gaussian.rs b/crates/spatialrust-scene/src/gaussian.rs index 572b1ec..cd99f75 100644 --- a/crates/spatialrust-scene/src/gaussian.rs +++ b/crates/spatialrust-scene/src/gaussian.rs @@ -1,4 +1,4 @@ -//! Feature-gated Gaussian scene primitives and CPU soft-splat renderer. +//! Feature-gated Gaussian scene primitives and CPU soft-splat renderer. use spatialrust_math::{Quat, Vec3}; @@ -60,14 +60,10 @@ impl GaussianScene { fn validate_primitive(primitive: &GaussianPrimitive) -> SceneResult<()> { if !(0.0..=1.0).contains(&primitive.opacity) { - return Err(SceneError::InvalidConfiguration( - "opacity must be in [0, 1]".into(), - )); + return Err(SceneError::InvalidConfiguration("opacity must be in [0, 1]".into())); } if primitive.color.iter().any(|c| !(0.0..=1.0).contains(c)) { - return Err(SceneError::InvalidConfiguration( - "color channels must be in [0, 1]".into(), - )); + return Err(SceneError::InvalidConfiguration("color channels must be in [0, 1]".into())); } if !(primitive.scale.x.is_finite() && primitive.scale.y.is_finite() @@ -151,14 +147,10 @@ pub fn render_gaussians_cpu( camera: &GaussianCamera, ) -> SceneResult { if camera.width == 0 || camera.height == 0 { - return Err(SceneError::InvalidConfiguration( - "camera dimensions must be non-zero".into(), - )); + return Err(SceneError::InvalidConfiguration("camera dimensions must be non-zero".into())); } if !(camera.fx.is_finite() && camera.fy.is_finite() && camera.fx > 0.0 && camera.fy > 0.0) { - return Err(SceneError::InvalidConfiguration( - "fx/fy must be finite and > 0".into(), - )); + return Err(SceneError::InvalidConfiguration("fx/fy must be finite and > 0".into())); } let rot = camera.rotation_camera_from_world.normalize().to_mat3(); @@ -182,11 +174,7 @@ pub fn render_gaussians_cpu( color: prim.color, }); } - projected.sort_by(|a, b| { - a.depth - .partial_cmp(&b.depth) - .unwrap_or(std::cmp::Ordering::Equal) - }); + projected.sort_by(|a, b| a.depth.partial_cmp(&b.depth).unwrap_or(std::cmp::Ordering::Equal)); let pixels = (camera.width as usize) * (camera.height as usize); let mut color = vec![0.0f32; pixels * 3]; @@ -225,11 +213,7 @@ pub fn render_gaussians_cpu( rgba.push(to_u8(color[i * 3 + 2])); rgba.push(to_u8(alpha[i])); } - Ok(GaussianFramebuffer { - width: camera.width, - height: camera.height, - rgba, - }) + Ok(GaussianFramebuffer { width: camera.width, height: camera.height, rgba }) } struct ProjectedSplat { @@ -247,9 +231,7 @@ fn to_u8(v: f32) -> u8 { #[cfg(test)] mod tests { - use super::{ - render_gaussians_cpu, GaussianCamera, GaussianPrimitive, GaussianScene, - }; + use super::{render_gaussians_cpu, GaussianCamera, GaussianPrimitive, GaussianScene}; use spatialrust_math::{Quat, Vec3}; #[test] @@ -287,4 +269,3 @@ mod tests { assert!(err.to_string().contains("scale")); } } - diff --git a/crates/spatialrust-scene/src/marching_cubes.rs b/crates/spatialrust-scene/src/marching_cubes.rs index 942716d..caefde3 100644 --- a/crates/spatialrust-scene/src/marching_cubes.rs +++ b/crates/spatialrust-scene/src/marching_cubes.rs @@ -3,14 +3,8 @@ use spatialrust_math::Vec3; /// Consistent six-tetrahedra covering of the unit cube (corner indices). -const TETS: [[usize; 4]; 6] = [ - [0, 2, 3, 7], - [0, 6, 2, 7], - [0, 4, 6, 7], - [0, 6, 1, 2], - [0, 1, 6, 4], - [5, 6, 1, 4], -]; +const TETS: [[usize; 4]; 6] = + [[0, 2, 3, 7], [0, 6, 2, 7], [0, 4, 6, 7], [0, 6, 1, 2], [0, 1, 6, 4], [5, 6, 1, 4]]; /// Appends triangles for one tetrahedron to `positions` / `indices`. pub(crate) fn polygonise_tet( @@ -92,11 +86,7 @@ fn interp(isolevel: f32, p1: Vec3, p2: Vec3, v1: f32, v2: f32) -> Vec3 return p1; } let t = (isolevel - v1) / (v2 - v1); - Vec3::new( - p1.x + t * (p2.x - p1.x), - p1.y + t * (p2.y - p1.y), - p1.z + t * (p2.z - p1.z), - ) + Vec3::new(p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y), p1.z + t * (p2.z - p1.z)) } #[inline] diff --git a/crates/spatialrust-scene/src/tsdf.rs b/crates/spatialrust-scene/src/tsdf.rs index 4275e57..041d45b 100644 --- a/crates/spatialrust-scene/src/tsdf.rs +++ b/crates/spatialrust-scene/src/tsdf.rs @@ -30,7 +30,7 @@ impl TsdfVolume { if !(truncation.is_finite() && truncation > 0.0) { return Err(SceneError::InvalidConfiguration("truncation must be > 0".into())); } - if dims.iter().any(|d| *d == 0) { + if dims.contains(&0) { return Err(SceneError::InvalidConfiguration("dims must be non-zero".into())); } let len = dims[0].saturating_mul(dims[1]).saturating_mul(dims[2]); @@ -83,7 +83,9 @@ impl TsdfVolume { /// Integrates every XYZ triple from interleaved storage. pub fn integrate_xyz(&mut self, xyz: &[f32], sensor_origin: Vec3) -> SceneResult<()> { if xyz.len() % 3 != 0 { - return Err(SceneError::InvalidConfiguration("xyz length must be a multiple of 3".into())); + return Err(SceneError::InvalidConfiguration( + "xyz length must be a multiple of 3".into(), + )); } for chunk in xyz.chunks_exact(3) { self.integrate_point(Vec3::new(chunk[0], chunk[1], chunk[2]), sensor_origin); @@ -169,16 +171,8 @@ impl TsdfVolume { } } -const CORNER_OFFSETS: [[usize; 3]; 8] = [ - [0, 0, 0], - [1, 0, 0], - [1, 1, 0], - [0, 1, 0], - [0, 0, 1], - [1, 0, 1], - [1, 1, 1], - [0, 1, 1], -]; +const CORNER_OFFSETS: [[usize; 3]; 8] = + [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]]; #[cfg(test)] mod tests { @@ -189,9 +183,7 @@ mod tests { fn integrates_and_extracts_non_empty_mesh() { let mut volume = TsdfVolume::try_new(Vec3::new(-1.0, -1.0, -1.0), 0.25, [8, 8, 8], 0.5).unwrap(); - volume - .integrate_xyz(&[0.0, 0.0, 0.0, 0.2, 0.0, 0.0], Vec3::new(0.0, 0.0, -1.0)) - .unwrap(); + volume.integrate_xyz(&[0.0, 0.0, 0.0, 0.2, 0.0, 0.0], Vec3::new(0.0, 0.0, -1.0)).unwrap(); let mesh = volume.extract_mesh(0.5); assert!(!mesh.positions.is_empty()); assert_eq!(mesh.indices.len() % 3, 0); diff --git a/crates/spatialrust-semantic/src/search.rs b/crates/spatialrust-semantic/src/search.rs index d25c1f4..d84388d 100644 --- a/crates/spatialrust-semantic/src/search.rs +++ b/crates/spatialrust-semantic/src/search.rs @@ -1,6 +1,8 @@ //! Multimodal fusion scoring and brute-force semantic search. -use crate::{cosine_similarity, Embedding, EntityId, SemanticEntity, SemanticError, SemanticResult}; +use crate::{ + cosine_similarity, Embedding, EntityId, SemanticEntity, SemanticError, SemanticResult, +}; /// Weighted fusion of embedding similarity and label confidence. #[derive(Clone, Copy, Debug, PartialEq)] @@ -74,7 +76,8 @@ impl SemanticSearchIndex { .iter() .map(|entity| fusion.score(entity, query).map(|score| (entity.id.clone(), score))) .collect::>>()?; - scored.sort_by(|a, b| b.1.score.partial_cmp(&a.1.score).unwrap_or(std::cmp::Ordering::Equal)); + scored + .sort_by(|a, b| b.1.score.partial_cmp(&a.1.score).unwrap_or(std::cmp::Ordering::Equal)); scored.truncate(k); Ok(scored) } diff --git a/crates/spatialrust-sync/src/clock.rs b/crates/spatialrust-sync/src/clock.rs index 52c4774..52d57a5 100644 --- a/crates/spatialrust-sync/src/clock.rs +++ b/crates/spatialrust-sync/src/clock.rs @@ -81,11 +81,7 @@ pub struct StampedTime { impl StampedTime { /// Creates a stamped time with exact sync quality. #[must_use] - pub fn exact( - clock: impl Into, - domain: ClockDomain, - timestamp: Timestamp, - ) -> Self { + pub fn exact(clock: impl Into, domain: ClockDomain, timestamp: Timestamp) -> Self { Self { clock: clock.into(), domain, timestamp, quality: SyncQuality::exact() } } diff --git a/crates/spatialrust-sync/src/frame_graph.rs b/crates/spatialrust-sync/src/frame_graph.rs index 58c2166..eb87785 100644 --- a/crates/spatialrust-sync/src/frame_graph.rs +++ b/crates/spatialrust-sync/src/frame_graph.rs @@ -55,10 +55,7 @@ impl FrameGraph { .entry(edge.child.0.clone()) .or_default() .retain(|(parent, _)| parent != &edge.parent.0); - self.reverse - .entry(edge.child.0.clone()) - .or_default() - .push((edge.parent.0, parent_t_child)); + self.reverse.entry(edge.child.0.clone()).or_default().push((edge.parent.0, parent_t_child)); Ok(()) } @@ -109,14 +106,20 @@ mod tests { .insert_edge(FrameEdge { parent: FrameId::new("base"), child: FrameId::new("sensor"), - child_t_parent: Isometry3::new(Quat::new(0.0, 0.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0)), + child_t_parent: Isometry3::new( + Quat::new(0.0, 0.0, 0.0, 1.0), + Vec3::new(1.0, 0.0, 0.0), + ), }) .unwrap(); graph .insert_edge(FrameEdge { parent: FrameId::new("sensor"), child: FrameId::new("lidar"), - child_t_parent: Isometry3::new(Quat::new(0.0, 0.0, 0.0, 1.0), Vec3::new(0.0, 2.0, 0.0)), + child_t_parent: Isometry3::new( + Quat::new(0.0, 0.0, 0.0, 1.0), + Vec3::new(0.0, 2.0, 0.0), + ), }) .unwrap(); let t = graph.lookup(&FrameId::new("base"), &FrameId::new("lidar")).unwrap(); diff --git a/crates/spatialrust-sync/src/lib.rs b/crates/spatialrust-sync/src/lib.rs index a61ea00..b441c8c 100644 --- a/crates/spatialrust-sync/src/lib.rs +++ b/crates/spatialrust-sync/src/lib.rs @@ -18,9 +18,7 @@ mod mcap_io; pub use clock::{ClockDomain, ClockId, StampedTime, SyncQuality}; pub use error::{SyncError, SyncResult}; pub use frame_graph::{FrameEdge, FrameGraph}; -pub use replay::{ - DeterministicReplayer, EpisodeIndex, MemoryEpisode, SyncWindow, TopicId, -}; +pub use replay::{DeterministicReplayer, EpisodeIndex, MemoryEpisode, SyncWindow, TopicId}; pub use stamped::StampedRecord; #[cfg(feature = "mcap")] diff --git a/crates/spatialrust-sync/src/mcap_io.rs b/crates/spatialrust-sync/src/mcap_io.rs index 208cf1d..bee05f3 100644 --- a/crates/spatialrust-sync/src/mcap_io.rs +++ b/crates/spatialrust-sync/src/mcap_io.rs @@ -36,12 +36,13 @@ const ENCODING: &str = "application/x-spatialrust-xyz-v1"; const SCHEMA_NAME: &str = "spatialrust/xyz/v1"; /// Writes a [`MemoryEpisode`] of XYZ records into an MCAP file. -pub fn write_memory_episode_mcap(path: impl AsRef, episode: &MemoryEpisode) -> SyncResult<()> { +pub fn write_memory_episode_mcap( + path: impl AsRef, + episode: &MemoryEpisode, +) -> SyncResult<()> { let file = File::create(path.as_ref()).map_err(io_err)?; let mut writer = Writer::new(BufWriter::new(file)).map_err(mcap_err)?; - let schema_id = writer - .add_schema(SCHEMA_NAME, ENCODING, &[]) - .map_err(mcap_err)?; + let schema_id = writer.add_schema(SCHEMA_NAME, ENCODING, &[]).map_err(mcap_err)?; let mut channels = BTreeMap::::new(); for (sequence, stamped) in episode.records().iter().enumerate() { @@ -50,12 +51,7 @@ pub fn write_memory_episode_mcap(path: impl AsRef, episode: &MemoryEpisode *id } else { let id = writer - .add_channel( - schema_id, - &topic, - ENCODING, - &BTreeMap::new(), - ) + .add_channel(schema_id, &topic, ENCODING, &BTreeMap::new()) .map_err(mcap_err)?; channels.insert(topic, id); id @@ -159,22 +155,14 @@ fn decode_stamped_xyz(message: &Message<'_>) -> SyncResult { buffers, SpatialMetadata::default(), )?; - let record = SpatialRecord::try_from_cloud( - schema_id, - SchemaVersion::new(major, minor), - cloud, - )?; + let record = SpatialRecord::try_from_cloud(schema_id, SchemaVersion::new(major, minor), cloud)?; let stamp = StampedTime { clock: ClockId::new(message.channel.topic.clone()), domain, timestamp: Timestamp::from_nanos(stamp_ns), quality: crate::SyncQuality::exact(), }; - Ok(StampedRecord::new( - TopicId::new(message.channel.topic.clone()), - stamp, - record, - )) + Ok(StampedRecord::new(TopicId::new(message.channel.topic.clone()), stamp, record)) } fn domain_byte(domain: ClockDomain) -> u8 { @@ -192,9 +180,7 @@ fn byte_domain(value: u8) -> SyncResult { 1 => Ok(ClockDomain::HostWall), 2 => Ok(ClockDomain::Sensor), 3 => Ok(ClockDomain::External), - other => Err(SyncError::InvalidConfiguration(format!( - "unknown clock domain byte {other}" - ))), + other => Err(SyncError::InvalidConfiguration(format!("unknown clock domain byte {other}"))), } } diff --git a/crates/spatialrust-sync/src/replay.rs b/crates/spatialrust-sync/src/replay.rs index fb86883..7a0f082 100644 --- a/crates/spatialrust-sync/src/replay.rs +++ b/crates/spatialrust-sync/src/replay.rs @@ -61,10 +61,8 @@ impl EpisodeIndex { pub fn build(records: &[StampedRecord]) -> Self { let mut entries = BTreeMap::new(); for (ordinal, record) in records.iter().enumerate() { - entries.insert( - (record.stamp.as_nanos(), record.topic.0.clone(), ordinal as u64), - ordinal, - ); + entries + .insert((record.stamp.as_nanos(), record.topic.0.clone(), ordinal as u64), ordinal); } Self { entries } } @@ -203,7 +201,8 @@ mod tests { SpatialMetadata::default(), ) .unwrap(); - let record = SpatialRecord::try_from_cloud("point", SchemaVersion::new(1, 0), cloud).unwrap(); + let record = + SpatialRecord::try_from_cloud("point", SchemaVersion::new(1, 0), cloud).unwrap(); StampedRecord::new( topic, StampedTime::exact("host", ClockDomain::HostSteady, Timestamp::from_nanos(nanos)), diff --git a/crates/spatialrust-tensor/src/dlpack.rs b/crates/spatialrust-tensor/src/dlpack.rs index 4bd2d71..2773a37 100644 --- a/crates/spatialrust-tensor/src/dlpack.rs +++ b/crates/spatialrust-tensor/src/dlpack.rs @@ -109,6 +109,13 @@ struct RawManagedTensorVersioned { dl_tensor: RawTensor, } +#[repr(C)] +struct RawManagedTensorLegacy { + dl_tensor: RawTensor, + manager_ctx: *mut c_void, + deleter: Option, +} + struct ExportContext { _allocation: TensorStorage, _shape: Box<[i64]>, @@ -123,6 +130,13 @@ pub struct DlpackExport { raw: NonNull, } +/// Owner for a legacy DLPack managed tensor exported without copying CPU data. +/// +/// This exists for consumers implementing the pre-1.0 capsule protocol. +pub struct DlpackLegacyExport { + raw: NonNull, +} + /// Calls the producer deleter for a raw pointer previously returned by /// [`DlpackExport::into_raw`]. /// @@ -135,6 +149,17 @@ pub unsafe fn release_dlpack_raw(raw: *mut c_void) { unsafe { call_deleter(raw.cast()) }; } +/// Calls the producer deleter for a legacy raw pointer. +/// +/// # Safety +/// +/// `raw` must carry exclusive deleter responsibility for a live +/// legacy `DLManagedTensor*`. It must not be used after this call. +pub unsafe fn release_dlpack_legacy_raw(raw: *mut c_void) { + // SAFETY: forwarded from the public ownership contract above. + unsafe { call_legacy_deleter(raw.cast()) }; +} + impl DlpackExport { /// Shares an owned CPU tensor allocation with a DLPack consumer without copying. pub fn from_tensor(tensor: &TensorBuffer) -> Result { @@ -209,10 +234,36 @@ impl Drop for DlpackExport { } } +impl DlpackLegacyExport { + /// Shares an owned CPU tensor allocation with a legacy DLPack consumer. + pub fn from_tensor(tensor: &TensorBuffer) -> Result { + let (context, tensor) = build_export(tensor)?; + let raw = Box::new(RawManagedTensorLegacy { + dl_tensor: tensor, + manager_ctx: Box::into_raw(context).cast(), + deleter: Some(delete_legacy_export), + }); + Ok(Self { raw: NonNull::from(Box::leak(raw)) }) + } + + /// Transfers deleter responsibility to an external DLPack consumer. + pub fn into_raw(self) -> *mut c_void { + let this = ManuallyDrop::new(self); + this.raw.as_ptr().cast() + } +} + +impl Drop for DlpackLegacyExport { + fn drop(&mut self) { + // SAFETY: this export uniquely owns deleter responsibility. + unsafe { call_legacy_deleter(self.raw.as_ptr()) }; + } +} + /// Validated owner of a DLPack producer's managed tensor. #[derive(Debug)] pub struct DlpackImport { - raw: NonNull, + raw: ImportedRaw, descriptor: TensorDescriptor, allocation_len: usize, data: *const u8, @@ -220,6 +271,12 @@ pub struct DlpackImport { flags: u64, } +#[derive(Debug)] +enum ImportedRaw { + Versioned(NonNull), + Legacy(NonNull), +} + impl DlpackImport { /// Takes deleter ownership of a DLPack managed tensor and validates its host view. /// @@ -300,7 +357,7 @@ impl DlpackImport { if range.end != 0 && tensor.data.is_null() { return Err(DlpackError::NullData); } - let raw = guard.disarm(); + let raw = ImportedRaw::Versioned(guard.disarm()); Ok(Self { raw, descriptor, @@ -311,6 +368,22 @@ impl DlpackImport { }) } + /// Takes deleter ownership of a legacy DLPack managed tensor. + /// + /// # Safety + /// + /// `raw` must be a live, exclusively transferred legacy `DLManagedTensor*`. + pub unsafe fn from_legacy_raw(raw: *mut c_void) -> Result { + let raw = NonNull::new(raw.cast::()) + .ok_or(DlpackError::NullManagedTensor)?; + let guard = IncomingLegacyGuard { raw: Some(raw) }; + // SAFETY: the caller promises a live legacy managed tensor. + let managed = unsafe { raw.as_ref() }; + let (descriptor, allocation_len, data) = validate_tensor(&managed.dl_tensor)?; + let raw = ImportedRaw::Legacy(guard.disarm()); + Ok(Self { raw, descriptor, allocation_len, data, version: (0, 0), flags: 0 }) + } + /// Returns producer ABI major and minor versions. pub const fn version(&self) -> (u32, u32) { self.version @@ -341,8 +414,13 @@ impl DlpackImport { impl Drop for DlpackImport { fn drop(&mut self) { - // SAFETY: this owner received exclusive deleter responsibility in `from_raw`. - unsafe { call_deleter(self.raw.as_ptr()) }; + // SAFETY: this owner received exclusive deleter responsibility. + unsafe { + match self.raw { + ImportedRaw::Versioned(raw) => call_deleter(raw.as_ptr()), + ImportedRaw::Legacy(raw) => call_legacy_deleter(raw.as_ptr()), + } + }; } } @@ -365,6 +443,25 @@ impl Drop for IncomingGuard { } } +struct IncomingLegacyGuard { + raw: Option>, +} + +impl IncomingLegacyGuard { + fn disarm(mut self) -> NonNull { + self.raw.take().expect("incoming pointer is present") + } +} + +impl Drop for IncomingLegacyGuard { + fn drop(&mut self) { + if let Some(raw) = self.raw { + // SAFETY: the guard owns the transferred pointer on error paths. + unsafe { call_legacy_deleter(raw.as_ptr()) }; + } + } +} + unsafe extern "C" fn delete_export(raw: *mut RawManagedTensorVersioned) { if raw.is_null() { return; @@ -378,6 +475,18 @@ unsafe extern "C" fn delete_export(raw: *mut RawManagedTensorVersioned) { } } +unsafe extern "C" fn delete_legacy_export(raw: *mut RawManagedTensorLegacy) { + if raw.is_null() { + return; + } + // SAFETY: installed only for allocations built by `DlpackLegacyExport`. + let managed = unsafe { Box::from_raw(raw) }; + if !managed.manager_ctx.is_null() { + // SAFETY: manager_ctx was created with Box::into_raw for ExportContext. + drop(unsafe { Box::from_raw(managed.manager_ctx.cast::()) }); + } +} + unsafe fn call_deleter(raw: *mut RawManagedTensorVersioned) { if raw.is_null() { return; @@ -389,6 +498,121 @@ unsafe fn call_deleter(raw: *mut RawManagedTensorVersioned) { } } +unsafe fn call_legacy_deleter(raw: *mut RawManagedTensorLegacy) { + if raw.is_null() { + return; + } + // SAFETY: caller owns a live legacy managed-tensor pointer. + if let Some(deleter) = unsafe { (*raw).deleter } { + // SAFETY: deleter belongs to this exact managed tensor. + unsafe { deleter(raw) }; + } +} + +fn build_export(tensor: &TensorBuffer) -> Result<(Box, RawTensor), DlpackError> { + let descriptor = tensor.descriptor(); + if !descriptor.device().is_host_accessible() { + return Err(TensorError::DeviceNotHostAccessible(descriptor.device()).into()); + } + let shape = descriptor + .shape() + .iter() + .map(|&dimension| i64::try_from(dimension).map_err(|_| DlpackError::IntegerConversion)) + .collect::, _>>()? + .into_boxed_slice(); + let strides = match descriptor.strides() { + Some(values) => values + .iter() + .map(|&stride| i64::try_from(stride).map_err(|_| DlpackError::IntegerConversion)) + .collect::, _>>()?, + None => compact_strides(descriptor.shape())?, + } + .into_boxed_slice(); + let ndim = + i32::try_from(descriptor.shape().len()).map_err(|_| DlpackError::IntegerConversion)?; + let byte_offset = + u64::try_from(descriptor.byte_offset()).map_err(|_| DlpackError::IntegerConversion)?; + let allocation = tensor.shared_allocation(); + let data = + if allocation.is_empty() { ptr::null_mut() } else { allocation.as_ptr().cast_mut().cast() }; + let shape_ptr = if shape.is_empty() { ptr::null_mut() } else { shape.as_ptr().cast_mut() }; + let strides_ptr = + if strides.is_empty() { ptr::null_mut() } else { strides.as_ptr().cast_mut() }; + let context = + Box::new(ExportContext { _allocation: allocation, _shape: shape, _strides: strides }); + let tensor = RawTensor { + data, + device: encode_device(descriptor.device()), + ndim, + dtype: encode_dtype(descriptor.dtype()), + shape: shape_ptr, + strides: strides_ptr, + byte_offset, + }; + Ok((context, tensor)) +} + +fn validate_tensor( + tensor: &RawTensor, +) -> Result<(TensorDescriptor, usize, *const u8), DlpackError> { + if tensor.ndim < 0 || tensor.ndim as usize > MAX_RANK { + return Err(DlpackError::InvalidRank(tensor.ndim)); + } + let rank = tensor.ndim as usize; + if rank != 0 && tensor.shape.is_null() { + return Err(DlpackError::NullShape); + } + let shape_values = if rank == 0 { + &[][..] + } else { + // SAFETY: the producer contract supplies `ndim` readable shape entries. + unsafe { slice::from_raw_parts(tensor.shape, rank) } + }; + let shape = shape_values + .iter() + .map(|&dimension| { + usize::try_from(dimension).map_err(|_| DlpackError::InvalidDimension(dimension)) + }) + .collect::, _>>()?; + let strides = if tensor.strides.is_null() { + None + } else { + // SAFETY: the producer contract supplies `ndim` readable stride entries. + let values = unsafe { slice::from_raw_parts(tensor.strides, rank) }; + Some( + values + .iter() + .map(|&stride| isize::try_from(stride).map_err(|_| DlpackError::IntegerConversion)) + .collect::, _>>()?, + ) + }; + let dtype = decode_dtype(tensor.dtype)?; + let device = decode_device(tensor.device)?; + let byte_offset = + usize::try_from(tensor.byte_offset).map_err(|_| DlpackError::IntegerConversion)?; + let descriptor = match strides { + Some(strides) => TensorDescriptor::try_strided(dtype, shape, strides, byte_offset, device)?, + None => { + let mut descriptor = TensorDescriptor::contiguous(dtype, shape, device); + if byte_offset != 0 { + descriptor = TensorDescriptor::try_strided( + dtype, + descriptor.shape().to_vec(), + compact_strides_isize(descriptor.shape())?, + byte_offset, + device, + )?; + } + descriptor + } + }; + let range = descriptor.required_byte_range()?; + if range.end != 0 && tensor.data.is_null() { + return Err(DlpackError::NullData); + } + Ok((descriptor, range.end, tensor.data.cast())) +} + fn compact_strides(shape: &[usize]) -> Result, DlpackError> { let mut output = vec![0; shape.len()]; let mut stride = 1_i64; @@ -488,7 +712,9 @@ fn decode_device(raw: RawDevice) -> Result { #[cfg(test)] mod tests { - use super::{DlpackError, DlpackExport, DlpackImport, DLPACK_MAJOR, DLPACK_MINOR}; + use super::{ + DlpackError, DlpackExport, DlpackImport, DlpackLegacyExport, DLPACK_MAJOR, DLPACK_MINOR, + }; use crate::{DataType, Device, TensorBuffer, TensorDescriptor}; #[test] @@ -526,6 +752,28 @@ mod tests { assert_eq!(view.allocation_bytes(), &[10, 20, 30, 40]); } + #[test] + fn legacy_cpu_roundtrip_is_zero_copy() { + let tensor = TensorBuffer::try_new( + (0_u8..12).collect(), + TensorDescriptor::contiguous(DataType::U8, vec![3, 4], Device::CPU), + ) + .unwrap(); + let original = tensor.allocation_bytes().as_ptr(); + let allocation = tensor.shared_allocation(); + let export = DlpackLegacyExport::from_tensor(&tensor).unwrap(); + assert_eq!(allocation.strong_count(), 3); + // SAFETY: into_raw transfers the live legacy export exactly once. + let imported = unsafe { DlpackImport::from_legacy_raw(export.into_raw()) }.unwrap(); + assert_eq!(imported.version(), (0, 0)); + assert_eq!(imported.flags(), 0); + let view = imported.view().unwrap(); + assert_eq!(view.descriptor().shape(), &[3, 4]); + assert_eq!(view.allocation_bytes().as_ptr(), original); + drop(imported); + assert_eq!(allocation.strong_count(), 2); + } + #[test] fn major_mismatch_is_rejected_and_deleted() { let tensor = TensorBuffer::try_new( diff --git a/crates/spatialrust-tensor/src/lib.rs b/crates/spatialrust-tensor/src/lib.rs index 505b49d..e2757dd 100644 --- a/crates/spatialrust-tensor/src/lib.rs +++ b/crates/spatialrust-tensor/src/lib.rs @@ -25,7 +25,8 @@ pub use spatial::{spatial_f32_field_view, SpatialTensorBridgeError}; mod dlpack; #[cfg(feature = "dlpack")] pub use dlpack::{ - release_dlpack_raw, DlpackError, DlpackExport, DlpackImport, DLPACK_MAJOR, DLPACK_MINOR, + release_dlpack_legacy_raw, release_dlpack_raw, DlpackError, DlpackExport, DlpackImport, + DlpackLegacyExport, DLPACK_MAJOR, DLPACK_MINOR, }; /// Tensor construction and layout errors. diff --git a/crates/spatialrust-vision/benches/geometry.rs b/crates/spatialrust-vision/benches/geometry.rs index 6c9128f..42cab60 100644 --- a/crates/spatialrust-vision/benches/geometry.rs +++ b/crates/spatialrust-vision/benches/geometry.rs @@ -42,10 +42,7 @@ fn benchmark_geometry(c: &mut Criterion) { for &count in &[64usize, 256, 1024] { let pairs = (0..count) .map(|index| { - let source = Vec2 { - x: (index % 32) as f64 * 10.0, - y: (index / 32) as f64 * 8.0, - }; + let source = Vec2 { x: (index % 32) as f64 * 10.0, y: (index / 32) as f64 * 8.0 }; PointCorrespondence2::try_new( source, Vec2 { x: source.x * 1.05 + 2.0, y: source.y * 0.98 - 1.0 }, @@ -117,8 +114,13 @@ fn benchmark_geometry(c: &mut Criterion) { lk.bench_function(BenchmarkId::from_parameter(name), |b| { b.iter(|| { black_box( - track_points_lucas_kanade(left.view(), next.view(), &points, Default::default()) - .unwrap(), + track_points_lucas_kanade( + left.view(), + next.view(), + &points, + Default::default(), + ) + .unwrap(), ) }); }); diff --git a/crates/spatialrust-vision/src/adapters.rs b/crates/spatialrust-vision/src/adapters.rs index abcffaf..bcc7953 100644 --- a/crates/spatialrust-vision/src/adapters.rs +++ b/crates/spatialrust-vision/src/adapters.rs @@ -95,11 +95,7 @@ pub fn detection_tensor_to_detections(tensor: &TensorBuffer) -> VisionResult::try_new(2, 2, (0..12).map(|v| v as f32).collect()).unwrap(); let tensor = planar_f32_to_nchw(&planar).unwrap(); assert_eq!(tensor.descriptor().shape(), &[1, 3, 2, 2]); - assert_eq!( - bytemuck::cast_slice::(tensor.allocation_bytes()), - planar.as_slice() - ); + assert_eq!(bytemuck::cast_slice::(tensor.allocation_bytes()), planar.as_slice()); } #[test] diff --git a/crates/spatialrust-vision/src/analysis.rs b/crates/spatialrust-vision/src/analysis.rs index dab4965..64af621 100644 --- a/crates/spatialrust-vision/src/analysis.rs +++ b/crates/spatialrust-vision/src/analysis.rs @@ -501,11 +501,9 @@ fn clip_histogram(histogram: &mut [usize; 256], clip_limit: usize) { histogram.iter_mut().for_each(|count| *count += batch); } let residual = clipped - batch * 256; - if residual > 0 { - let step = (256 / residual).max(1); - for index in (0..256).step_by(step).take(residual) { - histogram[index] += 1; - } + let step = 256_usize.checked_div(residual).unwrap_or(1).max(1); + for index in (0..256).step_by(step).take(residual) { + histogram[index] += 1; } } diff --git a/crates/spatialrust-vision/src/filter.rs b/crates/spatialrust-vision/src/filter.rs index f7b8c63..9c80eb2 100644 --- a/crates/spatialrust-vision/src/filter.rs +++ b/crates/spatialrust-vision/src/filter.rs @@ -1184,8 +1184,7 @@ mod tests { } let view = ImageView::::new(width, height, stride, &storage).unwrap(); let expected = gaussian_blur(view, 5, 5, 1.2, 1.2, BorderMode::Reflect101).unwrap(); - let actual = - gaussian_blur_u8(view, 5, 5, 1.2, 1.2, BorderMode::Reflect101).unwrap(); + let actual = gaussian_blur_u8(view, 5, 5, 1.2, 1.2, BorderMode::Reflect101).unwrap(); assert!(expected .as_slice() .iter() diff --git a/crates/spatialrust-vision/src/geometry.rs b/crates/spatialrust-vision/src/geometry.rs index 19747e5..c3a357d 100644 --- a/crates/spatialrust-vision/src/geometry.rs +++ b/crates/spatialrust-vision/src/geometry.rs @@ -59,12 +59,7 @@ impl CameraMatrix3 { } /// Builds a checked pinhole `K` with analytic inverse (zero skew). - pub(crate) fn try_from_pinhole( - fx: f64, - fy: f64, - cx: f64, - cy: f64, - ) -> VisionResult { + pub(crate) fn try_from_pinhole(fx: f64, fy: f64, cx: f64, cy: f64) -> VisionResult { if ![fx, fy, cx, cy].into_iter().all(f64::is_finite) || fx <= 0.0 || fy <= 0.0 { return Err(VisionError::InvalidParameter( "pinhole camera matrix requires finite positive focal lengths".into(), diff --git a/crates/spatialrust-vision/src/lib.rs b/crates/spatialrust-vision/src/lib.rs index 40a15c6..24e49ac 100644 --- a/crates/spatialrust-vision/src/lib.rs +++ b/crates/spatialrust-vision/src/lib.rs @@ -19,6 +19,8 @@ mod canny; #[cfg(feature = "feature2d")] mod corners; +#[cfg(feature = "ai-adapters")] +mod adapters; #[cfg(feature = "dense")] mod dense; #[cfg(feature = "detection")] @@ -35,18 +37,16 @@ mod matcher; mod morphology; #[cfg(feature = "geometry")] mod multiview; -#[cfg(feature = "geometry")] -mod optical_flow; #[cfg(feature = "odometry")] mod odometry; +#[cfg(feature = "geometry")] +mod optical_flow; +#[cfg(feature = "feature2d")] +mod orb; #[cfg(feature = "photography")] mod photography; #[cfg(feature = "geometry")] mod pnp; -#[cfg(feature = "ai-adapters")] -mod adapters; -#[cfg(feature = "feature2d")] -mod orb; #[cfg(feature = "preprocess")] mod preprocess; #[cfg(feature = "resize")] @@ -55,10 +55,10 @@ mod resize; mod spatial; #[cfg(feature = "geometry")] mod stereo; -#[cfg(feature = "warp")] -mod warp; #[cfg(feature = "video")] mod video; +#[cfg(feature = "warp")] +mod warp; pub use border::BorderMode; pub use error::{VisionError, VisionResult}; @@ -73,6 +73,8 @@ pub use canny::*; #[cfg(feature = "feature2d")] pub use corners::*; +#[cfg(feature = "ai-adapters")] +pub use adapters::*; #[cfg(feature = "dense")] pub use dense::*; #[cfg(feature = "detection")] @@ -89,18 +91,16 @@ pub use matcher::*; pub use morphology::*; #[cfg(feature = "geometry")] pub use multiview::*; -#[cfg(feature = "geometry")] -pub use optical_flow::*; #[cfg(feature = "odometry")] pub use odometry::*; +#[cfg(feature = "geometry")] +pub use optical_flow::*; +#[cfg(feature = "feature2d")] +pub use orb::*; #[cfg(feature = "photography")] pub use photography::*; #[cfg(feature = "geometry")] pub use pnp::*; -#[cfg(feature = "feature2d")] -pub use orb::*; -#[cfg(feature = "ai-adapters")] -pub use adapters::*; #[cfg(feature = "preprocess")] pub use preprocess::*; #[cfg(feature = "resize")] @@ -109,7 +109,7 @@ pub use resize::*; pub use spatial::*; #[cfg(feature = "geometry")] pub use stereo::*; -#[cfg(feature = "warp")] -pub use warp::*; #[cfg(feature = "video")] pub use video::*; +#[cfg(feature = "warp")] +pub use warp::*; diff --git a/crates/spatialrust-vision/src/optical_flow.rs b/crates/spatialrust-vision/src/optical_flow.rs index 70cd9ff..baf240b 100644 --- a/crates/spatialrust-vision/src/optical_flow.rs +++ b/crates/spatialrust-vision/src/optical_flow.rs @@ -118,10 +118,8 @@ pub fn track_points_lucas_kanade( continue; } let mut guess = previous_guess[index]; - let target = Vec2 { - x: previous_point.x * level_scale, - y: previous_point.y * level_scale, - }; + let target = + Vec2 { x: previous_point.x * level_scale, y: previous_point.y * level_scale }; match track_one_level( previous_pyramid[level].view(), next_pyramid[level].view(), @@ -171,10 +169,8 @@ fn track_one_level( if !(in_bounds(previous, px, py, radius) && in_bounds(next, qx, qy, radius)) { continue; } - let ix = 0.5 - * (sample(previous, px + 1.0, py)? - sample(previous, px - 1.0, py)?); - let iy = 0.5 - * (sample(previous, px, py + 1.0)? - sample(previous, px, py - 1.0)?); + let ix = 0.5 * (sample(previous, px + 1.0, py)? - sample(previous, px - 1.0, py)?); + let iy = 0.5 * (sample(previous, px, py + 1.0)? - sample(previous, px, py - 1.0)?); let it = sample(next, qx, qy)? - sample(previous, px, py)?; gxx += ix * ix; gxy += ix * iy; @@ -185,9 +181,7 @@ fn track_one_level( } } if samples < 4 { - return Err(VisionError::InvalidParameter( - "Lucas–Kanade patch left the image".into(), - )); + return Err(VisionError::InvalidParameter("Lucas–Kanade patch left the image".into())); } let det = gxx * gyy - gxy * gxy; if det.abs() < options.min_eigenvalue { @@ -268,14 +262,8 @@ fn sample(image: ImageView<'_, f32, 1>, x: f64, y: f64) -> VisionResult { let y0 = y.floor() as isize; let x1 = x0 + 1; let y1 = y0 + 1; - if x0 < 0 - || y0 < 0 - || x1 >= image.width() as isize - || y1 >= image.height() as isize - { - return Err(VisionError::InvalidParameter( - "Lucas–Kanade sample is out of bounds".into(), - )); + if x0 < 0 || y0 < 0 || x1 >= image.width() as isize || y1 >= image.height() as isize { + return Err(VisionError::InvalidParameter("Lucas–Kanade sample is out of bounds".into())); } let ax = x - x0 as f64; let ay = y - y0 as f64; @@ -339,11 +327,7 @@ mod tests { }, ) .unwrap(); - assert!( - tracked.status().iter().all(|&ok| ok), - "status={:?}", - tracked.status() - ); + assert!(tracked.status().iter().all(|&ok| ok), "status={:?}", tracked.status()); let actual = tracked.next_points()[0]; assert!( (actual.x - (points[0].x + shift as f64)).abs() < 1.5, diff --git a/crates/spatialrust-vision/src/pnp.rs b/crates/spatialrust-vision/src/pnp.rs index f3c2397..f45b858 100644 --- a/crates/spatialrust-vision/src/pnp.rs +++ b/crates/spatialrust-vision/src/pnp.rs @@ -47,9 +47,9 @@ pub fn solve_pnp_ransac( while iteration < iteration_limit { let indices = sample_unique(&mut rng, correspondences.len(), SAMPLE); let sample = indices.iter().map(|&index| correspondences[index]).collect::>(); - if let Ok(model) = estimate_pnp_dlt(&sample, camera).and_then(|pose| { - refine_pnp(&sample, camera, pose) - }) { + if let Ok(model) = + estimate_pnp_dlt(&sample, camera).and_then(|pose| refine_pnp(&sample, camera, pose)) + { let residuals = correspondences .iter() .copied() @@ -84,13 +84,10 @@ pub fn solve_pnp_ransac( } iteration += 1; } - let (_, best_inliers, _, count, _) = best.ok_or_else(|| { - VisionError::InvalidParameter("robust PnP found no valid model".into()) - })?; + let (_, best_inliers, _, count, _) = best + .ok_or_else(|| VisionError::InvalidParameter("robust PnP found no valid model".into()))?; if count < 4 { - return Err(VisionError::InvalidParameter( - "robust PnP found too few inliers".into(), - )); + return Err(VisionError::InvalidParameter("robust PnP found too few inliers".into())); } let inlier_pairs = correspondences .iter() @@ -119,7 +116,8 @@ pub fn project_object_point( "projected point lies behind or on the camera plane".into(), )); } - let normalized = Vec3::new(camera_point.x / camera_point.z, camera_point.y / camera_point.z, 1.0); + let normalized = + Vec3::new(camera_point.x / camera_point.z, camera_point.y / camera_point.z, 1.0); let pixel = camera.matrix().mul_vec3(normalized); Ok(Vec2 { x: pixel.x / pixel.z, y: pixel.y / pixel.z }) } @@ -187,17 +185,12 @@ fn estimate_pnp_dlt( let calibrated = camera.inverse().mul_mat3(projection); let calibrated_t = camera.inverse().mul_vec3(translation_part); let (rotation, scale) = orthonormalize_rotation(calibrated)?; - let translation = Vec3::new( - calibrated_t.x / scale, - calibrated_t.y / scale, - calibrated_t.z / scale, - ); + let translation = + Vec3::new(calibrated_t.x / scale, calibrated_t.y / scale, calibrated_t.z / scale); // Flip if most points have negative depth. let pose = AbsolutePose::try_new(rotation, translation)?; - let positive = correspondences - .iter() - .filter(|pair| pose.transform_point(pair.object()).z > 0.0) - .count(); + let positive = + correspondences.iter().filter(|pair| pose.transform_point(pair.object()).z > 0.0).count(); if positive * 2 < correspondences.len() { AbsolutePose::try_new( Mat3::from_rows( @@ -296,11 +289,7 @@ fn projection_jacobian( Ok(jacobian) } -fn pnp_residual( - pose: AbsolutePose, - pair: ObjectImageCorrespondence, - camera: CameraMatrix3, -) -> f64 { +fn pnp_residual(pose: AbsolutePose, pair: ObjectImageCorrespondence, camera: CameraMatrix3) -> f64 { match project_object_point(pose, camera, pair.object()) { Ok(pixel) => (pixel.x - pair.image().x).hypot(pixel.y - pair.image().y), Err(_) => f64::MAX, @@ -366,11 +355,8 @@ fn exp_so3(omega: Vec3) -> Mat3 { ); } let axis = omega.normalize(); - let skew = Mat3::from_rows( - [0.0, -axis.z, axis.y], - [axis.z, 0.0, -axis.x], - [-axis.y, axis.x, 0.0], - ); + let skew = + Mat3::from_rows([0.0, -axis.z, axis.y], [axis.z, 0.0, -axis.x], [-axis.y, axis.x, 0.0]); let skew2 = skew.mul_mat3(skew); let mut result = Mat3::::identity(); let s = theta.sin(); @@ -537,12 +523,8 @@ mod tests { }) .collect::>(); let estimated = solve_pnp(&pairs, camera).unwrap(); - for (expected, actual) in pose - .rotation() - .m - .iter() - .flatten() - .zip(estimated.rotation().m.iter().flatten()) + for (expected, actual) in + pose.rotation().m.iter().flatten().zip(estimated.rotation().m.iter().flatten()) { assert!((expected - actual).abs() < 2e-3); } diff --git a/crates/spatialrust-vision/src/stereo.rs b/crates/spatialrust-vision/src/stereo.rs index 82e3332..685c61d 100644 --- a/crates/spatialrust-vision/src/stereo.rs +++ b/crates/spatialrust-vision/src/stereo.rs @@ -3,9 +3,7 @@ use spatialrust_image::{Image, ImageView}; use spatialrust_math::{Mat3, Vec3}; -use crate::{ - CameraMatrix3, PixelComponent, RelativePose, VisionError, VisionResult, -}; +use crate::{CameraMatrix3, PixelComponent, RelativePose, VisionError, VisionResult}; /// Invalid disparity sentinel written by [`stereo_block_match`]. pub const INVALID_DISPARITY: f32 = -1.0; @@ -115,12 +113,7 @@ pub struct StereoBmOptions { impl Default for StereoBmOptions { fn default() -> Self { - Self { - window_size: 15, - min_disparity: 0, - num_disparities: 64, - uniqueness_ratio: 15.0, - } + Self { window_size: 15, min_disparity: 0, num_disparities: 64, uniqueness_ratio: 15.0 } } } @@ -163,23 +156,13 @@ pub fn stereo_rectify( let translation = rig.pose().translation(); let baseline = translation.length(); if baseline <= f64::EPSILON { - return Err(VisionError::InvalidParameter( - "stereo baseline must be non-zero".into(), - )); + return Err(VisionError::InvalidParameter("stereo baseline must be non-zero".into())); } let e1 = translation.normalize(); - let helper = if e1.x.abs() < 0.9 { - Vec3::new(1.0, 0.0, 0.0) - } else { - Vec3::new(0.0, 1.0, 0.0) - }; + let helper = if e1.x.abs() < 0.9 { Vec3::new(1.0, 0.0, 0.0) } else { Vec3::new(0.0, 1.0, 0.0) }; let e2 = e1.cross(helper).normalize(); let e3 = e1.cross(e2).normalize(); - let r_rect = Mat3::from_rows( - [e1.x, e1.y, e1.z], - [e2.x, e2.y, e2.z], - [e3.x, e3.y, e3.z], - ); + let r_rect = Mat3::from_rows([e1.x, e1.y, e1.z], [e2.x, e2.y, e2.z], [e3.x, e3.y, e3.z]); let left_rotation = r_rect; let right_rotation = r_rect.mul_mat3(rig.pose().rotation()); let fx = 0.5 * (rig.left().matrix().m[0][0] + rig.right().matrix().m[0][0]); @@ -287,11 +270,8 @@ pub fn disparity_to_depth( for y in 0..disparity.height() { for x in 0..disparity.width() { let d = f64::from(disparity.get(x, y).expect("in-bounds")[0]); - data[y * disparity.width() + x] = if d > 0.0 && d.is_finite() { - (focal_length * baseline / d) as f32 - } else { - 0.0 - }; + data[y * disparity.width() + x] = + if d > 0.0 && d.is_finite() { (focal_length * baseline / d) as f32 } else { 0.0 }; } } Ok(Image::try_new(disparity.width(), disparity.height(), data)?) @@ -367,11 +347,7 @@ fn invert_intrinsic(matrix: Mat3) -> VisionResult> { "rectified intrinsics must have non-zero focal lengths".into(), )); } - Ok(Mat3::from_rows( - [1.0 / fx, 0.0, -cx / fx], - [0.0, 1.0 / fy, -cy / fy], - [0.0, 0.0, 1.0], - )) + Ok(Mat3::from_rows([1.0 / fx, 0.0, -cx / fx], [0.0, 1.0 / fy, -cy / fy], [0.0, 0.0, 1.0])) } #[cfg(test)] @@ -394,11 +370,8 @@ mod tests { fn fronto_parallel_stereo_recovers_plane_depth() { let camera = camera(); let baseline = 0.1; - let pose = RelativePose::try_new( - Mat3::::identity(), - Vec3::new(baseline, 0.0, 0.0), - ) - .unwrap(); + let pose = + RelativePose::try_new(Mat3::::identity(), Vec3::new(baseline, 0.0, 0.0)).unwrap(); let rig = StereoRig::try_new(camera, camera, pose).unwrap(); let maps = stereo_rectify(rig, 160, 120).unwrap(); assert!((maps.baseline() - baseline).abs() < 1e-12); @@ -443,4 +416,4 @@ mod tests { let recovered = depth_map.get(80, 60).unwrap()[0]; assert!((f64::from(recovered) - depth).abs() < 0.15); } -} \ No newline at end of file +} diff --git a/crates/spatialrust/examples/north_star_demo.rs b/crates/spatialrust/examples/north_star_demo.rs index c5e6e62..c6360f1 100644 --- a/crates/spatialrust/examples/north_star_demo.rs +++ b/crates/spatialrust/examples/north_star_demo.rs @@ -46,20 +46,14 @@ fn main() { .expect("nchw"); let mut session = MockInferenceBackend - .create_session( - &ModelSource::Mock(MockProfile::SyntheticDepth), - &SessionOptions::default(), - ) + .create_session(&ModelSource::Mock(MockProfile::SyntheticDepth), &SessionOptions::default()) .expect("session"); let mut inputs = NamedTensors::new(); inputs.insert("images", tensor).expect("inputs"); let outputs = session .run_with_options( inputs, - RunOptions { - input_copy: CopyPolicy::Forbid, - output_copy: CopyPolicy::Allow, - }, + RunOptions { input_copy: CopyPolicy::Forbid, output_copy: CopyPolicy::Allow }, ) .expect("infer"); let depth = depth_tensor_to_depth_map(outputs.get("depth").expect("depth")).expect("decode"); @@ -78,8 +72,8 @@ fn main() { depth_map_to_point_cloud(&depth, &camera, DepthConversionOptions::default()).expect("xyz"); let xyz = collect_xyz(&cloud); - let record = - SpatialRecord::try_from_cloud("point", SchemaVersion::new(1, 0), cloud.clone()).expect("record"); + let record = SpatialRecord::try_from_cloud("point", SchemaVersion::new(1, 0), cloud.clone()) + .expect("record"); let stamped = StampedRecord::new( "camera/depth_cloud", StampedTime::exact("host", ClockDomain::HostSteady, Timestamp::from_nanos(1)), @@ -94,11 +88,8 @@ fn main() { dataset: None, }); - let mcap_path = std::env::temp_dir().join(format!( - "spatialrust-demo-{}-{}.mcap", - std::process::id(), - 1 - )); + let mcap_path = + std::env::temp_dir().join(format!("spatialrust-demo-{}-{}.mcap", std::process::id(), 1)); write_memory_episode_mcap(&mcap_path, &episode.memory).expect("mcap write"); let mcap_episode = read_memory_episode_mcap(&mcap_path).expect("mcap read"); let _ = std::fs::remove_file(&mcap_path); @@ -107,7 +98,8 @@ fn main() { let cdr = encode_point_cloud2_xyz(&pc2).expect("cdr"); let mut node = LoopbackRos2Node::new(); node.publish("/camera/points", cdr); - let decoded = decode_point_cloud2_xyz(&node.take("/camera/points").expect("take")).expect("decode"); + let decoded = + decode_point_cloud2_xyz(&node.take("/camera/points").expect("take")).expect("decode"); let mut volume = TsdfVolume::try_new(Vec3::new(-2.0, -2.0, 0.0), 0.25, [16, 16, 16], 0.5).expect("tsdf"); @@ -116,8 +108,7 @@ fn main() { let gltf = export_triangle_mesh_gltf_json(&mesh).expect("gltf"); let mut usd = MemoryUsdStageAdapter::new("demo.usda"); - usd.declare_mesh(UsdPrimPath::try_new("/World/Mesh").unwrap(), &mesh) - .expect("usd mesh"); + usd.declare_mesh(UsdPrimPath::try_new("/World/Mesh").unwrap(), &mesh).expect("usd mesh"); let usda = export_stage_usda(&usd).expect("usda"); let mut gaussians = GaussianScene::new(); diff --git a/crates/spatialrust/src/lib.rs b/crates/spatialrust/src/lib.rs index 203c4de..7ead4b9 100644 --- a/crates/spatialrust/src/lib.rs +++ b/crates/spatialrust/src/lib.rs @@ -22,42 +22,38 @@ pub use spatialrust_voxelize as voxelize; #[cfg(feature = "ai")] pub use spatialrust_ai as ai; +#[cfg(any(feature = "arrow-c-data", feature = "arrow-c-stream", feature = "arrow-c-device"))] +pub use spatialrust_arrow as arrow; #[cfg(feature = "camera")] pub use spatialrust_camera as camera; +#[cfg(feature = "distribute")] +pub use spatialrust_distribute as distribute; +#[cfg(feature = "episode")] +pub use spatialrust_episode as episode; #[cfg(feature = "image")] pub use spatialrust_image as image; #[cfg(feature = "image-io")] pub use spatialrust_image_io as image_io; -#[cfg(feature = "tensor")] -pub use spatialrust_tensor as tensor; -#[cfg(feature = "vision")] -pub use spatialrust_vision as vision; -#[cfg(feature = "records")] -pub use spatialrust_records as records; -#[cfg(any( - feature = "arrow-c-data", - feature = "arrow-c-stream", - feature = "arrow-c-device" -))] -pub use spatialrust_arrow as arrow; -#[cfg(feature = "sync")] -pub use spatialrust_sync as sync; +#[cfg(any(feature = "interchange-gltf", feature = "interchange-openusd"))] +pub use spatialrust_interchange as interchange; #[cfg(feature = "mapping")] pub use spatialrust_mapping as mapping; +#[cfg(feature = "platform")] +pub use spatialrust_platform as platform; +#[cfg(feature = "records")] +pub use spatialrust_records as records; +#[cfg(feature = "runtime")] +pub use spatialrust_runtime as runtime; #[cfg(feature = "scene")] pub use spatialrust_scene as scene; #[cfg(feature = "semantic")] pub use spatialrust_semantic as semantic; -#[cfg(feature = "episode")] -pub use spatialrust_episode as episode; -#[cfg(feature = "runtime")] -pub use spatialrust_runtime as runtime; -#[cfg(any(feature = "interchange-gltf", feature = "interchange-openusd"))] -pub use spatialrust_interchange as interchange; -#[cfg(feature = "distribute")] -pub use spatialrust_distribute as distribute; -#[cfg(feature = "platform")] -pub use spatialrust_platform as platform; +#[cfg(feature = "sync")] +pub use spatialrust_sync as sync; +#[cfg(feature = "tensor")] +pub use spatialrust_tensor as tensor; +#[cfg(feature = "vision")] +pub use spatialrust_vision as vision; pub use spatialrust_core::{ CpuDevice, DType, Device, DeviceKind, ExecutionPolicy, FieldSemantic, FrameId, HasIntensity, @@ -252,8 +248,7 @@ pub use spatialrust_image_io::{ #[cfg(feature = "camera-rgbd")] pub use spatialrust_camera::{ depth_to_point_cloud, depth_to_xyz_dense, depth_to_xyz_dense_into, rgbd_to_point_cloud, - BrownConrady, CameraError, CameraIntrinsics, - DepthConversionOptions, PinholeCamera, RgbdError, + BrownConrady, CameraError, CameraIntrinsics, DepthConversionOptions, PinholeCamera, RgbdError, }; #[cfg(feature = "vision")] diff --git a/crates/spatialrust/tests/north_star_pipeline.rs b/crates/spatialrust/tests/north_star_pipeline.rs index f73e8a7..3096ba9 100644 --- a/crates/spatialrust/tests/north_star_pipeline.rs +++ b/crates/spatialrust/tests/north_star_pipeline.rs @@ -40,8 +40,8 @@ use spatialrust::sync::{ }; use spatialrust::{ depth_map_to_point_cloud, depth_tensor_to_depth_map, rgb_u8_to_nchw_f32, CameraIntrinsics, - DepthConversionOptions, Image, Interpolation, Isometry3, PinholeCamera, PointCloud, Pose3, Quat, - Timestamp, Vec3, + DepthConversionOptions, Image, Interpolation, Isometry3, PinholeCamera, PointCloud, Pose3, + Quat, Timestamp, Vec3, }; #[test] @@ -71,20 +71,14 @@ fn north_star_image_to_gltf_pipeline() { .unwrap(); let mut session = MockInferenceBackend - .create_session( - &ModelSource::Mock(MockProfile::SyntheticDepth), - &SessionOptions::default(), - ) + .create_session(&ModelSource::Mock(MockProfile::SyntheticDepth), &SessionOptions::default()) .unwrap(); let mut inputs = NamedTensors::new(); inputs.insert("images", tensor).unwrap(); let outputs = session .run_with_options( inputs, - RunOptions { - input_copy: CopyPolicy::Forbid, - output_copy: CopyPolicy::Allow, - }, + RunOptions { input_copy: CopyPolicy::Forbid, output_copy: CopyPolicy::Allow }, ) .unwrap(); let depth = depth_tensor_to_depth_map(outputs.get("depth").unwrap()).unwrap(); @@ -105,8 +99,8 @@ fn north_star_image_to_gltf_pipeline() { let xyz = collect_xyz(&cloud); // 2) Versioned record → stamped multimodal episode - let record = SpatialRecord::try_from_cloud("point", SchemaVersion::new(1, 0), cloud.clone()) - .unwrap(); + let record = + SpatialRecord::try_from_cloud("point", SchemaVersion::new(1, 0), cloud.clone()).unwrap(); let stamp = StampedTime::exact("host", ClockDomain::HostSteady, Timestamp::from_nanos(1_000)); let stamped = StampedRecord::new("camera/depth_cloud", stamp.clone(), record); let memory = MemoryEpisode::from_records(vec![stamped]); @@ -175,8 +169,7 @@ fn north_star_image_to_gltf_pipeline() { assert!(gltf.contains("VEC3")); let mut usd = MemoryUsdStageAdapter::new("north_star.usda"); - usd.declare_mesh(UsdPrimPath::try_new("/World/TsdfMesh").unwrap(), &mesh) - .unwrap(); + usd.declare_mesh(UsdPrimPath::try_new("/World/TsdfMesh").unwrap(), &mesh).unwrap(); let usda = export_stage_usda(&usd).unwrap(); assert!(usda.starts_with("#usda 1.0")); let (prim, imported) = import_mesh_from_usda(&usda).unwrap(); @@ -206,26 +199,17 @@ fn north_star_image_to_gltf_pipeline() { index.insert(SemanticEntity { id: EntityId::new("surface"), centroid: Some(Vec3::new(0.0, 0.0, 1.0)), - labels: vec![OpenVocabLabel { - text: "plane".into(), - confidence: 0.8, - }], + labels: vec![OpenVocabLabel { text: "plane".into(), confidence: 0.8 }], embedding: Some(Embedding::try_new(vec![1.0, 0.0, 0.0]).unwrap()), }); let hits = index - .search( - &Embedding::try_new(vec![1.0, 0.0, 0.0]).unwrap(), - MultimodalFusion::default(), - 1, - ) + .search(&Embedding::try_new(vec![1.0, 0.0, 0.0]).unwrap(), MultimodalFusion::default(), 1) .unwrap(); assert_eq!(hits[0].0, EntityId::new("surface")); // 6) Bounded runtime + distribute graph let mut pipeline = BoundedPipeline::new(PipelineConfig { max_inflight: 4 }); - pipeline - .push(PipelineStage::new("reconstruct"), mesh.triangle_count()) - .unwrap(); + pipeline.push(PipelineStage::new("reconstruct"), mesh.triangle_count()).unwrap(); assert_eq!(pipeline.pop().unwrap().1, mesh.triangle_count()); let mut partitions = PartitionGraph::new(); diff --git a/crates/spatialrust/tests/vision_ai_pipeline.rs b/crates/spatialrust/tests/vision_ai_pipeline.rs index d1a91a9..20f8be5 100644 --- a/crates/spatialrust/tests/vision_ai_pipeline.rs +++ b/crates/spatialrust/tests/vision_ai_pipeline.rs @@ -5,8 +5,8 @@ #![cfg(all(feature = "ai-vision-pipeline", feature = "mvp"))] use spatialrust::ai::{ - CopyPolicy, InferenceBackend, MockInferenceBackend, MockProfile, ModelSession as _, - ModelSource, NamedTensors, RunOptions, SessionOptions, + CopyPolicy, InferenceBackend, MockInferenceBackend, MockProfile, ModelSource, NamedTensors, + RunOptions, SessionOptions, }; use spatialrust::{ depth_map_to_point_cloud, depth_tensor_to_depth_map, point_map_to_point_cloud, @@ -42,20 +42,14 @@ fn image_mock_depth_runs_through_spatial_pipeline() { let backend = MockInferenceBackend; let mut session = backend - .create_session( - &ModelSource::Mock(MockProfile::SyntheticDepth), - &SessionOptions::default(), - ) + .create_session(&ModelSource::Mock(MockProfile::SyntheticDepth), &SessionOptions::default()) .unwrap(); let mut inputs = NamedTensors::new(); inputs.insert("images", input).unwrap(); let outputs = session .run_with_options( inputs, - RunOptions { - input_copy: CopyPolicy::Forbid, - output_copy: CopyPolicy::Allow, - }, + RunOptions { input_copy: CopyPolicy::Forbid, output_copy: CopyPolicy::Allow }, ) .unwrap(); let depth = depth_tensor_to_depth_map(outputs.get("depth").unwrap()).unwrap();