Subject: Re: [PATCH v3] media: add virtio-media driver

From: "Michael S. Tsirkin" <[email-masked]>
Date: Tue, 27 May 2025 10:23:32 -0400

On Mon, May 26, 2025 at 02:13:16PM +0200, Mauro Carvalho Chehab wrote:
> Hi Michael,
> 
> Em Sat, 12 Apr 2025 13:08:01 +0900
> Alexandre Courbot <[email-masked]> escreveu:
> 
> > Add the first version of the virtio-media driver.
> > 
> > This driver acts roughly as a V4L2 relay between user-space and the
> > virtio virtual device on the host, so it is relatively simple, yet
> > unconventional. It doesn't use VB2 or other frameworks typically used in
> > a V4L2 driver, and most of its complexity resides in correctly and
> > efficiently building the virtio descriptor chain to pass to the host,
> > avoiding copies whenever possible. This is done by
> > scatterlist_builder.[ch].
> > 
> > virtio_media_ioctls.c proxies each supported ioctl to the host, using
> > code generated through macros for ioctls that can be forwarded directly,
> > which is most of them.
> > 
> > virtio_media_driver.c provides the expected driver hooks, and support
> > for mmapping and polling.
> > 
> >  This version supports MMAP buffers, while USERPTR buffers can also be
> >  enabled through a driver option. DMABUF support is still pending.
> 
> It sounds that you applied this one at the virtio tree, but it hasn't
> being reviewed or acked by media maintainers.
> 
> Please drop it.
> 
> Alexandre,
> 
> Please send media patches to media maintainers, c/c other subsystem
> maintainers, as otherwise they might end being merged without a
> proper review.
> 
> In this particular case, we need to double-check if this won't cause
> any issues, in special with regards to media locks and mutexes.
> 
> I'll try to look on it after this merge window, as it is too late
> for it to be applied during this one.
> 
> Regards,
> Mauro

New drivers generally can be merged during the merge window,
especially early.  It's up to you though.
I can keep it in next for now, so it gets some coverage by
tools scanning that tree.

