Skip to main content

denise_video/
decode.rs

1//! The stateful decode session: compressed bytes in, dmabuf frames out.
2//!
3//! This is the half of the format menu that most of the embedded world
4//! shares: the driver parses everything, userspace only moves buffers. The
5//! canonical M2M flow, spelled out because every step is load-bearing:
6//!
7//! 1. `S_FMT` the **output** (compressed) queue to the codec, allocate and
8//!    mmap its buffers, `STREAMON`.
9//! 2. Feed access units. The driver parses the parameter sets and raises
10//!    `SOURCE_CHANGE` when it knows the real dimensions.
11//! 3. Only then negotiate the **capture** queue: `G_FMT` for what the driver
12//!    chose, allocate its buffers, export each as a **dmabuf**, queue them
13//!    all, `STREAMON`.
14//! 4. Loop: dequeue a decoded frame, hand its dmabuf to the plane, requeue it
15//!    once scanout has moved on.
16//!
17//! Everything is non-blocking; [`Decoder::pump`] is called from the
18//! application's event loop and never waits.
19
20use std::fs::File;
21use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
22use std::path::Path;
23
24use crate::VideoError;
25use crate::annexb::Codec;
26use crate::v4l2;
27
28/// Compressed buffers on the output queue: enough to keep the decoder fed
29/// without pumping every frame.
30const OUTPUT_BUFFERS: u32 = 4;
31/// Room for one access unit. 1 MiB holds any 1080p AU with head to spare.
32const OUTPUT_BUFFER_BYTES: u32 = 1 << 20;
33
34/// One mmapped compressed-input buffer.
35struct OutputBuffer {
36    /// The mapping, kept alive for the session; unmapped on drop.
37    map: MmapRegion,
38    queued: bool,
39}
40
41/// An owned `mmap` region.
42///
43/// A hand-rolled holder rather than `memmap2`, because the mapping must come
44/// from the V4L2 buffer offset protocol and be unmapped exactly once.
45struct MmapRegion {
46    ptr: *mut core::ffi::c_void,
47    len: usize,
48}
49
50// SAFETY: the region is exclusively owned; the pointer is only dereferenced
51// through `&mut self`.
52unsafe impl Send for MmapRegion {}
53
54impl MmapRegion {
55    fn map(fd: BorrowedFd<'_>, offset: u64, len: usize) -> Result<Self, VideoError> {
56        use rustix::mm::{MapFlags, ProtFlags, mmap};
57        // SAFETY: mapping a fresh region chosen by the kernel (addr null); the
58        // fd and offset come from QUERYBUF on this very device, which is the
59        // documented way to reach a V4L2 MMAP buffer.
60        let ptr = unsafe {
61            mmap(
62                core::ptr::null_mut(),
63                len,
64                ProtFlags::READ | ProtFlags::WRITE,
65                MapFlags::SHARED,
66                fd,
67                offset,
68            )
69        }
70        .map_err(|e| VideoError::v4l2("mmap", e))?;
71        Ok(Self { ptr, len })
72    }
73
74    fn write(&mut self, bytes: &[u8]) -> usize {
75        let n = bytes.len().min(self.len);
76        // SAFETY: the region is `len` bytes, mapped read-write, exclusively
77        // owned; `n` is clamped to it.
78        unsafe {
79            core::ptr::copy_nonoverlapping(bytes.as_ptr(), self.ptr.cast::<u8>(), n);
80        }
81        n
82    }
83}
84
85impl Drop for MmapRegion {
86    fn drop(&mut self) {
87        // SAFETY: exactly the region mmap returned, unmapped once.
88        unsafe {
89            let _ = rustix::mm::munmap(self.ptr, self.len);
90        }
91    }
92}
93
94/// The negotiated capture side, once the stream's dimensions are known.
95struct CaptureSide {
96    /// One exported dmabuf per capture buffer, index-aligned.
97    dmabufs: Vec<OwnedFd>,
98    width: u32,
99    height: u32,
100    /// The driver's chosen fourcc: NV12 or YU12 on the boards this targets.
101    pixelformat: u32,
102    /// Bytes per luma row.
103    stride: u32,
104    /// Bytes per buffer — where the chroma planes live is derived from this
105    /// and the stride by the plane importer.
106    sizeimage: u32,
107}
108
109/// One decoded picture, ready for scanout.
110///
111/// Borrows nothing: the dmabuf stays owned by the [`Decoder`], and the frame
112/// names it by capture-buffer `index`. Display it, then give the buffer back
113/// with [`Decoder::recycle`] once scanout has moved past it.
114#[derive(Debug, Clone, Copy)]
115pub struct DecodedFrame {
116    /// Which capture buffer holds the picture — the key for
117    /// [`Decoder::dmabuf`] and [`Decoder::recycle`].
118    pub index: u32,
119    /// Picture width in pixels.
120    pub width: u32,
121    /// Picture height in pixels.
122    pub height: u32,
123    /// The V4L2 fourcc of the pixel data.
124    pub pixelformat: u32,
125    /// Bytes per luma row.
126    pub stride: u32,
127    /// Total bytes in the buffer.
128    pub sizeimage: u32,
129}
130
131/// A stateful V4L2 decode session on one device node.
132pub struct Decoder {
133    file: File,
134    codec: Codec,
135    output: Vec<OutputBuffer>,
136    capture: Option<CaptureSide>,
137    /// Set once `STREAMON` has run on the output queue.
138    streaming: bool,
139}
140
141impl Decoder {
142    /// Opens `path` and prepares the compressed side for `codec`.
143    ///
144    /// The capture side is deliberately not touched: its geometry belongs to
145    /// the stream, and the driver announces it through `SOURCE_CHANGE` once
146    /// it has parsed the headers this session feeds it.
147    pub fn open(path: impl AsRef<Path>, codec: Codec) -> Result<Self, VideoError> {
148        use rustix::fs::{OFlags, fcntl_setfl};
149        let path = path.as_ref();
150        let file = std::fs::OpenOptions::new()
151            .read(true)
152            .write(true)
153            .open(path)
154            .map_err(|source| VideoError::Open {
155                path: path.to_path_buf(),
156                source,
157            })?;
158        fcntl_setfl(&file, OFlags::NONBLOCK).map_err(|e| VideoError::v4l2("fcntl", e))?;
159        let fd = file.as_fd();
160
161        v4l2::subscribe_event(fd, v4l2::EVENT_SOURCE_CHANGE)
162            .map_err(|e| VideoError::v4l2("subscribe source_change", e))?;
163
164        // The compressed side: fourcc and a buffer size, no geometry — the
165        // stream knows its own.
166        let mut format = v4l2::Format::zeroed(v4l2::BUF_TYPE_OUTPUT_MPLANE);
167        {
168            let pix = format.pix_mp_mut();
169            pix.pixelformat = match codec {
170                Codec::H264 => v4l2::PIX_FMT_H264,
171                Codec::H265 => v4l2::PIX_FMT_HEVC,
172            };
173            pix.num_planes = 1;
174            pix.plane_fmt[0].sizeimage = OUTPUT_BUFFER_BYTES;
175        }
176        v4l2::s_fmt(fd, &mut format).map_err(|e| VideoError::v4l2("s_fmt output", e))?;
177
178        let granted = v4l2::reqbufs(fd, v4l2::BUF_TYPE_OUTPUT_MPLANE, OUTPUT_BUFFERS)
179            .map_err(|e| VideoError::v4l2("reqbufs output", e))?;
180        let mut output = Vec::with_capacity(granted as usize);
181        for index in 0..granted {
182            let mut planes = [v4l2::Plane::zeroed()];
183            v4l2::querybuf(fd, v4l2::BUF_TYPE_OUTPUT_MPLANE, index, &mut planes)
184                .map_err(|e| VideoError::v4l2("querybuf output", e))?;
185            // SAFETY-relevant reads of the union: for MEMORY_MMAP the kernel
186            // filled `mem_offset`.
187            let offset = unsafe { planes[0].m.mem_offset } as u64;
188            let len = planes[0].length as usize;
189            output.push(OutputBuffer {
190                map: MmapRegion::map(fd, offset, len)?,
191                queued: false,
192            });
193        }
194
195        Ok(Self {
196            file,
197            codec,
198            output,
199            capture: None,
200            streaming: false,
201        })
202    }
203
204    /// Which codec this session decodes.
205    pub fn codec(&self) -> Codec {
206        self.codec
207    }
208
209    /// Whether a compressed buffer is free right now — feed only when true.
210    pub fn ready_for_input(&mut self) -> bool {
211        self.reclaim_output();
212        self.output.iter().any(|b| !b.queued)
213    }
214
215    /// Queues one access unit, starting the stream on the first.
216    ///
217    /// Returns `false` — feeding nothing — when every buffer is in flight;
218    /// call [`Decoder::pump`] and try again. An access unit larger than the
219    /// buffer is truncated, which cannot happen inside the menu's 1080p bound.
220    pub fn feed(&mut self, access_unit: &[u8]) -> Result<bool, VideoError> {
221        self.reclaim_output();
222        let Some(index) = self.output.iter().position(|b| !b.queued) else {
223            return Ok(false);
224        };
225        let used = self.output[index].map.write(access_unit);
226        let fd = self.file.as_fd();
227        let mut planes = [v4l2::Plane::zeroed()];
228        planes[0].bytesused = used as u32;
229        let mut buffer =
230            v4l2::Buffer::mplane(index as u32, v4l2::BUF_TYPE_OUTPUT_MPLANE, &mut planes);
231        v4l2::qbuf(fd, &mut buffer).map_err(|e| VideoError::v4l2("qbuf output", e))?;
232        self.output[index].queued = true;
233        if !self.streaming {
234            v4l2::streamon(fd, v4l2::BUF_TYPE_OUTPUT_MPLANE)
235                .map_err(|e| VideoError::v4l2("streamon output", e))?;
236            self.streaming = true;
237        }
238        Ok(true)
239    }
240
241    /// Takes back compressed buffers the driver has consumed.
242    fn reclaim_output(&mut self) {
243        let fd = self.file.as_fd();
244        loop {
245            let mut planes = [v4l2::Plane::zeroed()];
246            let mut buffer = v4l2::Buffer::mplane(0, v4l2::BUF_TYPE_OUTPUT_MPLANE, &mut planes);
247            match v4l2::dqbuf(fd, &mut buffer) {
248                Ok(Some(())) => {
249                    if let Some(slot) = self.output.get_mut(buffer.index as usize) {
250                        slot.queued = false;
251                    }
252                }
253                _ => break,
254            }
255        }
256    }
257
258    /// Advances the session: handles the source-change handshake, and returns
259    /// a decoded frame when one is ready. Never blocks.
260    pub fn pump(&mut self) -> Result<Option<DecodedFrame>, VideoError> {
261        self.reclaim_output();
262
263        // The driver announcing it has parsed the stream is what makes the
264        // capture side negotiable at all.
265        while let Some(event) =
266            v4l2::dqevent(self.file.as_fd()).map_err(|e| VideoError::v4l2("dqevent", e))?
267        {
268            if event.event_type == v4l2::EVENT_SOURCE_CHANGE && self.capture.is_none() {
269                self.setup_capture()?;
270            }
271        }
272
273        let Some(capture) = &self.capture else {
274            return Ok(None);
275        };
276        let mut planes = [v4l2::Plane::zeroed()];
277        let mut buffer = v4l2::Buffer::mplane(0, v4l2::BUF_TYPE_CAPTURE_MPLANE, &mut planes);
278        match v4l2::dqbuf(self.file.as_fd(), &mut buffer)
279            .map_err(|e| VideoError::v4l2("dqbuf capture", e))?
280        {
281            None => Ok(None),
282            Some(()) => Ok(Some(DecodedFrame {
283                index: buffer.index,
284                width: capture.width,
285                height: capture.height,
286                pixelformat: capture.pixelformat,
287                stride: capture.stride,
288                sizeimage: capture.sizeimage,
289            })),
290        }
291    }
292
293    /// Negotiates the capture side after the driver has parsed the stream.
294    fn setup_capture(&mut self) -> Result<(), VideoError> {
295        let fd = self.file.as_fd();
296        let format = v4l2::g_fmt(fd, v4l2::BUF_TYPE_CAPTURE_MPLANE)
297            .map_err(|e| VideoError::v4l2("g_fmt capture", e))?;
298        let pix = format.pix_mp();
299        match pix.pixelformat {
300            v4l2::PIX_FMT_NV12 | v4l2::PIX_FMT_YUV420 => {}
301            other => return Err(VideoError::UnsupportedFormat(other)),
302        }
303
304        let granted = v4l2::reqbufs(fd, v4l2::BUF_TYPE_CAPTURE_MPLANE, 4)
305            .map_err(|e| VideoError::v4l2("reqbufs capture", e))?;
306        let mut dmabufs = Vec::with_capacity(granted as usize);
307        for index in 0..granted {
308            let raw = v4l2::expbuf(fd, v4l2::BUF_TYPE_CAPTURE_MPLANE, index, 0)
309                .map_err(|e| VideoError::v4l2("expbuf", e))?;
310            // SAFETY: EXPBUF returns a fresh fd owned by nobody else; wrapping
311            // it transfers that ownership exactly once.
312            dmabufs.push(unsafe { OwnedFd::from_raw_fd_checked(raw) });
313            let mut planes = [v4l2::Plane::zeroed()];
314            let mut buffer =
315                v4l2::Buffer::mplane(index, v4l2::BUF_TYPE_CAPTURE_MPLANE, &mut planes);
316            v4l2::qbuf(fd, &mut buffer).map_err(|e| VideoError::v4l2("qbuf capture", e))?;
317        }
318        v4l2::streamon(fd, v4l2::BUF_TYPE_CAPTURE_MPLANE)
319            .map_err(|e| VideoError::v4l2("streamon capture", e))?;
320
321        self.capture = Some(CaptureSide {
322            dmabufs,
323            width: pix.width,
324            height: pix.height,
325            pixelformat: pix.pixelformat,
326            stride: pix.plane_fmt[0].bytesperline,
327            sizeimage: pix.plane_fmt[0].sizeimage,
328        });
329        Ok(())
330    }
331
332    /// The dmabuf backing capture buffer `index`.
333    pub fn dmabuf(&self, index: u32) -> Option<BorrowedFd<'_>> {
334        self.capture
335            .as_ref()
336            .and_then(|c| c.dmabufs.get(index as usize))
337            .map(|fd| fd.as_fd())
338    }
339
340    /// Gives a displayed frame's buffer back to the decoder.
341    ///
342    /// Call once scanout has moved past it — in practice, when the *next*
343    /// frame has been flipped onto the plane.
344    pub fn recycle(&mut self, index: u32) -> Result<(), VideoError> {
345        let mut planes = [v4l2::Plane::zeroed()];
346        let mut buffer = v4l2::Buffer::mplane(index, v4l2::BUF_TYPE_CAPTURE_MPLANE, &mut planes);
347        v4l2::qbuf(self.file.as_fd(), &mut buffer).map_err(|e| VideoError::v4l2("qbuf capture", e))
348    }
349
350    /// Stops both queues, forgetting all stream state; the next
351    /// [`Decoder::feed`] starts the stream over. This is the whole of
352    /// seeking: a promo loop restarts from its parameter sets.
353    pub fn restart(&mut self) -> Result<(), VideoError> {
354        let fd = self.file.as_fd();
355        if self.streaming {
356            v4l2::streamoff(fd, v4l2::BUF_TYPE_OUTPUT_MPLANE)
357                .map_err(|e| VideoError::v4l2("streamoff output", e))?;
358            self.streaming = false;
359        }
360        for slot in &mut self.output {
361            slot.queued = false;
362        }
363        if self.capture.is_some() {
364            v4l2::streamoff(fd, v4l2::BUF_TYPE_CAPTURE_MPLANE)
365                .map_err(|e| VideoError::v4l2("streamoff capture", e))?;
366            // Requeue every capture buffer for the fresh run; geometry is
367            // unchanged — a loop plays the same file.
368            let count = self
369                .capture
370                .as_ref()
371                .map(|c| c.dmabufs.len() as u32)
372                .unwrap_or(0);
373            v4l2::streamon(fd, v4l2::BUF_TYPE_CAPTURE_MPLANE)
374                .map_err(|e| VideoError::v4l2("streamon capture", e))?;
375            for index in 0..count {
376                let mut planes = [v4l2::Plane::zeroed()];
377                let mut buffer =
378                    v4l2::Buffer::mplane(index, v4l2::BUF_TYPE_CAPTURE_MPLANE, &mut planes);
379                v4l2::qbuf(fd, &mut buffer).map_err(|e| VideoError::v4l2("qbuf capture", e))?;
380            }
381        }
382        Ok(())
383    }
384}
385
386/// `OwnedFd` construction from a raw fd, named so the SAFETY reasoning has an
387/// address.
388trait FromRawChecked {
389    /// SAFETY: `raw` must be an open fd owned by nobody else.
390    unsafe fn from_raw_fd_checked(raw: i32) -> OwnedFd;
391}
392
393impl FromRawChecked for OwnedFd {
394    unsafe fn from_raw_fd_checked(raw: i32) -> OwnedFd {
395        use std::os::fd::FromRawFd;
396        // SAFETY: forwarded from the caller's contract.
397        unsafe { OwnedFd::from_raw_fd(raw) }
398    }
399}