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