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 "g2d_convert",
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)]
451mod g2d_tests {
452 use super::*;
453 use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation};
454 use edgefirst_tensor::{
455 is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
456 };
457 use image::buffer::ConvertBuffer;
458
459 #[test]
460 #[cfg(target_os = "linux")]
461 fn test_g2d_formats_no_resize() {
462 for i in [
463 PixelFormat::Rgba,
464 PixelFormat::Yuyv,
465 PixelFormat::Rgb,
466 PixelFormat::Grey,
467 PixelFormat::Nv12,
468 ] {
469 for o in [
470 PixelFormat::Rgba,
471 PixelFormat::Yuyv,
472 PixelFormat::Rgb,
473 PixelFormat::Grey,
474 ] {
475 let res = test_g2d_format_no_resize_(i, o);
476 if let Err(e) = res {
477 println!("{i} to {o} failed: {e:?}");
478 } else {
479 println!("{i} to {o} success");
480 }
481 }
482 }
483 }
484
485 fn test_g2d_format_no_resize_(
486 g2d_in_fmt: PixelFormat,
487 g2d_out_fmt: PixelFormat,
488 ) -> Result<(), crate::Error> {
489 let dst_width = 1280;
490 let dst_height = 720;
491 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
492 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
493
494 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
496
497 let mut cpu_converter = CPUProcessor::new();
498
499 if g2d_in_fmt == PixelFormat::Nv12 {
501 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
502 src2.as_u8()
503 .unwrap()
504 .map()?
505 .as_mut_slice()
506 .copy_from_slice(&nv12_bytes);
507 } else {
508 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
509 }
510
511 let mut g2d_dst = TensorDyn::image(
512 dst_width,
513 dst_height,
514 g2d_out_fmt,
515 DType::U8,
516 Some(TensorMemory::Dma),
517 )?;
518 let mut g2d_converter = G2DProcessor::new()?;
519 let src2_dyn = src2;
520 let mut g2d_dst_dyn = g2d_dst;
521 g2d_converter.convert(
522 &src2_dyn,
523 &mut g2d_dst_dyn,
524 Rotation::None,
525 Flip::None,
526 Crop::no_crop(),
527 )?;
528 g2d_dst = {
529 let mut __t = g2d_dst_dyn.into_u8().unwrap();
530 __t.set_format(g2d_out_fmt)
531 .map_err(|e| crate::Error::Internal(e.to_string()))?;
532 TensorDyn::from(__t)
533 };
534
535 let mut cpu_dst =
536 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
537 cpu_converter.convert(
538 &g2d_dst,
539 &mut cpu_dst,
540 Rotation::None,
541 Flip::None,
542 Crop::no_crop(),
543 )?;
544
545 compare_images(
546 &src,
547 &cpu_dst,
548 0.98,
549 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
550 )
551 }
552
553 #[test]
554 #[cfg(target_os = "linux")]
555 fn test_g2d_formats_with_resize() {
556 for i in [
557 PixelFormat::Rgba,
558 PixelFormat::Yuyv,
559 PixelFormat::Rgb,
560 PixelFormat::Grey,
561 PixelFormat::Nv12,
562 ] {
563 for o in [
564 PixelFormat::Rgba,
565 PixelFormat::Yuyv,
566 PixelFormat::Rgb,
567 PixelFormat::Grey,
568 ] {
569 let res = test_g2d_format_with_resize_(i, o);
570 if let Err(e) = res {
571 println!("{i} to {o} failed: {e:?}");
572 } else {
573 println!("{i} to {o} success");
574 }
575 }
576 }
577 }
578
579 #[test]
580 #[cfg(target_os = "linux")]
581 fn test_g2d_formats_with_resize_dst_crop() {
582 for i in [
583 PixelFormat::Rgba,
584 PixelFormat::Yuyv,
585 PixelFormat::Rgb,
586 PixelFormat::Grey,
587 PixelFormat::Nv12,
588 ] {
589 for o in [
590 PixelFormat::Rgba,
591 PixelFormat::Yuyv,
592 PixelFormat::Rgb,
593 PixelFormat::Grey,
594 ] {
595 let res = test_g2d_format_with_resize_dst_crop(i, o);
596 if let Err(e) = res {
597 println!("{i} to {o} failed: {e:?}");
598 } else {
599 println!("{i} to {o} success");
600 }
601 }
602 }
603 }
604
605 fn test_g2d_format_with_resize_(
606 g2d_in_fmt: PixelFormat,
607 g2d_out_fmt: PixelFormat,
608 ) -> Result<(), crate::Error> {
609 let dst_width = 600;
610 let dst_height = 400;
611 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
612 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
613
614 let mut cpu_converter = CPUProcessor::new();
615
616 let mut reference = TensorDyn::image(
617 dst_width,
618 dst_height,
619 PixelFormat::Rgb,
620 DType::U8,
621 Some(TensorMemory::Dma),
622 )?;
623 cpu_converter.convert(
624 &src,
625 &mut reference,
626 Rotation::None,
627 Flip::None,
628 Crop::no_crop(),
629 )?;
630
631 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
633
634 if g2d_in_fmt == PixelFormat::Nv12 {
636 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
637 src2.as_u8()
638 .unwrap()
639 .map()?
640 .as_mut_slice()
641 .copy_from_slice(&nv12_bytes);
642 } else {
643 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
644 }
645
646 let mut g2d_dst = TensorDyn::image(
647 dst_width,
648 dst_height,
649 g2d_out_fmt,
650 DType::U8,
651 Some(TensorMemory::Dma),
652 )?;
653 let mut g2d_converter = G2DProcessor::new()?;
654 let src2_dyn = src2;
655 let mut g2d_dst_dyn = g2d_dst;
656 g2d_converter.convert(
657 &src2_dyn,
658 &mut g2d_dst_dyn,
659 Rotation::None,
660 Flip::None,
661 Crop::no_crop(),
662 )?;
663 g2d_dst = {
664 let mut __t = g2d_dst_dyn.into_u8().unwrap();
665 __t.set_format(g2d_out_fmt)
666 .map_err(|e| crate::Error::Internal(e.to_string()))?;
667 TensorDyn::from(__t)
668 };
669
670 let mut cpu_dst =
671 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
672 cpu_converter.convert(
673 &g2d_dst,
674 &mut cpu_dst,
675 Rotation::None,
676 Flip::None,
677 Crop::no_crop(),
678 )?;
679
680 compare_images(
681 &reference,
682 &cpu_dst,
683 0.98,
684 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
685 )
686 }
687
688 fn test_g2d_format_with_resize_dst_crop(
689 g2d_in_fmt: PixelFormat,
690 g2d_out_fmt: PixelFormat,
691 ) -> Result<(), crate::Error> {
692 let dst_width = 600;
693 let dst_height = 400;
694 let crop = Crop {
695 src_rect: None,
696 dst_rect: Some(Rect {
697 top: 100,
698 left: 100,
699 height: 100,
700 width: 200,
701 }),
702 dst_color: None,
703 };
704 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
705 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
706
707 let mut cpu_converter = CPUProcessor::new();
708
709 let mut reference = TensorDyn::image(
710 dst_width,
711 dst_height,
712 PixelFormat::Rgb,
713 DType::U8,
714 Some(TensorMemory::Dma),
715 )?;
716 reference
717 .as_u8()
718 .unwrap()
719 .map()
720 .unwrap()
721 .as_mut_slice()
722 .fill(128);
723 cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
724
725 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
727
728 if g2d_in_fmt == PixelFormat::Nv12 {
730 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
731 src2.as_u8()
732 .unwrap()
733 .map()?
734 .as_mut_slice()
735 .copy_from_slice(&nv12_bytes);
736 } else {
737 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
738 }
739
740 let mut g2d_dst = TensorDyn::image(
741 dst_width,
742 dst_height,
743 g2d_out_fmt,
744 DType::U8,
745 Some(TensorMemory::Dma),
746 )?;
747 g2d_dst
748 .as_u8()
749 .unwrap()
750 .map()
751 .unwrap()
752 .as_mut_slice()
753 .fill(128);
754 let mut g2d_converter = G2DProcessor::new()?;
755 let src2_dyn = src2;
756 let mut g2d_dst_dyn = g2d_dst;
757 g2d_converter.convert(
758 &src2_dyn,
759 &mut g2d_dst_dyn,
760 Rotation::None,
761 Flip::None,
762 crop,
763 )?;
764 g2d_dst = {
765 let mut __t = g2d_dst_dyn.into_u8().unwrap();
766 __t.set_format(g2d_out_fmt)
767 .map_err(|e| crate::Error::Internal(e.to_string()))?;
768 TensorDyn::from(__t)
769 };
770
771 let mut cpu_dst =
772 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
773 cpu_converter.convert(
774 &g2d_dst,
775 &mut cpu_dst,
776 Rotation::None,
777 Flip::None,
778 Crop::no_crop(),
779 )?;
780
781 compare_images(
782 &reference,
783 &cpu_dst,
784 0.98,
785 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
786 )
787 }
788
789 fn compare_images(
790 img1: &TensorDyn,
791 img2: &TensorDyn,
792 threshold: f64,
793 name: &str,
794 ) -> Result<(), crate::Error> {
795 assert_eq!(img1.height(), img2.height(), "Heights differ");
796 assert_eq!(img1.width(), img2.width(), "Widths differ");
797 assert_eq!(
798 img1.format().unwrap(),
799 img2.format().unwrap(),
800 "PixelFormat differ"
801 );
802 assert!(
803 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
804 "format must be Rgb or Rgba for comparison"
805 );
806 let image1 = match img1.format().unwrap() {
807 PixelFormat::Rgb => image::RgbImage::from_vec(
808 img1.width().unwrap() as u32,
809 img1.height().unwrap() as u32,
810 img1.as_u8().unwrap().map().unwrap().to_vec(),
811 )
812 .unwrap(),
813 PixelFormat::Rgba => image::RgbaImage::from_vec(
814 img1.width().unwrap() as u32,
815 img1.height().unwrap() as u32,
816 img1.as_u8().unwrap().map().unwrap().to_vec(),
817 )
818 .unwrap()
819 .convert(),
820
821 _ => unreachable!(),
822 };
823
824 let image2 = match img2.format().unwrap() {
825 PixelFormat::Rgb => image::RgbImage::from_vec(
826 img2.width().unwrap() as u32,
827 img2.height().unwrap() as u32,
828 img2.as_u8().unwrap().map().unwrap().to_vec(),
829 )
830 .unwrap(),
831 PixelFormat::Rgba => image::RgbaImage::from_vec(
832 img2.width().unwrap() as u32,
833 img2.height().unwrap() as u32,
834 img2.as_u8().unwrap().map().unwrap().to_vec(),
835 )
836 .unwrap()
837 .convert(),
838
839 _ => unreachable!(),
840 };
841
842 let similarity = image_compare::rgb_similarity_structure(
843 &image_compare::Algorithm::RootMeanSquared,
844 &image1,
845 &image2,
846 )
847 .expect("Image Comparison failed");
848
849 if similarity.score < threshold {
850 image1.save(format!("{name}_1.png")).unwrap();
851 image2.save(format!("{name}_2.png")).unwrap();
852 return Err(Error::Internal(format!(
853 "{name}: converted image and target image have similarity score too low: {} < {}",
854 similarity.score, threshold
855 )));
856 }
857 Ok(())
858 }
859
860 fn load_raw_image(
866 width: usize,
867 height: usize,
868 format: PixelFormat,
869 memory: Option<TensorMemory>,
870 bytes: &[u8],
871 ) -> Result<TensorDyn, crate::Error> {
872 let img = TensorDyn::image(width, height, format, DType::U8, memory)?;
873 let mut map = img.as_u8().unwrap().map()?;
874 map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
875 Ok(img)
876 }
877
878 #[test]
880 #[cfg(target_os = "linux")]
881 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
882 if !is_dma_available() {
883 return Ok(());
884 }
885 let src = load_raw_image(
887 1280,
888 720,
889 PixelFormat::Nv12,
890 Some(TensorMemory::Dma),
891 &edgefirst_bench::testdata::read("camera720p.nv12"),
892 )?;
893
894 let reference = load_raw_image(
896 1280,
897 720,
898 PixelFormat::Rgba,
899 None,
900 &edgefirst_bench::testdata::read("camera720p.rgba"),
901 )?;
902
903 let mut dst = TensorDyn::image(
905 1280,
906 720,
907 PixelFormat::Rgba,
908 DType::U8,
909 Some(TensorMemory::Dma),
910 )?;
911 let mut g2d = G2DProcessor::new()?;
912 let src_dyn = src;
913 let mut dst_dyn = dst;
914 g2d.convert(
915 &src_dyn,
916 &mut dst_dyn,
917 Rotation::None,
918 Flip::None,
919 Crop::no_crop(),
920 )?;
921 dst = {
922 let mut __t = dst_dyn.into_u8().unwrap();
923 __t.set_format(PixelFormat::Rgba)
924 .map_err(|e| crate::Error::Internal(e.to_string()))?;
925 TensorDyn::from(__t)
926 };
927
928 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
930 cpu_dst
931 .as_u8()
932 .unwrap()
933 .map()?
934 .as_mut_slice()
935 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
936
937 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
938 }
939
940 #[test]
942 #[cfg(target_os = "linux")]
943 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
944 if !is_dma_available() {
945 return Ok(());
946 }
947 let src = load_raw_image(
949 1280,
950 720,
951 PixelFormat::Nv12,
952 Some(TensorMemory::Dma),
953 &edgefirst_bench::testdata::read("camera720p.nv12"),
954 )?;
955
956 let reference = load_raw_image(
958 1280,
959 720,
960 PixelFormat::Rgb,
961 None,
962 &edgefirst_bench::testdata::read("camera720p.rgb"),
963 )?;
964
965 let mut dst = TensorDyn::image(
967 1280,
968 720,
969 PixelFormat::Rgb,
970 DType::U8,
971 Some(TensorMemory::Dma),
972 )?;
973 let mut g2d = G2DProcessor::new()?;
974 let src_dyn = src;
975 let mut dst_dyn = dst;
976 g2d.convert(
977 &src_dyn,
978 &mut dst_dyn,
979 Rotation::None,
980 Flip::None,
981 Crop::no_crop(),
982 )?;
983 dst = {
984 let mut __t = dst_dyn.into_u8().unwrap();
985 __t.set_format(PixelFormat::Rgb)
986 .map_err(|e| crate::Error::Internal(e.to_string()))?;
987 TensorDyn::from(__t)
988 };
989
990 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
992 cpu_dst
993 .as_u8()
994 .unwrap()
995 .map()?
996 .as_mut_slice()
997 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
998
999 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1000 }
1001
1002 #[test]
1004 #[cfg(target_os = "linux")]
1005 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1006 if !is_dma_available() {
1007 return Ok(());
1008 }
1009 let src = load_raw_image(
1011 1280,
1012 720,
1013 PixelFormat::Yuyv,
1014 Some(TensorMemory::Dma),
1015 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1016 )?;
1017
1018 let reference = load_raw_image(
1020 1280,
1021 720,
1022 PixelFormat::Rgba,
1023 None,
1024 &edgefirst_bench::testdata::read("camera720p.rgba"),
1025 )?;
1026
1027 let mut dst = TensorDyn::image(
1029 1280,
1030 720,
1031 PixelFormat::Rgba,
1032 DType::U8,
1033 Some(TensorMemory::Dma),
1034 )?;
1035 let mut g2d = G2DProcessor::new()?;
1036 let src_dyn = src;
1037 let mut dst_dyn = dst;
1038 g2d.convert(
1039 &src_dyn,
1040 &mut dst_dyn,
1041 Rotation::None,
1042 Flip::None,
1043 Crop::no_crop(),
1044 )?;
1045 dst = {
1046 let mut __t = dst_dyn.into_u8().unwrap();
1047 __t.set_format(PixelFormat::Rgba)
1048 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1049 TensorDyn::from(__t)
1050 };
1051
1052 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1054 cpu_dst
1055 .as_u8()
1056 .unwrap()
1057 .map()?
1058 .as_mut_slice()
1059 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1060
1061 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1062 }
1063
1064 #[test]
1066 #[cfg(target_os = "linux")]
1067 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1068 if !is_dma_available() {
1069 return Ok(());
1070 }
1071 let src = load_raw_image(
1073 1280,
1074 720,
1075 PixelFormat::Yuyv,
1076 Some(TensorMemory::Dma),
1077 &edgefirst_bench::testdata::read("camera720p.yuyv"),
1078 )?;
1079
1080 let reference = load_raw_image(
1082 1280,
1083 720,
1084 PixelFormat::Rgb,
1085 None,
1086 &edgefirst_bench::testdata::read("camera720p.rgb"),
1087 )?;
1088
1089 let mut dst = TensorDyn::image(
1091 1280,
1092 720,
1093 PixelFormat::Rgb,
1094 DType::U8,
1095 Some(TensorMemory::Dma),
1096 )?;
1097 let mut g2d = G2DProcessor::new()?;
1098 let src_dyn = src;
1099 let mut dst_dyn = dst;
1100 g2d.convert(
1101 &src_dyn,
1102 &mut dst_dyn,
1103 Rotation::None,
1104 Flip::None,
1105 Crop::no_crop(),
1106 )?;
1107 dst = {
1108 let mut __t = dst_dyn.into_u8().unwrap();
1109 __t.set_format(PixelFormat::Rgb)
1110 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1111 TensorDyn::from(__t)
1112 };
1113
1114 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1116 cpu_dst
1117 .as_u8()
1118 .unwrap()
1119 .map()?
1120 .as_mut_slice()
1121 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1122
1123 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1124 }
1125
1126 #[test]
1129 #[cfg(target_os = "linux")]
1130 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1131 fn test_g2d_bgra_no_resize() {
1132 for src_fmt in [
1133 PixelFormat::Rgba,
1134 PixelFormat::Yuyv,
1135 PixelFormat::Nv12,
1136 PixelFormat::Bgra,
1137 ] {
1138 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1139 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1140 });
1141 }
1142 }
1143
1144 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1145 let file = edgefirst_bench::testdata::read("zidane.jpg").to_vec();
1146 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
1147
1148 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
1150 let mut cpu_converter = CPUProcessor::new();
1151
1152 if g2d_in_fmt == PixelFormat::Nv12 {
1153 let nv12_bytes = edgefirst_bench::testdata::read("zidane.nv12");
1154 src2.as_u8()
1155 .unwrap()
1156 .map()?
1157 .as_mut_slice()
1158 .copy_from_slice(&nv12_bytes);
1159 } else {
1160 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1161 }
1162
1163 let mut g2d = G2DProcessor::new()?;
1164
1165 let mut bgra_dst = TensorDyn::image(
1167 1280,
1168 720,
1169 PixelFormat::Bgra,
1170 DType::U8,
1171 Some(TensorMemory::Dma),
1172 )?;
1173 let src2_dyn = src2;
1174 let mut bgra_dst_dyn = bgra_dst;
1175 g2d.convert(
1176 &src2_dyn,
1177 &mut bgra_dst_dyn,
1178 Rotation::None,
1179 Flip::None,
1180 Crop::no_crop(),
1181 )?;
1182 bgra_dst = {
1183 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1184 __t.set_format(PixelFormat::Bgra)
1185 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1186 TensorDyn::from(__t)
1187 };
1188
1189 let src2 = {
1191 let mut __t = src2_dyn.into_u8().unwrap();
1192 __t.set_format(g2d_in_fmt)
1193 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1194 TensorDyn::from(__t)
1195 };
1196
1197 let mut rgba_dst = TensorDyn::image(
1199 1280,
1200 720,
1201 PixelFormat::Rgba,
1202 DType::U8,
1203 Some(TensorMemory::Dma),
1204 )?;
1205 let src2_dyn2 = src2;
1206 let mut rgba_dst_dyn = rgba_dst;
1207 g2d.convert(
1208 &src2_dyn2,
1209 &mut rgba_dst_dyn,
1210 Rotation::None,
1211 Flip::None,
1212 Crop::no_crop(),
1213 )?;
1214 rgba_dst = {
1215 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1216 __t.set_format(PixelFormat::Rgba)
1217 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1218 TensorDyn::from(__t)
1219 };
1220
1221 let bgra_cpu = TensorDyn::image(1280, 720, PixelFormat::Bgra, DType::U8, None)?;
1223 bgra_cpu
1224 .as_u8()
1225 .unwrap()
1226 .map()?
1227 .as_mut_slice()
1228 .copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
1229
1230 let rgba_cpu = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1231 rgba_cpu
1232 .as_u8()
1233 .unwrap()
1234 .map()?
1235 .as_mut_slice()
1236 .copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
1237
1238 let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
1240 let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
1241 let bgra_buf = bgra_map.as_slice();
1242 let rgba_buf = rgba_map.as_slice();
1243
1244 assert_eq!(bgra_buf.len(), rgba_buf.len());
1245 for (i, (bc, rc)) in bgra_buf
1246 .chunks_exact(4)
1247 .zip(rgba_buf.chunks_exact(4))
1248 .enumerate()
1249 {
1250 assert_eq!(
1251 bc[0], rc[2],
1252 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1253 );
1254 assert_eq!(
1255 bc[1], rc[1],
1256 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1257 );
1258 assert_eq!(
1259 bc[2], rc[0],
1260 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1261 );
1262 assert_eq!(
1263 bc[3], rc[3],
1264 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1265 );
1266 }
1267 Ok(())
1268 }
1269
1270 fn surface_for(
1281 width: usize,
1282 height: usize,
1283 fmt: PixelFormat,
1284 offset: Option<usize>,
1285 row_stride: Option<usize>,
1286 ) -> Result<G2DSurface, crate::Error> {
1287 use edgefirst_tensor::TensorMemory;
1288 let mut t = Tensor::<u8>::image(width, height, fmt, Some(TensorMemory::Dma))?;
1289 if let Some(o) = offset {
1290 t.set_plane_offset(o);
1291 }
1292 if let Some(s) = row_stride {
1293 t.set_row_stride_unchecked(s);
1294 }
1295 tensor_to_g2d_surface(&t)
1296 }
1297
1298 #[test]
1299 fn g2d_surface_single_plane_no_offset() {
1300 if !is_dma_available() {
1301 return;
1302 }
1303 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1304 assert_ne!(s.planes[0], 0);
1306 assert_eq!(s.stride, 640);
1307 }
1308
1309 #[test]
1310 fn g2d_surface_single_plane_with_offset() {
1311 if !is_dma_available() {
1312 return;
1313 }
1314 use edgefirst_tensor::TensorMemory;
1315 let mut t =
1316 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1317 let s0 = tensor_to_g2d_surface(&t).unwrap();
1318 t.set_plane_offset(4096);
1319 let s1 = tensor_to_g2d_surface(&t).unwrap();
1320 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1321 }
1322
1323 #[test]
1324 fn g2d_surface_single_plane_zero_offset() {
1325 if !is_dma_available() {
1326 return;
1327 }
1328 use edgefirst_tensor::TensorMemory;
1329 let mut t =
1330 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1331 let s_none = tensor_to_g2d_surface(&t).unwrap();
1332 t.set_plane_offset(0);
1333 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1334 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1336 }
1337
1338 #[test]
1339 fn g2d_surface_stride_rgba() {
1340 if !is_dma_available() {
1341 return;
1342 }
1343 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1345 assert_eq!(s_default.stride, 640);
1346
1347 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1349 assert_eq!(s_custom.stride, 704);
1350 }
1351
1352 #[test]
1353 fn g2d_surface_stride_rgb() {
1354 if !is_dma_available() {
1355 return;
1356 }
1357 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1358 assert_eq!(s_default.stride, 640);
1359
1360 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1362 assert_eq!(s_custom.stride, 660);
1363 }
1364
1365 #[test]
1366 fn g2d_surface_stride_grey() {
1367 if !is_dma_available() {
1368 return;
1369 }
1370 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1372 Ok(s) => s,
1373 Err(crate::Error::G2D(..)) => return,
1374 Err(e) => panic!("unexpected error: {e:?}"),
1375 };
1376 assert_eq!(s.stride, 1024);
1378 }
1379
1380 #[test]
1381 fn g2d_surface_contiguous_nv12_offset() {
1382 if !is_dma_available() {
1383 return;
1384 }
1385 use edgefirst_tensor::TensorMemory;
1386 let mut t =
1387 Tensor::<u8>::image(640, 480, PixelFormat::Nv12, Some(TensorMemory::Dma)).unwrap();
1388 let s0 = tensor_to_g2d_surface(&t).unwrap();
1389
1390 t.set_plane_offset(8192);
1391 let s1 = tensor_to_g2d_surface(&t).unwrap();
1392
1393 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1395 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1399 }
1400
1401 #[test]
1402 fn g2d_surface_contiguous_nv12_stride() {
1403 if !is_dma_available() {
1404 return;
1405 }
1406 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1408 assert_eq!(s.stride, 640);
1409
1410 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1412 assert_eq!(s_padded.stride, 1024);
1413 }
1414
1415 #[test]
1416 fn g2d_surface_multiplane_nv12_offset() {
1417 if !is_dma_available() {
1418 return;
1419 }
1420 use edgefirst_tensor::TensorMemory;
1421
1422 let mut luma =
1424 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Dma), Some("luma")).unwrap();
1425 let mut chroma =
1426 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Dma), Some("chroma")).unwrap();
1427
1428 let luma_base = {
1430 let dma = luma.as_dma().unwrap();
1431 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1432 phys.address()
1433 };
1434 let chroma_base = {
1435 let dma = chroma.as_dma().unwrap();
1436 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1437 phys.address()
1438 };
1439
1440 luma.set_plane_offset(4096);
1442 chroma.set_plane_offset(2048);
1443 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1444 let s = tensor_to_g2d_surface(&combined).unwrap();
1445
1446 assert_eq!(s.planes[0], luma_base + 4096);
1448 assert_eq!(s.planes[1], chroma_base + 2048);
1450 }
1451}