[v3,03/29] libcamera: Add support for V4L2 requests
diff mbox series

Message ID 20251125162851.2301793-4-stefan.klug@ideasonboard.com
State Accepted
Headers show
Series
  • Full dewarper support on imx8mp
Related show

Commit Message

Stefan Klug Nov. 25, 2025, 4:28 p.m. UTC
The V4L2 requests API provides support to atomically tie controls to a
set of buffers. This is especially common for m2m devices. Such a
request is represented by an fd that is allocated via
MEDIA_IOC_REQUEST_ALLOC and then passed to the various V4L2 functions.

Implement a V4L2Request class to wrap such an fd and add the
corresponding utility functions.

Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>

---

Changes in v3:
- Replaced int by UniqueFD
- Fixed lots if typos from review

Changes in v2:
- Added documentation
---
 include/libcamera/internal/media_device.h     |   8 ++
 include/libcamera/internal/meson.build        |   1 +
 include/libcamera/internal/v4l2_device.h      |   5 +-
 include/libcamera/internal/v4l2_request.h     |  44 ++++++
 include/libcamera/internal/v4l2_videodevice.h |   3 +-
 src/libcamera/media_device.cpp                |  47 +++++++
 src/libcamera/meson.build                     |   1 +
 src/libcamera/v4l2_device.cpp                 |  30 +++-
 src/libcamera/v4l2_request.cpp                | 128 ++++++++++++++++++
 src/libcamera/v4l2_videodevice.cpp            |  10 +-
 10 files changed, 268 insertions(+), 9 deletions(-)
 create mode 100644 include/libcamera/internal/v4l2_request.h
 create mode 100644 src/libcamera/v4l2_request.cpp

Comments

Kieran Bingham Nov. 25, 2025, 5:03 p.m. UTC | #1
Quoting Stefan Klug (2025-11-25 16:28:15)
> The V4L2 requests API provides support to atomically tie controls to a
> set of buffers. This is especially common for m2m devices. Such a
> request is represented by an fd that is allocated via
> MEDIA_IOC_REQUEST_ALLOC and then passed to the various V4L2 functions.
> 
> Implement a V4L2Request class to wrap such an fd and add the
> corresponding utility functions.
> 
> Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
> 

Mostly minor comments below.

Only my question on safety of usage of the vector on allocate stops me
putting a tag in... if someone confirms that is safe then I could add
one ;-)



