use edgefirst_tensor::{
DType, PixelFormat, PixelLayout, Tensor, TensorMapTrait, TensorMemory, TensorTrait,
};
use edgefirst_tensor::{DetectBox, ProtoData, ProtoLayout, Segmentation};
use std::collections::BTreeSet;
use std::ffi::{c_char, c_void, CStr};
use std::time::Instant;
use super::EglDisplayKind;
use super::cache::{BufferImportKey, CacheKind, GlCacheStats, ImportCache};
use super::platform::{GlPlatform, Platform};
type PlatformDisplay = <Platform as GlPlatform>::Display;
type PlatformImport = <Platform as GlPlatform>::Import;
type PlatformHandle = <Platform as GlPlatform>::ImportHandle;
use super::resources::{Buffer, FrameBuffer, GlProgram, Texture, UploadSource};
use super::shaders::{
check_gl_error, generate_color_shader, generate_float_nhwc_packed_shader,
generate_float_rgba_shader, generate_instanced_segmentation_shader,
generate_nv_to_rgba_bilinear_int8_shader_2d, generate_nv_to_rgba_bilinear_shader_2d,
generate_nv_to_rgba_hw_bilinear_int8_shader_2d, generate_nv_to_rgba_hw_bilinear_shader_2d,
generate_nv_to_rgba_int8_shader_2d, generate_nv_to_rgba_shader_2d,
generate_nv_to_rgba_y2y_int8_shader, generate_nv_to_rgba_y2y_shader,
generate_packed_f32_nhwc_shader, generate_packed_rgba8_int8_shader_2d,
generate_packed_rgba8_shader_2d, generate_planar_rgb_f16_packed_shader,
generate_planar_rgb_int8_shader, generate_planar_rgb_int8_shader_2d,
generate_planar_rgb_shader, generate_planar_rgb_shader_2d, generate_proto_dequant_shader_int8,
generate_proto_repack_compute_shader, generate_proto_segmentation_shader,
generate_proto_segmentation_shader_f32, generate_proto_segmentation_shader_int8_bilinear,
generate_proto_segmentation_shader_int8_nearest, generate_segmentation_shader,
generate_texture_fragment_shader, generate_texture_fragment_shader_yuv,
generate_texture_int8_shader, generate_texture_int8_shader_yuv, generate_vertex_shader,
quant_mapped,
};
use super::{Int8InterpolationMode, RegionOfInterest, TransferBackend};
use crate::{Error, Flip, ImageProcessorTrait, ResolvedCrop, Rotation, DEFAULT_COLORS};
use edgefirst_tensor::TensorDyn;
mod float;
pub(super) use float::{classify_float_render, float_render_support, FloatRenderPath};
#[allow(unused_imports)]
pub(super) use float::packed_planar_layout;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum NvConvertPath {
ExternalSampler,
ShaderR8,
Cpu,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum NvPathPref {
Auto,
ForceSampler,
ForceShader,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DstTarget {
ZeroCopyImage,
Texture { readback: DstReadback },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DstReadback {
Mem,
Pbo(u32),
}
impl DstTarget {
fn feed(self) -> &'static str {
match self {
DstTarget::ZeroCopyImage => "zero_copy",
DstTarget::Texture {
readback: DstReadback::Mem,
} => "mapped_texture",
DstTarget::Texture {
readback: DstReadback::Pbo(_),
} => "pbo",
}
}
}
impl DstReadback {
fn pbo_id(self) -> Option<u32> {
match self {
DstReadback::Mem => None,
DstReadback::Pbo(id) => Some(id),
}
}
}
enum ProtoLayersData<'a> {
F32(ndarray::ArrayView3<'a, f32>),
F16(ndarray::ArrayView3<'a, half::f16>),
}
#[derive(Clone, Copy)]
struct NvUniformLocs {
img_size: i32,
tex_width: i32,
chroma_shift: i32,
chroma_lines: i32,
src_rect: i32,
luma_scale: i32,
luma_clamp: i32,
chroma_scale: i32,
chroma_clamp: i32,
src_extent: i32,
y_offset: i32,
y_scale: i32,
c_vr: i32,
c_ug: i32,
c_vg: i32,
c_ub: i32,
}
#[derive(Clone, Copy)]
pub(super) struct FloatQuadLocs {
pub(super) sampler: i32,
pub(super) src_rect_uv: i32,
pub(super) src_extent: i32,
pub(super) dst_rect_px: i32,
pub(super) pad_color: i32,
pub(super) dst_image_size: i32,
pub(super) norm_scale: i32,
pub(super) norm_offset: i32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct FloatNorm {
pub(super) scale: [f32; 4],
pub(super) offset: [f32; 4],
pub(super) pad: Option<[f32; 4]>,
}
impl FloatNorm {
pub(super) const IDENTITY: Self = Self {
scale: [1.0; 4],
offset: [0.0; 4],
pad: None,
};
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct IntQuant {
pub(super) scale: [f32; 3],
pub(super) offset: [f32; 3],
pub(super) zero: [f32; 3],
pub(super) lo: [f32; 3],
pub(super) hi: [f32; 3],
}
impl IntQuant {
pub(super) fn from_map(map: &crate::options::OutputMap) -> Self {
let b = map.bounds.unwrap_or([(0.0, 255.0); 4]);
Self {
scale: std::array::from_fn(|c| map.scale[c] * 255.0),
offset: std::array::from_fn(|c| map.offset[c]),
zero: std::array::from_fn(|c| map.zero_point[c]),
lo: std::array::from_fn(|c| b[c].0),
hi: std::array::from_fn(|c| b[c].1),
}
}
}
struct Int8Set {
texture: GlProgram,
texture_yuv: Option<GlProgram>,
planar: Option<GlProgram>,
planar_2d: GlProgram,
yuyv_2d: GlProgram,
yuyv_2d_locs: [i32; 8],
packed_rgba8_2d: GlProgram,
nv_r8: [GlProgram; 3],
nv_r8_uniforms: [NvUniformState; 3],
nv_y2y: Option<(GlProgram, NvUniformState)>,
texture_src_extent_loc: i32,
yuv_src_extent_loc: i32,
planar_src_extent_loc: i32,
}
impl Int8Set {
fn new(mapped: bool, y2y: bool) -> Result<Self, Error> {
let build = |src: &str| {
if mapped {
GlProgram::new(generate_vertex_shader(), &quant_mapped(src))
} else {
GlProgram::new(generate_vertex_shader(), src)
}
};
let external = |src: &str| Platform::EXTERNAL_OES.then(|| build(src)).transpose();
let texture = build(generate_texture_int8_shader())?;
let texture_yuv = external(generate_texture_int8_shader_yuv())?;
let planar = external(generate_planar_rgb_int8_shader())?;
let planar_2d = build(generate_planar_rgb_int8_shader_2d())?;
planar_2d.load_uniform_1i(c"tex", 0)?;
let yuyv_2d = build(super::shaders_common::YUYV_RGBA_2D_INT8_FRAGMENT)?;
let yuyv_2d_locs = yuyv_2d_locs(&yuyv_2d);
let packed_rgba8_2d = build(generate_packed_rgba8_int8_shader_2d())?;
packed_rgba8_2d.load_uniform_1i(c"tex", 1)?;
let nv_r8 = [
build(generate_nv_to_rgba_int8_shader_2d())?,
build(generate_nv_to_rgba_bilinear_int8_shader_2d())?,
build(generate_nv_to_rgba_hw_bilinear_int8_shader_2d())?,
];
let nv_r8_uniforms = nv_r8.each_ref().map(NvUniformState::resolve);
let nv_y2y = if y2y {
match build(generate_nv_to_rgba_y2y_int8_shader()) {
Ok(p) => {
let u = NvUniformState::resolve(&p);
Some((p, u))
}
Err(e) => {
log::info!("the int8 Y2Y NV program did not build: {e:?}");
None
}
}
} else {
None
};
let src_extent = |p: Option<&GlProgram>| {
p.map_or(-1, |p| {
unsafe { edgefirst_gl::gl::GetUniformLocation(p.id, c"src_extent".as_ptr()) }
})
};
Ok(Self {
texture_src_extent_loc: src_extent(Some(&texture)),
yuv_src_extent_loc: src_extent(texture_yuv.as_ref()),
planar_src_extent_loc: src_extent(planar.as_ref()),
texture,
texture_yuv,
planar,
planar_2d,
yuyv_2d,
yuyv_2d_locs,
packed_rgba8_2d,
nv_r8,
nv_r8_uniforms,
nv_y2y,
})
}
}
fn yuyv_2d_locs(p: &GlProgram) -> [i32; 8] {
unsafe {
edgefirst_gl::gl::UseProgram(p.id);
let tex = edgefirst_gl::gl::GetUniformLocation(p.id, c"tex".as_ptr());
edgefirst_gl::gl::Uniform1i(tex, 0);
[
c"src_size",
c"y_offset",
c"y_scale",
c"c_vr",
c"c_ug",
c"c_vg",
c"c_ub",
c"src_extent",
]
.map(|name| edgefirst_gl::gl::GetUniformLocation(p.id, name.as_ptr()))
}
}
struct NvUniformState {
locs: NvUniformLocs,
last_colorimetry: Option<(
edgefirst_tensor::ColorEncoding,
edgefirst_tensor::ColorRange,
)>,
}
impl NvUniformState {
fn resolve(program: &GlProgram) -> Self {
unsafe {
edgefirst_gl::gl::UseProgram(program.id);
let loc = |name: &std::ffi::CStr| {
edgefirst_gl::gl::GetUniformLocation(program.id, name.as_ptr())
};
edgefirst_gl::gl::Uniform1i(loc(c"src"), 0);
edgefirst_gl::gl::Uniform1i(loc(c"uv_tex"), 1);
NvUniformState {
locs: NvUniformLocs {
img_size: loc(c"img_size"),
tex_width: loc(c"tex_width"),
chroma_shift: loc(c"chroma_shift"),
chroma_lines: loc(c"chroma_lines"),
src_rect: loc(c"src_rect"),
luma_scale: loc(c"luma_scale"),
luma_clamp: loc(c"luma_clamp"),
chroma_scale: loc(c"chroma_scale"),
chroma_clamp: loc(c"chroma_clamp"),
src_extent: loc(c"src_extent"),
y_offset: loc(c"y_offset"),
y_scale: loc(c"y_scale"),
c_vr: loc(c"c_vr"),
c_ug: loc(c"c_ug"),
c_vg: loc(c"c_vg"),
c_ub: loc(c"c_ub"),
},
last_colorimetry: None,
}
}
}
}
#[derive(Clone, Copy, Debug)]
struct NvSampleRect {
left: usize,
top: usize,
right: usize,
bottom: usize,
resample: bool,
dst_pixels: usize,
}
impl NvSampleRect {
fn new(
src_crop: Option<crate::Rect>,
dst_crop: Option<crate::Rect>,
(src_w, src_h): (usize, usize),
(dst_w, dst_h): (usize, usize),
rotation: crate::Rotation,
) -> Self {
let src = src_crop.unwrap_or(crate::Rect::new(0, 0, src_w, src_h));
let dst = dst_crop.unwrap_or(crate::Rect::new(0, 0, dst_w, dst_h));
let (w, h) = match rotation {
crate::Rotation::Clockwise90 | crate::Rotation::CounterClockwise90 => {
(src.height, src.width)
}
_ => (src.width, src.height),
};
NvSampleRect {
left: src.left,
top: src.top,
right: src.left + src.width,
bottom: src.top + src.height,
resample: (w, h) != (dst.width, dst.height),
dst_pixels: dst.width * dst.height,
}
}
fn roi(&self, src_w: usize, src_h: usize) -> RegionOfInterest {
let crop = crate::Rect::new(
self.left,
self.top,
self.right - self.left,
self.bottom - self.top,
);
RegionOfInterest::from_crop(&crop, src_w, src_h)
}
}
fn nv_colorimetry(
colorimetry: Option<edgefirst_tensor::Colorimetry>,
height: Option<usize>,
) -> (
edgefirst_tensor::ColorEncoding,
edgefirst_tensor::ColorRange,
) {
let cm = crate::colorimetry::resolve_colorimetry(colorimetry, height);
(
cm.encoding
.unwrap_or(edgefirst_tensor::ColorEncoding::Bt709),
cm.range.unwrap_or(edgefirst_tensor::ColorRange::Limited),
)
}
const Y2Y_MAX_SOURCE_PER_OUTPUT_PIXEL: usize = 10;
fn y2y_pays_off(src_pixels: usize, dst_pixels: usize) -> bool {
src_pixels <= Y2Y_MAX_SOURCE_PER_OUTPUT_PIXEL * dst_pixels
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum NvSampling {
Single = 0,
Shader = 1,
Hardware = 2,
HardwareYuv = 3,
}
#[derive(Clone, Copy, Debug)]
struct NvSamplingInputs {
bilinear: bool,
hw_filter: bool,
imported: bool,
chroma_view: bool,
y2y: bool,
}
impl NvSampling {
fn choose(inputs: NvSamplingInputs) -> Self {
let NvSamplingInputs {
bilinear,
hw_filter,
imported,
chroma_view,
y2y,
} = inputs;
if !bilinear {
NvSampling::Single
} else if hw_filter && (!imported || chroma_view) {
NvSampling::Hardware
} else if hw_filter && y2y {
NvSampling::HardwareYuv
} else {
NvSampling::Shader
}
}
fn program(self, is_int8: bool) -> usize {
self as usize * 2 + usize::from(is_int8)
}
}
pub struct GLProcessorST {
camera_eglimage_texture: Texture,
camera_normal_texture: Texture,
render_texture: Texture,
segmentation_texture: Texture,
segmentation_program: GlProgram,
instanced_segmentation_program: GlProgram,
proto_texture: Texture,
proto_segmentation_program: GlProgram,
proto_segmentation_int8_nearest_program: GlProgram,
proto_segmentation_int8_bilinear_program: GlProgram,
proto_dequant_int8_program: GlProgram,
proto_segmentation_f32_program: GlProgram,
color_program: GlProgram,
box_line_width: f32,
cached_opacity: f32,
proto_tex_dims: (usize, usize, usize, u32),
has_float_linear: bool,
pub(super) has_bgra: bool,
pub(super) supports_f32_color: bool,
pub(super) supports_f16_color: bool,
int8_interpolation_mode: Int8InterpolationMode,
proto_dequant_texture: Texture,
proto_dequant_tex_dims: (usize, usize, usize, u32),
proto_dequant_fbo: FrameBuffer,
proto_quad_locs: std::cell::Cell<(u32, i32, i32)>,
proto_repack_compute_program: Option<u32>,
proto_compute_locs: (i32, i32, i32),
proto_ssbo: u32,
proto_ssbo_size: usize,
vertex_buffer: Buffer,
texture_buffer: Buffer,
convert_fbo: FrameBuffer,
draw_fbo: FrameBuffer,
draw_render_texture: Texture,
src_egl_cache: ImportCache<PlatformImport>,
dst_egl_cache: ImportCache<PlatformImport>,
bgra_warned: bool,
readback_scratch: Vec<u8>,
#[cfg(test)]
pub(super) fail_dst_import: bool,
probe_dst_import_only: bool,
dst_import_warned: std::collections::HashSet<u64>,
pub(super) is_vivante: bool,
is_mali: bool,
is_virtual_gpu: bool,
is_angle: bool,
pub(super) is_adreno: bool,
use_renderbuffer: bool,
packed_rgb_intermediate_tex: Texture,
packed_rgb_fbo: FrameBuffer,
packed_rgb_intermediate_size: (usize, usize),
texture_program: GlProgram,
texture_program_yuv: Option<GlProgram>,
texture_int8_program: GlProgram,
texture_int8_program_yuv: Option<GlProgram>,
texture_program_planar: Option<GlProgram>,
texture_program_planar_int8: Option<GlProgram>,
yuyv_program_2d: GlProgram,
yuyv_int8_program_2d: GlProgram,
yuyv_2d_locs: [[i32; 8]; 2],
texture_src_extent_locs: [i32; 2],
external_src_extent_locs: [i32; 4],
packed_rgba8_program_2d: GlProgram,
packed_rgba8_int8_program_2d: GlProgram,
texture_program_planar_2d: GlProgram,
texture_program_planar_int8_2d: GlProgram,
float_f32_nhwc_program: GlProgram,
float_f16_nchw_program: GlProgram,
float_nhwc_pack_program: GlProgram,
float_rgba_program: GlProgram,
float_render_texture: Texture,
float_render_tex_dims: (usize, usize, u32),
nv_r8_programs: [GlProgram; 6],
nv_r8_uniforms: [NvUniformState; 6],
nv_uv_texture: Texture,
nv_uv_egl_cache: ImportCache<PlatformImport>,
nv_y2y_programs: Option<([GlProgram; 2], [NvUniformState; 2])>,
nv_uv_import_refused: bool,
nv_r8_texture: Texture,
nv_r8_egl_cache: ImportCache<PlatformImport>,
pub(super) last_nv_convert_path: NvConvertPath,
pub(super) last_nv_sampling: Option<NvSampling>,
pub(super) convert_stats: super::cache::ConvertStats,
nv_import_warned: std::collections::HashSet<u64>,
pub(super) owned_pbos: std::sync::Arc<OwnedPbos>,
pub(super) float_quad_locs: std::collections::HashMap<u32, FloatQuadLocs>,
pub(super) float_norm: FloatNorm,
int8_programs: Vec<(u32, [i32; 6])>,
int_quant: Option<IntQuant>,
int8_mapped: bool,
int8_alt: Option<Int8Set>,
int_pad: Option<[u8; 4]>,
int_pad_output: bool,
pub(super) float_quad_pos_vbo: u32,
pub(super) float_quad_uv_vbo: u32,
nv_path_pref: NvPathPref,
colorimetry_mode: crate::ColorimetryMode,
colorimetry_env_pinned: bool,
interpolation: crate::Interpolation,
nv_hw_filter: bool,
pub(super) defer_finish: bool,
pub(super) pending_flush: bool,
pub(super) pending_submit: bool,
pub(super) gl_context: PlatformDisplay,
}
impl Drop for GLProcessorST {
fn drop(&mut self) {
if self.pending_submit {
Platform::submit_recorded(&self.gl_context);
self.pending_submit = false;
}
unsafe {
{
if self.proto_ssbo != 0 {
edgefirst_gl::gl::DeleteBuffers(1, &self.proto_ssbo);
}
if self.float_quad_pos_vbo != 0 {
let ids = [self.float_quad_pos_vbo, self.float_quad_uv_vbo];
edgefirst_gl::gl::DeleteBuffers(2, ids.as_ptr());
}
if let Some(program) = self.proto_repack_compute_program {
edgefirst_gl::gl::DeleteProgram(program);
}
}
}
}
}
fn warn_slow_path_once(call_site: &str, failing_setup: &str, err: &crate::Error) {
use std::sync::Once;
static SLOW_PATH_WARNED: Once = Once::new();
let mut emitted = false;
SLOW_PATH_WARNED.call_once(|| {
log::warn!(
"{call_site}: GL DMA-BUF fast path unavailable, falling back to CPU \
readback (10–20× slower). Cause: {failing_setup} returned {err:?}. \
On Mali Valhall (i.MX 95) this is usually because the destination \
tensor's row pitch is not 64-byte aligned — see \
`ImageProcessor::create_image` for automatic alignment. \
Subsequent fallbacks will be logged at debug level."
);
emitted = true;
});
if !emitted {
log::debug!("{call_site}: GL DMA-BUF fast path unavailable ({failing_setup}: {err:?})");
}
}
unsafe fn tensor_i8_as_u8_mut(t: &mut Tensor<i8>) -> &mut Tensor<u8> {
unsafe { &mut *(t as *mut Tensor<i8> as *mut Tensor<u8>) }
}
unsafe fn tensor_i8_as_u8(t: &Tensor<i8>) -> &Tensor<u8> {
unsafe { &*(t as *const Tensor<i8> as *const Tensor<u8>) }
}
fn dyn_to_u8_src(src: &TensorDyn) -> crate::Result<(&Tensor<u8>, PixelFormat)> {
match src.dtype() {
DType::U8 => {
let t = src.as_typed::<u8>().expect("dtype checked");
let fmt = t.format().ok_or(Error::NotAnImage)?;
Ok((t, fmt))
}
DType::I8 => {
let t = src.as_typed::<i8>().expect("dtype checked");
let fmt = t.format().ok_or(Error::NotAnImage)?;
Ok((unsafe { tensor_i8_as_u8(t) }, fmt))
}
_ => Err(Error::UnsupportedFormat(format!(
"GL backend requires u8 or i8 source, got {:?}",
src.dtype()
))),
}
}
fn dyn_to_u8_dst(dst: &mut TensorDyn) -> crate::Result<(&mut Tensor<u8>, PixelFormat, bool)> {
match dst.dtype() {
DType::U8 => {
let t = dst.as_typed_mut::<u8>().expect("dtype checked");
let fmt = t.format().ok_or(Error::NotAnImage)?;
Ok((t, fmt, false))
}
DType::I8 => {
let t = dst.as_typed_mut::<i8>().expect("dtype checked");
let fmt = t.format().ok_or(Error::NotAnImage)?;
Ok((unsafe { tensor_i8_as_u8_mut(t) }, fmt, true))
}
_ => Err(Error::UnsupportedFormat(format!(
"GL backend requires u8 or i8 destination, got {:?}",
dst.dtype()
))),
}
}
fn check_draw_dst_placement(dst: &TensorDyn) -> crate::Result<()> {
let is_view = dst.view_origin().is_some();
let plane_offset = dst.plane_offset().unwrap_or(0);
if is_view || plane_offset != 0 {
return Err(Error::NotSupported(format!(
"GL mask draw places its frame at the destination surface's \
origin -- viewport 0,0 on an imported surface, buffer offset 0 \
in a readback -- so it cannot fill a sub-region destination \
(view={is_view}, plane_offset={plane_offset}); CPU fallback \
handles it"
)));
}
Ok(())
}
impl ImageProcessorTrait for GLProcessorST {
fn convert(
&mut self,
src: &TensorDyn,
dst: &mut TensorDyn,
options: Option<crate::ConvertOptions>,
) -> crate::Result<()> {
self.convert_inner(src, dst, &options.unwrap_or_default())?;
let submit = !self.defer_finish;
if Platform::record_completion(&self.gl_context, dst, submit).is_some() && !submit {
self.pending_submit = true;
}
Ok(())
}
fn convert_deferred(
&mut self,
src: &TensorDyn,
dst: &mut TensorDyn,
options: Option<crate::ConvertOptions>,
) -> Result<(), crate::Error> {
self.defer_finish = true;
let result = self.convert(src, dst, options);
self.defer_finish = false;
if result.is_ok() {
self.pending_flush = true;
} else if self.pending_submit {
Platform::submit_recorded(&self.gl_context);
self.pending_submit = false;
}
result
}
fn flush(&mut self) -> Result<(), crate::Error> {
let _span = tracing::trace_span!("image.flush.gl", pending = self.pending_flush).entered();
self.flush_pending();
Ok(())
}
fn draw_decoded_masks(
&mut self,
dst: &mut TensorDyn,
detect: &[DetectBox],
segmentation: &[Segmentation],
overlay: crate::MaskOverlay<'_>,
) -> Result<(), crate::Error> {
check_draw_dst_placement(dst)?;
let bg = overlay.background.map(|bg| dyn_to_u8_src(bg)).transpose()?;
{
let (dst_u8, dst_fmt, _is_int8) = dyn_to_u8_dst(dst)?;
self.draw_decoded_masks_impl(
dst_u8,
dst_fmt,
detect,
segmentation,
overlay.opacity,
bg,
overlay.color_mode,
)?;
}
Platform::record_completion(&self.gl_context, dst, true);
Ok(())
}
fn draw_proto_masks(
&mut self,
dst: &mut TensorDyn,
detect: &[DetectBox],
proto_data: &ProtoData,
overlay: crate::MaskOverlay<'_>,
) -> crate::Result<()> {
check_draw_dst_placement(dst)?;
let bg = overlay.background.map(|bg| dyn_to_u8_src(bg)).transpose()?;
{
let (dst_u8, dst_fmt, _is_int8) = dyn_to_u8_dst(dst)?;
self.draw_proto_masks_impl(
dst_u8,
dst_fmt,
detect,
proto_data,
overlay.opacity,
bg,
overlay.color_mode,
)?;
}
Platform::record_completion(&self.gl_context, dst, true);
Ok(())
}
fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> crate::Result<()> {
if colors.is_empty() {
return Ok(());
}
let colors_f32 = colors
.iter()
.map(|c| {
[
c[0] as f32 / 255.0,
c[1] as f32 / 255.0,
c[2] as f32 / 255.0,
c[3] as f32 / 255.0,
]
})
.take(20)
.collect::<Vec<[f32; 4]>>();
self.segmentation_program
.load_uniform_4fv(c"colors", &colors_f32)?;
self.instanced_segmentation_program
.load_uniform_4fv(c"colors", &colors_f32)?;
self.proto_segmentation_program
.load_uniform_4fv(c"colors", &colors_f32)?;
self.proto_segmentation_int8_nearest_program
.load_uniform_4fv(c"colors", &colors_f32)?;
self.proto_segmentation_int8_bilinear_program
.load_uniform_4fv(c"colors", &colors_f32)?;
self.proto_segmentation_f32_program
.load_uniform_4fv(c"colors", &colors_f32)?;
self.color_program
.load_uniform_4fv(c"colors", &colors_f32)?;
Ok(())
}
fn set_box_line_width(&mut self, width: u32) -> crate::Result<()> {
crate::check_box_line_width(width)?;
self.box_line_width = width as f32;
Ok(())
}
}
fn software_gl_override_enabled() -> bool {
std::env::var_os("EDGEFIRST_ALLOW_SOFTWARE_GL").is_some_and(|v| v == "1")
}
const DEFAULT_EGL_CACHE_CAPACITY: usize = 16;
fn resolve_egl_cache_capacity(config: Option<usize>, env: Option<&str>) -> usize {
config
.filter(|&c| c > 0)
.or_else(|| env.and_then(|v| v.parse::<usize>().ok()).filter(|&c| c > 0))
.unwrap_or(DEFAULT_EGL_CACHE_CAPACITY)
}
fn should_reject_software_gl(is_software_renderer: bool, override_enabled: bool) -> bool {
is_software_renderer && !override_enabled
}
fn parse_nv_path_pref() -> NvPathPref {
match std::env::var("EDGEFIRST_NV_CONVERT_PATH") {
Ok(v) => match v.to_ascii_lowercase().as_str() {
"sampler" | "external" | "a" => NvPathPref::ForceSampler,
"shader" | "r8" | "b" => NvPathPref::ForceShader,
"auto" | "" => NvPathPref::Auto,
other => {
log::warn!(
"EDGEFIRST_NV_CONVERT_PATH={other:?} not recognised \
(expected sampler|shader|auto), using auto"
);
NvPathPref::Auto
}
},
Err(_) => NvPathPref::Auto,
}
}
fn parse_colorimetry_env() -> Option<crate::ColorimetryMode> {
match std::env::var("EDGEFIRST_COLORIMETRY") {
Ok(v) => match v.to_ascii_lowercase().as_str() {
"exact" => Some(crate::ColorimetryMode::Exact),
"fast" => Some(crate::ColorimetryMode::Fast),
"" => None,
other => {
log::warn!(
"EDGEFIRST_COLORIMETRY={other:?} not recognised \
(expected fast|exact), ignoring"
);
None
}
},
Err(_) => None,
}
}
fn apply_forced_transfer(backend: &mut TransferBackend) {
let Ok(val) = std::env::var("EDGEFIRST_FORCE_TRANSFER") else {
return;
};
let forced = match val.to_ascii_lowercase().as_str() {
"dmabuf" | "dma" => Some(TransferBackend::DmaBuf),
"pbo" => Some(TransferBackend::Pbo),
"sync" => Some(TransferBackend::Sync),
other => {
log::warn!(
"EDGEFIRST_FORCE_TRANSFER={other:?} not recognised \
(expected dmabuf|pbo|sync), ignoring"
);
None
}
};
if let Some(forced) = forced {
log::info!(
"EDGEFIRST_FORCE_TRANSFER override: {:?} → {forced:?}",
backend
);
*backend = forced;
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct RendererTraits {
vivante: bool,
mali: bool,
software: bool,
virtual_gpu: bool,
angle: bool,
adreno: bool,
}
fn requires_full_serialization(t: RendererTraits) -> bool {
t.vivante || t.virtual_gpu || t.angle || t.adreno
}
struct GlSupport {
has_float_linear: bool,
has_yuv_target: bool,
has_bgra: bool,
renderer: RendererTraits,
supports_f32_color: bool,
supports_f16_color: bool,
}
unsafe fn direct_read_supported(format: u32) -> bool {
if format == edgefirst_gl::gl::RGBA {
return true;
}
let mut impl_fmt = 0i32;
let mut impl_type = 0i32;
unsafe {
edgefirst_gl::gl::GetIntegerv(
edgefirst_gl::gl::IMPLEMENTATION_COLOR_READ_FORMAT,
&mut impl_fmt,
);
edgefirst_gl::gl::GetIntegerv(
edgefirst_gl::gl::IMPLEMENTATION_COLOR_READ_TYPE,
&mut impl_type,
);
}
impl_fmt as u32 == format && impl_type as u32 == edgefirst_gl::gl::UNSIGNED_BYTE
}
fn pack_channels(format: u32) -> usize {
match format {
edgefirst_gl::gl::RGB => 3,
edgefirst_gl::gl::RED => 1,
_ => 4,
}
}
fn spread_rows(buf: &mut [u8], rows: usize, tight_row: usize, stride: usize) {
if stride <= tight_row || rows < 2 || tight_row == 0 {
return;
}
for y in (1..rows).rev() {
buf.copy_within(y * tight_row..y * tight_row + tight_row, y * stride);
}
}
fn padded_readback_bytes(rows: usize, tight_row: usize, stride: usize) -> Option<usize> {
if stride <= tight_row || rows == 0 {
return None;
}
Some((rows - 1) * stride + tight_row)
}
fn pack_row_length(tight_row: usize, stride: usize, pixel_bytes: usize) -> Option<i32> {
if stride <= tight_row || pixel_bytes == 0 || !stride.is_multiple_of(pixel_bytes) {
return None;
}
i32::try_from(stride / pixel_bytes).ok()
}
fn unpack_row_length(
stride: Option<usize>,
tight_px: usize,
pixel_bytes: usize,
what: &str,
) -> Result<i32, Error> {
let Some(stride) = stride else {
return Ok(0);
};
if pixel_bytes == 0 || !stride.is_multiple_of(pixel_bytes) {
return Err(Error::NotSupported(format!(
"source row pitch {stride} B is not a whole number of \
{what} pixels ({pixel_bytes} B/px); upload would \
shear rows"
)));
}
let px = stride / pixel_bytes;
if px == tight_px {
return Ok(0);
}
i32::try_from(px).map_err(|_| {
Error::NotSupported(format!(
"source row pitch {stride} B is {px} {what} pixels, past what \
GL_UNPACK_ROW_LENGTH can express"
))
})
}
#[derive(Default)]
pub(crate) struct OwnedPbos(std::sync::Mutex<Vec<OwnedPbo>>);
struct OwnedPbo {
vtable: usize,
ops: std::sync::Weak<dyn edgefirst_tensor::PboOps>,
}
impl std::fmt::Debug for OwnedPbos {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let owned = self.0.lock().unwrap_or_else(|e| e.into_inner());
f.debug_tuple("OwnedPbos").field(&owned.len()).finish()
}
}
impl OwnedPbos {
pub(crate) fn insert(
&self,
vtable: Option<*const std::ffi::c_void>,
ops: &std::sync::Arc<dyn edgefirst_tensor::PboOps>,
) {
let Some(vtable) = vtable else { return };
let mut owned = self.0.lock().unwrap_or_else(|e| e.into_inner());
owned.retain(|p| p.ops.strong_count() > 0);
owned.push(OwnedPbo {
vtable: vtable as usize,
ops: std::sync::Arc::downgrade(ops),
});
}
fn contains(&self, vtable: Option<*const std::ffi::c_void>) -> bool {
let Some(vtable) = vtable else { return false };
let owned = self.0.lock().unwrap_or_else(|e| e.into_inner());
owned
.iter()
.any(|p| p.vtable == vtable as usize && p.ops.strong_count() > 0)
}
}
enum SrcPixels<'a> {
Pbo { buffer_id: u32, offset: usize },
Mapped(edgefirst_tensor::HostView<'a, u8>),
}
impl SrcPixels<'_> {
fn open(src: &Tensor<u8>, owned: &OwnedPbos) -> Result<Self, Error> {
match src.pbo_id() {
Some(buffer_id) if owned.contains(src.pbo_vtable_ptr()) => {
if src.pbo_is_mapped() == Some(true) {
return Err(Error::OpenGl(
"Cannot convert from a mapped PBO tensor".to_string(),
));
}
Ok(Self::Pbo {
buffer_id,
offset: src.plane_offset().unwrap_or(0),
})
}
_ => Ok(Self::Mapped(src.map_read()?)),
}
}
fn source(&self) -> UploadSource<'_> {
match self {
Self::Pbo { buffer_id, offset } => UploadSource::Pbo {
buffer_id: *buffer_id,
offset: *offset,
},
Self::Mapped(map) => UploadSource::Bytes(map.as_slice()),
}
}
fn record(&self, stats: &mut super::cache::ConvertStats, span: &tracing::Span) {
match self {
Self::Pbo { .. } => {
stats.src_pbo_uploads += 1;
span.record("src_feed", "pbo");
}
Self::Mapped(_) => {
stats.src_uploads += 1;
span.record("src_feed", "upload");
}
}
}
}
struct PboReadbackLayout {
stride: usize,
tight_row: usize,
needed: usize,
row_length: Option<i32>,
}
fn plan_pbo_readback(
stride: usize,
is_view: bool,
plane_offset: usize,
capacity: usize,
width: usize,
rows: usize,
pixel_bytes: usize,
) -> crate::Result<PboReadbackLayout> {
let tight_row = width * pixel_bytes;
if stride < tight_row {
return Err(crate::Error::OpenGl(format!(
"destination row pitch is {stride} B but one {width}-pixel row \
of this readback format is {tight_row} B"
)));
}
if is_view || plane_offset != 0 {
return Err(crate::Error::NotSupported(format!(
"GL PBO readback writes at buffer offset 0, so it cannot fill a \
sub-region destination (view={is_view}, plane_offset={plane_offset}); \
CPU fallback handles it"
)));
}
let needed = padded_readback_bytes(rows, tight_row, stride).unwrap_or(rows * tight_row);
if capacity < needed {
return Err(crate::Error::OpenGl(format!(
"PBO destination is {capacity} B but the readback needs {needed} B \
at a {stride} B pitch"
)));
}
Ok(PboReadbackLayout {
stride,
tight_row,
needed,
row_length: pack_row_length(tight_row, stride, pixel_bytes),
})
}
unsafe fn with_mapped_pbo(
buffer_id: u32,
len: usize,
access: u32,
what: &str,
f: impl FnOnce(&mut [u8]),
) -> crate::Result<()> {
let ok = unsafe {
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
let ptr = edgefirst_gl::gl::MapBufferRange(
edgefirst_gl::gl::PIXEL_PACK_BUFFER,
0,
len as isize,
access,
);
if ptr.is_null() {
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
return Err(crate::Error::OpenGl(format!(
"glMapBufferRange returned null for the {what}"
)));
}
f(std::slice::from_raw_parts_mut(ptr as *mut u8, len));
let ok = edgefirst_gl::gl::UnmapBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER);
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
ok
};
if ok == edgefirst_gl::gl::FALSE {
return Err(crate::Error::OpenGl(format!(
"PBO data was corrupted during mapping ({what})"
)));
}
Ok(())
}
unsafe fn read_pixels_rgba_scratch(w: usize, h: usize, scratch: &mut Vec<u8>) {
if scratch.len() < w * h * 4 {
scratch.resize(w * h * 4, 0);
}
unsafe {
edgefirst_gl::gl::ReadPixels(
0,
0,
w as i32,
h as i32,
edgefirst_gl::gl::RGBA,
edgefirst_gl::gl::UNSIGNED_BYTE,
scratch.as_mut_ptr() as *mut c_void,
);
}
}
unsafe fn read_pixels_into(
w: usize,
h: usize,
format: u32,
scratch: &mut Vec<u8>,
out: &mut [u8],
stride: usize,
) {
let channels = pack_channels(format);
let tight_row = w * channels;
unsafe {
if direct_read_supported(format) {
if stride == tight_row {
edgefirst_gl::gl::ReadPixels(
0,
0,
w as i32,
h as i32,
format,
edgefirst_gl::gl::UNSIGNED_BYTE,
out.as_mut_ptr() as *mut c_void,
);
return;
}
if let Some(px) = pack_row_length(tight_row, stride, channels) {
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::PACK_ROW_LENGTH, px);
edgefirst_gl::gl::ReadPixels(
0,
0,
w as i32,
h as i32,
format,
edgefirst_gl::gl::UNSIGNED_BYTE,
out.as_mut_ptr() as *mut c_void,
);
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::PACK_ROW_LENGTH, 0);
return;
}
if scratch.len() < h * tight_row {
scratch.resize(h * tight_row, 0);
}
edgefirst_gl::gl::ReadPixels(
0,
0,
w as i32,
h as i32,
format,
edgefirst_gl::gl::UNSIGNED_BYTE,
scratch.as_mut_ptr() as *mut c_void,
);
for y in 0..h {
out[y * stride..y * stride + tight_row]
.copy_from_slice(&scratch[y * tight_row..y * tight_row + tight_row]);
}
return;
}
read_pixels_rgba_scratch(w, h, scratch);
let rgba = scratch.as_chunks::<4>().0;
for y in 0..h {
for (px, dst_px) in rgba[y * w..]
.iter()
.take(w)
.zip(out[y * stride..y * stride + tight_row].chunks_exact_mut(channels))
{
dst_px.copy_from_slice(&px[..channels]);
}
}
}
}
fn classify_renderer(renderer: &str) -> RendererTraits {
let lower = renderer.to_ascii_lowercase();
RendererTraits {
vivante: lower.contains("vivante") || lower.contains("gc7000") || lower.contains("galcore"),
mali: lower.contains("mali"),
software: lower.contains("llvmpipe")
|| lower.contains("softpipe")
|| lower.contains("swrast")
|| lower.contains("software rasterizer")
|| lower.contains("basic render driver"),
virtual_gpu: lower.contains("paravirtual") || lower.contains("virtio"),
angle: lower.contains("angle"),
adreno: lower.contains("adreno"),
}
}
pub(super) const DMA_IMPORT_OFFSET_ALIGN: usize = 64;
fn unaligned_import_offset(plane_offset: usize) -> bool {
!plane_offset.is_multiple_of(DMA_IMPORT_OFFSET_ALIGN)
}
fn rejects_unaligned_import_offset(misplaces: bool, plane_offset: usize) -> bool {
misplaces && unaligned_import_offset(plane_offset)
}
fn source_import_plane0_offset(img: &Tensor<u8>, img_fmt: PixelFormat) -> usize {
#[cfg(target_os = "linux")]
{
super::dma_import::resolved_source_plane0_offset(img, img_fmt)
}
#[cfg(not(target_os = "linux"))]
{
let _ = img_fmt;
img.plane_offset().unwrap_or(0)
}
}
fn view_collapsed_dst_base(
view_origin: Option<edgefirst_tensor::ViewOrigin>,
plane_offset: Option<usize>,
) -> usize {
if view_origin.is_some() {
0
} else {
plane_offset.unwrap_or(0)
}
}
fn chroma_in_separate_buffer(img: &Tensor<u8>) -> bool {
img.chroma()
.is_some_and(|c| c.buffer_identity().id() != img.buffer_identity().id())
}
fn nv12_plane1_offset(plane0_offset: usize, pitch: usize, height: usize) -> usize {
plane0_offset.saturating_add(pitch.saturating_mul(height))
}
impl GLProcessorST {
pub(super) fn flush_pending(&mut self) {
if self.pending_flush {
unsafe { float::finish_via_fence() };
self.pending_flush = false;
}
if self.pending_submit {
Platform::submit_recorded(&self.gl_context);
self.pending_submit = false;
}
}
pub fn new(
kind: Option<EglDisplayKind>,
capacity: Option<usize>,
) -> Result<GLProcessorST, crate::Error> {
let gl_context = Platform::init_display(kind)?;
Platform::load_gl_once(&gl_context);
let GlSupport {
has_float_linear,
has_yuv_target,
has_bgra,
renderer:
RendererTraits {
vivante: is_vivante,
mali: is_mali,
software: is_software_renderer,
virtual_gpu: is_virtual_gpu,
angle: is_angle,
adreno: is_adreno,
},
supports_f32_color,
supports_f16_color,
} = Self::gl_check_support()?;
if should_reject_software_gl(is_software_renderer, software_gl_override_enabled()) {
return Err(crate::Error::NotSupported(
"software OpenGL renderer detected (llvmpipe/softpipe/swrast); \
GL backend disabled — check EGL ICD configuration if a \
hardware GPU is expected (set EDGEFIRST_ALLOW_SOFTWARE_GL=1 \
to override, e.g. for coverage on Mesa llvmpipe)"
.into(),
));
}
if is_software_renderer {
log::warn!(
"software OpenGL renderer in use (EDGEFIRST_ALLOW_SOFTWARE_GL=1); \
slower than the CPU backend — intended for CI coverage only"
);
}
unsafe {
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::PACK_ALIGNMENT, 1);
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::UNPACK_ALIGNMENT, 1);
}
let texture_program_planar = if Platform::EXTERNAL_OES {
Some(GlProgram::new(
generate_vertex_shader(),
generate_planar_rgb_shader(),
)?)
} else {
None
};
let texture_program =
GlProgram::new(generate_vertex_shader(), generate_texture_fragment_shader())?;
let texture_program_yuv = if Platform::EXTERNAL_OES {
Some(GlProgram::new(
generate_vertex_shader(),
generate_texture_fragment_shader_yuv(),
)?)
} else {
None
};
let int8 = Int8Set::new(false, has_yuv_target)?;
let texture_src_extent_locs = [
unsafe {
edgefirst_gl::gl::GetUniformLocation(texture_program.id, c"src_extent".as_ptr())
},
int8.texture_src_extent_loc,
];
let yuyv_program_2d = GlProgram::new(
generate_vertex_shader(),
super::shaders_common::YUYV_RGBA_2D_FRAGMENT,
)?;
let yuyv_2d_locs = [yuyv_2d_locs(&yuyv_program_2d), int8.yuyv_2d_locs];
let segmentation_program =
GlProgram::new(generate_vertex_shader(), generate_segmentation_shader())?;
segmentation_program.load_uniform_4fv(c"colors", &DEFAULT_COLORS)?;
let instanced_segmentation_program = GlProgram::new(
generate_vertex_shader(),
generate_instanced_segmentation_shader(),
)?;
instanced_segmentation_program.load_uniform_4fv(c"colors", &DEFAULT_COLORS)?;
let proto_segmentation_program = GlProgram::new(
generate_vertex_shader(),
generate_proto_segmentation_shader(),
)?;
proto_segmentation_program.load_uniform_4fv(c"colors", &DEFAULT_COLORS)?;
let proto_segmentation_int8_nearest_program = GlProgram::new(
generate_vertex_shader(),
generate_proto_segmentation_shader_int8_nearest(),
)?;
proto_segmentation_int8_nearest_program.load_uniform_4fv(c"colors", &DEFAULT_COLORS)?;
let proto_segmentation_int8_bilinear_program = GlProgram::new(
generate_vertex_shader(),
generate_proto_segmentation_shader_int8_bilinear(),
)?;
proto_segmentation_int8_bilinear_program.load_uniform_4fv(c"colors", &DEFAULT_COLORS)?;
let proto_dequant_int8_program = GlProgram::new(
generate_vertex_shader(),
generate_proto_dequant_shader_int8(),
)?;
let proto_segmentation_f32_program = GlProgram::new(
generate_vertex_shader(),
generate_proto_segmentation_shader_f32(),
)?;
proto_segmentation_f32_program.load_uniform_4fv(c"colors", &DEFAULT_COLORS)?;
let color_program = GlProgram::new(generate_vertex_shader(), generate_color_shader())?;
color_program.load_uniform_4fv(c"colors", &DEFAULT_COLORS)?;
let [yuv_loc, planar_loc] = [
texture_program_yuv.as_ref(),
texture_program_planar.as_ref(),
]
.map(|p| {
p.map_or(-1, |p| unsafe {
edgefirst_gl::gl::GetUniformLocation(p.id, c"src_extent".as_ptr())
})
});
let external_src_extent_locs = [
yuv_loc,
int8.yuv_src_extent_loc,
planar_loc,
int8.planar_src_extent_loc,
];
let texture_program_planar_2d =
GlProgram::new(generate_vertex_shader(), generate_planar_rgb_shader_2d())?;
let packed_rgba8_program_2d =
GlProgram::new(generate_vertex_shader(), generate_packed_rgba8_shader_2d())?;
let float_f32_nhwc_program =
GlProgram::new(generate_vertex_shader(), generate_packed_f32_nhwc_shader())?;
let float_f16_nchw_program = GlProgram::new(
generate_vertex_shader(),
generate_planar_rgb_f16_packed_shader(),
)?;
let float_nhwc_pack_program = GlProgram::new(
generate_vertex_shader(),
generate_float_nhwc_packed_shader(),
)?;
let float_rgba_program =
GlProgram::new(generate_vertex_shader(), generate_float_rgba_shader())?;
let Int8Set {
texture: texture_int8_program,
texture_yuv: texture_int8_program_yuv,
planar: texture_program_planar_int8,
planar_2d: texture_program_planar_int8_2d,
yuyv_2d: yuyv_int8_program_2d,
packed_rgba8_2d: packed_rgba8_int8_program_2d,
nv_r8: [nv_int8, nv_bilinear_int8, nv_hw_bilinear_int8],
nv_r8_uniforms: [nv_int8_u, nv_bilinear_int8_u, nv_hw_bilinear_int8_u],
nv_y2y: y2y_int8,
..
} = int8;
let [nv, nv_bilinear, nv_hw_bilinear] = [
generate_nv_to_rgba_shader_2d(),
generate_nv_to_rgba_bilinear_shader_2d(),
generate_nv_to_rgba_hw_bilinear_shader_2d(),
]
.map(|src| GlProgram::new(generate_vertex_shader(), src));
let [nv, nv_bilinear, nv_hw_bilinear] = [nv?, nv_bilinear?, nv_hw_bilinear?];
let [nv_u, nv_bilinear_u, nv_hw_bilinear_u] =
[&nv, &nv_bilinear, &nv_hw_bilinear].map(NvUniformState::resolve);
let nv_r8_programs = [
nv,
nv_int8,
nv_bilinear,
nv_bilinear_int8,
nv_hw_bilinear,
nv_hw_bilinear_int8,
];
let nv_r8_uniforms = [
nv_u,
nv_int8_u,
nv_bilinear_u,
nv_bilinear_int8_u,
nv_hw_bilinear_u,
nv_hw_bilinear_int8_u,
];
let nv_y2y_programs = if has_yuv_target {
match (
GlProgram::new(generate_vertex_shader(), generate_nv_to_rgba_y2y_shader()),
y2y_int8,
) {
(Ok(u8_program), Some((int8_program, int8_uniforms))) => {
let u8_uniforms = NvUniformState::resolve(&u8_program);
Some(([u8_program, int8_program], [u8_uniforms, int8_uniforms]))
}
(a, _) => {
log::info!(
"GL_EXT_YUV_target present but the Y2Y NV programs did not build \
({:?}); bilinear NV resizes of imports filter in the shader",
a.err()
);
None
}
}
} else {
None
};
packed_rgba8_program_2d.load_uniform_1i(c"tex", 1)?;
texture_program_planar_2d.load_uniform_1i(c"tex", 0)?;
let camera_eglimage_texture = Texture::new();
let camera_normal_texture = Texture::new();
let render_texture = Texture::new();
let draw_render_texture = Texture::new();
let segmentation_texture = Texture::new();
let proto_texture = Texture::new();
let proto_dequant_texture = Texture::new();
let vertex_buffer = Buffer::new(0, 3, 100);
let texture_buffer = Buffer::new(1, 2, 100);
let egl_cache_capacity = resolve_egl_cache_capacity(
capacity,
std::env::var("EDGEFIRST_EGL_CACHE_CAPACITY")
.ok()
.as_deref(),
);
let nv_path_pref = parse_nv_path_pref();
if nv_path_pref != NvPathPref::Auto {
log::info!("EDGEFIRST_NV_CONVERT_PATH override: {nv_path_pref:?}");
}
let colorimetry_env = parse_colorimetry_env();
if let Some(mode) = colorimetry_env {
log::info!("EDGEFIRST_COLORIMETRY override: {mode:?}");
}
let mut converter = GLProcessorST {
gl_context,
texture_program,
texture_program_yuv,
texture_int8_program,
texture_int8_program_yuv,
texture_program_planar,
texture_program_planar_int8,
yuyv_program_2d,
yuyv_int8_program_2d,
yuyv_2d_locs,
texture_src_extent_locs,
external_src_extent_locs,
packed_rgba8_program_2d,
packed_rgba8_int8_program_2d,
texture_program_planar_2d,
texture_program_planar_int8_2d,
float_f32_nhwc_program,
float_f16_nchw_program,
float_nhwc_pack_program,
float_rgba_program,
float_render_texture: Texture::new(),
float_render_tex_dims: (0, 0, 0),
camera_eglimage_texture,
camera_normal_texture,
segmentation_texture,
proto_texture,
proto_segmentation_int8_nearest_program,
proto_segmentation_int8_bilinear_program,
proto_dequant_int8_program,
proto_segmentation_f32_program,
has_float_linear,
has_bgra,
supports_f32_color,
supports_f16_color,
int8_interpolation_mode: Int8InterpolationMode::Bilinear,
proto_dequant_texture,
proto_dequant_tex_dims: (0, 0, 0, 0),
proto_dequant_fbo: FrameBuffer::new(),
proto_quad_locs: std::cell::Cell::new((0, -1, -1)),
vertex_buffer,
texture_buffer,
convert_fbo: FrameBuffer::new(),
draw_fbo: FrameBuffer::new(),
draw_render_texture,
src_egl_cache: ImportCache::new(egl_cache_capacity),
dst_egl_cache: ImportCache::new(egl_cache_capacity),
bgra_warned: false,
readback_scratch: Vec::new(),
#[cfg(test)]
fail_dst_import: false,
probe_dst_import_only: false,
dst_import_warned: std::collections::HashSet::new(),
is_vivante,
is_mali,
is_virtual_gpu,
is_angle,
is_adreno,
use_renderbuffer: std::env::var("EDGEFIRST_OPENGL_RENDERSURFACE")
.map(|v| v == "1")
.unwrap_or(false),
packed_rgb_intermediate_tex: Texture::new(),
packed_rgb_fbo: FrameBuffer::new(),
packed_rgb_intermediate_size: (0, 0),
render_texture,
segmentation_program,
instanced_segmentation_program,
proto_segmentation_program,
color_program,
box_line_width: crate::DEFAULT_BOX_LINE_WIDTH as f32,
cached_opacity: f32::NAN, proto_tex_dims: (0, 0, 0, 0),
proto_repack_compute_program: None,
proto_compute_locs: (-1, -1, -1),
proto_ssbo: 0,
proto_ssbo_size: 0,
nv_r8_programs,
nv_r8_uniforms,
nv_r8_texture: Texture::new(),
nv_r8_egl_cache: ImportCache::new(egl_cache_capacity),
nv_uv_texture: Texture::new(),
nv_uv_egl_cache: ImportCache::new(egl_cache_capacity),
nv_uv_import_refused: false,
nv_y2y_programs,
last_nv_convert_path: NvConvertPath::None,
last_nv_sampling: None,
convert_stats: Default::default(),
nv_import_warned: std::collections::HashSet::new(),
owned_pbos: Default::default(),
float_quad_locs: std::collections::HashMap::new(),
float_norm: FloatNorm::IDENTITY,
int8_programs: Vec::new(),
int8_mapped: false,
int8_alt: None,
int_quant: None,
int_pad: None,
int_pad_output: false,
float_quad_pos_vbo: 0,
float_quad_uv_vbo: 0,
nv_path_pref,
colorimetry_mode: colorimetry_env.unwrap_or_default(),
colorimetry_env_pinned: colorimetry_env.is_some(),
interpolation: crate::Interpolation::default(),
nv_hw_filter: std::env::var("EDGEFIRST_GL_NO_NV_HW_FILTER").map_or(true, |v| v != "1"),
defer_finish: false,
pending_flush: false,
pending_submit: false,
};
check_gl_error(function!(), line!())?;
let compute_enabled = converter.gl_context.has_compute
&& std::env::var("EDGEFIRST_PROTO_COMPUTE")
.map(|v| v == "1")
.unwrap_or(false);
if compute_enabled {
match Self::compile_compute_program(generate_proto_repack_compute_shader()) {
Ok(program) => {
log::info!("Proto repack compute shader compiled successfully");
converter.proto_repack_compute_program = Some(program);
unsafe {
converter.proto_compute_locs = (
edgefirst_gl::gl::GetUniformLocation(program, c"width".as_ptr()),
edgefirst_gl::gl::GetUniformLocation(program, c"height".as_ptr()),
edgefirst_gl::gl::GetUniformLocation(program, c"num_protos".as_ptr()),
);
}
let mut ssbo = 0u32;
unsafe { edgefirst_gl::gl::GenBuffers(1, &mut ssbo) };
converter.proto_ssbo = ssbo;
}
Err(e) => {
log::warn!("Proto repack compute shader failed: {e}; using CPU fallback");
}
}
}
log::debug!(
"GLProcessorST: DMA destination attachment mode: {}",
if converter.use_renderbuffer {
"renderbuffer (EDGEFIRST_OPENGL_RENDERSURFACE=1)"
} else {
"texture (default)"
}
);
if converter.gl_context.transfer_backend.is_dma() && !converter.verify_dma_buf_roundtrip() {
log::info!("DMA-buf verification failed — falling back to PBO transfers");
tracing::event!(
tracing::Level::INFO,
from = "dmabuf",
to = "pbo",
"image.convert.gl.transfer_downgrade"
);
converter.gl_context.transfer_backend = TransferBackend::Pbo;
}
if converter.gl_context.transfer_backend == TransferBackend::Sync {
log::info!("Upgrading transfer backend from Sync to Pbo (GL context available)");
converter.gl_context.transfer_backend = TransferBackend::Pbo;
}
apply_forced_transfer(&mut converter.gl_context.transfer_backend);
log::debug!(
"GLConverter created (transfer={:?})",
converter.gl_context.transfer_backend,
);
Ok(converter)
}
fn verify_dma_buf_roundtrip(&mut self) -> bool {
let src = match Tensor::<u8>::image(
64,
64,
PixelFormat::Rgba,
Some(TensorMemory::DmaBuf),
edgefirst_tensor::CpuAccess::Write,
) {
Ok(img) => img,
Err(e) => {
log::info!("verify_dma_buf_roundtrip: failed to allocate DMA source: {e}");
return false;
}
};
{
let mut map = match src.map_write() {
Ok(m) => m,
Err(e) => {
log::info!("verify_dma_buf_roundtrip: failed to map DMA source: {e}");
return false;
}
};
for pixel in map.as_chunks_mut::<4>().0 {
pixel[0] = 255; pixel[1] = 0; pixel[2] = 0; pixel[3] = 255; }
}
let mut dst = match Tensor::<u8>::image(
64,
64,
PixelFormat::Rgba,
Some(TensorMemory::DmaBuf),
edgefirst_tensor::CpuAccess::Read,
) {
Ok(img) => img,
Err(e) => {
log::info!("verify_dma_buf_roundtrip: failed to allocate DMA destination: {e}");
return false;
}
};
self.probe_dst_import_only = true;
let outcome = self.convert_via_engine(
&mut dst,
PixelFormat::Rgba,
&src,
PixelFormat::Rgba,
false,
Rotation::None,
Flip::None,
ResolvedCrop::no_crop(),
);
self.probe_dst_import_only = false;
if let Err(e) = outcome {
log::info!("verify_dma_buf_roundtrip: convert failed: {e}");
return false;
}
let map = match dst.map_read() {
Ok(m) => m,
Err(e) => {
log::info!("verify_dma_buf_roundtrip: failed to map DMA destination: {e}");
return false;
}
};
let offset = (32 * 64 + 32) * 4;
if map.len() < offset + 4 {
log::info!("verify_dma_buf_roundtrip: destination buffer too small");
return false;
}
let r = map[offset];
let g = map[offset + 1];
let b = map[offset + 2];
let a = map[offset + 3];
let pass = r > 250 && g < 5 && b < 5 && a > 250;
if pass {
log::info!("verify_dma_buf_roundtrip: PASSED (center pixel RGBA={r},{g},{b},{a})");
} else {
log::info!(
"verify_dma_buf_roundtrip: FAILED (center pixel RGBA={r},{g},{b},{a}, \
expected ~255,0,0,255)"
);
}
pass
}
pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) {
self.int8_interpolation_mode = mode;
log::debug!("Int8 interpolation mode set to {:?}", mode);
}
pub fn set_colorimetry_mode(&mut self, mode: crate::ColorimetryMode) {
if self.colorimetry_env_pinned {
if mode != self.colorimetry_mode {
log::info!(
"ColorimetryMode::{mode:?} requested but EDGEFIRST_COLORIMETRY pins \
{:?} — keeping the env override",
self.colorimetry_mode
);
}
return;
}
self.colorimetry_mode = mode;
log::debug!("Colorimetry mode set to {:?}", mode);
}
pub fn set_interpolation(&mut self, mode: crate::Interpolation) {
self.interpolation = mode;
log::debug!("Interpolation set to {mode:?}");
}
#[cfg(test)]
pub(super) fn has_y2y(&self) -> bool {
self.nv_y2y_programs.is_some()
}
#[cfg(test)]
pub(super) fn set_nv_hw_filter(&mut self, enabled: bool) {
self.nv_hw_filter = enabled;
}
pub(super) fn source_filter(&self) -> u32 {
match self.interpolation.supported() {
crate::Interpolation::Nearest => edgefirst_gl::gl::NEAREST,
_ => edgefirst_gl::gl::LINEAR,
}
}
pub(crate) fn egl_cache_stats(&self) -> GlCacheStats {
GlCacheStats {
src: self.src_egl_cache.stats(),
dst: self.dst_egl_cache.stats(),
nv_r8: self.nv_r8_egl_cache.stats(),
nv_uv: self.nv_uv_egl_cache.stats(),
}
}
fn usable_memory(
&self,
memory: TensorMemory,
pbo_vtable: Option<*const std::ffi::c_void>,
) -> TensorMemory {
match memory {
TensorMemory::Pbo if !self.owned_pbos.contains(pbo_vtable) => TensorMemory::Mem,
other => other,
}
}
pub(crate) fn convert_stats(&self) -> super::cache::ConvertStats {
self.convert_stats
}
fn convert_inner(
&mut self,
src: &TensorDyn,
dst: &mut TensorDyn,
options: &crate::ConvertOptions,
) -> crate::Result<()> {
let processor_mode = self.interpolation;
if let Some(mode) = options.get_interpolation() {
self.interpolation = mode;
}
let result = self.convert_resampling(src, dst, options);
self.interpolation = processor_mode;
result
}
fn convert_resampling(
&mut self,
src: &TensorDyn,
dst: &mut TensorDyn,
options: &crate::ConvertOptions,
) -> crate::Result<()> {
let (rotation, flip, crop) = (
options.get_rotation(),
options.get_flip(),
options.get_crop(),
);
if dst.view_origin().is_some()
&& !(self.gl_context.transfer_backend.is_zero_copy()
&& dst.memory() == TensorMemory::DmaBuf
&& matches!(
dst.dtype(),
edgefirst_tensor::DType::U8 | edgefirst_tensor::DType::I8
))
{
return Err(Error::NotSupported(
"GL view()/batch() destination is supported only for u8/i8 DMA buffers; \
CPU fallback handles other cases"
.into(),
));
}
let crop = crop.resolve(
src.width().unwrap_or(0),
src.height().unwrap_or(0),
dst.width().unwrap_or(0),
dst.height().unwrap_or(0),
options.get_placement(),
)?;
let mapping = crate::options::output_map_for(options, dst, crop.dst_rect.is_some())?;
let pad_output = mapping.as_ref().and_then(|(_, p)| *p);
let mut crop = crop;
let mut pad_output_exact = false;
self.float_norm = FloatNorm::IDENTITY;
self.int_pad = None;
self.int_pad_output = false;
let mut int_mapped = false;
if let Some((map, pad_output)) = &mapping {
let pad = crop.dst_color.map(|p| map.pad_values(p, *pad_output));
if map.bounds.is_some() {
if dst.format() == Some(PixelFormat::Bgra) {
return Err(Error::NotSupported(
"GL integer output mapping of Bgra; CPU fallback".into(),
));
}
self.use_int8_set(true)?;
self.load_int_quant(IntQuant::from_map(map))?;
self.int_pad = pad.map(|v| v.map(|x| x as i32 as u8));
int_mapped = true;
if let (Some(p), Some([_, _, _, a])) = (pad, crop.dst_color) {
let samples = [
map.sample_for(0, p[0]),
map.sample_for(1, p[1]),
map.sample_for(2, p[2]),
];
if let [Some(sr), Some(sg), Some(sb)] = samples {
crop.dst_color = Some([sr, sg, sb, a]);
pad_output_exact = true;
}
}
} else {
self.float_norm = FloatNorm {
scale: map.scale.map(|s| s * 255.0),
offset: map.offset,
pad,
};
}
}
if !int_mapped && dst.dtype() == edgefirst_tensor::DType::I8 {
self.use_int8_set(false)?;
}
let dst_dtype = dst.dtype();
if matches!(
dst_dtype,
edgefirst_tensor::DType::F16 | edgefirst_tensor::DType::F32
) {
let src_fmt = src.format().ok_or(Error::NotAnImage)?;
let dst_fmt = dst.format().ok_or(Error::NotAnImage)?;
let support = float_render_support(
self.is_vivante,
self.float_import_refused(),
self.supports_f32_color,
self.supports_f16_color,
);
let nv_src = matches!(
src_fmt,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24
);
let path = classify_float_render(
if nv_src { PixelFormat::Rgba } else { src_fmt },
dst_fmt,
dst_dtype,
self.usable_memory(dst.memory(), dst.pbo_vtable_ptr()),
support,
<Platform as GlPlatform>::ZERO_COPY_FLOAT,
);
if matches!(
path,
FloatRenderPath::ZeroCopyF16Nchw
| FloatRenderPath::ZeroCopyF32Nchw
| FloatRenderPath::ZeroCopyFloatNhwc
| FloatRenderPath::ZeroCopyFloatRgba
) {
let places = match dst_dtype {
edgefirst_tensor::DType::F16 => Platform::dst_import_places(
dst.as_typed::<half::f16>().expect("dtype checked"),
),
edgefirst_tensor::DType::F32 => {
Platform::dst_import_places(dst.as_typed::<f32>().expect("dtype checked"))
}
_ => unreachable!("dst_dtype is F16 or F32 in this branch"),
};
if !places {
return Err(Error::NotSupported(format!(
"GL float destination at plane offset {} cannot be placed by \
this platform/driver -- either no view origin and an import \
that binds the whole buffer from its origin, or a driver whose \
import rejects the offset -- and there is no mapped-texture \
readback for a float DMA destination to lower to; CPU fallback \
handles it",
dst.plane_offset().unwrap_or(0)
)));
}
}
match path {
FloatRenderPath::ZeroCopyF16Nchw
| FloatRenderPath::ZeroCopyF32Nchw
| FloatRenderPath::ZeroCopyFloatNhwc
| FloatRenderPath::ZeroCopyFloatRgba => {
let _span = Self::enter_image_convert_gl_span(
src_fmt,
dst_fmt,
false,
src.memory(),
dst.memory(),
);
let convert_span = tracing::Span::current();
return if nv_src {
self.convert_nv_to_float_two_pass(
src,
dst,
path,
rotation,
flip,
crop,
&convert_span,
)
} else {
self.convert_float_to_zero_copy(
src,
dst,
path,
rotation,
flip,
crop,
&convert_span,
)
};
}
FloatRenderPath::PboF16Nchw
| FloatRenderPath::PboF32Nhwc
| FloatRenderPath::PboF32Nchw
| FloatRenderPath::PboF16Nhwc => {
let _span = Self::enter_image_convert_gl_span(
src_fmt,
dst_fmt,
false,
src.memory(),
dst.memory(),
);
let convert_span = tracing::Span::current();
return self.convert_float_to_pbo(
src,
dst,
path,
rotation,
flip,
crop,
&convert_span,
);
}
FloatRenderPath::None => {}
}
}
let dst_odd = match (dst.width(), dst.height()) {
(Some(w), Some(h)) if w % 2 != 0 || h % 2 != 0 => Some((w, h)),
_ => None,
};
let (src_u8, src_fmt) = dyn_to_u8_src(src)?;
let (dst_u8, dst_fmt, is_int8) = dyn_to_u8_dst(dst)?;
let is_int8 = is_int8 || int_mapped;
self.int_pad_output = int_mapped && pad_output.is_some() && !pad_output_exact;
let result = self.convert_impl(
src_u8, src_fmt, dst_u8, dst_fmt, is_int8, rotation, flip, crop,
);
match result {
Err(e) if dst_odd.is_some() && matches!(e, Error::EGL(_) | Error::OpenGl(_)) => {
let (w, h) = dst_odd.unwrap();
Err(Error::NotSupported(format!(
"Conversion failed with {e:?} and target tensor has odd \
dimensions {w}x{h}, which is not supported by all platforms"
)))
}
other => other,
}
}
pub(crate) fn convert_fenced(
&mut self,
src: &TensorDyn,
dst: &mut TensorDyn,
options: crate::ConvertOptions,
) -> Result<Option<super::CompletionFence>, crate::Error> {
self.defer_finish = true;
let result = self.convert_inner(src, dst, &options);
self.defer_finish = false;
result?;
let recorded = Platform::record_completion(&self.gl_context, dst, true);
match Platform::export_completion_fence(&self.gl_context, recorded) {
Ok(Some(fd)) => Ok(Some(fd)),
Ok(None) => {
unsafe { edgefirst_gl::gl::Finish() };
check_gl_error(function!(), line!())?;
Ok(None)
}
Err(e) => {
log::debug!("native fence export failed ({e:?}); falling back to glFinish");
unsafe { edgefirst_gl::gl::Finish() };
check_gl_error(function!(), line!())?;
Ok(None)
}
}
}
pub(super) fn enter_image_convert_gl_span(
src_fmt: PixelFormat,
dst_fmt: PixelFormat,
is_int8: bool,
src_memory: TensorMemory,
dst_memory: TensorMemory,
) -> tracing::span::EnteredSpan {
tracing::trace_span!(
"image.convert.gl",
?src_fmt,
?dst_fmt,
is_int8,
src_memory = ?src_memory,
dst_memory = ?dst_memory,
src_feed = tracing::field::Empty,
dst_feed = tracing::field::Empty,
)
.entered()
}
#[allow(clippy::too_many_arguments)]
pub(super) fn convert_impl(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
dst: &mut Tensor<u8>,
dst_fmt: PixelFormat,
is_int8: bool,
rotation: crate::Rotation,
flip: Flip,
crop: ResolvedCrop,
) -> crate::Result<()> {
let _span = Self::enter_image_convert_gl_span(
src_fmt,
dst_fmt,
is_int8,
src.memory(),
dst.memory(),
);
if !Self::check_src_format_supported(self.gl_context.transfer_backend, src, src_fmt) {
if src_fmt == PixelFormat::Vyuy
&& self.gl_context.transfer_backend.is_dma()
&& src.memory() == TensorMemory::DmaBuf
{
log::warn!(
"VYUY format not supported via EGL DMA-BUF import; \
falling back to CPU/G2D path"
);
}
return Err(crate::Error::NotSupported(format!(
"Opengl doesn't support {src_fmt} source texture",
)));
}
if !Self::dst_format_renderable(
self.gl_context.transfer_backend,
dst.memory(),
dst_fmt,
self.has_bgra,
) {
return Err(crate::Error::NotSupported(format!(
"Opengl doesn't support {dst_fmt} destination texture",
)));
}
log::debug!(
"dst tensor: {:?} src tensor :{:?}",
dst.memory(),
src.memory()
);
check_gl_error(function!(), line!())?;
log::trace!(
"GL convert_impl: {src_fmt}→{dst_fmt} int8={is_int8} \
src_mem={:?} dst_mem={:?} transfer={:?}",
src.memory(),
dst.memory(),
self.gl_context.transfer_backend,
);
self.convert_via_engine(dst, dst_fmt, src, src_fmt, is_int8, rotation, flip, crop)
}
#[allow(clippy::too_many_arguments)]
fn convert_via_engine(
&mut self,
dst: &mut Tensor<u8>,
dst_fmt: PixelFormat,
src: &Tensor<u8>,
src_fmt: PixelFormat,
is_int8: bool,
rotation: crate::Rotation,
flip: Flip,
crop: ResolvedCrop,
) -> crate::Result<()> {
if dst.view_origin().is_some()
&& (dst_fmt == PixelFormat::Rgb || dst_fmt.layout() == PixelLayout::Planar)
{
return Err(crate::Error::NotSupported(
"GL view()/batch() destination not yet supported for packed-RGB / \
planar formats (CPU fallback handles it)"
.into(),
));
}
let dst_offset = view_collapsed_dst_base(dst.view_origin(), dst.plane_offset());
let places = Platform::dst_import_places(dst)
&& !(self.is_adreno && unaligned_import_offset(dst_offset));
if !places {
log::debug!(
"convert_via_engine: zero-copy destination importing at base offset \
{dst_offset} cannot be placed by this platform/driver; rendering \
to a texture and reading back through map()"
);
}
let convert_span = tracing::Span::current();
let mut lowering = super::render::lower_dst(
self.gl_context.transfer_backend.is_zero_copy() && places,
self.usable_memory(dst.memory(), dst.pbo_vtable_ptr()),
);
let src_host = matches!(
src.memory(),
TensorMemory::Mem | TensorMemory::Shm | TensorMemory::Pbo
);
let mut plan = super::render::plan_convert(src_fmt, src_host, dst_fmt, lowering);
if plan == super::render::ConvertPlan::TwoPassPackedRgb {
match self.probe_packed_rgb_dst_import(dst, dst_fmt) {
Ok(()) => {
convert_span.record("dst_feed", DstTarget::ZeroCopyImage.feed());
}
Err(e) if self.probe_dst_import_only => return Err(e),
Err(e) => {
self.note_dst_import_fallback(dst, dst_fmt, &e);
lowering = super::render::lower_dst(false, dst.memory());
plan = super::render::plan_convert(src_fmt, src_host, dst_fmt, lowering);
}
}
}
let _span = tracing::trace_span!(
"image.convert.gl.engine",
?plan,
?lowering,
src_pbo = src.memory() == TensorMemory::Pbo,
)
.entered();
match plan {
super::render::ConvertPlan::TwoPassPackedRgb => {
return self.convert_to_packed_rgb(
src,
src_fmt,
dst,
dst_fmt,
is_int8,
rotation,
flip,
crop,
&convert_span,
)
}
super::render::ConvertPlan::TwoPassNvPlanar => {
return self.convert_nv_to_planar_two_pass(
src,
src_fmt,
dst,
dst_fmt,
is_int8,
rotation,
flip,
crop,
lowering,
&convert_span,
)
}
super::render::ConvertPlan::SinglePass => {}
}
let (target, bound) = self.bind_dst(dst, dst_fmt, crop, lowering)?;
let replanned = super::render::plan_convert(src_fmt, src_host, dst_fmt, bound);
if replanned != super::render::ConvertPlan::SinglePass {
debug_assert_eq!(
replanned,
super::render::ConvertPlan::TwoPassNvPlanar,
"a texture lowering can only re-plan onto the planar two-pass route"
);
return self.convert_nv_to_planar_two_pass(
src,
src_fmt,
dst,
dst_fmt,
is_int8,
rotation,
flip,
crop,
bound,
&convert_span,
);
}
Self::record_dst_feed(&convert_span, target);
let band = match target {
DstTarget::ZeroCopyImage => dst.view_origin(),
DstTarget::Texture { .. } => None,
};
let crop = self.int8_bias_clear(is_int8, crop);
let start = Instant::now();
self.render_packed_or_planar(
src,
src_fmt,
dst,
dst_fmt,
band,
is_int8,
rotation,
flip,
crop,
&convert_span,
)?;
log::debug!("engine render ({plan:?}) takes {:?}", start.elapsed());
if let DstTarget::Texture { readback } = target {
let start = Instant::now();
self.readback_rendered(dst, dst_fmt, readback.pbo_id())?;
log::debug!("engine readback takes {:?}", start.elapsed());
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(super) fn draw_decoded_masks_impl(
&mut self,
dst: &mut Tensor<u8>,
dst_fmt: PixelFormat,
detect: &[DetectBox],
segmentation: &[Segmentation],
opacity: f32,
background: Option<(&Tensor<u8>, PixelFormat)>,
color_mode: crate::ColorMode,
) -> Result<(), crate::Error> {
use crate::FunctionTimer;
let _timer = FunctionTimer::new("GLProcessorST::draw_decoded_masks");
if !matches!(
dst_fmt,
PixelFormat::Rgba | PixelFormat::Bgra | PixelFormat::Rgb
) {
return Err(crate::Error::NotSupported(
"Opengl image rendering only supports RGBA, BGRA, or RGB images".to_string(),
));
}
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let memory = self.usable_memory(dst.memory(), dst.pbo_vtable_ptr());
if log::log_enabled!(log::Level::Trace) {
log::trace!(
"draw_decoded_masks: dst.memory()={memory:?} {dst_w}x{dst_h} fmt={dst_fmt:?}"
);
}
let pbo_buffer_id = if memory == TensorMemory::Pbo {
match (dst.pbo_id(), dst.pbo_is_mapped()) {
(Some(id), Some(false)) => Some(id),
_ => None,
}
} else {
None
};
let is_dma = match memory {
TensorMemory::DmaBuf => match self.setup_draw_renderbuffer_dma(dst, dst_fmt) {
Ok(()) => {
if log::log_enabled!(log::Level::Trace) {
log::trace!(
"draw_decoded_masks: DMA fast path (setup_draw_renderbuffer_dma OK)"
);
}
true
}
Err(e) => {
warn_slow_path_once("draw_decoded_masks", "setup_draw_renderbuffer_dma", &e);
if let Some(buffer_id) = pbo_buffer_id {
self.setup_renderbuffer_from_pbo(dst, dst_fmt, buffer_id)?;
} else {
self.setup_renderbuffer_non_dma(dst, dst_fmt, ResolvedCrop::no_crop())?;
}
false
}
},
_ if pbo_buffer_id.is_some() => {
if log::log_enabled!(log::Level::Trace) {
log::trace!("draw_decoded_masks: PBO path");
}
self.setup_renderbuffer_from_pbo(dst, dst_fmt, pbo_buffer_id.unwrap())?;
false
}
_ => {
if log::log_enabled!(log::Level::Trace) {
log::trace!("draw_decoded_masks: non-DMA fallback (memory={memory:?})");
}
self.setup_renderbuffer_non_dma(dst, dst_fmt, ResolvedCrop::no_crop())?;
false
}
};
if let Some((bg, bg_fmt)) = background {
if bg.width() != Some(dst_w) || bg.height() != Some(dst_h) {
return Err(crate::Error::InvalidShape(
"background dimensions do not match dst".into(),
));
}
if is_dma {
self.draw_background_into_bound_fbo(bg, bg_fmt)?;
} else {
self.upload_pixels_to_render_texture(bg, bg_fmt, dst_w, dst_h)?;
}
} else {
unsafe {
edgefirst_gl::gl::ClearColor(0.0, 0.0, 0.0, 0.0);
edgefirst_gl::gl::Clear(edgefirst_gl::gl::COLOR_BUFFER_BIT);
}
}
edgefirst_gl::enable(edgefirst_gl::gl::BLEND);
edgefirst_gl::blend_func_separate(
edgefirst_gl::gl::SRC_ALPHA,
edgefirst_gl::gl::ONE_MINUS_SRC_ALPHA,
edgefirst_gl::gl::ZERO,
edgefirst_gl::gl::ONE,
);
self.set_opacity_uniform(opacity)?;
self.render_box(dst_w, dst_h, detect, color_mode)?;
self.render_segmentation(detect, segmentation, color_mode)?;
edgefirst_gl::finish();
if !is_dma {
let format = match dst_fmt {
PixelFormat::Rgb => edgefirst_gl::gl::RGB,
PixelFormat::Rgba | PixelFormat::Bgra => edgefirst_gl::gl::RGBA,
_ => unreachable!(),
};
if let Some(buffer_id) = pbo_buffer_id {
self.read_pixels_into_pbo(
dst,
buffer_id,
dst_w,
dst_h,
format,
dst_fmt == PixelFormat::Bgra,
)?;
} else {
self.read_pixels_into_tensor(
dst,
dst_w,
dst_h,
format,
dst_fmt == PixelFormat::Bgra,
)?;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(super) fn draw_proto_masks_impl(
&mut self,
dst: &mut Tensor<u8>,
dst_fmt: PixelFormat,
detect: &[DetectBox],
proto_data: &ProtoData,
opacity: f32,
background: Option<(&Tensor<u8>, PixelFormat)>,
color_mode: crate::ColorMode,
) -> crate::Result<()> {
use crate::FunctionTimer;
let _timer = FunctionTimer::new("GLProcessorST::draw_proto_masks");
if !matches!(
dst_fmt,
PixelFormat::Rgba | PixelFormat::Bgra | PixelFormat::Rgb
) {
return Err(crate::Error::NotSupported(
"Opengl image rendering only supports RGBA, BGRA, or RGB images".to_string(),
));
}
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let memory = self.usable_memory(dst.memory(), dst.pbo_vtable_ptr());
if log::log_enabled!(log::Level::Trace) {
log::trace!(
"draw_proto_masks: dst.memory()={memory:?} {dst_w}x{dst_h} fmt={dst_fmt:?}"
);
}
let pbo_buffer_id = if memory == TensorMemory::Pbo {
match (dst.pbo_id(), dst.pbo_is_mapped()) {
(Some(id), Some(false)) => Some(id),
_ => None,
}
} else {
None
};
let is_dma = match memory {
TensorMemory::DmaBuf => match self.setup_draw_renderbuffer_dma(dst, dst_fmt) {
Ok(()) => {
if log::log_enabled!(log::Level::Trace) {
log::trace!(
"draw_proto_masks: DMA fast path (setup_draw_renderbuffer_dma OK)"
);
}
true
}
Err(e) => {
warn_slow_path_once("draw_proto_masks", "setup_draw_renderbuffer_dma", &e);
if let Some(buffer_id) = pbo_buffer_id {
self.setup_renderbuffer_from_pbo(dst, dst_fmt, buffer_id)?;
} else {
self.setup_renderbuffer_non_dma(dst, dst_fmt, ResolvedCrop::no_crop())?;
}
false
}
},
_ if pbo_buffer_id.is_some() => {
if log::log_enabled!(log::Level::Trace) {
log::trace!("draw_proto_masks: PBO path");
}
self.setup_renderbuffer_from_pbo(dst, dst_fmt, pbo_buffer_id.unwrap())?;
false
}
_ => {
if log::log_enabled!(log::Level::Trace) {
log::trace!("draw_proto_masks: non-DMA fallback (memory={memory:?})");
}
self.setup_renderbuffer_non_dma(dst, dst_fmt, ResolvedCrop::no_crop())?;
false
}
};
if let Some((bg, bg_fmt)) = background {
if bg.width() != Some(dst_w) || bg.height() != Some(dst_h) {
return Err(crate::Error::InvalidShape(
"background dimensions do not match dst".into(),
));
}
if is_dma {
self.draw_background_into_bound_fbo(bg, bg_fmt)?;
} else {
self.upload_pixels_to_render_texture(bg, bg_fmt, dst_w, dst_h)?;
}
} else {
unsafe {
edgefirst_gl::gl::ClearColor(0.0, 0.0, 0.0, 0.0);
edgefirst_gl::gl::Clear(edgefirst_gl::gl::COLOR_BUFFER_BIT);
}
}
edgefirst_gl::enable(edgefirst_gl::gl::BLEND);
edgefirst_gl::blend_func_separate(
edgefirst_gl::gl::SRC_ALPHA,
edgefirst_gl::gl::ONE_MINUS_SRC_ALPHA,
edgefirst_gl::gl::ZERO,
edgefirst_gl::gl::ONE,
);
self.set_opacity_uniform(opacity)?;
self.render_box(dst_w, dst_h, detect, color_mode)?;
self.render_proto_segmentation(detect, proto_data, color_mode)?;
edgefirst_gl::disable(edgefirst_gl::gl::BLEND);
edgefirst_gl::finish();
if !is_dma {
let format = match dst_fmt {
PixelFormat::Rgb => edgefirst_gl::gl::RGB,
PixelFormat::Rgba | PixelFormat::Bgra => edgefirst_gl::gl::RGBA,
_ => unreachable!(),
};
if let Some(buffer_id) = pbo_buffer_id {
self.read_pixels_into_pbo(
dst,
buffer_id,
dst_w,
dst_h,
format,
dst_fmt == PixelFormat::Bgra,
)?;
} else {
self.read_pixels_into_tensor(
dst,
dst_w,
dst_h,
format,
dst_fmt == PixelFormat::Bgra,
)?;
}
}
Ok(())
}
pub(super) fn check_src_format_supported(
backend: TransferBackend,
img: &Tensor<u8>,
fmt: PixelFormat,
) -> bool {
if backend.is_zero_copy() && img.memory() == TensorMemory::DmaBuf {
matches!(
fmt,
PixelFormat::Rgba
| PixelFormat::Grey
| PixelFormat::Yuyv
| PixelFormat::Nv12
| PixelFormat::Nv16
| PixelFormat::Nv24
)
} else {
let uploads_yuv = !backend.is_zero_copy();
let nv_single_plane = matches!(
fmt,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24
) && !img.is_multiplane();
matches!(
fmt,
PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey
) || (fmt == PixelFormat::Yuyv && uploads_yuv)
|| (nv_single_plane && (img.memory() != TensorMemory::Pbo || uploads_yuv))
}
}
pub(crate) fn dst_format_renderable(
backend: TransferBackend,
memory: TensorMemory,
fmt: PixelFormat,
has_bgra: bool,
) -> bool {
if fmt == PixelFormat::Bgra && !has_bgra {
return false;
}
if backend.is_zero_copy() && memory == TensorMemory::DmaBuf {
matches!(
fmt,
PixelFormat::Rgba
| PixelFormat::Bgra
| PixelFormat::Grey
| PixelFormat::PlanarRgb
| PixelFormat::PlanarRgba
| PixelFormat::Rgb
)
} else {
matches!(
fmt,
PixelFormat::Rgb
| PixelFormat::Rgba
| PixelFormat::Bgra
| PixelFormat::Grey
| PixelFormat::PlanarRgb
| PixelFormat::PlanarRgba
)
}
}
fn gl_check_support() -> Result<GlSupport, crate::Error> {
if let Ok(version) = edgefirst_gl::get_string(edgefirst_gl::gl::SHADING_LANGUAGE_VERSION) {
log::debug!("GL Shading Language Version: {version:?}");
} else {
log::warn!("Could not get GL Shading Language Version");
}
let mut traits = edgefirst_gl::get_string(edgefirst_gl::gl::RENDERER)
.map(|r| {
log::info!("GL_RENDERER: {r}");
classify_renderer(&r)
})
.unwrap_or_default();
traits.adreno &= cfg!(target_os = "linux");
let RendererTraits {
vivante: is_vivante,
mali: is_mali,
software: is_software_renderer,
virtual_gpu: is_virtual_gpu,
angle: is_angle,
adreno: is_adreno,
} = traits;
if is_vivante {
log::warn!(
"Vivante GPU detected — NV12 → planar RGB conversions will use \
two-pass workaround to avoid GPU hang (EDGEAI-1180)"
);
}
if is_mali {
log::info!(
"Mali GPU detected — a source DMA-BUF whose plane offset is not \
{DMA_IMPORT_OFFSET_ALIGN}-byte aligned will be uploaded \
rather than imported: Mali's import samples zeros from such an \
offset without reporting an EGL error (issue #165)"
);
}
if is_software_renderer {
log::warn!(
"Software OpenGL renderer detected — GPU backend will be disabled. \
Image processing will use the CPU backend instead. \
Check EGL ICD configuration if a hardware GPU is expected."
);
}
if is_virtual_gpu {
log::warn!(
"Virtualized GPU detected — concurrent GL across contexts \
mis-renders on paravirtual Metal (observed on macOS CI \
runners); processors will serialize per-message (Full \
policy, same as Vivante). Override with \
EDGEFIRST_GL_SERIALIZE=lifecycle."
);
} else if is_angle {
log::info!(
"ANGLE detected — a deferred draw does not reliably execute \
with the per-draw state its own processor programmed while \
another context is active, so processors will serialize \
per-message (Full policy). Costs throughput only when one \
process runs several ImageProcessors concurrently. Override \
with EDGEFIRST_GL_SERIALIZE=lifecycle."
);
}
if is_adreno {
log::info!(
"Adreno GPU detected — processors will serialize per-message \
(Full policy; override with EDGEFIRST_GL_SERIALIZE=lifecycle), \
DMA-BUF imports at a plane offset that is not \
{DMA_IMPORT_OFFSET_ALIGN}-byte aligned and two-buffer NV12 \
sources take the upload / readback routes"
);
}
let extensions = unsafe {
let str = edgefirst_gl::gl::GetString(edgefirst_gl::gl::EXTENSIONS);
if str.is_null() {
return Err(crate::Error::GLVersion(
"GL returned no supported extensions".to_string(),
));
}
CStr::from_ptr(str as *const c_char)
.to_string_lossy()
.to_string()
};
log::debug!("GL Extensions: {extensions}");
let required_ext: &[&str] = if Platform::EXTERNAL_OES {
&["GL_OES_EGL_image_external_essl3"]
} else {
&[]
};
let extensions = extensions.split_ascii_whitespace().collect::<BTreeSet<_>>();
for required in required_ext {
if !extensions.contains(required) {
return Err(crate::Error::GLVersion(format!(
"GL does not support {required} extension",
)));
}
}
let force_no_float_linear = std::env::var("EDGEFIRST_GL_NO_FLOAT_LINEAR")
.map(|v| v == "1")
.unwrap_or(false);
let has_float_linear =
extensions.contains("GL_OES_texture_float_linear") && !force_no_float_linear;
if force_no_float_linear {
log::info!("GL_OES_texture_float_linear forced OFF via EDGEFIRST_GL_NO_FLOAT_LINEAR");
}
log::debug!("GL_OES_texture_float_linear: {has_float_linear}");
let has_bgra = extensions.contains("GL_EXT_texture_format_BGRA8888");
log::debug!("GL_EXT_texture_format_BGRA8888: {has_bgra}");
let supports_f32_color = extensions.contains("GL_EXT_color_buffer_float");
let supports_f16_color = extensions.contains("GL_EXT_color_buffer_half_float");
log::debug!("GL_EXT_color_buffer_float: {supports_f32_color}");
log::debug!("GL_EXT_color_buffer_half_float: {supports_f16_color}");
let has_yuv_target = Platform::EXTERNAL_OES && extensions.contains("GL_EXT_YUV_target");
log::debug!("GL_EXT_YUV_target: {has_yuv_target}");
Ok(GlSupport {
has_float_linear,
has_yuv_target,
has_bgra,
renderer: traits,
supports_f32_color,
supports_f16_color,
})
}
fn invalidate_dst_textures(&mut self) {
self.render_texture.invalidate_egl_binding();
self.draw_render_texture.invalidate_egl_binding();
}
fn invalidate_src_textures(&mut self) {
self.camera_eglimage_texture.invalidate_egl_binding();
self.nv_r8_texture.invalidate_egl_binding();
self.nv_uv_texture.invalidate_egl_binding();
self.camera_normal_texture.invalidate_egl_binding();
}
fn probe_packed_rgb_dst_import(
&mut self,
dst: &Tensor<u8>,
dst_fmt: PixelFormat,
) -> crate::Result<()> {
let (Some(dst_w), Some(dst_h)) = (dst.width(), dst.height()) else {
return Ok(());
};
let Some(layout) = edgefirst_tensor::packed_rgb888_layout(dst_w, dst_h) else {
return Ok(());
};
self.get_or_create_egl_image_rgb(
dst,
dst_fmt,
layout.surface_w,
layout.surface_h,
super::platform::PackedImportFormat::Rgba8888,
)
.map(|_| ())
}
fn note_dst_import_fallback(&mut self, dst: &Tensor<u8>, dst_fmt: PixelFormat, e: &Error) {
self.convert_stats.dst_import_fallbacks += 1;
let w = dst.width().unwrap_or(0);
let h = dst.height().unwrap_or(0);
let offset = dst.plane_offset().unwrap_or(0);
if self.dst_import_warned.insert(dst.buffer_identity().id()) {
log::warn!(
"zero-copy destination import refused for {dst_fmt} ({w}x{h}) at plane \
offset {offset}; rendering to a texture and reading back through map() \
(slower, still correct): {e}"
);
} else {
log::debug!(
"zero-copy destination import refused for {dst_fmt} ({w}x{h}) at plane \
offset {offset} (repeat): {e}"
);
}
unsafe {
for _ in 0..8 {
if edgefirst_gl::gl::GetError() == edgefirst_gl::gl::NO_ERROR {
break;
}
}
}
}
fn bind_dst(
&mut self,
dst: &Tensor<u8>,
dst_fmt: PixelFormat,
crop: ResolvedCrop,
lowering: super::render::DstLowering,
) -> crate::Result<(DstTarget, super::render::DstLowering)> {
let effective = match lowering {
super::render::DstLowering::ZeroCopy => {
match self.setup_renderbuffer_dma(dst, dst_fmt) {
Ok(()) => (DstTarget::ZeroCopyImage, lowering),
Err(e) if self.probe_dst_import_only => return Err(e),
Err(e) => {
self.note_dst_import_fallback(dst, dst_fmt, &e);
self.setup_renderbuffer_non_dma(dst, dst_fmt, crop)?;
(
DstTarget::Texture {
readback: DstReadback::Mem,
},
super::render::DstLowering::TextureMem,
)
}
}
}
super::render::DstLowering::TexturePbo => {
let id = dst.pbo_id().ok_or_else(|| {
crate::Error::OpenGl(
"bind_dst: PBO-lowered destination is not a PBO tensor".to_string(),
)
})?;
if dst.pbo_is_mapped() == Some(true) {
return Err(crate::Error::OpenGl(
"Cannot convert to a mapped PBO tensor".to_string(),
));
}
self.setup_renderbuffer_from_pbo(dst, dst_fmt, id)?;
(
DstTarget::Texture {
readback: DstReadback::Pbo(id),
},
lowering,
)
}
super::render::DstLowering::TextureMem => {
self.setup_renderbuffer_non_dma(dst, dst_fmt, crop)?;
(
DstTarget::Texture {
readback: DstReadback::Mem,
},
lowering,
)
}
};
Ok(effective)
}
fn record_dst_feed(convert_span: &tracing::Span, target: DstTarget) {
convert_span.record("dst_feed", target.feed());
}
fn setup_renderbuffer_dma(
&mut self,
dst: &Tensor<u8>,
dst_fmt: PixelFormat,
) -> crate::Result<()> {
self.convert_fbo.bind();
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let (width, height) = if dst_fmt.layout() == PixelLayout::Planar {
(dst_w as i32, (dst_h * dst_fmt.channels()) as i32)
} else {
(dst_w as i32, dst_h as i32)
};
let dst_key = BufferImportKey::from_tensor(dst, dst_fmt, true);
let luma_id = dst_key.luma_id;
let dest_egl = self.get_or_create_egl_image(CacheKind::Dst, dst, dst_fmt)?;
match self.cached_dst_renderbuffer(dst, dst_fmt) {
Some(rbo) => unsafe {
edgefirst_gl::gl::BindRenderbuffer(edgefirst_gl::gl::RENDERBUFFER, rbo);
edgefirst_gl::gl::FramebufferRenderbuffer(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::RENDERBUFFER,
rbo,
);
check_gl_error(function!(), line!())?;
},
None => unsafe {
if let Some(p) = &self.texture_program_yuv {
edgefirst_gl::gl::UseProgram(p.id);
}
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(edgefirst_gl::gl::TEXTURE_2D, self.render_texture.id);
super::core::set_tex_filter(edgefirst_gl::gl::TEXTURE_2D, edgefirst_gl::gl::LINEAR);
if self
.render_texture
.bind_egl_image(&self.gl_context, dst_key, dest_egl)?
{
edgefirst_gl::gl::FramebufferTexture2D(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::TEXTURE_2D,
self.render_texture.id,
0,
);
log::trace!("setup_renderbuffer_dma: bound new dst EGLImage id={luma_id:#x}");
} else {
log::trace!(
"setup_renderbuffer_dma: reusing bound dst EGLImage id={luma_id:#x}"
);
}
check_gl_error(function!(), line!())?;
},
}
let vp = match dst.view_origin() {
Some(vo) => super::render::region_to_viewport_top_down(crate::Region::new(
vo.x, vo.y, dst_w, dst_h,
)),
None => super::render::Viewport {
x: 0,
y: 0,
w: width,
h: height,
},
};
unsafe {
edgefirst_gl::gl::Viewport(vp.x, vp.y, vp.w, vp.h);
}
Ok(())
}
fn setup_draw_renderbuffer_dma(
&mut self,
dst: &Tensor<u8>,
dst_fmt: PixelFormat,
) -> crate::Result<()> {
self.draw_fbo.bind();
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let (width, height) = if dst_fmt.layout() == PixelLayout::Planar {
(dst_w as i32, (dst_h * dst_fmt.channels()) as i32)
} else {
(dst_w as i32, dst_h as i32)
};
let dst_key = BufferImportKey::from_tensor(dst, dst_fmt, true);
let luma_id = dst_key.luma_id;
let dest_egl = self.get_or_create_egl_image(CacheKind::Dst, dst, dst_fmt)?;
match self.cached_dst_renderbuffer(dst, dst_fmt) {
Some(rbo) => unsafe {
edgefirst_gl::gl::BindRenderbuffer(edgefirst_gl::gl::RENDERBUFFER, rbo);
edgefirst_gl::gl::FramebufferRenderbuffer(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::RENDERBUFFER,
rbo,
);
check_gl_error(function!(), line!())?;
},
None => unsafe {
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(
edgefirst_gl::gl::TEXTURE_2D,
self.draw_render_texture.id,
);
super::core::set_tex_filter(edgefirst_gl::gl::TEXTURE_2D, edgefirst_gl::gl::LINEAR);
if self
.draw_render_texture
.bind_egl_image(&self.gl_context, dst_key, dest_egl)?
{
edgefirst_gl::gl::FramebufferTexture2D(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::TEXTURE_2D,
self.draw_render_texture.id,
0,
);
log::trace!(
"setup_draw_renderbuffer_dma: bound new dst EGLImage id={luma_id:#x}"
);
} else {
log::trace!(
"setup_draw_renderbuffer_dma: reusing bound dst EGLImage id={luma_id:#x}"
);
}
check_gl_error(function!(), line!())?;
},
}
unsafe {
edgefirst_gl::gl::Viewport(0, 0, width, height);
}
Ok(())
}
fn collect_int8_programs(&self) -> Vec<(u32, [i32; 6])> {
let mut ids = vec![
self.texture_int8_program.id,
self.yuyv_int8_program_2d.id,
self.packed_rgba8_int8_program_2d.id,
self.texture_program_planar_int8_2d.id,
];
ids.extend(self.texture_int8_program_yuv.iter().map(|p| p.id));
ids.extend(self.texture_program_planar_int8.iter().map(|p| p.id));
ids.extend(self.nv_r8_programs.iter().skip(1).step_by(2).map(|p| p.id));
if let Some((programs, _)) = &self.nv_y2y_programs {
ids.push(programs[1].id);
}
ids.into_iter()
.map(|id| {
let locs = [
c"q_scale",
c"q_offset",
c"q_lo",
c"q_hi",
c"q_zero",
c"q_wrap",
]
.map(|name| unsafe { edgefirst_gl::gl::GetUniformLocation(id, name.as_ptr()) });
(id, locs)
})
.collect()
}
unsafe fn upload_quant(locs: &[i32; 6], q: &IntQuant) {
let offset = q.offset.map(|o| o + 0.5);
let lo: [f32; 3] = std::array::from_fn(|c| q.lo[c] - q.zero[c]);
let hi: [f32; 3] = std::array::from_fn(|c| q.hi[c] - q.zero[c]);
let zero = q.zero.map(|z| z / 255.0);
let wrap = q.zero.map(|z| -z - 0.5);
for (loc, v) in locs.iter().zip([&q.scale, &offset, &lo, &hi, &zero, &wrap]) {
unsafe { edgefirst_gl::gl::Uniform3fv(*loc, 1, v.as_ptr()) };
}
}
fn use_int8_set(&mut self, mapped: bool) -> crate::Result<()> {
if self.int8_mapped == mapped {
return Ok(());
}
let mut alt = match self.int8_alt.take() {
Some(alt) => alt,
None => {
let set = Int8Set::new(true, self.nv_y2y_programs.is_some())?;
if set.nv_y2y.is_none() && self.nv_y2y_programs.take().is_some() {
log::info!(
"the mapped int8 Y2Y program did not build; NV filters in the shader"
);
}
set
}
};
self.swap_int8_set(&mut alt);
self.int8_alt = Some(alt);
self.int8_mapped = mapped;
if mapped && self.int8_programs.is_empty() {
self.int8_programs = self.collect_int8_programs();
}
Ok(())
}
fn swap_int8_set(&mut self, set: &mut Int8Set) {
use std::mem::swap;
swap(&mut self.texture_int8_program, &mut set.texture);
swap(&mut self.texture_int8_program_yuv, &mut set.texture_yuv);
swap(&mut self.texture_program_planar_int8, &mut set.planar);
swap(&mut self.texture_program_planar_int8_2d, &mut set.planar_2d);
swap(&mut self.yuyv_int8_program_2d, &mut set.yuyv_2d);
swap(&mut self.yuyv_2d_locs[1], &mut set.yuyv_2d_locs);
swap(
&mut self.packed_rgba8_int8_program_2d,
&mut set.packed_rgba8_2d,
);
for (i, (program, uniforms)) in set
.nv_r8
.iter_mut()
.zip(set.nv_r8_uniforms.iter_mut())
.enumerate()
{
swap(&mut self.nv_r8_programs[2 * i + 1], program);
swap(&mut self.nv_r8_uniforms[2 * i + 1], uniforms);
}
if let (Some((programs, uniforms)), Some((program, u))) =
(self.nv_y2y_programs.as_mut(), set.nv_y2y.as_mut())
{
swap(&mut programs[1], program);
swap(&mut uniforms[1], u);
}
swap(
&mut self.texture_src_extent_locs[1],
&mut set.texture_src_extent_loc,
);
swap(
&mut self.external_src_extent_locs[1],
&mut set.yuv_src_extent_loc,
);
swap(
&mut self.external_src_extent_locs[3],
&mut set.planar_src_extent_loc,
);
}
fn load_int_quant(&mut self, q: IntQuant) -> crate::Result<()> {
if self.int_quant == Some(q) {
return Ok(());
}
unsafe {
let mut current = 0i32;
edgefirst_gl::gl::GetIntegerv(edgefirst_gl::gl::CURRENT_PROGRAM, &mut current);
for (id, locs) in &self.int8_programs {
edgefirst_gl::gl::UseProgram(*id);
Self::upload_quant(locs, &q);
}
edgefirst_gl::gl::UseProgram(current as u32);
check_gl_error(function!(), line!())?;
}
self.int_quant = Some(q);
Ok(())
}
unsafe fn set_plane_quant(&self, program: u32, plane: Option<usize>) {
let Some(q) = self.int_quant else {
return;
};
let same = |v: &[f32; 3]| v.iter().all(|&x| x == v[0]);
if [&q.scale, &q.offset, &q.zero, &q.lo, &q.hi]
.into_iter()
.all(same)
{
return;
}
let Some((_, locs)) = self.int8_programs.iter().find(|(p, _)| *p == program) else {
return;
};
let q = match plane {
Some(p) if p < 3 => IntQuant {
scale: [q.scale[p]; 3],
offset: [q.offset[p]; 3],
zero: [q.zero[p]; 3],
lo: [q.lo[p]; 3],
hi: [q.hi[p]; 3],
},
_ => q,
};
unsafe { Self::upload_quant(locs, &q) };
}
fn int8_bias_clear(&self, is_int8: bool, mut crop: ResolvedCrop) -> ResolvedCrop {
if let (Some(color), Some(pad)) = (crop.dst_color.as_mut(), self.int_pad) {
*color = pad;
} else if is_int8 {
if let Some(ref mut color) = crop.dst_color {
color[0] ^= 0x80;
color[1] ^= 0x80;
color[2] ^= 0x80;
}
}
crop
}
#[allow(clippy::too_many_arguments)]
fn render_packed_or_planar(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
dst: &mut Tensor<u8>,
dst_fmt: PixelFormat,
band: Option<edgefirst_tensor::ViewOrigin>,
is_int8: bool,
rotation: crate::Rotation,
flip: Flip,
crop: ResolvedCrop,
convert_span: &tracing::Span,
) -> crate::Result<()> {
if dst_fmt.layout() == PixelLayout::Planar {
self.convert_to_planar(
src,
src_fmt,
dst,
dst_fmt,
is_int8,
rotation,
flip,
crop,
convert_span,
)
} else {
self.convert_to(
src,
src_fmt,
dst,
dst_fmt,
band,
is_int8,
rotation,
flip,
crop,
convert_span,
)
}
}
fn read_pixels_into_tensor(
&mut self,
dst: &mut Tensor<u8>,
width: usize,
rows: usize,
format: u32,
swap_rb: bool,
) -> crate::Result<()> {
let tight_row = width * pack_channels(format);
let stride = dst.effective_row_stride().unwrap_or(tight_row);
if stride < tight_row {
return Err(crate::Error::OpenGl(format!(
"destination row pitch is {stride} B but one {width}-pixel row \
of this readback format is {tight_row} B"
)));
}
let mut dst_map = dst.map_for_write()?;
let out = dst_map.as_mut_slice();
let needed = padded_readback_bytes(rows, tight_row, stride).unwrap_or(rows * tight_row);
if out.len() < needed {
return Err(crate::Error::OpenGl(format!(
"destination maps {} B, needs {needed} B at a {stride} B pitch",
out.len()
)));
}
unsafe {
edgefirst_gl::gl::ReadBuffer(edgefirst_gl::gl::COLOR_ATTACHMENT0);
read_pixels_into(width, rows, format, &mut self.readback_scratch, out, stride);
}
check_gl_error(function!(), line!())?;
if swap_rb {
for y in 0..rows {
for chunk in out[y * stride..y * stride + tight_row]
.as_chunks_mut::<4>()
.0
{
chunk.swap(0, 2);
}
}
}
Ok(())
}
fn read_pixels_into_pbo(
&mut self,
dst: &Tensor<u8>,
buffer_id: u32,
width: usize,
rows: usize,
format: u32,
swap_rb: bool,
) -> crate::Result<()> {
let channels = pack_channels(format);
let PboReadbackLayout {
stride,
tight_row,
needed,
row_length,
} = plan_pbo_readback(
dst.effective_row_stride().unwrap_or(width * channels),
dst.view_origin().is_some(),
dst.plane_offset().unwrap_or(0),
dst.capacity_bytes(),
width,
rows,
channels,
)?;
let mut spread = false;
unsafe {
edgefirst_gl::gl::ReadBuffer(edgefirst_gl::gl::COLOR_ATTACHMENT0);
if direct_read_supported(format) {
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
if let Some(pixels) = row_length {
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::PACK_ROW_LENGTH, pixels);
}
edgefirst_gl::gl::ReadPixels(
0,
0,
width as i32,
rows as i32,
format,
edgefirst_gl::gl::UNSIGNED_BYTE,
std::ptr::null_mut(),
);
if row_length.is_some() {
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::PACK_ROW_LENGTH, 0);
}
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
edgefirst_gl::gl::Finish();
spread = stride > tight_row && row_length.is_none();
} else {
read_pixels_rgba_scratch(width, rows, &mut self.readback_scratch);
let scratch = &self.readback_scratch;
let access = if stride > tight_row {
edgefirst_gl::gl::MAP_READ_BIT | edgefirst_gl::gl::MAP_WRITE_BIT
} else {
edgefirst_gl::gl::MAP_WRITE_BIT | edgefirst_gl::gl::MAP_INVALIDATE_RANGE_BIT
};
with_mapped_pbo(buffer_id, needed, access, "PBO readback repack", |out| {
for (y, row) in out.chunks_mut(stride).take(rows).enumerate() {
let pixels = scratch[y * width * 4..(y + 1) * width * 4]
.as_chunks::<4>()
.0;
for (px, dst_px) in pixels
.iter()
.zip(row[..tight_row].chunks_exact_mut(channels))
{
dst_px.copy_from_slice(&px[..channels]);
}
}
})?;
}
}
check_gl_error(function!(), line!())?;
if spread || swap_rb {
unsafe {
with_mapped_pbo(
buffer_id,
needed,
edgefirst_gl::gl::MAP_READ_BIT | edgefirst_gl::gl::MAP_WRITE_BIT,
"PBO readback row placement",
|buf| {
if spread {
spread_rows(buf, rows, tight_row, stride);
}
if swap_rb {
for y in 0..rows {
for chunk in buf[y * stride..y * stride + tight_row]
.as_chunks_mut::<4>()
.0
{
chunk.swap(0, 2);
}
}
}
},
)?;
}
check_gl_error(function!(), line!())?;
}
Ok(())
}
fn readback_rendered(
&mut self,
dst: &mut Tensor<u8>,
dst_fmt: PixelFormat,
pbo_id: Option<u32>,
) -> crate::Result<()> {
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let dest_format = match dst_fmt {
PixelFormat::Rgb => edgefirst_gl::gl::RGB,
PixelFormat::Rgba | PixelFormat::Bgra => edgefirst_gl::gl::RGBA,
PixelFormat::Grey => edgefirst_gl::gl::RED,
PixelFormat::PlanarRgb | PixelFormat::PlanarRgba => edgefirst_gl::gl::RED,
_ => {
return Err(crate::Error::NotSupported(format!(
"GL readback not supported for {dst_fmt}"
)))
}
};
let read_h = if dst_fmt.layout() == PixelLayout::Planar {
dst_h * dst_fmt.channels()
} else {
dst_h
};
match pbo_id {
None => self.read_pixels_into_tensor(
dst,
dst_w,
read_h,
dest_format,
dst_fmt == PixelFormat::Bgra,
),
Some(buffer_id) => self.read_pixels_into_pbo(
dst,
buffer_id,
dst_w,
read_h,
dest_format,
dst_fmt == PixelFormat::Bgra,
),
}
}
fn setup_renderbuffer_non_dma(
&mut self,
dst: &Tensor<u8>,
dst_fmt: PixelFormat,
crop: ResolvedCrop,
) -> crate::Result<()> {
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let is_planar = dst_fmt.layout() == PixelLayout::Planar;
let (width, height) = if is_planar {
let height = match dst_fmt.channels() {
4 => dst_h * 4,
3 => dst_h * 3,
1 => dst_h,
_ => unreachable!(),
};
(dst_w as i32, height as i32)
} else {
(dst_w as i32, dst_h as i32)
};
let is_bgra = !is_planar && dst_fmt == PixelFormat::Bgra;
if is_bgra && !self.bgra_warned {
log::warn!(
"BGRA destination: using RGBA internal format with CPU R↔B byte-swap workaround"
);
self.bgra_warned = true;
}
let format = if is_planar {
edgefirst_gl::gl::RED
} else {
match dst_fmt {
PixelFormat::Rgb => edgefirst_gl::gl::RGB,
PixelFormat::Rgba | PixelFormat::Bgra => edgefirst_gl::gl::RGBA, PixelFormat::Grey => edgefirst_gl::gl::RED,
_ => unreachable!(),
}
};
let start = Instant::now();
self.convert_fbo.bind();
let map;
let mut swapped_buf;
let seeded = crop.dst_color.is_none()
&& crop.dst_rect.is_some_and(|c| {
c.top != 0 || c.left != 0 || c.height != dst_h || c.width != dst_w
});
let pixels = if !seeded {
std::ptr::null()
} else {
map = dst.map_read()?;
if is_bgra {
swapped_buf = map.as_slice().to_vec();
for chunk in swapped_buf.as_chunks_mut::<4>().0 {
chunk.swap(0, 2);
}
swapped_buf.as_ptr() as *const c_void
} else {
map.as_ptr() as *const c_void
}
};
unsafe {
edgefirst_gl::gl::UseProgram(self.texture_program.id);
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(edgefirst_gl::gl::TEXTURE_2D, self.render_texture.id);
super::core::set_tex_filter(edgefirst_gl::gl::TEXTURE_2D, edgefirst_gl::gl::LINEAR);
edgefirst_gl::gl::TexImage2D(
edgefirst_gl::gl::TEXTURE_2D,
0,
self.render_internal_format(format),
width,
height,
0,
format,
edgefirst_gl::gl::UNSIGNED_BYTE,
pixels,
);
self.render_texture.invalidate_egl_binding();
check_gl_error(function!(), line!())?;
edgefirst_gl::gl::FramebufferTexture2D(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::TEXTURE_2D,
self.render_texture.id,
0,
);
check_gl_error(function!(), line!())?;
edgefirst_gl::gl::Viewport(0, 0, width, height);
}
log::debug!("Set up framebuffer takes {:?}", start.elapsed());
Ok(())
}
fn draw_background_into_bound_fbo(
&mut self,
bg: &Tensor<u8>,
bg_fmt: PixelFormat,
) -> crate::Result<()> {
edgefirst_gl::disable(edgefirst_gl::gl::BLEND);
let full_src = RegionOfInterest {
left: 0.0,
top: 1.0,
right: 1.0,
bottom: 0.0,
};
let full_dst = RegionOfInterest {
left: -1.0,
top: 1.0,
right: 1.0,
bottom: -1.0,
};
if Platform::EXTERNAL_OES && bg.memory() == TensorMemory::DmaBuf {
let bg_egl = self.get_or_create_egl_image(CacheKind::Src, bg, bg_fmt)?;
self.draw_camera_texture_eglimage(
bg,
bg_fmt,
bg_egl,
full_src,
full_dst,
0,
crate::Flip::None,
false,
None,
)
} else {
self.draw_src_texture(
bg,
bg_fmt,
full_src,
full_dst,
0,
crate::Flip::None,
false,
&tracing::Span::none(),
)
}
}
fn upload_pixels_to_render_texture(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
dst_w: usize,
dst_h: usize,
) -> crate::Result<()> {
use edgefirst_tensor::TensorMapTrait;
let map = src.map_read()?;
let src_slice = map.as_slice();
let format = match src_fmt {
PixelFormat::Rgb => edgefirst_gl::gl::RGB,
PixelFormat::Rgba | PixelFormat::Bgra => edgefirst_gl::gl::RGBA,
_ => {
return Err(crate::Error::NotSupported(format!(
"non-DMA bg upload not supported for {src_fmt}",
)))
}
};
let swapped: Option<Vec<u8>> = if src_fmt == PixelFormat::Bgra {
let mut v = src_slice.to_vec();
for chunk in v.as_chunks_mut::<4>().0 {
chunk.swap(0, 2);
}
Some(v)
} else {
None
};
let pixels = swapped.as_deref().unwrap_or(src_slice).as_ptr() as *const c_void;
unsafe {
edgefirst_gl::gl::BindTexture(edgefirst_gl::gl::TEXTURE_2D, self.render_texture.id);
edgefirst_gl::gl::TexImage2D(
edgefirst_gl::gl::TEXTURE_2D,
0,
format as i32,
dst_w as i32,
dst_h as i32,
0,
format,
edgefirst_gl::gl::UNSIGNED_BYTE,
pixels,
);
self.render_texture.invalidate_egl_binding();
check_gl_error(function!(), line!())?;
edgefirst_gl::gl::FramebufferTexture2D(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::TEXTURE_2D,
self.render_texture.id,
0,
);
check_gl_error(function!(), line!())?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn setup_renderbuffer_from_pbo_inner(
&mut self,
width: i32,
height: i32,
buffer_id: u32,
internal_format: u32,
client_format: u32,
gl_type: u32,
row_length: Option<i32>,
) -> crate::Result<()> {
self.convert_fbo.bind();
unsafe {
edgefirst_gl::gl::UseProgram(self.texture_program.id);
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(edgefirst_gl::gl::TEXTURE_2D, self.render_texture.id);
super::core::set_tex_filter(edgefirst_gl::gl::TEXTURE_2D, edgefirst_gl::gl::LINEAR);
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_UNPACK_BUFFER, buffer_id);
if let Some(px) = row_length {
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::UNPACK_ROW_LENGTH, px);
}
edgefirst_gl::gl::TexImage2D(
edgefirst_gl::gl::TEXTURE_2D,
0,
internal_format as i32,
width,
height,
0,
client_format,
gl_type,
std::ptr::null(),
);
if row_length.is_some() {
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::UNPACK_ROW_LENGTH, 0);
}
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_UNPACK_BUFFER, 0);
self.render_texture.invalidate_egl_binding();
check_gl_error(function!(), line!())?;
edgefirst_gl::gl::FramebufferTexture2D(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::TEXTURE_2D,
self.render_texture.id,
0,
);
check_gl_error(function!(), line!())?;
edgefirst_gl::gl::Viewport(0, 0, width, height);
}
Ok(())
}
fn setup_renderbuffer_from_pbo(
&mut self,
dst: &Tensor<u8>,
dst_fmt: PixelFormat,
buffer_id: u32,
) -> crate::Result<()> {
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let is_planar = dst_fmt.layout() == PixelLayout::Planar;
let (width, height) = if is_planar {
let height = match dst_fmt.channels() {
4 => dst_h * 4,
3 => dst_h * 3,
1 => dst_h,
_ => unreachable!(),
};
(dst_w as i32, height as i32)
} else {
(dst_w as i32, dst_h as i32)
};
let format = if is_planar {
edgefirst_gl::gl::RED
} else {
match dst_fmt {
PixelFormat::Rgb => edgefirst_gl::gl::RGB,
PixelFormat::Rgba | PixelFormat::Bgra => edgefirst_gl::gl::RGBA,
PixelFormat::Grey => edgefirst_gl::gl::RED,
_ => {
return Err(crate::Error::NotSupported(format!(
"PBO renderbuffer not supported for {dst_fmt}",
)))
}
}
};
let px_bytes = match format {
edgefirst_gl::gl::RGBA => 4,
edgefirst_gl::gl::RGB => 3,
_ => 1, };
let tight_row = width as usize * px_bytes;
let row_length = dst
.effective_row_stride()
.and_then(|stride| pack_row_length(tight_row, stride, px_bytes));
self.setup_renderbuffer_from_pbo_inner(
width,
height,
buffer_id,
self.render_internal_format(format) as u32,
format,
edgefirst_gl::gl::UNSIGNED_BYTE,
row_length,
)
}
fn select_nv_path(
&self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
render_fmt: PixelFormat,
) -> NvConvertPath {
let shader_capable = matches!(
src_fmt,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24
) && !src.is_multiplane();
if !shader_capable {
return NvConvertPath::ExternalSampler;
}
match self.nv_path_pref {
NvPathPref::ForceShader => NvConvertPath::ShaderR8,
NvPathPref::ForceSampler => {
if src_fmt == PixelFormat::Nv12
&& self.is_vivante
&& render_fmt == PixelFormat::Grey
{
log::warn!(
"EDGEFIRST_NV_CONVERT_PATH=sampler refused for NV12→Grey on Vivante \
(samplerExternalOES → R8 render target hangs the GPU); using shader"
);
NvConvertPath::ShaderR8
} else if src_fmt == PixelFormat::Nv12 {
NvConvertPath::ExternalSampler
} else {
log::warn!(
"EDGEFIRST_NV_CONVERT_PATH=sampler unavailable for {src_fmt:?} \
(no external-sampler path); using shader"
);
NvConvertPath::ShaderR8
}
}
NvPathPref::Auto => {
let vivante_sampler_usable = src_fmt == PixelFormat::Nv12
&& self.is_vivante
&& render_fmt != PixelFormat::Grey
&& src.width().is_some_and(|w| w.is_multiple_of(4));
let take_sampler = vivante_sampler_usable
&& match self.colorimetry_mode {
crate::ColorimetryMode::Fast => true,
crate::ColorimetryMode::Exact => {
let cm = crate::colorimetry::resolve_colorimetry(
src.colorimetry(),
src.height(),
);
cm.encoding == Some(edgefirst_tensor::ColorEncoding::Bt601)
&& cm.range == Some(edgefirst_tensor::ColorRange::Limited)
}
};
if take_sampler {
NvConvertPath::ExternalSampler
} else {
NvConvertPath::ShaderR8
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn convert_to(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
dst: &Tensor<u8>,
_dst_fmt: PixelFormat,
band: Option<edgefirst_tensor::ViewOrigin>,
is_int8: bool,
rotation: crate::Rotation,
flip: Flip,
crop: ResolvedCrop,
convert_span: &tracing::Span,
) -> Result<(), crate::Error> {
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
self.convert_to_dims(
src,
src_fmt,
dst_w,
dst_h,
band,
_dst_fmt,
is_int8,
rotation,
flip,
crop,
convert_span,
)
}
#[allow(clippy::too_many_arguments)]
fn convert_to_dims(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
dst_w: usize,
dst_h: usize,
dst_view_origin: Option<edgefirst_tensor::ViewOrigin>,
dst_fmt: PixelFormat,
is_int8: bool,
rotation: crate::Rotation,
flip: Flip,
crop: ResolvedCrop,
convert_span: &tracing::Span,
) -> Result<(), crate::Error> {
let src_w = src.width().ok_or(Error::NotAnImage)?;
let src_h = src.height().ok_or(Error::NotAnImage)?;
check_gl_error(function!(), line!())?;
struct ScissorGuard(bool);
impl Drop for ScissorGuard {
fn drop(&mut self) {
if self.0 {
unsafe { edgefirst_gl::gl::Disable(edgefirst_gl::gl::SCISSOR_TEST) };
}
}
}
let _scissor = match dst_view_origin {
Some(vo) => {
let vp = super::render::region_to_viewport_top_down(crate::Region::new(
vo.x, vo.y, dst_w, dst_h,
));
unsafe {
edgefirst_gl::gl::Scissor(vp.x, vp.y, vp.w, vp.h);
edgefirst_gl::gl::Enable(edgefirst_gl::gl::SCISSOR_TEST);
}
ScissorGuard(true)
}
None => ScissorGuard(false),
};
let has_crop = crop
.dst_rect
.is_some_and(|x| x.left != 0 || x.top != 0 || x.width != dst_w || x.height != dst_h);
if has_crop {
if let Some(dst_color) = crop.dst_color {
unsafe {
edgefirst_gl::gl::ClearColor(
dst_color[0] as f32 / 255.0,
dst_color[1] as f32 / 255.0,
dst_color[2] as f32 / 255.0,
dst_color[3] as f32 / 255.0,
);
edgefirst_gl::gl::Clear(edgefirst_gl::gl::COLOR_BUFFER_BIT);
};
}
}
let src_roi = if let Some(crop) = crop.src_rect {
RegionOfInterest::from_crop(&crop, src_w, src_h)
} else {
RegionOfInterest {
left: 0.,
top: 1.,
right: 1.,
bottom: 0.,
}
};
let cvt_screen_coord = |normalized| normalized * 2.0 - 1.0;
let dst_roi = if let Some(crop) = crop.dst_rect {
RegionOfInterest {
left: cvt_screen_coord(crop.left as f32 / dst_w as f32),
top: cvt_screen_coord((crop.top + crop.height) as f32 / dst_h as f32),
right: cvt_screen_coord((crop.left + crop.width) as f32 / dst_w as f32),
bottom: cvt_screen_coord(crop.top as f32 / dst_h as f32),
}
} else {
RegionOfInterest {
left: -1.,
top: 1.,
right: 1.,
bottom: -1.,
}
};
let rotation_offset = match rotation {
crate::Rotation::None => 0,
crate::Rotation::Clockwise90 => 1,
crate::Rotation::Rotate180 => 2,
crate::Rotation::CounterClockwise90 => 3,
};
let nv_rect = NvSampleRect::new(
crop.src_rect,
crop.dst_rect,
(src_w, src_h),
(dst_w, dst_h),
rotation,
);
if self.gl_context.transfer_backend.is_zero_copy() && src.memory() == TensorMemory::DmaBuf {
let chosen = self.select_nv_path(src, src_fmt, dst_fmt);
if !Platform::EXTERNAL_OES && chosen == NvConvertPath::ExternalSampler {
self.draw_src_texture(
src,
src_fmt,
src_roi,
dst_roi,
rotation_offset,
flip,
is_int8,
convert_span,
)?;
} else if chosen == NvConvertPath::ShaderR8 {
match self.get_or_create_nv_r8_egl_image(src, src_fmt) {
Ok(r8_egl) => {
tracing::trace!(
path = "ShaderR8",
src_fmt = ?src_fmt,
"image.convert.gl.nv_path"
);
self.convert_stats.src_imports += 1;
convert_span.record("src_feed", "import");
match self.draw_nv_texture_2d(
src,
src_fmt,
Some(r8_egl),
nv_rect,
dst_roi,
rotation_offset,
flip,
is_int8,
convert_span,
) {
Ok(()) => self.last_nv_convert_path = NvConvertPath::ShaderR8,
Err(e) => {
self.last_nv_convert_path = NvConvertPath::Cpu;
return Err(e);
}
}
}
Err(e) => {
let src_w = src.width().unwrap_or(0);
let src_h = src.height().unwrap_or(0);
self.convert_stats.zero_copy_declines += 1;
if self.nv_import_warned.insert(src.buffer_identity().id()) {
log::warn!(
"Path B R8 EGLImage creation failed for {src_fmt} \
({src_w}x{src_h}); uploading the combined plane instead: {e}"
);
} else {
log::debug!(
"Path B R8 EGLImage creation failed for {src_fmt} \
({src_w}x{src_h}) (repeat): {e}"
);
}
let start = Instant::now();
match self.draw_nv_texture_2d(
src,
src_fmt,
None,
nv_rect,
dst_roi,
rotation_offset,
flip,
is_int8,
convert_span,
) {
Ok(()) => self.last_nv_convert_path = NvConvertPath::ShaderR8,
Err(e) => {
self.last_nv_convert_path = NvConvertPath::Cpu;
return Err(e);
}
}
log::debug!("NV R8 upload takes {:?}", start.elapsed());
}
}
} else {
match self.get_or_create_egl_image(CacheKind::Src, src, src_fmt) {
Ok(src_egl) => {
if src_fmt == PixelFormat::Nv12 {
tracing::trace!(path = "ExternalSampler", src_fmt = ?src_fmt, "image.convert.gl.nv_path");
}
self.convert_stats.src_imports += 1;
convert_span.record("src_feed", "import");
match self.draw_camera_texture_eglimage(
src,
src_fmt,
src_egl,
src_roi,
dst_roi,
rotation_offset,
flip,
is_int8,
None,
) {
Ok(()) => {
if src_fmt == PixelFormat::Nv12 {
self.last_nv_convert_path = NvConvertPath::ExternalSampler;
}
}
Err(e) => {
if src_fmt == PixelFormat::Nv12 {
self.last_nv_convert_path = NvConvertPath::Cpu;
}
return Err(e);
}
}
}
Err(e) => {
let src_w = src.width().unwrap_or(0);
let src_h = src.height().unwrap_or(0);
let nv_upload_capable = matches!(
src_fmt,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24
) && !src.is_multiplane();
if src_fmt == PixelFormat::Nv12 && !nv_upload_capable {
self.last_nv_convert_path = NvConvertPath::Cpu;
}
self.convert_stats.zero_copy_declines += 1;
if self.nv_import_warned.insert(src.buffer_identity().id()) {
log::warn!(
"EGL image creation failed for {src_fmt} ({src_w}x{src_h}), \
falling back to texture upload (slower): {e}"
);
} else {
log::debug!(
"EGL image creation failed for {src_fmt} ({src_w}x{src_h}) \
(repeat): {e}"
);
}
let start = Instant::now();
if nv_upload_capable {
match self.draw_nv_texture_2d(
src,
src_fmt,
None,
nv_rect,
dst_roi,
rotation_offset,
flip,
is_int8,
convert_span,
) {
Ok(()) => {
if src_fmt == PixelFormat::Nv12 {
self.last_nv_convert_path = NvConvertPath::ShaderR8;
}
}
Err(e) => {
if src_fmt == PixelFormat::Nv12 {
self.last_nv_convert_path = NvConvertPath::Cpu;
}
return Err(e);
}
}
log::debug!("NV R8 upload takes {:?}", start.elapsed());
} else if matches!(
src_fmt,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24
) {
return Err(e);
} else {
self.draw_src_texture(
src,
src_fmt,
src_roi,
dst_roi,
rotation_offset,
flip,
is_int8,
convert_span,
)?;
log::debug!("draw_src_texture takes {:?}", start.elapsed());
}
}
}
}
} else if matches!(
src_fmt,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24
) && !src.is_multiplane()
{
tracing::trace!(path = "ShaderR8-upload", src_fmt = ?src_fmt, "image.convert.gl.nv_path");
match self.draw_nv_texture_2d(
src,
src_fmt,
None,
nv_rect,
dst_roi,
rotation_offset,
flip,
is_int8,
convert_span,
) {
Ok(()) => self.last_nv_convert_path = NvConvertPath::ShaderR8,
Err(e) => {
self.last_nv_convert_path = NvConvertPath::Cpu;
return Err(e);
}
}
} else {
if matches!(
src_fmt,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24
) {
self.last_nv_convert_path = NvConvertPath::Cpu;
}
let start = Instant::now();
self.draw_src_texture(
src,
src_fmt,
src_roi,
dst_roi,
rotation_offset,
flip,
is_int8,
convert_span,
)?;
log::debug!("draw_src_texture takes {:?}", start.elapsed());
}
if !self.defer_finish {
let start = Instant::now();
unsafe { edgefirst_gl::gl::Finish() };
log::debug!("gl_Finish takes {:?}", start.elapsed());
}
check_gl_error(function!(), line!())?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn convert_to_planar(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
dst: &Tensor<u8>,
dst_fmt: PixelFormat,
is_int8: bool,
rotation: crate::Rotation,
flip: Flip,
crop: ResolvedCrop,
convert_span: &tracing::Span,
) -> Result<(), crate::Error> {
let src_w = src.width().ok_or(Error::NotAnImage)?;
let src_h = src.height().ok_or(Error::NotAnImage)?;
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let alpha = match dst_fmt {
PixelFormat::PlanarRgb => false,
PixelFormat::PlanarRgba => true,
_ => {
return Err(crate::Error::NotSupported(
"Destination format must be PlanarRgb or PlanarRgba".to_string(),
));
}
};
log::trace!(
"convert_to_planar: int8={is_int8}, interpolation={:?}",
self.int8_interpolation_mode
);
let src_roi = if let Some(crop) = crop.src_rect {
RegionOfInterest::from_crop(&crop, src_w, src_h)
} else {
RegionOfInterest {
left: 0.,
top: 1.,
right: 1.,
bottom: 0.,
}
};
let cvt_screen_coord = |normalized| normalized * 2.0 - 1.0;
let dst_roi = if let Some(crop) = crop.dst_rect {
RegionOfInterest {
left: cvt_screen_coord(crop.left as f32 / dst_w as f32),
top: cvt_screen_coord((crop.top + crop.height) as f32 / dst_h as f32),
right: cvt_screen_coord((crop.left + crop.width) as f32 / dst_w as f32),
bottom: cvt_screen_coord(crop.top as f32 / dst_h as f32),
}
} else {
RegionOfInterest {
left: -1.,
top: 1.,
right: 1.,
bottom: -1.,
}
};
let rotation_offset = match rotation {
crate::Rotation::None => 0,
crate::Rotation::Clockwise90 => 1,
crate::Rotation::Rotate180 => 2,
crate::Rotation::CounterClockwise90 => 3,
};
let has_crop = crop
.dst_rect
.is_some_and(|x| x.left != 0 || x.top != 0 || x.width != dst_w || x.height != dst_h);
if has_crop {
if let Some(dst_color) = crop.dst_color {
self.clear_rect_planar(
dst_w,
dst_h,
[
dst_color[0] as f32 / 255.0,
dst_color[1] as f32 / 255.0,
dst_color[2] as f32 / 255.0,
dst_color[3] as f32 / 255.0,
],
alpha,
)?;
}
}
let src_key = BufferImportKey::from_tensor(src, src_fmt, false);
let src_egl = self.get_or_create_egl_image(CacheKind::Src, src, src_fmt)?;
self.convert_stats.src_imports += 1;
convert_span.record("src_feed", "import");
let src_map = self.cached_src_import_map(src, src_fmt);
let src_extent =
super::render::sample_clamp_rect_within((src_w, src_h), src_map, src_roi.uv_bounds());
let src_roi = super::render::scale_roi_to_import(src_roi, (src_w, src_h), src_map);
self.draw_camera_texture_to_rgb_planar(
src_key,
src_egl,
src_roi,
src_extent,
dst_roi,
rotation_offset,
flip,
alpha,
is_int8,
)?;
unsafe { edgefirst_gl::gl::Finish() };
check_gl_error(function!(), line!())?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn convert_to_packed_rgb(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
dst: &mut Tensor<u8>,
dst_fmt: PixelFormat,
is_int8: bool,
rotation: crate::Rotation,
flip: Flip,
crop: ResolvedCrop,
convert_span: &tracing::Span,
) -> crate::Result<()> {
if self.int_pad_output {
return Err(Error::NotSupported(
"GL two-pass integer convert cannot write an output-domain pad; CPU fallback"
.into(),
));
}
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let layout = edgefirst_tensor::packed_rgb888_layout(dst_w, dst_h).ok_or_else(|| {
crate::Error::NotSupported(format!(
"Packed RGB requires width%4==0 (W*3 bytes must divide into \
whole RGBA8888 texels), got width={dst_w}"
))
})?;
let render_w = layout.surface_w;
let render_h = layout.surface_h;
log::debug!(
"convert_to_packed_rgb: {dst_w}x{dst_h} -> {render_w}x{render_h} two-pass int8={is_int8}",
);
let _pass1 =
tracing::trace_span!("image.convert.gl.pack_rgb.pass1_rgba", dst_w, dst_h).entered();
self.ensure_packed_rgb_intermediate(dst_w, dst_h)?;
self.packed_rgb_fbo.bind();
unsafe {
edgefirst_gl::gl::FramebufferTexture2D(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::TEXTURE_2D,
self.packed_rgb_intermediate_tex.id,
0,
);
check_gl_error(function!(), line!())?;
edgefirst_gl::gl::Viewport(0, 0, dst_w as i32, dst_h as i32);
}
let saved_defer = self.defer_finish;
self.defer_finish = true;
let pass1 = self.convert_to(
src,
src_fmt,
dst,
dst_fmt,
None,
false,
rotation,
flip,
crop,
convert_span,
);
self.defer_finish = saved_defer;
pass1?;
drop(_pass1);
let _pass2 =
tracing::trace_span!("image.convert.gl.pack_rgb.pass2_pack", render_w, render_h)
.entered();
self.convert_fbo.bind();
let dest_egl = self.get_or_create_egl_image_rgb(
dst,
dst_fmt,
render_w,
render_h,
super::platform::PackedImportFormat::Rgba8888,
)?;
unsafe {
match self.cached_dst_renderbuffer(dst, dst_fmt) {
Some(rbo) => {
edgefirst_gl::gl::BindRenderbuffer(edgefirst_gl::gl::RENDERBUFFER, rbo);
edgefirst_gl::gl::FramebufferRenderbuffer(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::RENDERBUFFER,
rbo,
);
}
None => {
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(
edgefirst_gl::gl::TEXTURE_2D,
self.render_texture.id,
);
super::core::set_tex_filter(
edgefirst_gl::gl::TEXTURE_2D,
edgefirst_gl::gl::NEAREST,
);
Platform::attach_tex_image_2d(&self.gl_context, dest_egl)?;
self.render_texture.invalidate_egl_binding();
edgefirst_gl::gl::FramebufferTexture2D(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::TEXTURE_2D,
self.render_texture.id,
0,
);
}
}
check_gl_error(function!(), line!())?;
edgefirst_gl::gl::Viewport(0, 0, render_w as i32, render_h as i32);
}
let program = if is_int8 {
&self.packed_rgba8_int8_program_2d
} else {
&self.packed_rgba8_program_2d
};
unsafe {
edgefirst_gl::gl::UseProgram(program.id);
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE1);
edgefirst_gl::gl::BindTexture(
edgefirst_gl::gl::TEXTURE_2D,
self.packed_rgb_intermediate_tex.id,
);
super::core::set_tex_filter(edgefirst_gl::gl::TEXTURE_2D, edgefirst_gl::gl::NEAREST);
}
self.draw_fullscreen_quad()?;
unsafe {
edgefirst_gl::gl::BindTexture(edgefirst_gl::gl::TEXTURE_2D, 0);
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
};
unsafe { edgefirst_gl::gl::Finish() };
check_gl_error(function!(), line!())?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn convert_nv_to_planar_two_pass(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
dst: &mut Tensor<u8>,
dst_fmt: PixelFormat,
is_int8: bool,
rotation: crate::Rotation,
flip: Flip,
crop: ResolvedCrop,
lowering: super::render::DstLowering,
convert_span: &tracing::Span,
) -> crate::Result<()> {
if self.int_pad_output {
return Err(Error::NotSupported(
"GL two-pass integer convert cannot write an output-domain pad; CPU fallback"
.into(),
));
}
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let alpha = match dst_fmt {
PixelFormat::PlanarRgb => false,
PixelFormat::PlanarRgba => true,
_ => {
return Err(crate::Error::NotSupported(
"Destination format must be PlanarRgb or PlanarRgba".to_string(),
));
}
};
log::debug!(
"convert_nv_to_planar_two_pass: {src_fmt}→{dst_fmt} {dst_w}x{dst_h} int8={is_int8}",
);
let _pass1 = tracing::trace_span!("image.convert.gl.nv_to_planar.pass1_rgba", dst_w, dst_h)
.entered();
self.ensure_packed_rgb_intermediate(dst_w, dst_h)?;
self.packed_rgb_fbo.bind();
unsafe {
edgefirst_gl::gl::FramebufferTexture2D(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::TEXTURE_2D,
self.packed_rgb_intermediate_tex.id,
0,
);
check_gl_error(function!(), line!())?;
edgefirst_gl::gl::Viewport(0, 0, dst_w as i32, dst_h as i32);
}
let saved_defer = self.defer_finish;
self.defer_finish = true;
let pass1 = self.convert_to(
src,
src_fmt,
dst,
dst_fmt,
None,
false,
rotation,
flip,
crop,
convert_span,
);
self.defer_finish = saved_defer;
pass1?;
drop(_pass1);
let _pass2 = tracing::trace_span!(
"image.convert.gl.nv_to_planar.pass2_deinterleave",
dst_w,
dst_h
)
.entered();
let (target, _bound) = self.bind_dst(dst, dst_fmt, crop, lowering)?;
Self::record_dst_feed(convert_span, target);
let dst_roi = RegionOfInterest {
left: -1.,
top: 1.,
right: 1.,
bottom: -1.,
};
self.draw_intermediate_to_rgb_planar(dst_roi, alpha, is_int8)?;
unsafe { edgefirst_gl::gl::Finish() };
check_gl_error(function!(), line!())?;
if let DstTarget::Texture { readback } = target {
self.readback_rendered(dst, dst_fmt, readback.pbo_id())?;
}
Ok(())
}
fn ensure_packed_rgb_intermediate(&mut self, width: usize, height: usize) -> crate::Result<()> {
if self.packed_rgb_intermediate_size == (width, height) {
return Ok(());
}
unsafe {
edgefirst_gl::gl::BindTexture(
edgefirst_gl::gl::TEXTURE_2D,
self.packed_rgb_intermediate_tex.id,
);
super::core::set_tex_filter(edgefirst_gl::gl::TEXTURE_2D, edgefirst_gl::gl::NEAREST);
edgefirst_gl::gl::TexImage2D(
edgefirst_gl::gl::TEXTURE_2D,
0,
edgefirst_gl::gl::RGBA as i32,
width as i32,
height as i32,
0,
edgefirst_gl::gl::RGBA,
edgefirst_gl::gl::UNSIGNED_BYTE,
std::ptr::null(),
);
check_gl_error(function!(), line!())?;
}
self.packed_rgb_intermediate_size = (width, height);
Ok(())
}
fn draw_fullscreen_quad(&self) -> Result<(), crate::Error> {
unsafe {
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
let vertices: [f32; 12] = [
-1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0, ];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * vertices.len()) as isize,
vertices.as_ptr() as *const c_void,
);
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.texture_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.texture_buffer.buffer_index);
let tex_coords: [f32; 8] = [0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * tex_coords.len()) as isize,
tex_coords.as_ptr() as *const c_void,
);
let indices: [u32; 4] = [0, 1, 2, 3];
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_FAN,
indices.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
indices.as_ptr() as *const c_void,
);
}
check_gl_error(function!(), line!())?;
Ok(())
}
fn clear_rect_planar(
&self,
width: usize,
height: usize,
color: [f32; 4],
alpha: bool,
) -> Result<(), Error> {
let split = if alpha { 4 } else { 3 };
unsafe {
if color[..split].iter().all(|&c| c == color[0]) {
edgefirst_gl::gl::ClearColor(color[0], color[0], color[0], 1.0);
edgefirst_gl::gl::Clear(edgefirst_gl::gl::COLOR_BUFFER_BIT);
return Ok(());
}
edgefirst_gl::gl::Enable(edgefirst_gl::gl::SCISSOR_TEST);
for (i, c) in color.iter().enumerate().take(split) {
edgefirst_gl::gl::Scissor(0, (i * height) as i32, width as i32, height as i32);
edgefirst_gl::gl::ClearColor(*c, *c, *c, 1.0);
edgefirst_gl::gl::Clear(edgefirst_gl::gl::COLOR_BUFFER_BIT);
}
edgefirst_gl::gl::Disable(edgefirst_gl::gl::SCISSOR_TEST);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn draw_camera_texture_to_rgb_planar(
&mut self,
src_key: BufferImportKey,
egl_img: PlatformHandle,
src_roi: RegionOfInterest,
src_extent: [f32; 4],
mut dst_roi: RegionOfInterest,
rotation_offset: usize,
flip: Flip,
alpha: bool,
int8: bool,
) -> Result<(), Error> {
let texture_target = edgefirst_gl::gl::TEXTURE_EXTERNAL_OES;
match flip {
Flip::None => {}
Flip::Vertical => {
std::mem::swap(&mut dst_roi.top, &mut dst_roi.bottom);
}
Flip::Horizontal => {
std::mem::swap(&mut dst_roi.left, &mut dst_roi.right);
}
}
let program_id = if int8 {
&self.texture_program_planar_int8
} else {
&self.texture_program_planar
}
.as_ref()
.ok_or_else(|| {
Error::NotSupported("external-OES planar program unavailable on this platform".into())
})?
.id;
let raw_alpha_id = self.texture_program_planar.as_ref().map_or(0, |p| p.id);
unsafe {
edgefirst_gl::gl::UseProgram(program_id);
let [e0, e1, e2, e3] = src_extent;
edgefirst_gl::gl::Uniform4f(self.external_src_extent_loc(int8, true), e0, e1, e2, e3);
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(texture_target, self.camera_eglimage_texture.id);
super::core::set_tex_filter(texture_target, self.source_filter());
edgefirst_gl::gl::TexParameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_WRAP_S,
edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
);
edgefirst_gl::gl::TexParameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_WRAP_T,
edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
);
if self.camera_eglimage_texture.bind_egl_image_external(
&self.gl_context,
src_key,
egl_img,
)? {
check_gl_error(function!(), line!())?;
log::trace!(
"draw_camera_planar: bound new src EGLImage id={:#x}",
src_key.luma_id
);
} else {
log::trace!(
"draw_camera_planar: reusing bound src EGLImage id={:#x}",
src_key.luma_id
);
}
let y_centers = if alpha {
vec![-3.0 / 4.0, -1.0 / 4.0, 1.0 / 4.0, 3.0 / 4.0]
} else {
vec![-2.0 / 3.0, 0.0, 2.0 / 3.0]
};
let planes = y_centers.len() as f32;
let swizzles = [
edgefirst_gl::gl::RED,
edgefirst_gl::gl::GREEN,
edgefirst_gl::gl::BLUE,
edgefirst_gl::gl::ALPHA,
];
for (i, y_center) in y_centers.iter().enumerate() {
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
let camera_vertices: [f32; 12] = [
dst_roi.left,
dst_roi.top / planes + y_center,
0., dst_roi.right,
dst_roi.top / planes + y_center,
0., dst_roi.right,
dst_roi.bottom / planes + y_center,
0., dst_roi.left,
dst_roi.bottom / planes + y_center,
0., ];
edgefirst_gl::gl::BufferData(
edgefirst_gl::gl::ARRAY_BUFFER,
(size_of::<f32>() * camera_vertices.len()) as isize,
camera_vertices.as_ptr() as *const c_void,
edgefirst_gl::gl::DYNAMIC_DRAW,
);
edgefirst_gl::gl::BindBuffer(
edgefirst_gl::gl::ARRAY_BUFFER,
self.texture_buffer.id,
);
edgefirst_gl::gl::EnableVertexAttribArray(self.texture_buffer.buffer_index);
let texture_vertices: [f32; 16] = [
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
];
edgefirst_gl::gl::BufferData(
edgefirst_gl::gl::ARRAY_BUFFER,
(size_of::<f32>() * 8) as isize,
(texture_vertices[(rotation_offset * 2)..]).as_ptr() as *const c_void,
edgefirst_gl::gl::DYNAMIC_DRAW,
);
let vertices_index: [u32; 4] = [0, 1, 2, 3];
edgefirst_gl::gl::TexParameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_SWIZZLE_R,
swizzles[i] as i32,
);
if int8 && i == 3 {
self.set_plane_quant(program_id, None);
edgefirst_gl::gl::UseProgram(raw_alpha_id);
let [e0, e1, e2, e3] = src_extent;
edgefirst_gl::gl::Uniform4f(
self.external_src_extent_loc(false, true),
e0,
e1,
e2,
e3,
);
} else {
self.set_plane_quant(program_id, Some(i));
}
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_FAN,
vertices_index.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
vertices_index.as_ptr() as *const c_void,
);
}
if !(int8 && alpha) {
self.set_plane_quant(program_id, None);
}
edgefirst_gl::gl::TexParameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_SWIZZLE_R,
edgefirst_gl::gl::RED as i32,
);
check_gl_error(function!(), line!())?;
}
Ok(())
}
fn draw_intermediate_to_rgb_planar(
&self,
dst_roi: RegionOfInterest,
alpha: bool,
int8: bool,
) -> Result<(), Error> {
let texture_target = edgefirst_gl::gl::TEXTURE_2D;
unsafe {
let program = if int8 {
&self.texture_program_planar_int8_2d
} else {
&self.texture_program_planar_2d
};
edgefirst_gl::gl::UseProgram(program.id);
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(texture_target, self.packed_rgb_intermediate_tex.id);
super::core::set_tex_filter_clamp(texture_target, edgefirst_gl::gl::NEAREST);
check_gl_error(function!(), line!())?;
let y_centers = if alpha {
vec![-3.0 / 4.0, -1.0 / 4.0, 1.0 / 4.0, 3.0 / 4.0]
} else {
vec![-2.0 / 3.0, 0.0, 2.0 / 3.0]
};
let planes = y_centers.len() as f32;
let swizzles = [
edgefirst_gl::gl::RED,
edgefirst_gl::gl::GREEN,
edgefirst_gl::gl::BLUE,
edgefirst_gl::gl::ALPHA,
];
let src_roi = RegionOfInterest {
left: 0.0,
top: 1.0,
right: 1.0,
bottom: 0.0,
};
for (i, y_center) in y_centers.iter().enumerate() {
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
let camera_vertices: [f32; 12] = [
dst_roi.left,
dst_roi.top / planes + y_center,
0., dst_roi.right,
dst_roi.top / planes + y_center,
0., dst_roi.right,
dst_roi.bottom / planes + y_center,
0., dst_roi.left,
dst_roi.bottom / planes + y_center,
0., ];
edgefirst_gl::gl::BufferData(
edgefirst_gl::gl::ARRAY_BUFFER,
(size_of::<f32>() * camera_vertices.len()) as isize,
camera_vertices.as_ptr() as *const c_void,
edgefirst_gl::gl::DYNAMIC_DRAW,
);
edgefirst_gl::gl::BindBuffer(
edgefirst_gl::gl::ARRAY_BUFFER,
self.texture_buffer.id,
);
edgefirst_gl::gl::EnableVertexAttribArray(self.texture_buffer.buffer_index);
let texture_vertices: [f32; 8] = [
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
];
edgefirst_gl::gl::BufferData(
edgefirst_gl::gl::ARRAY_BUFFER,
(size_of::<f32>() * texture_vertices.len()) as isize,
texture_vertices.as_ptr() as *const c_void,
edgefirst_gl::gl::DYNAMIC_DRAW,
);
let vertices_index: [u32; 4] = [0, 1, 2, 3];
edgefirst_gl::gl::TexParameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_SWIZZLE_R,
swizzles[i] as i32,
);
if int8 && i == 3 {
self.set_plane_quant(program.id, None);
edgefirst_gl::gl::UseProgram(self.texture_program_planar_2d.id);
} else {
self.set_plane_quant(program.id, Some(i));
}
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_FAN,
vertices_index.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
vertices_index.as_ptr() as *const c_void,
);
}
if !(int8 && alpha) {
self.set_plane_quant(program.id, None);
}
edgefirst_gl::gl::TexParameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_SWIZZLE_R,
edgefirst_gl::gl::RED as i32,
);
check_gl_error(function!(), line!())?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn draw_src_texture(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
mut src_roi: RegionOfInterest,
mut dst_roi: RegionOfInterest,
rotation_offset: usize,
flip: Flip,
is_int8: bool,
convert_span: &tracing::Span,
) -> Result<(), Error> {
let src_w = src.width().ok_or(Error::NotAnImage)?;
let src_h = src.height().ok_or(Error::NotAnImage)?;
let texture_target = edgefirst_gl::gl::TEXTURE_2D;
let zero_copy_attach = if !self.gl_context.transfer_backend.is_dma()
&& self.gl_context.transfer_backend.is_zero_copy()
&& src.memory() == TensorMemory::DmaBuf
&& matches!(
src_fmt,
PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::Yuyv
) {
let key = BufferImportKey::from_tensor(src, src_fmt, false);
match self.get_or_create_egl_image(CacheKind::Src, src, src_fmt) {
Ok(handle) => Some((key, handle)),
Err(e) => {
self.convert_stats.zero_copy_declines += 1;
log::debug!("zero-copy source import failed ({e:?}); uploading instead");
None
}
}
} else {
None
};
let import_map = if zero_copy_attach.is_some() {
self.cached_src_import_map(src, src_fmt)
} else {
super::render::ImportMap::WHOLE
};
let [u0, v0, u1, v1] = super::render::sample_clamp_rect_within(
(src_w, src_h),
import_map,
src_roi.uv_bounds(),
);
src_roi = super::render::scale_roi_to_import(src_roi, (src_w, src_h), import_map);
let texture_format = match src_fmt {
PixelFormat::Rgb => edgefirst_gl::gl::RGB,
PixelFormat::Rgba => edgefirst_gl::gl::RGBA,
PixelFormat::Grey => edgefirst_gl::gl::RED,
PixelFormat::Yuyv => edgefirst_gl::gl::RG,
_ => {
return Err(Error::NotSupported(format!(
"draw_src_texture does not support {src_fmt:?} (use DMA-BUF path for YUV)",
)));
}
};
let program_id = if src_fmt == PixelFormat::Yuyv {
if is_int8 {
self.yuyv_int8_program_2d.id
} else {
self.yuyv_program_2d.id
}
} else if is_int8 {
self.texture_int8_program.id
} else {
self.texture_program.id
};
unsafe {
edgefirst_gl::gl::UseProgram(program_id);
if src_fmt == PixelFormat::Yuyv {
let cm = crate::colorimetry::resolve_colorimetry(src.colorimetry(), src.height());
let coeffs = crate::colorimetry::yuv_to_rgb_coeffs(
cm.encoding
.unwrap_or(edgefirst_tensor::ColorEncoding::Bt709),
cm.range.unwrap_or(edgefirst_tensor::ColorRange::Limited),
);
let [src_size, y_offset, y_scale, c_vr, c_ug, c_vg, c_ub, src_extent] =
self.yuyv_2d_locs[usize::from(is_int8)];
debug_assert_eq!(
import_map.origin,
(0, 0),
"YUYV must not be sampled through a shifted import"
);
let (grid_w, grid_h) = import_map
.extent
.map_or((src_w as f32, src_h as f32), |(pw, ph)| {
(pw as f32, ph as f32)
});
edgefirst_gl::gl::Uniform2f(src_size, grid_w, grid_h);
edgefirst_gl::gl::Uniform4f(src_extent, u0, v0, u1, v1);
edgefirst_gl::gl::Uniform1f(y_offset, coeffs.y_offset);
edgefirst_gl::gl::Uniform1f(y_scale, coeffs.y_scale);
edgefirst_gl::gl::Uniform1f(c_vr, coeffs.c_vr);
edgefirst_gl::gl::Uniform1f(c_ug, coeffs.c_ug);
edgefirst_gl::gl::Uniform1f(c_vg, coeffs.c_vg);
edgefirst_gl::gl::Uniform1f(c_ub, coeffs.c_ub);
} else {
edgefirst_gl::gl::Uniform4f(self.texture_src_extent_loc(is_int8), u0, v0, u1, v1);
}
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(texture_target, self.camera_normal_texture.id);
super::core::set_tex_filter_clamp(texture_target, self.source_filter());
if src_fmt == PixelFormat::Grey {
for swizzle in [
edgefirst_gl::gl::TEXTURE_SWIZZLE_R,
edgefirst_gl::gl::TEXTURE_SWIZZLE_G,
edgefirst_gl::gl::TEXTURE_SWIZZLE_B,
] {
edgefirst_gl::gl::TexParameteri(
edgefirst_gl::gl::TEXTURE_2D,
swizzle,
edgefirst_gl::gl::RED as i32,
);
}
} else {
for (swizzle, src_comp) in [
(edgefirst_gl::gl::TEXTURE_SWIZZLE_R, edgefirst_gl::gl::RED),
(edgefirst_gl::gl::TEXTURE_SWIZZLE_G, edgefirst_gl::gl::GREEN),
(edgefirst_gl::gl::TEXTURE_SWIZZLE_B, edgefirst_gl::gl::BLUE),
] {
edgefirst_gl::gl::TexParameteri(
edgefirst_gl::gl::TEXTURE_2D,
swizzle,
src_comp as i32,
);
}
}
if let Some((key, handle)) = zero_copy_attach {
self.camera_normal_texture
.bind_egl_image(&self.gl_context, key, handle)?;
self.camera_normal_texture.forget_storage();
self.convert_stats.src_imports += 1;
convert_span.record("src_feed", "import");
} else {
let src_bpp = src_fmt.channels().max(1);
let row_len_px = unpack_row_length(
src.effective_row_stride(),
src_w,
src_bpp,
&format!("{src_fmt:?}"),
)?;
let pixels = SrcPixels::open(src, &self.owned_pbos)?;
pixels.record(&mut self.convert_stats, convert_span);
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::UNPACK_ROW_LENGTH, row_len_px);
let row_stride_b = if row_len_px == 0 {
src_w * src_bpp
} else {
row_len_px as usize * src_bpp
};
let required = if src_h == 0 {
0
} else {
(src_h - 1) * row_stride_b + src_w * src_bpp
};
let uploaded = self.camera_normal_texture.upload(
texture_target,
src_w,
src_h,
texture_format,
required,
pixels.source(),
);
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::UNPACK_ROW_LENGTH, 0);
uploaded?;
}
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
match flip {
Flip::None => {}
Flip::Vertical => {
std::mem::swap(&mut dst_roi.top, &mut dst_roi.bottom);
}
Flip::Horizontal => {
std::mem::swap(&mut dst_roi.left, &mut dst_roi.right);
}
}
let camera_vertices: [f32; 12] = [
dst_roi.left,
dst_roi.top,
0., dst_roi.right,
dst_roi.top,
0., dst_roi.right,
dst_roi.bottom,
0., dst_roi.left,
dst_roi.bottom,
0., ];
edgefirst_gl::gl::BufferData(
edgefirst_gl::gl::ARRAY_BUFFER,
(size_of::<f32>() * camera_vertices.len()) as isize,
camera_vertices.as_ptr() as *const c_void,
edgefirst_gl::gl::DYNAMIC_DRAW,
);
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.texture_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.texture_buffer.buffer_index);
let texture_vertices: [f32; 16] = [
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
];
edgefirst_gl::gl::BufferData(
edgefirst_gl::gl::ARRAY_BUFFER,
(size_of::<f32>() * 8) as isize,
(texture_vertices[(rotation_offset * 2)..]).as_ptr() as *const c_void,
edgefirst_gl::gl::DYNAMIC_DRAW,
);
let vertices_index: [u32; 4] = [0, 1, 2, 3];
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_FAN,
vertices_index.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
vertices_index.as_ptr() as *const c_void,
);
check_gl_error(function!(), line!())?;
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
fn draw_camera_texture_eglimage(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
egl_img: PlatformHandle,
src_roi: RegionOfInterest,
mut dst_roi: RegionOfInterest,
rotation_offset: usize,
flip: Flip,
is_int8: bool,
y2y: Option<(
edgefirst_tensor::ColorEncoding,
edgefirst_tensor::ColorRange,
)>,
) -> Result<(), Error> {
let src_key = BufferImportKey::from_tensor(src, src_fmt, false);
let luma_id = src_key.luma_id;
let crop_uv = src_roi.uv_bounds();
let src_roi = self.scale_src_roi(src_roi, src, src_fmt);
let (src_w, src_h) = (
src.width().ok_or(Error::NotAnImage)?,
src.height().ok_or(Error::NotAnImage)?,
);
let [e0, e1, e2, e3] = super::render::sample_clamp_rect_within(
(src_w, src_h),
self.cached_src_import_map(src, src_fmt),
crop_uv,
);
let (program_id, extent_loc, y2y_upload) = match y2y {
Some(colorimetry) => {
let (programs, uniforms) = self.nv_y2y_programs.as_ref().ok_or_else(|| {
Error::NotSupported("Y2Y NV programs unavailable on this GPU".into())
})?;
let state = &uniforms[usize::from(is_int8)];
let upload = (state.last_colorimetry != Some(colorimetry))
.then_some((state.locs, colorimetry));
(
programs[usize::from(is_int8)].id,
state.locs.src_extent,
upload,
)
}
None => {
let program = if is_int8 {
&self.texture_int8_program_yuv
} else {
&self.texture_program_yuv
}
.as_ref()
.ok_or_else(|| {
Error::NotSupported(
"external-OES sampler program unavailable on this platform".into(),
)
})?;
(
program.id,
self.external_src_extent_loc(is_int8, false),
None,
)
}
};
let texture_target = edgefirst_gl::gl::TEXTURE_EXTERNAL_OES;
unsafe {
edgefirst_gl::gl::UseProgram(program_id);
edgefirst_gl::gl::Uniform4f(extent_loc, e0, e1, e2, e3);
if let Some((locs, (encoding, range))) = y2y_upload {
let k = crate::colorimetry::yuv_to_rgb_coeffs(encoding, range);
edgefirst_gl::gl::Uniform1f(locs.y_offset, k.y_offset);
edgefirst_gl::gl::Uniform1f(locs.y_scale, k.y_scale);
edgefirst_gl::gl::Uniform1f(locs.c_vr, k.c_vr);
edgefirst_gl::gl::Uniform1f(locs.c_ug, k.c_ug);
edgefirst_gl::gl::Uniform1f(locs.c_vg, k.c_vg);
edgefirst_gl::gl::Uniform1f(locs.c_ub, k.c_ub);
}
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(texture_target, self.camera_eglimage_texture.id);
super::core::set_tex_filter_clamp(texture_target, self.source_filter());
if self.camera_eglimage_texture.bind_egl_image_external(
&self.gl_context,
src_key,
egl_img,
)? {
check_gl_error(function!(), line!())?;
log::trace!("draw_camera: bound new src EGLImage id={luma_id:#x}");
} else {
log::trace!("draw_camera: reusing bound src EGLImage id={luma_id:#x}");
}
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
match flip {
Flip::None => {}
Flip::Vertical => {
std::mem::swap(&mut dst_roi.top, &mut dst_roi.bottom);
}
Flip::Horizontal => {
std::mem::swap(&mut dst_roi.left, &mut dst_roi.right);
}
}
let camera_vertices: [f32; 12] = [
dst_roi.left,
dst_roi.top,
0., dst_roi.right,
dst_roi.top,
0., dst_roi.right,
dst_roi.bottom,
0., dst_roi.left,
dst_roi.bottom,
0., ];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * camera_vertices.len()) as isize,
camera_vertices.as_ptr() as *const c_void,
);
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.texture_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.texture_buffer.buffer_index);
let texture_vertices: [f32; 16] = [
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * 8) as isize,
(texture_vertices[(rotation_offset * 2)..]).as_ptr() as *const c_void,
);
let vertices_index: [u32; 4] = [0, 1, 2, 3];
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_FAN,
vertices_index.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
vertices_index.as_ptr() as *const c_void,
);
}
check_gl_error(function!(), line!())?;
if let (Some(colorimetry), Some((_, uniforms))) = (y2y, self.nv_y2y_programs.as_mut()) {
uniforms[usize::from(is_int8)].last_colorimetry = Some(colorimetry);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn draw_nv_texture_2d(
&mut self,
src: &Tensor<u8>,
src_fmt: PixelFormat,
r8_src: Option<PlatformHandle>,
src_rect: NvSampleRect,
mut dst_roi: RegionOfInterest,
rotation_offset: usize,
flip: Flip,
is_int8: bool,
convert_span: &tracing::Span,
) -> Result<(), Error> {
let src_w = src.width().ok_or(Error::NotAnImage)?;
let src_h = src.height().ok_or(Error::NotAnImage)?;
let tex_width = src.effective_row_stride().unwrap_or(src_w) as i32;
let layout = src_fmt.chroma_layout().ok_or_else(|| {
Error::NotSupported(format!(
"draw_nv_texture_2d: unsupported format {src_fmt:?}"
))
})?;
let chroma_shift_x = layout.shift_x as i32;
let chroma_shift_y = layout.shift_y as i32;
let chroma_lines = layout.uv_rows_per_luma as i32;
let src_key = BufferImportKey::from_tensor(src, src_fmt, false);
let luma_id = src_key.luma_id;
let bilinear =
src_rect.resample && self.interpolation.supported() == crate::Interpolation::Bilinear;
let uv_import = if bilinear && self.nv_hw_filter && r8_src.is_some() {
self.get_or_create_nv_uv_egl_image(src, src_fmt)
.inspect_err(|e| log::debug!("NV chroma view unavailable ({e}); shader-filtered"))
.ok()
} else {
None
};
let mut sampling = NvSampling::choose(NvSamplingInputs {
bilinear,
hw_filter: self.nv_hw_filter,
imported: r8_src.is_some(),
chroma_view: uv_import.is_some(),
y2y: self.nv_y2y_programs.is_some()
&& src_fmt == PixelFormat::Nv12
&& y2y_pays_off(src_w * src_h, src_rect.dst_pixels),
});
let colorimetry = nv_colorimetry(src.colorimetry(), src.height());
if sampling == NvSampling::HardwareYuv {
match self.get_or_create_egl_image(CacheKind::Src, src, src_fmt) {
Ok(img) => {
self.draw_camera_texture_eglimage(
src,
src_fmt,
img,
src_rect.roi(src_w, src_h),
dst_roi,
rotation_offset,
flip,
is_int8,
Some(colorimetry),
)?;
self.last_nv_sampling = Some(sampling);
return Ok(());
}
Err(e) => {
log::debug!("NV12 import for Y2Y unavailable ({e}); shader-filtered");
sampling = NvSampling::Shader;
}
}
}
let program = sampling.program(is_int8);
let r8_filter = if sampling == NvSampling::Hardware {
edgefirst_gl::gl::LINEAR
} else {
edgefirst_gl::gl::NEAREST
};
let (shift_x, shift_y) = (layout.shift_x, layout.shift_y);
let chroma_w = src_w.div_ceil(1 << shift_x);
let chroma_h = src_h.div_ceil(1 << shift_y);
let combined_h = src_fmt.combined_plane_height(src_h).ok_or_else(|| {
Error::NotSupported(format!(
"draw_nv_texture_2d: {src_fmt:?} is not semi-planar"
))
})?;
let prog_id = self.nv_r8_programs[program].id;
let state = &mut self.nv_r8_uniforms[program];
let locs = state.locs;
let upload_colorimetry = state.last_colorimetry != Some(colorimetry);
unsafe {
edgefirst_gl::gl::UseProgram(prog_id);
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::gl::BindTexture(edgefirst_gl::gl::TEXTURE_2D, self.nv_r8_texture.id);
super::core::set_tex_filter_clamp(edgefirst_gl::gl::TEXTURE_2D, r8_filter);
match r8_src {
Some(egl_img) => {
if self
.nv_r8_texture
.bind_egl_image(&self.gl_context, src_key, egl_img)?
{
check_gl_error(function!(), line!())?;
log::trace!("draw_nv_texture_2d: bound new R8 EGLImage id={luma_id:#x}");
} else {
log::trace!(
"draw_nv_texture_2d: reusing bound R8 EGLImage id={luma_id:#x}"
);
}
}
None => {
let combined_h = src_fmt.combined_plane_height(src_h).ok_or_else(|| {
Error::NotSupported(format!(
"draw_nv_texture_2d upload: {src_fmt:?} is not semi-planar"
))
})?;
let needed = tex_width as usize * combined_h;
let pixels = SrcPixels::open(src, &self.owned_pbos)?;
pixels.record(&mut self.convert_stats, convert_span);
if self.is_adreno {
self.nv_r8_texture = Texture::new();
edgefirst_gl::gl::BindTexture(
edgefirst_gl::gl::TEXTURE_2D,
self.nv_r8_texture.id,
);
super::core::set_tex_filter_clamp(edgefirst_gl::gl::TEXTURE_2D, r8_filter);
}
self.nv_r8_texture.forget_storage();
self.nv_r8_texture.upload(
edgefirst_gl::gl::TEXTURE_2D,
tex_width as usize,
combined_h,
edgefirst_gl::gl::RED,
needed,
pixels.source(),
)?;
check_gl_error(function!(), line!())?;
log::trace!("draw_nv_texture_2d: uploaded R8 ({tex_width}x{combined_h})");
}
}
if sampling == NvSampling::Hardware {
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE1);
let bound = self.bind_nv_chroma(src, src_key, uv_import, chroma_w, chroma_h);
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
bound?;
}
edgefirst_gl::gl::Uniform2i(locs.img_size, src_w as i32, src_h as i32);
edgefirst_gl::gl::Uniform1i(locs.tex_width, tex_width);
edgefirst_gl::gl::Uniform2i(locs.chroma_shift, chroma_shift_x, chroma_shift_y);
edgefirst_gl::gl::Uniform1i(locs.chroma_lines, chroma_lines);
if sampling == NvSampling::Hardware {
let (tw, th) = (tex_width as f32, combined_h as f32);
edgefirst_gl::gl::Uniform2f(locs.luma_scale, src_w as f32 / tw, src_h as f32 / th);
edgefirst_gl::gl::Uniform4f(
locs.luma_clamp,
(src_rect.left as f32 + 0.5) / tw,
(src_rect.top as f32 + 0.5) / th,
(src_rect.right as f32 - 0.5) / tw,
(src_rect.bottom as f32 - 0.5) / th,
);
let (cw, ch) = (chroma_w as f32, chroma_h as f32);
edgefirst_gl::gl::Uniform2f(
locs.chroma_scale,
src_w as f32 / (chroma_w << shift_x) as f32,
src_h as f32 / (chroma_h << shift_y) as f32,
);
edgefirst_gl::gl::Uniform4f(
locs.chroma_clamp,
((src_rect.left >> shift_x) as f32 + 0.5) / cw,
((src_rect.top >> shift_y) as f32 + 0.5) / ch,
(src_rect.right.div_ceil(1 << shift_x) as f32 - 0.5) / cw,
(src_rect.bottom.div_ceil(1 << shift_y) as f32 - 0.5) / ch,
);
}
edgefirst_gl::gl::Uniform4i(
locs.src_rect,
src_rect.left as i32,
src_rect.top as i32,
src_rect.right as i32 - 1,
src_rect.bottom as i32 - 1,
);
if upload_colorimetry {
let coeffs = crate::colorimetry::yuv_to_rgb_coeffs(colorimetry.0, colorimetry.1);
edgefirst_gl::gl::Uniform1f(locs.y_offset, coeffs.y_offset);
edgefirst_gl::gl::Uniform1f(locs.y_scale, coeffs.y_scale);
edgefirst_gl::gl::Uniform1f(locs.c_vr, coeffs.c_vr);
edgefirst_gl::gl::Uniform1f(locs.c_ug, coeffs.c_ug);
edgefirst_gl::gl::Uniform1f(locs.c_vg, coeffs.c_vg);
edgefirst_gl::gl::Uniform1f(locs.c_ub, coeffs.c_ub);
}
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
match flip {
Flip::None => {}
Flip::Vertical => {
std::mem::swap(&mut dst_roi.top, &mut dst_roi.bottom);
}
Flip::Horizontal => {
std::mem::swap(&mut dst_roi.left, &mut dst_roi.right);
}
}
let camera_vertices: [f32; 12] = [
dst_roi.left,
dst_roi.top,
0.,
dst_roi.right,
dst_roi.top,
0.,
dst_roi.right,
dst_roi.bottom,
0.,
dst_roi.left,
dst_roi.bottom,
0.,
];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * camera_vertices.len()) as isize,
camera_vertices.as_ptr() as *const c_void,
);
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.texture_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.texture_buffer.buffer_index);
let src_roi = RegionOfInterest {
left: src_rect.left as f32 / src_w as f32,
top: src_rect.bottom as f32 / src_h as f32,
right: src_rect.right as f32 / src_w as f32,
bottom: src_rect.top as f32 / src_h as f32,
};
let texture_vertices: [f32; 16] = [
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * 8) as isize,
(texture_vertices[(rotation_offset * 2)..]).as_ptr() as *const c_void,
);
let vertices_index: [u32; 4] = [0, 1, 2, 3];
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_FAN,
vertices_index.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
vertices_index.as_ptr() as *const c_void,
);
if sampling == NvSampling::Hardware {
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE1);
edgefirst_gl::gl::BindTexture(edgefirst_gl::gl::TEXTURE_2D, 0);
edgefirst_gl::gl::ActiveTexture(edgefirst_gl::gl::TEXTURE0);
}
}
self.last_nv_sampling = Some(sampling);
check_gl_error(function!(), line!())?;
if upload_colorimetry {
self.nv_r8_uniforms[program].last_colorimetry = Some(colorimetry);
}
Ok(())
}
fn get_or_create_nv_r8_egl_image(
&mut self,
img: &Tensor<u8>,
img_fmt: PixelFormat,
) -> Result<PlatformHandle, crate::Error> {
Platform::validate_import_identity(img, "NV source")?;
let offset = img.plane_offset().unwrap_or(0);
if rejects_unaligned_import_offset(self.misplaces_unaligned_offsets(), offset) {
return Err(crate::Error::NotSupported(format!(
"this GPU misreads a DMA-BUF imported at a plane offset that \
is not {DMA_IMPORT_OFFSET_ALIGN}-byte aligned ({offset}); \
uploading the window instead (issue #165)"
)));
}
let id = BufferImportKey::from_tensor(img, img_fmt, false);
let luma_id = id.luma_id;
{
let ts = self.nv_r8_egl_cache.next_timestamp();
if let Some(cached) = self.nv_r8_egl_cache.entries.get_mut(&id) {
self.nv_r8_egl_cache.hits += 1;
cached.last_used = ts;
log::trace!("nv_r8_egl_cache hit: id={luma_id:#x}");
return Ok(Platform::import_handle(&cached.import));
}
self.nv_r8_egl_cache.misses += 1;
log::trace!("nv_r8_egl_cache miss: id={luma_id:#x}");
}
let egl_image_obj = Platform::import_buffer_nv_r8(&self.gl_context, img, img_fmt)?;
self.nv_r8_texture.invalidate_egl_binding();
let handle = Platform::import_handle(&egl_image_obj);
self.nv_r8_egl_cache.insert(id, egl_image_obj, None);
Ok(handle)
}
fn bind_nv_chroma(
&mut self,
src: &Tensor<u8>,
src_key: BufferImportKey,
import: Option<PlatformHandle>,
chroma_w: usize,
chroma_h: usize,
) -> Result<(), Error> {
if self.is_adreno && import.is_none() {
self.nv_uv_texture = Texture::new();
}
unsafe {
edgefirst_gl::gl::BindTexture(edgefirst_gl::gl::TEXTURE_2D, self.nv_uv_texture.id);
super::core::set_tex_filter_clamp(
edgefirst_gl::gl::TEXTURE_2D,
edgefirst_gl::gl::LINEAR,
);
}
if let Some(handle) = import {
unsafe {
self.nv_uv_texture
.bind_egl_image(&self.gl_context, src_key, handle)?;
}
return check_gl_error(function!(), line!());
}
let src_w = src.width().ok_or(Error::NotAnImage)?;
let src_h = src.height().ok_or(Error::NotAnImage)?;
let layout = src
.format()
.and_then(|f| f.chroma_layout())
.ok_or_else(|| Error::NotSupported("NV chroma upload: not semi-planar".into()))?;
let stride = src.effective_row_stride().unwrap_or(src_w);
let pitch = stride * layout.uv_rows_per_luma;
let pixels = SrcPixels::open(src, &self.owned_pbos)?;
self.nv_uv_texture.forget_storage();
unsafe {
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::UNPACK_ROW_LENGTH, (pitch / 2) as i32);
}
let uploaded = self.nv_uv_texture.upload(
edgefirst_gl::gl::TEXTURE_2D,
chroma_w,
chroma_h,
edgefirst_gl::gl::RG,
pitch * (chroma_h - 1) + chroma_w * 2,
pixels.source().offset_by(stride * src_h),
);
unsafe {
edgefirst_gl::gl::PixelStorei(edgefirst_gl::gl::UNPACK_ROW_LENGTH, 0);
}
uploaded?;
check_gl_error(function!(), line!())
}
fn get_or_create_nv_uv_egl_image(
&mut self,
img: &Tensor<u8>,
img_fmt: PixelFormat,
) -> Result<PlatformHandle, crate::Error> {
if !Platform::NV_CHROMA_IMPORT || self.nv_uv_import_refused {
return Err(crate::Error::NotSupported(
"no chroma-plane import on this platform or driver".into(),
));
}
Platform::validate_import_identity(img, "NV chroma source")?;
let height = img.height().ok_or(Error::NotAnImage)?;
let stride = img
.effective_row_stride()
.unwrap_or(img.width().ok_or(Error::NotAnImage)?);
let offset = img.plane_offset().unwrap_or(0) + stride * height;
if rejects_unaligned_import_offset(self.misplaces_unaligned_offsets(), offset) {
return Err(crate::Error::NotSupported(format!(
"chroma plane offset {offset} is not {DMA_IMPORT_OFFSET_ALIGN}-byte aligned"
)));
}
let id = BufferImportKey::from_tensor(img, img_fmt, false);
{
let ts = self.nv_uv_egl_cache.next_timestamp();
if let Some(cached) = self.nv_uv_egl_cache.entries.get_mut(&id) {
self.nv_uv_egl_cache.hits += 1;
cached.last_used = ts;
return Ok(Platform::import_handle(&cached.import));
}
}
let egl_image_obj = Platform::import_buffer_nv_chroma(&self.gl_context, img, img_fmt)
.inspect_err(|e| {
log::info!("NV chroma-plane import refused ({e}); resizing with the shader filter");
self.nv_uv_import_refused = true;
})?;
self.nv_uv_egl_cache.misses += 1;
self.nv_uv_texture.invalidate_egl_binding();
let handle = Platform::import_handle(&egl_image_obj);
self.nv_uv_egl_cache.insert(id, egl_image_obj, None);
Ok(handle)
}
fn get_or_create_egl_image(
&mut self,
cache: CacheKind,
img: &Tensor<u8>,
img_fmt: PixelFormat,
) -> Result<PlatformHandle, crate::Error> {
#[cfg(test)]
if cache == CacheKind::Dst && self.fail_dst_import {
return Err(crate::Error::OpenGl(
"EDGEFIRST test hook: zero-copy destination import refused".to_string(),
));
}
Platform::validate_import_identity(
img,
match cache {
CacheKind::Src => "source",
CacheKind::Dst => "destination",
},
)?;
if cache == CacheKind::Src {
let offset = source_import_plane0_offset(img, img_fmt);
let chroma_offset = (img_fmt == PixelFormat::Nv12).then(|| {
if img.is_multiplane() {
img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0)
} else {
nv12_plane1_offset(
offset,
img.effective_row_stride()
.unwrap_or_else(|| img.width().unwrap_or(0)),
img.height().unwrap_or(0),
)
}
});
let planes = [
Some(("plane 0", offset)),
chroma_offset.map(|o| ("chroma plane", o)),
];
if let Some((which, bad)) = planes.into_iter().flatten().find(|&(_, o)| {
rejects_unaligned_import_offset(self.misplaces_unaligned_offsets(), o)
}) {
return Err(crate::Error::NotSupported(format!(
"this GPU misreads a DMA-BUF imported at a plane offset that \
is not {DMA_IMPORT_OFFSET_ALIGN}-byte aligned ({which} at \
{bad}); uploading the window instead (issue #165)"
)));
}
if self.is_adreno && chroma_in_separate_buffer(img) {
return Err(crate::Error::NotSupported(
"Adreno misreads a semi-planar source whose chroma is a \
separate DMA-BUF (only the first buffer is imported)"
.into(),
));
}
}
let id = BufferImportKey::from_tensor(img, img_fmt, cache == CacheKind::Dst);
let luma_id = id.luma_id;
{
let egl_cache = match cache {
CacheKind::Src => &mut self.src_egl_cache,
CacheKind::Dst => &mut self.dst_egl_cache,
};
let ts = egl_cache.next_timestamp();
if let Some(cached) = egl_cache.entries.get_mut(&id) {
egl_cache.hits += 1;
cached.last_used = ts;
log::trace!("ImportCache {:?} hit: id={luma_id:#x}", cache);
return Ok(Platform::import_handle(&cached.import));
}
egl_cache.misses += 1;
log::trace!("ImportCache {:?} miss: id={luma_id:#x}", cache);
}
let for_dst = cache == CacheKind::Dst;
let egl_image_obj = Platform::import_buffer(&self.gl_context, img, img_fmt, for_dst)?;
let rbo = if cache == CacheKind::Dst && self.use_renderbuffer {
let mut rbo: u32 = 0;
unsafe {
edgefirst_gl::gl::GenRenderbuffers(1, &mut rbo);
edgefirst_gl::gl::BindRenderbuffer(edgefirst_gl::gl::RENDERBUFFER, rbo);
Platform::attach_renderbuffer_storage(
&self.gl_context,
Platform::import_handle(&egl_image_obj),
)?;
if let Err(e) = check_gl_error(function!(), line!()) {
edgefirst_gl::gl::DeleteRenderbuffers(1, &rbo);
return Err(e);
}
}
Some(rbo)
} else {
None
};
match cache {
CacheKind::Src => self.invalidate_src_textures(),
CacheKind::Dst => self.invalidate_dst_textures(),
}
let handle = Platform::import_handle(&egl_image_obj);
let egl_cache = match cache {
CacheKind::Src => &mut self.src_egl_cache,
CacheKind::Dst => &mut self.dst_egl_cache,
};
egl_cache.insert(id, egl_image_obj, rbo);
Ok(handle)
}
fn cached_src_import_map(
&self,
img: &Tensor<u8>,
fmt: PixelFormat,
) -> super::render::ImportMap {
let id = BufferImportKey::from_tensor(img, fmt, false);
self.src_egl_cache
.entries
.get(&id)
.map(|entry| {
let map = super::render::ImportMap {
extent: Platform::import_extent(&entry.import),
origin: Platform::import_origin(&entry.import),
};
debug_assert!(
map.origin == (0, 0) || map.extent.is_some(),
"an import origin {:?} without an extent is dropped by the fold",
map.origin,
);
map
})
.unwrap_or(super::render::ImportMap::WHOLE)
}
fn scale_src_roi(
&self,
roi: RegionOfInterest,
img: &Tensor<u8>,
fmt: PixelFormat,
) -> RegionOfInterest {
let (Some(w), Some(h)) = (img.width(), img.height()) else {
return roi;
};
super::render::scale_roi_to_import(roi, (w, h), self.cached_src_import_map(img, fmt))
}
fn texture_src_extent_loc(&self, is_int8: bool) -> i32 {
let [plain, int8] = self.texture_src_extent_locs;
if is_int8 {
int8
} else {
plain
}
}
fn external_src_extent_loc(&self, is_int8: bool, planar: bool) -> i32 {
let [yuv, yuv_int8, pl, pl_int8] = self.external_src_extent_locs;
match (planar, is_int8) {
(false, false) => yuv,
(false, true) => yuv_int8,
(true, false) => pl,
(true, true) => pl_int8,
}
}
pub(super) fn float_src_mapping_for_feed(
&self,
feed: float::FloatSrcFeed,
src_rect_uv: [f32; 4],
src_u8: &Tensor<u8>,
src_w: usize,
src_h: usize,
) -> ([f32; 4], [f32; 4]) {
let map = if feed == float::FloatSrcFeed::Import {
self.cached_src_import_map(src_u8, PixelFormat::Rgba)
} else {
super::render::ImportMap::WHOLE
};
(
super::render::scale_uv_rect_to_import(src_rect_uv, (src_w, src_h), map),
super::render::sample_clamp_rect((src_w, src_h), map),
)
}
fn cached_dst_renderbuffer<T>(&self, img: &Tensor<T>, fmt: PixelFormat) -> Option<u32>
where
T: num_traits::Num + Clone + std::fmt::Debug + Send + Sync + edgefirst_tensor::Element,
{
let id = BufferImportKey::from_tensor(img, fmt, true);
self.dst_egl_cache
.entries
.get(&id)
.and_then(|entry| entry.renderbuffer)
}
fn get_or_create_egl_image_rgb<T>(
&mut self,
img: &Tensor<T>,
img_fmt: PixelFormat,
width: usize,
height: usize,
packed: super::platform::PackedImportFormat,
) -> Result<PlatformHandle, crate::Error>
where
T: num_traits::Num + Clone + std::fmt::Debug + Send + Sync + edgefirst_tensor::Element,
{
#[cfg(test)]
if self.fail_dst_import {
return Err(crate::Error::OpenGl(
"EDGEFIRST test hook: zero-copy destination import refused".to_string(),
));
}
Platform::validate_import_identity(img, "packed destination")?;
let id = BufferImportKey::from_tensor(img, img_fmt, true);
let ts = self.dst_egl_cache.next_timestamp();
if let Some(cached) = self.dst_egl_cache.entries.get_mut(&id) {
self.dst_egl_cache.hits += 1;
cached.last_used = ts;
log::trace!("ImportCache dst (RGB) hit: id={:#x}", id.luma_id);
return Ok(Platform::import_handle(&cached.import));
}
self.dst_egl_cache.misses += 1;
log::trace!("ImportCache dst (RGB) miss: id={:#x}", id.luma_id);
self.invalidate_dst_textures();
let egl_image_obj =
Platform::import_buffer_packed(&self.gl_context, img, width, height, packed)?;
let handle = Platform::import_handle(&egl_image_obj);
let rbo = if self.use_renderbuffer {
let mut rbo: u32 = 0;
unsafe {
edgefirst_gl::gl::GenRenderbuffers(1, &mut rbo);
edgefirst_gl::gl::BindRenderbuffer(edgefirst_gl::gl::RENDERBUFFER, rbo);
Platform::attach_renderbuffer_storage(
&self.gl_context,
Platform::import_handle(&egl_image_obj),
)?;
if let Err(e) = check_gl_error(function!(), line!()) {
edgefirst_gl::gl::DeleteRenderbuffers(1, &rbo);
return Err(e);
}
}
Some(rbo)
} else {
None
};
self.dst_egl_cache.insert(id, egl_image_obj, rbo);
Ok(handle)
}
fn reshape_segmentation_to_rgba(&self, segmentation: &[u8], shape: [usize; 3]) -> Vec<u8> {
let [height, width, classes] = shape;
let n_layer_stride = height * width * 4;
let n_row_stride = width * 4;
let n_col_stride = 4;
let row_stride = width * classes;
let col_stride = classes;
let mut new_segmentation = vec![0u8; n_layer_stride * classes.div_ceil(4)];
for i in 0..height {
for j in 0..width {
for k in 0..classes.div_ceil(4) * 4 {
if k >= classes {
new_segmentation[n_layer_stride * (k / 4)
+ i * n_row_stride
+ j * n_col_stride
+ k % 4] = 0;
} else {
new_segmentation[n_layer_stride * (k / 4)
+ i * n_row_stride
+ j * n_col_stride
+ k % 4] = segmentation[i * row_stride + j * col_stride + k];
}
}
}
}
new_segmentation
}
fn render_modelpack_segmentation(
&mut self,
dst_roi: RegionOfInterest,
segmentation: &[u8],
shape: [usize; 3],
) -> Result<(), crate::Error> {
log::debug!("start render_segmentation_to_image");
let new_segmentation = self.reshape_segmentation_to_rgba(segmentation, shape);
let [height, width, classes] = shape;
let format = edgefirst_gl::gl::RGBA;
let texture_target = edgefirst_gl::gl::TEXTURE_2D_ARRAY;
self.segmentation_program
.load_uniform_1i(c"background_index", shape[2] as i32 - 1)?;
edgefirst_gl::use_program(self.segmentation_program.id);
edgefirst_gl::bind_texture(texture_target, self.segmentation_texture.id);
edgefirst_gl::active_texture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::tex_parameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_MIN_FILTER,
edgefirst_gl::gl::LINEAR as i32,
);
edgefirst_gl::tex_parameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_MAG_FILTER,
edgefirst_gl::gl::LINEAR as i32,
);
edgefirst_gl::tex_parameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_WRAP_S,
edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
);
edgefirst_gl::tex_parameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_WRAP_T,
edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
);
edgefirst_gl::tex_image3d(
texture_target,
0,
format as i32,
width as i32,
height as i32,
classes.div_ceil(4) as i32,
0,
format,
edgefirst_gl::gl::UNSIGNED_BYTE,
Some(&new_segmentation),
);
let src_roi = RegionOfInterest {
left: 0.,
top: 1.,
right: 1.,
bottom: 0.,
};
unsafe {
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
let camera_vertices: [f32; 12] = [
dst_roi.left,
dst_roi.top,
0., dst_roi.right,
dst_roi.top,
0., dst_roi.right,
dst_roi.bottom,
0., dst_roi.left,
dst_roi.bottom,
0., ];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * camera_vertices.len()) as isize,
camera_vertices.as_ptr() as *const c_void,
);
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.texture_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.texture_buffer.buffer_index);
let texture_vertices: [f32; 8] = [
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * 8) as isize,
(texture_vertices[0..]).as_ptr() as *const c_void,
);
let vertices_index: [u32; 4] = [0, 1, 2, 3];
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_FAN,
vertices_index.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
vertices_index.as_ptr() as *const c_void,
);
}
Ok(())
}
fn setup_yolo_segmentation_pass(&self) {
let texture_target = edgefirst_gl::gl::TEXTURE_2D;
edgefirst_gl::use_program(self.instanced_segmentation_program.id);
edgefirst_gl::active_texture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::bind_texture(texture_target, self.segmentation_texture.id);
edgefirst_gl::tex_parameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_MIN_FILTER,
edgefirst_gl::gl::LINEAR as i32,
);
edgefirst_gl::tex_parameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_MAG_FILTER,
edgefirst_gl::gl::LINEAR as i32,
);
edgefirst_gl::tex_parameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_WRAP_S,
edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
);
edgefirst_gl::tex_parameteri(
texture_target,
edgefirst_gl::gl::TEXTURE_WRAP_T,
edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
);
}
fn render_yolo_segmentation(
&mut self,
dst_roi: RegionOfInterest,
segmentation: &[u8],
shape: [usize; 2],
class: usize,
) -> Result<(), crate::Error> {
let [height, width] = shape;
let texture_target = edgefirst_gl::gl::TEXTURE_2D;
edgefirst_gl::tex_image2d(
texture_target,
0,
edgefirst_gl::gl::R8 as i32,
width as i32,
height as i32,
0,
edgefirst_gl::gl::RED,
edgefirst_gl::gl::UNSIGNED_BYTE,
Some(segmentation),
);
self.instanced_segmentation_program
.load_uniform_1i(c"class_index", class as i32)?;
let u_lo = 0.5 / width as f32;
let v_lo = 0.5 / height as f32;
let u_hi = (width as f32 - 0.5) / width as f32;
let v_hi = (height as f32 - 0.5) / height as f32;
let src_roi = RegionOfInterest {
left: u_lo,
top: v_hi,
right: u_hi,
bottom: v_lo,
};
unsafe {
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
let camera_vertices: [f32; 12] = [
dst_roi.left,
dst_roi.top,
0., dst_roi.right,
dst_roi.top,
0., dst_roi.right,
dst_roi.bottom,
0., dst_roi.left,
dst_roi.bottom,
0., ];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * camera_vertices.len()) as isize,
camera_vertices.as_ptr() as *const c_void,
);
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.texture_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.texture_buffer.buffer_index);
let texture_vertices: [f32; 8] = [
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * 8) as isize,
(texture_vertices).as_ptr() as *const c_void,
);
let vertices_index: [u32; 4] = [0, 1, 2, 3];
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_FAN,
vertices_index.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
vertices_index.as_ptr() as *const c_void,
);
if self.is_vivante {
edgefirst_gl::gl::Finish();
}
}
Ok(())
}
fn render_proto_segmentation(
&mut self,
detect: &[DetectBox],
proto_data: &ProtoData,
color_mode: crate::ColorMode,
) -> crate::Result<()> {
use edgefirst_tensor::{DType, QuantMode, TensorMapTrait, TensorTrait};
if detect.is_empty() {
return Ok(());
}
let proto_shape = proto_data.protos.shape();
if proto_shape.len() != 3 {
return Err(crate::Error::InvalidShape(format!(
"protos tensor must be rank-3, got {proto_shape:?}"
)));
}
let (height, width, num_protos) = match proto_data.layout {
ProtoLayout::Nhwc => (proto_shape[0], proto_shape[1], proto_shape[2]),
ProtoLayout::Nchw => (proto_shape[1], proto_shape[2], proto_shape[0]),
};
let coeff_shape = proto_data.mask_coefficients.shape();
if coeff_shape.len() != 2 || coeff_shape[1] != num_protos {
return Err(crate::Error::InvalidShape(format!(
"mask_coefficients shape {coeff_shape:?} incompatible with protos \
{proto_shape:?} (expected [N, {num_protos}])"
)));
}
if coeff_shape[0] == 0 {
return Ok(());
}
let texture_target = edgefirst_gl::gl::TEXTURE_2D_ARRAY;
let plan = super::proto_dispatch::plan_proto(
proto_data.protos.dtype(),
proto_data.layout,
self.proto_repack_compute_program.is_some(),
self.has_float_linear,
self.int8_interpolation_mode,
)?;
let _span = tracing::trace_span!(
"image.draw.gl.proto",
dtype = ?proto_data.protos.dtype(),
upload = ?plan.upload,
program = ?plan.program,
num_protos,
detections = detect.len(),
)
.entered();
let mc_map_f32;
let mc_map_f16;
let mc_map_i8;
let coeff_widen_f16: Vec<f32>;
let coeff_dequant: Vec<f32>;
let coeff_slice: &[f32] = match proto_data.mask_coefficients.dtype() {
DType::F32 => {
let t = proto_data.mask_coefficients.as_f32().expect("F32");
mc_map_f32 = t.map_read()?;
mc_map_f32.as_slice()
}
DType::F16 => {
let t = proto_data.mask_coefficients.as_f16().expect("F16");
mc_map_f16 = t.map_read()?;
coeff_widen_f16 = mc_map_f16.as_slice().iter().map(|v| v.to_f32()).collect();
&coeff_widen_f16[..]
}
DType::I8 => {
let t = proto_data.mask_coefficients.as_i8().expect("I8");
mc_map_i8 = t.map_read()?;
let quant = t.quantization();
coeff_dequant = super::proto_dispatch::dequant_coeffs(
mc_map_i8.as_slice(),
quant.as_ref().map(|q| q.mode()),
"I8",
)?;
&coeff_dequant[..]
}
DType::I16 => {
let t = proto_data.mask_coefficients.as_i16().expect("I16");
let mc_map_i16 = t.map_read()?;
let quant = t.quantization();
coeff_dequant = super::proto_dispatch::dequant_coeffs(
mc_map_i16.as_slice(),
quant.as_ref().map(|q| q.mode()),
"I16",
)?;
&coeff_dequant[..]
}
other => {
return Err(crate::Error::InvalidShape(format!(
"mask_coefficients dtype {other:?} not supported on GL seg path"
)));
}
};
match proto_data.protos.dtype() {
DType::I8 => {
let t = proto_data.protos.as_i8().expect("I8");
let m = t.map_read()?;
let quant = t.quantization().ok_or_else(|| {
crate::Error::InvalidShape("I8 protos require quantization metadata".into())
})?;
let (scale, zp) = match quant.mode() {
QuantMode::PerTensor { scale, zero_point } => (scale, zero_point),
QuantMode::PerTensorSymmetric { scale } => (scale, 0_i32),
QuantMode::PerChannel { axis, .. }
| QuantMode::PerChannelSymmetric { axis, .. } => {
return Err(crate::Error::NotSupported(format!(
"GL seg path: per-channel quantization (axis={axis}) \
not yet supported — falls back to CPU in caller"
)));
}
};
let protos_view = ndarray::ArrayView3::<i8>::from_shape(
(height, width, num_protos),
m.as_slice(),
)
.map_err(|e| crate::Error::InvalidShape(format!("{e}")))?;
let quantization = (scale, zp);
self.render_proto_segmentation_int8(
plan,
detect,
coeff_slice,
protos_view,
quantization,
height,
width,
num_protos,
texture_target,
color_mode,
)?;
}
DType::F32 => {
let t = proto_data.protos.as_f32().expect("F32");
let m = t.map_read()?;
let protos_view = ndarray::ArrayView3::<f32>::from_shape(
(height, width, num_protos),
m.as_slice(),
)
.map_err(|e| crate::Error::InvalidShape(format!("{e}")))?;
self.render_proto_layers(
plan,
detect,
coeff_slice,
ProtoLayersData::F32(protos_view),
height,
width,
num_protos,
texture_target,
color_mode,
)?;
}
DType::F16 => {
let t = proto_data.protos.as_f16().expect("F16");
let m = t.map_read()?;
let protos_view = ndarray::ArrayView3::<half::f16>::from_shape(
(height, width, num_protos),
m.as_slice(),
)
.map_err(|e| crate::Error::InvalidShape(format!("{e}")))?;
self.render_proto_layers(
plan,
detect,
coeff_slice,
ProtoLayersData::F16(protos_view),
height,
width,
num_protos,
texture_target,
color_mode,
)?;
}
other => {
return Err(crate::Error::InvalidShape(format!(
"GL seg path: proto dtype {other:?} not supported"
)));
}
}
unsafe { edgefirst_gl::gl::Finish() };
Ok(())
}
fn render_proto_detection_quads(
&self,
program: &GlProgram,
detect: &[DetectBox],
mask_coefficients: &[f32],
num_protos: usize,
color_mode: crate::ColorMode,
) -> crate::Result<()> {
let cvt_screen_coord = |normalized: f32| normalized * 2.0 - 1.0;
let (cached_id, mut loc_coeff, mut loc_class) = self.proto_quad_locs.get();
if cached_id != program.id {
unsafe {
loc_coeff =
edgefirst_gl::gl::GetUniformLocation(program.id, c"mask_coeff".as_ptr());
loc_class =
edgefirst_gl::gl::GetUniformLocation(program.id, c"class_index".as_ptr());
}
self.proto_quad_locs.set((program.id, loc_coeff, loc_class));
}
for (idx, (det, coeff)) in detect
.iter()
.zip(mask_coefficients.chunks_exact(num_protos))
.enumerate()
{
let color_index = color_mode.index(idx, det.label);
let mut packed_coeff = [[0.0f32; 4]; 8];
for (i, val) in coeff.iter().enumerate().take(32) {
packed_coeff[i / 4][i % 4] = *val;
}
unsafe {
edgefirst_gl::gl::Uniform4fv(loc_coeff, 8, packed_coeff.as_ptr() as *const f32);
edgefirst_gl::gl::Uniform1i(loc_class, color_index as i32);
}
let dst_roi = RegionOfInterest {
left: cvt_screen_coord(det.bbox.xmin),
top: cvt_screen_coord(det.bbox.ymax),
right: cvt_screen_coord(det.bbox.xmax),
bottom: cvt_screen_coord(det.bbox.ymin),
};
let src_roi = RegionOfInterest {
left: det.bbox.xmin,
top: det.bbox.ymax,
right: det.bbox.xmax,
bottom: det.bbox.ymin,
};
unsafe {
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
let camera_vertices: [f32; 12] = [
dst_roi.left,
dst_roi.top,
0.,
dst_roi.right,
dst_roi.top,
0.,
dst_roi.right,
dst_roi.bottom,
0.,
dst_roi.left,
dst_roi.bottom,
0.,
];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * camera_vertices.len()) as isize,
camera_vertices.as_ptr() as *const c_void,
);
edgefirst_gl::gl::BindBuffer(
edgefirst_gl::gl::ARRAY_BUFFER,
self.texture_buffer.id,
);
edgefirst_gl::gl::EnableVertexAttribArray(self.texture_buffer.buffer_index);
let texture_vertices: [f32; 8] = [
src_roi.left,
src_roi.top,
src_roi.right,
src_roi.top,
src_roi.right,
src_roi.bottom,
src_roi.left,
src_roi.bottom,
];
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::ARRAY_BUFFER,
0,
(size_of::<f32>() * 8) as isize,
texture_vertices.as_ptr() as *const c_void,
);
let vertices_index: [u32; 4] = [0, 1, 2, 3];
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_FAN,
vertices_index.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
vertices_index.as_ptr() as *const c_void,
);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn render_proto_segmentation_int8(
&mut self,
plan: super::proto_dispatch::ProtoPlan,
detect: &[DetectBox],
mask_coefficients: &[f32],
protos: ndarray::ArrayView3<'_, i8>,
quantization: (f32, i32),
height: usize,
width: usize,
num_protos: usize,
texture_target: u32,
color_mode: crate::ColorMode,
) -> crate::Result<()> {
if plan.upload == super::proto_dispatch::ProtoUpload::I8Compute {
let compute_program = self.proto_repack_compute_program.ok_or_else(|| {
crate::Error::InvalidShape(
"plan selected I8Compute without a compiled compute program".into(),
)
})?;
let mut data = match protos.as_slice() {
Some(s) => std::borrow::Cow::Borrowed(s),
None => std::borrow::Cow::Owned(protos.iter().copied().collect()),
};
if data.len() % 4 != 0 {
let mut owned = data.into_owned();
owned.resize(owned.len().next_multiple_of(4), 0);
data = std::borrow::Cow::Owned(owned);
}
let data_bytes = data.len();
if self.ensure_proto_texture(
texture_target,
edgefirst_gl::gl::R32I,
width,
height,
num_protos,
) {
Self::set_proto_tex_params(texture_target, edgefirst_gl::gl::NEAREST);
}
unsafe {
edgefirst_gl::gl::BindBuffer(
edgefirst_gl::gl::SHADER_STORAGE_BUFFER,
self.proto_ssbo,
);
if data_bytes > self.proto_ssbo_size {
edgefirst_gl::gl::BufferData(
edgefirst_gl::gl::SHADER_STORAGE_BUFFER,
data_bytes as isize,
data.as_ptr() as *const std::ffi::c_void,
edgefirst_gl::gl::STREAM_DRAW,
);
self.proto_ssbo_size = data_bytes;
} else {
edgefirst_gl::gl::BufferSubData(
edgefirst_gl::gl::SHADER_STORAGE_BUFFER,
0,
data_bytes as isize,
data.as_ptr() as *const std::ffi::c_void,
);
}
edgefirst_gl::gl::BindBufferBase(
edgefirst_gl::gl::SHADER_STORAGE_BUFFER,
0,
self.proto_ssbo,
);
edgefirst_gl::gl::BindImageTexture(
0,
self.proto_texture.id,
0,
edgefirst_gl::gl::TRUE,
0,
edgefirst_gl::gl::WRITE_ONLY,
edgefirst_gl::gl::R32I,
);
edgefirst_gl::gl::UseProgram(compute_program);
let (loc_w, loc_h, loc_np) = self.proto_compute_locs;
edgefirst_gl::gl::Uniform1i(loc_w, width as i32);
edgefirst_gl::gl::Uniform1i(loc_h, height as i32);
edgefirst_gl::gl::Uniform1i(loc_np, num_protos as i32);
let groups_x = width.div_ceil(16) as u32;
let groups_y = height.div_ceil(16) as u32;
edgefirst_gl::gl::DispatchCompute(groups_x, groups_y, 1);
edgefirst_gl::gl::MemoryBarrier(
edgefirst_gl::gl::TEXTURE_FETCH_BARRIER_BIT
| edgefirst_gl::gl::SHADER_IMAGE_ACCESS_BARRIER_BIT,
);
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::SHADER_STORAGE_BUFFER, 0);
loop {
let err = edgefirst_gl::gl::GetError();
if err == edgefirst_gl::gl::NO_ERROR {
break;
}
log::debug!("GL error after compute dispatch: 0x{err:x}");
}
}
} else {
let tex_data = super::proto_dispatch::repack_layers(protos);
self.upload_proto_texture(
texture_target,
edgefirst_gl::gl::R8I,
width,
height,
num_protos,
edgefirst_gl::gl::RED_INTEGER,
edgefirst_gl::gl::BYTE,
edgefirst_gl::gl::NEAREST,
&tex_data,
);
}
let (proto_scale, proto_zp) = quantization;
let proto_scaled_zp = -(proto_zp as f32) * proto_scale;
match plan.program {
super::proto_dispatch::ProtoProgram::Int8Nearest
| super::proto_dispatch::ProtoProgram::Int8Bilinear => {
let program = if plan.program == super::proto_dispatch::ProtoProgram::Int8Nearest {
&self.proto_segmentation_int8_nearest_program
} else {
&self.proto_segmentation_int8_bilinear_program
};
edgefirst_gl::use_program(program.id);
program.load_uniform_1i(c"num_protos", num_protos as i32)?;
program.load_uniform_1f(c"proto_scale", proto_scale)?;
program.load_uniform_1f(c"proto_scaled_zp", proto_scaled_zp)?;
self.render_proto_detection_quads(
program,
detect,
mask_coefficients,
num_protos,
color_mode,
)?;
}
super::proto_dispatch::ProtoProgram::Int8TwoPass => {
self.render_proto_int8_two_pass(
detect,
mask_coefficients,
quantization,
height,
width,
num_protos,
texture_target,
color_mode,
)?;
}
other => {
return Err(crate::Error::InvalidShape(format!(
"proto program {other:?} is not an int8 program"
)));
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn render_proto_int8_two_pass(
&mut self,
detect: &[DetectBox],
mask_coefficients: &[f32],
quantization: (f32, i32),
height: usize,
width: usize,
num_protos: usize,
texture_target: u32,
color_mode: crate::ColorMode,
) -> crate::Result<()> {
let num_layers = num_protos.div_ceil(4);
let (saved_fbo, saved_viewport, blend_was_enabled) = unsafe {
let mut fbo: i32 = 0;
edgefirst_gl::gl::GetIntegerv(edgefirst_gl::gl::FRAMEBUFFER_BINDING, &mut fbo);
let mut vp = [0i32; 4];
edgefirst_gl::gl::GetIntegerv(edgefirst_gl::gl::VIEWPORT, vp.as_mut_ptr());
let blend =
edgefirst_gl::gl::IsEnabled(edgefirst_gl::gl::BLEND) == edgefirst_gl::gl::TRUE;
(fbo as u32, vp, blend)
};
if Self::ensure_immutable_tex_array(
&mut self.proto_dequant_texture,
&mut self.proto_dequant_tex_dims,
texture_target,
edgefirst_gl::gl::RGBA16F,
width,
height,
num_layers,
) {
Self::set_proto_tex_params(texture_target, edgefirst_gl::gl::LINEAR);
}
edgefirst_gl::disable(edgefirst_gl::gl::BLEND);
let (proto_scale, proto_zp) = quantization;
let proto_scaled_zp = -(proto_zp as f32) * proto_scale;
let dequant_program = &self.proto_dequant_int8_program;
edgefirst_gl::use_program(dequant_program.id);
dequant_program.load_uniform_1f(c"proto_scale", proto_scale)?;
dequant_program.load_uniform_1f(c"proto_scaled_zp", proto_scaled_zp)?;
edgefirst_gl::active_texture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::bind_texture(texture_target, self.proto_texture.id);
for layer in 0..num_layers {
self.proto_dequant_fbo.bind();
unsafe {
edgefirst_gl::gl::FramebufferTextureLayer(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
self.proto_dequant_texture.id,
0,
layer as i32,
);
edgefirst_gl::gl::Viewport(0, 0, width as i32, height as i32);
}
dequant_program.load_uniform_1i(c"base_layer", (layer * 4) as i32)?;
self.draw_fullscreen_quad()?;
}
unsafe {
edgefirst_gl::gl::BindFramebuffer(edgefirst_gl::gl::FRAMEBUFFER, saved_fbo);
edgefirst_gl::gl::Viewport(
saved_viewport[0],
saved_viewport[1],
saved_viewport[2],
saved_viewport[3],
);
}
if blend_was_enabled {
edgefirst_gl::enable(edgefirst_gl::gl::BLEND);
}
let program = &self.proto_segmentation_program;
edgefirst_gl::use_program(program.id);
edgefirst_gl::active_texture(edgefirst_gl::gl::TEXTURE0);
edgefirst_gl::bind_texture(texture_target, self.proto_dequant_texture.id);
program.load_uniform_1i(c"num_layers", num_layers as i32)?;
self.render_proto_detection_quads(
program,
detect,
mask_coefficients,
num_protos,
color_mode,
)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn render_proto_layers(
&mut self,
plan: super::proto_dispatch::ProtoPlan,
detect: &[DetectBox],
mask_coefficients: &[f32],
data: ProtoLayersData<'_>,
height: usize,
width: usize,
num_protos: usize,
texture_target: u32,
color_mode: crate::ColorMode,
) -> crate::Result<()> {
use super::proto_dispatch::{CountUniform, ProtoProgram, ProtoUpload};
let num_layers = num_protos.div_ceil(4);
match (plan.upload, &data) {
(ProtoUpload::F32R32f, ProtoLayersData::F32(v)) => {
let tex_data = super::proto_dispatch::repack_layers(*v);
self.upload_proto_texture(
texture_target,
edgefirst_gl::gl::R32F,
width,
height,
num_protos,
edgefirst_gl::gl::RED,
edgefirst_gl::gl::FLOAT,
edgefirst_gl::gl::LINEAR,
&tex_data,
);
}
(ProtoUpload::F32ToRgba16f, ProtoLayersData::F32(v)) => {
let (tex_data, _) =
super::proto_dispatch::repack_rgba_f16_layers(*v, half::f16::from_f32);
self.upload_proto_texture(
texture_target,
edgefirst_gl::gl::RGBA16F,
width,
height,
num_layers,
edgefirst_gl::gl::RGBA,
edgefirst_gl::gl::HALF_FLOAT,
edgefirst_gl::gl::LINEAR,
&tex_data,
);
}
(ProtoUpload::F16Rgba16f, ProtoLayersData::F16(v)) => {
let (tex_data, _) = super::proto_dispatch::repack_rgba_f16_layers(*v, |x| x);
self.upload_proto_texture(
texture_target,
edgefirst_gl::gl::RGBA16F,
width,
height,
num_layers,
edgefirst_gl::gl::RGBA,
edgefirst_gl::gl::HALF_FLOAT,
edgefirst_gl::gl::LINEAR,
&tex_data,
);
}
(upload, _) => {
return Err(crate::Error::InvalidShape(format!(
"proto upload {upload:?} incompatible with the provided proto data"
)));
}
}
let program = match plan.program {
ProtoProgram::F32 => &self.proto_segmentation_f32_program,
ProtoProgram::F16 => &self.proto_segmentation_program,
other => {
return Err(crate::Error::InvalidShape(format!(
"proto program {other:?} is not a layered-float program"
)));
}
};
edgefirst_gl::use_program(program.id);
match plan.count_uniform {
CountUniform::NumProtos => program.load_uniform_1i(c"num_protos", num_protos as i32)?,
CountUniform::NumLayers => program.load_uniform_1i(c"num_layers", num_layers as i32)?,
}
self.render_proto_detection_quads(
program,
detect,
mask_coefficients,
num_protos,
color_mode,
)
}
fn render_segmentation(
&mut self,
detect: &[DetectBox],
segmentation: &[Segmentation],
color_mode: crate::ColorMode,
) -> crate::Result<()> {
if segmentation.is_empty() {
return Ok(());
}
if let Some(bad) = segmentation
.iter()
.find(|s| s.segmentation.shape().len() != 3)
{
return Err(crate::Error::InvalidShape(format!(
"segmentation must be [H, W, C], got {:?}",
bad.segmentation.shape()
)));
}
let is_modelpack = segmentation[0].segmentation.shape()[2] > 1;
let cvt_screen_coord = |normalized| normalized * 2.0 - 1.0;
if is_modelpack {
let seg = &segmentation[0];
let dst_roi = RegionOfInterest {
left: cvt_screen_coord(seg.xmin),
top: cvt_screen_coord(seg.ymax),
right: cvt_screen_coord(seg.xmax),
bottom: cvt_screen_coord(seg.ymin),
};
let segment = seg.segmentation.as_u8().ok_or(Error::Internal(
"segmentation must be a U8 tensor".to_owned(),
))?;
let segment_map = segment.map_read()?;
let slice = segment_map.as_slice();
self.render_modelpack_segmentation(
dst_roi,
slice,
[
seg.segmentation.shape()[0],
seg.segmentation.shape()[1],
seg.segmentation.shape()[2],
],
)?;
} else {
self.setup_yolo_segmentation_pass();
for (idx, (seg, det)) in segmentation.iter().zip(detect).enumerate() {
let color_index = color_mode.index(idx, det.label);
let dst_roi = RegionOfInterest {
left: cvt_screen_coord(seg.xmin),
top: cvt_screen_coord(seg.ymax),
right: cvt_screen_coord(seg.xmax),
bottom: cvt_screen_coord(seg.ymin),
};
let segment = seg.segmentation.as_u8().ok_or(Error::Internal(
"segmentation must be a U8 tensor".to_owned(),
))?;
let segment_map = segment.map_read()?;
let slice = segment_map.as_slice();
self.render_yolo_segmentation(
dst_roi,
slice,
[seg.segmentation.shape()[0], seg.segmentation.shape()[1]],
color_index,
)?;
}
}
edgefirst_gl::disable(edgefirst_gl::gl::BLEND);
Ok(())
}
fn compile_compute_program(source: &str) -> Result<u32, Error> {
unsafe {
let cs = edgefirst_gl::gl::CreateShader(edgefirst_gl::gl::COMPUTE_SHADER);
if super::shaders::compile_shader_from_str(cs, source, "proto_repack_compute").is_err()
{
edgefirst_gl::gl::DeleteShader(cs);
return Err(Error::OpenGl("compute shader compile failed".into()));
}
let program = edgefirst_gl::gl::CreateProgram();
edgefirst_gl::gl::AttachShader(program, cs);
edgefirst_gl::gl::LinkProgram(program);
let mut linked: i32 = 0;
edgefirst_gl::gl::GetProgramiv(program, edgefirst_gl::gl::LINK_STATUS, &mut linked);
edgefirst_gl::gl::DeleteShader(cs);
if linked == 0 {
let mut log_len = 0;
edgefirst_gl::gl::GetProgramiv(
program,
edgefirst_gl::gl::INFO_LOG_LENGTH,
&mut log_len,
);
let mut log_buf: Vec<u8> = vec![0; log_len as usize];
edgefirst_gl::gl::GetProgramInfoLog(
program,
log_len,
std::ptr::null_mut(),
log_buf.as_mut_ptr() as *mut std::ffi::c_char,
);
let msg = String::from_utf8_lossy(&log_buf);
edgefirst_gl::gl::DeleteProgram(program);
return Err(Error::OpenGl(format!("compute program link failed: {msg}")));
}
Ok(program)
}
}
#[allow(clippy::too_many_arguments)]
fn ensure_proto_texture(
&mut self,
target: u32,
internal_fmt: u32,
w: usize,
h: usize,
layers: usize,
) -> bool {
Self::ensure_immutable_tex_array(
&mut self.proto_texture,
&mut self.proto_tex_dims,
target,
internal_fmt,
w,
h,
layers,
)
}
fn ensure_immutable_tex_array(
texture: &mut Texture,
tex_dims: &mut (usize, usize, usize, u32),
target: u32,
internal_fmt: u32,
w: usize,
h: usize,
layers: usize,
) -> bool {
let dims = (w, h, layers, internal_fmt);
edgefirst_gl::active_texture(edgefirst_gl::gl::TEXTURE0);
if dims == *tex_dims {
edgefirst_gl::bind_texture(target, texture.id);
return false;
}
*texture = Texture::new();
edgefirst_gl::bind_texture(target, texture.id);
unsafe {
edgefirst_gl::gl::TexStorage3D(
target,
1,
internal_fmt,
w as i32,
h as i32,
layers as i32,
);
}
*tex_dims = dims;
true
}
fn set_proto_tex_params(target: u32, filter: u32) {
unsafe { super::core::set_tex_filter(target, filter) };
edgefirst_gl::tex_parameteri(
target,
edgefirst_gl::gl::TEXTURE_WRAP_S,
edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
);
edgefirst_gl::tex_parameteri(
target,
edgefirst_gl::gl::TEXTURE_WRAP_T,
edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
);
}
#[allow(clippy::too_many_arguments)]
fn upload_proto_texture<T: Copy>(
&mut self,
target: u32,
internal_fmt: u32,
w: usize,
h: usize,
layers: usize,
format: u32,
dtype: u32,
filter: u32,
data: &[T],
) {
if self.ensure_proto_texture(target, internal_fmt, w, h, layers) {
Self::set_proto_tex_params(target, filter);
}
unsafe {
edgefirst_gl::gl::TexSubImage3D(
target,
0,
0,
0,
0,
w as i32,
h as i32,
layers as i32,
format,
dtype,
data.as_ptr() as *const std::ffi::c_void,
);
}
}
fn set_opacity_uniform(&mut self, opacity: f32) -> Result<(), Error> {
if (opacity - self.cached_opacity).abs() < f32::EPSILON {
return Ok(());
}
for program in [
&self.color_program,
&self.segmentation_program,
&self.instanced_segmentation_program,
&self.proto_segmentation_program,
&self.proto_segmentation_int8_nearest_program,
&self.proto_segmentation_int8_bilinear_program,
&self.proto_segmentation_f32_program,
] {
program.load_uniform_1f(c"opacity", opacity)?;
}
self.cached_opacity = opacity;
Ok(())
}
fn render_box(
&mut self,
dst_w: usize,
dst_h: usize,
detect: &[DetectBox],
color_mode: crate::ColorMode,
) -> Result<(), Error> {
unsafe {
edgefirst_gl::gl::UseProgram(self.color_program.id);
let rescale = |x: f32| x * 2.0 - 1.0;
let thickness = self.box_line_width;
for (idx, d) in detect.iter().enumerate() {
let color_index = color_mode.index(idx, d.label);
self.color_program
.load_uniform_1i(c"class_index", color_index as i32)?;
edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::ARRAY_BUFFER, self.vertex_buffer.id);
edgefirst_gl::gl::EnableVertexAttribArray(self.vertex_buffer.buffer_index);
let bbox: [f32; 4] = d.bbox.into();
let outer_box = [
bbox[0] - thickness / dst_w as f32,
bbox[1] - thickness / dst_h as f32,
bbox[2] + thickness / dst_w as f32,
bbox[3] + thickness / dst_h as f32,
];
let camera_vertices: [f32; 24] = [
rescale(bbox[0]),
rescale(bbox[3]),
0., rescale(bbox[2]),
rescale(bbox[3]),
0., rescale(bbox[2]),
rescale(bbox[1]),
0., rescale(bbox[0]),
rescale(bbox[1]),
0., rescale(outer_box[0]),
rescale(outer_box[3]),
0., rescale(outer_box[2]),
rescale(outer_box[3]),
0., rescale(outer_box[2]),
rescale(outer_box[1]),
0., rescale(outer_box[0]),
rescale(outer_box[1]),
0., ];
edgefirst_gl::gl::BufferData(
edgefirst_gl::gl::ARRAY_BUFFER,
(size_of::<f32>() * camera_vertices.len()) as isize,
camera_vertices.as_ptr() as *const c_void,
edgefirst_gl::gl::DYNAMIC_DRAW,
);
let vertices_index: [u32; 10] = [0, 1, 5, 2, 6, 3, 7, 0, 4, 5];
edgefirst_gl::gl::DrawElements(
edgefirst_gl::gl::TRIANGLE_STRIP,
vertices_index.len() as i32,
edgefirst_gl::gl::UNSIGNED_INT,
vertices_index.as_ptr() as *const c_void,
);
}
}
check_gl_error(function!(), line!())?;
Ok(())
}
pub(super) fn is_vivante(&self) -> bool {
self.is_vivante
}
pub(super) fn supported_render_dtypes(&self) -> crate::RenderDtypeSupport {
float_render_support(
self.is_vivante,
self.float_import_refused(),
self.supports_f32_color,
self.supports_f16_color,
)
}
#[allow(clippy::too_many_arguments)]
fn convert_nv_to_float_two_pass(
&mut self,
src: &TensorDyn,
dst: &mut TensorDyn,
path: FloatRenderPath,
rotation: crate::Rotation,
flip: Flip,
crop: ResolvedCrop,
convert_span: &tracing::Span,
) -> crate::Result<()> {
if rotation != crate::Rotation::None || flip != Flip::None {
return Err(crate::Error::NotSupported(
"GL fused NV→float: rotation/flip not supported; using CPU fallback".to_string(),
));
}
let src_u8 = src.as_u8().ok_or(Error::NotAnImage)?;
let src_fmt = src.format().ok_or(Error::NotAnImage)?;
let dst_w = dst.width().ok_or(Error::NotAnImage)?;
let dst_h = dst.height().ok_or(Error::NotAnImage)?;
let _span =
tracing::trace_span!("image.convert.gl.nv_to_planar_float", dst_w, dst_h).entered();
self.ensure_packed_rgb_intermediate(dst_w, dst_h)?;
self.packed_rgb_fbo.bind();
unsafe {
edgefirst_gl::gl::FramebufferTexture2D(
edgefirst_gl::gl::FRAMEBUFFER,
edgefirst_gl::gl::COLOR_ATTACHMENT0,
edgefirst_gl::gl::TEXTURE_2D,
self.packed_rgb_intermediate_tex.id,
0,
);
check_gl_error(function!(), line!())?;
edgefirst_gl::gl::Viewport(0, 0, dst_w as i32, dst_h as i32);
}
let saved_defer = self.defer_finish;
self.defer_finish = true;
let pass1 = self.convert_to_dims(
src_u8,
src_fmt,
dst_w,
dst_h,
None,
PixelFormat::Rgba,
false,
rotation,
flip,
crop,
convert_span,
);
self.defer_finish = saved_defer;
pass1?;
let (mut src_rect_uv, mut dst_rect_px, mut pad_color) =
super::core::float_crop_uniforms(&ResolvedCrop::no_crop(), dst_w, dst_h, dst_w, dst_h)?;
if let Some(c) = crop.dst_color {
pad_color = c.map(|v| v as f32 / 255.0);
}
if let Some(r) = crop.dst_rect {
dst_rect_px = [r.left as f32, r.top as f32, r.width as f32, r.height as f32];
src_rect_uv = [
r.left as f32 / dst_w as f32,
r.top as f32 / dst_h as f32,
r.width as f32 / dst_w as f32,
r.height as f32 / dst_h as f32,
];
}
let src_extent =
super::render::sample_clamp_rect((dst_w, dst_h), super::render::ImportMap::WHOLE);
self.render_float_to_zero_copy_tail(
self.packed_rgb_intermediate_tex.id,
src_rect_uv,
src_extent,
dst_rect_px,
pad_color,
dst,
path,
convert_span,
)
}
pub(super) fn end_gpu_pass_if_synced(&self) {
if !self.pending_flush {
Platform::end_gpu_pass(&self.gl_context);
}
}
pub(super) fn begin_gpu_pass(&self) {
Platform::begin_gpu_pass(&self.gl_context);
}
pub(super) fn misplaces_unaligned_offsets(&self) -> bool {
self.is_mali || self.is_adreno
}
fn float_import_refused(&self) -> bool {
self.is_adreno && self.gl_context.transfer_backend.is_zero_copy()
}
fn render_internal_format(&self, format: u32) -> i32 {
if self.is_adreno && format == edgefirst_gl::gl::RED {
edgefirst_gl::gl::R8 as i32
} else {
format as i32
}
}
pub(super) fn platform_caps(&self) -> super::platform::PlatformCaps {
super::platform::PlatformCaps {
transfer_backend: self.gl_context.transfer_backend,
render_dtypes: self.supported_render_dtypes(),
serialize_gl: requires_full_serialization(RendererTraits {
vivante: self.is_vivante(),
mali: self.is_mali,
software: false,
virtual_gpu: self.is_virtual_gpu,
angle: self.is_angle,
adreno: self.is_adreno,
}),
external_oes: Platform::EXTERNAL_OES,
native_fence_sync: Platform::native_fence_sync(&self.gl_context),
has_bgra: self.has_bgra,
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::{
pack_row_length, padded_readback_bytes, plan_pbo_readback, resolve_egl_cache_capacity,
should_reject_software_gl, spread_rows, unpack_row_length, Error,
DEFAULT_EGL_CACHE_CAPACITY,
};
#[test]
fn y2y_pays_off_below_ten_source_pixels_per_output_pixel() {
let out = 640 * 640;
assert!(super::y2y_pays_off(1280 * 720, out));
assert!(super::y2y_pays_off(1920 * 1080, out));
assert!(!super::y2y_pays_off(3840 * 2160, out));
assert!(super::y2y_pays_off(3840 * 2160, 1280 * 1280));
}
#[test]
fn nv_sampling_choice_covers_every_binding() {
use super::{NvSampling, NvSamplingInputs};
for bits in 0u8..32 {
let inputs = NvSamplingInputs {
bilinear: bits & 1 != 0,
hw_filter: bits & 2 != 0,
imported: bits & 4 != 0,
chroma_view: bits & 8 != 0,
y2y: bits & 16 != 0,
};
let expected = if !inputs.bilinear {
NvSampling::Single
} else if !inputs.hw_filter {
NvSampling::Shader
} else if !inputs.imported || inputs.chroma_view {
NvSampling::Hardware
} else if inputs.y2y {
NvSampling::HardwareYuv
} else {
NvSampling::Shader
};
assert_eq!(NvSampling::choose(inputs), expected, "{inputs:?}");
}
}
#[test]
fn nv_sampling_takes_y2y_only_for_an_import_without_a_chroma_view() {
use super::{NvSampling, NvSamplingInputs};
let mali = NvSamplingInputs {
bilinear: true,
hw_filter: true,
imported: true,
chroma_view: false,
y2y: true,
};
assert_eq!(NvSampling::choose(mali), NvSampling::HardwareYuv);
let chroma_view = NvSamplingInputs {
chroma_view: true,
..mali
};
assert_eq!(NvSampling::choose(chroma_view), NvSampling::Hardware);
let opted_out = NvSamplingInputs {
hw_filter: false,
..mali
};
assert_eq!(NvSampling::choose(opted_out), NvSampling::Shader);
let nearest = NvSamplingInputs {
bilinear: false,
..mali
};
assert_eq!(NvSampling::choose(nearest), NvSampling::Single);
}
#[test]
fn nv_sample_rect_roi_maps_the_crop_edges() {
let rect = super::NvSampleRect::new(
Some(crate::Rect::new(160, 90, 960, 540)),
None,
(1280, 720),
(640, 640),
crate::Rotation::None,
);
let roi = rect.roi(1280, 720);
assert_eq!(roi.left, 0.125);
assert_eq!(roi.right, 0.875);
assert_eq!(roi.top, 0.875);
assert_eq!(roi.bottom, 0.125);
let whole =
super::NvSampleRect::new(None, None, (1280, 720), (640, 640), crate::Rotation::None)
.roi(1280, 720);
assert_eq!(whole.uv_bounds(), [0.0, 0.0, 1.0, 1.0]);
}
#[test]
fn nv_colorimetry_resolves_missing_axes_by_height() {
use edgefirst_tensor::{ColorEncoding, ColorRange, Colorimetry};
assert_eq!(
super::nv_colorimetry(None, Some(720)),
(ColorEncoding::Bt709, ColorRange::Limited)
);
assert_eq!(
super::nv_colorimetry(None, Some(480)),
(ColorEncoding::Bt601, ColorRange::Limited)
);
let full_601 = Colorimetry {
encoding: Some(ColorEncoding::Bt601),
range: Some(ColorRange::Full),
..Default::default()
};
assert_eq!(
super::nv_colorimetry(Some(full_601), Some(1080)),
(ColorEncoding::Bt601, ColorRange::Full)
);
}
#[test]
fn spread_rows_leaves_a_tight_destination_alone() {
let mut buf: Vec<u8> = (0..12).collect();
let before = buf.clone();
spread_rows(&mut buf, 4, 3, 3);
assert_eq!(buf, before, "a tight pitch is a no-op");
spread_rows(&mut buf, 4, 3, 2);
assert_eq!(buf, before, "a pitch below the row is a no-op");
}
#[test]
fn spread_rows_handles_zero_and_one_row() {
let mut empty: Vec<u8> = Vec::new();
spread_rows(&mut empty, 0, 3, 8);
assert!(empty.is_empty());
let mut one = vec![1u8, 2, 3, 0, 0, 0, 0, 0];
let before = one.clone();
spread_rows(&mut one, 1, 3, 8);
assert_eq!(one, before, "row 0 already starts at offset 0");
}
#[test]
fn spread_rows_places_each_row_at_the_pitch() {
let (rows, tight, stride) = (4usize, 3usize, 5usize);
let mut buf = vec![0u8; (rows - 1) * stride + tight];
for y in 0..rows {
buf[y * tight..y * tight + tight].fill(b'a' + y as u8);
}
spread_rows(&mut buf, rows, tight, stride);
for y in 0..rows {
assert_eq!(
&buf[y * stride..y * stride + tight],
&[b'a' + y as u8; 3],
"row {y} did not land at offset {}",
y * stride
);
}
}
#[test]
fn padded_readback_bytes_reports_only_a_padded_span() {
assert_eq!(padded_readback_bytes(4, 3, 3), None);
assert_eq!(padded_readback_bytes(4, 3, 2), None);
assert_eq!(padded_readback_bytes(0, 3, 8), None);
assert_eq!(padded_readback_bytes(4, 3, 5), Some(3 * 5 + 3));
assert_eq!(padded_readback_bytes(1, 3, 5), Some(3));
}
#[test]
fn pack_row_length_covers_only_a_padded_whole_pixel_pitch() {
assert_eq!(pack_row_length(64, 64, 4), None);
assert_eq!(pack_row_length(64, 32, 4), None);
assert_eq!(pack_row_length(64, 128, 4), Some(32));
assert_eq!(pack_row_length(48, 128, 3), None);
assert_eq!(pack_row_length(16, 17, 1), Some(17));
assert_eq!(pack_row_length(16, 17, 0), None);
}
#[test]
fn unpack_row_length_refuses_a_pitch_that_is_not_whole_pixels() {
assert!(matches!(unpack_row_length(None, 64, 4, "Rgba"), Ok(0)));
assert!(matches!(unpack_row_length(Some(256), 64, 4, "Rgba"), Ok(0)));
assert!(matches!(
unpack_row_length(Some(320), 64, 4, "Rgba"),
Ok(80)
));
assert!(matches!(unpack_row_length(Some(17), 16, 1, "Grey"), Ok(17)));
let Err(e) = unpack_row_length(Some(1024), 341, 3, "Rgb") else {
panic!("a pitch that is not a whole number of pixels must be refused");
};
assert!(
matches!(e, Error::NotSupported(ref m) if m.contains("1024") && m.contains("Rgb")),
"unexpected error: {e:?}"
);
assert!(matches!(
unpack_row_length(Some(1026), 256, 4, "RGBA"),
Err(Error::NotSupported(_))
));
assert!(matches!(
unpack_row_length(Some(16), 16, 0, "Rgba"),
Err(Error::NotSupported(_))
));
}
#[test]
fn plan_pbo_readback_refuses_what_it_cannot_place() {
let tight = plan_pbo_readback(256, false, 0, 256 * 8, 64, 8, 4).expect("tight");
assert_eq!((tight.stride, tight.tight_row), (256, 256));
assert_eq!(tight.needed, 256 * 8, "a tight read fills every row");
assert_eq!(tight.row_length, None, "GL already lands tight rows");
let padded = plan_pbo_readback(320, false, 0, 320 * 8, 64, 8, 4).expect("padded");
assert_eq!(padded.row_length, Some(80));
assert_eq!(padded.needed, 320 * 7 + 256);
let odd = plan_pbo_readback(257, false, 0, 257 * 8, 64, 8, 4).expect("odd pitch");
assert_eq!(odd.row_length, None);
assert_eq!(odd.needed, 257 * 7 + 256);
assert_eq!(
plan_pbo_readback(132, false, 0, 1 << 16, 16, 12, 8)
.expect("f16 plane row")
.row_length,
None
);
assert_eq!(
plan_pbo_readback(136, false, 0, 1 << 16, 16, 12, 8)
.expect("f16 plane row")
.row_length,
Some(17)
);
assert!(plan_pbo_readback(255, false, 0, 1 << 16, 64, 8, 4).is_err());
assert!(plan_pbo_readback(256, true, 0, 1 << 16, 64, 8, 4).is_err());
assert!(plan_pbo_readback(256, false, 64, 1 << 16, 64, 8, 4).is_err());
assert!(plan_pbo_readback(320, false, 0, 320 * 7 + 255, 64, 8, 4).is_err());
}
#[test]
fn cache_capacity_config_beats_environment() {
assert_eq!(resolve_egl_cache_capacity(Some(4), Some("64")), 4);
}
#[test]
fn cache_capacity_falls_back_through_env_to_default() {
assert_eq!(resolve_egl_cache_capacity(None, Some("64")), 64);
assert_eq!(
resolve_egl_cache_capacity(None, None),
DEFAULT_EGL_CACHE_CAPACITY
);
}
#[test]
fn cache_capacity_rejects_zero_and_garbage_at_both_levels() {
assert_eq!(resolve_egl_cache_capacity(Some(0), Some("32")), 32);
assert_eq!(
resolve_egl_cache_capacity(Some(0), Some("0")),
DEFAULT_EGL_CACHE_CAPACITY
);
assert_eq!(
resolve_egl_cache_capacity(None, Some("plenty")),
DEFAULT_EGL_CACHE_CAPACITY
);
assert_eq!(
resolve_egl_cache_capacity(None, Some("-1")),
DEFAULT_EGL_CACHE_CAPACITY
);
}
#[test]
fn software_gl_rejected_by_default() {
assert!(should_reject_software_gl(true, false));
}
#[test]
fn software_gl_allowed_with_override() {
assert!(!should_reject_software_gl(true, true));
}
#[test]
fn hardware_gl_never_rejected() {
assert!(!should_reject_software_gl(false, false));
assert!(!should_reject_software_gl(false, true));
}
#[test]
fn classify_renderer_fleet_strings() {
use super::{classify_renderer, RendererTraits};
let real_gpu = RendererTraits::default();
let t = classify_renderer("Vivante GC7000UL");
assert!(t.vivante && !t.software && !t.virtual_gpu && !t.angle && !t.mali);
let t = classify_renderer(
"ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Max, \
Version 26.4.1 (Build 25E253))",
);
assert!(t.angle && !t.virtual_gpu && !t.software && !t.vivante);
assert_eq!(
classify_renderer("Mali-G310"),
RendererTraits {
mali: true,
..RendererTraits::default()
}
);
assert_eq!(
classify_renderer("Mali-G52"),
RendererTraits {
mali: true,
..RendererTraits::default()
}
);
assert_eq!(classify_renderer("V3D 7.1"), real_gpu);
assert_eq!(
classify_renderer("Adreno (TM) 663"),
RendererTraits {
adreno: true,
..RendererTraits::default()
}
);
assert_eq!(
classify_renderer("NVIDIA Tegra Orin (nvgpu)/integrated"),
real_gpu
);
let t = classify_renderer("llvmpipe (LLVM 15.0.7, 256 bits)");
assert!(t.software && !t.vivante && !t.virtual_gpu && !t.angle);
let t = classify_renderer(
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002484) Direct3D11 vs_5_0 ps_5_0, \
D3D11-32.0.16.1656)",
);
assert!(t.angle && !t.software && !t.virtual_gpu && !t.vivante);
let t = classify_renderer(
"ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0, \
D3D11-10.0.26100.1)",
);
assert!(t.angle && t.software && !t.virtual_gpu && !t.vivante);
let t = classify_renderer(
"ANGLE (Apple, ANGLE Metal Renderer: Apple Paravirtual device, \
Version 15.7.7 (Build 24G720))",
);
assert!(t.virtual_gpu && t.angle && !t.software && !t.vivante);
}
#[test]
fn mali_declines_only_unaligned_source_offsets() {
use super::rejects_unaligned_import_offset as rejects;
assert!(rejects(true, 32));
assert!(rejects(true, 2080));
assert!(!rejects(true, 0));
assert!(!rejects(true, 64));
assert!(!rejects(true, 2048));
assert!(!rejects(false, 32));
assert!(!rejects(false, 2080));
assert!(!rejects(true, 256));
}
#[cfg(target_os = "linux")]
#[test]
fn mali_declines_the_imports_the_fold_could_not_move() {
use super::super::dma_import::resolve_source_plane0;
use super::{PixelFormat, DMA_IMPORT_OFFSET_ALIGN};
let aligned = |fmt, w, pitch, off| {
resolve_source_plane0(fmt, w, pitch, off)
.0
.is_multiple_of(DMA_IMPORT_OFFSET_ALIGN)
};
assert!(aligned(PixelFormat::Rgba, 16, 256, 2080));
assert!(aligned(PixelFormat::Bgra, 16, 256, 32));
assert!(aligned(PixelFormat::Rgba, 64, 256, 2048));
assert!(aligned(PixelFormat::Nv12, 64, 64, 0));
assert!(!aligned(PixelFormat::Rgba, 64, 256, 32));
assert!(!aligned(PixelFormat::Rgb, 16, 320, 323));
for fmt in [
PixelFormat::Nv12,
PixelFormat::Nv16,
PixelFormat::Nv24,
PixelFormat::Yuyv,
PixelFormat::Vyuy,
PixelFormat::PlanarRgb,
] {
assert!(!aligned(fmt, 64, 256, 32), "{fmt:?} must not be exempt");
}
}
#[test]
fn the_offset_rule_is_a_source_rule_and_only_malis() {
use super::rejects_unaligned_import_offset;
assert!(
rejects_unaligned_import_offset(true, 2080),
"the Mali source rule covers 2080"
);
assert!(rejects_unaligned_import_offset(true, 32));
assert!(!rejects_unaligned_import_offset(true, 2048));
assert!(!rejects_unaligned_import_offset(true, 0));
assert!(rejects_unaligned_import_offset(true, 16416));
assert!(rejects_unaligned_import_offset(true, 6176));
assert!(!rejects_unaligned_import_offset(true, 16384));
assert!(!rejects_unaligned_import_offset(false, 2080));
assert!(!rejects_unaligned_import_offset(false, 32));
}
#[test]
fn a_view_destination_imports_at_base_zero() {
use super::view_collapsed_dst_base as base;
use edgefirst_tensor::ViewOrigin;
let vo = Some(ViewOrigin {
parent_width: 64,
parent_height: 64,
parent_row_stride: 256,
x: 8,
y: 8,
});
assert_eq!(base(vo, Some(2080)), 0);
assert_eq!(base(vo, Some(2048)), 0);
assert_eq!(base(vo, None), 0);
assert_eq!(base(None, Some(2080)), 2080);
assert_eq!(base(None, Some(2048)), 2048);
assert_eq!(base(None, None), 0);
let raw = |plane_offset: Option<usize>| plane_offset.unwrap_or(0);
assert_eq!(raw(Some(2080)), 2080);
assert_ne!(base(vo, Some(2080)), raw(Some(2080)));
}
#[test]
fn nv12_chroma_plane_offset_is_checked_independently_of_plane_zero() {
use super::{nv12_plane1_offset as plane1, rejects_unaligned_import_offset as rejects};
assert_eq!(plane1(0, 256, 64), 16384);
assert_eq!(plane1(0, 256, 65), 16640);
assert!(!rejects(true, plane1(0, 256, 64)));
assert!(!rejects(true, plane1(0, 256, 65)));
assert!(!rejects(true, 0), "plane 0 looks fine");
assert_eq!(plane1(0, 100, 3), 300);
assert!(rejects(true, plane1(0, 100, 3)), "plane 1 must be caught");
assert_eq!(plane1(0, 48, 5), 240);
assert!(rejects(true, plane1(0, 48, 5)));
assert_eq!(plane1(0, 96, 1), 96);
assert!(rejects(true, plane1(0, 96, 1)));
assert_eq!(plane1(0, 100, 64), 6400);
assert!(!rejects(true, plane1(0, 100, 64)));
assert_eq!(plane1(0, 96, 2), 192);
assert!(!rejects(true, plane1(0, 96, 2)));
assert_eq!(plane1(2048, 256, 64), 18432);
assert!(!rejects(true, plane1(2048, 256, 64)));
assert!(rejects(true, plane1(32, 256, 64)));
assert_eq!(plane1(usize::MAX, 256, 64), usize::MAX);
assert_eq!(plane1(0, usize::MAX, 2), usize::MAX);
assert!(!rejects(false, plane1(0, 100, 3)));
assert!(!rejects(false, plane1(0, 96, 1)));
}
#[test]
fn serialization_policy_per_renderer() {
use super::{classify_renderer, requires_full_serialization};
let policy = |r: &str| requires_full_serialization(classify_renderer(r));
assert!(!policy("Mali-G310"));
assert!(!policy("V3D 7.1"));
assert!(!policy("NVIDIA Tegra Orin (nvgpu)/integrated"));
assert!(policy("Vivante GC7000UL"));
assert!(policy("Adreno (TM) 663"));
assert!(policy(
"ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Max, \
Version 27.0 (Build 26A5388g))"
));
assert!(policy(
"ANGLE (Apple, ANGLE Metal Renderer: Apple Paravirtual device, \
Version 15.7.7 (Build 24G720))"
));
assert!(policy(
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002484) Direct3D11 vs_5_0 ps_5_0, \
D3D11-32.0.16.1656)"
));
}
}