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, 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            (s, d) => {
156                return Err(Error::NotSupported(format!(
157                    "G2D does not support {} to {} conversion",
158                    s.display(),
159                    d.display()
160                )));
161            }
162        }
163        self.convert_(src, dst, rotation, flip, crop)
164    }
165
166    fn convert_ref(
167        &mut self,
168        src: &TensorImage,
169        dst: &mut TensorImageRef<'_>,
170        rotation: Rotation,
171        flip: Flip,
172        crop: Crop,
173    ) -> Result<()> {
174        // G2D doesn't support PLANAR_RGB output, delegate to CPU
175        let mut cpu = CPUProcessor::new();
176        cpu.convert_ref(src, dst, rotation, flip, crop)
177    }
178
179    fn draw_masks(
180        &mut self,
181        _dst: &mut TensorImage,
182        _detect: &[crate::DetectBox],
183        _segmentation: &[crate::Segmentation],
184    ) -> Result<()> {
185        Err(Error::NotImplemented(
186            "G2D does not support drawing detection or segmentation overlays".to_string(),
187        ))
188    }
189
190    fn draw_masks_proto(
191        &mut self,
192        _dst: &mut TensorImage,
193        _detect: &[crate::DetectBox],
194        _proto_data: &crate::ProtoData,
195    ) -> Result<()> {
196        Err(Error::NotImplemented(
197            "G2D does not support drawing detection or segmentation overlays".to_string(),
198        ))
199    }
200
201    fn decode_masks_atlas(
202        &mut self,
203        _detect: &[crate::DetectBox],
204        _proto_data: crate::ProtoData,
205        _output_width: usize,
206        _output_height: usize,
207    ) -> Result<(Vec<u8>, Vec<crate::MaskRegion>)> {
208        Err(Error::NotImplemented(
209            "G2D does not support decoding mask atlas".to_string(),
210        ))
211    }
212
213    fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
214        Err(Error::NotImplemented(
215            "G2D does not support setting colors for rendering detection or segmentation overlays"
216                .to_string(),
217        ))
218    }
219}
220
221impl TryFrom<&TensorImage> for G2DSurface {
222    type Error = Error;
223
224    fn try_from(img: &TensorImage) -> Result<Self, Self::Error> {
225        let phys: G2DPhysical = match img.tensor() {
226            Tensor::Dma(t) => t.as_raw_fd(),
227            _ => {
228                return Err(Error::NotImplemented(
229                    "g2d only supports Dma memory".to_string(),
230                ));
231            }
232        }
233        .try_into()?;
234
235        // NV12 is a two-plane format: Y plane followed by interleaved UV plane
236        // planes[0] = Y plane start, planes[1] = UV plane start (Y size = width *
237        // height)
238        let base_addr = phys.address();
239        let planes = if img.fourcc() == NV12 {
240            let uv_offset = (img.width() * img.height()) as u64;
241            [base_addr, base_addr + uv_offset, 0]
242        } else {
243            [base_addr, 0, 0]
244        };
245
246        Ok(Self {
247            planes,
248            format: G2DFormat::try_from(img.fourcc())?.format(),
249            left: 0,
250            top: 0,
251            right: img.width() as i32,
252            bottom: img.height() as i32,
253            stride: img.width() as i32,
254            width: img.width() as i32,
255            height: img.height() as i32,
256            blendfunc: 0,
257            clrcolor: 0,
258            rot: 0,
259            global_alpha: 0,
260        })
261    }
262}
263
264impl TryFrom<&mut TensorImage> for G2DSurface {
265    type Error = Error;
266
267    fn try_from(img: &mut TensorImage) -> Result<Self, Self::Error> {
268        let phys: G2DPhysical = match img.tensor() {
269            Tensor::Dma(t) => t.as_raw_fd(),
270            _ => {
271                return Err(Error::NotImplemented(
272                    "g2d only supports Dma memory".to_string(),
273                ));
274            }
275        }
276        .try_into()?;
277
278        // NV12 is a two-plane format: Y plane followed by interleaved UV plane
279        let base_addr = phys.address();
280        let planes = if img.fourcc() == NV12 {
281            let uv_offset = (img.width() * img.height()) as u64;
282            [base_addr, base_addr + uv_offset, 0]
283        } else {
284            [base_addr, 0, 0]
285        };
286
287        Ok(Self {
288            planes,
289            format: G2DFormat::try_from(img.fourcc())?.format(),
290            left: 0,
291            top: 0,
292            right: img.width() as i32,
293            bottom: img.height() as i32,
294            stride: img.width() as i32,
295            width: img.width() as i32,
296            height: img.height() as i32,
297            blendfunc: 0,
298            clrcolor: 0,
299            rot: 0,
300            global_alpha: 0,
301        })
302    }
303}
304
305#[cfg(feature = "g2d_test_formats")]
306#[cfg(test)]
307mod g2d_tests {
308    use super::*;
309    use crate::{
310        CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation, TensorImage, GREY,
311        NV12, RGB, RGBA, YUYV,
312    };
313    use edgefirst_tensor::{is_dma_available, TensorMapTrait, TensorMemory, TensorTrait};
314    use four_char_code::FourCharCode;
315    use image::buffer::ConvertBuffer;
316
317    #[test]
318    #[cfg(target_os = "linux")]
319    fn test_g2d_formats_no_resize() {
320        for i in [RGBA, YUYV, RGB, GREY, NV12] {
321            for o in [RGBA, YUYV, RGB, GREY] {
322                let res = test_g2d_format_no_resize_(i, o);
323                if let Err(e) = res {
324                    println!("{} to {} failed: {e:?}", i.display(), o.display());
325                } else {
326                    println!("{} to {} success", i.display(), o.display());
327                }
328            }
329        }
330    }
331
332    fn test_g2d_format_no_resize_(
333        g2d_in_fmt: FourCharCode,
334        g2d_out_fmt: FourCharCode,
335    ) -> Result<(), crate::Error> {
336        let dst_width = 1280;
337        let dst_height = 720;
338        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
339        let src = TensorImage::load_jpeg(&file, Some(RGB), None)?;
340
341        // Create DMA buffer for G2D input
342        let mut src2 = TensorImage::new(1280, 720, g2d_in_fmt, Some(TensorMemory::Dma))?;
343
344        let mut cpu_converter = CPUProcessor::new();
345
346        // For NV12 input, load from file since CPU doesn't support RGB→NV12
347        if g2d_in_fmt == NV12 {
348            let nv12_bytes = include_bytes!("../../../testdata/zidane.nv12");
349            src2.tensor()
350                .map()?
351                .as_mut_slice()
352                .copy_from_slice(nv12_bytes);
353        } else {
354            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
355        }
356
357        let mut g2d_dst =
358            TensorImage::new(dst_width, dst_height, g2d_out_fmt, Some(TensorMemory::Dma))?;
359        let mut g2d_converter = G2DProcessor::new()?;
360        g2d_converter.convert_(
361            &src2,
362            &mut g2d_dst,
363            Rotation::None,
364            Flip::None,
365            Crop::no_crop(),
366        )?;
367
368        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGB, None)?;
369        cpu_converter.convert(
370            &g2d_dst,
371            &mut cpu_dst,
372            Rotation::None,
373            Flip::None,
374            Crop::no_crop(),
375        )?;
376
377        compare_images(
378            &src,
379            &cpu_dst,
380            0.98,
381            &format!("{}_to_{}", g2d_in_fmt.display(), g2d_out_fmt.display()),
382        )
383    }
384
385    #[test]
386    #[cfg(target_os = "linux")]
387    fn test_g2d_formats_with_resize() {
388        for i in [RGBA, YUYV, RGB, GREY, NV12] {
389            for o in [RGBA, YUYV, RGB, GREY] {
390                let res = test_g2d_format_with_resize_(i, o);
391                if let Err(e) = res {
392                    println!("{} to {} failed: {e:?}", i.display(), o.display());
393                } else {
394                    println!("{} to {} success", i.display(), o.display());
395                }
396            }
397        }
398    }
399
400    #[test]
401    #[cfg(target_os = "linux")]
402    fn test_g2d_formats_with_resize_dst_crop() {
403        for i in [RGBA, YUYV, RGB, GREY, NV12] {
404            for o in [RGBA, YUYV, RGB, GREY] {
405                let res = test_g2d_format_with_resize_dst_crop(i, o);
406                if let Err(e) = res {
407                    println!("{} to {} failed: {e:?}", i.display(), o.display());
408                } else {
409                    println!("{} to {} success", i.display(), o.display());
410                }
411            }
412        }
413    }
414
415    fn test_g2d_format_with_resize_(
416        g2d_in_fmt: FourCharCode,
417        g2d_out_fmt: FourCharCode,
418    ) -> Result<(), crate::Error> {
419        let dst_width = 600;
420        let dst_height = 400;
421        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
422        let src = TensorImage::load_jpeg(&file, Some(RGB), None)?;
423
424        let mut cpu_converter = CPUProcessor::new();
425
426        let mut reference = TensorImage::new(dst_width, dst_height, RGB, Some(TensorMemory::Dma))?;
427        cpu_converter.convert(
428            &src,
429            &mut reference,
430            Rotation::None,
431            Flip::None,
432            Crop::no_crop(),
433        )?;
434
435        // Create DMA buffer for G2D input
436        let mut src2 = TensorImage::new(1280, 720, g2d_in_fmt, Some(TensorMemory::Dma))?;
437
438        // For NV12 input, load from file since CPU doesn't support RGB→NV12
439        if g2d_in_fmt == NV12 {
440            let nv12_bytes = include_bytes!("../../../testdata/zidane.nv12");
441            src2.tensor()
442                .map()?
443                .as_mut_slice()
444                .copy_from_slice(nv12_bytes);
445        } else {
446            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
447        }
448
449        let mut g2d_dst =
450            TensorImage::new(dst_width, dst_height, g2d_out_fmt, Some(TensorMemory::Dma))?;
451        let mut g2d_converter = G2DProcessor::new()?;
452        g2d_converter.convert_(
453            &src2,
454            &mut g2d_dst,
455            Rotation::None,
456            Flip::None,
457            Crop::no_crop(),
458        )?;
459
460        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGB, None)?;
461        cpu_converter.convert(
462            &g2d_dst,
463            &mut cpu_dst,
464            Rotation::None,
465            Flip::None,
466            Crop::no_crop(),
467        )?;
468
469        compare_images(
470            &reference,
471            &cpu_dst,
472            0.98,
473            &format!(
474                "{}_to_{}_resized",
475                g2d_in_fmt.display(),
476                g2d_out_fmt.display()
477            ),
478        )
479    }
480
481    fn test_g2d_format_with_resize_dst_crop(
482        g2d_in_fmt: FourCharCode,
483        g2d_out_fmt: FourCharCode,
484    ) -> Result<(), crate::Error> {
485        let dst_width = 600;
486        let dst_height = 400;
487        let crop = Crop {
488            src_rect: None,
489            dst_rect: Some(Rect {
490                top: 100,
491                left: 100,
492                height: 100,
493                width: 200,
494            }),
495            dst_color: None,
496        };
497        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
498        let src = TensorImage::load_jpeg(&file, Some(RGB), None)?;
499
500        let mut cpu_converter = CPUProcessor::new();
501
502        let mut reference = TensorImage::new(dst_width, dst_height, RGB, Some(TensorMemory::Dma))?;
503        reference.tensor.map().unwrap().as_mut_slice().fill(128);
504        cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
505
506        // Create DMA buffer for G2D input
507        let mut src2 = TensorImage::new(1280, 720, g2d_in_fmt, Some(TensorMemory::Dma))?;
508
509        // For NV12 input, load from file since CPU doesn't support RGB→NV12
510        if g2d_in_fmt == NV12 {
511            let nv12_bytes = include_bytes!("../../../testdata/zidane.nv12");
512            src2.tensor()
513                .map()?
514                .as_mut_slice()
515                .copy_from_slice(nv12_bytes);
516        } else {
517            cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
518        }
519
520        let mut g2d_dst =
521            TensorImage::new(dst_width, dst_height, g2d_out_fmt, Some(TensorMemory::Dma))?;
522        g2d_dst.tensor.map().unwrap().as_mut_slice().fill(128);
523        let mut g2d_converter = G2DProcessor::new()?;
524        g2d_converter.convert_(&src2, &mut g2d_dst, Rotation::None, Flip::None, crop)?;
525
526        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGB, None)?;
527        cpu_converter.convert(
528            &g2d_dst,
529            &mut cpu_dst,
530            Rotation::None,
531            Flip::None,
532            Crop::no_crop(),
533        )?;
534
535        compare_images(
536            &reference,
537            &cpu_dst,
538            0.98,
539            &format!(
540                "{}_to_{}_resized_dst_crop",
541                g2d_in_fmt.display(),
542                g2d_out_fmt.display()
543            ),
544        )
545    }
546
547    fn compare_images(
548        img1: &TensorImage,
549        img2: &TensorImage,
550        threshold: f64,
551        name: &str,
552    ) -> Result<(), crate::Error> {
553        assert_eq!(img1.height(), img2.height(), "Heights differ");
554        assert_eq!(img1.width(), img2.width(), "Widths differ");
555        assert_eq!(img1.fourcc(), img2.fourcc(), "FourCC differ");
556        assert!(
557            matches!(img1.fourcc(), RGB | RGBA),
558            "FourCC must be RGB or RGBA for comparison"
559        );
560        let image1 = match img1.fourcc() {
561            RGB => image::RgbImage::from_vec(
562                img1.width() as u32,
563                img1.height() as u32,
564                img1.tensor().map().unwrap().to_vec(),
565            )
566            .unwrap(),
567            RGBA => image::RgbaImage::from_vec(
568                img1.width() as u32,
569                img1.height() as u32,
570                img1.tensor().map().unwrap().to_vec(),
571            )
572            .unwrap()
573            .convert(),
574
575            _ => unreachable!(),
576        };
577
578        let image2 = match img2.fourcc() {
579            RGB => image::RgbImage::from_vec(
580                img2.width() as u32,
581                img2.height() as u32,
582                img2.tensor().map().unwrap().to_vec(),
583            )
584            .unwrap(),
585            RGBA => image::RgbaImage::from_vec(
586                img2.width() as u32,
587                img2.height() as u32,
588                img2.tensor().map().unwrap().to_vec(),
589            )
590            .unwrap()
591            .convert(),
592
593            _ => unreachable!(),
594        };
595
596        let similarity = image_compare::rgb_similarity_structure(
597            &image_compare::Algorithm::RootMeanSquared,
598            &image1,
599            &image2,
600        )
601        .expect("Image Comparison failed");
602
603        if similarity.score < threshold {
604            image1.save(format!("{name}_1.png")).unwrap();
605            image2.save(format!("{name}_2.png")).unwrap();
606            // similarity
607            //     .image
608            //     .to_color_map()
609            //     .save(format!("{name}.png"))
610            //     .unwrap();
611            return Err(Error::Internal(format!(
612                "{name}: converted image and target image have similarity score too low: {} < {}",
613                similarity.score, threshold
614            )));
615        }
616        Ok(())
617    }
618
619    // =========================================================================
620    // NV12 Reference Validation Tests
621    // These tests compare G2D NV12 conversions against ffmpeg-generated references
622    // =========================================================================
623
624    fn load_raw_image(
625        width: usize,
626        height: usize,
627        fourcc: FourCharCode,
628        memory: Option<TensorMemory>,
629        bytes: &[u8],
630    ) -> Result<TensorImage, crate::Error> {
631        let img = TensorImage::new(width, height, fourcc, memory)?;
632        let mut map = img.tensor().map()?;
633        map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
634        Ok(img)
635    }
636
637    /// Test G2D NV12→RGBA conversion against ffmpeg reference
638    #[test]
639    #[cfg(target_os = "linux")]
640    fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
641        if !is_dma_available() {
642            return Ok(());
643        }
644        // Load NV12 source
645        let src = load_raw_image(
646            1280,
647            720,
648            NV12,
649            Some(TensorMemory::Dma),
650            include_bytes!("../../../testdata/camera720p.nv12"),
651        )?;
652
653        // Load RGBA reference (ffmpeg-generated)
654        let reference = load_raw_image(
655            1280,
656            720,
657            RGBA,
658            None,
659            include_bytes!("../../../testdata/camera720p.rgba"),
660        )?;
661
662        // Convert using G2D
663        let mut dst = TensorImage::new(1280, 720, RGBA, Some(TensorMemory::Dma))?;
664        let mut g2d = G2DProcessor::new()?;
665        g2d.convert_(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())?;
666
667        // Copy to CPU for comparison
668        let cpu_dst = TensorImage::new(1280, 720, RGBA, None)?;
669        cpu_dst
670            .tensor()
671            .map()?
672            .as_mut_slice()
673            .copy_from_slice(dst.tensor().map()?.as_slice());
674
675        compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
676    }
677
678    /// Test G2D NV12→RGB conversion against ffmpeg reference
679    #[test]
680    #[cfg(target_os = "linux")]
681    fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
682        if !is_dma_available() {
683            return Ok(());
684        }
685        // Load NV12 source
686        let src = load_raw_image(
687            1280,
688            720,
689            NV12,
690            Some(TensorMemory::Dma),
691            include_bytes!("../../../testdata/camera720p.nv12"),
692        )?;
693
694        // Load RGB reference (ffmpeg-generated)
695        let reference = load_raw_image(
696            1280,
697            720,
698            RGB,
699            None,
700            include_bytes!("../../../testdata/camera720p.rgb"),
701        )?;
702
703        // Convert using G2D
704        let mut dst = TensorImage::new(1280, 720, RGB, Some(TensorMemory::Dma))?;
705        let mut g2d = G2DProcessor::new()?;
706        g2d.convert_(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())?;
707
708        // Copy to CPU for comparison
709        let cpu_dst = TensorImage::new(1280, 720, RGB, None)?;
710        cpu_dst
711            .tensor()
712            .map()?
713            .as_mut_slice()
714            .copy_from_slice(dst.tensor().map()?.as_slice());
715
716        compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
717    }
718
719    /// Test G2D YUYV→RGBA conversion against ffmpeg reference
720    #[test]
721    #[cfg(target_os = "linux")]
722    fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
723        if !is_dma_available() {
724            return Ok(());
725        }
726        // Load YUYV source
727        let src = load_raw_image(
728            1280,
729            720,
730            YUYV,
731            Some(TensorMemory::Dma),
732            include_bytes!("../../../testdata/camera720p.yuyv"),
733        )?;
734
735        // Load RGBA reference (ffmpeg-generated)
736        let reference = load_raw_image(
737            1280,
738            720,
739            RGBA,
740            None,
741            include_bytes!("../../../testdata/camera720p.rgba"),
742        )?;
743
744        // Convert using G2D
745        let mut dst = TensorImage::new(1280, 720, RGBA, Some(TensorMemory::Dma))?;
746        let mut g2d = G2DProcessor::new()?;
747        g2d.convert_(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())?;
748
749        // Copy to CPU for comparison
750        let cpu_dst = TensorImage::new(1280, 720, RGBA, None)?;
751        cpu_dst
752            .tensor()
753            .map()?
754            .as_mut_slice()
755            .copy_from_slice(dst.tensor().map()?.as_slice());
756
757        compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
758    }
759
760    /// Test G2D YUYV→RGB conversion against ffmpeg reference
761    #[test]
762    #[cfg(target_os = "linux")]
763    fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
764        if !is_dma_available() {
765            return Ok(());
766        }
767        // Load YUYV source
768        let src = load_raw_image(
769            1280,
770            720,
771            YUYV,
772            Some(TensorMemory::Dma),
773            include_bytes!("../../../testdata/camera720p.yuyv"),
774        )?;
775
776        // Load RGB reference (ffmpeg-generated)
777        let reference = load_raw_image(
778            1280,
779            720,
780            RGB,
781            None,
782            include_bytes!("../../../testdata/camera720p.rgb"),
783        )?;
784
785        // Convert using G2D
786        let mut dst = TensorImage::new(1280, 720, RGB, Some(TensorMemory::Dma))?;
787        let mut g2d = G2DProcessor::new()?;
788        g2d.convert_(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())?;
789
790        // Copy to CPU for comparison
791        let cpu_dst = TensorImage::new(1280, 720, RGB, None)?;
792        cpu_dst
793            .tensor()
794            .map()?
795            .as_mut_slice()
796            .copy_from_slice(dst.tensor().map()?.as_slice());
797
798        compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
799    }
800}