> ---
> 
> Changes in v3:
> - Replaced int by UniqueFD
> - Fixed lots if typos from review
> 
> Changes in v2:
> - Added documentation
> ---
>  include/libcamera/internal/media_device.h     |   8 ++
>  include/libcamera/internal/meson.build        |   1 +
>  include/libcamera/internal/v4l2_device.h      |   5 +-
>  include/libcamera/internal/v4l2_request.h     |  44 ++++++
>  include/libcamera/internal/v4l2_videodevice.h |   3 +-
>  src/libcamera/media_device.cpp                |  47 +++++++
>  src/libcamera/meson.build                     |   1 +
>  src/libcamera/v4l2_device.cpp                 |  30 +++-
>  src/libcamera/v4l2_request.cpp                | 128 ++++++++++++++++++
>  src/libcamera/v4l2_videodevice.cpp            |  10 +-
>  10 files changed, 268 insertions(+), 9 deletions(-)
>  create mode 100644 include/libcamera/internal/v4l2_request.h
>  create mode 100644 src/libcamera/v4l2_request.cpp
> 
> diff --git a/include/libcamera/internal/media_device.h b/include/libcamera/internal/media_device.h
> index b3a48b98d64b..2eb3ad988b09 100644
> --- a/include/libcamera/internal/media_device.h
> +++ b/include/libcamera/internal/media_device.h
> @@ -8,6 +8,7 @@
>  #pragma once
>  
>  #include <map>
> +#include <optional>
>  #include <string>
>  #include <vector>
>  
> @@ -18,6 +19,7 @@
>  #include <libcamera/base/unique_fd.h>
>  
>  #include "libcamera/internal/media_object.h"
> +#include "libcamera/internal/v4l2_request.h"
>  
>  namespace libcamera {
>  
> @@ -57,6 +59,11 @@ public:
>  
>         std::vector<MediaEntity *> locateEntities(unsigned int function);
>  
> +       int allocateRequests(unsigned int count,
> +                            std::vector<std::unique_ptr<V4L2Request>> *requests);
> +
> +       bool supportsRequests();
> +
>  protected:
>         std::string logPrefix() const override;
>  
> @@ -87,6 +94,7 @@ private:
>         UniqueFD fd_;
>         bool valid_;
>         bool acquired_;
> +       std::optional<bool> supportsRequests_;

std::optional is intriguing here... I'd default false until known
otherwise...

>         std::map<unsigned int, MediaObject *> objects_;
>         std::vector<MediaEntity *> entities_;
> diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build
> index 45c299f6a332..e9540a2f734f 100644
> --- a/include/libcamera/internal/meson.build
> +++ b/include/libcamera/internal/meson.build
> @@ -44,6 +44,7 @@ libcamera_internal_headers = files([
>      'sysfs.h',
>      'v4l2_device.h',
>      'v4l2_pixelformat.h',
> +    'v4l2_request.h',
>      'v4l2_subdevice.h',
>      'v4l2_videodevice.h',
>      'vector.h',
> diff --git a/include/libcamera/internal/v4l2_device.h b/include/libcamera/internal/v4l2_device.h
> index 5bc9da96677d..dbbd118abd00 100644
> --- a/include/libcamera/internal/v4l2_device.h
> +++ b/include/libcamera/internal/v4l2_device.h
> @@ -24,6 +24,7 @@
>  #include <libcamera/controls.h>
>  
>  #include "libcamera/internal/formats.h"
> +#include "libcamera/internal/v4l2_request.h"
>  
>  namespace libcamera {
>  
> @@ -37,8 +38,8 @@ public:
>  
>         const ControlInfoMap &controls() const { return controls_; }
>  
> -       ControlList getControls(Span<const uint32_t> ids);
> -       int setControls(ControlList *ctrls);
> +       ControlList getControls(Span<const uint32_t> ids, const V4L2Request *request = nullptr);
> +       int setControls(ControlList *ctrls, const V4L2Request *request = nullptr);
>  
>         const struct v4l2_query_ext_ctrl *controlInfo(uint32_t id) const;
>  
> diff --git a/include/libcamera/internal/v4l2_request.h b/include/libcamera/internal/v4l2_request.h
> new file mode 100644
> index 000000000000..376c79ceedba
> --- /dev/null
> +++ b/include/libcamera/internal/v4l2_request.h
> @@ -0,0 +1,44 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2025, Ideas On Board
> + *
> + * V4L2 requests
> + */
> +
> +#pragma once
> +
> +#include <string>
> +
> +#include <linux/videodev2.h>
> +
> +#include <libcamera/base/event_notifier.h>
> +#include <libcamera/base/log.h>
> +#include <libcamera/base/signal.h>
> +#include <libcamera/base/unique_fd.h>
> +
> +namespace libcamera {
> +
> +class V4L2Request : protected Loggable
> +{
> +public:
> +       bool isValid() const { return fd_.isValid(); }
> +       int fd() const { return fd_.get(); }
> +
> +       int reinit();
> +       int queue();
> +
> +       V4L2Request(UniqueFD &&fd);
> +
> +       Signal<V4L2Request *> requestDone;
> +
> +private:
> +       LIBCAMERA_DISABLE_COPY_AND_MOVE(V4L2Request)
> +
> +       void requestReady();
> +       std::string logPrefix() const override;
> +
> +       UniqueFD fd_;
> +       EventNotifier fdNotifier_;
> +};
> +
> +} /* namespace libcamera */
> diff --git a/include/libcamera/internal/v4l2_videodevice.h b/include/libcamera/internal/v4l2_videodevice.h
> index 6caafc4dcf08..57db0036db6b 100644
> --- a/include/libcamera/internal/v4l2_videodevice.h
> +++ b/include/libcamera/internal/v4l2_videodevice.h
> @@ -33,6 +33,7 @@
>  #include "libcamera/internal/formats.h"
>  #include "libcamera/internal/v4l2_device.h"
>  #include "libcamera/internal/v4l2_pixelformat.h"
> +#include "libcamera/internal/v4l2_request.h"
>  
>  namespace libcamera {
>  
> @@ -217,7 +218,7 @@ public:
>         int importBuffers(unsigned int count);
>         int releaseBuffers();
>  
> -       int queueBuffer(FrameBuffer *buffer);
> +       int queueBuffer(FrameBuffer *buffer, const V4L2Request *request = nullptr);
>         Signal<FrameBuffer *> bufferReady;
>  
>         int streamOn();
> diff --git a/src/libcamera/media_device.cpp b/src/libcamera/media_device.cpp
> index 353f34a81eca..2a848ebed998 100644
> --- a/src/libcamera/media_device.cpp
> +++ b/src/libcamera/media_device.cpp
> @@ -20,6 +20,7 @@
>  #include <linux/media.h>
>  
>  #include <libcamera/base/log.h>
> +#include "libcamera/internal/v4l2_request.h"
>  
>  /**
>   * \file media_device.h
> @@ -851,4 +852,50 @@ std::vector<MediaEntity *> MediaDevice::locateEntities(unsigned int function)
>         return found;
>  }
>  
> +/**
> + * \brief Allocate requests
> + * \param[in] count Number of requests to allocate
> + * \param[out] requests Vector to store allocated requests
> + *
> + * Allocates and stores \a count requests in \a requests. If allocation fails,
> + * an error is returned and \a requests is cleared.
> + *
> + * \return 0 on success or a negative error code otherwise
> + */
> +int MediaDevice::allocateRequests(unsigned int count,
> +                                 std::vector<std::unique_ptr<V4L2Request>> *requests)
> +{
> +       requests->resize(count);
> +       for (unsigned int i = 0; i < count; i++) {
> +               int requestFd;
> +               int ret = ::ioctl(fd_.get(), MEDIA_IOC_REQUEST_ALLOC, &requestFd);
> +               if (ret < 0) {
> +                       requests->clear();
> +                       return -errno;
> +               }
> +               (*requests)[i] = std::make_unique<V4L2Request>(UniqueFD(requestFd));

Is this line safe on a vector? Is the size pre-determined before coming
in here? Or should this be something like:

	requests->push_back(std::make_unique<V4L2Request>(UniqueFD(requestFd));

I'm sure that's how we use vectors elsewhere...

> +       }
> +
> +       return 0;
> +}
> +
> +/**
> + * \brief Check if requests are supported
> + *
> + * Checks if the device supports V4L2 requests by trying to allocate a single
> + * request. The result is cached, so the allocation is only tried once.
> + *
> + * \return True if the device supports requests, false otherwise
> + */
> +bool MediaDevice::supportsRequests()
> +{
> +       if (supportsRequests_.has_value())

Ohhh now I see why it's an optional. Indeed.

> +               return supportsRequests_.value();
> +
> +       std::vector<std::unique_ptr<V4L2Request>> requests;
> +       supportsRequests_ = (allocateRequests(1, &requests) == 0);
> +
> +       return supportsRequests_.value();
> +}
> +
>  } /* namespace libcamera */
> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
> index 5b9b86f211f1..34e20f557514 100644
> --- a/src/libcamera/meson.build
> +++ b/src/libcamera/meson.build
> @@ -54,6 +54,7 @@ libcamera_internal_sources = files([
>      'sysfs.cpp',
>      'v4l2_device.cpp',
>      'v4l2_pixelformat.cpp',
> +    'v4l2_request.cpp',
>      'v4l2_subdevice.cpp',
>      'v4l2_videodevice.cpp',
>      'vector.cpp',
> diff --git a/src/libcamera/v4l2_device.cpp b/src/libcamera/v4l2_device.cpp
> index 8c78b8c424e5..8dcd5e618938 100644
> --- a/src/libcamera/v4l2_device.cpp
> +++ b/src/libcamera/v4l2_device.cpp
> @@ -162,6 +162,7 @@ void V4L2Device::close()
>  /**
>   * \brief Read controls from the device
>   * \param[in] ids The list of controls to read, specified by their ID
> + * \param[in] request An optional request
>   *
>   * This function reads the value of all controls contained in \a ids, and
>   * returns their values as a ControlList.
> @@ -171,10 +172,12 @@ void V4L2Device::close()
>   * during validation of the requested controls, no control is read and this
>   * function returns an empty control list.
>   *
> + * If \a request is specified the controls tied to that request are read.
> + *
>   * \return The control values in a ControlList on success, or an empty list on
>   * error
>   */
> -ControlList V4L2Device::getControls(Span<const uint32_t> ids)
> +ControlList V4L2Device::getControls(Span<const uint32_t> ids, const V4L2Request *request)
>  {
>         if (ids.empty())
>                 return {};
> @@ -242,10 +245,16 @@ ControlList V4L2Device::getControls(Span<const uint32_t> ids)
>         }
>  
>         struct v4l2_ext_controls v4l2ExtCtrls = {};
> -       v4l2ExtCtrls.which = V4L2_CTRL_WHICH_CUR_VAL;
>         v4l2ExtCtrls.controls = v4l2Ctrls.data();
>         v4l2ExtCtrls.count = v4l2Ctrls.size();
>  
> +       if (request) {
> +               v4l2ExtCtrls.which = V4L2_CTRL_WHICH_REQUEST_VAL;
> +               v4l2ExtCtrls.request_fd = request->fd();
> +       } else {
> +               v4l2ExtCtrls.which = V4L2_CTRL_WHICH_CUR_VAL;
> +       }
> +
>         int ret = ioctl(VIDIOC_G_EXT_CTRLS, &v4l2ExtCtrls);
>         if (ret) {
>                 unsigned int errorIdx = v4l2ExtCtrls.error_idx;
> @@ -273,6 +282,7 @@ ControlList V4L2Device::getControls(Span<const uint32_t> ids)
>  /**
>   * \brief Write controls to the device
>   * \param[in] ctrls The list of controls to write
> + * \param[in] request An optional request
>   *
>   * This function writes the value of all controls contained in \a ctrls, and
>   * stores the values actually applied to the device in the corresponding
> @@ -288,11 +298,15 @@ ControlList V4L2Device::getControls(Span<const uint32_t> ids)
>   * are written and their values are updated in \a ctrls, while all other
>   * controls are not written and their values are not changed.
>   *
> + * If \a request is set, the controls will be applied to that request. If the
> + * device doesn't support requests, -EACCESS will be returned. If \a request is
> + * invalid, -EINVAL will be returned.
> + *
>   * \return 0 on success or an error code otherwise
> - * \retval -EINVAL One of the control is not supported or not accessible
> + * \retval -EINVAL One of the controls is not supported or not accessible

Missing adding

    \retval -EACCESS the device does not support requests

>   * \retval i The index of the control that failed
>   */
> -int V4L2Device::setControls(ControlList *ctrls)
> +int V4L2Device::setControls(ControlList *ctrls, const V4L2Request *request)
>  {
>         if (ctrls->empty())
>                 return 0;
> @@ -377,10 +391,16 @@ int V4L2Device::setControls(ControlList *ctrls)
>         }
>  
>         struct v4l2_ext_controls v4l2ExtCtrls = {};
> -       v4l2ExtCtrls.which = V4L2_CTRL_WHICH_CUR_VAL;
>         v4l2ExtCtrls.controls = v4l2Ctrls.data();
>         v4l2ExtCtrls.count = v4l2Ctrls.size();
>  
> +       if (request) {
> +               v4l2ExtCtrls.which = V4L2_CTRL_WHICH_REQUEST_VAL;
> +               v4l2ExtCtrls.request_fd = request->fd();
> +       } else {
> +               v4l2ExtCtrls.which = V4L2_CTRL_WHICH_CUR_VAL;
> +       }
> +
>         int ret = ioctl(VIDIOC_S_EXT_CTRLS, &v4l2ExtCtrls);
>         if (ret) {
>                 unsigned int errorIdx = v4l2ExtCtrls.error_idx;
> diff --git a/src/libcamera/v4l2_request.cpp b/src/libcamera/v4l2_request.cpp
> new file mode 100644
> index 000000000000..9441d7c4017b
> --- /dev/null
> +++ b/src/libcamera/v4l2_request.cpp
> @@ -0,0 +1,128 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2025, Ideas On Board
> + *
> + * V4L2 Request API
> + */
> +
> +#include "libcamera/internal/v4l2_request.h"
> +
> +#include <fcntl.h>
> +#include <stdlib.h>
> +#include <sys/ioctl.h>
> +#include <sys/syscall.h>
> +#include <unistd.h>
> +
> +#include <linux/media.h>
> +
> +#include <libcamera/base/event_notifier.h>
> +#include <libcamera/base/log.h>
> +
> +/**
> + * \file v4l2_request.h
> + * \brief V4L2 Request
> + */
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(V4L2)
> +
> +/**
> + * \class V4L2Request
> + * \brief V4L2Request object and API
> + *
> + * The V4L2Request class wraps a V4L2 request fd and provides some convenience
> + * functions to handle request.
> + *
> + * It is usually constructed by calling \a MediaDevice::allocateRequests().
> + *
> + * A request can then be passed to the V4L2Device::setControls(),
> + * V4L2Device::getControls() and V4L2VideoDevice::queueBuffer().
> + */
> +
> +/**
> + * \brief Construct a V4L2Request
> + * \param[in] fd The request fd
> + */
> +V4L2Request::V4L2Request(UniqueFD &&fd)
> +       : fd_(std::move(fd)), fdNotifier_(fd_.get(), EventNotifier::Exception)
> +{
> +       if (!fd_.isValid())
> +               return;
> +
> +       fdNotifier_.activated.connect(this, &V4L2Request::requestReady);
> +       fdNotifier_.setEnabled(false);
> +}
> +
> +/**
> + * \fn V4L2Request::isValid()
> + * \brief Check if the request is valid
> + *
> + * Checks if the underlying fd is valid.
> + *
> + * \return True if the request is valid, false otherwise
> + */
> +
> +/**
> + * \fn V4L2Request::fd()
> + * \brief Get the file descriptor
> + *
> + * \return The file descriptor wrapped by this V4L2Request
> + */
> +
> +/**
> + * \var V4L2Request::requestDone
> + * \brief Signal that is emitted when the request is done
> + */
> +
> +/**
> + * \brief Reinit the request
> + *
> + * Calls MEDIA_REQUEST_IOC_REINIT on the request fd.
> + *
> + * \return 0 on success or a negative error code otherwise
> + */
> +int V4L2Request::reinit()
> +{
> +       fdNotifier_.setEnabled(false);
> +
> +       if (::ioctl(fd_.get(), MEDIA_REQUEST_IOC_REINIT) < 0)
> +               return -errno;
> +
> +       return 0;
> +}
> +
> +/**
> + * \brief Queue the request
> + *
> + * Calls MEDIA_REQUEST_IOC_QUEUE on the request fd.
> + *
> + * \return 0 on success or a negative error code otherwise
> + */
> +int V4L2Request::queue()
> +{
> +       if (::ioctl(fd_.get(), MEDIA_REQUEST_IOC_QUEUE) < 0)
> +               return -errno;
> +
> +       fdNotifier_.setEnabled(true);
> +
> +       return 0;
> +}
> +
> +std::string V4L2Request::logPrefix() const
> +{
> +       return "Request [" + std::to_string(fd()) + "]";
> +}
> +
> +/**
> + * \brief Slot to handle request done events
> + *
> + * When this slot is called, the request is done and the requestDone will be
> + * emitted.
> + */
> +void V4L2Request::requestReady()
> +{
> +       requestDone.emit(this);
> +}
> +
> +} /* namespace libcamera */
> diff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp
> index 7b48d911db73..25b61d049a0e 100644
> --- a/src/libcamera/v4l2_videodevice.cpp
> +++ b/src/libcamera/v4l2_videodevice.cpp
> @@ -30,6 +30,7 @@
>  #include "libcamera/internal/framebuffer.h"
>  #include "libcamera/internal/media_device.h"
>  #include "libcamera/internal/media_object.h"
> +#include "libcamera/internal/v4l2_request.h"
>  
>  /**
>   * \file v4l2_videodevice.h
> @@ -1629,6 +1630,7 @@ int V4L2VideoDevice::releaseBuffers()
>  /**
>   * \brief Queue a buffer to the video device
>   * \param[in] buffer The buffer to be queued
> + * \param[in] request An optional request
>   *
>   * For capture video devices the \a buffer will be filled with data by the
>   * device. For output video devices the \a buffer shall contain valid data and
> @@ -1641,9 +1643,11 @@ int V4L2VideoDevice::releaseBuffers()
>   * Note that queueBuffer() will fail if the device is in the process of being
>   * stopped from a streaming state through streamOff().
>   *
> + * If \a request is specified, the buffer will be tied to that request.
> + *
>   * \return 0 on success or a negative error code otherwise
>   */
> -int V4L2VideoDevice::queueBuffer(FrameBuffer *buffer)
> +int V4L2VideoDevice::queueBuffer(FrameBuffer *buffer, const V4L2Request *request)
>  {
>         struct v4l2_plane v4l2Planes[VIDEO_MAX_PLANES] = {};
>         struct v4l2_buffer buf = {};
> @@ -1674,6 +1678,10 @@ int V4L2VideoDevice::queueBuffer(FrameBuffer *buffer)
>         buf.type = bufferType_;
>         buf.memory = memoryType_;
>         buf.field = V4L2_FIELD_NONE;
> +       if (request) {
> +               buf.flags = V4L2_BUF_FLAG_REQUEST_FD;
> +               buf.request_fd = request->fd();
> +       }
>  
>         bool multiPlanar = V4L2_TYPE_IS_MULTIPLANAR(buf.type);
>         Span<const FrameBuffer::Plane> planes = buffer->planes();
> -- 
> 2.51.0
>
Kieran Bingham Nov. 25, 2025, 6:19 p.m. UTC | #2
Quoting Kieran Bingham (2025-11-25 17:03:00)
> Quoting Stefan Klug (2025-11-25 16:28:15)
> > The V4L2 requests API provides support to atomically tie controls to a
> > set of buffers. This is especially common for m2m devices. Such a
> > request is represented by an fd that is allocated via
> > MEDIA_IOC_REQUEST_ALLOC and then passed to the various V4L2 functions.
> > 
> > Implement a V4L2Request class to wrap such an fd and add the
> > corresponding utility functions.
> > 
> > Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
> > 
> 
> Mostly minor comments below.
> 
> Only my question on safety of usage of the vector on allocate stops me
> putting a tag in... if someone confirms that is safe then I could add
> one ;-)
> 
> 
> 
> > ---
> > 
> > Changes in v3:
> > - Replaced int by UniqueFD
> > - Fixed lots if typos from review
> > 
> > Changes in v2:
> > - Added documentation
> > ---
> >  include/libcamera/internal/media_device.h     |   8 ++
> >  include/libcamera/internal/meson.build        |   1 +
> >  include/libcamera/internal/v4l2_device.h      |   5 +-
> >  include/libcamera/internal/v4l2_request.h     |  44 ++++++
> >  include/libcamera/internal/v4l2_videodevice.h |   3 +-
> >  src/libcamera/media_device.cpp                |  47 +++++++
> >  src/libcamera/meson.build                     |   1 +
> >  src/libcamera/v4l2_device.cpp                 |  30 +++-
> >  src/libcamera/v4l2_request.cpp                | 128 ++++++++++++++++++
> >  src/libcamera/v4l2_videodevice.cpp            |  10 +-
> >  10 files changed, 268 insertions(+), 9 deletions(-)
> >  create mode 100644 include/libcamera/internal/v4l2_request.h
> >  create mode 100644 src/libcamera/v4l2_request.cpp
> > 
> > diff --git a/include/libcamera/internal/media_device.h b/include/libcamera/internal/media_device.h
> > index b3a48b98d64b..2eb3ad988b09 100644
> > --- a/include/libcamera/internal/media_device.h
> > +++ b/include/libcamera/internal/media_device.h
> > @@ -8,6 +8,7 @@
> >  #pragma once
> >  
> >  #include <map>
> > +#include <optional>
> >  #include <string>
> >  #include <vector>
> >  
> > @@ -18,6 +19,7 @@
> >  #include <libcamera/base/unique_fd.h>
> >  
> >  #include "libcamera/internal/media_object.h"
> > +#include "libcamera/internal/v4l2_request.h"
> >  
> >  namespace libcamera {
> >  
> > @@ -57,6 +59,11 @@ public:
> >  
> >         std::vector<MediaEntity *> locateEntities(unsigned int function);
> >  
> > +       int allocateRequests(unsigned int count,
> > +                            std::vector<std::unique_ptr<V4L2Request>> *requests);
> > +
> > +       bool supportsRequests();
> > +
> >  protected:
> >         std::string logPrefix() const override;
> >  
> > @@ -87,6 +94,7 @@ private:
> >         UniqueFD fd_;
> >         bool valid_;
> >         bool acquired_;
> > +       std::optional<bool> supportsRequests_;
> 
> std::optional is intriguing here... I'd default false until known
> otherwise...
> 
> >         std::map<unsigned int, MediaObject *> objects_;
> >         std::vector<MediaEntity *> entities_;
> > diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build
> > index 45c299f6a332..e9540a2f734f 100644
> > --- a/include/libcamera/internal/meson.build
> > +++ b/include/libcamera/internal/meson.build
> > @@ -44,6 +44,7 @@ libcamera_internal_headers = files([
> >      'sysfs.h',
> >      'v4l2_device.h',
> >      'v4l2_pixelformat.h',
> > +    'v4l2_request.h',
> >      'v4l2_subdevice.h',
> >      'v4l2_videodevice.h',
> >      'vector.h',
> > diff --git a/include/libcamera/internal/v4l2_device.h b/include/libcamera/internal/v4l2_device.h
> > index 5bc9da96677d..dbbd118abd00 100644
> > --- a/include/libcamera/internal/v4l2_device.h
> > +++ b/include/libcamera/internal/v4l2_device.h
> > @@ -24,6 +24,7 @@
> >  #include <libcamera/controls.h>
> >  
> >  #include "libcamera/internal/formats.h"
> > +#include "libcamera/internal/v4l2_request.h"
> >  
> >  namespace libcamera {
> >  
> > @@ -37,8 +38,8 @@ public:
> >  
> >         const ControlInfoMap &controls() const { return controls_; }
> >  
> > -       ControlList getControls(Span<const uint32_t> ids);
> > -       int setControls(ControlList *ctrls);
> > +       ControlList getControls(Span<const uint32_t> ids, const V4L2Request *request = nullptr);
> > +       int setControls(ControlList *ctrls, const V4L2Request *request = nullptr);
> >  
> >         const struct v4l2_query_ext_ctrl *controlInfo(uint32_t id) const;
> >  
> > diff --git a/include/libcamera/internal/v4l2_request.h b/include/libcamera/internal/v4l2_request.h
> > new file mode 100644
> > index 000000000000..376c79ceedba
> > --- /dev/null
> > +++ b/include/libcamera/internal/v4l2_request.h
> > @@ -0,0 +1,44 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2025, Ideas On Board
> > + *
> > + * V4L2 requests
> > + */
> > +
> > +#pragma once
> > +
> > +#include <string>
> > +
> > +#include <linux/videodev2.h>
> > +
> > +#include <libcamera/base/event_notifier.h>
> > +#include <libcamera/base/log.h>
> > +#include <libcamera/base/signal.h>
> > +#include <libcamera/base/unique_fd.h>
> > +
> > +namespace libcamera {
> > +
> > +class V4L2Request : protected Loggable
> > +{
> > +public:
> > +       bool isValid() const { return fd_.isValid(); }
> > +       int fd() const { return fd_.get(); }
> > +
> > +       int reinit();
> > +       int queue();
> > +
> > +       V4L2Request(UniqueFD &&fd);
> > +
> > +       Signal<V4L2Request *> requestDone;
> > +
> > +private:
> > +       LIBCAMERA_DISABLE_COPY_AND_MOVE(V4L2Request)
> > +
> > +       void requestReady();
> > +       std::string logPrefix() const override;
> > +
> > +       UniqueFD fd_;
> > +       EventNotifier fdNotifier_;
> > +};
> > +
> > +} /* namespace libcamera */
> > diff --git a/include/libcamera/internal/v4l2_videodevice.h b/include/libcamera/internal/v4l2_videodevice.h
> > index 6caafc4dcf08..57db0036db6b 100644
> > --- a/include/libcamera/internal/v4l2_videodevice.h
> > +++ b/include/libcamera/internal/v4l2_videodevice.h
> > @@ -33,6 +33,7 @@
> >  #include "libcamera/internal/formats.h"
> >  #include "libcamera/internal/v4l2_device.h"
> >  #include "libcamera/internal/v4l2_pixelformat.h"
> > +#include "libcamera/internal/v4l2_request.h"
> >  
> >  namespace libcamera {
> >  
> > @@ -217,7 +218,7 @@ public:
> >         int importBuffers(unsigned int count);
> >         int releaseBuffers();
> >  
> > -       int queueBuffer(FrameBuffer *buffer);
> > +       int queueBuffer(FrameBuffer *buffer, const V4L2Request *request = nullptr);
> >         Signal<FrameBuffer *> bufferReady;
> >  
> >         int streamOn();
> > diff --git a/src/libcamera/media_device.cpp b/src/libcamera/media_device.cpp
> > index 353f34a81eca..2a848ebed998 100644
> > --- a/src/libcamera/media_device.cpp
> > +++ b/src/libcamera/media_device.cpp
> > @@ -20,6 +20,7 @@
> >  #include <linux/media.h>
> >  
> >  #include <libcamera/base/log.h>
> > +#include "libcamera/internal/v4l2_request.h"
> >  
> >  /**
> >   * \file media_device.h
> > @@ -851,4 +852,50 @@ std::vector<MediaEntity *> MediaDevice::locateEntities(unsigned int function)
> >         return found;
> >  }
> >  
> > +/**
> > + * \brief Allocate requests
> > + * \param[in] count Number of requests to allocate
> > + * \param[out] requests Vector to store allocated requests
> > + *
> > + * Allocates and stores \a count requests in \a requests. If allocation fails,
> > + * an error is returned and \a requests is cleared.
> > + *
> > + * \return 0 on success or a negative error code otherwise
> > + */
> > +int MediaDevice::allocateRequests(unsigned int count,
> > +                                 std::vector<std::unique_ptr<V4L2Request>> *requests)
> > +{
> > +       requests->resize(count);

There it is - it was hiding in plain sight. I missed it.

Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>

> > +       for (unsigned int i = 0; i < count; i++) {
> > +               int requestFd;
> > +               int ret = ::ioctl(fd_.get(), MEDIA_IOC_REQUEST_ALLOC, &requestFd);
> > +               if (ret < 0) {
> > +                       requests->clear();
> > +                       return -errno;
> > +               }
> > +               (*requests)[i] = std::make_unique<V4L2Request>(UniqueFD(requestFd));
> 
> Is this line safe on a vector? Is the size pre-determined before coming
> in here? Or should this be something like:
> 
>         requests->push_back(std::make_unique<V4L2Request>(UniqueFD(requestFd));
> 
> I'm sure that's how we use vectors elsewhere...

--
Kieran

Patch
diff mbox series

diff --git a/include/libcamera/internal/media_device.h b/include/libcamera/internal/media_device.h
index b3a48b98d64b..2eb3ad988b09 100644
--- a/include/libcamera/internal/media_device.h
+++ b/include/libcamera/internal/media_device.h
@@ -8,6 +8,7 @@ 
 #pragma once
 
 #include <map>
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -18,6 +19,7 @@ 
 #include <libcamera/base/unique_fd.h>
 
 #include "libcamera/internal/media_object.h"
+#include "libcamera/internal/v4l2_request.h"
 
 namespace libcamera {
 
@@ -57,6 +59,11 @@  public:
 
 	std::vector<MediaEntity *> locateEntities(unsigned int function);
 
+	int allocateRequests(unsigned int count,
+			     std::vector<std::unique_ptr<V4L2Request>> *requests);
+
+	bool supportsRequests();
+
 protected:
 	std::string logPrefix() const override;
 
@@ -87,6 +94,7 @@  private:
 	UniqueFD fd_;
 	bool valid_;
 	bool acquired_;
+	std::optional<bool> supportsRequests_;
 
 	std::map<unsigned int, MediaObject *> objects_;
 	std::vector<MediaEntity *> entities_;
diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build
index 45c299f6a332..e9540a2f734f 100644
--- a/include/libcamera/internal/meson.build
+++ b/include/libcamera/internal/meson.build
@@ -44,6 +44,7 @@  libcamera_internal_headers = files([
     'sysfs.h',
     'v4l2_device.h',
     'v4l2_pixelformat.h',
+    'v4l2_request.h',
     'v4l2_subdevice.h',
     'v4l2_videodevice.h',
     'vector.h',
diff --git a/include/libcamera/internal/v4l2_device.h b/include/libcamera/internal/v4l2_device.h
index 5bc9da96677d..dbbd118abd00 100644
--- a/include/libcamera/internal/v4l2_device.h
+++ b/include/libcamera/internal/v4l2_device.h
@@ -24,6 +24,7 @@ 
 #include <libcamera/controls.h>
 
 #include "libcamera/internal/formats.h"
+#include "libcamera/internal/v4l2_request.h"
 
 namespace libcamera {
 
@@ -37,8 +38,8 @@  public:
 
 	const ControlInfoMap &controls() const { return controls_; }
 
-	ControlList getControls(Span<const uint32_t> ids);
-	int setControls(ControlList *ctrls);
+	ControlList getControls(Span<const uint32_t> ids, const V4L2Request *request = nullptr);
+	int setControls(ControlList *ctrls, const V4L2Request *request = nullptr);
 
 	const struct v4l2_query_ext_ctrl *controlInfo(uint32_t id) const;
 
diff --git a/include/libcamera/internal/v4l2_request.h b/include/libcamera/internal/v4l2_request.h
new file mode 100644
index 000000000000..376c79ceedba
--- /dev/null
+++ b/include/libcamera/internal/v4l2_request.h
@@ -0,0 +1,44 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board
+ *
+ * V4L2 requests
+ */
+
+#pragma once
+
+#include <string>
+
+#include <linux/videodev2.h>
+
+#include <libcamera/base/event_notifier.h>
+#include <libcamera/base/log.h>
+#include <libcamera/base/signal.h>
+#include <libcamera/base/unique_fd.h>
+
+namespace libcamera {
+
+class V4L2Request : protected Loggable
+{
+public:
+	bool isValid() const { return fd_.isValid(); }
+	int fd() const { return fd_.get(); }
+
+	int reinit();
+	int queue();
+
+	V4L2Request(UniqueFD &&fd);
+
+	Signal<V4L2Request *> requestDone;
+
+private:
+	LIBCAMERA_DISABLE_COPY_AND_MOVE(V4L2Request)
+
+	void requestReady();
+	std::string logPrefix() const override;
+
+	UniqueFD fd_;
+	EventNotifier fdNotifier_;
+};
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/v4l2_videodevice.h b/include/libcamera/internal/v4l2_videodevice.h
index 6caafc4dcf08..57db0036db6b 100644
--- a/include/libcamera/internal/v4l2_videodevice.h
+++ b/include/libcamera/internal/v4l2_videodevice.h
@@ -33,6 +33,7 @@ 
 #include "libcamera/internal/formats.h"
 #include "libcamera/internal/v4l2_device.h"
 #include "libcamera/internal/v4l2_pixelformat.h"
+#include "libcamera/internal/v4l2_request.h"
 
 namespace libcamera {
 
@@ -217,7 +218,7 @@  public:
 	int importBuffers(unsigned int count);
 	int releaseBuffers();
 
-	int queueBuffer(FrameBuffer *buffer);
+	int queueBuffer(FrameBuffer *buffer, const V4L2Request *request = nullptr);
 	Signal<FrameBuffer *> bufferReady;
 
 	int streamOn();
diff --git a/src/libcamera/media_device.cpp b/src/libcamera/media_device.cpp
index 353f34a81eca..2a848ebed998 100644
--- a/src/libcamera/media_device.cpp
+++ b/src/libcamera/media_device.cpp
@@ -20,6 +20,7 @@ 
 #include <linux/media.h>
 
 #include <libcamera/base/log.h>
+#include "libcamera/internal/v4l2_request.h"
 
 /**
  * \file media_device.h
@@ -851,4 +852,50 @@  std::vector<MediaEntity *> MediaDevice::locateEntities(unsigned int function)
 	return found;
 }
 
+/**
+ * \brief Allocate requests
+ * \param[in] count Number of requests to allocate
+ * \param[out] requests Vector to store allocated requests
+ *
+ * Allocates and stores \a count requests in \a requests. If allocation fails,
+ * an error is returned and \a requests is cleared.
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+int MediaDevice::allocateRequests(unsigned int count,
+				  std::vector<std::unique_ptr<V4L2Request>> *requests)
+{
+	requests->resize(count);
+	for (unsigned int i = 0; i < count; i++) {
+		int requestFd;
+		int ret = ::ioctl(fd_.get(), MEDIA_IOC_REQUEST_ALLOC, &requestFd);
+		if (ret < 0) {
+			requests->clear();
+			return -errno;
+		}
+		(*requests)[i] = std::make_unique<V4L2Request>(UniqueFD(requestFd));
+	}
+
+	return 0;
+}
+
+/**
+ * \brief Check if requests are supported
+ *
+ * Checks if the device supports V4L2 requests by trying to allocate a single
+ * request. The result is cached, so the allocation is only tried once.
+ *
+ * \return True if the device supports requests, false otherwise
+ */
+bool MediaDevice::supportsRequests()
+{
+	if (supportsRequests_.has_value())
+		return supportsRequests_.value();
+
+	std::vector<std::unique_ptr<V4L2Request>> requests;
+	supportsRequests_ = (allocateRequests(1, &requests) == 0);
+
+	return supportsRequests_.value();
+}
+
 } /* namespace libcamera */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 5b9b86f211f1..34e20f557514 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -54,6 +54,7 @@  libcamera_internal_sources = files([
     'sysfs.cpp',
     'v4l2_device.cpp',
     'v4l2_pixelformat.cpp',
+    'v4l2_request.cpp',
     'v4l2_subdevice.cpp',
     'v4l2_videodevice.cpp',
     'vector.cpp',
diff --git a/src/libcamera/v4l2_device.cpp b/src/libcamera/v4l2_device.cpp
index 8c78b8c424e5..8dcd5e618938 100644
--- a/src/libcamera/v4l2_device.cpp
+++ b/src/libcamera/v4l2_device.cpp
@@ -162,6 +162,7 @@  void V4L2Device::close()
 /**
  * \brief Read controls from the device
  * \param[in] ids The list of controls to read, specified by their ID
+ * \param[in] request An optional request
  *
  * This function reads the value of all controls contained in \a ids, and
  * returns their values as a ControlList.
@@ -171,10 +172,12 @@  void V4L2Device::close()
  * during validation of the requested controls, no control is read and this
  * function returns an empty control list.
  *
+ * If \a request is specified the controls tied to that request are read.
+ *
  * \return The control values in a ControlList on success, or an empty list on
  * error
  */
-ControlList V4L2Device::getControls(Span<const uint32_t> ids)
+ControlList V4L2Device::getControls(Span<const uint32_t> ids, const V4L2Request *request)
 {
 	if (ids.empty())
 		return {};
@@ -242,10 +245,16 @@  ControlList V4L2Device::getControls(Span<const uint32_t> ids)
 	}
 
 	struct v4l2_ext_controls v4l2ExtCtrls = {};
-	v4l2ExtCtrls.which = V4L2_CTRL_WHICH_CUR_VAL;
 	v4l2ExtCtrls.controls = v4l2Ctrls.data();
 	v4l2ExtCtrls.count = v4l2Ctrls.size();
 
+	if (request) {
+		v4l2ExtCtrls.which = V4L2_CTRL_WHICH_REQUEST_VAL;
+		v4l2ExtCtrls.request_fd = request->fd();
+	} else {
+		v4l2ExtCtrls.which = V4L2_CTRL_WHICH_CUR_VAL;
+	}
+
 	int ret = ioctl(VIDIOC_G_EXT_CTRLS, &v4l2ExtCtrls);
 	if (ret) {
 		unsigned int errorIdx = v4l2ExtCtrls.error_idx;
@@ -273,6 +282,7 @@  ControlList V4L2Device::getControls(Span<const uint32_t> ids)
 /**
  * \brief Write controls to the device
  * \param[in] ctrls The list of controls to write
+ * \param[in] request An optional request
  *
  * This function writes the value of all controls contained in \a ctrls, and
  * stores the values actually applied to the device in the corresponding
@@ -288,11 +298,15 @@  ControlList V4L2Device::getControls(Span<const uint32_t> ids)
  * are written and their values are updated in \a ctrls, while all other
  * controls are not written and their values are not changed.
  *
+ * If \a request is set, the controls will be applied to that request. If the
+ * device doesn't support requests, -EACCESS will be returned. If \a request is
+ * invalid, -EINVAL will be returned.
+ *
  * \return 0 on success or an error code otherwise
- * \retval -EINVAL One of the control is not supported or not accessible
+ * \retval -EINVAL One of the controls is not supported or not accessible
  * \retval i The index of the control that failed
  */
-int V4L2Device::setControls(ControlList *ctrls)
+int V4L2Device::setControls(ControlList *ctrls, const V4L2Request *request)
 {
 	if (ctrls->empty())
 		return 0;
@@ -377,10 +391,16 @@  int V4L2Device::setControls(ControlList *ctrls)
 	}
 
 	struct v4l2_ext_controls v4l2ExtCtrls = {};
-	v4l2ExtCtrls.which = V4L2_CTRL_WHICH_CUR_VAL;
 	v4l2ExtCtrls.controls = v4l2Ctrls.data();
 	v4l2ExtCtrls.count = v4l2Ctrls.size();
 
+	if (request) {
+		v4l2ExtCtrls.which = V4L2_CTRL_WHICH_REQUEST_VAL;
+		v4l2ExtCtrls.request_fd = request->fd();
+	} else {
+		v4l2ExtCtrls.which = V4L2_CTRL_WHICH_CUR_VAL;
+	}
+
 	int ret = ioctl(VIDIOC_S_EXT_CTRLS, &v4l2ExtCtrls);
 	if (ret) {
 		unsigned int errorIdx = v4l2ExtCtrls.error_idx;
diff --git a/src/libcamera/v4l2_request.cpp b/src/libcamera/v4l2_request.cpp
new file mode 100644
index 000000000000..9441d7c4017b
--- /dev/null
+++ b/src/libcamera/v4l2_request.cpp
@@ -0,0 +1,128 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board
+ *
+ * V4L2 Request API
+ */
+
+#include "libcamera/internal/v4l2_request.h"
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+#include <linux/media.h>
+
+#include <libcamera/base/event_notifier.h>
+#include <libcamera/base/log.h>
+
+/**
+ * \file v4l2_request.h
+ * \brief V4L2 Request
+ */
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(V4L2)
+
+/**
+ * \class V4L2Request
+ * \brief V4L2Request object and API
+ *
+ * The V4L2Request class wraps a V4L2 request fd and provides some convenience
+ * functions to handle request.
+ *
+ * It is usually constructed by calling \a MediaDevice::allocateRequests().
+ *
+ * A request can then be passed to the V4L2Device::setControls(),
+ * V4L2Device::getControls() and V4L2VideoDevice::queueBuffer().
+ */
+
+/**
+ * \brief Construct a V4L2Request
+ * \param[in] fd The request fd
+ */
+V4L2Request::V4L2Request(UniqueFD &&fd)
+	: fd_(std::move(fd)), fdNotifier_(fd_.get(), EventNotifier::Exception)
+{
+	if (!fd_.isValid())
+		return;
+
+	fdNotifier_.activated.connect(this, &V4L2Request::requestReady);
+	fdNotifier_.setEnabled(false);
+}
+
+/**
+ * \fn V4L2Request::isValid()
+ * \brief Check if the request is valid
+ *
+ * Checks if the underlying fd is valid.
+ *
+ * \return True if the request is valid, false otherwise
+ */
+
+/**
+ * \fn V4L2Request::fd()
+ * \brief Get the file descriptor
+ *
+ * \return The file descriptor wrapped by this V4L2Request
+ */
+
+/**
+ * \var V4L2Request::requestDone
+ * \brief Signal that is emitted when the request is done
+ */
+
+/**
+ * \brief Reinit the request
+ *
+ * Calls MEDIA_REQUEST_IOC_REINIT on the request fd.
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Request::reinit()
+{
+	fdNotifier_.setEnabled(false);
+
+	if (::ioctl(fd_.get(), MEDIA_REQUEST_IOC_REINIT) < 0)
+		return -errno;
+
+	return 0;
+}
+
+/**
+ * \brief Queue the request
+ *
+ * Calls MEDIA_REQUEST_IOC_QUEUE on the request fd.
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Request::queue()
+{
+	if (::ioctl(fd_.get(), MEDIA_REQUEST_IOC_QUEUE) < 0)
+		return -errno;
+
+	fdNotifier_.setEnabled(true);
+
+	return 0;
+}
+
+std::string V4L2Request::logPrefix() const
+{
+	return "Request [" + std::to_string(fd()) + "]";
+}
+
+/**
+ * \brief Slot to handle request done events
+ *
+ * When this slot is called, the request is done and the requestDone will be
+ * emitted.
+ */
+void V4L2Request::requestReady()
+{
+	requestDone.emit(this);
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp
index 7b48d911db73..25b61d049a0e 100644
--- a/src/libcamera/v4l2_videodevice.cpp
+++ b/src/libcamera/v4l2_videodevice.cpp
@@ -30,6 +30,7 @@ 
 #include "libcamera/internal/framebuffer.h"
 #include "libcamera/internal/media_device.h"
 #include "libcamera/internal/media_object.h"
+#include "libcamera/internal/v4l2_request.h"
 
 /**
  * \file v4l2_videodevice.h
@@ -1629,6 +1630,7 @@  int V4L2VideoDevice::releaseBuffers()
 /**
  * \brief Queue a buffer to the video device
  * \param[in] buffer The buffer to be queued
+ * \param[in] request An optional request
  *
  * For capture video devices the \a buffer will be filled with data by the
  * device. For output video devices the \a buffer shall contain valid data and
@@ -1641,9 +1643,11 @@  int V4L2VideoDevice::releaseBuffers()
  * Note that queueBuffer() will fail if the device is in the process of being
  * stopped from a streaming state through streamOff().
  *
+ * If \a request is specified, the buffer will be tied to that request.
+ *
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2VideoDevice::queueBuffer(FrameBuffer *buffer)
+int V4L2VideoDevice::queueBuffer(FrameBuffer *buffer, const V4L2Request *request)
 {
 	struct v4l2_plane v4l2Planes[VIDEO_MAX_PLANES] = {};
 	struct v4l2_buffer buf = {};
@@ -1674,6 +1678,10 @@  int V4L2VideoDevice::queueBuffer(FrameBuffer *buffer)
 	buf.type = bufferType_;
 	buf.memory = memoryType_;
 	buf.field = V4L2_FIELD_NONE;
+	if (request) {
+		buf.flags = V4L2_BUF_FLAG_REQUEST_FD;
+		buf.request_fd = request->fd();
+	}
 
 	bool multiPlanar = V4L2_TYPE_IS_MULTIPLANAR(buf.type);
 	Span<const FrameBuffer::Plane> planes = buffer->planes();