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 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_u8().unwrap();
198 let dst = if is_int8_dst {
201 let i8_tensor = dst_dyn.as_i8_mut().unwrap();
209 unsafe { &mut *(i8_tensor as *mut Tensor<i8> as *mut Tensor<u8>) }
210 } else {
211 dst_dyn.as_u8_mut().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_u8_mut().ok_or(Error::NotAnImage)?;
455
456 if dst.as_dma().is_none() {
460 return Err(Error::NotImplemented(
461 "g2d only supports Dma memory".to_string(),
462 ));
463 }
464
465 let mut dst_surface = tensor_to_g2d_surface(dst)?;
466
467 match background {
468 None => {
469 let start = Instant::now();
473 g2d.clear(&mut dst_surface, [0, 0, 0, 0])?;
474 g2d.finish()?;
475 log::trace!("g2d clear (empty frame) takes {:?}", start.elapsed());
476 }
477 Some(bg_dyn) => {
478 if bg_dyn.shape() != dst.shape() {
483 return Err(Error::InvalidShape(
484 "background shape does not match dst".into(),
485 ));
486 }
487 if bg_dyn.format() != dst.format() {
488 return Err(Error::InvalidShape(
489 "background pixel format does not match dst".into(),
490 ));
491 }
492 if bg_dyn.dtype() != DType::U8 {
493 return Err(Error::NotSupported(
494 "G2D only supports u8 background tensors".to_string(),
495 ));
496 }
497 let bg = bg_dyn.as_u8().ok_or(Error::NotAnImage)?;
498 if bg.as_dma().is_none() {
499 return Err(Error::NotImplemented(
500 "g2d background must be Dma-backed".to_string(),
501 ));
502 }
503 let src_surface = tensor_to_g2d_surface(bg)?;
504 let start = Instant::now();
505 g2d.blit(&src_surface, &dst_surface)?;
506 g2d.finish()?;
507 log::trace!("g2d blit (bg→dst) takes {:?}", start.elapsed());
508 }
509 }
510 Ok(())
511}
512
513fn chroma_subsample_shifts(fmt: PixelFormat) -> (u32, u32) {
517 match fmt.chroma_layout() {
518 Some(cl) => (cl.shift_x, cl.shift_y),
519 None if matches!(fmt, PixelFormat::Yuyv | PixelFormat::Vyuy) => (1, 0),
522 None => (0, 0),
523 }
524}
525
526fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
530 let fmt = img.format().ok_or(Error::NotAnImage)?;
531 let dma = img
532 .as_dma()
533 .ok_or_else(|| Error::NotImplemented("g2d only supports Dma memory".to_string()))?;
534 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into()?;
535
536 let base_addr = phys.address();
543 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
544 let planes = if fmt == PixelFormat::Nv12 {
545 if img.is_multiplane() {
546 let chroma = img.chroma().unwrap();
548 let chroma_dma = chroma.as_dma().ok_or_else(|| {
549 Error::NotImplemented("g2d multiplane chroma must be DMA-backed".to_string())
550 })?;
551 let uv_phys: G2DPhysical = chroma_dma.fd.as_raw_fd().try_into()?;
552 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
553 [
554 base_addr + luma_offset,
555 uv_phys.address() + chroma_offset,
556 0,
557 ]
558 } else {
559 let w = img.width().unwrap();
560 let h = img.height().unwrap();
561 let stride = img.effective_row_stride().unwrap_or(w.next_multiple_of(2));
562 let uv_offset = (luma_offset as usize + stride * h) as u64;
563 [base_addr + luma_offset, base_addr + uv_offset, 0]
564 }
565 } else {
566 [base_addr + luma_offset, 0, 0]
567 };
568
569 let w = img.width().unwrap();
570 let h = img.height().unwrap();
571 let fourcc = pixelfmt_to_fourcc(fmt);
572
573 let stride_pixels = match img.effective_row_stride() {
576 Some(s) => {
577 let channels = fmt.channels();
578 if s % channels != 0 {
579 return Err(Error::NotImplemented(
580 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
581 ));
582 }
583 s / channels
584 }
585 None => w,
586 };
587
588 Ok(G2DSurface {
589 planes,
590 format: G2DFormat::try_from(fourcc)?.format(),
591 left: 0,
592 top: 0,
593 right: w as i32,
594 bottom: h as i32,
595 stride: stride_pixels as i32,
596 width: w as i32,
597 height: h as i32,
598 blendfunc: 0,
599 clrcolor: 0,
600 rot: 0,
601 global_alpha: 0,
602 })
603}
604
605#[cfg(test)]
606mod g2d_predicate_tests {
607 use super::*;
608 use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry};
609
610 #[test]
611 fn g2d_declines_full_range_and_bt2020_yuv() {
612 let full = Colorimetry::default()
613 .with_range(ColorRange::Full)
614 .with_encoding(ColorEncoding::Bt709);
615 let lim = Colorimetry::default()
616 .with_range(ColorRange::Limited)
617 .with_encoding(ColorEncoding::Bt709);
618 let bt2020 = Colorimetry::default()
619 .with_range(ColorRange::Limited)
620 .with_encoding(ColorEncoding::Bt2020);
621 assert!(!g2d_can_handle(&full, true)); assert!(g2d_can_handle(&lim, true)); assert!(!g2d_can_handle(&bt2020, true)); assert!(g2d_can_handle(&full, false)); }
626}
627
628#[cfg(feature = "g2d_test_formats")]
629#[cfg(test)]
630#[allow(deprecated)]
631mod g2d_tests {
632 use super::*;
633 use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rotation};
634 use edgefirst_tensor::{
635 is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
636 };
637 use image::buffer::ConvertBuffer;
638
639 #[cfg(target_os = "linux")]
646 fn is_g2d_available() -> bool {
647 G2DProcessor::new().is_ok()
648 }
649
650 #[test]
651 #[cfg(target_os = "linux")]
652 fn test_g2d_formats_no_resize() {
653 for i in [
654 PixelFormat::Rgba,
655 PixelFormat::Yuyv,
656 PixelFormat::Rgb,
657 PixelFormat::Grey,
658 PixelFormat::Nv12,
659 ] {
660 for o in [
661 PixelFormat::Rgba,
662 PixelFormat::Yuyv,
663 PixelFormat::Rgb,
664 PixelFormat::Grey,
665 ] {
666 let res = test_g2d_format_no_resize_(i, o);
667 if let Err(e) = res {
668 println!("{i} to {o} failed: {e:?}");
669 } else {
670 println!("{i} to {o} success");
671 }
672 }
673 }
674 }
675
676 fn test_g2d_format_no_resize_(
677 g2d_in_fmt: PixelFormat,
678 g2d_out_fmt: PixelFormat,
679 ) -> Result<(), crate::Error> {
680 let dst_width = 1280;
681 let dst_height = 720;
682 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
683 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
684
685 let mut src2 = TensorDyn::image(
687 1280,
688 720,
689 g2d_in_fmt,
690 DType::U8,
691 Some(TensorMemory::Dma),
692 edgefirst_tensor::CpuAccess::ReadWrite,
693 )?;
694
695 let mut cpu_converter = CPUProcessor::new();
696
697 if g2d_in_fmt == PixelFormat::Nv12 {
699 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
700 src2.as_u8()
701 .unwrap()
702 .map()?
703 .as_mut_slice()
704 .copy_from_slice(&nv12_bytes);
705 } else {
706 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
707 }
708
709 let mut g2d_dst = TensorDyn::image(
710 dst_width,
711 dst_height,
712 g2d_out_fmt,
713 DType::U8,
714 Some(TensorMemory::Dma),
715 edgefirst_tensor::CpuAccess::ReadWrite,
716 )?;
717 let mut g2d_converter = G2DProcessor::new()?;
718 let src2_dyn = src2;
719 let mut g2d_dst_dyn = g2d_dst;
720 g2d_converter.convert(
721 &src2_dyn,
722 &mut g2d_dst_dyn,
723 Rotation::None,
724 Flip::None,
725 Crop::no_crop(),
726 )?;
727 g2d_dst = {
728 let mut __t = g2d_dst_dyn.into_u8().unwrap();
729 __t.set_format(g2d_out_fmt)
730 .map_err(|e| crate::Error::Internal(e.to_string()))?;
731 TensorDyn::from(__t)
732 };
733
734 let mut cpu_dst = TensorDyn::image(
735 dst_width,
736 dst_height,
737 PixelFormat::Rgb,
738 DType::U8,
739 None,
740 edgefirst_tensor::CpuAccess::ReadWrite,
741 )?;
742 cpu_converter.convert(
743 &g2d_dst,
744 &mut cpu_dst,
745 Rotation::None,
746 Flip::None,
747 Crop::no_crop(),
748 )?;
749
750 compare_images(
751 &src,
752 &cpu_dst,
753 0.98,
754 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
755 )
756 }
757
758 #[test]
759 #[cfg(target_os = "linux")]
760 fn test_g2d_formats_with_resize() {
761 for i in [
762 PixelFormat::Rgba,
763 PixelFormat::Yuyv,
764 PixelFormat::Rgb,
765 PixelFormat::Grey,
766 PixelFormat::Nv12,
767 ] {
768 for o in [
769 PixelFormat::Rgba,
770 PixelFormat::Yuyv,
771 PixelFormat::Rgb,
772 PixelFormat::Grey,
773 ] {
774 let res = test_g2d_format_with_resize_(i, o);
775 if let Err(e) = res {
776 println!("{i} to {o} failed: {e:?}");
777 } else {
778 println!("{i} to {o} success");
779 }
780 }
781 }
782 }
783
784 #[test]
785 #[cfg(target_os = "linux")]
786 fn test_g2d_formats_with_resize_dst_crop() {
787 for i in [
788 PixelFormat::Rgba,
789 PixelFormat::Yuyv,
790 PixelFormat::Rgb,
791 PixelFormat::Grey,
792 PixelFormat::Nv12,
793 ] {
794 for o in [
795 PixelFormat::Rgba,
796 PixelFormat::Yuyv,
797 PixelFormat::Rgb,
798 PixelFormat::Grey,
799 ] {
800 let res = test_g2d_format_with_resize_dst_crop(i, o);
801 if let Err(e) = res {
802 println!("{i} to {o} failed: {e:?}");
803 } else {
804 println!("{i} to {o} success");
805 }
806 }
807 }
808 }
809
810 fn test_g2d_format_with_resize_(
811 g2d_in_fmt: PixelFormat,
812 g2d_out_fmt: PixelFormat,
813 ) -> Result<(), crate::Error> {
814 let dst_width = 600;
815 let dst_height = 400;
816 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
817 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
818
819 let mut cpu_converter = CPUProcessor::new();
820
821 let mut reference = TensorDyn::image(
822 dst_width,
823 dst_height,
824 PixelFormat::Rgb,
825 DType::U8,
826 Some(TensorMemory::Dma),
827 edgefirst_tensor::CpuAccess::ReadWrite,
828 )?;
829 cpu_converter.convert(
830 &src,
831 &mut reference,
832 Rotation::None,
833 Flip::None,
834 Crop::no_crop(),
835 )?;
836
837 let mut src2 = TensorDyn::image(
839 1280,
840 720,
841 g2d_in_fmt,
842 DType::U8,
843 Some(TensorMemory::Dma),
844 edgefirst_tensor::CpuAccess::ReadWrite,
845 )?;
846
847 if g2d_in_fmt == PixelFormat::Nv12 {
849 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
850 src2.as_u8()
851 .unwrap()
852 .map()?
853 .as_mut_slice()
854 .copy_from_slice(&nv12_bytes);
855 } else {
856 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
857 }
858
859 let mut g2d_dst = TensorDyn::image(
860 dst_width,
861 dst_height,
862 g2d_out_fmt,
863 DType::U8,
864 Some(TensorMemory::Dma),
865 edgefirst_tensor::CpuAccess::ReadWrite,
866 )?;
867 let mut g2d_converter = G2DProcessor::new()?;
868 let src2_dyn = src2;
869 let mut g2d_dst_dyn = g2d_dst;
870 g2d_converter.convert(
871 &src2_dyn,
872 &mut g2d_dst_dyn,
873 Rotation::None,
874 Flip::None,
875 Crop::no_crop(),
876 )?;
877 g2d_dst = {
878 let mut __t = g2d_dst_dyn.into_u8().unwrap();
879 __t.set_format(g2d_out_fmt)
880 .map_err(|e| crate::Error::Internal(e.to_string()))?;
881 TensorDyn::from(__t)
882 };
883
884 let mut cpu_dst = TensorDyn::image(
885 dst_width,
886 dst_height,
887 PixelFormat::Rgb,
888 DType::U8,
889 None,
890 edgefirst_tensor::CpuAccess::ReadWrite,
891 )?;
892 cpu_converter.convert(
893 &g2d_dst,
894 &mut cpu_dst,
895 Rotation::None,
896 Flip::None,
897 Crop::no_crop(),
898 )?;
899
900 compare_images(
901 &reference,
902 &cpu_dst,
903 0.98,
904 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
905 )
906 }
907
908 fn test_g2d_format_with_resize_dst_crop(
909 g2d_in_fmt: PixelFormat,
910 g2d_out_fmt: PixelFormat,
911 ) -> Result<(), crate::Error> {
912 let dst_width = 600;
913 let dst_height = 400;
914 let region = crate::Region::new(100, 100, 200, 100);
919 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
920 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
921
922 let mut cpu_converter = CPUProcessor::new();
923
924 let reference = TensorDyn::image(
925 dst_width,
926 dst_height,
927 PixelFormat::Rgb,
928 DType::U8,
929 Some(TensorMemory::Dma),
930 edgefirst_tensor::CpuAccess::ReadWrite,
931 )?;
932 reference
933 .as_u8()
934 .unwrap()
935 .map()
936 .unwrap()
937 .as_mut_slice()
938 .fill(128);
939 cpu_converter.convert(
940 &src,
941 &mut reference.view(region)?,
942 Rotation::None,
943 Flip::None,
944 Crop::no_crop(),
945 )?;
946
947 let mut src2 = TensorDyn::image(
949 1280,
950 720,
951 g2d_in_fmt,
952 DType::U8,
953 Some(TensorMemory::Dma),
954 edgefirst_tensor::CpuAccess::ReadWrite,
955 )?;
956
957 if g2d_in_fmt == PixelFormat::Nv12 {
959 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
960 src2.as_u8()
961 .unwrap()
962 .map()?
963 .as_mut_slice()
964 .copy_from_slice(&nv12_bytes);
965 } else {
966 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
967 }
968
969 let mut g2d_dst = TensorDyn::image(
970 dst_width,
971 dst_height,
972 g2d_out_fmt,
973 DType::U8,
974 Some(TensorMemory::Dma),
975 edgefirst_tensor::CpuAccess::ReadWrite,
976 )?;
977 g2d_dst
978 .as_u8()
979 .unwrap()
980 .map()
981 .unwrap()
982 .as_mut_slice()
983 .fill(128);
984 let mut g2d_converter = G2DProcessor::new()?;
985 let src2_dyn = src2;
986 let g2d_dst_dyn = g2d_dst;
987 g2d_converter.convert(
988 &src2_dyn,
989 &mut g2d_dst_dyn.view(region)?,
990 Rotation::None,
991 Flip::None,
992 Crop::no_crop(),
993 )?;
994 g2d_dst = {
995 let mut __t = g2d_dst_dyn.into_u8().unwrap();
996 __t.set_format(g2d_out_fmt)
997 .map_err(|e| crate::Error::Internal(e.to_string()))?;
998 TensorDyn::from(__t)
999 };
1000
1001 let mut cpu_dst = TensorDyn::image(
1002 dst_width,
1003 dst_height,
1004 PixelFormat::Rgb,
1005 DType::U8,
1006 None,
1007 edgefirst_tensor::CpuAccess::ReadWrite,
1008 )?;
1009 cpu_converter.convert(
1010 &g2d_dst,
1011 &mut cpu_dst,
1012 Rotation::None,
1013 Flip::None,
1014 Crop::no_crop(),
1015 )?;
1016
1017 compare_images(
1018 &reference,
1019 &cpu_dst,
1020 0.98,
1021 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
1022 )
1023 }
1024
1025 fn compare_images(
1026 img1: &TensorDyn,
1027 img2: &TensorDyn,
1028 threshold: f64,
1029 name: &str,
1030 ) -> Result<(), crate::Error> {
1031 assert_eq!(img1.height(), img2.height(), "Heights differ");
1032 assert_eq!(img1.width(), img2.width(), "Widths differ");
1033 assert_eq!(
1034 img1.format().unwrap(),
1035 img2.format().unwrap(),
1036 "PixelFormat differ"
1037 );
1038 assert!(
1039 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
1040 "format must be Rgb or Rgba for comparison"
1041 );
1042 let image1 = match img1.format().unwrap() {
1043 PixelFormat::Rgb => image::RgbImage::from_vec(
1044 img1.width().unwrap() as u32,
1045 img1.height().unwrap() as u32,
1046 img1.as_u8().unwrap().map().unwrap().to_vec(),
1047 )
1048 .unwrap(),
1049 PixelFormat::Rgba => image::RgbaImage::from_vec(
1050 img1.width().unwrap() as u32,
1051 img1.height().unwrap() as u32,
1052 img1.as_u8().unwrap().map().unwrap().to_vec(),
1053 )
1054 .unwrap()
1055 .convert(),
1056
1057 _ => unreachable!(),
1058 };
1059
1060 let image2 = match img2.format().unwrap() {
1061 PixelFormat::Rgb => image::RgbImage::from_vec(
1062 img2.width().unwrap() as u32,
1063 img2.height().unwrap() as u32,
1064 img2.as_u8().unwrap().map().unwrap().to_vec(),
1065 )
1066 .unwrap(),
1067 PixelFormat::Rgba => image::RgbaImage::from_vec(
1068 img2.width().unwrap() as u32,
1069 img2.height().unwrap() as u32,
1070 img2.as_u8().unwrap().map().unwrap().to_vec(),
1071 )
1072 .unwrap()
1073 .convert(),
1074
1075 _ => unreachable!(),
1076 };
1077
1078 let similarity = image_compare::rgb_similarity_structure(
1079 &image_compare::Algorithm::RootMeanSquared,
1080 &image1,
1081 &image2,
1082 )
1083 .expect("Image Comparison failed");
1084
1085 if similarity.score < threshold {
1086 image1.save(format!("{name}_1.png")).unwrap();
1087 image2.save(format!("{name}_2.png")).unwrap();
1088 return Err(Error::Internal(format!(
1089 "{name}: converted image and target image have similarity score too low: {} < {}",
1090 similarity.score, threshold
1091 )));
1092 }
1093 Ok(())
1094 }
1095
1096 fn load_raw_image(
1102 width: usize,
1103 height: usize,
1104 format: PixelFormat,
1105 memory: Option<TensorMemory>,
1106 bytes: &[u8],
1107 ) -> Result<TensorDyn, crate::Error> {
1108 let img = TensorDyn::image(
1109 width,
1110 height,
1111 format,
1112 DType::U8,
1113 memory,
1114 edgefirst_tensor::CpuAccess::ReadWrite,
1115 )?;
1116 let mut map = img.as_u8().unwrap().map()?;
1117 let dst = map.as_mut_slice();
1118 if bytes.len() > dst.len() {
1119 return Err(crate::Error::InvalidShape(format!(
1120 "load_raw_image: {} input bytes exceed {}-byte image buffer",
1121 bytes.len(),
1122 dst.len()
1123 )));
1124 }
1125 dst[..bytes.len()].copy_from_slice(bytes);
1126 Ok(img)
1127 }
1128
1129 #[test]
1131 #[cfg(target_os = "linux")]
1132 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
1133 if !is_dma_available() || !is_g2d_available() {
1134 return Ok(());
1135 }
1136 let src = load_raw_image(
1138 1280,
1139 720,
1140 PixelFormat::Nv12,
1141 Some(TensorMemory::Dma),
1142 &edgefirst_bench::testdata::read("camera720p.nv12"),
1143 )?;
1144
1145 let reference = load_raw_image(
1147 1280,
1148 720,
1149 PixelFormat::Rgba,
1150 None,
1151 &edgefirst_bench::testdata::read("camera720p.rgba"),
1152 )?;
1153
1154 let mut dst = TensorDyn::image(
1156 1280,
1157 720,
1158 PixelFormat::Rgba,
1159 DType::U8,
1160 Some(TensorMemory::Dma),
1161 edgefirst_tensor::CpuAccess::ReadWrite,
1162 )?;
1163 let mut g2d = G2DProcessor::new()?;
1164 let src_dyn = src;
1165 let mut dst_dyn = dst;
1166 g2d.convert(
1167 &src_dyn,
1168 &mut dst_dyn,
1169 Rotation::None,
1170 Flip::None,
1171 Crop::no_crop(),
1172 )?;
1173 dst = {
1174 let mut __t = dst_dyn.into_u8().unwrap();
1175 __t.set_format(PixelFormat::Rgba)
1176 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1177 TensorDyn::from(__t)
1178 };
1179
1180 let cpu_dst = TensorDyn::image(
1182 1280,
1183 720,
1184 PixelFormat::Rgba,
1185 DType::U8,
1186 None,
1187 edgefirst_tensor::CpuAccess::ReadWrite,
1188 )?;
1189 cpu_dst
1190 .as_u8()
1191 .unwrap()
1192 .map()?
1193 .as_mut_slice()
1194 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1195
1196 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
1197 }
1198
1199 #[test]
1201 #[cfg(target_os = "linux")]
1202 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
1203 if !is_dma_available() || !is_g2d_available() {
1204 return Ok(());
1205 }
1206 let src = load_raw_image(
1208 1280,
1209 720,
1210 PixelFormat::Nv12,
1211 Some(TensorMemory::Dma),
1212 &edgefirst_bench::testdata::read("camera720p.nv12"),
1213 )?;
1214
1215 let reference = load_raw_image(
1217 1280,
1218 720,
1219 PixelFormat::Rgb,
1220 None,
1221 &edgefirst_bench::testdata::read("camera720p.rgb"),
1222 )?;
1223
1224 let mut dst = TensorDyn::image(
1226 1280,
1227 720,
1228 PixelFormat::Rgb,
1229 DType::U8,
1230 Some(TensorMemory::Dma),
1231 edgefirst_tensor::CpuAccess::ReadWrite,
1232 )?;
1233 let mut g2d = G2DProcessor::new()?;
1234 let src_dyn = src;
1235 let mut dst_dyn = dst;
1236 g2d.convert(
1237 &src_dyn,
1238 &mut dst_dyn,
1239 Rotation::None,
1240 Flip::None,
1241 Crop::no_crop(),
1242 )?;
1243 dst = {
1244 let mut __t = dst_dyn.into_u8().unwrap();
1245 __t.set_format(PixelFormat::Rgb)
1246 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1247 TensorDyn::from(__t)
1248 };
1249
1250 let cpu_dst = TensorDyn::image(
1252 1280,
1253 720,
1254 PixelFormat::Rgb,
1255 DType::U8,
1256 None,
1257 edgefirst_tensor::CpuAccess::ReadWrite,
1258 )?;
1259 cpu_dst
1260 .as_u8()
1261 .unwrap()
1262 .map()?
1263 .as_mut_slice()
1264 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1265
1266 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1267 }
1268
1269 #[test]
1271 #[cfg(target_os = "linux")]
1272 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1273 if !is_dma_available() || !is_g2d_available() {
1274 return Ok(());
1275 }
1276 let src = load_raw_image(
1278 1280,
1279 720,
1280 PixelFormat::Yuyv,
1281 Some(TensorMemory::Dma),
1282 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1283 )?;
1284
1285 let reference = load_raw_image(
1287 1280,
1288 720,
1289 PixelFormat::Rgba,
1290 None,
1291 &edgefirst_bench::testdata::read("camera720p.rgba"),
1292 )?;
1293
1294 let mut dst = TensorDyn::image(
1296 1280,
1297 720,
1298 PixelFormat::Rgba,
1299 DType::U8,
1300 Some(TensorMemory::Dma),
1301 edgefirst_tensor::CpuAccess::ReadWrite,
1302 )?;
1303 let mut g2d = G2DProcessor::new()?;
1304 let src_dyn = src;
1305 let mut dst_dyn = dst;
1306 g2d.convert(
1307 &src_dyn,
1308 &mut dst_dyn,
1309 Rotation::None,
1310 Flip::None,
1311 Crop::no_crop(),
1312 )?;
1313 dst = {
1314 let mut __t = dst_dyn.into_u8().unwrap();
1315 __t.set_format(PixelFormat::Rgba)
1316 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1317 TensorDyn::from(__t)
1318 };
1319
1320 let cpu_dst = TensorDyn::image(
1322 1280,
1323 720,
1324 PixelFormat::Rgba,
1325 DType::U8,
1326 None,
1327 edgefirst_tensor::CpuAccess::ReadWrite,
1328 )?;
1329 cpu_dst
1330 .as_u8()
1331 .unwrap()
1332 .map()?
1333 .as_mut_slice()
1334 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1335
1336 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1337 }
1338
1339 #[test]
1341 #[cfg(target_os = "linux")]
1342 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1343 if !is_dma_available() || !is_g2d_available() {
1344 return Ok(());
1345 }
1346 let src = load_raw_image(
1348 1280,
1349 720,
1350 PixelFormat::Yuyv,
1351 Some(TensorMemory::Dma),
1352 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1353 )?;
1354
1355 let reference = load_raw_image(
1357 1280,
1358 720,
1359 PixelFormat::Rgb,
1360 None,
1361 &edgefirst_bench::testdata::read("camera720p.rgb"),
1362 )?;
1363
1364 let mut dst = TensorDyn::image(
1366 1280,
1367 720,
1368 PixelFormat::Rgb,
1369 DType::U8,
1370 Some(TensorMemory::Dma),
1371 edgefirst_tensor::CpuAccess::ReadWrite,
1372 )?;
1373 let mut g2d = G2DProcessor::new()?;
1374 let src_dyn = src;
1375 let mut dst_dyn = dst;
1376 g2d.convert(
1377 &src_dyn,
1378 &mut dst_dyn,
1379 Rotation::None,
1380 Flip::None,
1381 Crop::no_crop(),
1382 )?;
1383 dst = {
1384 let mut __t = dst_dyn.into_u8().unwrap();
1385 __t.set_format(PixelFormat::Rgb)
1386 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1387 TensorDyn::from(__t)
1388 };
1389
1390 let cpu_dst = TensorDyn::image(
1392 1280,
1393 720,
1394 PixelFormat::Rgb,
1395 DType::U8,
1396 None,
1397 edgefirst_tensor::CpuAccess::ReadWrite,
1398 )?;
1399 cpu_dst
1400 .as_u8()
1401 .unwrap()
1402 .map()?
1403 .as_mut_slice()
1404 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1405
1406 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1407 }
1408
1409 #[test]
1412 #[cfg(target_os = "linux")]
1413 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1414 fn test_g2d_bgra_no_resize() {
1415 for src_fmt in [
1416 PixelFormat::Rgba,
1417 PixelFormat::Yuyv,
1418 PixelFormat::Nv12,
1419 PixelFormat::Bgra,
1420 ] {
1421 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1422 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1423 });
1424 }
1425 }
1426
1427 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1428 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1429 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1430
1431 let mut src2 = TensorDyn::image(
1433 1280,
1434 720,
1435 g2d_in_fmt,
1436 DType::U8,
1437 Some(TensorMemory::Dma),
1438 edgefirst_tensor::CpuAccess::ReadWrite,
1439 )?;
1440 let mut cpu_converter = CPUProcessor::new();
1441
1442 if g2d_in_fmt == PixelFormat::Nv12 {
1443 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1444 src2.as_u8()
1445 .unwrap()
1446 .map()?
1447 .as_mut_slice()
1448 .copy_from_slice(&nv12_bytes);
1449 } else {
1450 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1451 }
1452
1453 let mut g2d = G2DProcessor::new()?;
1454
1455 let mut bgra_dst = TensorDyn::image(
1457 1280,
1458 720,
1459 PixelFormat::Bgra,
1460 DType::U8,
1461 Some(TensorMemory::Dma),
1462 edgefirst_tensor::CpuAccess::ReadWrite,
1463 )?;
1464 let src2_dyn = src2;
1465 let mut bgra_dst_dyn = bgra_dst;
1466 g2d.convert(
1467 &src2_dyn,
1468 &mut bgra_dst_dyn,
1469 Rotation::None,
1470 Flip::None,
1471 Crop::no_crop(),
1472 )?;
1473 bgra_dst = {
1474 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1475 __t.set_format(PixelFormat::Bgra)
1476 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1477 TensorDyn::from(__t)
1478 };
1479
1480 let src2 = {
1482 let mut __t = src2_dyn.into_u8().unwrap();
1483 __t.set_format(g2d_in_fmt)
1484 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1485 TensorDyn::from(__t)
1486 };
1487
1488 let mut rgba_dst = TensorDyn::image(
1490 1280,
1491 720,
1492 PixelFormat::Rgba,
1493 DType::U8,
1494 Some(TensorMemory::Dma),
1495 edgefirst_tensor::CpuAccess::ReadWrite,
1496 )?;
1497 let src2_dyn2 = src2;
1498 let mut rgba_dst_dyn = rgba_dst;
1499 g2d.convert(
1500 &src2_dyn2,
1501 &mut rgba_dst_dyn,
1502 Rotation::None,
1503 Flip::None,
1504 Crop::no_crop(),
1505 )?;
1506 rgba_dst = {
1507 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1508 __t.set_format(PixelFormat::Rgba)
1509 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1510 TensorDyn::from(__t)
1511 };
1512
1513 let bgra_cpu = TensorDyn::image(
1515 1280,
1516 720,
1517 PixelFormat::Bgra,
1518 DType::U8,
1519 None,
1520 edgefirst_tensor::CpuAccess::ReadWrite,
1521 )?;
1522 bgra_cpu
1523 .as_u8()
1524 .unwrap()
1525 .map()?
1526 .as_mut_slice()
1527 .copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
1528
1529 let rgba_cpu = TensorDyn::image(
1530 1280,
1531 720,
1532 PixelFormat::Rgba,
1533 DType::U8,
1534 None,
1535 edgefirst_tensor::CpuAccess::ReadWrite,
1536 )?;
1537 rgba_cpu
1538 .as_u8()
1539 .unwrap()
1540 .map()?
1541 .as_mut_slice()
1542 .copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
1543
1544 let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
1546 let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
1547 let bgra_buf = bgra_map.as_slice();
1548 let rgba_buf = rgba_map.as_slice();
1549
1550 assert_eq!(bgra_buf.len(), rgba_buf.len());
1551 for (i, (bc, rc)) in bgra_buf
1552 .chunks_exact(4)
1553 .zip(rgba_buf.chunks_exact(4))
1554 .enumerate()
1555 {
1556 assert_eq!(
1557 bc[0], rc[2],
1558 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1559 );
1560 assert_eq!(
1561 bc[1], rc[1],
1562 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1563 );
1564 assert_eq!(
1565 bc[2], rc[0],
1566 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1567 );
1568 assert_eq!(
1569 bc[3], rc[3],
1570 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1571 );
1572 }
1573 Ok(())
1574 }
1575
1576 fn surface_for(
1587 width: usize,
1588 height: usize,
1589 fmt: PixelFormat,
1590 offset: Option<usize>,
1591 row_stride: Option<usize>,
1592 ) -> Result<G2DSurface, crate::Error> {
1593 use edgefirst_tensor::TensorMemory;
1594 let mut t = Tensor::<u8>::image(
1595 width,
1596 height,
1597 fmt,
1598 Some(TensorMemory::Dma),
1599 edgefirst_tensor::CpuAccess::ReadWrite,
1600 )?;
1601 if let Some(o) = offset {
1602 t.set_plane_offset(o);
1603 }
1604 if let Some(s) = row_stride {
1605 t.set_row_stride_unchecked(s);
1606 }
1607 tensor_to_g2d_surface(&t)
1608 }
1609
1610 #[test]
1611 fn g2d_surface_single_plane_no_offset() {
1612 if !is_dma_available() || !is_g2d_available() {
1613 return;
1614 }
1615 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1616 assert_ne!(s.planes[0], 0);
1618 assert_eq!(s.stride, 640);
1619 }
1620
1621 #[test]
1622 fn g2d_surface_single_plane_with_offset() {
1623 if !is_dma_available() || !is_g2d_available() {
1624 return;
1625 }
1626 use edgefirst_tensor::TensorMemory;
1627 let mut t = Tensor::<u8>::image(
1628 640,
1629 480,
1630 PixelFormat::Rgba,
1631 Some(TensorMemory::Dma),
1632 edgefirst_tensor::CpuAccess::ReadWrite,
1633 )
1634 .unwrap();
1635 let s0 = tensor_to_g2d_surface(&t).unwrap();
1636 t.set_plane_offset(4096);
1637 let s1 = tensor_to_g2d_surface(&t).unwrap();
1638 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1639 }
1640
1641 #[test]
1642 fn g2d_surface_single_plane_zero_offset() {
1643 if !is_dma_available() || !is_g2d_available() {
1644 return;
1645 }
1646 use edgefirst_tensor::TensorMemory;
1647 let mut t = Tensor::<u8>::image(
1648 640,
1649 480,
1650 PixelFormat::Rgba,
1651 Some(TensorMemory::Dma),
1652 edgefirst_tensor::CpuAccess::ReadWrite,
1653 )
1654 .unwrap();
1655 let s_none = tensor_to_g2d_surface(&t).unwrap();
1656 t.set_plane_offset(0);
1657 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1658 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1660 }
1661
1662 #[test]
1663 fn g2d_surface_stride_rgba() {
1664 if !is_dma_available() || !is_g2d_available() {
1665 return;
1666 }
1667 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1669 assert_eq!(s_default.stride, 640);
1670
1671 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1673 assert_eq!(s_custom.stride, 704);
1674 }
1675
1676 #[test]
1677 fn g2d_surface_stride_rgb() {
1678 if !is_dma_available() || !is_g2d_available() {
1679 return;
1680 }
1681 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1682 assert_eq!(s_default.stride, 640);
1683
1684 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1686 assert_eq!(s_custom.stride, 660);
1687 }
1688
1689 #[test]
1690 fn g2d_surface_stride_grey() {
1691 if !is_dma_available() || !is_g2d_available() {
1692 return;
1693 }
1694 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1696 Ok(s) => s,
1697 Err(crate::Error::G2D(..)) => return,
1698 Err(e) => panic!("unexpected error: {e:?}"),
1699 };
1700 assert_eq!(s.stride, 1024);
1702 }
1703
1704 #[test]
1705 fn g2d_surface_contiguous_nv12_offset() {
1706 if !is_dma_available() || !is_g2d_available() {
1707 return;
1708 }
1709 use edgefirst_tensor::TensorMemory;
1710 let mut t = Tensor::<u8>::image(
1711 640,
1712 480,
1713 PixelFormat::Nv12,
1714 Some(TensorMemory::Dma),
1715 edgefirst_tensor::CpuAccess::ReadWrite,
1716 )
1717 .unwrap();
1718 let s0 = tensor_to_g2d_surface(&t).unwrap();
1719
1720 t.set_plane_offset(8192);
1721 let s1 = tensor_to_g2d_surface(&t).unwrap();
1722
1723 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1725 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1729 }
1730
1731 #[test]
1732 fn g2d_surface_contiguous_nv12_stride() {
1733 if !is_dma_available() || !is_g2d_available() {
1734 return;
1735 }
1736 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1738 assert_eq!(s.stride, 640);
1739
1740 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1742 assert_eq!(s_padded.stride, 1024);
1743 }
1744
1745 #[test]
1746 fn g2d_surface_multiplane_nv12_offset() {
1747 if !is_dma_available() || !is_g2d_available() {
1748 return;
1749 }
1750 use edgefirst_tensor::TensorMemory;
1751
1752 let mut luma =
1754 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Dma), Some("luma")).unwrap();
1755 let mut chroma =
1756 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Dma), Some("chroma")).unwrap();
1757
1758 let luma_base = {
1760 let dma = luma.as_dma().unwrap();
1761 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1762 phys.address()
1763 };
1764 let chroma_base = {
1765 let dma = chroma.as_dma().unwrap();
1766 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1767 phys.address()
1768 };
1769
1770 luma.set_plane_offset(4096);
1772 chroma.set_plane_offset(2048);
1773 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1774 let s = tensor_to_g2d_surface(&combined).unwrap();
1775
1776 assert_eq!(s.planes[0], luma_base + 4096);
1778 assert_eq!(s.planes[1], chroma_base + 2048);
1780 }
1781
1782 #[cfg(target_os = "linux")]
1810 fn fill_patterned_nv12(t: &TensorDyn) {
1811 let w = t.width().unwrap();
1812 let h = t.height().unwrap();
1813 let stride = t.effective_row_stride().unwrap();
1814
1815 let chroma_h = h.div_ceil(2);
1817 let chroma_w = w.div_ceil(2);
1818
1819 let bound = t.as_u8().unwrap();
1820 let mut m = bound.map().unwrap();
1821 let buf = m.as_mut_slice();
1822 let uv_start = stride * h;
1823
1824 for r in 0..h {
1826 for c in 0..w {
1827 buf[r * stride + c] = ((r * 3 + c * 5) % 256) as u8;
1828 }
1829 }
1830 for cr_row in 0..chroma_h {
1832 for cc in 0..chroma_w {
1833 let cb_val = ((cc * 7 + cr_row * 11 + 40) % 256) as u8;
1834 let cr_val = ((cc * 13 + cr_row * 3 + 80) % 256) as u8;
1835 let uv_byte = uv_start + cr_row * stride + cc * 2;
1836 buf[uv_byte] = cb_val;
1837 buf[uv_byte + 1] = cr_val;
1838 }
1839 }
1840 }
1841
1842 #[cfg(target_os = "linux")]
1847 fn compare_g2d_vs_cpu_rgba(
1848 g2d_dst: &TensorDyn,
1849 cpu_dst: &TensorDyn,
1850 w: usize,
1851 h: usize,
1852 tol: u32,
1853 ) -> (u32, Option<(usize, usize, usize)>) {
1854 let channels = 4usize; let g2d_t = g2d_dst.as_u8().unwrap();
1856 let cpu_t = cpu_dst.as_u8().unwrap();
1857 let g2d_stride = g2d_t.effective_row_stride().unwrap_or(w * channels);
1858 let cpu_stride = cpu_t.effective_row_stride().unwrap_or(w * channels);
1859 let g2d_map = g2d_t.map().unwrap();
1860 let cpu_map = cpu_t.map().unwrap();
1861 let g2d_px = g2d_map.as_slice();
1862 let cpu_px = cpu_map.as_slice();
1863 let mut max_diff = 0u32;
1864 let mut first_fail: Option<(usize, usize, usize)> = None;
1865 for row in 0..h {
1866 for col in 0..w {
1867 for ch in 0..3usize {
1868 let gi = row * g2d_stride + col * channels + ch;
1870 let ci = row * cpu_stride + col * channels + ch;
1871 let d = (g2d_px[gi] as i32 - cpu_px[ci] as i32).unsigned_abs();
1872 if d > max_diff {
1873 max_diff = d;
1874 }
1875 if first_fail.is_none() && d > tol {
1876 first_fail = Some((col, row, ch));
1877 }
1878 }
1879 }
1880 }
1881 (max_diff, first_fail)
1882 }
1883
1884 #[test]
1900 #[cfg(target_os = "linux")]
1901 fn d01_nv12_odd_w_g2d_vs_cpu() {
1902 if !is_dma_available() {
1903 eprintln!("SKIPPED: d01_nv12_odd_w_g2d_vs_cpu - DMA not available");
1904 return;
1905 }
1906 let (w, h) = (65usize, 64usize);
1907
1908 let src_dma = TensorDyn::image(
1909 w,
1910 h,
1911 PixelFormat::Nv12,
1912 DType::U8,
1913 Some(TensorMemory::Dma),
1914 edgefirst_tensor::CpuAccess::ReadWrite,
1915 )
1916 .unwrap();
1917 let src_mem = TensorDyn::image(
1918 w,
1919 h,
1920 PixelFormat::Nv12,
1921 DType::U8,
1922 Some(TensorMemory::Mem),
1923 edgefirst_tensor::CpuAccess::ReadWrite,
1924 )
1925 .unwrap();
1926 fill_patterned_nv12(&src_dma);
1927 fill_patterned_nv12(&src_mem);
1928
1929 let mut cpu_dst = TensorDyn::image(
1931 w,
1932 h,
1933 PixelFormat::Rgba,
1934 DType::U8,
1935 None,
1936 edgefirst_tensor::CpuAccess::ReadWrite,
1937 )
1938 .unwrap();
1939 CPUProcessor::new()
1940 .convert(
1941 &src_mem,
1942 &mut cpu_dst,
1943 Rotation::None,
1944 Flip::None,
1945 Crop::no_crop(),
1946 )
1947 .unwrap();
1948
1949 let mut g2d_dst = TensorDyn::image(
1951 w,
1952 h,
1953 PixelFormat::Rgba,
1954 DType::U8,
1955 Some(TensorMemory::Dma),
1956 edgefirst_tensor::CpuAccess::ReadWrite,
1957 )
1958 .unwrap();
1959 let mut g2d = match G2DProcessor::new() {
1960 Ok(g) => g,
1961 Err(e) => {
1962 eprintln!("SKIPPED: d01_nv12_odd_w_g2d_vs_cpu - G2D not available: {e}");
1963 return;
1964 }
1965 };
1966
1967 g2d.convert(
1969 &src_dma,
1970 &mut g2d_dst,
1971 Rotation::None,
1972 Flip::None,
1973 Crop::no_crop(),
1974 )
1975 .unwrap_or_else(|e| {
1976 panic!("D-01: G2D rejected odd-width (65) NV12 source: {e}");
1977 });
1978
1979 let tol = 4u32;
1981 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
1982 eprintln!("D-01 NV12 odd-W G2D vs CPU: max_diff={max_diff}");
1983 if max_diff > tol {
1990 eprintln!(
1991 "WARNING: D-01 NV12 odd-W G2D vs CPU max_diff={max_diff} > {tol} \
1992 (G2D fixed-point rounding; first bad at {first_fail:?})"
1993 );
1994 }
1995 assert!(
1996 max_diff <= 35,
1997 "D-01: gross NV12 odd-W G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
1998 );
1999 }
2000
2001 #[test]
2013 #[cfg(target_os = "linux")]
2014 fn d03_nv12_odd_both_g2d_vs_cpu() {
2015 if !is_dma_available() {
2016 eprintln!("SKIPPED: d03_nv12_odd_both_g2d_vs_cpu - DMA not available");
2017 return;
2018 }
2019 let (w, h) = (65usize, 63usize);
2020
2021 let src_dma = TensorDyn::image(
2022 w,
2023 h,
2024 PixelFormat::Nv12,
2025 DType::U8,
2026 Some(TensorMemory::Dma),
2027 edgefirst_tensor::CpuAccess::ReadWrite,
2028 )
2029 .unwrap();
2030 let src_mem = TensorDyn::image(
2031 w,
2032 h,
2033 PixelFormat::Nv12,
2034 DType::U8,
2035 Some(TensorMemory::Mem),
2036 edgefirst_tensor::CpuAccess::ReadWrite,
2037 )
2038 .unwrap();
2039 fill_patterned_nv12(&src_dma);
2040 fill_patterned_nv12(&src_mem);
2041
2042 let mut cpu_dst = TensorDyn::image(
2044 w,
2045 h,
2046 PixelFormat::Rgba,
2047 DType::U8,
2048 None,
2049 edgefirst_tensor::CpuAccess::ReadWrite,
2050 )
2051 .unwrap();
2052 CPUProcessor::new()
2053 .convert(
2054 &src_mem,
2055 &mut cpu_dst,
2056 Rotation::None,
2057 Flip::None,
2058 Crop::no_crop(),
2059 )
2060 .unwrap();
2061
2062 let mut g2d_dst = TensorDyn::image(
2064 w,
2065 h,
2066 PixelFormat::Rgba,
2067 DType::U8,
2068 Some(TensorMemory::Dma),
2069 edgefirst_tensor::CpuAccess::ReadWrite,
2070 )
2071 .unwrap();
2072 let mut g2d = match G2DProcessor::new() {
2073 Ok(g) => g,
2074 Err(e) => {
2075 eprintln!("SKIPPED: d03_nv12_odd_both_g2d_vs_cpu - G2D not available: {e}");
2076 return;
2077 }
2078 };
2079
2080 g2d.convert(
2083 &src_dma,
2084 &mut g2d_dst,
2085 Rotation::None,
2086 Flip::None,
2087 Crop::no_crop(),
2088 )
2089 .unwrap_or_else(|e| {
2090 panic!("D-03: G2D rejected odd-both (65×63) NV12 source: {e}");
2091 });
2092
2093 let tol = 4u32;
2094 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
2095 eprintln!("D-03 NV12 odd-both G2D vs CPU: max_diff={max_diff}");
2096 if max_diff > tol {
2100 eprintln!(
2101 "WARNING: D-03 NV12 odd-both G2D vs CPU max_diff={max_diff} > {tol} \
2102 (G2D fixed-point rounding; first bad at {first_fail:?})"
2103 );
2104 }
2105 assert!(
2106 max_diff <= 35,
2107 "D-03: gross NV12 odd-both G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
2108 );
2109 }
2110}