Skip to main content

edgefirst_image/
g2d.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4#![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
12/// Convert a PixelFormat to the G2D-compatible FourCharCode.
13fn 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        // Planar formats have no standard FourCC; use RGBA as fallback
25        _ => four_char_code!("RGBA"),
26    }
27}
28
29/// G2DConverter implements the ImageProcessor trait using the NXP G2D
30/// library for hardware-accelerated image processing on i.MX platforms.
31#[derive(Debug)]
32pub struct G2DProcessor {
33    g2d: G2D,
34}
35
36unsafe impl Send for G2DProcessor {}
37unsafe impl Sync for G2DProcessor {}
38
39impl G2DProcessor {
40    /// Creates a new G2DConverter instance.
41    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    /// Returns the G2D library version as defined by _G2D_VERSION in the shared
50    /// library.
51    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        // Validate supported format pairs
91        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            // VYUY: i.MX8MP G2D hardware rejects VYUY blits (only YUYV/UYVY
100            // among packed YUV 4:2:2). ImageProcessor falls through to CPU.
101            (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        // For i8 destinations, reinterpret as u8 for G2D (same byte layout).
120        // The XOR 0x80 post-pass is applied after the blit completes.
121        let dst = if is_int8_dst {
122            // SAFETY: Tensor<i8> and Tensor<u8> have identical memory layout.
123            // The T parameter only affects PhantomData<T> (zero-sized) in
124            // TensorStorage variants and the typed view from map(). The chroma
125            // field (Option<Box<Tensor<T>>>) is also layout-identical. This
126            // reinterpreted reference is used only for shape/fd access and the
127            // G2D blit (which operates on raw DMA bytes). It does not outlive
128            // dst_dyn and is never stored.
129            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        // Clear the destination with the letterbox color before blitting the
162        // image into the sub-region.
163        //
164        // g2d_clear does not support 3-byte-per-pixel formats (RGB888, BGR888).
165        // For those formats, fall back to CPU fill after the blit.
166        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.width as usize + crop_rect.left)
184                * dst_fmt.channels()) as u64;
185
186            dst_surface.right = crop_rect.width as i32;
187            dst_surface.bottom = crop_rect.height as i32;
188            dst_surface.width = crop_rect.width as i32;
189            dst_surface.height = crop_rect.height as i32;
190        }
191
192        log::trace!("G2D blit: {src_fmt}→{dst_fmt} int8={is_int8_dst}");
193        self.g2d.blit(&src_surface, &dst_surface)?;
194        self.g2d.finish()?;
195        log::trace!("G2D blit complete");
196
197        // CPU fallback for RGB888 (unsupported by g2d_clear)
198        if needs_clear && dst_fmt == Rgb {
199            if let (Some(dst_color), Some(dst_rect)) = (crop.dst_color, crop.dst_rect) {
200                let start = Instant::now();
201                CPUProcessor::fill_image_outside_crop_u8(dst, dst_color, dst_rect)?;
202                log::trace!("cpu fill takes {:?}", start.elapsed());
203            }
204        }
205
206        // Apply XOR 0x80 for int8 output (u8→i8 bias conversion).
207        // map() triggers DMA_BUF_SYNC_START (cache invalidation) so CPU reads
208        // the G2D-written data correctly. The map drop triggers DMA_BUF_SYNC_END
209        // (cache flush) so downstream DMA consumers see the XOR'd data.
210        if is_int8_dst {
211            let start = Instant::now();
212            let mut map = dst.map()?;
213            crate::cpu::apply_int8_xor_bias(map.as_mut_slice(), dst_fmt);
214            log::trace!("g2d int8 XOR 0x80 post-pass takes {:?}", start.elapsed());
215        }
216
217        Ok(())
218    }
219}
220
221impl ImageProcessorTrait for G2DProcessor {
222    fn convert(
223        &mut self,
224        src: &TensorDyn,
225        dst: &mut TensorDyn,
226        rotation: Rotation,
227        flip: Flip,
228        crop: Crop,
229    ) -> Result<()> {
230        self.convert_impl(src, dst, rotation, flip, crop)
231    }
232
233    fn draw_masks(
234        &mut self,
235        _dst: &mut TensorDyn,
236        _detect: &[crate::DetectBox],
237        _segmentation: &[crate::Segmentation],
238    ) -> Result<()> {
239        Err(Error::NotImplemented(
240            "G2D does not support drawing detection or segmentation overlays".to_string(),
241        ))
242    }
243
244    fn draw_masks_proto(
245        &mut self,
246        _dst: &mut TensorDyn,
247        _detect: &[crate::DetectBox],
248        _proto_data: &crate::ProtoData,
249    ) -> Result<()> {
250        Err(Error::NotImplemented(
251            "G2D does not support drawing detection or segmentation overlays".to_string(),
252        ))
253    }
254
255    fn decode_masks_atlas(
256        &mut self,
257        _detect: &[crate::DetectBox],
258        _proto_data: crate::ProtoData,
259        _output_width: usize,
260        _output_height: usize,
261    ) -> Result<(Vec<u8>, Vec<crate::MaskRegion>)> {
262        Err(Error::NotImplemented(
263            "G2D does not support decoding mask atlas".to_string(),
264        ))
265    }
266
267    fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
268        Err(Error::NotImplemented(
269            "G2D does not support setting colors for rendering detection or segmentation overlays"
270                .to_string(),
271        ))
272    }
273}
274
275/// Build a `G2DSurface` from a `Tensor<u8>` that carries pixel-format metadata.
276///
277/// The tensor must be backed by DMA memory and must have a pixel format set.
278fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
279    let fmt = img.format().ok_or(Error::NotAnImage)?;
280    let dma = img
281        .as_dma()
282        .ok_or_else(|| Error::NotImplemented("g2d only supports Dma memory".to_string()))?;
283    let phys: G2DPhysical = dma.fd.as_raw_fd().try_into()?;
284
285    // NV12 is a two-plane format: Y plane followed by interleaved UV plane.
286    // planes[0] = Y plane start, planes[1] = UV plane start (Y size = width * height)
287    let base_addr = phys.address();
288    let planes = if fmt == PixelFormat::Nv12 {
289        if img.is_multiplane() {
290            // Multiplane: UV in separate DMA-BUF, get its physical address
291            let chroma = img.chroma().unwrap();
292            let chroma_dma = chroma.as_dma().ok_or_else(|| {
293                Error::NotImplemented("g2d multiplane chroma must be DMA-backed".to_string())
294            })?;
295            let uv_phys: G2DPhysical = chroma_dma.fd.as_raw_fd().try_into()?;
296            [base_addr, uv_phys.address(), 0]
297        } else {
298            let w = img.width().unwrap();
299            let h = img.height().unwrap();
300            let stride = img.effective_row_stride().unwrap_or(w);
301            let offset = img.plane_offset().unwrap_or(0);
302            let uv_offset = (offset + stride * h) as u64;
303            [base_addr, base_addr + uv_offset, 0]
304        }
305    } else {
306        [base_addr, 0, 0]
307    };
308
309    let w = img.width().unwrap();
310    let h = img.height().unwrap();
311    let fourcc = pixelfmt_to_fourcc(fmt);
312
313    Ok(G2DSurface {
314        planes,
315        format: G2DFormat::try_from(fourcc)?.format(),
316        left: 0,
317        top: 0,
318        right: w as i32,
319        bottom: h as i32,
320        stride: w as i32,
321        width: w as i32,
322        height: h as i32,
323        blendfunc: 0,
324        clrcolor: 0,
325        rot: 0,
326        global_alpha: 0,
327    })
328}
329
330#[cfg(feature = "g2d_test_formats")]
331#[cfg(test)]
332mod g2d_tests {
333    use super::*;
334    use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation};
335    use edgefirst_tensor::{
336        is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
337    };
338    use image::buffer::ConvertBuffer;
339
340    #[test]
341    #[cfg(target_os = "linux")]
342    fn test_g2d_formats_no_resize() {
343        for i in [
344            PixelFormat::Rgba,
345            PixelFormat::Yuyv,
346            PixelFormat::Rgb,
347            PixelFormat::Grey,
348            PixelFormat::Nv12,
349        ] {
350            for o in [
351                PixelFormat::Rgba,
352                PixelFormat::Yuyv,
353                PixelFormat::Rgb,
354                PixelFormat::Grey,
355            ] {
356                let res = test_g2d_format_no_resize_(i, o);
357                if let Err(e) = res {
358                    println!("{i} to {o} failed: {e:?}");
359                } else {
360                    println!("{i} to {o} success");
361                }
362            }
363        }
364    }
365
366    fn test_g2d_format_no_resize_(
367        g2d_in_fmt: PixelFormat,
368        g2d_out_fmt: PixelFormat,
369    ) -> Result<(), crate::Error> {
370        let dst_width = 1280;
371        let dst_height = 720;
372        let file = include_bytes!(concat!(
373            env!("CARGO_MANIFEST_DIR"),
374            "/../../testdata/zidane.jpg"
375        ))
376        .to_vec();
377        let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
378
379        // Create DMA buffer for G2D input
380        let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
381
382        let mut cpu_converter = CPUProcessor::new();
383
384        // For PixelFormat::Nv12 input, load from file since CPU doesn't support PixelFormat::Rgb→PixelFormat::Nv12
385        if g2d_in_fmt == PixelFormat::Nv12 {
386            let nv12_bytes = include_bytes!(concat!(
387                env!("CARGO_MANIFEST_DIR"),
388                "/../../testdata/zidane.nv12"
389            ));
390            src2.as_u8()
391                .unwrap()
392                .map()?
393                .as_mut_slice()
394                .copy_from_slice(nv12_bytes);
395        } else {
396            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
397        }
398
399        let mut g2d_dst = TensorDyn::image(
400            dst_width,
401            dst_height,
402            g2d_out_fmt,
403            DType::U8,
404            Some(TensorMemory::Dma),
405        )?;
406        let mut g2d_converter = G2DProcessor::new()?;
407        let src2_dyn = src2;
408        let mut g2d_dst_dyn = g2d_dst;
409        g2d_converter.convert(
410            &src2_dyn,
411            &mut g2d_dst_dyn,
412            Rotation::None,
413            Flip::None,
414            Crop::no_crop(),
415        )?;
416        g2d_dst = {
417            let mut __t = g2d_dst_dyn.into_u8().unwrap();
418            __t.set_format(g2d_out_fmt)
419                .map_err(|e| crate::Error::Internal(e.to_string()))?;
420            TensorDyn::from(__t)
421        };
422
423        let mut cpu_dst =
424            TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
425        cpu_converter.convert(
426            &g2d_dst,
427            &mut cpu_dst,
428            Rotation::None,
429            Flip::None,
430            Crop::no_crop(),
431        )?;
432
433        compare_images(
434            &src,
435            &cpu_dst,
436            0.98,
437            &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
438        )
439    }
440
441    #[test]
442    #[cfg(target_os = "linux")]
443    fn test_g2d_formats_with_resize() {
444        for i in [
445            PixelFormat::Rgba,
446            PixelFormat::Yuyv,
447            PixelFormat::Rgb,
448            PixelFormat::Grey,
449            PixelFormat::Nv12,
450        ] {
451            for o in [
452                PixelFormat::Rgba,
453                PixelFormat::Yuyv,
454                PixelFormat::Rgb,
455                PixelFormat::Grey,
456            ] {
457                let res = test_g2d_format_with_resize_(i, o);
458                if let Err(e) = res {
459                    println!("{i} to {o} failed: {e:?}");
460                } else {
461                    println!("{i} to {o} success");
462                }
463            }
464        }
465    }
466
467    #[test]
468    #[cfg(target_os = "linux")]
469    fn test_g2d_formats_with_resize_dst_crop() {
470        for i in [
471            PixelFormat::Rgba,
472            PixelFormat::Yuyv,
473            PixelFormat::Rgb,
474            PixelFormat::Grey,
475            PixelFormat::Nv12,
476        ] {
477            for o in [
478                PixelFormat::Rgba,
479                PixelFormat::Yuyv,
480                PixelFormat::Rgb,
481                PixelFormat::Grey,
482            ] {
483                let res = test_g2d_format_with_resize_dst_crop(i, o);
484                if let Err(e) = res {
485                    println!("{i} to {o} failed: {e:?}");
486                } else {
487                    println!("{i} to {o} success");
488                }
489            }
490        }
491    }
492
493    fn test_g2d_format_with_resize_(
494        g2d_in_fmt: PixelFormat,
495        g2d_out_fmt: PixelFormat,
496    ) -> Result<(), crate::Error> {
497        let dst_width = 600;
498        let dst_height = 400;
499        let file = include_bytes!(concat!(
500            env!("CARGO_MANIFEST_DIR"),
501            "/../../testdata/zidane.jpg"
502        ))
503        .to_vec();
504        let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
505
506        let mut cpu_converter = CPUProcessor::new();
507
508        let mut reference = TensorDyn::image(
509            dst_width,
510            dst_height,
511            PixelFormat::Rgb,
512            DType::U8,
513            Some(TensorMemory::Dma),
514        )?;
515        cpu_converter.convert(
516            &src,
517            &mut reference,
518            Rotation::None,
519            Flip::None,
520            Crop::no_crop(),
521        )?;
522
523        // Create DMA buffer for G2D input
524        let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
525
526        // For PixelFormat::Nv12 input, load from file since CPU doesn't support PixelFormat::Rgb→PixelFormat::Nv12
527        if g2d_in_fmt == PixelFormat::Nv12 {
528            let nv12_bytes = include_bytes!(concat!(
529                env!("CARGO_MANIFEST_DIR"),
530                "/../../testdata/zidane.nv12"
531            ));
532            src2.as_u8()
533                .unwrap()
534                .map()?
535                .as_mut_slice()
536                .copy_from_slice(nv12_bytes);
537        } else {
538            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
539        }
540
541        let mut g2d_dst = TensorDyn::image(
542            dst_width,
543            dst_height,
544            g2d_out_fmt,
545            DType::U8,
546            Some(TensorMemory::Dma),
547        )?;
548        let mut g2d_converter = G2DProcessor::new()?;
549        let src2_dyn = src2;
550        let mut g2d_dst_dyn = g2d_dst;
551        g2d_converter.convert(
552            &src2_dyn,
553            &mut g2d_dst_dyn,
554            Rotation::None,
555            Flip::None,
556            Crop::no_crop(),
557        )?;
558        g2d_dst = {
559            let mut __t = g2d_dst_dyn.into_u8().unwrap();
560            __t.set_format(g2d_out_fmt)
561                .map_err(|e| crate::Error::Internal(e.to_string()))?;
562            TensorDyn::from(__t)
563        };
564
565        let mut cpu_dst =
566            TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
567        cpu_converter.convert(
568            &g2d_dst,
569            &mut cpu_dst,
570            Rotation::None,
571            Flip::None,
572            Crop::no_crop(),
573        )?;
574
575        compare_images(
576            &reference,
577            &cpu_dst,
578            0.98,
579            &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
580        )
581    }
582
583    fn test_g2d_format_with_resize_dst_crop(
584        g2d_in_fmt: PixelFormat,
585        g2d_out_fmt: PixelFormat,
586    ) -> Result<(), crate::Error> {
587        let dst_width = 600;
588        let dst_height = 400;
589        let crop = Crop {
590            src_rect: None,
591            dst_rect: Some(Rect {
592                top: 100,
593                left: 100,
594                height: 100,
595                width: 200,
596            }),
597            dst_color: None,
598        };
599        let file = include_bytes!(concat!(
600            env!("CARGO_MANIFEST_DIR"),
601            "/../../testdata/zidane.jpg"
602        ))
603        .to_vec();
604        let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
605
606        let mut cpu_converter = CPUProcessor::new();
607
608        let mut reference = TensorDyn::image(
609            dst_width,
610            dst_height,
611            PixelFormat::Rgb,
612            DType::U8,
613            Some(TensorMemory::Dma),
614        )?;
615        reference
616            .as_u8()
617            .unwrap()
618            .map()
619            .unwrap()
620            .as_mut_slice()
621            .fill(128);
622        cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
623
624        // Create DMA buffer for G2D input
625        let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
626
627        // For PixelFormat::Nv12 input, load from file since CPU doesn't support PixelFormat::Rgb→PixelFormat::Nv12
628        if g2d_in_fmt == PixelFormat::Nv12 {
629            let nv12_bytes = include_bytes!(concat!(
630                env!("CARGO_MANIFEST_DIR"),
631                "/../../testdata/zidane.nv12"
632            ));
633            src2.as_u8()
634                .unwrap()
635                .map()?
636                .as_mut_slice()
637                .copy_from_slice(nv12_bytes);
638        } else {
639            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
640        }
641
642        let mut g2d_dst = TensorDyn::image(
643            dst_width,
644            dst_height,
645            g2d_out_fmt,
646            DType::U8,
647            Some(TensorMemory::Dma),
648        )?;
649        g2d_dst
650            .as_u8()
651            .unwrap()
652            .map()
653            .unwrap()
654            .as_mut_slice()
655            .fill(128);
656        let mut g2d_converter = G2DProcessor::new()?;
657        let src2_dyn = src2;
658        let mut g2d_dst_dyn = g2d_dst;
659        g2d_converter.convert(
660            &src2_dyn,
661            &mut g2d_dst_dyn,
662            Rotation::None,
663            Flip::None,
664            crop,
665        )?;
666        g2d_dst = {
667            let mut __t = g2d_dst_dyn.into_u8().unwrap();
668            __t.set_format(g2d_out_fmt)
669                .map_err(|e| crate::Error::Internal(e.to_string()))?;
670            TensorDyn::from(__t)
671        };
672
673        let mut cpu_dst =
674            TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
675        cpu_converter.convert(
676            &g2d_dst,
677            &mut cpu_dst,
678            Rotation::None,
679            Flip::None,
680            Crop::no_crop(),
681        )?;
682
683        compare_images(
684            &reference,
685            &cpu_dst,
686            0.98,
687            &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
688        )
689    }
690
691    fn compare_images(
692        img1: &TensorDyn,
693        img2: &TensorDyn,
694        threshold: f64,
695        name: &str,
696    ) -> Result<(), crate::Error> {
697        assert_eq!(img1.height(), img2.height(), "Heights differ");
698        assert_eq!(img1.width(), img2.width(), "Widths differ");
699        assert_eq!(
700            img1.format().unwrap(),
701            img2.format().unwrap(),
702            "PixelFormat differ"
703        );
704        assert!(
705            matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
706            "format must be Rgb or Rgba for comparison"
707        );
708        let image1 = match img1.format().unwrap() {
709            PixelFormat::Rgb => image::RgbImage::from_vec(
710                img1.width().unwrap() as u32,
711                img1.height().unwrap() as u32,
712                img1.as_u8().unwrap().map().unwrap().to_vec(),
713            )
714            .unwrap(),
715            PixelFormat::Rgba => image::RgbaImage::from_vec(
716                img1.width().unwrap() as u32,
717                img1.height().unwrap() as u32,
718                img1.as_u8().unwrap().map().unwrap().to_vec(),
719            )
720            .unwrap()
721            .convert(),
722
723            _ => unreachable!(),
724        };
725
726        let image2 = match img2.format().unwrap() {
727            PixelFormat::Rgb => image::RgbImage::from_vec(
728                img2.width().unwrap() as u32,
729                img2.height().unwrap() as u32,
730                img2.as_u8().unwrap().map().unwrap().to_vec(),
731            )
732            .unwrap(),
733            PixelFormat::Rgba => image::RgbaImage::from_vec(
734                img2.width().unwrap() as u32,
735                img2.height().unwrap() as u32,
736                img2.as_u8().unwrap().map().unwrap().to_vec(),
737            )
738            .unwrap()
739            .convert(),
740
741            _ => unreachable!(),
742        };
743
744        let similarity = image_compare::rgb_similarity_structure(
745            &image_compare::Algorithm::RootMeanSquared,
746            &image1,
747            &image2,
748        )
749        .expect("Image Comparison failed");
750
751        if similarity.score < threshold {
752            image1.save(format!("{name}_1.png")).unwrap();
753            image2.save(format!("{name}_2.png")).unwrap();
754            return Err(Error::Internal(format!(
755                "{name}: converted image and target image have similarity score too low: {} < {}",
756                similarity.score, threshold
757            )));
758        }
759        Ok(())
760    }
761
762    // =========================================================================
763    // PixelFormat::Nv12 Reference Validation Tests
764    // These tests compare G2D PixelFormat::Nv12 conversions against ffmpeg-generated references
765    // =========================================================================
766
767    fn load_raw_image(
768        width: usize,
769        height: usize,
770        format: PixelFormat,
771        memory: Option<TensorMemory>,
772        bytes: &[u8],
773    ) -> Result<TensorDyn, crate::Error> {
774        let img = TensorDyn::image(width, height, format, DType::U8, memory)?;
775        let mut map = img.as_u8().unwrap().map()?;
776        map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
777        Ok(img)
778    }
779
780    /// Test G2D PixelFormat::Nv12→PixelFormat::Rgba conversion against ffmpeg reference
781    #[test]
782    #[cfg(target_os = "linux")]
783    fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
784        if !is_dma_available() {
785            return Ok(());
786        }
787        // Load PixelFormat::Nv12 source
788        let src = load_raw_image(
789            1280,
790            720,
791            PixelFormat::Nv12,
792            Some(TensorMemory::Dma),
793            include_bytes!(concat!(
794                env!("CARGO_MANIFEST_DIR"),
795                "/../../testdata/camera720p.nv12"
796            )),
797        )?;
798
799        // Load PixelFormat::Rgba reference (ffmpeg-generated)
800        let reference = load_raw_image(
801            1280,
802            720,
803            PixelFormat::Rgba,
804            None,
805            include_bytes!(concat!(
806                env!("CARGO_MANIFEST_DIR"),
807                "/../../testdata/camera720p.rgba"
808            )),
809        )?;
810
811        // Convert using G2D
812        let mut dst = TensorDyn::image(
813            1280,
814            720,
815            PixelFormat::Rgba,
816            DType::U8,
817            Some(TensorMemory::Dma),
818        )?;
819        let mut g2d = G2DProcessor::new()?;
820        let src_dyn = src;
821        let mut dst_dyn = dst;
822        g2d.convert(
823            &src_dyn,
824            &mut dst_dyn,
825            Rotation::None,
826            Flip::None,
827            Crop::no_crop(),
828        )?;
829        dst = {
830            let mut __t = dst_dyn.into_u8().unwrap();
831            __t.set_format(PixelFormat::Rgba)
832                .map_err(|e| crate::Error::Internal(e.to_string()))?;
833            TensorDyn::from(__t)
834        };
835
836        // Copy to CPU for comparison
837        let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
838        cpu_dst
839            .as_u8()
840            .unwrap()
841            .map()?
842            .as_mut_slice()
843            .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
844
845        compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
846    }
847
848    /// Test G2D PixelFormat::Nv12→PixelFormat::Rgb conversion against ffmpeg reference
849    #[test]
850    #[cfg(target_os = "linux")]
851    fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
852        if !is_dma_available() {
853            return Ok(());
854        }
855        // Load PixelFormat::Nv12 source
856        let src = load_raw_image(
857            1280,
858            720,
859            PixelFormat::Nv12,
860            Some(TensorMemory::Dma),
861            include_bytes!(concat!(
862                env!("CARGO_MANIFEST_DIR"),
863                "/../../testdata/camera720p.nv12"
864            )),
865        )?;
866
867        // Load PixelFormat::Rgb reference (ffmpeg-generated)
868        let reference = load_raw_image(
869            1280,
870            720,
871            PixelFormat::Rgb,
872            None,
873            include_bytes!(concat!(
874                env!("CARGO_MANIFEST_DIR"),
875                "/../../testdata/camera720p.rgb"
876            )),
877        )?;
878
879        // Convert using G2D
880        let mut dst = TensorDyn::image(
881            1280,
882            720,
883            PixelFormat::Rgb,
884            DType::U8,
885            Some(TensorMemory::Dma),
886        )?;
887        let mut g2d = G2DProcessor::new()?;
888        let src_dyn = src;
889        let mut dst_dyn = dst;
890        g2d.convert(
891            &src_dyn,
892            &mut dst_dyn,
893            Rotation::None,
894            Flip::None,
895            Crop::no_crop(),
896        )?;
897        dst = {
898            let mut __t = dst_dyn.into_u8().unwrap();
899            __t.set_format(PixelFormat::Rgb)
900                .map_err(|e| crate::Error::Internal(e.to_string()))?;
901            TensorDyn::from(__t)
902        };
903
904        // Copy to CPU for comparison
905        let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
906        cpu_dst
907            .as_u8()
908            .unwrap()
909            .map()?
910            .as_mut_slice()
911            .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
912
913        compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
914    }
915
916    /// Test G2D PixelFormat::Yuyv→PixelFormat::Rgba conversion against ffmpeg reference
917    #[test]
918    #[cfg(target_os = "linux")]
919    fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
920        if !is_dma_available() {
921            return Ok(());
922        }
923        // Load PixelFormat::Yuyv source
924        let src = load_raw_image(
925            1280,
926            720,
927            PixelFormat::Yuyv,
928            Some(TensorMemory::Dma),
929            include_bytes!(concat!(
930                env!("CARGO_MANIFEST_DIR"),
931                "/../../testdata/camera720p.yuyv"
932            )),
933        )?;
934
935        // Load PixelFormat::Rgba reference (ffmpeg-generated)
936        let reference = load_raw_image(
937            1280,
938            720,
939            PixelFormat::Rgba,
940            None,
941            include_bytes!(concat!(
942                env!("CARGO_MANIFEST_DIR"),
943                "/../../testdata/camera720p.rgba"
944            )),
945        )?;
946
947        // Convert using G2D
948        let mut dst = TensorDyn::image(
949            1280,
950            720,
951            PixelFormat::Rgba,
952            DType::U8,
953            Some(TensorMemory::Dma),
954        )?;
955        let mut g2d = G2DProcessor::new()?;
956        let src_dyn = src;
957        let mut dst_dyn = dst;
958        g2d.convert(
959            &src_dyn,
960            &mut dst_dyn,
961            Rotation::None,
962            Flip::None,
963            Crop::no_crop(),
964        )?;
965        dst = {
966            let mut __t = dst_dyn.into_u8().unwrap();
967            __t.set_format(PixelFormat::Rgba)
968                .map_err(|e| crate::Error::Internal(e.to_string()))?;
969            TensorDyn::from(__t)
970        };
971
972        // Copy to CPU for comparison
973        let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
974        cpu_dst
975            .as_u8()
976            .unwrap()
977            .map()?
978            .as_mut_slice()
979            .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
980
981        compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
982    }
983
984    /// Test G2D PixelFormat::Yuyv→PixelFormat::Rgb conversion against ffmpeg reference
985    #[test]
986    #[cfg(target_os = "linux")]
987    fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
988        if !is_dma_available() {
989            return Ok(());
990        }
991        // Load PixelFormat::Yuyv source
992        let src = load_raw_image(
993            1280,
994            720,
995            PixelFormat::Yuyv,
996            Some(TensorMemory::Dma),
997            include_bytes!(concat!(
998                env!("CARGO_MANIFEST_DIR"),
999                "/../../testdata/camera720p.yuyv"
1000            )),
1001        )?;
1002
1003        // Load PixelFormat::Rgb reference (ffmpeg-generated)
1004        let reference = load_raw_image(
1005            1280,
1006            720,
1007            PixelFormat::Rgb,
1008            None,
1009            include_bytes!(concat!(
1010                env!("CARGO_MANIFEST_DIR"),
1011                "/../../testdata/camera720p.rgb"
1012            )),
1013        )?;
1014
1015        // Convert using G2D
1016        let mut dst = TensorDyn::image(
1017            1280,
1018            720,
1019            PixelFormat::Rgb,
1020            DType::U8,
1021            Some(TensorMemory::Dma),
1022        )?;
1023        let mut g2d = G2DProcessor::new()?;
1024        let src_dyn = src;
1025        let mut dst_dyn = dst;
1026        g2d.convert(
1027            &src_dyn,
1028            &mut dst_dyn,
1029            Rotation::None,
1030            Flip::None,
1031            Crop::no_crop(),
1032        )?;
1033        dst = {
1034            let mut __t = dst_dyn.into_u8().unwrap();
1035            __t.set_format(PixelFormat::Rgb)
1036                .map_err(|e| crate::Error::Internal(e.to_string()))?;
1037            TensorDyn::from(__t)
1038        };
1039
1040        // Copy to CPU for comparison
1041        let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1042        cpu_dst
1043            .as_u8()
1044            .unwrap()
1045            .map()?
1046            .as_mut_slice()
1047            .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1048
1049        compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1050    }
1051
1052    /// Test G2D native PixelFormat::Bgra conversion for all supported source formats.
1053    /// Compares G2D src→PixelFormat::Bgra against G2D src→PixelFormat::Rgba by verifying R↔B swap.
1054    #[test]
1055    #[cfg(target_os = "linux")]
1056    #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1057    fn test_g2d_bgra_no_resize() {
1058        for src_fmt in [
1059            PixelFormat::Rgba,
1060            PixelFormat::Yuyv,
1061            PixelFormat::Nv12,
1062            PixelFormat::Bgra,
1063        ] {
1064            test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1065                panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1066            });
1067        }
1068    }
1069
1070    fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1071        let file = include_bytes!(concat!(
1072            env!("CARGO_MANIFEST_DIR"),
1073            "/../../testdata/zidane.jpg"
1074        ))
1075        .to_vec();
1076        let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
1077
1078        // Create DMA buffer for G2D input
1079        let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
1080        let mut cpu_converter = CPUProcessor::new();
1081
1082        if g2d_in_fmt == PixelFormat::Nv12 {
1083            let nv12_bytes = include_bytes!(concat!(
1084                env!("CARGO_MANIFEST_DIR"),
1085                "/../../testdata/zidane.nv12"
1086            ));
1087            src2.as_u8()
1088                .unwrap()
1089                .map()?
1090                .as_mut_slice()
1091                .copy_from_slice(nv12_bytes);
1092        } else {
1093            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1094        }
1095
1096        let mut g2d = G2DProcessor::new()?;
1097
1098        // Convert to PixelFormat::Bgra via G2D
1099        let mut bgra_dst = TensorDyn::image(
1100            1280,
1101            720,
1102            PixelFormat::Bgra,
1103            DType::U8,
1104            Some(TensorMemory::Dma),
1105        )?;
1106        let src2_dyn = src2;
1107        let mut bgra_dst_dyn = bgra_dst;
1108        g2d.convert(
1109            &src2_dyn,
1110            &mut bgra_dst_dyn,
1111            Rotation::None,
1112            Flip::None,
1113            Crop::no_crop(),
1114        )?;
1115        bgra_dst = {
1116            let mut __t = bgra_dst_dyn.into_u8().unwrap();
1117            __t.set_format(PixelFormat::Bgra)
1118                .map_err(|e| crate::Error::Internal(e.to_string()))?;
1119            TensorDyn::from(__t)
1120        };
1121
1122        // Reconstruct src2 from dyn for PixelFormat::Rgba conversion
1123        let src2 = {
1124            let mut __t = src2_dyn.into_u8().unwrap();
1125            __t.set_format(g2d_in_fmt)
1126                .map_err(|e| crate::Error::Internal(e.to_string()))?;
1127            TensorDyn::from(__t)
1128        };
1129
1130        // Convert to PixelFormat::Rgba via G2D as reference
1131        let mut rgba_dst = TensorDyn::image(
1132            1280,
1133            720,
1134            PixelFormat::Rgba,
1135            DType::U8,
1136            Some(TensorMemory::Dma),
1137        )?;
1138        let src2_dyn2 = src2;
1139        let mut rgba_dst_dyn = rgba_dst;
1140        g2d.convert(
1141            &src2_dyn2,
1142            &mut rgba_dst_dyn,
1143            Rotation::None,
1144            Flip::None,
1145            Crop::no_crop(),
1146        )?;
1147        rgba_dst = {
1148            let mut __t = rgba_dst_dyn.into_u8().unwrap();
1149            __t.set_format(PixelFormat::Rgba)
1150                .map_err(|e| crate::Error::Internal(e.to_string()))?;
1151            TensorDyn::from(__t)
1152        };
1153
1154        // Copy both to CPU memory for comparison
1155        let bgra_cpu = TensorDyn::image(1280, 720, PixelFormat::Bgra, DType::U8, None)?;
1156        bgra_cpu
1157            .as_u8()
1158            .unwrap()
1159            .map()?
1160            .as_mut_slice()
1161            .copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
1162
1163        let rgba_cpu = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1164        rgba_cpu
1165            .as_u8()
1166            .unwrap()
1167            .map()?
1168            .as_mut_slice()
1169            .copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
1170
1171        // Verify PixelFormat::Bgra output has R↔B swapped vs PixelFormat::Rgba output
1172        let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
1173        let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
1174        let bgra_buf = bgra_map.as_slice();
1175        let rgba_buf = rgba_map.as_slice();
1176
1177        assert_eq!(bgra_buf.len(), rgba_buf.len());
1178        for (i, (bc, rc)) in bgra_buf
1179            .chunks_exact(4)
1180            .zip(rgba_buf.chunks_exact(4))
1181            .enumerate()
1182        {
1183            assert_eq!(
1184                bc[0], rc[2],
1185                "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1186            );
1187            assert_eq!(
1188                bc[1], rc[1],
1189                "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1190            );
1191            assert_eq!(
1192                bc[2], rc[0],
1193                "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1194            );
1195            assert_eq!(
1196                bc[3], rc[3],
1197                "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1198            );
1199        }
1200        Ok(())
1201    }
1202}