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::{
7    CPUProcessor, Crop, Error, Flip, ImageProcessorTrait, Result, Rotation, TensorImage,
8    TensorImageRef, BGRA, NV12, RGB, RGBA, YUYV,
9};
10use edgefirst_tensor::Tensor;
11use g2d_sys::{G2DFormat, G2DPhysical, G2DSurface, G2D};
12use std::{os::fd::AsRawFd, time::Instant};
13
14/// G2DConverter implements the ImageProcessor trait using the NXP G2D
15/// library for hardware-accelerated image processing on i.MX platforms.
16#[derive(Debug)]
17pub struct G2DProcessor {
18    g2d: G2D,
19}
20
21unsafe impl Send for G2DProcessor {}
22unsafe impl Sync for G2DProcessor {}
23
24impl G2DProcessor {
25    /// Creates a new G2DConverter instance.
26    pub fn new() -> Result<Self> {
27        let mut g2d = G2D::new("libg2d.so.2")?;
28        g2d.set_bt709_colorspace()?;
29
30        log::debug!("G2DConverter created with version {:?}", g2d.version());
31        Ok(Self { g2d })
32    }
33
34    /// Returns the G2D library version as defined by _G2D_VERSION in the shared
35    /// library.
36    pub fn version(&self) -> g2d_sys::Version {
37        self.g2d.version()
38    }
39
40    fn convert_(
41        &mut self,
42        src: &TensorImage,
43        dst: &mut TensorImage,
44        rotation: Rotation,
45        flip: Flip,
46        crop: Crop,
47    ) -> Result<()> {
48        let mut src_surface: G2DSurface = src.try_into()?;
49        let mut dst_surface: G2DSurface = dst.try_into()?;
50
51        src_surface.rot = match flip {
52            Flip::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
53            Flip::Vertical => g2d_sys::g2d_rotation_G2D_FLIP_V,
54            Flip::Horizontal => g2d_sys::g2d_rotation_G2D_FLIP_H,
55        };
56
57        dst_surface.rot = match rotation {
58            Rotation::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
59            Rotation::Clockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_90,
60            Rotation::Rotate180 => g2d_sys::g2d_rotation_G2D_ROTATION_180,
61            Rotation::CounterClockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_270,
62        };
63
64        if let Some(crop_rect) = crop.src_rect {
65            src_surface.left = crop_rect.left as i32;
66            src_surface.top = crop_rect.top as i32;
67            src_surface.right = (crop_rect.left + crop_rect.width) as i32;
68            src_surface.bottom = (crop_rect.top + crop_rect.height) as i32;
69        }
70
71        // Clear the destination with the letterbox color before blitting the
72        // image into the sub-region.
73        //
74        // g2d_clear does not support 3-byte-per-pixel formats (RGB888, BGR888).
75        // For those formats, fall back to CPU fill after the blit.
76        let needs_clear = crop.dst_color.is_some()
77            && crop.dst_rect.is_some_and(|dst_rect| {
78                dst_rect.left != 0
79                    || dst_rect.top != 0
80                    || dst_rect.width != dst.width()
81                    || dst_rect.height != dst.height()
82            });
83
84        if needs_clear && dst.fourcc != RGB {
85            if let Some(dst_color) = crop.dst_color {
86                let start = Instant::now();
87                self.g2d.clear(&mut dst_surface, dst_color)?;
88                log::trace!("g2d clear takes {:?}", start.elapsed());
89            }
90        }
91
92        if let Some(crop_rect) = crop.dst_rect {
93            dst_surface.planes[0] += ((crop_rect.top * dst_surface.width as usize + crop_rect.left)
94                * dst.channels()) as u64;
95
96            dst_surface.right = crop_rect.width as i32;
97            dst_surface.bottom = crop_rect.height as i32;
98            dst_surface.width = crop_rect.width as i32;
99            dst_surface.height = crop_rect.height as i32;
100        }
101
102        log::trace!("Blitting from {src_surface:?} to {dst_surface:?}");
103        self.g2d.blit(&src_surface, &dst_surface)?;
104        self.g2d.finish()?;
105
106        // CPU fallback for RGB888 (unsupported by g2d_clear)
107        if needs_clear && dst.fourcc == RGB {
108            if let (Some(dst_color), Some(dst_rect)) = (crop.dst_color, crop.dst_rect) {
109                let start = Instant::now();
110                CPUProcessor::fill_image_outside_crop(dst, dst_color, dst_rect)?;
111                log::trace!("cpu fill takes {:?}", start.elapsed());
112            }
113        }
114
115        Ok(())
116    }
117}
118
119impl ImageProcessorTrait for G2DProcessor {
120    /// Converts the source image to the destination image using G2D.
121    ///
122    /// # Arguments
123    ///
124    /// * `dst` - The destination image to be converted to.
125    /// * `src` - The source image to convert from.
126    /// * `rotation` - The rotation to apply to the destination image (after
127    ///   crop if specified).
128    /// * `crop` - An optional rectangle specifying the area to crop from the
129    ///   source image.
130    ///
131    /// # Returns
132    ///
133    /// A `Result` indicating success or failure of the conversion.
134    fn convert(
135        &mut self,
136        src: &TensorImage,
137        dst: &mut TensorImage,
138        rotation: Rotation,
139        flip: Flip,
140        crop: Crop,
141    ) -> Result<()> {
142        crop.check_crop(src, dst)?;
143        match (src.fourcc(), dst.fourcc()) {
144            (RGBA, RGBA) => {}
145            (RGBA, YUYV) => {}
146            (RGBA, RGB) => {}
147            (YUYV, RGBA) => {}
148            (YUYV, YUYV) => {}
149            (YUYV, RGB) => {}
150            // VYUY: i.MX8MP G2D hardware rejects VYUY blits (only YUYV/UYVY
151            // among packed YUV 4:2:2). ImageProcessor falls through to CPU.
152            (NV12, RGBA) => {}
153            (NV12, YUYV) => {}
154            (NV12, RGB) => {}
155            (RGBA, BGRA) => {}
156            (YUYV, BGRA) => {}
157            (NV12, BGRA) => {}
158            (BGRA, BGRA) => {}
159            (s, d) => {
160                return Err(Error::NotSupported(format!(
161                    "G2D does not support {} to {} conversion",
162                    s.display(),
163                    d.display()
164                )));
165            }
166        }
167        self.convert_(src, dst, rotation, flip, crop)
168    }
169
170    fn convert_ref(
171        &mut self,
172        src: &TensorImage,
173        dst: &mut TensorImageRef<'_>,
174        rotation: Rotation,
175        flip: Flip,
176        crop: Crop,
177    ) -> Result<()> {
178        // G2D doesn't support PLANAR_RGB output, delegate to CPU
179        let mut cpu = CPUProcessor::new();
180        cpu.convert_ref(src, dst, rotation, flip, crop)
181    }
182
183    fn draw_masks(
184        &mut self,
185        _dst: &mut TensorImage,
186        _detect: &[crate::DetectBox],
187        _segmentation: &[crate::Segmentation],
188    ) -> Result<()> {
189        Err(Error::NotImplemented(
190            "G2D does not support drawing detection or segmentation overlays".to_string(),
191        ))
192    }
193
194    fn draw_masks_proto(
195        &mut self,
196        _dst: &mut TensorImage,
197        _detect: &[crate::DetectBox],
198        _proto_data: &crate::ProtoData,
199    ) -> Result<()> {
200        Err(Error::NotImplemented(
201            "G2D does not support drawing detection or segmentation overlays".to_string(),
202        ))
203    }
204
205    fn decode_masks_atlas(
206        &mut self,
207        _detect: &[crate::DetectBox],
208        _proto_data: crate::ProtoData,
209        _output_width: usize,
210        _output_height: usize,
211    ) -> Result<(Vec<u8>, Vec<crate::MaskRegion>)> {
212        Err(Error::NotImplemented(
213            "G2D does not support decoding mask atlas".to_string(),
214        ))
215    }
216
217    fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
218        Err(Error::NotImplemented(
219            "G2D does not support setting colors for rendering detection or segmentation overlays"
220                .to_string(),
221        ))
222    }
223}
224
225impl TryFrom<&TensorImage> for G2DSurface {
226    type Error = Error;
227
228    fn try_from(img: &TensorImage) -> Result<Self, Self::Error> {
229        let phys: G2DPhysical = match img.tensor() {
230            Tensor::Dma(t) => t.as_raw_fd(),
231            _ => {
232                return Err(Error::NotImplemented(
233                    "g2d only supports Dma memory".to_string(),
234                ));
235            }
236        }
237        .try_into()?;
238
239        // NV12 is a two-plane format: Y plane followed by interleaved UV plane
240        // planes[0] = Y plane start, planes[1] = UV plane start (Y size = width *
241        // height)
242        let base_addr = phys.address();
243        let planes = if img.fourcc() == NV12 {
244            let uv_offset = (img.width() * img.height()) as u64;
245            [base_addr, base_addr + uv_offset, 0]
246        } else {
247            [base_addr, 0, 0]
248        };
249
250        Ok(Self {
251            planes,
252            format: G2DFormat::try_from(img.fourcc())?.format(),
253            left: 0,
254            top: 0,
255            right: img.width() as i32,
256            bottom: img.height() as i32,
257            stride: img.width() as i32,
258            width: img.width() as i32,
259            height: img.height() as i32,
260            blendfunc: 0,
261            clrcolor: 0,
262            rot: 0,
263            global_alpha: 0,
264        })
265    }
266}
267
268impl TryFrom<&mut TensorImage> for G2DSurface {
269    type Error = Error;
270
271    fn try_from(img: &mut TensorImage) -> Result<Self, Self::Error> {
272        let phys: G2DPhysical = match img.tensor() {
273            Tensor::Dma(t) => t.as_raw_fd(),
274            _ => {
275                return Err(Error::NotImplemented(
276                    "g2d only supports Dma memory".to_string(),
277                ));
278            }
279        }
280        .try_into()?;
281
282        // NV12 is a two-plane format: Y plane followed by interleaved UV plane
283        let base_addr = phys.address();
284        let planes = if img.fourcc() == NV12 {
285            let uv_offset = (img.width() * img.height()) as u64;
286            [base_addr, base_addr + uv_offset, 0]
287        } else {
288            [base_addr, 0, 0]
289        };
290
291        Ok(Self {
292            planes,
293            format: G2DFormat::try_from(img.fourcc())?.format(),
294            left: 0,
295            top: 0,
296            right: img.width() as i32,
297            bottom: img.height() as i32,
298            stride: img.width() as i32,
299            width: img.width() as i32,
300            height: img.height() as i32,
301            blendfunc: 0,
302            clrcolor: 0,
303            rot: 0,
304            global_alpha: 0,
305        })
306    }
307}
308
309#[cfg(feature = "g2d_test_formats")]
310#[cfg(test)]
311mod g2d_tests {
312    use super::*;
313    use crate::{
314        CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation, TensorImage, BGRA,
315        GREY, NV12, RGB, RGBA, YUYV,
316    };
317    use edgefirst_tensor::{is_dma_available, TensorMapTrait, TensorMemory, TensorTrait};
318    use four_char_code::FourCharCode;
319    use image::buffer::ConvertBuffer;
320
321    #[test]
322    #[cfg(target_os = "linux")]
323    fn test_g2d_formats_no_resize() {
324        for i in [RGBA, YUYV, RGB, GREY, NV12] {
325            for o in [RGBA, YUYV, RGB, GREY] {
326                let res = test_g2d_format_no_resize_(i, o);
327                if let Err(e) = res {
328                    println!("{} to {} failed: {e:?}", i.display(), o.display());
329                } else {
330                    println!("{} to {} success", i.display(), o.display());
331                }
332            }
333        }
334    }
335
336    fn test_g2d_format_no_resize_(
337        g2d_in_fmt: FourCharCode,
338        g2d_out_fmt: FourCharCode,
339    ) -> Result<(), crate::Error> {
340        let dst_width = 1280;
341        let dst_height = 720;
342        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
343        let src = TensorImage::load_jpeg(&file, Some(RGB), None)?;
344
345        // Create DMA buffer for G2D input
346        let mut src2 = TensorImage::new(1280, 720, g2d_in_fmt, Some(TensorMemory::Dma))?;
347
348        let mut cpu_converter = CPUProcessor::new();
349
350        // For NV12 input, load from file since CPU doesn't support RGB→NV12
351        if g2d_in_fmt == NV12 {
352            let nv12_bytes = include_bytes!("../../../testdata/zidane.nv12");
353            src2.tensor()
354                .map()?
355                .as_mut_slice()
356                .copy_from_slice(nv12_bytes);
357        } else {
358            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
359        }
360
361        let mut g2d_dst =
362            TensorImage::new(dst_width, dst_height, g2d_out_fmt, Some(TensorMemory::Dma))?;
363        let mut g2d_converter = G2DProcessor::new()?;
364        g2d_converter.convert_(
365            &src2,
366            &mut g2d_dst,
367            Rotation::None,
368            Flip::None,
369            Crop::no_crop(),
370        )?;
371
372        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGB, None)?;
373        cpu_converter.convert(
374            &g2d_dst,
375            &mut cpu_dst,
376            Rotation::None,
377            Flip::None,
378            Crop::no_crop(),
379        )?;
380
381        compare_images(
382            &src,
383            &cpu_dst,
384            0.98,
385            &format!("{}_to_{}", g2d_in_fmt.display(), g2d_out_fmt.display()),
386        )
387    }
388
389    #[test]
390    #[cfg(target_os = "linux")]
391    fn test_g2d_formats_with_resize() {
392        for i in [RGBA, YUYV, RGB, GREY, NV12] {
393            for o in [RGBA, YUYV, RGB, GREY] {
394                let res = test_g2d_format_with_resize_(i, o);
395                if let Err(e) = res {
396                    println!("{} to {} failed: {e:?}", i.display(), o.display());
397                } else {
398                    println!("{} to {} success", i.display(), o.display());
399                }
400            }
401        }
402    }
403
404    #[test]
405    #[cfg(target_os = "linux")]
406    fn test_g2d_formats_with_resize_dst_crop() {
407        for i in [RGBA, YUYV, RGB, GREY, NV12] {
408            for o in [RGBA, YUYV, RGB, GREY] {
409                let res = test_g2d_format_with_resize_dst_crop(i, o);
410                if let Err(e) = res {
411                    println!("{} to {} failed: {e:?}", i.display(), o.display());
412                } else {
413                    println!("{} to {} success", i.display(), o.display());
414                }
415            }
416        }
417    }
418
419    fn test_g2d_format_with_resize_(
420        g2d_in_fmt: FourCharCode,
421        g2d_out_fmt: FourCharCode,
422    ) -> Result<(), crate::Error> {
423        let dst_width = 600;
424        let dst_height = 400;
425        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
426        let src = TensorImage::load_jpeg(&file, Some(RGB), None)?;
427
428        let mut cpu_converter = CPUProcessor::new();
429
430        let mut reference = TensorImage::new(dst_width, dst_height, RGB, Some(TensorMemory::Dma))?;
431        cpu_converter.convert(
432            &src,
433            &mut reference,
434            Rotation::None,
435            Flip::None,
436            Crop::no_crop(),
437        )?;
438
439        // Create DMA buffer for G2D input
440        let mut src2 = TensorImage::new(1280, 720, g2d_in_fmt, Some(TensorMemory::Dma))?;
441
442        // For NV12 input, load from file since CPU doesn't support RGB→NV12
443        if g2d_in_fmt == NV12 {
444            let nv12_bytes = include_bytes!("../../../testdata/zidane.nv12");
445            src2.tensor()
446                .map()?
447                .as_mut_slice()
448                .copy_from_slice(nv12_bytes);
449        } else {
450            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
451        }
452
453        let mut g2d_dst =
454            TensorImage::new(dst_width, dst_height, g2d_out_fmt, Some(TensorMemory::Dma))?;
455        let mut g2d_converter = G2DProcessor::new()?;
456        g2d_converter.convert_(
457            &src2,
458            &mut g2d_dst,
459            Rotation::None,
460            Flip::None,
461            Crop::no_crop(),
462        )?;
463
464        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGB, None)?;
465        cpu_converter.convert(
466            &g2d_dst,
467            &mut cpu_dst,
468            Rotation::None,
469            Flip::None,
470            Crop::no_crop(),
471        )?;
472
473        compare_images(
474            &reference,
475            &cpu_dst,
476            0.98,
477            &format!(
478                "{}_to_{}_resized",
479                g2d_in_fmt.display(),
480                g2d_out_fmt.display()
481            ),
482        )
483    }
484
485    fn test_g2d_format_with_resize_dst_crop(
486        g2d_in_fmt: FourCharCode,
487        g2d_out_fmt: FourCharCode,
488    ) -> Result<(), crate::Error> {
489        let dst_width = 600;
490        let dst_height = 400;
491        let crop = Crop {
492            src_rect: None,
493            dst_rect: Some(Rect {
494                top: 100,
495                left: 100,
496                height: 100,
497                width: 200,
498            }),
499            dst_color: None,
500        };
501        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
502        let src = TensorImage::load_jpeg(&file, Some(RGB), None)?;
503
504        let mut cpu_converter = CPUProcessor::new();
505
506        let mut reference = TensorImage::new(dst_width, dst_height, RGB, Some(TensorMemory::Dma))?;
507        reference.tensor.map().unwrap().as_mut_slice().fill(128);
508        cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
509
510        // Create DMA buffer for G2D input
511        let mut src2 = TensorImage::new(1280, 720, g2d_in_fmt, Some(TensorMemory::Dma))?;
512
513        // For NV12 input, load from file since CPU doesn't support RGB→NV12
514        if g2d_in_fmt == NV12 {
515            let nv12_bytes = include_bytes!("../../../testdata/zidane.nv12");
516            src2.tensor()
517                .map()?
518                .as_mut_slice()
519                .copy_from_slice(nv12_bytes);
520        } else {
521            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
522        }
523
524        let mut g2d_dst =
525            TensorImage::new(dst_width, dst_height, g2d_out_fmt, Some(TensorMemory::Dma))?;
526        g2d_dst.tensor.map().unwrap().as_mut_slice().fill(128);
527        let mut g2d_converter = G2DProcessor::new()?;
528        g2d_converter.convert_(&src2, &mut g2d_dst, Rotation::None, Flip::None, crop)?;
529
530        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGB, None)?;
531        cpu_converter.convert(
532            &g2d_dst,
533            &mut cpu_dst,
534            Rotation::None,
535            Flip::None,
536            Crop::no_crop(),
537        )?;
538
539        compare_images(
540            &reference,
541            &cpu_dst,
542            0.98,
543            &format!(
544                "{}_to_{}_resized_dst_crop",
545                g2d_in_fmt.display(),
546                g2d_out_fmt.display()
547            ),
548        )
549    }
550
551    fn compare_images(
552        img1: &TensorImage,
553        img2: &TensorImage,
554        threshold: f64,
555        name: &str,
556    ) -> Result<(), crate::Error> {
557        assert_eq!(img1.height(), img2.height(), "Heights differ");
558        assert_eq!(img1.width(), img2.width(), "Widths differ");
559        assert_eq!(img1.fourcc(), img2.fourcc(), "FourCC differ");
560        assert!(
561            matches!(img1.fourcc(), RGB | RGBA),
562            "FourCC must be RGB or RGBA for comparison"
563        );
564        let image1 = match img1.fourcc() {
565            RGB => image::RgbImage::from_vec(
566                img1.width() as u32,
567                img1.height() as u32,
568                img1.tensor().map().unwrap().to_vec(),
569            )
570            .unwrap(),
571            RGBA => image::RgbaImage::from_vec(
572                img1.width() as u32,
573                img1.height() as u32,
574                img1.tensor().map().unwrap().to_vec(),
575            )
576            .unwrap()
577            .convert(),
578
579            _ => unreachable!(),
580        };
581
582        let image2 = match img2.fourcc() {
583            RGB => image::RgbImage::from_vec(
584                img2.width() as u32,
585                img2.height() as u32,
586                img2.tensor().map().unwrap().to_vec(),
587            )
588            .unwrap(),
589            RGBA => image::RgbaImage::from_vec(
590                img2.width() as u32,
591                img2.height() as u32,
592                img2.tensor().map().unwrap().to_vec(),
593            )
594            .unwrap()
595            .convert(),
596
597            _ => unreachable!(),
598        };
599
600        let similarity = image_compare::rgb_similarity_structure(
601            &image_compare::Algorithm::RootMeanSquared,
602            &image1,
603            &image2,
604        )
605        .expect("Image Comparison failed");
606
607        if similarity.score < threshold {
608            image1.save(format!("{name}_1.png")).unwrap();
609            image2.save(format!("{name}_2.png")).unwrap();
610            // similarity
611            //     .image
612            //     .to_color_map()
613            //     .save(format!("{name}.png"))
614            //     .unwrap();
615            return Err(Error::Internal(format!(
616                "{name}: converted image and target image have similarity score too low: {} < {}",
617                similarity.score, threshold
618            )));
619        }
620        Ok(())
621    }
622
623    // =========================================================================
624    // NV12 Reference Validation Tests
625    // These tests compare G2D NV12 conversions against ffmpeg-generated references
626    // =========================================================================
627
628    fn load_raw_image(
629        width: usize,
630        height: usize,
631        fourcc: FourCharCode,
632        memory: Option<TensorMemory>,
633        bytes: &[u8],
634    ) -> Result<TensorImage, crate::Error> {
635        let img = TensorImage::new(width, height, fourcc, memory)?;
636        let mut map = img.tensor().map()?;
637        map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
638        Ok(img)
639    }
640
641    /// Test G2D NV12→RGBA conversion against ffmpeg reference
642    #[test]
643    #[cfg(target_os = "linux")]
644    fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
645        if !is_dma_available() {
646            return Ok(());
647        }
648        // Load NV12 source
649        let src = load_raw_image(
650            1280,
651            720,
652            NV12,
653            Some(TensorMemory::Dma),
654            include_bytes!("../../../testdata/camera720p.nv12"),
655        )?;
656
657        // Load RGBA reference (ffmpeg-generated)
658        let reference = load_raw_image(
659            1280,
660            720,
661            RGBA,
662            None,
663            include_bytes!("../../../testdata/camera720p.rgba"),
664        )?;
665
666        // Convert using G2D
667        let mut dst = TensorImage::new(1280, 720, RGBA, Some(TensorMemory::Dma))?;
668        let mut g2d = G2DProcessor::new()?;
669        g2d.convert_(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())?;
670
671        // Copy to CPU for comparison
672        let cpu_dst = TensorImage::new(1280, 720, RGBA, None)?;
673        cpu_dst
674            .tensor()
675            .map()?
676            .as_mut_slice()
677            .copy_from_slice(dst.tensor().map()?.as_slice());
678
679        compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
680    }
681
682    /// Test G2D NV12→RGB conversion against ffmpeg reference
683    #[test]
684    #[cfg(target_os = "linux")]
685    fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
686        if !is_dma_available() {
687            return Ok(());
688        }
689        // Load NV12 source
690        let src = load_raw_image(
691            1280,
692            720,
693            NV12,
694            Some(TensorMemory::Dma),
695            include_bytes!("../../../testdata/camera720p.nv12"),
696        )?;
697
698        // Load RGB reference (ffmpeg-generated)
699        let reference = load_raw_image(
700            1280,
701            720,
702            RGB,
703            None,
704            include_bytes!("../../../testdata/camera720p.rgb"),
705        )?;
706
707        // Convert using G2D
708        let mut dst = TensorImage::new(1280, 720, RGB, Some(TensorMemory::Dma))?;
709        let mut g2d = G2DProcessor::new()?;
710        g2d.convert_(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())?;
711
712        // Copy to CPU for comparison
713        let cpu_dst = TensorImage::new(1280, 720, RGB, None)?;
714        cpu_dst
715            .tensor()
716            .map()?
717            .as_mut_slice()
718            .copy_from_slice(dst.tensor().map()?.as_slice());
719
720        compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
721    }
722
723    /// Test G2D YUYV→RGBA conversion against ffmpeg reference
724    #[test]
725    #[cfg(target_os = "linux")]
726    fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
727        if !is_dma_available() {
728            return Ok(());
729        }
730        // Load YUYV source
731        let src = load_raw_image(
732            1280,
733            720,
734            YUYV,
735            Some(TensorMemory::Dma),
736            include_bytes!("../../../testdata/camera720p.yuyv"),
737        )?;
738
739        // Load RGBA reference (ffmpeg-generated)
740        let reference = load_raw_image(
741            1280,
742            720,
743            RGBA,
744            None,
745            include_bytes!("../../../testdata/camera720p.rgba"),
746        )?;
747
748        // Convert using G2D
749        let mut dst = TensorImage::new(1280, 720, RGBA, Some(TensorMemory::Dma))?;
750        let mut g2d = G2DProcessor::new()?;
751        g2d.convert_(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())?;
752
753        // Copy to CPU for comparison
754        let cpu_dst = TensorImage::new(1280, 720, RGBA, None)?;
755        cpu_dst
756            .tensor()
757            .map()?
758            .as_mut_slice()
759            .copy_from_slice(dst.tensor().map()?.as_slice());
760
761        compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
762    }
763
764    /// Test G2D YUYV→RGB conversion against ffmpeg reference
765    #[test]
766    #[cfg(target_os = "linux")]
767    fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
768        if !is_dma_available() {
769            return Ok(());
770        }
771        // Load YUYV source
772        let src = load_raw_image(
773            1280,
774            720,
775            YUYV,
776            Some(TensorMemory::Dma),
777            include_bytes!("../../../testdata/camera720p.yuyv"),
778        )?;
779
780        // Load RGB reference (ffmpeg-generated)
781        let reference = load_raw_image(
782            1280,
783            720,
784            RGB,
785            None,
786            include_bytes!("../../../testdata/camera720p.rgb"),
787        )?;
788
789        // Convert using G2D
790        let mut dst = TensorImage::new(1280, 720, RGB, Some(TensorMemory::Dma))?;
791        let mut g2d = G2DProcessor::new()?;
792        g2d.convert_(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())?;
793
794        // Copy to CPU for comparison
795        let cpu_dst = TensorImage::new(1280, 720, RGB, None)?;
796        cpu_dst
797            .tensor()
798            .map()?
799            .as_mut_slice()
800            .copy_from_slice(dst.tensor().map()?.as_slice());
801
802        compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
803    }
804
805    /// Test G2D native BGRA conversion for all supported source formats.
806    /// Compares G2D src→BGRA against G2D src→RGBA by verifying R↔B swap.
807    #[test]
808    #[cfg(target_os = "linux")]
809    fn test_g2d_bgra_no_resize() {
810        for src_fmt in [RGBA, YUYV, NV12, BGRA] {
811            test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
812                panic!("{} to BGRA failed: {e:?}", src_fmt.display());
813            });
814        }
815    }
816
817    fn test_g2d_bgra_no_resize_(g2d_in_fmt: FourCharCode) -> Result<(), crate::Error> {
818        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
819        let src = TensorImage::load_jpeg(&file, Some(RGB), None)?;
820
821        // Create DMA buffer for G2D input
822        let mut src2 = TensorImage::new(1280, 720, g2d_in_fmt, Some(TensorMemory::Dma))?;
823        let mut cpu_converter = CPUProcessor::new();
824
825        if g2d_in_fmt == NV12 {
826            let nv12_bytes = include_bytes!("../../../testdata/zidane.nv12");
827            src2.tensor()
828                .map()?
829                .as_mut_slice()
830                .copy_from_slice(nv12_bytes);
831        } else {
832            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
833        }
834
835        let mut g2d = G2DProcessor::new()?;
836
837        // Convert to BGRA via G2D
838        let mut bgra_dst = TensorImage::new(1280, 720, BGRA, Some(TensorMemory::Dma))?;
839        g2d.convert_(
840            &src2,
841            &mut bgra_dst,
842            Rotation::None,
843            Flip::None,
844            Crop::no_crop(),
845        )?;
846
847        // Convert to RGBA via G2D as reference
848        let mut rgba_dst = TensorImage::new(1280, 720, RGBA, Some(TensorMemory::Dma))?;
849        g2d.convert_(
850            &src2,
851            &mut rgba_dst,
852            Rotation::None,
853            Flip::None,
854            Crop::no_crop(),
855        )?;
856
857        // Copy both to CPU memory for comparison
858        let bgra_cpu = TensorImage::new(1280, 720, BGRA, None)?;
859        bgra_cpu
860            .tensor()
861            .map()?
862            .as_mut_slice()
863            .copy_from_slice(bgra_dst.tensor().map()?.as_slice());
864
865        let rgba_cpu = TensorImage::new(1280, 720, RGBA, None)?;
866        rgba_cpu
867            .tensor()
868            .map()?
869            .as_mut_slice()
870            .copy_from_slice(rgba_dst.tensor().map()?.as_slice());
871
872        // Verify BGRA output has R↔B swapped vs RGBA output
873        let bgra_map = bgra_cpu.tensor().map()?;
874        let rgba_map = rgba_cpu.tensor().map()?;
875        let bgra_buf = bgra_map.as_slice();
876        let rgba_buf = rgba_map.as_slice();
877
878        assert_eq!(bgra_buf.len(), rgba_buf.len());
879        for (i, (bc, rc)) in bgra_buf
880            .chunks_exact(4)
881            .zip(rgba_buf.chunks_exact(4))
882            .enumerate()
883        {
884            assert_eq!(
885                bc[0],
886                rc[2],
887                "{} to BGRA: pixel {i} B mismatch",
888                g2d_in_fmt.display()
889            );
890            assert_eq!(
891                bc[1],
892                rc[1],
893                "{} to BGRA: pixel {i} G mismatch",
894                g2d_in_fmt.display()
895            );
896            assert_eq!(
897                bc[2],
898                rc[0],
899                "{} to BGRA: pixel {i} R mismatch",
900                g2d_in_fmt.display()
901            );
902            assert_eq!(
903                bc[3],
904                rc[3],
905                "{} to BGRA: pixel {i} A mismatch",
906                g2d_in_fmt.display()
907            );
908        }
909        Ok(())
910    }
911}