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, TensorMapTrait,
10 TensorMemory, 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}
63
64unsafe impl Send for G2DProcessor {}
65unsafe impl Sync for G2DProcessor {}
66
67impl G2DProcessor {
68 pub fn new() -> Result<Self> {
76 let mut g2d = G2D::new("libg2d.so.2")?;
77 g2d.set_bt601_colorspace()?;
83
84 log::debug!("G2DConverter created with version {:?}", g2d.version());
85 Ok(Self { g2d })
86 }
87
88 pub fn version(&self) -> g2d_sys::Version {
91 self.g2d.version()
92 }
93
94 fn convert_impl(
95 &mut self,
96 src_dyn: &TensorDyn,
97 dst_dyn: &mut TensorDyn,
98 rotation: Rotation,
99 flip: Flip,
100 crop: ResolvedCrop,
101 ) -> Result<()> {
102 let _span = tracing::trace_span!(
103 "image.convert.g2d",
104 src_fmt = ?src_dyn.format(),
105 dst_fmt = ?dst_dyn.format(),
106 )
107 .entered();
108 if log::log_enabled!(log::Level::Trace) {
109 log::trace!(
110 "G2D convert: {:?}({:?}/{:?}) → {:?}({:?}/{:?})",
111 src_dyn.format(),
112 src_dyn.dtype(),
113 src_dyn.memory(),
114 dst_dyn.format(),
115 dst_dyn.dtype(),
116 dst_dyn.memory(),
117 );
118 }
119
120 if src_dyn.dtype() != DType::U8 {
121 return Err(Error::NotSupported(
122 "G2D only supports u8 source tensors".to_string(),
123 ));
124 }
125 let is_int8_dst = dst_dyn.dtype() == DType::I8;
126 if dst_dyn.dtype() != DType::U8 && !is_int8_dst {
127 return Err(Error::NotSupported(
128 "G2D only supports u8 or i8 destination tensors".to_string(),
129 ));
130 }
131
132 let src_fmt = src_dyn.format().ok_or(Error::NotAnImage)?;
133 let dst_fmt = dst_dyn.format().ok_or(Error::NotAnImage)?;
134
135 use PixelFormat::*;
137 match (src_fmt, dst_fmt) {
138 (Rgba, Rgba) => {}
139 (Rgba, Yuyv) => {}
140 (Rgba, Rgb) => {}
141 (Yuyv, Rgba) => {}
142 (Yuyv, Yuyv) => {}
143 (Yuyv, Rgb) => {}
144 (Nv12, Rgba) => {}
147 (Nv12, Yuyv) => {}
148 (Nv12, Rgb) => {}
149 (Rgba, Bgra) => {}
150 (Yuyv, Bgra) => {}
151 (Nv12, Bgra) => {}
152 (Bgra, Bgra) => {}
153 (s, d) => {
154 return Err(Error::NotSupported(format!(
155 "G2D does not support {} to {} conversion",
156 s, d
157 )));
158 }
159 }
160
161 let src_is_yuv = src_fmt.is_yuv();
173 let dst_is_yuv = dst_fmt.is_yuv();
174 if src_is_yuv || dst_is_yuv {
175 let cm = if src_is_yuv {
176 effective_colorimetry(src_dyn)
177 } else {
178 effective_colorimetry(dst_dyn)
179 };
180 if !g2d_can_handle(&cm, true) {
181 return Err(Error::NotSupported(format!(
182 "G2D cannot express colorimetry {:?}/{:?} (matrix-only, \
183 no range control, no BT.2020)",
184 cm.encoding, cm.range
185 )));
186 }
187 match cm.encoding {
188 Some(ColorEncoding::Bt601) => self.g2d.set_bt601_colorspace()?,
189 Some(ColorEncoding::Bt709) => self.g2d.set_bt709_colorspace()?,
190 _ => self.g2d.set_bt709_colorspace()?,
193 }
194 }
195
196 let src = src_dyn.as_typed::<u8>().unwrap();
198 let dst = if is_int8_dst {
201 let i8_tensor = dst_dyn.as_typed_mut::<i8>().unwrap();
209 unsafe { &mut *(i8_tensor as *mut Tensor<i8> as *mut Tensor<u8>) }
210 } else {
211 dst_dyn.as_typed_mut::<u8>().unwrap()
212 };
213
214 let mut src_surface = tensor_to_g2d_surface(src)?;
215 let mut dst_surface = tensor_to_g2d_surface(dst)?;
216
217 src_surface.rot = match flip {
218 Flip::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
219 Flip::Vertical => g2d_sys::g2d_rotation_G2D_FLIP_V,
220 Flip::Horizontal => g2d_sys::g2d_rotation_G2D_FLIP_H,
221 };
222
223 dst_surface.rot = match rotation {
224 Rotation::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
225 Rotation::Clockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_90,
226 Rotation::Rotate180 => g2d_sys::g2d_rotation_G2D_ROTATION_180,
227 Rotation::CounterClockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_270,
228 };
229
230 if let Some(crop_rect) = crop.src_rect {
231 src_surface.left = crop_rect.left as i32;
232 src_surface.top = crop_rect.top as i32;
233 src_surface.right = (crop_rect.left + crop_rect.width) as i32;
234 src_surface.bottom = (crop_rect.top + crop_rect.height) as i32;
235 }
236
237 let dst_w = dst.width().unwrap();
238 let dst_h = dst.height().unwrap();
239
240 let needs_clear = crop.dst_color.is_some()
246 && crop.dst_rect.is_some_and(|dst_rect| {
247 dst_rect.left != 0
248 || dst_rect.top != 0
249 || dst_rect.width != dst_w
250 || dst_rect.height != dst_h
251 });
252
253 if needs_clear && dst_fmt != Rgb {
254 if let Some(dst_color) = crop.dst_color {
255 let start = Instant::now();
256 self.g2d.clear(&mut dst_surface, dst_color)?;
257 log::trace!("g2d clear takes {:?}", start.elapsed());
258 }
259 }
260
261 if let Some(crop_rect) = crop.dst_rect {
262 dst_surface.planes[0] += ((crop_rect.top * dst_surface.stride as usize
266 + crop_rect.left)
267 * dst_fmt.channels()) as u64;
268
269 dst_surface.right = crop_rect.width as i32;
270 dst_surface.bottom = crop_rect.height as i32;
271 dst_surface.width = crop_rect.width as i32;
272 dst_surface.height = crop_rect.height as i32;
273 }
274
275 let (sx_src, sy_src) = chroma_subsample_shifts(src_fmt);
289 let (sx_dst, sy_dst) = chroma_subsample_shifts(dst_fmt);
290 let pad_w = (sx_src | sx_dst) > 0;
291 let pad_h = (sy_src | sy_dst) > 0;
292 if pad_w || pad_h {
293 let even = |v: i32| v + (v & 1);
294 if pad_h && even(dst_surface.bottom) != dst_surface.bottom {
295 let page = match unsafe { libc::sysconf(libc::_SC_PAGESIZE) } {
306 n if n > 0 => n as usize,
307 _ => 4096,
308 };
309 let row_bytes = dst_surface.stride as usize * dst_fmt.channels();
310 let mapped = (row_bytes * dst_h).next_multiple_of(page);
311 let needed = row_bytes * even(dst_surface.bottom) as usize;
312 if crop.dst_rect.is_some() || needed > mapped {
313 return Err(Error::NotSupported(format!(
314 "G2D: odd-height {dst_fmt} destination cannot hold the padding \
315 row for the i.MX95 DPU even-dimension workaround; deferring to \
316 GL/CPU"
317 )));
318 }
319 }
320 if pad_w {
321 src_surface.right = even(src_surface.right);
322 src_surface.width = even(src_surface.width);
323 dst_surface.right = even(dst_surface.right);
324 dst_surface.width = even(dst_surface.width);
325 }
326 if pad_h {
327 src_surface.bottom = even(src_surface.bottom);
328 src_surface.height = even(src_surface.height);
329 dst_surface.bottom = even(dst_surface.bottom);
330 dst_surface.height = even(dst_surface.height);
331 }
332 }
333
334 log::trace!("G2D blit: {src_fmt}→{dst_fmt} int8={is_int8_dst}");
335 self.g2d.blit(&src_surface, &dst_surface)?;
336 self.g2d.finish()?;
337 log::trace!("G2D blit complete");
338
339 if needs_clear && dst_fmt == Rgb {
341 if let (Some(dst_color), Some(dst_rect)) = (crop.dst_color, crop.dst_rect) {
342 let start = Instant::now();
343 CPUProcessor::fill_image_outside_crop_u8(dst, dst_color, dst_rect)?;
344 log::trace!("cpu fill takes {:?}", start.elapsed());
345 }
346 }
347
348 if is_int8_dst {
356 let start = Instant::now();
357 let mut map = dst.map()?;
358 crate::cpu::apply_int8_xor_bias(map.as_mut_slice(), dst_fmt);
359 log::trace!("g2d int8 XOR 0x80 post-pass takes {:?}", start.elapsed());
360 }
361
362 Ok(())
363 }
364}
365
366impl ImageProcessorTrait for G2DProcessor {
367 fn convert(
368 &mut self,
369 src: &TensorDyn,
370 dst: &mut TensorDyn,
371 rotation: Rotation,
372 flip: Flip,
373 crop: Crop,
374 ) -> Result<()> {
375 let crop = crop.resolve(
381 src.width().unwrap_or(0),
382 src.height().unwrap_or(0),
383 dst.width().unwrap_or(0),
384 dst.height().unwrap_or(0),
385 )?;
386 self.convert_impl(src, dst, rotation, flip, crop)
387 }
388
389 fn draw_decoded_masks(
390 &mut self,
391 dst: &mut TensorDyn,
392 detect: &[crate::DetectBox],
393 segmentation: &[crate::Segmentation],
394 overlay: crate::MaskOverlay<'_>,
395 ) -> Result<()> {
396 if !detect.is_empty() || !segmentation.is_empty() {
400 return Err(Error::NotImplemented(
401 "G2D does not support drawing detection or segmentation overlays".to_string(),
402 ));
403 }
404 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
405 }
406
407 fn draw_proto_masks(
408 &mut self,
409 dst: &mut TensorDyn,
410 detect: &[crate::DetectBox],
411 _proto_data: &crate::ProtoData,
412 overlay: crate::MaskOverlay<'_>,
413 ) -> Result<()> {
414 if !detect.is_empty() {
417 return Err(Error::NotImplemented(
418 "G2D does not support drawing detection or segmentation overlays".to_string(),
419 ));
420 }
421 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
422 }
423
424 fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
425 Err(Error::NotImplemented(
426 "G2D does not support setting colors for rendering detection or segmentation overlays"
427 .to_string(),
428 ))
429 }
430}
431
432fn draw_empty_frame_g2d(
445 g2d: &mut G2D,
446 dst_dyn: &mut TensorDyn,
447 background: Option<&TensorDyn>,
448) -> Result<()> {
449 if dst_dyn.dtype() != DType::U8 {
450 return Err(Error::NotSupported(
451 "G2D only supports u8 destination tensors".to_string(),
452 ));
453 }
454 let dst = dst_dyn.as_typed_mut::<u8>().ok_or(Error::NotAnImage)?;
455
456 if TensorTrait::memory(dst) != TensorMemory::DmaBuf {
466 return Err(Error::NotImplemented(
467 "g2d only supports Dma memory".to_string(),
468 ));
469 }
470
471 let mut dst_surface = tensor_to_g2d_surface(dst)?;
472
473 match background {
474 None => {
475 let start = Instant::now();
479 g2d.clear(&mut dst_surface, [0, 0, 0, 0])?;
480 g2d.finish()?;
481 log::trace!("g2d clear (empty frame) takes {:?}", start.elapsed());
482 }
483 Some(bg_dyn) => {
484 if bg_dyn.shape() != dst.shape() {
489 return Err(Error::InvalidShape(
490 "background shape does not match dst".into(),
491 ));
492 }
493 if bg_dyn.format() != dst.format() {
494 return Err(Error::InvalidShape(
495 "background pixel format does not match dst".into(),
496 ));
497 }
498 if bg_dyn.dtype() != DType::U8 {
499 return Err(Error::NotSupported(
500 "G2D only supports u8 background tensors".to_string(),
501 ));
502 }
503 let bg = bg_dyn.as_typed::<u8>().ok_or(Error::NotAnImage)?;
504 if TensorTrait::memory(bg) != TensorMemory::DmaBuf {
506 return Err(Error::NotImplemented(
507 "g2d background must be Dma-backed".to_string(),
508 ));
509 }
510 let src_surface = tensor_to_g2d_surface(bg)?;
511 let start = Instant::now();
512 g2d.blit(&src_surface, &dst_surface)?;
513 g2d.finish()?;
514 log::trace!("g2d blit (bg→dst) takes {:?}", start.elapsed());
515 }
516 }
517 Ok(())
518}
519
520fn chroma_subsample_shifts(fmt: PixelFormat) -> (u32, u32) {
524 match fmt.chroma_layout() {
525 Some(cl) => (cl.shift_x, cl.shift_y),
526 None if matches!(fmt, PixelFormat::Yuyv | PixelFormat::Vyuy) => (1, 0),
529 None => (0, 0),
530 }
531}
532
533fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
537 let fmt = img.format().ok_or(Error::NotAnImage)?;
538 let fd = img
541 .dmabuf()
542 .map_err(|e| Error::NotImplemented(format!("g2d only supports Dma memory ({e})")))?;
543 let phys: G2DPhysical = fd.as_raw_fd().try_into()?;
544
545 let base_addr = phys.address();
552 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
553 let planes = if fmt == PixelFormat::Nv12 {
554 if img.is_multiplane() {
555 let chroma = img.chroma().unwrap();
557 let chroma_fd = chroma.dmabuf().map_err(|e| {
558 Error::NotImplemented(format!("g2d multiplane chroma must be DMA-backed ({e})"))
559 })?;
560 let uv_phys: G2DPhysical = chroma_fd.as_raw_fd().try_into()?;
561 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
562 [
563 base_addr + luma_offset,
564 uv_phys.address() + chroma_offset,
565 0,
566 ]
567 } else {
568 let w = img.width().unwrap();
569 let h = img.height().unwrap();
570 let stride = img.effective_row_stride().unwrap_or(w.next_multiple_of(2));
571 let uv_offset = (luma_offset as usize + stride * h) as u64;
572 [base_addr + luma_offset, base_addr + uv_offset, 0]
573 }
574 } else {
575 [base_addr + luma_offset, 0, 0]
576 };
577
578 let w = img.width().unwrap();
579 let h = img.height().unwrap();
580 let fourcc = pixelfmt_to_fourcc(fmt);
581
582 let stride_pixels = match img.effective_row_stride() {
585 Some(s) => {
586 let channels = fmt.channels();
587 if s % channels != 0 {
588 return Err(Error::NotImplemented(
589 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
590 ));
591 }
592 s / channels
593 }
594 None => w,
595 };
596
597 Ok(G2DSurface {
598 planes,
599 format: G2DFormat::try_from(fourcc)?.format(),
600 left: 0,
601 top: 0,
602 right: w as i32,
603 bottom: h as i32,
604 stride: stride_pixels as i32,
605 width: w as i32,
606 height: h as i32,
607 blendfunc: 0,
608 clrcolor: 0,
609 rot: 0,
610 global_alpha: 0,
611 })
612}
613
614#[cfg(test)]
615mod g2d_predicate_tests {
616 use super::*;
617 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry};
618
619 #[test]
620 fn g2d_declines_full_range_and_bt2020_yuv() {
621 let full = Colorimetry::default()
622 .with_range(ColorRange::Full)
623 .with_encoding(ColorEncoding::Bt709);
624 let lim = Colorimetry::default()
625 .with_range(ColorRange::Limited)
626 .with_encoding(ColorEncoding::Bt709);
627 let bt2020 = Colorimetry::default()
628 .with_range(ColorRange::Limited)
629 .with_encoding(ColorEncoding::Bt2020);
630 assert!(!g2d_can_handle(&full, true)); assert!(g2d_can_handle(&lim, true)); assert!(!g2d_can_handle(&bt2020, true)); assert!(g2d_can_handle(&full, false)); }
635}
636
637#[cfg(feature = "g2d_test_formats")]
638#[cfg(test)]
639#[allow(deprecated)]
640mod g2d_tests {
641 use super::*;
642 use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rotation};
643 use edgefirst_tensor::{
644 is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
645 };
646 use image::buffer::ConvertBuffer;
647
648 #[cfg(target_os = "linux")]
655 fn is_g2d_available() -> bool {
656 G2DProcessor::new().is_ok()
657 }
658
659 #[test]
660 #[cfg(target_os = "linux")]
661 fn test_g2d_formats_no_resize() {
662 for i in [
663 PixelFormat::Rgba,
664 PixelFormat::Yuyv,
665 PixelFormat::Rgb,
666 PixelFormat::Grey,
667 PixelFormat::Nv12,
668 ] {
669 for o in [
670 PixelFormat::Rgba,
671 PixelFormat::Yuyv,
672 PixelFormat::Rgb,
673 PixelFormat::Grey,
674 ] {
675 let res = test_g2d_format_no_resize_(i, o);
676 if let Err(e) = res {
677 println!("{i} to {o} failed: {e:?}");
678 } else {
679 println!("{i} to {o} success");
680 }
681 }
682 }
683 }
684
685 fn test_g2d_format_no_resize_(
686 g2d_in_fmt: PixelFormat,
687 g2d_out_fmt: PixelFormat,
688 ) -> Result<(), crate::Error> {
689 let dst_width = 1280;
690 let dst_height = 720;
691 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
692 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
693
694 let mut src2 = TensorDyn::image(
696 1280,
697 720,
698 g2d_in_fmt,
699 DType::U8,
700 Some(TensorMemory::DmaBuf),
701 edgefirst_tensor::CpuAccess::ReadWrite,
702 )?;
703
704 let mut cpu_converter = CPUProcessor::new();
705
706 if g2d_in_fmt == PixelFormat::Nv12 {
708 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
709 src2.as_typed::<u8>()
710 .unwrap()
711 .map()?
712 .as_mut_slice()
713 .copy_from_slice(&nv12_bytes);
714 } else {
715 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
716 }
717
718 let mut g2d_dst = TensorDyn::image(
719 dst_width,
720 dst_height,
721 g2d_out_fmt,
722 DType::U8,
723 Some(TensorMemory::DmaBuf),
724 edgefirst_tensor::CpuAccess::ReadWrite,
725 )?;
726 let mut g2d_converter = G2DProcessor::new()?;
727 let src2_dyn = src2;
728 let mut g2d_dst_dyn = g2d_dst;
729 g2d_converter.convert(
730 &src2_dyn,
731 &mut g2d_dst_dyn,
732 Rotation::None,
733 Flip::None,
734 Crop::no_crop(),
735 )?;
736 g2d_dst = {
737 let mut __t = g2d_dst_dyn.into_u8().unwrap();
738 __t.set_format(g2d_out_fmt)
739 .map_err(|e| crate::Error::Internal(e.to_string()))?;
740 TensorDyn::from(__t)
741 };
742
743 let mut cpu_dst = TensorDyn::image(
744 dst_width,
745 dst_height,
746 PixelFormat::Rgb,
747 DType::U8,
748 None,
749 edgefirst_tensor::CpuAccess::ReadWrite,
750 )?;
751 cpu_converter.convert(
752 &g2d_dst,
753 &mut cpu_dst,
754 Rotation::None,
755 Flip::None,
756 Crop::no_crop(),
757 )?;
758
759 compare_images(
760 &src,
761 &cpu_dst,
762 0.98,
763 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
764 )
765 }
766
767 #[test]
768 #[cfg(target_os = "linux")]
769 fn test_g2d_formats_with_resize() {
770 for i in [
771 PixelFormat::Rgba,
772 PixelFormat::Yuyv,
773 PixelFormat::Rgb,
774 PixelFormat::Grey,
775 PixelFormat::Nv12,
776 ] {
777 for o in [
778 PixelFormat::Rgba,
779 PixelFormat::Yuyv,
780 PixelFormat::Rgb,
781 PixelFormat::Grey,
782 ] {
783 let res = test_g2d_format_with_resize_(i, o);
784 if let Err(e) = res {
785 println!("{i} to {o} failed: {e:?}");
786 } else {
787 println!("{i} to {o} success");
788 }
789 }
790 }
791 }
792
793 #[test]
794 #[cfg(target_os = "linux")]
795 fn test_g2d_formats_with_resize_dst_crop() {
796 for i in [
797 PixelFormat::Rgba,
798 PixelFormat::Yuyv,
799 PixelFormat::Rgb,
800 PixelFormat::Grey,
801 PixelFormat::Nv12,
802 ] {
803 for o in [
804 PixelFormat::Rgba,
805 PixelFormat::Yuyv,
806 PixelFormat::Rgb,
807 PixelFormat::Grey,
808 ] {
809 let res = test_g2d_format_with_resize_dst_crop(i, o);
810 if let Err(e) = res {
811 println!("{i} to {o} failed: {e:?}");
812 } else {
813 println!("{i} to {o} success");
814 }
815 }
816 }
817 }
818
819 fn test_g2d_format_with_resize_(
820 g2d_in_fmt: PixelFormat,
821 g2d_out_fmt: PixelFormat,
822 ) -> Result<(), crate::Error> {
823 let dst_width = 600;
824 let dst_height = 400;
825 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
826 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
827
828 let mut cpu_converter = CPUProcessor::new();
829
830 let mut reference = TensorDyn::image(
831 dst_width,
832 dst_height,
833 PixelFormat::Rgb,
834 DType::U8,
835 Some(TensorMemory::DmaBuf),
836 edgefirst_tensor::CpuAccess::ReadWrite,
837 )?;
838 cpu_converter.convert(
839 &src,
840 &mut reference,
841 Rotation::None,
842 Flip::None,
843 Crop::no_crop(),
844 )?;
845
846 let mut src2 = TensorDyn::image(
848 1280,
849 720,
850 g2d_in_fmt,
851 DType::U8,
852 Some(TensorMemory::DmaBuf),
853 edgefirst_tensor::CpuAccess::ReadWrite,
854 )?;
855
856 if g2d_in_fmt == PixelFormat::Nv12 {
858 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
859 src2.as_typed::<u8>()
860 .unwrap()
861 .map()?
862 .as_mut_slice()
863 .copy_from_slice(&nv12_bytes);
864 } else {
865 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
866 }
867
868 let mut g2d_dst = TensorDyn::image(
869 dst_width,
870 dst_height,
871 g2d_out_fmt,
872 DType::U8,
873 Some(TensorMemory::DmaBuf),
874 edgefirst_tensor::CpuAccess::ReadWrite,
875 )?;
876 let mut g2d_converter = G2DProcessor::new()?;
877 let src2_dyn = src2;
878 let mut g2d_dst_dyn = g2d_dst;
879 g2d_converter.convert(
880 &src2_dyn,
881 &mut g2d_dst_dyn,
882 Rotation::None,
883 Flip::None,
884 Crop::no_crop(),
885 )?;
886 g2d_dst = {
887 let mut __t = g2d_dst_dyn.into_u8().unwrap();
888 __t.set_format(g2d_out_fmt)
889 .map_err(|e| crate::Error::Internal(e.to_string()))?;
890 TensorDyn::from(__t)
891 };
892
893 let mut cpu_dst = TensorDyn::image(
894 dst_width,
895 dst_height,
896 PixelFormat::Rgb,
897 DType::U8,
898 None,
899 edgefirst_tensor::CpuAccess::ReadWrite,
900 )?;
901 cpu_converter.convert(
902 &g2d_dst,
903 &mut cpu_dst,
904 Rotation::None,
905 Flip::None,
906 Crop::no_crop(),
907 )?;
908
909 compare_images(
910 &reference,
911 &cpu_dst,
912 0.98,
913 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
914 )
915 }
916
917 fn test_g2d_format_with_resize_dst_crop(
918 g2d_in_fmt: PixelFormat,
919 g2d_out_fmt: PixelFormat,
920 ) -> Result<(), crate::Error> {
921 let dst_width = 600;
922 let dst_height = 400;
923 let region = crate::Region::new(100, 100, 200, 100);
928 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
929 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
930
931 let mut cpu_converter = CPUProcessor::new();
932
933 let reference = TensorDyn::image(
934 dst_width,
935 dst_height,
936 PixelFormat::Rgb,
937 DType::U8,
938 Some(TensorMemory::DmaBuf),
939 edgefirst_tensor::CpuAccess::ReadWrite,
940 )?;
941 reference
942 .as_typed::<u8>()
943 .unwrap()
944 .map()
945 .unwrap()
946 .as_mut_slice()
947 .fill(128);
948 cpu_converter.convert(
949 &src,
950 &mut reference.view(region)?,
951 Rotation::None,
952 Flip::None,
953 Crop::no_crop(),
954 )?;
955
956 let mut src2 = TensorDyn::image(
958 1280,
959 720,
960 g2d_in_fmt,
961 DType::U8,
962 Some(TensorMemory::DmaBuf),
963 edgefirst_tensor::CpuAccess::ReadWrite,
964 )?;
965
966 if g2d_in_fmt == PixelFormat::Nv12 {
968 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
969 src2.as_typed::<u8>()
970 .unwrap()
971 .map()?
972 .as_mut_slice()
973 .copy_from_slice(&nv12_bytes);
974 } else {
975 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
976 }
977
978 let mut g2d_dst = TensorDyn::image(
979 dst_width,
980 dst_height,
981 g2d_out_fmt,
982 DType::U8,
983 Some(TensorMemory::DmaBuf),
984 edgefirst_tensor::CpuAccess::ReadWrite,
985 )?;
986 g2d_dst
987 .as_typed::<u8>()
988 .unwrap()
989 .map()
990 .unwrap()
991 .as_mut_slice()
992 .fill(128);
993 let mut g2d_converter = G2DProcessor::new()?;
994 let src2_dyn = src2;
995 let g2d_dst_dyn = g2d_dst;
996 g2d_converter.convert(
997 &src2_dyn,
998 &mut g2d_dst_dyn.view(region)?,
999 Rotation::None,
1000 Flip::None,
1001 Crop::no_crop(),
1002 )?;
1003 g2d_dst = {
1004 let mut __t = g2d_dst_dyn.into_u8().unwrap();
1005 __t.set_format(g2d_out_fmt)
1006 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1007 TensorDyn::from(__t)
1008 };
1009
1010 let mut cpu_dst = TensorDyn::image(
1011 dst_width,
1012 dst_height,
1013 PixelFormat::Rgb,
1014 DType::U8,
1015 None,
1016 edgefirst_tensor::CpuAccess::ReadWrite,
1017 )?;
1018 cpu_converter.convert(
1019 &g2d_dst,
1020 &mut cpu_dst,
1021 Rotation::None,
1022 Flip::None,
1023 Crop::no_crop(),
1024 )?;
1025
1026 compare_images(
1027 &reference,
1028 &cpu_dst,
1029 0.98,
1030 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
1031 )
1032 }
1033
1034 fn compare_images(
1035 img1: &TensorDyn,
1036 img2: &TensorDyn,
1037 threshold: f64,
1038 name: &str,
1039 ) -> Result<(), crate::Error> {
1040 assert_eq!(img1.height(), img2.height(), "Heights differ");
1041 assert_eq!(img1.width(), img2.width(), "Widths differ");
1042 assert_eq!(
1043 img1.format().unwrap(),
1044 img2.format().unwrap(),
1045 "PixelFormat differ"
1046 );
1047 assert!(
1048 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
1049 "format must be Rgb or Rgba for comparison"
1050 );
1051 let image1 = match img1.format().unwrap() {
1052 PixelFormat::Rgb => image::RgbImage::from_vec(
1053 img1.width().unwrap() as u32,
1054 img1.height().unwrap() as u32,
1055 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1056 )
1057 .unwrap(),
1058 PixelFormat::Rgba => image::RgbaImage::from_vec(
1059 img1.width().unwrap() as u32,
1060 img1.height().unwrap() as u32,
1061 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1062 )
1063 .unwrap()
1064 .convert(),
1065
1066 _ => unreachable!(),
1067 };
1068
1069 let image2 = match img2.format().unwrap() {
1070 PixelFormat::Rgb => image::RgbImage::from_vec(
1071 img2.width().unwrap() as u32,
1072 img2.height().unwrap() as u32,
1073 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1074 )
1075 .unwrap(),
1076 PixelFormat::Rgba => image::RgbaImage::from_vec(
1077 img2.width().unwrap() as u32,
1078 img2.height().unwrap() as u32,
1079 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1080 )
1081 .unwrap()
1082 .convert(),
1083
1084 _ => unreachable!(),
1085 };
1086
1087 let similarity = image_compare::rgb_similarity_structure(
1088 &image_compare::Algorithm::RootMeanSquared,
1089 &image1,
1090 &image2,
1091 )
1092 .expect("Image Comparison failed");
1093
1094 if similarity.score < threshold {
1095 image1.save(format!("{name}_1.png")).unwrap();
1096 image2.save(format!("{name}_2.png")).unwrap();
1097 return Err(Error::Internal(format!(
1098 "{name}: converted image and target image have similarity score too low: {} < {}",
1099 similarity.score, threshold
1100 )));
1101 }
1102 Ok(())
1103 }
1104
1105 fn load_raw_image(
1111 width: usize,
1112 height: usize,
1113 format: PixelFormat,
1114 memory: Option<TensorMemory>,
1115 bytes: &[u8],
1116 ) -> Result<TensorDyn, crate::Error> {
1117 let img = TensorDyn::image(
1118 width,
1119 height,
1120 format,
1121 DType::U8,
1122 memory,
1123 edgefirst_tensor::CpuAccess::ReadWrite,
1124 )?;
1125 let mut map = img.as_typed::<u8>().unwrap().map()?;
1126 let dst = map.as_mut_slice();
1127 if bytes.len() > dst.len() {
1128 return Err(crate::Error::InvalidShape(format!(
1129 "load_raw_image: {} input bytes exceed {}-byte image buffer",
1130 bytes.len(),
1131 dst.len()
1132 )));
1133 }
1134 dst[..bytes.len()].copy_from_slice(bytes);
1135 Ok(img)
1136 }
1137
1138 #[test]
1140 #[cfg(target_os = "linux")]
1141 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
1142 if !is_dma_available() || !is_g2d_available() {
1143 return Ok(());
1144 }
1145 let src = load_raw_image(
1147 1280,
1148 720,
1149 PixelFormat::Nv12,
1150 Some(TensorMemory::DmaBuf),
1151 &edgefirst_bench::testdata::read("camera720p.nv12"),
1152 )?;
1153
1154 let reference = load_raw_image(
1156 1280,
1157 720,
1158 PixelFormat::Rgba,
1159 None,
1160 &edgefirst_bench::testdata::read("camera720p.rgba"),
1161 )?;
1162
1163 let mut dst = TensorDyn::image(
1165 1280,
1166 720,
1167 PixelFormat::Rgba,
1168 DType::U8,
1169 Some(TensorMemory::DmaBuf),
1170 edgefirst_tensor::CpuAccess::ReadWrite,
1171 )?;
1172 let mut g2d = G2DProcessor::new()?;
1173 let src_dyn = src;
1174 let mut dst_dyn = dst;
1175 g2d.convert(
1176 &src_dyn,
1177 &mut dst_dyn,
1178 Rotation::None,
1179 Flip::None,
1180 Crop::no_crop(),
1181 )?;
1182 dst = {
1183 let mut __t = dst_dyn.into_u8().unwrap();
1184 __t.set_format(PixelFormat::Rgba)
1185 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1186 TensorDyn::from(__t)
1187 };
1188
1189 let cpu_dst = TensorDyn::image(
1191 1280,
1192 720,
1193 PixelFormat::Rgba,
1194 DType::U8,
1195 None,
1196 edgefirst_tensor::CpuAccess::ReadWrite,
1197 )?;
1198 cpu_dst
1199 .as_typed::<u8>()
1200 .unwrap()
1201 .map()?
1202 .as_mut_slice()
1203 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1204
1205 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
1206 }
1207
1208 #[test]
1210 #[cfg(target_os = "linux")]
1211 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
1212 if !is_dma_available() || !is_g2d_available() {
1213 return Ok(());
1214 }
1215 let src = load_raw_image(
1217 1280,
1218 720,
1219 PixelFormat::Nv12,
1220 Some(TensorMemory::DmaBuf),
1221 &edgefirst_bench::testdata::read("camera720p.nv12"),
1222 )?;
1223
1224 let reference = load_raw_image(
1226 1280,
1227 720,
1228 PixelFormat::Rgb,
1229 None,
1230 &edgefirst_bench::testdata::read("camera720p.rgb"),
1231 )?;
1232
1233 let mut dst = TensorDyn::image(
1235 1280,
1236 720,
1237 PixelFormat::Rgb,
1238 DType::U8,
1239 Some(TensorMemory::DmaBuf),
1240 edgefirst_tensor::CpuAccess::ReadWrite,
1241 )?;
1242 let mut g2d = G2DProcessor::new()?;
1243 let src_dyn = src;
1244 let mut dst_dyn = dst;
1245 g2d.convert(
1246 &src_dyn,
1247 &mut dst_dyn,
1248 Rotation::None,
1249 Flip::None,
1250 Crop::no_crop(),
1251 )?;
1252 dst = {
1253 let mut __t = dst_dyn.into_u8().unwrap();
1254 __t.set_format(PixelFormat::Rgb)
1255 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1256 TensorDyn::from(__t)
1257 };
1258
1259 let cpu_dst = TensorDyn::image(
1261 1280,
1262 720,
1263 PixelFormat::Rgb,
1264 DType::U8,
1265 None,
1266 edgefirst_tensor::CpuAccess::ReadWrite,
1267 )?;
1268 cpu_dst
1269 .as_typed::<u8>()
1270 .unwrap()
1271 .map()?
1272 .as_mut_slice()
1273 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1274
1275 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1276 }
1277
1278 #[test]
1280 #[cfg(target_os = "linux")]
1281 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1282 if !is_dma_available() || !is_g2d_available() {
1283 return Ok(());
1284 }
1285 let src = load_raw_image(
1287 1280,
1288 720,
1289 PixelFormat::Yuyv,
1290 Some(TensorMemory::DmaBuf),
1291 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1292 )?;
1293
1294 let reference = load_raw_image(
1296 1280,
1297 720,
1298 PixelFormat::Rgba,
1299 None,
1300 &edgefirst_bench::testdata::read("camera720p.rgba"),
1301 )?;
1302
1303 let mut dst = TensorDyn::image(
1305 1280,
1306 720,
1307 PixelFormat::Rgba,
1308 DType::U8,
1309 Some(TensorMemory::DmaBuf),
1310 edgefirst_tensor::CpuAccess::ReadWrite,
1311 )?;
1312 let mut g2d = G2DProcessor::new()?;
1313 let src_dyn = src;
1314 let mut dst_dyn = dst;
1315 g2d.convert(
1316 &src_dyn,
1317 &mut dst_dyn,
1318 Rotation::None,
1319 Flip::None,
1320 Crop::no_crop(),
1321 )?;
1322 dst = {
1323 let mut __t = dst_dyn.into_u8().unwrap();
1324 __t.set_format(PixelFormat::Rgba)
1325 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1326 TensorDyn::from(__t)
1327 };
1328
1329 let cpu_dst = TensorDyn::image(
1331 1280,
1332 720,
1333 PixelFormat::Rgba,
1334 DType::U8,
1335 None,
1336 edgefirst_tensor::CpuAccess::ReadWrite,
1337 )?;
1338 cpu_dst
1339 .as_typed::<u8>()
1340 .unwrap()
1341 .map()?
1342 .as_mut_slice()
1343 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1344
1345 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1346 }
1347
1348 #[test]
1350 #[cfg(target_os = "linux")]
1351 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1352 if !is_dma_available() || !is_g2d_available() {
1353 return Ok(());
1354 }
1355 let src = load_raw_image(
1357 1280,
1358 720,
1359 PixelFormat::Yuyv,
1360 Some(TensorMemory::DmaBuf),
1361 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1362 )?;
1363
1364 let reference = load_raw_image(
1366 1280,
1367 720,
1368 PixelFormat::Rgb,
1369 None,
1370 &edgefirst_bench::testdata::read("camera720p.rgb"),
1371 )?;
1372
1373 let mut dst = TensorDyn::image(
1375 1280,
1376 720,
1377 PixelFormat::Rgb,
1378 DType::U8,
1379 Some(TensorMemory::DmaBuf),
1380 edgefirst_tensor::CpuAccess::ReadWrite,
1381 )?;
1382 let mut g2d = G2DProcessor::new()?;
1383 let src_dyn = src;
1384 let mut dst_dyn = dst;
1385 g2d.convert(
1386 &src_dyn,
1387 &mut dst_dyn,
1388 Rotation::None,
1389 Flip::None,
1390 Crop::no_crop(),
1391 )?;
1392 dst = {
1393 let mut __t = dst_dyn.into_u8().unwrap();
1394 __t.set_format(PixelFormat::Rgb)
1395 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1396 TensorDyn::from(__t)
1397 };
1398
1399 let cpu_dst = TensorDyn::image(
1401 1280,
1402 720,
1403 PixelFormat::Rgb,
1404 DType::U8,
1405 None,
1406 edgefirst_tensor::CpuAccess::ReadWrite,
1407 )?;
1408 cpu_dst
1409 .as_typed::<u8>()
1410 .unwrap()
1411 .map()?
1412 .as_mut_slice()
1413 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1414
1415 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1416 }
1417
1418 #[test]
1421 #[cfg(target_os = "linux")]
1422 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1423 fn test_g2d_bgra_no_resize() {
1424 for src_fmt in [
1425 PixelFormat::Rgba,
1426 PixelFormat::Yuyv,
1427 PixelFormat::Nv12,
1428 PixelFormat::Bgra,
1429 ] {
1430 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1431 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1432 });
1433 }
1434 }
1435
1436 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1437 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1438 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1439
1440 let mut src2 = TensorDyn::image(
1442 1280,
1443 720,
1444 g2d_in_fmt,
1445 DType::U8,
1446 Some(TensorMemory::DmaBuf),
1447 edgefirst_tensor::CpuAccess::ReadWrite,
1448 )?;
1449 let mut cpu_converter = CPUProcessor::new();
1450
1451 if g2d_in_fmt == PixelFormat::Nv12 {
1452 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1453 src2.as_typed::<u8>()
1454 .unwrap()
1455 .map()?
1456 .as_mut_slice()
1457 .copy_from_slice(&nv12_bytes);
1458 } else {
1459 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1460 }
1461
1462 let mut g2d = G2DProcessor::new()?;
1463
1464 let mut bgra_dst = TensorDyn::image(
1466 1280,
1467 720,
1468 PixelFormat::Bgra,
1469 DType::U8,
1470 Some(TensorMemory::DmaBuf),
1471 edgefirst_tensor::CpuAccess::ReadWrite,
1472 )?;
1473 let src2_dyn = src2;
1474 let mut bgra_dst_dyn = bgra_dst;
1475 g2d.convert(
1476 &src2_dyn,
1477 &mut bgra_dst_dyn,
1478 Rotation::None,
1479 Flip::None,
1480 Crop::no_crop(),
1481 )?;
1482 bgra_dst = {
1483 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1484 __t.set_format(PixelFormat::Bgra)
1485 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1486 TensorDyn::from(__t)
1487 };
1488
1489 let src2 = {
1491 let mut __t = src2_dyn.into_u8().unwrap();
1492 __t.set_format(g2d_in_fmt)
1493 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1494 TensorDyn::from(__t)
1495 };
1496
1497 let mut rgba_dst = TensorDyn::image(
1499 1280,
1500 720,
1501 PixelFormat::Rgba,
1502 DType::U8,
1503 Some(TensorMemory::DmaBuf),
1504 edgefirst_tensor::CpuAccess::ReadWrite,
1505 )?;
1506 let src2_dyn2 = src2;
1507 let mut rgba_dst_dyn = rgba_dst;
1508 g2d.convert(
1509 &src2_dyn2,
1510 &mut rgba_dst_dyn,
1511 Rotation::None,
1512 Flip::None,
1513 Crop::no_crop(),
1514 )?;
1515 rgba_dst = {
1516 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1517 __t.set_format(PixelFormat::Rgba)
1518 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1519 TensorDyn::from(__t)
1520 };
1521
1522 let bgra_cpu = TensorDyn::image(
1524 1280,
1525 720,
1526 PixelFormat::Bgra,
1527 DType::U8,
1528 None,
1529 edgefirst_tensor::CpuAccess::ReadWrite,
1530 )?;
1531 bgra_cpu
1532 .as_typed::<u8>()
1533 .unwrap()
1534 .map()?
1535 .as_mut_slice()
1536 .copy_from_slice(bgra_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1537
1538 let rgba_cpu = TensorDyn::image(
1539 1280,
1540 720,
1541 PixelFormat::Rgba,
1542 DType::U8,
1543 None,
1544 edgefirst_tensor::CpuAccess::ReadWrite,
1545 )?;
1546 rgba_cpu
1547 .as_typed::<u8>()
1548 .unwrap()
1549 .map()?
1550 .as_mut_slice()
1551 .copy_from_slice(rgba_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1552
1553 let bgra_map = bgra_cpu.as_typed::<u8>().unwrap().map()?;
1555 let rgba_map = rgba_cpu.as_typed::<u8>().unwrap().map()?;
1556 let bgra_buf = bgra_map.as_slice();
1557 let rgba_buf = rgba_map.as_slice();
1558
1559 assert_eq!(bgra_buf.len(), rgba_buf.len());
1560 for (i, (bc, rc)) in bgra_buf
1561 .as_chunks::<4>()
1562 .0
1563 .iter()
1564 .zip(rgba_buf.as_chunks::<4>().0)
1565 .enumerate()
1566 {
1567 assert_eq!(
1568 bc[0], rc[2],
1569 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1570 );
1571 assert_eq!(
1572 bc[1], rc[1],
1573 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1574 );
1575 assert_eq!(
1576 bc[2], rc[0],
1577 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1578 );
1579 assert_eq!(
1580 bc[3], rc[3],
1581 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1582 );
1583 }
1584 Ok(())
1585 }
1586
1587 fn surface_for(
1598 width: usize,
1599 height: usize,
1600 fmt: PixelFormat,
1601 offset: Option<usize>,
1602 row_stride: Option<usize>,
1603 ) -> Result<G2DSurface, crate::Error> {
1604 use edgefirst_tensor::TensorMemory;
1605 let mut t = Tensor::<u8>::image(
1606 width,
1607 height,
1608 fmt,
1609 Some(TensorMemory::DmaBuf),
1610 edgefirst_tensor::CpuAccess::ReadWrite,
1611 )?;
1612 if let Some(o) = offset {
1613 t.set_plane_offset(o);
1614 }
1615 if let Some(s) = row_stride {
1616 t.set_row_stride_unchecked(s);
1617 }
1618 tensor_to_g2d_surface(&t)
1619 }
1620
1621 #[test]
1622 fn g2d_surface_single_plane_no_offset() {
1623 if !is_dma_available() || !is_g2d_available() {
1624 return;
1625 }
1626 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1627 assert_ne!(s.planes[0], 0);
1629 assert_eq!(s.stride, 640);
1630 }
1631
1632 #[test]
1633 fn g2d_surface_single_plane_with_offset() {
1634 if !is_dma_available() || !is_g2d_available() {
1635 return;
1636 }
1637 use edgefirst_tensor::TensorMemory;
1638 let mut t = Tensor::<u8>::image(
1639 640,
1640 480,
1641 PixelFormat::Rgba,
1642 Some(TensorMemory::DmaBuf),
1643 edgefirst_tensor::CpuAccess::ReadWrite,
1644 )
1645 .unwrap();
1646 let s0 = tensor_to_g2d_surface(&t).unwrap();
1647 t.set_plane_offset(4096);
1648 let s1 = tensor_to_g2d_surface(&t).unwrap();
1649 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1650 }
1651
1652 #[test]
1653 fn g2d_surface_single_plane_zero_offset() {
1654 if !is_dma_available() || !is_g2d_available() {
1655 return;
1656 }
1657 use edgefirst_tensor::TensorMemory;
1658 let mut t = Tensor::<u8>::image(
1659 640,
1660 480,
1661 PixelFormat::Rgba,
1662 Some(TensorMemory::DmaBuf),
1663 edgefirst_tensor::CpuAccess::ReadWrite,
1664 )
1665 .unwrap();
1666 let s_none = tensor_to_g2d_surface(&t).unwrap();
1667 t.set_plane_offset(0);
1668 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1669 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1671 }
1672
1673 #[test]
1674 fn g2d_surface_stride_rgba() {
1675 if !is_dma_available() || !is_g2d_available() {
1676 return;
1677 }
1678 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1680 assert_eq!(s_default.stride, 640);
1681
1682 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1684 assert_eq!(s_custom.stride, 704);
1685 }
1686
1687 #[test]
1688 fn g2d_surface_stride_rgb() {
1689 if !is_dma_available() || !is_g2d_available() {
1690 return;
1691 }
1692 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1693 assert_eq!(s_default.stride, 640);
1694
1695 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1697 assert_eq!(s_custom.stride, 660);
1698 }
1699
1700 #[test]
1701 fn g2d_surface_stride_grey() {
1702 if !is_dma_available() || !is_g2d_available() {
1703 return;
1704 }
1705 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1707 Ok(s) => s,
1708 Err(crate::Error::G2D(..)) => return,
1709 Err(e) => panic!("unexpected error: {e:?}"),
1710 };
1711 assert_eq!(s.stride, 1024);
1713 }
1714
1715 #[test]
1716 fn g2d_surface_contiguous_nv12_offset() {
1717 if !is_dma_available() || !is_g2d_available() {
1718 return;
1719 }
1720 use edgefirst_tensor::TensorMemory;
1721 let mut t = Tensor::<u8>::image(
1722 640,
1723 480,
1724 PixelFormat::Nv12,
1725 Some(TensorMemory::DmaBuf),
1726 edgefirst_tensor::CpuAccess::ReadWrite,
1727 )
1728 .unwrap();
1729 let s0 = tensor_to_g2d_surface(&t).unwrap();
1730
1731 t.set_plane_offset(8192);
1732 let s1 = tensor_to_g2d_surface(&t).unwrap();
1733
1734 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1736 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1740 }
1741
1742 #[test]
1743 fn g2d_surface_contiguous_nv12_stride() {
1744 if !is_dma_available() || !is_g2d_available() {
1745 return;
1746 }
1747 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1749 assert_eq!(s.stride, 640);
1750
1751 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1753 assert_eq!(s_padded.stride, 1024);
1754 }
1755
1756 #[test]
1757 fn g2d_surface_multiplane_nv12_offset() {
1758 if !is_dma_available() || !is_g2d_available() {
1759 return;
1760 }
1761 use edgefirst_tensor::TensorMemory;
1762
1763 let mut luma =
1765 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::DmaBuf), Some("luma")).unwrap();
1766 let mut chroma =
1767 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::DmaBuf), Some("chroma")).unwrap();
1768
1769 let luma_base = {
1771 let dma = luma.as_dma().unwrap();
1772 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1773 phys.address()
1774 };
1775 let chroma_base = {
1776 let dma = chroma.as_dma().unwrap();
1777 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1778 phys.address()
1779 };
1780
1781 luma.set_plane_offset(4096);
1783 chroma.set_plane_offset(2048);
1784 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1785 let s = tensor_to_g2d_surface(&combined).unwrap();
1786
1787 assert_eq!(s.planes[0], luma_base + 4096);
1789 assert_eq!(s.planes[1], chroma_base + 2048);
1791 }
1792
1793 #[cfg(target_os = "linux")]
1821 fn fill_patterned_nv12(t: &TensorDyn) {
1822 let w = t.width().unwrap();
1823 let h = t.height().unwrap();
1824 let stride = t.effective_row_stride().unwrap();
1825
1826 let chroma_h = h.div_ceil(2);
1828 let chroma_w = w.div_ceil(2);
1829
1830 let bound = t.as_typed::<u8>().unwrap();
1831 let mut m = bound.map().unwrap();
1832 let buf = m.as_mut_slice();
1833 let uv_start = stride * h;
1834
1835 for r in 0..h {
1837 for c in 0..w {
1838 buf[r * stride + c] = ((r * 3 + c * 5) % 256) as u8;
1839 }
1840 }
1841 for cr_row in 0..chroma_h {
1843 for cc in 0..chroma_w {
1844 let cb_val = ((cc * 7 + cr_row * 11 + 40) % 256) as u8;
1845 let cr_val = ((cc * 13 + cr_row * 3 + 80) % 256) as u8;
1846 let uv_byte = uv_start + cr_row * stride + cc * 2;
1847 buf[uv_byte] = cb_val;
1848 buf[uv_byte + 1] = cr_val;
1849 }
1850 }
1851 }
1852
1853 #[cfg(target_os = "linux")]
1858 fn compare_g2d_vs_cpu_rgba(
1859 g2d_dst: &TensorDyn,
1860 cpu_dst: &TensorDyn,
1861 w: usize,
1862 h: usize,
1863 tol: u32,
1864 ) -> (u32, Option<(usize, usize, usize)>) {
1865 let channels = 4usize; let g2d_t = g2d_dst.as_typed::<u8>().unwrap();
1867 let cpu_t = cpu_dst.as_typed::<u8>().unwrap();
1868 let g2d_stride = g2d_t.effective_row_stride().unwrap_or(w * channels);
1869 let cpu_stride = cpu_t.effective_row_stride().unwrap_or(w * channels);
1870 let g2d_map = g2d_t.map().unwrap();
1871 let cpu_map = cpu_t.map().unwrap();
1872 let g2d_px = g2d_map.as_slice();
1873 let cpu_px = cpu_map.as_slice();
1874 let mut max_diff = 0u32;
1875 let mut first_fail: Option<(usize, usize, usize)> = None;
1876 for row in 0..h {
1877 for col in 0..w {
1878 for ch in 0..3usize {
1879 let gi = row * g2d_stride + col * channels + ch;
1881 let ci = row * cpu_stride + col * channels + ch;
1882 let d = (g2d_px[gi] as i32 - cpu_px[ci] as i32).unsigned_abs();
1883 if d > max_diff {
1884 max_diff = d;
1885 }
1886 if first_fail.is_none() && d > tol {
1887 first_fail = Some((col, row, ch));
1888 }
1889 }
1890 }
1891 }
1892 (max_diff, first_fail)
1893 }
1894
1895 #[test]
1911 #[cfg(target_os = "linux")]
1912 fn d01_nv12_odd_w_g2d_vs_cpu() {
1913 if !is_dma_available() {
1914 eprintln!("SKIPPED: d01_nv12_odd_w_g2d_vs_cpu - DMA not available");
1915 return;
1916 }
1917 let (w, h) = (65usize, 64usize);
1918
1919 let src_dma = TensorDyn::image(
1920 w,
1921 h,
1922 PixelFormat::Nv12,
1923 DType::U8,
1924 Some(TensorMemory::DmaBuf),
1925 edgefirst_tensor::CpuAccess::ReadWrite,
1926 )
1927 .unwrap();
1928 let src_mem = TensorDyn::image(
1929 w,
1930 h,
1931 PixelFormat::Nv12,
1932 DType::U8,
1933 Some(TensorMemory::Mem),
1934 edgefirst_tensor::CpuAccess::ReadWrite,
1935 )
1936 .unwrap();
1937 fill_patterned_nv12(&src_dma);
1938 fill_patterned_nv12(&src_mem);
1939
1940 let mut cpu_dst = TensorDyn::image(
1942 w,
1943 h,
1944 PixelFormat::Rgba,
1945 DType::U8,
1946 None,
1947 edgefirst_tensor::CpuAccess::ReadWrite,
1948 )
1949 .unwrap();
1950 CPUProcessor::new()
1951 .convert(
1952 &src_mem,
1953 &mut cpu_dst,
1954 Rotation::None,
1955 Flip::None,
1956 Crop::no_crop(),
1957 )
1958 .unwrap();
1959
1960 let mut g2d_dst = TensorDyn::image(
1962 w,
1963 h,
1964 PixelFormat::Rgba,
1965 DType::U8,
1966 Some(TensorMemory::DmaBuf),
1967 edgefirst_tensor::CpuAccess::ReadWrite,
1968 )
1969 .unwrap();
1970 let mut g2d = match G2DProcessor::new() {
1971 Ok(g) => g,
1972 Err(e) => {
1973 eprintln!("SKIPPED: d01_nv12_odd_w_g2d_vs_cpu - G2D not available: {e}");
1974 return;
1975 }
1976 };
1977
1978 g2d.convert(
1980 &src_dma,
1981 &mut g2d_dst,
1982 Rotation::None,
1983 Flip::None,
1984 Crop::no_crop(),
1985 )
1986 .unwrap_or_else(|e| {
1987 panic!("D-01: G2D rejected odd-width (65) NV12 source: {e}");
1988 });
1989
1990 let tol = 4u32;
1992 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
1993 eprintln!("D-01 NV12 odd-W G2D vs CPU: max_diff={max_diff}");
1994 if max_diff > tol {
2001 eprintln!(
2002 "WARNING: D-01 NV12 odd-W G2D vs CPU max_diff={max_diff} > {tol} \
2003 (G2D fixed-point rounding; first bad at {first_fail:?})"
2004 );
2005 }
2006 assert!(
2007 max_diff <= 35,
2008 "D-01: gross NV12 odd-W G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2009 );
2010 }
2011
2012 #[test]
2024 #[cfg(target_os = "linux")]
2025 fn d03_nv12_odd_both_g2d_vs_cpu() {
2026 if !is_dma_available() {
2027 eprintln!("SKIPPED: d03_nv12_odd_both_g2d_vs_cpu - DMA not available");
2028 return;
2029 }
2030 let (w, h) = (65usize, 63usize);
2031
2032 let src_dma = TensorDyn::image(
2033 w,
2034 h,
2035 PixelFormat::Nv12,
2036 DType::U8,
2037 Some(TensorMemory::DmaBuf),
2038 edgefirst_tensor::CpuAccess::ReadWrite,
2039 )
2040 .unwrap();
2041 let src_mem = TensorDyn::image(
2042 w,
2043 h,
2044 PixelFormat::Nv12,
2045 DType::U8,
2046 Some(TensorMemory::Mem),
2047 edgefirst_tensor::CpuAccess::ReadWrite,
2048 )
2049 .unwrap();
2050 fill_patterned_nv12(&src_dma);
2051 fill_patterned_nv12(&src_mem);
2052
2053 let mut cpu_dst = TensorDyn::image(
2055 w,
2056 h,
2057 PixelFormat::Rgba,
2058 DType::U8,
2059 None,
2060 edgefirst_tensor::CpuAccess::ReadWrite,
2061 )
2062 .unwrap();
2063 CPUProcessor::new()
2064 .convert(
2065 &src_mem,
2066 &mut cpu_dst,
2067 Rotation::None,
2068 Flip::None,
2069 Crop::no_crop(),
2070 )
2071 .unwrap();
2072
2073 let mut g2d_dst = TensorDyn::image(
2075 w,
2076 h,
2077 PixelFormat::Rgba,
2078 DType::U8,
2079 Some(TensorMemory::DmaBuf),
2080 edgefirst_tensor::CpuAccess::ReadWrite,
2081 )
2082 .unwrap();
2083 let mut g2d = match G2DProcessor::new() {
2084 Ok(g) => g,
2085 Err(e) => {
2086 eprintln!("SKIPPED: d03_nv12_odd_both_g2d_vs_cpu - G2D not available: {e}");
2087 return;
2088 }
2089 };
2090
2091 g2d.convert(
2094 &src_dma,
2095 &mut g2d_dst,
2096 Rotation::None,
2097 Flip::None,
2098 Crop::no_crop(),
2099 )
2100 .unwrap_or_else(|e| {
2101 panic!("D-03: G2D rejected odd-both (65×63) NV12 source: {e}");
2102 });
2103
2104 let tol = 4u32;
2105 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
2106 eprintln!("D-03 NV12 odd-both G2D vs CPU: max_diff={max_diff}");
2107 if max_diff > tol {
2111 eprintln!(
2112 "WARNING: D-03 NV12 odd-both G2D vs CPU max_diff={max_diff} > {tol} \
2113 (G2D fixed-point rounding; first bad at {first_fail:?})"
2114 );
2115 }
2116 assert!(
2117 max_diff <= 35,
2118 "D-03: gross NV12 odd-both G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2119 );
2120 }
2121}