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 Err(Error::NotImplemented(
248 "G2D does not support drawing detection or segmentation overlays".to_string(),
249 ))
250 }
251
252 fn draw_proto_masks(
253 &mut self,
254 _dst: &mut TensorDyn,
255 _detect: &[crate::DetectBox],
256 _proto_data: &crate::ProtoData,
257 _overlay: crate::MaskOverlay<'_>,
258 ) -> Result<()> {
259 Err(Error::NotImplemented(
260 "G2D does not support drawing detection or segmentation overlays".to_string(),
261 ))
262 }
263
264 fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
265 Err(Error::NotImplemented(
266 "G2D does not support setting colors for rendering detection or segmentation overlays"
267 .to_string(),
268 ))
269 }
270}
271
272fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
276 let fmt = img.format().ok_or(Error::NotAnImage)?;
277 let dma = img
278 .as_dma()
279 .ok_or_else(|| Error::NotImplemented("g2d only supports Dma memory".to_string()))?;
280 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into()?;
281
282 let base_addr = phys.address();
289 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
290 let planes = if fmt == PixelFormat::Nv12 {
291 if img.is_multiplane() {
292 let chroma = img.chroma().unwrap();
294 let chroma_dma = chroma.as_dma().ok_or_else(|| {
295 Error::NotImplemented("g2d multiplane chroma must be DMA-backed".to_string())
296 })?;
297 let uv_phys: G2DPhysical = chroma_dma.fd.as_raw_fd().try_into()?;
298 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
299 [
300 base_addr + luma_offset,
301 uv_phys.address() + chroma_offset,
302 0,
303 ]
304 } else {
305 let w = img.width().unwrap();
306 let h = img.height().unwrap();
307 let stride = img.effective_row_stride().unwrap_or(w);
308 let uv_offset = (luma_offset as usize + stride * h) as u64;
309 [base_addr + luma_offset, base_addr + uv_offset, 0]
310 }
311 } else {
312 [base_addr + luma_offset, 0, 0]
313 };
314
315 let w = img.width().unwrap();
316 let h = img.height().unwrap();
317 let fourcc = pixelfmt_to_fourcc(fmt);
318
319 let stride_pixels = match img.effective_row_stride() {
322 Some(s) => {
323 let channels = fmt.channels();
324 if s % channels != 0 {
325 return Err(Error::NotImplemented(
326 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
327 ));
328 }
329 s / channels
330 }
331 None => w,
332 };
333
334 Ok(G2DSurface {
335 planes,
336 format: G2DFormat::try_from(fourcc)?.format(),
337 left: 0,
338 top: 0,
339 right: w as i32,
340 bottom: h as i32,
341 stride: stride_pixels as i32,
342 width: w as i32,
343 height: h as i32,
344 blendfunc: 0,
345 clrcolor: 0,
346 rot: 0,
347 global_alpha: 0,
348 })
349}
350
351#[cfg(feature = "g2d_test_formats")]
352#[cfg(test)]
353mod g2d_tests {
354 use super::*;
355 use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation};
356 use edgefirst_tensor::{
357 is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
358 };
359 use image::buffer::ConvertBuffer;
360
361 #[test]
362 #[cfg(target_os = "linux")]
363 fn test_g2d_formats_no_resize() {
364 for i in [
365 PixelFormat::Rgba,
366 PixelFormat::Yuyv,
367 PixelFormat::Rgb,
368 PixelFormat::Grey,
369 PixelFormat::Nv12,
370 ] {
371 for o in [
372 PixelFormat::Rgba,
373 PixelFormat::Yuyv,
374 PixelFormat::Rgb,
375 PixelFormat::Grey,
376 ] {
377 let res = test_g2d_format_no_resize_(i, o);
378 if let Err(e) = res {
379 println!("{i} to {o} failed: {e:?}");
380 } else {
381 println!("{i} to {o} success");
382 }
383 }
384 }
385 }
386
387 fn test_g2d_format_no_resize_(
388 g2d_in_fmt: PixelFormat,
389 g2d_out_fmt: PixelFormat,
390 ) -> Result<(), crate::Error> {
391 let dst_width = 1280;
392 let dst_height = 720;
393 let file = include_bytes!(concat!(
394 env!("CARGO_MANIFEST_DIR"),
395 "/../../testdata/zidane.jpg"
396 ))
397 .to_vec();
398 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
399
400 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
402
403 let mut cpu_converter = CPUProcessor::new();
404
405 if g2d_in_fmt == PixelFormat::Nv12 {
407 let nv12_bytes = include_bytes!(concat!(
408 env!("CARGO_MANIFEST_DIR"),
409 "/../../testdata/zidane.nv12"
410 ));
411 src2.as_u8()
412 .unwrap()
413 .map()?
414 .as_mut_slice()
415 .copy_from_slice(nv12_bytes);
416 } else {
417 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
418 }
419
420 let mut g2d_dst = TensorDyn::image(
421 dst_width,
422 dst_height,
423 g2d_out_fmt,
424 DType::U8,
425 Some(TensorMemory::Dma),
426 )?;
427 let mut g2d_converter = G2DProcessor::new()?;
428 let src2_dyn = src2;
429 let mut g2d_dst_dyn = g2d_dst;
430 g2d_converter.convert(
431 &src2_dyn,
432 &mut g2d_dst_dyn,
433 Rotation::None,
434 Flip::None,
435 Crop::no_crop(),
436 )?;
437 g2d_dst = {
438 let mut __t = g2d_dst_dyn.into_u8().unwrap();
439 __t.set_format(g2d_out_fmt)
440 .map_err(|e| crate::Error::Internal(e.to_string()))?;
441 TensorDyn::from(__t)
442 };
443
444 let mut cpu_dst =
445 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
446 cpu_converter.convert(
447 &g2d_dst,
448 &mut cpu_dst,
449 Rotation::None,
450 Flip::None,
451 Crop::no_crop(),
452 )?;
453
454 compare_images(
455 &src,
456 &cpu_dst,
457 0.98,
458 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
459 )
460 }
461
462 #[test]
463 #[cfg(target_os = "linux")]
464 fn test_g2d_formats_with_resize() {
465 for i in [
466 PixelFormat::Rgba,
467 PixelFormat::Yuyv,
468 PixelFormat::Rgb,
469 PixelFormat::Grey,
470 PixelFormat::Nv12,
471 ] {
472 for o in [
473 PixelFormat::Rgba,
474 PixelFormat::Yuyv,
475 PixelFormat::Rgb,
476 PixelFormat::Grey,
477 ] {
478 let res = test_g2d_format_with_resize_(i, o);
479 if let Err(e) = res {
480 println!("{i} to {o} failed: {e:?}");
481 } else {
482 println!("{i} to {o} success");
483 }
484 }
485 }
486 }
487
488 #[test]
489 #[cfg(target_os = "linux")]
490 fn test_g2d_formats_with_resize_dst_crop() {
491 for i in [
492 PixelFormat::Rgba,
493 PixelFormat::Yuyv,
494 PixelFormat::Rgb,
495 PixelFormat::Grey,
496 PixelFormat::Nv12,
497 ] {
498 for o in [
499 PixelFormat::Rgba,
500 PixelFormat::Yuyv,
501 PixelFormat::Rgb,
502 PixelFormat::Grey,
503 ] {
504 let res = test_g2d_format_with_resize_dst_crop(i, o);
505 if let Err(e) = res {
506 println!("{i} to {o} failed: {e:?}");
507 } else {
508 println!("{i} to {o} success");
509 }
510 }
511 }
512 }
513
514 fn test_g2d_format_with_resize_(
515 g2d_in_fmt: PixelFormat,
516 g2d_out_fmt: PixelFormat,
517 ) -> Result<(), crate::Error> {
518 let dst_width = 600;
519 let dst_height = 400;
520 let file = include_bytes!(concat!(
521 env!("CARGO_MANIFEST_DIR"),
522 "/../../testdata/zidane.jpg"
523 ))
524 .to_vec();
525 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
526
527 let mut cpu_converter = CPUProcessor::new();
528
529 let mut reference = TensorDyn::image(
530 dst_width,
531 dst_height,
532 PixelFormat::Rgb,
533 DType::U8,
534 Some(TensorMemory::Dma),
535 )?;
536 cpu_converter.convert(
537 &src,
538 &mut reference,
539 Rotation::None,
540 Flip::None,
541 Crop::no_crop(),
542 )?;
543
544 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
546
547 if g2d_in_fmt == PixelFormat::Nv12 {
549 let nv12_bytes = include_bytes!(concat!(
550 env!("CARGO_MANIFEST_DIR"),
551 "/../../testdata/zidane.nv12"
552 ));
553 src2.as_u8()
554 .unwrap()
555 .map()?
556 .as_mut_slice()
557 .copy_from_slice(nv12_bytes);
558 } else {
559 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
560 }
561
562 let mut g2d_dst = TensorDyn::image(
563 dst_width,
564 dst_height,
565 g2d_out_fmt,
566 DType::U8,
567 Some(TensorMemory::Dma),
568 )?;
569 let mut g2d_converter = G2DProcessor::new()?;
570 let src2_dyn = src2;
571 let mut g2d_dst_dyn = g2d_dst;
572 g2d_converter.convert(
573 &src2_dyn,
574 &mut g2d_dst_dyn,
575 Rotation::None,
576 Flip::None,
577 Crop::no_crop(),
578 )?;
579 g2d_dst = {
580 let mut __t = g2d_dst_dyn.into_u8().unwrap();
581 __t.set_format(g2d_out_fmt)
582 .map_err(|e| crate::Error::Internal(e.to_string()))?;
583 TensorDyn::from(__t)
584 };
585
586 let mut cpu_dst =
587 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
588 cpu_converter.convert(
589 &g2d_dst,
590 &mut cpu_dst,
591 Rotation::None,
592 Flip::None,
593 Crop::no_crop(),
594 )?;
595
596 compare_images(
597 &reference,
598 &cpu_dst,
599 0.98,
600 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
601 )
602 }
603
604 fn test_g2d_format_with_resize_dst_crop(
605 g2d_in_fmt: PixelFormat,
606 g2d_out_fmt: PixelFormat,
607 ) -> Result<(), crate::Error> {
608 let dst_width = 600;
609 let dst_height = 400;
610 let crop = Crop {
611 src_rect: None,
612 dst_rect: Some(Rect {
613 top: 100,
614 left: 100,
615 height: 100,
616 width: 200,
617 }),
618 dst_color: None,
619 };
620 let file = include_bytes!(concat!(
621 env!("CARGO_MANIFEST_DIR"),
622 "/../../testdata/zidane.jpg"
623 ))
624 .to_vec();
625 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
626
627 let mut cpu_converter = CPUProcessor::new();
628
629 let mut reference = TensorDyn::image(
630 dst_width,
631 dst_height,
632 PixelFormat::Rgb,
633 DType::U8,
634 Some(TensorMemory::Dma),
635 )?;
636 reference
637 .as_u8()
638 .unwrap()
639 .map()
640 .unwrap()
641 .as_mut_slice()
642 .fill(128);
643 cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
644
645 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
647
648 if g2d_in_fmt == PixelFormat::Nv12 {
650 let nv12_bytes = include_bytes!(concat!(
651 env!("CARGO_MANIFEST_DIR"),
652 "/../../testdata/zidane.nv12"
653 ));
654 src2.as_u8()
655 .unwrap()
656 .map()?
657 .as_mut_slice()
658 .copy_from_slice(nv12_bytes);
659 } else {
660 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
661 }
662
663 let mut g2d_dst = TensorDyn::image(
664 dst_width,
665 dst_height,
666 g2d_out_fmt,
667 DType::U8,
668 Some(TensorMemory::Dma),
669 )?;
670 g2d_dst
671 .as_u8()
672 .unwrap()
673 .map()
674 .unwrap()
675 .as_mut_slice()
676 .fill(128);
677 let mut g2d_converter = G2DProcessor::new()?;
678 let src2_dyn = src2;
679 let mut g2d_dst_dyn = g2d_dst;
680 g2d_converter.convert(
681 &src2_dyn,
682 &mut g2d_dst_dyn,
683 Rotation::None,
684 Flip::None,
685 crop,
686 )?;
687 g2d_dst = {
688 let mut __t = g2d_dst_dyn.into_u8().unwrap();
689 __t.set_format(g2d_out_fmt)
690 .map_err(|e| crate::Error::Internal(e.to_string()))?;
691 TensorDyn::from(__t)
692 };
693
694 let mut cpu_dst =
695 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
696 cpu_converter.convert(
697 &g2d_dst,
698 &mut cpu_dst,
699 Rotation::None,
700 Flip::None,
701 Crop::no_crop(),
702 )?;
703
704 compare_images(
705 &reference,
706 &cpu_dst,
707 0.98,
708 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
709 )
710 }
711
712 fn compare_images(
713 img1: &TensorDyn,
714 img2: &TensorDyn,
715 threshold: f64,
716 name: &str,
717 ) -> Result<(), crate::Error> {
718 assert_eq!(img1.height(), img2.height(), "Heights differ");
719 assert_eq!(img1.width(), img2.width(), "Widths differ");
720 assert_eq!(
721 img1.format().unwrap(),
722 img2.format().unwrap(),
723 "PixelFormat differ"
724 );
725 assert!(
726 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
727 "format must be Rgb or Rgba for comparison"
728 );
729 let image1 = match img1.format().unwrap() {
730 PixelFormat::Rgb => image::RgbImage::from_vec(
731 img1.width().unwrap() as u32,
732 img1.height().unwrap() as u32,
733 img1.as_u8().unwrap().map().unwrap().to_vec(),
734 )
735 .unwrap(),
736 PixelFormat::Rgba => image::RgbaImage::from_vec(
737 img1.width().unwrap() as u32,
738 img1.height().unwrap() as u32,
739 img1.as_u8().unwrap().map().unwrap().to_vec(),
740 )
741 .unwrap()
742 .convert(),
743
744 _ => unreachable!(),
745 };
746
747 let image2 = match img2.format().unwrap() {
748 PixelFormat::Rgb => image::RgbImage::from_vec(
749 img2.width().unwrap() as u32,
750 img2.height().unwrap() as u32,
751 img2.as_u8().unwrap().map().unwrap().to_vec(),
752 )
753 .unwrap(),
754 PixelFormat::Rgba => image::RgbaImage::from_vec(
755 img2.width().unwrap() as u32,
756 img2.height().unwrap() as u32,
757 img2.as_u8().unwrap().map().unwrap().to_vec(),
758 )
759 .unwrap()
760 .convert(),
761
762 _ => unreachable!(),
763 };
764
765 let similarity = image_compare::rgb_similarity_structure(
766 &image_compare::Algorithm::RootMeanSquared,
767 &image1,
768 &image2,
769 )
770 .expect("Image Comparison failed");
771
772 if similarity.score < threshold {
773 image1.save(format!("{name}_1.png")).unwrap();
774 image2.save(format!("{name}_2.png")).unwrap();
775 return Err(Error::Internal(format!(
776 "{name}: converted image and target image have similarity score too low: {} < {}",
777 similarity.score, threshold
778 )));
779 }
780 Ok(())
781 }
782
783 fn load_raw_image(
789 width: usize,
790 height: usize,
791 format: PixelFormat,
792 memory: Option<TensorMemory>,
793 bytes: &[u8],
794 ) -> Result<TensorDyn, crate::Error> {
795 let img = TensorDyn::image(width, height, format, DType::U8, memory)?;
796 let mut map = img.as_u8().unwrap().map()?;
797 map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
798 Ok(img)
799 }
800
801 #[test]
803 #[cfg(target_os = "linux")]
804 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
805 if !is_dma_available() {
806 return Ok(());
807 }
808 let src = load_raw_image(
810 1280,
811 720,
812 PixelFormat::Nv12,
813 Some(TensorMemory::Dma),
814 include_bytes!(concat!(
815 env!("CARGO_MANIFEST_DIR"),
816 "/../../testdata/camera720p.nv12"
817 )),
818 )?;
819
820 let reference = load_raw_image(
822 1280,
823 720,
824 PixelFormat::Rgba,
825 None,
826 include_bytes!(concat!(
827 env!("CARGO_MANIFEST_DIR"),
828 "/../../testdata/camera720p.rgba"
829 )),
830 )?;
831
832 let mut dst = TensorDyn::image(
834 1280,
835 720,
836 PixelFormat::Rgba,
837 DType::U8,
838 Some(TensorMemory::Dma),
839 )?;
840 let mut g2d = G2DProcessor::new()?;
841 let src_dyn = src;
842 let mut dst_dyn = dst;
843 g2d.convert(
844 &src_dyn,
845 &mut dst_dyn,
846 Rotation::None,
847 Flip::None,
848 Crop::no_crop(),
849 )?;
850 dst = {
851 let mut __t = dst_dyn.into_u8().unwrap();
852 __t.set_format(PixelFormat::Rgba)
853 .map_err(|e| crate::Error::Internal(e.to_string()))?;
854 TensorDyn::from(__t)
855 };
856
857 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
859 cpu_dst
860 .as_u8()
861 .unwrap()
862 .map()?
863 .as_mut_slice()
864 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
865
866 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
867 }
868
869 #[test]
871 #[cfg(target_os = "linux")]
872 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
873 if !is_dma_available() {
874 return Ok(());
875 }
876 let src = load_raw_image(
878 1280,
879 720,
880 PixelFormat::Nv12,
881 Some(TensorMemory::Dma),
882 include_bytes!(concat!(
883 env!("CARGO_MANIFEST_DIR"),
884 "/../../testdata/camera720p.nv12"
885 )),
886 )?;
887
888 let reference = load_raw_image(
890 1280,
891 720,
892 PixelFormat::Rgb,
893 None,
894 include_bytes!(concat!(
895 env!("CARGO_MANIFEST_DIR"),
896 "/../../testdata/camera720p.rgb"
897 )),
898 )?;
899
900 let mut dst = TensorDyn::image(
902 1280,
903 720,
904 PixelFormat::Rgb,
905 DType::U8,
906 Some(TensorMemory::Dma),
907 )?;
908 let mut g2d = G2DProcessor::new()?;
909 let src_dyn = src;
910 let mut dst_dyn = dst;
911 g2d.convert(
912 &src_dyn,
913 &mut dst_dyn,
914 Rotation::None,
915 Flip::None,
916 Crop::no_crop(),
917 )?;
918 dst = {
919 let mut __t = dst_dyn.into_u8().unwrap();
920 __t.set_format(PixelFormat::Rgb)
921 .map_err(|e| crate::Error::Internal(e.to_string()))?;
922 TensorDyn::from(__t)
923 };
924
925 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
927 cpu_dst
928 .as_u8()
929 .unwrap()
930 .map()?
931 .as_mut_slice()
932 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
933
934 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
935 }
936
937 #[test]
939 #[cfg(target_os = "linux")]
940 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
941 if !is_dma_available() {
942 return Ok(());
943 }
944 let src = load_raw_image(
946 1280,
947 720,
948 PixelFormat::Yuyv,
949 Some(TensorMemory::Dma),
950 include_bytes!(concat!(
951 env!("CARGO_MANIFEST_DIR"),
952 "/../../testdata/camera720p.yuyv"
953 )),
954 )?;
955
956 let reference = load_raw_image(
958 1280,
959 720,
960 PixelFormat::Rgba,
961 None,
962 include_bytes!(concat!(
963 env!("CARGO_MANIFEST_DIR"),
964 "/../../testdata/camera720p.rgba"
965 )),
966 )?;
967
968 let mut dst = TensorDyn::image(
970 1280,
971 720,
972 PixelFormat::Rgba,
973 DType::U8,
974 Some(TensorMemory::Dma),
975 )?;
976 let mut g2d = G2DProcessor::new()?;
977 let src_dyn = src;
978 let mut dst_dyn = dst;
979 g2d.convert(
980 &src_dyn,
981 &mut dst_dyn,
982 Rotation::None,
983 Flip::None,
984 Crop::no_crop(),
985 )?;
986 dst = {
987 let mut __t = dst_dyn.into_u8().unwrap();
988 __t.set_format(PixelFormat::Rgba)
989 .map_err(|e| crate::Error::Internal(e.to_string()))?;
990 TensorDyn::from(__t)
991 };
992
993 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
995 cpu_dst
996 .as_u8()
997 .unwrap()
998 .map()?
999 .as_mut_slice()
1000 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1001
1002 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1003 }
1004
1005 #[test]
1007 #[cfg(target_os = "linux")]
1008 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1009 if !is_dma_available() {
1010 return Ok(());
1011 }
1012 let src = load_raw_image(
1014 1280,
1015 720,
1016 PixelFormat::Yuyv,
1017 Some(TensorMemory::Dma),
1018 include_bytes!(concat!(
1019 env!("CARGO_MANIFEST_DIR"),
1020 "/../../testdata/camera720p.yuyv"
1021 )),
1022 )?;
1023
1024 let reference = load_raw_image(
1026 1280,
1027 720,
1028 PixelFormat::Rgb,
1029 None,
1030 include_bytes!(concat!(
1031 env!("CARGO_MANIFEST_DIR"),
1032 "/../../testdata/camera720p.rgb"
1033 )),
1034 )?;
1035
1036 let mut dst = TensorDyn::image(
1038 1280,
1039 720,
1040 PixelFormat::Rgb,
1041 DType::U8,
1042 Some(TensorMemory::Dma),
1043 )?;
1044 let mut g2d = G2DProcessor::new()?;
1045 let src_dyn = src;
1046 let mut dst_dyn = dst;
1047 g2d.convert(
1048 &src_dyn,
1049 &mut dst_dyn,
1050 Rotation::None,
1051 Flip::None,
1052 Crop::no_crop(),
1053 )?;
1054 dst = {
1055 let mut __t = dst_dyn.into_u8().unwrap();
1056 __t.set_format(PixelFormat::Rgb)
1057 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1058 TensorDyn::from(__t)
1059 };
1060
1061 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1063 cpu_dst
1064 .as_u8()
1065 .unwrap()
1066 .map()?
1067 .as_mut_slice()
1068 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1069
1070 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1071 }
1072
1073 #[test]
1076 #[cfg(target_os = "linux")]
1077 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1078 fn test_g2d_bgra_no_resize() {
1079 for src_fmt in [
1080 PixelFormat::Rgba,
1081 PixelFormat::Yuyv,
1082 PixelFormat::Nv12,
1083 PixelFormat::Bgra,
1084 ] {
1085 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1086 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1087 });
1088 }
1089 }
1090
1091 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1092 let file = include_bytes!(concat!(
1093 env!("CARGO_MANIFEST_DIR"),
1094 "/../../testdata/zidane.jpg"
1095 ))
1096 .to_vec();
1097 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
1098
1099 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
1101 let mut cpu_converter = CPUProcessor::new();
1102
1103 if g2d_in_fmt == PixelFormat::Nv12 {
1104 let nv12_bytes = include_bytes!(concat!(
1105 env!("CARGO_MANIFEST_DIR"),
1106 "/../../testdata/zidane.nv12"
1107 ));
1108 src2.as_u8()
1109 .unwrap()
1110 .map()?
1111 .as_mut_slice()
1112 .copy_from_slice(nv12_bytes);
1113 } else {
1114 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1115 }
1116
1117 let mut g2d = G2DProcessor::new()?;
1118
1119 let mut bgra_dst = TensorDyn::image(
1121 1280,
1122 720,
1123 PixelFormat::Bgra,
1124 DType::U8,
1125 Some(TensorMemory::Dma),
1126 )?;
1127 let src2_dyn = src2;
1128 let mut bgra_dst_dyn = bgra_dst;
1129 g2d.convert(
1130 &src2_dyn,
1131 &mut bgra_dst_dyn,
1132 Rotation::None,
1133 Flip::None,
1134 Crop::no_crop(),
1135 )?;
1136 bgra_dst = {
1137 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1138 __t.set_format(PixelFormat::Bgra)
1139 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1140 TensorDyn::from(__t)
1141 };
1142
1143 let src2 = {
1145 let mut __t = src2_dyn.into_u8().unwrap();
1146 __t.set_format(g2d_in_fmt)
1147 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1148 TensorDyn::from(__t)
1149 };
1150
1151 let mut rgba_dst = TensorDyn::image(
1153 1280,
1154 720,
1155 PixelFormat::Rgba,
1156 DType::U8,
1157 Some(TensorMemory::Dma),
1158 )?;
1159 let src2_dyn2 = src2;
1160 let mut rgba_dst_dyn = rgba_dst;
1161 g2d.convert(
1162 &src2_dyn2,
1163 &mut rgba_dst_dyn,
1164 Rotation::None,
1165 Flip::None,
1166 Crop::no_crop(),
1167 )?;
1168 rgba_dst = {
1169 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1170 __t.set_format(PixelFormat::Rgba)
1171 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1172 TensorDyn::from(__t)
1173 };
1174
1175 let bgra_cpu = TensorDyn::image(1280, 720, PixelFormat::Bgra, DType::U8, None)?;
1177 bgra_cpu
1178 .as_u8()
1179 .unwrap()
1180 .map()?
1181 .as_mut_slice()
1182 .copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
1183
1184 let rgba_cpu = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1185 rgba_cpu
1186 .as_u8()
1187 .unwrap()
1188 .map()?
1189 .as_mut_slice()
1190 .copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
1191
1192 let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
1194 let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
1195 let bgra_buf = bgra_map.as_slice();
1196 let rgba_buf = rgba_map.as_slice();
1197
1198 assert_eq!(bgra_buf.len(), rgba_buf.len());
1199 for (i, (bc, rc)) in bgra_buf
1200 .chunks_exact(4)
1201 .zip(rgba_buf.chunks_exact(4))
1202 .enumerate()
1203 {
1204 assert_eq!(
1205 bc[0], rc[2],
1206 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1207 );
1208 assert_eq!(
1209 bc[1], rc[1],
1210 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1211 );
1212 assert_eq!(
1213 bc[2], rc[0],
1214 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1215 );
1216 assert_eq!(
1217 bc[3], rc[3],
1218 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1219 );
1220 }
1221 Ok(())
1222 }
1223
1224 fn surface_for(
1235 width: usize,
1236 height: usize,
1237 fmt: PixelFormat,
1238 offset: Option<usize>,
1239 row_stride: Option<usize>,
1240 ) -> Result<G2DSurface, crate::Error> {
1241 use edgefirst_tensor::TensorMemory;
1242 let mut t = Tensor::<u8>::image(width, height, fmt, Some(TensorMemory::Dma))?;
1243 if let Some(o) = offset {
1244 t.set_plane_offset(o);
1245 }
1246 if let Some(s) = row_stride {
1247 t.set_row_stride_unchecked(s);
1248 }
1249 tensor_to_g2d_surface(&t)
1250 }
1251
1252 #[test]
1253 fn g2d_surface_single_plane_no_offset() {
1254 if !is_dma_available() {
1255 return;
1256 }
1257 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1258 assert_ne!(s.planes[0], 0);
1260 assert_eq!(s.stride, 640);
1261 }
1262
1263 #[test]
1264 fn g2d_surface_single_plane_with_offset() {
1265 if !is_dma_available() {
1266 return;
1267 }
1268 use edgefirst_tensor::TensorMemory;
1269 let mut t =
1270 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1271 let s0 = tensor_to_g2d_surface(&t).unwrap();
1272 t.set_plane_offset(4096);
1273 let s1 = tensor_to_g2d_surface(&t).unwrap();
1274 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1275 }
1276
1277 #[test]
1278 fn g2d_surface_single_plane_zero_offset() {
1279 if !is_dma_available() {
1280 return;
1281 }
1282 use edgefirst_tensor::TensorMemory;
1283 let mut t =
1284 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1285 let s_none = tensor_to_g2d_surface(&t).unwrap();
1286 t.set_plane_offset(0);
1287 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1288 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1290 }
1291
1292 #[test]
1293 fn g2d_surface_stride_rgba() {
1294 if !is_dma_available() {
1295 return;
1296 }
1297 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1299 assert_eq!(s_default.stride, 640);
1300
1301 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1303 assert_eq!(s_custom.stride, 704);
1304 }
1305
1306 #[test]
1307 fn g2d_surface_stride_rgb() {
1308 if !is_dma_available() {
1309 return;
1310 }
1311 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1312 assert_eq!(s_default.stride, 640);
1313
1314 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1316 assert_eq!(s_custom.stride, 660);
1317 }
1318
1319 #[test]
1320 fn g2d_surface_stride_grey() {
1321 if !is_dma_available() {
1322 return;
1323 }
1324 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1326 Ok(s) => s,
1327 Err(crate::Error::G2D(..)) => return,
1328 Err(e) => panic!("unexpected error: {e:?}"),
1329 };
1330 assert_eq!(s.stride, 1024);
1332 }
1333
1334 #[test]
1335 fn g2d_surface_contiguous_nv12_offset() {
1336 if !is_dma_available() {
1337 return;
1338 }
1339 use edgefirst_tensor::TensorMemory;
1340 let mut t =
1341 Tensor::<u8>::image(640, 480, PixelFormat::Nv12, Some(TensorMemory::Dma)).unwrap();
1342 let s0 = tensor_to_g2d_surface(&t).unwrap();
1343
1344 t.set_plane_offset(8192);
1345 let s1 = tensor_to_g2d_surface(&t).unwrap();
1346
1347 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1349 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1353 }
1354
1355 #[test]
1356 fn g2d_surface_contiguous_nv12_stride() {
1357 if !is_dma_available() {
1358 return;
1359 }
1360 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1362 assert_eq!(s.stride, 640);
1363
1364 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1366 assert_eq!(s_padded.stride, 1024);
1367 }
1368
1369 #[test]
1370 fn g2d_surface_multiplane_nv12_offset() {
1371 if !is_dma_available() {
1372 return;
1373 }
1374 use edgefirst_tensor::TensorMemory;
1375
1376 let mut luma =
1378 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Dma), Some("luma")).unwrap();
1379 let mut chroma =
1380 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Dma), Some("chroma")).unwrap();
1381
1382 let luma_base = {
1384 let dma = luma.as_dma().unwrap();
1385 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1386 phys.address()
1387 };
1388 let chroma_base = {
1389 let dma = chroma.as_dma().unwrap();
1390 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1391 phys.address()
1392 };
1393
1394 luma.set_plane_offset(4096);
1396 chroma.set_plane_offset(2048);
1397 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1398 let s = tensor_to_g2d_surface(&combined).unwrap();
1399
1400 assert_eq!(s.planes[0], luma_base + 4096);
1402 assert_eq!(s.planes[1], chroma_base + 2048);
1404 }
1405}