V4L2 Buffer States and Queues-Free Linux Device Drivers Course

PREV_LEC  |  NEXT_LEC

V4L2 Buffer States and Queues
Free Linux Kernel Development Course — V4L2 & Video Capture Device Drivers, Lecture 5
Chapter: V4L2 Video Capture
Level: Intermediate
Kernel: 6.x mainline

Welcome back to EmbeddedPathashala’s free Linux kernel development course. In the previous lecture of this free linux device drivers course we introduced the videobuf2 (vb2) framework and looked at the individual fields of struct vb2_buffer. In this lecture we finish the buffer lifecycle by walking through every state a vb2 buffer can be in, the exact kernel API that moves a buffer between those states, and how the vb2_queue structure ties a whole set of buffers together into one streaming pipeline. By the end you will be able to reason about buffer ownership in any V4L2 capture driver — whether it belongs to userspace, to videobuf2, or to your hardware.

free embedded systems course free linux development course free linux device drivers course free linux kernel development course videobuf2 vb2_queue

What You Will Learn

By the end of this free linux kernel development course lecture you will:

Understand the full vb2 buffer state machine Use vb2_buffer_done() correctly in a capture driver Read and configure struct vb2_queue Trace a buffer from QBUF to DQBUF Debug stuck or leaked buffers

Prerequisites

This lecture continues directly from the earlier lectures of this free linux device drivers course. You should already be comfortable with:

struct video_device and video_register_device() v4l2_file_operations and video_ioctl2 struct vb2_buffer field layout (previous lecture) Basic character device driver writing in the kernel

The vb2 Buffer State Machine, Explained

Every buffer that videobuf2 manages is, at any given instant, owned by exactly one of three parties: userspace, the vb2 core itself, or your driver’s hardware queue. The framework encodes this ownership as a single enum value stored in the buffer’s state field, and the entire point of the state machine is to make that ownership transfer explicit and race-free. A capture driver never touches buffer memory it does not currently own, and vb2 refuses ioctls that would violate the ownership rules — for example, you cannot QBUF a buffer that is already queued.

Think of it as a relay race with three runners: userspace, videobuf2, and your driver’s DMA engine. The baton (the buffer) is only ever in one runner’s hand. Userspace hands the baton off by calling VIDIOC_QBUF. videobuf2 briefly holds it while it may call your buf_prepare callback, then hands it to your driver by invoking buf_queue. Your driver programs the DMA engine and, once the hardware signals frame-complete (usually in an interrupt handler), hands the baton back to videobuf2 by calling vb2_buffer_done(). Userspace finally reclaims it with VIDIOC_DQBUF.

The Complete State Enum on Current Kernels

The old textbooks that circulate around this topic usually list only four states. Current mainline kernel has seven, because request-API support (used for atomic multi-buffer configuration, common on modern ISP-based camera drivers) added an extra state. Here is the up-to-date enum, as defined in include/media/videobuf2-core.h:

enum vb2_buffer_state {
    VB2_BUF_STATE_DEQUEUED,   /* buffer is under userspace control        */
    VB2_BUF_STATE_IN_REQUEST, /* buffer is queued inside a media request  */
    VB2_BUF_STATE_PREPARING,  /* buffer is being prepared by videobuf2    */
    VB2_BUF_STATE_QUEUED,     /* buffer is in videobuf2, not yet in driver */
    VB2_BUF_STATE_ACTIVE,     /* buffer is in the driver / hardware queue */
    VB2_BUF_STATE_DONE,       /* driver finished, waiting for userspace   */
    VB2_BUF_STATE_ERROR,      /* same as DONE, but the capture failed     */
};
Buffer Ownership Handoff (Inline HTML Diagram)
[ DEQUEUED ] –VIDIOC_QBUF–> [ PREPARING ] –prepared–> [ QUEUED ] | buf_queue() callback v [ DEQUEUED ] <–VIDIOC_DQBUF– [ DONE / ERROR ] userspace owns the buffer PREPARING -> videobuf2 core owns the buffer QUEUED -> videobuf2 core owns the buffer, waiting for the driver ACTIVE -> the driver / hardware DMA engine owns the buffer DONE/ERROR -> videobuf2 core owns the buffer, ready for userspace

vb2_buffer_done() — Returning a Buffer From Hardware

This is the single most important function call inside any V4L2 capture driver’s interrupt or workqueue handler. It is how your driver tells videobuf2 “the DMA engine is finished with this buffer, please give it back to userspace.” The current signature is:

