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