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