1use bincode::de::Decoder;
2use bincode::error::DecodeError;
3use bincode::{Decode, Encode};
4use core::fmt::Debug;
5use core::ops::Range;
6use cu29::prelude::*;
7
8#[cfg(feature = "image")]
9use image::{ImageBuffer, Pixel};
10#[cfg(feature = "kornia")]
11use kornia_image::Image;
12#[cfg(feature = "kornia")]
13use kornia_tensor::MemoryDomain;
14use serde::{Deserialize, Serialize, Serializer};
15
16#[derive(Default, Debug, Encode, Decode, Clone, Copy, Serialize, Deserialize, Reflect)]
17#[bincode(describe)]
18pub struct CuImageBufferFormat {
19 pub width: u32,
20 pub height: u32,
21 pub stride: u32,
22 pub pixel_format: [u8; 4],
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct CuImagePlaneLayout {
27 pub offset_bytes: usize,
28 pub row_bytes: u32,
29 pub stride_bytes: u32,
30 pub height: u32,
31}
32
33impl CuImagePlaneLayout {
34 pub fn byte_len(&self) -> usize {
35 self.stride_bytes as usize * self.height as usize
36 }
37
38 pub fn byte_range(&self) -> Range<usize> {
39 self.offset_bytes..self.offset_bytes + self.byte_len()
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44enum CuImageMemoryLayout {
45 Packed { bytes_per_pixel: u32 },
46 SemiPlanar420,
47 Planar420,
48 SinglePlane,
49}
50
51impl CuImageBufferFormat {
52 fn memory_layout(&self) -> CuImageMemoryLayout {
53 match &self.pixel_format {
54 b"GRAY" | b"Y800" => CuImageMemoryLayout::Packed { bytes_per_pixel: 1 },
55 b"YUYV" | b"UYVY" => CuImageMemoryLayout::Packed { bytes_per_pixel: 2 },
56 b"RGB3" | b"BGR3" | b"RGB " | b"BGR " => {
57 CuImageMemoryLayout::Packed { bytes_per_pixel: 3 }
58 }
59 b"RGBA" | b"BGRA" => CuImageMemoryLayout::Packed { bytes_per_pixel: 4 },
60 b"NV12" | b"NV21" => CuImageMemoryLayout::SemiPlanar420,
61 b"I420" | b"YV12" => CuImageMemoryLayout::Planar420,
62 _ => CuImageMemoryLayout::SinglePlane,
63 }
64 }
65
66 pub fn plane_count(&self) -> usize {
67 match self.memory_layout() {
68 CuImageMemoryLayout::Planar420 => 3,
69 CuImageMemoryLayout::SemiPlanar420 => 2,
70 CuImageMemoryLayout::Packed { .. } | CuImageMemoryLayout::SinglePlane => 1,
71 }
72 }
73
74 pub fn is_packed(&self) -> bool {
75 matches!(self.memory_layout(), CuImageMemoryLayout::Packed { .. })
76 }
77
78 pub fn packed_row_bytes(&self) -> Option<u32> {
79 match self.memory_layout() {
80 CuImageMemoryLayout::Packed { bytes_per_pixel } => Some(self.width * bytes_per_pixel),
81 CuImageMemoryLayout::SemiPlanar420
82 | CuImageMemoryLayout::Planar420
83 | CuImageMemoryLayout::SinglePlane => None,
84 }
85 }
86
87 pub fn plane(&self, index: usize) -> Option<CuImagePlaneLayout> {
88 let y_plane_bytes = self.stride as usize * self.height as usize;
89 let chroma_height = self.height.div_ceil(2);
90
91 match self.memory_layout() {
92 CuImageMemoryLayout::Packed { bytes_per_pixel } if index == 0 => {
93 Some(CuImagePlaneLayout {
94 offset_bytes: 0,
95 row_bytes: self.width * bytes_per_pixel,
96 stride_bytes: self.stride,
97 height: self.height,
98 })
99 }
100 CuImageMemoryLayout::SinglePlane if index == 0 => Some(CuImagePlaneLayout {
101 offset_bytes: 0,
102 row_bytes: self.stride,
103 stride_bytes: self.stride,
104 height: self.height,
105 }),
106 CuImageMemoryLayout::SemiPlanar420 if index == 0 => Some(CuImagePlaneLayout {
107 offset_bytes: 0,
108 row_bytes: self.width,
109 stride_bytes: self.stride,
110 height: self.height,
111 }),
112 CuImageMemoryLayout::SemiPlanar420 if index == 1 => Some(CuImagePlaneLayout {
113 offset_bytes: y_plane_bytes,
114 row_bytes: self.width.div_ceil(2) * 2,
115 stride_bytes: self.stride,
116 height: chroma_height,
117 }),
118 CuImageMemoryLayout::Planar420 if index == 0 => Some(CuImagePlaneLayout {
119 offset_bytes: 0,
120 row_bytes: self.width,
121 stride_bytes: self.stride,
122 height: self.height,
123 }),
124 CuImageMemoryLayout::Planar420 if index == 1 => Some({
125 let chroma_stride = self.stride.div_ceil(2);
126 CuImagePlaneLayout {
127 offset_bytes: y_plane_bytes,
128 row_bytes: self.width.div_ceil(2),
129 stride_bytes: chroma_stride,
130 height: chroma_height,
131 }
132 }),
133 CuImageMemoryLayout::Planar420 if index == 2 => Some({
134 let chroma_stride = self.stride.div_ceil(2);
135 let chroma_plane_bytes = chroma_stride as usize * chroma_height as usize;
136 CuImagePlaneLayout {
137 offset_bytes: y_plane_bytes + chroma_plane_bytes,
138 row_bytes: self.width.div_ceil(2),
139 stride_bytes: chroma_stride,
140 height: chroma_height,
141 }
142 }),
143 _ => None,
144 }
145 }
146
147 pub fn is_valid(&self) -> bool {
148 (0..self.plane_count()).all(|index| {
149 self.plane(index)
150 .map(|plane| plane.row_bytes <= plane.stride_bytes)
151 .unwrap_or(false)
152 })
153 }
154
155 pub fn required_bytes(&self) -> usize {
156 self.plane(self.plane_count().saturating_sub(1))
157 .map(|plane| plane.offset_bytes + plane.byte_len())
158 .unwrap_or(0)
159 }
160
161 pub fn byte_size(&self) -> usize {
162 self.required_bytes()
163 }
164}
165
166#[derive(Debug, Default, Clone, Encode, Reflect)]
167#[reflect(from_reflect = false, no_field_bounds, type_path = false)]
168#[bincode(describe)]
169pub struct CuImage<A>
170where
171 A: ArrayLike<Element = u8> + Send + Sync + 'static,
172{
173 pub seq: u64,
174 pub format: CuImageBufferFormat,
175 pub buffer_handle: CuHandle<A>,
176}
177
178impl<A> TypePath for CuImage<A>
179where
180 A: ArrayLike<Element = u8> + Send + Sync + 'static,
181{
182 fn type_path() -> &'static str {
183 "cu_sensor_payloads::CuImage"
184 }
185
186 fn short_type_path() -> &'static str {
187 "CuImage"
188 }
189
190 fn type_ident() -> Option<&'static str> {
191 Some("CuImage")
192 }
193
194 fn crate_name() -> Option<&'static str> {
195 Some("cu_sensor_payloads")
196 }
197
198 fn module_path() -> Option<&'static str> {
199 Some("cu_sensor_payloads")
200 }
201}
202
203impl<A> Decode<()> for CuImage<A>
204where
205 A: ArrayLike<Element = u8> + Send + Sync + 'static,
206 CuHandle<A>: Decode<()>,
207{
208 fn decode<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<Self, DecodeError> {
209 let seq: u64 = Decode::decode(decoder)?;
210 let format: CuImageBufferFormat = Decode::decode(decoder)?;
211 let buffer_handle: CuHandle<A> = Decode::decode(decoder)?;
212
213 Ok(Self {
214 seq,
215 format,
216 buffer_handle,
217 })
218 }
219}
220
221impl<'de, A> Deserialize<'de> for CuImage<A>
222where
223 A: ArrayLike<Element = u8> + Send + Sync + 'static,
224 CuHandle<A>: Deserialize<'de>,
225{
226 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
227 where
228 D: serde::Deserializer<'de>,
229 {
230 #[derive(Deserialize)]
231 struct CuImageWire<H> {
232 seq: u64,
233 format: CuImageBufferFormat,
234 handle: H,
235 }
236
237 let wire = CuImageWire::<CuHandle<A>>::deserialize(deserializer)?;
238 Ok(Self {
239 seq: wire.seq,
240 format: wire.format,
241 buffer_handle: wire.handle,
242 })
243 }
244}
245
246impl<A> Serialize for CuImage<A>
247where
248 A: ArrayLike<Element = u8> + Send + Sync + 'static,
249 CuHandle<A>: Serialize,
250{
251 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
252 where
253 S: Serializer,
254 {
255 use serde::ser::SerializeStruct;
256 let mut struct_ = serializer.serialize_struct("CuImage", 3)?;
257 struct_.serialize_field("seq", &self.seq)?;
258 struct_.serialize_field("format", &self.format)?;
259 struct_.serialize_field("handle", &self.buffer_handle)?;
260 struct_.end()
261 }
262}
263
264impl<A> CuImage<A>
265where
266 A: ArrayLike<Element = u8> + Send + Sync + 'static,
267{
268 pub fn new(format: CuImageBufferFormat, buffer_handle: CuHandle<A>) -> Self {
269 assert!(
270 format.is_valid(),
271 "Image format layout is invalid for the declared stride."
272 );
273 assert!(
274 format.required_bytes() <= buffer_handle.with_inner(|i| i.len()),
275 "Buffer size must at least match the format."
276 );
277 CuImage {
278 seq: 0,
279 format,
280 buffer_handle,
281 }
282 }
283}
284
285impl<A> cu29::pool::HandleContentAware for CuImage<A> where
287 A: ArrayLike<Element = u8> + Send + Sync + 'static
288{
289}
290
291impl<A> CuImage<A>
292where
293 A: ArrayLike<Element = u8> + Send + Sync + 'static,
294{
295 pub fn payload_should_log(&self) -> bool {
300 self.buffer_handle.payload_should_log()
301 }
302
303 pub fn apply_handle_content_policy(&self, mode: cu29::pool::HandleContent) {
308 self.buffer_handle.apply_handle_content_policy(mode);
309 }
310
311 pub fn mark_touched(&self) {
315 self.buffer_handle.mark_touched();
316 }
317
318 pub fn with_plane_bytes<R>(
319 &self,
320 plane_index: usize,
321 f: impl FnOnce(&[u8], CuImagePlaneLayout) -> R,
322 ) -> CuResult<R> {
323 let plane = self
324 .format
325 .plane(plane_index)
326 .ok_or_else(|| CuError::from(format!("Invalid image plane index {plane_index}")))?;
327 Ok(self.buffer_handle.with_inner(|inner| {
328 let range = plane.byte_range();
329 f(&inner[range], plane)
330 }))
331 }
332
333 pub fn with_plane_bytes_mut<R>(
334 &mut self,
335 plane_index: usize,
336 f: impl FnOnce(&mut [u8], CuImagePlaneLayout) -> R,
337 ) -> CuResult<R> {
338 let plane = self
339 .format
340 .plane(plane_index)
341 .ok_or_else(|| CuError::from(format!("Invalid image plane index {plane_index}")))?;
342 Ok(self.buffer_handle.with_inner_mut(|inner| {
343 let range = plane.byte_range();
344 f(&mut inner[range], plane)
345 }))
346 }
347
348 #[cfg(feature = "image")]
350 pub fn as_image_buffer<P: Pixel>(&self) -> CuResult<ImageBuffer<P, &[P::Subpixel]>> {
351 let width = self.format.width;
352 let height = self.format.height;
353 let plane = self
354 .format
355 .plane(0)
356 .ok_or_else(|| CuError::from("Image format has no addressable planes"))?;
357 if self.format.plane_count() != 1 {
358 return Err(CuError::from(
359 "ImageBuffer compatibility requires a single-plane packed image.",
360 ));
361 }
362 if plane.row_bytes != plane.stride_bytes {
363 return Err(CuError::from(
364 "ImageBuffer compatibility requires tightly packed rows without padding.",
365 ));
366 }
367
368 self.with_plane_bytes(0, |data, _| {
369 let raw_pixels: &[P::Subpixel] = unsafe {
370 core::slice::from_raw_parts(
371 data.as_ptr() as *const P::Subpixel,
372 data.len() / core::mem::size_of::<P::Subpixel>(),
373 )
374 };
375 ImageBuffer::from_raw(width, height, raw_pixels)
376 .ok_or("Could not create the image:: buffer".into())
377 })?
378 }
379
380 #[cfg(feature = "kornia")]
386 pub fn as_kornia_image<T: Clone, const C: usize>(&self) -> CuResult<Image<T, C>> {
387 let width = self.format.width as usize;
388 let height = self.format.height as usize;
389 let plane = self
390 .format
391 .plane(0)
392 .ok_or_else(|| CuError::from("Image format has no addressable planes"))?;
393 if self.format.plane_count() != 1 {
394 return Err(CuError::from(
395 "Kornia compatibility requires a single-plane packed image.",
396 ));
397 }
398 if plane.row_bytes != plane.stride_bytes {
399 return Err(CuError::from(
400 "Kornia compatibility requires tightly packed rows without padding.",
401 ));
402 }
403
404 let len_bytes = width * height * C * core::mem::size_of::<T>();
405 let data = self.with_plane_bytes(0, |data, _| {
406 if data.len() < len_bytes {
407 return Err(CuError::from(
408 "Image plane is smaller than the requested Kornia image.",
409 ));
410 }
411 if !(data.as_ptr() as usize).is_multiple_of(core::mem::align_of::<T>()) {
412 return Err(CuError::from(
413 "Image plane is not aligned for the requested Kornia element type.",
414 ));
415 }
416 Ok(data.as_ptr() as *const T)
417 })??;
418
419 unsafe {
422 Image::from_borrowed_readonly(
423 [height, width].into(),
424 data,
425 MemoryDomain::Host,
426 std::sync::Arc::new(self.buffer_handle.clone()),
427 )
428 }
429 .map_err(|e| CuError::new_with_cause("Could not create a Kornia Image", e))
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::{CuImageBufferFormat, CuImagePlaneLayout};
436 use cu29_value::decode::{ValueDecodeDescription, ValueDecodeLimits};
437
438 #[test]
439 fn test_catalog_decodes_native_image() {
440 use super::CuImage;
441 use cu29::prelude::*;
442
443 let mut sample = CuImage::new(
444 CuImageBufferFormat {
445 width: 1,
446 height: 1,
447 stride: 3,
448 pixel_format: *b"RGB3",
449 },
450 CuHandle::new_detached(vec![1u8, 2, 255]),
451 );
452 sample.seq = 42;
453 let config = bincode::config::standard();
454 let bytes = bincode::encode_to_vec(&sample, config).unwrap();
455 let description = ValueDecodeDescription::from_type::<CuImage<Vec<u8>>>().unwrap();
456 let (decoded, used) = description
457 .decode(&bytes, config, ValueDecodeLimits::default())
458 .unwrap();
459 assert_eq!(used, bytes.len());
460 let Value::Map(fields) = decoded else {
461 panic!("expected named image fields")
462 };
463 assert_eq!(fields[&Value::String("seq".into())], Value::U64(42));
464 assert_eq!(
465 fields[&Value::String("format".into())],
466 to_value(sample.format).unwrap()
467 );
468 assert_eq!(
469 fields[&Value::String("buffer_handle".into())],
470 Value::Seq(vec![Value::U8(1), Value::U8(2), Value::U8(255)])
471 );
472 }
473
474 fn assert_plane(
475 plane: Option<CuImagePlaneLayout>,
476 offset_bytes: usize,
477 row_bytes: u32,
478 stride_bytes: u32,
479 height: u32,
480 ) {
481 assert_eq!(
482 plane,
483 Some(CuImagePlaneLayout {
484 offset_bytes,
485 row_bytes,
486 stride_bytes,
487 height,
488 })
489 );
490 }
491
492 #[test]
493 fn packed_rgb3_layout_uses_single_plane() {
494 let format = CuImageBufferFormat {
495 width: 4,
496 height: 2,
497 stride: 12,
498 pixel_format: *b"RGB3",
499 };
500
501 assert!(format.is_packed());
502 assert_eq!(format.plane_count(), 1);
503 assert_eq!(format.packed_row_bytes(), Some(12));
504 assert_plane(format.plane(0), 0, 12, 12, 2);
505 assert_eq!(format.required_bytes(), 24);
506 assert!(format.is_valid());
507 }
508
509 #[test]
510 fn nv12_layout_exposes_two_planes() {
511 let format = CuImageBufferFormat {
512 width: 640,
513 height: 360,
514 stride: 640,
515 pixel_format: *b"NV12",
516 };
517
518 assert!(!format.is_packed());
519 assert_eq!(format.plane_count(), 2);
520 assert_plane(format.plane(0), 0, 640, 640, 360);
521 assert_plane(format.plane(1), 230_400, 640, 640, 180);
522 assert_eq!(format.required_bytes(), 345_600);
523 assert!(format.is_valid());
524 }
525
526 #[test]
527 fn i420_layout_exposes_three_planes() {
528 let format = CuImageBufferFormat {
529 width: 640,
530 height: 360,
531 stride: 640,
532 pixel_format: *b"I420",
533 };
534
535 assert_eq!(format.plane_count(), 3);
536 assert_plane(format.plane(0), 0, 640, 640, 360);
537 assert_plane(format.plane(1), 230_400, 320, 320, 180);
538 assert_plane(format.plane(2), 288_000, 320, 320, 180);
539 assert_eq!(format.required_bytes(), 345_600);
540 assert!(format.is_valid());
541 }
542
543 #[test]
544 fn invalid_stride_is_detected_for_packed_formats() {
545 let format = CuImageBufferFormat {
546 width: 4,
547 height: 2,
548 stride: 4,
549 pixel_format: *b"RGB3",
550 };
551
552 assert!(!format.is_valid());
553 assert_eq!(format.packed_row_bytes(), Some(12));
554 }
555
556 #[test]
557 fn byte_size_for_packed_formats_is_stride_times_height() {
558 let format = CuImageBufferFormat {
559 width: 4,
560 height: 3,
561 stride: 16,
562 pixel_format: *b"BGRA",
563 };
564
565 assert_eq!(format.byte_size(), 48);
566 }
567
568 #[test]
569 fn byte_size_for_nv12_includes_uv_plane() {
570 let format = CuImageBufferFormat {
571 width: 1280,
572 height: 720,
573 stride: 1280,
574 pixel_format: *b"NV12",
575 };
576
577 assert_eq!(format.byte_size(), 1_382_400);
578 }
579
580 #[test]
581 fn byte_size_for_i420_includes_chroma_planes() {
582 let format = CuImageBufferFormat {
583 width: 640,
584 height: 480,
585 stride: 640,
586 pixel_format: *b"I420",
587 };
588
589 assert_eq!(format.byte_size(), 460_800);
590 }
591
592 #[cfg(feature = "kornia")]
611 mod kornia {
612 use super::super::{CuImage, CuImageBufferFormat};
613 use cu29::pool::CuHandle;
614
615 const RGB_2X2: CuImageBufferFormat = CuImageBufferFormat {
616 width: 2,
617 height: 2,
618 stride: 6,
619 pixel_format: *b"RGB3",
620 };
621
622 fn rgb_image() -> CuImage<Vec<u8>> {
623 CuImage::new(RGB_2X2, CuHandle::new_detached((0u8..12).collect()))
624 }
625
626 #[test]
627 fn as_kornia_image_wraps_the_plane() {
628 let image = rgb_image()
631 .as_kornia_image::<u8, 3>()
632 .expect("a packed RGB plane converts");
633 assert_eq!((image.width(), image.height()), (2, 2));
634 assert_eq!(image.as_slice(), (0u8..12).collect::<Vec<_>>().as_slice());
635 }
636
637 #[test]
638 fn as_kornia_image_refuses_a_plane_too_small_for_the_request() {
639 assert!(rgb_image().as_kornia_image::<u8, 4>().is_err());
641 }
642 }
643
644 mod only_log_what_you_use {
645 use crate::CuImage;
646 use bincode::config;
647 use cu29::config::HandleContent;
648 use cu29::cutask::{CuMsg, encode_metadata_only};
649 use cu29::pool::CuHandle;
650 const FORMAT: super::super::CuImageBufferFormat = super::super::CuImageBufferFormat {
657 width: 2,
658 height: 2,
659 stride: 2,
660 pixel_format: *b"GRAY",
661 };
662
663 fn make_image() -> CuImage<Vec<u8>> {
664 let handle = CuHandle::new_detached(vec![0xDE, 0xAD, 0xBE, 0xEF]);
667 CuImage::new(FORMAT, handle)
668 }
669
670 fn encode_with_policy(msg: &CuMsg<CuImage<Vec<u8>>>, mode: HandleContent) -> Vec<u8> {
679 let should_log = match msg.payload() {
680 Some(p) => {
681 p.apply_handle_content_policy(mode);
682 p.payload_should_log()
683 }
684 None => false,
685 };
686
687 use bincode::enc::write::Writer;
689 struct VecWriter(Vec<u8>);
690 impl Writer for VecWriter {
691 fn write(&mut self, bytes: &[u8]) -> Result<(), bincode::error::EncodeError> {
692 self.0.extend_from_slice(bytes);
693 Ok(())
694 }
695 }
696 let mut encoder =
697 bincode::enc::EncoderImpl::new(VecWriter(Vec::new()), config::standard());
698 use bincode::enc::Encode as _;
699 if should_log {
700 msg.encode(&mut encoder).expect("encode");
701 } else {
702 encode_metadata_only(msg, &mut encoder).expect("encode metadata-only");
703 }
704 encoder.into_writer().0
705 }
706
707 #[test]
709 fn touched_only_untouched_skips_payload() {
710 let image = make_image();
711 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
712 let bytes = encode_with_policy(&msg, HandleContent::TouchedOnly);
713 assert_eq!(
714 bytes.first().copied(),
715 Some(0u8),
716 "untouched TouchedOnly must emit no-payload tag"
717 );
718 }
719
720 #[test]
722 fn touched_only_touched_keeps_payload() {
723 let image = make_image();
724 image.mark_touched();
725 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
726 let bytes = encode_with_policy(&msg, HandleContent::TouchedOnly);
727 assert_eq!(bytes.first().copied(), Some(1u8));
728 assert!(bytes.len() > 8, "payload bytes must follow the present tag");
729 }
730
731 #[test]
733 fn none_always_skips_payload() {
734 let image = make_image();
735 image.mark_touched();
736 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
737 let bytes = encode_with_policy(&msg, HandleContent::None);
738 assert_eq!(bytes.first().copied(), Some(0u8));
739 }
740
741 #[test]
745 fn all_mode_keeps_payload_with_or_without_touch() {
746 for touched in [false, true] {
747 let image = make_image();
748 if touched {
749 image.mark_touched();
750 }
751 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
752 let bytes = encode_with_policy(&msg, HandleContent::All);
753 assert_eq!(
754 bytes.first().copied(),
755 Some(1u8),
756 "All mode must keep payload (touched={touched})"
757 );
758 }
759 }
760
761 #[test]
764 fn touched_flag_is_shared_across_clones() {
765 let image = make_image();
766 image.apply_handle_content_policy(HandleContent::TouchedOnly);
767 let consumer_view = image.buffer_handle.clone();
768 let _ = consumer_view.with_touched_inner(|inner| inner.as_ref()[0]);
769 assert!(image.payload_should_log());
770 }
771 }
772}