1#![cfg(target_os = "linux")]
5
6use crate::colorimetry::effective_colorimetry;
7use crate::{CPUProcessor, Error, Flip, ImageProcessorTrait, ResolvedCrop, Result, Rotation};
8use edgefirst_tensor::{
9 ColorEncoding, ColorRange, Colorimetry, DType, PixelFormat, Tensor, TensorDyn, TensorMemory,
10 TensorTrait,
11};
12use four_char_code::FourCharCode;
13use g2d_sys::{G2DFormat, G2DPhysical, G2DSurface, G2D};
14use std::{os::fd::AsRawFd, time::Instant};
15
16pub(crate) fn g2d_can_handle(cm: &Colorimetry, src_is_yuv: bool) -> bool {
34 !(src_is_yuv && cm.range == Some(ColorRange::Full))
37 && cm.encoding != Some(ColorEncoding::Bt2020)
38}
39
40const G2D_SOURCES: [PixelFormat; 4] = [
42 PixelFormat::Rgba,
43 PixelFormat::Yuyv,
44 PixelFormat::Nv12,
45 PixelFormat::Bgra,
46];
47
48fn g2d_supports(src: PixelFormat, dst: PixelFormat) -> bool {
51 use PixelFormat::*;
52 matches!(
53 (src, dst),
54 (Rgba | Yuyv | Nv12, Rgba | Yuyv | Rgb | Bgra) | (Bgra, Bgra)
55 )
56}
57
58pub(crate) fn dst_cpu_access(
61 memory: TensorMemory,
62 format: PixelFormat,
63 dtype: DType,
64) -> Option<edgefirst_tensor::CpuAccess> {
65 let takes = memory == TensorMemory::DmaBuf
66 && matches!(dtype, DType::U8 | DType::I8)
67 && G2D_SOURCES.iter().any(|&s| g2d_supports(s, format));
68 takes.then(|| g2d_post_pass_access(format, dtype, true))
69}
70
71pub(crate) fn g2d_post_pass_access(
76 format: PixelFormat,
77 dtype: DType,
78 fills_border: bool,
79) -> edgefirst_tensor::CpuAccess {
80 use edgefirst_tensor::CpuAccess;
81 if dtype == DType::I8 || (fills_border && format == PixelFormat::Rgb) {
82 CpuAccess::ReadWrite
83 } else {
84 CpuAccess::None
85 }
86}
87
88fn fills_border(crop: &ResolvedCrop, dst_w: usize, dst_h: usize) -> bool {
91 crop.dst_color.is_some()
92 && crop
93 .dst_rect
94 .is_some_and(|r| r.left != 0 || r.top != 0 || r.width != dst_w || r.height != dst_h)
95}
96
97fn require_post_pass_access(dst: &TensorDyn, crop: &ResolvedCrop) -> Result<()> {
100 let (Some(fmt), Some(w), Some(h)) = (dst.format(), dst.width(), dst.height()) else {
101 return Ok(());
102 };
103 let needed = g2d_post_pass_access(fmt, dst.dtype(), fills_border(crop, w, h));
104 Ok(dst.cpu_access().require(needed)?)
105}
106
107fn pixelfmt_to_fourcc(fmt: PixelFormat) -> FourCharCode {
109 use four_char_code::four_char_code;
110 match fmt {
111 PixelFormat::Rgb => four_char_code!("RGB "),
112 PixelFormat::Rgba => four_char_code!("RGBA"),
113 PixelFormat::Bgra => four_char_code!("BGRA"),
114 PixelFormat::Grey => four_char_code!("Y800"),
115 PixelFormat::Yuyv => four_char_code!("YUYV"),
116 PixelFormat::Vyuy => four_char_code!("VYUY"),
117 PixelFormat::Nv12 => four_char_code!("NV12"),
118 PixelFormat::Nv16 => four_char_code!("NV16"),
119 _ => four_char_code!("RGBA"),
121 }
122}
123
124#[derive(Debug)]
127pub struct G2DProcessor {
128 g2d: G2D,
129 interpolation: crate::Interpolation,
130}
131
132unsafe impl Send for G2DProcessor {}
133unsafe impl Sync for G2DProcessor {}
134
135impl G2DProcessor {
136 pub fn new() -> Result<Self> {
144 let mut g2d = G2D::new("libg2d.so.2")?;
145 g2d.set_bt601_colorspace()?;
151
152 log::debug!("G2DConverter created with version {:?}", g2d.version());
153 Ok(Self {
154 g2d,
155 interpolation: crate::Interpolation::default(),
156 })
157 }
158
159 pub fn set_interpolation(&mut self, mode: crate::Interpolation) {
164 self.interpolation = mode;
165 }
166
167 pub fn version(&self) -> g2d_sys::Version {
170 self.g2d.version()
171 }
172
173 fn convert_impl(
174 &mut self,
175 src_dyn: &TensorDyn,
176 dst_dyn: &mut TensorDyn,
177 rotation: Rotation,
178 flip: Flip,
179 crop: ResolvedCrop,
180 ) -> Result<()> {
181 let _span = tracing::trace_span!(
182 "image.convert.g2d",
183 src_fmt = ?src_dyn.format(),
184 dst_fmt = ?dst_dyn.format(),
185 )
186 .entered();
187 if log::log_enabled!(log::Level::Trace) {
188 log::trace!(
189 "G2D convert: {:?}({:?}/{:?}) → {:?}({:?}/{:?})",
190 src_dyn.format(),
191 src_dyn.dtype(),
192 src_dyn.memory(),
193 dst_dyn.format(),
194 dst_dyn.dtype(),
195 dst_dyn.memory(),
196 );
197 }
198
199 if src_dyn.dtype() != DType::U8 {
200 return Err(Error::NotSupported(
201 "G2D only supports u8 source tensors".to_string(),
202 ));
203 }
204 let is_int8_dst = dst_dyn.dtype() == DType::I8;
205 if dst_dyn.dtype() != DType::U8 && !is_int8_dst {
206 return Err(Error::NotSupported(
207 "G2D only supports u8 or i8 destination tensors".to_string(),
208 ));
209 }
210
211 let src_fmt = src_dyn.format().ok_or(Error::NotAnImage)?;
212 let dst_fmt = dst_dyn.format().ok_or(Error::NotAnImage)?;
213
214 if !g2d_supports(src_fmt, dst_fmt) {
215 return Err(Error::NotSupported(format!(
216 "G2D does not support {src_fmt} to {dst_fmt} conversion"
217 )));
218 }
219 require_post_pass_access(dst_dyn, &crop)?;
220
221 let src_is_yuv = src_fmt.is_yuv();
233 let dst_is_yuv = dst_fmt.is_yuv();
234 if src_is_yuv || dst_is_yuv {
235 let cm = if src_is_yuv {
236 effective_colorimetry(src_dyn)
237 } else {
238 effective_colorimetry(dst_dyn)
239 };
240 if !g2d_can_handle(&cm, true) {
241 return Err(Error::NotSupported(format!(
242 "G2D cannot express colorimetry {:?}/{:?} (matrix-only, \
243 no range control, no BT.2020)",
244 cm.encoding, cm.range
245 )));
246 }
247 match cm.encoding {
248 Some(ColorEncoding::Bt601) => self.g2d.set_bt601_colorspace()?,
249 Some(ColorEncoding::Bt709) => self.g2d.set_bt709_colorspace()?,
250 _ => self.g2d.set_bt709_colorspace()?,
253 }
254 }
255
256 let src = src_dyn.as_typed::<u8>().unwrap();
258 let dst = if is_int8_dst {
261 let i8_tensor = dst_dyn.as_typed_mut::<i8>().unwrap();
269 unsafe { &mut *(i8_tensor as *mut Tensor<i8> as *mut Tensor<u8>) }
270 } else {
271 dst_dyn.as_typed_mut::<u8>().unwrap()
272 };
273
274 let mut src_surface = tensor_to_g2d_surface(src)?;
275 let mut dst_surface = tensor_to_g2d_surface(dst)?;
276
277 src_surface.rot = match flip {
278 Flip::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
279 Flip::Vertical => g2d_sys::g2d_rotation_G2D_FLIP_V,
280 Flip::Horizontal => g2d_sys::g2d_rotation_G2D_FLIP_H,
281 };
282
283 dst_surface.rot = match rotation {
284 Rotation::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
285 Rotation::Clockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_90,
286 Rotation::Rotate180 => g2d_sys::g2d_rotation_G2D_ROTATION_180,
287 Rotation::CounterClockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_270,
288 };
289
290 if let Some(crop_rect) = crop.src_rect {
291 src_surface.left = crop_rect.left as i32;
292 src_surface.top = crop_rect.top as i32;
293 src_surface.right = (crop_rect.left + crop_rect.width) as i32;
294 src_surface.bottom = (crop_rect.top + crop_rect.height) as i32;
295 }
296
297 let dst_w = dst.width().unwrap();
298 let dst_h = dst.height().unwrap();
299
300 let needs_clear = fills_border(&crop, dst_w, dst_h);
306
307 if needs_clear && dst_fmt != PixelFormat::Rgb {
308 if let Some(dst_color) = crop.dst_color {
309 let start = Instant::now();
310 self.g2d.clear(&mut dst_surface, dst_color)?;
311 log::trace!("g2d clear takes {:?}", start.elapsed());
312 }
313 }
314
315 if let Some(crop_rect) = crop.dst_rect {
316 dst_surface.planes[0] += ((crop_rect.top * dst_surface.stride as usize
320 + crop_rect.left)
321 * dst_fmt.channels()) as u64;
322
323 dst_surface.right = crop_rect.width as i32;
324 dst_surface.bottom = crop_rect.height as i32;
325 dst_surface.width = crop_rect.width as i32;
326 dst_surface.height = crop_rect.height as i32;
327 }
328
329 let (sx_src, sy_src) = chroma_subsample_shifts(src_fmt);
343 let (sx_dst, sy_dst) = chroma_subsample_shifts(dst_fmt);
344 let pad_w = (sx_src | sx_dst) > 0;
345 let pad_h = (sy_src | sy_dst) > 0;
346 if pad_w || pad_h {
347 let even = |v: i32| v + (v & 1);
348 if pad_h && even(dst_surface.bottom) != dst_surface.bottom {
349 let page = match unsafe { libc::sysconf(libc::_SC_PAGESIZE) } {
360 n if n > 0 => n as usize,
361 _ => 4096,
362 };
363 let row_bytes = dst_surface.stride as usize * dst_fmt.channels();
364 let mapped = (row_bytes * dst_h).next_multiple_of(page);
365 let needed = row_bytes * even(dst_surface.bottom) as usize;
366 if crop.dst_rect.is_some() || needed > mapped {
367 return Err(Error::NotSupported(format!(
368 "G2D: odd-height {dst_fmt} destination cannot hold the padding \
369 row for the i.MX95 DPU even-dimension workaround; deferring to \
370 GL/CPU"
371 )));
372 }
373 }
374 if pad_w {
375 src_surface.right = even(src_surface.right);
376 src_surface.width = even(src_surface.width);
377 dst_surface.right = even(dst_surface.right);
378 dst_surface.width = even(dst_surface.width);
379 }
380 if pad_h {
381 src_surface.bottom = even(src_surface.bottom);
382 src_surface.height = even(src_surface.height);
383 dst_surface.bottom = even(dst_surface.bottom);
384 dst_surface.height = even(dst_surface.height);
385 }
386 }
387
388 log::trace!("G2D blit: {src_fmt}→{dst_fmt} int8={is_int8_dst}");
389 self.g2d.blit(&src_surface, &dst_surface)?;
390 self.g2d.finish()?;
391 log::trace!("G2D blit complete");
392
393 if needs_clear && dst_fmt == PixelFormat::Rgb {
395 if let (Some(dst_color), Some(dst_rect)) = (crop.dst_color, crop.dst_rect) {
396 let start = Instant::now();
397 CPUProcessor::fill_image_outside_crop_u8(dst, dst_color, dst_rect, true)?;
398 log::trace!("cpu fill takes {:?}", start.elapsed());
399 }
400 }
401
402 if is_int8_dst {
415 let start = Instant::now();
416 crate::cpu::apply_int8_xor_bias_rows(dst, dst_fmt)?;
417 log::trace!("g2d int8 XOR 0x80 post-pass takes {:?}", start.elapsed());
418 }
419
420 Ok(())
421 }
422}
423
424impl ImageProcessorTrait for G2DProcessor {
425 fn convert(
426 &mut self,
427 src: &TensorDyn,
428 dst: &mut TensorDyn,
429 options: Option<crate::ConvertOptions>,
430 ) -> Result<()> {
431 let options = options.unwrap_or_default();
432 let (rotation, flip, crop) = (
433 options.get_rotation(),
434 options.get_flip(),
435 options.get_crop(),
436 );
437 let crop = crop.resolve(
443 src.width().unwrap_or(0),
444 src.height().unwrap_or(0),
445 dst.width().unwrap_or(0),
446 dst.height().unwrap_or(0),
447 options.get_placement(),
448 )?;
449 if crate::options::output_map_for(&options, dst, crop.dst_rect.is_some())?.is_some() {
450 return Err(Error::NotSupported(
451 "G2D does not normalize or quantize; GL or CPU maps the output".into(),
452 ));
453 }
454 if self.interpolation == crate::Interpolation::Nearest
455 && crop.resizes(
456 (src.width().unwrap_or(0), src.height().unwrap_or(0)),
457 (dst.width().unwrap_or(0), dst.height().unwrap_or(0)),
458 rotation,
459 )
460 {
461 return Err(Error::NotSupported(
462 "G2D scales with a fixed filter; Interpolation::Nearest resizes elsewhere".into(),
463 ));
464 }
465 self.convert_impl(src, dst, rotation, flip, crop)
466 }
467
468 fn draw_decoded_masks(
469 &mut self,
470 dst: &mut TensorDyn,
471 detect: &[crate::DetectBox],
472 segmentation: &[crate::Segmentation],
473 overlay: crate::MaskOverlay<'_>,
474 ) -> Result<()> {
475 if !detect.is_empty() || !segmentation.is_empty() {
479 return Err(Error::NotImplemented(
480 "G2D does not support drawing detection or segmentation overlays".to_string(),
481 ));
482 }
483 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
484 }
485
486 fn draw_proto_masks(
487 &mut self,
488 dst: &mut TensorDyn,
489 detect: &[crate::DetectBox],
490 _proto_data: &crate::ProtoData,
491 overlay: crate::MaskOverlay<'_>,
492 ) -> Result<()> {
493 if !detect.is_empty() {
496 return Err(Error::NotImplemented(
497 "G2D does not support drawing detection or segmentation overlays".to_string(),
498 ));
499 }
500 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
501 }
502
503 fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
504 Err(Error::NotImplemented(
505 "G2D does not support setting colors for rendering detection or segmentation overlays"
506 .to_string(),
507 ))
508 }
509}
510
511fn draw_empty_frame_g2d(
524 g2d: &mut G2D,
525 dst_dyn: &mut TensorDyn,
526 background: Option<&TensorDyn>,
527) -> Result<()> {
528 if dst_dyn.dtype() != DType::U8 {
529 return Err(Error::NotSupported(
530 "G2D only supports u8 destination tensors".to_string(),
531 ));
532 }
533 let dst = dst_dyn.as_typed_mut::<u8>().ok_or(Error::NotAnImage)?;
534
535 if TensorTrait::memory(dst) != TensorMemory::DmaBuf {
545 return Err(Error::NotImplemented(
546 "g2d only supports Dma memory".to_string(),
547 ));
548 }
549
550 let mut dst_surface = tensor_to_g2d_surface(dst)?;
551
552 match background {
553 None => {
554 let start = Instant::now();
558 g2d.clear(&mut dst_surface, [0, 0, 0, 0])?;
559 g2d.finish()?;
560 log::trace!("g2d clear (empty frame) takes {:?}", start.elapsed());
561 }
562 Some(bg_dyn) => {
563 if bg_dyn.shape() != dst.shape() {
568 return Err(Error::InvalidShape(
569 "background shape does not match dst".into(),
570 ));
571 }
572 if bg_dyn.format() != dst.format() {
573 return Err(Error::InvalidShape(
574 "background pixel format does not match dst".into(),
575 ));
576 }
577 if bg_dyn.dtype() != DType::U8 {
578 return Err(Error::NotSupported(
579 "G2D only supports u8 background tensors".to_string(),
580 ));
581 }
582 let bg = bg_dyn.as_typed::<u8>().ok_or(Error::NotAnImage)?;
583 if TensorTrait::memory(bg) != TensorMemory::DmaBuf {
585 return Err(Error::NotImplemented(
586 "g2d background must be Dma-backed".to_string(),
587 ));
588 }
589 let src_surface = tensor_to_g2d_surface(bg)?;
590 let start = Instant::now();
591 g2d.blit(&src_surface, &dst_surface)?;
592 g2d.finish()?;
593 log::trace!("g2d blit (bg→dst) takes {:?}", start.elapsed());
594 }
595 }
596 Ok(())
597}
598
599fn chroma_subsample_shifts(fmt: PixelFormat) -> (u32, u32) {
603 match fmt.chroma_layout() {
604 Some(cl) => (cl.shift_x, cl.shift_y),
605 None if matches!(fmt, PixelFormat::Yuyv | PixelFormat::Vyuy) => (1, 0),
608 None => (0, 0),
609 }
610}
611
612fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
616 let fmt = img.format().ok_or(Error::NotAnImage)?;
617 let fd = img
620 .dmabuf()
621 .map_err(|e| Error::NotImplemented(format!("g2d only supports Dma memory ({e})")))?;
622 let phys: G2DPhysical = fd.as_raw_fd().try_into()?;
623
624 let base_addr = phys.address();
631 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
632 let planes = if fmt == PixelFormat::Nv12 {
633 if img.is_multiplane() {
634 let chroma = img.chroma().unwrap();
636 let chroma_fd = chroma.dmabuf().map_err(|e| {
637 Error::NotImplemented(format!("g2d multiplane chroma must be DMA-backed ({e})"))
638 })?;
639 let uv_phys: G2DPhysical = chroma_fd.as_raw_fd().try_into()?;
640 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
641 [
642 base_addr + luma_offset,
643 uv_phys.address() + chroma_offset,
644 0,
645 ]
646 } else {
647 let w = img.width().unwrap();
648 let h = img.height().unwrap();
649 let stride = img.effective_row_stride().unwrap_or(w.next_multiple_of(2));
650 let uv_offset = (luma_offset as usize + stride * h) as u64;
651 [base_addr + luma_offset, base_addr + uv_offset, 0]
652 }
653 } else {
654 [base_addr + luma_offset, 0, 0]
655 };
656
657 let w = img.width().unwrap();
658 let h = img.height().unwrap();
659 let fourcc = pixelfmt_to_fourcc(fmt);
660
661 let stride_pixels = match img.effective_row_stride() {
664 Some(s) => {
665 let channels = fmt.channels();
666 if s % channels != 0 {
667 return Err(Error::NotImplemented(
668 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
669 ));
670 }
671 s / channels
672 }
673 None => w,
674 };
675
676 Ok(G2DSurface {
677 planes,
678 format: G2DFormat::try_from(fourcc)?.format(),
679 left: 0,
680 top: 0,
681 right: w as i32,
682 bottom: h as i32,
683 stride: stride_pixels as i32,
684 width: w as i32,
685 height: h as i32,
686 blendfunc: 0,
687 clrcolor: 0,
688 rot: 0,
689 global_alpha: 0,
690 })
691}
692
693#[cfg(test)]
694mod g2d_predicate_tests {
695 use super::*;
696 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry};
697
698 #[test]
699 fn g2d_declines_full_range_and_bt2020_yuv() {
700 let full = Colorimetry::default()
701 .with_range(ColorRange::Full)
702 .with_encoding(ColorEncoding::Bt709);
703 let lim = Colorimetry::default()
704 .with_range(ColorRange::Limited)
705 .with_encoding(ColorEncoding::Bt709);
706 let bt2020 = Colorimetry::default()
707 .with_range(ColorRange::Limited)
708 .with_encoding(ColorEncoding::Bt2020);
709 assert!(!g2d_can_handle(&full, true)); assert!(g2d_can_handle(&lim, true)); assert!(!g2d_can_handle(&bt2020, true)); assert!(g2d_can_handle(&full, false)); }
714
715 #[test]
716 fn post_pass_access_covers_the_int8_bias_and_the_rgb_border() {
717 use edgefirst_tensor::CpuAccess;
718 let pass = g2d_post_pass_access;
719 assert_eq!(
720 pass(PixelFormat::Rgba, DType::I8, false),
721 CpuAccess::ReadWrite
722 );
723 assert_eq!(
724 pass(PixelFormat::Rgb, DType::I8, true),
725 CpuAccess::ReadWrite
726 );
727 assert_eq!(pass(PixelFormat::Rgb, DType::U8, false), CpuAccess::None);
728 assert_eq!(pass(PixelFormat::Rgba, DType::U8, true), CpuAccess::None);
729 let dma = TensorMemory::DmaBuf;
730 assert_eq!(
731 dst_cpu_access(dma, PixelFormat::Bgra, DType::I8),
732 Some(CpuAccess::ReadWrite)
733 );
734 assert_eq!(
735 dst_cpu_access(dma, PixelFormat::Rgba, DType::U8),
736 Some(CpuAccess::None)
737 );
738 assert_eq!(dst_cpu_access(dma, PixelFormat::Nv12, DType::U8), None);
739 assert_eq!(dst_cpu_access(dma, PixelFormat::Rgba, DType::F32), None);
740 assert_eq!(
741 dst_cpu_access(TensorMemory::Mem, PixelFormat::Rgba, DType::U8),
742 None
743 );
744 assert!(g2d_supports(PixelFormat::Nv12, PixelFormat::Rgb));
745 assert!(!g2d_supports(PixelFormat::Bgra, PixelFormat::Rgba));
746 assert!(!g2d_supports(PixelFormat::Vyuy, PixelFormat::Rgba));
747 }
748
749 #[test]
752 fn post_passes_are_checked_against_the_destination_up_front() {
753 use edgefirst_tensor::{CpuAccess, Error as TensorError};
754 let image = |fmt, dtype, access| {
755 TensorDyn::image(64, 48, fmt, dtype, Some(TensorMemory::Mem), access).unwrap()
756 };
757 let border = ResolvedCrop {
758 src_rect: None,
759 dst_rect: Some(crate::Rect::new(0, 8, 64, 32)),
760 dst_color: Some([0, 0, 0, 255]),
761 };
762 let denied = |r: Result<()>, requested| match r {
763 Err(Error::Tensor(TensorError::CpuAccessDenied { requested: r, .. })) => {
764 assert_eq!(r, requested)
765 }
766 other => panic!("expected CpuAccessDenied, got {other:?}"),
767 };
768 let int8 = image(PixelFormat::Rgba, DType::I8, CpuAccess::Write);
769 denied(
770 require_post_pass_access(&int8, &ResolvedCrop::no_crop()),
771 CpuAccess::ReadWrite,
772 );
773 let rgb = image(PixelFormat::Rgb, DType::U8, CpuAccess::None);
774 denied(
775 require_post_pass_access(&rgb, &border),
776 CpuAccess::ReadWrite,
777 );
778 require_post_pass_access(&rgb, &ResolvedCrop::no_crop()).unwrap();
779 let rgba = image(PixelFormat::Rgba, DType::U8, CpuAccess::None);
780 require_post_pass_access(&rgba, &border).unwrap();
781 }
782
783 #[test]
786 fn rgb_border_post_pass_needs_readwrite() {
787 use edgefirst_tensor::CpuAccess;
788 assert_eq!(
789 g2d_post_pass_access(PixelFormat::Rgb, DType::U8, true),
790 CpuAccess::ReadWrite
791 );
792 let border = ResolvedCrop {
793 src_rect: None,
794 dst_rect: Some(crate::Rect::new(8, 0, 48, 48)),
795 dst_color: Some([0, 0, 0, 255]),
796 };
797 let image = |access| {
798 TensorDyn::image(
799 64,
800 48,
801 PixelFormat::Rgb,
802 DType::U8,
803 Some(TensorMemory::Mem),
804 access,
805 )
806 .unwrap()
807 };
808 assert!(matches!(
809 require_post_pass_access(&image(CpuAccess::Write), &border),
810 Err(Error::Tensor(
811 edgefirst_tensor::Error::CpuAccessDenied { .. }
812 ))
813 ));
814 require_post_pass_access(&image(CpuAccess::ReadWrite), &border).unwrap();
815 }
816}
817
818#[cfg(test)]
819#[allow(deprecated)]
820mod g2d_tests {
821 use super::*;
822 use crate::test_support::is_dma_available;
823 use crate::{CPUProcessor, G2DProcessor, ImageProcessorTrait};
824 use edgefirst_tensor::{
825 DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
826 };
827 use image::buffer::ConvertBuffer;
828
829 #[cfg(target_os = "linux")]
836 fn is_g2d_available() -> bool {
837 crate::test_support::g2d_required_or(G2DProcessor::new().is_ok())
838 }
839
840 #[cfg(target_os = "linux")]
842 fn dma_and_g2d_or_skip() -> bool {
843 let why = if !is_dma_available() {
844 "DMA not available"
845 } else if !is_g2d_available() {
846 "G2D not available"
847 } else {
848 return true;
849 };
850 crate::test_support::report_skip(&format!(
851 "{}: {why}",
852 crate::test_support::current_test()
853 ));
854 false
855 }
856
857 #[test]
858 #[cfg(target_os = "linux")]
859 fn test_g2d_formats_no_resize() {
860 if !dma_and_g2d_or_skip() {
861 return;
862 }
863 for i in [
864 PixelFormat::Rgba,
865 PixelFormat::Yuyv,
866 PixelFormat::Rgb,
867 PixelFormat::Grey,
868 PixelFormat::Nv12,
869 ] {
870 for o in [
871 PixelFormat::Rgba,
872 PixelFormat::Yuyv,
873 PixelFormat::Rgb,
874 PixelFormat::Grey,
875 ] {
876 let res = test_g2d_format_no_resize_(i, o);
877 if let Err(e) = res {
878 crate::test_support::report_skip(&format!(
879 "{}: {i} to {o} failed: {e:?}",
880 crate::test_support::current_test()
881 ));
882 }
883 }
884 }
885 }
886
887 fn test_g2d_format_no_resize_(
888 g2d_in_fmt: PixelFormat,
889 g2d_out_fmt: PixelFormat,
890 ) -> Result<(), crate::Error> {
891 let dst_width = 1280;
892 let dst_height = 720;
893 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
894 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
895
896 let mut src2 = TensorDyn::image(
898 1280,
899 720,
900 g2d_in_fmt,
901 DType::U8,
902 Some(TensorMemory::DmaBuf),
903 edgefirst_tensor::CpuAccess::ReadWrite,
904 )?;
905
906 let mut cpu_converter = CPUProcessor::new();
907
908 if g2d_in_fmt == PixelFormat::Nv12 {
910 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
911 src2.as_typed::<u8>()
912 .unwrap()
913 .map()?
914 .as_mut_slice()
915 .copy_from_slice(&nv12_bytes);
916 } else {
917 cpu_converter.convert(&src, &mut src2, None)?;
918 }
919
920 let mut g2d_dst = TensorDyn::image(
921 dst_width,
922 dst_height,
923 g2d_out_fmt,
924 DType::U8,
925 Some(TensorMemory::DmaBuf),
926 edgefirst_tensor::CpuAccess::ReadWrite,
927 )?;
928 let mut g2d_converter = G2DProcessor::new()?;
929 let src2_dyn = src2;
930 let mut g2d_dst_dyn = g2d_dst;
931 g2d_converter.convert(&src2_dyn, &mut g2d_dst_dyn, None)?;
932 g2d_dst = {
933 let mut __t = g2d_dst_dyn.into_u8().unwrap();
934 __t.set_format(g2d_out_fmt)
935 .map_err(|e| crate::Error::Internal(e.to_string()))?;
936 TensorDyn::from(__t)
937 };
938
939 let mut cpu_dst = TensorDyn::image(
940 dst_width,
941 dst_height,
942 PixelFormat::Rgb,
943 DType::U8,
944 None,
945 edgefirst_tensor::CpuAccess::ReadWrite,
946 )?;
947 cpu_converter.convert(&g2d_dst, &mut cpu_dst, None)?;
948
949 compare_images(
950 &src,
951 &cpu_dst,
952 0.98,
953 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
954 )
955 }
956
957 #[test]
958 #[cfg(target_os = "linux")]
959 fn test_g2d_formats_with_resize() {
960 if !dma_and_g2d_or_skip() {
961 return;
962 }
963 for i in [
964 PixelFormat::Rgba,
965 PixelFormat::Yuyv,
966 PixelFormat::Rgb,
967 PixelFormat::Grey,
968 PixelFormat::Nv12,
969 ] {
970 for o in [
971 PixelFormat::Rgba,
972 PixelFormat::Yuyv,
973 PixelFormat::Rgb,
974 PixelFormat::Grey,
975 ] {
976 let res = test_g2d_format_with_resize_(i, o);
977 if let Err(e) = res {
978 crate::test_support::report_skip(&format!(
979 "{}: {i} to {o} failed: {e:?}",
980 crate::test_support::current_test()
981 ));
982 }
983 }
984 }
985 }
986
987 #[test]
988 #[cfg(target_os = "linux")]
989 fn test_g2d_formats_with_resize_dst_crop() {
990 if !dma_and_g2d_or_skip() {
991 return;
992 }
993 for i in [
994 PixelFormat::Rgba,
995 PixelFormat::Yuyv,
996 PixelFormat::Rgb,
997 PixelFormat::Grey,
998 PixelFormat::Nv12,
999 ] {
1000 for o in [
1001 PixelFormat::Rgba,
1002 PixelFormat::Yuyv,
1003 PixelFormat::Rgb,
1004 PixelFormat::Grey,
1005 ] {
1006 let res = test_g2d_format_with_resize_dst_crop(i, o);
1007 if let Err(e) = res {
1008 crate::test_support::report_skip(&format!(
1009 "{}: {i} to {o} failed: {e:?}",
1010 crate::test_support::current_test()
1011 ));
1012 }
1013 }
1014 }
1015 }
1016
1017 fn test_g2d_format_with_resize_(
1018 g2d_in_fmt: PixelFormat,
1019 g2d_out_fmt: PixelFormat,
1020 ) -> Result<(), crate::Error> {
1021 let dst_width = 600;
1022 let dst_height = 400;
1023 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1024 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1025
1026 let mut cpu_converter = CPUProcessor::new();
1027
1028 let mut reference = TensorDyn::image(
1029 dst_width,
1030 dst_height,
1031 PixelFormat::Rgb,
1032 DType::U8,
1033 Some(TensorMemory::DmaBuf),
1034 edgefirst_tensor::CpuAccess::ReadWrite,
1035 )?;
1036 cpu_converter.convert(&src, &mut reference, None)?;
1037
1038 let mut src2 = TensorDyn::image(
1040 1280,
1041 720,
1042 g2d_in_fmt,
1043 DType::U8,
1044 Some(TensorMemory::DmaBuf),
1045 edgefirst_tensor::CpuAccess::ReadWrite,
1046 )?;
1047
1048 if g2d_in_fmt == PixelFormat::Nv12 {
1050 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1051 src2.as_typed::<u8>()
1052 .unwrap()
1053 .map()?
1054 .as_mut_slice()
1055 .copy_from_slice(&nv12_bytes);
1056 } else {
1057 cpu_converter.convert(&src, &mut src2, None)?;
1058 }
1059
1060 let mut g2d_dst = TensorDyn::image(
1061 dst_width,
1062 dst_height,
1063 g2d_out_fmt,
1064 DType::U8,
1065 Some(TensorMemory::DmaBuf),
1066 edgefirst_tensor::CpuAccess::ReadWrite,
1067 )?;
1068 let mut g2d_converter = G2DProcessor::new()?;
1069 let src2_dyn = src2;
1070 let mut g2d_dst_dyn = g2d_dst;
1071 g2d_converter.convert(&src2_dyn, &mut g2d_dst_dyn, None)?;
1072 g2d_dst = {
1073 let mut __t = g2d_dst_dyn.into_u8().unwrap();
1074 __t.set_format(g2d_out_fmt)
1075 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1076 TensorDyn::from(__t)
1077 };
1078
1079 let mut cpu_dst = TensorDyn::image(
1080 dst_width,
1081 dst_height,
1082 PixelFormat::Rgb,
1083 DType::U8,
1084 None,
1085 edgefirst_tensor::CpuAccess::ReadWrite,
1086 )?;
1087 cpu_converter.convert(&g2d_dst, &mut cpu_dst, None)?;
1088
1089 compare_images(
1090 &reference,
1091 &cpu_dst,
1092 0.98,
1093 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
1094 )
1095 }
1096
1097 fn test_g2d_format_with_resize_dst_crop(
1098 g2d_in_fmt: PixelFormat,
1099 g2d_out_fmt: PixelFormat,
1100 ) -> Result<(), crate::Error> {
1101 let dst_width = 600;
1102 let dst_height = 400;
1103 let region = crate::Region::new(100, 100, 200, 100);
1108 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1109 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1110
1111 let mut cpu_converter = CPUProcessor::new();
1112
1113 let reference = TensorDyn::image(
1114 dst_width,
1115 dst_height,
1116 PixelFormat::Rgb,
1117 DType::U8,
1118 Some(TensorMemory::DmaBuf),
1119 edgefirst_tensor::CpuAccess::ReadWrite,
1120 )?;
1121 reference
1122 .as_typed::<u8>()
1123 .unwrap()
1124 .map()
1125 .unwrap()
1126 .as_mut_slice()
1127 .fill(128);
1128 cpu_converter.convert(&src, &mut reference.view(region)?, None)?;
1129
1130 let mut src2 = TensorDyn::image(
1132 1280,
1133 720,
1134 g2d_in_fmt,
1135 DType::U8,
1136 Some(TensorMemory::DmaBuf),
1137 edgefirst_tensor::CpuAccess::ReadWrite,
1138 )?;
1139
1140 if g2d_in_fmt == PixelFormat::Nv12 {
1142 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1143 src2.as_typed::<u8>()
1144 .unwrap()
1145 .map()?
1146 .as_mut_slice()
1147 .copy_from_slice(&nv12_bytes);
1148 } else {
1149 cpu_converter.convert(&src, &mut src2, None)?;
1150 }
1151
1152 let mut g2d_dst = TensorDyn::image(
1153 dst_width,
1154 dst_height,
1155 g2d_out_fmt,
1156 DType::U8,
1157 Some(TensorMemory::DmaBuf),
1158 edgefirst_tensor::CpuAccess::ReadWrite,
1159 )?;
1160 g2d_dst
1161 .as_typed::<u8>()
1162 .unwrap()
1163 .map()
1164 .unwrap()
1165 .as_mut_slice()
1166 .fill(128);
1167 let mut g2d_converter = G2DProcessor::new()?;
1168 let src2_dyn = src2;
1169 let g2d_dst_dyn = g2d_dst;
1170 g2d_converter.convert(&src2_dyn, &mut g2d_dst_dyn.view(region)?, None)?;
1171 g2d_dst = {
1172 let mut __t = g2d_dst_dyn.into_u8().unwrap();
1173 __t.set_format(g2d_out_fmt)
1174 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1175 TensorDyn::from(__t)
1176 };
1177
1178 let mut cpu_dst = TensorDyn::image(
1179 dst_width,
1180 dst_height,
1181 PixelFormat::Rgb,
1182 DType::U8,
1183 None,
1184 edgefirst_tensor::CpuAccess::ReadWrite,
1185 )?;
1186 cpu_converter.convert(&g2d_dst, &mut cpu_dst, None)?;
1187
1188 compare_images(
1189 &reference,
1190 &cpu_dst,
1191 0.98,
1192 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
1193 )
1194 }
1195
1196 fn compare_images(
1197 img1: &TensorDyn,
1198 img2: &TensorDyn,
1199 threshold: f64,
1200 name: &str,
1201 ) -> Result<(), crate::Error> {
1202 assert_eq!(img1.height(), img2.height(), "Heights differ");
1203 assert_eq!(img1.width(), img2.width(), "Widths differ");
1204 assert_eq!(
1205 img1.format().unwrap(),
1206 img2.format().unwrap(),
1207 "PixelFormat differ"
1208 );
1209 assert!(
1210 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
1211 "format must be Rgb or Rgba for comparison"
1212 );
1213 let image1 = match img1.format().unwrap() {
1214 PixelFormat::Rgb => image::RgbImage::from_vec(
1215 img1.width().unwrap() as u32,
1216 img1.height().unwrap() as u32,
1217 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1218 )
1219 .unwrap(),
1220 PixelFormat::Rgba => image::RgbaImage::from_vec(
1221 img1.width().unwrap() as u32,
1222 img1.height().unwrap() as u32,
1223 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1224 )
1225 .unwrap()
1226 .convert(),
1227
1228 _ => unreachable!(),
1229 };
1230
1231 let image2 = match img2.format().unwrap() {
1232 PixelFormat::Rgb => image::RgbImage::from_vec(
1233 img2.width().unwrap() as u32,
1234 img2.height().unwrap() as u32,
1235 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1236 )
1237 .unwrap(),
1238 PixelFormat::Rgba => image::RgbaImage::from_vec(
1239 img2.width().unwrap() as u32,
1240 img2.height().unwrap() as u32,
1241 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1242 )
1243 .unwrap()
1244 .convert(),
1245
1246 _ => unreachable!(),
1247 };
1248
1249 let similarity = image_compare::rgb_similarity_structure(
1250 &image_compare::Algorithm::RootMeanSquared,
1251 &image1,
1252 &image2,
1253 )
1254 .expect("Image Comparison failed");
1255
1256 if similarity.score < threshold {
1257 image1.save(format!("{name}_1.png")).unwrap();
1258 image2.save(format!("{name}_2.png")).unwrap();
1259 return Err(Error::Internal(format!(
1260 "{name}: converted image and target image have similarity score too low: {} < {}",
1261 similarity.score, threshold
1262 )));
1263 }
1264 Ok(())
1265 }
1266
1267 fn load_raw_image(
1273 width: usize,
1274 height: usize,
1275 format: PixelFormat,
1276 memory: Option<TensorMemory>,
1277 bytes: &[u8],
1278 ) -> Result<TensorDyn, crate::Error> {
1279 let img = TensorDyn::image(
1280 width,
1281 height,
1282 format,
1283 DType::U8,
1284 memory,
1285 edgefirst_tensor::CpuAccess::ReadWrite,
1286 )?;
1287 let mut map = img.as_typed::<u8>().unwrap().map()?;
1288 let dst = map.as_mut_slice();
1289 if bytes.len() > dst.len() {
1290 return Err(crate::Error::InvalidShape(format!(
1291 "load_raw_image: {} input bytes exceed {}-byte image buffer",
1292 bytes.len(),
1293 dst.len()
1294 )));
1295 }
1296 dst[..bytes.len()].copy_from_slice(bytes);
1297 Ok(img)
1298 }
1299
1300 #[test]
1302 #[cfg(target_os = "linux")]
1303 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
1304 if !dma_and_g2d_or_skip() {
1305 return Ok(());
1306 }
1307 let src = load_raw_image(
1309 1280,
1310 720,
1311 PixelFormat::Nv12,
1312 Some(TensorMemory::DmaBuf),
1313 &edgefirst_bench::testdata::read("camera720p.nv12"),
1314 )?;
1315
1316 let reference = load_raw_image(
1318 1280,
1319 720,
1320 PixelFormat::Rgba,
1321 None,
1322 &edgefirst_bench::testdata::read("camera720p.rgba"),
1323 )?;
1324
1325 let mut dst = TensorDyn::image(
1327 1280,
1328 720,
1329 PixelFormat::Rgba,
1330 DType::U8,
1331 Some(TensorMemory::DmaBuf),
1332 edgefirst_tensor::CpuAccess::ReadWrite,
1333 )?;
1334 let mut g2d = G2DProcessor::new()?;
1335 let src_dyn = src;
1336 let mut dst_dyn = dst;
1337 g2d.convert(&src_dyn, &mut dst_dyn, None)?;
1338 dst = {
1339 let mut __t = dst_dyn.into_u8().unwrap();
1340 __t.set_format(PixelFormat::Rgba)
1341 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1342 TensorDyn::from(__t)
1343 };
1344
1345 let cpu_dst = TensorDyn::image(
1347 1280,
1348 720,
1349 PixelFormat::Rgba,
1350 DType::U8,
1351 None,
1352 edgefirst_tensor::CpuAccess::ReadWrite,
1353 )?;
1354 cpu_dst
1355 .as_typed::<u8>()
1356 .unwrap()
1357 .map()?
1358 .as_mut_slice()
1359 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1360
1361 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
1362 }
1363
1364 #[test]
1366 #[cfg(target_os = "linux")]
1367 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
1368 if !dma_and_g2d_or_skip() {
1369 return Ok(());
1370 }
1371 let src = load_raw_image(
1373 1280,
1374 720,
1375 PixelFormat::Nv12,
1376 Some(TensorMemory::DmaBuf),
1377 &edgefirst_bench::testdata::read("camera720p.nv12"),
1378 )?;
1379
1380 let reference = load_raw_image(
1382 1280,
1383 720,
1384 PixelFormat::Rgb,
1385 None,
1386 &edgefirst_bench::testdata::read("camera720p.rgb"),
1387 )?;
1388
1389 let mut dst = TensorDyn::image(
1391 1280,
1392 720,
1393 PixelFormat::Rgb,
1394 DType::U8,
1395 Some(TensorMemory::DmaBuf),
1396 edgefirst_tensor::CpuAccess::ReadWrite,
1397 )?;
1398 let mut g2d = G2DProcessor::new()?;
1399 let src_dyn = src;
1400 let mut dst_dyn = dst;
1401 g2d.convert(&src_dyn, &mut dst_dyn, None)?;
1402 dst = {
1403 let mut __t = dst_dyn.into_u8().unwrap();
1404 __t.set_format(PixelFormat::Rgb)
1405 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1406 TensorDyn::from(__t)
1407 };
1408
1409 let cpu_dst = TensorDyn::image(
1411 1280,
1412 720,
1413 PixelFormat::Rgb,
1414 DType::U8,
1415 None,
1416 edgefirst_tensor::CpuAccess::ReadWrite,
1417 )?;
1418 cpu_dst
1419 .as_typed::<u8>()
1420 .unwrap()
1421 .map()?
1422 .as_mut_slice()
1423 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1424
1425 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1426 }
1427
1428 #[test]
1430 #[cfg(target_os = "linux")]
1431 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1432 if !dma_and_g2d_or_skip() {
1433 return Ok(());
1434 }
1435 let src = load_raw_image(
1437 1280,
1438 720,
1439 PixelFormat::Yuyv,
1440 Some(TensorMemory::DmaBuf),
1441 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1442 )?;
1443
1444 let reference = load_raw_image(
1446 1280,
1447 720,
1448 PixelFormat::Rgba,
1449 None,
1450 &edgefirst_bench::testdata::read("camera720p.rgba"),
1451 )?;
1452
1453 let mut dst = TensorDyn::image(
1455 1280,
1456 720,
1457 PixelFormat::Rgba,
1458 DType::U8,
1459 Some(TensorMemory::DmaBuf),
1460 edgefirst_tensor::CpuAccess::ReadWrite,
1461 )?;
1462 let mut g2d = G2DProcessor::new()?;
1463 let src_dyn = src;
1464 let mut dst_dyn = dst;
1465 g2d.convert(&src_dyn, &mut dst_dyn, None)?;
1466 dst = {
1467 let mut __t = dst_dyn.into_u8().unwrap();
1468 __t.set_format(PixelFormat::Rgba)
1469 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1470 TensorDyn::from(__t)
1471 };
1472
1473 let cpu_dst = TensorDyn::image(
1475 1280,
1476 720,
1477 PixelFormat::Rgba,
1478 DType::U8,
1479 None,
1480 edgefirst_tensor::CpuAccess::ReadWrite,
1481 )?;
1482 cpu_dst
1483 .as_typed::<u8>()
1484 .unwrap()
1485 .map()?
1486 .as_mut_slice()
1487 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1488
1489 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1490 }
1491
1492 #[test]
1494 #[cfg(target_os = "linux")]
1495 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1496 if !dma_and_g2d_or_skip() {
1497 return Ok(());
1498 }
1499 let src = load_raw_image(
1501 1280,
1502 720,
1503 PixelFormat::Yuyv,
1504 Some(TensorMemory::DmaBuf),
1505 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1506 )?;
1507
1508 let reference = load_raw_image(
1510 1280,
1511 720,
1512 PixelFormat::Rgb,
1513 None,
1514 &edgefirst_bench::testdata::read("camera720p.rgb"),
1515 )?;
1516
1517 let mut dst = TensorDyn::image(
1519 1280,
1520 720,
1521 PixelFormat::Rgb,
1522 DType::U8,
1523 Some(TensorMemory::DmaBuf),
1524 edgefirst_tensor::CpuAccess::ReadWrite,
1525 )?;
1526 let mut g2d = G2DProcessor::new()?;
1527 let src_dyn = src;
1528 let mut dst_dyn = dst;
1529 g2d.convert(&src_dyn, &mut dst_dyn, None)?;
1530 dst = {
1531 let mut __t = dst_dyn.into_u8().unwrap();
1532 __t.set_format(PixelFormat::Rgb)
1533 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1534 TensorDyn::from(__t)
1535 };
1536
1537 let cpu_dst = TensorDyn::image(
1539 1280,
1540 720,
1541 PixelFormat::Rgb,
1542 DType::U8,
1543 None,
1544 edgefirst_tensor::CpuAccess::ReadWrite,
1545 )?;
1546 cpu_dst
1547 .as_typed::<u8>()
1548 .unwrap()
1549 .map()?
1550 .as_mut_slice()
1551 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1552
1553 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1554 }
1555
1556 #[test]
1559 #[cfg(target_os = "linux")]
1560 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1561 fn test_g2d_bgra_no_resize() {
1562 for src_fmt in [
1563 PixelFormat::Rgba,
1564 PixelFormat::Yuyv,
1565 PixelFormat::Nv12,
1566 PixelFormat::Bgra,
1567 ] {
1568 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1569 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1570 });
1571 }
1572 }
1573
1574 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1575 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1576 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1577
1578 let mut src2 = TensorDyn::image(
1580 1280,
1581 720,
1582 g2d_in_fmt,
1583 DType::U8,
1584 Some(TensorMemory::DmaBuf),
1585 edgefirst_tensor::CpuAccess::ReadWrite,
1586 )?;
1587 let mut cpu_converter = CPUProcessor::new();
1588
1589 if g2d_in_fmt == PixelFormat::Nv12 {
1590 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1591 src2.as_typed::<u8>()
1592 .unwrap()
1593 .map()?
1594 .as_mut_slice()
1595 .copy_from_slice(&nv12_bytes);
1596 } else {
1597 cpu_converter.convert(&src, &mut src2, None)?;
1598 }
1599
1600 let mut g2d = G2DProcessor::new()?;
1601
1602 let mut bgra_dst = TensorDyn::image(
1604 1280,
1605 720,
1606 PixelFormat::Bgra,
1607 DType::U8,
1608 Some(TensorMemory::DmaBuf),
1609 edgefirst_tensor::CpuAccess::ReadWrite,
1610 )?;
1611 let src2_dyn = src2;
1612 let mut bgra_dst_dyn = bgra_dst;
1613 g2d.convert(&src2_dyn, &mut bgra_dst_dyn, None)?;
1614 bgra_dst = {
1615 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1616 __t.set_format(PixelFormat::Bgra)
1617 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1618 TensorDyn::from(__t)
1619 };
1620
1621 let src2 = {
1623 let mut __t = src2_dyn.into_u8().unwrap();
1624 __t.set_format(g2d_in_fmt)
1625 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1626 TensorDyn::from(__t)
1627 };
1628
1629 let mut rgba_dst = TensorDyn::image(
1631 1280,
1632 720,
1633 PixelFormat::Rgba,
1634 DType::U8,
1635 Some(TensorMemory::DmaBuf),
1636 edgefirst_tensor::CpuAccess::ReadWrite,
1637 )?;
1638 let src2_dyn2 = src2;
1639 let mut rgba_dst_dyn = rgba_dst;
1640 g2d.convert(&src2_dyn2, &mut rgba_dst_dyn, None)?;
1641 rgba_dst = {
1642 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1643 __t.set_format(PixelFormat::Rgba)
1644 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1645 TensorDyn::from(__t)
1646 };
1647
1648 let bgra_cpu = TensorDyn::image(
1650 1280,
1651 720,
1652 PixelFormat::Bgra,
1653 DType::U8,
1654 None,
1655 edgefirst_tensor::CpuAccess::ReadWrite,
1656 )?;
1657 bgra_cpu
1658 .as_typed::<u8>()
1659 .unwrap()
1660 .map()?
1661 .as_mut_slice()
1662 .copy_from_slice(bgra_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1663
1664 let rgba_cpu = TensorDyn::image(
1665 1280,
1666 720,
1667 PixelFormat::Rgba,
1668 DType::U8,
1669 None,
1670 edgefirst_tensor::CpuAccess::ReadWrite,
1671 )?;
1672 rgba_cpu
1673 .as_typed::<u8>()
1674 .unwrap()
1675 .map()?
1676 .as_mut_slice()
1677 .copy_from_slice(rgba_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1678
1679 let bgra_map = bgra_cpu.as_typed::<u8>().unwrap().map()?;
1681 let rgba_map = rgba_cpu.as_typed::<u8>().unwrap().map()?;
1682 let bgra_buf = bgra_map.as_slice();
1683 let rgba_buf = rgba_map.as_slice();
1684
1685 assert_eq!(bgra_buf.len(), rgba_buf.len());
1686 for (i, (bc, rc)) in bgra_buf
1687 .as_chunks::<4>()
1688 .0
1689 .iter()
1690 .zip(rgba_buf.as_chunks::<4>().0)
1691 .enumerate()
1692 {
1693 assert_eq!(
1694 bc[0], rc[2],
1695 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1696 );
1697 assert_eq!(
1698 bc[1], rc[1],
1699 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1700 );
1701 assert_eq!(
1702 bc[2], rc[0],
1703 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1704 );
1705 assert_eq!(
1706 bc[3], rc[3],
1707 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1708 );
1709 }
1710 Ok(())
1711 }
1712
1713 fn surface_for(
1724 width: usize,
1725 height: usize,
1726 fmt: PixelFormat,
1727 offset: Option<usize>,
1728 row_stride: Option<usize>,
1729 ) -> Result<G2DSurface, crate::Error> {
1730 use edgefirst_tensor::TensorMemory;
1731 let mut t = Tensor::<u8>::image(
1732 width,
1733 height,
1734 fmt,
1735 Some(TensorMemory::DmaBuf),
1736 edgefirst_tensor::CpuAccess::ReadWrite,
1737 )?;
1738 if let Some(o) = offset {
1739 t.set_plane_offset(o);
1740 }
1741 if let Some(s) = row_stride {
1742 t.set_row_stride_unchecked(s);
1743 }
1744 tensor_to_g2d_surface(&t)
1745 }
1746
1747 #[test]
1748 fn g2d_surface_single_plane_no_offset() {
1749 if !dma_and_g2d_or_skip() {
1750 return;
1751 }
1752 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1753 assert_ne!(s.planes[0], 0);
1755 assert_eq!(s.stride, 640);
1756 }
1757
1758 #[test]
1759 fn g2d_surface_single_plane_with_offset() {
1760 if !dma_and_g2d_or_skip() {
1761 return;
1762 }
1763 use edgefirst_tensor::TensorMemory;
1764 let mut t = Tensor::<u8>::image(
1765 640,
1766 480,
1767 PixelFormat::Rgba,
1768 Some(TensorMemory::DmaBuf),
1769 edgefirst_tensor::CpuAccess::ReadWrite,
1770 )
1771 .unwrap();
1772 let s0 = tensor_to_g2d_surface(&t).unwrap();
1773 t.set_plane_offset(4096);
1774 let s1 = tensor_to_g2d_surface(&t).unwrap();
1775 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1776 }
1777
1778 #[test]
1779 fn g2d_surface_single_plane_zero_offset() {
1780 if !dma_and_g2d_or_skip() {
1781 return;
1782 }
1783 use edgefirst_tensor::TensorMemory;
1784 let mut t = Tensor::<u8>::image(
1785 640,
1786 480,
1787 PixelFormat::Rgba,
1788 Some(TensorMemory::DmaBuf),
1789 edgefirst_tensor::CpuAccess::ReadWrite,
1790 )
1791 .unwrap();
1792 let s_none = tensor_to_g2d_surface(&t).unwrap();
1793 t.set_plane_offset(0);
1794 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1795 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1797 }
1798
1799 #[test]
1800 fn g2d_surface_stride_rgba() {
1801 if !dma_and_g2d_or_skip() {
1802 return;
1803 }
1804 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1806 assert_eq!(s_default.stride, 640);
1807
1808 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1810 assert_eq!(s_custom.stride, 704);
1811 }
1812
1813 #[test]
1814 fn g2d_surface_stride_rgb() {
1815 if !dma_and_g2d_or_skip() {
1816 return;
1817 }
1818 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1819 assert_eq!(s_default.stride, 640);
1820
1821 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1823 assert_eq!(s_custom.stride, 660);
1824 }
1825
1826 #[test]
1827 fn g2d_surface_stride_grey() {
1828 if !dma_and_g2d_or_skip() {
1829 return;
1830 }
1831 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1833 Ok(s) => s,
1834 Err(e @ crate::Error::G2D(..)) => {
1835 crate::test_support::report_skip(&format!(
1836 "g2d_surface_stride_grey: this G2D does not support Y800: {e:?}"
1837 ));
1838 return;
1839 }
1840 Err(e) => panic!("unexpected error: {e:?}"),
1841 };
1842 assert_eq!(s.stride, 1024);
1844 }
1845
1846 #[test]
1847 fn g2d_surface_contiguous_nv12_offset() {
1848 if !dma_and_g2d_or_skip() {
1849 return;
1850 }
1851 use edgefirst_tensor::TensorMemory;
1852 let mut t = Tensor::<u8>::image(
1853 640,
1854 480,
1855 PixelFormat::Nv12,
1856 Some(TensorMemory::DmaBuf),
1857 edgefirst_tensor::CpuAccess::ReadWrite,
1858 )
1859 .unwrap();
1860 let s0 = tensor_to_g2d_surface(&t).unwrap();
1861
1862 t.set_plane_offset(8192);
1863 let s1 = tensor_to_g2d_surface(&t).unwrap();
1864
1865 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1867 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1871 }
1872
1873 #[test]
1874 fn g2d_surface_contiguous_nv12_stride() {
1875 if !dma_and_g2d_or_skip() {
1876 return;
1877 }
1878 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1880 assert_eq!(s.stride, 640);
1881
1882 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1884 assert_eq!(s_padded.stride, 1024);
1885 }
1886
1887 #[test]
1888 fn g2d_surface_multiplane_nv12_offset() {
1889 if !dma_and_g2d_or_skip() {
1890 return;
1891 }
1892 use edgefirst_tensor::TensorMemory;
1893
1894 let mut luma =
1896 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::DmaBuf), Some("luma")).unwrap();
1897 let mut chroma =
1898 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::DmaBuf), Some("chroma")).unwrap();
1899
1900 let luma_base = {
1902 let dma = luma.as_dma().unwrap();
1903 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1904 phys.address()
1905 };
1906 let chroma_base = {
1907 let dma = chroma.as_dma().unwrap();
1908 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1909 phys.address()
1910 };
1911
1912 luma.set_plane_offset(4096);
1914 chroma.set_plane_offset(2048);
1915 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1916 let s = tensor_to_g2d_surface(&combined).unwrap();
1917
1918 assert_eq!(s.planes[0], luma_base + 4096);
1920 assert_eq!(s.planes[1], chroma_base + 2048);
1922 }
1923
1924 #[cfg(target_os = "linux")]
1952 fn fill_patterned_nv12(t: &TensorDyn) {
1953 let w = t.width().unwrap();
1954 let h = t.height().unwrap();
1955 let stride = t.effective_row_stride().unwrap();
1956
1957 let chroma_h = h.div_ceil(2);
1959 let chroma_w = w.div_ceil(2);
1960
1961 let bound = t.as_typed::<u8>().unwrap();
1962 let mut m = bound.map().unwrap();
1963 let buf = m.as_mut_slice();
1964 let uv_start = stride * h;
1965
1966 for r in 0..h {
1968 for c in 0..w {
1969 buf[r * stride + c] = ((r * 3 + c * 5) % 256) as u8;
1970 }
1971 }
1972 for cr_row in 0..chroma_h {
1974 for cc in 0..chroma_w {
1975 let cb_val = ((cc * 7 + cr_row * 11 + 40) % 256) as u8;
1976 let cr_val = ((cc * 13 + cr_row * 3 + 80) % 256) as u8;
1977 let uv_byte = uv_start + cr_row * stride + cc * 2;
1978 buf[uv_byte] = cb_val;
1979 buf[uv_byte + 1] = cr_val;
1980 }
1981 }
1982 }
1983
1984 #[cfg(target_os = "linux")]
1989 fn compare_g2d_vs_cpu_rgba(
1990 g2d_dst: &TensorDyn,
1991 cpu_dst: &TensorDyn,
1992 w: usize,
1993 h: usize,
1994 tol: u32,
1995 ) -> (u32, Option<(usize, usize, usize)>) {
1996 let channels = 4usize; let g2d_t = g2d_dst.as_typed::<u8>().unwrap();
1998 let cpu_t = cpu_dst.as_typed::<u8>().unwrap();
1999 let g2d_stride = g2d_t.effective_row_stride().unwrap_or(w * channels);
2000 let cpu_stride = cpu_t.effective_row_stride().unwrap_or(w * channels);
2001 let g2d_map = g2d_t.map().unwrap();
2002 let cpu_map = cpu_t.map().unwrap();
2003 let g2d_px = g2d_map.as_slice();
2004 let cpu_px = cpu_map.as_slice();
2005 let mut max_diff = 0u32;
2006 let mut first_fail: Option<(usize, usize, usize)> = None;
2007 for row in 0..h {
2008 for col in 0..w {
2009 for ch in 0..3usize {
2010 let gi = row * g2d_stride + col * channels + ch;
2012 let ci = row * cpu_stride + col * channels + ch;
2013 let d = (g2d_px[gi] as i32 - cpu_px[ci] as i32).unsigned_abs();
2014 if d > max_diff {
2015 max_diff = d;
2016 }
2017 if first_fail.is_none() && d > tol {
2018 first_fail = Some((col, row, ch));
2019 }
2020 }
2021 }
2022 }
2023 (max_diff, first_fail)
2024 }
2025
2026 #[test]
2042 #[cfg(target_os = "linux")]
2043 fn d01_nv12_odd_w_g2d_vs_cpu() {
2044 if !is_dma_available() {
2045 crate::test_support::report_skip("d01_nv12_odd_w_g2d_vs_cpu - DMA not available");
2046 return;
2047 }
2048 let (w, h) = (65usize, 64usize);
2049
2050 let src_dma = TensorDyn::image(
2051 w,
2052 h,
2053 PixelFormat::Nv12,
2054 DType::U8,
2055 Some(TensorMemory::DmaBuf),
2056 edgefirst_tensor::CpuAccess::ReadWrite,
2057 )
2058 .unwrap();
2059 let src_mem = TensorDyn::image(
2060 w,
2061 h,
2062 PixelFormat::Nv12,
2063 DType::U8,
2064 Some(TensorMemory::Mem),
2065 edgefirst_tensor::CpuAccess::ReadWrite,
2066 )
2067 .unwrap();
2068 fill_patterned_nv12(&src_dma);
2069 fill_patterned_nv12(&src_mem);
2070
2071 let mut cpu_dst = TensorDyn::image(
2073 w,
2074 h,
2075 PixelFormat::Rgba,
2076 DType::U8,
2077 None,
2078 edgefirst_tensor::CpuAccess::ReadWrite,
2079 )
2080 .unwrap();
2081 CPUProcessor::new()
2082 .convert(&src_mem, &mut cpu_dst, None)
2083 .unwrap();
2084
2085 let mut g2d_dst = TensorDyn::image(
2087 w,
2088 h,
2089 PixelFormat::Rgba,
2090 DType::U8,
2091 Some(TensorMemory::DmaBuf),
2092 edgefirst_tensor::CpuAccess::ReadWrite,
2093 )
2094 .unwrap();
2095 let mut g2d = match G2DProcessor::new() {
2096 Ok(g) => g,
2097 Err(e) => {
2098 crate::test_support::g2d_required_or(false);
2099 crate::test_support::report_skip(&format!(
2100 "d01_nv12_odd_w_g2d_vs_cpu - G2D not available: {e}"
2101 ));
2102 return;
2103 }
2104 };
2105
2106 g2d.convert(&src_dma, &mut g2d_dst, None)
2108 .unwrap_or_else(|e| {
2109 panic!("D-01: G2D rejected odd-width (65) NV12 source: {e}");
2110 });
2111
2112 let tol = 4u32;
2114 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
2115 eprintln!("D-01 NV12 odd-W G2D vs CPU: max_diff={max_diff}");
2116 if max_diff > tol {
2134 eprintln!(
2135 "WARNING: D-01 NV12 odd-W G2D vs CPU max_diff={max_diff} > {tol} \
2136 (G2D fixed-point rounding; first bad at {first_fail:?})"
2137 );
2138 }
2139 assert!(
2140 max_diff <= 35,
2141 "D-01: gross NV12 odd-W G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2142 );
2143 }
2144
2145 #[test]
2157 #[cfg(target_os = "linux")]
2158 fn d03_nv12_odd_both_g2d_vs_cpu() {
2159 if !is_dma_available() {
2160 crate::test_support::report_skip("d03_nv12_odd_both_g2d_vs_cpu - DMA not available");
2161 return;
2162 }
2163 let (w, h) = (65usize, 63usize);
2164
2165 let src_dma = TensorDyn::image(
2166 w,
2167 h,
2168 PixelFormat::Nv12,
2169 DType::U8,
2170 Some(TensorMemory::DmaBuf),
2171 edgefirst_tensor::CpuAccess::ReadWrite,
2172 )
2173 .unwrap();
2174 let src_mem = TensorDyn::image(
2175 w,
2176 h,
2177 PixelFormat::Nv12,
2178 DType::U8,
2179 Some(TensorMemory::Mem),
2180 edgefirst_tensor::CpuAccess::ReadWrite,
2181 )
2182 .unwrap();
2183 fill_patterned_nv12(&src_dma);
2184 fill_patterned_nv12(&src_mem);
2185
2186 let mut cpu_dst = TensorDyn::image(
2188 w,
2189 h,
2190 PixelFormat::Rgba,
2191 DType::U8,
2192 None,
2193 edgefirst_tensor::CpuAccess::ReadWrite,
2194 )
2195 .unwrap();
2196 CPUProcessor::new()
2197 .convert(&src_mem, &mut cpu_dst, None)
2198 .unwrap();
2199
2200 let mut g2d_dst = TensorDyn::image(
2202 w,
2203 h,
2204 PixelFormat::Rgba,
2205 DType::U8,
2206 Some(TensorMemory::DmaBuf),
2207 edgefirst_tensor::CpuAccess::ReadWrite,
2208 )
2209 .unwrap();
2210 let mut g2d = match G2DProcessor::new() {
2211 Ok(g) => g,
2212 Err(e) => {
2213 crate::test_support::g2d_required_or(false);
2214 crate::test_support::report_skip(&format!(
2215 "d03_nv12_odd_both_g2d_vs_cpu - G2D not available: {e}"
2216 ));
2217 return;
2218 }
2219 };
2220
2221 g2d.convert(&src_dma, &mut g2d_dst, None)
2224 .unwrap_or_else(|e| {
2225 panic!("D-03: G2D rejected odd-both (65×63) NV12 source: {e}");
2226 });
2227
2228 let tol = 4u32;
2229 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
2230 eprintln!("D-03 NV12 odd-both G2D vs CPU: max_diff={max_diff}");
2231 if max_diff > tol {
2236 eprintln!(
2237 "WARNING: D-03 NV12 odd-both G2D vs CPU max_diff={max_diff} > {tol} \
2238 (G2D fixed-point rounding; first bad at {first_fail:?})"
2239 );
2240 }
2241 assert!(
2242 max_diff <= 35,
2243 "D-03: gross NV12 odd-both G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2244 );
2245 }
2246 #[test]
2258 #[cfg(target_os = "linux")]
2259 fn g2d_int8_dst_biases_only_the_pixel_bytes_at_the_declared_stride() {
2260 if !dma_and_g2d_or_skip() {
2261 return;
2262 }
2263 const W: usize = 64;
2264 const H: usize = 16;
2265 const POISON: u8 = 0xAB;
2266 let fmt = PixelFormat::Rgba;
2267 let bpp = fmt.channels();
2268 let row_bytes = W * bpp;
2269 let stride = row_bytes + 64;
2271
2272 let src = TensorDyn::image(
2273 W,
2274 H,
2275 fmt,
2276 DType::U8,
2277 Some(TensorMemory::DmaBuf),
2278 edgefirst_tensor::CpuAccess::ReadWrite,
2279 )
2280 .expect("DMA source");
2281 {
2282 let t = src.as_typed::<u8>().unwrap();
2283 let s = t.effective_row_stride().unwrap();
2284 let mut m = t.map().unwrap();
2285 let buf = m.as_mut_slice();
2286 for y in 0..H {
2287 for i in 0..row_bytes {
2288 buf[y * s + i] = (y * 31 + i * 7) as u8;
2289 }
2290 }
2291 }
2292
2293 let mut u8_dst = TensorDyn::image_with_stride(
2294 W,
2295 H,
2296 fmt,
2297 DType::U8,
2298 stride,
2299 Some(TensorMemory::DmaBuf),
2300 edgefirst_tensor::CpuAccess::ReadWrite,
2301 )
2302 .expect("padded U8 destination");
2303 let mut i8_dst = TensorDyn::image_with_stride(
2304 W,
2305 H,
2306 fmt,
2307 DType::I8,
2308 stride,
2309 Some(TensorMemory::DmaBuf),
2310 edgefirst_tensor::CpuAccess::ReadWrite,
2311 )
2312 .expect("padded I8 destination");
2313 assert_eq!(i8_dst.effective_row_stride(), Some(stride));
2314 u8_dst
2318 .as_typed::<u8>()
2319 .unwrap()
2320 .map()
2321 .unwrap()
2322 .as_mut_slice()
2323 .fill(POISON);
2324 i8_dst
2325 .as_typed::<i8>()
2326 .unwrap()
2327 .map()
2328 .unwrap()
2329 .as_mut_slice()
2330 .fill(POISON as i8);
2331
2332 let mut g2d = G2DProcessor::new().unwrap();
2333 g2d.convert(&src, &mut u8_dst, None).expect("U8 blit");
2334 g2d.convert(&src, &mut i8_dst, None).expect("I8 blit");
2335
2336 let u8_stride = u8_dst.effective_row_stride().unwrap();
2337 let want = u8_dst
2338 .as_typed::<u8>()
2339 .unwrap()
2340 .map()
2341 .unwrap()
2342 .as_slice()
2343 .to_vec();
2344 let got: Vec<u8> = i8_dst
2345 .as_typed::<i8>()
2346 .unwrap()
2347 .map()
2348 .unwrap()
2349 .as_slice()
2350 .iter()
2351 .map(|&v| v as u8)
2352 .collect();
2353 assert!(
2357 got.len() >= H * stride && want.len() >= H * u8_stride,
2358 "a {stride} B pitch over {H} rows needs {} B, mapped {} (I8) and {} (U8)",
2359 H * stride,
2360 got.len(),
2361 want.len()
2362 );
2363 for y in 0..H {
2367 assert!(
2368 want[y * u8_stride + row_bytes..(y + 1) * u8_stride]
2369 .iter()
2370 .all(|&b| b == POISON),
2371 "row {y}: the G2D blit itself wrote past the pixels; the I8 \
2372 assertions below cannot separate that from the bias pass"
2373 );
2374 }
2375 for y in 0..H {
2376 for x in 0..W {
2377 for c in 0..bpp {
2378 let g = got[y * stride + x * bpp + c];
2379 let w = want[y * u8_stride + x * bpp + c];
2380 let expect = if c == bpp - 1 { w } else { w ^ 0x80 };
2381 assert_eq!(g, expect, "pixel ({x},{y}) channel {c}");
2382 }
2383 }
2384 assert!(
2385 got[y * stride + row_bytes..(y + 1) * stride]
2386 .iter()
2387 .all(|&b| b == POISON),
2388 "row {y}: the padding past the pixels was biased"
2389 );
2390 }
2391 }
2392}