#![cfg(target_os = "linux")]
use crate::{CPUProcessor, Crop, Error, Flip, ImageProcessorTrait, Result, Rotation};
use edgefirst_tensor::{DType, PixelFormat, Tensor, TensorDyn, TensorMapTrait, TensorTrait};
use four_char_code::FourCharCode;
use g2d_sys::{G2DFormat, G2DPhysical, G2DSurface, G2D};
use std::{os::fd::AsRawFd, time::Instant};
fn pixelfmt_to_fourcc(fmt: PixelFormat) -> FourCharCode {
use four_char_code::four_char_code;
match fmt {
PixelFormat::Rgb => four_char_code!("RGB "),
PixelFormat::Rgba => four_char_code!("RGBA"),
PixelFormat::Bgra => four_char_code!("BGRA"),
PixelFormat::Grey => four_char_code!("Y800"),
PixelFormat::Yuyv => four_char_code!("YUYV"),
PixelFormat::Vyuy => four_char_code!("VYUY"),
PixelFormat::Nv12 => four_char_code!("NV12"),
PixelFormat::Nv16 => four_char_code!("NV16"),
_ => four_char_code!("RGBA"),
}
}
#[derive(Debug)]
pub struct G2DProcessor {
g2d: G2D,
}
unsafe impl Send for G2DProcessor {}
unsafe impl Sync for G2DProcessor {}
impl G2DProcessor {
pub fn new() -> Result<Self> {
let mut g2d = G2D::new("libg2d.so.2")?;
g2d.set_bt709_colorspace()?;
log::debug!("G2DConverter created with version {:?}", g2d.version());
Ok(Self { g2d })
}
pub fn version(&self) -> g2d_sys::Version {
self.g2d.version()
}
fn convert_impl(
&mut self,
src_dyn: &TensorDyn,
dst_dyn: &mut TensorDyn,
rotation: Rotation,
flip: Flip,
crop: Crop,
) -> Result<()> {
if log::log_enabled!(log::Level::Trace) {
log::trace!(
"G2D convert: {:?}({:?}/{:?}) → {:?}({:?}/{:?})",
src_dyn.format(),
src_dyn.dtype(),
src_dyn.memory(),
dst_dyn.format(),
dst_dyn.dtype(),
dst_dyn.memory(),
);
}
if src_dyn.dtype() != DType::U8 {
return Err(Error::NotSupported(
"G2D only supports u8 source tensors".to_string(),
));
}
let is_int8_dst = dst_dyn.dtype() == DType::I8;
if dst_dyn.dtype() != DType::U8 && !is_int8_dst {
return Err(Error::NotSupported(
"G2D only supports u8 or i8 destination tensors".to_string(),
));
}
let src_fmt = src_dyn.format().ok_or(Error::NotAnImage)?;
let dst_fmt = dst_dyn.format().ok_or(Error::NotAnImage)?;
use PixelFormat::*;
match (src_fmt, dst_fmt) {
(Rgba, Rgba) => {}
(Rgba, Yuyv) => {}
(Rgba, Rgb) => {}
(Yuyv, Rgba) => {}
(Yuyv, Yuyv) => {}
(Yuyv, Rgb) => {}
(Nv12, Rgba) => {}
(Nv12, Yuyv) => {}
(Nv12, Rgb) => {}
(Rgba, Bgra) => {}
(Yuyv, Bgra) => {}
(Nv12, Bgra) => {}
(Bgra, Bgra) => {}
(s, d) => {
return Err(Error::NotSupported(format!(
"G2D does not support {} to {} conversion",
s, d
)));
}
}
crop.check_crop_dyn(src_dyn, dst_dyn)?;
let src = src_dyn.as_u8().unwrap();
let dst = if is_int8_dst {
let i8_tensor = dst_dyn.as_i8_mut().unwrap();
unsafe { &mut *(i8_tensor as *mut Tensor<i8> as *mut Tensor<u8>) }
} else {
dst_dyn.as_u8_mut().unwrap()
};
let mut src_surface = tensor_to_g2d_surface(src)?;
let mut dst_surface = tensor_to_g2d_surface(dst)?;
src_surface.rot = match flip {
Flip::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
Flip::Vertical => g2d_sys::g2d_rotation_G2D_FLIP_V,
Flip::Horizontal => g2d_sys::g2d_rotation_G2D_FLIP_H,
};
dst_surface.rot = match rotation {
Rotation::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
Rotation::Clockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_90,
Rotation::Rotate180 => g2d_sys::g2d_rotation_G2D_ROTATION_180,
Rotation::CounterClockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_270,
};
if let Some(crop_rect) = crop.src_rect {
src_surface.left = crop_rect.left as i32;
src_surface.top = crop_rect.top as i32;
src_surface.right = (crop_rect.left + crop_rect.width) as i32;
src_surface.bottom = (crop_rect.top + crop_rect.height) as i32;
}
let dst_w = dst.width().unwrap();
let dst_h = dst.height().unwrap();
let needs_clear = crop.dst_color.is_some()
&& crop.dst_rect.is_some_and(|dst_rect| {
dst_rect.left != 0
|| dst_rect.top != 0
|| dst_rect.width != dst_w
|| dst_rect.height != dst_h
});
if needs_clear && dst_fmt != Rgb {
if let Some(dst_color) = crop.dst_color {
let start = Instant::now();
self.g2d.clear(&mut dst_surface, dst_color)?;
log::trace!("g2d clear takes {:?}", start.elapsed());
}
}
if let Some(crop_rect) = crop.dst_rect {
dst_surface.planes[0] += ((crop_rect.top * dst_surface.stride as usize
+ crop_rect.left)
* dst_fmt.channels()) as u64;
dst_surface.right = crop_rect.width as i32;
dst_surface.bottom = crop_rect.height as i32;
dst_surface.width = crop_rect.width as i32;
dst_surface.height = crop_rect.height as i32;
}
log::trace!("G2D blit: {src_fmt}→{dst_fmt} int8={is_int8_dst}");
self.g2d.blit(&src_surface, &dst_surface)?;
self.g2d.finish()?;
log::trace!("G2D blit complete");
if needs_clear && dst_fmt == Rgb {
if let (Some(dst_color), Some(dst_rect)) = (crop.dst_color, crop.dst_rect) {
let start = Instant::now();
CPUProcessor::fill_image_outside_crop_u8(dst, dst_color, dst_rect)?;
log::trace!("cpu fill takes {:?}", start.elapsed());
}
}
if is_int8_dst {
let start = Instant::now();
let mut map = dst.map()?;
crate::cpu::apply_int8_xor_bias(map.as_mut_slice(), dst_fmt);
log::trace!("g2d int8 XOR 0x80 post-pass takes {:?}", start.elapsed());
}
Ok(())
}
}
impl ImageProcessorTrait for G2DProcessor {
fn convert(
&mut self,
src: &TensorDyn,
dst: &mut TensorDyn,
rotation: Rotation,
flip: Flip,
crop: Crop,
) -> Result<()> {
self.convert_impl(src, dst, rotation, flip, crop)
}
fn draw_decoded_masks(
&mut self,
dst: &mut TensorDyn,
detect: &[crate::DetectBox],
segmentation: &[crate::Segmentation],
overlay: crate::MaskOverlay<'_>,
) -> Result<()> {
if !detect.is_empty() || !segmentation.is_empty() {
return Err(Error::NotImplemented(
"G2D does not support drawing detection or segmentation overlays".to_string(),
));
}
draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
}
fn draw_proto_masks(
&mut self,
dst: &mut TensorDyn,
detect: &[crate::DetectBox],
_proto_data: &crate::ProtoData,
overlay: crate::MaskOverlay<'_>,
) -> Result<()> {
if !detect.is_empty() {
return Err(Error::NotImplemented(
"G2D does not support drawing detection or segmentation overlays".to_string(),
));
}
draw_empty_frame_g2d(&mut self.g2d, dst, overlay.background)
}
fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
Err(Error::NotImplemented(
"G2D does not support setting colors for rendering detection or segmentation overlays"
.to_string(),
))
}
}
fn draw_empty_frame_g2d(
g2d: &mut G2D,
dst_dyn: &mut TensorDyn,
background: Option<&TensorDyn>,
) -> Result<()> {
if dst_dyn.dtype() != DType::U8 {
return Err(Error::NotSupported(
"G2D only supports u8 destination tensors".to_string(),
));
}
let dst = dst_dyn.as_u8_mut().ok_or(Error::NotAnImage)?;
if dst.as_dma().is_none() {
return Err(Error::NotImplemented(
"g2d only supports Dma memory".to_string(),
));
}
let mut dst_surface = tensor_to_g2d_surface(dst)?;
match background {
None => {
let start = Instant::now();
g2d.clear(&mut dst_surface, [0, 0, 0, 0])?;
g2d.finish()?;
log::trace!("g2d clear (empty frame) takes {:?}", start.elapsed());
}
Some(bg_dyn) => {
if bg_dyn.shape() != dst.shape() {
return Err(Error::InvalidShape(
"background shape does not match dst".into(),
));
}
if bg_dyn.format() != dst.format() {
return Err(Error::InvalidShape(
"background pixel format does not match dst".into(),
));
}
if bg_dyn.dtype() != DType::U8 {
return Err(Error::NotSupported(
"G2D only supports u8 background tensors".to_string(),
));
}
let bg = bg_dyn.as_u8().ok_or(Error::NotAnImage)?;
if bg.as_dma().is_none() {
return Err(Error::NotImplemented(
"g2d background must be Dma-backed".to_string(),
));
}
let src_surface = tensor_to_g2d_surface(bg)?;
let start = Instant::now();
g2d.blit(&src_surface, &dst_surface)?;
g2d.finish()?;
log::trace!("g2d blit (bg→dst) takes {:?}", start.elapsed());
}
}
Ok(())
}
fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
let fmt = img.format().ok_or(Error::NotAnImage)?;
let dma = img
.as_dma()
.ok_or_else(|| Error::NotImplemented("g2d only supports Dma memory".to_string()))?;
let phys: G2DPhysical = dma.fd.as_raw_fd().try_into()?;
let base_addr = phys.address();
let luma_offset = img.plane_offset().unwrap_or(0) as u64;
let planes = if fmt == PixelFormat::Nv12 {
if img.is_multiplane() {
let chroma = img.chroma().unwrap();
let chroma_dma = chroma.as_dma().ok_or_else(|| {
Error::NotImplemented("g2d multiplane chroma must be DMA-backed".to_string())
})?;
let uv_phys: G2DPhysical = chroma_dma.fd.as_raw_fd().try_into()?;
let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
[
base_addr + luma_offset,
uv_phys.address() + chroma_offset,
0,
]
} else {
let w = img.width().unwrap();
let h = img.height().unwrap();
let stride = img.effective_row_stride().unwrap_or(w);
let uv_offset = (luma_offset as usize + stride * h) as u64;
[base_addr + luma_offset, base_addr + uv_offset, 0]
}
} else {
[base_addr + luma_offset, 0, 0]
};
let w = img.width().unwrap();
let h = img.height().unwrap();
let fourcc = pixelfmt_to_fourcc(fmt);
let stride_pixels = match img.effective_row_stride() {
Some(s) => {
let channels = fmt.channels();
if s % channels != 0 {
return Err(Error::NotImplemented(
"g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
));
}
s / channels
}
None => w,
};
Ok(G2DSurface {
planes,
format: G2DFormat::try_from(fourcc)?.format(),
left: 0,
top: 0,
right: w as i32,
bottom: h as i32,
stride: stride_pixels as i32,
width: w as i32,
height: h as i32,
blendfunc: 0,
clrcolor: 0,
rot: 0,
global_alpha: 0,
})
}
#[cfg(feature = "g2d_test_formats")]
#[cfg(test)]
mod g2d_tests {
use super::*;
use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation};
use edgefirst_tensor::{
is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
};
use image::buffer::ConvertBuffer;
#[test]
#[cfg(target_os = "linux")]
fn test_g2d_formats_no_resize() {
for i in [
PixelFormat::Rgba,
PixelFormat::Yuyv,
PixelFormat::Rgb,
PixelFormat::Grey,
PixelFormat::Nv12,
] {
for o in [
PixelFormat::Rgba,
PixelFormat::Yuyv,
PixelFormat::Rgb,
PixelFormat::Grey,
] {
let res = test_g2d_format_no_resize_(i, o);
if let Err(e) = res {
println!("{i} to {o} failed: {e:?}");
} else {
println!("{i} to {o} success");
}
}
}
}
fn test_g2d_format_no_resize_(
g2d_in_fmt: PixelFormat,
g2d_out_fmt: PixelFormat,
) -> Result<(), crate::Error> {
let dst_width = 1280;
let dst_height = 720;
let file = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/zidane.jpg"
))
.to_vec();
let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
let mut cpu_converter = CPUProcessor::new();
if g2d_in_fmt == PixelFormat::Nv12 {
let nv12_bytes = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/zidane.nv12"
));
src2.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(nv12_bytes);
} else {
cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
}
let mut g2d_dst = TensorDyn::image(
dst_width,
dst_height,
g2d_out_fmt,
DType::U8,
Some(TensorMemory::Dma),
)?;
let mut g2d_converter = G2DProcessor::new()?;
let src2_dyn = src2;
let mut g2d_dst_dyn = g2d_dst;
g2d_converter.convert(
&src2_dyn,
&mut g2d_dst_dyn,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
g2d_dst = {
let mut __t = g2d_dst_dyn.into_u8().unwrap();
__t.set_format(g2d_out_fmt)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let mut cpu_dst =
TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
cpu_converter.convert(
&g2d_dst,
&mut cpu_dst,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
compare_images(
&src,
&cpu_dst,
0.98,
&format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
)
}
#[test]
#[cfg(target_os = "linux")]
fn test_g2d_formats_with_resize() {
for i in [
PixelFormat::Rgba,
PixelFormat::Yuyv,
PixelFormat::Rgb,
PixelFormat::Grey,
PixelFormat::Nv12,
] {
for o in [
PixelFormat::Rgba,
PixelFormat::Yuyv,
PixelFormat::Rgb,
PixelFormat::Grey,
] {
let res = test_g2d_format_with_resize_(i, o);
if let Err(e) = res {
println!("{i} to {o} failed: {e:?}");
} else {
println!("{i} to {o} success");
}
}
}
}
#[test]
#[cfg(target_os = "linux")]
fn test_g2d_formats_with_resize_dst_crop() {
for i in [
PixelFormat::Rgba,
PixelFormat::Yuyv,
PixelFormat::Rgb,
PixelFormat::Grey,
PixelFormat::Nv12,
] {
for o in [
PixelFormat::Rgba,
PixelFormat::Yuyv,
PixelFormat::Rgb,
PixelFormat::Grey,
] {
let res = test_g2d_format_with_resize_dst_crop(i, o);
if let Err(e) = res {
println!("{i} to {o} failed: {e:?}");
} else {
println!("{i} to {o} success");
}
}
}
}
fn test_g2d_format_with_resize_(
g2d_in_fmt: PixelFormat,
g2d_out_fmt: PixelFormat,
) -> Result<(), crate::Error> {
let dst_width = 600;
let dst_height = 400;
let file = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/zidane.jpg"
))
.to_vec();
let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
let mut cpu_converter = CPUProcessor::new();
let mut reference = TensorDyn::image(
dst_width,
dst_height,
PixelFormat::Rgb,
DType::U8,
Some(TensorMemory::Dma),
)?;
cpu_converter.convert(
&src,
&mut reference,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
if g2d_in_fmt == PixelFormat::Nv12 {
let nv12_bytes = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/zidane.nv12"
));
src2.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(nv12_bytes);
} else {
cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
}
let mut g2d_dst = TensorDyn::image(
dst_width,
dst_height,
g2d_out_fmt,
DType::U8,
Some(TensorMemory::Dma),
)?;
let mut g2d_converter = G2DProcessor::new()?;
let src2_dyn = src2;
let mut g2d_dst_dyn = g2d_dst;
g2d_converter.convert(
&src2_dyn,
&mut g2d_dst_dyn,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
g2d_dst = {
let mut __t = g2d_dst_dyn.into_u8().unwrap();
__t.set_format(g2d_out_fmt)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let mut cpu_dst =
TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
cpu_converter.convert(
&g2d_dst,
&mut cpu_dst,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
compare_images(
&reference,
&cpu_dst,
0.98,
&format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
)
}
fn test_g2d_format_with_resize_dst_crop(
g2d_in_fmt: PixelFormat,
g2d_out_fmt: PixelFormat,
) -> Result<(), crate::Error> {
let dst_width = 600;
let dst_height = 400;
let crop = Crop {
src_rect: None,
dst_rect: Some(Rect {
top: 100,
left: 100,
height: 100,
width: 200,
}),
dst_color: None,
};
let file = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/zidane.jpg"
))
.to_vec();
let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
let mut cpu_converter = CPUProcessor::new();
let mut reference = TensorDyn::image(
dst_width,
dst_height,
PixelFormat::Rgb,
DType::U8,
Some(TensorMemory::Dma),
)?;
reference
.as_u8()
.unwrap()
.map()
.unwrap()
.as_mut_slice()
.fill(128);
cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
if g2d_in_fmt == PixelFormat::Nv12 {
let nv12_bytes = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/zidane.nv12"
));
src2.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(nv12_bytes);
} else {
cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
}
let mut g2d_dst = TensorDyn::image(
dst_width,
dst_height,
g2d_out_fmt,
DType::U8,
Some(TensorMemory::Dma),
)?;
g2d_dst
.as_u8()
.unwrap()
.map()
.unwrap()
.as_mut_slice()
.fill(128);
let mut g2d_converter = G2DProcessor::new()?;
let src2_dyn = src2;
let mut g2d_dst_dyn = g2d_dst;
g2d_converter.convert(
&src2_dyn,
&mut g2d_dst_dyn,
Rotation::None,
Flip::None,
crop,
)?;
g2d_dst = {
let mut __t = g2d_dst_dyn.into_u8().unwrap();
__t.set_format(g2d_out_fmt)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let mut cpu_dst =
TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
cpu_converter.convert(
&g2d_dst,
&mut cpu_dst,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
compare_images(
&reference,
&cpu_dst,
0.98,
&format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
)
}
fn compare_images(
img1: &TensorDyn,
img2: &TensorDyn,
threshold: f64,
name: &str,
) -> Result<(), crate::Error> {
assert_eq!(img1.height(), img2.height(), "Heights differ");
assert_eq!(img1.width(), img2.width(), "Widths differ");
assert_eq!(
img1.format().unwrap(),
img2.format().unwrap(),
"PixelFormat differ"
);
assert!(
matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
"format must be Rgb or Rgba for comparison"
);
let image1 = match img1.format().unwrap() {
PixelFormat::Rgb => image::RgbImage::from_vec(
img1.width().unwrap() as u32,
img1.height().unwrap() as u32,
img1.as_u8().unwrap().map().unwrap().to_vec(),
)
.unwrap(),
PixelFormat::Rgba => image::RgbaImage::from_vec(
img1.width().unwrap() as u32,
img1.height().unwrap() as u32,
img1.as_u8().unwrap().map().unwrap().to_vec(),
)
.unwrap()
.convert(),
_ => unreachable!(),
};
let image2 = match img2.format().unwrap() {
PixelFormat::Rgb => image::RgbImage::from_vec(
img2.width().unwrap() as u32,
img2.height().unwrap() as u32,
img2.as_u8().unwrap().map().unwrap().to_vec(),
)
.unwrap(),
PixelFormat::Rgba => image::RgbaImage::from_vec(
img2.width().unwrap() as u32,
img2.height().unwrap() as u32,
img2.as_u8().unwrap().map().unwrap().to_vec(),
)
.unwrap()
.convert(),
_ => unreachable!(),
};
let similarity = image_compare::rgb_similarity_structure(
&image_compare::Algorithm::RootMeanSquared,
&image1,
&image2,
)
.expect("Image Comparison failed");
if similarity.score < threshold {
image1.save(format!("{name}_1.png")).unwrap();
image2.save(format!("{name}_2.png")).unwrap();
return Err(Error::Internal(format!(
"{name}: converted image and target image have similarity score too low: {} < {}",
similarity.score, threshold
)));
}
Ok(())
}
fn load_raw_image(
width: usize,
height: usize,
format: PixelFormat,
memory: Option<TensorMemory>,
bytes: &[u8],
) -> Result<TensorDyn, crate::Error> {
let img = TensorDyn::image(width, height, format, DType::U8, memory)?;
let mut map = img.as_u8().unwrap().map()?;
map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
Ok(img)
}
#[test]
#[cfg(target_os = "linux")]
fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
if !is_dma_available() {
return Ok(());
}
let src = load_raw_image(
1280,
720,
PixelFormat::Nv12,
Some(TensorMemory::Dma),
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/camera720p.nv12"
)),
)?;
let reference = load_raw_image(
1280,
720,
PixelFormat::Rgba,
None,
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/camera720p.rgba"
)),
)?;
let mut dst = TensorDyn::image(
1280,
720,
PixelFormat::Rgba,
DType::U8,
Some(TensorMemory::Dma),
)?;
let mut g2d = G2DProcessor::new()?;
let src_dyn = src;
let mut dst_dyn = dst;
g2d.convert(
&src_dyn,
&mut dst_dyn,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
dst = {
let mut __t = dst_dyn.into_u8().unwrap();
__t.set_format(PixelFormat::Rgba)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
cpu_dst
.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
}
#[test]
#[cfg(target_os = "linux")]
fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
if !is_dma_available() {
return Ok(());
}
let src = load_raw_image(
1280,
720,
PixelFormat::Nv12,
Some(TensorMemory::Dma),
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/camera720p.nv12"
)),
)?;
let reference = load_raw_image(
1280,
720,
PixelFormat::Rgb,
None,
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/camera720p.rgb"
)),
)?;
let mut dst = TensorDyn::image(
1280,
720,
PixelFormat::Rgb,
DType::U8,
Some(TensorMemory::Dma),
)?;
let mut g2d = G2DProcessor::new()?;
let src_dyn = src;
let mut dst_dyn = dst;
g2d.convert(
&src_dyn,
&mut dst_dyn,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
dst = {
let mut __t = dst_dyn.into_u8().unwrap();
__t.set_format(PixelFormat::Rgb)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
cpu_dst
.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
}
#[test]
#[cfg(target_os = "linux")]
fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
if !is_dma_available() {
return Ok(());
}
let src = load_raw_image(
1280,
720,
PixelFormat::Yuyv,
Some(TensorMemory::Dma),
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/camera720p.yuyv"
)),
)?;
let reference = load_raw_image(
1280,
720,
PixelFormat::Rgba,
None,
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/camera720p.rgba"
)),
)?;
let mut dst = TensorDyn::image(
1280,
720,
PixelFormat::Rgba,
DType::U8,
Some(TensorMemory::Dma),
)?;
let mut g2d = G2DProcessor::new()?;
let src_dyn = src;
let mut dst_dyn = dst;
g2d.convert(
&src_dyn,
&mut dst_dyn,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
dst = {
let mut __t = dst_dyn.into_u8().unwrap();
__t.set_format(PixelFormat::Rgba)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
cpu_dst
.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
}
#[test]
#[cfg(target_os = "linux")]
fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
if !is_dma_available() {
return Ok(());
}
let src = load_raw_image(
1280,
720,
PixelFormat::Yuyv,
Some(TensorMemory::Dma),
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/camera720p.yuyv"
)),
)?;
let reference = load_raw_image(
1280,
720,
PixelFormat::Rgb,
None,
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/camera720p.rgb"
)),
)?;
let mut dst = TensorDyn::image(
1280,
720,
PixelFormat::Rgb,
DType::U8,
Some(TensorMemory::Dma),
)?;
let mut g2d = G2DProcessor::new()?;
let src_dyn = src;
let mut dst_dyn = dst;
g2d.convert(
&src_dyn,
&mut dst_dyn,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
dst = {
let mut __t = dst_dyn.into_u8().unwrap();
__t.set_format(PixelFormat::Rgb)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
cpu_dst
.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
}
#[test]
#[cfg(target_os = "linux")]
#[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
fn test_g2d_bgra_no_resize() {
for src_fmt in [
PixelFormat::Rgba,
PixelFormat::Yuyv,
PixelFormat::Nv12,
PixelFormat::Bgra,
] {
test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
});
}
}
fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
let file = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/zidane.jpg"
))
.to_vec();
let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
let mut cpu_converter = CPUProcessor::new();
if g2d_in_fmt == PixelFormat::Nv12 {
let nv12_bytes = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../testdata/zidane.nv12"
));
src2.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(nv12_bytes);
} else {
cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
}
let mut g2d = G2DProcessor::new()?;
let mut bgra_dst = TensorDyn::image(
1280,
720,
PixelFormat::Bgra,
DType::U8,
Some(TensorMemory::Dma),
)?;
let src2_dyn = src2;
let mut bgra_dst_dyn = bgra_dst;
g2d.convert(
&src2_dyn,
&mut bgra_dst_dyn,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
bgra_dst = {
let mut __t = bgra_dst_dyn.into_u8().unwrap();
__t.set_format(PixelFormat::Bgra)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let src2 = {
let mut __t = src2_dyn.into_u8().unwrap();
__t.set_format(g2d_in_fmt)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let mut rgba_dst = TensorDyn::image(
1280,
720,
PixelFormat::Rgba,
DType::U8,
Some(TensorMemory::Dma),
)?;
let src2_dyn2 = src2;
let mut rgba_dst_dyn = rgba_dst;
g2d.convert(
&src2_dyn2,
&mut rgba_dst_dyn,
Rotation::None,
Flip::None,
Crop::no_crop(),
)?;
rgba_dst = {
let mut __t = rgba_dst_dyn.into_u8().unwrap();
__t.set_format(PixelFormat::Rgba)
.map_err(|e| crate::Error::Internal(e.to_string()))?;
TensorDyn::from(__t)
};
let bgra_cpu = TensorDyn::image(1280, 720, PixelFormat::Bgra, DType::U8, None)?;
bgra_cpu
.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
let rgba_cpu = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
rgba_cpu
.as_u8()
.unwrap()
.map()?
.as_mut_slice()
.copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
let bgra_buf = bgra_map.as_slice();
let rgba_buf = rgba_map.as_slice();
assert_eq!(bgra_buf.len(), rgba_buf.len());
for (i, (bc, rc)) in bgra_buf
.chunks_exact(4)
.zip(rgba_buf.chunks_exact(4))
.enumerate()
{
assert_eq!(
bc[0], rc[2],
"{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
);
assert_eq!(
bc[1], rc[1],
"{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
);
assert_eq!(
bc[2], rc[0],
"{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
);
assert_eq!(
bc[3], rc[3],
"{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
);
}
Ok(())
}
fn surface_for(
width: usize,
height: usize,
fmt: PixelFormat,
offset: Option<usize>,
row_stride: Option<usize>,
) -> Result<G2DSurface, crate::Error> {
use edgefirst_tensor::TensorMemory;
let mut t = Tensor::<u8>::image(width, height, fmt, Some(TensorMemory::Dma))?;
if let Some(o) = offset {
t.set_plane_offset(o);
}
if let Some(s) = row_stride {
t.set_row_stride_unchecked(s);
}
tensor_to_g2d_surface(&t)
}
#[test]
fn g2d_surface_single_plane_no_offset() {
if !is_dma_available() {
return;
}
let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
assert_ne!(s.planes[0], 0);
assert_eq!(s.stride, 640);
}
#[test]
fn g2d_surface_single_plane_with_offset() {
if !is_dma_available() {
return;
}
use edgefirst_tensor::TensorMemory;
let mut t =
Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
let s0 = tensor_to_g2d_surface(&t).unwrap();
t.set_plane_offset(4096);
let s1 = tensor_to_g2d_surface(&t).unwrap();
assert_eq!(s1.planes[0], s0.planes[0] + 4096);
}
#[test]
fn g2d_surface_single_plane_zero_offset() {
if !is_dma_available() {
return;
}
use edgefirst_tensor::TensorMemory;
let mut t =
Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
let s_none = tensor_to_g2d_surface(&t).unwrap();
t.set_plane_offset(0);
let s_zero = tensor_to_g2d_surface(&t).unwrap();
assert_eq!(s_none.planes[0], s_zero.planes[0]);
}
#[test]
fn g2d_surface_stride_rgba() {
if !is_dma_available() {
return;
}
let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
assert_eq!(s_default.stride, 640);
let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
assert_eq!(s_custom.stride, 704);
}
#[test]
fn g2d_surface_stride_rgb() {
if !is_dma_available() {
return;
}
let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
assert_eq!(s_default.stride, 640);
let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
assert_eq!(s_custom.stride, 660);
}
#[test]
fn g2d_surface_stride_grey() {
if !is_dma_available() {
return;
}
let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
Ok(s) => s,
Err(crate::Error::G2D(..)) => return,
Err(e) => panic!("unexpected error: {e:?}"),
};
assert_eq!(s.stride, 1024);
}
#[test]
fn g2d_surface_contiguous_nv12_offset() {
if !is_dma_available() {
return;
}
use edgefirst_tensor::TensorMemory;
let mut t =
Tensor::<u8>::image(640, 480, PixelFormat::Nv12, Some(TensorMemory::Dma)).unwrap();
let s0 = tensor_to_g2d_surface(&t).unwrap();
t.set_plane_offset(8192);
let s1 = tensor_to_g2d_surface(&t).unwrap();
assert_eq!(s1.planes[0], s0.planes[0] + 8192);
assert_eq!(s1.planes[1], s0.planes[1] + 8192);
}
#[test]
fn g2d_surface_contiguous_nv12_stride() {
if !is_dma_available() {
return;
}
let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
assert_eq!(s.stride, 640);
let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
assert_eq!(s_padded.stride, 1024);
}
#[test]
fn g2d_surface_multiplane_nv12_offset() {
if !is_dma_available() {
return;
}
use edgefirst_tensor::TensorMemory;
let mut luma =
Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Dma), Some("luma")).unwrap();
let mut chroma =
Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Dma), Some("chroma")).unwrap();
let luma_base = {
let dma = luma.as_dma().unwrap();
let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
phys.address()
};
let chroma_base = {
let dma = chroma.as_dma().unwrap();
let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
phys.address()
};
luma.set_plane_offset(4096);
chroma.set_plane_offset(2048);
let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
let s = tensor_to_g2d_surface(&combined).unwrap();
assert_eq!(s.planes[0], luma_base + 4096);
assert_eq!(s.planes[1], chroma_base + 2048);
}
}