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 options
455 .get_interpolation()
456 .unwrap_or(self.interpolation)
457 .supported()
458 == crate::Interpolation::Nearest
459 && crop.resizes(
460 (src.width().unwrap_or(0), src.height().unwrap_or(0)),
461 (dst.width().unwrap_or(0), dst.height().unwrap_or(0)),
462 rotation,
463 )
464 {
465 return Err(Error::NotSupported(
466 "G2D scales with a fixed filter; Interpolation::Nearest resizes elsewhere".into(),
467 ));
468 }
469 self.convert_impl(src, dst, rotation, flip, crop)
470 }
471
472 fn draw_decoded_masks(
473 &mut self,
474 dst: &mut TensorDyn,
475 detect: &[crate::DetectBox],
476 segmentation: &[crate::Segmentation],
477 overlay: crate::MaskOverlay<'_>,
478 ) -> Result<()> {
479 if !detect.is_empty() || !segmentation.is_empty() {
483 return Err(Error::NotImplemented(
484 "G2D does not support drawing detection or segmentation overlays".to_string(),
485 ));
486 }
487 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
488 }
489
490 fn draw_proto_masks(
491 &mut self,
492 dst: &mut TensorDyn,
493 detect: &[crate::DetectBox],
494 _proto_data: &crate::ProtoData,
495 overlay: crate::MaskOverlay<'_>,
496 ) -> Result<()> {
497 if !detect.is_empty() {
500 return Err(Error::NotImplemented(
501 "G2D does not support drawing detection or segmentation overlays".to_string(),
502 ));
503 }
504 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
505 }
506
507 fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
508 Err(Error::NotImplemented(
509 "G2D does not support setting colors for rendering detection or segmentation overlays"
510 .to_string(),
511 ))
512 }
513}
514
515fn draw_empty_frame_g2d(
528 g2d: &mut G2D,
529 dst_dyn: &mut TensorDyn,
530 background: Option<&TensorDyn>,
531) -> Result<()> {
532 if dst_dyn.dtype() != DType::U8 {
533 return Err(Error::NotSupported(
534 "G2D only supports u8 destination tensors".to_string(),
535 ));
536 }
537 let dst = dst_dyn.as_typed_mut::<u8>().ok_or(Error::NotAnImage)?;
538
539 if TensorTrait::memory(dst) != TensorMemory::DmaBuf {
549 return Err(Error::NotImplemented(
550 "g2d only supports Dma memory".to_string(),
551 ));
552 }
553
554 let mut dst_surface = tensor_to_g2d_surface(dst)?;
555
556 match background {
557 None => {
558 let start = Instant::now();
562 g2d.clear(&mut dst_surface, [0, 0, 0, 0])?;
563 g2d.finish()?;
564 log::trace!("g2d clear (empty frame) takes {:?}", start.elapsed());
565 }
566 Some(bg_dyn) => {
567 if bg_dyn.shape() != dst.shape() {
572 return Err(Error::InvalidShape(
573 "background shape does not match dst".into(),
574 ));
575 }
576 if bg_dyn.format() != dst.format() {
577 return Err(Error::InvalidShape(
578 "background pixel format does not match dst".into(),
579 ));
580 }
581 if bg_dyn.dtype() != DType::U8 {
582 return Err(Error::NotSupported(
583 "G2D only supports u8 background tensors".to_string(),
584 ));
585 }
586 let bg = bg_dyn.as_typed::<u8>().ok_or(Error::NotAnImage)?;
587 if TensorTrait::memory(bg) != TensorMemory::DmaBuf {
589 return Err(Error::NotImplemented(
590 "g2d background must be Dma-backed".to_string(),
591 ));
592 }
593 let src_surface = tensor_to_g2d_surface(bg)?;
594 let start = Instant::now();
595 g2d.blit(&src_surface, &dst_surface)?;
596 g2d.finish()?;
597 log::trace!("g2d blit (bg→dst) takes {:?}", start.elapsed());
598 }
599 }
600 Ok(())
601}
602
603fn chroma_subsample_shifts(fmt: PixelFormat) -> (u32, u32) {
607 match fmt.chroma_layout() {
608 Some(cl) => (cl.shift_x, cl.shift_y),
609 None if matches!(fmt, PixelFormat::Yuyv | PixelFormat::Vyuy) => (1, 0),
612 None => (0, 0),
613 }
614}
615
616fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
620 let fmt = img.format().ok_or(Error::NotAnImage)?;
621 let fd = img
624 .dmabuf()
625 .map_err(|e| Error::NotImplemented(format!("g2d only supports Dma memory ({e})")))?;
626 let phys: G2DPhysical = fd.as_raw_fd().try_into()?;
627
628 let base_addr = phys.address();
635 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
636 let planes = if fmt == PixelFormat::Nv12 {
637 if img.is_multiplane() {
638 let chroma = img.chroma().unwrap();
640 let chroma_fd = chroma.dmabuf().map_err(|e| {
641 Error::NotImplemented(format!("g2d multiplane chroma must be DMA-backed ({e})"))
642 })?;
643 let uv_phys: G2DPhysical = chroma_fd.as_raw_fd().try_into()?;
644 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
645 [
646 base_addr + luma_offset,
647 uv_phys.address() + chroma_offset,
648 0,
649 ]
650 } else {
651 let w = img.width().unwrap();
652 let h = img.height().unwrap();
653 let stride = img.effective_row_stride().unwrap_or(w.next_multiple_of(2));
654 let uv_offset = (luma_offset as usize + stride * h) as u64;
655 [base_addr + luma_offset, base_addr + uv_offset, 0]
656 }
657 } else {
658 [base_addr + luma_offset, 0, 0]
659 };
660
661 let w = img.width().unwrap();
662 let h = img.height().unwrap();
663 let fourcc = pixelfmt_to_fourcc(fmt);
664
665 let stride_pixels = match img.effective_row_stride() {
668 Some(s) => {
669 let channels = fmt.channels();
670 if s % channels != 0 {
671 return Err(Error::NotImplemented(
672 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
673 ));
674 }
675 s / channels
676 }
677 None => w,
678 };
679
680 Ok(G2DSurface {
681 planes,
682 format: G2DFormat::try_from(fourcc)?.format(),
683 left: 0,
684 top: 0,
685 right: w as i32,
686 bottom: h as i32,
687 stride: stride_pixels as i32,
688 width: w as i32,
689 height: h as i32,
690 blendfunc: 0,
691 clrcolor: 0,
692 rot: 0,
693 global_alpha: 0,
694 })
695}
696
697#[cfg(test)]
698mod g2d_predicate_tests {
699 use super::*;
700 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry};
701
702 #[test]
703 fn g2d_declines_full_range_and_bt2020_yuv() {
704 let full = Colorimetry::default()
705 .with_range(ColorRange::Full)
706 .with_encoding(ColorEncoding::Bt709);
707 let lim = Colorimetry::default()
708 .with_range(ColorRange::Limited)
709 .with_encoding(ColorEncoding::Bt709);
710 let bt2020 = Colorimetry::default()
711 .with_range(ColorRange::Limited)
712 .with_encoding(ColorEncoding::Bt2020);
713 assert!(!g2d_can_handle(&full, true)); assert!(g2d_can_handle(&lim, true)); assert!(!g2d_can_handle(&bt2020, true)); assert!(g2d_can_handle(&full, false)); }
718
719 #[test]
720 fn post_pass_access_covers_the_int8_bias_and_the_rgb_border() {
721 use edgefirst_tensor::CpuAccess;
722 let pass = g2d_post_pass_access;
723 assert_eq!(
724 pass(PixelFormat::Rgba, DType::I8, false),
725 CpuAccess::ReadWrite
726 );
727 assert_eq!(
728 pass(PixelFormat::Rgb, DType::I8, true),
729 CpuAccess::ReadWrite
730 );
731 assert_eq!(pass(PixelFormat::Rgb, DType::U8, false), CpuAccess::None);
732 assert_eq!(pass(PixelFormat::Rgba, DType::U8, true), CpuAccess::None);
733 let dma = TensorMemory::DmaBuf;
734 assert_eq!(
735 dst_cpu_access(dma, PixelFormat::Bgra, DType::I8),
736 Some(CpuAccess::ReadWrite)
737 );
738 assert_eq!(
739 dst_cpu_access(dma, PixelFormat::Rgba, DType::U8),
740 Some(CpuAccess::None)
741 );
742 assert_eq!(dst_cpu_access(dma, PixelFormat::Nv12, DType::U8), None);
743 assert_eq!(dst_cpu_access(dma, PixelFormat::Rgba, DType::F32), None);
744 assert_eq!(
745 dst_cpu_access(TensorMemory::Mem, PixelFormat::Rgba, DType::U8),
746 None
747 );
748 assert!(g2d_supports(PixelFormat::Nv12, PixelFormat::Rgb));
749 assert!(!g2d_supports(PixelFormat::Bgra, PixelFormat::Rgba));
750 assert!(!g2d_supports(PixelFormat::Vyuy, PixelFormat::Rgba));
751 }
752
753 #[test]
756 fn post_passes_are_checked_against_the_destination_up_front() {
757 use edgefirst_tensor::{CpuAccess, Error as TensorError};
758 let image = |fmt, dtype, access| {
759 TensorDyn::image(64, 48, fmt, dtype, Some(TensorMemory::Mem), access).unwrap()
760 };
761 let border = ResolvedCrop {
762 src_rect: None,
763 dst_rect: Some(crate::Rect::new(0, 8, 64, 32)),
764 dst_color: Some([0, 0, 0, 255]),
765 };
766 let denied = |r: Result<()>, requested| match r {
767 Err(Error::Tensor(TensorError::CpuAccessDenied { requested: r, .. })) => {
768 assert_eq!(r, requested)
769 }
770 other => panic!("expected CpuAccessDenied, got {other:?}"),
771 };
772 let int8 = image(PixelFormat::Rgba, DType::I8, CpuAccess::Write);
773 denied(
774 require_post_pass_access(&int8, &ResolvedCrop::no_crop()),
775 CpuAccess::ReadWrite,
776 );
777 let rgb = image(PixelFormat::Rgb, DType::U8, CpuAccess::None);
778 denied(
779 require_post_pass_access(&rgb, &border),
780 CpuAccess::ReadWrite,
781 );
782 require_post_pass_access(&rgb, &ResolvedCrop::no_crop()).unwrap();
783 let rgba = image(PixelFormat::Rgba, DType::U8, CpuAccess::None);
784 require_post_pass_access(&rgba, &border).unwrap();
785 }
786
787 #[test]
790 fn rgb_border_post_pass_needs_readwrite() {
791 use edgefirst_tensor::CpuAccess;
792 assert_eq!(
793 g2d_post_pass_access(PixelFormat::Rgb, DType::U8, true),
794 CpuAccess::ReadWrite
795 );
796 let border = ResolvedCrop {
797 src_rect: None,
798 dst_rect: Some(crate::Rect::new(8, 0, 48, 48)),
799 dst_color: Some([0, 0, 0, 255]),
800 };
801 let image = |access| {
802 TensorDyn::image(
803 64,
804 48,
805 PixelFormat::Rgb,
806 DType::U8,
807 Some(TensorMemory::Mem),
808 access,
809 )
810 .unwrap()
811 };
812 assert!(matches!(
813 require_post_pass_access(&image(CpuAccess::Write), &border),
814 Err(Error::Tensor(
815 edgefirst_tensor::Error::CpuAccessDenied { .. }
816 ))
817 ));
818 require_post_pass_access(&image(CpuAccess::ReadWrite), &border).unwrap();
819 }
820}
821
822#[cfg(test)]
823#[allow(deprecated)]
824mod g2d_tests {
825 use super::*;
826 use crate::test_support::is_dma_available;
827 use crate::{CPUProcessor, G2DProcessor, ImageProcessorTrait};
828 use edgefirst_tensor::{
829 DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
830 };
831 use image::buffer::ConvertBuffer;
832
833 #[cfg(target_os = "linux")]
840 fn is_g2d_available() -> bool {
841 crate::test_support::g2d_required_or(G2DProcessor::new().is_ok())
842 }
843
844 #[cfg(target_os = "linux")]
846 fn dma_and_g2d_or_skip() -> bool {
847 let why = if !is_dma_available() {
848 "DMA not available"
849 } else if !is_g2d_available() {
850 "G2D not available"
851 } else {
852 return true;
853 };
854 crate::test_support::report_skip(&format!(
855 "{}: {why}",
856 crate::test_support::current_test()
857 ));
858 false
859 }
860
861 #[test]
862 #[cfg(target_os = "linux")]
863 fn test_g2d_formats_no_resize() {
864 if !dma_and_g2d_or_skip() {
865 return;
866 }
867 for i in [
868 PixelFormat::Rgba,
869 PixelFormat::Yuyv,
870 PixelFormat::Rgb,
871 PixelFormat::Grey,
872 PixelFormat::Nv12,
873 ] {
874 for o in [
875 PixelFormat::Rgba,
876 PixelFormat::Yuyv,
877 PixelFormat::Rgb,
878 PixelFormat::Grey,
879 ] {
880 let res = test_g2d_format_no_resize_(i, o);
881 if let Err(e) = res {
882 crate::test_support::report_skip(&format!(
883 "{}: {i} to {o} failed: {e:?}",
884 crate::test_support::current_test()
885 ));
886 }
887 }
888 }
889 }
890
891 fn test_g2d_format_no_resize_(
892 g2d_in_fmt: PixelFormat,
893 g2d_out_fmt: PixelFormat,
894 ) -> Result<(), crate::Error> {
895 let dst_width = 1280;
896 let dst_height = 720;
897 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
898 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
899
900 let mut src2 = TensorDyn::image(
902 1280,
903 720,
904 g2d_in_fmt,
905 DType::U8,
906 Some(TensorMemory::DmaBuf),
907 edgefirst_tensor::CpuAccess::ReadWrite,
908 )?;
909
910 let mut cpu_converter = CPUProcessor::new();
911
912 if g2d_in_fmt == PixelFormat::Nv12 {
914 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
915 src2.as_typed::<u8>()
916 .unwrap()
917 .map()?
918 .as_mut_slice()
919 .copy_from_slice(&nv12_bytes);
920 } else {
921 cpu_converter.convert(&src, &mut src2, None)?;
922 }
923
924 let mut g2d_dst = TensorDyn::image(
925 dst_width,
926 dst_height,
927 g2d_out_fmt,
928 DType::U8,
929 Some(TensorMemory::DmaBuf),
930 edgefirst_tensor::CpuAccess::ReadWrite,
931 )?;
932 let mut g2d_converter = G2DProcessor::new()?;
933 let src2_dyn = src2;
934 let mut g2d_dst_dyn = g2d_dst;
935 g2d_converter.convert(&src2_dyn, &mut g2d_dst_dyn, None)?;
936 g2d_dst = {
937 let mut __t = g2d_dst_dyn.into_u8().unwrap();
938 __t.set_format(g2d_out_fmt)
939 .map_err(|e| crate::Error::Internal(e.to_string()))?;
940 TensorDyn::from(__t)
941 };
942
943 let mut cpu_dst = TensorDyn::image(
944 dst_width,
945 dst_height,
946 PixelFormat::Rgb,
947 DType::U8,
948 None,
949 edgefirst_tensor::CpuAccess::ReadWrite,
950 )?;
951 cpu_converter.convert(&g2d_dst, &mut cpu_dst, None)?;
952
953 compare_images(
954 &src,
955 &cpu_dst,
956 0.98,
957 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
958 )
959 }
960
961 #[test]
962 #[cfg(target_os = "linux")]
963 fn test_g2d_formats_with_resize() {
964 if !dma_and_g2d_or_skip() {
965 return;
966 }
967 for i in [
968 PixelFormat::Rgba,
969 PixelFormat::Yuyv,
970 PixelFormat::Rgb,
971 PixelFormat::Grey,
972 PixelFormat::Nv12,
973 ] {
974 for o in [
975 PixelFormat::Rgba,
976 PixelFormat::Yuyv,
977 PixelFormat::Rgb,
978 PixelFormat::Grey,
979 ] {
980 let res = test_g2d_format_with_resize_(i, o);
981 if let Err(e) = res {
982 crate::test_support::report_skip(&format!(
983 "{}: {i} to {o} failed: {e:?}",
984 crate::test_support::current_test()
985 ));
986 }
987 }
988 }
989 }
990
991 #[test]
992 #[cfg(target_os = "linux")]
993 fn test_g2d_formats_with_resize_dst_crop() {
994 if !dma_and_g2d_or_skip() {
995 return;
996 }
997 for i in [
998 PixelFormat::Rgba,
999 PixelFormat::Yuyv,
1000 PixelFormat::Rgb,
1001 PixelFormat::Grey,
1002 PixelFormat::Nv12,
1003 ] {
1004 for o in [
1005 PixelFormat::Rgba,
1006 PixelFormat::Yuyv,
1007 PixelFormat::Rgb,
1008 PixelFormat::Grey,
1009 ] {
1010 let res = test_g2d_format_with_resize_dst_crop(i, o);
1011 if let Err(e) = res {
1012 crate::test_support::report_skip(&format!(
1013 "{}: {i} to {o} failed: {e:?}",
1014 crate::test_support::current_test()
1015 ));
1016 }
1017 }
1018 }
1019 }
1020
1021 fn test_g2d_format_with_resize_(
1022 g2d_in_fmt: PixelFormat,
1023 g2d_out_fmt: PixelFormat,
1024 ) -> Result<(), crate::Error> {
1025 let dst_width = 600;
1026 let dst_height = 400;
1027 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1028 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1029
1030 let mut cpu_converter = CPUProcessor::new();
1031
1032 let mut reference = TensorDyn::image(
1033 dst_width,
1034 dst_height,
1035 PixelFormat::Rgb,
1036 DType::U8,
1037 Some(TensorMemory::DmaBuf),
1038 edgefirst_tensor::CpuAccess::ReadWrite,
1039 )?;
1040 cpu_converter.convert(&src, &mut reference, None)?;
1041
1042 let mut src2 = TensorDyn::image(
1044 1280,
1045 720,
1046 g2d_in_fmt,
1047 DType::U8,
1048 Some(TensorMemory::DmaBuf),
1049 edgefirst_tensor::CpuAccess::ReadWrite,
1050 )?;
1051
1052 if g2d_in_fmt == PixelFormat::Nv12 {
1054 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1055 src2.as_typed::<u8>()
1056 .unwrap()
1057 .map()?
1058 .as_mut_slice()
1059 .copy_from_slice(&nv12_bytes);
1060 } else {
1061 cpu_converter.convert(&src, &mut src2, None)?;
1062 }
1063
1064 let mut g2d_dst = TensorDyn::image(
1065 dst_width,
1066 dst_height,
1067 g2d_out_fmt,
1068 DType::U8,
1069 Some(TensorMemory::DmaBuf),
1070 edgefirst_tensor::CpuAccess::ReadWrite,
1071 )?;
1072 let mut g2d_converter = G2DProcessor::new()?;
1073 let src2_dyn = src2;
1074 let mut g2d_dst_dyn = g2d_dst;
1075 g2d_converter.convert(&src2_dyn, &mut g2d_dst_dyn, None)?;
1076 g2d_dst = {
1077 let mut __t = g2d_dst_dyn.into_u8().unwrap();
1078 __t.set_format(g2d_out_fmt)
1079 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1080 TensorDyn::from(__t)
1081 };
1082
1083 let mut cpu_dst = TensorDyn::image(
1084 dst_width,
1085 dst_height,
1086 PixelFormat::Rgb,
1087 DType::U8,
1088 None,
1089 edgefirst_tensor::CpuAccess::ReadWrite,
1090 )?;
1091 cpu_converter.convert(&g2d_dst, &mut cpu_dst, None)?;
1092
1093 compare_images(
1094 &reference,
1095 &cpu_dst,
1096 0.98,
1097 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
1098 )
1099 }
1100
1101 fn test_g2d_format_with_resize_dst_crop(
1102 g2d_in_fmt: PixelFormat,
1103 g2d_out_fmt: PixelFormat,
1104 ) -> Result<(), crate::Error> {
1105 let dst_width = 600;
1106 let dst_height = 400;
1107 let region = crate::Region::new(100, 100, 200, 100);
1112 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1113 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1114
1115 let mut cpu_converter = CPUProcessor::new();
1116
1117 let reference = TensorDyn::image(
1118 dst_width,
1119 dst_height,
1120 PixelFormat::Rgb,
1121 DType::U8,
1122 Some(TensorMemory::DmaBuf),
1123 edgefirst_tensor::CpuAccess::ReadWrite,
1124 )?;
1125 reference
1126 .as_typed::<u8>()
1127 .unwrap()
1128 .map()
1129 .unwrap()
1130 .as_mut_slice()
1131 .fill(128);
1132 cpu_converter.convert(&src, &mut reference.view(region)?, None)?;
1133
1134 let mut src2 = TensorDyn::image(
1136 1280,
1137 720,
1138 g2d_in_fmt,
1139 DType::U8,
1140 Some(TensorMemory::DmaBuf),
1141 edgefirst_tensor::CpuAccess::ReadWrite,
1142 )?;
1143
1144 if g2d_in_fmt == PixelFormat::Nv12 {
1146 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1147 src2.as_typed::<u8>()
1148 .unwrap()
1149 .map()?
1150 .as_mut_slice()
1151 .copy_from_slice(&nv12_bytes);
1152 } else {
1153 cpu_converter.convert(&src, &mut src2, None)?;
1154 }
1155
1156 let mut g2d_dst = TensorDyn::image(
1157 dst_width,
1158 dst_height,
1159 g2d_out_fmt,
1160 DType::U8,
1161 Some(TensorMemory::DmaBuf),
1162 edgefirst_tensor::CpuAccess::ReadWrite,
1163 )?;
1164 g2d_dst
1165 .as_typed::<u8>()
1166 .unwrap()
1167 .map()
1168 .unwrap()
1169 .as_mut_slice()
1170 .fill(128);
1171 let mut g2d_converter = G2DProcessor::new()?;
1172 let src2_dyn = src2;
1173 let g2d_dst_dyn = g2d_dst;
1174 g2d_converter.convert(&src2_dyn, &mut g2d_dst_dyn.view(region)?, None)?;
1175 g2d_dst = {
1176 let mut __t = g2d_dst_dyn.into_u8().unwrap();
1177 __t.set_format(g2d_out_fmt)
1178 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1179 TensorDyn::from(__t)
1180 };
1181
1182 let mut cpu_dst = TensorDyn::image(
1183 dst_width,
1184 dst_height,
1185 PixelFormat::Rgb,
1186 DType::U8,
1187 None,
1188 edgefirst_tensor::CpuAccess::ReadWrite,
1189 )?;
1190 cpu_converter.convert(&g2d_dst, &mut cpu_dst, None)?;
1191
1192 compare_images(
1193 &reference,
1194 &cpu_dst,
1195 0.98,
1196 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
1197 )
1198 }
1199
1200 fn compare_images(
1201 img1: &TensorDyn,
1202 img2: &TensorDyn,
1203 threshold: f64,
1204 name: &str,
1205 ) -> Result<(), crate::Error> {
1206 assert_eq!(img1.height(), img2.height(), "Heights differ");
1207 assert_eq!(img1.width(), img2.width(), "Widths differ");
1208 assert_eq!(
1209 img1.format().unwrap(),
1210 img2.format().unwrap(),
1211 "PixelFormat differ"
1212 );
1213 assert!(
1214 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
1215 "format must be Rgb or Rgba for comparison"
1216 );
1217 let image1 = match img1.format().unwrap() {
1218 PixelFormat::Rgb => image::RgbImage::from_vec(
1219 img1.width().unwrap() as u32,
1220 img1.height().unwrap() as u32,
1221 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1222 )
1223 .unwrap(),
1224 PixelFormat::Rgba => image::RgbaImage::from_vec(
1225 img1.width().unwrap() as u32,
1226 img1.height().unwrap() as u32,
1227 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1228 )
1229 .unwrap()
1230 .convert(),
1231
1232 _ => unreachable!(),
1233 };
1234
1235 let image2 = match img2.format().unwrap() {
1236 PixelFormat::Rgb => image::RgbImage::from_vec(
1237 img2.width().unwrap() as u32,
1238 img2.height().unwrap() as u32,
1239 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1240 )
1241 .unwrap(),
1242 PixelFormat::Rgba => image::RgbaImage::from_vec(
1243 img2.width().unwrap() as u32,
1244 img2.height().unwrap() as u32,
1245 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1246 )
1247 .unwrap()
1248 .convert(),
1249
1250 _ => unreachable!(),
1251 };
1252
1253 let similarity = image_compare::rgb_similarity_structure(
1254 &image_compare::Algorithm::RootMeanSquared,
1255 &image1,
1256 &image2,
1257 )
1258 .expect("Image Comparison failed");
1259
1260 if similarity.score < threshold {
1261 image1.save(format!("{name}_1.png")).unwrap();
1262 image2.save(format!("{name}_2.png")).unwrap();
1263 return Err(Error::Internal(format!(
1264 "{name}: converted image and target image have similarity score too low: {} < {}",
1265 similarity.score, threshold
1266 )));
1267 }
1268 Ok(())
1269 }
1270
1271 fn load_raw_image(
1277 width: usize,
1278 height: usize,
1279 format: PixelFormat,
1280 memory: Option<TensorMemory>,
1281 bytes: &[u8],
1282 ) -> Result<TensorDyn, crate::Error> {
1283 let img = TensorDyn::image(
1284 width,
1285 height,
1286 format,
1287 DType::U8,
1288 memory,
1289 edgefirst_tensor::CpuAccess::ReadWrite,
1290 )?;
1291 let mut map = img.as_typed::<u8>().unwrap().map()?;
1292 let dst = map.as_mut_slice();
1293 if bytes.len() > dst.len() {
1294 return Err(crate::Error::InvalidShape(format!(
1295 "load_raw_image: {} input bytes exceed {}-byte image buffer",
1296 bytes.len(),
1297 dst.len()
1298 )));
1299 }
1300 dst[..bytes.len()].copy_from_slice(bytes);
1301 Ok(img)
1302 }
1303
1304 #[test]
1306 #[cfg(target_os = "linux")]
1307 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
1308 if !dma_and_g2d_or_skip() {
1309 return Ok(());
1310 }
1311 let src = load_raw_image(
1313 1280,
1314 720,
1315 PixelFormat::Nv12,
1316 Some(TensorMemory::DmaBuf),
1317 &edgefirst_bench::testdata::read("camera720p.nv12"),
1318 )?;
1319
1320 let reference = load_raw_image(
1322 1280,
1323 720,
1324 PixelFormat::Rgba,
1325 None,
1326 &edgefirst_bench::testdata::read("camera720p.rgba"),
1327 )?;
1328
1329 let mut dst = TensorDyn::image(
1331 1280,
1332 720,
1333 PixelFormat::Rgba,
1334 DType::U8,
1335 Some(TensorMemory::DmaBuf),
1336 edgefirst_tensor::CpuAccess::ReadWrite,
1337 )?;
1338 let mut g2d = G2DProcessor::new()?;
1339 let src_dyn = src;
1340 let mut dst_dyn = dst;
1341 g2d.convert(&src_dyn, &mut dst_dyn, None)?;
1342 dst = {
1343 let mut __t = dst_dyn.into_u8().unwrap();
1344 __t.set_format(PixelFormat::Rgba)
1345 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1346 TensorDyn::from(__t)
1347 };
1348
1349 let cpu_dst = TensorDyn::image(
1351 1280,
1352 720,
1353 PixelFormat::Rgba,
1354 DType::U8,
1355 None,
1356 edgefirst_tensor::CpuAccess::ReadWrite,
1357 )?;
1358 cpu_dst
1359 .as_typed::<u8>()
1360 .unwrap()
1361 .map()?
1362 .as_mut_slice()
1363 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1364
1365 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
1366 }
1367
1368 #[test]
1370 #[cfg(target_os = "linux")]
1371 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
1372 if !dma_and_g2d_or_skip() {
1373 return Ok(());
1374 }
1375 let src = load_raw_image(
1377 1280,
1378 720,
1379 PixelFormat::Nv12,
1380 Some(TensorMemory::DmaBuf),
1381 &edgefirst_bench::testdata::read("camera720p.nv12"),
1382 )?;
1383
1384 let reference = load_raw_image(
1386 1280,
1387 720,
1388 PixelFormat::Rgb,
1389 None,
1390 &edgefirst_bench::testdata::read("camera720p.rgb"),
1391 )?;
1392
1393 let mut dst = TensorDyn::image(
1395 1280,
1396 720,
1397 PixelFormat::Rgb,
1398 DType::U8,
1399 Some(TensorMemory::DmaBuf),
1400 edgefirst_tensor::CpuAccess::ReadWrite,
1401 )?;
1402 let mut g2d = G2DProcessor::new()?;
1403 let src_dyn = src;
1404 let mut dst_dyn = dst;
1405 g2d.convert(&src_dyn, &mut dst_dyn, None)?;
1406 dst = {
1407 let mut __t = dst_dyn.into_u8().unwrap();
1408 __t.set_format(PixelFormat::Rgb)
1409 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1410 TensorDyn::from(__t)
1411 };
1412
1413 let cpu_dst = TensorDyn::image(
1415 1280,
1416 720,
1417 PixelFormat::Rgb,
1418 DType::U8,
1419 None,
1420 edgefirst_tensor::CpuAccess::ReadWrite,
1421 )?;
1422 cpu_dst
1423 .as_typed::<u8>()
1424 .unwrap()
1425 .map()?
1426 .as_mut_slice()
1427 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1428
1429 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1430 }
1431
1432 #[test]
1434 #[cfg(target_os = "linux")]
1435 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1436 if !dma_and_g2d_or_skip() {
1437 return Ok(());
1438 }
1439 let src = load_raw_image(
1441 1280,
1442 720,
1443 PixelFormat::Yuyv,
1444 Some(TensorMemory::DmaBuf),
1445 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1446 )?;
1447
1448 let reference = load_raw_image(
1450 1280,
1451 720,
1452 PixelFormat::Rgba,
1453 None,
1454 &edgefirst_bench::testdata::read("camera720p.rgba"),
1455 )?;
1456
1457 let mut dst = TensorDyn::image(
1459 1280,
1460 720,
1461 PixelFormat::Rgba,
1462 DType::U8,
1463 Some(TensorMemory::DmaBuf),
1464 edgefirst_tensor::CpuAccess::ReadWrite,
1465 )?;
1466 let mut g2d = G2DProcessor::new()?;
1467 let src_dyn = src;
1468 let mut dst_dyn = dst;
1469 g2d.convert(&src_dyn, &mut dst_dyn, None)?;
1470 dst = {
1471 let mut __t = dst_dyn.into_u8().unwrap();
1472 __t.set_format(PixelFormat::Rgba)
1473 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1474 TensorDyn::from(__t)
1475 };
1476
1477 let cpu_dst = TensorDyn::image(
1479 1280,
1480 720,
1481 PixelFormat::Rgba,
1482 DType::U8,
1483 None,
1484 edgefirst_tensor::CpuAccess::ReadWrite,
1485 )?;
1486 cpu_dst
1487 .as_typed::<u8>()
1488 .unwrap()
1489 .map()?
1490 .as_mut_slice()
1491 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1492
1493 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1494 }
1495
1496 #[test]
1498 #[cfg(target_os = "linux")]
1499 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1500 if !dma_and_g2d_or_skip() {
1501 return Ok(());
1502 }
1503 let src = load_raw_image(
1505 1280,
1506 720,
1507 PixelFormat::Yuyv,
1508 Some(TensorMemory::DmaBuf),
1509 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1510 )?;
1511
1512 let reference = load_raw_image(
1514 1280,
1515 720,
1516 PixelFormat::Rgb,
1517 None,
1518 &edgefirst_bench::testdata::read("camera720p.rgb"),
1519 )?;
1520
1521 let mut dst = TensorDyn::image(
1523 1280,
1524 720,
1525 PixelFormat::Rgb,
1526 DType::U8,
1527 Some(TensorMemory::DmaBuf),
1528 edgefirst_tensor::CpuAccess::ReadWrite,
1529 )?;
1530 let mut g2d = G2DProcessor::new()?;
1531 let src_dyn = src;
1532 let mut dst_dyn = dst;
1533 g2d.convert(&src_dyn, &mut dst_dyn, None)?;
1534 dst = {
1535 let mut __t = dst_dyn.into_u8().unwrap();
1536 __t.set_format(PixelFormat::Rgb)
1537 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1538 TensorDyn::from(__t)
1539 };
1540
1541 let cpu_dst = TensorDyn::image(
1543 1280,
1544 720,
1545 PixelFormat::Rgb,
1546 DType::U8,
1547 None,
1548 edgefirst_tensor::CpuAccess::ReadWrite,
1549 )?;
1550 cpu_dst
1551 .as_typed::<u8>()
1552 .unwrap()
1553 .map()?
1554 .as_mut_slice()
1555 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1556
1557 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1558 }
1559
1560 #[test]
1563 #[cfg(target_os = "linux")]
1564 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1565 fn test_g2d_bgra_no_resize() {
1566 for src_fmt in [
1567 PixelFormat::Rgba,
1568 PixelFormat::Yuyv,
1569 PixelFormat::Nv12,
1570 PixelFormat::Bgra,
1571 ] {
1572 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1573 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1574 });
1575 }
1576 }
1577
1578 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1579 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1580 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1581
1582 let mut src2 = TensorDyn::image(
1584 1280,
1585 720,
1586 g2d_in_fmt,
1587 DType::U8,
1588 Some(TensorMemory::DmaBuf),
1589 edgefirst_tensor::CpuAccess::ReadWrite,
1590 )?;
1591 let mut cpu_converter = CPUProcessor::new();
1592
1593 if g2d_in_fmt == PixelFormat::Nv12 {
1594 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1595 src2.as_typed::<u8>()
1596 .unwrap()
1597 .map()?
1598 .as_mut_slice()
1599 .copy_from_slice(&nv12_bytes);
1600 } else {
1601 cpu_converter.convert(&src, &mut src2, None)?;
1602 }
1603
1604 let mut g2d = G2DProcessor::new()?;
1605
1606 let mut bgra_dst = TensorDyn::image(
1608 1280,
1609 720,
1610 PixelFormat::Bgra,
1611 DType::U8,
1612 Some(TensorMemory::DmaBuf),
1613 edgefirst_tensor::CpuAccess::ReadWrite,
1614 )?;
1615 let src2_dyn = src2;
1616 let mut bgra_dst_dyn = bgra_dst;
1617 g2d.convert(&src2_dyn, &mut bgra_dst_dyn, None)?;
1618 bgra_dst = {
1619 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1620 __t.set_format(PixelFormat::Bgra)
1621 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1622 TensorDyn::from(__t)
1623 };
1624
1625 let src2 = {
1627 let mut __t = src2_dyn.into_u8().unwrap();
1628 __t.set_format(g2d_in_fmt)
1629 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1630 TensorDyn::from(__t)
1631 };
1632
1633 let mut rgba_dst = TensorDyn::image(
1635 1280,
1636 720,
1637 PixelFormat::Rgba,
1638 DType::U8,
1639 Some(TensorMemory::DmaBuf),
1640 edgefirst_tensor::CpuAccess::ReadWrite,
1641 )?;
1642 let src2_dyn2 = src2;
1643 let mut rgba_dst_dyn = rgba_dst;
1644 g2d.convert(&src2_dyn2, &mut rgba_dst_dyn, None)?;
1645 rgba_dst = {
1646 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1647 __t.set_format(PixelFormat::Rgba)
1648 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1649 TensorDyn::from(__t)
1650 };
1651
1652 let bgra_cpu = TensorDyn::image(
1654 1280,
1655 720,
1656 PixelFormat::Bgra,
1657 DType::U8,
1658 None,
1659 edgefirst_tensor::CpuAccess::ReadWrite,
1660 )?;
1661 bgra_cpu
1662 .as_typed::<u8>()
1663 .unwrap()
1664 .map()?
1665 .as_mut_slice()
1666 .copy_from_slice(bgra_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1667
1668 let rgba_cpu = TensorDyn::image(
1669 1280,
1670 720,
1671 PixelFormat::Rgba,
1672 DType::U8,
1673 None,
1674 edgefirst_tensor::CpuAccess::ReadWrite,
1675 )?;
1676 rgba_cpu
1677 .as_typed::<u8>()
1678 .unwrap()
1679 .map()?
1680 .as_mut_slice()
1681 .copy_from_slice(rgba_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1682
1683 let bgra_map = bgra_cpu.as_typed::<u8>().unwrap().map()?;
1685 let rgba_map = rgba_cpu.as_typed::<u8>().unwrap().map()?;
1686 let bgra_buf = bgra_map.as_slice();
1687 let rgba_buf = rgba_map.as_slice();
1688
1689 assert_eq!(bgra_buf.len(), rgba_buf.len());
1690 for (i, (bc, rc)) in bgra_buf
1691 .as_chunks::<4>()
1692 .0
1693 .iter()
1694 .zip(rgba_buf.as_chunks::<4>().0)
1695 .enumerate()
1696 {
1697 assert_eq!(
1698 bc[0], rc[2],
1699 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1700 );
1701 assert_eq!(
1702 bc[1], rc[1],
1703 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1704 );
1705 assert_eq!(
1706 bc[2], rc[0],
1707 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1708 );
1709 assert_eq!(
1710 bc[3], rc[3],
1711 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1712 );
1713 }
1714 Ok(())
1715 }
1716
1717 fn surface_for(
1728 width: usize,
1729 height: usize,
1730 fmt: PixelFormat,
1731 offset: Option<usize>,
1732 row_stride: Option<usize>,
1733 ) -> Result<G2DSurface, crate::Error> {
1734 use edgefirst_tensor::TensorMemory;
1735 let mut t = Tensor::<u8>::image(
1736 width,
1737 height,
1738 fmt,
1739 Some(TensorMemory::DmaBuf),
1740 edgefirst_tensor::CpuAccess::ReadWrite,
1741 )?;
1742 if let Some(o) = offset {
1743 t.set_plane_offset(o);
1744 }
1745 if let Some(s) = row_stride {
1746 t.set_row_stride_unchecked(s);
1747 }
1748 tensor_to_g2d_surface(&t)
1749 }
1750
1751 #[test]
1752 fn g2d_surface_single_plane_no_offset() {
1753 if !dma_and_g2d_or_skip() {
1754 return;
1755 }
1756 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1757 assert_ne!(s.planes[0], 0);
1759 assert_eq!(s.stride, 640);
1760 }
1761
1762 #[test]
1763 fn g2d_surface_single_plane_with_offset() {
1764 if !dma_and_g2d_or_skip() {
1765 return;
1766 }
1767 use edgefirst_tensor::TensorMemory;
1768 let mut t = Tensor::<u8>::image(
1769 640,
1770 480,
1771 PixelFormat::Rgba,
1772 Some(TensorMemory::DmaBuf),
1773 edgefirst_tensor::CpuAccess::ReadWrite,
1774 )
1775 .unwrap();
1776 let s0 = tensor_to_g2d_surface(&t).unwrap();
1777 t.set_plane_offset(4096);
1778 let s1 = tensor_to_g2d_surface(&t).unwrap();
1779 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1780 }
1781
1782 #[test]
1783 fn g2d_surface_single_plane_zero_offset() {
1784 if !dma_and_g2d_or_skip() {
1785 return;
1786 }
1787 use edgefirst_tensor::TensorMemory;
1788 let mut t = Tensor::<u8>::image(
1789 640,
1790 480,
1791 PixelFormat::Rgba,
1792 Some(TensorMemory::DmaBuf),
1793 edgefirst_tensor::CpuAccess::ReadWrite,
1794 )
1795 .unwrap();
1796 let s_none = tensor_to_g2d_surface(&t).unwrap();
1797 t.set_plane_offset(0);
1798 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1799 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1801 }
1802
1803 #[test]
1804 fn g2d_surface_stride_rgba() {
1805 if !dma_and_g2d_or_skip() {
1806 return;
1807 }
1808 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1810 assert_eq!(s_default.stride, 640);
1811
1812 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1814 assert_eq!(s_custom.stride, 704);
1815 }
1816
1817 #[test]
1818 fn g2d_surface_stride_rgb() {
1819 if !dma_and_g2d_or_skip() {
1820 return;
1821 }
1822 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1823 assert_eq!(s_default.stride, 640);
1824
1825 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1827 assert_eq!(s_custom.stride, 660);
1828 }
1829
1830 #[test]
1831 fn g2d_surface_stride_grey() {
1832 if !dma_and_g2d_or_skip() {
1833 return;
1834 }
1835 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1837 Ok(s) => s,
1838 Err(e @ crate::Error::G2D(..)) => {
1839 crate::test_support::report_skip(&format!(
1840 "g2d_surface_stride_grey: this G2D does not support Y800: {e:?}"
1841 ));
1842 return;
1843 }
1844 Err(e) => panic!("unexpected error: {e:?}"),
1845 };
1846 assert_eq!(s.stride, 1024);
1848 }
1849
1850 #[test]
1851 fn g2d_surface_contiguous_nv12_offset() {
1852 if !dma_and_g2d_or_skip() {
1853 return;
1854 }
1855 use edgefirst_tensor::TensorMemory;
1856 let mut t = Tensor::<u8>::image(
1857 640,
1858 480,
1859 PixelFormat::Nv12,
1860 Some(TensorMemory::DmaBuf),
1861 edgefirst_tensor::CpuAccess::ReadWrite,
1862 )
1863 .unwrap();
1864 let s0 = tensor_to_g2d_surface(&t).unwrap();
1865
1866 t.set_plane_offset(8192);
1867 let s1 = tensor_to_g2d_surface(&t).unwrap();
1868
1869 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1871 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1875 }
1876
1877 #[test]
1878 fn g2d_surface_contiguous_nv12_stride() {
1879 if !dma_and_g2d_or_skip() {
1880 return;
1881 }
1882 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1884 assert_eq!(s.stride, 640);
1885
1886 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1888 assert_eq!(s_padded.stride, 1024);
1889 }
1890
1891 #[test]
1892 fn g2d_surface_multiplane_nv12_offset() {
1893 if !dma_and_g2d_or_skip() {
1894 return;
1895 }
1896 use edgefirst_tensor::TensorMemory;
1897
1898 let mut luma =
1900 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::DmaBuf), Some("luma")).unwrap();
1901 let mut chroma =
1902 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::DmaBuf), Some("chroma")).unwrap();
1903
1904 let luma_base = {
1906 let dma = luma.as_dma().unwrap();
1907 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1908 phys.address()
1909 };
1910 let chroma_base = {
1911 let dma = chroma.as_dma().unwrap();
1912 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1913 phys.address()
1914 };
1915
1916 luma.set_plane_offset(4096);
1918 chroma.set_plane_offset(2048);
1919 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1920 let s = tensor_to_g2d_surface(&combined).unwrap();
1921
1922 assert_eq!(s.planes[0], luma_base + 4096);
1924 assert_eq!(s.planes[1], chroma_base + 2048);
1926 }
1927
1928 #[cfg(target_os = "linux")]
1956 fn fill_patterned_nv12(t: &TensorDyn) {
1957 let w = t.width().unwrap();
1958 let h = t.height().unwrap();
1959 let stride = t.effective_row_stride().unwrap();
1960
1961 let chroma_h = h.div_ceil(2);
1963 let chroma_w = w.div_ceil(2);
1964
1965 let bound = t.as_typed::<u8>().unwrap();
1966 let mut m = bound.map().unwrap();
1967 let buf = m.as_mut_slice();
1968 let uv_start = stride * h;
1969
1970 for r in 0..h {
1972 for c in 0..w {
1973 buf[r * stride + c] = ((r * 3 + c * 5) % 256) as u8;
1974 }
1975 }
1976 for cr_row in 0..chroma_h {
1978 for cc in 0..chroma_w {
1979 let cb_val = ((cc * 7 + cr_row * 11 + 40) % 256) as u8;
1980 let cr_val = ((cc * 13 + cr_row * 3 + 80) % 256) as u8;
1981 let uv_byte = uv_start + cr_row * stride + cc * 2;
1982 buf[uv_byte] = cb_val;
1983 buf[uv_byte + 1] = cr_val;
1984 }
1985 }
1986 }
1987
1988 #[cfg(target_os = "linux")]
1993 fn compare_g2d_vs_cpu_rgba(
1994 g2d_dst: &TensorDyn,
1995 cpu_dst: &TensorDyn,
1996 w: usize,
1997 h: usize,
1998 tol: u32,
1999 ) -> (u32, Option<(usize, usize, usize)>) {
2000 let channels = 4usize; let g2d_t = g2d_dst.as_typed::<u8>().unwrap();
2002 let cpu_t = cpu_dst.as_typed::<u8>().unwrap();
2003 let g2d_stride = g2d_t.effective_row_stride().unwrap_or(w * channels);
2004 let cpu_stride = cpu_t.effective_row_stride().unwrap_or(w * channels);
2005 let g2d_map = g2d_t.map().unwrap();
2006 let cpu_map = cpu_t.map().unwrap();
2007 let g2d_px = g2d_map.as_slice();
2008 let cpu_px = cpu_map.as_slice();
2009 let mut max_diff = 0u32;
2010 let mut first_fail: Option<(usize, usize, usize)> = None;
2011 for row in 0..h {
2012 for col in 0..w {
2013 for ch in 0..3usize {
2014 let gi = row * g2d_stride + col * channels + ch;
2016 let ci = row * cpu_stride + col * channels + ch;
2017 let d = (g2d_px[gi] as i32 - cpu_px[ci] as i32).unsigned_abs();
2018 if d > max_diff {
2019 max_diff = d;
2020 }
2021 if first_fail.is_none() && d > tol {
2022 first_fail = Some((col, row, ch));
2023 }
2024 }
2025 }
2026 }
2027 (max_diff, first_fail)
2028 }
2029
2030 #[test]
2046 #[cfg(target_os = "linux")]
2047 fn d01_nv12_odd_w_g2d_vs_cpu() {
2048 if !is_dma_available() {
2049 crate::test_support::report_skip("d01_nv12_odd_w_g2d_vs_cpu - DMA not available");
2050 return;
2051 }
2052 let (w, h) = (65usize, 64usize);
2053
2054 let src_dma = TensorDyn::image(
2055 w,
2056 h,
2057 PixelFormat::Nv12,
2058 DType::U8,
2059 Some(TensorMemory::DmaBuf),
2060 edgefirst_tensor::CpuAccess::ReadWrite,
2061 )
2062 .unwrap();
2063 let src_mem = TensorDyn::image(
2064 w,
2065 h,
2066 PixelFormat::Nv12,
2067 DType::U8,
2068 Some(TensorMemory::Mem),
2069 edgefirst_tensor::CpuAccess::ReadWrite,
2070 )
2071 .unwrap();
2072 fill_patterned_nv12(&src_dma);
2073 fill_patterned_nv12(&src_mem);
2074
2075 let mut cpu_dst = TensorDyn::image(
2077 w,
2078 h,
2079 PixelFormat::Rgba,
2080 DType::U8,
2081 None,
2082 edgefirst_tensor::CpuAccess::ReadWrite,
2083 )
2084 .unwrap();
2085 CPUProcessor::new()
2086 .convert(&src_mem, &mut cpu_dst, None)
2087 .unwrap();
2088
2089 let mut g2d_dst = TensorDyn::image(
2091 w,
2092 h,
2093 PixelFormat::Rgba,
2094 DType::U8,
2095 Some(TensorMemory::DmaBuf),
2096 edgefirst_tensor::CpuAccess::ReadWrite,
2097 )
2098 .unwrap();
2099 let mut g2d = match G2DProcessor::new() {
2100 Ok(g) => g,
2101 Err(e) => {
2102 crate::test_support::g2d_required_or(false);
2103 crate::test_support::report_skip(&format!(
2104 "d01_nv12_odd_w_g2d_vs_cpu - G2D not available: {e}"
2105 ));
2106 return;
2107 }
2108 };
2109
2110 g2d.convert(&src_dma, &mut g2d_dst, None)
2112 .unwrap_or_else(|e| {
2113 panic!("D-01: G2D rejected odd-width (65) NV12 source: {e}");
2114 });
2115
2116 let tol = 4u32;
2118 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
2119 eprintln!("D-01 NV12 odd-W G2D vs CPU: max_diff={max_diff}");
2120 if max_diff > tol {
2138 eprintln!(
2139 "WARNING: D-01 NV12 odd-W G2D vs CPU max_diff={max_diff} > {tol} \
2140 (G2D fixed-point rounding; first bad at {first_fail:?})"
2141 );
2142 }
2143 assert!(
2144 max_diff <= 35,
2145 "D-01: gross NV12 odd-W G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2146 );
2147 }
2148
2149 #[test]
2161 #[cfg(target_os = "linux")]
2162 fn d03_nv12_odd_both_g2d_vs_cpu() {
2163 if !is_dma_available() {
2164 crate::test_support::report_skip("d03_nv12_odd_both_g2d_vs_cpu - DMA not available");
2165 return;
2166 }
2167 let (w, h) = (65usize, 63usize);
2168
2169 let src_dma = TensorDyn::image(
2170 w,
2171 h,
2172 PixelFormat::Nv12,
2173 DType::U8,
2174 Some(TensorMemory::DmaBuf),
2175 edgefirst_tensor::CpuAccess::ReadWrite,
2176 )
2177 .unwrap();
2178 let src_mem = TensorDyn::image(
2179 w,
2180 h,
2181 PixelFormat::Nv12,
2182 DType::U8,
2183 Some(TensorMemory::Mem),
2184 edgefirst_tensor::CpuAccess::ReadWrite,
2185 )
2186 .unwrap();
2187 fill_patterned_nv12(&src_dma);
2188 fill_patterned_nv12(&src_mem);
2189
2190 let mut cpu_dst = TensorDyn::image(
2192 w,
2193 h,
2194 PixelFormat::Rgba,
2195 DType::U8,
2196 None,
2197 edgefirst_tensor::CpuAccess::ReadWrite,
2198 )
2199 .unwrap();
2200 CPUProcessor::new()
2201 .convert(&src_mem, &mut cpu_dst, None)
2202 .unwrap();
2203
2204 let mut g2d_dst = TensorDyn::image(
2206 w,
2207 h,
2208 PixelFormat::Rgba,
2209 DType::U8,
2210 Some(TensorMemory::DmaBuf),
2211 edgefirst_tensor::CpuAccess::ReadWrite,
2212 )
2213 .unwrap();
2214 let mut g2d = match G2DProcessor::new() {
2215 Ok(g) => g,
2216 Err(e) => {
2217 crate::test_support::g2d_required_or(false);
2218 crate::test_support::report_skip(&format!(
2219 "d03_nv12_odd_both_g2d_vs_cpu - G2D not available: {e}"
2220 ));
2221 return;
2222 }
2223 };
2224
2225 g2d.convert(&src_dma, &mut g2d_dst, None)
2228 .unwrap_or_else(|e| {
2229 panic!("D-03: G2D rejected odd-both (65×63) NV12 source: {e}");
2230 });
2231
2232 let tol = 4u32;
2233 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
2234 eprintln!("D-03 NV12 odd-both G2D vs CPU: max_diff={max_diff}");
2235 if max_diff > tol {
2240 eprintln!(
2241 "WARNING: D-03 NV12 odd-both G2D vs CPU max_diff={max_diff} > {tol} \
2242 (G2D fixed-point rounding; first bad at {first_fail:?})"
2243 );
2244 }
2245 assert!(
2246 max_diff <= 35,
2247 "D-03: gross NV12 odd-both G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2248 );
2249 }
2250 #[test]
2262 #[cfg(target_os = "linux")]
2263 fn g2d_int8_dst_biases_only_the_pixel_bytes_at_the_declared_stride() {
2264 if !dma_and_g2d_or_skip() {
2265 return;
2266 }
2267 const W: usize = 64;
2268 const H: usize = 16;
2269 const POISON: u8 = 0xAB;
2270 let fmt = PixelFormat::Rgba;
2271 let bpp = fmt.channels();
2272 let row_bytes = W * bpp;
2273 let stride = row_bytes + 64;
2275
2276 let src = TensorDyn::image(
2277 W,
2278 H,
2279 fmt,
2280 DType::U8,
2281 Some(TensorMemory::DmaBuf),
2282 edgefirst_tensor::CpuAccess::ReadWrite,
2283 )
2284 .expect("DMA source");
2285 {
2286 let t = src.as_typed::<u8>().unwrap();
2287 let s = t.effective_row_stride().unwrap();
2288 let mut m = t.map().unwrap();
2289 let buf = m.as_mut_slice();
2290 for y in 0..H {
2291 for i in 0..row_bytes {
2292 buf[y * s + i] = (y * 31 + i * 7) as u8;
2293 }
2294 }
2295 }
2296
2297 let mut u8_dst = TensorDyn::image_with_stride(
2298 W,
2299 H,
2300 fmt,
2301 DType::U8,
2302 stride,
2303 Some(TensorMemory::DmaBuf),
2304 edgefirst_tensor::CpuAccess::ReadWrite,
2305 )
2306 .expect("padded U8 destination");
2307 let mut i8_dst = TensorDyn::image_with_stride(
2308 W,
2309 H,
2310 fmt,
2311 DType::I8,
2312 stride,
2313 Some(TensorMemory::DmaBuf),
2314 edgefirst_tensor::CpuAccess::ReadWrite,
2315 )
2316 .expect("padded I8 destination");
2317 assert_eq!(i8_dst.effective_row_stride(), Some(stride));
2318 u8_dst
2322 .as_typed::<u8>()
2323 .unwrap()
2324 .map()
2325 .unwrap()
2326 .as_mut_slice()
2327 .fill(POISON);
2328 i8_dst
2329 .as_typed::<i8>()
2330 .unwrap()
2331 .map()
2332 .unwrap()
2333 .as_mut_slice()
2334 .fill(POISON as i8);
2335
2336 let mut g2d = G2DProcessor::new().unwrap();
2337 g2d.convert(&src, &mut u8_dst, None).expect("U8 blit");
2338 g2d.convert(&src, &mut i8_dst, None).expect("I8 blit");
2339
2340 let u8_stride = u8_dst.effective_row_stride().unwrap();
2341 let want = u8_dst
2342 .as_typed::<u8>()
2343 .unwrap()
2344 .map()
2345 .unwrap()
2346 .as_slice()
2347 .to_vec();
2348 let got: Vec<u8> = i8_dst
2349 .as_typed::<i8>()
2350 .unwrap()
2351 .map()
2352 .unwrap()
2353 .as_slice()
2354 .iter()
2355 .map(|&v| v as u8)
2356 .collect();
2357 assert!(
2361 got.len() >= H * stride && want.len() >= H * u8_stride,
2362 "a {stride} B pitch over {H} rows needs {} B, mapped {} (I8) and {} (U8)",
2363 H * stride,
2364 got.len(),
2365 want.len()
2366 );
2367 for y in 0..H {
2371 assert!(
2372 want[y * u8_stride + row_bytes..(y + 1) * u8_stride]
2373 .iter()
2374 .all(|&b| b == POISON),
2375 "row {y}: the G2D blit itself wrote past the pixels; the I8 \
2376 assertions below cannot separate that from the bias pass"
2377 );
2378 }
2379 for y in 0..H {
2380 for x in 0..W {
2381 for c in 0..bpp {
2382 let g = got[y * stride + x * bpp + c];
2383 let w = want[y * u8_stride + x * bpp + c];
2384 let expect = if c == bpp - 1 { w } else { w ^ 0x80 };
2385 assert_eq!(g, expect, "pixel ({x},{y}) channel {c}");
2386 }
2387 }
2388 assert!(
2389 got[y * stride + row_bytes..(y + 1) * stride]
2390 .iter()
2391 .all(|&b| b == POISON),
2392 "row {y}: the padding past the pixels was biased"
2393 );
2394 }
2395 }
2396}