void vb2_buffer_done(struct vb2_buffer *vb, enum vb2_buffer_state state);

The state argument must be one of three values: VB2_BUF_STATE_DONE on a successful capture, VB2_BUF_STATE_ERROR if the hardware reported a corrupted frame or a DMA fault, or VB2_BUF_STATE_QUEUED if you want to hand the buffer straight back into the driver’s own active queue without userspace ever seeing it (used during error recovery). Any other value passed here is treated as a driver bug and videobuf2 will downgrade it to VB2_BUF_STATE_ERROR and print a warning.

struct vb2_queue: The Container That Owns All the Buffers

A single vb2_buffer only makes sense in the context of the queue it belongs to. The vb2_queue structure is what your driver actually allocates, embeds inside its private device structure, and initializes once at probe time. It is the object videobuf2 uses to track how many buffers exist, which memory model userspace picked, and which callback table (your vb2_ops) should run at each stage of the handoff we just described. A trimmed, field-by-field view of the fields you will set in a typical capture driver on current kernels:

struct vb2_queue {
    unsigned int         type;        /* V4L2_BUF_TYPE_VIDEO_CAPTURE, etc.        */
    unsigned int         io_modes;    /* VB2_MMAP | VB2_USERPTR | VB2_DMABUF       */
    void                 *drv_priv;   /* pointer back to your private device data */
    unsigned int         buf_struct_size; /* sizeof(your custom buffer struct)    */
    u32                  timestamp_flags;
    const struct vb2_ops        *ops;     /* your queue_setup/buf_queue/... table */
    const struct vb2_mem_ops    *mem_ops; /* usually &vb2_dma_contig_memops or
                                              &vb2_vmalloc_memops                  */
    struct mutex          *lock;      /* serializes ioctls against this queue     */
    enum v4l2_buf_type     ...;
    /* ... additional fields for min_buffers_needed, gfp_flags, dma_dir, etc. */
};

Two fields deserve special attention for anyone new to this framework. buf_struct_size tells videobuf2 how large each buffer allocation should be, because most drivers embed struct vb2_buffer as the first member of their own larger buffer struct (to attach a DMA scatter-gather list or a physical address, for example). ops points to your vb2_ops table — the callbacks queue_setup, buf_prepare, buf_queue, start_streaming, and stop_streaming are what actually drive the state transitions shown in the diagram above.

vb2_ops CallbackCalled WhenTypical Driver Action
queue_setupVIDIOC_REQBUFSReport how many buffers/planes and their sizes
buf_prepareBefore a buffer becomes QUEUEDValidate buffer, map DMA addresses
buf_queueBuffer moves QUEUED -> ACTIVEPush buffer onto driver’s hardware list
start_streamingVIDIOC_STREAMONArm DMA engine, enable capture interrupt
stop_streamingVIDIOC_STREAMOFFDisable hardware, return all buffers via vb2_buffer_done()

Hands-On: Extending ep_vcap With a Working Buffer Queue

We continue building on ep_vcap, the original demo capture driver from this free embedded linux course series. Here we add a minimal vb2_ops table and a simulated “hardware done” path using a kernel timer instead of real DMA hardware, so you can run and observe the full state machine on any Linux machine without a capture card.

#include <linux/module.h>
#include <linux/timer.h>
#include <media/videobuf2-core.h>
#include <media/videobuf2-vmalloc.h>

struct ep_vcap_buffer {
    struct vb2_buffer   vb;      /* must be first member */
    struct list_head    list;
};

struct ep_vcap_dev {
    struct vb2_queue     queue;
    struct list_head     active_list;
    spinlock_t           qlock;
    struct timer_list     fake_dma_timer;
};

static int ep_queue_setup(struct vb2_queue *vq, unsigned int *nbuffers,
                           unsigned int *nplanes, unsigned int sizes[],
                           struct device *alloc_devs[])
{
    if (*nplanes)
        return sizes[0] < PAGE_SIZE ? -EINVAL : 0;

    *nplanes = 1;
    sizes[0] = PAGE_SIZE;   /* one fake "frame" per buffer for this demo */
    return 0;
}

