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, 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> Clone for CuImage<A>
181where
182 A: ArrayLike<Element = u8> + Send + Sync + 'static,
183{
184 fn clone(&self) -> Self {
185 Self {
186 seq: self.seq,
187 format: self.format,
188 buffer_handle: self.buffer_handle.clone(),
189 }
190 }
191}
192
193impl<A> Default for CuImage<A>
194where
195 A: ArrayLike<Element = u8> + Send + Sync + 'static,
196{
197 fn default() -> Self {
198 Self {
199 seq: 0,
200 format: CuImageBufferFormat::default(),
201 buffer_handle: CuHandle::default(),
202 }
203 }
204}
205
206impl<A> TypePath for CuImage<A>
207where
208 A: ArrayLike<Element = u8> + Send + Sync + 'static,
209{
210 fn type_path() -> &'static str {
211 "cu_sensor_payloads::CuImage"
212 }
213
214 fn short_type_path() -> &'static str {
215 "CuImage"
216 }
217
218 fn type_ident() -> Option<&'static str> {
219 Some("CuImage")
220 }
221
222 fn crate_name() -> Option<&'static str> {
223 Some("cu_sensor_payloads")
224 }
225
226 fn module_path() -> Option<&'static str> {
227 Some("cu_sensor_payloads")
228 }
229}
230
231impl<A> Decode<()> for CuImage<A>
232where
233 A: ArrayLike<Element = u8> + Send + Sync + 'static,
234 CuHandle<A>: Decode<()>,
235{
236 fn decode<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<Self, DecodeError> {
237 let seq: u64 = Decode::decode(decoder)?;
238 let format: CuImageBufferFormat = Decode::decode(decoder)?;
239 let buffer_handle: CuHandle<A> = Decode::decode(decoder)?;
240
241 Ok(Self {
242 seq,
243 format,
244 buffer_handle,
245 })
246 }
247}
248
249impl<'de, A> Deserialize<'de> for CuImage<A>
250where
251 A: ArrayLike<Element = u8> + Send + Sync + 'static,
252 CuHandle<A>: Deserialize<'de>,
253{
254 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
255 where
256 D: serde::Deserializer<'de>,
257 {
258 #[derive(Deserialize)]
259 struct CuImageWire<H> {
260 seq: u64,
261 format: CuImageBufferFormat,
262 handle: H,
263 }
264
265 let wire = CuImageWire::<CuHandle<A>>::deserialize(deserializer)?;
266 Ok(Self {
267 seq: wire.seq,
268 format: wire.format,
269 buffer_handle: wire.handle,
270 })
271 }
272}
273
274impl<A> Serialize for CuImage<A>
275where
276 A: ArrayLike<Element = u8> + Send + Sync + 'static,
277 CuHandle<A>: Serialize,
278{
279 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
280 where
281 S: Serializer,
282 {
283 use serde::ser::SerializeStruct;
284 let mut struct_ = serializer.serialize_struct("CuImage", 3)?;
285 struct_.serialize_field("seq", &self.seq)?;
286 struct_.serialize_field("format", &self.format)?;
287 struct_.serialize_field("handle", &self.buffer_handle)?;
288 struct_.end()
289 }
290}
291
292impl<A> CuImage<A>
293where
294 A: ArrayLike<Element = u8> + Send + Sync + 'static,
295{
296 pub fn new(format: CuImageBufferFormat, buffer_handle: CuHandle<A>) -> Self {
297 assert!(
298 format.is_valid(),
299 "Image format layout is invalid for the declared stride."
300 );
301 assert!(
302 format.required_bytes() <= buffer_handle.with_inner(|i| i.len()),
303 "Buffer size must at least match the format."
304 );
305 CuImage {
306 seq: 0,
307 format,
308 buffer_handle,
309 }
310 }
311}
312
313impl<A> cu29::pool::HandleContentAware for CuImage<A> where
315 A: ArrayLike<Element = u8> + Send + Sync + 'static
316{
317}
318
319impl<A> CuImage<A>
320where
321 A: ArrayLike<Element = u8> + Send + Sync + 'static,
322{
323 pub fn payload_should_log(&self) -> bool {
328 self.buffer_handle.payload_should_log()
329 }
330
331 pub fn apply_handle_content_policy(&self, mode: cu29::pool::HandleContent) {
336 self.buffer_handle.apply_handle_content_policy(mode);
337 }
338
339 pub fn mark_touched(&self) {
343 self.buffer_handle.mark_touched();
344 }
345
346 pub fn with_plane_bytes<R>(
347 &self,
348 plane_index: usize,
349 f: impl FnOnce(&[u8], CuImagePlaneLayout) -> R,
350 ) -> CuResult<R> {
351 let plane = self
352 .format
353 .plane(plane_index)
354 .ok_or_else(|| CuError::from(format!("Invalid image plane index {plane_index}")))?;
355 Ok(self.buffer_handle.with_inner(|inner| {
356 let range = plane.byte_range();
357 f(&inner[range], plane)
358 }))
359 }
360
361 pub fn with_plane_bytes_mut<R>(
362 &mut self,
363 plane_index: usize,
364 f: impl FnOnce(&mut [u8], CuImagePlaneLayout) -> R,
365 ) -> CuResult<R> {
366 let plane = self
367 .format
368 .plane(plane_index)
369 .ok_or_else(|| CuError::from(format!("Invalid image plane index {plane_index}")))?;
370 Ok(self.buffer_handle.with_inner_mut(|inner| {
371 let range = plane.byte_range();
372 f(&mut inner[range], plane)
373 }))
374 }
375
376 #[cfg(feature = "image")]
378 pub fn as_image_buffer<P: Pixel>(&self) -> CuResult<ImageBuffer<P, &[P::Subpixel]>> {
379 let width = self.format.width;
380 let height = self.format.height;
381 let plane = self
382 .format
383 .plane(0)
384 .ok_or_else(|| CuError::from("Image format has no addressable planes"))?;
385 if self.format.plane_count() != 1 {
386 return Err(CuError::from(
387 "ImageBuffer compatibility requires a single-plane packed image.",
388 ));
389 }
390 if plane.row_bytes != plane.stride_bytes {
391 return Err(CuError::from(
392 "ImageBuffer compatibility requires tightly packed rows without padding.",
393 ));
394 }
395
396 self.with_plane_bytes(0, |data, _| {
397 let raw_pixels: &[P::Subpixel] = unsafe {
398 core::slice::from_raw_parts(
399 data.as_ptr() as *const P::Subpixel,
400 data.len() / core::mem::size_of::<P::Subpixel>(),
401 )
402 };
403 ImageBuffer::from_raw(width, height, raw_pixels)
404 .ok_or("Could not create the image:: buffer".into())
405 })?
406 }
407
408 #[cfg(feature = "kornia")]
414 pub fn as_kornia_image<T: Clone, const C: usize>(&self) -> CuResult<Image<T, C>> {
415 let width = self.format.width as usize;
416 let height = self.format.height as usize;
417 let plane = self
418 .format
419 .plane(0)
420 .ok_or_else(|| CuError::from("Image format has no addressable planes"))?;
421 if self.format.plane_count() != 1 {
422 return Err(CuError::from(
423 "Kornia compatibility requires a single-plane packed image.",
424 ));
425 }
426 if plane.row_bytes != plane.stride_bytes {
427 return Err(CuError::from(
428 "Kornia compatibility requires tightly packed rows without padding.",
429 ));
430 }
431
432 let len_bytes = width * height * C * core::mem::size_of::<T>();
433 let data = self.with_plane_bytes(0, |data, _| {
434 if data.len() < len_bytes {
435 return Err(CuError::from(
436 "Image plane is smaller than the requested Kornia image.",
437 ));
438 }
439 if !(data.as_ptr() as usize).is_multiple_of(core::mem::align_of::<T>()) {
440 return Err(CuError::from(
441 "Image plane is not aligned for the requested Kornia element type.",
442 ));
443 }
444 Ok(data.as_ptr() as *const T)
445 })??;
446
447 unsafe {
450 Image::from_borrowed_readonly(
451 [height, width].into(),
452 data,
453 MemoryDomain::Host,
454 std::sync::Arc::new(self.buffer_handle.clone()),
455 )
456 }
457 .map_err(|e| CuError::new_with_cause("Could not create a Kornia Image", e))
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::{CuImageBufferFormat, CuImagePlaneLayout};
464 use cu29_value::decode::{ValueDecodeDescription, ValueDecodeLimits};
465
466 #[cfg(feature = "std")]
467 #[test]
468 fn test_shared_memory_image_clones_its_handle_and_satisfies_payload_contract() {
469 use super::CuImage;
470 use cu29::pool::CuPool;
471 use cu29::pool::CuSharedMemoryBuffer;
472 use cu29::pool::CuSharedMemoryPool;
473 use cu29::prelude::*;
474
475 fn assert_payload<T: CuMsgPayload>() {}
476 assert_payload::<CuImage<CuSharedMemoryBuffer<u8>>>();
477
478 let pool = CuSharedMemoryPool::new("image-clone-test", 1, 3).unwrap();
479 let handle = pool.acquire().unwrap();
480 handle.with_inner_mut(|bytes| bytes.copy_from_slice(&[12, 34, 56]));
481 let mut image = CuImage::new(
482 CuImageBufferFormat {
483 width: 1,
484 height: 1,
485 stride: 3,
486 pixel_format: *b"RGB3",
487 },
488 handle,
489 );
490 image.seq = 42;
491 let cloned = image.clone();
492 assert_eq!(cloned.seq, 42);
493 assert_eq!(cloned.format.pixel_format, *b"RGB3");
494 assert_eq!(
495 cloned.buffer_handle.storage_id(),
496 image.buffer_handle.storage_id()
497 );
498 drop(image);
499 assert!(pool.acquire().is_none());
500 assert_eq!(
501 cloned
502 .with_plane_bytes(0, |bytes, _| bytes.to_vec())
503 .unwrap(),
504 [12, 34, 56]
505 );
506
507 let config = bincode::config::standard();
508 let encoded = bincode::encode_to_vec(&cloned, config).unwrap();
509 drop(cloned);
510 assert!(pool.acquire().is_some());
511 let (decoded, used): (CuImage<CuSharedMemoryBuffer<u8>>, usize) =
512 bincode::decode_from_slice(&encoded, config).unwrap();
513 assert_eq!(used, encoded.len());
514 assert_eq!(decoded.seq, 42);
515 assert_eq!(
516 decoded
517 .with_plane_bytes(0, |bytes, _| bytes.to_vec())
518 .unwrap(),
519 [12, 34, 56]
520 );
521 }
522
523 #[test]
524 fn test_catalog_decodes_native_image() {
525 use super::CuImage;
526 use cu29::prelude::*;
527
528 let mut sample = CuImage::new(
529 CuImageBufferFormat {
530 width: 1,
531 height: 1,
532 stride: 3,
533 pixel_format: *b"RGB3",
534 },
535 CuHandle::new_detached(vec![1u8, 2, 255]),
536 );
537 sample.seq = 42;
538 let config = bincode::config::standard();
539 let bytes = bincode::encode_to_vec(&sample, config).unwrap();
540 let description = ValueDecodeDescription::from_type::<CuImage<Vec<u8>>>().unwrap();
541 let (decoded, used) = description
542 .decode(&bytes, config, ValueDecodeLimits::default())
543 .unwrap();
544 assert_eq!(used, bytes.len());
545 let Value::Map(fields) = decoded else {
546 panic!("expected named image fields")
547 };
548 assert_eq!(fields[&Value::String("seq".into())], Value::U64(42));
549 assert_eq!(
550 fields[&Value::String("format".into())],
551 to_value(sample.format).unwrap()
552 );
553 assert_eq!(
554 fields[&Value::String("buffer_handle".into())],
555 Value::Seq(vec![Value::U8(1), Value::U8(2), Value::U8(255)])
556 );
557 }
558
559 fn assert_plane(
560 plane: Option<CuImagePlaneLayout>,
561 offset_bytes: usize,
562 row_bytes: u32,
563 stride_bytes: u32,
564 height: u32,
565 ) {
566 assert_eq!(
567 plane,
568 Some(CuImagePlaneLayout {
569 offset_bytes,
570 row_bytes,
571 stride_bytes,
572 height,
573 })
574 );
575 }
576
577 #[test]
578 fn packed_rgb3_layout_uses_single_plane() {
579 let format = CuImageBufferFormat {
580 width: 4,
581 height: 2,
582 stride: 12,
583 pixel_format: *b"RGB3",
584 };
585
586 assert!(format.is_packed());
587 assert_eq!(format.plane_count(), 1);
588 assert_eq!(format.packed_row_bytes(), Some(12));
589 assert_plane(format.plane(0), 0, 12, 12, 2);
590 assert_eq!(format.required_bytes(), 24);
591 assert!(format.is_valid());
592 }
593
594 #[test]
595 fn nv12_layout_exposes_two_planes() {
596 let format = CuImageBufferFormat {
597 width: 640,
598 height: 360,
599 stride: 640,
600 pixel_format: *b"NV12",
601 };
602
603 assert!(!format.is_packed());
604 assert_eq!(format.plane_count(), 2);
605 assert_plane(format.plane(0), 0, 640, 640, 360);
606 assert_plane(format.plane(1), 230_400, 640, 640, 180);
607 assert_eq!(format.required_bytes(), 345_600);
608 assert!(format.is_valid());
609 }
610
611 #[test]
612 fn i420_layout_exposes_three_planes() {
613 let format = CuImageBufferFormat {
614 width: 640,
615 height: 360,
616 stride: 640,
617 pixel_format: *b"I420",
618 };
619
620 assert_eq!(format.plane_count(), 3);
621 assert_plane(format.plane(0), 0, 640, 640, 360);
622 assert_plane(format.plane(1), 230_400, 320, 320, 180);
623 assert_plane(format.plane(2), 288_000, 320, 320, 180);
624 assert_eq!(format.required_bytes(), 345_600);
625 assert!(format.is_valid());
626 }
627
628 #[test]
629 fn invalid_stride_is_detected_for_packed_formats() {
630 let format = CuImageBufferFormat {
631 width: 4,
632 height: 2,
633 stride: 4,
634 pixel_format: *b"RGB3",
635 };
636
637 assert!(!format.is_valid());
638 assert_eq!(format.packed_row_bytes(), Some(12));
639 }
640
641 #[test]
642 fn byte_size_for_packed_formats_is_stride_times_height() {
643 let format = CuImageBufferFormat {
644 width: 4,
645 height: 3,
646 stride: 16,
647 pixel_format: *b"BGRA",
648 };
649
650 assert_eq!(format.byte_size(), 48);
651 }
652
653 #[test]
654 fn byte_size_for_nv12_includes_uv_plane() {
655 let format = CuImageBufferFormat {
656 width: 1280,
657 height: 720,
658 stride: 1280,
659 pixel_format: *b"NV12",
660 };
661
662 assert_eq!(format.byte_size(), 1_382_400);
663 }
664
665 #[test]
666 fn byte_size_for_i420_includes_chroma_planes() {
667 let format = CuImageBufferFormat {
668 width: 640,
669 height: 480,
670 stride: 640,
671 pixel_format: *b"I420",
672 };
673
674 assert_eq!(format.byte_size(), 460_800);
675 }
676
677 #[cfg(feature = "kornia")]
696 mod kornia {
697 use super::super::{CuImage, CuImageBufferFormat};
698 use cu29::pool::CuHandle;
699
700 const RGB_2X2: CuImageBufferFormat = CuImageBufferFormat {
701 width: 2,
702 height: 2,
703 stride: 6,
704 pixel_format: *b"RGB3",
705 };
706
707 fn rgb_image() -> CuImage<Vec<u8>> {
708 CuImage::new(RGB_2X2, CuHandle::new_detached((0u8..12).collect()))
709 }
710
711 #[test]
712 fn as_kornia_image_wraps_the_plane() {
713 let image = rgb_image()
716 .as_kornia_image::<u8, 3>()
717 .expect("a packed RGB plane converts");
718 assert_eq!((image.width(), image.height()), (2, 2));
719 assert_eq!(image.as_slice(), (0u8..12).collect::<Vec<_>>().as_slice());
720 }
721
722 #[test]
723 fn as_kornia_image_refuses_a_plane_too_small_for_the_request() {
724 assert!(rgb_image().as_kornia_image::<u8, 4>().is_err());
726 }
727 }
728
729 mod only_log_what_you_use {
730 use crate::CuImage;
731 use bincode::config;
732 use cu29::config::HandleContent;
733 use cu29::cutask::{CuMsg, encode_metadata_only};
734 use cu29::pool::CuHandle;
735 const FORMAT: super::super::CuImageBufferFormat = super::super::CuImageBufferFormat {
742 width: 2,
743 height: 2,
744 stride: 2,
745 pixel_format: *b"GRAY",
746 };
747
748 fn make_image() -> CuImage<Vec<u8>> {
749 let handle = CuHandle::new_detached(vec![0xDE, 0xAD, 0xBE, 0xEF]);
752 CuImage::new(FORMAT, handle)
753 }
754
755 fn encode_with_policy(msg: &CuMsg<CuImage<Vec<u8>>>, mode: HandleContent) -> Vec<u8> {
764 let should_log = match msg.payload() {
765 Some(p) => {
766 p.apply_handle_content_policy(mode);
767 p.payload_should_log()
768 }
769 None => false,
770 };
771
772 use bincode::enc::write::Writer;
774 struct VecWriter(Vec<u8>);
775 impl Writer for VecWriter {
776 fn write(&mut self, bytes: &[u8]) -> Result<(), bincode::error::EncodeError> {
777 self.0.extend_from_slice(bytes);
778 Ok(())
779 }
780 }
781 let mut encoder =
782 bincode::enc::EncoderImpl::new(VecWriter(Vec::new()), config::standard());
783 use bincode::enc::Encode as _;
784 if should_log {
785 msg.encode(&mut encoder).expect("encode");
786 } else {
787 encode_metadata_only(msg, &mut encoder).expect("encode metadata-only");
788 }
789 encoder.into_writer().0
790 }
791
792 #[test]
794 fn touched_only_untouched_skips_payload() {
795 let image = make_image();
796 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
797 let bytes = encode_with_policy(&msg, HandleContent::TouchedOnly);
798 assert_eq!(
799 bytes.first().copied(),
800 Some(0u8),
801 "untouched TouchedOnly must emit no-payload tag"
802 );
803 }
804
805 #[test]
807 fn touched_only_touched_keeps_payload() {
808 let image = make_image();
809 image.mark_touched();
810 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
811 let bytes = encode_with_policy(&msg, HandleContent::TouchedOnly);
812 assert_eq!(bytes.first().copied(), Some(1u8));
813 assert!(bytes.len() > 8, "payload bytes must follow the present tag");
814 }
815
816 #[test]
818 fn none_always_skips_payload() {
819 let image = make_image();
820 image.mark_touched();
821 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
822 let bytes = encode_with_policy(&msg, HandleContent::None);
823 assert_eq!(bytes.first().copied(), Some(0u8));
824 }
825
826 #[test]
830 fn all_mode_keeps_payload_with_or_without_touch() {
831 for touched in [false, true] {
832 let image = make_image();
833 if touched {
834 image.mark_touched();
835 }
836 let msg: CuMsg<CuImage<Vec<u8>>> = CuMsg::new(Some(image));
837 let bytes = encode_with_policy(&msg, HandleContent::All);
838 assert_eq!(
839 bytes.first().copied(),
840 Some(1u8),
841 "All mode must keep payload (touched={touched})"
842 );
843 }
844 }
845
846 #[test]
849 fn touched_flag_is_shared_across_clones() {
850 let image = make_image();
851 image.apply_handle_content_policy(HandleContent::TouchedOnly);
852 let consumer_view = image.buffer_handle.clone();
853 let _ = consumer_view.with_touched_inner(|inner| inner.as_ref()[0]);
854 assert!(image.payload_should_log());
855 }
856 }
857}