Skip to main content

cu_sensor_payloads/
depth.rs

1use bincode::de::Decoder;
2use bincode::enc::Encoder;
3use bincode::error::{DecodeError, EncodeError};
4use bincode::{Decode, Encode};
5use core::fmt::Debug;
6use core::marker::PhantomData;
7use cu29::prelude::*;
8use cu29::units::si::f32::Length;
9use cu29::units::si::length::meter;
10use serde::{Deserialize, Serialize, Serializer};
11
12/// Raster layout for a depth map whose samples are physical lengths.
13///
14/// `stride` is expressed in elements, not bytes, and may be greater than `width`
15/// when rows contain padding.
16#[derive(
17    Default, Debug, Encode, Decode, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect,
18)]
19#[bincode(describe)]
20pub struct CuDepthMapFormat {
21    pub width: u32,
22    pub height: u32,
23    pub stride: u32,
24}
25
26impl CuDepthMapFormat {
27    pub fn is_valid(&self) -> bool {
28        self.width <= self.stride
29    }
30
31    pub fn required_elements(&self) -> usize {
32        self.stride as usize * self.height as usize
33    }
34}
35
36/// Raw invalid value used by a [`CuDepthEncodingDescriptor`].
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
38#[serde(rename_all = "snake_case")]
39pub enum CuDepthInvalidValue {
40    Unsigned(u64),
41    Signed(i64),
42    FloatBits(u64),
43}
44
45/// Static depth-encoding metadata exposed to debuggers and offline tooling.
46#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
47pub struct CuDepthEncodingDescriptor {
48    /// Number of native sample units in one meter.
49    pub units_per_meter: f32,
50    /// Raw value reserved for invalid measurements, when the encoding has one.
51    pub invalid: Option<CuDepthInvalidValue>,
52}
53
54/// Compile-time description of a depth map's native sample representation.
55pub trait CuDepthEncoding: Copy + Debug + Default + Send + Sync + 'static {
56    type Sample: ElementType;
57
58    const DESCRIPTOR: CuDepthEncodingDescriptor;
59    const SERIALIZED_MAP_SCHEMA: &'static str = GENERIC_DEPTH_MAP_SERIALIZED_SCHEMA;
60    const TYPE_PATH: &'static str;
61    const SHORT_TYPE_PATH: &'static str;
62    const TYPE_IDENT: &'static str;
63
64    /// Converts one native sample into a typed physical length.
65    ///
66    /// Returns `None` when the raw sample represents an invalid measurement.
67    fn decode(sample: Self::Sample) -> Option<Length>;
68
69    /// Converts a typed physical length into one native sample.
70    ///
71    /// `None` represents an invalid measurement in the target encoding.
72    fn encode_sample(sample: Option<Length>) -> Self::Sample;
73}
74
75/// Default depth encoding backed directly by unit-safe [`Length`] samples.
76#[derive(Debug, Default, Clone, Copy)]
77pub struct CuDepthLength;
78
79impl CuDepthEncoding for CuDepthLength {
80    type Sample = Length;
81
82    const DESCRIPTOR: CuDepthEncodingDescriptor = CuDepthEncodingDescriptor {
83        units_per_meter: 1.0,
84        invalid: None,
85    };
86    const SERIALIZED_MAP_SCHEMA: &'static str = LENGTH_DEPTH_MAP_SERIALIZED_SCHEMA;
87    const TYPE_PATH: &'static str = "cu_sensor_payloads::CuDepthMap";
88    const SHORT_TYPE_PATH: &'static str = "CuDepthMap";
89    const TYPE_IDENT: &'static str = "CuDepthMap";
90
91    fn decode(sample: Self::Sample) -> Option<Length> {
92        Some(sample)
93    }
94
95    fn encode_sample(sample: Option<Length>) -> Self::Sample {
96        sample.unwrap_or_else(|| Length::new::<meter>(f32::NAN))
97    }
98}
99
100/// Builds a typed length for use in a compile-time depth resolution.
101///
102/// `Length` stores its value in meters internally, but its usual unit-aware
103/// constructor is not currently a `const fn`.
104pub const fn depth_resolution_meters(meters: f32) -> Length {
105    Length { value: meters }
106}
107
108/// Compile-time physical distance represented by one integer increment.
109pub trait CuDepthScale: Copy + Debug + Default + Send + Sync + 'static {
110    const RESOLUTION: Length;
111    const UNITS_PER_METER: f32 = 1.0 / Self::RESOLUTION.value;
112}
113
114/// Rational depth resolution expressed in meters.
115///
116/// For example, `CuDepthScaleRatio<1, 10>` represents 10 centimeters per
117/// integer increment.
118#[derive(Debug, Default, Clone, Copy)]
119pub struct CuDepthScaleRatio<const NUMERATOR: u64, const DENOMINATOR: u64>;
120
121impl<const NUMERATOR: u64, const DENOMINATOR: u64> CuDepthScale
122    for CuDepthScaleRatio<NUMERATOR, DENOMINATOR>
123{
124    const RESOLUTION: Length = depth_resolution_meters(NUMERATOR as f32 / DENOMINATOR as f32);
125    const UNITS_PER_METER: f32 = DENOMINATOR as f32 / NUMERATOR as f32;
126}
127
128/// One millimeter per integer increment.
129pub type CuDepthMillimeter = CuDepthScaleRatio<1, 1_000>;
130
131/// One decimeter (10 centimeters) per integer increment.
132pub type CuDepthDecimeter = CuDepthScaleRatio<1, 10>;
133
134/// Unsigned integer storage supported by [`CuDepthInteger`].
135pub trait CuDepthIntegerSample: ElementType + 'static {
136    const MAX: u64;
137
138    fn to_u64(self) -> u64;
139    fn from_u64(value: u64) -> Self;
140}
141
142macro_rules! impl_depth_integer_sample {
143    ($($sample:ty),+ $(,)?) => {
144        $(
145            impl CuDepthIntegerSample for $sample {
146                const MAX: u64 = <$sample>::MAX as u64;
147
148                fn to_u64(self) -> u64 {
149                    self as u64
150                }
151
152                fn from_u64(value: u64) -> Self {
153                    value as Self
154                }
155            }
156        )+
157    };
158}
159
160impl_depth_integer_sample!(u8, u16, u32, u64);
161
162/// Compact unsigned-integer depth encoding with a compile-time physical scale.
163///
164/// Zero represents an invalid measurement. The sample width and resolution are
165/// independent: for example, `CuDepthInteger<u16, CuDepthDecimeter>` covers
166/// depths in 10-centimeter increments.
167///
168/// A 200-meter camera with 10-centimeter increments can use:
169///
170/// ```
171/// use cu_sensor_payloads::{
172///     CuDepthInteger, CuDepthMap, CuDepthScaleRatio,
173/// };
174///
175/// type LongRangeDepth =
176///     CuDepthMap<Vec<u16>, CuDepthInteger<u16, CuDepthScaleRatio<1, 10>>>;
177/// ```
178#[derive(Debug, Default, Clone, Copy)]
179pub struct CuDepthInteger<T, S>(PhantomData<(T, S)>);
180
181impl<T, S> CuDepthEncoding for CuDepthInteger<T, S>
182where
183    T: CuDepthIntegerSample,
184    S: CuDepthScale,
185{
186    type Sample = T;
187
188    const DESCRIPTOR: CuDepthEncodingDescriptor = CuDepthEncodingDescriptor {
189        units_per_meter: S::UNITS_PER_METER,
190        invalid: Some(CuDepthInvalidValue::Unsigned(0)),
191    };
192    const SERIALIZED_MAP_SCHEMA: &'static str = INTEGER_DEPTH_MAP_SERIALIZED_SCHEMA;
193    const TYPE_PATH: &'static str = "cu_sensor_payloads::CuDepthMapInteger";
194    const SHORT_TYPE_PATH: &'static str = "CuDepthMapInteger";
195    const TYPE_IDENT: &'static str = "CuDepthMapInteger";
196
197    fn decode(sample: Self::Sample) -> Option<Length> {
198        let raw = sample.to_u64();
199        (raw != 0).then(|| depth_resolution_meters(raw as f32 / S::UNITS_PER_METER))
200    }
201
202    fn encode_sample(sample: Option<Length>) -> Self::Sample {
203        let Some(meters) = sample.map(|sample| sample.get::<meter>()) else {
204            return T::from_u64(0);
205        };
206        if !meters.is_finite() || meters <= 0.0 {
207            return T::from_u64(0);
208        }
209        let raw = (meters * S::UNITS_PER_METER)
210            .round()
211            .clamp(1.0, T::MAX as f32) as u64;
212        T::from_u64(raw)
213    }
214}
215
216/// Standard depth-map payload backed by a Copper handle.
217///
218/// The default encoding stores [`Length`] directly. Alternate encodings can
219/// retain a sensor's compact native representation without a producer-side
220/// conversion or copy.
221#[derive(Debug, Default, Clone, Reflect)]
222#[reflect(
223    from_reflect = false,
224    no_field_bounds,
225    type_path = false,
226    SerializedPayloadSchema
227)]
228pub struct CuDepthMap<A, E = CuDepthLength>
229where
230    E: CuDepthEncoding,
231    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
232{
233    pub format: CuDepthMapFormat,
234    pub buffer_handle: CuHandle<A>,
235    #[reflect(ignore)]
236    encoding: PhantomData<E>,
237}
238
239impl<A, E> TypePath for CuDepthMap<A, E>
240where
241    E: CuDepthEncoding,
242    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
243{
244    fn type_path() -> &'static str {
245        E::TYPE_PATH
246    }
247
248    fn short_type_path() -> &'static str {
249        E::SHORT_TYPE_PATH
250    }
251
252    fn type_ident() -> Option<&'static str> {
253        Some(E::TYPE_IDENT)
254    }
255
256    fn crate_name() -> Option<&'static str> {
257        Some("cu_sensor_payloads")
258    }
259
260    fn module_path() -> Option<&'static str> {
261        Some("cu_sensor_payloads")
262    }
263}
264
265impl<A, E> Encode for CuDepthMap<A, E>
266where
267    E: CuDepthEncoding,
268    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
269    CuHandle<A>: Encode,
270{
271    fn encode<Enc: Encoder>(&self, encoder: &mut Enc) -> Result<(), EncodeError> {
272        Encode::encode(&self.format, encoder)?;
273        Encode::encode(&self.buffer_handle, encoder)
274    }
275}
276
277impl<A, E> bincode::ValueDecode for CuDepthMap<A, E>
278where
279    E: CuDepthEncoding,
280    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
281    CuHandle<A>: bincode::ValueDecode,
282{
283    const DECODE: &'static bincode::ValueDecodeSpec = &bincode::ValueDecodeSpec::Record {
284        shape: bincode::value_decode::RecordShape::Struct,
285        fields: &[
286            bincode::value_decode::ValueDecodeField {
287                selector: bincode::value_decode::FieldSelector::Named("format"),
288                declaration_index: 0,
289                value: bincode::value_decode::ValueDecodeRef::of::<CuDepthMapFormat>(),
290            },
291            bincode::value_decode::ValueDecodeField {
292                selector: bincode::value_decode::FieldSelector::Named("buffer_handle"),
293                declaration_index: 1,
294                value: bincode::value_decode::ValueDecodeRef::of::<CuHandle<A>>(),
295            },
296        ],
297    };
298}
299
300impl<A, E> Decode<()> for CuDepthMap<A, E>
301where
302    E: CuDepthEncoding,
303    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
304    CuHandle<A>: Decode<()>,
305{
306    fn decode<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<Self, DecodeError> {
307        Ok(Self {
308            format: Decode::decode(decoder)?,
309            buffer_handle: Decode::decode(decoder)?,
310            encoding: PhantomData,
311        })
312    }
313}
314
315impl<'de, A, E> Deserialize<'de> for CuDepthMap<A, E>
316where
317    E: CuDepthEncoding,
318    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
319    CuHandle<A>: Deserialize<'de>,
320{
321    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
322    where
323        D: serde::Deserializer<'de>,
324    {
325        #[derive(Deserialize)]
326        struct Wire<H> {
327            format: CuDepthMapFormat,
328            encoding: Option<CuDepthEncodingDescriptor>,
329            handle: H,
330        }
331
332        let wire = Wire::<CuHandle<A>>::deserialize(deserializer)?;
333        if let Some(encoding) = wire.encoding
334            && encoding != E::DESCRIPTOR
335        {
336            return Err(serde::de::Error::custom(format!(
337                "Depth encoding {:?} does not match {}",
338                encoding,
339                E::SHORT_TYPE_PATH
340            )));
341        }
342        Ok(Self {
343            format: wire.format,
344            buffer_handle: wire.handle,
345            encoding: PhantomData,
346        })
347    }
348}
349
350impl<A, E> Serialize for CuDepthMap<A, E>
351where
352    E: CuDepthEncoding,
353    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
354    CuHandle<A>: Serialize,
355{
356    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
357    where
358        S: Serializer,
359    {
360        use serde::ser::SerializeStruct;
361        let mut state = serializer.serialize_struct(E::SHORT_TYPE_PATH, 3)?;
362        state.serialize_field("format", &self.format)?;
363        state.serialize_field("encoding", &E::DESCRIPTOR)?;
364        state.serialize_field("handle", &self.buffer_handle)?;
365        state.end()
366    }
367}
368
369impl<A, E> CuDepthMap<A, E>
370where
371    E: CuDepthEncoding,
372    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
373{
374    pub const fn encoding_descriptor() -> CuDepthEncodingDescriptor {
375        E::DESCRIPTOR
376    }
377
378    pub fn new_encoded(format: CuDepthMapFormat, buffer_handle: CuHandle<A>) -> Self {
379        assert!(
380            E::DESCRIPTOR.units_per_meter.is_finite() && E::DESCRIPTOR.units_per_meter > 0.0,
381            "Depth encoding must have a finite positive resolution."
382        );
383        assert!(
384            format.is_valid(),
385            "Depth-map stride must be at least its width."
386        );
387        assert!(
388            format.required_elements() <= buffer_handle.with_inner(|inner| inner.len()),
389            "Buffer size must at least match the depth-map format."
390        );
391        Self {
392            format,
393            buffer_handle,
394            encoding: PhantomData,
395        }
396    }
397
398    /// Returns the native encoded sample at `(x, y)`.
399    pub fn get_raw(&self, x: u32, y: u32) -> Option<E::Sample> {
400        if x >= self.format.width || y >= self.format.height {
401            return None;
402        }
403        let index = y as usize * self.format.stride as usize + x as usize;
404        self.buffer_handle
405            .with_inner(|inner| inner.as_ref().get(index).copied())
406    }
407
408    /// Converts a native sample into a physical length.
409    pub fn decode_sample(sample: E::Sample) -> Option<Length> {
410        E::decode(sample)
411    }
412
413    /// Returns the physical depth sample at `(x, y)`.
414    ///
415    /// Returns `None` for out-of-bounds coordinates and encoded invalid values.
416    pub fn get(&self, x: u32, y: u32) -> Option<Length> {
417        Self::decode_sample(self.get_raw(x, y)?)
418    }
419
420    /// Returns the sample at `(x, y)` converted to meters.
421    pub fn get_meters(&self, x: u32, y: u32) -> Option<f32> {
422        self.get(x, y).map(|depth| depth.get::<meter>())
423    }
424
425    /// Converts this map into a caller-provided target map.
426    ///
427    /// Only logical pixels are written; target row padding is left untouched.
428    /// This performs one source lock, one target lock, and no allocation.
429    pub fn convert_into<B, F>(&self, target: &mut CuDepthMap<B, F>) -> CuResult<()>
430    where
431        F: CuDepthEncoding,
432        B: ArrayLike<Element = F::Sample> + Send + Sync + 'static,
433    {
434        if self.format.width != target.format.width || self.format.height != target.format.height {
435            return Err(CuError::from(format!(
436                "Depth-map dimensions differ: source is {}x{}, target is {}x{}",
437                self.format.width, self.format.height, target.format.width, target.format.height
438            )));
439        }
440        if self.buffer_handle.storage_id() == target.buffer_handle.storage_id() {
441            return Err(CuError::from(
442                "Depth-map conversion requires distinct source and target storage",
443            ));
444        }
445
446        let convert = |source: &[E::Sample],
447                       source_format: CuDepthMapFormat,
448                       destination: &mut [F::Sample],
449                       destination_format: CuDepthMapFormat| {
450            for y in 0..source_format.height as usize {
451                let source_row = y * source_format.stride as usize;
452                let destination_row = y * destination_format.stride as usize;
453                for x in 0..source_format.width as usize {
454                    destination[destination_row + x] =
455                        F::encode_sample(E::decode(source[source_row + x]));
456                }
457            }
458        };
459
460        if self.buffer_handle.storage_id() < target.buffer_handle.storage_id() {
461            self.with_samples(|source, source_format| {
462                target.with_samples_mut(|destination, destination_format| {
463                    convert(source, source_format, destination, destination_format);
464                });
465            });
466        } else {
467            target.with_samples_mut(|destination, destination_format| {
468                self.with_samples(|source, source_format| {
469                    convert(source, source_format, destination, destination_format);
470                });
471            });
472        }
473        Ok(())
474    }
475
476    /// Accesses all samples under a single buffer lock.
477    ///
478    /// The slice covers exactly [`CuDepthMapFormat::required_elements`], including
479    /// any row padding described by [`CuDepthMapFormat::stride`].
480    pub fn with_samples<R>(&self, f: impl FnOnce(&[E::Sample], CuDepthMapFormat) -> R) -> R {
481        let format = self.format;
482        self.buffer_handle
483            .with_inner(|inner| f(&inner[..format.required_elements()], format))
484    }
485
486    /// Mutably accesses all samples under a single buffer lock.
487    ///
488    /// The slice covers exactly [`CuDepthMapFormat::required_elements`], including
489    /// any row padding described by [`CuDepthMapFormat::stride`].
490    pub fn with_samples_mut<R>(
491        &mut self,
492        f: impl FnOnce(&mut [E::Sample], CuDepthMapFormat) -> R,
493    ) -> R {
494        let format = self.format;
495        self.buffer_handle
496            .with_inner_mut(|inner| f(&mut inner[..format.required_elements()], format))
497    }
498
499    pub fn payload_should_log(&self) -> bool {
500        self.buffer_handle.payload_should_log()
501    }
502
503    pub fn apply_handle_content_policy(&self, mode: cu29::pool::HandleContent) {
504        self.buffer_handle.apply_handle_content_policy(mode);
505    }
506
507    pub fn mark_touched(&self) {
508        self.buffer_handle.mark_touched();
509    }
510}
511
512impl<A> CuDepthMap<A, CuDepthLength>
513where
514    A: ArrayLike<Element = Length> + Send + Sync + 'static,
515{
516    pub fn new(format: CuDepthMapFormat, buffer_handle: CuHandle<A>) -> Self {
517        Self::new_encoded(format, buffer_handle)
518    }
519}
520
521impl<A, T, S> CuDepthMap<A, CuDepthInteger<T, S>>
522where
523    T: CuDepthIntegerSample,
524    S: CuDepthScale,
525    A: ArrayLike<Element = T> + Send + Sync + 'static,
526{
527    pub fn from_integer(format: CuDepthMapFormat, buffer_handle: CuHandle<A>) -> Self {
528        Self::new_encoded(format, buffer_handle)
529    }
530}
531
532impl<A, E> cu29::pool::HandleContentAware for CuDepthMap<A, E>
533where
534    E: CuDepthEncoding,
535    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
536{
537}
538
539macro_rules! depth_map_serialized_schema {
540    ($sample_schema:literal, $invalid_schema:literal) => {
541        concat!(
542            r#"{"$schema":"https://json-schema.org/draft-07/schema#","type":"object","properties":{
543"format":{"type":"object","properties":{"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"stride":{"type":"integer","minimum":0}},"required":["width","height","stride"],"additionalProperties":false},
544"encoding":{"type":"object","properties":{"units_per_meter":{"type":"number"},"invalid":"#,
545            $invalid_schema,
546            r#"},"required":["units_per_meter","invalid"],"additionalProperties":false},
547"handle":{"type":"array","items":"#,
548            $sample_schema,
549            r#"}},"required":["format","encoding","handle"],"additionalProperties":false}"#
550        )
551    };
552}
553
554const LENGTH_DEPTH_MAP_SERIALIZED_SCHEMA: &str =
555    depth_map_serialized_schema!(r#"{"type":"number"}"#, r#"{"type":"null"}"#);
556
557const INTEGER_DEPTH_MAP_SERIALIZED_SCHEMA: &str = depth_map_serialized_schema!(
558    r#"{"type":"integer","minimum":0}"#,
559    r#"{"type":"object","properties":{"unsigned":{"type":"integer","const":0}},"required":["unsigned"],"additionalProperties":false}"#
560);
561
562const GENERIC_DEPTH_MAP_SERIALIZED_SCHEMA: &str = depth_map_serialized_schema!(r#"{}"#, r#"{}"#);
563
564impl<A, E> SerializedPayloadSchema for CuDepthMap<A, E>
565where
566    E: CuDepthEncoding,
567    A: ArrayLike<Element = E::Sample> + Send + Sync + 'static,
568{
569    fn serialized_payload_schema() -> &'static str {
570        E::SERIALIZED_MAP_SCHEMA
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577    use cu29_value::decode::{ValueDecodeDescription, ValueDecodeLimits};
578
579    type U16MillimeterDepth = CuDepthMap<Vec<u16>, CuDepthInteger<u16, CuDepthMillimeter>>;
580
581    const FORMAT: CuDepthMapFormat = CuDepthMapFormat {
582        width: 3,
583        height: 2,
584        stride: 4,
585    };
586
587    #[test]
588    fn test_catalog_decodes_native_integer_depth() {
589        let sample = U16MillimeterDepth::new_encoded(
590            CuDepthMapFormat {
591                width: 3,
592                height: 1,
593                stride: 3,
594            },
595            CuHandle::new_detached(vec![0u16, 1000, 65535]),
596        );
597        let config = bincode::config::standard();
598        let bytes = bincode::encode_to_vec(&sample, config).unwrap();
599        let description = ValueDecodeDescription::from_type::<U16MillimeterDepth>().unwrap();
600        let (decoded, used) = description
601            .decode(&bytes, config, ValueDecodeLimits::default())
602            .unwrap();
603        assert_eq!(used, bytes.len());
604        let Value::Map(fields) = decoded else {
605            panic!("expected named depth fields")
606        };
607        assert_eq!(
608            fields[&Value::String("buffer_handle".into())],
609            Value::Seq(vec![Value::U16(0), Value::U16(1000), Value::U16(65535)])
610        );
611    }
612
613    #[test]
614    fn padded_depth_map_indexes_distances_in_meters() {
615        let depth = CuDepthMap::new(
616            FORMAT,
617            CuHandle::new_detached(
618                [1.0, 1.5, f32::NAN, 99.0, 2.0, 2.5, 3.0, 99.0]
619                    .map(Length::new::<meter>)
620                    .to_vec(),
621            ),
622        );
623
624        assert_eq!(depth.get(0, 0), Some(Length::new::<meter>(1.0)));
625        assert_eq!(depth.get_meters(1, 1), Some(2.5));
626        assert!(depth.get_meters(2, 0).expect("in bounds").is_nan());
627        assert_eq!(depth.get(3, 0), None);
628        assert_eq!(depth.get(0, 2), None);
629    }
630
631    #[test]
632    fn sample_slice_covers_the_declared_layout_under_one_access() {
633        let depth = CuDepthMap::new(
634            FORMAT,
635            CuHandle::new_detached(
636                [1.0, 1.5, 2.0, 99.0, 2.5, 3.0, 3.5, 99.0, 100.0]
637                    .map(Length::new::<meter>)
638                    .to_vec(),
639            ),
640        );
641
642        depth.with_samples(|samples, format| {
643            assert_eq!(format, FORMAT);
644            assert_eq!(samples.len(), FORMAT.required_elements());
645            assert_eq!(samples[format.stride as usize], Length::new::<meter>(2.5));
646            assert_eq!(samples[3], Length::new::<meter>(99.0));
647        });
648    }
649
650    #[test]
651    fn sample_slice_can_be_mutated_under_one_access() {
652        let mut depth = CuDepthMap::new(
653            FORMAT,
654            CuHandle::new_detached(vec![Length::new::<meter>(0.0); FORMAT.required_elements()]),
655        );
656
657        depth.with_samples_mut(|samples, format| {
658            let index = format.stride as usize + 1;
659            samples[index] = Length::new::<meter>(4.25);
660        });
661
662        assert_eq!(depth.get_meters(1, 1), Some(4.25));
663    }
664
665    #[test]
666    fn compact_millimeter_depth_decodes_without_changing_raw_storage() {
667        let depth = U16MillimeterDepth::from_integer(
668            FORMAT,
669            CuHandle::new_detached(vec![1_000, 1_500, 0, 99, 2_000, 2_500, 3_000, 99]),
670        );
671
672        assert_eq!(depth.get_raw(1, 0), Some(1_500));
673        assert_eq!(depth.get_meters(1, 0), Some(1.5));
674        assert_eq!(depth.get_raw(2, 0), Some(0));
675        assert_eq!(depth.get(2, 0), None);
676        depth.with_samples(|samples, format| {
677            assert_eq!(format, FORMAT);
678            assert_eq!(samples, &[1_000, 1_500, 0, 99, 2_000, 2_500, 3_000, 99]);
679        });
680    }
681
682    #[test]
683    fn depth_maps_convert_between_encodings_without_touching_padding() {
684        let source = CuDepthMap::new(
685            FORMAT,
686            CuHandle::new_detached(
687                [1.234, f32::NAN, 2.0, -999.0, 3.5, 4.0, 5.0, -999.0]
688                    .map(Length::new::<meter>)
689                    .to_vec(),
690            ),
691        );
692        let target_format = CuDepthMapFormat {
693            width: FORMAT.width,
694            height: FORMAT.height,
695            stride: 5,
696        };
697        let mut compact = U16MillimeterDepth::from_integer(
698            target_format,
699            CuHandle::new_detached(vec![777u16; target_format.required_elements()]),
700        );
701
702        source.convert_into(&mut compact).expect("convert to u16");
703        compact.with_samples(|samples, _| {
704            assert_eq!(
705                samples,
706                &[1_234, 0, 2_000, 777, 777, 3_500, 4_000, 5_000, 777, 777]
707            );
708        });
709
710        let mut typed = CuDepthMap::new(
711            FORMAT,
712            CuHandle::new_detached(vec![Length::new::<meter>(-1.0); FORMAT.required_elements()]),
713        );
714        compact.convert_into(&mut typed).expect("convert to Length");
715        assert_eq!(typed.get_meters(0, 0), Some(1.234));
716        assert!(
717            typed
718                .get_raw(1, 0)
719                .expect("in bounds")
720                .get::<meter>()
721                .is_nan()
722        );
723    }
724
725    #[test]
726    fn depth_encodings_have_distinct_type_paths() {
727        assert_eq!(
728            <CuDepthMap<Vec<Length>> as TypePath>::type_path(),
729            "cu_sensor_payloads::CuDepthMap"
730        );
731        assert_eq!(
732            <U16MillimeterDepth as TypePath>::type_path(),
733            "cu_sensor_payloads::CuDepthMapInteger"
734        );
735    }
736
737    #[test]
738    fn integer_depth_supports_all_widths_and_arbitrary_length_scales() {
739        fn assert_integer_sample<T: CuDepthIntegerSample>() {}
740        assert_integer_sample::<u8>();
741        assert_integer_sample::<u16>();
742        assert_integer_sample::<u32>();
743        assert_integer_sample::<u64>();
744
745        type U16DecimeterDepth = CuDepthMap<Vec<u16>, CuDepthInteger<u16, CuDepthDecimeter>>;
746        let format = CuDepthMapFormat {
747            width: 1,
748            height: 1,
749            stride: 1,
750        };
751        let depth = U16DecimeterDepth::from_integer(format, CuHandle::new_detached(vec![2_000]));
752
753        assert_eq!(CuDepthDecimeter::RESOLUTION, Length::new::<meter>(0.1));
754        assert_eq!(
755            U16DecimeterDepth::encoding_descriptor().units_per_meter,
756            10.0
757        );
758        assert_eq!(depth.get_meters(0, 0), Some(200.0));
759    }
760
761    #[test]
762    fn format_rejects_short_stride() {
763        assert!(
764            !CuDepthMapFormat {
765                width: 4,
766                height: 2,
767                stride: 3,
768            }
769            .is_valid()
770        );
771    }
772
773    #[test]
774    fn serde_wire_uses_handle_field() {
775        let depth = CuDepthMap::new(
776            FORMAT,
777            CuHandle::new_detached(
778                [1.0, 1.5, 2.0, 0.0, 2.5, 3.0, 3.5, 0.0]
779                    .map(Length::new::<meter>)
780                    .to_vec(),
781            ),
782        );
783        let value = serde_json::to_value(depth).expect("serialize depth");
784        assert_eq!(value["format"]["stride"], 4);
785        assert_eq!(value["encoding"]["units_per_meter"], 1.0);
786        assert!(value["encoding"]["invalid"].is_null());
787        assert!(value.get("handle").is_some());
788        assert!(value.get("buffer_handle").is_none());
789        assert!(value.get("seq").is_none());
790    }
791
792    #[test]
793    fn compact_serde_wire_describes_units_and_invalid_value() {
794        let depth = U16MillimeterDepth::from_integer(
795            FORMAT,
796            CuHandle::new_detached(vec![0u16; FORMAT.required_elements()]),
797        );
798        let value = serde_json::to_value(depth).expect("serialize compact depth");
799
800        assert_eq!(value["encoding"]["units_per_meter"], 1_000.0);
801        assert_eq!(value["encoding"]["invalid"]["unsigned"], 0);
802    }
803
804    #[test]
805    fn compact_serialized_schema_matches_depth_wire_fields() {
806        let schema = serde_json::from_str::<serde_json::Value>(
807            U16MillimeterDepth::serialized_payload_schema(),
808        )
809        .expect("parse compact depth schema");
810
811        assert_eq!(schema["properties"]["handle"]["items"]["type"], "integer");
812        assert_eq!(
813            schema["properties"]["encoding"]["properties"]["invalid"]["properties"]["unsigned"]["const"],
814            0
815        );
816        assert_eq!(
817            schema["required"],
818            serde_json::json!(["format", "encoding", "handle"])
819        );
820    }
821}