mod buffers;
mod device;
mod format;
mod ioctl;
use std::os::fd::{BorrowedFd, RawFd};
use std::os::raw::c_int;
use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
use crate::error::CodecError;
use crate::jpeg::markers::JpegHeaders;
use crate::options::ImageInfo;
use crate::pixel::ImagePixel;
use buffers::Mmap;
use device::{ApiVariant, ProbedDevice};
use edgefirst_tensor::{PixelFormat, Tensor, TensorMemory, TensorTrait};
use format::CapKind;
use std::os::fd::AsRawFd;
const SOURCE_CHANGE_TIMEOUT_MS: i32 = 200;
const DECODE_TIMEOUT_MS: i32 = 2000;
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),
}
#[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() {
Some(dev) => V4l2Probe::Ready(V4l2Context::new(dev)),
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);
}
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)) => 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
}
}
pub(crate) struct V4l2Context {
device: ProbedDevice,
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: Mmap,
cap: Capture,
}
struct Capture {
kind: CapKind,
cap_h: usize,
luma_stride: usize,
target: CaptureTarget,
}
enum CaptureTarget {
DstDma,
Scratch,
}
impl V4l2Context {
fn new(device: ProbedDevice) -> Self {
Self {
device,
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::Dma;
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 {
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 fd = self.device.fd();
let stream = self
.stream
.as_mut()
.ok_or_else(|| DecodeErr::Reset("no stream".into()))?;
let staged = stage_jpeg(stream.out_map.as_mut_slice(), data);
qbuf_output(fd, staged).map_err(|e| DecodeErr::Reset(format!("QBUF OUTPUT: {e}")))
}
#[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();
let fd = self.device.fd();
const OUT: u32 = ioctl::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
let sub = ioctl::v4l2_event_subscription {
type_: ioctl::V4L2_EVENT_SOURCE_CHANGE,
..Default::default()
};
unsafe { ioctl::vidioc_subscribe_event(fd, &sub) }
.map_err(|e| DecodeErr::Reset(format!("SUBSCRIBE_EVENT: {e}")))?;
let out_request = needed.max(OUT_SIZE_FLOOR);
let mut ofmt = ioctl::v4l2_format {
type_: OUT,
..Default::default()
};
{
let p = unsafe { ofmt.pix_mp() };
p.width = final_w as u32;
p.height = final_h as u32;
p.pixelformat = ioctl::V4L2_PIX_FMT_JPEG;
p.field = ioctl::V4L2_FIELD_NONE;
p.num_planes = 1;
p.plane_fmt[0].sizeimage = out_request;
}
unsafe { ioctl::vidioc_s_fmt(fd, &mut ofmt) }
.map_err(|e| DecodeErr::Reset(format!("S_FMT OUTPUT: {e}")))?;
reqbufs(fd, OUT, 1, ioctl::V4L2_MEMORY_MMAP)
.map_err(|e| DecodeErr::Reset(format!("REQBUFS OUTPUT: {e}")))?;
let (olen, ooff) =
querybuf(fd, OUT, 1).map_err(|e| DecodeErr::Reset(format!("QUERYBUF OUTPUT: {e}")))?;
let mut out_map = Mmap::new(borrow(fd), olen, ooff)
.map_err(|e| DecodeErr::Reset(format!("mmap OUTPUT: {e}")))?;
streamon(fd, OUT).map_err(|e| DecodeErr::Reset(format!("STREAMON OUTPUT: {e}")))?;
let staged = stage_jpeg(out_map.as_mut_slice(), data);
qbuf_output(fd, staged).map_err(|e| DecodeErr::Reset(format!("QBUF OUTPUT: {e}")))?;
poll_ready(fd, PollFlags::POLLPRI, SOURCE_CHANGE_TIMEOUT_MS);
drain_events(fd);
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();
let fd = self.device.fd();
const CAP: u32 = ioctl::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
if self.stream.is_none() {
return Err(DecodeErr::Reset("no stream to reconfigure".into()));
}
streamoff(fd, CAP)
.map_err(|e| DecodeErr::Reset(format!("STREAMOFF CAPTURE (reconf): {e}")))?;
reqbufs(fd, CAP, 0, ioctl::V4L2_MEMORY_DMABUF)
.map_err(|e| DecodeErr::Reset(format!("REQBUFS CAPTURE 0 (reconf): {e}")))?;
let staged = {
let stream = self
.stream
.as_mut()
.ok_or_else(|| DecodeErr::Reset("no stream to reconfigure".into()))?;
stage_jpeg(stream.out_map.as_mut_slice(), data)
};
qbuf_output(fd, staged)
.map_err(|e| DecodeErr::Reset(format!("QBUF OUTPUT (reconf): {e}")))?;
poll_ready(fd, PollFlags::POLLPRI, SOURCE_CHANGE_TIMEOUT_MS);
drain_events(fd);
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> {
let fd = self.device.fd();
const CAP: u32 = ioctl::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
let mut cfmt = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
unsafe { ioctl::vidioc_g_fmt(fd, &mut cfmt) }
.map_err(|e| DecodeErr::Reset(format!("G_FMT CAPTURE: {e}")))?;
let mut cap = *unsafe { cfmt.pix_mp() };
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",
ioctl::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 = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { nfmt.pix_mp() };
p.width = cap.width;
p.height = cap.height;
p.pixelformat = ioctl::V4L2_PIX_FMT_NV12;
p.field = ioctl::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 _ = unsafe { ioctl::vidioc_s_fmt(fd, &mut nfmt) };
let mut gfmt = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
if unsafe { ioctl::vidioc_g_fmt(fd, &mut gfmt) }.is_ok() {
let got = *unsafe { gfmt.pix_mp() };
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",
ioctl::fourcc_str(got.pixelformat)
);
}
}
}
}
let cap0 = cap;
let want_zc = dma_capable
&& matches!(output_fmt, PixelFormat::Nv12 | PixelFormat::Grey)
&& matches!(kind, CapKind::Nv12 | CapKind::Grey)
&& final_w.is_multiple_of(16)
&& final_h.is_multiple_of(16);
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 => (ioctl::V4L2_PIX_FMT_NV12, cap0.height as usize * 3 / 2),
_ => (ioctl::V4L2_PIX_FMT_GREY, cap0.height as usize),
};
let mut sfmt = ioctl::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 = ioctl::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 _ = unsafe { ioctl::vidioc_s_fmt(fd, &mut sfmt) };
let mut gfmt = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
unsafe { ioctl::vidioc_g_fmt(fd, &mut gfmt) }
.map_err(|e| DecodeErr::Reset(format!("G_FMT CAPTURE (zc): {e}")))?;
let cap_zc = *unsafe { gfmt.pix_mp() };
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 = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { rfmt.pix_mp() };
*p = cap0;
}
let _ = unsafe { ioctl::vidioc_s_fmt(fd, &mut rfmt) };
cap = cap0;
}
}
if dst_dma {
reqbufs(fd, CAP, 1, ioctl::V4L2_MEMORY_DMABUF)
.map_err(|e| DecodeErr::Reset(format!("REQBUFS CAPTURE (dmabuf): {e}")))?;
streamon(fd, CAP).map_err(|e| DecodeErr::Reset(format!("STREAMON CAPTURE: {e}")))?;
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 = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { sfmt.pix_mp() };
p.width = cap.width;
p.height = cap.height;
p.pixelformat = ioctl::V4L2_PIX_FMT_NV12;
p.field = ioctl::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 _ = unsafe { ioctl::vidioc_s_fmt(fd, &mut sfmt) };
let mut gfmt = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
unsafe { ioctl::vidioc_g_fmt(fd, &mut gfmt) }
.map_err(|e| DecodeErr::Reset(format!("G_FMT CAPTURE (scratch): {e}")))?;
single = *unsafe { gfmt.pix_mp() };
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",
ioctl::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(),
));
}
reqbufs(fd, CAP, 1, ioctl::V4L2_MEMORY_DMABUF)
.map_err(|e| DecodeErr::Reset(format!("REQBUFS CAPTURE (scratch): {e}")))?;
streamon(fd, CAP).map_err(|e| DecodeErr::Reset(format!("STREAMON CAPTURE: {e}")))?;
Ok(Capture {
kind,
cap_h: single.height as usize,
luma_stride: single.plane_fmt[0].bytesperline as usize,
target: CaptureTarget::Scratch,
})
}
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::Dma),
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 fd = self.device.fd();
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 dmabuf_fd = dst
.dmabuf()
.map_err(|e| DecodeErr::Fatal(e.into()))?
.as_raw_fd();
let capacity = dst.capacity_bytes();
qbuf_capture_dmabuf(fd, dmabuf_fd, capacity)
.map_err(|e| DecodeErr::Reset(format!("QBUF CAPTURE (dmabuf): {e}")))?;
if !poll_ready(fd, PollFlags::POLLIN, DECODE_TIMEOUT_MS) {
return Err(DecodeErr::Reset("CAPTURE decode timeout".into()));
}
dqbuf_capture(fd)
.map_err(|e| DecodeErr::Reset(format!("DQBUF CAPTURE (dmabuf): {e}")))?;
dqbuf_output(fd).map_err(|e| DecodeErr::Reset(format!("DQBUF OUTPUT: {e}")))?;
return Ok(ImageInfo {
width: final_w,
height: final_h,
format: output_fmt,
row_stride: dst_stride,
rotation_degrees: 0,
flip_horizontal: false,
});
}
let (scratch_fd, capacity) = {
let t = self
.scratch
.as_ref()
.ok_or_else(|| DecodeErr::Reset("capture scratch missing".into()))?;
(
t.dmabuf()
.map_err(|e| DecodeErr::Fatal(e.into()))?
.as_raw_fd(),
t.capacity_bytes(),
)
};
qbuf_capture_dmabuf(fd, scratch_fd, capacity)
.map_err(|e| DecodeErr::Reset(format!("QBUF CAPTURE (scratch): {e}")))?;
if !poll_ready(fd, PollFlags::POLLIN, DECODE_TIMEOUT_MS) {
return Err(DecodeErr::Reset("CAPTURE decode timeout".into()));
}
dqbuf_capture(fd).map_err(|e| DecodeErr::Reset(format!("DQBUF CAPTURE (scratch): {e}")))?;
dqbuf_output(fd).map_err(|e| DecodeErr::Reset(format!("DQBUF OUTPUT: {e}")))?;
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 Some(stream) = self.stream.take() else {
return;
};
let fd = self.device.fd();
let _ = streamoff(fd, ioctl::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE);
let _ = streamoff(fd, ioctl::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE);
drop(stream);
let _ = reqbufs(
fd,
ioctl::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
0,
ioctl::V4L2_MEMORY_DMABUF,
);
let _ = reqbufs(
fd,
ioctl::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
0,
ioctl::V4L2_MEMORY_MMAP,
);
}
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 == ioctl::V4L2_COLORSPACE_JPEG {
return true;
}
let enc_ok = ycbcr_enc == 0 || ycbcr_enc == ioctl::V4L2_YCBCR_ENC_601;
let quant_ok = quantization == ioctl::V4L2_QUANTIZATION_DEFAULT
|| quantization == ioctl::V4L2_QUANTIZATION_FULL_RANGE;
enc_ok && quant_ok
}
fn borrow(fd: RawFd) -> BorrowedFd<'static> {
unsafe { BorrowedFd::borrow_raw(fd) }
}
fn reqbufs(fd: RawFd, buf_type: u32, count: u32, memory: u32) -> nix::Result<()> {
let mut rb = ioctl::v4l2_requestbuffers {
count,
type_: buf_type,
memory,
..Default::default()
};
unsafe { ioctl::vidioc_reqbufs(fd, &mut rb) }.map(|_| ())
}
fn streamon(fd: RawFd, buf_type: u32) -> nix::Result<()> {
let t: c_int = buf_type as c_int;
unsafe { ioctl::vidioc_streamon(fd, &t) }.map(|_| ())
}
fn streamoff(fd: RawFd, buf_type: u32) -> nix::Result<()> {
let t: c_int = buf_type as c_int;
unsafe { ioctl::vidioc_streamoff(fd, &t) }.map(|_| ())
}
fn querybuf(fd: RawFd, buf_type: u32, num_planes: usize) -> nix::Result<(usize, i64)> {
let mut planes = [ioctl::v4l2_plane::default(); ioctl::VIDEO_MAX_PLANES];
let mut b = ioctl::v4l2_buffer {
type_: buf_type,
memory: ioctl::V4L2_MEMORY_MMAP,
index: 0,
length: num_planes as u32,
..Default::default()
};
b.set_planes(planes.as_mut_ptr());
unsafe { ioctl::vidioc_querybuf(fd, &mut b) }?;
Ok((planes[0].length as usize, planes[0].mem_offset() as i64))
}
fn qbuf_output(fd: RawFd, bytesused: usize) -> nix::Result<()> {
let mut planes = [ioctl::v4l2_plane::default(); ioctl::VIDEO_MAX_PLANES];
planes[0].bytesused = bytesused as u32;
let mut b = ioctl::v4l2_buffer {
type_: ioctl::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
memory: ioctl::V4L2_MEMORY_MMAP,
index: 0,
length: 1,
..Default::default()
};
b.set_planes(planes.as_mut_ptr());
unsafe { ioctl::vidioc_qbuf(fd, &mut b) }.map(|_| ())
}
fn dqbuf_output(fd: RawFd) -> nix::Result<()> {
let mut planes = [ioctl::v4l2_plane::default(); ioctl::VIDEO_MAX_PLANES];
let mut b = ioctl::v4l2_buffer {
type_: ioctl::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
memory: ioctl::V4L2_MEMORY_MMAP,
length: 1,
..Default::default()
};
b.set_planes(planes.as_mut_ptr());
unsafe { ioctl::vidioc_dqbuf(fd, &mut b) }.map(|_| ())
}
fn qbuf_capture_dmabuf(fd: RawFd, dmabuf_fd: RawFd, length: usize) -> nix::Result<()> {
let mut planes = [ioctl::v4l2_plane::default(); ioctl::VIDEO_MAX_PLANES];
planes[0].set_fd(dmabuf_fd);
planes[0].length = length as u32;
let mut b = ioctl::v4l2_buffer {
type_: ioctl::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
memory: ioctl::V4L2_MEMORY_DMABUF,
index: 0,
length: 1,
..Default::default()
};
b.set_planes(planes.as_mut_ptr());
unsafe { ioctl::vidioc_qbuf(fd, &mut b) }.map(|_| ())
}
fn dqbuf_capture(fd: RawFd) -> nix::Result<()> {
let mut planes = [ioctl::v4l2_plane::default(); ioctl::VIDEO_MAX_PLANES];
let mut b = ioctl::v4l2_buffer {
type_: ioctl::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
memory: ioctl::V4L2_MEMORY_DMABUF,
length: 1,
..Default::default()
};
b.set_planes(planes.as_mut_ptr());
unsafe { ioctl::vidioc_dqbuf(fd, &mut b) }.map(|_| ())
}
fn drain_events(fd: RawFd) {
for _ in 0..MAX_EVENTS {
if !poll_ready(fd, PollFlags::POLLPRI, 0) {
break;
}
let mut ev = ioctl::v4l2_event::default();
if unsafe { ioctl::vidioc_dqevent(fd, &mut ev) }.is_err() {
break;
}
if ev.pending == 0 {
break;
}
}
}
fn poll_ready(fd: RawFd, flags: PollFlags, ms: i32) -> bool {
let bfd = borrow(fd);
let mut pfd = [PollFd::new(bfd, flags)];
let timeout = PollTimeout::try_from(ms).unwrap_or(PollTimeout::ZERO);
match poll(&mut pfd, timeout) {
Ok(n) if n > 0 => pfd[0]
.revents()
.map(|r| r.intersects(flags))
.unwrap_or(false),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
const ENC_709: u8 = 2;
const QUANT_LIMITED: u8 = 2;
#[test]
fn colorimetry_accepts_jpeg_and_full_range_601() {
assert!(colorimetry_ok(
ioctl::V4L2_COLORSPACE_JPEG,
ENC_709,
QUANT_LIMITED
));
assert!(colorimetry_ok(
ioctl::V4L2_COLORSPACE_SRGB,
ioctl::V4L2_YCBCR_ENC_601,
ioctl::V4L2_QUANTIZATION_FULL_RANGE
));
assert!(colorimetry_ok(ioctl::V4L2_COLORSPACE_SRGB, 0, 0));
}
#[test]
fn colorimetry_rejects_non_601_or_limited_range() {
assert!(!colorimetry_ok(
ioctl::V4L2_COLORSPACE_SRGB,
ENC_709,
ioctl::V4L2_QUANTIZATION_FULL_RANGE
));
assert!(!colorimetry_ok(
ioctl::V4L2_COLORSPACE_SRGB,
ioctl::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 OUT: u32 = ioctl::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
const CAP: u32 = ioctl::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
let Some(dev) = device::probe() else {
eprintln!("skip: no v4l2 jpeg decoder on this host");
return;
};
if dev.api != ApiVariant::MultiPlanar {
eprintln!("skip: device is not multi-planar");
return;
}
let (Some(jpeg_a), Some(jpeg_b)) =
(testdata_file("zidane.jpg"), testdata_file("giraffe.jpg"))
else {
eprintln!("skip: testdata not found (set EDGEFIRST_TESTDATA_DIR)");
return;
};
let scratch = match Tensor::<u8>::image(
4096,
1024,
PixelFormat::Grey,
Some(TensorMemory::Dma),
edgefirst_tensor::CpuAccess::ReadWrite,
) {
Ok(t) => t,
Err(e) => {
eprintln!("skip: no DMA heap ({e})");
return;
}
};
let scratch_fd = scratch.dmabuf().unwrap().as_raw_fd();
let scratch_cap = scratch.capacity_bytes();
let fd = dev.fd();
let sub = ioctl::v4l2_event_subscription {
type_: ioctl::V4L2_EVENT_SOURCE_CHANGE,
..Default::default()
};
unsafe { ioctl::vidioc_subscribe_event(fd, &sub) }.expect("SUBSCRIBE_EVENT");
let mut ofmt = ioctl::v4l2_format {
type_: OUT,
..Default::default()
};
{
let p = unsafe { ofmt.pix_mp() };
p.width = 1280;
p.height = 720;
p.pixelformat = ioctl::V4L2_PIX_FMT_JPEG;
p.field = ioctl::V4L2_FIELD_NONE;
p.num_planes = 1;
p.plane_fmt[0].sizeimage = 2 * 1024 * 1024;
}
unsafe { ioctl::vidioc_s_fmt(fd, &mut ofmt) }.expect("S_FMT OUTPUT");
reqbufs(fd, OUT, 1, ioctl::V4L2_MEMORY_MMAP).expect("REQBUFS OUTPUT");
let (olen, ooff) = querybuf(fd, OUT, 1).expect("QUERYBUF OUTPUT");
let mut out_map = Mmap::new(borrow(fd), olen, ooff).expect("mmap OUTPUT");
streamon(fd, OUT).expect("STREAMON OUTPUT");
let mut cap_live = false;
for round in 0..10 {
for (name, jpeg) in [("zidane 1280x720", &jpeg_a), ("giraffe 640x640", &jpeg_b)] {
let t0 = Instant::now();
if cap_live {
streamoff(fd, CAP).expect("STREAMOFF CAPTURE");
reqbufs(fd, CAP, 0, ioctl::V4L2_MEMORY_DMABUF).expect("REQBUFS CAPTURE 0");
}
out_map.as_mut_slice()[..jpeg.len()].copy_from_slice(jpeg);
qbuf_output(fd, jpeg.len()).expect("QBUF OUTPUT");
poll_ready(fd, PollFlags::POLLPRI, SOURCE_CHANGE_TIMEOUT_MS);
drain_events(fd);
let mut gfmt = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
unsafe { ioctl::vidioc_g_fmt(fd, &mut gfmt) }.expect("G_FMT CAPTURE");
let got = *unsafe { gfmt.pix_mp() };
let t_gfmt = t0.elapsed();
let mut sfmt = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { sfmt.pix_mp() };
p.width = got.width;
p.height = got.height;
p.pixelformat = ioctl::V4L2_PIX_FMT_NV12;
p.field = ioctl::V4L2_FIELD_NONE;
p.colorspace = got.colorspace;
p.num_planes = 1;
}
let sfmt_res = unsafe { ioctl::vidioc_s_fmt(fd, &mut sfmt) };
let mut gfmt2 = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
unsafe { ioctl::vidioc_g_fmt(fd, &mut gfmt2) }.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();
reqbufs(fd, CAP, 1, ioctl::V4L2_MEMORY_DMABUF).expect("REQBUFS CAPTURE dmabuf");
let t_reqbufs = t1.elapsed();
streamon(fd, CAP).expect("STREAMON CAPTURE");
cap_live = true;
qbuf_capture_dmabuf(fd, scratch_fd, scratch_cap).expect("QBUF CAPTURE dmabuf");
assert!(
poll_ready(fd, PollFlags::POLLIN, DECODE_TIMEOUT_MS),
"decode timeout"
);
dqbuf_capture(fd).expect("DQBUF CAPTURE");
dqbuf_output(fd).expect("DQBUF OUTPUT");
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={}",
ioctl::fourcc_str(cap_fmt.pixelformat),
cap_fmt.width,
cap_fmt.height,
sizeimage,
);
}
}
let _ = streamoff(fd, CAP);
let _ = streamoff(fd, OUT);
let _ = reqbufs(fd, CAP, 0, ioctl::V4L2_MEMORY_DMABUF);
let _ = reqbufs(fd, OUT, 0, ioctl::V4L2_MEMORY_MMAP);
}
fn reqbufs_n(fd: RawFd, buf_type: u32, count: u32, memory: u32) -> nix::Result<u32> {
let mut rb = ioctl::v4l2_requestbuffers {
count,
type_: buf_type,
memory,
..Default::default()
};
unsafe { ioctl::vidioc_reqbufs(fd, &mut rb) }.map(|_| rb.count)
}
fn querybuf_idx(fd: RawFd, buf_type: u32, index: u32) -> nix::Result<(usize, i64)> {
let mut planes = [ioctl::v4l2_plane::default(); ioctl::VIDEO_MAX_PLANES];
let mut b = ioctl::v4l2_buffer {
type_: buf_type,
memory: ioctl::V4L2_MEMORY_MMAP,
index,
length: 1,
..Default::default()
};
b.set_planes(planes.as_mut_ptr());
unsafe { ioctl::vidioc_querybuf(fd, &mut b) }?;
Ok((planes[0].length as usize, planes[0].mem_offset() as i64))
}
fn qbuf_out_idx(fd: RawFd, index: u32, bytesused: usize) -> nix::Result<()> {
let mut planes = [ioctl::v4l2_plane::default(); ioctl::VIDEO_MAX_PLANES];
planes[0].bytesused = bytesused as u32;
let mut b = ioctl::v4l2_buffer {
type_: ioctl::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
memory: ioctl::V4L2_MEMORY_MMAP,
index,
length: 1,
..Default::default()
};
b.set_planes(planes.as_mut_ptr());
unsafe { ioctl::vidioc_qbuf(fd, &mut b) }.map(|_| ())
}
fn qbuf_cap_fd_idx(fd: RawFd, index: u32, dmabuf_fd: RawFd, len: usize) -> nix::Result<()> {
let mut planes = [ioctl::v4l2_plane::default(); ioctl::VIDEO_MAX_PLANES];
planes[0].set_fd(dmabuf_fd);
planes[0].length = len as u32;
let mut b = ioctl::v4l2_buffer {
type_: ioctl::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
memory: ioctl::V4L2_MEMORY_DMABUF,
index,
length: 1,
..Default::default()
};
b.set_planes(planes.as_mut_ptr());
unsafe { ioctl::vidioc_qbuf(fd, &mut b) }.map(|_| ())
}
fn dqbuf_idx(fd: RawFd, buf_type: u32, memory: u32) -> nix::Result<u32> {
let mut planes = [ioctl::v4l2_plane::default(); ioctl::VIDEO_MAX_PLANES];
let mut b = ioctl::v4l2_buffer {
type_: buf_type,
memory,
length: 1,
..Default::default()
};
b.set_planes(planes.as_mut_ptr());
unsafe { ioctl::vidioc_dqbuf(fd, &mut b) }.map(|_| b.index)
}
fn run_throughput(dev: &ProbedDevice, jpeg: &[u8], depth: u32, frames: usize) -> Option<f64> {
use std::time::Instant;
const OUT: u32 = ioctl::V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
const CAP: u32 = ioctl::V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
let fd = dev.fd();
let sub = ioctl::v4l2_event_subscription {
type_: ioctl::V4L2_EVENT_SOURCE_CHANGE,
..Default::default()
};
unsafe { ioctl::vidioc_subscribe_event(fd, &sub) }.ok()?;
let mut ofmt = ioctl::v4l2_format {
type_: OUT,
..Default::default()
};
{
let p = unsafe { ofmt.pix_mp() };
p.width = 1280;
p.height = 720;
p.pixelformat = ioctl::V4L2_PIX_FMT_JPEG;
p.field = ioctl::V4L2_FIELD_NONE;
p.num_planes = 1;
p.plane_fmt[0].sizeimage = ((jpeg.len() + 4095) & !4095) as u32;
}
unsafe { ioctl::vidioc_s_fmt(fd, &mut ofmt) }.ok()?;
let got_out = reqbufs_n(fd, OUT, depth, ioctl::V4L2_MEMORY_MMAP).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 (len, off) = querybuf_idx(fd, OUT, i).ok()?;
let mut m = Mmap::new(borrow(fd), len, off).ok()?;
m.as_mut_slice()[..jpeg.len()].copy_from_slice(jpeg);
out_maps.push(m);
}
streamon(fd, OUT).ok()?;
qbuf_out_idx(fd, 0, jpeg.len()).ok()?;
poll_ready(fd, PollFlags::POLLPRI, SOURCE_CHANGE_TIMEOUT_MS);
drain_events(fd);
let mut gfmt = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
unsafe { ioctl::vidioc_g_fmt(fd, &mut gfmt) }.ok()?;
let got = *unsafe { gfmt.pix_mp() };
let mut sfmt = ioctl::v4l2_format {
type_: CAP,
..Default::default()
};
{
let p = unsafe { sfmt.pix_mp() };
p.width = got.width;
p.height = got.height;
p.pixelformat = ioctl::V4L2_PIX_FMT_NV12;
p.field = ioctl::V4L2_FIELD_NONE;
p.colorspace = got.colorspace;
p.num_planes = 1;
}
unsafe { ioctl::vidioc_s_fmt(fd, &mut sfmt) }.ok()?;
unsafe { ioctl::vidioc_g_fmt(fd, &mut gfmt) }.ok()?;
let cap_fmt = *unsafe { gfmt.pix_mp() };
if cap_fmt.num_planes != 1 {
eprintln!(" capture is not single-plane; skipping");
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 = Tensor::<u8>::image(
4096,
rows,
PixelFormat::Grey,
Some(TensorMemory::Dma),
edgefirst_tensor::CpuAccess::ReadWrite,
)
.ok()?;
scratches.push(t);
}
let scratch_fds: Vec<RawFd> = scratches
.iter()
.map(|t| t.dmabuf().unwrap().as_raw_fd())
.collect();
let got_cap = reqbufs_n(fd, CAP, depth, ioctl::V4L2_MEMORY_DMABUF).ok()?;
if got_cap < depth {
eprintln!(" driver clamped CAPTURE buffers: {depth} -> {got_cap}");
}
streamon(fd, CAP).ok()?;
let live = depth.min(got_out).min(got_cap);
for i in 0..live {
qbuf_cap_fd_idx(
fd,
i,
scratch_fds[i as usize],
scratches[i as usize].capacity_bytes(),
)
.ok()?;
}
for i in 1..live {
qbuf_out_idx(fd, i, jpeg.len()).ok()?;
}
let warmup = 30usize;
let mut t0 = Instant::now();
for n in 0..(warmup + frames) {
if n == warmup {
t0 = Instant::now();
}
if !poll_ready(fd, PollFlags::POLLIN, DECODE_TIMEOUT_MS) {
eprintln!(" decode timeout at frame {n}");
return None;
}
let ci = dqbuf_idx(fd, CAP, ioctl::V4L2_MEMORY_DMABUF).ok()?;
let oi = dqbuf_idx(fd, OUT, ioctl::V4L2_MEMORY_MMAP).ok()?;
qbuf_out_idx(fd, oi, jpeg.len()).ok()?;
qbuf_cap_fd_idx(
fd,
ci,
scratch_fds[ci as usize],
scratches[ci as usize].capacity_bytes(),
)
.ok()?;
}
let fps = frames as f64 / t0.elapsed().as_secs_f64();
let _ = streamoff(fd, CAP);
let _ = streamoff(fd, OUT);
let _ = reqbufs(fd, CAP, 0, ioctl::V4L2_MEMORY_DMABUF);
let _ = reqbufs(fd, OUT, 0, ioctl::V4L2_MEMORY_MMAP);
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 {
eprintln!("skip: testdata not found (set EDGEFIRST_TESTDATA_DIR)");
return;
};
let frames = 300usize;
for depth in [1u32, 2, 4] {
let Some(dev) = device::probe() else {
eprintln!("skip: no v4l2 jpeg decoder");
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 => {
eprintln!("skip: could not open 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");
}
}
}