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