> > 
> > Signed-off-by: Alexandre Courbot <[email-masked]>
> > Signed-off-by: Alexandre Courbot <[email-masked]>
> > ---
> > This patch adds the virtio-media kernel driver. Virtio-media [1]
> > encapsulates the V4L2 structures and protocol to enable the
> > virtualization of host media devices into a guest. It's specification is
> > in the final stages [2] of being merged and the virtualization of
> > cameras and video accelerator devices has already been demonstrated
> > using crosvm [3] and QEmu. v4l2-compliance also passes on all tested
> > devices, which includes the "simple" virtual test device, proxied host
> > UVC and vivid devices, and the FFmpeg virtual decoder devices (refer to
> > [3] in order to test these if desired).
> > 
> > Virtio-media is merged in AOSP [4] and ChromeOS. Upstreaming of the
> > driver is overdue, but I hope we can start the review process and
> > converge into something that can be merged.
> > 
> > Limitations:
> > 
> > - The driver is currently only available to little-endian, 64-bit
> >   kernels. This is because some of the V4L2 structures used for
> >   communication between guest and host have a layout dependent on the
> >   architecture, and the virtio-media protocol is standardized on the
> >   little-endian 64-bit versions. This can be fixed with a conversion
> >   layer similar to the one used to convert 32-bit ioctls to their 64-bit
> >   counterpart.
> > - DMABUF support is currently missing. It should be implemented using
> >   virtio objects, with possible support for memfds using the
> >   SHARED_PAGES memory type.
> > - No support for the media API and requests. While the use-case for
> >   these is less important on virtual devices where we want to present an
> >   abstraction as high as possible to limit VM exits, they do exist and
> >   it would be nice to add behind a virtio feature bit.
> > - Locking in the driver is still very basic. This is something I want to
> >   improve before merging, but I didn't want to delay upstream review any
> >   further.
> > 
> > [1] https://github.com/chromeos/virtio-media
> > [2] https://lore.kernel.org/virtio-comment/[email-masked]/
> > [3] https://github.com/chromeos/virtio-media/blob/main/TRY_IT_OUT.md
> > [4] https://android.googlesource.com/platform/external/virtio-media/
> > ---
> > Changes in v3:
> > - Rebased on top of v6.15-rc1 and removes obsolete control callbacks.
> > - Link to v2: https://lore.kernel.org/r/[email-masked]
> > 
> > Changes in v2:
> > - Fixed kernel test robot and media CI warnings (ignored a few false
> >   positives).
> > - Changed in-driver email address to personal one since my Google one
> >   will soon become invalid.
> > - Link to v1: https://lore.kernel.org/r/[email-masked]
> > ---
> >  MAINTAINERS                                |    6 +
> >  drivers/media/Kconfig                      |   13 +
> >  drivers/media/Makefile                     |    2 +
> >  drivers/media/virtio/Makefile              |    9 +
> >  drivers/media/virtio/protocol.h            |  288 ++++++
> >  drivers/media/virtio/scatterlist_builder.c |  563 ++++++++++++
> >  drivers/media/virtio/scatterlist_builder.h |  111 +++
> >  drivers/media/virtio/session.h             |  109 +++
> >  drivers/media/virtio/virtio_media.h        |   93 ++
> >  drivers/media/virtio/virtio_media_driver.c |  959 ++++++++++++++++++++
> >  drivers/media/virtio/virtio_media_ioctls.c | 1297 ++++++++++++++++++++++++++++
> >  include/uapi/linux/virtio_ids.h            |    1 +
> >  12 files changed, 3451 insertions(+)
> > 
> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index 96b82704950184bd71623ff41fc4df31e4c7fe87..f60e17011124fe8c0be0343d4f87e1458f311dcc 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -25641,6 +25641,12 @@ S:	Maintained
> >  F:	drivers/iommu/virtio-iommu.c
> >  F:	include/uapi/linux/virtio_iommu.h
> >  
> > +VIRTIO MEDIA DRIVER
> > +M:	Alexandre Courbot <[email-masked]>
> > +L:	[email-masked]
> > +S:	Maintained
> > +F:	drivers/media/virtio/
> > +
> >  VIRTIO MEM DRIVER
> >  M:	David Hildenbrand <[email-masked]>
> >  L:	[email-masked]
> > diff --git a/drivers/media/Kconfig b/drivers/media/Kconfig
> > index 6abc9302cd84d8563b7877d3d3da4b7e05a6b5d2..12bbb169c0b04565271092c7ac608b0fb11c0244 100644
> > --- a/drivers/media/Kconfig
> > +++ b/drivers/media/Kconfig
> > @@ -230,6 +230,19 @@ source "drivers/media/platform/Kconfig"
> >  source "drivers/media/mmc/Kconfig"
> >  endif
> >  
> > +config MEDIA_VIRTIO
> > +	tristate "Virtio-media Driver"
> > +	depends on VIRTIO && VIDEO_DEV && 64BIT && (X86 || (ARM && CPU_LITTLE_ENDIAN))
> > +	select VIDEOBUF2_CORE
> > +	select VIDEOBUF2_MEMOPS
> > +	help
> > +	  Enables the virtio-media driver.
> > +
> > +	  This driver is used to virtualize media devices such as cameras or
> > +	  decoders from a host into a guest using the V4L2 protocol.
> > +
> > +	  If unsure, say N.
> > +
> >  if MEDIA_TEST_SUPPORT
> >  source "drivers/media/test-drivers/Kconfig"
> >  endif
> > diff --git a/drivers/media/Makefile b/drivers/media/Makefile
> > index 20fac24e4f0f13134c12cd859141c8b0387030fa..7a1377661919701f27f4fa2b5ee2dcb1045deb3c 100644
> > --- a/drivers/media/Makefile
> > +++ b/drivers/media/Makefile
> > @@ -25,6 +25,8 @@ obj-y += rc/
> >  
> >  obj-$(CONFIG_CEC_CORE) += cec/
> >  
> > +obj-$(CONFIG_MEDIA_VIRTIO) += virtio/
> > +
> >  #
> >  # Finally, merge the drivers that require the core
> >  #
> > diff --git a/drivers/media/virtio/Makefile b/drivers/media/virtio/Makefile
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..16f91304420d70e1212cc46f3b12f314a510c051
> > --- /dev/null
> > +++ b/drivers/media/virtio/Makefile
> > @@ -0,0 +1,9 @@
> > +# SPDX-License-Identifier: GPL-2.0
> > +#
> > +# Makefile for the virtio-media device driver.
> > +
> > +virtio-media-objs := scatterlist_builder.o virtio_media_ioctls.o \
> > +			virtio_media_driver.o
> > +
> > +obj-$(CONFIG_MEDIA_VIRTIO) += virtio-media.o
> > +
> > diff --git a/drivers/media/virtio/protocol.h b/drivers/media/virtio/protocol.h
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..a22758cda5aabe75c5c94ce8d1b40583c8652710
> > --- /dev/null
> > +++ b/drivers/media/virtio/protocol.h
> > @@ -0,0 +1,288 @@
> > +/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+ */
> > +
> > +/*
> > + * Definitions of virtio-media protocol structures.
> > + *
> > + * Copyright (c) 2024-2025 Google LLC.
> > + */
> > +
> > +#ifndef __VIRTIO_MEDIA_PROTOCOL_H
> > +#define __VIRTIO_MEDIA_PROTOCOL_H
> > +
> > +#include <linux/videodev2.h>
> > +
> > +/*
> > + * Virtio protocol definition.
> > + */
> > +
> > +/**
> > + * struct virtio_media_cmd_header - Header for all virtio-media commands.
> > + * @cmd: one of VIRTIO_MEDIA_CMD_*.
> > + * @__reserved: must be set to zero by the driver.
> > + *
> > + * This header starts all commands from the driver to the device on the
> > + * commandq.
> > + */
> > +struct virtio_media_cmd_header {
> > +	u32 cmd;
> > +	u32 __reserved;
> > +};
> > +
> > +/**
> > + * struct virtio_media_resp_header - Header for all virtio-media responses.
> > + * @status: 0 if the command was successful, or one of the standard Linux error
> > + * codes.
> > + * @__reserved: must be set to zero by the device.
> > + *
> > + * This header starts all responses from the device to the driver on the
> > + * commandq.
> > + */
> > +struct virtio_media_resp_header {
> > +	u32 status;
> > +	u32 __reserved;
> > +};
> > +
> > +/**
> > + * VIRTIO_MEDIA_CMD_OPEN - Command for creating a new session.
> > + *
> > + * This is the equivalent of calling `open` on a V4L2 device node. Upon
> > + * success, a session id is returned which can be used to perform other
> > + * commands on the session, notably ioctls.
> > + */
> > +#define VIRTIO_MEDIA_CMD_OPEN 1
> > +
> > +/**
> > + * struct virtio_media_cmd_open - Driver command for VIRTIO_MEDIA_CMD_OPEN.
> > + * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_OPEN.
> > + */
> > +struct virtio_media_cmd_open {
> > +	struct virtio_media_cmd_header hdr;
> > +};
> > +
> > +/**
> > + * struct virtio_media_resp_open - Device response for VIRTIO_MEDIA_CMD_OPEN.
> > + * @hdr: header containing the status of the command.
> > + * @session_id: if hdr.status == 0, contains the id of the newly created session.
> > + * @__reserved: must be set to zero by the device.
> > + */
> > +struct virtio_media_resp_open {
> > +	struct virtio_media_resp_header hdr;
> > +	u32 session_id;
> > +	u32 __reserved;
> > +};
> > +
> > +/**
> > + * VIRTIO_MEDIA_CMD_CLOSE - Command for closing an active session.
> > + *
> > + * This is the equivalent of calling `close` on a previously opened V4L2
> > + * session. All resources associated with this session will be freed and the
> > + * session ID shall not be used again after queueing this command.
> > + *
> > + * This command does not require a response from the device.
> > + */
> > +#define VIRTIO_MEDIA_CMD_CLOSE 2
> > +
> > +/**
> > + * struct virtio_media_cmd_close - Driver command for VIRTIO_MEDIA_CMD_CLOSE.
> > + * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_CLOSE.
> > + * @session_id: id of the session to close.
> > + * @__reserved: must be set to zero by the driver.
> > + */
> > +struct virtio_media_cmd_close {
> > +	struct virtio_media_cmd_header hdr;
> > +	u32 session_id;
> > +	u32 __reserved;
> > +};
> > +
> > +/**
> > + * VIRTIO_MEDIA_CMD_IOCTL - Driver command for executing an ioctl.
> > + *
> > + * This command asks the device to run one of the `VIDIOC_*` ioctls on the
> > + * active session.
> > + *
> > + * The code of the ioctl is extracted from the VIDIOC_* definitions in
> > + * `videodev2.h`, and consists of the second argument of the `_IO*` macro.
> > + *
> > + * Each ioctl has a payload, which is defined by the third argument of the
> > + * `_IO*` macro defining it. It can be writable by the driver (`_IOW`), the
> > + * device (`_IOR`), or both (`_IOWR`).
> > + *
> > + * If an ioctl is writable by the driver, it must be followed by a
> > + * driver-writable descriptor containing the payload.
> > + *
> > + * If an ioctl is writable by the device, it must be followed by a
> > + * device-writable descriptor of the size of the payload that the device will
> > + * write into.
> > + *
> > + */
> > +#define VIRTIO_MEDIA_CMD_IOCTL 3
> > +
> > +/**
> > + * struct virtio_media_cmd_ioctl - Driver command for VIRTIO_MEDIA_CMD_IOCTL.
> > + * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_IOCTL.
> > + * @session_id: id of the session to run the ioctl on.
> > + * @code: code of the ioctl to run.
> > + */
> > +struct virtio_media_cmd_ioctl {
> > +	struct virtio_media_cmd_header hdr;
> > +	u32 session_id;
> > +	u32 code;
> > +};
> > +
> > +/**
> > + * struct virtio_media_resp_ioctl - Device response for VIRTIO_MEDIA_CMD_IOCTL.
> > + * @hdr: header containing the status of the ioctl.
> > + */
> > +struct virtio_media_resp_ioctl {
> > +	struct virtio_media_resp_header hdr;
> > +};
> > +
> > +/**
> > + * struct virtio_media_sg_entry - Description of part of a scattered guest memory.
> > + * @start: start guest address of the memory segment.
> > + * @len: length of this memory segment.
> > + * @__reserved: must be set to zero by the driver.
> > + */
> > +struct virtio_media_sg_entry {
> > +	u64 start;
> > +	u32 len;
> > +	u32 __reserved;
> > +};
> > +
> > +/**
> > + * enum virtio_media_memory - Memory types supported by virtio-media.
> > + * @VIRTIO_MEDIA_MMAP: memory allocated and managed by device. Can be mapped
> > + * into the guest using VIRTIO_MEDIA_CMD_MMAP.
> > + * @VIRTIO_MEDIA_SHARED_PAGES: memory allocated by the driver. Passed to the
> > + * device using virtio_media_sg_entry.
> > + * @VIRTIO_MEDIA_OBJECT: memory backed by a virtio object.
> > + */
> > +enum virtio_media_memory {
> > +	VIRTIO_MEDIA_MMAP = V4L2_MEMORY_MMAP,
> > +	VIRTIO_MEDIA_SHARED_PAGES = V4L2_MEMORY_USERPTR,
> > +	VIRTIO_MEDIA_OBJECT = V4L2_MEMORY_DMABUF,
> > +};
> > +
> > +#define VIRTIO_MEDIA_MMAP_FLAG_RW (1 << 0)
> > +
> > +/**
> > + * VIRTIO_MEDIA_CMD_MMAP - Command for mapping a MMAP buffer into the driver's
> > + * address space.
> > + *
> > + */
> > +#define VIRTIO_MEDIA_CMD_MMAP 4
> > +
> > +/**
> > + * struct virtio_media_cmd_mmap - Driver command for VIRTIO_MEDIA_CMD_MMAP.
> > + * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_MMAP.
> > + * @session_id: ID of the session we are mapping for.
> > + * @flags: combination of VIRTIO_MEDIA_MMAP_FLAG_*.
> > + * @offset: mem_offset field of the plane to map, as returned by VIDIOC_QUERYBUF.
> > + */
> > +struct virtio_media_cmd_mmap {
> > +	struct virtio_media_cmd_header hdr;
> > +	u32 session_id;
> > +	u32 flags;
> > +	u32 offset;
> > +};
> > +
> > +/**
> > + * struct virtio_media_resp_mmap - Device response for VIRTIO_MEDIA_CMD_MMAP.
> > + * @hdr: header containing the status of the command.
> > + * @driver_addr: offset into SHM region 0 of the start of the mapping.
> > + * @len: length of the mapping.
> > + */
> > +struct virtio_media_resp_mmap {
> > +	struct virtio_media_resp_header hdr;
> > +	u64 driver_addr;
> > +	u64 len;
> > +};
> > +
> > +/**
> > + * VIRTIO_MEDIA_CMD_MUNMAP - Unmap a MMAP buffer previously mapped using
> > + * VIRTIO_MEDIA_CMD_MMAP.
> > + */
> > +#define VIRTIO_MEDIA_CMD_MUNMAP 5
> > +
> > +/**
> > + * struct virtio_media_cmd_munmap - Driver command for VIRTIO_MEDIA_CMD_MUNMAP.
> > + * @hdr: header with cmd member set to VIRTIO_MEDIA_CMD_MUNMAP.
> > + * @driver_addr: offset into SHM region 0 at which the buffer has been previously
> > + * mapped.
> > + */
> > +struct virtio_media_cmd_munmap {
> > +	struct virtio_media_cmd_header hdr;
> > +	u64 driver_addr;
> > +};
> > +
> > +/**
> > + * struct virtio_media_resp_munmap - Device response for VIRTIO_MEDIA_CMD_MUNMAP.
> > + * @hdr: header containing the status of the command.
> > + */
> > +struct virtio_media_resp_munmap {
> > +	struct virtio_media_resp_header hdr;
> > +};
> > +
> > +#define VIRTIO_MEDIA_EVT_ERROR 0
> > +#define VIRTIO_MEDIA_EVT_DQBUF 1
> > +#define VIRTIO_MEDIA_EVT_EVENT 2
> > +
> > +/**
> > + * struct virtio_media_event_header - Header for events on the eventq.
> > + * @event: one of VIRTIO_MEDIA_EVT_*
> > + * @session_id: ID of the session the event applies to.
> > + */
> > +struct virtio_media_event_header {
> > +	u32 event;
> > +	u32 session_id;
> > +};
> > +
> > +/**
> > + * struct virtio_media_event_error - Unrecoverable device-side error.
> > + * @hdr: header for the event.
> > + * @errno: error code describing the kind of error that occurred.
> > + * @__reserved: must to set to zero by the device.
> > + *
> > + * Upon receiving this event, the session mentioned in the header is considered
> > + * corrupted and closed.
> > + *
> > + */
> > +struct virtio_media_event_error {
> > +	struct virtio_media_event_header hdr;
> > +	u32 errno;
> > +	u32 __reserved;
> > +};
> > +
> > +#define VIRTIO_MEDIA_MAX_PLANES VIDEO_MAX_PLANES
> > +
> > +/**
> > + * struct virtio_media_event_dqbuf - Dequeued buffer event.
> > + * @hdr: header for the event.
> > + * @buffer: struct v4l2_buffer describing the buffer that has been dequeued.
> > + * @planes: plane information for the dequeued buffer.
> > + *
> > + * This event is used to signal that a buffer is not being used anymore by the
> > + * device and is returned to the driver.
> > + */
> > +struct virtio_media_event_dqbuf {
> > +	struct virtio_media_event_header hdr;
> > +	struct v4l2_buffer buffer;
> > +	struct v4l2_plane planes[VIRTIO_MEDIA_MAX_PLANES];
> > +};
> > +
> > +/**
> > + * struct virtio_media_event_event - V4L2 event.
> > + * @hdr: header for the event.
> > + * @event: description of the event that occurred.
> > + *
> > + * This event signals that a V4L2 event has been emitted for a session.
> > + */
> > +struct virtio_media_event_event {
> > +	struct virtio_media_event_header hdr;
> > +	struct v4l2_event event;
> > +};
> > +
> > +/* Maximum size of an event. We will queue descriptors of this size on the eventq. */
> > +#define VIRTIO_MEDIA_EVENT_MAX_SIZE sizeof(struct virtio_media_event_dqbuf)
> > +
> > +#endif // __VIRTIO_MEDIA_PROTOCOL_H
> > diff --git a/drivers/media/virtio/scatterlist_builder.c b/drivers/media/virtio/scatterlist_builder.c
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..2837689f385e81c0c0a99ffd67ac583b426bf186
> > --- /dev/null
> > +++ b/drivers/media/virtio/scatterlist_builder.c
> > @@ -0,0 +1,563 @@
> > +// SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+
> > +
> > +/*
> > + * Scatterlist builder helpers for virtio-media.
> > + *
> > + * Copyright (c) 2024-2025 Google LLC.
> > + */
> > +
> > +#include <linux/moduleparam.h>
> > +#include <linux/scatterlist.h>
> > +#include <linux/videodev2.h>
> > +#include <media/videobuf2-memops.h>
> > +
> > +#include "protocol.h"
> > +#include "scatterlist_builder.h"
> > +#include "session.h"
> > +
> > +/*
> > + * If set to ``true``, then the driver will always copy the data passed to the
> > + * host into the shadow buffer (instead of trying to map the source memory into
> > + * the SG table directly when possible).
> > + */
> > +static bool always_use_shadow_buffer;
> > +module_param(always_use_shadow_buffer, bool, 0660);
> > +
> > +/* Convert a V4L2 IOCTL into the IOCTL code we can give to the host */
> > +#define VIRTIO_MEDIA_IOCTL_CODE(IOCTL) ((IOCTL >> _IOC_NRSHIFT) & _IOC_NRMASK)
> > +
> > +/**
> > + * scatterlist_builder_add_descriptor() - Add a descriptor to the chain.
> > + * @builder: builder to use.
> > + * @desc_index: index of the descriptor to add.
> > + *
> > + * Returns ``-ENOSPC`` if ``sgs`` is already full.
> > + */
> > +int scatterlist_builder_add_descriptor(struct scatterlist_builder *builder,
> > +				       size_t desc_index)
> > +{
> > +	if (builder->cur_sg >= builder->num_sgs)
> > +		return -ENOSPC;
> > +	builder->sgs[builder->cur_sg++] = &builder->descs[desc_index];
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * scatterlist_builder_add_data() - Append arbitrary data to the descriptor chain.
> > + * @builder: builder to use.
> > + * @data: pointer to the data to add to the descriptor chain.
> > + * @len: length of the data to add.
> > + *
> > + * @data will either be directly referenced, or copied into the shadow buffer
> > + * to be referenced from there.
> > + */
> > +int scatterlist_builder_add_data(struct scatterlist_builder *builder,
> > +				 void *data, size_t len)
> > +{
> > +	const size_t cur_desc = builder->cur_desc;
> > +
> > +	if (len == 0)
> > +		return 0;
> > +
> > +	if (builder->cur_desc >= builder->num_descs)
> > +		return -ENOSPC;
> > +
> > +	if (!always_use_shadow_buffer && virt_addr_valid(data + len)) {
> > +		/*
> > +		 * If "data" is in the 1:1 physical memory mapping then we can
> > +		 * use a single SG entry and avoid copying.
> > +		 */
> > +		struct page *page = virt_to_page(data);
> > +		size_t offset = (((size_t)data) & ~PAGE_MASK);
> > +		struct scatterlist *next_desc =
> > +			&builder->descs[builder->cur_desc];
> > +
> > +		memset(next_desc, 0, sizeof(*next_desc));
> > +		sg_set_page(next_desc, page, len, offset);
> > +		builder->cur_desc++;
> > +	} else if (!always_use_shadow_buffer && is_vmalloc_addr(data)) {
> > +		int prev_pfn = -2;
> > +
> > +		/*
> > +		 * If "data" has been vmalloc'ed, we need at most one entry per
> > +		 * memory page but can avoid copying.
> > +		 */
> > +		while (len > 0) {
> > +			struct page *page = vmalloc_to_page(data);
> > +			int cur_pfn = page_to_pfn(page);
> > +			/* All pages but the first will start at offset 0. */
> > +			unsigned long offset =
> > +				(((unsigned long)data) & ~PAGE_MASK);
> > +			size_t len_in_page = min(PAGE_SIZE - offset, len);
> > +			struct scatterlist *next_desc =
> > +				&builder->descs[builder->cur_desc];
> > +
> > +			if (builder->cur_desc >= builder->num_descs)
> > +				return -ENOSPC;
> > +
> > +			/* Optimize contiguous pages */
> > +			if (cur_pfn == prev_pfn + 1) {
> > +				(next_desc - 1)->length += len_in_page;
> > +			} else {
> > +				memset(next_desc, 0, sizeof(*next_desc));
> > +				sg_set_page(next_desc, page, len_in_page,
> > +					    offset);
> > +				builder->cur_desc++;
> > +			}
> > +			data += len_in_page;
> > +			len -= len_in_page;
> > +			prev_pfn = cur_pfn;
> > +		}
> > +	} else {
> > +		/*
> > +		 * As a last resort, copy into the shadow buffer and reference
> > +		 * it with a single SG entry. Calling
> > +		 * `scatterlist_builder_retrieve_data` will be necessary to copy
> > +		 * the data written by the device back into @data.
> > +		 */
> > +		void *shadow_buffer =
> > +			builder->shadow_buffer + builder->shadow_buffer_pos;
> > +		struct page *page = virt_to_page(shadow_buffer);
> > +		unsigned long offset =
> > +			(((unsigned long)shadow_buffer) & ~PAGE_MASK);
> > +		struct scatterlist *next_desc =
> > +			&builder->descs[builder->cur_desc];
> > +
> > +		if (len >
> > +		    builder->shadow_buffer_size - builder->shadow_buffer_pos)
> > +			return -ENOSPC;
> > +
> > +		memcpy(shadow_buffer, data, len);
> > +		memset(next_desc, 0, sizeof(*next_desc));
> > +		sg_set_page(next_desc, page, len, offset);
> > +		builder->cur_desc++;
> > +		builder->shadow_buffer_pos += len;
> > +	}
> > +
> > +	sg_mark_end(&builder->descs[builder->cur_desc - 1]);
> > +	return scatterlist_builder_add_descriptor(builder, cur_desc);
> > +}
> > +
> > +/**
> > + * scatterlist_builder_retrieve_data() - Retrieve a response written by the
> > + * device on the shadow buffer.
> > + * @builder: builder to use.
> > + * @sg_index: index of the descriptor to read from.
> > + * @data: destination for the shadowed data.
> > + *
> > + * If the shadow buffer is pointed to by the descriptor at index @sg_index of
> > + * the chain, then ``sg->length`` bytes are copied back from it into @data.
> > + * Otherwise nothing is done since the device has written into @data directly.
> > + *
> > + * @data must have originally been added by ``scatterlist_builder_add_data`` as
> > + * the same size as passed to ``scatterlist_builder_add_data`` will be copied
> > + * back.
> > + */
> > +int scatterlist_builder_retrieve_data(struct scatterlist_builder *builder,
> > +				      size_t sg_index, void *data)
> > +{
> > +	void *shadow_buf = builder->shadow_buffer;
> > +	struct scatterlist *sg;
> > +	void *kaddr;
> > +
> > +	/* We can only retrieve from the range of sgs currently set. */
> > +	if (sg_index >= builder->cur_sg)
> > +		return -ERANGE;
> > +
> > +	sg = builder->sgs[sg_index];
> > +	kaddr = pfn_to_kaddr(page_to_pfn(sg_page(sg))) + sg->offset;
> > +
> > +	if (kaddr >= shadow_buf &&
> > +	    kaddr < shadow_buf + VIRTIO_SHADOW_BUF_SIZE) {
> > +		if (kaddr + sg->length >= shadow_buf + VIRTIO_SHADOW_BUF_SIZE)
> > +			return -EINVAL;
> > +
> > +		memcpy(data, kaddr, sg->length);
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * scatterlist_builder_add_ioctl_cmd() - Add an ioctl command to the descriptor
> > + * chain.
> > + * @builder: builder to use.
> > + * @session: session on behalf of which the ioctl command is added.
> > + * @ioctl_code: code of the ioctl to add (i.e. ``VIDIOC_*``).
> > + */
> > +int scatterlist_builder_add_ioctl_cmd(struct scatterlist_builder *builder,
> > +				      struct virtio_media_session *session,
> > +				      u32 ioctl_code)
> > +{
> > +	struct virtio_media_cmd_ioctl *cmd_ioctl = &session->cmd.ioctl;
> > +
> > +	cmd_ioctl->hdr.cmd = VIRTIO_MEDIA_CMD_IOCTL;
> > +	cmd_ioctl->session_id = session->id;
> > +	cmd_ioctl->code = VIRTIO_MEDIA_IOCTL_CODE(ioctl_code);
> > +
> > +	return scatterlist_builder_add_data(builder, cmd_ioctl,
> > +					    sizeof(*cmd_ioctl));
> > +}
> > +
> > +/**
> > + * scatterlist_builder_add_ioctl_resp() - Add storage to receive an ioctl
> > + * response to the descriptor chain.
> > + * @builder: builder to use.
> > + * @session: session on behalf of which the ioctl response is added.
> > + */
> > +int scatterlist_builder_add_ioctl_resp(struct scatterlist_builder *builder,
> > +				       struct virtio_media_session *session)
> > +{
> > +	struct virtio_media_resp_ioctl *resp_ioctl = &session->resp.ioctl;
> > +
> > +	return scatterlist_builder_add_data(builder, resp_ioctl,
> > +					    sizeof(*resp_ioctl));
> > +}
> > +
> > +/**
> > + * __scatterlist_builder_add_userptr() - Add user pages to @builder.
> > + * @builder: builder to use.
> > + * @userptr: pointer to userspace memory that we want to add.
> > + * @length: length of the data to add.
> > + * @sg_list: output parameter. Upon success, points to the area of the shadow
> > + * buffer containing the array of SG entries to be added to the descriptor
> > + * chain.
> > + * @nents: output parameter. Upon success, contains the number of entries
> > + * pointed to by @sg_list.
> > + *
> > + * Data referenced by userspace pointers can be potentially large and very
> > + * scattered, which could overwhelm the descriptor chain if added as-is. For
> > + * these, we instead build an array of ``struct virtio_media_sg_entry`` in the
> > + * shadow buffer and reference it using a single descriptor.
> > + *
> > + * This function is a helper to perform that. Callers should then add the
> > + * descriptor to the chain properly.
> > + *
> > + * Returns -EFAULT if @userptr is not a valid user address, which is a case the
> > + * driver should consider as "normal" operation. All other failures signal a
> > + * problem with the driver.
> > + */
> > +static int
> > +__scatterlist_builder_add_userptr(struct scatterlist_builder *builder,
> > +				  unsigned long userptr, unsigned long length,
> > +				  struct virtio_media_sg_entry **sg_list,
> > +				  int *nents)
> > +{
> > +	struct sg_table sg_table = {};
> > +	struct frame_vector *framevec;
> > +	struct scatterlist *sg_iter;
> > +	struct page **pages;
> > +	const unsigned int offset = userptr & ~PAGE_MASK;
> > +	unsigned int pages_count;
> > +	size_t entries_size;
> > +	int i;
> > +	int ret;
> > +
> > +	framevec = vb2_create_framevec(userptr, length, true);
> > +	if (IS_ERR(framevec)) {
> > +		if (PTR_ERR(framevec) != -EFAULT) {
> > +			pr_warn("error %ld creating frame vector for userptr 0x%lx, length 0x%lx\n",
> > +				PTR_ERR(framevec), userptr, length);
> > +		} else {
> > +			/* -EINVAL is expected in case of invalid userptr. */
> > +			framevec = ERR_PTR(-EINVAL);
> > +		}
> > +		return PTR_ERR(framevec);
> > +	}
> > +
> > +	pages = frame_vector_pages(framevec);
> > +	if (IS_ERR(pages)) {
> > +		pr_warn("error getting vector pages\n");
> > +		ret = PTR_ERR(pages);
> > +		goto done;
> > +	}
> > +	pages_count = frame_vector_count(framevec);
> > +	ret = sg_alloc_table_from_pages(&sg_table, pages, pages_count, offset,
> > +					length, 0);
> > +	if (ret) {
> > +		pr_warn("error creating sg table\n");
> > +		goto done;
> > +	}
> > +
> > +	/* Allocate our actual SG in the shadow buffer. */
> > +	*nents = sg_nents(sg_table.sgl);
> > +	entries_size = sizeof(**sg_list) * *nents;
> > +	if (builder->shadow_buffer_pos + entries_size >
> > +	    builder->shadow_buffer_size) {
> > +		ret = -ENOMEM;
> > +		goto free_sg;
> > +	}
> > +
> > +	*sg_list = builder->shadow_buffer + builder->shadow_buffer_pos;
> > +	builder->shadow_buffer_pos += entries_size;
> > +
> > +	for_each_sgtable_sg(&sg_table, sg_iter, i) {
> > +		struct virtio_media_sg_entry *sg_entry = &(*sg_list)[i];
> > +
> > +		sg_entry->start = sg_phys(sg_iter);
> > +		sg_entry->len = sg_iter->length;
> > +	}
> > +
> > +free_sg:
> > +	sg_free_table(&sg_table);
> > +
> > +done:
> > +	vb2_destroy_framevec(framevec);
> > +	return ret;
> > +}
> > +
> > +/**
> > + * scatterlist_builder_add_userptr() - Add a user-memory buffer using an array
> > + * of ``struct virtio_media_sg_entry``.
> > + * @builder: builder to use.
> > + * @userptr: pointer to userspace memory that we want to add.
> > + * @length: length of the data to add.
> > + *
> > + * Upon success, an array of ``struct virtio_media_sg_entry`` referencing
> > + * @userptr has been built into the shadow buffer, and that array added to the
> > + * descriptor chain.
> > + */
> > +static int scatterlist_builder_add_userptr(struct scatterlist_builder *builder,
> > +					   unsigned long userptr,
> > +					   unsigned long length)
> > +{
> > +	int ret;
> > +	int nents;
> > +	struct virtio_media_sg_entry *sg_list;
> > +
> > +	ret = __scatterlist_builder_add_userptr(builder, userptr, length,
> > +						&sg_list, &nents);
> > +	if (ret)
> > +		return ret;
> > +
> > +	ret = scatterlist_builder_add_data(builder, sg_list,
> > +					   sizeof(*sg_list) * nents);
> > +	if (ret)
> > +		return ret;
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * scatterlist_builder_add_buffer() - Add a ``v4l2_buffer`` and its planes to
> > + * the descriptor chain.
> > + * @builder: builder to use.
> > + * @b: ``v4l2_buffer`` to add.
> > + */
> > +int scatterlist_builder_add_buffer(struct scatterlist_builder *builder,
> > +				   struct v4l2_buffer *b)
> > +{
> > +	int i;
> > +	int ret;
> > +
> > +	/* Fixup: plane length must be zero if userptr is NULL */
> > +	if (!V4L2_TYPE_IS_MULTIPLANAR(b->type) &&
> > +	    b->memory == V4L2_MEMORY_USERPTR && b->m.userptr == 0)
> > +		b->length = 0;
> > +
> > +	/* v4l2_buffer */
> > +	ret = scatterlist_builder_add_data(builder, b, sizeof(*b));
> > +	if (ret)
> > +		return ret;
> > +
> > +	if (V4L2_TYPE_IS_MULTIPLANAR(b->type) && b->length > 0) {
> > +		/* Fixup: plane length must be zero if userptr is NULL */
> > +		if (b->memory == V4L2_MEMORY_USERPTR) {
> > +			for (i = 0; i < b->length; i++) {
> > +				struct v4l2_plane *plane = &b->m.planes[i];
> > +
> > +				if (plane->m.userptr == 0)
> > +					plane->length = 0;
> > +			}
> > +		}
> > +
> > +		/* Array of v4l2_planes */
> > +		ret = scatterlist_builder_add_data(builder, b->m.planes,
> > +						   sizeof(struct v4l2_plane) *
> > +							   b->length);
> > +		if (ret)
> > +			return ret;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * scatterlist_builder_add_buffer_userptr() - Add the payload of a ``USERTPR``
> > + * v4l2_buffer to the descriptor chain.
> > + * @builder: builder to use.
> > + * @b: ``v4l2_buffer`` which ``USERPTR`` payload we want to add.
> > + *
> > + * Add an array of ``virtio_media_sg_entry`` pointing to a ``USERPTR`` buffer's
> > + * contents. Does nothing if the buffer is not of type ``USERPTR``. This is
> > + * split out of :ref:`scatterlist_builder_add_buffer` because we only want to
> > + * add these to the device-readable part of the descriptor chain.
> > + */
> > +int scatterlist_builder_add_buffer_userptr(struct scatterlist_builder *builder,
> > +					   struct v4l2_buffer *b)
> > +{
> > +	int i;
> > +	int ret;
> > +
> > +	if (b->memory != V4L2_MEMORY_USERPTR)
> > +		return 0;
> > +
> > +	if (V4L2_TYPE_IS_MULTIPLANAR(b->type)) {
> > +		for (i = 0; i < b->length; i++) {
> > +			struct v4l2_plane *plane = &b->m.planes[i];
> > +
> > +			if (b->memory == V4L2_MEMORY_USERPTR &&
> > +			    plane->length > 0) {
> > +				ret = scatterlist_builder_add_userptr(
> > +					builder, plane->m.userptr,
> > +					plane->length);
> > +				if (ret)
> > +					return ret;
> > +			}
> > +		}
> > +	} else if (b->length > 0) {
> > +		ret = scatterlist_builder_add_userptr(builder, b->m.userptr,
> > +						      b->length);
> > +		if (ret)
> > +			return ret;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * scatterlist_builder_retrieve_buffer() - Retrieve a v4l2_buffer written by
> > + * the device on the shadow buffer, if needed.
> > + * @builder: builder to use.
> > + * @sg_index: index of the first SG entry of the buffer in the builder's
> > + * descriptor chain.
> > + * @b: v4l2_buffer to copy shadow buffer data into.
> > + * @orig_planes: the original ``planes`` pointer, to be restored if the buffer
> > + * is multi-planar.
> > + *
> > + * If the v4l2_buffer pointed by @buffer_sgs was copied into the shadow buffer,
> > + * then its updated content is copied back into @b. Otherwise nothing is done
> > + * as the device has written into @b directly.
> > + *
> > + * @orig_planes is used to restore the original ``planes`` pointer in case it
> > + * gets modified by the host. The specification stipulates that the host should
> > + * not modify it, but we enforce this for additional safety.
> > + */
> > +int scatterlist_builder_retrieve_buffer(struct scatterlist_builder *builder,
> > +					size_t sg_index, struct v4l2_buffer *b,
> > +					struct v4l2_plane *orig_planes)
> > +{
> > +	int ret;
> > +
> > +	ret = scatterlist_builder_retrieve_data(builder, sg_index++, b);
> > +	if (ret)
> > +		return ret;
> > +
> > +	if (V4L2_TYPE_IS_MULTIPLANAR(b->type)) {
> > +		b->m.planes = orig_planes;
> > +
> > +		if (orig_planes != NULL) {
> > +			ret = scatterlist_builder_retrieve_data(
> > +				builder, sg_index++, b->m.planes);
> > +			if (ret)
> > +				return ret;
> > +		}
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * scatterlist_builder_add_ext_ctrls() - Add a v4l2_ext_controls and its
> > + * controls to @builder.
> > + * @builder: builder to use.
> > + * @ctrls: ``struct v4l2_ext_controls`` to add.
> > + *
> > + * Add @ctrls and its array of `struct v4l2_ext_control` to the descriptor chain.
> > + */
> > +int scatterlist_builder_add_ext_ctrls(struct scatterlist_builder *builder,
> > +				      struct v4l2_ext_controls *ctrls)
> > +{
> > +	int ret;
> > +
> > +	/* v4l2_ext_controls */
> > +	ret = scatterlist_builder_add_data(builder, ctrls, sizeof(*ctrls));
> > +	if (ret)
> > +		return ret;
> > +
> > +	if (ctrls->count > 0) {
> > +		/* array of v4l2_controls */
> > +		ret = scatterlist_builder_add_data(builder, ctrls->controls,
> > +						   sizeof(ctrls->controls[0]) *
> > +							   ctrls->count);
> > +		if (ret)
> > +			return ret;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * scatterlist_builder_add_ext_ctrls_userptrs() - Add the userspace payloads of
> > + * a ``struct v4l2_ext_controls`` to the descriptor chain.
> > + * @builder: builder to use.
> > + * @ctrls: ``struct v4l2_ext_controls`` from which we want to add the userspace payload of.
> > + *
> > + * Add the userspace payloads of @ctrls to the descriptor chain. This is split
> > + * out of :ref:`scatterlist_builder_add_ext_ctrls` because we only want to add
> > + * these to the device-readable part of the descriptor chain.
> > + */
> > +int scatterlist_builder_add_ext_ctrls_userptrs(
> > +	struct scatterlist_builder *builder, struct v4l2_ext_controls *ctrls)
> > +{
> > +	int i;
> > +	int ret;
> > +
> > +	/* Pointers to user memory in individual controls */
> > +	for (i = 0; i < ctrls->count; i++) {
> > +		struct v4l2_ext_control *ctrl = &ctrls->controls[i];
> > +
> > +		if (ctrl->size > 0) {
> > +			ret = scatterlist_builder_add_userptr(
> > +				builder, (unsigned long)ctrl->ptr, ctrl->size);
> > +			if (ret)
> > +				return ret;
> > +		}
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * scatterlist_builder_retrieve_ext_ctrls() - Retrieve controls written by the
> > + * device on the shadow buffer, if needed.
> > + * @builder: builder to use.
> > + * @sg_index: index of the first SG entry of the controls in the builder's
> > + * descriptor chain.
> > + * @ctrls: ``struct v4l2_ext_controls`` to copy shadow buffer data into.
> > + *
> > + * If the shadow buffer is pointed to by @sg, copy its content back into @ctrls.
> > + */
> > +int scatterlist_builder_retrieve_ext_ctrls(struct scatterlist_builder *builder,
> > +					   size_t sg_index,
> > +					   struct v4l2_ext_controls *ctrls)
> > +{
> > +	struct v4l2_ext_control *controls_backup = ctrls->controls;
> > +	int ret;
> > +
> > +	ret = scatterlist_builder_retrieve_data(builder, sg_index++, ctrls);
> > +	if (ret)
> > +		return ret;
> > +
> > +	ctrls->controls = controls_backup;
> > +
> > +	if (ctrls->count > 0 && ctrls->controls) {
> > +		ret = scatterlist_builder_retrieve_data(builder, sg_index++,
> > +							ctrls->controls);
> > +		if (ret)
> > +			return ret;
> > +	}
> > +
> > +	return 0;
> > +}
> > diff --git a/drivers/media/virtio/scatterlist_builder.h b/drivers/media/virtio/scatterlist_builder.h
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..c8323c31ac21953580a0b4a4cb366841e510666f
> > --- /dev/null
> > +++ b/drivers/media/virtio/scatterlist_builder.h
> > @@ -0,0 +1,111 @@
> > +/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+ */
> > +
> > +/*
> > + * Scatterlist builder helpers for virtio-media.
> > + *
> > + * Copyright (c) 2024-2025 Google LLC.
> > + */
> > +
> > +#ifndef __VIRTIO_MEDIA_SCATTERLIST_BUILDER_H
> > +#define __VIRTIO_MEDIA_SCATTERLIST_BUILDER_H
> > +
> > +#include <linux/scatterlist.h>
> > +
> > +#include "session.h"
> > +
> > +/**
> > + * struct scatterlist_builder - helper to build a scatterlist from data.
> > + * @descs: pool of descriptors to use.
> > + * @num_descs: number of entries in descs.
> > + * @cur_desc: next descriptor to be used in @descs.
> > + * @shadow_buffer: pointer to a shadow buffer where elements that cannot be
> > + * mapped directly into the scatterlist get copied.
> > + * @shadow_buffer_size: size of @shadow_buffer.
> > + * @shadow_buffer_pos: current position in @shadow_buffer.
> > + * @sgs: descriptor chain to eventually pass to virtio functions.
> > + * @num_sgs: total number of entries in @sgs.
> > + * @cur_sg: next entry in @sgs to be used.
> > + *
> > + * Virtio passes data from the driver to the device (through e.g.
> > + * ``virtqueue_add_sgs``) via a scatterlist that the device interprets as a
> > + * linear view over scattered driver memory.
> > + *
> > + * In virtio-media, the payload of ioctls from user-space can for the most part
> > + * be passed as-is, or after slight modification, which makes it tempting to
> > + * just forward the ioctl payload received from user-space as-is instead of
> > + * doing another copy into a dedicated buffer. This structure helps with this.
> > + *
> > + * virtio-media descriptor chains are typically made of the following parts:
> > + *
> > + * Device-readable:
> > + * - A command structure, i.e. ``virtio_media_cmd_*``,
> > + * - An ioctl payload (one of the regular ioctl parameters),
> > + * - (optionally) arrays of ``virtio_media_sg_entry`` describing the content of
> > + *   buffers in guest memory.
> > + *
> > + * Device-writable:
> > + * - A response structure, i.e. ``virtio_media_resp_*``,
> > + * - An ioctl payload, that the device will write to.
> > + *
> > + * This structure helps laying out the descriptor chain into its @sgs member in
> > + * an optimal way, by building a scatterlist adapted to the originating memory
> > + * of the data we want to pass to the device while avoiding copies when
> > + * possible.
> > + *
> > + * It is made of a pool of ``struct scatterlist`` (@descs) that is used to
> > + * build the final descriptor chain @sgs, and a @shadow_buffer where data that
> > + * cannot (or should not) be mapped directly by the host can be temporarily
> > + * copied.
> > + */
> > +struct scatterlist_builder {
> > +	struct scatterlist *descs;
> > +	size_t num_descs;
> > +	size_t cur_desc;
> > +
> > +	void *shadow_buffer;
> > +	size_t shadow_buffer_size;
> > +	size_t shadow_buffer_pos;
> > +
> > +	struct scatterlist **sgs;
> > +	size_t num_sgs;
> > +	size_t cur_sg;
> > +};
> > +
> > +int scatterlist_builder_add_descriptor(struct scatterlist_builder *builder,
> > +				       size_t desc_index);
> > +
> > +int scatterlist_builder_add_data(struct scatterlist_builder *builder,
> > +				 void *data, size_t len);
> > +
> > +int scatterlist_builder_retrieve_data(struct scatterlist_builder *builder,
> > +				      size_t sg_index, void *data);
> > +
> > +int scatterlist_builder_add_ioctl_cmd(struct scatterlist_builder *builder,
> > +				      struct virtio_media_session *session,
> > +				      u32 ioctl_code);
> > +
> > +int scatterlist_builder_add_ioctl_resp(struct scatterlist_builder *builder,
> > +				       struct virtio_media_session *session);
> > +
> > +int scatterlist_builder_add_buffer(struct scatterlist_builder *builder,
> > +				   struct v4l2_buffer *buffer);
> > +
> > +int scatterlist_builder_add_buffer_userptr(struct scatterlist_builder *builder,
> > +					   struct v4l2_buffer *b);
> > +
> > +int scatterlist_builder_retrieve_buffer(struct scatterlist_builder *builder,
> > +					size_t sg_index,
> > +					struct v4l2_buffer *buffer,
> > +					struct v4l2_plane *orig_planes);
> > +
> > +int scatterlist_builder_add_ext_ctrls(struct scatterlist_builder *builder,
> > +				      struct v4l2_ext_controls *ctrls);
> > +
> > +int scatterlist_builder_add_ext_ctrls_userptrs(
> > +	struct scatterlist_builder *builder, struct v4l2_ext_controls *ctrls);
> > +
> > +int scatterlist_builder_retrieve_ext_ctrls(struct scatterlist_builder *builder,
> > +					   size_t sg_index,
> > +					   struct v4l2_ext_controls *ctrls);
> > +
> > +#endif // __VIRTIO_MEDIA_SCATTERLIST_BUILDER_H
> > diff --git a/drivers/media/virtio/session.h b/drivers/media/virtio/session.h
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..b643d0d950477d56d4bb5db481818a3912af5c1f
> > --- /dev/null
> > +++ b/drivers/media/virtio/session.h
> > @@ -0,0 +1,109 @@
> > +/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+ */
> > +
> > +/*
> > + * Definitions of virtio-media session related structures.
> > + *
> > + * Copyright (c) 2024-2025 Google LLC.
> > + */
> > +
> > +#ifndef __VIRTIO_MEDIA_SESSION_H
> > +#define __VIRTIO_MEDIA_SESSION_H
> > +
> > +#include <linux/scatterlist.h>
> > +#include <media/v4l2-fh.h>
> > +
> > +#include "protocol.h"
> > +
> > +#define VIRTIO_MEDIA_LAST_QUEUE (V4L2_BUF_TYPE_META_OUTPUT)
> > +
> > +/*
> > + * Size of the per-session virtio shadow and event buffers. 16K should be
> > + * enough to contain everything we need.
> > + */
> > +#define VIRTIO_SHADOW_BUF_SIZE 0x4000
> > +
> > +/**
> > + * struct virtio_media_buffer - Current state of a buffer.
> > + * @buffer: ``struct v4l2_buffer`` with current information about the buffer.
> > + * @planes: backing planes array for @buffer.
> > + * @list: link into the list of buffers pending dequeue.
> > + */
> > +struct virtio_media_buffer {
> > +	struct v4l2_buffer buffer;
> > +	struct v4l2_plane planes[VIDEO_MAX_PLANES];
> > +	struct list_head list;
> > +};
> > +
> > +/**
> > + * struct virtio_media_queue_state - Represents the state of a V4L2 queue.
> > + * @streaming: Whether the queue is currently streaming.
> > + * @allocated_bufs: How many buffers are currently allocated.
> > + * @is_capture_last: set to true when the last buffer has been received on a
> > + * capture queue, so we can return -EPIPE on subsequent DQBUF requests.
> > + * @buffers: Buffer state array of size @allocated_bufs.
> > + * @queued_bufs: How many buffers are currently queued on the device.
> > + * @pending_dqbufs: Buffers that are available for being dequeued.
> > + */
> > +struct virtio_media_queue_state {
> > +	bool streaming;
> > +	size_t allocated_bufs;
> > +	bool is_capture_last;
> > +
> > +	struct virtio_media_buffer *buffers;
> > +	size_t queued_bufs;
> > +	struct list_head pending_dqbufs;
> > +};
> > +
> > +/**
> > + * struct virtio_media_session - A session on a virtio_media device.
> > + * @fh: file handler for the session.
> > + * @id: session ID used to communicate with the device.
> > + * @nonblocking_dequeue: whether dequeue should block or not (nonblocking if
> > + * file opened with O_NONBLOCK).
> > + * @uses_mplane: whether the queues for this session use the MPLANE API or not.
> > + * @cmd: union of session-related commands. A session can have one command currently running.
> > + * @resp: union of session-related responses. A session can wait on one command only.
> > + * @shadow_buf: shadow buffer where data to be added to the descriptor chain can
> > + * be staged before being sent to the device.
> > + * @command_sgs: SG table gathering descriptors for a given command and its response.
> > + * @queues: state of all the queues for this session.
> > + * @queues_lock: protects all members fo the queues for this session.
> > + * virtio_media_queue_state`.
> > + * @dqbuf_wait: waitqueue for dequeued buffers, if ``VIDIOC_DQBUF`` needs to
> > + * block or when polling.
> > + * @list: link into the list of sessions for the device.
> > + */
> > +struct virtio_media_session {
> > +	struct v4l2_fh fh;
> > +	u32 id;
> > +	bool nonblocking_dequeue;
> > +	bool uses_mplane;
> > +
> > +	union {
> > +		struct virtio_media_cmd_close close;
> > +		struct virtio_media_cmd_ioctl ioctl;
> > +		struct virtio_media_cmd_mmap mmap;
> > +	} cmd;
> > +
> > +	union {
> > +		struct virtio_media_resp_ioctl ioctl;
> > +		struct virtio_media_resp_mmap mmap;
> > +	} resp;
> > +
> > +	void *shadow_buf;
> > +
> > +	struct sg_table command_sgs;
> > +
> > +	struct virtio_media_queue_state queues[VIRTIO_MEDIA_LAST_QUEUE + 1];
> > +	struct mutex queues_lock;
> > +	wait_queue_head_t dqbuf_wait;
> > +
> > +	struct list_head list;
> > +};
> > +
> > +static inline struct virtio_media_session *fh_to_session(struct v4l2_fh *fh)
> > +{
> > +	return container_of(fh, struct virtio_media_session, fh);
> > +}
> > +
> > +#endif // __VIRTIO_MEDIA_SESSION_H
> > diff --git a/drivers/media/virtio/virtio_media.h b/drivers/media/virtio/virtio_media.h
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..0aa503defdd6a08e12335276f7ccbabc3d53df09
> > --- /dev/null
> > +++ b/drivers/media/virtio/virtio_media.h
> > @@ -0,0 +1,93 @@
> > +/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+ */
> > +
> > +/*
> > + * Virtio-media structures & functions declarations.
> > + *
> > + * Copyright (c) 2024-2025 Google LLC.
> > + */
> > +
> > +#ifndef __VIRTIO_MEDIA_H
> > +#define __VIRTIO_MEDIA_H
> > +
> > +#include <linux/virtio_config.h>
> > +#include <media/v4l2-device.h>
> > +
> > +#include "protocol.h"
> > +
> > +#define DESC_CHAIN_MAX_LEN SG_MAX_SINGLE_ALLOC
> > +
> > +#define VIRTIO_MEDIA_DEFAULT_DRIVER_NAME "virtio-media"
> > +
> > +extern char *virtio_media_driver_name;
> > +extern bool virtio_media_allow_userptr;
> > +
> > +/**
> > + * struct virtio_media - Virtio-media device.
> > + * @v4l2_dev: v4l2_device for the media device.
> > + * @video_dev: video_device for the media device.
> > + * @virtio_dev: virtio device for the media device.
> > + * @commandq: virtio command queue.
> > + * @eventq: virtio event queue.
> > + * @eventq_work: work to run when events are received on @eventq.
> > + * @mmap_region: region into which MMAP buffers are mapped by the host.
> > + * @event_buffer: buffer for event descriptors.
> > + * @sessions: list of active sessions on the device.
> > + * @sessions_lock: protects @sessions and ``virtio_media_session::list``.
> > + * @events_lock: prevents concurrent processing of events.
> > + * @cmd: union of device-related commands.
> > + * @resp: union of device-related responses.
> > + * @vlock: serializes access to the command queue.
> > + * @wq: waitqueue for host responses on the command queue.
> > + */
> > +struct virtio_media {
> > +	struct v4l2_device v4l2_dev;
> > +	struct video_device video_dev;
> > +
> > +	struct virtio_device *virtio_dev;
> > +	struct virtqueue *commandq;
> > +	struct virtqueue *eventq;
> > +	struct work_struct eventq_work;
> > +
> > +	struct virtio_shm_region mmap_region;
> > +
> > +	void *event_buffer;
> > +
> > +	struct list_head sessions;
> > +	struct mutex sessions_lock;
> > +
> > +	struct mutex events_lock;
> > +
> > +	union {
> > +		struct virtio_media_cmd_open open;
> > +		struct virtio_media_cmd_munmap munmap;
> > +	} cmd;
> > +
> > +	union {
> > +		struct virtio_media_resp_open open;
> > +		struct virtio_media_resp_munmap munmap;
> > +	} resp;
> > +
> > +	struct mutex vlock;
> > +	wait_queue_head_t wq;
> > +};
> > +
> > +static inline struct virtio_media *
> > +to_virtio_media(struct video_device *video_dev)
> > +{
> > +	return container_of(video_dev, struct virtio_media, video_dev);
> > +}
> > +
> > +/* virtio_media_driver.c */
> > +
> > +int virtio_media_send_command(struct virtio_media *vv, struct scatterlist **sgs,
> > +			      const size_t out_sgs, const size_t in_sgs,
> > +			      size_t minimum_resp_len, size_t *resp_len);
> > +void virtio_media_process_events(struct virtio_media *vv);
> > +
> > +/* virtio_media_ioctls.c */
> > +
> > +long virtio_media_device_ioctl(struct file *file, unsigned int cmd,
> > +			       unsigned long arg);
> > +extern const struct v4l2_ioctl_ops virtio_media_ioctl_ops;
> > +
> > +#endif // __VIRTIO_MEDIA_H
> > diff --git a/drivers/media/virtio/virtio_media_driver.c b/drivers/media/virtio/virtio_media_driver.c
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..e8d6dc453f2240c7809152c2a04813120bd3aca2
> > --- /dev/null
> > +++ b/drivers/media/virtio/virtio_media_driver.c
> > @@ -0,0 +1,959 @@
> > +// SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+
> > +
> > +/*
> > + * Virtio-media driver.
> > + *
> > + * Copyright (c) 2024-2025 Google LLC.
> > + */
> > +
> > +#include <linux/delay.h>
> > +#include <linux/device.h>
> > +#include <linux/dev_printk.h>
> > +#include <linux/mm.h>
> > +#include <linux/mutex.h>
> > +#include <linux/scatterlist.h>
> > +#include <linux/types.h>
> > +#include <linux/videodev2.h>
> > +#include <linux/vmalloc.h>
> > +#include <linux/wait.h>
> > +#include <linux/workqueue.h>
> > +#include <linux/module.h>
> > +#include <linux/moduleparam.h>
> > +#include <linux/virtio.h>
> > +#include <linux/virtio_config.h>
> > +#include <linux/virtio_ids.h>
> > +
> > +#include <media/frame_vector.h>
> > +#include <media/v4l2-dev.h>
> > +#include <media/v4l2-event.h>
> > +#include <media/videobuf2-memops.h>
> > +#include <media/v4l2-device.h>
> > +#include <media/v4l2-ioctl.h>
> > +
> > +#include "protocol.h"
> > +#include "session.h"
> > +#include "virtio_media.h"
> > +
> > +#define VIRTIO_MEDIA_NUM_EVENT_BUFS 16
> > +
> > +/* ID of the SHM region into which MMAP buffer will be mapped. */
> > +#define VIRTIO_MEDIA_SHM_MMAP 0
> > +
> > +/*
> > + * Name of the driver to expose to user-space.
> > + *
> > + * This is configurable because v4l2-compliance has workarounds specific to
> > + * some drivers. When proxying these directly from the host, this allows it to
> > + * apply them as needed.
> > + */
> > +char *virtio_media_driver_name;
> > +module_param_named(driver_name, virtio_media_driver_name, charp, 0660);
> > +
> > +/*
> > + * Whether USERPTR buffers are allowed.
> > + *
> > + * This is disabled by default as USERPTR buffers are dangerous, but the option
> > + * is left to enable them if desired.
> > + */
> > +bool virtio_media_allow_userptr;
> > +module_param_named(allow_userptr, virtio_media_allow_userptr, bool, 0660);
> > +
> > +/**
> > + * virtio_media_session_alloc - Allocate a new session.
> > + * @vv: virtio-media device the session belongs to.
> > + * @id: ID of the session.
> > + * @nonblocking_dequeue: whether dequeuing of buffers should be blocking or
> > + * not.
> > + *
> > + * The ``id`` and ``list`` fields must still be set by the caller.
> > + */
> > +static struct virtio_media_session *
> > +virtio_media_session_alloc(struct virtio_media *vv, u32 id,
> > +			   bool nonblocking_dequeue)
> > +{
> > +	struct virtio_media_session *session;
> > +	int i;
> > +	int ret;
> > +
> > +	session = kzalloc(sizeof(*session), GFP_KERNEL);
> > +	if (!session)
> > +		goto err_session;
> > +
> > +	session->shadow_buf = kzalloc(VIRTIO_SHADOW_BUF_SIZE, GFP_KERNEL);
> > +	if (!session->shadow_buf)
> > +		goto err_shadow_buf;
> > +
> > +	ret = sg_alloc_table(&session->command_sgs, DESC_CHAIN_MAX_LEN,
> > +			     GFP_KERNEL);
> > +	if (ret)
> > +		goto err_payload_sgs;
> > +
> > +	session->id = id;
> > +	session->nonblocking_dequeue = nonblocking_dequeue;
> > +
> > +	INIT_LIST_HEAD(&session->list);
> > +	v4l2_fh_init(&session->fh, &vv->video_dev);
> > +	v4l2_fh_add(&session->fh);
> > +
> > +	for (i = 0; i <= VIRTIO_MEDIA_LAST_QUEUE; i++)
> > +		INIT_LIST_HEAD(&session->queues[i].pending_dqbufs);
> > +	mutex_init(&session->queues_lock);
> > +
> > +	init_waitqueue_head(&session->dqbuf_wait);
> > +
> > +	mutex_lock(&vv->sessions_lock);
> > +	list_add_tail(&session->list, &vv->sessions);
> > +	mutex_unlock(&vv->sessions_lock);
> > +
> > +	return session;
> > +
> > +err_payload_sgs:
> > +	kfree(session->shadow_buf);
> > +err_shadow_buf:
> > +	kfree(session);
> > +err_session:
> > +	return ERR_PTR(-ENOMEM);
> > +}
> > +
> > +/**
> > + * virtio_media_session_free - Free all resources of a session.
> > + * @vv: virtio-media device the session belongs to.
> > + * @session: session to destroy.
> > + *
> > + * All the resources of @sesssion, as well as the backing memory of @session
> > + * itself, are freed.
> > + */
> > +static void virtio_media_session_free(struct virtio_media *vv,
> > +				      struct virtio_media_session *session)
> > +{
> > +	int i;
> > +
> > +	mutex_lock(&vv->sessions_lock);
> > +	list_del(&session->list);
> > +	mutex_unlock(&vv->sessions_lock);
> > +
> > +	v4l2_fh_del(&session->fh);
> > +	v4l2_fh_exit(&session->fh);
> > +
> > +	sg_free_table(&session->command_sgs);
> > +
> > +	for (i = 0; i <= VIRTIO_MEDIA_LAST_QUEUE; i++)
> > +		vfree(session->queues[i].buffers);
> > +
> > +	kfree(session->shadow_buf);
> > +	kfree(session);
> > +}
> > +
> > +/**
> > + * virtio_media_session_close - Close and free a session.
> > + * @vv: virtio-media device the session belongs to.
> > + * @session: session to close and destroy.
> > + *
> > + * This send the ``VIRTIO_MEDIA_CMD_CLOSE`` command to the device, and frees
> > + * all resources used by @session.
> > + */
> > +static int virtio_media_session_close(struct virtio_media *vv,
> > +				      struct virtio_media_session *session)
> > +{
> > +	struct virtio_media_cmd_close *cmd_close = &session->cmd.close;
> > +	struct scatterlist cmd_sg = {};
> > +	struct scatterlist *sgs[1] = { &cmd_sg };
> > +	int ret;
> > +
> > +	mutex_lock(&vv->vlock);
> > +
> > +	cmd_close->hdr.cmd = VIRTIO_MEDIA_CMD_CLOSE;
> > +	cmd_close->session_id = session->id;
> > +
> > +	sg_set_buf(&cmd_sg, cmd_close, sizeof(*cmd_close));
> > +	sg_mark_end(&cmd_sg);
> > +
> > +	ret = virtio_media_send_command(vv, sgs, 1, 0, 0, NULL);
> > +	mutex_unlock(&vv->vlock);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	virtio_media_session_free(vv, session);
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * virtio_media_find_session - Lookup for the session with a given ID.
> > + * @vv: virtio-media device to lookup the session from.
> > + * @id: ID of the session to lookup.
> > + */
> > +static struct virtio_media_session *
> > +virtio_media_find_session(struct virtio_media *vv, u32 id)
> > +{
> > +	struct list_head *p;
> > +	struct virtio_media_session *session = NULL;
> > +
> > +	mutex_lock(&vv->sessions_lock);
> > +	list_for_each(p, &vv->sessions) {
> > +		struct virtio_media_session *s =
> > +			list_entry(p, struct virtio_media_session, list);
> > +		if (s->id == id) {
> > +			session = s;
> > +			break;
> > +		}
> > +	}
> > +	mutex_unlock(&vv->sessions_lock);
> > +
> > +	return session;
> > +}
> > +
> > +/**
> > + * struct virtio_media_cmd_callback_param - Callback parameters to the virtio command queue.
> > + * @vv: virtio-media device in use.
> > + * @done: flag to be switched once the command is completed.
> > + * @resp_len: length of the received response from the command. Only valid
> > + * after @done_flag has switched to ``true``.
> > + */
> > +struct virtio_media_cmd_callback_param {
> > +	struct virtio_media *vv;
> > +	bool done;
> > +	size_t resp_len;
> > +};
> > +
> > +/**
> > + * commandq_callback: Callback for the command queue.
> > + * @queue: command virtqueue.
> > + *
> > + * This just wakes up the thread that was waiting on the command to complete.
> > + */
> > +static void commandq_callback(struct virtqueue *queue)
> > +{
> > +	unsigned int len;
> > +	struct virtio_media_cmd_callback_param *param;
> > +
> > +process_bufs:
> > +	while ((param = virtqueue_get_buf(queue, &len))) {
> > +		param->done = true;
> > +		param->resp_len = len;
> > +		wake_up(&param->vv->wq);
> > +	}
> > +
> > +	if (!virtqueue_enable_cb(queue)) {
> > +		virtqueue_disable_cb(queue);
> > +		goto process_bufs;
> > +	}
> > +}
> > +
> > +/**
> > + * virtio_media_kick_command - send a command to the commandq.
> > + * @vv: virtio-media device in use.
> > + * @sgs: descriptor chain to send.
> > + * @out_sgs: number of device-readable descriptors in @sgs.
> > + * @in_sgs: number of device-writable descriptors in @sgs.
> > + * @resp_len: output parameter. Upon success, contains the size of the response
> > + * in bytes.
> > + *
> > + */
> > +static int virtio_media_kick_command(struct virtio_media *vv,
> > +				     struct scatterlist **sgs,
> > +				     const size_t out_sgs, const size_t in_sgs,
> > +				     size_t *resp_len)
> > +{
> > +	struct virtio_media_cmd_callback_param cb_param = {
> > +		.vv = vv,
> > +		.done = false,
> > +		.resp_len = 0,
> > +	};
> > +	struct virtio_media_resp_header *resp_header;
> > +	int ret;
> > +
> > +	ret = virtqueue_add_sgs(vv->commandq, sgs, out_sgs, in_sgs, &cb_param,
> > +				GFP_ATOMIC);
> > +	if (ret) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "failed to add sgs to command virtqueue\n");
> > +		return ret;
> > +	}
> > +
> > +	if (!virtqueue_kick(vv->commandq)) {
> > +		v4l2_err(&vv->v4l2_dev, "failed to kick command virtqueue\n");
> > +		return -EINVAL;
> > +	}
> > +
> > +	/* Wait for the response. */
> > +	ret = wait_event_timeout(vv->wq, cb_param.done, 5 * HZ);
> > +	if (ret == 0) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "timed out waiting for response to command\n");
> > +		return -ETIMEDOUT;
> > +	}
> > +
> > +	if (resp_len)
> > +		*resp_len = cb_param.resp_len;
> > +
> > +	if (in_sgs > 0) {
> > +		/*
> > +		 * If we expect a response, make sure we have at least a
> > +		 * response header - anything shorter is invalid.
> > +		 */
> > +		if (cb_param.resp_len < sizeof(*resp_header)) {
> > +			v4l2_err(&vv->v4l2_dev,
> > +				 "received response header is too short\n");
> > +			return -EINVAL;
> > +		}
> > +
> > +		resp_header = sg_virt(sgs[out_sgs]);
> > +		if (resp_header->status)
> > +			/* Host returns a positive error code. */
> > +			return -resp_header->status;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * virtio_media_send_command - Send a command to the device and wait for its
> > + * response.
> > + * @vv: virtio-media device in use.
> > + * @sgs: descriptor chain to send.
> > + * @out_sgs: number of device-readable descriptors in @sgs.
> > + * @in_sgs: number of device-writable descriptors in @sgs.
> > + * @minimum_resp_len: minimum length of the response expected by the caller
> > + * when the command is successful. Anything shorter than that will result in
> > + * ``-EINVAL`` being returned.
> > + * @resp_len: output parameter. Upon success, contains the size of the response
> > + * in bytes.
> > + */
> > +int virtio_media_send_command(struct virtio_media *vv, struct scatterlist **sgs,
> > +			      const size_t out_sgs, const size_t in_sgs,
> > +			      size_t minimum_resp_len, size_t *resp_len)
> > +{
> > +	size_t local_resp_len = resp_len ? *resp_len : 0;
> > +	int ret = virtio_media_kick_command(vv, sgs, out_sgs, in_sgs,
> > +					    &local_resp_len);
> > +	if (resp_len)
> > +		*resp_len = local_resp_len;
> > +
> > +	/* If the host could not process the command, there is no valid response */
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	/* Make sure the host wrote a complete reply. */
> > +	if (local_resp_len < minimum_resp_len) {
> > +		v4l2_err(
> > +			&vv->v4l2_dev,
> > +			"received response is too short: received %zu, expected at least %zu\n",
> > +			local_resp_len, minimum_resp_len);
> > +		return -EINVAL;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * virtio_media_send_event_buffer() - Sends an event buffer to the host so it
> > + * can return it with an event.
> > + * @vv: virtio-media device in use.
> > + * @event_buffer: pointer to the event buffer to send to the device.
> > + */
> > +static int virtio_media_send_event_buffer(struct virtio_media *vv,
> > +					  void *event_buffer)
> > +{
> > +	struct scatterlist *sgs[1], vresp;
> > +	int ret;
> > +
> > +	sg_init_one(&vresp, event_buffer, VIRTIO_MEDIA_EVENT_MAX_SIZE);
> > +	sgs[0] = &vresp;
> > +
> > +	ret = virtqueue_add_sgs(vv->eventq, sgs, 0, 1, event_buffer,
> > +				GFP_ATOMIC);
> > +	if (ret) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "failed to add sgs to event virtqueue\n");
> > +		return ret;
> > +	}
> > +
> > +	if (!virtqueue_kick(vv->eventq)) {
> > +		v4l2_err(&vv->v4l2_dev, "failed to kick event virtqueue\n");
> > +		return -EINVAL;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * eventq_callback() - Callback for the event queue.
> > + * @queue: event virtqueue.
> > + *
> > + * This just schedules for event work to be run.
> > + */
> > +static void eventq_callback(struct virtqueue *queue)
> > +{
> > +	struct virtio_media *vv = queue->vdev->priv;
> > +
> > +	schedule_work(&vv->eventq_work);
> > +}
> > +
> > +/**
> > + * virtio_media_process_dqbuf_event() - Process a dequeued event for a session.
> > + * @vv: virtio-media device in use.
> > + * @session: session the event is addressed to.
> > + * @dqbuf_evt: the dequeued event to process.
> > + *
> > + * Invalid events are ignored with an error log.
> > + */
> > +static void
> > +virtio_media_process_dqbuf_event(struct virtio_media *vv,
> > +				 struct virtio_media_session *session,
> > +				 struct virtio_media_event_dqbuf *dqbuf_evt)
> > +{
> > +	struct virtio_media_buffer *dqbuf;
> > +	const enum v4l2_buf_type queue_type = dqbuf_evt->buffer.type;
> > +	struct virtio_media_queue_state *queue;
> > +	typeof(dqbuf->buffer.m) buffer_m;
> > +	typeof(dqbuf->buffer.m.planes[0].m) plane_m;
> > +	int i;
> > +
> > +	if (queue_type >= ARRAY_SIZE(session->queues)) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "unmanaged queue %d passed to dqbuf event",
> > +			 dqbuf_evt->buffer.type);
> > +		return;
> > +	}
> > +	queue = &session->queues[queue_type];
> > +
> > +	if (dqbuf_evt->buffer.index >= queue->allocated_bufs) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "invalid buffer ID %d for queue %d in dqbuf event",
> > +			 dqbuf_evt->buffer.index, dqbuf_evt->buffer.type);
> > +		return;
> > +	}
> > +
> > +	dqbuf = &queue->buffers[dqbuf_evt->buffer.index];
> > +
> > +	/*
> > +	 * Preserve the 'm' union that was passed to us during QBUF so userspace
> > +	 * gets back the information it submitted.
> > +	 */
> > +	buffer_m = dqbuf->buffer.m;
> > +	memcpy(&dqbuf->buffer, &dqbuf_evt->buffer, sizeof(dqbuf->buffer));
> > +	dqbuf->buffer.m = buffer_m;
> > +	if (V4L2_TYPE_IS_MULTIPLANAR(dqbuf->buffer.type)) {
> > +		if (dqbuf->buffer.length > VIDEO_MAX_PLANES) {
> > +			v4l2_err(
> > +				&vv->v4l2_dev,
> > +				"invalid number of planes received from host for a multiplanar buffer\n");
> > +			return;
> > +		}
> > +		for (i = 0; i < dqbuf->buffer.length; i++) {
> > +			plane_m = dqbuf->planes[i].m;
> > +			memcpy(&dqbuf->planes[i], &dqbuf_evt->planes[i],
> > +			       sizeof(struct v4l2_plane));
> > +			dqbuf->planes[i].m = plane_m;
> > +		}
> > +	}
> > +
> > +	/* Set the DONE flag as the buffer is waiting for being dequeued. */
> > +	dqbuf->buffer.flags |= V4L2_BUF_FLAG_DONE;
> > +
> > +	mutex_lock(&session->queues_lock);
> > +	list_add_tail(&dqbuf->list, &queue->pending_dqbufs);
> > +	queue->queued_bufs -= 1;
> > +	mutex_unlock(&session->queues_lock);
> > +
> > +	wake_up(&session->dqbuf_wait);
> > +}
> > +
> > +/**
> > + * virtio_media_process_events() - Process all pending events on a device.
> > + * @vv: device which pending events we want to process.
> > + *
> > + * Retrieves all pending events on @vv's event queue and dispatch them to their
> > + * corresponding session.
> > + *
> > + * Invalid events are ignored with an error log.
> > + */
> > +void virtio_media_process_events(struct virtio_media *vv)
> > +{
> > +	struct virtio_media_event_error *error_evt;
> > +	struct virtio_media_event_dqbuf *dqbuf_evt;
> > +	struct virtio_media_event_event *event_evt;
> > +	struct virtio_media_session *session;
> > +	struct virtio_media_event_header *evt;
> > +	unsigned int len;
> > +
> > +	mutex_lock(&vv->events_lock);
> > +
> > +process_bufs:
> > +	while ((evt = virtqueue_get_buf(vv->eventq, &len))) {
> > +		/* Make sure we received enough data */
> > +		if (len < sizeof(*evt)) {
> > +			v4l2_err(
> > +				&vv->v4l2_dev,
> > +				"event is too short: got %u, expected at least %zu\n",
> > +				len, sizeof(*evt));
> > +			goto end_of_event;
> > +		}
> > +
> > +		session = virtio_media_find_session(vv, evt->session_id);
> > +		if (!session) {
> > +			v4l2_err(&vv->v4l2_dev, "cannot find session %d\n",
> > +				 evt->session_id);
> > +			goto end_of_event;
> > +		}
> > +
> > +		switch (evt->event) {
> > +		case VIRTIO_MEDIA_EVT_ERROR:
> > +			if (len < sizeof(*error_evt)) {
> > +				v4l2_err(
> > +					&vv->v4l2_dev,
> > +					"error event is too short: got %u, expected %zu\n",
> > +					len, sizeof(*error_evt));
> > +				break;
> > +			}
> > +			error_evt = (struct virtio_media_event_error *)evt;
> > +			v4l2_err(&vv->v4l2_dev,
> > +				 "received error %d for session %d",
> > +				 error_evt->errno, error_evt->hdr.session_id);
> > +			virtio_media_session_close(vv, session);
> > +			break;
> > +
> > +		/*
> > +		 * Dequeued buffer: put it into the right queue so user-space can dequeue
> > +		 * it.
> > +		 */
> > +		case VIRTIO_MEDIA_EVT_DQBUF:
> > +			if (len < sizeof(*dqbuf_evt)) {
> > +				v4l2_err(
> > +					&vv->v4l2_dev,
> > +					"dqbuf event is too short: got %u, expected %zu\n",
> > +					len, sizeof(*dqbuf_evt));
> > +				break;
> > +			}
> > +			dqbuf_evt = (struct virtio_media_event_dqbuf *)evt;
> > +			virtio_media_process_dqbuf_event(vv, session,
> > +							 dqbuf_evt);
> > +			break;
> > +
> > +		case VIRTIO_MEDIA_EVT_EVENT:
> > +			if (len < sizeof(*event_evt)) {
> > +				v4l2_err(
> > +					&vv->v4l2_dev,
> > +					"session event is too short: got %u expected %zu\n",
> > +					len, sizeof(*event_evt));
> > +				break;
> > +			}
> > +
> > +			event_evt = (struct virtio_media_event_event *)evt;
> > +			v4l2_event_queue_fh(&session->fh, &event_evt->event);
> > +			break;
> > +
> > +		default:
> > +			v4l2_err(&vv->v4l2_dev, "unknown event type %d\n",
> > +				 evt->event);
> > +			break;
> > +		}
> > +
> > +end_of_event:
> > +		virtio_media_send_event_buffer(vv, evt);
> > +	}
> > +
> > +	if (!virtqueue_enable_cb(vv->eventq)) {
> > +		virtqueue_disable_cb(vv->eventq);
> > +		goto process_bufs;
> > +	}
> > +
> > +	mutex_unlock(&vv->events_lock);
> > +}
> > +
> > +static void virtio_media_event_work(struct work_struct *work)
> > +{
> > +	struct virtio_media *vv =
> > +		container_of(work, struct virtio_media, eventq_work);
> > +
> > +	virtio_media_process_events(vv);
> > +}
> > +
> > +/**
> > + * virtio_media_device_open() - Create a new session from an opened file.
> > + * @file: opened file for the session.
> > + */
> > +static int virtio_media_device_open(struct file *file)
> > +{
> > +	struct video_device *video_dev = video_devdata(file);
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct virtio_media_cmd_open *cmd_open = &vv->cmd.open;
> > +	struct virtio_media_resp_open *resp_open = &vv->resp.open;
> > +	struct scatterlist cmd_sg = {}, resp_sg = {};
> > +	struct scatterlist *sgs[2] = { &cmd_sg, &resp_sg };
> > +	struct virtio_media_session *session;
> > +	u32 session_id;
> > +	int ret;
> > +
> > +	mutex_lock(&vv->vlock);
> > +
> > +	sg_set_buf(&cmd_sg, cmd_open, sizeof(*cmd_open));
> > +	sg_mark_end(&cmd_sg);
> > +
> > +	sg_set_buf(&resp_sg, resp_open, sizeof(*resp_open));
> > +	sg_mark_end(&resp_sg);
> > +
> > +	cmd_open->hdr.cmd = VIRTIO_MEDIA_CMD_OPEN;
> > +	ret = virtio_media_send_command(vv, sgs, 1, 1, sizeof(*resp_open),
> > +					NULL);
> > +	session_id = resp_open->session_id;
> > +	mutex_unlock(&vv->vlock);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	session = virtio_media_session_alloc(vv, session_id,
> > +					     (file->f_flags & O_NONBLOCK));
> > +	if (IS_ERR(session))
> > +		return PTR_ERR(session);
> > +
> > +	file->private_data = &session->fh;
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * virtio_media_device_close() - Close a previously opened session.
> > + * @file: file of the session to close.
> > + *
> > + * This sends to ``VIRTIO_MEDIA_CMD_CLOSE`` command to the device, and close
> > + * the session on the driver side.
> > + */
> > +static int virtio_media_device_close(struct file *file)
> > +{
> > +	struct video_device *video_dev = video_devdata(file);
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct virtio_media_session *session =
> > +		fh_to_session(file->private_data);
> > +
> > +	return virtio_media_session_close(vv, session);
> > +}
> > +
> > +/**
> > + * virtio_media_device_poll() - Poll logic for a virtio-media device.
> > + * @file: file of the session to poll.
> > + * @wait: poll table to wait on.
> > + */
> > +static __poll_t virtio_media_device_poll(struct file *file, poll_table *wait)
> > +{
> > +	struct virtio_media_session *session =
> > +		fh_to_session(file->private_data);
> > +	enum v4l2_buf_type capture_type =
> > +		session->uses_mplane ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE :
> > +				       V4L2_BUF_TYPE_VIDEO_CAPTURE;
> > +	enum v4l2_buf_type output_type =
> > +		session->uses_mplane ? V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE :
> > +				       V4L2_BUF_TYPE_VIDEO_OUTPUT;
> > +	struct virtio_media_queue_state *capture_queue =
> > +		&session->queues[capture_type];
> > +	struct virtio_media_queue_state *output_queue =
> > +		&session->queues[output_type];
> > +	__poll_t req_events = poll_requested_events(wait);
> > +	__poll_t rc = 0;
> > +
> > +	poll_wait(file, &session->dqbuf_wait, wait);
> > +	poll_wait(file, &session->fh.wait, wait);
> > +
> > +	mutex_lock(&session->queues_lock);
> > +	if (req_events & (EPOLLIN | EPOLLRDNORM)) {
> > +		if (!capture_queue->streaming ||
> > +		    (capture_queue->queued_bufs == 0 &&
> > +		     list_empty(&capture_queue->pending_dqbufs)))
> > +			rc |= EPOLLERR;
> > +		else if (!list_empty(&capture_queue->pending_dqbufs))
> > +			rc |= EPOLLIN | EPOLLRDNORM;
> > +	}
> > +	if (req_events & (EPOLLOUT | EPOLLWRNORM)) {
> > +		if (!output_queue->streaming)
> > +			rc |= EPOLLERR;
> > +		else if (output_queue->queued_bufs <
> > +			 output_queue->allocated_bufs)
> > +			rc |= EPOLLOUT | EPOLLWRNORM;
> > +	}
> > +	mutex_unlock(&session->queues_lock);
> > +
> > +	if (v4l2_event_pending(&session->fh))
> > +		rc |= EPOLLPRI;
> > +
> > +	return rc;
> > +}
> > +
> > +static void virtio_media_vma_close_locked(struct vm_area_struct *vma)
> > +{
> > +	struct virtio_media *vv = vma->vm_private_data;
> > +	struct virtio_media_cmd_munmap *cmd_munmap = &vv->cmd.munmap;
> > +	struct virtio_media_resp_munmap *resp_munmap = &vv->resp.munmap;
> > +	struct scatterlist cmd_sg = {}, resp_sg = {};
> > +	struct scatterlist *sgs[2] = { &cmd_sg, &resp_sg };
> > +	int ret;
> > +
> > +	sg_set_buf(&cmd_sg, cmd_munmap, sizeof(*cmd_munmap));
> > +	sg_mark_end(&cmd_sg);
> > +
> > +	sg_set_buf(&resp_sg, resp_munmap, sizeof(*resp_munmap));
> > +	sg_mark_end(&resp_sg);
> > +
> > +	cmd_munmap->hdr.cmd = VIRTIO_MEDIA_CMD_MUNMAP;
> > +	cmd_munmap->driver_addr =
> > +		(vma->vm_pgoff << PAGE_SHIFT) - vv->mmap_region.addr;
> > +	ret = virtio_media_send_command(vv, sgs, 1, 1, sizeof(*resp_munmap),
> > +					NULL);
> > +	if (ret < 0) {
> > +		v4l2_err(&vv->v4l2_dev, "host failed to unmap buffer: %d\n",
> > +			 ret);
> > +	}
> > +}
> > +
> > +/**
> > + * virtio_media_vma_close() - Close a MMAP buffer mapping.
> > + * @vma: VMA of the mapping to close.
> > + *
> > + * Inform the host that a previously created MMAP mapping is no longer needed
> > + * and can be removed.
> > + */
> > +static void virtio_media_vma_close(struct vm_area_struct *vma)
> > +{
> > +	struct virtio_media *vv = vma->vm_private_data;
> > +
> > +	mutex_lock(&vv->vlock);
> > +	virtio_media_vma_close_locked(vma);
> > +	mutex_unlock(&vv->vlock);
> > +}
> > +
> > +static const struct vm_operations_struct virtio_media_vm_ops = {
> > +	.close = virtio_media_vma_close,
> > +};
> > +
> > +/**
> > + * virtio_media_device_mmap - Perform a mmap request from userspace.
> > + * @file: opened file of the session to map for.
> > + * @vma: VM area struct describing the desired mapping.
> > + *
> > + * This requests the host to map a MMAP buffer for us, so we can then make that
> > + * mapping visible into user-space address space.
> > + */
> > +static int virtio_media_device_mmap(struct file *file,
> > +				    struct vm_area_struct *vma)
> > +{
> > +	struct video_device *video_dev = video_devdata(file);
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct virtio_media_session *session =
> > +		fh_to_session(file->private_data);
> > +	struct virtio_media_cmd_mmap *cmd_mmap = &session->cmd.mmap;
> > +	struct virtio_media_resp_mmap *resp_mmap = &session->resp.mmap;
> > +	struct scatterlist cmd_sg = {}, resp_sg = {};
> > +	struct scatterlist *sgs[2] = { &cmd_sg, &resp_sg };
> > +	int ret;
> > +
> > +	if (!(vma->vm_flags & VM_SHARED))
> > +		return -EINVAL;
> > +	if (!(vma->vm_flags & (VM_READ | VM_WRITE)))
> > +		return -EINVAL;
> > +
> > +	mutex_lock(&vv->vlock);
> > +
> > +	cmd_mmap->hdr.cmd = VIRTIO_MEDIA_CMD_MMAP;
> > +	cmd_mmap->session_id = session->id;
> > +	cmd_mmap->flags =
> > +		(vma->vm_flags & VM_WRITE) ? VIRTIO_MEDIA_MMAP_FLAG_RW : 0;
> > +	cmd_mmap->offset = vma->vm_pgoff << PAGE_SHIFT;
> > +
> > +	sg_set_buf(&cmd_sg, cmd_mmap, sizeof(*cmd_mmap));
> > +	sg_mark_end(&cmd_sg);
> > +
> > +	sg_set_buf(&resp_sg, resp_mmap, sizeof(*resp_mmap));
> > +	sg_mark_end(&resp_sg);
> > +
> > +	/*
> > +	 * The host performs reference counting and is smart enough to return the
> > +	 * same guest physical address if this is called several times on the same
> > +	 * buffer.
> > +	 */
> > +	ret = virtio_media_send_command(vv, sgs, 1, 1, sizeof(*resp_mmap),
> > +					NULL);
> > +	if (ret < 0)
> > +		goto end;
> > +
> > +	vma->vm_private_data = vv;
> > +	/*
> > +	 * Keep the guest address at which the buffer is mapped since we will
> > +	 * use that to unmap.
> > +	 */
> > +	vma->vm_pgoff = (resp_mmap->driver_addr + vv->mmap_region.addr) >>
> > +			PAGE_SHIFT;
> > +
> > +	/*
> > +	 * We cannot let the mapping be larger than the buffer.
> > +	 */
> > +	if (vma->vm_end - vma->vm_start > PAGE_ALIGN(resp_mmap->len)) {
> > +		dev_dbg(&video_dev->dev,
> > +			"invalid MMAP, as it would overflow buffer length\n");
> > +		virtio_media_vma_close_locked(vma);
> > +		ret = -EINVAL;
> > +		goto end;
> > +	}
> > +
> > +	ret = io_remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,
> > +				 vma->vm_end - vma->vm_start,
> > +				 vma->vm_page_prot);
> > +	if (ret)
> > +		goto end;
> > +
> > +	vma->vm_ops = &virtio_media_vm_ops;
> > +
> > +end:
> > +	mutex_unlock(&vv->vlock);
> > +	return ret;
> > +}
> > +
> > +static const struct v4l2_file_operations virtio_media_fops = {
> > +	.owner = THIS_MODULE,
> > +	.open = virtio_media_device_open,
> > +	.release = virtio_media_device_close,
> > +	.poll = virtio_media_device_poll,
> > +	.unlocked_ioctl = virtio_media_device_ioctl,
> > +	.mmap = virtio_media_device_mmap,
> > +};
> > +
> > +static int virtio_media_probe(struct virtio_device *virtio_dev)
> > +{
> > +	struct device *dev = &virtio_dev->dev;
> > +	struct virtqueue *vqs[2];
> > +	static struct virtqueue_info vq_info[2] = {
> > +		{
> > +			.name = "command",
> > +			.callback = commandq_callback,
> > +		},
> > +		{
> > +			.name = "event",
> > +			.callback = eventq_callback,
> > +		},
> > +	};
> > +	struct virtio_media *vv;
> > +	struct video_device *vd;
> > +	int i;
> > +	int ret;
> > +
> > +	vv = devm_kzalloc(dev, sizeof(*vv), GFP_KERNEL);
> > +	if (!vv)
> > +		return -ENOMEM;
> > +
> > +	vv->event_buffer = devm_kzalloc(
> > +		dev, VIRTIO_MEDIA_EVENT_MAX_SIZE * VIRTIO_MEDIA_NUM_EVENT_BUFS,
> > +		GFP_KERNEL);
> > +	if (!vv->event_buffer)
> > +		return -ENOMEM;
> > +
> > +	INIT_LIST_HEAD(&vv->sessions);
> > +	mutex_init(&vv->sessions_lock);
> > +	mutex_init(&vv->events_lock);
> > +	mutex_init(&vv->vlock);
> > +
> > +	vv->virtio_dev = virtio_dev;
> > +	virtio_dev->priv = vv;
> > +
> > +	init_waitqueue_head(&vv->wq);
> > +
> > +	ret = v4l2_device_register(dev, &vv->v4l2_dev);
> > +	if (ret)
> > +		return ret;
> > +
> > +	ret = virtio_find_vqs(virtio_dev, 2, vqs, vq_info, NULL);
> > +	if (ret)
> > +		goto err_find_vqs;
> > +
> > +	vv->commandq = vqs[0];
> > +	vv->eventq = vqs[1];
> > +	INIT_WORK(&vv->eventq_work, virtio_media_event_work);
> > +
> > +	/* Get MMAP buffer mapping SHM region */
> > +	virtio_get_shm_region(virtio_dev, &vv->mmap_region,
> > +			      VIRTIO_MEDIA_SHM_MMAP);
> > +
> > +	vd = &vv->video_dev;
> > +
> > +	vd->v4l2_dev = &vv->v4l2_dev;
> > +	vd->vfl_type = VFL_TYPE_VIDEO;
> > +	vd->ioctl_ops = &virtio_media_ioctl_ops;
> > +	vd->fops = &virtio_media_fops;
> > +	vd->device_caps = virtio_cread32(virtio_dev, 0);
> > +	if (vd->device_caps & (V4L2_CAP_VIDEO_M2M | V4L2_CAP_VIDEO_M2M_MPLANE))
> > +		vd->vfl_dir = VFL_DIR_M2M;
> > +	else if (vd->device_caps &
> > +		 (V4L2_CAP_VIDEO_OUTPUT | V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE))
> > +		vd->vfl_dir = VFL_DIR_TX;
> > +	else
> > +		vd->vfl_dir = VFL_DIR_RX;
> > +	vd->release = video_device_release_empty;
> > +	strscpy(vd->name, "virtio-media", sizeof(vd->name));
> > +
> > +	video_set_drvdata(vd, vv);
> > +
> > +	ret = video_register_device(vd, virtio_cread32(virtio_dev, 4), 0);
> > +	if (ret)
> > +		goto err_register_device;
> > +
> > +	for (i = 0; i < VIRTIO_MEDIA_NUM_EVENT_BUFS; i++) {
> > +		ret = virtio_media_send_event_buffer(
> > +			vv, vv->event_buffer + VIRTIO_MEDIA_EVENT_MAX_SIZE * i);
> > +		if (ret)
> > +			goto err_send_event_buffer;
> > +	}
> > +
> > +	virtio_device_ready(virtio_dev);
> > +
> > +	return 0;
> > +
> > +err_send_event_buffer:
> > +	video_unregister_device(&vv->video_dev);
> > +err_register_device:
> > +	virtio_dev->config->del_vqs(virtio_dev);
> > +err_find_vqs:
> > +	v4l2_device_unregister(&vv->v4l2_dev);
> > +
> > +	return ret;
> > +}
> > +
> > +static void virtio_media_remove(struct virtio_device *virtio_dev)
> > +{
> > +	struct virtio_media *vv = virtio_dev->priv;
> > +	struct list_head *p, *n;
> > +
> > +	cancel_work_sync(&vv->eventq_work);
> > +	virtio_reset_device(virtio_dev);
> > +
> > +	v4l2_device_unregister(&vv->v4l2_dev);
> > +	virtio_dev->config->del_vqs(virtio_dev);
> > +	video_unregister_device(&vv->video_dev);
> > +
> > +	list_for_each_safe(p, n, &vv->sessions) {
> > +		struct virtio_media_session *s =
> > +			list_entry(p, struct virtio_media_session, list);
> > +
> > +		virtio_media_session_free(vv, s);
> > +	}
> > +}
> > +
> > +static struct virtio_device_id id_table[] = {
> > +	{ VIRTIO_ID_MEDIA, VIRTIO_DEV_ANY_ID },
> > +	{ 0 },
> > +};
> > +
> > +static unsigned int features[] = {};
> > +
> > +static struct virtio_driver virtio_media_driver = {
> > +	.feature_table = features,
> > +	.feature_table_size = ARRAY_SIZE(features),
> > +	.driver.name = VIRTIO_MEDIA_DEFAULT_DRIVER_NAME,
> > +	.driver.owner = THIS_MODULE,
> > +	.id_table = id_table,
> > +	.probe = virtio_media_probe,
> > +	.remove = virtio_media_remove,
> > +};
> > +
> > +module_virtio_driver(virtio_media_driver);
> > +
> > +MODULE_DEVICE_TABLE(virtio, id_table);
> > +MODULE_DESCRIPTION("virtio media driver");
> > +MODULE_AUTHOR("Alexandre Courbot <[email-masked]>");
> > +MODULE_LICENSE("Dual BSD/GPL");
> > diff --git a/drivers/media/virtio/virtio_media_ioctls.c b/drivers/media/virtio/virtio_media_ioctls.c
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..863cdfbaaadc7241110c82ce6880bc5675c23894
> > --- /dev/null
> > +++ b/drivers/media/virtio/virtio_media_ioctls.c
> > @@ -0,0 +1,1297 @@
> > +// SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0+
> > +
> > +/*
> > + * Ioctls implementations for the virtio-media driver.
> > + *
> > + * Copyright (c) 2024-2025 Google LLC.
> > + */
> > +
> > +#include <linux/mutex.h>
> > +#include <linux/videodev2.h>
> > +#include <linux/virtio_config.h>
> > +#include <linux/vmalloc.h>
> > +#include <media/v4l2-event.h>
> > +#include <media/v4l2-ioctl.h>
> > +
> > +#include "scatterlist_builder.h"
> > +#include "virtio_media.h"
> > +
> > +/**
> > + * virtio_media_send_r_ioctl() - Send a read-only ioctl to the device.
> > + * @fh: file handler of the session doing the ioctl.
> > + * @ioctl: ``VIDIOC_*`` ioctl code.
> > + * @ioctl_data: pointer to the ioctl payload.
> > + * @ioctl_data_len: length in bytes of the ioctl payload.
> > + *
> > + * Send an ioctl that has no driver payload, but expects a response from the
> > + * host (i.e. an ioctl specified with ``_IOR``).
> > + */
> > +static int virtio_media_send_r_ioctl(struct v4l2_fh *fh, u32 ioctl,
> > +				     void *ioctl_data, size_t ioctl_data_len)
> > +{
> > +	struct video_device *video_dev = fh->vdev;
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	struct scatterlist *sgs[3];
> > +	struct scatterlist_builder builder = {
> > +		.descs = session->command_sgs.sgl,
> > +		.num_descs = DESC_CHAIN_MAX_LEN,
> > +		.cur_desc = 0,
> > +		.shadow_buffer = session->shadow_buf,
> > +		.shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
> > +		.shadow_buffer_pos = 0,
> > +		.sgs = sgs,
> > +		.num_sgs = ARRAY_SIZE(sgs),
> > +		.cur_sg = 0,
> > +	};
> > +	int ret;
> > +
> > +	/* Command descriptor */
> > +	ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* Response descriptor */
> > +	ret = scatterlist_builder_add_ioctl_resp(&builder, session);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* Response payload */
> > +	ret = scatterlist_builder_add_data(&builder, ioctl_data,
> > +					   ioctl_data_len);
> > +	if (ret) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "failed to prepare command descriptor chain\n");
> > +		return ret;
> > +	}
> > +
> > +	ret = virtio_media_send_command(
> > +		vv, sgs, 1, 2,
> > +		sizeof(struct virtio_media_resp_ioctl) + ioctl_data_len, NULL);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	ret = scatterlist_builder_retrieve_data(&builder, 2, ioctl_data);
> > +	if (ret) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "failed to retrieve response descriptor chain\n");
> > +		return ret;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * virtio_media_send_w_ioctl() - Send a write-only ioctl to the device.
> > + * @fh: file handler of the session doing the ioctl.
> > + * @ioctl: ``VIDIOC_*`` ioctl code.
> > + * @ioctl_data: pointer to the ioctl payload.
> > + * @ioctl_data_len: length in bytes of the ioctl payload.
> > + *
> > + * Send an ioctl that does not expect a reply beyond an error status (i.e. an
> > + * ioctl specified with ``_IOW``) to the host.
> > + */
> > +static int virtio_media_send_w_ioctl(struct v4l2_fh *fh, u32 ioctl,
> > +				     const void *ioctl_data,
> > +				     size_t ioctl_data_len)
> > +{
> > +	struct video_device *video_dev = fh->vdev;
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	struct scatterlist *sgs[3];
> > +	struct scatterlist_builder builder = {
> > +		.descs = session->command_sgs.sgl,
> > +		.num_descs = DESC_CHAIN_MAX_LEN,
> > +		.cur_desc = 0,
> > +		.shadow_buffer = session->shadow_buf,
> > +		.shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
> > +		.shadow_buffer_pos = 0,
> > +		.sgs = sgs,
> > +		.num_sgs = ARRAY_SIZE(sgs),
> > +		.cur_sg = 0,
> > +	};
> > +	int ret;
> > +
> > +	/* Command descriptor */
> > +	ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* Command payload */
> > +	ret = scatterlist_builder_add_data(&builder, (void *)ioctl_data,
> > +					   ioctl_data_len);
> > +	if (ret) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "failed to prepare command descriptor chain\n");
> > +		return ret;
> > +	}
> > +
> > +	/* Response descriptor */
> > +	ret = scatterlist_builder_add_ioctl_resp(&builder, session);
> > +	if (ret)
> > +		return ret;
> > +
> > +	ret = virtio_media_send_command(
> > +		vv, sgs, 2, 1, sizeof(struct virtio_media_resp_ioctl), NULL);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * virtio_media_send_wr_ioctl() - Send a read-write ioctl to the device.
> > + * @fh: file handler of the session doing the ioctl.
> > + * @ioctl: ``VIDIOC_*`` ioctl code.
> > + * @ioctl_data: pointer to the ioctl payload.
> > + * @ioctl_data_len: length in bytes of the ioctl payload.
> > + * @minimum_resp_payload: minimum expected length of the response's payload.
> > + *
> > + * Sends an ioctl that expects a response of exactly the same size as the
> > + * input (i.e. an ioctl specified with ``_IOWR``) to the host.
> > + *
> > + * This corresponds to what most V4L2 ioctls do. For instance
> > + * ``VIDIOC_ENUM_FMT`` takes a partially-initialized ``struct v4l2_fmtdesc``
> > + * and returns its filled version.
> > + */
> > +static int virtio_media_send_wr_ioctl(struct v4l2_fh *fh, u32 ioctl,
> > +				      void *ioctl_data, size_t ioctl_data_len,
> > +				      size_t minimum_resp_payload)
> > +{
> > +	struct video_device *video_dev = fh->vdev;
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	struct scatterlist *sgs[4];
> > +	struct scatterlist_builder builder = {
> > +		.descs = session->command_sgs.sgl,
> > +		.num_descs = DESC_CHAIN_MAX_LEN,
> > +		.cur_desc = 0,
> > +		.shadow_buffer = session->shadow_buf,
> > +		.shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
> > +		.shadow_buffer_pos = 0,
> > +		.sgs = sgs,
> > +		.num_sgs = ARRAY_SIZE(sgs),
> > +		.cur_sg = 0,
> > +	};
> > +	int ret;
> > +
> > +	/* Command descriptor */
> > +	ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* Command payload */
> > +	ret = scatterlist_builder_add_data(&builder, ioctl_data,
> > +					   ioctl_data_len);
> > +	if (ret) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "failed to prepare command descriptor chain\n");
> > +		return ret;
> > +	}
> > +
> > +	/* Response descriptor */
> > +	ret = scatterlist_builder_add_ioctl_resp(&builder, session);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* Response payload, same as command */
> > +	ret = scatterlist_builder_add_descriptor(&builder, 1);
> > +	if (ret)
> > +		return ret;
> > +
> > +	ret = virtio_media_send_command(vv, sgs, 2, 2,
> > +					sizeof(struct virtio_media_resp_ioctl) +
> > +						minimum_resp_payload,
> > +					NULL);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	ret = scatterlist_builder_retrieve_data(&builder, 3, ioctl_data);
> > +	if (ret) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "failed to retrieve response descriptor chain\n");
> > +		return ret;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * virtio_media_send_buffer_ioctl() - Send an ioctl taking a buffer as
> > + * parameter to the device.
> > + * @fh: file handler of the session doing the ioctl.
> > + * @ioctl: ``VIDIOC_*`` ioctl code.
> > + * @b: ``v4l2_buffer`` to be sent as the ioctl payload.
> > + *
> > + * Buffers can require an additional descriptor to send their planes array, and
> > + * can have pointers to userspace memory hence this dedicated function.
> > + */
> > +static int virtio_media_send_buffer_ioctl(struct v4l2_fh *fh, u32 ioctl,
> > +					  struct v4l2_buffer *b)
> > +{
> > +	struct video_device *video_dev = fh->vdev;
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	struct v4l2_plane *orig_planes = NULL;
> > +	struct scatterlist *sgs[64];
> > +	/* End of the device-readable buffer SGs, to reuse in device-writable section. */
> > +	size_t num_cmd_sgs;
> > +	size_t end_buf_sg;
> > +	struct scatterlist_builder builder = {
> > +		.descs = session->command_sgs.sgl,
> > +		.num_descs = DESC_CHAIN_MAX_LEN,
> > +		.cur_desc = 0,
> > +		.shadow_buffer = session->shadow_buf,
> > +		.shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
> > +		.shadow_buffer_pos = 0,
> > +		.sgs = sgs,
> > +		.num_sgs = ARRAY_SIZE(sgs),
> > +		.cur_sg = 0,
> > +	};
> > +	size_t resp_len;
> > +	int ret;
> > +	int i;
> > +
> > +	if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
> > +		return -EINVAL;
> > +
> > +	if (V4L2_TYPE_IS_MULTIPLANAR(b->type))
> > +		orig_planes = b->m.planes;
> > +
> > +	/* Command descriptor */
> > +	ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* Command payload (struct v4l2_buffer) */
> > +	ret = scatterlist_builder_add_buffer(&builder, b);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	end_buf_sg = builder.cur_sg;
> > +
> > +	/* Payload of SHARED_PAGES buffers, if relevant */
> > +	ret = scatterlist_builder_add_buffer_userptr(&builder, b);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	num_cmd_sgs = builder.cur_sg;
> > +
> > +	/* Response descriptor */
> > +	ret = scatterlist_builder_add_ioctl_resp(&builder, session);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* Response payload (same as input, but no userptr mapping) */
> > +	for (i = 1; i < end_buf_sg; i++) {
> > +		ret = scatterlist_builder_add_descriptor(&builder, i);
> > +		if (ret < 0)
> > +			return ret;
> > +	}
> > +
> > +	ret = virtio_media_send_command(
> > +		vv, builder.sgs, num_cmd_sgs, builder.cur_sg - num_cmd_sgs,
> > +		sizeof(struct virtio_media_resp_ioctl) + sizeof(*b), &resp_len);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	resp_len -= sizeof(struct virtio_media_resp_ioctl);
> > +
> > +	/* Make sure that the reply length covers our v4l2_buffer */
> > +	if (resp_len < sizeof(*b))
> > +		return -EINVAL;
> > +
> > +	ret = scatterlist_builder_retrieve_buffer(&builder, num_cmd_sgs + 1, b,
> > +						  orig_planes);
> > +	if (ret) {
> > +		v4l2_err(&vv->v4l2_dev,
> > +			 "failed to retrieve response descriptor chain\n");
> > +		return ret;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * virtio_media_send_ext_controls_ioctl() - Send an ioctl taking extended
> > + * controls as parameters to the device.
> > + * @fh: file handler of the session doing the ioctl.
> > + * @ioctl: ``VIDIOC_*`` ioctl code.
> > + * @ctrls: ``v4l2_ext_controls`` to be sent as the ioctl payload.
> > + *
> > + * Queues an ioctl that sends a ``v4l2_ext_controls`` to the host and receives
> > + * an updated version.
> > + *
> > + * ``v4l2_ext_controls`` has a pointer to an array of ``v4l2_ext_control``, and
> > + * also potentially pointers to user-space memory that we need to map properly,
> > + * hence the dedicated function.
> > + */
> > +static int virtio_media_send_ext_controls_ioctl(struct v4l2_fh *fh, u32 ioctl,
> > +						struct v4l2_ext_controls *ctrls)
> > +{
> > +	struct video_device *video_dev = fh->vdev;
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	size_t num_cmd_sgs;
> > +	size_t end_ctrls_sg;
> > +	struct v4l2_ext_control *controls_backup = ctrls->controls;
> > +	const u32 num_ctrls = ctrls->count;
> > +	struct scatterlist *sgs[64];
> > +	struct scatterlist_builder builder = {
> > +		.descs = session->command_sgs.sgl,
> > +		.num_descs = DESC_CHAIN_MAX_LEN,
> > +		.cur_desc = 0,
> > +		.shadow_buffer = session->shadow_buf,
> > +		.shadow_buffer_size = VIRTIO_SHADOW_BUF_SIZE,
> > +		.shadow_buffer_pos = 0,
> > +		.sgs = sgs,
> > +		.num_sgs = ARRAY_SIZE(sgs),
> > +		.cur_sg = 0,
> > +	};
> > +	size_t resp_len = 0;
> > +	int ret;
> > +	int i;
> > +
> > +	/* Command descriptor */
> > +	ret = scatterlist_builder_add_ioctl_cmd(&builder, session, ioctl);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* v4l2_controls */
> > +	ret = scatterlist_builder_add_ext_ctrls(&builder, ctrls);
> > +	if (ret)
> > +		return ret;
> > +
> > +	end_ctrls_sg = builder.cur_sg;
> > +
> > +	ret = scatterlist_builder_add_ext_ctrls_userptrs(&builder, ctrls);
> > +	if (ret)
> > +		return ret;
> > +
> > +	num_cmd_sgs = builder.cur_sg;
> > +
> > +	/* Response descriptor */
> > +	ret = scatterlist_builder_add_ioctl_resp(&builder, session);
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* Response payload (same as input but without userptrs) */
> > +	for (i = 1; i < end_ctrls_sg; i++) {
> > +		ret = scatterlist_builder_add_descriptor(&builder, i);
> > +		if (ret < 0)
> > +			return ret;
> > +	}
> > +
> > +	ret = virtio_media_send_command(
> > +		vv, builder.sgs, num_cmd_sgs, builder.cur_sg - num_cmd_sgs,
> > +		sizeof(struct virtio_media_resp_ioctl) + sizeof(*ctrls),
> > +		&resp_len);
> > +
> > +	/* Just in case the host touched these. */
> > +	ctrls->controls = controls_backup;
> > +	if (ctrls->count != num_ctrls) {
> > +		v4l2_err(
> > +			&vv->v4l2_dev,
> > +			"device returned a number of controls different than the one submitted\n");
> > +	}
> > +	if (ctrls->count > num_ctrls)
> > +		return -ENOSPC;
> > +
> > +	/* Event if we have received an error, we may need to read our payload back */
> > +	if (ret < 0 && resp_len >= sizeof(struct virtio_media_resp_ioctl) +
> > +					   sizeof(*ctrls)) {
> > +		/* Deliberately ignore the error here as we want to return the previous one */
> > +		scatterlist_builder_retrieve_ext_ctrls(&builder,
> > +						       num_cmd_sgs + 1, ctrls);
> > +		return ret;
> > +	}
> > +
> > +	resp_len -= sizeof(struct virtio_media_resp_ioctl);
> > +
> > +	/* Make sure that the reply's length covers our v4l2_ext_controls */
> > +	if (resp_len < sizeof(*ctrls))
> > +		return -EINVAL;
> > +
> > +	ret = scatterlist_builder_retrieve_ext_ctrls(&builder, num_cmd_sgs + 1,
> > +						     ctrls);
> > +	if (ret)
> > +		return ret;
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * virtio_media_clear_queue() - clear all pending buffers on a streamed-off queue.
> > + * @session: session which the queue to clear belongs to.
> > + * @queue: state of the queue to clear.
> > + *
> > + * Helper function to clear the list of buffers waiting to be dequeued on a
> > + * queue that has just been streamed off.
> > + */
> > +static void virtio_media_clear_queue(struct virtio_media_session *session,
> > +				     struct virtio_media_queue_state *queue)
> > +{
> > +	struct list_head *p, *n;
> > +	int i;
> > +
> > +	mutex_lock(&session->queues_lock);
> > +
> > +	list_for_each_safe(p, n, &queue->pending_dqbufs) {
> > +		struct virtio_media_buffer *dqbuf =
> > +			list_entry(p, struct virtio_media_buffer, list);
> > +
> > +		list_del(&dqbuf->list);
> > +	}
> > +
> > +	/* All buffers are now dequeued. */
> > +	for (i = 0; i < queue->allocated_bufs; i++)
> > +		queue->buffers[i].buffer.flags = 0;
> > +
> > +	queue->queued_bufs = 0;
> > +	queue->streaming = false;
> > +	queue->is_capture_last = false;
> > +
> > +	mutex_unlock(&session->queues_lock);
> > +}
> > +
> > +/*
> > + * Macros suitable for defining ioctls with a constant size payload.
> > + */
> > +
> > +#define SIMPLE_WR_IOCTL(name, ioctl, payload_t)                       \
> > +	static int virtio_media_##name(struct file *file, void *fh,   \
> > +				       payload_t *payload)            \
> > +	{                                                             \
> > +		return virtio_media_send_wr_ioctl(fh, ioctl, payload, \
> > +						  sizeof(*payload),   \
> > +						  sizeof(*payload));  \
> > +	}
> > +#define SIMPLE_R_IOCTL(name, ioctl, payload_t)                       \
> > +	static int virtio_media_##name(struct file *file, void *fh,  \
> > +				       payload_t *payload)           \
> > +	{                                                            \
> > +		return virtio_media_send_r_ioctl(fh, ioctl, payload, \
> > +						 sizeof(*payload));  \
> > +	}
> > +#define SIMPLE_W_IOCTL(name, ioctl, payload_t)                       \
> > +	static int virtio_media_##name(struct file *file, void *fh,  \
> > +				       payload_t *payload)           \
> > +	{                                                            \
> > +		return virtio_media_send_w_ioctl(fh, ioctl, payload, \
> > +						 sizeof(*payload));  \
> > +	}
> > +
> > +/*
> > + * V4L2 ioctl handlers.
> > + *
> > + * Most of these functions just forward the ioctl to the host, for these we can
> > + * use one of the SIMPLE_*_IOCTL macros. Exceptions that have their own
> > + * standalone function follow.
> > + */
> > +
> > +SIMPLE_WR_IOCTL(enum_fmt, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc)
> > +SIMPLE_WR_IOCTL(g_fmt, VIDIOC_G_FMT, struct v4l2_format)
> > +SIMPLE_WR_IOCTL(s_fmt, VIDIOC_S_FMT, struct v4l2_format)
> > +SIMPLE_WR_IOCTL(try_fmt, VIDIOC_TRY_FMT, struct v4l2_format)
> > +SIMPLE_WR_IOCTL(enum_framesizes, VIDIOC_ENUM_FRAMESIZES,
> > +		struct v4l2_frmsizeenum)
> > +SIMPLE_WR_IOCTL(enum_frameintervals, VIDIOC_ENUM_FRAMEINTERVALS,
> > +		struct v4l2_frmivalenum)
> > +SIMPLE_WR_IOCTL(query_ext_ctrl, VIDIOC_QUERY_EXT_CTRL,
> > +		struct v4l2_query_ext_ctrl)
> > +SIMPLE_WR_IOCTL(s_dv_timings, VIDIOC_S_DV_TIMINGS, struct v4l2_dv_timings)
> > +SIMPLE_WR_IOCTL(g_dv_timings, VIDIOC_G_DV_TIMINGS, struct v4l2_dv_timings)
> > +SIMPLE_R_IOCTL(query_dv_timings, VIDIOC_QUERY_DV_TIMINGS,
> > +	       struct v4l2_dv_timings)
> > +SIMPLE_WR_IOCTL(enum_dv_timings, VIDIOC_ENUM_DV_TIMINGS,
> > +		struct v4l2_enum_dv_timings)
> > +SIMPLE_WR_IOCTL(dv_timings_cap, VIDIOC_DV_TIMINGS_CAP,
> > +		struct v4l2_dv_timings_cap)
> > +SIMPLE_WR_IOCTL(enuminput, VIDIOC_ENUMINPUT, struct v4l2_input)
> > +SIMPLE_WR_IOCTL(querymenu, VIDIOC_QUERYMENU, struct v4l2_querymenu)
> > +SIMPLE_WR_IOCTL(enumoutput, VIDIOC_ENUMOUTPUT, struct v4l2_output)
> > +SIMPLE_WR_IOCTL(enumaudio, VIDIOC_ENUMAUDIO, struct v4l2_audio)
> > +SIMPLE_R_IOCTL(g_audio, VIDIOC_G_AUDIO, struct v4l2_audio)
> > +SIMPLE_W_IOCTL(s_audio, VIDIOC_S_AUDIO, const struct v4l2_audio)
> > +SIMPLE_WR_IOCTL(enumaudout, VIDIOC_ENUMAUDOUT, struct v4l2_audioout)
> > +SIMPLE_R_IOCTL(g_audout, VIDIOC_G_AUDOUT, struct v4l2_audioout)
> > +SIMPLE_W_IOCTL(s_audout, VIDIOC_S_AUDOUT, const struct v4l2_audioout)
> > +SIMPLE_WR_IOCTL(g_modulator, VIDIOC_G_MODULATOR, struct v4l2_modulator)
> > +SIMPLE_W_IOCTL(s_modulator, VIDIOC_S_MODULATOR, const struct v4l2_modulator)
> > +SIMPLE_WR_IOCTL(g_selection, VIDIOC_G_SELECTION, struct v4l2_selection)
> > +SIMPLE_WR_IOCTL(s_selection, VIDIOC_S_SELECTION, struct v4l2_selection)
> > +SIMPLE_R_IOCTL(g_enc_index, VIDIOC_G_ENC_INDEX, struct v4l2_enc_idx)
> > +SIMPLE_WR_IOCTL(encoder_cmd, VIDIOC_ENCODER_CMD, struct v4l2_encoder_cmd)
> > +SIMPLE_WR_IOCTL(try_encoder_cmd, VIDIOC_TRY_ENCODER_CMD,
> > +		struct v4l2_encoder_cmd)
> > +SIMPLE_WR_IOCTL(try_decoder_cmd, VIDIOC_TRY_DECODER_CMD,
> > +		struct v4l2_decoder_cmd)
> > +SIMPLE_WR_IOCTL(g_parm, VIDIOC_G_PARM, struct v4l2_streamparm)
> > +SIMPLE_WR_IOCTL(s_parm, VIDIOC_S_PARM, struct v4l2_streamparm)
> > +SIMPLE_R_IOCTL(g_std, VIDIOC_G_STD, v4l2_std_id)
> > +SIMPLE_R_IOCTL(querystd, VIDIOC_QUERYSTD, v4l2_std_id)
> > +SIMPLE_WR_IOCTL(enumstd, VIDIOC_ENUMSTD, struct v4l2_standard)
> > +SIMPLE_WR_IOCTL(g_tuner, VIDIOC_G_TUNER, struct v4l2_tuner)
> > +SIMPLE_W_IOCTL(s_tuner, VIDIOC_S_TUNER, const struct v4l2_tuner)
> > +SIMPLE_WR_IOCTL(g_frequency, VIDIOC_G_FREQUENCY, struct v4l2_frequency)
> > +SIMPLE_W_IOCTL(s_frequency, VIDIOC_S_FREQUENCY, const struct v4l2_frequency)
> > +SIMPLE_WR_IOCTL(enum_freq_bands, VIDIOC_ENUM_FREQ_BANDS,
> > +		struct v4l2_frequency_band)
> > +SIMPLE_WR_IOCTL(g_sliced_vbi_cap, VIDIOC_G_SLICED_VBI_CAP,
> > +		struct v4l2_sliced_vbi_cap)
> > +SIMPLE_W_IOCTL(s_hw_freq_seek, VIDIOC_S_HW_FREQ_SEEK,
> > +	       const struct v4l2_hw_freq_seek)
> > +
> > +/*
> > + * QUERYCAP is handled by reading the configuration area.
> > + *
> > + */
> > +
> > +static int virtio_media_querycap(struct file *file, void *fh,
> > +				 struct v4l2_capability *cap)
> > +{
> > +	struct video_device *video_dev = video_devdata(file);
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +
> > +	strscpy(cap->bus_info, "platform:virtio-media");
> > +
> > +	if (!virtio_media_driver_name)
> > +		strscpy(cap->driver, VIRTIO_MEDIA_DEFAULT_DRIVER_NAME);
> > +	else
> > +		strscpy(cap->driver, virtio_media_driver_name);
> > +
> > +	virtio_cread_bytes(vv->virtio_dev, 8, cap->card, sizeof(cap->card));
> > +
> > +	cap->capabilities = video_dev->device_caps | V4L2_CAP_DEVICE_CAPS;
> > +	cap->device_caps = video_dev->device_caps;
> > +
> > +	return 0;
> > +}
> > +
> > +/*
> > + * Extended control ioctls are handled mostly identically.
> > + */
> > +
> > +static int virtio_media_g_ext_ctrls(struct file *file, void *fh,
> > +				    struct v4l2_ext_controls *ctrls)
> > +{
> > +	return virtio_media_send_ext_controls_ioctl(fh, VIDIOC_G_EXT_CTRLS,
> > +						    ctrls);
> > +}
> > +
> > +static int virtio_media_s_ext_ctrls(struct file *file, void *fh,
> > +				    struct v4l2_ext_controls *ctrls)
> > +{
> > +	return virtio_media_send_ext_controls_ioctl(fh, VIDIOC_S_EXT_CTRLS,
> > +						    ctrls);
> > +}
> > +
> > +static int virtio_media_try_ext_ctrls(struct file *file, void *fh,
> > +				      struct v4l2_ext_controls *ctrls)
> > +{
> > +	return virtio_media_send_ext_controls_ioctl(fh, VIDIOC_TRY_EXT_CTRLS,
> > +						    ctrls);
> > +}
> > +
> > +/*
> > + * Subscribe/unsubscribe from an event.
> > + */
> > +
> > +static int
> > +virtio_media_subscribe_event(struct v4l2_fh *fh,
> > +			     const struct v4l2_event_subscription *sub)
> > +{
> > +	struct video_device *video_dev = fh->vdev;
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	int ret;
> > +
> > +	/* First subscribe to the event in the guest. */
> > +	switch (sub->type) {
> > +	case V4L2_EVENT_SOURCE_CHANGE:
> > +		ret = v4l2_src_change_event_subscribe(fh, sub);
> > +		break;
> > +	default:
> > +		ret = v4l2_event_subscribe(fh, sub, 1, NULL);
> > +		break;
> > +	}
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* Then ask the host to signal us these events. */
> > +	ret = virtio_media_send_w_ioctl(fh, VIDIOC_SUBSCRIBE_EVENT, sub,
> > +					sizeof(*sub));
> > +	if (ret < 0) {
> > +		v4l2_event_unsubscribe(fh, sub);
> > +		return ret;
> > +	}
> > +
> > +	/*
> > +	 * Subscribing to an event may result in that event being signaled
> > +	 * immediately. Process all pending events to make sure we don't miss it.
> > +	 */
> > +	if (sub->flags & V4L2_EVENT_SUB_FL_SEND_INITIAL)
> > +		virtio_media_process_events(vv);
> > +
> > +	return 0;
> > +}
> > +
> > +static int
> > +virtio_media_unsubscribe_event(struct v4l2_fh *fh,
> > +			       const struct v4l2_event_subscription *sub)
> > +{
> > +	int ret;
> > +
> > +	ret = virtio_media_send_w_ioctl(fh, VIDIOC_UNSUBSCRIBE_EVENT, sub,
> > +					sizeof(*sub));
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	ret = v4l2_event_unsubscribe(fh, sub);
> > +	if (ret)
> > +		return ret;
> > +
> > +	return 0;
> > +}
> > +
> > +/*
> > + * Streamon/off affect the local queue state.
> > + */
> > +
> > +static int virtio_media_streamon(struct file *file, void *fh,
> > +				 enum v4l2_buf_type i)
> > +{
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	int ret;
> > +
> > +	if (i > VIRTIO_MEDIA_LAST_QUEUE)
> > +		return -EINVAL;
> > +
> > +	ret = virtio_media_send_w_ioctl(fh, VIDIOC_STREAMON, &i, sizeof(i));
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	session->queues[i].streaming = true;
> > +
> > +	return 0;
> > +}
> > +
> > +static int virtio_media_streamoff(struct file *file, void *fh,
> > +				  enum v4l2_buf_type i)
> > +{
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	int ret;
> > +
> > +	if (i > VIRTIO_MEDIA_LAST_QUEUE)
> > +		return -EINVAL;
> > +
> > +	ret = virtio_media_send_w_ioctl(fh, VIDIOC_STREAMOFF, &i, sizeof(i));
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	virtio_media_clear_queue(session, &session->queues[i]);
> > +
> > +	return 0;
> > +}
> > +
> > +/*
> > + * Buffer creation/queuing functions deal with the local driver state.
> > + */
> > +
> > +static int virtio_media_reqbufs(struct file *file, void *fh,
> > +				struct v4l2_requestbuffers *b)
> > +{
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	struct virtio_media_queue_state *queue;
> > +	int ret;
> > +
> > +	if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
> > +		return -EINVAL;
> > +
> > +	if (b->memory == V4L2_MEMORY_USERPTR && !virtio_media_allow_userptr)
> > +		return -EINVAL;
> > +
> > +	ret = virtio_media_send_wr_ioctl(fh, VIDIOC_REQBUFS, b, sizeof(*b),
> > +					 sizeof(*b));
> > +	if (ret)
> > +		return ret;
> > +
> > +	queue = &session->queues[b->type];
> > +
> > +	/* REQBUFS(0) is an implicit STREAMOFF. */
> > +	if (b->count == 0)
> > +		virtio_media_clear_queue(session, queue);
> > +
> > +	vfree(queue->buffers);
> > +	queue->buffers = NULL;
> > +
> > +	if (b->count > 0) {
> > +		queue->buffers =
> > +			vzalloc(sizeof(struct virtio_media_buffer) * b->count);
> > +		if (!queue->buffers)
> > +			return -ENOMEM;
> > +	}
> > +
> > +	queue->allocated_bufs = b->count;
> > +
> > +	/*
> > +	 * If a multiplanar queue is successfully used here, this means
> > +	 * we are using the multiplanar interface.
> > +	 */
> > +	if (V4L2_TYPE_IS_MULTIPLANAR(b->type))
> > +		session->uses_mplane = true;
> > +
> > +	if (!virtio_media_allow_userptr)
> > +		b->capabilities &= ~V4L2_BUF_CAP_SUPPORTS_USERPTR;
> > +
> > +	/* We do not support DMABUF yet. */
> > +	b->capabilities &= ~V4L2_BUF_CAP_SUPPORTS_DMABUF;
> > +
> > +	return 0;
> > +}
> > +
> > +static int virtio_media_querybuf(struct file *file, void *fh,
> > +				 struct v4l2_buffer *b)
> > +{
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	struct virtio_media_queue_state *queue;
> > +	struct virtio_media_buffer *buffer;
> > +	int ret;
> > +
> > +	ret = virtio_media_send_buffer_ioctl(fh, VIDIOC_QUERYBUF, b);
> > +	if (ret)
> > +		return ret;
> > +
> > +	if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
> > +		return -EINVAL;
> > +
> > +	queue = &session->queues[b->type];
> > +	if (b->index >= queue->allocated_bufs)
> > +		return -EINVAL;
> > +
> > +	buffer = &queue->buffers[b->index];
> > +	/* Set the DONE flag if the buffer is waiting in our own dequeue queue. */
> > +	b->flags |= (buffer->buffer.flags & V4L2_BUF_FLAG_DONE);
> > +
> > +	return 0;
> > +}
> > +
> > +static int virtio_media_create_bufs(struct file *file, void *fh,
> > +				    struct v4l2_create_buffers *b)
> > +{
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	struct virtio_media_queue_state *queue;
> > +	struct virtio_media_buffer *buffers;
> > +	u32 type = b->format.type;
> > +	int ret;
> > +
> > +	if (type > VIRTIO_MEDIA_LAST_QUEUE)
> > +		return -EINVAL;
> > +
> > +	queue = &session->queues[type];
> > +
> > +	ret = virtio_media_send_wr_ioctl(fh, VIDIOC_CREATE_BUFS, b, sizeof(*b),
> > +					 sizeof(*b));
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* If count is zero, we were just checking for format. */
> > +	if (b->count == 0)
> > +		return 0;
> > +
> > +	buffers = queue->buffers;
> > +
> > +	queue->buffers =
> > +		vzalloc(sizeof(*queue->buffers) * (b->index + b->count));
> > +	if (!queue->buffers) {
> > +		queue->buffers = buffers;
> > +		return -ENOMEM;
> > +	}
> > +
> > +	memcpy(queue->buffers, buffers,
> > +	       sizeof(*buffers) * queue->allocated_bufs);
> > +	vfree(buffers);
> > +
> > +	queue->allocated_bufs = b->index + b->count;
> > +
> > +	return 0;
> > +}
> > +
> > +static int virtio_media_prepare_buf(struct file *file, void *fh,
> > +				    struct v4l2_buffer *b)
> > +{
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	struct virtio_media_queue_state *queue;
> > +	struct virtio_media_buffer *buffer;
> > +	int i, ret;
> > +
> > +	if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
> > +		return -EINVAL;
> > +	queue = &session->queues[b->type];
> > +	if (b->index >= queue->allocated_bufs)
> > +		return -EINVAL;
> > +	buffer = &queue->buffers[b->index];
> > +
> > +	buffer->buffer.m = b->m;
> > +	if (V4L2_TYPE_IS_MULTIPLANAR(b->type)) {
> > +		if (b->length > VIDEO_MAX_PLANES)
> > +			return -EINVAL;
> > +		for (i = 0; i < b->length; i++)
> > +			buffer->planes[i].m = b->m.planes[i].m;
> > +	}
> > +
> > +	ret = virtio_media_send_buffer_ioctl(fh, VIDIOC_PREPARE_BUF, b);
> > +	if (ret)
> > +		return ret;
> > +
> > +	buffer->buffer.flags = V4L2_BUF_FLAG_PREPARED;
> > +
> > +	return 0;
> > +}
> > +
> > +static int virtio_media_qbuf(struct file *file, void *fh, struct v4l2_buffer *b)
> > +{
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	struct virtio_media_queue_state *queue;
> > +	struct virtio_media_buffer *buffer;
> > +	bool prepared;
> > +	u32 old_flags;
> > +	int i, ret;
> > +
> > +	if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
> > +		return -EINVAL;
> > +	queue = &session->queues[b->type];
> > +	if (b->index >= queue->allocated_bufs)
> > +		return -EINVAL;
> > +	buffer = &queue->buffers[b->index];
> > +	prepared = buffer->buffer.flags & V4L2_BUF_FLAG_PREPARED;
> > +
> > +	/*
> > +	 * Store the buffer and plane `m` information so we can retrieve it again
> > +	 * when DQBUF occurs.
> > +	 */
> > +	if (!prepared) {
> > +		buffer->buffer.m = b->m;
> > +		if (V4L2_TYPE_IS_MULTIPLANAR(b->type)) {
> > +			if (b->length > VIDEO_MAX_PLANES)
> > +				return -EINVAL;
> > +			for (i = 0; i < b->length; i++)
> > +				buffer->planes[i].m = b->m.planes[i].m;
> > +		}
> > +	}
> > +	old_flags = buffer->buffer.flags;
> > +	buffer->buffer.flags = V4L2_BUF_FLAG_QUEUED;
> > +
> > +	ret = virtio_media_send_buffer_ioctl(fh, VIDIOC_QBUF, b);
> > +	if (ret) {
> > +		/* Rollback the previous flags as the buffer is not queued. */
> > +		buffer->buffer.flags = old_flags;
> > +		return ret;
> > +	}
> > +
> > +	queue->queued_bufs += 1;
> > +
> > +	return 0;
> > +}
> > +
> > +static int virtio_media_dqbuf(struct file *file, void *fh,
> > +			      struct v4l2_buffer *b)
> > +{
> > +	struct video_device *video_dev = video_devdata(file);
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct virtio_media_session *session =
> > +		fh_to_session(file->private_data);
> > +	struct virtio_media_buffer *dqbuf;
> > +	struct virtio_media_queue_state *queue;
> > +	struct list_head *buffer_queue;
> > +	struct v4l2_plane *planes_backup = NULL;
> > +	const bool is_multiplanar = V4L2_TYPE_IS_MULTIPLANAR(b->type);
> > +	int ret;
> > +
> > +	if (b->type > VIRTIO_MEDIA_LAST_QUEUE)
> > +		return -EINVAL;
> > +
> > +	queue = &session->queues[b->type];
> > +
> > +	/*
> > +	 * If a buffer with the LAST flag has been returned, subsequent calls to DQBUF
> > +	 * must return -EPIPE until the queue is cleared.
> > +	 */
> > +	if (queue->is_capture_last)
> > +		return -EPIPE;
> > +
> > +	buffer_queue = &queue->pending_dqbufs;
> > +
> > +	if (session->nonblocking_dequeue) {
> > +		if (list_empty(buffer_queue))
> > +			return -EAGAIN;
> > +	} else if (queue->allocated_bufs == 0) {
> > +		return -EINVAL;
> > +	} else if (!queue->streaming) {
> > +		return -EINVAL;
> > +	}
> > +
> > +	/*
> > +	 * vv->lock has been acquired by virtio_media_device_ioctl. Release it
> > +	 * while we want to other ioctls for this session can be processed and
> > +	 * potentially trigger dqbuf_wait.
> > +	 */
> > +	mutex_unlock(&vv->vlock);
> > +	ret = wait_event_interruptible(session->dqbuf_wait,
> > +				       !list_empty(buffer_queue));
> > +	mutex_lock(&vv->vlock);
> > +	if (ret)
> > +		return -EINTR;
> > +
> > +	mutex_lock(&session->queues_lock);
> > +	dqbuf = list_first_entry(buffer_queue, struct virtio_media_buffer,
> > +				 list);
> > +	list_del(&dqbuf->list);
> > +	mutex_unlock(&session->queues_lock);
> > +
> > +	/* Clear the DONE flag as the buffer is now being dequeued. */
> > +	dqbuf->buffer.flags &= ~V4L2_BUF_FLAG_DONE;
> > +
> > +	if (is_multiplanar) {
> > +		size_t nb_planes = min_t(u32, b->length, VIDEO_MAX_PLANES);
> > +
> > +		memcpy(b->m.planes, dqbuf->planes,
> > +		       nb_planes * sizeof(struct v4l2_plane));
> > +		planes_backup = b->m.planes;
> > +	}
> > +
> > +	memcpy(b, &dqbuf->buffer, sizeof(*b));
> > +
> > +	if (is_multiplanar)
> > +		b->m.planes = planes_backup;
> > +
> > +	if (V4L2_TYPE_IS_CAPTURE(b->type) && b->flags & V4L2_BUF_FLAG_LAST)
> > +		queue->is_capture_last = true;
> > +
> > +	return 0;
> > +}
> > +
> > +/*
> > + * s/g_input/output work with an unsigned int - recast this to a u32 so the
> > + * size is unambiguous.
> > + */
> > +
> > +static int virtio_media_g_input(struct file *file, void *fh, unsigned int *i)
> > +{
> > +	u32 input;
> > +	int ret;
> > +
> > +	ret = virtio_media_send_wr_ioctl(fh, VIDIOC_G_INPUT, &input,
> > +					 sizeof(input), sizeof(input));
> > +	if (ret)
> > +		return ret;
> > +
> > +	*i = input;
> > +
> > +	return 0;
> > +}
> > +
> > +static int virtio_media_s_input(struct file *file, void *fh, unsigned int i)
> > +{
> > +	u32 input = i;
> > +
> > +	return virtio_media_send_wr_ioctl(fh, VIDIOC_S_INPUT, &input,
> > +					  sizeof(input), sizeof(input));
> > +}
> > +
> > +static int virtio_media_g_output(struct file *file, void *fh, unsigned int *o)
> > +{
> > +	u32 output;
> > +	int ret;
> > +
> > +	ret = virtio_media_send_wr_ioctl(fh, VIDIOC_G_OUTPUT, &output,
> > +					 sizeof(output), sizeof(output));
> > +	if (ret)
> > +		return ret;
> > +
> > +	*o = output;
> > +
> > +	return 0;
> > +}
> > +
> > +static int virtio_media_s_output(struct file *file, void *fh, unsigned int o)
> > +{
> > +	u32 output = o;
> > +
> > +	return virtio_media_send_wr_ioctl(fh, VIDIOC_S_OUTPUT, &output,
> > +					  sizeof(output), sizeof(output));
> > +}
> > +
> > +/*
> > + * decoder_cmd can affect the state of the CAPTURE queue.
> > + */
> > +
> > +static int virtio_media_decoder_cmd(struct file *file, void *fh,
> > +				    struct v4l2_decoder_cmd *cmd)
> > +{
> > +	struct virtio_media_session *session = fh_to_session(fh);
> > +	int ret;
> > +
> > +	ret = virtio_media_send_wr_ioctl(fh, VIDIOC_DECODER_CMD, cmd,
> > +					 sizeof(*cmd), sizeof(*cmd));
> > +	if (ret)
> > +		return ret;
> > +
> > +	/* A START command makes the CAPTURE queue able to dequeue again. */
> > +	if (cmd->cmd == V4L2_DEC_CMD_START) {
> > +		session->queues[V4L2_BUF_TYPE_VIDEO_CAPTURE].is_capture_last =
> > +			false;
> > +		session->queues[V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE]
> > +			.is_capture_last = false;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/*
> > + * s_std doesn't work with a pointer, so we cannot use SIMPLE_W_IOCTL.
> > + */
> > +
> > +static int virtio_media_s_std(struct file *file, void *fh, v4l2_std_id s)
> > +{
> > +	int ret;
> > +
> > +	ret = virtio_media_send_w_ioctl(fh, VIDIOC_S_STD, &s, sizeof(s));
> > +	if (ret)
> > +		return ret;
> > +
> > +	return 0;
> > +}
> > +
> > +const struct v4l2_ioctl_ops virtio_media_ioctl_ops = {
> > +	/* VIDIOC_QUERYCAP handler */
> > +	.vidioc_querycap = virtio_media_querycap,
> > +
> > +	/* VIDIOC_ENUM_FMT handlers */
> > +	.vidioc_enum_fmt_vid_cap = virtio_media_enum_fmt,
> > +	.vidioc_enum_fmt_vid_overlay = virtio_media_enum_fmt,
> > +	.vidioc_enum_fmt_vid_out = virtio_media_enum_fmt,
> > +	.vidioc_enum_fmt_sdr_cap = virtio_media_enum_fmt,
> > +	.vidioc_enum_fmt_sdr_out = virtio_media_enum_fmt,
> > +	.vidioc_enum_fmt_meta_cap = virtio_media_enum_fmt,
> > +	.vidioc_enum_fmt_meta_out = virtio_media_enum_fmt,
> > +
> > +	/* VIDIOC_G_FMT handlers */
> > +	.vidioc_g_fmt_vid_cap = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_vid_overlay = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_vid_out = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_vid_out_overlay = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_vbi_cap = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_vbi_out = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_sliced_vbi_cap = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_sliced_vbi_out = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_vid_cap_mplane = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_vid_out_mplane = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_sdr_cap = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_sdr_out = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_meta_cap = virtio_media_g_fmt,
> > +	.vidioc_g_fmt_meta_out = virtio_media_g_fmt,
> > +
> > +	/* VIDIOC_S_FMT handlers */
> > +	.vidioc_s_fmt_vid_cap = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_vid_overlay = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_vid_out = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_vid_out_overlay = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_vbi_cap = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_vbi_out = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_sliced_vbi_cap = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_sliced_vbi_out = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_vid_cap_mplane = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_vid_out_mplane = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_sdr_cap = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_sdr_out = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_meta_cap = virtio_media_s_fmt,
> > +	.vidioc_s_fmt_meta_out = virtio_media_s_fmt,
> > +
> > +	/* VIDIOC_TRY_FMT handlers */
> > +	.vidioc_try_fmt_vid_cap = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_vid_overlay = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_vid_out = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_vid_out_overlay = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_vbi_cap = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_vbi_out = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_sliced_vbi_cap = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_sliced_vbi_out = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_vid_cap_mplane = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_vid_out_mplane = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_sdr_cap = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_sdr_out = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_meta_cap = virtio_media_try_fmt,
> > +	.vidioc_try_fmt_meta_out = virtio_media_try_fmt,
> > +
> > +	/* Buffer handlers */
> > +	.vidioc_reqbufs = virtio_media_reqbufs,
> > +	.vidioc_querybuf = virtio_media_querybuf,
> > +	.vidioc_qbuf = virtio_media_qbuf,
> > +	.vidioc_expbuf = NULL,
> > +	.vidioc_dqbuf = virtio_media_dqbuf,
> > +	.vidioc_create_bufs = virtio_media_create_bufs,
> > +	.vidioc_prepare_buf = virtio_media_prepare_buf,
> > +	/* Overlay interface not supported yet */
> > +	.vidioc_overlay = NULL,
> > +	/* Overlay interface not supported yet */
> > +	.vidioc_g_fbuf = NULL,
> > +	/* Overlay interface not supported yet */
> > +	.vidioc_s_fbuf = NULL,
> > +
> > +	/* Stream on/off */
> > +	.vidioc_streamon = virtio_media_streamon,
> > +	.vidioc_streamoff = virtio_media_streamoff,
> > +
> > +	/* Standard handling */
> > +	.vidioc_g_std = virtio_media_g_std,
> > +	.vidioc_s_std = virtio_media_s_std,
> > +	.vidioc_querystd = virtio_media_querystd,
> > +
> > +	/* Input handling */
> > +	.vidioc_enum_input = virtio_media_enuminput,
> > +	.vidioc_g_input = virtio_media_g_input,
> > +	.vidioc_s_input = virtio_media_s_input,
> > +
> > +	/* Output handling */
> > +	.vidioc_enum_output = virtio_media_enumoutput,
> > +	.vidioc_g_output = virtio_media_g_output,
> > +	.vidioc_s_output = virtio_media_s_output,
> > +
> > +	/* Control handling */
> > +	.vidioc_query_ext_ctrl = virtio_media_query_ext_ctrl,
> > +	.vidioc_g_ext_ctrls = virtio_media_g_ext_ctrls,
> > +	.vidioc_s_ext_ctrls = virtio_media_s_ext_ctrls,
> > +	.vidioc_try_ext_ctrls = virtio_media_try_ext_ctrls,
> > +	.vidioc_querymenu = virtio_media_querymenu,
> > +
> > +	/* Audio ioctls */
> > +	.vidioc_enumaudio = virtio_media_enumaudio,
> > +	.vidioc_g_audio = virtio_media_g_audio,
> > +	.vidioc_s_audio = virtio_media_s_audio,
> > +
> > +	/* Audio out ioctls */
> > +	.vidioc_enumaudout = virtio_media_enumaudout,
> > +	.vidioc_g_audout = virtio_media_g_audout,
> > +	.vidioc_s_audout = virtio_media_s_audout,
> > +	.vidioc_g_modulator = virtio_media_g_modulator,
> > +	.vidioc_s_modulator = virtio_media_s_modulator,
> > +
> > +	/* Crop ioctls */
> > +	/* Not directly an ioctl (part of VIDIOC_CROPCAP), so no need to implement */
> > +	.vidioc_g_pixelaspect = NULL,
> > +	.vidioc_g_selection = virtio_media_g_selection,
> > +	.vidioc_s_selection = virtio_media_s_selection,
> > +
> > +	/* Compression ioctls */
> > +	/* Deprecated in V4L2. */
> > +	.vidioc_g_jpegcomp = NULL,
> > +	/* Deprecated in V4L2. */
> > +	.vidioc_s_jpegcomp = NULL,
> > +	.vidioc_g_enc_index = virtio_media_g_enc_index,
> > +	.vidioc_encoder_cmd = virtio_media_encoder_cmd,
> > +	.vidioc_try_encoder_cmd = virtio_media_try_encoder_cmd,
> > +	.vidioc_decoder_cmd = virtio_media_decoder_cmd,
> > +	.vidioc_try_decoder_cmd = virtio_media_try_decoder_cmd,
> > +
> > +	/* Stream type-dependent parameter ioctls */
> > +	.vidioc_g_parm = virtio_media_g_parm,
> > +	.vidioc_s_parm = virtio_media_s_parm,
> > +
> > +	/* Tuner ioctls */
> > +	.vidioc_g_tuner = virtio_media_g_tuner,
> > +	.vidioc_s_tuner = virtio_media_s_tuner,
> > +	.vidioc_g_frequency = virtio_media_g_frequency,
> > +	.vidioc_s_frequency = virtio_media_s_frequency,
> > +	.vidioc_enum_freq_bands = virtio_media_enum_freq_bands,
> > +
> > +	/* Sliced VBI cap */
> > +	.vidioc_g_sliced_vbi_cap = virtio_media_g_sliced_vbi_cap,
> > +
> > +	/* Log status ioctl */
> > +	/* Guest-only operation */
> > +	.vidioc_log_status = NULL,
> > +
> > +	.vidioc_s_hw_freq_seek = virtio_media_s_hw_freq_seek,
> > +
> > +	.vidioc_enum_framesizes = virtio_media_enum_framesizes,
> > +	.vidioc_enum_frameintervals = virtio_media_enum_frameintervals,
> > +
> > +	/* DV Timings IOCTLs */
> > +	.vidioc_s_dv_timings = virtio_media_s_dv_timings,
> > +	.vidioc_g_dv_timings = virtio_media_g_dv_timings,
> > +	.vidioc_query_dv_timings = virtio_media_query_dv_timings,
> > +	.vidioc_enum_dv_timings = virtio_media_enum_dv_timings,
> > +	.vidioc_dv_timings_cap = virtio_media_dv_timings_cap,
> > +	.vidioc_g_edid = NULL,
> > +	.vidioc_s_edid = NULL,
> > +
> > +	.vidioc_subscribe_event = virtio_media_subscribe_event,
> > +	.vidioc_unsubscribe_event = virtio_media_unsubscribe_event,
> > +
> > +	/* For other private ioctls */
> > +	.vidioc_default = NULL,
> > +};
> > +
> > +long virtio_media_device_ioctl(struct file *file, unsigned int cmd,
> > +			       unsigned long arg)
> > +{
> > +	struct video_device *video_dev = video_devdata(file);
> > +	struct virtio_media *vv = to_virtio_media(video_dev);
> > +	struct v4l2_fh *vfh = NULL;
> > +	struct v4l2_standard standard;
> > +	v4l2_std_id std_id = 0;
> > +	int ret;
> > +
> > +	if (test_bit(V4L2_FL_USES_V4L2_FH, &video_dev->flags))
> > +		vfh = file->private_data;
> > +
> > +	mutex_lock(&vv->vlock);
> > +
> > +	/*
> > +	 * We need to handle a few ioctls manually because their result rely on
> > +	 * vfd->tvnorms, which is normally updated by the driver as S_INPUT is
> > +	 * called. Since we want to just pass these ioctls through, we have to hijack
> > +	 * them from here.
> > +	 */
> > +	switch (cmd) {
> > +	case VIDIOC_S_STD:
> > +		ret = copy_from_user(&std_id, (void __user *)arg,
> > +				     sizeof(std_id));
> > +		if (ret) {
> > +			ret = -EINVAL;
> > +			break;
> > +		}
> > +		ret = virtio_media_s_std(file, vfh, std_id);
> > +		break;
> > +	case VIDIOC_ENUMSTD:
> > +		ret = copy_from_user(&standard, (void __user *)arg,
> > +				     sizeof(standard));
> > +		if (ret) {
> > +			ret = -EINVAL;
> > +			break;
> > +		}
> > +		ret = virtio_media_enumstd(file, vfh, &standard);
> > +		if (ret)
> > +			break;
> > +		ret = copy_to_user((void __user *)arg, &standard,
> > +				   sizeof(standard));
> > +		if (ret)
> > +			ret = -EINVAL;
> > +		break;
> > +	case VIDIOC_QUERYSTD:
> > +		ret = virtio_media_querystd(file, vfh, &std_id);
> > +		if (ret)
> > +			break;
> > +		ret = copy_to_user((void __user *)arg, &std_id, sizeof(std_id));
> > +		if (ret)
> > +			ret = -EINVAL;
> > +		break;
> > +	default:
> > +		ret = video_ioctl2(file, cmd, arg);
> > +		break;
> > +	}
> > +
> > +	mutex_unlock(&vv->vlock);
> > +
> > +	return ret;
> > +}
> > diff --git a/include/uapi/linux/virtio_ids.h b/include/uapi/linux/virtio_ids.h
> > index 7aa2eb76620508fdc915533f74973d76308d3ef5..b4bb0ace0b26e37224c975f89bbf669c51921816 100644
> > --- a/include/uapi/linux/virtio_ids.h
> > +++ b/include/uapi/linux/virtio_ids.h
> > @@ -68,6 +68,7 @@
> >  #define VIRTIO_ID_AUDIO_POLICY		39 /* virtio audio policy */
> >  #define VIRTIO_ID_BT			40 /* virtio bluetooth */
> >  #define VIRTIO_ID_GPIO			41 /* virtio gpio */
> > +#define VIRTIO_ID_MEDIA			48 /* virtio media */
> >  
> >  /*
> >   * Virtio Transitional IDs
> > 
> > ---
> > base-commit: 0af2f6be1b4281385b618cb86ad946eded089ac8
> > change-id: 20241229-virtio-media-25067bb27526
> > 
> > Best regards,
> 
> 
> 
> Thanks,
> Mauro