static void ep_buf_queue(struct vb2_buffer *vb)
{
    struct ep_vcap_dev *dev = vb2_get_drv_priv(vb->vb2_queue);
    struct ep_vcap_buffer *ebuf =
        container_of(vb, struct ep_vcap_buffer, vb);
    unsigned long flags;

    spin_lock_irqsave(&dev->qlock, flags);
    list_add_tail(&ebuf->list, &dev->active_list);
    spin_unlock_irqrestore(&dev->qlock, flags);

    /* buffer is now VB2_BUF_STATE_ACTIVE; a real driver would kick DMA here */
    mod_timer(&dev->fake_dma_timer, jiffies + msecs_to_jiffies(33));
}

/* Simulated "frame complete" interrupt, using a timer instead of real IRQ */
static void ep_fake_dma_complete(struct timer_list *t)
{
    struct ep_vcap_dev *dev = from_timer(dev, t, fake_dma_timer);
    struct ep_vcap_buffer *ebuf;
    unsigned long flags;

    spin_lock_irqsave(&dev->qlock, flags);
    ebuf = list_first_entry_or_null(&dev->active_list,
                                     struct ep_vcap_buffer, list);
    if (ebuf)
        list_del(&ebuf->list);
    spin_unlock_irqrestore(&dev->qlock, flags);

    if (ebuf) {
        ebuf->vb.timestamp = ktime_get_ns();
        vb2_buffer_done(&ebuf->vb, VB2_BUF_STATE_DONE);
        pr_info("ep_vcap: buffer index %u marked DONE\n", ebuf->vb.index);
    }
}

static const struct vb2_ops ep_vcap_qops = {
    .queue_setup     = ep_queue_setup,
    .buf_queue       = ep_buf_queue,
    .wait_prepare    = vb2_ops_wait_prepare,
    .wait_finish     = vb2_ops_wait_finish,
};

Notice how ep_buf_queue() is exactly the moment a buffer transitions from QUEUED to ACTIVE — videobuf2 calls it for you, you never set vb->state by hand. Likewise, calling vb2_buffer_done() is the only correct way to move a buffer to DONE; setting the state field directly is a common beginner mistake that we cover below.

Building and Loading the Module

Build against your running kernel’s headers as usual for any out-of-tree module in this free linux kernel development course:

$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_vcap.ko
$ sudo v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=3

Expected dmesg Output

Watch buffers cycle through ACTIVE to DONE in real time:

$ dmesg -w
[  912.113045] ep_vcap: probe successful, video0 registered
[  914.220981] ep_vcap: buffer index 0 marked DONE
[  914.253912] ep_vcap: buffer index 1 marked DONE
[  914.287004] ep_vcap: buffer index 2 marked DONE
[  914.290117] ep_vcap: streaming stopped, 0 buffers left ACTIVE

Real-World Use Cases

Every mainline V4L2 capture driver you will read in the kernel tree — UVC webcam drivers, SoC camera bridge drivers, HDMI capture bridges — follows exactly this same state machine. Learning it once here means you can open any of those drivers and immediately locate the buf_queue callback to see what triggers hardware DMA, and the interrupt handler to see where vb2_buffer_done() is called. This is also the exact mechanism that powers zero-copy pipelines between a capture driver and a downstream encoder using DMABUF, which is why understanding buffer ownership matters well beyond simple webcam drivers.

Common Mistakes and Troubleshooting

Setting vb->state directly instead of calling vb2_buffer_done()

This bypasses cache synchronization and internal bookkeeping videobuf2 performs on your behalf, leading to stale frame data reaching userspace.

Forgetting to drain the ACTIVE list in stop_streaming

Every buffer your driver is holding must be returned with vb2_buffer_done(vb, VB2_BUF_STATE_ERROR) before stop_streaming returns, or VIDIOC_STREAMOFF will hang.

Racing buf_queue against the interrupt handler

Always protect the active buffer list with a spinlock, since buf_queue runs in process context while your completion handler often runs in interrupt or softirq context.

Best Practices, Performance and Security Considerations

Keep buf_queue() short — just enqueue and kick hardware, never block Validate buffer size in buf_prepare, not after DMA has started Always pair every ACTIVE buffer with exactly one vb2_buffer_done() call Use VB2_BUF_STATE_ERROR (not silently dropping frames) to surface hardware faults to userspace Never trust buffer contents supplied via USERPTR without proper pinning and validation

Summary and Key Takeaways

