use crate::Region;
use edgefirst_tensor::{PixelFormat, PixelLayout, TensorMemory};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum DstLowering {
ZeroCopy,
TextureMem,
TexturePbo,
}
pub(super) fn lower_dst(zero_copy_import: bool, dst_mem: TensorMemory) -> DstLowering {
match dst_mem {
TensorMemory::DmaBuf if zero_copy_import => DstLowering::ZeroCopy,
TensorMemory::Pbo => DstLowering::TexturePbo,
_ => DstLowering::TextureMem,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ConvertPlan {
SinglePass,
TwoPassPackedRgb,
TwoPassNvPlanar,
}
pub(super) fn plan_convert(
src_fmt: PixelFormat,
dst_fmt: PixelFormat,
lowering: DstLowering,
) -> ConvertPlan {
if lowering == DstLowering::ZeroCopy && dst_fmt == PixelFormat::Rgb {
return ConvertPlan::TwoPassPackedRgb;
}
if dst_fmt.layout() == PixelLayout::Planar && lowering != DstLowering::ZeroCopy {
return ConvertPlan::TwoPassNvPlanar;
}
if dst_fmt.layout() == PixelLayout::Planar
&& matches!(
src_fmt,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24
)
{
return ConvertPlan::TwoPassNvPlanar;
}
ConvertPlan::SinglePass
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Viewport {
pub(super) x: i32,
pub(super) y: i32,
pub(super) w: i32,
pub(super) h: i32,
}
#[inline]
pub(super) fn region_to_viewport_top_down(region: Region) -> Viewport {
Viewport {
x: region.x as i32,
y: region.y as i32,
w: region.width as i32,
h: region.height as i32,
}
}
#[allow(dead_code)]
pub(super) fn region_to_viewport(region: Region, parent_h: usize) -> Viewport {
let y_flipped = parent_h.saturating_sub(region.y.saturating_add(region.height));
Viewport {
x: region.x as i32,
y: y_flipped as i32,
w: region.width as i32,
h: region.height as i32,
}
}
#[allow(dead_code)]
pub(super) fn source_uv(region: Option<Region>, src_w: usize, src_h: usize) -> [f32; 4] {
match region {
Some(r) if src_w > 0 && src_h > 0 => [
r.x as f32 / src_w as f32,
r.y as f32 / src_h as f32,
r.width as f32 / src_w as f32,
r.height as f32 / src_h as f32,
],
_ => [0.0, 0.0, 1.0, 1.0],
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum BatchPath {
OneImport,
Chunked(usize),
PerTileImport,
}
#[allow(dead_code)]
pub(super) fn plan_batch(n_tiles: usize, rows_per_tile: usize, max_rows: usize) -> BatchPath {
if rows_per_tile == 0 || n_tiles == 0 {
return BatchPath::OneImport;
}
if rows_per_tile > max_rows {
return BatchPath::PerTileImport;
}
let tiles_per_chunk = max_rows / rows_per_tile;
if tiles_per_chunk >= n_tiles {
BatchPath::OneImport
} else {
BatchPath::Chunked(tiles_per_chunk)
}
}
fn axis_map(logical: usize, extent: u32) -> (f32, f32) {
if extent == 0 || logical == 0 || logical as u64 >= extent as u64 {
return (1.0, 1.0);
}
let scale = logical as f32 / extent as f32;
(scale, scale)
}
pub(super) fn scale_roi_to_import(
roi: super::RegionOfInterest,
logical: (usize, usize),
extent: Option<(u32, u32)>,
) -> super::RegionOfInterest {
let Some((pw, ph)) = extent else {
return roi;
};
let (sx, lx) = axis_map(logical.0, pw);
let (sy, ly) = axis_map(logical.1, ph);
super::RegionOfInterest {
left: (roi.left * sx).min(lx),
top: (roi.top * sy).min(ly),
right: (roi.right * sx).min(lx),
bottom: (roi.bottom * sy).min(ly),
}
}
pub(super) fn scale_uv_rect_to_import(
rect: [f32; 4],
logical: (usize, usize),
extent: Option<(u32, u32)>,
) -> [f32; 4] {
let Some((pw, ph)) = extent else {
return rect;
};
let (sx, lx) = axis_map(logical.0, pw);
let (sy, ly) = axis_map(logical.1, ph);
let left = (rect[0] * sx).min(lx);
let top = (rect[1] * sy).min(ly);
[
left,
top,
(rect[2] * sx).min(lx - left).max(0.0),
(rect[3] * sy).min(ly - top).max(0.0),
]
}
pub(super) fn sample_clamp_rect(logical: (usize, usize), extent: Option<(u32, u32)>) -> [f32; 4] {
fn axis(logical: usize, extent: Option<u32>) -> (f32, f32) {
if logical == 0 {
return (0.0, 1.0);
}
let texture = match extent {
Some(e) if e as u64 > logical as u64 => e as f32,
_ => logical as f32,
};
(0.5 / texture, (logical as f32 - 0.5) / texture)
}
let (u_min, u_max) = axis(logical.0, extent.map(|e| e.0));
let (v_min, v_max) = axis(logical.1, extent.map(|e| e.1));
[u_min, v_min, u_max, v_max]
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
fn region(x: usize, y: usize, w: usize, h: usize) -> Region {
Region::new(x, y, w, h)
}
#[test]
fn viewport_top_down_is_identity() {
let ys: Vec<i32> = (0..3)
.map(|n| region_to_viewport_top_down(region(0, n * 16, 64, 16)).y)
.collect();
assert_eq!(ys, vec![0, 16, 32]);
assert_eq!(
region_to_viewport_top_down(region(10, 4, 20, 8)),
Viewport {
x: 10,
y: 4,
w: 20,
h: 8
}
);
}
#[test]
fn viewport_whole_image_is_origin() {
let vp = region_to_viewport(region(0, 0, 64, 48), 48);
assert_eq!(
vp,
Viewport {
x: 0,
y: 0,
w: 64,
h: 48
}
);
}
#[test]
fn viewport_top_tile_lands_at_top_in_gl_coords() {
let vp = region_to_viewport(region(0, 0, 64, 16), 48);
assert_eq!(
vp,
Viewport {
x: 0,
y: 32,
w: 64,
h: 16
}
);
}
#[test]
fn viewport_bottom_tile_lands_at_origin() {
let vp = region_to_viewport(region(0, 32, 64, 16), 48);
assert_eq!(
vp,
Viewport {
x: 0,
y: 0,
w: 64,
h: 16
}
);
}
#[test]
fn viewport_batch_bands_tile_without_overlap() {
let h = 16;
let ys: Vec<i32> = (0..3)
.map(|n| region_to_viewport(region(0, n * h, 64, h), 3 * h).y)
.collect();
assert_eq!(ys, vec![32, 16, 0]);
}
#[test]
fn viewport_x_offset_preserved() {
let vp = region_to_viewport(region(10, 4, 20, 8), 32);
assert_eq!(
vp,
Viewport {
x: 10,
y: 32 - 12,
w: 20,
h: 8
}
);
}
#[test]
fn source_uv_none_is_identity() {
assert_eq!(source_uv(None, 640, 480), [0.0, 0.0, 1.0, 1.0]);
}
#[test]
fn source_uv_half_rect() {
assert_eq!(
source_uv(Some(region(320, 0, 320, 480)), 640, 480),
[0.5, 0.0, 0.5, 1.0]
);
}
#[test]
fn source_uv_zero_dims_falls_back_to_identity() {
assert_eq!(
source_uv(Some(region(0, 0, 1, 1)), 0, 0),
[0.0, 0.0, 1.0, 1.0]
);
}
#[test]
fn plan_batch_fits_one_import() {
assert_eq!(plan_batch(4, 64, 2048), BatchPath::OneImport);
}
#[test]
fn plan_batch_chunks_when_over_limit() {
assert_eq!(plan_batch(40, 64, 2048), BatchPath::Chunked(32));
}
#[test]
fn plan_batch_single_oversize_tile_is_per_tile() {
assert_eq!(plan_batch(8, 3072, 2048), BatchPath::PerTileImport);
}
#[test]
fn plan_batch_exact_fit_is_one_import() {
assert_eq!(plan_batch(32, 64, 2048), BatchPath::OneImport);
}
#[test]
fn plan_batch_degenerate_inputs() {
assert_eq!(plan_batch(0, 64, 2048), BatchPath::OneImport);
assert_eq!(plan_batch(4, 0, 2048), BatchPath::OneImport);
}
#[test]
fn plan_convert_full_table() {
use DstLowering::*;
use PixelFormat::*;
let nv = [Nv12, Nv16, Nv24];
let non_nv = [Rgba, Bgra, Grey, Yuyv];
for src in nv.iter().chain(&non_nv) {
assert_eq!(
plan_convert(*src, Rgb, ZeroCopy),
ConvertPlan::TwoPassPackedRgb,
"{src:?}->Rgb zero-copy"
);
}
for src in nv {
assert_eq!(
plan_convert(src, PlanarRgb, ZeroCopy),
ConvertPlan::TwoPassNvPlanar
);
assert_eq!(
plan_convert(src, PlanarRgba, ZeroCopy),
ConvertPlan::TwoPassNvPlanar
);
assert_eq!(plan_convert(src, Rgba, ZeroCopy), ConvertPlan::SinglePass);
}
for src in non_nv {
assert_eq!(
plan_convert(src, PlanarRgb, ZeroCopy),
ConvertPlan::SinglePass
);
assert_eq!(plan_convert(src, Rgba, ZeroCopy), ConvertPlan::SinglePass);
}
for lowering in [TextureMem, TexturePbo] {
for src in nv.iter().chain(&non_nv) {
for dst in [PlanarRgb, PlanarRgba] {
assert_eq!(
plan_convert(*src, dst, lowering),
ConvertPlan::TwoPassNvPlanar,
"{src:?}->{dst:?} via {lowering:?}"
);
}
for dst in [Rgba, Bgra, Rgb, Grey] {
assert_eq!(
plan_convert(*src, dst, lowering),
ConvertPlan::SinglePass,
"{src:?}->{dst:?} via {lowering:?}"
);
}
}
}
}
#[test]
fn lower_dst_full_table() {
use TensorMemory::*;
assert_eq!(lower_dst(true, DmaBuf), DstLowering::ZeroCopy);
assert_eq!(lower_dst(true, Pbo), DstLowering::TexturePbo);
assert_eq!(lower_dst(true, Mem), DstLowering::TextureMem);
assert_eq!(lower_dst(true, Shm), DstLowering::TextureMem);
assert_eq!(lower_dst(false, DmaBuf), DstLowering::TextureMem);
assert_eq!(lower_dst(false, Pbo), DstLowering::TexturePbo);
assert_eq!(lower_dst(false, Mem), DstLowering::TextureMem);
assert_eq!(lower_dst(false, Shm), DstLowering::TextureMem);
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod import_extent_tests {
use super::*;
fn roi() -> super::super::RegionOfInterest {
super::super::RegionOfInterest {
left: 0.0,
top: 1.0,
right: 1.0,
bottom: 0.0,
}
}
#[test]
fn a_narrowed_logical_image_scales_by_its_share_of_the_texture() {
let r = scale_roi_to_import(roi(), (128, 96), Some((128, 128)));
assert_eq!(r.left, 0.0);
assert_eq!(r.right, 1.0);
assert_eq!(r.top, 0.75);
assert_eq!(r.bottom, 0.0);
}
#[test]
fn both_axes_scale_and_an_interior_rectangle_is_unchanged_in_shape() {
let src = super::super::RegionOfInterest {
left: 0.25,
top: 0.75,
right: 0.5,
bottom: 0.25,
};
let r = scale_roi_to_import(src, (64, 100), Some((128, 200)));
assert_eq!((r.left, r.right), (0.125, 0.25));
assert_eq!((r.top, r.bottom), (0.375, 0.125));
}
#[test]
fn equal_extents_and_no_extent_leave_the_rectangle_alone() {
let r = scale_roi_to_import(roi(), (128, 96), Some((128, 96)));
assert_eq!((r.left, r.top, r.right, r.bottom), (0.0, 1.0, 1.0, 0.0));
let r = scale_roi_to_import(roi(), (128, 96), None);
assert_eq!((r.left, r.top, r.right, r.bottom), (0.0, 1.0, 1.0, 0.0));
}
#[test]
fn a_logical_image_larger_than_the_extent_is_not_magnified() {
let r = scale_roi_to_import(roi(), (256, 256), Some((128, 128)));
assert_eq!((r.left, r.top, r.right, r.bottom), (0.0, 1.0, 1.0, 0.0));
}
#[test]
fn a_degenerate_extent_leaves_the_rectangle_alone() {
let r = scale_roi_to_import(roi(), (128, 96), Some((0, 0)));
assert_eq!((r.left, r.top, r.right, r.bottom), (0.0, 1.0, 1.0, 0.0));
}
#[test]
fn a_uv_rect_scales_its_origin_and_trims_its_size() {
let r = scale_uv_rect_to_import([0.0, 0.0, 1.0, 1.0], (128, 96), Some((128, 128)));
assert_eq!([r[0], r[1]], [0.0, 0.0]);
assert_eq!(r[2], 1.0);
assert_eq!(r[3], 0.75);
}
#[test]
fn a_uv_rect_keeps_an_interior_crop_and_passes_no_extent_through() {
let r = scale_uv_rect_to_import([0.25, 0.25, 0.5, 0.5], (96, 96), Some((128, 128)));
assert_eq!([r[0], r[1]], [0.1875, 0.1875]);
assert_eq!([r[2], r[3]], [0.375, 0.375]);
let r = scale_uv_rect_to_import([0.25, 0.25, 0.5, 0.5], (96, 96), None);
assert_eq!(r, [0.25, 0.25, 0.5, 0.5]);
}
#[test]
fn a_narrowed_image_clamps_samples_half_a_texel_inside_its_edge() {
let r = sample_clamp_rect((128, 96), Some((128, 128)));
assert_eq!(r, [0.5 / 128.0, 0.5 / 128.0, 127.5 / 128.0, 95.5 / 128.0]);
}
#[test]
fn no_extent_clamps_to_the_textures_own_edge_texels() {
let r = sample_clamp_rect((128, 96), None);
assert_eq!(r, [0.5 / 128.0, 0.5 / 96.0, 127.5 / 128.0, 95.5 / 96.0]);
assert_eq!(sample_clamp_rect((128, 96), Some((128, 96))), r);
}
#[test]
fn an_extent_smaller_than_the_image_is_ignored() {
assert_eq!(
sample_clamp_rect((256, 256), Some((128, 128))),
sample_clamp_rect((256, 256), None)
);
}
#[test]
fn a_degenerate_logical_size_leaves_the_whole_texture_reachable() {
assert_eq!(
sample_clamp_rect((0, 0), Some((128, 128))),
[0.0, 0.0, 1.0, 1.0]
);
assert_eq!(
sample_clamp_rect((0, 96), None),
[0.0, 0.5 / 96.0, 1.0, 95.5 / 96.0]
);
}
#[test]
fn a_one_texel_image_clamps_to_its_single_texel_centre() {
assert_eq!(
sample_clamp_rect((1, 1), Some((128, 128))),
[0.5 / 128.0; 4]
);
assert_eq!(sample_clamp_rect((1, 1), None), [0.5; 4]);
}
}