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