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 if log::log_enabled!(log::Level::Trace) {
64 log::trace!(
65 "G2D convert: {:?}({:?}/{:?}) → {:?}({:?}/{:?})",
66 src_dyn.format(),
67 src_dyn.dtype(),
68 src_dyn.memory(),
69 dst_dyn.format(),
70 dst_dyn.dtype(),
71 dst_dyn.memory(),
72 );
73 }
74
75 if src_dyn.dtype() != DType::U8 {
76 return Err(Error::NotSupported(
77 "G2D only supports u8 source tensors".to_string(),
78 ));
79 }
80 let is_int8_dst = dst_dyn.dtype() == DType::I8;
81 if dst_dyn.dtype() != DType::U8 && !is_int8_dst {
82 return Err(Error::NotSupported(
83 "G2D only supports u8 or i8 destination tensors".to_string(),
84 ));
85 }
86
87 let src_fmt = src_dyn.format().ok_or(Error::NotAnImage)?;
88 let dst_fmt = dst_dyn.format().ok_or(Error::NotAnImage)?;
89
90 use PixelFormat::*;
92 match (src_fmt, dst_fmt) {
93 (Rgba, Rgba) => {}
94 (Rgba, Yuyv) => {}
95 (Rgba, Rgb) => {}
96 (Yuyv, Rgba) => {}
97 (Yuyv, Yuyv) => {}
98 (Yuyv, Rgb) => {}
99 (Nv12, Rgba) => {}
102 (Nv12, Yuyv) => {}
103 (Nv12, Rgb) => {}
104 (Rgba, Bgra) => {}
105 (Yuyv, Bgra) => {}
106 (Nv12, Bgra) => {}
107 (Bgra, Bgra) => {}
108 (s, d) => {
109 return Err(Error::NotSupported(format!(
110 "G2D does not support {} to {} conversion",
111 s, d
112 )));
113 }
114 }
115
116 crop.check_crop_dyn(src_dyn, dst_dyn)?;
117
118 let src = src_dyn.as_u8().unwrap();
119 let dst = if is_int8_dst {
122 let i8_tensor = dst_dyn.as_i8_mut().unwrap();
130 unsafe { &mut *(i8_tensor as *mut Tensor<i8> as *mut Tensor<u8>) }
131 } else {
132 dst_dyn.as_u8_mut().unwrap()
133 };
134
135 let mut src_surface = tensor_to_g2d_surface(src)?;
136 let mut dst_surface = tensor_to_g2d_surface(dst)?;
137
138 src_surface.rot = match flip {
139 Flip::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
140 Flip::Vertical => g2d_sys::g2d_rotation_G2D_FLIP_V,
141 Flip::Horizontal => g2d_sys::g2d_rotation_G2D_FLIP_H,
142 };
143
144 dst_surface.rot = match rotation {
145 Rotation::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
146 Rotation::Clockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_90,
147 Rotation::Rotate180 => g2d_sys::g2d_rotation_G2D_ROTATION_180,
148 Rotation::CounterClockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_270,
149 };
150
151 if let Some(crop_rect) = crop.src_rect {
152 src_surface.left = crop_rect.left as i32;
153 src_surface.top = crop_rect.top as i32;
154 src_surface.right = (crop_rect.left + crop_rect.width) as i32;
155 src_surface.bottom = (crop_rect.top + crop_rect.height) as i32;
156 }
157
158 let dst_w = dst.width().unwrap();
159 let dst_h = dst.height().unwrap();
160
161 let needs_clear = crop.dst_color.is_some()
167 && crop.dst_rect.is_some_and(|dst_rect| {
168 dst_rect.left != 0
169 || dst_rect.top != 0
170 || dst_rect.width != dst_w
171 || dst_rect.height != dst_h
172 });
173
174 if needs_clear && dst_fmt != Rgb {
175 if let Some(dst_color) = crop.dst_color {
176 let start = Instant::now();
177 self.g2d.clear(&mut dst_surface, dst_color)?;
178 log::trace!("g2d clear takes {:?}", start.elapsed());
179 }
180 }
181
182 if let Some(crop_rect) = crop.dst_rect {
183 dst_surface.planes[0] += ((crop_rect.top * dst_surface.stride as usize
187 + crop_rect.left)
188 * dst_fmt.channels()) as u64;
189
190 dst_surface.right = crop_rect.width as i32;
191 dst_surface.bottom = crop_rect.height as i32;
192 dst_surface.width = crop_rect.width as i32;
193 dst_surface.height = crop_rect.height as i32;
194 }
195
196 log::trace!("G2D blit: {src_fmt}→{dst_fmt} int8={is_int8_dst}");
197 self.g2d.blit(&src_surface, &dst_surface)?;
198 self.g2d.finish()?;
199 log::trace!("G2D blit complete");
200
201 if needs_clear && dst_fmt == Rgb {
203 if let (Some(dst_color), Some(dst_rect)) = (crop.dst_color, crop.dst_rect) {
204 let start = Instant::now();
205 CPUProcessor::fill_image_outside_crop_u8(dst, dst_color, dst_rect)?;
206 log::trace!("cpu fill takes {:?}", start.elapsed());
207 }
208 }
209
210 if is_int8_dst {
218 let start = Instant::now();
219 let mut map = dst.map()?;
220 crate::cpu::apply_int8_xor_bias(map.as_mut_slice(), dst_fmt);
221 log::trace!("g2d int8 XOR 0x80 post-pass takes {:?}", start.elapsed());
222 }
223
224 Ok(())
225 }
226}
227
228impl ImageProcessorTrait for G2DProcessor {
229 fn convert(
230 &mut self,
231 src: &TensorDyn,
232 dst: &mut TensorDyn,
233 rotation: Rotation,
234 flip: Flip,
235 crop: Crop,
236 ) -> Result<()> {
237 self.convert_impl(src, dst, rotation, flip, crop)
238 }
239
240 fn draw_decoded_masks(
241 &mut self,
242 dst: &mut TensorDyn,
243 detect: &[crate::DetectBox],
244 segmentation: &[crate::Segmentation],
245 overlay: crate::MaskOverlay<'_>,
246 ) -> Result<()> {
247 if !detect.is_empty() || !segmentation.is_empty() {
251 return Err(Error::NotImplemented(
252 "G2D does not support drawing detection or segmentation overlays".to_string(),
253 ));
254 }
255 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
256 }
257
258 fn draw_proto_masks(
259 &mut self,
260 dst: &mut TensorDyn,
261 detect: &[crate::DetectBox],
262 _proto_data: &crate::ProtoData,
263 overlay: crate::MaskOverlay<'_>,
264 ) -> Result<()> {
265 if !detect.is_empty() {
268 return Err(Error::NotImplemented(
269 "G2D does not support drawing detection or segmentation overlays".to_string(),
270 ));
271 }
272 draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
273 }
274
275 fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
276 Err(Error::NotImplemented(
277 "G2D does not support setting colors for rendering detection or segmentation overlays"
278 .to_string(),
279 ))
280 }
281}
282
283fn draw_empty_frame_g2d(
296 g2d: &mut G2D,
297 dst_dyn: &mut TensorDyn,
298 background: Option<&TensorDyn>,
299) -> Result<()> {
300 if dst_dyn.dtype() != DType::U8 {
301 return Err(Error::NotSupported(
302 "G2D only supports u8 destination tensors".to_string(),
303 ));
304 }
305 let dst = dst_dyn.as_u8_mut().ok_or(Error::NotAnImage)?;
306
307 if dst.as_dma().is_none() {
311 return Err(Error::NotImplemented(
312 "g2d only supports Dma memory".to_string(),
313 ));
314 }
315
316 let mut dst_surface = tensor_to_g2d_surface(dst)?;
317
318 match background {
319 None => {
320 let start = Instant::now();
324 g2d.clear(&mut dst_surface, [0, 0, 0, 0])?;
325 g2d.finish()?;
326 log::trace!("g2d clear (empty frame) takes {:?}", start.elapsed());
327 }
328 Some(bg_dyn) => {
329 if bg_dyn.shape() != dst.shape() {
334 return Err(Error::InvalidShape(
335 "background shape does not match dst".into(),
336 ));
337 }
338 if bg_dyn.format() != dst.format() {
339 return Err(Error::InvalidShape(
340 "background pixel format does not match dst".into(),
341 ));
342 }
343 if bg_dyn.dtype() != DType::U8 {
344 return Err(Error::NotSupported(
345 "G2D only supports u8 background tensors".to_string(),
346 ));
347 }
348 let bg = bg_dyn.as_u8().ok_or(Error::NotAnImage)?;
349 if bg.as_dma().is_none() {
350 return Err(Error::NotImplemented(
351 "g2d background must be Dma-backed".to_string(),
352 ));
353 }
354 let src_surface = tensor_to_g2d_surface(bg)?;
355 let start = Instant::now();
356 g2d.blit(&src_surface, &dst_surface)?;
357 g2d.finish()?;
358 log::trace!("g2d blit (bg→dst) takes {:?}", start.elapsed());
359 }
360 }
361 Ok(())
362}
363
364fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
368 let fmt = img.format().ok_or(Error::NotAnImage)?;
369 let dma = img
370 .as_dma()
371 .ok_or_else(|| Error::NotImplemented("g2d only supports Dma memory".to_string()))?;
372 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into()?;
373
374 let base_addr = phys.address();
381 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
382 let planes = if fmt == PixelFormat::Nv12 {
383 if img.is_multiplane() {
384 let chroma = img.chroma().unwrap();
386 let chroma_dma = chroma.as_dma().ok_or_else(|| {
387 Error::NotImplemented("g2d multiplane chroma must be DMA-backed".to_string())
388 })?;
389 let uv_phys: G2DPhysical = chroma_dma.fd.as_raw_fd().try_into()?;
390 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
391 [
392 base_addr + luma_offset,
393 uv_phys.address() + chroma_offset,
394 0,
395 ]
396 } else {
397 let w = img.width().unwrap();
398 let h = img.height().unwrap();
399 let stride = img.effective_row_stride().unwrap_or(w);
400 let uv_offset = (luma_offset as usize + stride * h) as u64;
401 [base_addr + luma_offset, base_addr + uv_offset, 0]
402 }
403 } else {
404 [base_addr + luma_offset, 0, 0]
405 };
406
407 let w = img.width().unwrap();
408 let h = img.height().unwrap();
409 let fourcc = pixelfmt_to_fourcc(fmt);
410
411 let stride_pixels = match img.effective_row_stride() {
414 Some(s) => {
415 let channels = fmt.channels();
416 if s % channels != 0 {
417 return Err(Error::NotImplemented(
418 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
419 ));
420 }
421 s / channels
422 }
423 None => w,
424 };
425
426 Ok(G2DSurface {
427 planes,
428 format: G2DFormat::try_from(fourcc)?.format(),
429 left: 0,
430 top: 0,
431 right: w as i32,
432 bottom: h as i32,
433 stride: stride_pixels as i32,
434 width: w as i32,
435 height: h as i32,
436 blendfunc: 0,
437 clrcolor: 0,
438 rot: 0,
439 global_alpha: 0,
440 })
441}
442
443#[cfg(feature = "g2d_test_formats")]
444#[cfg(test)]
445mod g2d_tests {
446 use super::*;
447 use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation};
448 use edgefirst_tensor::{
449 is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
450 };
451 use image::buffer::ConvertBuffer;
452
453 #[test]
454 #[cfg(target_os = "linux")]
455 fn test_g2d_formats_no_resize() {
456 for i in [
457 PixelFormat::Rgba,
458 PixelFormat::Yuyv,
459 PixelFormat::Rgb,
460 PixelFormat::Grey,
461 PixelFormat::Nv12,
462 ] {
463 for o in [
464 PixelFormat::Rgba,
465 PixelFormat::Yuyv,
466 PixelFormat::Rgb,
467 PixelFormat::Grey,
468 ] {
469 let res = test_g2d_format_no_resize_(i, o);
470 if let Err(e) = res {
471 println!("{i} to {o} failed: {e:?}");
472 } else {
473 println!("{i} to {o} success");
474 }
475 }
476 }
477 }
478
479 fn test_g2d_format_no_resize_(
480 g2d_in_fmt: PixelFormat,
481 g2d_out_fmt: PixelFormat,
482 ) -> Result<(), crate::Error> {
483 let dst_width = 1280;
484 let dst_height = 720;
485 let file = include_bytes!(concat!(
486 env!("CARGO_MANIFEST_DIR"),
487 "/../../testdata/zidane.jpg"
488 ))
489 .to_vec();
490 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
491
492 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
494
495 let mut cpu_converter = CPUProcessor::new();
496
497 if g2d_in_fmt == PixelFormat::Nv12 {
499 let nv12_bytes = include_bytes!(concat!(
500 env!("CARGO_MANIFEST_DIR"),
501 "/../../testdata/zidane.nv12"
502 ));
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 = include_bytes!(concat!(
613 env!("CARGO_MANIFEST_DIR"),
614 "/../../testdata/zidane.jpg"
615 ))
616 .to_vec();
617 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
618
619 let mut cpu_converter = CPUProcessor::new();
620
621 let mut reference = TensorDyn::image(
622 dst_width,
623 dst_height,
624 PixelFormat::Rgb,
625 DType::U8,
626 Some(TensorMemory::Dma),
627 )?;
628 cpu_converter.convert(
629 &src,
630 &mut reference,
631 Rotation::None,
632 Flip::None,
633 Crop::no_crop(),
634 )?;
635
636 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
638
639 if g2d_in_fmt == PixelFormat::Nv12 {
641 let nv12_bytes = include_bytes!(concat!(
642 env!("CARGO_MANIFEST_DIR"),
643 "/../../testdata/zidane.nv12"
644 ));
645 src2.as_u8()
646 .unwrap()
647 .map()?
648 .as_mut_slice()
649 .copy_from_slice(nv12_bytes);
650 } else {
651 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
652 }
653
654 let mut g2d_dst = TensorDyn::image(
655 dst_width,
656 dst_height,
657 g2d_out_fmt,
658 DType::U8,
659 Some(TensorMemory::Dma),
660 )?;
661 let mut g2d_converter = G2DProcessor::new()?;
662 let src2_dyn = src2;
663 let mut g2d_dst_dyn = g2d_dst;
664 g2d_converter.convert(
665 &src2_dyn,
666 &mut g2d_dst_dyn,
667 Rotation::None,
668 Flip::None,
669 Crop::no_crop(),
670 )?;
671 g2d_dst = {
672 let mut __t = g2d_dst_dyn.into_u8().unwrap();
673 __t.set_format(g2d_out_fmt)
674 .map_err(|e| crate::Error::Internal(e.to_string()))?;
675 TensorDyn::from(__t)
676 };
677
678 let mut cpu_dst =
679 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
680 cpu_converter.convert(
681 &g2d_dst,
682 &mut cpu_dst,
683 Rotation::None,
684 Flip::None,
685 Crop::no_crop(),
686 )?;
687
688 compare_images(
689 &reference,
690 &cpu_dst,
691 0.98,
692 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
693 )
694 }
695
696 fn test_g2d_format_with_resize_dst_crop(
697 g2d_in_fmt: PixelFormat,
698 g2d_out_fmt: PixelFormat,
699 ) -> Result<(), crate::Error> {
700 let dst_width = 600;
701 let dst_height = 400;
702 let crop = Crop {
703 src_rect: None,
704 dst_rect: Some(Rect {
705 top: 100,
706 left: 100,
707 height: 100,
708 width: 200,
709 }),
710 dst_color: None,
711 };
712 let file = include_bytes!(concat!(
713 env!("CARGO_MANIFEST_DIR"),
714 "/../../testdata/zidane.jpg"
715 ))
716 .to_vec();
717 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
718
719 let mut cpu_converter = CPUProcessor::new();
720
721 let mut reference = TensorDyn::image(
722 dst_width,
723 dst_height,
724 PixelFormat::Rgb,
725 DType::U8,
726 Some(TensorMemory::Dma),
727 )?;
728 reference
729 .as_u8()
730 .unwrap()
731 .map()
732 .unwrap()
733 .as_mut_slice()
734 .fill(128);
735 cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
736
737 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
739
740 if g2d_in_fmt == PixelFormat::Nv12 {
742 let nv12_bytes = include_bytes!(concat!(
743 env!("CARGO_MANIFEST_DIR"),
744 "/../../testdata/zidane.nv12"
745 ));
746 src2.as_u8()
747 .unwrap()
748 .map()?
749 .as_mut_slice()
750 .copy_from_slice(nv12_bytes);
751 } else {
752 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
753 }
754
755 let mut g2d_dst = TensorDyn::image(
756 dst_width,
757 dst_height,
758 g2d_out_fmt,
759 DType::U8,
760 Some(TensorMemory::Dma),
761 )?;
762 g2d_dst
763 .as_u8()
764 .unwrap()
765 .map()
766 .unwrap()
767 .as_mut_slice()
768 .fill(128);
769 let mut g2d_converter = G2DProcessor::new()?;
770 let src2_dyn = src2;
771 let mut g2d_dst_dyn = g2d_dst;
772 g2d_converter.convert(
773 &src2_dyn,
774 &mut g2d_dst_dyn,
775 Rotation::None,
776 Flip::None,
777 crop,
778 )?;
779 g2d_dst = {
780 let mut __t = g2d_dst_dyn.into_u8().unwrap();
781 __t.set_format(g2d_out_fmt)
782 .map_err(|e| crate::Error::Internal(e.to_string()))?;
783 TensorDyn::from(__t)
784 };
785
786 let mut cpu_dst =
787 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
788 cpu_converter.convert(
789 &g2d_dst,
790 &mut cpu_dst,
791 Rotation::None,
792 Flip::None,
793 Crop::no_crop(),
794 )?;
795
796 compare_images(
797 &reference,
798 &cpu_dst,
799 0.98,
800 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
801 )
802 }
803
804 fn compare_images(
805 img1: &TensorDyn,
806 img2: &TensorDyn,
807 threshold: f64,
808 name: &str,
809 ) -> Result<(), crate::Error> {
810 assert_eq!(img1.height(), img2.height(), "Heights differ");
811 assert_eq!(img1.width(), img2.width(), "Widths differ");
812 assert_eq!(
813 img1.format().unwrap(),
814 img2.format().unwrap(),
815 "PixelFormat differ"
816 );
817 assert!(
818 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
819 "format must be Rgb or Rgba for comparison"
820 );
821 let image1 = match img1.format().unwrap() {
822 PixelFormat::Rgb => image::RgbImage::from_vec(
823 img1.width().unwrap() as u32,
824 img1.height().unwrap() as u32,
825 img1.as_u8().unwrap().map().unwrap().to_vec(),
826 )
827 .unwrap(),
828 PixelFormat::Rgba => image::RgbaImage::from_vec(
829 img1.width().unwrap() as u32,
830 img1.height().unwrap() as u32,
831 img1.as_u8().unwrap().map().unwrap().to_vec(),
832 )
833 .unwrap()
834 .convert(),
835
836 _ => unreachable!(),
837 };
838
839 let image2 = match img2.format().unwrap() {
840 PixelFormat::Rgb => image::RgbImage::from_vec(
841 img2.width().unwrap() as u32,
842 img2.height().unwrap() as u32,
843 img2.as_u8().unwrap().map().unwrap().to_vec(),
844 )
845 .unwrap(),
846 PixelFormat::Rgba => image::RgbaImage::from_vec(
847 img2.width().unwrap() as u32,
848 img2.height().unwrap() as u32,
849 img2.as_u8().unwrap().map().unwrap().to_vec(),
850 )
851 .unwrap()
852 .convert(),
853
854 _ => unreachable!(),
855 };
856
857 let similarity = image_compare::rgb_similarity_structure(
858 &image_compare::Algorithm::RootMeanSquared,
859 &image1,
860 &image2,
861 )
862 .expect("Image Comparison failed");
863
864 if similarity.score < threshold {
865 image1.save(format!("{name}_1.png")).unwrap();
866 image2.save(format!("{name}_2.png")).unwrap();
867 return Err(Error::Internal(format!(
868 "{name}: converted image and target image have similarity score too low: {} < {}",
869 similarity.score, threshold
870 )));
871 }
872 Ok(())
873 }
874
875 fn load_raw_image(
881 width: usize,
882 height: usize,
883 format: PixelFormat,
884 memory: Option<TensorMemory>,
885 bytes: &[u8],
886 ) -> Result<TensorDyn, crate::Error> {
887 let img = TensorDyn::image(width, height, format, DType::U8, memory)?;
888 let mut map = img.as_u8().unwrap().map()?;
889 map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
890 Ok(img)
891 }
892
893 #[test]
895 #[cfg(target_os = "linux")]
896 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
897 if !is_dma_available() {
898 return Ok(());
899 }
900 let src = load_raw_image(
902 1280,
903 720,
904 PixelFormat::Nv12,
905 Some(TensorMemory::Dma),
906 include_bytes!(concat!(
907 env!("CARGO_MANIFEST_DIR"),
908 "/../../testdata/camera720p.nv12"
909 )),
910 )?;
911
912 let reference = load_raw_image(
914 1280,
915 720,
916 PixelFormat::Rgba,
917 None,
918 include_bytes!(concat!(
919 env!("CARGO_MANIFEST_DIR"),
920 "/../../testdata/camera720p.rgba"
921 )),
922 )?;
923
924 let mut dst = TensorDyn::image(
926 1280,
927 720,
928 PixelFormat::Rgba,
929 DType::U8,
930 Some(TensorMemory::Dma),
931 )?;
932 let mut g2d = G2DProcessor::new()?;
933 let src_dyn = src;
934 let mut dst_dyn = dst;
935 g2d.convert(
936 &src_dyn,
937 &mut dst_dyn,
938 Rotation::None,
939 Flip::None,
940 Crop::no_crop(),
941 )?;
942 dst = {
943 let mut __t = dst_dyn.into_u8().unwrap();
944 __t.set_format(PixelFormat::Rgba)
945 .map_err(|e| crate::Error::Internal(e.to_string()))?;
946 TensorDyn::from(__t)
947 };
948
949 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
951 cpu_dst
952 .as_u8()
953 .unwrap()
954 .map()?
955 .as_mut_slice()
956 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
957
958 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
959 }
960
961 #[test]
963 #[cfg(target_os = "linux")]
964 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
965 if !is_dma_available() {
966 return Ok(());
967 }
968 let src = load_raw_image(
970 1280,
971 720,
972 PixelFormat::Nv12,
973 Some(TensorMemory::Dma),
974 include_bytes!(concat!(
975 env!("CARGO_MANIFEST_DIR"),
976 "/../../testdata/camera720p.nv12"
977 )),
978 )?;
979
980 let reference = load_raw_image(
982 1280,
983 720,
984 PixelFormat::Rgb,
985 None,
986 include_bytes!(concat!(
987 env!("CARGO_MANIFEST_DIR"),
988 "/../../testdata/camera720p.rgb"
989 )),
990 )?;
991
992 let mut dst = TensorDyn::image(
994 1280,
995 720,
996 PixelFormat::Rgb,
997 DType::U8,
998 Some(TensorMemory::Dma),
999 )?;
1000 let mut g2d = G2DProcessor::new()?;
1001 let src_dyn = src;
1002 let mut dst_dyn = dst;
1003 g2d.convert(
1004 &src_dyn,
1005 &mut dst_dyn,
1006 Rotation::None,
1007 Flip::None,
1008 Crop::no_crop(),
1009 )?;
1010 dst = {
1011 let mut __t = dst_dyn.into_u8().unwrap();
1012 __t.set_format(PixelFormat::Rgb)
1013 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1014 TensorDyn::from(__t)
1015 };
1016
1017 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1019 cpu_dst
1020 .as_u8()
1021 .unwrap()
1022 .map()?
1023 .as_mut_slice()
1024 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1025
1026 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
1027 }
1028
1029 #[test]
1031 #[cfg(target_os = "linux")]
1032 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
1033 if !is_dma_available() {
1034 return Ok(());
1035 }
1036 let src = load_raw_image(
1038 1280,
1039 720,
1040 PixelFormat::Yuyv,
1041 Some(TensorMemory::Dma),
1042 include_bytes!(concat!(
1043 env!("CARGO_MANIFEST_DIR"),
1044 "/../../testdata/camera720p.yuyv"
1045 )),
1046 )?;
1047
1048 let reference = load_raw_image(
1050 1280,
1051 720,
1052 PixelFormat::Rgba,
1053 None,
1054 include_bytes!(concat!(
1055 env!("CARGO_MANIFEST_DIR"),
1056 "/../../testdata/camera720p.rgba"
1057 )),
1058 )?;
1059
1060 let mut dst = TensorDyn::image(
1062 1280,
1063 720,
1064 PixelFormat::Rgba,
1065 DType::U8,
1066 Some(TensorMemory::Dma),
1067 )?;
1068 let mut g2d = G2DProcessor::new()?;
1069 let src_dyn = src;
1070 let mut dst_dyn = dst;
1071 g2d.convert(
1072 &src_dyn,
1073 &mut dst_dyn,
1074 Rotation::None,
1075 Flip::None,
1076 Crop::no_crop(),
1077 )?;
1078 dst = {
1079 let mut __t = dst_dyn.into_u8().unwrap();
1080 __t.set_format(PixelFormat::Rgba)
1081 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1082 TensorDyn::from(__t)
1083 };
1084
1085 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1087 cpu_dst
1088 .as_u8()
1089 .unwrap()
1090 .map()?
1091 .as_mut_slice()
1092 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1093
1094 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1095 }
1096
1097 #[test]
1099 #[cfg(target_os = "linux")]
1100 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1101 if !is_dma_available() {
1102 return Ok(());
1103 }
1104 let src = load_raw_image(
1106 1280,
1107 720,
1108 PixelFormat::Yuyv,
1109 Some(TensorMemory::Dma),
1110 include_bytes!(concat!(
1111 env!("CARGO_MANIFEST_DIR"),
1112 "/../../testdata/camera720p.yuyv"
1113 )),
1114 )?;
1115
1116 let reference = load_raw_image(
1118 1280,
1119 720,
1120 PixelFormat::Rgb,
1121 None,
1122 include_bytes!(concat!(
1123 env!("CARGO_MANIFEST_DIR"),
1124 "/../../testdata/camera720p.rgb"
1125 )),
1126 )?;
1127
1128 let mut dst = TensorDyn::image(
1130 1280,
1131 720,
1132 PixelFormat::Rgb,
1133 DType::U8,
1134 Some(TensorMemory::Dma),
1135 )?;
1136 let mut g2d = G2DProcessor::new()?;
1137 let src_dyn = src;
1138 let mut dst_dyn = dst;
1139 g2d.convert(
1140 &src_dyn,
1141 &mut dst_dyn,
1142 Rotation::None,
1143 Flip::None,
1144 Crop::no_crop(),
1145 )?;
1146 dst = {
1147 let mut __t = dst_dyn.into_u8().unwrap();
1148 __t.set_format(PixelFormat::Rgb)
1149 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1150 TensorDyn::from(__t)
1151 };
1152
1153 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1155 cpu_dst
1156 .as_u8()
1157 .unwrap()
1158 .map()?
1159 .as_mut_slice()
1160 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1161
1162 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1163 }
1164
1165 #[test]
1168 #[cfg(target_os = "linux")]
1169 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1170 fn test_g2d_bgra_no_resize() {
1171 for src_fmt in [
1172 PixelFormat::Rgba,
1173 PixelFormat::Yuyv,
1174 PixelFormat::Nv12,
1175 PixelFormat::Bgra,
1176 ] {
1177 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1178 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1179 });
1180 }
1181 }
1182
1183 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1184 let file = include_bytes!(concat!(
1185 env!("CARGO_MANIFEST_DIR"),
1186 "/../../testdata/zidane.jpg"
1187 ))
1188 .to_vec();
1189 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
1190
1191 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
1193 let mut cpu_converter = CPUProcessor::new();
1194
1195 if g2d_in_fmt == PixelFormat::Nv12 {
1196 let nv12_bytes = include_bytes!(concat!(
1197 env!("CARGO_MANIFEST_DIR"),
1198 "/../../testdata/zidane.nv12"
1199 ));
1200 src2.as_u8()
1201 .unwrap()
1202 .map()?
1203 .as_mut_slice()
1204 .copy_from_slice(nv12_bytes);
1205 } else {
1206 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1207 }
1208
1209 let mut g2d = G2DProcessor::new()?;
1210
1211 let mut bgra_dst = TensorDyn::image(
1213 1280,
1214 720,
1215 PixelFormat::Bgra,
1216 DType::U8,
1217 Some(TensorMemory::Dma),
1218 )?;
1219 let src2_dyn = src2;
1220 let mut bgra_dst_dyn = bgra_dst;
1221 g2d.convert(
1222 &src2_dyn,
1223 &mut bgra_dst_dyn,
1224 Rotation::None,
1225 Flip::None,
1226 Crop::no_crop(),
1227 )?;
1228 bgra_dst = {
1229 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1230 __t.set_format(PixelFormat::Bgra)
1231 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1232 TensorDyn::from(__t)
1233 };
1234
1235 let src2 = {
1237 let mut __t = src2_dyn.into_u8().unwrap();
1238 __t.set_format(g2d_in_fmt)
1239 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1240 TensorDyn::from(__t)
1241 };
1242
1243 let mut rgba_dst = TensorDyn::image(
1245 1280,
1246 720,
1247 PixelFormat::Rgba,
1248 DType::U8,
1249 Some(TensorMemory::Dma),
1250 )?;
1251 let src2_dyn2 = src2;
1252 let mut rgba_dst_dyn = rgba_dst;
1253 g2d.convert(
1254 &src2_dyn2,
1255 &mut rgba_dst_dyn,
1256 Rotation::None,
1257 Flip::None,
1258 Crop::no_crop(),
1259 )?;
1260 rgba_dst = {
1261 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1262 __t.set_format(PixelFormat::Rgba)
1263 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1264 TensorDyn::from(__t)
1265 };
1266
1267 let bgra_cpu = TensorDyn::image(1280, 720, PixelFormat::Bgra, DType::U8, None)?;
1269 bgra_cpu
1270 .as_u8()
1271 .unwrap()
1272 .map()?
1273 .as_mut_slice()
1274 .copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
1275
1276 let rgba_cpu = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1277 rgba_cpu
1278 .as_u8()
1279 .unwrap()
1280 .map()?
1281 .as_mut_slice()
1282 .copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
1283
1284 let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
1286 let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
1287 let bgra_buf = bgra_map.as_slice();
1288 let rgba_buf = rgba_map.as_slice();
1289
1290 assert_eq!(bgra_buf.len(), rgba_buf.len());
1291 for (i, (bc, rc)) in bgra_buf
1292 .chunks_exact(4)
1293 .zip(rgba_buf.chunks_exact(4))
1294 .enumerate()
1295 {
1296 assert_eq!(
1297 bc[0], rc[2],
1298 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1299 );
1300 assert_eq!(
1301 bc[1], rc[1],
1302 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1303 );
1304 assert_eq!(
1305 bc[2], rc[0],
1306 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1307 );
1308 assert_eq!(
1309 bc[3], rc[3],
1310 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1311 );
1312 }
1313 Ok(())
1314 }
1315
1316 fn surface_for(
1327 width: usize,
1328 height: usize,
1329 fmt: PixelFormat,
1330 offset: Option<usize>,
1331 row_stride: Option<usize>,
1332 ) -> Result<G2DSurface, crate::Error> {
1333 use edgefirst_tensor::TensorMemory;
1334 let mut t = Tensor::<u8>::image(width, height, fmt, Some(TensorMemory::Dma))?;
1335 if let Some(o) = offset {
1336 t.set_plane_offset(o);
1337 }
1338 if let Some(s) = row_stride {
1339 t.set_row_stride_unchecked(s);
1340 }
1341 tensor_to_g2d_surface(&t)
1342 }
1343
1344 #[test]
1345 fn g2d_surface_single_plane_no_offset() {
1346 if !is_dma_available() {
1347 return;
1348 }
1349 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1350 assert_ne!(s.planes[0], 0);
1352 assert_eq!(s.stride, 640);
1353 }
1354
1355 #[test]
1356 fn g2d_surface_single_plane_with_offset() {
1357 if !is_dma_available() {
1358 return;
1359 }
1360 use edgefirst_tensor::TensorMemory;
1361 let mut t =
1362 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1363 let s0 = tensor_to_g2d_surface(&t).unwrap();
1364 t.set_plane_offset(4096);
1365 let s1 = tensor_to_g2d_surface(&t).unwrap();
1366 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1367 }
1368
1369 #[test]
1370 fn g2d_surface_single_plane_zero_offset() {
1371 if !is_dma_available() {
1372 return;
1373 }
1374 use edgefirst_tensor::TensorMemory;
1375 let mut t =
1376 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1377 let s_none = tensor_to_g2d_surface(&t).unwrap();
1378 t.set_plane_offset(0);
1379 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1380 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1382 }
1383
1384 #[test]
1385 fn g2d_surface_stride_rgba() {
1386 if !is_dma_available() {
1387 return;
1388 }
1389 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1391 assert_eq!(s_default.stride, 640);
1392
1393 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1395 assert_eq!(s_custom.stride, 704);
1396 }
1397
1398 #[test]
1399 fn g2d_surface_stride_rgb() {
1400 if !is_dma_available() {
1401 return;
1402 }
1403 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1404 assert_eq!(s_default.stride, 640);
1405
1406 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1408 assert_eq!(s_custom.stride, 660);
1409 }
1410
1411 #[test]
1412 fn g2d_surface_stride_grey() {
1413 if !is_dma_available() {
1414 return;
1415 }
1416 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1418 Ok(s) => s,
1419 Err(crate::Error::G2D(..)) => return,
1420 Err(e) => panic!("unexpected error: {e:?}"),
1421 };
1422 assert_eq!(s.stride, 1024);
1424 }
1425
1426 #[test]
1427 fn g2d_surface_contiguous_nv12_offset() {
1428 if !is_dma_available() {
1429 return;
1430 }
1431 use edgefirst_tensor::TensorMemory;
1432 let mut t =
1433 Tensor::<u8>::image(640, 480, PixelFormat::Nv12, Some(TensorMemory::Dma)).unwrap();
1434 let s0 = tensor_to_g2d_surface(&t).unwrap();
1435
1436 t.set_plane_offset(8192);
1437 let s1 = tensor_to_g2d_surface(&t).unwrap();
1438
1439 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1441 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1445 }
1446
1447 #[test]
1448 fn g2d_surface_contiguous_nv12_stride() {
1449 if !is_dma_available() {
1450 return;
1451 }
1452 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1454 assert_eq!(s.stride, 640);
1455
1456 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1458 assert_eq!(s_padded.stride, 1024);
1459 }
1460
1461 #[test]
1462 fn g2d_surface_multiplane_nv12_offset() {
1463 if !is_dma_available() {
1464 return;
1465 }
1466 use edgefirst_tensor::TensorMemory;
1467
1468 let mut luma =
1470 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Dma), Some("luma")).unwrap();
1471 let mut chroma =
1472 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Dma), Some("chroma")).unwrap();
1473
1474 let luma_base = {
1476 let dma = luma.as_dma().unwrap();
1477 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1478 phys.address()
1479 };
1480 let chroma_base = {
1481 let dma = chroma.as_dma().unwrap();
1482 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1483 phys.address()
1484 };
1485
1486 luma.set_plane_offset(4096);
1488 chroma.set_plane_offset(2048);
1489 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1490 let s = tensor_to_g2d_surface(&combined).unwrap();
1491
1492 assert_eq!(s.planes[0], luma_base + 4096);
1494 assert_eq!(s.planes[1], chroma_base + 2048);
1496 }
1497}