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_image::allocator::ImageAllocator;
14use serde::{Deserialize, Serialize, Serializer};
15
16#[derive(Default, Debug, Encode, Decode, Clone, Copy, Serialize, Deserialize, Reflect)]
17pub struct CuImageBufferFormat {
18 pub width: u32,
19 pub height: u32,
20 pub stride: u32,
21 pub pixel_format: [u8; 4],
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct CuImagePlaneLayout {
26 pub offset_bytes: usize,
27 pub row_bytes: u32,
28 pub stride_bytes: u32,
29 pub height: u32,
30}
31
32impl CuImagePlaneLayout {
33 pub fn byte_len(&self) -> usize {
34 self.stride_bytes as usize * self.height as usize
35 }
36
37 pub fn byte_range(&self) -> Range<usize> {
38 self.offset_bytes..self.offset_bytes + self.byte_len()
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43enum CuImageMemoryLayout {
44 Packed { bytes_per_pixel: u32 },
45 SemiPlanar420,
46 Planar420,
47 SinglePlane,
48}
49
50impl CuImageBufferFormat {
51 fn memory_layout(&self) -> CuImageMemoryLayout {
52 match &self.pixel_format {
53 b"GRAY" | b"Y800" => CuImageMemoryLayout::Packed { bytes_per_pixel: 1 },
54 b"YUYV" | b"UYVY" => CuImageMemoryLayout::Packed { bytes_per_pixel: 2 },
55 b"RGB3" | b"BGR3" | b"RGB " | b"BGR " => {
56 CuImageMemoryLayout::Packed { bytes_per_pixel: 3 }
57 }
58 b"RGBA" | b"BGRA" => CuImageMemoryLayout::Packed { bytes_per_pixel: 4 },
59 b"NV12" | b"NV21" => CuImageMemoryLayout::SemiPlanar420,
60 b"I420" | b"YV12" => CuImageMemoryLayout::Planar420,
61 _ => CuImageMemoryLayout::SinglePlane,
62 }
63 }
64
65 pub fn plane_count(&self) -> usize {
66 match self.memory_layout() {
67 CuImageMemoryLayout::Planar420 => 3,
68 CuImageMemoryLayout::SemiPlanar420 => 2,
69 CuImageMemoryLayout::Packed { .. } | CuImageMemoryLayout::SinglePlane => 1,
70 }
71 }
72
73 pub fn is_packed(&self) -> bool {
74 matches!(self.memory_layout(), CuImageMemoryLayout::Packed { .. })
75 }
76
77 pub fn packed_row_bytes(&self) -> Option<u32> {
78 match self.memory_layout() {
79 CuImageMemoryLayout::Packed { bytes_per_pixel } => Some(self.width * bytes_per_pixel),
80 CuImageMemoryLayout::SemiPlanar420
81 | CuImageMemoryLayout::Planar420
82 | CuImageMemoryLayout::SinglePlane => None,
83 }
84 }
85
86 pub fn plane(&self, index: usize) -> Option<CuImagePlaneLayout> {
87 let y_plane_bytes = self.stride as usize * self.height as usize;
88 let chroma_height = self.height.div_ceil(2);
89
90 match self.memory_layout() {
91 CuImageMemoryLayout::Packed { bytes_per_pixel } if index == 0 => {
92 Some(CuImagePlaneLayout {
93 offset_bytes: 0,
94 row_bytes: self.width * bytes_per_pixel,
95 stride_bytes: self.stride,
96 height: self.height,
97 })
98 }
99 CuImageMemoryLayout::SinglePlane if index == 0 => Some(CuImagePlaneLayout {
100 offset_bytes: 0,
101 row_bytes: self.stride,
102 stride_bytes: self.stride,
103 height: self.height,
104 }),
105 CuImageMemoryLayout::SemiPlanar420 if index == 0 => Some(CuImagePlaneLayout {
106 offset_bytes: 0,
107 row_bytes: self.width,
108 stride_bytes: self.stride,
109 height: self.height,
110 }),
111 CuImageMemoryLayout::SemiPlanar420 if index == 1 => Some(CuImagePlaneLayout {
112 offset_bytes: y_plane_bytes,
113 row_bytes: self.width.div_ceil(2) * 2,
114 stride_bytes: self.stride,
115 height: chroma_height,
116 }),
117 CuImageMemoryLayout::Planar420 if index == 0 => Some(CuImagePlaneLayout {
118 offset_bytes: 0,
119 row_bytes: self.width,
120 stride_bytes: self.stride,
121 height: self.height,
122 }),
123 CuImageMemoryLayout::Planar420 if index == 1 => Some({
124 let chroma_stride = self.stride.div_ceil(2);
125 CuImagePlaneLayout {
126 offset_bytes: y_plane_bytes,
127 row_bytes: self.width.div_ceil(2),
128 stride_bytes: chroma_stride,
129 height: chroma_height,
130 }
131 }),
132 CuImageMemoryLayout::Planar420 if index == 2 => Some({
133 let chroma_stride = self.stride.div_ceil(2);
134 let chroma_plane_bytes = chroma_stride as usize * chroma_height as usize;
135 CuImagePlaneLayout {
136 offset_bytes: y_plane_bytes + chroma_plane_bytes,
137 row_bytes: self.width.div_ceil(2),
138 stride_bytes: chroma_stride,
139 height: chroma_height,
140 }
141 }),
142 _ => None,
143 }
144 }
145
146 pub fn is_valid(&self) -> bool {
147 (0..self.plane_count()).all(|index| {
148 self.plane(index)
149 .map(|plane| plane.row_bytes <= plane.stride_bytes)
150 .unwrap_or(false)
151 })
152 }
153
154 pub fn required_bytes(&self) -> usize {
155 self.plane(self.plane_count().saturating_sub(1))
156 .map(|plane| plane.offset_bytes + plane.byte_len())
157 .unwrap_or(0)
158 }
159
160 pub fn byte_size(&self) -> usize {
161 self.required_bytes()
162 }
163}
164
165#[derive(Debug, Default, Clone, Encode, Reflect)]
166#[reflect(from_reflect = false, no_field_bounds, type_path = false)]
167pub struct CuImage<A>
168where
169 A: ArrayLike<Element = u8> + Send + Sync + 'static,
170{
171 pub seq: u64,
172 pub format: CuImageBufferFormat,
173 #[reflect(ignore)]
174 pub buffer_handle: CuHandle<A>,
175}
176
177impl<A> TypePath for CuImage<A>
178where
179 A: ArrayLike<Element = u8> + Send + Sync + 'static,
180{
181 fn type_path() -> &'static str {
182 "cu_sensor_payloads::CuImage"
183 }
184
185 fn short_type_path() -> &'static str {
186 "CuImage"
187 }
188
189 fn type_ident() -> Option<&'static str> {
190 Some("CuImage")
191 }
192
193 fn crate_name() -> Option<&'static str> {
194 Some("cu_sensor_payloads")
195 }
196
197 fn module_path() -> Option<&'static str> {
198 Some("cu_sensor_payloads")
199 }
200}
201
202impl<A> Decode<()> for CuImage<A>
203where
204 A: ArrayLike<Element = u8> + Send + Sync + 'static,
205 CuHandle<A>: Decode<()>,
206{
207 fn decode<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<Self, DecodeError> {
208 let seq: u64 = Decode::decode(decoder)?;
209 let format: CuImageBufferFormat = Decode::decode(decoder)?;
210 let buffer_handle: CuHandle<A> = Decode::decode(decoder)?;
211
212 Ok(Self {
213 seq,
214 format,
215 buffer_handle,
216 })
217 }
218}
219
220impl<'de, A> Deserialize<'de> for CuImage<A>
221where
222 A: ArrayLike<Element = u8> + Send + Sync + 'static,
223 CuHandle<A>: Deserialize<'de>,
224{
225 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
226 where
227 D: serde::Deserializer<'de>,
228 {
229 #[derive(Deserialize)]
230 struct CuImageWire<H> {
231 seq: u64,
232 format: CuImageBufferFormat,
233 handle: H,
234 }
235
236 let wire = CuImageWire::<CuHandle<A>>::deserialize(deserializer)?;
237 Ok(Self {
238 seq: wire.seq,
239 format: wire.format,
240 buffer_handle: wire.handle,
241 })
242 }
243}
244
245impl<A> Serialize for CuImage<A>
246where
247 A: ArrayLike<Element = u8> + Send + Sync + 'static,
248 CuHandle<A>: Serialize,
249{
250 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
251 where
252 S: Serializer,
253 {
254 use serde::ser::SerializeStruct;
255 let mut struct_ = serializer.serialize_struct("CuImage", 3)?;
256 struct_.serialize_field("seq", &self.seq)?;
257 struct_.serialize_field("format", &self.format)?;
258 struct_.serialize_field("handle", &self.buffer_handle)?;
259 struct_.end()
260 }
261}
262
263impl<A> CuImage<A>
264where
265 A: ArrayLike<Element = u8> + Send + Sync + 'static,
266{
267 pub fn new(format: CuImageBufferFormat, buffer_handle: CuHandle<A>) -> Self {
268 assert!(
269 format.is_valid(),
270 "Image format layout is invalid for the declared stride."
271 );
272 assert!(
273 format.required_bytes() <= buffer_handle.with_inner(|i| i.len()),
274 "Buffer size must at least match the format."
275 );
276 CuImage {
277 seq: 0,
278 format,
279 buffer_handle,
280 }
281 }
282}
283
284impl<A> cu29::pool::HandleContentAware for CuImage<A> where
286 A: ArrayLike<Element = u8> + Send + Sync + 'static
287{
288}
289
290impl<A> CuImage<A>
291where
292 A: ArrayLike<Element = u8> + Send + Sync + 'static,
293{
294 pub fn payload_should_log(&self) -> bool {
299 self.buffer_handle.payload_should_log()
300 }
301
302 pub fn apply_handle_content_policy(&self, mode: cu29::pool::HandleContent) {
307 self.buffer_handle.apply_handle_content_policy(mode);
308 }
309
310 pub fn mark_touched(&self) {
314 self.buffer_handle.mark_touched();
315 }
316
317 pub fn with_plane_bytes<R>(
318 &self,
319 plane_index: usize,
320 f: impl FnOnce(&[u8], CuImagePlaneLayout) -> R,
321 ) -> CuResult<R> {
322 let plane = self
323 .format
324 .plane(plane_index)
325 .ok_or_else(|| CuError::from(format!("Invalid image plane index {plane_index}")))?;
326 Ok(self.buffer_handle.with_inner(|inner| {
327 let range = plane.byte_range();
328 f(&inner[range], plane)
329 }))
330 }
331
332 pub fn with_plane_bytes_mut<R>(
333 &mut self,
334 plane_index: usize,
335 f: impl FnOnce(&mut [u8], CuImagePlaneLayout) -> R,
336 ) -> CuResult<R> {
337 let plane = self
338 .format
339 .plane(plane_index)
340 .ok_or_else(|| CuError::from(format!("Invalid image plane index {plane_index}")))?;
341 Ok(self.buffer_handle.with_inner_mut(|inner| {
342 let range = plane.byte_range();
343 f(&mut inner[range], plane)
344 }))
345 }
346
347 #[cfg(feature = "image")]
349 pub fn as_image_buffer<P: Pixel>(&self) -> CuResult<ImageBuffer<P, &[P::Subpixel]>> {
350 let width = self.format.width;
351 let height = self.format.height;
352 let plane = self
353 .format
354 .plane(0)
355 .ok_or_else(|| CuError::from("Image format has no addressable planes"))?;
356 if self.format.plane_count() != 1 {
357 return Err(CuError::from(
358 "ImageBuffer compatibility requires a single-plane packed image.",
359 ));
360 }
361 if plane.row_bytes != plane.stride_bytes {
362 return Err(CuError::from(
363 "ImageBuffer compatibility requires tightly packed rows without padding.",
364 ));
365 }
366
367 self.with_plane_bytes(0, |data, _| {
368 let raw_pixels: &[P::Subpixel] = unsafe {
369 core::slice::from_raw_parts(
370 data.as_ptr() as *const P::Subpixel,
371 data.len() / core::mem::size_of::<P::Subpixel>(),
372 )
373 };
374 ImageBuffer::from_raw(width, height, raw_pixels)
375 .ok_or("Could not create the image:: buffer".into())
376 })?
377 }
378
379 #[cfg(feature = "kornia")]
380 pub fn as_kornia_image<T: Clone, const C: usize, K: ImageAllocator>(
381 &self,
382 k: K,
383 ) -> CuResult<Image<T, C, K>> {
384 let width = self.format.width as usize;
385 let height = self.format.height as usize;
386 let plane = self
387 .format
388 .plane(0)
389 .ok_or_else(|| CuError::from("Image format has no addressable planes"))?;
390 if self.format.plane_count() != 1 {
391 return Err(CuError::from(
392 "Kornia compatibility requires a single-plane packed image.",
393 ));
394 }
395 if plane.row_bytes != plane.stride_bytes {
396 return Err(CuError::from(
397 "Kornia compatibility requires tightly packed rows without padding.",
398 ));
399 }
400
401 let size = width * height * C;
402 self.with_plane_bytes(0, |data, _| {
403 let raw_pixels: &[T] = unsafe {
404 core::slice::from_raw_parts(
405 data.as_ptr() as *const T,
406 data.len() / core::mem::size_of::<T>(),
407 )
408 };
409
410 unsafe { Image::from_raw_parts([height, width].into(), raw_pixels.as_ptr(), size, k) }
411 .map_err(|e| CuError::new_with_cause("Could not create a Kornia Image", e))
412 })?
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::{CuImageBufferFormat, CuImagePlaneLayout};
419
420 fn assert_plane(
421 plane: Option<CuImagePlaneLayout>,
422 offset_bytes: usize,
423 row_bytes: u32,
424 stride_bytes: u32,
425 height: u32,
426 ) {
427 assert_eq!(
428 plane,
429 Some(CuImagePlaneLayout {
430 offset_bytes,
431 row_bytes,
432 stride_bytes,
433 height,
434 })
435 );
436 }
437
438 #[test]
439 fn packed_rgb3_layout_uses_single_plane() {
440 let format = CuImageBufferFormat {
441 width: 4,
442 height: 2,
443 stride: 12,
444 pixel_format: *b"RGB3",
445 };
446
447 assert!(format.is_packed());
448 assert_eq!(format.plane_count(), 1);
449 assert_eq!(format.packed_row_bytes(), Some(12));
450 assert_plane(format.plane(0), 0, 12, 12, 2);
451 assert_eq!(format.required_bytes(), 24);
452 assert!(format.is_valid());
453 }
454
455 #[test]
456 fn nv12_layout_exposes_two_planes() {
457 let format = CuImageBufferFormat {
458 width: 640,
459 height: 360,
460 stride: 640,
461 pixel_format: *b"NV12",
462 };
463
464 assert!(!format.is_packed());
465 assert_eq!(format.plane_count(), 2);
466 assert_plane(format.plane(0), 0, 640, 640, 360);
467 assert_plane(format.plane(1), 230_400, 640, 640, 180);
468 assert_eq!(format.required_bytes(), 345_600);
469 assert!(format.is_valid());
470 }
471
472 #[test]
473 fn i420_layout_exposes_three_planes() {
474 let format = CuImageBufferFormat {
475 width: 640,
476 height: 360,
477 stride: 640,
478 pixel_format: *b"I420",
479 };
480
481 assert_eq!(format.plane_count(), 3);
482 assert_plane(format.plane(0), 0, 640, 640, 360);
483 assert_plane(format.plane(1), 230_400, 320, 320, 180);
484 assert_plane(format.plane(2), 288_000, 320, 320, 180);
485 assert_eq!(format.required_bytes(), 345_600);
486 assert!(format.is_valid());
487 }
488
489 #[test]
490 fn invalid_stride_is_detected_for_packed_formats() {
491 let format = CuImageBufferFormat {
492 width: 4,
493 height: 2,
494 stride: 4,
495 pixel_format: *b"RGB3",
496 };
497
498 assert!(!format.is_valid());
499 assert_eq!(format.packed_row_bytes(), Some(12));
500 }
501
502 #[test]
503 fn byte_size_for_packed_formats_is_stride_times_height() {
504 let format = CuImageBufferFormat {
505 width: 4,
506 height: 3,
507 stride: 16,
508 pixel_format: *b"BGRA",
509 };
510
511 assert_eq!(format.byte_size(), 48);
512 }
513
514 #[test]
515 fn byte_size_for_nv12_includes_uv_plane() {
516 let format = CuImageBufferFormat {
517 width: 1280,
518 height: 720,
519 stride: 1280,
520 pixel_format: *b"NV12",
521 };
522
523 assert_eq!(format.byte_size(), 1_382_400);
524 }
525
526 #[test]
527 fn byte_size_for_i420_includes_chroma_planes() {
528 let format = CuImageBufferFormat {
529 width: 640,
530 height: 480,
531 stride: 640,
532 pixel_format: *b"I420",
533 };
534
535 assert_eq!(format.byte_size(), 460_800);
536 }
537
538 mod only_log_what_you_use {
557 use crate::CuImage;
558 use bincode::config;
559 use cu29::config::HandleContent;
560 use cu29::cutask::{CuMsg, encode_metadata_only};
561 use cu29::pool::CuHandle;
562 const FORMAT: super::super::CuImageBufferFormat = super::super::CuImageBufferFormat {
569 width: 2,
570 height: 2,
571 stride: 2,
572 pixel_format: *b"GRAY",
573 };
574
575 fn make_image() -> CuImage<Vec<u8>> {
576 let handle = CuHandle::new_detached(vec![0xDE, 0xAD, 0xBE, 0xEF]);
579 CuImage::new(FORMAT, handle)
580 }
581
582 fn encode_with_policy(msg: &CuMsg<CuImage<Vec<u8>>>, mode: HandleContent) -> Vec<u8> {
591 let should_log = match msg.payload() {
592 Some(p) => {
593 p.apply_handle_content_policy(mode);
594 p.payload_should_log()
595 }
596 None => false,
597 };
598
599 use bincode::enc::write::Writer;
601 struct VecWriter(Vec<u8>);
602 impl Writer for VecWriter {
603 fn write(&mut self, bytes: &[u8]) -> Result<(), bincode::error::EncodeError> {
604 self.0.extend_from_slice(bytes);
605 Ok(())
606 }
607 }
608 let mut encoder =
609 bincode::enc::EncoderImpl::new(VecWriter(Vec::new()), config::standard());
610 use bincode::enc::Encode as _;
611 if should_log {
612 msg.encode(&mut encoder).expect("encode");
613 } else {
614 encode_metadata_only(msg, &mut encoder).expect("encode metadata-only");
615 }
616 encoder.into_writer().0
617 }
618
619 #[test]
621 fn touched_only_untouched_skips_payload() {
622 let image = make_image();
623 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
624 let bytes = encode_with_policy(&msg, HandleContent::TouchedOnly);
625 assert_eq!(
626 bytes.first().copied(),
627 Some(0u8),
628 "untouched TouchedOnly must emit no-payload tag"
629 );
630 }
631
632 #[test]
634 fn touched_only_touched_keeps_payload() {
635 let image = make_image();
636 image.mark_touched();
637 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
638 let bytes = encode_with_policy(&msg, HandleContent::TouchedOnly);
639 assert_eq!(bytes.first().copied(), Some(1u8));
640 assert!(bytes.len() > 8, "payload bytes must follow the present tag");
641 }
642
643 #[test]
645 fn none_always_skips_payload() {
646 let image = make_image();
647 image.mark_touched();
648 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
649 let bytes = encode_with_policy(&msg, HandleContent::None);
650 assert_eq!(bytes.first().copied(), Some(0u8));
651 }
652
653 #[test]
657 fn all_mode_keeps_payload_with_or_without_touch() {
658 for touched in [false, true] {
659 let image = make_image();
660 if touched {
661 image.mark_touched();
662 }
663 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
664 let bytes = encode_with_policy(&msg, HandleContent::All);
665 assert_eq!(
666 bytes.first().copied(),
667 Some(1u8),
668 "All mode must keep payload (touched={touched})"
669 );
670 }
671 }
672
673 #[test]
676 fn touched_flag_is_shared_across_clones() {
677 let image = make_image();
678 image.apply_handle_content_policy(HandleContent::TouchedOnly);
679 let consumer_view = image.buffer_handle.clone();
680 let _ = consumer_view.with_touched_inner(|inner| inner.as_ref()[0]);
681 assert!(image.payload_should_log());
682 }
683 }
684}