mod device;
mod format;
use std::time::Duration;
use crate::error::CodecError;
use crate::jpeg::markers::JpegHeaders;
use crate::options::ImageInfo;
use crate::pixel::ImagePixel;
use device::{ApiVariant, ProbedDevice};
use edgefirst_tensor::{PixelFormat, Tensor, TensorMemory, TensorTrait};
use edgefirst_v4l2::m2m::M2m;
use edgefirst_v4l2::queue::{BufType, Dequeued, Mapping, Memory, Plane, Queue};
use edgefirst_v4l2::{events, uapi};
use format::CapKind;
const SOURCE_CHANGE_TIMEOUT: Duration = Duration::from_millis(200);
const DECODE_TIMEOUT: Duration = Duration::from_secs(2);
const MAX_EVENTS: usize = 16;
const MAX_CONSECUTIVE_FAILURES: u32 = 8;
const OUT_SIZE_FLOOR: u32 = 2 * 1024 * 1024;
const SCRATCH_SIZE_FLOOR: usize = 2 * 1024 * 1024;
const SCRATCH_ROW_BYTES: usize = 4096;
pub(crate) enum V4l2Decode {
Fallback(String),
Fatal(CodecError),
}
enum DecodeErr {
Reset(String),
Unsupported(String),
Fatal(CodecError),
}
pub(crate) fn device_available() -> bool {
device::probe().is_some()
}
#[allow(clippy::large_enum_variant)]
#[derive(Default)]
pub(crate) enum V4l2Probe {
#[default]
Unprobed,
Unavailable,
Ready(V4l2Context),
}
impl V4l2Probe {
fn ensure_probed(&mut self) -> Option<&mut V4l2Context> {
if matches!(self, V4l2Probe::Unprobed) {
*self = match device::probe().and_then(V4l2Context::new) {
Some(ctx) => V4l2Probe::Ready(ctx),
None => V4l2Probe::Unavailable,
};
}
match self {
V4l2Probe::Ready(ctx) => Some(ctx),
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn try_decode<T: ImagePixel>(
&mut self,
data: &[u8],
_headers: &JpegHeaders,
dst: &mut Tensor<T>,
output_fmt: PixelFormat,
final_w: usize,
final_h: usize,
dst_stride: usize,
) -> Result<Option<ImageInfo>, V4l2Decode> {
let Some(ctx) = self.ensure_probed() else {
return Ok(None);
};
if ctx.device.api != ApiVariant::MultiPlanar {
return Ok(None);
}
require_copy_out_access(dst, output_fmt, final_w, final_h).map_err(V4l2Decode::Fatal)?;
let result = ctx.decode::<T>(data, dst, output_fmt, final_w, final_h, dst_stride);
let out = match result {
Ok(info) => {
ctx.failures = 0;
Ok(Some(info))
}
Err(DecodeErr::Reset(why)) => {
ctx.fail_reset();
Err(V4l2Decode::Fallback(why))
}
Err(DecodeErr::Unsupported(why)) => {
ctx.drop_stream();
Err(V4l2Decode::Fallback(why))
}
Err(DecodeErr::Fatal(e)) => {
if ctx.m2m.output().queued_count() != 0 || ctx.m2m.capture().queued_count() != 0 {
ctx.drop_stream();
}
Err(V4l2Decode::Fatal(e))
}
};
if let V4l2Probe::Ready(ctx) = self {
if ctx.failures >= MAX_CONSECUTIVE_FAILURES {
log::warn!(
"v4l2 jpeg decoder disabled after {} consecutive failures",
ctx.failures
);
*self = V4l2Probe::Unavailable;
}
}
out
}
}
fn dst_cpu_access(
dma_capable: bool,
output_fmt: PixelFormat,
final_w: usize,
final_h: usize,
) -> edgefirst_tensor::CpuAccess {
let zero_copy = dma_capable
&& matches!(output_fmt, PixelFormat::Nv12 | PixelFormat::Grey)
&& final_w.is_multiple_of(16)
&& final_h.is_multiple_of(16);
if zero_copy {
edgefirst_tensor::CpuAccess::None
} else {
edgefirst_tensor::CpuAccess::Write
}
}
fn require_copy_out_access<T: ImagePixel>(
dst: &Tensor<T>,
output_fmt: PixelFormat,
final_w: usize,
final_h: usize,
) -> Result<(), CodecError> {
let dma_capable = dst.memory() == TensorMemory::DmaBuf;
let needed = dst_cpu_access(dma_capable, output_fmt, final_w, final_h);
Ok(dst.cpu_access().require(needed)?)
}
fn require_scratch_copy_out<T: ImagePixel>(dst: &Tensor<T>) -> Result<(), DecodeErr> {
dst.cpu_access()
.require(edgefirst_tensor::CpuAccess::Write)
.map_err(|e| DecodeErr::Fatal(e.into()))
}
pub(crate) struct V4l2Context {
device: ProbedDevice,
m2m: M2m,
stream: Option<Stream>,
scratch: Option<Tensor<u8>>,
scratch_failed: bool,
failures: u32,
}
struct Stream {
jpeg_w: u32,
jpeg_h: u32,
out_fmt: PixelFormat,
dst_stride: usize,
out_sizeimage: u32,
out_map: Mapping,
cap: Capture,
}
struct Capture {
kind: CapKind,
cap_h: usize,
luma_stride: usize,
target: CaptureTarget,
}
enum CaptureTarget {
DstDma,
Scratch,
}
impl V4l2Context {
fn new(device: ProbedDevice) -> Option<Self> {
let multiplanar = device.api == ApiVariant::MultiPlanar;
let m2m = M2m::new(&device.dev, multiplanar)
.map_err(|e| log::debug!("v4l2: cannot create queues: {e}"))
.ok()?;
Some(Self {
device,
m2m,
stream: None,
scratch: None,
scratch_failed: false,
failures: 0,
})
}
fn decode<T: ImagePixel>(
&mut self,
data: &[u8],
dst: &mut Tensor<T>,
output_fmt: PixelFormat,
final_w: usize,
final_h: usize,
dst_stride: usize,
) -> Result<ImageInfo, DecodeErr> {
let needed = ((data.len() + 4095) & !4095) as u32;
let dma_capable = dst.memory() == TensorMemory::DmaBuf;
let dst_capacity = dst.capacity_bytes();
if self.scratch_failed && !dma_capable {
return Err(DecodeErr::Unsupported(
"hardware decode requires DMA buffers (dma_heap unavailable)".into(),
));
}
let reuse = matches!(
&self.stream,
Some(s) if s.jpeg_w == final_w as u32
&& s.jpeg_h == final_h as u32
&& s.out_fmt == output_fmt
&& s.out_sizeimage >= needed
&& match s.cap.target {
CaptureTarget::DstDma => dma_capable && s.dst_stride == dst_stride,
CaptureTarget::Scratch => true,
}
);
let reconfigure = !reuse && matches!(&self.stream, Some(s) if s.out_sizeimage >= needed);
if reuse {
if matches!(&self.stream, Some(s) if matches!(s.cap.target, CaptureTarget::Scratch)) {
require_scratch_copy_out(dst)?;
}
self.requeue_output(data)?;
} else if reconfigure {
if let Err(e) = self.reconfigure_capture(
data,
output_fmt,
final_w,
final_h,
dst_stride,
dst_capacity,
dma_capable,
) {
match e {
DecodeErr::Reset(why) => {
log::debug!("v4l2: reconfigure failed ({why}); rebuilding stream");
self.rebuild_stream(
data,
output_fmt,
final_w,
final_h,
dst_stride,
dst_capacity,
dma_capable,
needed,
)?;
}
other => return Err(other),
}
}
} else {
self.rebuild_stream(
data,
output_fmt,
final_w,
final_h,
dst_stride,
dst_capacity,
dma_capable,
needed,
)?;
}
self.collect::<T>(dst, output_fmt, final_w, final_h, dst_stride)
}
fn requeue_output(&mut self, data: &[u8]) -> Result<(), DecodeErr> {
let staged = self.stage_output(data)?;
self.queue_output(staged, "QBUF OUTPUT")
}
fn stage_output(&mut self, data: &[u8]) -> Result<usize, DecodeErr> {
if self.m2m.output().is_queued(0) {
return Err(DecodeErr::Reset("OUTPUT buffer still queued".into()));
}
let stream = self
.stream
.as_mut()
.ok_or_else(|| DecodeErr::Reset("no stream".into()))?;
Ok(stage_jpeg(unsafe { stream.out_map.as_mut_slice() }, data))
}
fn queue_output(&self, staged: usize, what: &str) -> Result<(), DecodeErr> {
self.m2m
.output()
.enqueue(
0,
&[Plane::Mmap {
bytesused: staged as u32,
}],
None,
)
.map_err(|e| DecodeErr::Reset(format!("{what}: {e}")))
}
fn await_source_change(&self) {
let _ = events::wait(&self.device.dev, Some(SOURCE_CHANGE_TIMEOUT));
let _ = events::drain(&self.device.dev, MAX_EVENTS);
}
fn capture_format(&self, what: &str) -> Result<uapi::v4l2_pix_format_mplane, DecodeErr> {
let mut fmt = self
.device
.dev
.format(BufType::VideoCaptureMplane)
.map_err(|e| DecodeErr::Reset(format!("{what}: {e}")))?;
Ok(*unsafe { fmt.pix_mp() })
}
#[allow(clippy::too_many_arguments)]
fn rebuild_stream(
&mut self,
data: &[u8],
output_fmt: PixelFormat,
final_w: usize,
final_h: usize,
dst_stride: usize,
dst_capacity: usize,
dma_capable: bool,
needed: u32,
) -> Result<(), DecodeErr> {
let _span =
tracing::trace_span!("codec.decode_jpeg.v4l2_rebuild", w = final_w, h = final_h)
.entered();
self.drop_stream();
events::subscribe(&self.device.dev, uapi::V4L2_EVENT_SOURCE_CHANGE, 0, 0)
.map_err(|e| DecodeErr::Reset(format!("SUBSCRIBE_EVENT: {e}")))?;
let out_request = needed.max(OUT_SIZE_FLOOR);
let mut ofmt = uapi::v4l2_format {
type_: uapi::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
..Default::default()
};
{
let p = unsafe { ofmt.pix_mp() };
p.width = final_w as u32;
p.height = final_h as u32;
p.pixelformat = uapi::V4L2_PIX_FMT_JPEG;
p.field = uapi::V4L2_FIELD_NONE;
p.num_planes = 1;
p.plane_fmt[0].sizeimage = out_request;
}
self.device
.dev
.set_format(&mut ofmt)
.map_err(|e| DecodeErr::Reset(format!("S_FMT OUTPUT: {e}")))?;
self.m2m
.output_mut()
.request(Memory::Mmap, 1)
.map_err(|e| DecodeErr::Reset(format!("REQBUFS OUTPUT: {e}")))?;
let out_map = self
.m2m
.output()
.map(0)
.map_err(|e| DecodeErr::Reset(format!("mmap OUTPUT: {e}")))?
.into_iter()
.next()
.ok_or_else(|| DecodeErr::Reset("mmap OUTPUT: no plane".into()))?;
let olen = out_map.len();
self.m2m
.output()
.stream_on()
.map_err(|e| DecodeErr::Reset(format!("STREAMON OUTPUT: {e}")))?;
let staged = stage_jpeg(unsafe { out_map.as_mut_slice() }, data);
self.queue_output(staged, "QBUF OUTPUT")?;
self.await_source_change();
let cap = self.configure_capture(
output_fmt,
final_w,
final_h,
dst_stride,
dst_capacity,
dma_capable,
)?;
self.stream = Some(Stream {
jpeg_w: final_w as u32,
jpeg_h: final_h as u32,
out_fmt: output_fmt,
dst_stride,
out_sizeimage: olen.min(u32::MAX as usize) as u32,
out_map,
cap,
});
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn reconfigure_capture(
&mut self,
data: &[u8],
output_fmt: PixelFormat,
final_w: usize,
final_h: usize,
dst_stride: usize,
dst_capacity: usize,
dma_capable: bool,
) -> Result<(), DecodeErr> {
let _span = tracing::trace_span!(
"codec.decode_jpeg.v4l2_reconfigure",
w = final_w,
h = final_h
)
.entered();
if self.stream.is_none() {
return Err(DecodeErr::Reset("no stream to reconfigure".into()));
}
self.m2m
.capture_mut()
.free()
.map_err(|e| DecodeErr::Reset(format!("free CAPTURE (reconf): {e}")))?;
let staged = self.stage_output(data)?;
self.queue_output(staged, "QBUF OUTPUT (reconf)")?;
self.await_source_change();
let cap = self.configure_capture(
output_fmt,
final_w,
final_h,
dst_stride,
dst_capacity,
dma_capable,
)?;
let stream = self
.stream
.as_mut()
.ok_or_else(|| DecodeErr::Reset("no stream to reconfigure".into()))?;
stream.jpeg_w = final_w as u32;
stream.jpeg_h = final_h as u32;
stream.out_fmt = output_fmt;
stream.dst_stride = dst_stride;
stream.cap = cap;
Ok(())
}
fn configure_capture(
&mut self,
output_fmt: PixelFormat,
final_w: usize,
final_h: usize,
dst_stride: usize,
dst_capacity: usize,
dma_capable: bool,
) -> Result<Capture, DecodeErr> {
const CAP: u32 = uapi::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
let mut cap = self.capture_format("G_FMT CAPTURE")?;
if (cap.width as usize) < final_w || (cap.height as usize) < final_h {
return Err(DecodeErr::Reset(format!(
"stale CAPTURE geometry {}x{} for {}x{} image",
cap.width, cap.height, final_w, final_h
)));
}
let mut kind = format::classify(cap.pixelformat).ok_or_else(|| {
DecodeErr::Unsupported(format!(
"capture format {} unsupported",
uapi::fourcc_str(cap.pixelformat)
))
})?;
if !colorimetry_ok(cap.colorspace, cap.ycbcr_enc, cap.quantization) {
return Err(DecodeErr::Unsupported(format!(
"capture colorimetry (cs={}, enc={}, quant={}) not BT.601 full-range",
cap.colorspace, cap.ycbcr_enc, cap.quantization
)));
}
if matches!(kind, CapKind::Yuv444Packed) && matches!(output_fmt, PixelFormat::Nv12) {
let mut nfmt = uapi::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { nfmt.pix_mp() };
p.width = cap.width;
p.height = cap.height;
p.pixelformat = uapi::V4L2_PIX_FMT_NV12;
p.field = uapi::V4L2_FIELD_NONE;
p.colorspace = cap.colorspace;
p.ycbcr_enc = cap.ycbcr_enc;
p.quantization = cap.quantization;
p.xfer_func = cap.xfer_func;
p.num_planes = 1;
}
let _ = self.device.dev.set_format(&mut nfmt);
if let Ok(got) = self.capture_format("G_FMT CAPTURE (nv12)") {
if let Some(new_kind) = format::classify(got.pixelformat) {
if matches!(new_kind, CapKind::Nv12) {
log::debug!(
"v4l2: negotiated 4:4:4 CAPTURE from YUV3 to NV12 (hardware 4:4:4→4:2:0)"
);
cap = got;
kind = new_kind;
} else {
log::debug!(
"v4l2: driver kept {} for 4:4:4 JPEG — will deinterleave to NV24",
uapi::fourcc_str(got.pixelformat)
);
}
}
}
}
let cap0 = cap;
let want_zc = dst_cpu_access(dma_capable, output_fmt, final_w, final_h)
== edgefirst_tensor::CpuAccess::None
&& matches!(kind, CapKind::Nv12 | CapKind::Grey);
let mut dst_dma = false;
if want_zc {
debug_assert_eq!(
cap0.height as usize, final_h,
"zero-copy requires the MCU-padded CAPTURE height to equal the image height"
);
debug_assert_eq!(
cap0.width as usize, final_w,
"zero-copy requires the MCU-padded CAPTURE width to equal the image width"
);
let (target_fourcc, total_h) = match output_fmt {
PixelFormat::Nv12 => (uapi::V4L2_PIX_FMT_NV12, cap0.height as usize * 3 / 2),
_ => (uapi::V4L2_PIX_FMT_GREY, cap0.height as usize),
};
let mut sfmt = uapi::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { sfmt.pix_mp() };
p.width = cap0.width;
p.height = cap0.height;
p.pixelformat = target_fourcc;
p.field = uapi::V4L2_FIELD_NONE;
p.colorspace = cap0.colorspace;
p.ycbcr_enc = cap0.ycbcr_enc;
p.quantization = cap0.quantization;
p.xfer_func = cap0.xfer_func;
p.num_planes = 1;
p.plane_fmt[0].bytesperline = dst_stride as u32;
p.plane_fmt[0].sizeimage = (dst_stride * total_h) as u32;
}
let _ = self.device.dev.set_format(&mut sfmt);
let cap_zc = self.capture_format("G_FMT CAPTURE (zc)")?;
let ok = cap_zc.pixelformat == target_fourcc
&& cap_zc.num_planes == 1
&& cap_zc.width as usize == final_w
&& cap_zc.height as usize == final_h
&& cap_zc.plane_fmt[0].bytesperline as usize == dst_stride
&& dst_capacity >= cap_zc.plane_fmt[0].sizeimage as usize;
if ok {
cap = cap_zc;
dst_dma = true;
} else {
let mut rfmt = uapi::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { rfmt.pix_mp() };
*p = cap0;
}
let _ = self.device.dev.set_format(&mut rfmt);
cap = cap0;
}
}
if dst_dma {
self.start_capture("dmabuf")?;
return Ok(Capture {
kind,
cap_h: cap.height as usize,
luma_stride: cap.plane_fmt[0].bytesperline as usize,
target: CaptureTarget::DstDma,
});
}
let mut single = cap;
if cap.num_planes != 1 {
let mut sfmt = uapi::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { sfmt.pix_mp() };
p.width = cap.width;
p.height = cap.height;
p.pixelformat = uapi::V4L2_PIX_FMT_NV12;
p.field = uapi::V4L2_FIELD_NONE;
p.colorspace = cap.colorspace;
p.ycbcr_enc = cap.ycbcr_enc;
p.quantization = cap.quantization;
p.xfer_func = cap.xfer_func;
p.num_planes = 1;
}
let _ = self.device.dev.set_format(&mut sfmt);
single = self.capture_format("G_FMT CAPTURE (scratch)")?;
if single.num_planes != 1 {
return Err(DecodeErr::Unsupported(format!(
"driver insists on a {}-plane CAPTURE; single-plane DMABUF required",
single.num_planes
)));
}
}
let kind = format::classify(single.pixelformat).ok_or_else(|| {
DecodeErr::Unsupported(format!(
"capture format {} unsupported",
uapi::fourcc_str(single.pixelformat)
))
})?;
let sizeimage = single.plane_fmt[0].sizeimage as usize;
if !self.ensure_scratch(sizeimage) {
return Err(DecodeErr::Unsupported(
"hardware decode requires DMA buffers (dma_heap unavailable)".into(),
));
}
self.start_capture("scratch")?;
Ok(Capture {
kind,
cap_h: single.height as usize,
luma_stride: single.plane_fmt[0].bytesperline as usize,
target: CaptureTarget::Scratch,
})
}
fn start_capture(&mut self, target: &str) -> Result<(), DecodeErr> {
self.m2m
.capture_mut()
.request(Memory::DmaBuf, 1)
.map_err(|e| DecodeErr::Reset(format!("REQBUFS CAPTURE ({target}): {e}")))?;
self.m2m
.capture()
.stream_on()
.map_err(|e| DecodeErr::Reset(format!("STREAMON CAPTURE: {e}")))
}
fn decode_into(
&self,
fd: std::os::fd::BorrowedFd<'_>,
length: usize,
target: &str,
) -> Result<(), DecodeErr> {
self.m2m
.capture()
.enqueue(
0,
&[Plane::DmaBuf {
fd,
length: length as u32,
bytesused: 0,
data_offset: 0,
}],
None,
)
.map_err(|e| DecodeErr::Reset(format!("QBUF CAPTURE ({target}): {e}")))?;
match dequeue_within(self.m2m.capture(), DECODE_TIMEOUT) {
Ok(Some(_)) => {}
Ok(None) => return Err(DecodeErr::Reset("CAPTURE decode timeout".into())),
Err(e) => return Err(DecodeErr::Reset(format!("DQBUF CAPTURE ({target}): {e}"))),
}
match dequeue_within(self.m2m.output(), DECODE_TIMEOUT) {
Ok(Some(_)) => Ok(()),
Ok(None) => Err(DecodeErr::Reset("OUTPUT not returned".into())),
Err(e) => Err(DecodeErr::Reset(format!("DQBUF OUTPUT: {e}"))),
}
}
fn ensure_scratch(&mut self, sizeimage: usize) -> bool {
if let Some(s) = &self.scratch {
if s.capacity_bytes() >= sizeimage {
return true;
}
}
let want = (sizeimage + sizeimage / 2).max(SCRATCH_SIZE_FLOOR);
let rows = want.div_ceil(SCRATCH_ROW_BYTES);
self.scratch = None;
match Tensor::<u8>::image(
SCRATCH_ROW_BYTES,
rows,
PixelFormat::Grey,
Some(TensorMemory::DmaBuf),
edgefirst_tensor::CpuAccess::Read,
) {
Ok(t) => {
log::debug!(
"v4l2: capture scratch dmabuf allocated ({} bytes)",
t.capacity_bytes()
);
self.scratch = Some(t);
true
}
Err(e) => {
log::debug!("v4l2: no DMA scratch ({e}); hardware decode unavailable");
self.scratch_failed = true;
false
}
}
}
fn collect<T: ImagePixel>(
&mut self,
dst: &mut Tensor<T>,
output_fmt: PixelFormat,
final_w: usize,
final_h: usize,
dst_stride: usize,
) -> Result<ImageInfo, DecodeErr> {
let is_dst_dma = {
let s = self
.stream
.as_ref()
.ok_or_else(|| DecodeErr::Reset("no stream".into()))?;
matches!(s.cap.target, CaptureTarget::DstDma)
};
let _span = tracing::trace_span!(
"codec.decode_jpeg.v4l2_collect",
target = if is_dst_dma { "dst_dma" } else { "scratch" },
)
.entered();
if is_dst_dma {
let capacity = dst.capacity_bytes();
let dmabuf = dst.dmabuf().map_err(|e| DecodeErr::Fatal(e.into()))?;
self.decode_into(dmabuf, capacity, "dmabuf")?;
return Ok(ImageInfo {
width: final_w,
height: final_h,
format: output_fmt,
row_stride: dst_stride,
rotation_degrees: 0,
flip_horizontal: false,
});
}
require_scratch_copy_out(dst)?;
{
let t = self
.scratch
.as_ref()
.ok_or_else(|| DecodeErr::Reset("capture scratch missing".into()))?;
let dmabuf = t.dmabuf().map_err(|e| DecodeErr::Fatal(e.into()))?;
self.decode_into(dmabuf, t.capacity_bytes(), "scratch")?;
}
let stream = self
.stream
.as_ref()
.ok_or_else(|| DecodeErr::Reset("no stream".into()))?;
let smap = self
.scratch
.as_mut()
.ok_or_else(|| DecodeErr::Reset("capture scratch missing".into()))?
.map_read()
.map_err(|e| DecodeErr::Fatal(e.into()))?;
write_planes(
&stream.cap,
&smap,
dst,
output_fmt,
final_w,
final_h,
dst_stride,
)
}
fn drop_stream(&mut self) {
let stream = self.stream.take();
let active = |q: &Queue| q.is_streaming() || !q.is_empty();
if stream.is_none() && !active(self.m2m.output()) && !active(self.m2m.capture()) {
return;
}
let _ = self.m2m.stream_off();
drop(stream);
let _ = self.m2m.capture_mut().free();
let _ = self.m2m.output_mut().free();
}
fn fail_reset(&mut self) {
self.drop_stream();
self.failures = self.failures.saturating_add(1);
}
}
impl Drop for V4l2Context {
fn drop(&mut self) {
self.drop_stream();
}
}
fn write_planes<T: ImagePixel>(
cap: &Capture,
src: &[u8],
dst: &mut Tensor<T>,
output_fmt: PixelFormat,
final_w: usize,
final_h: usize,
dst_stride: usize,
) -> Result<ImageInfo, DecodeErr> {
let _span = tracing::trace_span!(
"codec.decode_jpeg.v4l2_copy_out",
kind = match cap.kind {
CapKind::Yuv444Packed => "yuv24_to_nv24",
CapKind::Nv12 => "nv12",
CapKind::Grey => "grey",
},
)
.entered();
let y_stride = cap.luma_stride;
if matches!(cap.kind, CapKind::Yuv444Packed) {
if !matches!(output_fmt, PixelFormat::Nv24) {
return Err(DecodeErr::Unsupported(format!(
"4:4:4 YUV3 capture but output format is {output_fmt:?} (expected Nv24)"
)));
}
let mut map = dst.map_write().map_err(|e| DecodeErr::Fatal(e.into()))?;
let dst_bytes: &mut [T] = &mut map;
let d: &mut [u8] = unsafe {
std::slice::from_raw_parts_mut(dst_bytes.as_mut_ptr() as *mut u8, dst_bytes.len())
};
deinterleave_yuv24_to_nv24(src, y_stride, d, dst_stride, final_w, final_h);
drop(map);
return Ok(ImageInfo {
width: final_w,
height: final_h,
format: PixelFormat::Nv24,
row_stride: dst_stride,
rotation_degrees: 0,
flip_horizontal: false,
});
}
let (y_plane, chroma): (&[u8], Option<(&[u8], usize)>) = match &cap.kind {
CapKind::Nv12 => {
let ys = cap.luma_stride * cap.cap_h;
(&src[..ys], Some((&src[ys..], cap.luma_stride)))
}
CapKind::Grey => (src, None),
CapKind::Yuv444Packed => unreachable!("handled above"),
};
let actual_fmt = match &cap.kind {
CapKind::Grey => PixelFormat::Grey,
CapKind::Nv12 => PixelFormat::Nv12,
CapKind::Yuv444Packed => unreachable!("handled above"),
};
if actual_fmt != output_fmt {
dst.configure_image(final_w, final_h, actual_fmt)
.map_err(|e| DecodeErr::Fatal(e.into()))?;
}
let mut map = dst.map_write().map_err(|e| DecodeErr::Fatal(e.into()))?;
let dst_bytes: &mut [T] = &mut map;
let d: &mut [u8] = unsafe {
std::slice::from_raw_parts_mut(dst_bytes.as_mut_ptr() as *mut u8, dst_bytes.len())
};
match actual_fmt {
PixelFormat::Grey => {
for y in 0..final_h {
let s = y * y_stride;
let o = y * dst_stride;
d[o..o + final_w].copy_from_slice(&y_plane[s..s + final_w]);
}
}
PixelFormat::Nv12 => {
let Some((cbcr, c_stride)) = chroma else {
return Err(DecodeErr::Unsupported(
"NV12 output requires a chroma plane from the decoder".into(),
));
};
for y in 0..final_h {
let s = y * y_stride;
let o = y * dst_stride;
d[o..o + final_w].copy_from_slice(&y_plane[s..s + final_w]);
}
let uv_base = final_h * dst_stride;
copy_nv12_chroma(
cbcr,
c_stride,
&mut d[uv_base..],
dst_stride,
final_w,
final_h,
);
}
other => {
return Err(DecodeErr::Unsupported(format!(
"output {other:?} unsupported"
)));
}
}
drop(map);
Ok(ImageInfo {
width: final_w,
height: final_h,
format: actual_fmt,
row_stride: dst_stride,
rotation_degrees: 0,
flip_horizontal: false,
})
}
fn stage_jpeg(out: &mut [u8], data: &[u8]) -> usize {
match copy_jpeg_stripped(out, data) {
Some(n) => n,
None => {
out[..data.len()].copy_from_slice(data);
data.len()
}
}
}
fn copy_jpeg_stripped(out: &mut [u8], data: &[u8]) -> Option<usize> {
if data.len() < 4 || data[0] != 0xFF || data[1] != 0xD8 {
return None;
}
out.get_mut(..2)?.copy_from_slice(&data[..2]); let mut o = 2;
let mut i = 2;
loop {
if i + 4 > data.len() || data[i] != 0xFF {
return None;
}
let marker = data[i + 1];
if marker == 0xDA {
let rest = &data[i..];
out.get_mut(o..o + rest.len())?.copy_from_slice(rest);
return Some(o + rest.len());
}
if marker == 0xFF || marker == 0x01 || (0xD0..=0xD9).contains(&marker) {
return None;
}
let len = ((data[i + 2] as usize) << 8) | data[i + 3] as usize;
if len < 2 || i + 2 + len > data.len() {
return None;
}
let keep = !matches!(marker, 0xE1..=0xED | 0xEF | 0xFE);
if keep {
let seg = &data[i..i + 2 + len];
out.get_mut(o..o + seg.len())?.copy_from_slice(seg);
o += seg.len();
}
i += 2 + len;
}
}
fn deinterleave_yuv24_to_nv24(
src: &[u8],
src_stride: usize,
dst: &mut [u8],
dst_stride: usize,
w: usize,
h: usize,
) {
let (y_region, uv_region) = dst.split_at_mut(h * dst_stride);
for row in 0..h {
let s = &src[row * src_stride..][..w * 3];
let dy = &mut y_region[row * dst_stride..][..w];
let duv = &mut uv_region[row * 2 * dst_stride..][..w * 2];
deinterleave_row(s, dy, duv, w);
}
}
#[inline]
fn deinterleave_row(src: &[u8], dy: &mut [u8], duv: &mut [u8], w: usize) {
#[cfg(target_arch = "aarch64")]
{
unsafe { deinterleave_row_neon(src, dy, duv, w) }
}
#[cfg(not(target_arch = "aarch64"))]
deinterleave_row_scalar(src, dy, duv, 0, w);
}
fn deinterleave_row_scalar(src: &[u8], dy: &mut [u8], duv: &mut [u8], start: usize, w: usize) {
for x in start..w {
let s = x * 3;
dy[x] = src[s];
duv[2 * x] = src[s + 1];
duv[2 * x + 1] = src[s + 2];
}
}
#[cfg(target_arch = "aarch64")]
unsafe fn deinterleave_row_neon(src: &[u8], dy: &mut [u8], duv: &mut [u8], w: usize) {
use core::arch::aarch64::{uint8x16x2_t, vld3q_u8, vst1q_u8, vst2q_u8};
let mut x = 0usize;
while x + 16 <= w {
let v = vld3q_u8(src.as_ptr().add(x * 3));
vst1q_u8(dy.as_mut_ptr().add(x), v.0);
vst2q_u8(duv.as_mut_ptr().add(x * 2), uint8x16x2_t(v.1, v.2));
x += 16;
}
deinterleave_row_scalar(src, dy, duv, x, w);
}
fn copy_nv12_chroma(
src: &[u8],
src_stride: usize,
dst: &mut [u8],
dst_stride: usize,
final_w: usize,
final_h: usize,
) {
let chroma_w = final_w.next_multiple_of(2);
for cy in 0..final_h.div_ceil(2) {
let s = cy * src_stride;
let o = cy * dst_stride;
dst[o..o + chroma_w].copy_from_slice(&src[s..s + chroma_w]);
}
}
fn colorimetry_ok(colorspace: u32, ycbcr_enc: u8, quantization: u8) -> bool {
if colorspace == uapi::V4L2_COLORSPACE_JPEG {
return true;
}
let enc_ok = ycbcr_enc == 0 || ycbcr_enc == uapi::V4L2_YCBCR_ENC_601;
let quant_ok = quantization == uapi::V4L2_QUANTIZATION_DEFAULT
|| quantization == uapi::V4L2_QUANTIZATION_FULL_RANGE;
enc_ok && quant_ok
}
fn dequeue_within(queue: &Queue, timeout: Duration) -> edgefirst_v4l2::Result<Option<Dequeued>> {
if !queue.wait(Some(timeout))? {
return Ok(None);
}
queue.dequeue()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_copy_needs_dma_nv12_or_grey_and_mcu_aligned_dims() {
use edgefirst_tensor::CpuAccess::{None as ZeroCopy, Write as CopyOut};
assert_eq!(dst_cpu_access(true, PixelFormat::Nv12, 640, 480), ZeroCopy);
assert_eq!(dst_cpu_access(true, PixelFormat::Grey, 16, 16), ZeroCopy);
assert_eq!(dst_cpu_access(false, PixelFormat::Nv12, 640, 480), CopyOut);
assert_eq!(dst_cpu_access(true, PixelFormat::Nv24, 640, 480), CopyOut);
assert_eq!(dst_cpu_access(true, PixelFormat::Nv12, 650, 480), CopyOut);
assert_eq!(dst_cpu_access(true, PixelFormat::Nv12, 640, 470), CopyOut);
}
#[test]
fn copy_out_requires_a_declared_write() {
use edgefirst_tensor::{CpuAccess, Error as TensorError};
for (declared, refused) in [
(CpuAccess::None, true),
(CpuAccess::Read, true),
(CpuAccess::Write, false),
(CpuAccess::ReadWrite, false),
] {
let dst =
Tensor::<u8>::image(64, 48, PixelFormat::Nv12, Some(TensorMemory::Mem), declared)
.unwrap();
match require_copy_out_access(&dst, PixelFormat::Nv12, 64, 48) {
Err(CodecError::Tensor(TensorError::CpuAccessDenied {
requested: CpuAccess::Write,
..
})) => assert!(refused, "{declared:?}"),
Ok(()) => assert!(!refused, "{declared:?}"),
other => panic!("{declared:?}: {other:?}"),
}
}
}
const ENC_709: u8 = 2;
const QUANT_LIMITED: u8 = 2;
#[test]
fn colorimetry_accepts_jpeg_and_full_range_601() {
assert!(colorimetry_ok(
uapi::V4L2_COLORSPACE_JPEG,
ENC_709,
QUANT_LIMITED
));
assert!(colorimetry_ok(
uapi::V4L2_COLORSPACE_SRGB,
uapi::V4L2_YCBCR_ENC_601,
uapi::V4L2_QUANTIZATION_FULL_RANGE
));
assert!(colorimetry_ok(uapi::V4L2_COLORSPACE_SRGB, 0, 0));
}
#[test]
fn colorimetry_rejects_non_601_or_limited_range() {
assert!(!colorimetry_ok(
uapi::V4L2_COLORSPACE_SRGB,
ENC_709,
uapi::V4L2_QUANTIZATION_FULL_RANGE
));
assert!(!colorimetry_ok(
uapi::V4L2_COLORSPACE_SRGB,
uapi::V4L2_YCBCR_ENC_601,
QUANT_LIMITED
));
}
#[test]
fn copy_nv12_chroma_odd_w_preserves_last_cr() {
let final_w = 5usize;
let final_h = 2usize;
let src_stride = 8usize; let dst_stride = 64usize;
let mut src = vec![0u8; src_stride];
src[0] = 10; src[1] = 11; src[2] = 20; src[3] = 21; src[4] = 30; src[5] = 31;
let mut dst = vec![0u8; dst_stride];
copy_nv12_chroma(&src, src_stride, &mut dst, dst_stride, final_w, final_h);
assert_eq!(
&dst[..6],
&src[..6],
"copy_nv12_chroma: first 6 bytes (even(5)) must be copied verbatim"
);
assert_eq!(
dst[5], 31,
"copy_nv12_chroma: last Cr at byte 5 must be preserved for odd final_w=5"
);
}
#[test]
fn copy_nv12_chroma_even_w_no_extra_byte() {
let final_w = 4usize; let final_h = 2usize;
let src_stride = 8usize;
let dst_stride = 64usize;
let mut src = vec![0u8; src_stride];
src[0] = 1;
src[1] = 2;
src[2] = 3;
src[3] = 4; src[4] = 99;
let mut dst = vec![0u8; dst_stride];
copy_nv12_chroma(&src, src_stride, &mut dst, dst_stride, final_w, final_h);
assert_eq!(&dst[..4], &src[..4]);
assert_eq!(
dst[4], 0,
"copy_nv12_chroma: byte beyond even(final_w) must remain untouched"
);
}
fn fake_jpeg(segments: &[(u8, &[u8])]) -> Vec<u8> {
let mut v = vec![0xFF, 0xD8];
for (marker, payload) in segments {
v.extend_from_slice(&[0xFF, *marker]);
let len = (payload.len() + 2) as u16;
v.extend_from_slice(&len.to_be_bytes());
v.extend_from_slice(payload);
}
v.extend_from_slice(&[0xFF, 0xDA, 0x00, 0x08, 1, 1, 0, 0, 63, 0]);
v.extend_from_slice(&[0xAB; 32]);
v.extend_from_slice(&[0xFF, 0xD9]);
v
}
#[test]
fn stage_jpeg_strips_metadata_segments() {
let evil_app13 = [
b"Photoshop 3.0\x00".as_slice(),
&[0xFF, 0xD8, 0xFF, 0xDA, 0x12, 0x34],
]
.concat();
let src = fake_jpeg(&[
(0xE0, b"JFIF\x00"), (0xE1, &[0x55; 64]), (0xED, &evil_app13), (0xDB, &[0x11; 65]), (0xC0, &[8, 0, 16, 0, 16, 1, 1, 0x11, 0]), (0xEE, b"Adobe\x00"), (0xC4, &[0x22; 20]), (0xFE, b"comment"), ]);
let want = fake_jpeg(&[
(0xE0, b"JFIF\x00"),
(0xDB, &[0x11; 65]),
(0xC0, &[8, 0, 16, 0, 16, 1, 1, 0x11, 0]),
(0xEE, b"Adobe\x00"),
(0xC4, &[0x22; 20]),
]);
let mut out = vec![0u8; src.len()];
let n = stage_jpeg(&mut out, &src);
assert_eq!(&out[..n], &want[..]);
}
#[test]
fn stage_jpeg_falls_back_to_verbatim() {
for bad in [
&b"not a jpeg at all"[..],
&[0xFF, 0xD8, 0x00, 0x00, 0xFF][..], &[0xFF, 0xD8, 0xFF, 0xE1, 0xFF, 0xFF, 0x00][..], ] {
let mut out = vec![0u8; bad.len()];
let n = stage_jpeg(&mut out, bad);
assert_eq!(&out[..n], bad);
}
}
#[test]
fn deinterleave_yuv24_matches_reference() {
for &(w, h) in &[(37usize, 5usize), (16, 3), (1, 1), (64, 2), (53, 4)] {
let src_stride = (w * 3).next_multiple_of(64);
let dst_stride = w.next_multiple_of(64);
let mut src = vec![0u8; src_stride * h];
for (i, v) in src.iter_mut().enumerate() {
*v = (i % 251) as u8;
}
let mut got = vec![0u8; 3 * h * dst_stride];
deinterleave_yuv24_to_nv24(&src, src_stride, &mut got, dst_stride, w, h);
let mut want = vec![0u8; 3 * h * dst_stride];
let uv_base = h * dst_stride;
for y in 0..h {
for x in 0..w {
let s = y * src_stride + x * 3;
want[y * dst_stride + x] = src[s];
let u = uv_base + y * 2 * dst_stride + x * 2;
want[u] = src[s + 1];
want[u + 1] = src[s + 2];
}
}
assert_eq!(got, want, "mismatch at {w}x{h}");
}
}
fn testdata_file(name: &str) -> Option<Vec<u8>> {
let root = std::env::var("EDGEFIRST_TESTDATA_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../testdata")
});
std::fs::read(root.join(name)).ok()
}
#[test]
#[ignore = "on-target hardware probe; run with --ignored --nocapture on a JPEG M2M device"]
fn probe_dmabuf_reconfigure() {
use std::time::Instant;
const CAP: u32 = uapi::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
let Some(dev) = device::probe() else {
crate::test_support::report_skip(
"probe_dmabuf_reconfigure - no v4l2 jpeg decoder on this host",
);
return;
};
if dev.api != ApiVariant::MultiPlanar {
crate::test_support::report_skip(
"probe_dmabuf_reconfigure - device is not multi-planar",
);
return;
}
let (Some(jpeg_a), Some(jpeg_b)) =
(testdata_file("zidane.jpg"), testdata_file("giraffe.jpg"))
else {
crate::test_support::report_skip(
"probe_dmabuf_reconfigure - testdata not found (set EDGEFIRST_TESTDATA_DIR)",
);
return;
};
let Some(scratch) = crate::test_support::dma_allocated(
Tensor::<u8>::image(
4096,
1024,
PixelFormat::Grey,
Some(TensorMemory::DmaBuf),
edgefirst_tensor::CpuAccess::ReadWrite,
),
"probe_dmabuf_reconfigure scratch DMA-BUF",
) else {
return;
};
let scratch_fd = scratch.dmabuf().unwrap();
let scratch_cap = scratch.capacity_bytes();
let mut m2m = M2m::new(&dev.dev, true).expect("queues");
events::subscribe(&dev.dev, uapi::V4L2_EVENT_SOURCE_CHANGE, 0, 0).expect("SUBSCRIBE_EVENT");
let mut ofmt = uapi::v4l2_format {
type_: uapi::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
..Default::default()
};
{
let p = unsafe { ofmt.pix_mp() };
p.width = 1280;
p.height = 720;
p.pixelformat = uapi::V4L2_PIX_FMT_JPEG;
p.field = uapi::V4L2_FIELD_NONE;
p.num_planes = 1;
p.plane_fmt[0].sizeimage = 2 * 1024 * 1024;
}
dev.dev.set_format(&mut ofmt).expect("S_FMT OUTPUT");
m2m.output_mut()
.request(Memory::Mmap, 1)
.expect("REQBUFS OUTPUT");
let out_map = m2m.output().map(0).expect("mmap OUTPUT").remove(0);
m2m.output().stream_on().expect("STREAMON OUTPUT");
for round in 0..10 {
for (name, jpeg) in [("zidane 1280x720", &jpeg_a), ("giraffe 640x640", &jpeg_b)] {
let t0 = Instant::now();
if m2m.capture().is_streaming() {
m2m.capture_mut()
.free()
.expect("STREAMOFF + REQBUFS CAPTURE 0");
}
let coded = unsafe { out_map.as_mut_slice() };
coded[..jpeg.len()].copy_from_slice(jpeg);
m2m.output()
.enqueue(
0,
&[Plane::Mmap {
bytesused: jpeg.len() as u32,
}],
None,
)
.expect("QBUF OUTPUT");
let _ = events::wait(&dev.dev, Some(SOURCE_CHANGE_TIMEOUT));
let _ = events::drain(&dev.dev, MAX_EVENTS);
let mut gfmt = dev
.dev
.format(BufType::VideoCaptureMplane)
.expect("G_FMT CAPTURE");
let got = *unsafe { gfmt.pix_mp() };
let t_gfmt = t0.elapsed();
let mut sfmt = uapi::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { sfmt.pix_mp() };
p.width = got.width;
p.height = got.height;
p.pixelformat = uapi::V4L2_PIX_FMT_NV12;
p.field = uapi::V4L2_FIELD_NONE;
p.colorspace = got.colorspace;
p.num_planes = 1;
}
let sfmt_res = dev.dev.set_format(&mut sfmt);
let mut gfmt2 = dev
.dev
.format(BufType::VideoCaptureMplane)
.expect("G_FMT CAPTURE (post S_FMT)");
let cap_fmt = *unsafe { gfmt2.pix_mp() };
assert_eq!(
cap_fmt.num_planes, 1,
"probe requires the single-plane CAPTURE the backend uses"
);
let sizeimage = cap_fmt.plane_fmt[0].sizeimage as usize;
assert!(
scratch_cap >= sizeimage,
"scratch too small: {scratch_cap} < {sizeimage}"
);
let t1 = Instant::now();
m2m.capture_mut()
.request(Memory::DmaBuf, 1)
.expect("REQBUFS CAPTURE dmabuf");
let t_reqbufs = t1.elapsed();
m2m.capture().stream_on().expect("STREAMON CAPTURE");
m2m.capture()
.enqueue(
0,
&[Plane::DmaBuf {
fd: scratch_fd,
length: scratch_cap as u32,
bytesused: 0,
data_offset: 0,
}],
None,
)
.expect("QBUF CAPTURE dmabuf");
dequeue_within(m2m.capture(), DECODE_TIMEOUT)
.expect("DQBUF CAPTURE")
.expect("decode timeout");
dequeue_within(m2m.output(), DECODE_TIMEOUT)
.expect("DQBUF OUTPUT")
.expect("OUTPUT not returned");
let total = t0.elapsed();
let sfmt_str = match &sfmt_res {
Ok(()) => "ok".to_string(),
Err(e) => format!("ERR({e})"),
};
eprintln!(
"round {round:2} {name:18} total={total:9.2?} hdr+gfmt={t_gfmt:9.2?} \
reqbufs={t_reqbufs:9.2?} s_fmt={sfmt_str} fmt={} {}x{} sizeimage={}",
uapi::fourcc_str(cap_fmt.pixelformat),
cap_fmt.width,
cap_fmt.height,
sizeimage,
);
}
}
let _ = m2m.stream_off();
drop(out_map);
let _ = m2m.capture_mut().free();
let _ = m2m.output_mut().free();
}
fn run_throughput(dev: &ProbedDevice, jpeg: &[u8], depth: u32, frames: usize) -> Option<f64> {
use std::time::Instant;
const CAP: u32 = uapi::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
let mut m2m = M2m::new(&dev.dev, true).ok()?;
events::subscribe(&dev.dev, uapi::V4L2_EVENT_SOURCE_CHANGE, 0, 0).ok()?;
let mut ofmt = uapi::v4l2_format {
type_: uapi::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
..Default::default()
};
{
let p = unsafe { ofmt.pix_mp() };
p.width = 1280;
p.height = 720;
p.pixelformat = uapi::V4L2_PIX_FMT_JPEG;
p.field = uapi::V4L2_FIELD_NONE;
p.num_planes = 1;
p.plane_fmt[0].sizeimage = ((jpeg.len() + 4095) & !4095) as u32;
}
dev.dev.set_format(&mut ofmt).ok()?;
let got_out = m2m.output_mut().request(Memory::Mmap, depth).ok()?;
if got_out < depth {
eprintln!(" driver clamped OUTPUT buffers: {depth} -> {got_out}");
}
let mut out_maps = Vec::new();
for i in 0..got_out {
let m = m2m.output().map(i).ok()?.remove(0);
let staging = unsafe { m.as_mut_slice() };
staging[..jpeg.len()].copy_from_slice(jpeg);
out_maps.push(m);
}
let coded = [Plane::Mmap {
bytesused: jpeg.len() as u32,
}];
m2m.output().stream_on().ok()?;
m2m.output().enqueue(0, &coded, None).ok()?;
let _ = events::wait(&dev.dev, Some(SOURCE_CHANGE_TIMEOUT));
let _ = events::drain(&dev.dev, MAX_EVENTS);
let mut gfmt = dev.dev.format(BufType::VideoCaptureMplane).ok()?;
let got = *unsafe { gfmt.pix_mp() };
let mut sfmt = uapi::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { sfmt.pix_mp() };
p.width = got.width;
p.height = got.height;
p.pixelformat = uapi::V4L2_PIX_FMT_NV12;
p.field = uapi::V4L2_FIELD_NONE;
p.colorspace = got.colorspace;
p.num_planes = 1;
}
dev.dev.set_format(&mut sfmt).ok()?;
let mut gfmt = dev.dev.format(BufType::VideoCaptureMplane).ok()?;
let cap_fmt = *unsafe { gfmt.pix_mp() };
if cap_fmt.num_planes != 1 {
crate::test_support::report_skip("capture is not single-plane");
return None;
}
let sizeimage = cap_fmt.plane_fmt[0].sizeimage as usize;
let mut scratches = Vec::new();
for _ in 0..depth {
let rows = sizeimage.div_ceil(4096);
let t = crate::test_support::dma_allocated(
Tensor::<u8>::image(
4096,
rows,
PixelFormat::Grey,
Some(TensorMemory::DmaBuf),
edgefirst_tensor::CpuAccess::ReadWrite,
),
"run_throughput capture DMA-BUF",
)?;
scratches.push(t);
}
let got_cap = m2m.capture_mut().request(Memory::DmaBuf, depth).ok()?;
if got_cap < depth {
eprintln!(" driver clamped CAPTURE buffers: {depth} -> {got_cap}");
}
m2m.capture().stream_on().ok()?;
let queue_capture = |i: u32| {
let t = &scratches[i as usize];
m2m.capture().enqueue(
i,
&[Plane::DmaBuf {
fd: t.dmabuf().unwrap(),
length: t.capacity_bytes() as u32,
bytesused: 0,
data_offset: 0,
}],
None,
)
};
let live = depth.min(got_out).min(got_cap);
for i in 0..live {
queue_capture(i).ok()?;
}
for i in 1..live {
m2m.output().enqueue(i, &coded, None).ok()?;
}
let warmup = 30usize;
let mut t0 = Instant::now();
for n in 0..(warmup + frames) {
if n == warmup {
t0 = Instant::now();
}
let Some(c) = dequeue_within(m2m.capture(), DECODE_TIMEOUT).ok()? else {
eprintln!(" decode timeout at frame {n}");
return None;
};
let o = dequeue_within(m2m.output(), DECODE_TIMEOUT).ok()??;
m2m.output().enqueue(o.index, &coded, None).ok()?;
queue_capture(c.index).ok()?;
}
let fps = frames as f64 / t0.elapsed().as_secs_f64();
let _ = m2m.stream_off();
drop(out_maps);
let _ = m2m.capture_mut().free();
let _ = m2m.output_mut().free();
Some(fps)
}
#[test]
#[ignore = "on-target hardware probe; run with --ignored --nocapture on a JPEG M2M device"]
fn probe_decode_throughput() {
let Some(jpeg) = testdata_file("zidane.jpg") else {
crate::test_support::report_skip(
"probe_decode_throughput - testdata not found (set EDGEFIRST_TESTDATA_DIR)",
);
return;
};
let frames = 300usize;
for depth in [1u32, 2, 4] {
let Some(dev) = device::probe() else {
crate::test_support::report_skip(
"probe_decode_throughput - no v4l2 jpeg decoder on this host",
);
return;
};
match run_throughput(&dev, &jpeg, depth, frames) {
Some(fps) => eprintln!("single context, depth {depth}: {fps:7.1} FPS"),
None => eprintln!("single context, depth {depth}: failed"),
}
}
for contexts in [2usize, 4] {
let mut devs = Vec::new();
for _ in 0..contexts {
match device::probe() {
Some(d) => devs.push(d),
None => {
crate::test_support::report_skip(
"probe_decode_throughput - could not open a second context",
);
return;
}
}
}
let t0 = std::time::Instant::now();
std::thread::scope(|s| {
for dev in &devs {
let jpeg = &jpeg;
s.spawn(move || {
if run_throughput(dev, jpeg, 1, frames).is_none() {
eprintln!(" parallel context failed");
}
});
}
});
let total = contexts * (frames + 30);
let fps = total as f64 / t0.elapsed().as_secs_f64();
eprintln!("{contexts} contexts, depth 1: {fps:7.1} FPS aggregate");
}
}
fn leave_partial_setup(ctx: &mut V4l2Context, jpeg: &[u8], w: u32, h: u32, capture: bool) {
let mut ofmt = uapi::v4l2_format {
type_: uapi::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
..Default::default()
};
{
let p = unsafe { ofmt.pix_mp() };
p.width = w;
p.height = h;
p.pixelformat = uapi::V4L2_PIX_FMT_JPEG;
p.field = uapi::V4L2_FIELD_NONE;
p.num_planes = 1;
p.plane_fmt[0].sizeimage = OUT_SIZE_FLOOR;
}
ctx.device.dev.set_format(&mut ofmt).expect("S_FMT OUTPUT");
ctx.m2m
.output_mut()
.request(Memory::Mmap, 1)
.expect("REQBUFS OUTPUT");
let out_map = ctx.m2m.output().map(0).expect("mmap OUTPUT").remove(0);
ctx.m2m.output().stream_on().expect("STREAMON OUTPUT");
let staged = stage_jpeg(unsafe { out_map.as_mut_slice() }, jpeg);
assert!(ctx.queue_output(staged, "QBUF OUTPUT").is_ok());
if capture {
ctx.m2m
.capture_mut()
.request(Memory::DmaBuf, 1)
.expect("REQBUFS CAPTURE");
}
assert!(ctx.stream.is_none());
}
#[test]
fn v4l2_recovers_from_partial_setup() {
let Some(jpeg) = testdata_file("zidane.jpg") else {
crate::test_support::report_skip("testdata not found (set EDGEFIRST_TESTDATA_DIR)");
return;
};
let info = crate::peek_info(&jpeg).expect("peek zidane.jpg");
let (w, h) = (info.width, info.height);
for capture in [false, true] {
let Some(dev) = device::probe() else {
crate::test_support::report_skip("no v4l2 jpeg decoder on this host");
return;
};
if dev.api != ApiVariant::MultiPlanar {
crate::test_support::report_skip("v4l2 jpeg decoder is not multi-planar");
return;
}
let mut ctx = V4l2Context::new(dev).expect("v4l2 context");
leave_partial_setup(&mut ctx, &jpeg, w as u32, h as u32, capture);
ctx.fail_reset();
for (name, q) in [("OUTPUT", ctx.m2m.output()), ("CAPTURE", ctx.m2m.capture())] {
assert!(
!q.is_streaming(),
"{name} still streaming (capture={capture})"
);
assert!(q.is_empty(), "{name} buffers not freed (capture={capture})");
}
let mut dst = Tensor::<u8>::image(
w,
h,
PixelFormat::Nv12,
Some(TensorMemory::Mem),
edgefirst_tensor::CpuAccess::ReadWrite,
)
.expect("destination tensor");
match ctx.decode::<u8>(&jpeg, &mut dst, PixelFormat::Nv12, w, h, w) {
Ok(got) => assert_eq!((got.width, got.height), (w, h)),
Err(DecodeErr::Reset(why) | DecodeErr::Unsupported(why)) => {
panic!("hardware decode after reset failed (capture={capture}): {why}")
}
Err(DecodeErr::Fatal(e)) => panic!("decode after reset (capture={capture}): {e}"),
}
}
}
}