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 uv_offset = (w * h) as u64;
301            [base_addr, base_addr + uv_offset, 0]
302        }
303    } else {
304        [base_addr, 0, 0]
305    };
306
307    let w = img.width().unwrap();
308    let h = img.height().unwrap();
309    let fourcc = pixelfmt_to_fourcc(fmt);
310
311    Ok(G2DSurface {
312        planes,
313        format: G2DFormat::try_from(fourcc)?.format(),
314        left: 0,
315        top: 0,
316        right: w as i32,
317        bottom: h as i32,
318        stride: w as i32,
319        width: w as i32,
320        height: h as i32,
321        blendfunc: 0,
322        clrcolor: 0,
323        rot: 0,
324        global_alpha: 0,
325    })
326}
327
328#[cfg(feature = "g2d_test_formats")]
329#[cfg(test)]
330mod g2d_tests {
331    use super::*;
332    use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation};
333    use edgefirst_tensor::{
334        is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
335    };
336    use image::buffer::ConvertBuffer;
337
338    #[test]
339    #[cfg(target_os = "linux")]
340    fn test_g2d_formats_no_resize() {
341        for i in [
342            PixelFormat::Rgba,
343            PixelFormat::Yuyv,
344            PixelFormat::Rgb,
345            PixelFormat::Grey,
346            PixelFormat::Nv12,
347        ] {
348            for o in [
349                PixelFormat::Rgba,
350                PixelFormat::Yuyv,
351                PixelFormat::Rgb,
352                PixelFormat::Grey,
353            ] {
354                let res = test_g2d_format_no_resize_(i, o);
355                if let Err(e) = res {
356                    println!("{i} to {o} failed: {e:?}");
357                } else {
358                    println!("{i} to {o} success");
359                }
360            }
361        }
362    }
363
364    fn test_g2d_format_no_resize_(
365        g2d_in_fmt: PixelFormat,
366        g2d_out_fmt: PixelFormat,
367    ) -> Result<(), crate::Error> {
368        let dst_width = 1280;
369        let dst_height = 720;
370        let file = include_bytes!(concat!(
371            env!("CARGO_MANIFEST_DIR"),
372            "/../../testdata/zidane.jpg"
373        ))
374        .to_vec();
375        let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
376
377        // Create DMA buffer for G2D input
378        let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
379
380        let mut cpu_converter = CPUProcessor::new();
381
382        // For PixelFormat::Nv12 input, load from file since CPU doesn't support PixelFormat::Rgb→PixelFormat::Nv12
383        if g2d_in_fmt == PixelFormat::Nv12 {
384            let nv12_bytes = include_bytes!(concat!(
385                env!("CARGO_MANIFEST_DIR"),
386                "/../../testdata/zidane.nv12"
387            ));
388            src2.as_u8()
389                .unwrap()
390                .map()?
391                .as_mut_slice()
392                .copy_from_slice(nv12_bytes);
393        } else {
394            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
395        }
396
397        let mut g2d_dst = TensorDyn::image(
398            dst_width,
399            dst_height,
400            g2d_out_fmt,
401            DType::U8,
402            Some(TensorMemory::Dma),
403        )?;
404        let mut g2d_converter = G2DProcessor::new()?;
405        let src2_dyn = src2;
406        let mut g2d_dst_dyn = g2d_dst;
407        g2d_converter.convert(
408            &src2_dyn,
409            &mut g2d_dst_dyn,
410            Rotation::None,
411            Flip::None,
412            Crop::no_crop(),
413        )?;
414        g2d_dst = {
415            let mut __t = g2d_dst_dyn.into_u8().unwrap();
416            __t.set_format(g2d_out_fmt)
417                .map_err(|e| crate::Error::Internal(e.to_string()))?;
418            TensorDyn::from(__t)
419        };
420
421        let mut cpu_dst =
422            TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
423        cpu_converter.convert(
424            &g2d_dst,
425            &mut cpu_dst,
426            Rotation::None,
427            Flip::None,
428            Crop::no_crop(),
429        )?;
430
431        compare_images(
432            &src,
433            &cpu_dst,
434            0.98,
435            &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
436        )
437    }
438
439    #[test]
440    #[cfg(target_os = "linux")]
441    fn test_g2d_formats_with_resize() {
442        for i in [
443            PixelFormat::Rgba,
444            PixelFormat::Yuyv,
445            PixelFormat::Rgb,
446            PixelFormat::Grey,
447            PixelFormat::Nv12,
448        ] {
449            for o in [
450                PixelFormat::Rgba,
451                PixelFormat::Yuyv,
452                PixelFormat::Rgb,
453                PixelFormat::Grey,
454            ] {
455                let res = test_g2d_format_with_resize_(i, o);
456                if let Err(e) = res {
457                    println!("{i} to {o} failed: {e:?}");
458                } else {
459                    println!("{i} to {o} success");
460                }
461            }
462        }
463    }
464
465    #[test]
466    #[cfg(target_os = "linux")]
467    fn test_g2d_formats_with_resize_dst_crop() {
468        for i in [
469            PixelFormat::Rgba,
470            PixelFormat::Yuyv,
471            PixelFormat::Rgb,
472            PixelFormat::Grey,
473            PixelFormat::Nv12,
474        ] {
475            for o in [
476                PixelFormat::Rgba,
477                PixelFormat::Yuyv,
478                PixelFormat::Rgb,
479                PixelFormat::Grey,
480            ] {
481                let res = test_g2d_format_with_resize_dst_crop(i, o);
482                if let Err(e) = res {
483                    println!("{i} to {o} failed: {e:?}");
484                } else {
485                    println!("{i} to {o} success");
486                }
487            }
488        }
489    }
490
491    fn test_g2d_format_with_resize_(
492        g2d_in_fmt: PixelFormat,
493        g2d_out_fmt: PixelFormat,
494    ) -> Result<(), crate::Error> {
495        let dst_width = 600;
496        let dst_height = 400;
497        let file = include_bytes!(concat!(
498            env!("CARGO_MANIFEST_DIR"),
499            "/../../testdata/zidane.jpg"
500        ))
501        .to_vec();
502        let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
503
504        let mut cpu_converter = CPUProcessor::new();
505
506        let mut reference = TensorDyn::image(
507            dst_width,
508            dst_height,
509            PixelFormat::Rgb,
510            DType::U8,
511            Some(TensorMemory::Dma),
512        )?;
513        cpu_converter.convert(
514            &src,
515            &mut reference,
516            Rotation::None,
517            Flip::None,
518            Crop::no_crop(),
519        )?;
520
521        // Create DMA buffer for G2D input
522        let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
523
524        // For PixelFormat::Nv12 input, load from file since CPU doesn't support PixelFormat::Rgb→PixelFormat::Nv12
525        if g2d_in_fmt == PixelFormat::Nv12 {
526            let nv12_bytes = include_bytes!(concat!(
527                env!("CARGO_MANIFEST_DIR"),
528                "/../../testdata/zidane.nv12"
529            ));
530            src2.as_u8()
531                .unwrap()
532                .map()?
533                .as_mut_slice()
534                .copy_from_slice(nv12_bytes);
535        } else {
536            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
537        }
538
539        let mut g2d_dst = TensorDyn::image(
540            dst_width,
541            dst_height,
542            g2d_out_fmt,
543            DType::U8,
544            Some(TensorMemory::Dma),
545        )?;
546        let mut g2d_converter = G2DProcessor::new()?;
547        let src2_dyn = src2;
548        let mut g2d_dst_dyn = g2d_dst;
549        g2d_converter.convert(
550            &src2_dyn,
551            &mut g2d_dst_dyn,
552            Rotation::None,
553            Flip::None,
554            Crop::no_crop(),
555        )?;
556        g2d_dst = {
557            let mut __t = g2d_dst_dyn.into_u8().unwrap();
558            __t.set_format(g2d_out_fmt)
559                .map_err(|e| crate::Error::Internal(e.to_string()))?;
560            TensorDyn::from(__t)
561        };
562
563        let mut cpu_dst =
564            TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
565        cpu_converter.convert(
566            &g2d_dst,
567            &mut cpu_dst,
568            Rotation::None,
569            Flip::None,
570            Crop::no_crop(),
571        )?;
572
573        compare_images(
574            &reference,
575            &cpu_dst,
576            0.98,
577            &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
578        )
579    }
580
581    fn test_g2d_format_with_resize_dst_crop(
582        g2d_in_fmt: PixelFormat,
583        g2d_out_fmt: PixelFormat,
584    ) -> Result<(), crate::Error> {
585        let dst_width = 600;
586        let dst_height = 400;
587        let crop = Crop {
588            src_rect: None,
589            dst_rect: Some(Rect {
590                top: 100,
591                left: 100,
592                height: 100,
593                width: 200,
594            }),
595            dst_color: None,
596        };
597        let file = include_bytes!(concat!(
598            env!("CARGO_MANIFEST_DIR"),
599            "/../../testdata/zidane.jpg"
600        ))
601        .to_vec();
602        let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
603
604        let mut cpu_converter = CPUProcessor::new();
605
606        let mut reference = TensorDyn::image(
607            dst_width,
608            dst_height,
609            PixelFormat::Rgb,
610            DType::U8,
611            Some(TensorMemory::Dma),
612        )?;
613        reference
614            .as_u8()
615            .unwrap()
616            .map()
617            .unwrap()
618            .as_mut_slice()
619            .fill(128);
620        cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
621
622        // Create DMA buffer for G2D input
623        let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
624
625        // For PixelFormat::Nv12 input, load from file since CPU doesn't support PixelFormat::Rgb→PixelFormat::Nv12
626        if g2d_in_fmt == PixelFormat::Nv12 {
627            let nv12_bytes = include_bytes!(concat!(
628                env!("CARGO_MANIFEST_DIR"),
629                "/../../testdata/zidane.nv12"
630            ));
631            src2.as_u8()
632                .unwrap()
633                .map()?
634                .as_mut_slice()
635                .copy_from_slice(nv12_bytes);
636        } else {
637            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
638        }
639
640        let mut g2d_dst = TensorDyn::image(
641            dst_width,
642            dst_height,
643            g2d_out_fmt,
644            DType::U8,
645            Some(TensorMemory::Dma),
646        )?;
647        g2d_dst
648            .as_u8()
649            .unwrap()
650            .map()
651            .unwrap()
652            .as_mut_slice()
653            .fill(128);
654        let mut g2d_converter = G2DProcessor::new()?;
655        let src2_dyn = src2;
656        let mut g2d_dst_dyn = g2d_dst;
657        g2d_converter.convert(
658            &src2_dyn,
659            &mut g2d_dst_dyn,
660            Rotation::None,
661            Flip::None,
662            crop,
663        )?;
664        g2d_dst = {
665            let mut __t = g2d_dst_dyn.into_u8().unwrap();
666            __t.set_format(g2d_out_fmt)
667                .map_err(|e| crate::Error::Internal(e.to_string()))?;
668            TensorDyn::from(__t)
669        };
670
671        let mut cpu_dst =
672            TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
673        cpu_converter.convert(
674            &g2d_dst,
675            &mut cpu_dst,
676            Rotation::None,
677            Flip::None,
678            Crop::no_crop(),
679        )?;
680
681        compare_images(
682            &reference,
683            &cpu_dst,
684            0.98,
685            &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
686        )
687    }
688
689    fn compare_images(
690        img1: &TensorDyn,
691        img2: &TensorDyn,
692        threshold: f64,
693        name: &str,
694    ) -> Result<(), crate::Error> {
695        assert_eq!(img1.height(), img2.height(), "Heights differ");
696        assert_eq!(img1.width(), img2.width(), "Widths differ");
697        assert_eq!(
698            img1.format().unwrap(),
699            img2.format().unwrap(),
700            "PixelFormat differ"
701        );
702        assert!(
703            matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
704            "format must be Rgb or Rgba for comparison"
705        );
706        let image1 = match img1.format().unwrap() {
707            PixelFormat::Rgb => image::RgbImage::from_vec(
708                img1.width().unwrap() as u32,
709                img1.height().unwrap() as u32,
710                img1.as_u8().unwrap().map().unwrap().to_vec(),
711            )
712            .unwrap(),
713            PixelFormat::Rgba => image::RgbaImage::from_vec(
714                img1.width().unwrap() as u32,
715                img1.height().unwrap() as u32,
716                img1.as_u8().unwrap().map().unwrap().to_vec(),
717            )
718            .unwrap()
719            .convert(),
720
721            _ => unreachable!(),
722        };
723
724        let image2 = match img2.format().unwrap() {
725            PixelFormat::Rgb => image::RgbImage::from_vec(
726                img2.width().unwrap() as u32,
727                img2.height().unwrap() as u32,
728                img2.as_u8().unwrap().map().unwrap().to_vec(),
729            )
730            .unwrap(),
731            PixelFormat::Rgba => image::RgbaImage::from_vec(
732                img2.width().unwrap() as u32,
733                img2.height().unwrap() as u32,
734                img2.as_u8().unwrap().map().unwrap().to_vec(),
735            )
736            .unwrap()
737            .convert(),
738
739            _ => unreachable!(),
740        };
741
742        let similarity = image_compare::rgb_similarity_structure(
743            &image_compare::Algorithm::RootMeanSquared,
744            &image1,
745            &image2,
746        )
747        .expect("Image Comparison failed");
748
749        if similarity.score < threshold {
750            image1.save(format!("{name}_1.png")).unwrap();
751            image2.save(format!("{name}_2.png")).unwrap();
752            return Err(Error::Internal(format!(
753                "{name}: converted image and target image have similarity score too low: {} < {}",
754                similarity.score, threshold
755            )));
756        }
757        Ok(())
758    }
759
760    // =========================================================================
761    // PixelFormat::Nv12 Reference Validation Tests
762    // These tests compare G2D PixelFormat::Nv12 conversions against ffmpeg-generated references
763    // =========================================================================
764
765    fn load_raw_image(
766        width: usize,
767        height: usize,
768        format: PixelFormat,
769        memory: Option<TensorMemory>,
770        bytes: &[u8],
771    ) -> Result<TensorDyn, crate::Error> {
772        let img = TensorDyn::image(width, height, format, DType::U8, memory)?;
773        let mut map = img.as_u8().unwrap().map()?;
774        map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
775        Ok(img)
776    }
777
778    /// Test G2D PixelFormat::Nv12→PixelFormat::Rgba conversion against ffmpeg reference
779    #[test]
780    #[cfg(target_os = "linux")]
781    fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
782        if !is_dma_available() {
783            return Ok(());
784        }
785        // Load PixelFormat::Nv12 source
786        let src = load_raw_image(
787            1280,
788            720,
789            PixelFormat::Nv12,
790            Some(TensorMemory::Dma),
791            include_bytes!(concat!(
792                env!("CARGO_MANIFEST_DIR"),
793                "/../../testdata/camera720p.nv12"
794            )),
795        )?;
796
797        // Load PixelFormat::Rgba reference (ffmpeg-generated)
798        let reference = load_raw_image(
799            1280,
800            720,
801            PixelFormat::Rgba,
802            None,
803            include_bytes!(concat!(
804                env!("CARGO_MANIFEST_DIR"),
805                "/../../testdata/camera720p.rgba"
806            )),
807        )?;
808
809        // Convert using G2D
810        let mut dst = TensorDyn::image(
811            1280,
812            720,
813            PixelFormat::Rgba,
814            DType::U8,
815            Some(TensorMemory::Dma),
816        )?;
817        let mut g2d = G2DProcessor::new()?;
818        let src_dyn = src;
819        let mut dst_dyn = dst;
820        g2d.convert(
821            &src_dyn,
822            &mut dst_dyn,
823            Rotation::None,
824            Flip::None,
825            Crop::no_crop(),
826        )?;
827        dst = {
828            let mut __t = dst_dyn.into_u8().unwrap();
829            __t.set_format(PixelFormat::Rgba)
830                .map_err(|e| crate::Error::Internal(e.to_string()))?;
831            TensorDyn::from(__t)
832        };
833
834        // Copy to CPU for comparison
835        let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
836        cpu_dst
837            .as_u8()
838            .unwrap()
839            .map()?
840            .as_mut_slice()
841            .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
842
843        compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
844    }
845
846    /// Test G2D PixelFormat::Nv12→PixelFormat::Rgb conversion against ffmpeg reference
847    #[test]
848    #[cfg(target_os = "linux")]
849    fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
850        if !is_dma_available() {
851            return Ok(());
852        }
853        // Load PixelFormat::Nv12 source
854        let src = load_raw_image(
855            1280,
856            720,
857            PixelFormat::Nv12,
858            Some(TensorMemory::Dma),
859            include_bytes!(concat!(
860                env!("CARGO_MANIFEST_DIR"),
861                "/../../testdata/camera720p.nv12"
862            )),
863        )?;
864
865        // Load PixelFormat::Rgb reference (ffmpeg-generated)
866        let reference = load_raw_image(
867            1280,
868            720,
869            PixelFormat::Rgb,
870            None,
871            include_bytes!(concat!(
872                env!("CARGO_MANIFEST_DIR"),
873                "/../../testdata/camera720p.rgb"
874            )),
875        )?;
876
877        // Convert using G2D
878        let mut dst = TensorDyn::image(
879            1280,
880            720,
881            PixelFormat::Rgb,
882            DType::U8,
883            Some(TensorMemory::Dma),
884        )?;
885        let mut g2d = G2DProcessor::new()?;
886        let src_dyn = src;
887        let mut dst_dyn = dst;
888        g2d.convert(
889            &src_dyn,
890            &mut dst_dyn,
891            Rotation::None,
892            Flip::None,
893            Crop::no_crop(),
894        )?;
895        dst = {
896            let mut __t = dst_dyn.into_u8().unwrap();
897            __t.set_format(PixelFormat::Rgb)
898                .map_err(|e| crate::Error::Internal(e.to_string()))?;
899            TensorDyn::from(__t)
900        };
901
902        // Copy to CPU for comparison
903        let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
904        cpu_dst
905            .as_u8()
906            .unwrap()
907            .map()?
908            .as_mut_slice()
909            .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
910
911        compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
912    }
913
914    /// Test G2D PixelFormat::Yuyv→PixelFormat::Rgba conversion against ffmpeg reference
915    #[test]
916    #[cfg(target_os = "linux")]
917    fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
918        if !is_dma_available() {
919            return Ok(());
920        }
921        // Load PixelFormat::Yuyv source
922        let src = load_raw_image(
923            1280,
924            720,
925            PixelFormat::Yuyv,
926            Some(TensorMemory::Dma),
927            include_bytes!(concat!(
928                env!("CARGO_MANIFEST_DIR"),
929                "/../../testdata/camera720p.yuyv"
930            )),
931        )?;
932
933        // Load PixelFormat::Rgba reference (ffmpeg-generated)
934        let reference = load_raw_image(
935            1280,
936            720,
937            PixelFormat::Rgba,
938            None,
939            include_bytes!(concat!(
940                env!("CARGO_MANIFEST_DIR"),
941                "/../../testdata/camera720p.rgba"
942            )),
943        )?;
944
945        // Convert using G2D
946        let mut dst = TensorDyn::image(
947            1280,
948            720,
949            PixelFormat::Rgba,
950            DType::U8,
951            Some(TensorMemory::Dma),
952        )?;
953        let mut g2d = G2DProcessor::new()?;
954        let src_dyn = src;
955        let mut dst_dyn = dst;
956        g2d.convert(
957            &src_dyn,
958            &mut dst_dyn,
959            Rotation::None,
960            Flip::None,
961            Crop::no_crop(),
962        )?;
963        dst = {
964            let mut __t = dst_dyn.into_u8().unwrap();
965            __t.set_format(PixelFormat::Rgba)
966                .map_err(|e| crate::Error::Internal(e.to_string()))?;
967            TensorDyn::from(__t)
968        };
969
970        // Copy to CPU for comparison
971        let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
972        cpu_dst
973            .as_u8()
974            .unwrap()
975            .map()?
976            .as_mut_slice()
977            .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
978
979        compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
980    }
981
982    /// Test G2D PixelFormat::Yuyv→PixelFormat::Rgb conversion against ffmpeg reference
983    #[test]
984    #[cfg(target_os = "linux")]
985    fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
986        if !is_dma_available() {
987            return Ok(());
988        }
989        // Load PixelFormat::Yuyv source
990        let src = load_raw_image(
991            1280,
992            720,
993            PixelFormat::Yuyv,
994            Some(TensorMemory::Dma),
995            include_bytes!(concat!(
996                env!("CARGO_MANIFEST_DIR"),
997                "/../../testdata/camera720p.yuyv"
998            )),
999        )?;
1000
1001        // Load PixelFormat::Rgb reference (ffmpeg-generated)
1002        let reference = load_raw_image(
1003            1280,
1004            720,
1005            PixelFormat::Rgb,
1006            None,
1007            include_bytes!(concat!(
1008                env!("CARGO_MANIFEST_DIR"),
1009                "/../../testdata/camera720p.rgb"
1010            )),
1011        )?;
1012
1013        // Convert using G2D
1014        let mut dst = TensorDyn::image(
1015            1280,
1016            720,
1017            PixelFormat::Rgb,
1018            DType::U8,
1019            Some(TensorMemory::Dma),
1020        )?;
1021        let mut g2d = G2DProcessor::new()?;
1022        let src_dyn = src;
1023        let mut dst_dyn = dst;
1024        g2d.convert(
1025            &src_dyn,
1026            &mut dst_dyn,
1027            Rotation::None,
1028            Flip::None,
1029            Crop::no_crop(),
1030        )?;
1031        dst = {
1032            let mut __t = dst_dyn.into_u8().unwrap();
1033            __t.set_format(PixelFormat::Rgb)
1034                .map_err(|e| crate::Error::Internal(e.to_string()))?;
1035            TensorDyn::from(__t)
1036        };
1037
1038        // Copy to CPU for comparison
1039        let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1040        cpu_dst
1041            .as_u8()
1042            .unwrap()
1043            .map()?
1044            .as_mut_slice()
1045            .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1046
1047        compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1048    }
1049
1050    /// Test G2D native PixelFormat::Bgra conversion for all supported source formats.
1051    /// Compares G2D src→PixelFormat::Bgra against G2D src→PixelFormat::Rgba by verifying R↔B swap.
1052    #[test]
1053    #[cfg(target_os = "linux")]
1054    fn test_g2d_bgra_no_resize() {
1055        for src_fmt in [
1056            PixelFormat::Rgba,
1057            PixelFormat::Yuyv,
1058            PixelFormat::Nv12,
1059            PixelFormat::Bgra,
1060        ] {
1061            test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1062                panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1063            });
1064        }
1065    }
1066
1067    fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1068        let file = include_bytes!(concat!(
1069            env!("CARGO_MANIFEST_DIR"),
1070            "/../../testdata/zidane.jpg"
1071        ))
1072        .to_vec();
1073        let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
1074
1075        // Create DMA buffer for G2D input
1076        let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
1077        let mut cpu_converter = CPUProcessor::new();
1078
1079        if g2d_in_fmt == PixelFormat::Nv12 {
1080            let nv12_bytes = include_bytes!(concat!(
1081                env!("CARGO_MANIFEST_DIR"),
1082                "/../../testdata/zidane.nv12"
1083            ));
1084            src2.as_u8()
1085                .unwrap()
1086                .map()?
1087                .as_mut_slice()
1088                .copy_from_slice(nv12_bytes);
1089        } else {
1090            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1091        }
1092
1093        let mut g2d = G2DProcessor::new()?;
1094
1095        // Convert to PixelFormat::Bgra via G2D
1096        let mut bgra_dst = TensorDyn::image(
1097            1280,
1098            720,
1099            PixelFormat::Bgra,
1100            DType::U8,
1101            Some(TensorMemory::Dma),
1102        )?;
1103        let src2_dyn = src2;
1104        let mut bgra_dst_dyn = bgra_dst;
1105        g2d.convert(
1106            &src2_dyn,
1107            &mut bgra_dst_dyn,
1108            Rotation::None,
1109            Flip::None,
1110            Crop::no_crop(),
1111        )?;
1112        bgra_dst = {
1113            let mut __t = bgra_dst_dyn.into_u8().unwrap();
1114            __t.set_format(PixelFormat::Bgra)
1115                .map_err(|e| crate::Error::Internal(e.to_string()))?;
1116            TensorDyn::from(__t)
1117        };
1118
1119        // Reconstruct src2 from dyn for PixelFormat::Rgba conversion
1120        let src2 = {
1121            let mut __t = src2_dyn.into_u8().unwrap();
1122            __t.set_format(g2d_in_fmt)
1123                .map_err(|e| crate::Error::Internal(e.to_string()))?;
1124            TensorDyn::from(__t)
1125        };
1126
1127        // Convert to PixelFormat::Rgba via G2D as reference
1128        let mut rgba_dst = TensorDyn::image(
1129            1280,
1130            720,
1131            PixelFormat::Rgba,
1132            DType::U8,
1133            Some(TensorMemory::Dma),
1134        )?;
1135        let src2_dyn2 = src2;
1136        let mut rgba_dst_dyn = rgba_dst;
1137        g2d.convert(
1138            &src2_dyn2,
1139            &mut rgba_dst_dyn,
1140            Rotation::None,
1141            Flip::None,
1142            Crop::no_crop(),
1143        )?;
1144        rgba_dst = {
1145            let mut __t = rgba_dst_dyn.into_u8().unwrap();
1146            __t.set_format(PixelFormat::Rgba)
1147                .map_err(|e| crate::Error::Internal(e.to_string()))?;
1148            TensorDyn::from(__t)
1149        };
1150
1151        // Copy both to CPU memory for comparison
1152        let bgra_cpu = TensorDyn::image(1280, 720, PixelFormat::Bgra, DType::U8, None)?;
1153        bgra_cpu
1154            .as_u8()
1155            .unwrap()
1156            .map()?
1157            .as_mut_slice()
1158            .copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
1159
1160        let rgba_cpu = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1161        rgba_cpu
1162            .as_u8()
1163            .unwrap()
1164            .map()?
1165            .as_mut_slice()
1166            .copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
1167
1168        // Verify PixelFormat::Bgra output has R↔B swapped vs PixelFormat::Rgba output
1169        let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
1170        let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
1171        let bgra_buf = bgra_map.as_slice();
1172        let rgba_buf = rgba_map.as_slice();
1173
1174        assert_eq!(bgra_buf.len(), rgba_buf.len());
1175        for (i, (bc, rc)) in bgra_buf
1176            .chunks_exact(4)
1177            .zip(rgba_buf.chunks_exact(4))
1178            .enumerate()
1179        {
1180            assert_eq!(
1181                bc[0], rc[2],
1182                "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1183            );
1184            assert_eq!(
1185                bc[1], rc[1],
1186                "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1187            );
1188            assert_eq!(
1189                bc[2], rc[0],
1190                "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1191            );
1192            assert_eq!(
1193                bc[3], rc[3],
1194                "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1195            );
1196        }
1197        Ok(())
1198    }
1199}