1#![cfg(target_os = "linux")]
5
6use crate::colorimetry::effective_colorimetry;
7use crate::{CPUProcessor, Crop, 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
40fn pixelfmt_to_fourcc(fmt: PixelFormat) -> FourCharCode {
42 use four_char_code::four_char_code;
43 match fmt {
44 PixelFormat::Rgb => four_char_code!("RGB "),
45 PixelFormat::Rgba => four_char_code!("RGBA"),
46 PixelFormat::Bgra => four_char_code!("BGRA"),
47 PixelFormat::Grey => four_char_code!("Y800"),
48 PixelFormat::Yuyv => four_char_code!("YUYV"),
49 PixelFormat::Vyuy => four_char_code!("VYUY"),
50 PixelFormat::Nv12 => four_char_code!("NV12"),
51 PixelFormat::Nv16 => four_char_code!("NV16"),
52 _ => four_char_code!("RGBA"),
54 }
55}
56
57#[derive(Debug)]
60pub struct G2DProcessor {
61 g2d: G2D,
62 interpolation: crate::Interpolation,
63}
64
65unsafe impl Send for G2DProcessor {}
66unsafe impl Sync for G2DProcessor {}
67
68impl G2DProcessor {
69 pub fn new() -> Result<Self> {
77 let mut g2d = G2D::new("libg2d.so.2")?;
78 g2d.set_bt601_colorspace()?;
84
85 log::debug!("G2DConverter created with version {:?}", g2d.version());
86 Ok(Self {
87 g2d,
88 interpolation: crate::Interpolation::default(),
89 })
90 }
91
92 pub fn set_interpolation(&mut self, mode: crate::Interpolation) {
97 self.interpolation = mode;
98 }
99
100 pub fn version(&self) -> g2d_sys::Version {
103 self.g2d.version()
104 }
105
106 fn convert_impl(
107 &mut self,
108 src_dyn: &TensorDyn,
109 dst_dyn: &mut TensorDyn,
110 rotation: Rotation,
111 flip: Flip,
112 crop: ResolvedCrop,
113 ) -> Result<()> {
114 let _span = tracing::trace_span!(
115 "image.convert.g2d",
116 src_fmt = ?src_dyn.format(),
117 dst_fmt = ?dst_dyn.format(),
118 )
119 .entered();
120 if log::log_enabled!(log::Level::Trace) {
121 log::trace!(
122 "G2D convert: {:?}({:?}/{:?}) → {:?}({:?}/{:?})",
123 src_dyn.format(),
124 src_dyn.dtype(),
125 src_dyn.memory(),
126 dst_dyn.format(),
127 dst_dyn.dtype(),
128 dst_dyn.memory(),
129 );
130 }
131
132 if src_dyn.dtype() != DType::U8 {
133 return Err(Error::NotSupported(
134 "G2D only supports u8 source tensors".to_string(),
135 ));
136 }
137 let is_int8_dst = dst_dyn.dtype() == DType::I8;
138 if dst_dyn.dtype() != DType::U8 && !is_int8_dst {
139 return Err(Error::NotSupported(
140 "G2D only supports u8 or i8 destination tensors".to_string(),
141 ));
142 }
143
144 let src_fmt = src_dyn.format().ok_or(Error::NotAnImage)?;
145 let dst_fmt = dst_dyn.format().ok_or(Error::NotAnImage)?;
146
147 use PixelFormat::*;
149 match (src_fmt, dst_fmt) {
150 (Rgba, Rgba) => {}
151 (Rgba, Yuyv) => {}
152 (Rgba, Rgb) => {}
153 (Yuyv, Rgba) => {}
154 (Yuyv, Yuyv) => {}
155 (Yuyv, Rgb) => {}
156 (Nv12, Rgba) => {}
159 (Nv12, Yuyv) => {}
160 (Nv12, Rgb) => {}
161 (Rgba, Bgra) => {}
162 (Yuyv, Bgra) => {}
163 (Nv12, Bgra) => {}
164 (Bgra, Bgra) => {}
165 (s, d) => {
166 return Err(Error::NotSupported(format!(
167 "G2D does not support {} to {} conversion",
168 s, d
169 )));
170 }
171 }
172
173 let src_is_yuv = src_fmt.is_yuv();
185 let dst_is_yuv = dst_fmt.is_yuv();
186 if src_is_yuv || dst_is_yuv {
187 let cm = if src_is_yuv {
188 effective_colorimetry(src_dyn)
189 } else {
190 effective_colorimetry(dst_dyn)
191 };
192 if !g2d_can_handle(&cm, true) {
193 return Err(Error::NotSupported(format!(
194 "G2D cannot express colorimetry {:?}/{:?} (matrix-only, \
195 no range control, no BT.2020)",
196 cm.encoding, cm.range
197 )));
198 }
199 match cm.encoding {
200 Some(ColorEncoding::Bt601) => self.g2d.set_bt601_colorspace()?,
201 Some(ColorEncoding::Bt709) => self.g2d.set_bt709_colorspace()?,
202 _ => self.g2d.set_bt709_colorspace()?,
205 }
206 }
207
208 let src = src_dyn.as_typed::<u8>().unwrap();
210 let dst = if is_int8_dst {
213 let i8_tensor = dst_dyn.as_typed_mut::<i8>().unwrap();
221 unsafe { &mut *(i8_tensor as *mut Tensor<i8> as *mut Tensor<u8>) }
222 } else {
223 dst_dyn.as_typed_mut::<u8>().unwrap()
224 };
225
226 let mut src_surface = tensor_to_g2d_surface(src)?;
227 let mut dst_surface = tensor_to_g2d_surface(dst)?;
228
229 src_surface.rot = match flip {
230 Flip::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
231 Flip::Vertical => g2d_sys::g2d_rotation_G2D_FLIP_V,
232 Flip::Horizontal => g2d_sys::g2d_rotation_G2D_FLIP_H,
233 };
234
235 dst_surface.rot = match rotation {
236 Rotation::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
237 Rotation::Clockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_90,
238 Rotation::Rotate180 => g2d_sys::g2d_rotation_G2D_ROTATION_180,
239 Rotation::CounterClockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_270,
240 };
241
242 if let Some(crop_rect) = crop.src_rect {
243 src_surface.left = crop_rect.left as i32;
244 src_surface.top = crop_rect.top as i32;
245 src_surface.right = (crop_rect.left + crop_rect.width) as i32;
246 src_surface.bottom = (crop_rect.top + crop_rect.height) as i32;
247 }
248
249 let dst_w = dst.width().unwrap();
250 let dst_h = dst.height().unwrap();
251
252 let needs_clear = crop.dst_color.is_some()
258 && crop.dst_rect.is_some_and(|dst_rect| {
259 dst_rect.left != 0
260 || dst_rect.top != 0
261 || dst_rect.width != dst_w
262 || dst_rect.height != dst_h
263 });
264
265 if needs_clear && dst_fmt != Rgb {
266 if let Some(dst_color) = crop.dst_color {
267 let start = Instant::now();
268 self.g2d.clear(&mut dst_surface, dst_color)?;
269 log::trace!("g2d clear takes {:?}", start.elapsed());
270 }
271 }
272
273 if let Some(crop_rect) = crop.dst_rect {
274 dst_surface.planes[0] += ((crop_rect.top * dst_surface.stride as usize
278 + crop_rect.left)
279 * dst_fmt.channels()) as u64;
280
281 dst_surface.right = crop_rect.width as i32;
282 dst_surface.bottom = crop_rect.height as i32;
283 dst_surface.width = crop_rect.width as i32;
284 dst_surface.height = crop_rect.height as i32;
285 }
286
287 let (sx_src, sy_src) = chroma_subsample_shifts(src_fmt);
301 let (sx_dst, sy_dst) = chroma_subsample_shifts(dst_fmt);
302 let pad_w = (sx_src | sx_dst) > 0;
303 let pad_h = (sy_src | sy_dst) > 0;
304 if pad_w || pad_h {
305 let even = |v: i32| v + (v & 1);
306 if pad_h && even(dst_surface.bottom) != dst_surface.bottom {
307 let page = match unsafe { libc::sysconf(libc::_SC_PAGESIZE) } {
318 n if n > 0 => n as usize,
319 _ => 4096,
320 };
321 let row_bytes = dst_surface.stride as usize * dst_fmt.channels();
322 let mapped = (row_bytes * dst_h).next_multiple_of(page);
323 let needed = row_bytes * even(dst_surface.bottom) as usize;
324 if crop.dst_rect.is_some() || needed > mapped {
325 return Err(Error::NotSupported(format!(
326 "G2D: odd-height {dst_fmt} destination cannot hold the padding \
327 row for the i.MX95 DPU even-dimension workaround; deferring to \
328 GL/CPU"
329 )));
330 }
331 }
332 if pad_w {
333 src_surface.right = even(src_surface.right);
334 src_surface.width = even(src_surface.width);
335 dst_surface.right = even(dst_surface.right);
336 dst_surface.width = even(dst_surface.width);
337 }
338 if pad_h {
339 src_surface.bottom = even(src_surface.bottom);
340 src_surface.height = even(src_surface.height);
341 dst_surface.bottom = even(dst_surface.bottom);
342 dst_surface.height = even(dst_surface.height);
343 }
344 }
345
346 log::trace!("G2D blit: {src_fmt}→{dst_fmt} int8={is_int8_dst}");
347 self.g2d.blit(&src_surface, &dst_surface)?;
348 self.g2d.finish()?;
349 log::trace!("G2D blit complete");
350
351 if needs_clear && dst_fmt == Rgb {
353 if let (Some(dst_color), Some(dst_rect)) = (crop.dst_color, crop.dst_rect) {
354 let start = Instant::now();
355 CPUProcessor::fill_image_outside_crop_u8(dst, dst_color, dst_rect)?;
356 log::trace!("cpu fill takes {:?}", start.elapsed());
357 }
358 }
359
360 if is_int8_dst {
373 let start = Instant::now();
374 crate::cpu::apply_int8_xor_bias_rows(dst, dst_fmt)?;
375 log::trace!("g2d int8 XOR 0x80 post-pass takes {:?}", start.elapsed());
376 }
377
378 Ok(())
379 }
380}
381
382impl ImageProcessorTrait for G2DProcessor {
383 fn convert(
384 &mut self,
385 src: &TensorDyn,
386 dst: &mut TensorDyn,
387 rotation: Rotation,
388 flip: Flip,
389 crop: Crop,
390 ) -> Result<()> {
391 let crop = crop.resolve(
397 src.width().unwrap_or(0),
398 src.height().unwrap_or(0),
399 dst.width().unwrap_or(0),
400 dst.height().unwrap_or(0),
401 )?;
402 if self.interpolation == crate::Interpolation::Nearest
403 && crop.resizes(
404 (src.width().unwrap_or(0), src.height().unwrap_or(0)),
405 (dst.width().unwrap_or(0), dst.height().unwrap_or(0)),
406 rotation,
407 )
408 {
409 return Err(Error::NotSupported(
410 "G2D scales with a fixed filter; Interpolation::Nearest resizes elsewhere".into(),
411 ));
412 }
413 self.convert_impl(src, dst, rotation, flip, crop)
414 }
415
416 fn draw_decoded_masks(
417 &mut self,
418 dst: &mut TensorDyn,
419 detect: &[crate::DetectBox],
420 segmentation: &[crate::Segmentation],
421 overlay: crate::MaskOverlay<'_>,
422 ) -> Result<()> {
423 if !detect.is_empty() || !segmentation.is_empty() {
427 return Err(Error::NotImplemented(
428 "G2D does not support drawing detection or segmentation overlays".to_string(),
429 ));
430 }
431 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
432 }
433
434 fn draw_proto_masks(
435 &mut self,
436 dst: &mut TensorDyn,
437 detect: &[crate::DetectBox],
438 _proto_data: &crate::ProtoData,
439 overlay: crate::MaskOverlay<'_>,
440 ) -> Result<()> {
441 if !detect.is_empty() {
444 return Err(Error::NotImplemented(
445 "G2D does not support drawing detection or segmentation overlays".to_string(),
446 ));
447 }
448 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
449 }
450
451 fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
452 Err(Error::NotImplemented(
453 "G2D does not support setting colors for rendering detection or segmentation overlays"
454 .to_string(),
455 ))
456 }
457}
458
459fn draw_empty_frame_g2d(
472 g2d: &mut G2D,
473 dst_dyn: &mut TensorDyn,
474 background: Option<&TensorDyn>,
475) -> Result<()> {
476 if dst_dyn.dtype() != DType::U8 {
477 return Err(Error::NotSupported(
478 "G2D only supports u8 destination tensors".to_string(),
479 ));
480 }
481 let dst = dst_dyn.as_typed_mut::<u8>().ok_or(Error::NotAnImage)?;
482
483 if TensorTrait::memory(dst) != TensorMemory::DmaBuf {
493 return Err(Error::NotImplemented(
494 "g2d only supports Dma memory".to_string(),
495 ));
496 }
497
498 let mut dst_surface = tensor_to_g2d_surface(dst)?;
499
500 match background {
501 None => {
502 let start = Instant::now();
506 g2d.clear(&mut dst_surface, [0, 0, 0, 0])?;
507 g2d.finish()?;
508 log::trace!("g2d clear (empty frame) takes {:?}", start.elapsed());
509 }
510 Some(bg_dyn) => {
511 if bg_dyn.shape() != dst.shape() {
516 return Err(Error::InvalidShape(
517 "background shape does not match dst".into(),
518 ));
519 }
520 if bg_dyn.format() != dst.format() {
521 return Err(Error::InvalidShape(
522 "background pixel format does not match dst".into(),
523 ));
524 }
525 if bg_dyn.dtype() != DType::U8 {
526 return Err(Error::NotSupported(
527 "G2D only supports u8 background tensors".to_string(),
528 ));
529 }
530 let bg = bg_dyn.as_typed::<u8>().ok_or(Error::NotAnImage)?;
531 if TensorTrait::memory(bg) != TensorMemory::DmaBuf {
533 return Err(Error::NotImplemented(
534 "g2d background must be Dma-backed".to_string(),
535 ));
536 }
537 let src_surface = tensor_to_g2d_surface(bg)?;
538 let start = Instant::now();
539 g2d.blit(&src_surface, &dst_surface)?;
540 g2d.finish()?;
541 log::trace!("g2d blit (bg→dst) takes {:?}", start.elapsed());
542 }
543 }
544 Ok(())
545}
546
547fn chroma_subsample_shifts(fmt: PixelFormat) -> (u32, u32) {
551 match fmt.chroma_layout() {
552 Some(cl) => (cl.shift_x, cl.shift_y),
553 None if matches!(fmt, PixelFormat::Yuyv | PixelFormat::Vyuy) => (1, 0),
556 None => (0, 0),
557 }
558}
559
560fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
564 let fmt = img.format().ok_or(Error::NotAnImage)?;
565 let fd = img
568 .dmabuf()
569 .map_err(|e| Error::NotImplemented(format!("g2d only supports Dma memory ({e})")))?;
570 let phys: G2DPhysical = fd.as_raw_fd().try_into()?;
571
572 let base_addr = phys.address();
579 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
580 let planes = if fmt == PixelFormat::Nv12 {
581 if img.is_multiplane() {
582 let chroma = img.chroma().unwrap();
584 let chroma_fd = chroma.dmabuf().map_err(|e| {
585 Error::NotImplemented(format!("g2d multiplane chroma must be DMA-backed ({e})"))
586 })?;
587 let uv_phys: G2DPhysical = chroma_fd.as_raw_fd().try_into()?;
588 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
589 [
590 base_addr + luma_offset,
591 uv_phys.address() + chroma_offset,
592 0,
593 ]
594 } else {
595 let w = img.width().unwrap();
596 let h = img.height().unwrap();
597 let stride = img.effective_row_stride().unwrap_or(w.next_multiple_of(2));
598 let uv_offset = (luma_offset as usize + stride * h) as u64;
599 [base_addr + luma_offset, base_addr + uv_offset, 0]
600 }
601 } else {
602 [base_addr + luma_offset, 0, 0]
603 };
604
605 let w = img.width().unwrap();
606 let h = img.height().unwrap();
607 let fourcc = pixelfmt_to_fourcc(fmt);
608
609 let stride_pixels = match img.effective_row_stride() {
612 Some(s) => {
613 let channels = fmt.channels();
614 if s % channels != 0 {
615 return Err(Error::NotImplemented(
616 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
617 ));
618 }
619 s / channels
620 }
621 None => w,
622 };
623
624 Ok(G2DSurface {
625 planes,
626 format: G2DFormat::try_from(fourcc)?.format(),
627 left: 0,
628 top: 0,
629 right: w as i32,
630 bottom: h as i32,
631 stride: stride_pixels as i32,
632 width: w as i32,
633 height: h as i32,
634 blendfunc: 0,
635 clrcolor: 0,
636 rot: 0,
637 global_alpha: 0,
638 })
639}
640
641#[cfg(test)]
642mod g2d_predicate_tests {
643 use super::*;
644 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry};
645
646 #[test]
647 fn g2d_declines_full_range_and_bt2020_yuv() {
648 let full = Colorimetry::default()
649 .with_range(ColorRange::Full)
650 .with_encoding(ColorEncoding::Bt709);
651 let lim = Colorimetry::default()
652 .with_range(ColorRange::Limited)
653 .with_encoding(ColorEncoding::Bt709);
654 let bt2020 = Colorimetry::default()
655 .with_range(ColorRange::Limited)
656 .with_encoding(ColorEncoding::Bt2020);
657 assert!(!g2d_can_handle(&full, true)); assert!(g2d_can_handle(&lim, true)); assert!(!g2d_can_handle(&bt2020, true)); assert!(g2d_can_handle(&full, false)); }
662}
663
664#[cfg(feature = "g2d_test_formats")]
665#[cfg(test)]
666#[allow(deprecated)]
667mod g2d_tests {
668 use super::*;
669 use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rotation};
670 use edgefirst_tensor::{
671 is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
672 };
673 use image::buffer::ConvertBuffer;
674
675 #[cfg(target_os = "linux")]
682 fn is_g2d_available() -> bool {
683 G2DProcessor::new().is_ok()
684 }
685
686 #[test]
687 #[cfg(target_os = "linux")]
688 fn test_g2d_formats_no_resize() {
689 for i in [
690 PixelFormat::Rgba,
691 PixelFormat::Yuyv,
692 PixelFormat::Rgb,
693 PixelFormat::Grey,
694 PixelFormat::Nv12,
695 ] {
696 for o in [
697 PixelFormat::Rgba,
698 PixelFormat::Yuyv,
699 PixelFormat::Rgb,
700 PixelFormat::Grey,
701 ] {
702 let res = test_g2d_format_no_resize_(i, o);
703 if let Err(e) = res {
704 println!("{i} to {o} failed: {e:?}");
705 } else {
706 println!("{i} to {o} success");
707 }
708 }
709 }
710 }
711
712 fn test_g2d_format_no_resize_(
713 g2d_in_fmt: PixelFormat,
714 g2d_out_fmt: PixelFormat,
715 ) -> Result<(), crate::Error> {
716 let dst_width = 1280;
717 let dst_height = 720;
718 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
719 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
720
721 let mut src2 = TensorDyn::image(
723 1280,
724 720,
725 g2d_in_fmt,
726 DType::U8,
727 Some(TensorMemory::DmaBuf),
728 edgefirst_tensor::CpuAccess::ReadWrite,
729 )?;
730
731 let mut cpu_converter = CPUProcessor::new();
732
733 if g2d_in_fmt == PixelFormat::Nv12 {
735 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
736 src2.as_typed::<u8>()
737 .unwrap()
738 .map()?
739 .as_mut_slice()
740 .copy_from_slice(&nv12_bytes);
741 } else {
742 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
743 }
744
745 let mut g2d_dst = TensorDyn::image(
746 dst_width,
747 dst_height,
748 g2d_out_fmt,
749 DType::U8,
750 Some(TensorMemory::DmaBuf),
751 edgefirst_tensor::CpuAccess::ReadWrite,
752 )?;
753 let mut g2d_converter = G2DProcessor::new()?;
754 let src2_dyn = src2;
755 let mut g2d_dst_dyn = g2d_dst;
756 g2d_converter.convert(
757 &src2_dyn,
758 &mut g2d_dst_dyn,
759 Rotation::None,
760 Flip::None,
761 Crop::no_crop(),
762 )?;
763 g2d_dst = {
764 let mut __t = g2d_dst_dyn.into_u8().unwrap();
765 __t.set_format(g2d_out_fmt)
766 .map_err(|e| crate::Error::Internal(e.to_string()))?;
767 TensorDyn::from(__t)
768 };
769
770 let mut cpu_dst = TensorDyn::image(
771 dst_width,
772 dst_height,
773 PixelFormat::Rgb,
774 DType::U8,
775 None,
776 edgefirst_tensor::CpuAccess::ReadWrite,
777 )?;
778 cpu_converter.convert(
779 &g2d_dst,
780 &mut cpu_dst,
781 Rotation::None,
782 Flip::None,
783 Crop::no_crop(),
784 )?;
785
786 compare_images(
787 &src,
788 &cpu_dst,
789 0.98,
790 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
791 )
792 }
793
794 #[test]
795 #[cfg(target_os = "linux")]
796 fn test_g2d_formats_with_resize() {
797 for i in [
798 PixelFormat::Rgba,
799 PixelFormat::Yuyv,
800 PixelFormat::Rgb,
801 PixelFormat::Grey,
802 PixelFormat::Nv12,
803 ] {
804 for o in [
805 PixelFormat::Rgba,
806 PixelFormat::Yuyv,
807 PixelFormat::Rgb,
808 PixelFormat::Grey,
809 ] {
810 let res = test_g2d_format_with_resize_(i, o);
811 if let Err(e) = res {
812 println!("{i} to {o} failed: {e:?}");
813 } else {
814 println!("{i} to {o} success");
815 }
816 }
817 }
818 }
819
820 #[test]
821 #[cfg(target_os = "linux")]
822 fn test_g2d_formats_with_resize_dst_crop() {
823 for i in [
824 PixelFormat::Rgba,
825 PixelFormat::Yuyv,
826 PixelFormat::Rgb,
827 PixelFormat::Grey,
828 PixelFormat::Nv12,
829 ] {
830 for o in [
831 PixelFormat::Rgba,
832 PixelFormat::Yuyv,
833 PixelFormat::Rgb,
834 PixelFormat::Grey,
835 ] {
836 let res = test_g2d_format_with_resize_dst_crop(i, o);
837 if let Err(e) = res {
838 println!("{i} to {o} failed: {e:?}");
839 } else {
840 println!("{i} to {o} success");
841 }
842 }
843 }
844 }
845
846 fn test_g2d_format_with_resize_(
847 g2d_in_fmt: PixelFormat,
848 g2d_out_fmt: PixelFormat,
849 ) -> Result<(), crate::Error> {
850 let dst_width = 600;
851 let dst_height = 400;
852 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
853 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
854
855 let mut cpu_converter = CPUProcessor::new();
856
857 let mut reference = TensorDyn::image(
858 dst_width,
859 dst_height,
860 PixelFormat::Rgb,
861 DType::U8,
862 Some(TensorMemory::DmaBuf),
863 edgefirst_tensor::CpuAccess::ReadWrite,
864 )?;
865 cpu_converter.convert(
866 &src,
867 &mut reference,
868 Rotation::None,
869 Flip::None,
870 Crop::no_crop(),
871 )?;
872
873 let mut src2 = TensorDyn::image(
875 1280,
876 720,
877 g2d_in_fmt,
878 DType::U8,
879 Some(TensorMemory::DmaBuf),
880 edgefirst_tensor::CpuAccess::ReadWrite,
881 )?;
882
883 if g2d_in_fmt == PixelFormat::Nv12 {
885 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
886 src2.as_typed::<u8>()
887 .unwrap()
888 .map()?
889 .as_mut_slice()
890 .copy_from_slice(&nv12_bytes);
891 } else {
892 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
893 }
894
895 let mut g2d_dst = TensorDyn::image(
896 dst_width,
897 dst_height,
898 g2d_out_fmt,
899 DType::U8,
900 Some(TensorMemory::DmaBuf),
901 edgefirst_tensor::CpuAccess::ReadWrite,
902 )?;
903 let mut g2d_converter = G2DProcessor::new()?;
904 let src2_dyn = src2;
905 let mut g2d_dst_dyn = g2d_dst;
906 g2d_converter.convert(
907 &src2_dyn,
908 &mut g2d_dst_dyn,
909 Rotation::None,
910 Flip::None,
911 Crop::no_crop(),
912 )?;
913 g2d_dst = {
914 let mut __t = g2d_dst_dyn.into_u8().unwrap();
915 __t.set_format(g2d_out_fmt)
916 .map_err(|e| crate::Error::Internal(e.to_string()))?;
917 TensorDyn::from(__t)
918 };
919
920 let mut cpu_dst = TensorDyn::image(
921 dst_width,
922 dst_height,
923 PixelFormat::Rgb,
924 DType::U8,
925 None,
926 edgefirst_tensor::CpuAccess::ReadWrite,
927 )?;
928 cpu_converter.convert(
929 &g2d_dst,
930 &mut cpu_dst,
931 Rotation::None,
932 Flip::None,
933 Crop::no_crop(),
934 )?;
935
936 compare_images(
937 &reference,
938 &cpu_dst,
939 0.98,
940 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
941 )
942 }
943
944 fn test_g2d_format_with_resize_dst_crop(
945 g2d_in_fmt: PixelFormat,
946 g2d_out_fmt: PixelFormat,
947 ) -> Result<(), crate::Error> {
948 let dst_width = 600;
949 let dst_height = 400;
950 let region = crate::Region::new(100, 100, 200, 100);
955 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
956 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
957
958 let mut cpu_converter = CPUProcessor::new();
959
960 let reference = TensorDyn::image(
961 dst_width,
962 dst_height,
963 PixelFormat::Rgb,
964 DType::U8,
965 Some(TensorMemory::DmaBuf),
966 edgefirst_tensor::CpuAccess::ReadWrite,
967 )?;
968 reference
969 .as_typed::<u8>()
970 .unwrap()
971 .map()
972 .unwrap()
973 .as_mut_slice()
974 .fill(128);
975 cpu_converter.convert(
976 &src,
977 &mut reference.view(region)?,
978 Rotation::None,
979 Flip::None,
980 Crop::no_crop(),
981 )?;
982
983 let mut src2 = TensorDyn::image(
985 1280,
986 720,
987 g2d_in_fmt,
988 DType::U8,
989 Some(TensorMemory::DmaBuf),
990 edgefirst_tensor::CpuAccess::ReadWrite,
991 )?;
992
993 if g2d_in_fmt == PixelFormat::Nv12 {
995 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
996 src2.as_typed::<u8>()
997 .unwrap()
998 .map()?
999 .as_mut_slice()
1000 .copy_from_slice(&nv12_bytes);
1001 } else {
1002 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1003 }
1004
1005 let mut g2d_dst = TensorDyn::image(
1006 dst_width,
1007 dst_height,
1008 g2d_out_fmt,
1009 DType::U8,
1010 Some(TensorMemory::DmaBuf),
1011 edgefirst_tensor::CpuAccess::ReadWrite,
1012 )?;
1013 g2d_dst
1014 .as_typed::<u8>()
1015 .unwrap()
1016 .map()
1017 .unwrap()
1018 .as_mut_slice()
1019 .fill(128);
1020 let mut g2d_converter = G2DProcessor::new()?;
1021 let src2_dyn = src2;
1022 let g2d_dst_dyn = g2d_dst;
1023 g2d_converter.convert(
1024 &src2_dyn,
1025 &mut g2d_dst_dyn.view(region)?,
1026 Rotation::None,
1027 Flip::None,
1028 Crop::no_crop(),
1029 )?;
1030 g2d_dst = {
1031 let mut __t = g2d_dst_dyn.into_u8().unwrap();
1032 __t.set_format(g2d_out_fmt)
1033 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1034 TensorDyn::from(__t)
1035 };
1036
1037 let mut cpu_dst = TensorDyn::image(
1038 dst_width,
1039 dst_height,
1040 PixelFormat::Rgb,
1041 DType::U8,
1042 None,
1043 edgefirst_tensor::CpuAccess::ReadWrite,
1044 )?;
1045 cpu_converter.convert(
1046 &g2d_dst,
1047 &mut cpu_dst,
1048 Rotation::None,
1049 Flip::None,
1050 Crop::no_crop(),
1051 )?;
1052
1053 compare_images(
1054 &reference,
1055 &cpu_dst,
1056 0.98,
1057 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
1058 )
1059 }
1060
1061 fn compare_images(
1062 img1: &TensorDyn,
1063 img2: &TensorDyn,
1064 threshold: f64,
1065 name: &str,
1066 ) -> Result<(), crate::Error> {
1067 assert_eq!(img1.height(), img2.height(), "Heights differ");
1068 assert_eq!(img1.width(), img2.width(), "Widths differ");
1069 assert_eq!(
1070 img1.format().unwrap(),
1071 img2.format().unwrap(),
1072 "PixelFormat differ"
1073 );
1074 assert!(
1075 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
1076 "format must be Rgb or Rgba for comparison"
1077 );
1078 let image1 = match img1.format().unwrap() {
1079 PixelFormat::Rgb => image::RgbImage::from_vec(
1080 img1.width().unwrap() as u32,
1081 img1.height().unwrap() as u32,
1082 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1083 )
1084 .unwrap(),
1085 PixelFormat::Rgba => image::RgbaImage::from_vec(
1086 img1.width().unwrap() as u32,
1087 img1.height().unwrap() as u32,
1088 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1089 )
1090 .unwrap()
1091 .convert(),
1092
1093 _ => unreachable!(),
1094 };
1095
1096 let image2 = match img2.format().unwrap() {
1097 PixelFormat::Rgb => image::RgbImage::from_vec(
1098 img2.width().unwrap() as u32,
1099 img2.height().unwrap() as u32,
1100 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1101 )
1102 .unwrap(),
1103 PixelFormat::Rgba => image::RgbaImage::from_vec(
1104 img2.width().unwrap() as u32,
1105 img2.height().unwrap() as u32,
1106 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1107 )
1108 .unwrap()
1109 .convert(),
1110
1111 _ => unreachable!(),
1112 };
1113
1114 let similarity = image_compare::rgb_similarity_structure(
1115 &image_compare::Algorithm::RootMeanSquared,
1116 &image1,
1117 &image2,
1118 )
1119 .expect("Image Comparison failed");
1120
1121 if similarity.score < threshold {
1122 image1.save(format!("{name}_1.png")).unwrap();
1123 image2.save(format!("{name}_2.png")).unwrap();
1124 return Err(Error::Internal(format!(
1125 "{name}: converted image and target image have similarity score too low: {} < {}",
1126 similarity.score, threshold
1127 )));
1128 }
1129 Ok(())
1130 }
1131
1132 fn load_raw_image(
1138 width: usize,
1139 height: usize,
1140 format: PixelFormat,
1141 memory: Option<TensorMemory>,
1142 bytes: &[u8],
1143 ) -> Result<TensorDyn, crate::Error> {
1144 let img = TensorDyn::image(
1145 width,
1146 height,
1147 format,
1148 DType::U8,
1149 memory,
1150 edgefirst_tensor::CpuAccess::ReadWrite,
1151 )?;
1152 let mut map = img.as_typed::<u8>().unwrap().map()?;
1153 let dst = map.as_mut_slice();
1154 if bytes.len() > dst.len() {
1155 return Err(crate::Error::InvalidShape(format!(
1156 "load_raw_image: {} input bytes exceed {}-byte image buffer",
1157 bytes.len(),
1158 dst.len()
1159 )));
1160 }
1161 dst[..bytes.len()].copy_from_slice(bytes);
1162 Ok(img)
1163 }
1164
1165 #[test]
1167 #[cfg(target_os = "linux")]
1168 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
1169 if !is_dma_available() || !is_g2d_available() {
1170 return Ok(());
1171 }
1172 let src = load_raw_image(
1174 1280,
1175 720,
1176 PixelFormat::Nv12,
1177 Some(TensorMemory::DmaBuf),
1178 &edgefirst_bench::testdata::read("camera720p.nv12"),
1179 )?;
1180
1181 let reference = load_raw_image(
1183 1280,
1184 720,
1185 PixelFormat::Rgba,
1186 None,
1187 &edgefirst_bench::testdata::read("camera720p.rgba"),
1188 )?;
1189
1190 let mut dst = TensorDyn::image(
1192 1280,
1193 720,
1194 PixelFormat::Rgba,
1195 DType::U8,
1196 Some(TensorMemory::DmaBuf),
1197 edgefirst_tensor::CpuAccess::ReadWrite,
1198 )?;
1199 let mut g2d = G2DProcessor::new()?;
1200 let src_dyn = src;
1201 let mut dst_dyn = dst;
1202 g2d.convert(
1203 &src_dyn,
1204 &mut dst_dyn,
1205 Rotation::None,
1206 Flip::None,
1207 Crop::no_crop(),
1208 )?;
1209 dst = {
1210 let mut __t = dst_dyn.into_u8().unwrap();
1211 __t.set_format(PixelFormat::Rgba)
1212 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1213 TensorDyn::from(__t)
1214 };
1215
1216 let cpu_dst = TensorDyn::image(
1218 1280,
1219 720,
1220 PixelFormat::Rgba,
1221 DType::U8,
1222 None,
1223 edgefirst_tensor::CpuAccess::ReadWrite,
1224 )?;
1225 cpu_dst
1226 .as_typed::<u8>()
1227 .unwrap()
1228 .map()?
1229 .as_mut_slice()
1230 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1231
1232 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
1233 }
1234
1235 #[test]
1237 #[cfg(target_os = "linux")]
1238 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
1239 if !is_dma_available() || !is_g2d_available() {
1240 return Ok(());
1241 }
1242 let src = load_raw_image(
1244 1280,
1245 720,
1246 PixelFormat::Nv12,
1247 Some(TensorMemory::DmaBuf),
1248 &edgefirst_bench::testdata::read("camera720p.nv12"),
1249 )?;
1250
1251 let reference = load_raw_image(
1253 1280,
1254 720,
1255 PixelFormat::Rgb,
1256 None,
1257 &edgefirst_bench::testdata::read("camera720p.rgb"),
1258 )?;
1259
1260 let mut dst = TensorDyn::image(
1262 1280,
1263 720,
1264 PixelFormat::Rgb,
1265 DType::U8,
1266 Some(TensorMemory::DmaBuf),
1267 edgefirst_tensor::CpuAccess::ReadWrite,
1268 )?;
1269 let mut g2d = G2DProcessor::new()?;
1270 let src_dyn = src;
1271 let mut dst_dyn = dst;
1272 g2d.convert(
1273 &src_dyn,
1274 &mut dst_dyn,
1275 Rotation::None,
1276 Flip::None,
1277 Crop::no_crop(),
1278 )?;
1279 dst = {
1280 let mut __t = dst_dyn.into_u8().unwrap();
1281 __t.set_format(PixelFormat::Rgb)
1282 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1283 TensorDyn::from(__t)
1284 };
1285
1286 let cpu_dst = TensorDyn::image(
1288 1280,
1289 720,
1290 PixelFormat::Rgb,
1291 DType::U8,
1292 None,
1293 edgefirst_tensor::CpuAccess::ReadWrite,
1294 )?;
1295 cpu_dst
1296 .as_typed::<u8>()
1297 .unwrap()
1298 .map()?
1299 .as_mut_slice()
1300 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1301
1302 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1303 }
1304
1305 #[test]
1307 #[cfg(target_os = "linux")]
1308 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1309 if !is_dma_available() || !is_g2d_available() {
1310 return Ok(());
1311 }
1312 let src = load_raw_image(
1314 1280,
1315 720,
1316 PixelFormat::Yuyv,
1317 Some(TensorMemory::DmaBuf),
1318 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1319 )?;
1320
1321 let reference = load_raw_image(
1323 1280,
1324 720,
1325 PixelFormat::Rgba,
1326 None,
1327 &edgefirst_bench::testdata::read("camera720p.rgba"),
1328 )?;
1329
1330 let mut dst = TensorDyn::image(
1332 1280,
1333 720,
1334 PixelFormat::Rgba,
1335 DType::U8,
1336 Some(TensorMemory::DmaBuf),
1337 edgefirst_tensor::CpuAccess::ReadWrite,
1338 )?;
1339 let mut g2d = G2DProcessor::new()?;
1340 let src_dyn = src;
1341 let mut dst_dyn = dst;
1342 g2d.convert(
1343 &src_dyn,
1344 &mut dst_dyn,
1345 Rotation::None,
1346 Flip::None,
1347 Crop::no_crop(),
1348 )?;
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_yuyv_to_rgba_reference")
1373 }
1374
1375 #[test]
1377 #[cfg(target_os = "linux")]
1378 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1379 if !is_dma_available() || !is_g2d_available() {
1380 return Ok(());
1381 }
1382 let src = load_raw_image(
1384 1280,
1385 720,
1386 PixelFormat::Yuyv,
1387 Some(TensorMemory::DmaBuf),
1388 &edgefirst_bench::testdata::read("camera720p.yuyv"),
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(
1413 &src_dyn,
1414 &mut dst_dyn,
1415 Rotation::None,
1416 Flip::None,
1417 Crop::no_crop(),
1418 )?;
1419 dst = {
1420 let mut __t = dst_dyn.into_u8().unwrap();
1421 __t.set_format(PixelFormat::Rgb)
1422 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1423 TensorDyn::from(__t)
1424 };
1425
1426 let cpu_dst = TensorDyn::image(
1428 1280,
1429 720,
1430 PixelFormat::Rgb,
1431 DType::U8,
1432 None,
1433 edgefirst_tensor::CpuAccess::ReadWrite,
1434 )?;
1435 cpu_dst
1436 .as_typed::<u8>()
1437 .unwrap()
1438 .map()?
1439 .as_mut_slice()
1440 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1441
1442 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1443 }
1444
1445 #[test]
1448 #[cfg(target_os = "linux")]
1449 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1450 fn test_g2d_bgra_no_resize() {
1451 for src_fmt in [
1452 PixelFormat::Rgba,
1453 PixelFormat::Yuyv,
1454 PixelFormat::Nv12,
1455 PixelFormat::Bgra,
1456 ] {
1457 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1458 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1459 });
1460 }
1461 }
1462
1463 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1464 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1465 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1466
1467 let mut src2 = TensorDyn::image(
1469 1280,
1470 720,
1471 g2d_in_fmt,
1472 DType::U8,
1473 Some(TensorMemory::DmaBuf),
1474 edgefirst_tensor::CpuAccess::ReadWrite,
1475 )?;
1476 let mut cpu_converter = CPUProcessor::new();
1477
1478 if g2d_in_fmt == PixelFormat::Nv12 {
1479 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1480 src2.as_typed::<u8>()
1481 .unwrap()
1482 .map()?
1483 .as_mut_slice()
1484 .copy_from_slice(&nv12_bytes);
1485 } else {
1486 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1487 }
1488
1489 let mut g2d = G2DProcessor::new()?;
1490
1491 let mut bgra_dst = TensorDyn::image(
1493 1280,
1494 720,
1495 PixelFormat::Bgra,
1496 DType::U8,
1497 Some(TensorMemory::DmaBuf),
1498 edgefirst_tensor::CpuAccess::ReadWrite,
1499 )?;
1500 let src2_dyn = src2;
1501 let mut bgra_dst_dyn = bgra_dst;
1502 g2d.convert(
1503 &src2_dyn,
1504 &mut bgra_dst_dyn,
1505 Rotation::None,
1506 Flip::None,
1507 Crop::no_crop(),
1508 )?;
1509 bgra_dst = {
1510 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1511 __t.set_format(PixelFormat::Bgra)
1512 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1513 TensorDyn::from(__t)
1514 };
1515
1516 let src2 = {
1518 let mut __t = src2_dyn.into_u8().unwrap();
1519 __t.set_format(g2d_in_fmt)
1520 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1521 TensorDyn::from(__t)
1522 };
1523
1524 let mut rgba_dst = TensorDyn::image(
1526 1280,
1527 720,
1528 PixelFormat::Rgba,
1529 DType::U8,
1530 Some(TensorMemory::DmaBuf),
1531 edgefirst_tensor::CpuAccess::ReadWrite,
1532 )?;
1533 let src2_dyn2 = src2;
1534 let mut rgba_dst_dyn = rgba_dst;
1535 g2d.convert(
1536 &src2_dyn2,
1537 &mut rgba_dst_dyn,
1538 Rotation::None,
1539 Flip::None,
1540 Crop::no_crop(),
1541 )?;
1542 rgba_dst = {
1543 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1544 __t.set_format(PixelFormat::Rgba)
1545 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1546 TensorDyn::from(__t)
1547 };
1548
1549 let bgra_cpu = TensorDyn::image(
1551 1280,
1552 720,
1553 PixelFormat::Bgra,
1554 DType::U8,
1555 None,
1556 edgefirst_tensor::CpuAccess::ReadWrite,
1557 )?;
1558 bgra_cpu
1559 .as_typed::<u8>()
1560 .unwrap()
1561 .map()?
1562 .as_mut_slice()
1563 .copy_from_slice(bgra_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1564
1565 let rgba_cpu = TensorDyn::image(
1566 1280,
1567 720,
1568 PixelFormat::Rgba,
1569 DType::U8,
1570 None,
1571 edgefirst_tensor::CpuAccess::ReadWrite,
1572 )?;
1573 rgba_cpu
1574 .as_typed::<u8>()
1575 .unwrap()
1576 .map()?
1577 .as_mut_slice()
1578 .copy_from_slice(rgba_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1579
1580 let bgra_map = bgra_cpu.as_typed::<u8>().unwrap().map()?;
1582 let rgba_map = rgba_cpu.as_typed::<u8>().unwrap().map()?;
1583 let bgra_buf = bgra_map.as_slice();
1584 let rgba_buf = rgba_map.as_slice();
1585
1586 assert_eq!(bgra_buf.len(), rgba_buf.len());
1587 for (i, (bc, rc)) in bgra_buf
1588 .as_chunks::<4>()
1589 .0
1590 .iter()
1591 .zip(rgba_buf.as_chunks::<4>().0)
1592 .enumerate()
1593 {
1594 assert_eq!(
1595 bc[0], rc[2],
1596 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1597 );
1598 assert_eq!(
1599 bc[1], rc[1],
1600 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1601 );
1602 assert_eq!(
1603 bc[2], rc[0],
1604 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1605 );
1606 assert_eq!(
1607 bc[3], rc[3],
1608 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1609 );
1610 }
1611 Ok(())
1612 }
1613
1614 fn surface_for(
1625 width: usize,
1626 height: usize,
1627 fmt: PixelFormat,
1628 offset: Option<usize>,
1629 row_stride: Option<usize>,
1630 ) -> Result<G2DSurface, crate::Error> {
1631 use edgefirst_tensor::TensorMemory;
1632 let mut t = Tensor::<u8>::image(
1633 width,
1634 height,
1635 fmt,
1636 Some(TensorMemory::DmaBuf),
1637 edgefirst_tensor::CpuAccess::ReadWrite,
1638 )?;
1639 if let Some(o) = offset {
1640 t.set_plane_offset(o);
1641 }
1642 if let Some(s) = row_stride {
1643 t.set_row_stride_unchecked(s);
1644 }
1645 tensor_to_g2d_surface(&t)
1646 }
1647
1648 #[test]
1649 fn g2d_surface_single_plane_no_offset() {
1650 if !is_dma_available() || !is_g2d_available() {
1651 return;
1652 }
1653 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1654 assert_ne!(s.planes[0], 0);
1656 assert_eq!(s.stride, 640);
1657 }
1658
1659 #[test]
1660 fn g2d_surface_single_plane_with_offset() {
1661 if !is_dma_available() || !is_g2d_available() {
1662 return;
1663 }
1664 use edgefirst_tensor::TensorMemory;
1665 let mut t = Tensor::<u8>::image(
1666 640,
1667 480,
1668 PixelFormat::Rgba,
1669 Some(TensorMemory::DmaBuf),
1670 edgefirst_tensor::CpuAccess::ReadWrite,
1671 )
1672 .unwrap();
1673 let s0 = tensor_to_g2d_surface(&t).unwrap();
1674 t.set_plane_offset(4096);
1675 let s1 = tensor_to_g2d_surface(&t).unwrap();
1676 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1677 }
1678
1679 #[test]
1680 fn g2d_surface_single_plane_zero_offset() {
1681 if !is_dma_available() || !is_g2d_available() {
1682 return;
1683 }
1684 use edgefirst_tensor::TensorMemory;
1685 let mut t = Tensor::<u8>::image(
1686 640,
1687 480,
1688 PixelFormat::Rgba,
1689 Some(TensorMemory::DmaBuf),
1690 edgefirst_tensor::CpuAccess::ReadWrite,
1691 )
1692 .unwrap();
1693 let s_none = tensor_to_g2d_surface(&t).unwrap();
1694 t.set_plane_offset(0);
1695 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1696 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1698 }
1699
1700 #[test]
1701 fn g2d_surface_stride_rgba() {
1702 if !is_dma_available() || !is_g2d_available() {
1703 return;
1704 }
1705 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1707 assert_eq!(s_default.stride, 640);
1708
1709 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1711 assert_eq!(s_custom.stride, 704);
1712 }
1713
1714 #[test]
1715 fn g2d_surface_stride_rgb() {
1716 if !is_dma_available() || !is_g2d_available() {
1717 return;
1718 }
1719 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1720 assert_eq!(s_default.stride, 640);
1721
1722 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1724 assert_eq!(s_custom.stride, 660);
1725 }
1726
1727 #[test]
1728 fn g2d_surface_stride_grey() {
1729 if !is_dma_available() || !is_g2d_available() {
1730 return;
1731 }
1732 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1734 Ok(s) => s,
1735 Err(crate::Error::G2D(..)) => return,
1736 Err(e) => panic!("unexpected error: {e:?}"),
1737 };
1738 assert_eq!(s.stride, 1024);
1740 }
1741
1742 #[test]
1743 fn g2d_surface_contiguous_nv12_offset() {
1744 if !is_dma_available() || !is_g2d_available() {
1745 return;
1746 }
1747 use edgefirst_tensor::TensorMemory;
1748 let mut t = Tensor::<u8>::image(
1749 640,
1750 480,
1751 PixelFormat::Nv12,
1752 Some(TensorMemory::DmaBuf),
1753 edgefirst_tensor::CpuAccess::ReadWrite,
1754 )
1755 .unwrap();
1756 let s0 = tensor_to_g2d_surface(&t).unwrap();
1757
1758 t.set_plane_offset(8192);
1759 let s1 = tensor_to_g2d_surface(&t).unwrap();
1760
1761 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1763 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1767 }
1768
1769 #[test]
1770 fn g2d_surface_contiguous_nv12_stride() {
1771 if !is_dma_available() || !is_g2d_available() {
1772 return;
1773 }
1774 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1776 assert_eq!(s.stride, 640);
1777
1778 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1780 assert_eq!(s_padded.stride, 1024);
1781 }
1782
1783 #[test]
1784 fn g2d_surface_multiplane_nv12_offset() {
1785 if !is_dma_available() || !is_g2d_available() {
1786 return;
1787 }
1788 use edgefirst_tensor::TensorMemory;
1789
1790 let mut luma =
1792 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::DmaBuf), Some("luma")).unwrap();
1793 let mut chroma =
1794 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::DmaBuf), Some("chroma")).unwrap();
1795
1796 let luma_base = {
1798 let dma = luma.as_dma().unwrap();
1799 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1800 phys.address()
1801 };
1802 let chroma_base = {
1803 let dma = chroma.as_dma().unwrap();
1804 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1805 phys.address()
1806 };
1807
1808 luma.set_plane_offset(4096);
1810 chroma.set_plane_offset(2048);
1811 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1812 let s = tensor_to_g2d_surface(&combined).unwrap();
1813
1814 assert_eq!(s.planes[0], luma_base + 4096);
1816 assert_eq!(s.planes[1], chroma_base + 2048);
1818 }
1819
1820 #[cfg(target_os = "linux")]
1848 fn fill_patterned_nv12(t: &TensorDyn) {
1849 let w = t.width().unwrap();
1850 let h = t.height().unwrap();
1851 let stride = t.effective_row_stride().unwrap();
1852
1853 let chroma_h = h.div_ceil(2);
1855 let chroma_w = w.div_ceil(2);
1856
1857 let bound = t.as_typed::<u8>().unwrap();
1858 let mut m = bound.map().unwrap();
1859 let buf = m.as_mut_slice();
1860 let uv_start = stride * h;
1861
1862 for r in 0..h {
1864 for c in 0..w {
1865 buf[r * stride + c] = ((r * 3 + c * 5) % 256) as u8;
1866 }
1867 }
1868 for cr_row in 0..chroma_h {
1870 for cc in 0..chroma_w {
1871 let cb_val = ((cc * 7 + cr_row * 11 + 40) % 256) as u8;
1872 let cr_val = ((cc * 13 + cr_row * 3 + 80) % 256) as u8;
1873 let uv_byte = uv_start + cr_row * stride + cc * 2;
1874 buf[uv_byte] = cb_val;
1875 buf[uv_byte + 1] = cr_val;
1876 }
1877 }
1878 }
1879
1880 #[cfg(target_os = "linux")]
1885 fn compare_g2d_vs_cpu_rgba(
1886 g2d_dst: &TensorDyn,
1887 cpu_dst: &TensorDyn,
1888 w: usize,
1889 h: usize,
1890 tol: u32,
1891 ) -> (u32, Option<(usize, usize, usize)>) {
1892 let channels = 4usize; let g2d_t = g2d_dst.as_typed::<u8>().unwrap();
1894 let cpu_t = cpu_dst.as_typed::<u8>().unwrap();
1895 let g2d_stride = g2d_t.effective_row_stride().unwrap_or(w * channels);
1896 let cpu_stride = cpu_t.effective_row_stride().unwrap_or(w * channels);
1897 let g2d_map = g2d_t.map().unwrap();
1898 let cpu_map = cpu_t.map().unwrap();
1899 let g2d_px = g2d_map.as_slice();
1900 let cpu_px = cpu_map.as_slice();
1901 let mut max_diff = 0u32;
1902 let mut first_fail: Option<(usize, usize, usize)> = None;
1903 for row in 0..h {
1904 for col in 0..w {
1905 for ch in 0..3usize {
1906 let gi = row * g2d_stride + col * channels + ch;
1908 let ci = row * cpu_stride + col * channels + ch;
1909 let d = (g2d_px[gi] as i32 - cpu_px[ci] as i32).unsigned_abs();
1910 if d > max_diff {
1911 max_diff = d;
1912 }
1913 if first_fail.is_none() && d > tol {
1914 first_fail = Some((col, row, ch));
1915 }
1916 }
1917 }
1918 }
1919 (max_diff, first_fail)
1920 }
1921
1922 #[test]
1938 #[cfg(target_os = "linux")]
1939 fn d01_nv12_odd_w_g2d_vs_cpu() {
1940 if !is_dma_available() {
1941 crate::test_support::report_skip("d01_nv12_odd_w_g2d_vs_cpu - DMA not available");
1942 return;
1943 }
1944 let (w, h) = (65usize, 64usize);
1945
1946 let src_dma = TensorDyn::image(
1947 w,
1948 h,
1949 PixelFormat::Nv12,
1950 DType::U8,
1951 Some(TensorMemory::DmaBuf),
1952 edgefirst_tensor::CpuAccess::ReadWrite,
1953 )
1954 .unwrap();
1955 let src_mem = TensorDyn::image(
1956 w,
1957 h,
1958 PixelFormat::Nv12,
1959 DType::U8,
1960 Some(TensorMemory::Mem),
1961 edgefirst_tensor::CpuAccess::ReadWrite,
1962 )
1963 .unwrap();
1964 fill_patterned_nv12(&src_dma);
1965 fill_patterned_nv12(&src_mem);
1966
1967 let mut cpu_dst = TensorDyn::image(
1969 w,
1970 h,
1971 PixelFormat::Rgba,
1972 DType::U8,
1973 None,
1974 edgefirst_tensor::CpuAccess::ReadWrite,
1975 )
1976 .unwrap();
1977 CPUProcessor::new()
1978 .convert(
1979 &src_mem,
1980 &mut cpu_dst,
1981 Rotation::None,
1982 Flip::None,
1983 Crop::no_crop(),
1984 )
1985 .unwrap();
1986
1987 let mut g2d_dst = TensorDyn::image(
1989 w,
1990 h,
1991 PixelFormat::Rgba,
1992 DType::U8,
1993 Some(TensorMemory::DmaBuf),
1994 edgefirst_tensor::CpuAccess::ReadWrite,
1995 )
1996 .unwrap();
1997 let mut g2d = match G2DProcessor::new() {
1998 Ok(g) => g,
1999 Err(e) => {
2000 crate::test_support::report_skip(&format!(
2001 "d01_nv12_odd_w_g2d_vs_cpu - G2D not available: {e}"
2002 ));
2003 return;
2004 }
2005 };
2006
2007 g2d.convert(
2009 &src_dma,
2010 &mut g2d_dst,
2011 Rotation::None,
2012 Flip::None,
2013 Crop::no_crop(),
2014 )
2015 .unwrap_or_else(|e| {
2016 panic!("D-01: G2D rejected odd-width (65) NV12 source: {e}");
2017 });
2018
2019 let tol = 4u32;
2021 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
2022 eprintln!("D-01 NV12 odd-W G2D vs CPU: max_diff={max_diff}");
2023 if max_diff > tol {
2041 eprintln!(
2042 "WARNING: D-01 NV12 odd-W G2D vs CPU max_diff={max_diff} > {tol} \
2043 (G2D fixed-point rounding; first bad at {first_fail:?})"
2044 );
2045 }
2046 assert!(
2047 max_diff <= 35,
2048 "D-01: gross NV12 odd-W G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2049 );
2050 }
2051
2052 #[test]
2064 #[cfg(target_os = "linux")]
2065 fn d03_nv12_odd_both_g2d_vs_cpu() {
2066 if !is_dma_available() {
2067 crate::test_support::report_skip("d03_nv12_odd_both_g2d_vs_cpu - DMA not available");
2068 return;
2069 }
2070 let (w, h) = (65usize, 63usize);
2071
2072 let src_dma = TensorDyn::image(
2073 w,
2074 h,
2075 PixelFormat::Nv12,
2076 DType::U8,
2077 Some(TensorMemory::DmaBuf),
2078 edgefirst_tensor::CpuAccess::ReadWrite,
2079 )
2080 .unwrap();
2081 let src_mem = TensorDyn::image(
2082 w,
2083 h,
2084 PixelFormat::Nv12,
2085 DType::U8,
2086 Some(TensorMemory::Mem),
2087 edgefirst_tensor::CpuAccess::ReadWrite,
2088 )
2089 .unwrap();
2090 fill_patterned_nv12(&src_dma);
2091 fill_patterned_nv12(&src_mem);
2092
2093 let mut cpu_dst = TensorDyn::image(
2095 w,
2096 h,
2097 PixelFormat::Rgba,
2098 DType::U8,
2099 None,
2100 edgefirst_tensor::CpuAccess::ReadWrite,
2101 )
2102 .unwrap();
2103 CPUProcessor::new()
2104 .convert(
2105 &src_mem,
2106 &mut cpu_dst,
2107 Rotation::None,
2108 Flip::None,
2109 Crop::no_crop(),
2110 )
2111 .unwrap();
2112
2113 let mut g2d_dst = TensorDyn::image(
2115 w,
2116 h,
2117 PixelFormat::Rgba,
2118 DType::U8,
2119 Some(TensorMemory::DmaBuf),
2120 edgefirst_tensor::CpuAccess::ReadWrite,
2121 )
2122 .unwrap();
2123 let mut g2d = match G2DProcessor::new() {
2124 Ok(g) => g,
2125 Err(e) => {
2126 crate::test_support::report_skip(&format!(
2127 "d03_nv12_odd_both_g2d_vs_cpu - G2D not available: {e}"
2128 ));
2129 return;
2130 }
2131 };
2132
2133 g2d.convert(
2136 &src_dma,
2137 &mut g2d_dst,
2138 Rotation::None,
2139 Flip::None,
2140 Crop::no_crop(),
2141 )
2142 .unwrap_or_else(|e| {
2143 panic!("D-03: G2D rejected odd-both (65×63) NV12 source: {e}");
2144 });
2145
2146 let tol = 4u32;
2147 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
2148 eprintln!("D-03 NV12 odd-both G2D vs CPU: max_diff={max_diff}");
2149 if max_diff > tol {
2154 eprintln!(
2155 "WARNING: D-03 NV12 odd-both G2D vs CPU max_diff={max_diff} > {tol} \
2156 (G2D fixed-point rounding; first bad at {first_fail:?})"
2157 );
2158 }
2159 assert!(
2160 max_diff <= 35,
2161 "D-03: gross NV12 odd-both G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2162 );
2163 }
2164 #[test]
2176 #[cfg(target_os = "linux")]
2177 fn g2d_int8_dst_biases_only_the_pixel_bytes_at_the_declared_stride() {
2178 let skip = |why: &str| {
2182 use std::io::Write;
2183 let _ = writeln!(
2184 &mut std::io::stderr(),
2185 "SKIPPED: g2d_int8_dst_biases_only_the_pixel_bytes_at_the_declared_stride - {why}"
2186 );
2187 };
2188 if !is_dma_available() {
2189 skip("DMA not available");
2190 return;
2191 }
2192 if !is_g2d_available() {
2193 skip("G2D not available");
2194 return;
2195 }
2196 const W: usize = 64;
2197 const H: usize = 16;
2198 const POISON: u8 = 0xAB;
2199 let fmt = PixelFormat::Rgba;
2200 let bpp = fmt.channels();
2201 let row_bytes = W * bpp;
2202 let stride = row_bytes + 64;
2204
2205 let src = TensorDyn::image(
2206 W,
2207 H,
2208 fmt,
2209 DType::U8,
2210 Some(TensorMemory::DmaBuf),
2211 edgefirst_tensor::CpuAccess::ReadWrite,
2212 )
2213 .expect("DMA source");
2214 {
2215 let t = src.as_typed::<u8>().unwrap();
2216 let s = t.effective_row_stride().unwrap();
2217 let mut m = t.map().unwrap();
2218 let buf = m.as_mut_slice();
2219 for y in 0..H {
2220 for i in 0..row_bytes {
2221 buf[y * s + i] = (y * 31 + i * 7) as u8;
2222 }
2223 }
2224 }
2225
2226 let mut u8_dst = TensorDyn::image_with_stride(
2227 W,
2228 H,
2229 fmt,
2230 DType::U8,
2231 stride,
2232 Some(TensorMemory::DmaBuf),
2233 edgefirst_tensor::CpuAccess::ReadWrite,
2234 )
2235 .expect("padded U8 destination");
2236 let mut i8_dst = TensorDyn::image_with_stride(
2237 W,
2238 H,
2239 fmt,
2240 DType::I8,
2241 stride,
2242 Some(TensorMemory::DmaBuf),
2243 edgefirst_tensor::CpuAccess::ReadWrite,
2244 )
2245 .expect("padded I8 destination");
2246 assert_eq!(i8_dst.effective_row_stride(), Some(stride));
2247 u8_dst
2251 .as_typed::<u8>()
2252 .unwrap()
2253 .map()
2254 .unwrap()
2255 .as_mut_slice()
2256 .fill(POISON);
2257 i8_dst
2258 .as_typed::<i8>()
2259 .unwrap()
2260 .map()
2261 .unwrap()
2262 .as_mut_slice()
2263 .fill(POISON as i8);
2264
2265 let mut g2d = G2DProcessor::new().unwrap();
2266 g2d.convert(
2267 &src,
2268 &mut u8_dst,
2269 Rotation::None,
2270 Flip::None,
2271 Crop::no_crop(),
2272 )
2273 .expect("U8 blit");
2274 g2d.convert(
2275 &src,
2276 &mut i8_dst,
2277 Rotation::None,
2278 Flip::None,
2279 Crop::no_crop(),
2280 )
2281 .expect("I8 blit");
2282
2283 let u8_stride = u8_dst.effective_row_stride().unwrap();
2284 let want = u8_dst
2285 .as_typed::<u8>()
2286 .unwrap()
2287 .map()
2288 .unwrap()
2289 .as_slice()
2290 .to_vec();
2291 let got: Vec<u8> = i8_dst
2292 .as_typed::<i8>()
2293 .unwrap()
2294 .map()
2295 .unwrap()
2296 .as_slice()
2297 .iter()
2298 .map(|&v| v as u8)
2299 .collect();
2300 assert!(
2304 got.len() >= H * stride && want.len() >= H * u8_stride,
2305 "a {stride} B pitch over {H} rows needs {} B, mapped {} (I8) and {} (U8)",
2306 H * stride,
2307 got.len(),
2308 want.len()
2309 );
2310 for y in 0..H {
2314 assert!(
2315 want[y * u8_stride + row_bytes..(y + 1) * u8_stride]
2316 .iter()
2317 .all(|&b| b == POISON),
2318 "row {y}: the G2D blit itself wrote past the pixels; the I8 \
2319 assertions below cannot separate that from the bias pass"
2320 );
2321 }
2322 for y in 0..H {
2323 for x in 0..W {
2324 for c in 0..bpp {
2325 let g = got[y * stride + x * bpp + c];
2326 let w = want[y * u8_stride + x * bpp + c];
2327 let expect = if c == bpp - 1 { w } else { w ^ 0x80 };
2328 assert_eq!(g, expect, "pixel ({x},{y}) channel {c}");
2329 }
2330 }
2331 assert!(
2332 got[y * stride + row_bytes..(y + 1) * stride]
2333 .iter()
2334 .all(|&b| b == POISON),
2335 "row {y}: the padding past the pixels was biased"
2336 );
2337 }
2338 }
2339}