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}
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 {
361 let start = Instant::now();
362 crate::cpu::apply_int8_xor_bias_rows(dst, dst_fmt)?;
363 log::trace!("g2d int8 XOR 0x80 post-pass takes {:?}", start.elapsed());
364 }
365
366 Ok(())
367 }
368}
369
370impl ImageProcessorTrait for G2DProcessor {
371 fn convert(
372 &mut self,
373 src: &TensorDyn,
374 dst: &mut TensorDyn,
375 rotation: Rotation,
376 flip: Flip,
377 crop: Crop,
378 ) -> Result<()> {
379 let crop = crop.resolve(
385 src.width().unwrap_or(0),
386 src.height().unwrap_or(0),
387 dst.width().unwrap_or(0),
388 dst.height().unwrap_or(0),
389 )?;
390 self.convert_impl(src, dst, rotation, flip, crop)
391 }
392
393 fn draw_decoded_masks(
394 &mut self,
395 dst: &mut TensorDyn,
396 detect: &[crate::DetectBox],
397 segmentation: &[crate::Segmentation],
398 overlay: crate::MaskOverlay<'_>,
399 ) -> Result<()> {
400 if !detect.is_empty() || !segmentation.is_empty() {
404 return Err(Error::NotImplemented(
405 "G2D does not support drawing detection or segmentation overlays".to_string(),
406 ));
407 }
408 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
409 }
410
411 fn draw_proto_masks(
412 &mut self,
413 dst: &mut TensorDyn,
414 detect: &[crate::DetectBox],
415 _proto_data: &crate::ProtoData,
416 overlay: crate::MaskOverlay<'_>,
417 ) -> Result<()> {
418 if !detect.is_empty() {
421 return Err(Error::NotImplemented(
422 "G2D does not support drawing detection or segmentation overlays".to_string(),
423 ));
424 }
425 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
426 }
427
428 fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
429 Err(Error::NotImplemented(
430 "G2D does not support setting colors for rendering detection or segmentation overlays"
431 .to_string(),
432 ))
433 }
434}
435
436fn draw_empty_frame_g2d(
449 g2d: &mut G2D,
450 dst_dyn: &mut TensorDyn,
451 background: Option<&TensorDyn>,
452) -> Result<()> {
453 if dst_dyn.dtype() != DType::U8 {
454 return Err(Error::NotSupported(
455 "G2D only supports u8 destination tensors".to_string(),
456 ));
457 }
458 let dst = dst_dyn.as_typed_mut::<u8>().ok_or(Error::NotAnImage)?;
459
460 if TensorTrait::memory(dst) != TensorMemory::DmaBuf {
470 return Err(Error::NotImplemented(
471 "g2d only supports Dma memory".to_string(),
472 ));
473 }
474
475 let mut dst_surface = tensor_to_g2d_surface(dst)?;
476
477 match background {
478 None => {
479 let start = Instant::now();
483 g2d.clear(&mut dst_surface, [0, 0, 0, 0])?;
484 g2d.finish()?;
485 log::trace!("g2d clear (empty frame) takes {:?}", start.elapsed());
486 }
487 Some(bg_dyn) => {
488 if bg_dyn.shape() != dst.shape() {
493 return Err(Error::InvalidShape(
494 "background shape does not match dst".into(),
495 ));
496 }
497 if bg_dyn.format() != dst.format() {
498 return Err(Error::InvalidShape(
499 "background pixel format does not match dst".into(),
500 ));
501 }
502 if bg_dyn.dtype() != DType::U8 {
503 return Err(Error::NotSupported(
504 "G2D only supports u8 background tensors".to_string(),
505 ));
506 }
507 let bg = bg_dyn.as_typed::<u8>().ok_or(Error::NotAnImage)?;
508 if TensorTrait::memory(bg) != TensorMemory::DmaBuf {
510 return Err(Error::NotImplemented(
511 "g2d background must be Dma-backed".to_string(),
512 ));
513 }
514 let src_surface = tensor_to_g2d_surface(bg)?;
515 let start = Instant::now();
516 g2d.blit(&src_surface, &dst_surface)?;
517 g2d.finish()?;
518 log::trace!("g2d blit (bg→dst) takes {:?}", start.elapsed());
519 }
520 }
521 Ok(())
522}
523
524fn chroma_subsample_shifts(fmt: PixelFormat) -> (u32, u32) {
528 match fmt.chroma_layout() {
529 Some(cl) => (cl.shift_x, cl.shift_y),
530 None if matches!(fmt, PixelFormat::Yuyv | PixelFormat::Vyuy) => (1, 0),
533 None => (0, 0),
534 }
535}
536
537fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
541 let fmt = img.format().ok_or(Error::NotAnImage)?;
542 let fd = img
545 .dmabuf()
546 .map_err(|e| Error::NotImplemented(format!("g2d only supports Dma memory ({e})")))?;
547 let phys: G2DPhysical = fd.as_raw_fd().try_into()?;
548
549 let base_addr = phys.address();
556 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
557 let planes = if fmt == PixelFormat::Nv12 {
558 if img.is_multiplane() {
559 let chroma = img.chroma().unwrap();
561 let chroma_fd = chroma.dmabuf().map_err(|e| {
562 Error::NotImplemented(format!("g2d multiplane chroma must be DMA-backed ({e})"))
563 })?;
564 let uv_phys: G2DPhysical = chroma_fd.as_raw_fd().try_into()?;
565 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
566 [
567 base_addr + luma_offset,
568 uv_phys.address() + chroma_offset,
569 0,
570 ]
571 } else {
572 let w = img.width().unwrap();
573 let h = img.height().unwrap();
574 let stride = img.effective_row_stride().unwrap_or(w.next_multiple_of(2));
575 let uv_offset = (luma_offset as usize + stride * h) as u64;
576 [base_addr + luma_offset, base_addr + uv_offset, 0]
577 }
578 } else {
579 [base_addr + luma_offset, 0, 0]
580 };
581
582 let w = img.width().unwrap();
583 let h = img.height().unwrap();
584 let fourcc = pixelfmt_to_fourcc(fmt);
585
586 let stride_pixels = match img.effective_row_stride() {
589 Some(s) => {
590 let channels = fmt.channels();
591 if s % channels != 0 {
592 return Err(Error::NotImplemented(
593 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
594 ));
595 }
596 s / channels
597 }
598 None => w,
599 };
600
601 Ok(G2DSurface {
602 planes,
603 format: G2DFormat::try_from(fourcc)?.format(),
604 left: 0,
605 top: 0,
606 right: w as i32,
607 bottom: h as i32,
608 stride: stride_pixels as i32,
609 width: w as i32,
610 height: h as i32,
611 blendfunc: 0,
612 clrcolor: 0,
613 rot: 0,
614 global_alpha: 0,
615 })
616}
617
618#[cfg(test)]
619mod g2d_predicate_tests {
620 use super::*;
621 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry};
622
623 #[test]
624 fn g2d_declines_full_range_and_bt2020_yuv() {
625 let full = Colorimetry::default()
626 .with_range(ColorRange::Full)
627 .with_encoding(ColorEncoding::Bt709);
628 let lim = Colorimetry::default()
629 .with_range(ColorRange::Limited)
630 .with_encoding(ColorEncoding::Bt709);
631 let bt2020 = Colorimetry::default()
632 .with_range(ColorRange::Limited)
633 .with_encoding(ColorEncoding::Bt2020);
634 assert!(!g2d_can_handle(&full, true)); assert!(g2d_can_handle(&lim, true)); assert!(!g2d_can_handle(&bt2020, true)); assert!(g2d_can_handle(&full, false)); }
639}
640
641#[cfg(feature = "g2d_test_formats")]
642#[cfg(test)]
643#[allow(deprecated)]
644mod g2d_tests {
645 use super::*;
646 use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rotation};
647 use edgefirst_tensor::{
648 is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
649 };
650 use image::buffer::ConvertBuffer;
651
652 #[cfg(target_os = "linux")]
659 fn is_g2d_available() -> bool {
660 G2DProcessor::new().is_ok()
661 }
662
663 #[test]
664 #[cfg(target_os = "linux")]
665 fn test_g2d_formats_no_resize() {
666 for i in [
667 PixelFormat::Rgba,
668 PixelFormat::Yuyv,
669 PixelFormat::Rgb,
670 PixelFormat::Grey,
671 PixelFormat::Nv12,
672 ] {
673 for o in [
674 PixelFormat::Rgba,
675 PixelFormat::Yuyv,
676 PixelFormat::Rgb,
677 PixelFormat::Grey,
678 ] {
679 let res = test_g2d_format_no_resize_(i, o);
680 if let Err(e) = res {
681 println!("{i} to {o} failed: {e:?}");
682 } else {
683 println!("{i} to {o} success");
684 }
685 }
686 }
687 }
688
689 fn test_g2d_format_no_resize_(
690 g2d_in_fmt: PixelFormat,
691 g2d_out_fmt: PixelFormat,
692 ) -> Result<(), crate::Error> {
693 let dst_width = 1280;
694 let dst_height = 720;
695 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
696 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
697
698 let mut src2 = TensorDyn::image(
700 1280,
701 720,
702 g2d_in_fmt,
703 DType::U8,
704 Some(TensorMemory::DmaBuf),
705 edgefirst_tensor::CpuAccess::ReadWrite,
706 )?;
707
708 let mut cpu_converter = CPUProcessor::new();
709
710 if g2d_in_fmt == PixelFormat::Nv12 {
712 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
713 src2.as_typed::<u8>()
714 .unwrap()
715 .map()?
716 .as_mut_slice()
717 .copy_from_slice(&nv12_bytes);
718 } else {
719 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
720 }
721
722 let mut g2d_dst = TensorDyn::image(
723 dst_width,
724 dst_height,
725 g2d_out_fmt,
726 DType::U8,
727 Some(TensorMemory::DmaBuf),
728 edgefirst_tensor::CpuAccess::ReadWrite,
729 )?;
730 let mut g2d_converter = G2DProcessor::new()?;
731 let src2_dyn = src2;
732 let mut g2d_dst_dyn = g2d_dst;
733 g2d_converter.convert(
734 &src2_dyn,
735 &mut g2d_dst_dyn,
736 Rotation::None,
737 Flip::None,
738 Crop::no_crop(),
739 )?;
740 g2d_dst = {
741 let mut __t = g2d_dst_dyn.into_u8().unwrap();
742 __t.set_format(g2d_out_fmt)
743 .map_err(|e| crate::Error::Internal(e.to_string()))?;
744 TensorDyn::from(__t)
745 };
746
747 let mut cpu_dst = TensorDyn::image(
748 dst_width,
749 dst_height,
750 PixelFormat::Rgb,
751 DType::U8,
752 None,
753 edgefirst_tensor::CpuAccess::ReadWrite,
754 )?;
755 cpu_converter.convert(
756 &g2d_dst,
757 &mut cpu_dst,
758 Rotation::None,
759 Flip::None,
760 Crop::no_crop(),
761 )?;
762
763 compare_images(
764 &src,
765 &cpu_dst,
766 0.98,
767 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
768 )
769 }
770
771 #[test]
772 #[cfg(target_os = "linux")]
773 fn test_g2d_formats_with_resize() {
774 for i in [
775 PixelFormat::Rgba,
776 PixelFormat::Yuyv,
777 PixelFormat::Rgb,
778 PixelFormat::Grey,
779 PixelFormat::Nv12,
780 ] {
781 for o in [
782 PixelFormat::Rgba,
783 PixelFormat::Yuyv,
784 PixelFormat::Rgb,
785 PixelFormat::Grey,
786 ] {
787 let res = test_g2d_format_with_resize_(i, o);
788 if let Err(e) = res {
789 println!("{i} to {o} failed: {e:?}");
790 } else {
791 println!("{i} to {o} success");
792 }
793 }
794 }
795 }
796
797 #[test]
798 #[cfg(target_os = "linux")]
799 fn test_g2d_formats_with_resize_dst_crop() {
800 for i in [
801 PixelFormat::Rgba,
802 PixelFormat::Yuyv,
803 PixelFormat::Rgb,
804 PixelFormat::Grey,
805 PixelFormat::Nv12,
806 ] {
807 for o in [
808 PixelFormat::Rgba,
809 PixelFormat::Yuyv,
810 PixelFormat::Rgb,
811 PixelFormat::Grey,
812 ] {
813 let res = test_g2d_format_with_resize_dst_crop(i, o);
814 if let Err(e) = res {
815 println!("{i} to {o} failed: {e:?}");
816 } else {
817 println!("{i} to {o} success");
818 }
819 }
820 }
821 }
822
823 fn test_g2d_format_with_resize_(
824 g2d_in_fmt: PixelFormat,
825 g2d_out_fmt: PixelFormat,
826 ) -> Result<(), crate::Error> {
827 let dst_width = 600;
828 let dst_height = 400;
829 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
830 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
831
832 let mut cpu_converter = CPUProcessor::new();
833
834 let mut reference = TensorDyn::image(
835 dst_width,
836 dst_height,
837 PixelFormat::Rgb,
838 DType::U8,
839 Some(TensorMemory::DmaBuf),
840 edgefirst_tensor::CpuAccess::ReadWrite,
841 )?;
842 cpu_converter.convert(
843 &src,
844 &mut reference,
845 Rotation::None,
846 Flip::None,
847 Crop::no_crop(),
848 )?;
849
850 let mut src2 = TensorDyn::image(
852 1280,
853 720,
854 g2d_in_fmt,
855 DType::U8,
856 Some(TensorMemory::DmaBuf),
857 edgefirst_tensor::CpuAccess::ReadWrite,
858 )?;
859
860 if g2d_in_fmt == PixelFormat::Nv12 {
862 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
863 src2.as_typed::<u8>()
864 .unwrap()
865 .map()?
866 .as_mut_slice()
867 .copy_from_slice(&nv12_bytes);
868 } else {
869 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
870 }
871
872 let mut g2d_dst = TensorDyn::image(
873 dst_width,
874 dst_height,
875 g2d_out_fmt,
876 DType::U8,
877 Some(TensorMemory::DmaBuf),
878 edgefirst_tensor::CpuAccess::ReadWrite,
879 )?;
880 let mut g2d_converter = G2DProcessor::new()?;
881 let src2_dyn = src2;
882 let mut g2d_dst_dyn = g2d_dst;
883 g2d_converter.convert(
884 &src2_dyn,
885 &mut g2d_dst_dyn,
886 Rotation::None,
887 Flip::None,
888 Crop::no_crop(),
889 )?;
890 g2d_dst = {
891 let mut __t = g2d_dst_dyn.into_u8().unwrap();
892 __t.set_format(g2d_out_fmt)
893 .map_err(|e| crate::Error::Internal(e.to_string()))?;
894 TensorDyn::from(__t)
895 };
896
897 let mut cpu_dst = TensorDyn::image(
898 dst_width,
899 dst_height,
900 PixelFormat::Rgb,
901 DType::U8,
902 None,
903 edgefirst_tensor::CpuAccess::ReadWrite,
904 )?;
905 cpu_converter.convert(
906 &g2d_dst,
907 &mut cpu_dst,
908 Rotation::None,
909 Flip::None,
910 Crop::no_crop(),
911 )?;
912
913 compare_images(
914 &reference,
915 &cpu_dst,
916 0.98,
917 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
918 )
919 }
920
921 fn test_g2d_format_with_resize_dst_crop(
922 g2d_in_fmt: PixelFormat,
923 g2d_out_fmt: PixelFormat,
924 ) -> Result<(), crate::Error> {
925 let dst_width = 600;
926 let dst_height = 400;
927 let region = crate::Region::new(100, 100, 200, 100);
932 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
933 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
934
935 let mut cpu_converter = CPUProcessor::new();
936
937 let reference = TensorDyn::image(
938 dst_width,
939 dst_height,
940 PixelFormat::Rgb,
941 DType::U8,
942 Some(TensorMemory::DmaBuf),
943 edgefirst_tensor::CpuAccess::ReadWrite,
944 )?;
945 reference
946 .as_typed::<u8>()
947 .unwrap()
948 .map()
949 .unwrap()
950 .as_mut_slice()
951 .fill(128);
952 cpu_converter.convert(
953 &src,
954 &mut reference.view(region)?,
955 Rotation::None,
956 Flip::None,
957 Crop::no_crop(),
958 )?;
959
960 let mut src2 = TensorDyn::image(
962 1280,
963 720,
964 g2d_in_fmt,
965 DType::U8,
966 Some(TensorMemory::DmaBuf),
967 edgefirst_tensor::CpuAccess::ReadWrite,
968 )?;
969
970 if g2d_in_fmt == PixelFormat::Nv12 {
972 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
973 src2.as_typed::<u8>()
974 .unwrap()
975 .map()?
976 .as_mut_slice()
977 .copy_from_slice(&nv12_bytes);
978 } else {
979 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
980 }
981
982 let mut g2d_dst = TensorDyn::image(
983 dst_width,
984 dst_height,
985 g2d_out_fmt,
986 DType::U8,
987 Some(TensorMemory::DmaBuf),
988 edgefirst_tensor::CpuAccess::ReadWrite,
989 )?;
990 g2d_dst
991 .as_typed::<u8>()
992 .unwrap()
993 .map()
994 .unwrap()
995 .as_mut_slice()
996 .fill(128);
997 let mut g2d_converter = G2DProcessor::new()?;
998 let src2_dyn = src2;
999 let g2d_dst_dyn = g2d_dst;
1000 g2d_converter.convert(
1001 &src2_dyn,
1002 &mut g2d_dst_dyn.view(region)?,
1003 Rotation::None,
1004 Flip::None,
1005 Crop::no_crop(),
1006 )?;
1007 g2d_dst = {
1008 let mut __t = g2d_dst_dyn.into_u8().unwrap();
1009 __t.set_format(g2d_out_fmt)
1010 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1011 TensorDyn::from(__t)
1012 };
1013
1014 let mut cpu_dst = TensorDyn::image(
1015 dst_width,
1016 dst_height,
1017 PixelFormat::Rgb,
1018 DType::U8,
1019 None,
1020 edgefirst_tensor::CpuAccess::ReadWrite,
1021 )?;
1022 cpu_converter.convert(
1023 &g2d_dst,
1024 &mut cpu_dst,
1025 Rotation::None,
1026 Flip::None,
1027 Crop::no_crop(),
1028 )?;
1029
1030 compare_images(
1031 &reference,
1032 &cpu_dst,
1033 0.98,
1034 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
1035 )
1036 }
1037
1038 fn compare_images(
1039 img1: &TensorDyn,
1040 img2: &TensorDyn,
1041 threshold: f64,
1042 name: &str,
1043 ) -> Result<(), crate::Error> {
1044 assert_eq!(img1.height(), img2.height(), "Heights differ");
1045 assert_eq!(img1.width(), img2.width(), "Widths differ");
1046 assert_eq!(
1047 img1.format().unwrap(),
1048 img2.format().unwrap(),
1049 "PixelFormat differ"
1050 );
1051 assert!(
1052 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
1053 "format must be Rgb or Rgba for comparison"
1054 );
1055 let image1 = match img1.format().unwrap() {
1056 PixelFormat::Rgb => image::RgbImage::from_vec(
1057 img1.width().unwrap() as u32,
1058 img1.height().unwrap() as u32,
1059 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1060 )
1061 .unwrap(),
1062 PixelFormat::Rgba => image::RgbaImage::from_vec(
1063 img1.width().unwrap() as u32,
1064 img1.height().unwrap() as u32,
1065 img1.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1066 )
1067 .unwrap()
1068 .convert(),
1069
1070 _ => unreachable!(),
1071 };
1072
1073 let image2 = match img2.format().unwrap() {
1074 PixelFormat::Rgb => image::RgbImage::from_vec(
1075 img2.width().unwrap() as u32,
1076 img2.height().unwrap() as u32,
1077 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1078 )
1079 .unwrap(),
1080 PixelFormat::Rgba => image::RgbaImage::from_vec(
1081 img2.width().unwrap() as u32,
1082 img2.height().unwrap() as u32,
1083 img2.as_typed::<u8>().unwrap().map().unwrap().to_vec(),
1084 )
1085 .unwrap()
1086 .convert(),
1087
1088 _ => unreachable!(),
1089 };
1090
1091 let similarity = image_compare::rgb_similarity_structure(
1092 &image_compare::Algorithm::RootMeanSquared,
1093 &image1,
1094 &image2,
1095 )
1096 .expect("Image Comparison failed");
1097
1098 if similarity.score < threshold {
1099 image1.save(format!("{name}_1.png")).unwrap();
1100 image2.save(format!("{name}_2.png")).unwrap();
1101 return Err(Error::Internal(format!(
1102 "{name}: converted image and target image have similarity score too low: {} < {}",
1103 similarity.score, threshold
1104 )));
1105 }
1106 Ok(())
1107 }
1108
1109 fn load_raw_image(
1115 width: usize,
1116 height: usize,
1117 format: PixelFormat,
1118 memory: Option<TensorMemory>,
1119 bytes: &[u8],
1120 ) -> Result<TensorDyn, crate::Error> {
1121 let img = TensorDyn::image(
1122 width,
1123 height,
1124 format,
1125 DType::U8,
1126 memory,
1127 edgefirst_tensor::CpuAccess::ReadWrite,
1128 )?;
1129 let mut map = img.as_typed::<u8>().unwrap().map()?;
1130 let dst = map.as_mut_slice();
1131 if bytes.len() > dst.len() {
1132 return Err(crate::Error::InvalidShape(format!(
1133 "load_raw_image: {} input bytes exceed {}-byte image buffer",
1134 bytes.len(),
1135 dst.len()
1136 )));
1137 }
1138 dst[..bytes.len()].copy_from_slice(bytes);
1139 Ok(img)
1140 }
1141
1142 #[test]
1144 #[cfg(target_os = "linux")]
1145 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
1146 if !is_dma_available() || !is_g2d_available() {
1147 return Ok(());
1148 }
1149 let src = load_raw_image(
1151 1280,
1152 720,
1153 PixelFormat::Nv12,
1154 Some(TensorMemory::DmaBuf),
1155 &edgefirst_bench::testdata::read("camera720p.nv12"),
1156 )?;
1157
1158 let reference = load_raw_image(
1160 1280,
1161 720,
1162 PixelFormat::Rgba,
1163 None,
1164 &edgefirst_bench::testdata::read("camera720p.rgba"),
1165 )?;
1166
1167 let mut dst = TensorDyn::image(
1169 1280,
1170 720,
1171 PixelFormat::Rgba,
1172 DType::U8,
1173 Some(TensorMemory::DmaBuf),
1174 edgefirst_tensor::CpuAccess::ReadWrite,
1175 )?;
1176 let mut g2d = G2DProcessor::new()?;
1177 let src_dyn = src;
1178 let mut dst_dyn = dst;
1179 g2d.convert(
1180 &src_dyn,
1181 &mut dst_dyn,
1182 Rotation::None,
1183 Flip::None,
1184 Crop::no_crop(),
1185 )?;
1186 dst = {
1187 let mut __t = dst_dyn.into_u8().unwrap();
1188 __t.set_format(PixelFormat::Rgba)
1189 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1190 TensorDyn::from(__t)
1191 };
1192
1193 let cpu_dst = TensorDyn::image(
1195 1280,
1196 720,
1197 PixelFormat::Rgba,
1198 DType::U8,
1199 None,
1200 edgefirst_tensor::CpuAccess::ReadWrite,
1201 )?;
1202 cpu_dst
1203 .as_typed::<u8>()
1204 .unwrap()
1205 .map()?
1206 .as_mut_slice()
1207 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1208
1209 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
1210 }
1211
1212 #[test]
1214 #[cfg(target_os = "linux")]
1215 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
1216 if !is_dma_available() || !is_g2d_available() {
1217 return Ok(());
1218 }
1219 let src = load_raw_image(
1221 1280,
1222 720,
1223 PixelFormat::Nv12,
1224 Some(TensorMemory::DmaBuf),
1225 &edgefirst_bench::testdata::read("camera720p.nv12"),
1226 )?;
1227
1228 let reference = load_raw_image(
1230 1280,
1231 720,
1232 PixelFormat::Rgb,
1233 None,
1234 &edgefirst_bench::testdata::read("camera720p.rgb"),
1235 )?;
1236
1237 let mut dst = TensorDyn::image(
1239 1280,
1240 720,
1241 PixelFormat::Rgb,
1242 DType::U8,
1243 Some(TensorMemory::DmaBuf),
1244 edgefirst_tensor::CpuAccess::ReadWrite,
1245 )?;
1246 let mut g2d = G2DProcessor::new()?;
1247 let src_dyn = src;
1248 let mut dst_dyn = dst;
1249 g2d.convert(
1250 &src_dyn,
1251 &mut dst_dyn,
1252 Rotation::None,
1253 Flip::None,
1254 Crop::no_crop(),
1255 )?;
1256 dst = {
1257 let mut __t = dst_dyn.into_u8().unwrap();
1258 __t.set_format(PixelFormat::Rgb)
1259 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1260 TensorDyn::from(__t)
1261 };
1262
1263 let cpu_dst = TensorDyn::image(
1265 1280,
1266 720,
1267 PixelFormat::Rgb,
1268 DType::U8,
1269 None,
1270 edgefirst_tensor::CpuAccess::ReadWrite,
1271 )?;
1272 cpu_dst
1273 .as_typed::<u8>()
1274 .unwrap()
1275 .map()?
1276 .as_mut_slice()
1277 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1278
1279 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1280 }
1281
1282 #[test]
1284 #[cfg(target_os = "linux")]
1285 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1286 if !is_dma_available() || !is_g2d_available() {
1287 return Ok(());
1288 }
1289 let src = load_raw_image(
1291 1280,
1292 720,
1293 PixelFormat::Yuyv,
1294 Some(TensorMemory::DmaBuf),
1295 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1296 )?;
1297
1298 let reference = load_raw_image(
1300 1280,
1301 720,
1302 PixelFormat::Rgba,
1303 None,
1304 &edgefirst_bench::testdata::read("camera720p.rgba"),
1305 )?;
1306
1307 let mut dst = TensorDyn::image(
1309 1280,
1310 720,
1311 PixelFormat::Rgba,
1312 DType::U8,
1313 Some(TensorMemory::DmaBuf),
1314 edgefirst_tensor::CpuAccess::ReadWrite,
1315 )?;
1316 let mut g2d = G2DProcessor::new()?;
1317 let src_dyn = src;
1318 let mut dst_dyn = dst;
1319 g2d.convert(
1320 &src_dyn,
1321 &mut dst_dyn,
1322 Rotation::None,
1323 Flip::None,
1324 Crop::no_crop(),
1325 )?;
1326 dst = {
1327 let mut __t = dst_dyn.into_u8().unwrap();
1328 __t.set_format(PixelFormat::Rgba)
1329 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1330 TensorDyn::from(__t)
1331 };
1332
1333 let cpu_dst = TensorDyn::image(
1335 1280,
1336 720,
1337 PixelFormat::Rgba,
1338 DType::U8,
1339 None,
1340 edgefirst_tensor::CpuAccess::ReadWrite,
1341 )?;
1342 cpu_dst
1343 .as_typed::<u8>()
1344 .unwrap()
1345 .map()?
1346 .as_mut_slice()
1347 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1348
1349 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1350 }
1351
1352 #[test]
1354 #[cfg(target_os = "linux")]
1355 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1356 if !is_dma_available() || !is_g2d_available() {
1357 return Ok(());
1358 }
1359 let src = load_raw_image(
1361 1280,
1362 720,
1363 PixelFormat::Yuyv,
1364 Some(TensorMemory::DmaBuf),
1365 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1366 )?;
1367
1368 let reference = load_raw_image(
1370 1280,
1371 720,
1372 PixelFormat::Rgb,
1373 None,
1374 &edgefirst_bench::testdata::read("camera720p.rgb"),
1375 )?;
1376
1377 let mut dst = TensorDyn::image(
1379 1280,
1380 720,
1381 PixelFormat::Rgb,
1382 DType::U8,
1383 Some(TensorMemory::DmaBuf),
1384 edgefirst_tensor::CpuAccess::ReadWrite,
1385 )?;
1386 let mut g2d = G2DProcessor::new()?;
1387 let src_dyn = src;
1388 let mut dst_dyn = dst;
1389 g2d.convert(
1390 &src_dyn,
1391 &mut dst_dyn,
1392 Rotation::None,
1393 Flip::None,
1394 Crop::no_crop(),
1395 )?;
1396 dst = {
1397 let mut __t = dst_dyn.into_u8().unwrap();
1398 __t.set_format(PixelFormat::Rgb)
1399 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1400 TensorDyn::from(__t)
1401 };
1402
1403 let cpu_dst = TensorDyn::image(
1405 1280,
1406 720,
1407 PixelFormat::Rgb,
1408 DType::U8,
1409 None,
1410 edgefirst_tensor::CpuAccess::ReadWrite,
1411 )?;
1412 cpu_dst
1413 .as_typed::<u8>()
1414 .unwrap()
1415 .map()?
1416 .as_mut_slice()
1417 .copy_from_slice(dst.as_typed::<u8>().unwrap().map()?.as_slice());
1418
1419 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1420 }
1421
1422 #[test]
1425 #[cfg(target_os = "linux")]
1426 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1427 fn test_g2d_bgra_no_resize() {
1428 for src_fmt in [
1429 PixelFormat::Rgba,
1430 PixelFormat::Yuyv,
1431 PixelFormat::Nv12,
1432 PixelFormat::Bgra,
1433 ] {
1434 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1435 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1436 });
1437 }
1438 }
1439
1440 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1441 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1442 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1443
1444 let mut src2 = TensorDyn::image(
1446 1280,
1447 720,
1448 g2d_in_fmt,
1449 DType::U8,
1450 Some(TensorMemory::DmaBuf),
1451 edgefirst_tensor::CpuAccess::ReadWrite,
1452 )?;
1453 let mut cpu_converter = CPUProcessor::new();
1454
1455 if g2d_in_fmt == PixelFormat::Nv12 {
1456 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1457 src2.as_typed::<u8>()
1458 .unwrap()
1459 .map()?
1460 .as_mut_slice()
1461 .copy_from_slice(&nv12_bytes);
1462 } else {
1463 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1464 }
1465
1466 let mut g2d = G2DProcessor::new()?;
1467
1468 let mut bgra_dst = TensorDyn::image(
1470 1280,
1471 720,
1472 PixelFormat::Bgra,
1473 DType::U8,
1474 Some(TensorMemory::DmaBuf),
1475 edgefirst_tensor::CpuAccess::ReadWrite,
1476 )?;
1477 let src2_dyn = src2;
1478 let mut bgra_dst_dyn = bgra_dst;
1479 g2d.convert(
1480 &src2_dyn,
1481 &mut bgra_dst_dyn,
1482 Rotation::None,
1483 Flip::None,
1484 Crop::no_crop(),
1485 )?;
1486 bgra_dst = {
1487 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1488 __t.set_format(PixelFormat::Bgra)
1489 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1490 TensorDyn::from(__t)
1491 };
1492
1493 let src2 = {
1495 let mut __t = src2_dyn.into_u8().unwrap();
1496 __t.set_format(g2d_in_fmt)
1497 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1498 TensorDyn::from(__t)
1499 };
1500
1501 let mut rgba_dst = TensorDyn::image(
1503 1280,
1504 720,
1505 PixelFormat::Rgba,
1506 DType::U8,
1507 Some(TensorMemory::DmaBuf),
1508 edgefirst_tensor::CpuAccess::ReadWrite,
1509 )?;
1510 let src2_dyn2 = src2;
1511 let mut rgba_dst_dyn = rgba_dst;
1512 g2d.convert(
1513 &src2_dyn2,
1514 &mut rgba_dst_dyn,
1515 Rotation::None,
1516 Flip::None,
1517 Crop::no_crop(),
1518 )?;
1519 rgba_dst = {
1520 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1521 __t.set_format(PixelFormat::Rgba)
1522 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1523 TensorDyn::from(__t)
1524 };
1525
1526 let bgra_cpu = TensorDyn::image(
1528 1280,
1529 720,
1530 PixelFormat::Bgra,
1531 DType::U8,
1532 None,
1533 edgefirst_tensor::CpuAccess::ReadWrite,
1534 )?;
1535 bgra_cpu
1536 .as_typed::<u8>()
1537 .unwrap()
1538 .map()?
1539 .as_mut_slice()
1540 .copy_from_slice(bgra_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1541
1542 let rgba_cpu = TensorDyn::image(
1543 1280,
1544 720,
1545 PixelFormat::Rgba,
1546 DType::U8,
1547 None,
1548 edgefirst_tensor::CpuAccess::ReadWrite,
1549 )?;
1550 rgba_cpu
1551 .as_typed::<u8>()
1552 .unwrap()
1553 .map()?
1554 .as_mut_slice()
1555 .copy_from_slice(rgba_dst.as_typed::<u8>().unwrap().map()?.as_slice());
1556
1557 let bgra_map = bgra_cpu.as_typed::<u8>().unwrap().map()?;
1559 let rgba_map = rgba_cpu.as_typed::<u8>().unwrap().map()?;
1560 let bgra_buf = bgra_map.as_slice();
1561 let rgba_buf = rgba_map.as_slice();
1562
1563 assert_eq!(bgra_buf.len(), rgba_buf.len());
1564 for (i, (bc, rc)) in bgra_buf
1565 .as_chunks::<4>()
1566 .0
1567 .iter()
1568 .zip(rgba_buf.as_chunks::<4>().0)
1569 .enumerate()
1570 {
1571 assert_eq!(
1572 bc[0], rc[2],
1573 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1574 );
1575 assert_eq!(
1576 bc[1], rc[1],
1577 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1578 );
1579 assert_eq!(
1580 bc[2], rc[0],
1581 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1582 );
1583 assert_eq!(
1584 bc[3], rc[3],
1585 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1586 );
1587 }
1588 Ok(())
1589 }
1590
1591 fn surface_for(
1602 width: usize,
1603 height: usize,
1604 fmt: PixelFormat,
1605 offset: Option<usize>,
1606 row_stride: Option<usize>,
1607 ) -> Result<G2DSurface, crate::Error> {
1608 use edgefirst_tensor::TensorMemory;
1609 let mut t = Tensor::<u8>::image(
1610 width,
1611 height,
1612 fmt,
1613 Some(TensorMemory::DmaBuf),
1614 edgefirst_tensor::CpuAccess::ReadWrite,
1615 )?;
1616 if let Some(o) = offset {
1617 t.set_plane_offset(o);
1618 }
1619 if let Some(s) = row_stride {
1620 t.set_row_stride_unchecked(s);
1621 }
1622 tensor_to_g2d_surface(&t)
1623 }
1624
1625 #[test]
1626 fn g2d_surface_single_plane_no_offset() {
1627 if !is_dma_available() || !is_g2d_available() {
1628 return;
1629 }
1630 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1631 assert_ne!(s.planes[0], 0);
1633 assert_eq!(s.stride, 640);
1634 }
1635
1636 #[test]
1637 fn g2d_surface_single_plane_with_offset() {
1638 if !is_dma_available() || !is_g2d_available() {
1639 return;
1640 }
1641 use edgefirst_tensor::TensorMemory;
1642 let mut t = Tensor::<u8>::image(
1643 640,
1644 480,
1645 PixelFormat::Rgba,
1646 Some(TensorMemory::DmaBuf),
1647 edgefirst_tensor::CpuAccess::ReadWrite,
1648 )
1649 .unwrap();
1650 let s0 = tensor_to_g2d_surface(&t).unwrap();
1651 t.set_plane_offset(4096);
1652 let s1 = tensor_to_g2d_surface(&t).unwrap();
1653 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1654 }
1655
1656 #[test]
1657 fn g2d_surface_single_plane_zero_offset() {
1658 if !is_dma_available() || !is_g2d_available() {
1659 return;
1660 }
1661 use edgefirst_tensor::TensorMemory;
1662 let mut t = Tensor::<u8>::image(
1663 640,
1664 480,
1665 PixelFormat::Rgba,
1666 Some(TensorMemory::DmaBuf),
1667 edgefirst_tensor::CpuAccess::ReadWrite,
1668 )
1669 .unwrap();
1670 let s_none = tensor_to_g2d_surface(&t).unwrap();
1671 t.set_plane_offset(0);
1672 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1673 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1675 }
1676
1677 #[test]
1678 fn g2d_surface_stride_rgba() {
1679 if !is_dma_available() || !is_g2d_available() {
1680 return;
1681 }
1682 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1684 assert_eq!(s_default.stride, 640);
1685
1686 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1688 assert_eq!(s_custom.stride, 704);
1689 }
1690
1691 #[test]
1692 fn g2d_surface_stride_rgb() {
1693 if !is_dma_available() || !is_g2d_available() {
1694 return;
1695 }
1696 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1697 assert_eq!(s_default.stride, 640);
1698
1699 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1701 assert_eq!(s_custom.stride, 660);
1702 }
1703
1704 #[test]
1705 fn g2d_surface_stride_grey() {
1706 if !is_dma_available() || !is_g2d_available() {
1707 return;
1708 }
1709 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1711 Ok(s) => s,
1712 Err(crate::Error::G2D(..)) => return,
1713 Err(e) => panic!("unexpected error: {e:?}"),
1714 };
1715 assert_eq!(s.stride, 1024);
1717 }
1718
1719 #[test]
1720 fn g2d_surface_contiguous_nv12_offset() {
1721 if !is_dma_available() || !is_g2d_available() {
1722 return;
1723 }
1724 use edgefirst_tensor::TensorMemory;
1725 let mut t = Tensor::<u8>::image(
1726 640,
1727 480,
1728 PixelFormat::Nv12,
1729 Some(TensorMemory::DmaBuf),
1730 edgefirst_tensor::CpuAccess::ReadWrite,
1731 )
1732 .unwrap();
1733 let s0 = tensor_to_g2d_surface(&t).unwrap();
1734
1735 t.set_plane_offset(8192);
1736 let s1 = tensor_to_g2d_surface(&t).unwrap();
1737
1738 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1740 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1744 }
1745
1746 #[test]
1747 fn g2d_surface_contiguous_nv12_stride() {
1748 if !is_dma_available() || !is_g2d_available() {
1749 return;
1750 }
1751 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1753 assert_eq!(s.stride, 640);
1754
1755 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1757 assert_eq!(s_padded.stride, 1024);
1758 }
1759
1760 #[test]
1761 fn g2d_surface_multiplane_nv12_offset() {
1762 if !is_dma_available() || !is_g2d_available() {
1763 return;
1764 }
1765 use edgefirst_tensor::TensorMemory;
1766
1767 let mut luma =
1769 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::DmaBuf), Some("luma")).unwrap();
1770 let mut chroma =
1771 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::DmaBuf), Some("chroma")).unwrap();
1772
1773 let luma_base = {
1775 let dma = luma.as_dma().unwrap();
1776 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1777 phys.address()
1778 };
1779 let chroma_base = {
1780 let dma = chroma.as_dma().unwrap();
1781 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1782 phys.address()
1783 };
1784
1785 luma.set_plane_offset(4096);
1787 chroma.set_plane_offset(2048);
1788 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1789 let s = tensor_to_g2d_surface(&combined).unwrap();
1790
1791 assert_eq!(s.planes[0], luma_base + 4096);
1793 assert_eq!(s.planes[1], chroma_base + 2048);
1795 }
1796
1797 #[cfg(target_os = "linux")]
1825 fn fill_patterned_nv12(t: &TensorDyn) {
1826 let w = t.width().unwrap();
1827 let h = t.height().unwrap();
1828 let stride = t.effective_row_stride().unwrap();
1829
1830 let chroma_h = h.div_ceil(2);
1832 let chroma_w = w.div_ceil(2);
1833
1834 let bound = t.as_typed::<u8>().unwrap();
1835 let mut m = bound.map().unwrap();
1836 let buf = m.as_mut_slice();
1837 let uv_start = stride * h;
1838
1839 for r in 0..h {
1841 for c in 0..w {
1842 buf[r * stride + c] = ((r * 3 + c * 5) % 256) as u8;
1843 }
1844 }
1845 for cr_row in 0..chroma_h {
1847 for cc in 0..chroma_w {
1848 let cb_val = ((cc * 7 + cr_row * 11 + 40) % 256) as u8;
1849 let cr_val = ((cc * 13 + cr_row * 3 + 80) % 256) as u8;
1850 let uv_byte = uv_start + cr_row * stride + cc * 2;
1851 buf[uv_byte] = cb_val;
1852 buf[uv_byte + 1] = cr_val;
1853 }
1854 }
1855 }
1856
1857 #[cfg(target_os = "linux")]
1862 fn compare_g2d_vs_cpu_rgba(
1863 g2d_dst: &TensorDyn,
1864 cpu_dst: &TensorDyn,
1865 w: usize,
1866 h: usize,
1867 tol: u32,
1868 ) -> (u32, Option<(usize, usize, usize)>) {
1869 let channels = 4usize; let g2d_t = g2d_dst.as_typed::<u8>().unwrap();
1871 let cpu_t = cpu_dst.as_typed::<u8>().unwrap();
1872 let g2d_stride = g2d_t.effective_row_stride().unwrap_or(w * channels);
1873 let cpu_stride = cpu_t.effective_row_stride().unwrap_or(w * channels);
1874 let g2d_map = g2d_t.map().unwrap();
1875 let cpu_map = cpu_t.map().unwrap();
1876 let g2d_px = g2d_map.as_slice();
1877 let cpu_px = cpu_map.as_slice();
1878 let mut max_diff = 0u32;
1879 let mut first_fail: Option<(usize, usize, usize)> = None;
1880 for row in 0..h {
1881 for col in 0..w {
1882 for ch in 0..3usize {
1883 let gi = row * g2d_stride + col * channels + ch;
1885 let ci = row * cpu_stride + col * channels + ch;
1886 let d = (g2d_px[gi] as i32 - cpu_px[ci] as i32).unsigned_abs();
1887 if d > max_diff {
1888 max_diff = d;
1889 }
1890 if first_fail.is_none() && d > tol {
1891 first_fail = Some((col, row, ch));
1892 }
1893 }
1894 }
1895 }
1896 (max_diff, first_fail)
1897 }
1898
1899 #[test]
1915 #[cfg(target_os = "linux")]
1916 fn d01_nv12_odd_w_g2d_vs_cpu() {
1917 if !is_dma_available() {
1918 eprintln!("SKIPPED: d01_nv12_odd_w_g2d_vs_cpu - DMA not available");
1919 return;
1920 }
1921 let (w, h) = (65usize, 64usize);
1922
1923 let src_dma = TensorDyn::image(
1924 w,
1925 h,
1926 PixelFormat::Nv12,
1927 DType::U8,
1928 Some(TensorMemory::DmaBuf),
1929 edgefirst_tensor::CpuAccess::ReadWrite,
1930 )
1931 .unwrap();
1932 let src_mem = TensorDyn::image(
1933 w,
1934 h,
1935 PixelFormat::Nv12,
1936 DType::U8,
1937 Some(TensorMemory::Mem),
1938 edgefirst_tensor::CpuAccess::ReadWrite,
1939 )
1940 .unwrap();
1941 fill_patterned_nv12(&src_dma);
1942 fill_patterned_nv12(&src_mem);
1943
1944 let mut cpu_dst = TensorDyn::image(
1946 w,
1947 h,
1948 PixelFormat::Rgba,
1949 DType::U8,
1950 None,
1951 edgefirst_tensor::CpuAccess::ReadWrite,
1952 )
1953 .unwrap();
1954 CPUProcessor::new()
1955 .convert(
1956 &src_mem,
1957 &mut cpu_dst,
1958 Rotation::None,
1959 Flip::None,
1960 Crop::no_crop(),
1961 )
1962 .unwrap();
1963
1964 let mut g2d_dst = TensorDyn::image(
1966 w,
1967 h,
1968 PixelFormat::Rgba,
1969 DType::U8,
1970 Some(TensorMemory::DmaBuf),
1971 edgefirst_tensor::CpuAccess::ReadWrite,
1972 )
1973 .unwrap();
1974 let mut g2d = match G2DProcessor::new() {
1975 Ok(g) => g,
1976 Err(e) => {
1977 eprintln!("SKIPPED: d01_nv12_odd_w_g2d_vs_cpu - G2D not available: {e}");
1978 return;
1979 }
1980 };
1981
1982 g2d.convert(
1984 &src_dma,
1985 &mut g2d_dst,
1986 Rotation::None,
1987 Flip::None,
1988 Crop::no_crop(),
1989 )
1990 .unwrap_or_else(|e| {
1991 panic!("D-01: G2D rejected odd-width (65) NV12 source: {e}");
1992 });
1993
1994 let tol = 4u32;
1996 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
1997 eprintln!("D-01 NV12 odd-W G2D vs CPU: max_diff={max_diff}");
1998 if max_diff > tol {
2005 eprintln!(
2006 "WARNING: D-01 NV12 odd-W G2D vs CPU max_diff={max_diff} > {tol} \
2007 (G2D fixed-point rounding; first bad at {first_fail:?})"
2008 );
2009 }
2010 assert!(
2011 max_diff <= 35,
2012 "D-01: gross NV12 odd-W G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2013 );
2014 }
2015
2016 #[test]
2028 #[cfg(target_os = "linux")]
2029 fn d03_nv12_odd_both_g2d_vs_cpu() {
2030 if !is_dma_available() {
2031 eprintln!("SKIPPED: d03_nv12_odd_both_g2d_vs_cpu - DMA not available");
2032 return;
2033 }
2034 let (w, h) = (65usize, 63usize);
2035
2036 let src_dma = TensorDyn::image(
2037 w,
2038 h,
2039 PixelFormat::Nv12,
2040 DType::U8,
2041 Some(TensorMemory::DmaBuf),
2042 edgefirst_tensor::CpuAccess::ReadWrite,
2043 )
2044 .unwrap();
2045 let src_mem = TensorDyn::image(
2046 w,
2047 h,
2048 PixelFormat::Nv12,
2049 DType::U8,
2050 Some(TensorMemory::Mem),
2051 edgefirst_tensor::CpuAccess::ReadWrite,
2052 )
2053 .unwrap();
2054 fill_patterned_nv12(&src_dma);
2055 fill_patterned_nv12(&src_mem);
2056
2057 let mut cpu_dst = TensorDyn::image(
2059 w,
2060 h,
2061 PixelFormat::Rgba,
2062 DType::U8,
2063 None,
2064 edgefirst_tensor::CpuAccess::ReadWrite,
2065 )
2066 .unwrap();
2067 CPUProcessor::new()
2068 .convert(
2069 &src_mem,
2070 &mut cpu_dst,
2071 Rotation::None,
2072 Flip::None,
2073 Crop::no_crop(),
2074 )
2075 .unwrap();
2076
2077 let mut g2d_dst = TensorDyn::image(
2079 w,
2080 h,
2081 PixelFormat::Rgba,
2082 DType::U8,
2083 Some(TensorMemory::DmaBuf),
2084 edgefirst_tensor::CpuAccess::ReadWrite,
2085 )
2086 .unwrap();
2087 let mut g2d = match G2DProcessor::new() {
2088 Ok(g) => g,
2089 Err(e) => {
2090 eprintln!("SKIPPED: d03_nv12_odd_both_g2d_vs_cpu - G2D not available: {e}");
2091 return;
2092 }
2093 };
2094
2095 g2d.convert(
2098 &src_dma,
2099 &mut g2d_dst,
2100 Rotation::None,
2101 Flip::None,
2102 Crop::no_crop(),
2103 )
2104 .unwrap_or_else(|e| {
2105 panic!("D-03: G2D rejected odd-both (65×63) NV12 source: {e}");
2106 });
2107
2108 let tol = 4u32;
2109 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
2110 eprintln!("D-03 NV12 odd-both G2D vs CPU: max_diff={max_diff}");
2111 if max_diff > tol {
2115 eprintln!(
2116 "WARNING: D-03 NV12 odd-both G2D vs CPU max_diff={max_diff} > {tol} \
2117 (G2D fixed-point rounding; first bad at {first_fail:?})"
2118 );
2119 }
2120 assert!(
2121 max_diff <= 35,
2122 "D-03: gross NV12 odd-both G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2123 );
2124 }
2125 #[test]
2137 #[cfg(target_os = "linux")]
2138 fn g2d_int8_dst_biases_only_the_pixel_bytes_at_the_declared_stride() {
2139 let skip = |why: &str| {
2143 use std::io::Write;
2144 let _ = writeln!(
2145 &mut std::io::stderr(),
2146 "SKIPPED: g2d_int8_dst_biases_only_the_pixel_bytes_at_the_declared_stride - {why}"
2147 );
2148 };
2149 if !is_dma_available() {
2150 skip("DMA not available");
2151 return;
2152 }
2153 if !is_g2d_available() {
2154 skip("G2D not available");
2155 return;
2156 }
2157 const W: usize = 64;
2158 const H: usize = 16;
2159 const POISON: u8 = 0xAB;
2160 let fmt = PixelFormat::Rgba;
2161 let bpp = fmt.channels();
2162 let row_bytes = W * bpp;
2163 let stride = row_bytes + 64;
2165
2166 let src = TensorDyn::image(
2167 W,
2168 H,
2169 fmt,
2170 DType::U8,
2171 Some(TensorMemory::DmaBuf),
2172 edgefirst_tensor::CpuAccess::ReadWrite,
2173 )
2174 .expect("DMA source");
2175 {
2176 let t = src.as_typed::<u8>().unwrap();
2177 let s = t.effective_row_stride().unwrap();
2178 let mut m = t.map().unwrap();
2179 let buf = m.as_mut_slice();
2180 for y in 0..H {
2181 for i in 0..row_bytes {
2182 buf[y * s + i] = (y * 31 + i * 7) as u8;
2183 }
2184 }
2185 }
2186
2187 let mut u8_dst = TensorDyn::image_with_stride(
2188 W,
2189 H,
2190 fmt,
2191 DType::U8,
2192 stride,
2193 Some(TensorMemory::DmaBuf),
2194 edgefirst_tensor::CpuAccess::ReadWrite,
2195 )
2196 .expect("padded U8 destination");
2197 let mut i8_dst = TensorDyn::image_with_stride(
2198 W,
2199 H,
2200 fmt,
2201 DType::I8,
2202 stride,
2203 Some(TensorMemory::DmaBuf),
2204 edgefirst_tensor::CpuAccess::ReadWrite,
2205 )
2206 .expect("padded I8 destination");
2207 assert_eq!(i8_dst.effective_row_stride(), Some(stride));
2208 u8_dst
2212 .as_typed::<u8>()
2213 .unwrap()
2214 .map()
2215 .unwrap()
2216 .as_mut_slice()
2217 .fill(POISON);
2218 i8_dst
2219 .as_typed::<i8>()
2220 .unwrap()
2221 .map()
2222 .unwrap()
2223 .as_mut_slice()
2224 .fill(POISON as i8);
2225
2226 let mut g2d = G2DProcessor::new().unwrap();
2227 g2d.convert(
2228 &src,
2229 &mut u8_dst,
2230 Rotation::None,
2231 Flip::None,
2232 Crop::no_crop(),
2233 )
2234 .expect("U8 blit");
2235 g2d.convert(
2236 &src,
2237 &mut i8_dst,
2238 Rotation::None,
2239 Flip::None,
2240 Crop::no_crop(),
2241 )
2242 .expect("I8 blit");
2243
2244 let u8_stride = u8_dst.effective_row_stride().unwrap();
2245 let want = u8_dst
2246 .as_typed::<u8>()
2247 .unwrap()
2248 .map()
2249 .unwrap()
2250 .as_slice()
2251 .to_vec();
2252 let got: Vec<u8> = i8_dst
2253 .as_typed::<i8>()
2254 .unwrap()
2255 .map()
2256 .unwrap()
2257 .as_slice()
2258 .iter()
2259 .map(|&v| v as u8)
2260 .collect();
2261 assert!(
2265 got.len() >= H * stride && want.len() >= H * u8_stride,
2266 "a {stride} B pitch over {H} rows needs {} B, mapped {} (I8) and {} (U8)",
2267 H * stride,
2268 got.len(),
2269 want.len()
2270 );
2271 for y in 0..H {
2275 assert!(
2276 want[y * u8_stride + row_bytes..(y + 1) * u8_stride]
2277 .iter()
2278 .all(|&b| b == POISON),
2279 "row {y}: the G2D blit itself wrote past the pixels; the I8 \
2280 assertions below cannot separate that from the bias pass"
2281 );
2282 }
2283 for y in 0..H {
2284 for x in 0..W {
2285 for c in 0..bpp {
2286 let g = got[y * stride + x * bpp + c];
2287 let w = want[y * u8_stride + x * bpp + c];
2288 let expect = if c == bpp - 1 { w } else { w ^ 0x80 };
2289 assert_eq!(g, expect, "pixel ({x},{y}) channel {c}");
2290 }
2291 }
2292 assert!(
2293 got[y * stride + row_bytes..(y + 1) * stride]
2294 .iter()
2295 .all(|&b| b == POISON),
2296 "row {y}: the padding past the pixels was biased"
2297 );
2298 }
2299 }
2300}