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