1#![cfg(target_os = "linux")]
5
6use crate::{CPUProcessor, Crop, Error, Flip, ImageProcessorTrait, Result, Rotation};
7use edgefirst_tensor::{DType, PixelFormat, Tensor, TensorDyn, TensorMapTrait, TensorTrait};
8use four_char_code::FourCharCode;
9use g2d_sys::{G2DFormat, G2DPhysical, G2DSurface, G2D};
10use std::{os::fd::AsRawFd, time::Instant};
11
12fn pixelfmt_to_fourcc(fmt: PixelFormat) -> FourCharCode {
14 use four_char_code::four_char_code;
15 match fmt {
16 PixelFormat::Rgb => four_char_code!("RGB "),
17 PixelFormat::Rgba => four_char_code!("RGBA"),
18 PixelFormat::Bgra => four_char_code!("BGRA"),
19 PixelFormat::Grey => four_char_code!("Y800"),
20 PixelFormat::Yuyv => four_char_code!("YUYV"),
21 PixelFormat::Vyuy => four_char_code!("VYUY"),
22 PixelFormat::Nv12 => four_char_code!("NV12"),
23 PixelFormat::Nv16 => four_char_code!("NV16"),
24 _ => four_char_code!("RGBA"),
26 }
27}
28
29#[derive(Debug)]
32pub struct G2DProcessor {
33 g2d: G2D,
34}
35
36unsafe impl Send for G2DProcessor {}
37unsafe impl Sync for G2DProcessor {}
38
39impl G2DProcessor {
40 pub fn new() -> Result<Self> {
42 let mut g2d = G2D::new("libg2d.so.2")?;
43 g2d.set_bt709_colorspace()?;
44
45 log::debug!("G2DConverter created with version {:?}", g2d.version());
46 Ok(Self { g2d })
47 }
48
49 pub fn version(&self) -> g2d_sys::Version {
52 self.g2d.version()
53 }
54
55 fn convert_impl(
56 &mut self,
57 src_dyn: &TensorDyn,
58 dst_dyn: &mut TensorDyn,
59 rotation: Rotation,
60 flip: Flip,
61 crop: Crop,
62 ) -> Result<()> {
63 let _span = tracing::trace_span!(
64 "image.convert.g2d",
65 src_fmt = ?src_dyn.format(),
66 dst_fmt = ?dst_dyn.format(),
67 )
68 .entered();
69 if log::log_enabled!(log::Level::Trace) {
70 log::trace!(
71 "G2D convert: {:?}({:?}/{:?}) → {:?}({:?}/{:?})",
72 src_dyn.format(),
73 src_dyn.dtype(),
74 src_dyn.memory(),
75 dst_dyn.format(),
76 dst_dyn.dtype(),
77 dst_dyn.memory(),
78 );
79 }
80
81 if src_dyn.dtype() != DType::U8 {
82 return Err(Error::NotSupported(
83 "G2D only supports u8 source tensors".to_string(),
84 ));
85 }
86 let is_int8_dst = dst_dyn.dtype() == DType::I8;
87 if dst_dyn.dtype() != DType::U8 && !is_int8_dst {
88 return Err(Error::NotSupported(
89 "G2D only supports u8 or i8 destination tensors".to_string(),
90 ));
91 }
92
93 let src_fmt = src_dyn.format().ok_or(Error::NotAnImage)?;
94 let dst_fmt = dst_dyn.format().ok_or(Error::NotAnImage)?;
95
96 use PixelFormat::*;
98 match (src_fmt, dst_fmt) {
99 (Rgba, Rgba) => {}
100 (Rgba, Yuyv) => {}
101 (Rgba, Rgb) => {}
102 (Yuyv, Rgba) => {}
103 (Yuyv, Yuyv) => {}
104 (Yuyv, Rgb) => {}
105 (Nv12, Rgba) => {}
108 (Nv12, Yuyv) => {}
109 (Nv12, Rgb) => {}
110 (Rgba, Bgra) => {}
111 (Yuyv, Bgra) => {}
112 (Nv12, Bgra) => {}
113 (Bgra, Bgra) => {}
114 (s, d) => {
115 return Err(Error::NotSupported(format!(
116 "G2D does not support {} to {} conversion",
117 s, d
118 )));
119 }
120 }
121
122 crop.check_crop_dyn(src_dyn, dst_dyn)?;
123
124 let src = src_dyn.as_u8().unwrap();
125 let dst = if is_int8_dst {
128 let i8_tensor = dst_dyn.as_i8_mut().unwrap();
136 unsafe { &mut *(i8_tensor as *mut Tensor<i8> as *mut Tensor<u8>) }
137 } else {
138 dst_dyn.as_u8_mut().unwrap()
139 };
140
141 let mut src_surface = tensor_to_g2d_surface(src)?;
142 let mut dst_surface = tensor_to_g2d_surface(dst)?;
143
144 src_surface.rot = match flip {
145 Flip::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
146 Flip::Vertical => g2d_sys::g2d_rotation_G2D_FLIP_V,
147 Flip::Horizontal => g2d_sys::g2d_rotation_G2D_FLIP_H,
148 };
149
150 dst_surface.rot = match rotation {
151 Rotation::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
152 Rotation::Clockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_90,
153 Rotation::Rotate180 => g2d_sys::g2d_rotation_G2D_ROTATION_180,
154 Rotation::CounterClockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_270,
155 };
156
157 if let Some(crop_rect) = crop.src_rect {
158 src_surface.left = crop_rect.left as i32;
159 src_surface.top = crop_rect.top as i32;
160 src_surface.right = (crop_rect.left + crop_rect.width) as i32;
161 src_surface.bottom = (crop_rect.top + crop_rect.height) as i32;
162 }
163
164 let dst_w = dst.width().unwrap();
165 let dst_h = dst.height().unwrap();
166
167 let needs_clear = crop.dst_color.is_some()
173 && crop.dst_rect.is_some_and(|dst_rect| {
174 dst_rect.left != 0
175 || dst_rect.top != 0
176 || dst_rect.width != dst_w
177 || dst_rect.height != dst_h
178 });
179
180 if needs_clear && dst_fmt != Rgb {
181 if let Some(dst_color) = crop.dst_color {
182 let start = Instant::now();
183 self.g2d.clear(&mut dst_surface, dst_color)?;
184 log::trace!("g2d clear takes {:?}", start.elapsed());
185 }
186 }
187
188 if let Some(crop_rect) = crop.dst_rect {
189 dst_surface.planes[0] += ((crop_rect.top * dst_surface.stride as usize
193 + crop_rect.left)
194 * dst_fmt.channels()) as u64;
195
196 dst_surface.right = crop_rect.width as i32;
197 dst_surface.bottom = crop_rect.height as i32;
198 dst_surface.width = crop_rect.width as i32;
199 dst_surface.height = crop_rect.height as i32;
200 }
201
202 log::trace!("G2D blit: {src_fmt}→{dst_fmt} int8={is_int8_dst}");
203 self.g2d.blit(&src_surface, &dst_surface)?;
204 self.g2d.finish()?;
205 log::trace!("G2D blit complete");
206
207 if needs_clear && dst_fmt == Rgb {
209 if let (Some(dst_color), Some(dst_rect)) = (crop.dst_color, crop.dst_rect) {
210 let start = Instant::now();
211 CPUProcessor::fill_image_outside_crop_u8(dst, dst_color, dst_rect)?;
212 log::trace!("cpu fill takes {:?}", start.elapsed());
213 }
214 }
215
216 if is_int8_dst {
224 let start = Instant::now();
225 let mut map = dst.map()?;
226 crate::cpu::apply_int8_xor_bias(map.as_mut_slice(), dst_fmt);
227 log::trace!("g2d int8 XOR 0x80 post-pass takes {:?}", start.elapsed());
228 }
229
230 Ok(())
231 }
232}
233
234impl ImageProcessorTrait for G2DProcessor {
235 fn convert(
236 &mut self,
237 src: &TensorDyn,
238 dst: &mut TensorDyn,
239 rotation: Rotation,
240 flip: Flip,
241 crop: Crop,
242 ) -> Result<()> {
243 self.convert_impl(src, dst, rotation, flip, crop)
244 }
245
246 fn draw_decoded_masks(
247 &mut self,
248 dst: &mut TensorDyn,
249 detect: &[crate::DetectBox],
250 segmentation: &[crate::Segmentation],
251 overlay: crate::MaskOverlay<'_>,
252 ) -> Result<()> {
253 if !detect.is_empty() || !segmentation.is_empty() {
257 return Err(Error::NotImplemented(
258 "G2D does not support drawing detection or segmentation overlays".to_string(),
259 ));
260 }
261 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
262 }
263
264 fn draw_proto_masks(
265 &mut self,
266 dst: &mut TensorDyn,
267 detect: &[crate::DetectBox],
268 _proto_data: &crate::ProtoData,
269 overlay: crate::MaskOverlay<'_>,
270 ) -> Result<()> {
271 if !detect.is_empty() {
274 return Err(Error::NotImplemented(
275 "G2D does not support drawing detection or segmentation overlays".to_string(),
276 ));
277 }
278 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
279 }
280
281 fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
282 Err(Error::NotImplemented(
283 "G2D does not support setting colors for rendering detection or segmentation overlays"
284 .to_string(),
285 ))
286 }
287}
288
289fn draw_empty_frame_g2d(
302 g2d: &mut G2D,
303 dst_dyn: &mut TensorDyn,
304 background: Option<&TensorDyn>,
305) -> Result<()> {
306 if dst_dyn.dtype() != DType::U8 {
307 return Err(Error::NotSupported(
308 "G2D only supports u8 destination tensors".to_string(),
309 ));
310 }
311 let dst = dst_dyn.as_u8_mut().ok_or(Error::NotAnImage)?;
312
313 if dst.as_dma().is_none() {
317 return Err(Error::NotImplemented(
318 "g2d only supports Dma memory".to_string(),
319 ));
320 }
321
322 let mut dst_surface = tensor_to_g2d_surface(dst)?;
323
324 match background {
325 None => {
326 let start = Instant::now();
330 g2d.clear(&mut dst_surface, [0, 0, 0, 0])?;
331 g2d.finish()?;
332 log::trace!("g2d clear (empty frame) takes {:?}", start.elapsed());
333 }
334 Some(bg_dyn) => {
335 if bg_dyn.shape() != dst.shape() {
340 return Err(Error::InvalidShape(
341 "background shape does not match dst".into(),
342 ));
343 }
344 if bg_dyn.format() != dst.format() {
345 return Err(Error::InvalidShape(
346 "background pixel format does not match dst".into(),
347 ));
348 }
349 if bg_dyn.dtype() != DType::U8 {
350 return Err(Error::NotSupported(
351 "G2D only supports u8 background tensors".to_string(),
352 ));
353 }
354 let bg = bg_dyn.as_u8().ok_or(Error::NotAnImage)?;
355 if bg.as_dma().is_none() {
356 return Err(Error::NotImplemented(
357 "g2d background must be Dma-backed".to_string(),
358 ));
359 }
360 let src_surface = tensor_to_g2d_surface(bg)?;
361 let start = Instant::now();
362 g2d.blit(&src_surface, &dst_surface)?;
363 g2d.finish()?;
364 log::trace!("g2d blit (bg→dst) takes {:?}", start.elapsed());
365 }
366 }
367 Ok(())
368}
369
370fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
374 let fmt = img.format().ok_or(Error::NotAnImage)?;
375 let dma = img
376 .as_dma()
377 .ok_or_else(|| Error::NotImplemented("g2d only supports Dma memory".to_string()))?;
378 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into()?;
379
380 let base_addr = phys.address();
387 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
388 let planes = if fmt == PixelFormat::Nv12 {
389 if img.is_multiplane() {
390 let chroma = img.chroma().unwrap();
392 let chroma_dma = chroma.as_dma().ok_or_else(|| {
393 Error::NotImplemented("g2d multiplane chroma must be DMA-backed".to_string())
394 })?;
395 let uv_phys: G2DPhysical = chroma_dma.fd.as_raw_fd().try_into()?;
396 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
397 [
398 base_addr + luma_offset,
399 uv_phys.address() + chroma_offset,
400 0,
401 ]
402 } else {
403 let w = img.width().unwrap();
404 let h = img.height().unwrap();
405 let stride = img.effective_row_stride().unwrap_or(w);
406 let uv_offset = (luma_offset as usize + stride * h) as u64;
407 [base_addr + luma_offset, base_addr + uv_offset, 0]
408 }
409 } else {
410 [base_addr + luma_offset, 0, 0]
411 };
412
413 let w = img.width().unwrap();
414 let h = img.height().unwrap();
415 let fourcc = pixelfmt_to_fourcc(fmt);
416
417 let stride_pixels = match img.effective_row_stride() {
420 Some(s) => {
421 let channels = fmt.channels();
422 if s % channels != 0 {
423 return Err(Error::NotImplemented(
424 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
425 ));
426 }
427 s / channels
428 }
429 None => w,
430 };
431
432 Ok(G2DSurface {
433 planes,
434 format: G2DFormat::try_from(fourcc)?.format(),
435 left: 0,
436 top: 0,
437 right: w as i32,
438 bottom: h as i32,
439 stride: stride_pixels as i32,
440 width: w as i32,
441 height: h as i32,
442 blendfunc: 0,
443 clrcolor: 0,
444 rot: 0,
445 global_alpha: 0,
446 })
447}
448
449#[cfg(feature = "g2d_test_formats")]
450#[cfg(test)]
451#[allow(deprecated)]
452mod g2d_tests {
453 use super::*;
454 use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation};
455 use edgefirst_tensor::{
456 is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
457 };
458 use image::buffer::ConvertBuffer;
459
460 #[test]
461 #[cfg(target_os = "linux")]
462 fn test_g2d_formats_no_resize() {
463 for i in [
464 PixelFormat::Rgba,
465 PixelFormat::Yuyv,
466 PixelFormat::Rgb,
467 PixelFormat::Grey,
468 PixelFormat::Nv12,
469 ] {
470 for o in [
471 PixelFormat::Rgba,
472 PixelFormat::Yuyv,
473 PixelFormat::Rgb,
474 PixelFormat::Grey,
475 ] {
476 let res = test_g2d_format_no_resize_(i, o);
477 if let Err(e) = res {
478 println!("{i} to {o} failed: {e:?}");
479 } else {
480 println!("{i} to {o} success");
481 }
482 }
483 }
484 }
485
486 fn test_g2d_format_no_resize_(
487 g2d_in_fmt: PixelFormat,
488 g2d_out_fmt: PixelFormat,
489 ) -> Result<(), crate::Error> {
490 let dst_width = 1280;
491 let dst_height = 720;
492 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
493 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
494
495 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
497
498 let mut cpu_converter = CPUProcessor::new();
499
500 if g2d_in_fmt == PixelFormat::Nv12 {
502 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
503 src2.as_u8()
504 .unwrap()
505 .map()?
506 .as_mut_slice()
507 .copy_from_slice(&nv12_bytes);
508 } else {
509 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
510 }
511
512 let mut g2d_dst = TensorDyn::image(
513 dst_width,
514 dst_height,
515 g2d_out_fmt,
516 DType::U8,
517 Some(TensorMemory::Dma),
518 )?;
519 let mut g2d_converter = G2DProcessor::new()?;
520 let src2_dyn = src2;
521 let mut g2d_dst_dyn = g2d_dst;
522 g2d_converter.convert(
523 &src2_dyn,
524 &mut g2d_dst_dyn,
525 Rotation::None,
526 Flip::None,
527 Crop::no_crop(),
528 )?;
529 g2d_dst = {
530 let mut __t = g2d_dst_dyn.into_u8().unwrap();
531 __t.set_format(g2d_out_fmt)
532 .map_err(|e| crate::Error::Internal(e.to_string()))?;
533 TensorDyn::from(__t)
534 };
535
536 let mut cpu_dst =
537 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
538 cpu_converter.convert(
539 &g2d_dst,
540 &mut cpu_dst,
541 Rotation::None,
542 Flip::None,
543 Crop::no_crop(),
544 )?;
545
546 compare_images(
547 &src,
548 &cpu_dst,
549 0.98,
550 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
551 )
552 }
553
554 #[test]
555 #[cfg(target_os = "linux")]
556 fn test_g2d_formats_with_resize() {
557 for i in [
558 PixelFormat::Rgba,
559 PixelFormat::Yuyv,
560 PixelFormat::Rgb,
561 PixelFormat::Grey,
562 PixelFormat::Nv12,
563 ] {
564 for o in [
565 PixelFormat::Rgba,
566 PixelFormat::Yuyv,
567 PixelFormat::Rgb,
568 PixelFormat::Grey,
569 ] {
570 let res = test_g2d_format_with_resize_(i, o);
571 if let Err(e) = res {
572 println!("{i} to {o} failed: {e:?}");
573 } else {
574 println!("{i} to {o} success");
575 }
576 }
577 }
578 }
579
580 #[test]
581 #[cfg(target_os = "linux")]
582 fn test_g2d_formats_with_resize_dst_crop() {
583 for i in [
584 PixelFormat::Rgba,
585 PixelFormat::Yuyv,
586 PixelFormat::Rgb,
587 PixelFormat::Grey,
588 PixelFormat::Nv12,
589 ] {
590 for o in [
591 PixelFormat::Rgba,
592 PixelFormat::Yuyv,
593 PixelFormat::Rgb,
594 PixelFormat::Grey,
595 ] {
596 let res = test_g2d_format_with_resize_dst_crop(i, o);
597 if let Err(e) = res {
598 println!("{i} to {o} failed: {e:?}");
599 } else {
600 println!("{i} to {o} success");
601 }
602 }
603 }
604 }
605
606 fn test_g2d_format_with_resize_(
607 g2d_in_fmt: PixelFormat,
608 g2d_out_fmt: PixelFormat,
609 ) -> Result<(), crate::Error> {
610 let dst_width = 600;
611 let dst_height = 400;
612 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
613 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
614
615 let mut cpu_converter = CPUProcessor::new();
616
617 let mut reference = TensorDyn::image(
618 dst_width,
619 dst_height,
620 PixelFormat::Rgb,
621 DType::U8,
622 Some(TensorMemory::Dma),
623 )?;
624 cpu_converter.convert(
625 &src,
626 &mut reference,
627 Rotation::None,
628 Flip::None,
629 Crop::no_crop(),
630 )?;
631
632 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
634
635 if g2d_in_fmt == PixelFormat::Nv12 {
637 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
638 src2.as_u8()
639 .unwrap()
640 .map()?
641 .as_mut_slice()
642 .copy_from_slice(&nv12_bytes);
643 } else {
644 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
645 }
646
647 let mut g2d_dst = TensorDyn::image(
648 dst_width,
649 dst_height,
650 g2d_out_fmt,
651 DType::U8,
652 Some(TensorMemory::Dma),
653 )?;
654 let mut g2d_converter = G2DProcessor::new()?;
655 let src2_dyn = src2;
656 let mut g2d_dst_dyn = g2d_dst;
657 g2d_converter.convert(
658 &src2_dyn,
659 &mut g2d_dst_dyn,
660 Rotation::None,
661 Flip::None,
662 Crop::no_crop(),
663 )?;
664 g2d_dst = {
665 let mut __t = g2d_dst_dyn.into_u8().unwrap();
666 __t.set_format(g2d_out_fmt)
667 .map_err(|e| crate::Error::Internal(e.to_string()))?;
668 TensorDyn::from(__t)
669 };
670
671 let mut cpu_dst =
672 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
673 cpu_converter.convert(
674 &g2d_dst,
675 &mut cpu_dst,
676 Rotation::None,
677 Flip::None,
678 Crop::no_crop(),
679 )?;
680
681 compare_images(
682 &reference,
683 &cpu_dst,
684 0.98,
685 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
686 )
687 }
688
689 fn test_g2d_format_with_resize_dst_crop(
690 g2d_in_fmt: PixelFormat,
691 g2d_out_fmt: PixelFormat,
692 ) -> Result<(), crate::Error> {
693 let dst_width = 600;
694 let dst_height = 400;
695 let crop = Crop {
696 src_rect: None,
697 dst_rect: Some(Rect {
698 top: 100,
699 left: 100,
700 height: 100,
701 width: 200,
702 }),
703 dst_color: None,
704 };
705 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
706 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
707
708 let mut cpu_converter = CPUProcessor::new();
709
710 let mut reference = TensorDyn::image(
711 dst_width,
712 dst_height,
713 PixelFormat::Rgb,
714 DType::U8,
715 Some(TensorMemory::Dma),
716 )?;
717 reference
718 .as_u8()
719 .unwrap()
720 .map()
721 .unwrap()
722 .as_mut_slice()
723 .fill(128);
724 cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
725
726 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
728
729 if g2d_in_fmt == PixelFormat::Nv12 {
731 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
732 src2.as_u8()
733 .unwrap()
734 .map()?
735 .as_mut_slice()
736 .copy_from_slice(&nv12_bytes);
737 } else {
738 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
739 }
740
741 let mut g2d_dst = TensorDyn::image(
742 dst_width,
743 dst_height,
744 g2d_out_fmt,
745 DType::U8,
746 Some(TensorMemory::Dma),
747 )?;
748 g2d_dst
749 .as_u8()
750 .unwrap()
751 .map()
752 .unwrap()
753 .as_mut_slice()
754 .fill(128);
755 let mut g2d_converter = G2DProcessor::new()?;
756 let src2_dyn = src2;
757 let mut g2d_dst_dyn = g2d_dst;
758 g2d_converter.convert(
759 &src2_dyn,
760 &mut g2d_dst_dyn,
761 Rotation::None,
762 Flip::None,
763 crop,
764 )?;
765 g2d_dst = {
766 let mut __t = g2d_dst_dyn.into_u8().unwrap();
767 __t.set_format(g2d_out_fmt)
768 .map_err(|e| crate::Error::Internal(e.to_string()))?;
769 TensorDyn::from(__t)
770 };
771
772 let mut cpu_dst =
773 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
774 cpu_converter.convert(
775 &g2d_dst,
776 &mut cpu_dst,
777 Rotation::None,
778 Flip::None,
779 Crop::no_crop(),
780 )?;
781
782 compare_images(
783 &reference,
784 &cpu_dst,
785 0.98,
786 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
787 )
788 }
789
790 fn compare_images(
791 img1: &TensorDyn,
792 img2: &TensorDyn,
793 threshold: f64,
794 name: &str,
795 ) -> Result<(), crate::Error> {
796 assert_eq!(img1.height(), img2.height(), "Heights differ");
797 assert_eq!(img1.width(), img2.width(), "Widths differ");
798 assert_eq!(
799 img1.format().unwrap(),
800 img2.format().unwrap(),
801 "PixelFormat differ"
802 );
803 assert!(
804 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
805 "format must be Rgb or Rgba for comparison"
806 );
807 let image1 = match img1.format().unwrap() {
808 PixelFormat::Rgb => image::RgbImage::from_vec(
809 img1.width().unwrap() as u32,
810 img1.height().unwrap() as u32,
811 img1.as_u8().unwrap().map().unwrap().to_vec(),
812 )
813 .unwrap(),
814 PixelFormat::Rgba => image::RgbaImage::from_vec(
815 img1.width().unwrap() as u32,
816 img1.height().unwrap() as u32,
817 img1.as_u8().unwrap().map().unwrap().to_vec(),
818 )
819 .unwrap()
820 .convert(),
821
822 _ => unreachable!(),
823 };
824
825 let image2 = match img2.format().unwrap() {
826 PixelFormat::Rgb => image::RgbImage::from_vec(
827 img2.width().unwrap() as u32,
828 img2.height().unwrap() as u32,
829 img2.as_u8().unwrap().map().unwrap().to_vec(),
830 )
831 .unwrap(),
832 PixelFormat::Rgba => image::RgbaImage::from_vec(
833 img2.width().unwrap() as u32,
834 img2.height().unwrap() as u32,
835 img2.as_u8().unwrap().map().unwrap().to_vec(),
836 )
837 .unwrap()
838 .convert(),
839
840 _ => unreachable!(),
841 };
842
843 let similarity = image_compare::rgb_similarity_structure(
844 &image_compare::Algorithm::RootMeanSquared,
845 &image1,
846 &image2,
847 )
848 .expect("Image Comparison failed");
849
850 if similarity.score < threshold {
851 image1.save(format!("{name}_1.png")).unwrap();
852 image2.save(format!("{name}_2.png")).unwrap();
853 return Err(Error::Internal(format!(
854 "{name}: converted image and target image have similarity score too low: {} < {}",
855 similarity.score, threshold
856 )));
857 }
858 Ok(())
859 }
860
861 fn load_raw_image(
867 width: usize,
868 height: usize,
869 format: PixelFormat,
870 memory: Option<TensorMemory>,
871 bytes: &[u8],
872 ) -> Result<TensorDyn, crate::Error> {
873 let img = TensorDyn::image(width, height, format, DType::U8, memory)?;
874 let mut map = img.as_u8().unwrap().map()?;
875 map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
876 Ok(img)
877 }
878
879 #[test]
881 #[cfg(target_os = "linux")]
882 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
883 if !is_dma_available() {
884 return Ok(());
885 }
886 let src = load_raw_image(
888 1280,
889 720,
890 PixelFormat::Nv12,
891 Some(TensorMemory::Dma),
892 &edgefirst_bench::testdata::read("camera720p.nv12"),
893 )?;
894
895 let reference = load_raw_image(
897 1280,
898 720,
899 PixelFormat::Rgba,
900 None,
901 &edgefirst_bench::testdata::read("camera720p.rgba"),
902 )?;
903
904 let mut dst = TensorDyn::image(
906 1280,
907 720,
908 PixelFormat::Rgba,
909 DType::U8,
910 Some(TensorMemory::Dma),
911 )?;
912 let mut g2d = G2DProcessor::new()?;
913 let src_dyn = src;
914 let mut dst_dyn = dst;
915 g2d.convert(
916 &src_dyn,
917 &mut dst_dyn,
918 Rotation::None,
919 Flip::None,
920 Crop::no_crop(),
921 )?;
922 dst = {
923 let mut __t = dst_dyn.into_u8().unwrap();
924 __t.set_format(PixelFormat::Rgba)
925 .map_err(|e| crate::Error::Internal(e.to_string()))?;
926 TensorDyn::from(__t)
927 };
928
929 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
931 cpu_dst
932 .as_u8()
933 .unwrap()
934 .map()?
935 .as_mut_slice()
936 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
937
938 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
939 }
940
941 #[test]
943 #[cfg(target_os = "linux")]
944 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
945 if !is_dma_available() {
946 return Ok(());
947 }
948 let src = load_raw_image(
950 1280,
951 720,
952 PixelFormat::Nv12,
953 Some(TensorMemory::Dma),
954 &edgefirst_bench::testdata::read("camera720p.nv12"),
955 )?;
956
957 let reference = load_raw_image(
959 1280,
960 720,
961 PixelFormat::Rgb,
962 None,
963 &edgefirst_bench::testdata::read("camera720p.rgb"),
964 )?;
965
966 let mut dst = TensorDyn::image(
968 1280,
969 720,
970 PixelFormat::Rgb,
971 DType::U8,
972 Some(TensorMemory::Dma),
973 )?;
974 let mut g2d = G2DProcessor::new()?;
975 let src_dyn = src;
976 let mut dst_dyn = dst;
977 g2d.convert(
978 &src_dyn,
979 &mut dst_dyn,
980 Rotation::None,
981 Flip::None,
982 Crop::no_crop(),
983 )?;
984 dst = {
985 let mut __t = dst_dyn.into_u8().unwrap();
986 __t.set_format(PixelFormat::Rgb)
987 .map_err(|e| crate::Error::Internal(e.to_string()))?;
988 TensorDyn::from(__t)
989 };
990
991 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
993 cpu_dst
994 .as_u8()
995 .unwrap()
996 .map()?
997 .as_mut_slice()
998 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
999
1000 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1001 }
1002
1003 #[test]
1005 #[cfg(target_os = "linux")]
1006 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1007 if !is_dma_available() {
1008 return Ok(());
1009 }
1010 let src = load_raw_image(
1012 1280,
1013 720,
1014 PixelFormat::Yuyv,
1015 Some(TensorMemory::Dma),
1016 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1017 )?;
1018
1019 let reference = load_raw_image(
1021 1280,
1022 720,
1023 PixelFormat::Rgba,
1024 None,
1025 &edgefirst_bench::testdata::read("camera720p.rgba"),
1026 )?;
1027
1028 let mut dst = TensorDyn::image(
1030 1280,
1031 720,
1032 PixelFormat::Rgba,
1033 DType::U8,
1034 Some(TensorMemory::Dma),
1035 )?;
1036 let mut g2d = G2DProcessor::new()?;
1037 let src_dyn = src;
1038 let mut dst_dyn = dst;
1039 g2d.convert(
1040 &src_dyn,
1041 &mut dst_dyn,
1042 Rotation::None,
1043 Flip::None,
1044 Crop::no_crop(),
1045 )?;
1046 dst = {
1047 let mut __t = dst_dyn.into_u8().unwrap();
1048 __t.set_format(PixelFormat::Rgba)
1049 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1050 TensorDyn::from(__t)
1051 };
1052
1053 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1055 cpu_dst
1056 .as_u8()
1057 .unwrap()
1058 .map()?
1059 .as_mut_slice()
1060 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1061
1062 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1063 }
1064
1065 #[test]
1067 #[cfg(target_os = "linux")]
1068 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1069 if !is_dma_available() {
1070 return Ok(());
1071 }
1072 let src = load_raw_image(
1074 1280,
1075 720,
1076 PixelFormat::Yuyv,
1077 Some(TensorMemory::Dma),
1078 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1079 )?;
1080
1081 let reference = load_raw_image(
1083 1280,
1084 720,
1085 PixelFormat::Rgb,
1086 None,
1087 &edgefirst_bench::testdata::read("camera720p.rgb"),
1088 )?;
1089
1090 let mut dst = TensorDyn::image(
1092 1280,
1093 720,
1094 PixelFormat::Rgb,
1095 DType::U8,
1096 Some(TensorMemory::Dma),
1097 )?;
1098 let mut g2d = G2DProcessor::new()?;
1099 let src_dyn = src;
1100 let mut dst_dyn = dst;
1101 g2d.convert(
1102 &src_dyn,
1103 &mut dst_dyn,
1104 Rotation::None,
1105 Flip::None,
1106 Crop::no_crop(),
1107 )?;
1108 dst = {
1109 let mut __t = dst_dyn.into_u8().unwrap();
1110 __t.set_format(PixelFormat::Rgb)
1111 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1112 TensorDyn::from(__t)
1113 };
1114
1115 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1117 cpu_dst
1118 .as_u8()
1119 .unwrap()
1120 .map()?
1121 .as_mut_slice()
1122 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1123
1124 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1125 }
1126
1127 #[test]
1130 #[cfg(target_os = "linux")]
1131 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1132 fn test_g2d_bgra_no_resize() {
1133 for src_fmt in [
1134 PixelFormat::Rgba,
1135 PixelFormat::Yuyv,
1136 PixelFormat::Nv12,
1137 PixelFormat::Bgra,
1138 ] {
1139 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1140 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1141 });
1142 }
1143 }
1144
1145 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1146 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1147 let src = crate::load_image_test_helper(&file, Some(PixelFormat::Rgb), None)?;
1148
1149 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
1151 let mut cpu_converter = CPUProcessor::new();
1152
1153 if g2d_in_fmt == PixelFormat::Nv12 {
1154 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1155 src2.as_u8()
1156 .unwrap()
1157 .map()?
1158 .as_mut_slice()
1159 .copy_from_slice(&nv12_bytes);
1160 } else {
1161 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1162 }
1163
1164 let mut g2d = G2DProcessor::new()?;
1165
1166 let mut bgra_dst = TensorDyn::image(
1168 1280,
1169 720,
1170 PixelFormat::Bgra,
1171 DType::U8,
1172 Some(TensorMemory::Dma),
1173 )?;
1174 let src2_dyn = src2;
1175 let mut bgra_dst_dyn = bgra_dst;
1176 g2d.convert(
1177 &src2_dyn,
1178 &mut bgra_dst_dyn,
1179 Rotation::None,
1180 Flip::None,
1181 Crop::no_crop(),
1182 )?;
1183 bgra_dst = {
1184 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1185 __t.set_format(PixelFormat::Bgra)
1186 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1187 TensorDyn::from(__t)
1188 };
1189
1190 let src2 = {
1192 let mut __t = src2_dyn.into_u8().unwrap();
1193 __t.set_format(g2d_in_fmt)
1194 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1195 TensorDyn::from(__t)
1196 };
1197
1198 let mut rgba_dst = TensorDyn::image(
1200 1280,
1201 720,
1202 PixelFormat::Rgba,
1203 DType::U8,
1204 Some(TensorMemory::Dma),
1205 )?;
1206 let src2_dyn2 = src2;
1207 let mut rgba_dst_dyn = rgba_dst;
1208 g2d.convert(
1209 &src2_dyn2,
1210 &mut rgba_dst_dyn,
1211 Rotation::None,
1212 Flip::None,
1213 Crop::no_crop(),
1214 )?;
1215 rgba_dst = {
1216 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1217 __t.set_format(PixelFormat::Rgba)
1218 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1219 TensorDyn::from(__t)
1220 };
1221
1222 let bgra_cpu = TensorDyn::image(1280, 720, PixelFormat::Bgra, DType::U8, None)?;
1224 bgra_cpu
1225 .as_u8()
1226 .unwrap()
1227 .map()?
1228 .as_mut_slice()
1229 .copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
1230
1231 let rgba_cpu = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1232 rgba_cpu
1233 .as_u8()
1234 .unwrap()
1235 .map()?
1236 .as_mut_slice()
1237 .copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
1238
1239 let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
1241 let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
1242 let bgra_buf = bgra_map.as_slice();
1243 let rgba_buf = rgba_map.as_slice();
1244
1245 assert_eq!(bgra_buf.len(), rgba_buf.len());
1246 for (i, (bc, rc)) in bgra_buf
1247 .chunks_exact(4)
1248 .zip(rgba_buf.chunks_exact(4))
1249 .enumerate()
1250 {
1251 assert_eq!(
1252 bc[0], rc[2],
1253 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1254 );
1255 assert_eq!(
1256 bc[1], rc[1],
1257 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1258 );
1259 assert_eq!(
1260 bc[2], rc[0],
1261 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1262 );
1263 assert_eq!(
1264 bc[3], rc[3],
1265 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1266 );
1267 }
1268 Ok(())
1269 }
1270
1271 fn surface_for(
1282 width: usize,
1283 height: usize,
1284 fmt: PixelFormat,
1285 offset: Option<usize>,
1286 row_stride: Option<usize>,
1287 ) -> Result<G2DSurface, crate::Error> {
1288 use edgefirst_tensor::TensorMemory;
1289 let mut t = Tensor::<u8>::image(width, height, fmt, Some(TensorMemory::Dma))?;
1290 if let Some(o) = offset {
1291 t.set_plane_offset(o);
1292 }
1293 if let Some(s) = row_stride {
1294 t.set_row_stride_unchecked(s);
1295 }
1296 tensor_to_g2d_surface(&t)
1297 }
1298
1299 #[test]
1300 fn g2d_surface_single_plane_no_offset() {
1301 if !is_dma_available() {
1302 return;
1303 }
1304 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1305 assert_ne!(s.planes[0], 0);
1307 assert_eq!(s.stride, 640);
1308 }
1309
1310 #[test]
1311 fn g2d_surface_single_plane_with_offset() {
1312 if !is_dma_available() {
1313 return;
1314 }
1315 use edgefirst_tensor::TensorMemory;
1316 let mut t =
1317 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1318 let s0 = tensor_to_g2d_surface(&t).unwrap();
1319 t.set_plane_offset(4096);
1320 let s1 = tensor_to_g2d_surface(&t).unwrap();
1321 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1322 }
1323
1324 #[test]
1325 fn g2d_surface_single_plane_zero_offset() {
1326 if !is_dma_available() {
1327 return;
1328 }
1329 use edgefirst_tensor::TensorMemory;
1330 let mut t =
1331 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1332 let s_none = tensor_to_g2d_surface(&t).unwrap();
1333 t.set_plane_offset(0);
1334 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1335 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1337 }
1338
1339 #[test]
1340 fn g2d_surface_stride_rgba() {
1341 if !is_dma_available() {
1342 return;
1343 }
1344 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1346 assert_eq!(s_default.stride, 640);
1347
1348 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1350 assert_eq!(s_custom.stride, 704);
1351 }
1352
1353 #[test]
1354 fn g2d_surface_stride_rgb() {
1355 if !is_dma_available() {
1356 return;
1357 }
1358 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1359 assert_eq!(s_default.stride, 640);
1360
1361 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1363 assert_eq!(s_custom.stride, 660);
1364 }
1365
1366 #[test]
1367 fn g2d_surface_stride_grey() {
1368 if !is_dma_available() {
1369 return;
1370 }
1371 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1373 Ok(s) => s,
1374 Err(crate::Error::G2D(..)) => return,
1375 Err(e) => panic!("unexpected error: {e:?}"),
1376 };
1377 assert_eq!(s.stride, 1024);
1379 }
1380
1381 #[test]
1382 fn g2d_surface_contiguous_nv12_offset() {
1383 if !is_dma_available() {
1384 return;
1385 }
1386 use edgefirst_tensor::TensorMemory;
1387 let mut t =
1388 Tensor::<u8>::image(640, 480, PixelFormat::Nv12, Some(TensorMemory::Dma)).unwrap();
1389 let s0 = tensor_to_g2d_surface(&t).unwrap();
1390
1391 t.set_plane_offset(8192);
1392 let s1 = tensor_to_g2d_surface(&t).unwrap();
1393
1394 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1396 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1400 }
1401
1402 #[test]
1403 fn g2d_surface_contiguous_nv12_stride() {
1404 if !is_dma_available() {
1405 return;
1406 }
1407 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1409 assert_eq!(s.stride, 640);
1410
1411 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1413 assert_eq!(s_padded.stride, 1024);
1414 }
1415
1416 #[test]
1417 fn g2d_surface_multiplane_nv12_offset() {
1418 if !is_dma_available() {
1419 return;
1420 }
1421 use edgefirst_tensor::TensorMemory;
1422
1423 let mut luma =
1425 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Dma), Some("luma")).unwrap();
1426 let mut chroma =
1427 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Dma), Some("chroma")).unwrap();
1428
1429 let luma_base = {
1431 let dma = luma.as_dma().unwrap();
1432 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1433 phys.address()
1434 };
1435 let chroma_base = {
1436 let dma = chroma.as_dma().unwrap();
1437 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1438 phys.address()
1439 };
1440
1441 luma.set_plane_offset(4096);
1443 chroma.set_plane_offset(2048);
1444 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1445 let s = tensor_to_g2d_surface(&combined).unwrap();
1446
1447 assert_eq!(s.planes[0], luma_base + 4096);
1449 assert_eq!(s.planes[1], chroma_base + 2048);
1451 }
1452}