In this lecture of our free linux device drivers course you learned that every vb2 buffer moves through a well-defined ownership chain — DEQUEUED, PREPARING, QUEUED, ACTIVE, and finally DONE or ERROR — and that vb2_buffer_done() is the only correct way for a driver to hand a buffer back to videobuf2. You also saw how struct vb2_queue ties buffers, memory operations, and your vb2_ops callback table together, and built a working demo driver that exercises the full cycle without needing real capture hardware. This state machine is the backbone of every V4L2 capture driver in the mainline kernel, making it one of the most valuable concepts in this free linux kernel development course.

Frequently Asked Questions

What is the difference between VB2_BUF_STATE_QUEUED and VB2_BUF_STATE_ACTIVE?

QUEUED means videobuf2 core has the buffer but your driver has not yet been handed it via buf_queue(). ACTIVE means your driver’s buf_queue() callback has run and the buffer is currently owned by your hardware/DMA engine.

Can I call vb2_buffer_done() from interrupt context?

Yes, this is the normal case for real capture hardware — most drivers call it directly from the frame-complete interrupt handler or from a tasklet/workqueue scheduled by that handler.

What happens if I never call vb2_buffer_done() for an ACTIVE buffer?

The buffer stays stuck in ACTIVE forever. VIDIOC_STREAMOFF and VIDIOC_DQBUF will hang waiting for it, since videobuf2 cannot reclaim a buffer your driver still owns.

Why was VB2_BUF_STATE_IN_REQUEST added?

It supports the media request API, which lets userspace bundle a buffer together with a full set of controls that must be applied atomically — common on modern ISP pipelines with per-frame exposure/gain changes.

Is struct vb2_queue the same as struct video_device?

No. video_device represents the /dev/videoN character device node itself, while vb2_queue is the buffer-management object your driver embeds and initializes to handle streaming I/O for that device.

Do I need to write my own buf_prepare callback?

Only if you need extra validation or DMA mapping beyond what videobuf2 already does by default — many simple drivers omit it entirely and rely on queue_setup and buf_queue alone.

Where can I practice this without real capture hardware?

The vivid virtual video driver in the mainline kernel (drivers/media/test-drivers/vivid) is the best sandbox — it implements this exact state machine against fully simulated video data.

{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ {“@type”: “Question”, “name”: “What is the difference between VB2_BUF_STATE_QUEUED and VB2_BUF_STATE_ACTIVE?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “QUEUED means videobuf2 core has the buffer but your driver has not yet been handed it via buf_queue(). ACTIVE means your driver’s buf_queue() callback has run and the buffer is currently owned by your hardware/DMA engine.”}}, {“@type”: “Question”, “name”: “Can I call vb2_buffer_done() from interrupt context?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Yes, this is the normal case for real capture hardware — most drivers call it directly from the frame-complete interrupt handler or from a tasklet/workqueue scheduled by that handler.”}}, {“@type”: “Question”, “name”: “What happens if I never call vb2_buffer_done() for an ACTIVE buffer?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “The buffer stays stuck in ACTIVE forever. VIDIOC_STREAMOFF and VIDIOC_DQBUF will hang waiting for it, since videobuf2 cannot reclaim a buffer your driver still owns.”}}, {“@type”: “Question”, “name”: “Why was VB2_BUF_STATE_IN_REQUEST added?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “It supports the media request API, which lets userspace bundle a buffer together with a full set of controls that must be applied atomically — common on modern ISP pipelines with per-frame exposure/gain changes.”}}, {“@type”: “Question”, “name”: “Is struct vb2_queue the same as struct video_device?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “No. video_device represents the /dev/videoN character device node itself, while vb2_queue is the buffer-management object your driver embeds and initializes to handle streaming I/O for that device.”}}, {“@type”: “Question”, “name”: “Do I need to write my own buf_prepare callback?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Only if you need extra validation or DMA mapping beyond what videobuf2 already does by default — many simple drivers omit it entirely and rely on queue_setup and buf_queue alone.”}}, {“@type”: “Question”, “name”: “Where can I practice this without real capture hardware?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “The vivid virtual video driver in the mainline kernel (drivers/media/test-drivers/vivid) is the best sandbox — it implements this exact state machine against fully simulated video data.”}} ] }

Continue the Free Linux Kernel Development Course

Next up: configuring vb2_queue io_modes, memory ops, and streaming control ioctls in detail.

Next Lecture Browse Full Course Index

PREV_LEC  |  NEXT_LEC

<!– ============================================================

Leave a Reply

Your email address will not be published. Required fields are marked *