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(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
687
688 let mut cpu_converter = CPUProcessor::new();
689
690 if g2d_in_fmt == PixelFormat::Nv12 {
692 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
693 src2.as_u8()
694 .unwrap()
695 .map()?
696 .as_mut_slice()
697 .copy_from_slice(&nv12_bytes);
698 } else {
699 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
700 }
701
702 let mut g2d_dst = TensorDyn::image(
703 dst_width,
704 dst_height,
705 g2d_out_fmt,
706 DType::U8,
707 Some(TensorMemory::Dma),
708 )?;
709 let mut g2d_converter = G2DProcessor::new()?;
710 let src2_dyn = src2;
711 let mut g2d_dst_dyn = g2d_dst;
712 g2d_converter.convert(
713 &src2_dyn,
714 &mut g2d_dst_dyn,
715 Rotation::None,
716 Flip::None,
717 Crop::no_crop(),
718 )?;
719 g2d_dst = {
720 let mut __t = g2d_dst_dyn.into_u8().unwrap();
721 __t.set_format(g2d_out_fmt)
722 .map_err(|e| crate::Error::Internal(e.to_string()))?;
723 TensorDyn::from(__t)
724 };
725
726 let mut cpu_dst =
727 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
728 cpu_converter.convert(
729 &g2d_dst,
730 &mut cpu_dst,
731 Rotation::None,
732 Flip::None,
733 Crop::no_crop(),
734 )?;
735
736 compare_images(
737 &src,
738 &cpu_dst,
739 0.98,
740 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
741 )
742 }
743
744 #[test]
745 #[cfg(target_os = "linux")]
746 fn test_g2d_formats_with_resize() {
747 for i in [
748 PixelFormat::Rgba,
749 PixelFormat::Yuyv,
750 PixelFormat::Rgb,
751 PixelFormat::Grey,
752 PixelFormat::Nv12,
753 ] {
754 for o in [
755 PixelFormat::Rgba,
756 PixelFormat::Yuyv,
757 PixelFormat::Rgb,
758 PixelFormat::Grey,
759 ] {
760 let res = test_g2d_format_with_resize_(i, o);
761 if let Err(e) = res {
762 println!("{i} to {o} failed: {e:?}");
763 } else {
764 println!("{i} to {o} success");
765 }
766 }
767 }
768 }
769
770 #[test]
771 #[cfg(target_os = "linux")]
772 fn test_g2d_formats_with_resize_dst_crop() {
773 for i in [
774 PixelFormat::Rgba,
775 PixelFormat::Yuyv,
776 PixelFormat::Rgb,
777 PixelFormat::Grey,
778 PixelFormat::Nv12,
779 ] {
780 for o in [
781 PixelFormat::Rgba,
782 PixelFormat::Yuyv,
783 PixelFormat::Rgb,
784 PixelFormat::Grey,
785 ] {
786 let res = test_g2d_format_with_resize_dst_crop(i, o);
787 if let Err(e) = res {
788 println!("{i} to {o} failed: {e:?}");
789 } else {
790 println!("{i} to {o} success");
791 }
792 }
793 }
794 }
795
796 fn test_g2d_format_with_resize_(
797 g2d_in_fmt: PixelFormat,
798 g2d_out_fmt: PixelFormat,
799 ) -> Result<(), crate::Error> {
800 let dst_width = 600;
801 let dst_height = 400;
802 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
803 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
804
805 let mut cpu_converter = CPUProcessor::new();
806
807 let mut reference = TensorDyn::image(
808 dst_width,
809 dst_height,
810 PixelFormat::Rgb,
811 DType::U8,
812 Some(TensorMemory::Dma),
813 )?;
814 cpu_converter.convert(
815 &src,
816 &mut reference,
817 Rotation::None,
818 Flip::None,
819 Crop::no_crop(),
820 )?;
821
822 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
824
825 if g2d_in_fmt == PixelFormat::Nv12 {
827 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
828 src2.as_u8()
829 .unwrap()
830 .map()?
831 .as_mut_slice()
832 .copy_from_slice(&nv12_bytes);
833 } else {
834 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
835 }
836
837 let mut g2d_dst = TensorDyn::image(
838 dst_width,
839 dst_height,
840 g2d_out_fmt,
841 DType::U8,
842 Some(TensorMemory::Dma),
843 )?;
844 let mut g2d_converter = G2DProcessor::new()?;
845 let src2_dyn = src2;
846 let mut g2d_dst_dyn = g2d_dst;
847 g2d_converter.convert(
848 &src2_dyn,
849 &mut g2d_dst_dyn,
850 Rotation::None,
851 Flip::None,
852 Crop::no_crop(),
853 )?;
854 g2d_dst = {
855 let mut __t = g2d_dst_dyn.into_u8().unwrap();
856 __t.set_format(g2d_out_fmt)
857 .map_err(|e| crate::Error::Internal(e.to_string()))?;
858 TensorDyn::from(__t)
859 };
860
861 let mut cpu_dst =
862 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
863 cpu_converter.convert(
864 &g2d_dst,
865 &mut cpu_dst,
866 Rotation::None,
867 Flip::None,
868 Crop::no_crop(),
869 )?;
870
871 compare_images(
872 &reference,
873 &cpu_dst,
874 0.98,
875 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
876 )
877 }
878
879 fn test_g2d_format_with_resize_dst_crop(
880 g2d_in_fmt: PixelFormat,
881 g2d_out_fmt: PixelFormat,
882 ) -> Result<(), crate::Error> {
883 let dst_width = 600;
884 let dst_height = 400;
885 let region = crate::Region::new(100, 100, 200, 100);
890 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
891 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
892
893 let mut cpu_converter = CPUProcessor::new();
894
895 let reference = TensorDyn::image(
896 dst_width,
897 dst_height,
898 PixelFormat::Rgb,
899 DType::U8,
900 Some(TensorMemory::Dma),
901 )?;
902 reference
903 .as_u8()
904 .unwrap()
905 .map()
906 .unwrap()
907 .as_mut_slice()
908 .fill(128);
909 cpu_converter.convert(
910 &src,
911 &mut reference.view(region)?,
912 Rotation::None,
913 Flip::None,
914 Crop::no_crop(),
915 )?;
916
917 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
919
920 if g2d_in_fmt == PixelFormat::Nv12 {
922 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
923 src2.as_u8()
924 .unwrap()
925 .map()?
926 .as_mut_slice()
927 .copy_from_slice(&nv12_bytes);
928 } else {
929 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
930 }
931
932 let mut g2d_dst = TensorDyn::image(
933 dst_width,
934 dst_height,
935 g2d_out_fmt,
936 DType::U8,
937 Some(TensorMemory::Dma),
938 )?;
939 g2d_dst
940 .as_u8()
941 .unwrap()
942 .map()
943 .unwrap()
944 .as_mut_slice()
945 .fill(128);
946 let mut g2d_converter = G2DProcessor::new()?;
947 let src2_dyn = src2;
948 let g2d_dst_dyn = g2d_dst;
949 g2d_converter.convert(
950 &src2_dyn,
951 &mut g2d_dst_dyn.view(region)?,
952 Rotation::None,
953 Flip::None,
954 Crop::no_crop(),
955 )?;
956 g2d_dst = {
957 let mut __t = g2d_dst_dyn.into_u8().unwrap();
958 __t.set_format(g2d_out_fmt)
959 .map_err(|e| crate::Error::Internal(e.to_string()))?;
960 TensorDyn::from(__t)
961 };
962
963 let mut cpu_dst =
964 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
965 cpu_converter.convert(
966 &g2d_dst,
967 &mut cpu_dst,
968 Rotation::None,
969 Flip::None,
970 Crop::no_crop(),
971 )?;
972
973 compare_images(
974 &reference,
975 &cpu_dst,
976 0.98,
977 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
978 )
979 }
980
981 fn compare_images(
982 img1: &TensorDyn,
983 img2: &TensorDyn,
984 threshold: f64,
985 name: &str,
986 ) -> Result<(), crate::Error> {
987 assert_eq!(img1.height(), img2.height(), "Heights differ");
988 assert_eq!(img1.width(), img2.width(), "Widths differ");
989 assert_eq!(
990 img1.format().unwrap(),
991 img2.format().unwrap(),
992 "PixelFormat differ"
993 );
994 assert!(
995 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
996 "format must be Rgb or Rgba for comparison"
997 );
998 let image1 = match img1.format().unwrap() {
999 PixelFormat::Rgb => image::RgbImage::from_vec(
1000 img1.width().unwrap() as u32,
1001 img1.height().unwrap() as u32,
1002 img1.as_u8().unwrap().map().unwrap().to_vec(),
1003 )
1004 .unwrap(),
1005 PixelFormat::Rgba => image::RgbaImage::from_vec(
1006 img1.width().unwrap() as u32,
1007 img1.height().unwrap() as u32,
1008 img1.as_u8().unwrap().map().unwrap().to_vec(),
1009 )
1010 .unwrap()
1011 .convert(),
1012
1013 _ => unreachable!(),
1014 };
1015
1016 let image2 = match img2.format().unwrap() {
1017 PixelFormat::Rgb => image::RgbImage::from_vec(
1018 img2.width().unwrap() as u32,
1019 img2.height().unwrap() as u32,
1020 img2.as_u8().unwrap().map().unwrap().to_vec(),
1021 )
1022 .unwrap(),
1023 PixelFormat::Rgba => image::RgbaImage::from_vec(
1024 img2.width().unwrap() as u32,
1025 img2.height().unwrap() as u32,
1026 img2.as_u8().unwrap().map().unwrap().to_vec(),
1027 )
1028 .unwrap()
1029 .convert(),
1030
1031 _ => unreachable!(),
1032 };
1033
1034 let similarity = image_compare::rgb_similarity_structure(
1035 &image_compare::Algorithm::RootMeanSquared,
1036 &image1,
1037 &image2,
1038 )
1039 .expect("Image Comparison failed");
1040
1041 if similarity.score < threshold {
1042 image1.save(format!("{name}_1.png")).unwrap();
1043 image2.save(format!("{name}_2.png")).unwrap();
1044 return Err(Error::Internal(format!(
1045 "{name}: converted image and target image have similarity score too low: {} < {}",
1046 similarity.score, threshold
1047 )));
1048 }
1049 Ok(())
1050 }
1051
1052 fn load_raw_image(
1058 width: usize,
1059 height: usize,
1060 format: PixelFormat,
1061 memory: Option<TensorMemory>,
1062 bytes: &[u8],
1063 ) -> Result<TensorDyn, crate::Error> {
1064 let img = TensorDyn::image(width, height, format, DType::U8, memory)?;
1065 let mut map = img.as_u8().unwrap().map()?;
1066 let dst = map.as_mut_slice();
1067 if bytes.len() > dst.len() {
1068 return Err(crate::Error::InvalidShape(format!(
1069 "load_raw_image: {} input bytes exceed {}-byte image buffer",
1070 bytes.len(),
1071 dst.len()
1072 )));
1073 }
1074 dst[..bytes.len()].copy_from_slice(bytes);
1075 Ok(img)
1076 }
1077
1078 #[test]
1080 #[cfg(target_os = "linux")]
1081 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
1082 if !is_dma_available() || !is_g2d_available() {
1083 return Ok(());
1084 }
1085 let src = load_raw_image(
1087 1280,
1088 720,
1089 PixelFormat::Nv12,
1090 Some(TensorMemory::Dma),
1091 &edgefirst_bench::testdata::read("camera720p.nv12"),
1092 )?;
1093
1094 let reference = load_raw_image(
1096 1280,
1097 720,
1098 PixelFormat::Rgba,
1099 None,
1100 &edgefirst_bench::testdata::read("camera720p.rgba"),
1101 )?;
1102
1103 let mut dst = TensorDyn::image(
1105 1280,
1106 720,
1107 PixelFormat::Rgba,
1108 DType::U8,
1109 Some(TensorMemory::Dma),
1110 )?;
1111 let mut g2d = G2DProcessor::new()?;
1112 let src_dyn = src;
1113 let mut dst_dyn = dst;
1114 g2d.convert(
1115 &src_dyn,
1116 &mut dst_dyn,
1117 Rotation::None,
1118 Flip::None,
1119 Crop::no_crop(),
1120 )?;
1121 dst = {
1122 let mut __t = dst_dyn.into_u8().unwrap();
1123 __t.set_format(PixelFormat::Rgba)
1124 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1125 TensorDyn::from(__t)
1126 };
1127
1128 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1130 cpu_dst
1131 .as_u8()
1132 .unwrap()
1133 .map()?
1134 .as_mut_slice()
1135 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1136
1137 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
1138 }
1139
1140 #[test]
1142 #[cfg(target_os = "linux")]
1143 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
1144 if !is_dma_available() || !is_g2d_available() {
1145 return Ok(());
1146 }
1147 let src = load_raw_image(
1149 1280,
1150 720,
1151 PixelFormat::Nv12,
1152 Some(TensorMemory::Dma),
1153 &edgefirst_bench::testdata::read("camera720p.nv12"),
1154 )?;
1155
1156 let reference = load_raw_image(
1158 1280,
1159 720,
1160 PixelFormat::Rgb,
1161 None,
1162 &edgefirst_bench::testdata::read("camera720p.rgb"),
1163 )?;
1164
1165 let mut dst = TensorDyn::image(
1167 1280,
1168 720,
1169 PixelFormat::Rgb,
1170 DType::U8,
1171 Some(TensorMemory::Dma),
1172 )?;
1173 let mut g2d = G2DProcessor::new()?;
1174 let src_dyn = src;
1175 let mut dst_dyn = dst;
1176 g2d.convert(
1177 &src_dyn,
1178 &mut dst_dyn,
1179 Rotation::None,
1180 Flip::None,
1181 Crop::no_crop(),
1182 )?;
1183 dst = {
1184 let mut __t = dst_dyn.into_u8().unwrap();
1185 __t.set_format(PixelFormat::Rgb)
1186 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1187 TensorDyn::from(__t)
1188 };
1189
1190 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1192 cpu_dst
1193 .as_u8()
1194 .unwrap()
1195 .map()?
1196 .as_mut_slice()
1197 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1198
1199 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1200 }
1201
1202 #[test]
1204 #[cfg(target_os = "linux")]
1205 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1206 if !is_dma_available() || !is_g2d_available() {
1207 return Ok(());
1208 }
1209 let src = load_raw_image(
1211 1280,
1212 720,
1213 PixelFormat::Yuyv,
1214 Some(TensorMemory::Dma),
1215 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1216 )?;
1217
1218 let reference = load_raw_image(
1220 1280,
1221 720,
1222 PixelFormat::Rgba,
1223 None,
1224 &edgefirst_bench::testdata::read("camera720p.rgba"),
1225 )?;
1226
1227 let mut dst = TensorDyn::image(
1229 1280,
1230 720,
1231 PixelFormat::Rgba,
1232 DType::U8,
1233 Some(TensorMemory::Dma),
1234 )?;
1235 let mut g2d = G2DProcessor::new()?;
1236 let src_dyn = src;
1237 let mut dst_dyn = dst;
1238 g2d.convert(
1239 &src_dyn,
1240 &mut dst_dyn,
1241 Rotation::None,
1242 Flip::None,
1243 Crop::no_crop(),
1244 )?;
1245 dst = {
1246 let mut __t = dst_dyn.into_u8().unwrap();
1247 __t.set_format(PixelFormat::Rgba)
1248 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1249 TensorDyn::from(__t)
1250 };
1251
1252 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1254 cpu_dst
1255 .as_u8()
1256 .unwrap()
1257 .map()?
1258 .as_mut_slice()
1259 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1260
1261 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1262 }
1263
1264 #[test]
1266 #[cfg(target_os = "linux")]
1267 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1268 if !is_dma_available() || !is_g2d_available() {
1269 return Ok(());
1270 }
1271 let src = load_raw_image(
1273 1280,
1274 720,
1275 PixelFormat::Yuyv,
1276 Some(TensorMemory::Dma),
1277 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1278 )?;
1279
1280 let reference = load_raw_image(
1282 1280,
1283 720,
1284 PixelFormat::Rgb,
1285 None,
1286 &edgefirst_bench::testdata::read("camera720p.rgb"),
1287 )?;
1288
1289 let mut dst = TensorDyn::image(
1291 1280,
1292 720,
1293 PixelFormat::Rgb,
1294 DType::U8,
1295 Some(TensorMemory::Dma),
1296 )?;
1297 let mut g2d = G2DProcessor::new()?;
1298 let src_dyn = src;
1299 let mut dst_dyn = dst;
1300 g2d.convert(
1301 &src_dyn,
1302 &mut dst_dyn,
1303 Rotation::None,
1304 Flip::None,
1305 Crop::no_crop(),
1306 )?;
1307 dst = {
1308 let mut __t = dst_dyn.into_u8().unwrap();
1309 __t.set_format(PixelFormat::Rgb)
1310 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1311 TensorDyn::from(__t)
1312 };
1313
1314 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1316 cpu_dst
1317 .as_u8()
1318 .unwrap()
1319 .map()?
1320 .as_mut_slice()
1321 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1322
1323 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1324 }
1325
1326 #[test]
1329 #[cfg(target_os = "linux")]
1330 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1331 fn test_g2d_bgra_no_resize() {
1332 for src_fmt in [
1333 PixelFormat::Rgba,
1334 PixelFormat::Yuyv,
1335 PixelFormat::Nv12,
1336 PixelFormat::Bgra,
1337 ] {
1338 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1339 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1340 });
1341 }
1342 }
1343
1344 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1345 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1346 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1347
1348 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
1350 let mut cpu_converter = CPUProcessor::new();
1351
1352 if g2d_in_fmt == PixelFormat::Nv12 {
1353 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1354 src2.as_u8()
1355 .unwrap()
1356 .map()?
1357 .as_mut_slice()
1358 .copy_from_slice(&nv12_bytes);
1359 } else {
1360 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1361 }
1362
1363 let mut g2d = G2DProcessor::new()?;
1364
1365 let mut bgra_dst = TensorDyn::image(
1367 1280,
1368 720,
1369 PixelFormat::Bgra,
1370 DType::U8,
1371 Some(TensorMemory::Dma),
1372 )?;
1373 let src2_dyn = src2;
1374 let mut bgra_dst_dyn = bgra_dst;
1375 g2d.convert(
1376 &src2_dyn,
1377 &mut bgra_dst_dyn,
1378 Rotation::None,
1379 Flip::None,
1380 Crop::no_crop(),
1381 )?;
1382 bgra_dst = {
1383 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1384 __t.set_format(PixelFormat::Bgra)
1385 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1386 TensorDyn::from(__t)
1387 };
1388
1389 let src2 = {
1391 let mut __t = src2_dyn.into_u8().unwrap();
1392 __t.set_format(g2d_in_fmt)
1393 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1394 TensorDyn::from(__t)
1395 };
1396
1397 let mut rgba_dst = TensorDyn::image(
1399 1280,
1400 720,
1401 PixelFormat::Rgba,
1402 DType::U8,
1403 Some(TensorMemory::Dma),
1404 )?;
1405 let src2_dyn2 = src2;
1406 let mut rgba_dst_dyn = rgba_dst;
1407 g2d.convert(
1408 &src2_dyn2,
1409 &mut rgba_dst_dyn,
1410 Rotation::None,
1411 Flip::None,
1412 Crop::no_crop(),
1413 )?;
1414 rgba_dst = {
1415 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1416 __t.set_format(PixelFormat::Rgba)
1417 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1418 TensorDyn::from(__t)
1419 };
1420
1421 let bgra_cpu = TensorDyn::image(1280, 720, PixelFormat::Bgra, DType::U8, None)?;
1423 bgra_cpu
1424 .as_u8()
1425 .unwrap()
1426 .map()?
1427 .as_mut_slice()
1428 .copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
1429
1430 let rgba_cpu = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1431 rgba_cpu
1432 .as_u8()
1433 .unwrap()
1434 .map()?
1435 .as_mut_slice()
1436 .copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
1437
1438 let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
1440 let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
1441 let bgra_buf = bgra_map.as_slice();
1442 let rgba_buf = rgba_map.as_slice();
1443
1444 assert_eq!(bgra_buf.len(), rgba_buf.len());
1445 for (i, (bc, rc)) in bgra_buf
1446 .chunks_exact(4)
1447 .zip(rgba_buf.chunks_exact(4))
1448 .enumerate()
1449 {
1450 assert_eq!(
1451 bc[0], rc[2],
1452 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1453 );
1454 assert_eq!(
1455 bc[1], rc[1],
1456 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1457 );
1458 assert_eq!(
1459 bc[2], rc[0],
1460 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1461 );
1462 assert_eq!(
1463 bc[3], rc[3],
1464 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1465 );
1466 }
1467 Ok(())
1468 }
1469
1470 fn surface_for(
1481 width: usize,
1482 height: usize,
1483 fmt: PixelFormat,
1484 offset: Option<usize>,
1485 row_stride: Option<usize>,
1486 ) -> Result<G2DSurface, crate::Error> {
1487 use edgefirst_tensor::TensorMemory;
1488 let mut t = Tensor::<u8>::image(width, height, fmt, Some(TensorMemory::Dma))?;
1489 if let Some(o) = offset {
1490 t.set_plane_offset(o);
1491 }
1492 if let Some(s) = row_stride {
1493 t.set_row_stride_unchecked(s);
1494 }
1495 tensor_to_g2d_surface(&t)
1496 }
1497
1498 #[test]
1499 fn g2d_surface_single_plane_no_offset() {
1500 if !is_dma_available() || !is_g2d_available() {
1501 return;
1502 }
1503 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1504 assert_ne!(s.planes[0], 0);
1506 assert_eq!(s.stride, 640);
1507 }
1508
1509 #[test]
1510 fn g2d_surface_single_plane_with_offset() {
1511 if !is_dma_available() || !is_g2d_available() {
1512 return;
1513 }
1514 use edgefirst_tensor::TensorMemory;
1515 let mut t =
1516 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1517 let s0 = tensor_to_g2d_surface(&t).unwrap();
1518 t.set_plane_offset(4096);
1519 let s1 = tensor_to_g2d_surface(&t).unwrap();
1520 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1521 }
1522
1523 #[test]
1524 fn g2d_surface_single_plane_zero_offset() {
1525 if !is_dma_available() || !is_g2d_available() {
1526 return;
1527 }
1528 use edgefirst_tensor::TensorMemory;
1529 let mut t =
1530 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1531 let s_none = tensor_to_g2d_surface(&t).unwrap();
1532 t.set_plane_offset(0);
1533 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1534 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1536 }
1537
1538 #[test]
1539 fn g2d_surface_stride_rgba() {
1540 if !is_dma_available() || !is_g2d_available() {
1541 return;
1542 }
1543 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1545 assert_eq!(s_default.stride, 640);
1546
1547 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1549 assert_eq!(s_custom.stride, 704);
1550 }
1551
1552 #[test]
1553 fn g2d_surface_stride_rgb() {
1554 if !is_dma_available() || !is_g2d_available() {
1555 return;
1556 }
1557 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1558 assert_eq!(s_default.stride, 640);
1559
1560 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1562 assert_eq!(s_custom.stride, 660);
1563 }
1564
1565 #[test]
1566 fn g2d_surface_stride_grey() {
1567 if !is_dma_available() || !is_g2d_available() {
1568 return;
1569 }
1570 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1572 Ok(s) => s,
1573 Err(crate::Error::G2D(..)) => return,
1574 Err(e) => panic!("unexpected error: {e:?}"),
1575 };
1576 assert_eq!(s.stride, 1024);
1578 }
1579
1580 #[test]
1581 fn g2d_surface_contiguous_nv12_offset() {
1582 if !is_dma_available() || !is_g2d_available() {
1583 return;
1584 }
1585 use edgefirst_tensor::TensorMemory;
1586 let mut t =
1587 Tensor::<u8>::image(640, 480, PixelFormat::Nv12, Some(TensorMemory::Dma)).unwrap();
1588 let s0 = tensor_to_g2d_surface(&t).unwrap();
1589
1590 t.set_plane_offset(8192);
1591 let s1 = tensor_to_g2d_surface(&t).unwrap();
1592
1593 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1595 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1599 }
1600
1601 #[test]
1602 fn g2d_surface_contiguous_nv12_stride() {
1603 if !is_dma_available() || !is_g2d_available() {
1604 return;
1605 }
1606 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1608 assert_eq!(s.stride, 640);
1609
1610 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1612 assert_eq!(s_padded.stride, 1024);
1613 }
1614
1615 #[test]
1616 fn g2d_surface_multiplane_nv12_offset() {
1617 if !is_dma_available() || !is_g2d_available() {
1618 return;
1619 }
1620 use edgefirst_tensor::TensorMemory;
1621
1622 let mut luma =
1624 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Dma), Some("luma")).unwrap();
1625 let mut chroma =
1626 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Dma), Some("chroma")).unwrap();
1627
1628 let luma_base = {
1630 let dma = luma.as_dma().unwrap();
1631 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1632 phys.address()
1633 };
1634 let chroma_base = {
1635 let dma = chroma.as_dma().unwrap();
1636 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1637 phys.address()
1638 };
1639
1640 luma.set_plane_offset(4096);
1642 chroma.set_plane_offset(2048);
1643 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1644 let s = tensor_to_g2d_surface(&combined).unwrap();
1645
1646 assert_eq!(s.planes[0], luma_base + 4096);
1648 assert_eq!(s.planes[1], chroma_base + 2048);
1650 }
1651
1652 #[cfg(target_os = "linux")]
1680 fn fill_patterned_nv12(t: &TensorDyn) {
1681 let w = t.width().unwrap();
1682 let h = t.height().unwrap();
1683 let stride = t.effective_row_stride().unwrap();
1684
1685 let chroma_h = h.div_ceil(2);
1687 let chroma_w = w.div_ceil(2);
1688
1689 let bound = t.as_u8().unwrap();
1690 let mut m = bound.map().unwrap();
1691 let buf = m.as_mut_slice();
1692 let uv_start = stride * h;
1693
1694 for r in 0..h {
1696 for c in 0..w {
1697 buf[r * stride + c] = ((r * 3 + c * 5) % 256) as u8;
1698 }
1699 }
1700 for cr_row in 0..chroma_h {
1702 for cc in 0..chroma_w {
1703 let cb_val = ((cc * 7 + cr_row * 11 + 40) % 256) as u8;
1704 let cr_val = ((cc * 13 + cr_row * 3 + 80) % 256) as u8;
1705 let uv_byte = uv_start + cr_row * stride + cc * 2;
1706 buf[uv_byte] = cb_val;
1707 buf[uv_byte + 1] = cr_val;
1708 }
1709 }
1710 }
1711
1712 #[cfg(target_os = "linux")]
1717 fn compare_g2d_vs_cpu_rgba(
1718 g2d_dst: &TensorDyn,
1719 cpu_dst: &TensorDyn,
1720 w: usize,
1721 h: usize,
1722 tol: u32,
1723 ) -> (u32, Option<(usize, usize, usize)>) {
1724 let channels = 4usize; let g2d_t = g2d_dst.as_u8().unwrap();
1726 let cpu_t = cpu_dst.as_u8().unwrap();
1727 let g2d_stride = g2d_t.effective_row_stride().unwrap_or(w * channels);
1728 let cpu_stride = cpu_t.effective_row_stride().unwrap_or(w * channels);
1729 let g2d_map = g2d_t.map().unwrap();
1730 let cpu_map = cpu_t.map().unwrap();
1731 let g2d_px = g2d_map.as_slice();
1732 let cpu_px = cpu_map.as_slice();
1733 let mut max_diff = 0u32;
1734 let mut first_fail: Option<(usize, usize, usize)> = None;
1735 for row in 0..h {
1736 for col in 0..w {
1737 for ch in 0..3usize {
1738 let gi = row * g2d_stride + col * channels + ch;
1740 let ci = row * cpu_stride + col * channels + ch;
1741 let d = (g2d_px[gi] as i32 - cpu_px[ci] as i32).unsigned_abs();
1742 if d > max_diff {
1743 max_diff = d;
1744 }
1745 if first_fail.is_none() && d > tol {
1746 first_fail = Some((col, row, ch));
1747 }
1748 }
1749 }
1750 }
1751 (max_diff, first_fail)
1752 }
1753
1754 #[test]
1770 #[cfg(target_os = "linux")]
1771 fn d01_nv12_odd_w_g2d_vs_cpu() {
1772 if !is_dma_available() {
1773 eprintln!("SKIPPED: d01_nv12_odd_w_g2d_vs_cpu - DMA not available");
1774 return;
1775 }
1776 let (w, h) = (65usize, 64usize);
1777
1778 let src_dma =
1779 TensorDyn::image(w, h, PixelFormat::Nv12, DType::U8, Some(TensorMemory::Dma)).unwrap();
1780 let src_mem =
1781 TensorDyn::image(w, h, PixelFormat::Nv12, DType::U8, Some(TensorMemory::Mem)).unwrap();
1782 fill_patterned_nv12(&src_dma);
1783 fill_patterned_nv12(&src_mem);
1784
1785 let mut cpu_dst = TensorDyn::image(w, h, PixelFormat::Rgba, DType::U8, None).unwrap();
1787 CPUProcessor::new()
1788 .convert(
1789 &src_mem,
1790 &mut cpu_dst,
1791 Rotation::None,
1792 Flip::None,
1793 Crop::no_crop(),
1794 )
1795 .unwrap();
1796
1797 let mut g2d_dst =
1799 TensorDyn::image(w, h, PixelFormat::Rgba, DType::U8, Some(TensorMemory::Dma)).unwrap();
1800 let mut g2d = match G2DProcessor::new() {
1801 Ok(g) => g,
1802 Err(e) => {
1803 eprintln!("SKIPPED: d01_nv12_odd_w_g2d_vs_cpu - G2D not available: {e}");
1804 return;
1805 }
1806 };
1807
1808 g2d.convert(
1810 &src_dma,
1811 &mut g2d_dst,
1812 Rotation::None,
1813 Flip::None,
1814 Crop::no_crop(),
1815 )
1816 .unwrap_or_else(|e| {
1817 panic!("D-01: G2D rejected odd-width (65) NV12 source: {e}");
1818 });
1819
1820 let tol = 4u32;
1822 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
1823 eprintln!("D-01 NV12 odd-W G2D vs CPU: max_diff={max_diff}");
1824 if max_diff > tol {
1831 eprintln!(
1832 "WARNING: D-01 NV12 odd-W G2D vs CPU max_diff={max_diff} > {tol} \
1833 (G2D fixed-point rounding; first bad at {first_fail:?})"
1834 );
1835 }
1836 assert!(
1837 max_diff <= 35,
1838 "D-01: gross NV12 odd-W G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
1839 );
1840 }
1841
1842 #[test]
1854 #[cfg(target_os = "linux")]
1855 fn d03_nv12_odd_both_g2d_vs_cpu() {
1856 if !is_dma_available() {
1857 eprintln!("SKIPPED: d03_nv12_odd_both_g2d_vs_cpu - DMA not available");
1858 return;
1859 }
1860 let (w, h) = (65usize, 63usize);
1861
1862 let src_dma =
1863 TensorDyn::image(w, h, PixelFormat::Nv12, DType::U8, Some(TensorMemory::Dma)).unwrap();
1864 let src_mem =
1865 TensorDyn::image(w, h, PixelFormat::Nv12, DType::U8, Some(TensorMemory::Mem)).unwrap();
1866 fill_patterned_nv12(&src_dma);
1867 fill_patterned_nv12(&src_mem);
1868
1869 let mut cpu_dst = TensorDyn::image(w, h, PixelFormat::Rgba, DType::U8, None).unwrap();
1871 CPUProcessor::new()
1872 .convert(
1873 &src_mem,
1874 &mut cpu_dst,
1875 Rotation::None,
1876 Flip::None,
1877 Crop::no_crop(),
1878 )
1879 .unwrap();
1880
1881 let mut g2d_dst =
1883 TensorDyn::image(w, h, PixelFormat::Rgba, DType::U8, Some(TensorMemory::Dma)).unwrap();
1884 let mut g2d = match G2DProcessor::new() {
1885 Ok(g) => g,
1886 Err(e) => {
1887 eprintln!("SKIPPED: d03_nv12_odd_both_g2d_vs_cpu - G2D not available: {e}");
1888 return;
1889 }
1890 };
1891
1892 g2d.convert(
1895 &src_dma,
1896 &mut g2d_dst,
1897 Rotation::None,
1898 Flip::None,
1899 Crop::no_crop(),
1900 )
1901 .unwrap_or_else(|e| {
1902 panic!("D-03: G2D rejected odd-both (65×63) NV12 source: {e}");
1903 });
1904
1905 let tol = 4u32;
1906 let (max_diff, first_fail) = compare_g2d_vs_cpu_rgba(&g2d_dst, &cpu_dst, w, h, tol);
1907 eprintln!("D-03 NV12 odd-both G2D vs CPU: max_diff={max_diff}");
1908 if max_diff > tol {
1912 eprintln!(
1913 "WARNING: D-03 NV12 odd-both G2D vs CPU max_diff={max_diff} > {tol} \
1914 (G2D fixed-point rounding; first bad at {first_fail:?})"
1915 );
1916 }
1917 assert!(
1918 max_diff <= 35,
1919 "D-03: gross NV12 odd-both G2D vs CPU mismatch max_diff={max_diff} (>35); first bad at {first_fail:?}"
1920 );
1921 }
1922}