[RFC PATCH] media: Virtual camera driver

Jarkko Sakkinen posted 1 patch 5 days, 22 hours ago
There is a newer version of this series
.../driver-api/media/drivers/index.rst        |    1 +
.../driver-api/media/drivers/vcam.rst         |   16 +
MAINTAINERS                                   |    8 +
drivers/media/Kconfig                         |   13 +
drivers/media/Makefile                        |    1 +
drivers/media/vcam.c                          | 1700 +++++++++++++++++
include/uapi/linux/vcam.h                     |  124 ++
7 files changed, 1863 insertions(+)
create mode 100644 Documentation/driver-api/media/drivers/vcam.rst
create mode 100644 drivers/media/vcam.c
create mode 100644 include/uapi/linux/vcam.h
[RFC PATCH] media: Virtual camera driver
Posted by Jarkko Sakkinen 5 days, 22 hours ago
vcam is a DMA-BUF backed virtual camera driver capable of creating video
capture devices to which data can be streamed through /dev/vcam after
calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
with VCAM_IOC_DEQUEUE.

Zero-copy semantics are supported for shared DMA-BUF between capture and
output.

Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
---
Early feedback e.g., is this completely in wrong direction? V4L2 world
is relatively alien world, and thus I need a sanity check ;-)
 .../driver-api/media/drivers/index.rst        |    1 +
 .../driver-api/media/drivers/vcam.rst         |   16 +
 MAINTAINERS                                   |    8 +
 drivers/media/Kconfig                         |   13 +
 drivers/media/Makefile                        |    1 +
 drivers/media/vcam.c                          | 1700 +++++++++++++++++
 include/uapi/linux/vcam.h                     |  124 ++
 7 files changed, 1863 insertions(+)
 create mode 100644 Documentation/driver-api/media/drivers/vcam.rst
 create mode 100644 drivers/media/vcam.c
 create mode 100644 include/uapi/linux/vcam.h

diff --git a/Documentation/driver-api/media/drivers/index.rst b/Documentation/driver-api/media/drivers/index.rst
index 7f6f3dcd5c90..211cafc9c070 100644
--- a/Documentation/driver-api/media/drivers/index.rst
+++ b/Documentation/driver-api/media/drivers/index.rst
@@ -27,6 +27,7 @@ Video4Linux (V4L) drivers
 	zoran
 	ccs/ccs
 	ipu6
+	vcam
 
 
 Digital TV drivers
diff --git a/Documentation/driver-api/media/drivers/vcam.rst b/Documentation/driver-api/media/drivers/vcam.rst
new file mode 100644
index 000000000000..b5a23144ebee
--- /dev/null
+++ b/Documentation/driver-api/media/drivers/vcam.rst
@@ -0,0 +1,16 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+===========================
+vcam: Virtual Camera Driver
+===========================
+
+Theory of Operation
+-------------------
+
+.. kernel-doc:: drivers/media/vcam.c
+   :doc: Theory of Operation
+
+Driver uAPI
+-----------
+
+.. kernel-doc:: include/uapi/linux/vcam.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 6863d5fa07a1..b8444ff48716 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27504,6 +27504,14 @@ S:	Maintained
 F:	drivers/media/common/videobuf2/*
 F:	include/media/videobuf2-*
 
+VCAM V4L2 DRIVER
+M:	Jarkko Sakkinen <jarkko@kernel.org>
+L:	linux-media@vger.kernel.org
+S:	Maintained
+T:	git git://git.kernel.org/pub/scm/linux/kernel/git/jarkko/linux-tpmdd.git
+F:	drivers/media/vcam.c
+F:	include/uapi/linux/vcam.h
+
 VIDTV VIRTUAL DIGITAL TV DRIVER
 M:	Daniel W. S. Almeida <dwlsalmeida@gmail.com>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/media/Kconfig b/drivers/media/Kconfig
index 6abc9302cd84..f2f4b2ec9135 100644
--- a/drivers/media/Kconfig
+++ b/drivers/media/Kconfig
@@ -239,6 +239,19 @@ source "drivers/media/firewire/Kconfig"
 # Common driver options
 source "drivers/media/common/Kconfig"
 
+config VCAM
+	tristate "V4L2 virtual camera"
+	depends on VIDEO_DEV
+	default m
+	select VIDEOBUF2_VMALLOC
+	help
+	  Say Y here to enable a DMA-BUF backed virtual camera driver capable
+	  of creating video capture devices to which data can be streamed
+	  through /dev/vcam after calling VCAM_IOC_CREATE. Frames are pushed
+	  with VCAM_IOC_QUEUE and recycled with VCAM_IOC_DEQUEUE.
+
+	  When in doubt, say N.
+
 endmenu
 
 #
diff --git a/drivers/media/Makefile b/drivers/media/Makefile
index 20fac24e4f0f..d539fecbe498 100644
--- a/drivers/media/Makefile
+++ b/drivers/media/Makefile
@@ -32,3 +32,4 @@ obj-$(CONFIG_CEC_CORE) += cec/
 obj-y += common/ platform/ pci/ usb/ mmc/ firewire/ spi/ test-drivers/
 obj-$(CONFIG_VIDEO_DEV) += radio/
 
+obj-$(CONFIG_VCAM) += vcam.o
diff --git a/drivers/media/vcam.c b/drivers/media/vcam.c
new file mode 100644
index 000000000000..82f4351d0499
--- /dev/null
+++ b/drivers/media/vcam.c
@@ -0,0 +1,1700 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) Jarkko Sakkinen 2025-2026
+ *
+ * Derived originally from v4l2loopback driver but is essentially a rewrite.
+ */
+
+/**
+ * DOC: Theory of Operation
+ *
+ * The driver exposes /dev/vcam for creating virtual capture devices via
+ * %VCAM_IOC_CREATE. The ioctl registers a video capture node and associates
+ * output buffers described by &struct vcam_frame with DMA-BUF file descriptors
+ * supplied by the caller. This also keeps output buffers owned by the caller,
+ * and accounted from the calling process.
+ *
+ * Frames are pushed to the capture device by queueing output buffers using
+ * %VCAM_IOC_QUEUE, and recycling them with %VCAM_IOC_DEQUEUE. Queueing without
+ * dequeuing eventually exhausts the output queue and stalls the producer.
+ *
+ * If both buffers reference the same DMA-BUF, the driver performs a zero-copy
+ * transfer by propagating metadata. Otherwise, if both buffers are mappable,
+ * the payload is copied into the capture buffer. When neither zero-copy nor a
+ * CPU mapping is possible, the capture buffer completes with an error.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/bitops.h>
+#include <linux/atomic.h>
+#include <linux/ctype.h>
+#include <linux/compat.h>
+#include <linux/dma-buf.h>
+#include <linux/dma-mapping.h>
+#include <linux/fdtable.h>
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/limits.h>
+#include <linux/device.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/miscdevice.h>
+#include <linux/poll.h>
+#include <linux/sched.h>
+#include <linux/time.h>
+#include <linux/time64.h>
+#include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/spinlock.h>
+#include <linux/sysfs.h>
+#include <linux/time.h>
+#include <linux/videodev2.h>
+#include <linux/wait.h>
+#include <media/v4l2-common.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>
+#include <media/videobuf2-v4l2.h>
+#include <media/videobuf2-vmalloc.h>
+#include <uapi/linux/vcam.h>
+
+#undef pr_fmt
+#define pr_fmt(fmt) "vcam: " fmt
+
+MODULE_DESCRIPTION("V4L2 virtual camera driver");
+MODULE_LICENSE("GPL");
+
+#define VCAM_CARD_LABEL_MAX sizeof_field(struct video_device, name)
+#define VCAM_FPS_MIN 1
+#define VCAM_FPS_MAX 1000
+
+#define VCAM_MIN_WIDTH 2
+#define VCAM_MIN_HEIGHT 2
+#define VCAM_MAX_WIDTH 8192
+#define VCAM_MAX_HEIGHT 8192
+#define VCAM_DEFAULT_WIDTH 640
+#define VCAM_DEFAULT_HEIGHT 480
+
+#define VCAM_MAX_FORMATS 16
+#define VCAM_MIN_FRAMES 2
+#define VCAM_MAX_FRAMES 32
+
+#define VCAM_STATUS_MASK (VCAM_STATUS_IDLE | VCAM_STATUS_STREAMING)
+
+enum vcam_flags {
+	VCAM_FLAG_IS_OPEN = 0x01,
+	VCAM_FLAG_CREATING = 0x02,
+	VCAM_FLAG_READY = 0x04,
+};
+
+struct vcam_buf {
+	struct vb2_v4l2_buffer vb;
+	struct list_head list;
+	unsigned long flags;
+};
+
+enum vcam_buf_flags {
+	VCAM_BUF_FLAG_MAPPABLE = BIT(0),
+};
+
+struct vcam {
+	unsigned long flags;
+	int device_nr;
+	struct v4l2_device v4l2_dev;
+	struct video_device *vdev;
+	struct vb2_queue capture_queue;
+	struct vb2_queue output_queue;
+	struct v4l2_pix_format pix_format;
+	struct v4l2_captureparm capture;
+	atomic_t sequence;
+	struct list_head capture_list;
+	struct list_head output_list;
+	u64 status;
+	wait_queue_head_t status_waitq;
+	enum vb2_memory output_memory;
+
+	/* Protects status flags and wait queue updates. */
+	spinlock_t status_lock;
+
+	/* Shared lock for vdev and VB2 queues. */
+	struct mutex lock;
+
+	/* Protects capture_list and output_list. */
+	spinlock_t frame_lock;
+
+	/*
+	 * Maintains a shared reference between processes having either
+	 * /dev/vcam or /dev/videoX open.
+	 */
+	struct kref ref;
+};
+
+enum vcam_format_flags {
+	VCAM_PLANAR = BIT(0),
+	VCAM_COMPRESSED = BIT(1),
+};
+
+struct vcam_format {
+	int fourcc;
+	int depth;
+	int flags;
+};
+
+const struct vcam_format vcam_formats[] = {
+	{
+		.fourcc = V4L2_PIX_FMT_YUYV,
+		.depth = 16,
+		.flags = 0,
+	},
+	{
+		.fourcc = V4L2_PIX_FMT_NV12,
+		.depth = 12,
+		.flags = VCAM_PLANAR,
+	},
+	{
+		.fourcc = V4L2_PIX_FMT_MJPEG,
+		.depth = 32,
+		.flags = VCAM_COMPRESSED,
+	},
+};
+
+#define VCAM_NR_FORMATS ARRAY_SIZE(vcam_formats)
+
+static const struct vcam_format *vcam_find_format(int fourcc)
+{
+	unsigned int i;
+
+	for (i = 0; i < VCAM_NR_FORMATS; i++) {
+		if (vcam_formats[i].fourcc == fourcc)
+			return vcam_formats + i;
+	}
+
+	return NULL;
+}
+
+static void vcam_fmt_descr(char *dst, size_t dst_len, u32 format)
+{
+	snprintf(dst, dst_len, "[%c%c%c%c]", (format >> 0) & 0xFF,
+		 (format >> 8) & 0xFF, (format >> 16) & 0xFF,
+		 (format >> 24) & 0xFF);
+}
+
+static void vcam_fourcc_str(char *dst, u32 format)
+{
+	dst[0] = (format >> 0) & 0xFF;
+	dst[1] = (format >> 8) & 0xFF;
+	dst[2] = (format >> 16) & 0xFF;
+	dst[3] = (format >> 24) & 0xFF;
+	dst[4] = '\0';
+}
+
+static inline bool vcam_is_streaming(struct vcam *data)
+{
+	return vb2_is_streaming(&data->output_queue) ||
+	       vb2_is_streaming(&data->capture_queue);
+}
+
+static bool vcam_status_mask_ready(struct vcam *dev, u64 mask)
+{
+	unsigned long flags;
+	bool ready;
+
+	spin_lock_irqsave(&dev->status_lock, flags);
+	ready = (dev->status & mask) == mask;
+	spin_unlock_irqrestore(&dev->status_lock, flags);
+
+	return ready;
+}
+
+static void vcam_status_update_stream(struct vcam *dev, bool on)
+{
+	unsigned long flags;
+	u64 old_flags;
+	u64 new_flags;
+
+	spin_lock_irqsave(&dev->status_lock, flags);
+	old_flags = dev->status;
+	if (on) {
+		dev->status &= ~VCAM_STATUS_IDLE;
+		dev->status |= VCAM_STATUS_STREAMING;
+	} else {
+		dev->status &= ~VCAM_STATUS_STREAMING;
+		dev->status |= VCAM_STATUS_IDLE;
+	}
+	new_flags = dev->status;
+	spin_unlock_irqrestore(&dev->status_lock, flags);
+
+	if (new_flags != old_flags)
+		wake_up_interruptible(&dev->status_waitq);
+}
+
+static u64 vcam_status_read(struct vcam *dev)
+{
+	unsigned long flags;
+	u64 flags_snapshot;
+
+	spin_lock_irqsave(&dev->status_lock, flags);
+	flags_snapshot = dev->status;
+	spin_unlock_irqrestore(&dev->status_lock, flags);
+
+	return flags_snapshot;
+}
+
+static bool vcam_tpf_valid(const struct v4l2_fract *tpf)
+{
+	u64 min_den = (u64)tpf->numerator * VCAM_FPS_MIN;
+	u64 max_den = (u64)tpf->numerator * VCAM_FPS_MAX;
+
+	if (!tpf->numerator || !tpf->denominator)
+		return false;
+	if ((u64)tpf->denominator < min_den)
+		return false;
+	if ((u64)tpf->denominator > max_den)
+		return false;
+
+	return true;
+}
+
+static bool vcam_pix_format_eq(const struct v4l2_pix_format *src,
+			       const struct v4l2_pix_format *dest)
+{
+	return src->width == dest->width && src->height == dest->height &&
+	       src->pixelformat == dest->pixelformat;
+}
+
+static int vcam_set_format(struct vcam *dev, struct v4l2_format *fmt)
+{
+	struct v4l2_pix_format *pix = &fmt->fmt.pix;
+	const struct vcam_format *format;
+	u64 bytesperline;
+	u64 sizeimage;
+
+	if (V4L2_TYPE_IS_MULTIPLANAR(fmt->type))
+		return -EINVAL;
+
+	if (!pix->width)
+		pix->width = VCAM_DEFAULT_WIDTH;
+	if (!pix->height)
+		pix->height = VCAM_DEFAULT_HEIGHT;
+
+	pix->width = clamp(pix->width, VCAM_MIN_WIDTH, VCAM_MAX_WIDTH);
+	pix->height = clamp(pix->height, VCAM_MIN_HEIGHT, VCAM_MAX_HEIGHT);
+
+	format = vcam_find_format(pix->pixelformat);
+	if (!format) {
+		format = &vcam_formats[0];
+		pix->pixelformat = format->fourcc;
+	}
+
+	if (format->flags & VCAM_PLANAR) {
+		pix->bytesperline = pix->width;
+		sizeimage = ((u64)pix->width * pix->height * format->depth) >>
+			    3;
+	} else if (format->flags & VCAM_COMPRESSED) {
+		pix->bytesperline = 0;
+		sizeimage = ((u64)pix->width * pix->height * format->depth) >>
+			    3;
+	} else {
+		bytesperline = ((u64)pix->width * format->depth) >> 3;
+		if (bytesperline > U32_MAX)
+			return -EOVERFLOW;
+
+		pix->bytesperline = bytesperline;
+		sizeimage = (u64)pix->height * bytesperline;
+	}
+
+	if (sizeimage > U32_MAX)
+		return -EOVERFLOW;
+
+	pix->sizeimage = sizeimage;
+
+	if (pix->colorspace == V4L2_COLORSPACE_DEFAULT ||
+	    pix->colorspace > V4L2_COLORSPACE_DCI_P3)
+		pix->colorspace = V4L2_COLORSPACE_SRGB;
+	if (pix->field == V4L2_FIELD_ANY)
+		pix->field = V4L2_FIELD_NONE;
+
+	return 0;
+}
+
+static int vcam_vidioc_querycap(struct file *file, void *priv,
+				struct v4l2_capability *cap)
+{
+	__u32 capabilities = V4L2_CAP_STREAMING | V4L2_CAP_VIDEO_CAPTURE;
+	struct vcam *dev = video_drvdata(file);
+
+	cap->device_caps = capabilities;
+	cap->capabilities = capabilities | V4L2_CAP_DEVICE_CAPS;
+
+	strscpy(cap->driver, "vcam", sizeof(cap->driver));
+	strscpy(cap->card, dev->vdev->name, sizeof(cap->card));
+	snprintf(cap->bus_info, sizeof(cap->bus_info), "vcam:%d",
+		 dev->device_nr);
+
+	return 0;
+}
+
+static int vcam_enum_framesizes(struct vcam *dev, struct v4l2_frmsizeenum *argp)
+{
+	if (argp->index)
+		return -EINVAL;
+
+	if (vcam_is_streaming(dev)) {
+		if (argp->pixel_format != dev->pix_format.pixelformat)
+			return -EINVAL;
+
+		argp->type = V4L2_FRMSIZE_TYPE_DISCRETE;
+
+		argp->discrete.width = dev->pix_format.width;
+		argp->discrete.height = dev->pix_format.height;
+	} else {
+		if (!vcam_find_format(argp->pixel_format))
+			return -EINVAL;
+
+		argp->type = V4L2_FRMSIZE_TYPE_CONTINUOUS;
+
+		argp->stepwise.min_width = VCAM_MIN_WIDTH;
+		argp->stepwise.min_height = VCAM_MIN_HEIGHT;
+		argp->stepwise.max_width = VCAM_MAX_WIDTH;
+		argp->stepwise.max_height = VCAM_MAX_HEIGHT;
+		argp->stepwise.step_width = 1;
+		argp->stepwise.step_height = 1;
+	}
+
+	return 0;
+}
+
+static int vcam_enum_frameintervals(struct vcam *dev,
+				    struct v4l2_frmivalenum *argp)
+{
+	if (argp->index)
+		return -EINVAL;
+
+	if (vcam_is_streaming(dev)) {
+		if (argp->width != dev->pix_format.width ||
+		    argp->height != dev->pix_format.height ||
+		    argp->pixel_format != dev->pix_format.pixelformat)
+			return -EINVAL;
+
+		argp->type = V4L2_FRMIVAL_TYPE_DISCRETE;
+		argp->discrete = dev->capture.timeperframe;
+	} else {
+		if (argp->width < VCAM_MIN_WIDTH ||
+		    argp->width > VCAM_MAX_WIDTH ||
+		    argp->height < VCAM_MIN_HEIGHT ||
+		    argp->height > VCAM_MAX_HEIGHT ||
+		    !vcam_find_format(argp->pixel_format))
+			return -EINVAL;
+
+		argp->type = V4L2_FRMIVAL_TYPE_CONTINUOUS;
+		argp->stepwise.min.numerator = 1;
+		argp->stepwise.min.denominator = VCAM_FPS_MAX;
+		argp->stepwise.max.numerator = 1;
+		argp->stepwise.max.denominator = VCAM_FPS_MIN;
+		argp->stepwise.step.numerator = 1;
+		argp->stepwise.step.denominator = 1;
+	}
+
+	return 0;
+}
+
+static int vcam_vidioc_enum_framesizes(struct file *file, void *fh,
+				       struct v4l2_frmsizeenum *argp)
+{
+	struct vcam *dev = video_drvdata(file);
+
+	return vcam_enum_framesizes(dev, argp);
+}
+
+static int vcam_vidioc_enum_frameintervals(struct file *file, void *fh,
+					   struct v4l2_frmivalenum *argp)
+{
+	struct vcam *dev = video_drvdata(file);
+
+	return vcam_enum_frameintervals(dev, argp);
+}
+
+static int vcam_vidioc_enum_fmt_cap(struct file *file, void *fh,
+				    struct v4l2_fmtdesc *f)
+{
+	struct vcam *dev;
+
+	dev = video_drvdata(file);
+
+	if (vcam_is_streaming(dev)) {
+		const __u32 format = dev->pix_format.pixelformat;
+
+		if (f->index)
+			return -EINVAL;
+
+		f->pixelformat = dev->pix_format.pixelformat;
+		vcam_fmt_descr(f->description, sizeof(f->description), format);
+	} else {
+		if (f->index >= VCAM_NR_FORMATS)
+			return -EINVAL;
+
+		f->pixelformat = vcam_formats[f->index].fourcc;
+		vcam_fmt_descr(f->description, sizeof(f->description),
+			       f->pixelformat);
+	}
+	f->flags = 0;
+	return 0;
+}
+
+static int vcam_vidioc_g_fmt_vid_cap(struct file *file, void *priv,
+				     struct v4l2_format *fmt)
+{
+	struct vcam *dev;
+
+	dev = video_drvdata(file);
+
+	fmt->fmt.pix = dev->pix_format;
+	return 0;
+}
+
+static int vcam_vidioc_try_fmt_vid_cap(struct file *file, void *priv,
+				       struct v4l2_format *fmt)
+{
+	struct vcam *dev = video_drvdata(file);
+
+	if (!V4L2_TYPE_IS_CAPTURE(fmt->type))
+		return -EINVAL;
+
+	if (vcam_is_streaming(dev)) {
+		if (!vcam_pix_format_eq(&dev->pix_format, &fmt->fmt.pix))
+			return -EBUSY;
+
+		fmt->fmt.pix = dev->pix_format;
+	}
+
+	return vcam_set_format(dev, fmt);
+}
+
+static int vcam_vidioc_s_fmt_vid_cap(struct file *file, void *priv,
+				     struct v4l2_format *fmt)
+{
+	struct vcam *dev = video_drvdata(file);
+	struct v4l2_format try_fmt = *fmt;
+	int ret;
+
+	if (!V4L2_TYPE_IS_CAPTURE(fmt->type))
+		return -EINVAL;
+
+	if (vcam_is_streaming(dev)) {
+		if (!vcam_pix_format_eq(&dev->pix_format, &fmt->fmt.pix))
+			return -EBUSY;
+
+		fmt->fmt.pix = dev->pix_format;
+	}
+
+	ret = vcam_set_format(dev, &try_fmt);
+	if (ret)
+		return ret;
+
+	if (vb2_is_busy(&dev->output_queue) &&
+	    !vcam_pix_format_eq(&dev->pix_format, &try_fmt.fmt.pix))
+		return -EBUSY;
+
+	dev->pix_format = try_fmt.fmt.pix;
+	*fmt = try_fmt;
+	return 0;
+}
+
+static int vcam_ioc_reqbufs(struct file *file, struct vcam *dev,
+			    struct v4l2_requestbuffers *req)
+{
+	int ret = 0;
+
+	if (req->type != V4L2_BUF_TYPE_VIDEO_OUTPUT)
+		return -EINVAL;
+
+	scoped_guard(mutex, &dev->lock)
+	{
+		if (vb2_queue_is_busy(&dev->output_queue, file)) {
+			ret = -EBUSY;
+			break;
+		}
+
+		ret = vb2_reqbufs(&dev->output_queue, req);
+		if (!ret)
+			dev->output_queue.owner =
+				req->count ? file->private_data : NULL;
+	}
+	return ret;
+}
+
+static int vcam_ioc_querybuf(struct file *file, struct vcam *dev,
+			     struct v4l2_buffer *buf)
+{
+	int ret = 0;
+
+	if (buf->type != V4L2_BUF_TYPE_VIDEO_OUTPUT)
+		return -EINVAL;
+
+	scoped_guard(mutex, &dev->lock)
+		ret = vb2_querybuf(&dev->output_queue, buf);
+
+	return ret;
+}
+
+static ssize_t formats_show(struct device *dev, struct device_attribute *attr,
+			    char *buf)
+{
+	struct vcam_format_entry {
+		u32 fourcc;
+		char name[5];
+	};
+	struct vcam_format_entry formats[VCAM_MAX_FORMATS];
+	struct vcam_format_entry tmp;
+	unsigned int count =
+		min_t(unsigned int, VCAM_NR_FORMATS, VCAM_MAX_FORMATS);
+	size_t len = 0;
+	unsigned int i, j;
+
+	for (i = 0; i < count; i++) {
+		formats[i].fourcc = vcam_formats[i].fourcc;
+		vcam_fourcc_str(formats[i].name, formats[i].fourcc);
+	}
+
+	for (i = 1; i < count; i++) {
+		for (j = i; j > 0; j--) {
+			if (strcmp(formats[j - 1].name, formats[j].name) <= 0)
+				break;
+			tmp = formats[j - 1];
+			formats[j - 1] = formats[j];
+			formats[j] = tmp;
+		}
+	}
+
+	for (i = 0; i < count; i++)
+		len += sysfs_emit_at(buf, len, "%s%s", i ? " " : "",
+				     formats[i].name);
+
+	len += sysfs_emit_at(buf, len, "\n");
+	return len;
+}
+
+static ssize_t max_width_show(struct device *dev, struct device_attribute *attr,
+			      char *buf)
+{
+	return sysfs_emit(buf, "%u\n", VCAM_MAX_WIDTH);
+}
+
+static ssize_t max_height_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "%u\n", VCAM_MAX_HEIGHT);
+}
+
+static ssize_t max_frames_show(struct device *dev,
+			       struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "%u\n", VCAM_MAX_FRAMES);
+}
+
+static DEVICE_ATTR_RO(formats);
+static DEVICE_ATTR_RO(max_frames);
+static DEVICE_ATTR_RO(max_height);
+static DEVICE_ATTR_RO(max_width);
+
+static struct attribute *vcam_attrs[] = {
+	&dev_attr_formats.attr,
+	&dev_attr_max_frames.attr,
+	&dev_attr_max_height.attr,
+	&dev_attr_max_width.attr,
+	NULL,
+};
+
+static const struct attribute_group vcam_attr_group = {
+	.attrs = vcam_attrs,
+};
+
+static const struct attribute_group *vcam_attr_groups[] = {
+	&vcam_attr_group,
+	NULL,
+};
+
+static int vcam_ioc_alloc(struct file *file, struct vcam *dev, u32 nr_frames,
+			  void __user *frames_user, enum vb2_memory memory)
+{
+	struct v4l2_requestbuffers req = {
+		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
+		.memory = memory,
+	};
+	struct v4l2_buffer buf;
+	struct vcam_frame *frames = NULL;
+	unsigned int i;
+	int ret;
+
+	if (memory == VB2_MEMORY_DMABUF &&
+	    !dev->output_queue.mem_ops->attach_dmabuf)
+		return -EOPNOTSUPP;
+
+	if (!frames_user)
+		return -EINVAL;
+
+	if (nr_frames) {
+		frames = kcalloc(nr_frames, sizeof(*frames), GFP_KERNEL);
+		if (!frames)
+			return -ENOMEM;
+	}
+
+	if (copy_from_user(frames, frames_user, nr_frames * sizeof(*frames))) {
+		ret = -EFAULT;
+		goto out_free;
+	}
+
+	req.count = nr_frames;
+	ret = vcam_ioc_reqbufs(file, dev, &req);
+	if (ret)
+		goto out_free;
+
+	if (req.count != nr_frames) {
+		struct v4l2_requestbuffers req_free = {
+			.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
+			.memory = memory,
+			.count = 0,
+		};
+
+		vcam_ioc_reqbufs(file, dev, &req_free);
+		ret = -ENOMEM;
+		goto out_free;
+	}
+
+	dev->output_memory = memory;
+
+	for (i = 0; i < nr_frames; i++) {
+		memset(&buf, 0, sizeof(buf));
+		buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
+		buf.memory = memory;
+		buf.index = i;
+
+		ret = vcam_ioc_querybuf(file, dev, &buf);
+		if (ret)
+			goto out_free_reqbufs;
+
+		frames[i].index = i;
+		frames[i].length = buf.length;
+	}
+
+	if (copy_to_user(frames_user, frames, nr_frames * sizeof(*frames)))
+		ret = -EFAULT;
+
+out_free_reqbufs:
+	if (ret) {
+		struct v4l2_requestbuffers req_free = {
+			.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
+			.memory = memory,
+			.count = 0,
+		};
+
+		vcam_ioc_reqbufs(file, dev, &req_free);
+		dev->output_memory = VB2_MEMORY_DMABUF;
+	}
+out_free:
+	kfree(frames);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static int vcam_ioc_queue(struct file *file, struct vcam *dev,
+			  struct vcam_ioc_queue *queue)
+{
+	struct v4l2_buffer buf = {
+		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
+		.memory = dev->output_memory,
+		.index = queue->index,
+		.bytesused = queue->length,
+	};
+	u32 remainder;
+	int ret;
+
+	if (queue->reserved)
+		return -EINVAL;
+
+	if (dev->output_memory == VB2_MEMORY_DMABUF) {
+		buf.m.fd = queue->fd;
+		buf.length = dev->pix_format.sizeimage;
+	}
+
+	buf.timestamp.tv_sec =
+		div_u64_rem(queue->timestamp, NSEC_PER_SEC, &remainder);
+	buf.timestamp.tv_usec = remainder / NSEC_PER_USEC;
+
+	scoped_guard(mutex, &dev->lock)
+	{
+		if (vb2_queue_is_busy(&dev->output_queue, file)) {
+			ret = -EBUSY;
+			break;
+		}
+
+		if (vb2_is_streaming(&dev->capture_queue) &&
+		    !vb2_is_streaming(&dev->output_queue)) {
+			ret = vb2_streamon(&dev->output_queue, buf.type);
+			if (ret)
+				break;
+		}
+
+		ret = vb2_qbuf(&dev->output_queue, dev->v4l2_dev.mdev, &buf);
+	}
+
+	return ret;
+}
+
+static int vcam_ioc_dequeue(struct file *file, struct vcam *dev,
+			    struct vcam_ioc_dequeue *queue)
+{
+	struct v4l2_buffer buf = {
+		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
+		.memory = dev->output_memory,
+	};
+	int ret;
+
+	scoped_guard(mutex, &dev->lock)
+	{
+		if (vb2_queue_is_busy(&dev->output_queue, file)) {
+			ret = -EBUSY;
+			break;
+		}
+
+		ret = vb2_dqbuf(&dev->output_queue, &buf,
+				file->f_flags & O_NONBLOCK);
+	}
+	if (ret)
+		return ret;
+
+	queue->index = buf.index;
+	queue->length = buf.bytesused;
+	queue->timestamp = (u64)buf.timestamp.tv_sec * NSEC_PER_SEC +
+			   (u64)buf.timestamp.tv_usec * NSEC_PER_USEC;
+	return 0;
+}
+
+static int vcam_ioc_status(struct vcam *dev, __u64 *status)
+{
+	*status = vcam_status_read(dev);
+	return 0;
+}
+
+static int vcam_ioc_wait(struct vcam *dev, struct vcam_ioc_wait *wait)
+{
+	int ret;
+
+	if (!wait->mask)
+		return -EINVAL;
+	if (wait->mask & ~VCAM_STATUS_MASK)
+		return -EINVAL;
+
+	ret = wait_event_interruptible(dev->status_waitq,
+				       vcam_status_mask_ready(dev, wait->mask));
+	if (ret)
+		return ret;
+
+	wait->status = vcam_status_read(dev);
+	return 0;
+}
+
+static long vcam_output_ioctl_core(struct file *file, unsigned int cmd,
+				   void *arg)
+{
+	struct vcam *dev = file->private_data;
+	long ret = 0;
+
+	switch (cmd) {
+	case VCAM_IOC_QUEUE:
+		ret = vcam_ioc_queue(file, dev, arg);
+		break;
+	case VCAM_IOC_DEQUEUE:
+		ret = vcam_ioc_dequeue(file, dev, arg);
+		break;
+	case VCAM_IOC_STATUS:
+		ret = vcam_ioc_status(dev, arg);
+		break;
+	case VCAM_IOC_WAIT:
+		ret = vcam_ioc_wait(dev, arg);
+		break;
+	default:
+		ret = -EOPNOTSUPP;
+		break;
+	}
+
+	return ret;
+}
+
+static long vcam_ioctl_common(struct file *file, unsigned int cmd,
+			      unsigned long arg)
+{
+	void __user *argp = (void __user *)arg;
+	void *karg;
+	size_t size;
+	long ret;
+
+	switch (cmd) {
+	case VCAM_IOC_QUEUE:
+		size = sizeof(struct vcam_ioc_queue);
+		break;
+	case VCAM_IOC_DEQUEUE:
+		size = sizeof(struct vcam_ioc_dequeue);
+		break;
+	case VCAM_IOC_STATUS:
+		size = sizeof(__u64);
+		break;
+	case VCAM_IOC_WAIT:
+		size = sizeof(struct vcam_ioc_wait);
+		break;
+	default:
+		return -ENOTTY;
+	}
+
+	if (size > SZ_4K)
+		return -ENOTTY;
+
+	karg = kzalloc(size, GFP_KERNEL);
+	if (!karg)
+		return -ENOMEM;
+
+	if (copy_from_user(karg, argp, size)) {
+		ret = -EFAULT;
+		goto out_free;
+	}
+
+	ret = vcam_output_ioctl_core(file, cmd, karg);
+	if (ret)
+		goto out_free;
+
+	if (copy_to_user(argp, karg, size)) {
+		ret = -EFAULT;
+		goto out_free;
+	}
+
+	ret = 0;
+out_free:
+	kfree(karg);
+	return ret;
+}
+
+static void __vcam_release(struct vcam *dev)
+{
+	if (!dev->vdev)
+		return;
+
+	vb2_queue_release(&dev->output_queue);
+	vb2_queue_release(&dev->capture_queue);
+
+	if (video_is_registered(dev->vdev))
+		video_unregister_device(dev->vdev);
+	else
+		video_device_release(dev->vdev);
+
+	v4l2_device_unregister(&dev->v4l2_dev);
+
+	dev->vdev = NULL;
+	dev->device_nr = -1;
+}
+
+static void vcam_release(struct kref *ref)
+{
+	struct vcam *dev;
+
+	dev = container_of(ref, struct vcam, ref);
+
+	if (!test_bit(VCAM_FLAG_CREATING, &dev->flags) || dev->device_nr < 0) {
+		kfree(dev);
+		return;
+	}
+
+	__vcam_release(dev);
+	kfree(dev);
+}
+
+static int __vcam_close(struct inode *inode, struct file *file)
+{
+	struct vcam *dev = file->private_data;
+
+	if (dev->vdev && video_is_registered(dev->vdev))
+		video_unregister_device(dev->vdev);
+
+	vb2_queue_release(&dev->output_queue);
+
+	dev->output_memory = VB2_MEMORY_DMABUF;
+
+	kref_put(&dev->ref, vcam_release);
+	return 0;
+}
+
+static int vcam_open(struct inode *inode, struct file *file)
+{
+	struct vcam *dev;
+	int ret = nonseekable_open(inode, file);
+
+	if (ret)
+		return ret;
+
+	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
+	if (!dev)
+		return -ENOMEM;
+
+	kref_init(&dev->ref);
+	dev->device_nr = -1;
+	file->private_data = dev;
+	return 0;
+}
+
+static int vcam_close(struct inode *inode, struct file *file)
+{
+	struct vcam *dev = file->private_data;
+	int ret = 0;
+
+	if (!dev)
+		return 0;
+
+	if (test_bit(VCAM_FLAG_CREATING, &dev->flags) && dev->device_nr >= 0)
+		ret = __vcam_close(inode, file);
+	else
+		kref_put(&dev->ref, vcam_release);
+
+	file->private_data = NULL;
+	return ret;
+}
+
+static __poll_t vcam_poll(struct file *file, struct poll_table_struct *pts)
+{
+	struct vcam *dev = file->private_data;
+
+	if (!dev || !test_bit(VCAM_FLAG_CREATING, &dev->flags) ||
+	    !test_bit(VCAM_FLAG_READY, &dev->flags) || dev->device_nr < 0)
+		return POLLERR;
+
+	return vb2_core_poll(&dev->output_queue, file, pts);
+}
+
+static int vcam_mmap(struct file *file, struct vm_area_struct *vma)
+{
+	struct vcam *dev = file->private_data;
+
+	if (!dev || !test_bit(VCAM_FLAG_CREATING, &dev->flags) ||
+	    !test_bit(VCAM_FLAG_READY, &dev->flags) || dev->device_nr < 0)
+		return -ENOTTY;
+
+	return vb2_mmap(&dev->output_queue, vma);
+}
+
+static int vcam_vidioc_g_parm(struct file *file, void *priv,
+			      struct v4l2_streamparm *parm)
+{
+	struct vcam *dev;
+
+	if (parm->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+		return -EINVAL;
+
+	dev = video_drvdata(file);
+	parm->parm.capture = dev->capture;
+	return 0;
+}
+
+static int vcam_vidioc_s_parm(struct file *file, void *priv,
+			      struct v4l2_streamparm *parm)
+{
+	struct v4l2_fract *tpf = &parm->parm.capture.timeperframe;
+	struct vcam *dev = video_drvdata(file);
+
+	if (!vcam_tpf_valid(tpf))
+		return -EINVAL;
+
+	if (parm->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+		return -EINVAL;
+
+	dev->capture.timeperframe = *tpf;
+	parm->parm.capture = dev->capture;
+	return 0;
+}
+
+static int vcam_vidioc_enum_input(struct file *file, void *fh,
+				  struct v4l2_input *inp)
+{
+	struct vcam *dev;
+	__u32 index = inp->index;
+
+	if (index != 0)
+		return -EINVAL;
+
+	memset(inp, 0, sizeof(*inp));
+
+	inp->index = index;
+	strscpy(inp->name, "vcam", sizeof(inp->name));
+	inp->type = V4L2_INPUT_TYPE_CAMERA;
+	inp->audioset = 0;
+	inp->tuner = 0;
+	inp->status = 0;
+
+	dev = video_drvdata(file);
+	if (!vb2_is_streaming(&dev->output_queue))
+		inp->status |= V4L2_IN_ST_NO_SIGNAL;
+
+	return 0;
+}
+
+static int vcam_vidioc_g_input(struct file *file, void *fh, unsigned int *i)
+{
+	*i = 0;
+	return 0;
+}
+
+static int vcam_vidioc_s_input(struct file *file, void *fh, unsigned int i)
+{
+	if (i == 0)
+		return 0;
+
+	return -EINVAL;
+}
+
+static int vcam_vidioc_streamon(struct file *file, void *fh,
+				enum v4l2_buf_type type)
+{
+	struct vcam *dev = video_drvdata(file);
+	int ret;
+
+	if (type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+		return -EINVAL;
+
+	if (vb2_queue_is_busy(&dev->capture_queue, file))
+		return -EBUSY;
+
+	ret = vb2_streamon(&dev->capture_queue, type);
+	if (ret)
+		return ret;
+
+	if (vb2_get_num_buffers(&dev->output_queue)) {
+		ret = vb2_streamon(&dev->output_queue,
+				   V4L2_BUF_TYPE_VIDEO_OUTPUT);
+		if (ret) {
+			vb2_streamoff(&dev->capture_queue, type);
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+static int vcam_vidioc_streamoff(struct file *file, void *fh,
+				 enum v4l2_buf_type type)
+{
+	struct vcam *dev = video_drvdata(file);
+	int ret;
+
+	if (type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+		return -EINVAL;
+
+	if (vb2_queue_is_busy(&dev->capture_queue, file))
+		return -EBUSY;
+
+	ret = vb2_streamoff(&dev->capture_queue, type);
+	if (ret)
+		return ret;
+
+	if (vb2_get_num_buffers(&dev->output_queue))
+		vb2_streamoff(&dev->output_queue, V4L2_BUF_TYPE_VIDEO_OUTPUT);
+
+	return 0;
+}
+
+static const struct v4l2_ioctl_ops vcam_ioctl_ops = {
+	.vidioc_querycap = &vcam_vidioc_querycap,
+	.vidioc_enum_framesizes = &vcam_vidioc_enum_framesizes,
+	.vidioc_enum_frameintervals = &vcam_vidioc_enum_frameintervals,
+	.vidioc_enum_input = &vcam_vidioc_enum_input,
+	.vidioc_g_input = &vcam_vidioc_g_input,
+	.vidioc_s_input = &vcam_vidioc_s_input,
+	.vidioc_enum_fmt_vid_cap = &vcam_vidioc_enum_fmt_cap,
+	.vidioc_g_fmt_vid_cap = &vcam_vidioc_g_fmt_vid_cap,
+	.vidioc_s_fmt_vid_cap = &vcam_vidioc_s_fmt_vid_cap,
+	.vidioc_try_fmt_vid_cap = &vcam_vidioc_try_fmt_vid_cap,
+	.vidioc_g_parm = &vcam_vidioc_g_parm,
+	.vidioc_s_parm = &vcam_vidioc_s_parm,
+
+	.vidioc_reqbufs = &vb2_ioctl_reqbufs,
+	.vidioc_create_bufs = &vb2_ioctl_create_bufs,
+	.vidioc_prepare_buf = &vb2_ioctl_prepare_buf,
+	.vidioc_querybuf = &vb2_ioctl_querybuf,
+	.vidioc_qbuf = &vb2_ioctl_qbuf,
+	.vidioc_dqbuf = &vb2_ioctl_dqbuf,
+	.vidioc_expbuf = &vb2_ioctl_expbuf,
+	.vidioc_streamon = &vcam_vidioc_streamon,
+	.vidioc_streamoff = &vcam_vidioc_streamoff,
+};
+
+static enum vb2_buffer_state vcam_buf_fill(struct vcam *dev,
+					   struct vcam_buf *buf,
+					   const void *src, u32 src_len,
+					   u64 timestamp)
+{
+	struct vb2_buffer *vb = &buf->vb.vb2_buf;
+	u32 sequence;
+	void *dst;
+
+	dst = vb2_plane_vaddr(vb, 0);
+	if (!dst)
+		return VB2_BUF_STATE_ERROR;
+
+	if (!src_len || src_len > dev->pix_format.sizeimage)
+		src_len = dev->pix_format.sizeimage;
+
+	if (!src)
+		return VB2_BUF_STATE_ERROR;
+
+	memcpy(dst, src, src_len);
+
+	sequence = (u32)(atomic_inc_return(&dev->sequence) - 1);
+
+	vb->timestamp = timestamp ? timestamp : ktime_get_ns();
+	buf->vb.sequence = sequence;
+	buf->vb.field = dev->pix_format.field;
+	vb2_set_plane_payload(vb, 0, src_len);
+
+	return VB2_BUF_STATE_DONE;
+}
+
+static bool vcam_buf_flip(struct vcam *dev, struct vb2_buffer *out_vb,
+			  struct vcam_buf *cap_buf, u32 bytesused)
+{
+	struct vb2_buffer *cap_vb = &cap_buf->vb.vb2_buf;
+	u32 sequence;
+
+	if (!out_vb->planes[0].dbuf || !cap_vb->planes[0].dbuf)
+		return false;
+
+	if (out_vb->planes[0].dbuf != cap_vb->planes[0].dbuf)
+		return false;
+
+	if (!bytesused)
+		bytesused = dev->pix_format.sizeimage;
+	if (bytesused > vb2_plane_size(cap_vb, 0))
+		bytesused = vb2_plane_size(cap_vb, 0);
+
+	sequence = (u32)(atomic_inc_return(&dev->sequence) - 1);
+
+	cap_vb->timestamp = out_vb->timestamp ? out_vb->timestamp :
+						ktime_get_ns();
+	cap_buf->vb.sequence = sequence;
+	cap_buf->vb.field = dev->pix_format.field;
+	vb2_set_plane_payload(cap_vb, 0, bytesused);
+
+	return true;
+}
+
+static bool vcam_buf_pair_dequeue(struct vcam *dev, struct vcam_buf **out_buf,
+				  struct vcam_buf **cap_buf)
+{
+	unsigned long flags;
+	bool dequeued = false;
+
+	spin_lock_irqsave(&dev->frame_lock, flags);
+	if (!list_empty(&dev->output_list) && !list_empty(&dev->capture_list)) {
+		*out_buf = list_first_entry(&dev->output_list, struct vcam_buf,
+					    list);
+		list_del(&(*out_buf)->list);
+		*cap_buf = list_first_entry(&dev->capture_list, struct vcam_buf,
+					    list);
+		list_del(&(*cap_buf)->list);
+		dequeued = true;
+	}
+	spin_unlock_irqrestore(&dev->frame_lock, flags);
+	return dequeued;
+}
+
+static void vcam_dequeue_frames(struct vcam *data)
+{
+	const struct vcam_format *format;
+	enum vb2_buffer_state cap_state;
+	struct vcam_buf *cap_buf;
+	struct vcam_buf *out_buf;
+	struct vb2_buffer *vb;
+	bool zero_copy;
+	u32 bytesused;
+	void *src;
+
+	if (!vcam_is_streaming(data))
+		return;
+
+	format = vcam_find_format(data->pix_format.pixelformat);
+	while (vcam_buf_pair_dequeue(data, &out_buf, &cap_buf)) {
+		cap_state = VB2_BUF_STATE_DONE;
+		vb = &out_buf->vb.vb2_buf;
+		bytesused = vb2_get_plane_payload(vb, 0);
+		if (!bytesused || bytesused > data->pix_format.sizeimage)
+			bytesused = data->pix_format.sizeimage;
+
+		if (bytesused < data->pix_format.sizeimage &&
+		    (!format || !(format->flags & VCAM_COMPRESSED))) {
+			cap_state = VB2_BUF_STATE_ERROR;
+			goto out_done;
+		}
+
+		zero_copy = vcam_buf_flip(data, vb, cap_buf, bytesused);
+		if (!zero_copy &&
+		    (!(out_buf->flags & VCAM_BUF_FLAG_MAPPABLE) ||
+		     !(cap_buf->flags & VCAM_BUF_FLAG_MAPPABLE))) {
+			dev_dbg(&data->vdev->dev,
+				"unshared unmappable capture and output");
+			cap_state = VB2_BUF_STATE_ERROR;
+			goto out_done;
+		}
+		if (!zero_copy) {
+			src = vb2_plane_vaddr(vb, 0);
+			if (!src) {
+				cap_state = VB2_BUF_STATE_ERROR;
+				goto out_done;
+			}
+
+			cap_state = vcam_buf_fill(data, cap_buf, src, bytesused,
+						  vb->timestamp);
+		}
+out_done:
+		vb2_buffer_done(&cap_buf->vb.vb2_buf, cap_state);
+
+		if (cap_state == VB2_BUF_STATE_ERROR)
+			vb2_buffer_done(vb, VB2_BUF_STATE_ERROR);
+		else
+			vb2_buffer_done(vb, VB2_BUF_STATE_DONE);
+	}
+}
+
+static int vcam_vdev_open(struct file *file)
+{
+	struct vcam *dev;
+	int ret;
+
+	dev = video_drvdata(file);
+	if (test_and_set_bit(VCAM_FLAG_IS_OPEN, &dev->flags))
+		return -EBUSY;
+	if (dev->device_nr < 0 || !test_bit(VCAM_FLAG_READY, &dev->flags)) {
+		clear_bit(VCAM_FLAG_IS_OPEN, &dev->flags);
+		return -ENODEV;
+	}
+
+	ret = v4l2_fh_open(file);
+	if (ret) {
+		clear_bit(VCAM_FLAG_IS_OPEN, &dev->flags);
+		return ret;
+	}
+
+	kref_get(&dev->ref);
+	return 0;
+}
+
+static int vcam_vdev_close(struct file *file)
+{
+	struct vcam *dev;
+	int ret;
+
+	dev = video_drvdata(file);
+	ret = _vb2_fop_release(file, NULL);
+	clear_bit(VCAM_FLAG_IS_OPEN, &dev->flags);
+
+	kref_put(&dev->ref, vcam_release);
+	return ret;
+}
+
+static const struct v4l2_file_operations vcam_vdev_fops = {
+	.owner = THIS_MODULE,
+	.open = vcam_vdev_open,
+	.release = vcam_vdev_close,
+	.poll = vb2_fop_poll,
+	.mmap = vb2_fop_mmap,
+	.unlocked_ioctl = video_ioctl2,
+};
+
+static int vcam_ioc_create_validate(struct vcam_ioc_create *config,
+				    char *card_label)
+{
+	long len, i;
+
+	if (config->device_nr != 0)
+		return -EINVAL;
+	if (config->reserved)
+		return -EINVAL;
+	if (config->nr_frames > VCAM_MAX_FRAMES)
+		return -E2BIG;
+	if (config->nr_frames < VCAM_MIN_FRAMES)
+		return -EINVAL;
+	if (!config->frames)
+		return -EINVAL;
+
+	memset(card_label, 0, VCAM_CARD_LABEL_MAX);
+	len = strncpy_from_user(card_label,
+				u64_to_user_ptr(config->device_name),
+				VCAM_CARD_LABEL_MAX);
+	if (len < 0)
+		return -EFAULT;
+	if (len >= VCAM_CARD_LABEL_MAX)
+		return -E2BIG;
+	if (!len)
+		return -EINVAL;
+	if (!isalnum((unsigned char)card_label[0]))
+		return -EINVAL;
+	for (i = 0; i < len; i++) {
+		if (!isalnum((unsigned char)card_label[i]) &&
+		    !isspace((unsigned char)card_label[i]))
+			return -EINVAL;
+	}
+	if (!isalnum((unsigned char)card_label[len - 1]))
+		return -EINVAL;
+
+	return len;
+}
+
+static int vcam_vb2_queue_setup(struct vb2_queue *queue,
+				unsigned int *nr_buffers,
+				unsigned int *nr_planes, unsigned int sizes[],
+				struct device *alloc_devs[])
+{
+	struct vcam *data = vb2_get_drv_priv(queue);
+	unsigned int sizeimage = data->pix_format.sizeimage;
+
+	if (!sizeimage)
+		return -EINVAL;
+
+	if (*nr_buffers < VCAM_MIN_FRAMES)
+		*nr_buffers = VCAM_MIN_FRAMES;
+
+	if (*nr_planes)
+		return sizes[0] < sizeimage ? -EINVAL : 0;
+
+	*nr_planes = 1;
+	sizes[0] = sizeimage;
+	return 0;
+}
+
+static int vcam_vb2_buf_prepare(struct vb2_buffer *vb)
+{
+	struct vcam *data = vb2_get_drv_priv(vb->vb2_queue);
+	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
+	struct vcam_buf *buf = container_of(vbuf, struct vcam_buf, vb);
+	unsigned int sizeimage = data->pix_format.sizeimage;
+	unsigned int bytesused;
+	void *vaddr;
+
+	if (vb2_plane_size(vb, 0) < sizeimage)
+		return -EINVAL;
+
+	vbuf->field = data->pix_format.field;
+	bytesused = vb2_get_plane_payload(vb, 0);
+	if (V4L2_TYPE_IS_OUTPUT(vb->vb2_queue->type) && !bytesused)
+		vb2_set_plane_payload(vb, 0, sizeimage);
+
+	buf->flags = VCAM_BUF_FLAG_MAPPABLE;
+	if (vb->planes[0].dbuf) {
+		vaddr = vb2_plane_vaddr(vb, 0);
+		if (!vaddr)
+			buf->flags &= ~VCAM_BUF_FLAG_MAPPABLE;
+	}
+	return 0;
+}
+
+static void vcam_vb2_buf_queue(struct vb2_buffer *vb)
+{
+	struct vcam *data = vb2_get_drv_priv(vb->vb2_queue);
+	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
+	struct vcam_buf *buf;
+	unsigned long flags;
+
+	buf = container_of(vbuf, struct vcam_buf, vb);
+
+	if (V4L2_TYPE_IS_OUTPUT(vb->vb2_queue->type)) {
+		spin_lock_irqsave(&data->frame_lock, flags);
+		list_add_tail(&buf->list, &data->output_list);
+		spin_unlock_irqrestore(&data->frame_lock, flags);
+	} else {
+		spin_lock_irqsave(&data->frame_lock, flags);
+		list_add_tail(&buf->list, &data->capture_list);
+		spin_unlock_irqrestore(&data->frame_lock, flags);
+	}
+
+	vcam_dequeue_frames(data);
+}
+
+static int vcam_vb2_prepare_streaming(struct vb2_queue *vq)
+{
+	return 0;
+}
+
+static int vcam_vb2_start_streaming(struct vb2_queue *vq, unsigned int count)
+{
+	struct vcam *data = vb2_get_drv_priv(vq);
+
+	if (V4L2_TYPE_IS_CAPTURE(vq->type)) {
+		atomic_set(&data->sequence, 0);
+		vcam_status_update_stream(data, true);
+	}
+
+	vcam_dequeue_frames(data);
+	return 0;
+}
+
+static void vcam_vb2_stop_streaming(struct vb2_queue *vq)
+{
+	struct vcam *data = vb2_get_drv_priv(vq);
+	struct vcam_buf *buf, *tmp;
+	unsigned long flags;
+	LIST_HEAD(done_list);
+
+	if (V4L2_TYPE_IS_CAPTURE(vq->type)) {
+		vcam_status_update_stream(data, false);
+		spin_lock_irqsave(&data->frame_lock, flags);
+		list_splice_init(&data->capture_list, &done_list);
+		list_splice_init(&data->output_list, &done_list);
+		spin_unlock_irqrestore(&data->frame_lock, flags);
+
+		list_for_each_entry_safe(buf, tmp, &done_list, list)
+			vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
+
+		return;
+	}
+
+	if (V4L2_TYPE_IS_OUTPUT(vq->type)) {
+		spin_lock_irqsave(&data->frame_lock, flags);
+		list_splice_init(&data->output_list, &done_list);
+		list_splice_init(&data->capture_list, &done_list);
+		spin_unlock_irqrestore(&data->frame_lock, flags);
+
+		list_for_each_entry_safe(buf, tmp, &done_list, list)
+			vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
+	}
+}
+
+static const struct vb2_ops vcam_vb2_ops = {
+	.queue_setup = vcam_vb2_queue_setup,
+	.buf_queue = vcam_vb2_buf_queue,
+	.buf_prepare = vcam_vb2_buf_prepare,
+	.prepare_streaming = vcam_vb2_prepare_streaming,
+	.start_streaming = vcam_vb2_start_streaming,
+	.stop_streaming = vcam_vb2_stop_streaming,
+};
+
+static int vcam_ioc_create(struct file *file, struct vcam *dev,
+			   struct vcam_ioc_create *config, char *card_label,
+			   unsigned int len)
+{
+	struct v4l2_format try_fmt;
+	struct video_device *vdev;
+	struct vb2_queue *queue;
+	struct v4l2_format fmt;
+	long ret;
+
+	strscpy(dev->v4l2_dev.name, "vcam", sizeof(dev->v4l2_dev.name));
+
+	ret = v4l2_device_register(NULL, &dev->v4l2_dev);
+	if (ret)
+		return ret;
+
+	vdev = video_device_alloc();
+	if (!vdev) {
+		ret = -ENOMEM;
+		goto err_unregister;
+	}
+
+	dev->vdev = vdev;
+	video_set_drvdata(vdev, dev);
+	memcpy(vdev->name, card_label, len);
+	vdev->name[len] = '\0';
+	vdev->vfl_type = VFL_TYPE_VIDEO;
+	vdev->fops = &vcam_vdev_fops;
+	vdev->ioctl_ops = &vcam_ioctl_ops;
+	vdev->release = &video_device_release;
+	vdev->minor = -1;
+	vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
+	vdev->vfl_dir = VFL_DIR_RX;
+
+	mutex_init(&dev->lock);
+	spin_lock_init(&dev->frame_lock);
+	spin_lock_init(&dev->status_lock);
+	INIT_LIST_HEAD(&dev->capture_list);
+	INIT_LIST_HEAD(&dev->output_list);
+	dev->status = VCAM_STATUS_IDLE;
+	dev->output_memory = VB2_MEMORY_DMABUF;
+	init_waitqueue_head(&dev->status_waitq);
+
+	dev->vdev->v4l2_dev = &dev->v4l2_dev;
+	dev->vdev->queue = &dev->capture_queue;
+	dev->vdev->lock = &dev->lock;
+	dev->capture.capability = 0;
+	dev->capture.capturemode = 0;
+	dev->capture.extendedmode = 0;
+	dev->capture.readbuffers = VCAM_MIN_FRAMES;
+	dev->capture.timeperframe.numerator = 1;
+	dev->capture.timeperframe.denominator = 30;
+
+	if (!IS_ENABLED(CONFIG_DMA_SHARED_BUFFER) ||
+	    !vb2_vmalloc_memops.attach_dmabuf) {
+		ret = -EOPNOTSUPP;
+		goto err_unregister;
+	}
+
+	fmt = (struct v4l2_format){
+		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
+		.fmt.pix = { .width = config->width,
+			     .height = config->height,
+			     .pixelformat = config->pixelformat,
+			     .colorspace = config->colorspace,
+			     .bytesperline = config->bytesperline,
+			     .field = V4L2_FIELD_NONE }
+	};
+
+	try_fmt = fmt;
+
+	ret = vcam_set_format(dev, &try_fmt);
+	if (ret)
+		goto err_unregister;
+
+	if ((fmt.fmt.pix.width && try_fmt.fmt.pix.width != fmt.fmt.pix.width) ||
+	    (fmt.fmt.pix.height &&
+	     try_fmt.fmt.pix.height != fmt.fmt.pix.height) ||
+	    try_fmt.fmt.pix.pixelformat != fmt.fmt.pix.pixelformat ||
+	    (fmt.fmt.pix.colorspace != V4L2_COLORSPACE_DEFAULT &&
+	     try_fmt.fmt.pix.colorspace != fmt.fmt.pix.colorspace) ||
+	    (fmt.fmt.pix.bytesperline &&
+	     try_fmt.fmt.pix.bytesperline != fmt.fmt.pix.bytesperline)) {
+		ret = -EINVAL;
+		goto err_unregister;
+	}
+
+	dev->pix_format = try_fmt.fmt.pix;
+
+	queue = &dev->capture_queue;
+	queue->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+	queue->io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
+	queue->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
+	queue->drv_priv = dev;
+	queue->buf_struct_size = sizeof(struct vcam_buf);
+	queue->ops = &vcam_vb2_ops;
+	queue->mem_ops = &vb2_vmalloc_memops;
+	queue->lock = &dev->lock;
+	queue->dev = &dev->vdev->dev;
+	ret = vb2_queue_init(queue);
+	if (ret)
+		goto err_unregister;
+
+	queue = &dev->output_queue;
+	queue->type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
+	queue->io_modes = VB2_DMABUF;
+	queue->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_COPY;
+	queue->drv_priv = dev;
+	queue->buf_struct_size = sizeof(struct vcam_buf);
+	queue->ops = &vcam_vb2_ops;
+	queue->mem_ops = &vb2_vmalloc_memops;
+	queue->lock = &dev->lock;
+	queue->dev = &dev->vdev->dev;
+	ret = vb2_queue_init(queue);
+	if (ret)
+		goto err_capture_queue;
+
+	ret = vcam_ioc_alloc(file, dev, config->nr_frames,
+			     u64_to_user_ptr(config->frames),
+			     VB2_MEMORY_DMABUF);
+	if (ret)
+		goto err_output_queue;
+
+	ret = video_register_device(dev->vdev, VFL_TYPE_VIDEO, -1);
+	if (ret < 0)
+		goto err_output_queue;
+
+	config->device_nr = dev->vdev->num;
+	return 0;
+
+err_output_queue:
+	vb2_queue_release(&dev->output_queue);
+
+err_capture_queue:
+	vb2_queue_release(&dev->capture_queue);
+
+err_unregister:
+	if (dev->vdev)
+		video_device_release(dev->vdev);
+	v4l2_device_unregister(&dev->v4l2_dev);
+	return ret;
+}
+
+static long vcam_ioctl(struct file *file, unsigned int cmd, unsigned long parm)
+{
+	struct vcam *dev = file->private_data;
+	char card_label[VCAM_CARD_LABEL_MAX];
+	struct vcam_ioc_create config;
+	long ret, len;
+
+	if (cmd != VCAM_IOC_CREATE) {
+		if (!dev || !test_bit(VCAM_FLAG_CREATING, &dev->flags) ||
+		    !test_bit(VCAM_FLAG_READY, &dev->flags) ||
+		    dev->device_nr < 0)
+			return -ENOTTY;
+		return vcam_ioctl_common(file, cmd, parm);
+	}
+
+	if (!dev)
+		return -ENOTTY;
+
+	if (test_and_set_bit(VCAM_FLAG_CREATING, &dev->flags))
+		return -EBUSY;
+
+	if (!parm) {
+		ret = -EINVAL;
+		goto err_clear;
+	}
+
+	if (copy_from_user(&config, (void *)parm, sizeof(config))) {
+		ret = -EFAULT;
+		goto err_clear;
+	}
+
+	len = vcam_ioc_create_validate(&config, card_label);
+	if (len < 0) {
+		ret = len;
+		goto err_clear;
+	}
+
+	ret = vcam_ioc_create(file, dev, &config, card_label, len);
+	if (ret)
+		goto err_clear;
+
+	if (copy_to_user((void *)parm, &config, sizeof(config))) {
+		ret = -EFAULT;
+		goto err_release;
+	}
+
+	dev->device_nr = dev->vdev->num;
+	snprintf(dev->v4l2_dev.name, sizeof(dev->v4l2_dev.name), "vcam-%d",
+		 dev->device_nr);
+	set_bit(VCAM_FLAG_READY, &dev->flags);
+	return 0;
+
+err_release:
+	__vcam_release(dev);
+
+err_clear:
+	clear_bit(VCAM_FLAG_CREATING, &dev->flags);
+	return ret;
+}
+
+static const struct file_operations vcam_fops = {
+	.owner = THIS_MODULE,
+	.open = vcam_open,
+	.unlocked_ioctl = vcam_ioctl,
+#ifdef CONFIG_COMPAT
+	.compat_ioctl = vcam_ioctl,
+#endif
+	.poll = vcam_poll,
+	.mmap = vcam_mmap,
+	.release = vcam_close,
+	.llseek = noop_llseek,
+};
+
+static struct miscdevice vcam_misc = {
+	.minor = MISC_DYNAMIC_MINOR,
+	.name = "vcam",
+	.fops = &vcam_fops,
+	.groups = vcam_attr_groups,
+};
+
+module_misc_device(vcam_misc);
diff --git a/include/uapi/linux/vcam.h b/include/uapi/linux/vcam.h
new file mode 100644
index 000000000000..aca0d1d32ee5
--- /dev/null
+++ b/include/uapi/linux/vcam.h
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */
+/*
+ * Copyright (c) Jarkko Sakkinen 2025-2026
+ */
+
+#ifndef _UAPI_LINUX_VCAM_H
+#define _UAPI_LINUX_VCAM_H
+
+#include <linux/types.h>
+#include <linux/ioctl.h>
+
+#define VCAM_IOC_BASE 'v'
+
+/**
+ * DOC: vcam uAPI
+ *
+ * The ioctl API of /dev/vcam provides ioctls for creating DMA-BUF backed
+ * virtual capture devices, and pushing image frames for consumption.
+ *
+ * Frames are queued with %VCAM_IOC_QUEUE and recycled with %VCAM_IOC_DEQUEUE.
+ * Queueing without dequeuing eventually exhausts the output queue.
+ */
+
+/**
+ * enum vcam_status - Status bits
+ * @VCAM_STATUS_IDLE: Capture queue is not streaming.
+ * @VCAM_STATUS_STREAMING: Capture queue is streaming.
+ */
+enum vcam_status {
+	VCAM_STATUS_IDLE = 1U << 0,
+	VCAM_STATUS_STREAMING = 1U << 1,
+};
+
+/**
+ * struct vcam_ioc_create - Create a virtual camera device
+ * @device_name: (input) User pointer to device name string.
+ * @width: (input) Frame width in pixels. Must be non-zero.
+ * @height: (input) Frame height in pixels. Must be non-zero.
+ * @pixelformat: (input) Four CC format code.
+ * @colorspace: (input) V4L2 colorspace value.
+ * @bytesperline: (input) Bytes per line in the output format.
+ * @reserved: Reserved for future use. Must be set to zero.
+ * @device_nr: (output) Device number (must be 0 on input).
+ * @nr_frames: (input) Number of entries in @frames.
+ * @frames: (input/output) User pointer to an array of &struct vcam_frame.
+ */
+struct vcam_ioc_create {
+	__u64 device_name;
+	__u32 width;
+	__u32 height;
+	__u32 pixelformat;
+	__u32 colorspace;
+	__u32 bytesperline;
+	__u32 reserved;
+	__u32 device_nr;
+	__u32 nr_frames;
+	__u64 frames;
+};
+
+/**
+ * struct vcam_frame - a frame descriptor
+ * @index: Frame index assigned by the driver.
+ * @length: Frame size in bytes.
+ */
+struct vcam_frame {
+	__u32 index;
+	__u32 length;
+};
+
+/**
+ * struct vcam_ioc_queue - Produce an output buffer
+ * @fd: (input) DMA-BUF file descriptor.
+ * @index: (input) Buffer index for %VCAM_IOC_QUEUE.
+ * @length: (input) Payload length in bytes for %VCAM_IOC_QUEUE.
+ * @reserved: Reserved for future use. Must be set to zero.
+ * @timestamp: (input) Timestamp in nanoseconds for %VCAM_IOC_QUEUE.
+ */
+struct vcam_ioc_queue {
+	__u32 fd;
+	__u32 index;
+	__u32 length;
+	__u32 reserved;
+	__u64 timestamp;
+};
+
+/**
+ * struct vcam_ioc_dequeue - Dequeue an output buffer
+ * @index: (output) Buffer index for %VCAM_IOC_DEQUEUE.
+ * @length: (output) Payload length in bytes for %VCAM_IOC_DEQUEUE.
+ * @timestamp: (output) Timestamp in nanoseconds for %VCAM_IOC_DEQUEUE.
+ */
+struct vcam_ioc_dequeue {
+	__u32 index;
+	__u32 length;
+	__u64 timestamp;
+};
+
+/**
+ * struct vcam_ioc_wait - Wait for capture status
+ * @mask: (input) Mask of status bits to wait for.
+ * @status: (output) Current status bit mask.
+ */
+struct vcam_ioc_wait {
+	__u64 mask;
+	__u64 status;
+};
+
+/**
+ * DOC: vcam ioctls
+ *
+ * %VCAM_IOC_CREATE: Creates a virtual camera device and associates output
+ * buffers described by &struct vcam_frame with DMA-BUF file descriptors.
+ * %VCAM_IOC_QUEUE: Enqueues an output buffer for capture.
+ * %VCAM_IOC_DEQUEUE: Dequeues a consumed output buffer for reuse.
+ * %VCAM_IOC_STATUS: Reads the driver status bits.
+ * %VCAM_IOC_WAIT: Waits for the subset of status bits to activate.
+ */
+#define VCAM_IOC_CREATE _IOWR(VCAM_IOC_BASE, 0x00, struct vcam_ioc_create)
+#define VCAM_IOC_QUEUE _IOW(VCAM_IOC_BASE, 0x01, struct vcam_ioc_queue)
+#define VCAM_IOC_DEQUEUE _IOR(VCAM_IOC_BASE, 0x02, struct vcam_ioc_dequeue)
+#define VCAM_IOC_STATUS _IOR(VCAM_IOC_BASE, 0x03, __u64)
+#define VCAM_IOC_WAIT _IOWR(VCAM_IOC_BASE, 0x04, struct vcam_ioc_wait)
+
+#endif /* _UAPI_LINUX_VCAM_H */
-- 
2.52.0
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 5 days, 17 hours ago
Hi Jarkko,

Thank you for the patch.

On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> vcam is a DMA-BUF backed virtual camera driver capable of creating video
> capture devices to which data can be streamed through /dev/vcam after
> calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> with VCAM_IOC_DEQUEUE.
> 
> Zero-copy semantics are supported for shared DMA-BUF between capture and
> output.
>
> Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> ---
> Early feedback e.g., is this completely in wrong direction? V4L2 world
> is relatively alien world, and thus I need a sanity check ;-)

We already have multiple virtual drivers, including vivid and vimc.
Could you please explain the rationale for yet another one, and why the
new features it provides (if any) can't be added to existing drivers ?

>  .../driver-api/media/drivers/index.rst        |    1 +
>  .../driver-api/media/drivers/vcam.rst         |   16 +
>  MAINTAINERS                                   |    8 +
>  drivers/media/Kconfig                         |   13 +
>  drivers/media/Makefile                        |    1 +
>  drivers/media/vcam.c                          | 1700 +++++++++++++++++
>  include/uapi/linux/vcam.h                     |  124 ++
>  7 files changed, 1863 insertions(+)
>  create mode 100644 Documentation/driver-api/media/drivers/vcam.rst
>  create mode 100644 drivers/media/vcam.c
>  create mode 100644 include/uapi/linux/vcam.h
> 
> diff --git a/Documentation/driver-api/media/drivers/index.rst b/Documentation/driver-api/media/drivers/index.rst
> index 7f6f3dcd5c90..211cafc9c070 100644
> --- a/Documentation/driver-api/media/drivers/index.rst
> +++ b/Documentation/driver-api/media/drivers/index.rst
> @@ -27,6 +27,7 @@ Video4Linux (V4L) drivers
>  	zoran
>  	ccs/ccs
>  	ipu6
> +	vcam
>  
>  
>  Digital TV drivers
> diff --git a/Documentation/driver-api/media/drivers/vcam.rst b/Documentation/driver-api/media/drivers/vcam.rst
> new file mode 100644
> index 000000000000..b5a23144ebee
> --- /dev/null
> +++ b/Documentation/driver-api/media/drivers/vcam.rst
> @@ -0,0 +1,16 @@
> +.. SPDX-License-Identifier: GPL-2.0
> +
> +===========================
> +vcam: Virtual Camera Driver
> +===========================
> +
> +Theory of Operation
> +-------------------
> +
> +.. kernel-doc:: drivers/media/vcam.c
> +   :doc: Theory of Operation
> +
> +Driver uAPI
> +-----------
> +
> +.. kernel-doc:: include/uapi/linux/vcam.h
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 6863d5fa07a1..b8444ff48716 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -27504,6 +27504,14 @@ S:	Maintained
>  F:	drivers/media/common/videobuf2/*
>  F:	include/media/videobuf2-*
>  
> +VCAM V4L2 DRIVER
> +M:	Jarkko Sakkinen <jarkko@kernel.org>
> +L:	linux-media@vger.kernel.org
> +S:	Maintained
> +T:	git git://git.kernel.org/pub/scm/linux/kernel/git/jarkko/linux-tpmdd.git
> +F:	drivers/media/vcam.c
> +F:	include/uapi/linux/vcam.h
> +
>  VIDTV VIRTUAL DIGITAL TV DRIVER
>  M:	Daniel W. S. Almeida <dwlsalmeida@gmail.com>
>  L:	linux-media@vger.kernel.org
> diff --git a/drivers/media/Kconfig b/drivers/media/Kconfig
> index 6abc9302cd84..f2f4b2ec9135 100644
> --- a/drivers/media/Kconfig
> +++ b/drivers/media/Kconfig
> @@ -239,6 +239,19 @@ source "drivers/media/firewire/Kconfig"
>  # Common driver options
>  source "drivers/media/common/Kconfig"
>  
> +config VCAM
> +	tristate "V4L2 virtual camera"
> +	depends on VIDEO_DEV
> +	default m
> +	select VIDEOBUF2_VMALLOC
> +	help
> +	  Say Y here to enable a DMA-BUF backed virtual camera driver capable
> +	  of creating video capture devices to which data can be streamed
> +	  through /dev/vcam after calling VCAM_IOC_CREATE. Frames are pushed
> +	  with VCAM_IOC_QUEUE and recycled with VCAM_IOC_DEQUEUE.
> +
> +	  When in doubt, say N.
> +
>  endmenu
>  
>  #
> diff --git a/drivers/media/Makefile b/drivers/media/Makefile
> index 20fac24e4f0f..d539fecbe498 100644
> --- a/drivers/media/Makefile
> +++ b/drivers/media/Makefile
> @@ -32,3 +32,4 @@ obj-$(CONFIG_CEC_CORE) += cec/
>  obj-y += common/ platform/ pci/ usb/ mmc/ firewire/ spi/ test-drivers/
>  obj-$(CONFIG_VIDEO_DEV) += radio/
>  
> +obj-$(CONFIG_VCAM) += vcam.o
> diff --git a/drivers/media/vcam.c b/drivers/media/vcam.c
> new file mode 100644
> index 000000000000..82f4351d0499
> --- /dev/null
> +++ b/drivers/media/vcam.c
> @@ -0,0 +1,1700 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (c) Jarkko Sakkinen 2025-2026
> + *
> + * Derived originally from v4l2loopback driver but is essentially a rewrite.
> + */
> +
> +/**
> + * DOC: Theory of Operation
> + *
> + * The driver exposes /dev/vcam for creating virtual capture devices via
> + * %VCAM_IOC_CREATE. The ioctl registers a video capture node and associates
> + * output buffers described by &struct vcam_frame with DMA-BUF file descriptors
> + * supplied by the caller. This also keeps output buffers owned by the caller,
> + * and accounted from the calling process.
> + *
> + * Frames are pushed to the capture device by queueing output buffers using
> + * %VCAM_IOC_QUEUE, and recycling them with %VCAM_IOC_DEQUEUE. Queueing without
> + * dequeuing eventually exhausts the output queue and stalls the producer.
> + *
> + * If both buffers reference the same DMA-BUF, the driver performs a zero-copy
> + * transfer by propagating metadata. Otherwise, if both buffers are mappable,
> + * the payload is copied into the capture buffer. When neither zero-copy nor a
> + * CPU mapping is possible, the capture buffer completes with an error.
> + */
> +
> +#include <linux/cleanup.h>
> +#include <linux/bitops.h>
> +#include <linux/atomic.h>
> +#include <linux/ctype.h>
> +#include <linux/compat.h>
> +#include <linux/dma-buf.h>
> +#include <linux/dma-mapping.h>
> +#include <linux/fdtable.h>
> +#include <linux/file.h>
> +#include <linux/fs.h>
> +#include <linux/limits.h>
> +#include <linux/device.h>
> +#include <linux/mm.h>
> +#include <linux/module.h>
> +#include <linux/miscdevice.h>
> +#include <linux/poll.h>
> +#include <linux/sched.h>
> +#include <linux/time.h>
> +#include <linux/time64.h>
> +#include <linux/math64.h>
> +#include <linux/minmax.h>
> +#include <linux/slab.h>
> +#include <linux/string.h>
> +#include <linux/spinlock.h>
> +#include <linux/sysfs.h>
> +#include <linux/time.h>
> +#include <linux/videodev2.h>
> +#include <linux/wait.h>
> +#include <media/v4l2-common.h>
> +#include <media/v4l2-device.h>
> +#include <media/v4l2-ioctl.h>
> +#include <media/videobuf2-v4l2.h>
> +#include <media/videobuf2-vmalloc.h>
> +#include <uapi/linux/vcam.h>
> +
> +#undef pr_fmt
> +#define pr_fmt(fmt) "vcam: " fmt
> +
> +MODULE_DESCRIPTION("V4L2 virtual camera driver");
> +MODULE_LICENSE("GPL");
> +
> +#define VCAM_CARD_LABEL_MAX sizeof_field(struct video_device, name)
> +#define VCAM_FPS_MIN 1
> +#define VCAM_FPS_MAX 1000
> +
> +#define VCAM_MIN_WIDTH 2
> +#define VCAM_MIN_HEIGHT 2
> +#define VCAM_MAX_WIDTH 8192
> +#define VCAM_MAX_HEIGHT 8192
> +#define VCAM_DEFAULT_WIDTH 640
> +#define VCAM_DEFAULT_HEIGHT 480
> +
> +#define VCAM_MAX_FORMATS 16
> +#define VCAM_MIN_FRAMES 2
> +#define VCAM_MAX_FRAMES 32
> +
> +#define VCAM_STATUS_MASK (VCAM_STATUS_IDLE | VCAM_STATUS_STREAMING)
> +
> +enum vcam_flags {
> +	VCAM_FLAG_IS_OPEN = 0x01,
> +	VCAM_FLAG_CREATING = 0x02,
> +	VCAM_FLAG_READY = 0x04,
> +};
> +
> +struct vcam_buf {
> +	struct vb2_v4l2_buffer vb;
> +	struct list_head list;
> +	unsigned long flags;
> +};
> +
> +enum vcam_buf_flags {
> +	VCAM_BUF_FLAG_MAPPABLE = BIT(0),
> +};
> +
> +struct vcam {
> +	unsigned long flags;
> +	int device_nr;
> +	struct v4l2_device v4l2_dev;
> +	struct video_device *vdev;
> +	struct vb2_queue capture_queue;
> +	struct vb2_queue output_queue;
> +	struct v4l2_pix_format pix_format;
> +	struct v4l2_captureparm capture;
> +	atomic_t sequence;
> +	struct list_head capture_list;
> +	struct list_head output_list;
> +	u64 status;
> +	wait_queue_head_t status_waitq;
> +	enum vb2_memory output_memory;
> +
> +	/* Protects status flags and wait queue updates. */
> +	spinlock_t status_lock;
> +
> +	/* Shared lock for vdev and VB2 queues. */
> +	struct mutex lock;
> +
> +	/* Protects capture_list and output_list. */
> +	spinlock_t frame_lock;
> +
> +	/*
> +	 * Maintains a shared reference between processes having either
> +	 * /dev/vcam or /dev/videoX open.
> +	 */
> +	struct kref ref;
> +};
> +
> +enum vcam_format_flags {
> +	VCAM_PLANAR = BIT(0),
> +	VCAM_COMPRESSED = BIT(1),
> +};
> +
> +struct vcam_format {
> +	int fourcc;
> +	int depth;
> +	int flags;
> +};
> +
> +const struct vcam_format vcam_formats[] = {
> +	{
> +		.fourcc = V4L2_PIX_FMT_YUYV,
> +		.depth = 16,
> +		.flags = 0,
> +	},
> +	{
> +		.fourcc = V4L2_PIX_FMT_NV12,
> +		.depth = 12,
> +		.flags = VCAM_PLANAR,
> +	},
> +	{
> +		.fourcc = V4L2_PIX_FMT_MJPEG,
> +		.depth = 32,
> +		.flags = VCAM_COMPRESSED,
> +	},
> +};
> +
> +#define VCAM_NR_FORMATS ARRAY_SIZE(vcam_formats)
> +
> +static const struct vcam_format *vcam_find_format(int fourcc)
> +{
> +	unsigned int i;
> +
> +	for (i = 0; i < VCAM_NR_FORMATS; i++) {
> +		if (vcam_formats[i].fourcc == fourcc)
> +			return vcam_formats + i;
> +	}
> +
> +	return NULL;
> +}
> +
> +static void vcam_fmt_descr(char *dst, size_t dst_len, u32 format)
> +{
> +	snprintf(dst, dst_len, "[%c%c%c%c]", (format >> 0) & 0xFF,
> +		 (format >> 8) & 0xFF, (format >> 16) & 0xFF,
> +		 (format >> 24) & 0xFF);
> +}
> +
> +static void vcam_fourcc_str(char *dst, u32 format)
> +{
> +	dst[0] = (format >> 0) & 0xFF;
> +	dst[1] = (format >> 8) & 0xFF;
> +	dst[2] = (format >> 16) & 0xFF;
> +	dst[3] = (format >> 24) & 0xFF;
> +	dst[4] = '\0';
> +}
> +
> +static inline bool vcam_is_streaming(struct vcam *data)
> +{
> +	return vb2_is_streaming(&data->output_queue) ||
> +	       vb2_is_streaming(&data->capture_queue);
> +}
> +
> +static bool vcam_status_mask_ready(struct vcam *dev, u64 mask)
> +{
> +	unsigned long flags;
> +	bool ready;
> +
> +	spin_lock_irqsave(&dev->status_lock, flags);
> +	ready = (dev->status & mask) == mask;
> +	spin_unlock_irqrestore(&dev->status_lock, flags);
> +
> +	return ready;
> +}
> +
> +static void vcam_status_update_stream(struct vcam *dev, bool on)
> +{
> +	unsigned long flags;
> +	u64 old_flags;
> +	u64 new_flags;
> +
> +	spin_lock_irqsave(&dev->status_lock, flags);
> +	old_flags = dev->status;
> +	if (on) {
> +		dev->status &= ~VCAM_STATUS_IDLE;
> +		dev->status |= VCAM_STATUS_STREAMING;
> +	} else {
> +		dev->status &= ~VCAM_STATUS_STREAMING;
> +		dev->status |= VCAM_STATUS_IDLE;
> +	}
> +	new_flags = dev->status;
> +	spin_unlock_irqrestore(&dev->status_lock, flags);
> +
> +	if (new_flags != old_flags)
> +		wake_up_interruptible(&dev->status_waitq);
> +}
> +
> +static u64 vcam_status_read(struct vcam *dev)
> +{
> +	unsigned long flags;
> +	u64 flags_snapshot;
> +
> +	spin_lock_irqsave(&dev->status_lock, flags);
> +	flags_snapshot = dev->status;
> +	spin_unlock_irqrestore(&dev->status_lock, flags);
> +
> +	return flags_snapshot;
> +}
> +
> +static bool vcam_tpf_valid(const struct v4l2_fract *tpf)
> +{
> +	u64 min_den = (u64)tpf->numerator * VCAM_FPS_MIN;
> +	u64 max_den = (u64)tpf->numerator * VCAM_FPS_MAX;
> +
> +	if (!tpf->numerator || !tpf->denominator)
> +		return false;
> +	if ((u64)tpf->denominator < min_den)
> +		return false;
> +	if ((u64)tpf->denominator > max_den)
> +		return false;
> +
> +	return true;
> +}
> +
> +static bool vcam_pix_format_eq(const struct v4l2_pix_format *src,
> +			       const struct v4l2_pix_format *dest)
> +{
> +	return src->width == dest->width && src->height == dest->height &&
> +	       src->pixelformat == dest->pixelformat;
> +}
> +
> +static int vcam_set_format(struct vcam *dev, struct v4l2_format *fmt)
> +{
> +	struct v4l2_pix_format *pix = &fmt->fmt.pix;
> +	const struct vcam_format *format;
> +	u64 bytesperline;
> +	u64 sizeimage;
> +
> +	if (V4L2_TYPE_IS_MULTIPLANAR(fmt->type))
> +		return -EINVAL;
> +
> +	if (!pix->width)
> +		pix->width = VCAM_DEFAULT_WIDTH;
> +	if (!pix->height)
> +		pix->height = VCAM_DEFAULT_HEIGHT;
> +
> +	pix->width = clamp(pix->width, VCAM_MIN_WIDTH, VCAM_MAX_WIDTH);
> +	pix->height = clamp(pix->height, VCAM_MIN_HEIGHT, VCAM_MAX_HEIGHT);
> +
> +	format = vcam_find_format(pix->pixelformat);
> +	if (!format) {
> +		format = &vcam_formats[0];
> +		pix->pixelformat = format->fourcc;
> +	}
> +
> +	if (format->flags & VCAM_PLANAR) {
> +		pix->bytesperline = pix->width;
> +		sizeimage = ((u64)pix->width * pix->height * format->depth) >>
> +			    3;
> +	} else if (format->flags & VCAM_COMPRESSED) {
> +		pix->bytesperline = 0;
> +		sizeimage = ((u64)pix->width * pix->height * format->depth) >>
> +			    3;
> +	} else {
> +		bytesperline = ((u64)pix->width * format->depth) >> 3;
> +		if (bytesperline > U32_MAX)
> +			return -EOVERFLOW;
> +
> +		pix->bytesperline = bytesperline;
> +		sizeimage = (u64)pix->height * bytesperline;
> +	}
> +
> +	if (sizeimage > U32_MAX)
> +		return -EOVERFLOW;
> +
> +	pix->sizeimage = sizeimage;
> +
> +	if (pix->colorspace == V4L2_COLORSPACE_DEFAULT ||
> +	    pix->colorspace > V4L2_COLORSPACE_DCI_P3)
> +		pix->colorspace = V4L2_COLORSPACE_SRGB;
> +	if (pix->field == V4L2_FIELD_ANY)
> +		pix->field = V4L2_FIELD_NONE;
> +
> +	return 0;
> +}
> +
> +static int vcam_vidioc_querycap(struct file *file, void *priv,
> +				struct v4l2_capability *cap)
> +{
> +	__u32 capabilities = V4L2_CAP_STREAMING | V4L2_CAP_VIDEO_CAPTURE;
> +	struct vcam *dev = video_drvdata(file);
> +
> +	cap->device_caps = capabilities;
> +	cap->capabilities = capabilities | V4L2_CAP_DEVICE_CAPS;
> +
> +	strscpy(cap->driver, "vcam", sizeof(cap->driver));
> +	strscpy(cap->card, dev->vdev->name, sizeof(cap->card));
> +	snprintf(cap->bus_info, sizeof(cap->bus_info), "vcam:%d",
> +		 dev->device_nr);
> +
> +	return 0;
> +}
> +
> +static int vcam_enum_framesizes(struct vcam *dev, struct v4l2_frmsizeenum *argp)
> +{
> +	if (argp->index)
> +		return -EINVAL;
> +
> +	if (vcam_is_streaming(dev)) {
> +		if (argp->pixel_format != dev->pix_format.pixelformat)
> +			return -EINVAL;
> +
> +		argp->type = V4L2_FRMSIZE_TYPE_DISCRETE;
> +
> +		argp->discrete.width = dev->pix_format.width;
> +		argp->discrete.height = dev->pix_format.height;
> +	} else {
> +		if (!vcam_find_format(argp->pixel_format))
> +			return -EINVAL;
> +
> +		argp->type = V4L2_FRMSIZE_TYPE_CONTINUOUS;
> +
> +		argp->stepwise.min_width = VCAM_MIN_WIDTH;
> +		argp->stepwise.min_height = VCAM_MIN_HEIGHT;
> +		argp->stepwise.max_width = VCAM_MAX_WIDTH;
> +		argp->stepwise.max_height = VCAM_MAX_HEIGHT;
> +		argp->stepwise.step_width = 1;
> +		argp->stepwise.step_height = 1;
> +	}
> +
> +	return 0;
> +}
> +
> +static int vcam_enum_frameintervals(struct vcam *dev,
> +				    struct v4l2_frmivalenum *argp)
> +{
> +	if (argp->index)
> +		return -EINVAL;
> +
> +	if (vcam_is_streaming(dev)) {
> +		if (argp->width != dev->pix_format.width ||
> +		    argp->height != dev->pix_format.height ||
> +		    argp->pixel_format != dev->pix_format.pixelformat)
> +			return -EINVAL;
> +
> +		argp->type = V4L2_FRMIVAL_TYPE_DISCRETE;
> +		argp->discrete = dev->capture.timeperframe;
> +	} else {
> +		if (argp->width < VCAM_MIN_WIDTH ||
> +		    argp->width > VCAM_MAX_WIDTH ||
> +		    argp->height < VCAM_MIN_HEIGHT ||
> +		    argp->height > VCAM_MAX_HEIGHT ||
> +		    !vcam_find_format(argp->pixel_format))
> +			return -EINVAL;
> +
> +		argp->type = V4L2_FRMIVAL_TYPE_CONTINUOUS;
> +		argp->stepwise.min.numerator = 1;
> +		argp->stepwise.min.denominator = VCAM_FPS_MAX;
> +		argp->stepwise.max.numerator = 1;
> +		argp->stepwise.max.denominator = VCAM_FPS_MIN;
> +		argp->stepwise.step.numerator = 1;
> +		argp->stepwise.step.denominator = 1;
> +	}
> +
> +	return 0;
> +}
> +
> +static int vcam_vidioc_enum_framesizes(struct file *file, void *fh,
> +				       struct v4l2_frmsizeenum *argp)
> +{
> +	struct vcam *dev = video_drvdata(file);
> +
> +	return vcam_enum_framesizes(dev, argp);
> +}
> +
> +static int vcam_vidioc_enum_frameintervals(struct file *file, void *fh,
> +					   struct v4l2_frmivalenum *argp)
> +{
> +	struct vcam *dev = video_drvdata(file);
> +
> +	return vcam_enum_frameintervals(dev, argp);
> +}
> +
> +static int vcam_vidioc_enum_fmt_cap(struct file *file, void *fh,
> +				    struct v4l2_fmtdesc *f)
> +{
> +	struct vcam *dev;
> +
> +	dev = video_drvdata(file);
> +
> +	if (vcam_is_streaming(dev)) {
> +		const __u32 format = dev->pix_format.pixelformat;
> +
> +		if (f->index)
> +			return -EINVAL;
> +
> +		f->pixelformat = dev->pix_format.pixelformat;
> +		vcam_fmt_descr(f->description, sizeof(f->description), format);
> +	} else {
> +		if (f->index >= VCAM_NR_FORMATS)
> +			return -EINVAL;
> +
> +		f->pixelformat = vcam_formats[f->index].fourcc;
> +		vcam_fmt_descr(f->description, sizeof(f->description),
> +			       f->pixelformat);
> +	}
> +	f->flags = 0;
> +	return 0;
> +}
> +
> +static int vcam_vidioc_g_fmt_vid_cap(struct file *file, void *priv,
> +				     struct v4l2_format *fmt)
> +{
> +	struct vcam *dev;
> +
> +	dev = video_drvdata(file);
> +
> +	fmt->fmt.pix = dev->pix_format;
> +	return 0;
> +}
> +
> +static int vcam_vidioc_try_fmt_vid_cap(struct file *file, void *priv,
> +				       struct v4l2_format *fmt)
> +{
> +	struct vcam *dev = video_drvdata(file);
> +
> +	if (!V4L2_TYPE_IS_CAPTURE(fmt->type))
> +		return -EINVAL;
> +
> +	if (vcam_is_streaming(dev)) {
> +		if (!vcam_pix_format_eq(&dev->pix_format, &fmt->fmt.pix))
> +			return -EBUSY;
> +
> +		fmt->fmt.pix = dev->pix_format;
> +	}
> +
> +	return vcam_set_format(dev, fmt);
> +}
> +
> +static int vcam_vidioc_s_fmt_vid_cap(struct file *file, void *priv,
> +				     struct v4l2_format *fmt)
> +{
> +	struct vcam *dev = video_drvdata(file);
> +	struct v4l2_format try_fmt = *fmt;
> +	int ret;
> +
> +	if (!V4L2_TYPE_IS_CAPTURE(fmt->type))
> +		return -EINVAL;
> +
> +	if (vcam_is_streaming(dev)) {
> +		if (!vcam_pix_format_eq(&dev->pix_format, &fmt->fmt.pix))
> +			return -EBUSY;
> +
> +		fmt->fmt.pix = dev->pix_format;
> +	}
> +
> +	ret = vcam_set_format(dev, &try_fmt);
> +	if (ret)
> +		return ret;
> +
> +	if (vb2_is_busy(&dev->output_queue) &&
> +	    !vcam_pix_format_eq(&dev->pix_format, &try_fmt.fmt.pix))
> +		return -EBUSY;
> +
> +	dev->pix_format = try_fmt.fmt.pix;
> +	*fmt = try_fmt;
> +	return 0;
> +}
> +
> +static int vcam_ioc_reqbufs(struct file *file, struct vcam *dev,
> +			    struct v4l2_requestbuffers *req)
> +{
> +	int ret = 0;
> +
> +	if (req->type != V4L2_BUF_TYPE_VIDEO_OUTPUT)
> +		return -EINVAL;
> +
> +	scoped_guard(mutex, &dev->lock)
> +	{
> +		if (vb2_queue_is_busy(&dev->output_queue, file)) {
> +			ret = -EBUSY;
> +			break;
> +		}
> +
> +		ret = vb2_reqbufs(&dev->output_queue, req);
> +		if (!ret)
> +			dev->output_queue.owner =
> +				req->count ? file->private_data : NULL;
> +	}
> +	return ret;
> +}
> +
> +static int vcam_ioc_querybuf(struct file *file, struct vcam *dev,
> +			     struct v4l2_buffer *buf)
> +{
> +	int ret = 0;
> +
> +	if (buf->type != V4L2_BUF_TYPE_VIDEO_OUTPUT)
> +		return -EINVAL;
> +
> +	scoped_guard(mutex, &dev->lock)
> +		ret = vb2_querybuf(&dev->output_queue, buf);
> +
> +	return ret;
> +}
> +
> +static ssize_t formats_show(struct device *dev, struct device_attribute *attr,
> +			    char *buf)
> +{
> +	struct vcam_format_entry {
> +		u32 fourcc;
> +		char name[5];
> +	};
> +	struct vcam_format_entry formats[VCAM_MAX_FORMATS];
> +	struct vcam_format_entry tmp;
> +	unsigned int count =
> +		min_t(unsigned int, VCAM_NR_FORMATS, VCAM_MAX_FORMATS);
> +	size_t len = 0;
> +	unsigned int i, j;
> +
> +	for (i = 0; i < count; i++) {
> +		formats[i].fourcc = vcam_formats[i].fourcc;
> +		vcam_fourcc_str(formats[i].name, formats[i].fourcc);
> +	}
> +
> +	for (i = 1; i < count; i++) {
> +		for (j = i; j > 0; j--) {
> +			if (strcmp(formats[j - 1].name, formats[j].name) <= 0)
> +				break;
> +			tmp = formats[j - 1];
> +			formats[j - 1] = formats[j];
> +			formats[j] = tmp;
> +		}
> +	}
> +
> +	for (i = 0; i < count; i++)
> +		len += sysfs_emit_at(buf, len, "%s%s", i ? " " : "",
> +				     formats[i].name);
> +
> +	len += sysfs_emit_at(buf, len, "\n");
> +	return len;
> +}
> +
> +static ssize_t max_width_show(struct device *dev, struct device_attribute *attr,
> +			      char *buf)
> +{
> +	return sysfs_emit(buf, "%u\n", VCAM_MAX_WIDTH);
> +}
> +
> +static ssize_t max_height_show(struct device *dev,
> +			       struct device_attribute *attr, char *buf)
> +{
> +	return sysfs_emit(buf, "%u\n", VCAM_MAX_HEIGHT);
> +}
> +
> +static ssize_t max_frames_show(struct device *dev,
> +			       struct device_attribute *attr, char *buf)
> +{
> +	return sysfs_emit(buf, "%u\n", VCAM_MAX_FRAMES);
> +}
> +
> +static DEVICE_ATTR_RO(formats);
> +static DEVICE_ATTR_RO(max_frames);
> +static DEVICE_ATTR_RO(max_height);
> +static DEVICE_ATTR_RO(max_width);
> +
> +static struct attribute *vcam_attrs[] = {
> +	&dev_attr_formats.attr,
> +	&dev_attr_max_frames.attr,
> +	&dev_attr_max_height.attr,
> +	&dev_attr_max_width.attr,
> +	NULL,
> +};
> +
> +static const struct attribute_group vcam_attr_group = {
> +	.attrs = vcam_attrs,
> +};
> +
> +static const struct attribute_group *vcam_attr_groups[] = {
> +	&vcam_attr_group,
> +	NULL,
> +};
> +
> +static int vcam_ioc_alloc(struct file *file, struct vcam *dev, u32 nr_frames,
> +			  void __user *frames_user, enum vb2_memory memory)
> +{
> +	struct v4l2_requestbuffers req = {
> +		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> +		.memory = memory,
> +	};
> +	struct v4l2_buffer buf;
> +	struct vcam_frame *frames = NULL;
> +	unsigned int i;
> +	int ret;
> +
> +	if (memory == VB2_MEMORY_DMABUF &&
> +	    !dev->output_queue.mem_ops->attach_dmabuf)
> +		return -EOPNOTSUPP;
> +
> +	if (!frames_user)
> +		return -EINVAL;
> +
> +	if (nr_frames) {
> +		frames = kcalloc(nr_frames, sizeof(*frames), GFP_KERNEL);
> +		if (!frames)
> +			return -ENOMEM;
> +	}
> +
> +	if (copy_from_user(frames, frames_user, nr_frames * sizeof(*frames))) {
> +		ret = -EFAULT;
> +		goto out_free;
> +	}
> +
> +	req.count = nr_frames;
> +	ret = vcam_ioc_reqbufs(file, dev, &req);
> +	if (ret)
> +		goto out_free;
> +
> +	if (req.count != nr_frames) {
> +		struct v4l2_requestbuffers req_free = {
> +			.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> +			.memory = memory,
> +			.count = 0,
> +		};
> +
> +		vcam_ioc_reqbufs(file, dev, &req_free);
> +		ret = -ENOMEM;
> +		goto out_free;
> +	}
> +
> +	dev->output_memory = memory;
> +
> +	for (i = 0; i < nr_frames; i++) {
> +		memset(&buf, 0, sizeof(buf));
> +		buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> +		buf.memory = memory;
> +		buf.index = i;
> +
> +		ret = vcam_ioc_querybuf(file, dev, &buf);
> +		if (ret)
> +			goto out_free_reqbufs;
> +
> +		frames[i].index = i;
> +		frames[i].length = buf.length;
> +	}
> +
> +	if (copy_to_user(frames_user, frames, nr_frames * sizeof(*frames)))
> +		ret = -EFAULT;
> +
> +out_free_reqbufs:
> +	if (ret) {
> +		struct v4l2_requestbuffers req_free = {
> +			.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> +			.memory = memory,
> +			.count = 0,
> +		};
> +
> +		vcam_ioc_reqbufs(file, dev, &req_free);
> +		dev->output_memory = VB2_MEMORY_DMABUF;
> +	}
> +out_free:
> +	kfree(frames);
> +	if (ret)
> +		return ret;
> +
> +	return 0;
> +}
> +
> +static int vcam_ioc_queue(struct file *file, struct vcam *dev,
> +			  struct vcam_ioc_queue *queue)
> +{
> +	struct v4l2_buffer buf = {
> +		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> +		.memory = dev->output_memory,
> +		.index = queue->index,
> +		.bytesused = queue->length,
> +	};
> +	u32 remainder;
> +	int ret;
> +
> +	if (queue->reserved)
> +		return -EINVAL;
> +
> +	if (dev->output_memory == VB2_MEMORY_DMABUF) {
> +		buf.m.fd = queue->fd;
> +		buf.length = dev->pix_format.sizeimage;
> +	}
> +
> +	buf.timestamp.tv_sec =
> +		div_u64_rem(queue->timestamp, NSEC_PER_SEC, &remainder);
> +	buf.timestamp.tv_usec = remainder / NSEC_PER_USEC;
> +
> +	scoped_guard(mutex, &dev->lock)
> +	{
> +		if (vb2_queue_is_busy(&dev->output_queue, file)) {
> +			ret = -EBUSY;
> +			break;
> +		}
> +
> +		if (vb2_is_streaming(&dev->capture_queue) &&
> +		    !vb2_is_streaming(&dev->output_queue)) {
> +			ret = vb2_streamon(&dev->output_queue, buf.type);
> +			if (ret)
> +				break;
> +		}
> +
> +		ret = vb2_qbuf(&dev->output_queue, dev->v4l2_dev.mdev, &buf);
> +	}
> +
> +	return ret;
> +}
> +
> +static int vcam_ioc_dequeue(struct file *file, struct vcam *dev,
> +			    struct vcam_ioc_dequeue *queue)
> +{
> +	struct v4l2_buffer buf = {
> +		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> +		.memory = dev->output_memory,
> +	};
> +	int ret;
> +
> +	scoped_guard(mutex, &dev->lock)
> +	{
> +		if (vb2_queue_is_busy(&dev->output_queue, file)) {
> +			ret = -EBUSY;
> +			break;
> +		}
> +
> +		ret = vb2_dqbuf(&dev->output_queue, &buf,
> +				file->f_flags & O_NONBLOCK);
> +	}
> +	if (ret)
> +		return ret;
> +
> +	queue->index = buf.index;
> +	queue->length = buf.bytesused;
> +	queue->timestamp = (u64)buf.timestamp.tv_sec * NSEC_PER_SEC +
> +			   (u64)buf.timestamp.tv_usec * NSEC_PER_USEC;
> +	return 0;
> +}
> +
> +static int vcam_ioc_status(struct vcam *dev, __u64 *status)
> +{
> +	*status = vcam_status_read(dev);
> +	return 0;
> +}
> +
> +static int vcam_ioc_wait(struct vcam *dev, struct vcam_ioc_wait *wait)
> +{
> +	int ret;
> +
> +	if (!wait->mask)
> +		return -EINVAL;
> +	if (wait->mask & ~VCAM_STATUS_MASK)
> +		return -EINVAL;
> +
> +	ret = wait_event_interruptible(dev->status_waitq,
> +				       vcam_status_mask_ready(dev, wait->mask));
> +	if (ret)
> +		return ret;
> +
> +	wait->status = vcam_status_read(dev);
> +	return 0;
> +}
> +
> +static long vcam_output_ioctl_core(struct file *file, unsigned int cmd,
> +				   void *arg)
> +{
> +	struct vcam *dev = file->private_data;
> +	long ret = 0;
> +
> +	switch (cmd) {
> +	case VCAM_IOC_QUEUE:
> +		ret = vcam_ioc_queue(file, dev, arg);
> +		break;
> +	case VCAM_IOC_DEQUEUE:
> +		ret = vcam_ioc_dequeue(file, dev, arg);
> +		break;
> +	case VCAM_IOC_STATUS:
> +		ret = vcam_ioc_status(dev, arg);
> +		break;
> +	case VCAM_IOC_WAIT:
> +		ret = vcam_ioc_wait(dev, arg);
> +		break;
> +	default:
> +		ret = -EOPNOTSUPP;
> +		break;
> +	}
> +
> +	return ret;
> +}
> +
> +static long vcam_ioctl_common(struct file *file, unsigned int cmd,
> +			      unsigned long arg)
> +{
> +	void __user *argp = (void __user *)arg;
> +	void *karg;
> +	size_t size;
> +	long ret;
> +
> +	switch (cmd) {
> +	case VCAM_IOC_QUEUE:
> +		size = sizeof(struct vcam_ioc_queue);
> +		break;
> +	case VCAM_IOC_DEQUEUE:
> +		size = sizeof(struct vcam_ioc_dequeue);
> +		break;
> +	case VCAM_IOC_STATUS:
> +		size = sizeof(__u64);
> +		break;
> +	case VCAM_IOC_WAIT:
> +		size = sizeof(struct vcam_ioc_wait);
> +		break;
> +	default:
> +		return -ENOTTY;
> +	}
> +
> +	if (size > SZ_4K)
> +		return -ENOTTY;
> +
> +	karg = kzalloc(size, GFP_KERNEL);
> +	if (!karg)
> +		return -ENOMEM;
> +
> +	if (copy_from_user(karg, argp, size)) {
> +		ret = -EFAULT;
> +		goto out_free;
> +	}
> +
> +	ret = vcam_output_ioctl_core(file, cmd, karg);
> +	if (ret)
> +		goto out_free;
> +
> +	if (copy_to_user(argp, karg, size)) {
> +		ret = -EFAULT;
> +		goto out_free;
> +	}
> +
> +	ret = 0;
> +out_free:
> +	kfree(karg);
> +	return ret;
> +}
> +
> +static void __vcam_release(struct vcam *dev)
> +{
> +	if (!dev->vdev)
> +		return;
> +
> +	vb2_queue_release(&dev->output_queue);
> +	vb2_queue_release(&dev->capture_queue);
> +
> +	if (video_is_registered(dev->vdev))
> +		video_unregister_device(dev->vdev);
> +	else
> +		video_device_release(dev->vdev);
> +
> +	v4l2_device_unregister(&dev->v4l2_dev);
> +
> +	dev->vdev = NULL;
> +	dev->device_nr = -1;
> +}
> +
> +static void vcam_release(struct kref *ref)
> +{
> +	struct vcam *dev;
> +
> +	dev = container_of(ref, struct vcam, ref);
> +
> +	if (!test_bit(VCAM_FLAG_CREATING, &dev->flags) || dev->device_nr < 0) {
> +		kfree(dev);
> +		return;
> +	}
> +
> +	__vcam_release(dev);
> +	kfree(dev);
> +}
> +
> +static int __vcam_close(struct inode *inode, struct file *file)
> +{
> +	struct vcam *dev = file->private_data;
> +
> +	if (dev->vdev && video_is_registered(dev->vdev))
> +		video_unregister_device(dev->vdev);
> +
> +	vb2_queue_release(&dev->output_queue);
> +
> +	dev->output_memory = VB2_MEMORY_DMABUF;
> +
> +	kref_put(&dev->ref, vcam_release);
> +	return 0;
> +}
> +
> +static int vcam_open(struct inode *inode, struct file *file)
> +{
> +	struct vcam *dev;
> +	int ret = nonseekable_open(inode, file);
> +
> +	if (ret)
> +		return ret;
> +
> +	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
> +	if (!dev)
> +		return -ENOMEM;
> +
> +	kref_init(&dev->ref);
> +	dev->device_nr = -1;
> +	file->private_data = dev;
> +	return 0;
> +}
> +
> +static int vcam_close(struct inode *inode, struct file *file)
> +{
> +	struct vcam *dev = file->private_data;
> +	int ret = 0;
> +
> +	if (!dev)
> +		return 0;
> +
> +	if (test_bit(VCAM_FLAG_CREATING, &dev->flags) && dev->device_nr >= 0)
> +		ret = __vcam_close(inode, file);
> +	else
> +		kref_put(&dev->ref, vcam_release);
> +
> +	file->private_data = NULL;
> +	return ret;
> +}
> +
> +static __poll_t vcam_poll(struct file *file, struct poll_table_struct *pts)
> +{
> +	struct vcam *dev = file->private_data;
> +
> +	if (!dev || !test_bit(VCAM_FLAG_CREATING, &dev->flags) ||
> +	    !test_bit(VCAM_FLAG_READY, &dev->flags) || dev->device_nr < 0)
> +		return POLLERR;
> +
> +	return vb2_core_poll(&dev->output_queue, file, pts);
> +}
> +
> +static int vcam_mmap(struct file *file, struct vm_area_struct *vma)
> +{
> +	struct vcam *dev = file->private_data;
> +
> +	if (!dev || !test_bit(VCAM_FLAG_CREATING, &dev->flags) ||
> +	    !test_bit(VCAM_FLAG_READY, &dev->flags) || dev->device_nr < 0)
> +		return -ENOTTY;
> +
> +	return vb2_mmap(&dev->output_queue, vma);
> +}
> +
> +static int vcam_vidioc_g_parm(struct file *file, void *priv,
> +			      struct v4l2_streamparm *parm)
> +{
> +	struct vcam *dev;
> +
> +	if (parm->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;
> +
> +	dev = video_drvdata(file);
> +	parm->parm.capture = dev->capture;
> +	return 0;
> +}
> +
> +static int vcam_vidioc_s_parm(struct file *file, void *priv,
> +			      struct v4l2_streamparm *parm)
> +{
> +	struct v4l2_fract *tpf = &parm->parm.capture.timeperframe;
> +	struct vcam *dev = video_drvdata(file);
> +
> +	if (!vcam_tpf_valid(tpf))
> +		return -EINVAL;
> +
> +	if (parm->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;
> +
> +	dev->capture.timeperframe = *tpf;
> +	parm->parm.capture = dev->capture;
> +	return 0;
> +}
> +
> +static int vcam_vidioc_enum_input(struct file *file, void *fh,
> +				  struct v4l2_input *inp)
> +{
> +	struct vcam *dev;
> +	__u32 index = inp->index;
> +
> +	if (index != 0)
> +		return -EINVAL;
> +
> +	memset(inp, 0, sizeof(*inp));
> +
> +	inp->index = index;
> +	strscpy(inp->name, "vcam", sizeof(inp->name));
> +	inp->type = V4L2_INPUT_TYPE_CAMERA;
> +	inp->audioset = 0;
> +	inp->tuner = 0;
> +	inp->status = 0;
> +
> +	dev = video_drvdata(file);
> +	if (!vb2_is_streaming(&dev->output_queue))
> +		inp->status |= V4L2_IN_ST_NO_SIGNAL;
> +
> +	return 0;
> +}
> +
> +static int vcam_vidioc_g_input(struct file *file, void *fh, unsigned int *i)
> +{
> +	*i = 0;
> +	return 0;
> +}
> +
> +static int vcam_vidioc_s_input(struct file *file, void *fh, unsigned int i)
> +{
> +	if (i == 0)
> +		return 0;
> +
> +	return -EINVAL;
> +}
> +
> +static int vcam_vidioc_streamon(struct file *file, void *fh,
> +				enum v4l2_buf_type type)
> +{
> +	struct vcam *dev = video_drvdata(file);
> +	int ret;
> +
> +	if (type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;
> +
> +	if (vb2_queue_is_busy(&dev->capture_queue, file))
> +		return -EBUSY;
> +
> +	ret = vb2_streamon(&dev->capture_queue, type);
> +	if (ret)
> +		return ret;
> +
> +	if (vb2_get_num_buffers(&dev->output_queue)) {
> +		ret = vb2_streamon(&dev->output_queue,
> +				   V4L2_BUF_TYPE_VIDEO_OUTPUT);
> +		if (ret) {
> +			vb2_streamoff(&dev->capture_queue, type);
> +			return ret;
> +		}
> +	}
> +
> +	return 0;
> +}
> +
> +static int vcam_vidioc_streamoff(struct file *file, void *fh,
> +				 enum v4l2_buf_type type)
> +{
> +	struct vcam *dev = video_drvdata(file);
> +	int ret;
> +
> +	if (type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> +		return -EINVAL;
> +
> +	if (vb2_queue_is_busy(&dev->capture_queue, file))
> +		return -EBUSY;
> +
> +	ret = vb2_streamoff(&dev->capture_queue, type);
> +	if (ret)
> +		return ret;
> +
> +	if (vb2_get_num_buffers(&dev->output_queue))
> +		vb2_streamoff(&dev->output_queue, V4L2_BUF_TYPE_VIDEO_OUTPUT);
> +
> +	return 0;
> +}
> +
> +static const struct v4l2_ioctl_ops vcam_ioctl_ops = {
> +	.vidioc_querycap = &vcam_vidioc_querycap,
> +	.vidioc_enum_framesizes = &vcam_vidioc_enum_framesizes,
> +	.vidioc_enum_frameintervals = &vcam_vidioc_enum_frameintervals,
> +	.vidioc_enum_input = &vcam_vidioc_enum_input,
> +	.vidioc_g_input = &vcam_vidioc_g_input,
> +	.vidioc_s_input = &vcam_vidioc_s_input,
> +	.vidioc_enum_fmt_vid_cap = &vcam_vidioc_enum_fmt_cap,
> +	.vidioc_g_fmt_vid_cap = &vcam_vidioc_g_fmt_vid_cap,
> +	.vidioc_s_fmt_vid_cap = &vcam_vidioc_s_fmt_vid_cap,
> +	.vidioc_try_fmt_vid_cap = &vcam_vidioc_try_fmt_vid_cap,
> +	.vidioc_g_parm = &vcam_vidioc_g_parm,
> +	.vidioc_s_parm = &vcam_vidioc_s_parm,
> +
> +	.vidioc_reqbufs = &vb2_ioctl_reqbufs,
> +	.vidioc_create_bufs = &vb2_ioctl_create_bufs,
> +	.vidioc_prepare_buf = &vb2_ioctl_prepare_buf,
> +	.vidioc_querybuf = &vb2_ioctl_querybuf,
> +	.vidioc_qbuf = &vb2_ioctl_qbuf,
> +	.vidioc_dqbuf = &vb2_ioctl_dqbuf,
> +	.vidioc_expbuf = &vb2_ioctl_expbuf,
> +	.vidioc_streamon = &vcam_vidioc_streamon,
> +	.vidioc_streamoff = &vcam_vidioc_streamoff,
> +};
> +
> +static enum vb2_buffer_state vcam_buf_fill(struct vcam *dev,
> +					   struct vcam_buf *buf,
> +					   const void *src, u32 src_len,
> +					   u64 timestamp)
> +{
> +	struct vb2_buffer *vb = &buf->vb.vb2_buf;
> +	u32 sequence;
> +	void *dst;
> +
> +	dst = vb2_plane_vaddr(vb, 0);
> +	if (!dst)
> +		return VB2_BUF_STATE_ERROR;
> +
> +	if (!src_len || src_len > dev->pix_format.sizeimage)
> +		src_len = dev->pix_format.sizeimage;
> +
> +	if (!src)
> +		return VB2_BUF_STATE_ERROR;
> +
> +	memcpy(dst, src, src_len);
> +
> +	sequence = (u32)(atomic_inc_return(&dev->sequence) - 1);
> +
> +	vb->timestamp = timestamp ? timestamp : ktime_get_ns();
> +	buf->vb.sequence = sequence;
> +	buf->vb.field = dev->pix_format.field;
> +	vb2_set_plane_payload(vb, 0, src_len);
> +
> +	return VB2_BUF_STATE_DONE;
> +}
> +
> +static bool vcam_buf_flip(struct vcam *dev, struct vb2_buffer *out_vb,
> +			  struct vcam_buf *cap_buf, u32 bytesused)
> +{
> +	struct vb2_buffer *cap_vb = &cap_buf->vb.vb2_buf;
> +	u32 sequence;
> +
> +	if (!out_vb->planes[0].dbuf || !cap_vb->planes[0].dbuf)
> +		return false;
> +
> +	if (out_vb->planes[0].dbuf != cap_vb->planes[0].dbuf)
> +		return false;
> +
> +	if (!bytesused)
> +		bytesused = dev->pix_format.sizeimage;
> +	if (bytesused > vb2_plane_size(cap_vb, 0))
> +		bytesused = vb2_plane_size(cap_vb, 0);
> +
> +	sequence = (u32)(atomic_inc_return(&dev->sequence) - 1);
> +
> +	cap_vb->timestamp = out_vb->timestamp ? out_vb->timestamp :
> +						ktime_get_ns();
> +	cap_buf->vb.sequence = sequence;
> +	cap_buf->vb.field = dev->pix_format.field;
> +	vb2_set_plane_payload(cap_vb, 0, bytesused);
> +
> +	return true;
> +}
> +
> +static bool vcam_buf_pair_dequeue(struct vcam *dev, struct vcam_buf **out_buf,
> +				  struct vcam_buf **cap_buf)
> +{
> +	unsigned long flags;
> +	bool dequeued = false;
> +
> +	spin_lock_irqsave(&dev->frame_lock, flags);
> +	if (!list_empty(&dev->output_list) && !list_empty(&dev->capture_list)) {
> +		*out_buf = list_first_entry(&dev->output_list, struct vcam_buf,
> +					    list);
> +		list_del(&(*out_buf)->list);
> +		*cap_buf = list_first_entry(&dev->capture_list, struct vcam_buf,
> +					    list);
> +		list_del(&(*cap_buf)->list);
> +		dequeued = true;
> +	}
> +	spin_unlock_irqrestore(&dev->frame_lock, flags);
> +	return dequeued;
> +}
> +
> +static void vcam_dequeue_frames(struct vcam *data)
> +{
> +	const struct vcam_format *format;
> +	enum vb2_buffer_state cap_state;
> +	struct vcam_buf *cap_buf;
> +	struct vcam_buf *out_buf;
> +	struct vb2_buffer *vb;
> +	bool zero_copy;
> +	u32 bytesused;
> +	void *src;
> +
> +	if (!vcam_is_streaming(data))
> +		return;
> +
> +	format = vcam_find_format(data->pix_format.pixelformat);
> +	while (vcam_buf_pair_dequeue(data, &out_buf, &cap_buf)) {
> +		cap_state = VB2_BUF_STATE_DONE;
> +		vb = &out_buf->vb.vb2_buf;
> +		bytesused = vb2_get_plane_payload(vb, 0);
> +		if (!bytesused || bytesused > data->pix_format.sizeimage)
> +			bytesused = data->pix_format.sizeimage;
> +
> +		if (bytesused < data->pix_format.sizeimage &&
> +		    (!format || !(format->flags & VCAM_COMPRESSED))) {
> +			cap_state = VB2_BUF_STATE_ERROR;
> +			goto out_done;
> +		}
> +
> +		zero_copy = vcam_buf_flip(data, vb, cap_buf, bytesused);
> +		if (!zero_copy &&
> +		    (!(out_buf->flags & VCAM_BUF_FLAG_MAPPABLE) ||
> +		     !(cap_buf->flags & VCAM_BUF_FLAG_MAPPABLE))) {
> +			dev_dbg(&data->vdev->dev,
> +				"unshared unmappable capture and output");
> +			cap_state = VB2_BUF_STATE_ERROR;
> +			goto out_done;
> +		}
> +		if (!zero_copy) {
> +			src = vb2_plane_vaddr(vb, 0);
> +			if (!src) {
> +				cap_state = VB2_BUF_STATE_ERROR;
> +				goto out_done;
> +			}
> +
> +			cap_state = vcam_buf_fill(data, cap_buf, src, bytesused,
> +						  vb->timestamp);
> +		}
> +out_done:
> +		vb2_buffer_done(&cap_buf->vb.vb2_buf, cap_state);
> +
> +		if (cap_state == VB2_BUF_STATE_ERROR)
> +			vb2_buffer_done(vb, VB2_BUF_STATE_ERROR);
> +		else
> +			vb2_buffer_done(vb, VB2_BUF_STATE_DONE);
> +	}
> +}
> +
> +static int vcam_vdev_open(struct file *file)
> +{
> +	struct vcam *dev;
> +	int ret;
> +
> +	dev = video_drvdata(file);
> +	if (test_and_set_bit(VCAM_FLAG_IS_OPEN, &dev->flags))
> +		return -EBUSY;
> +	if (dev->device_nr < 0 || !test_bit(VCAM_FLAG_READY, &dev->flags)) {
> +		clear_bit(VCAM_FLAG_IS_OPEN, &dev->flags);
> +		return -ENODEV;
> +	}
> +
> +	ret = v4l2_fh_open(file);
> +	if (ret) {
> +		clear_bit(VCAM_FLAG_IS_OPEN, &dev->flags);
> +		return ret;
> +	}
> +
> +	kref_get(&dev->ref);
> +	return 0;
> +}
> +
> +static int vcam_vdev_close(struct file *file)
> +{
> +	struct vcam *dev;
> +	int ret;
> +
> +	dev = video_drvdata(file);
> +	ret = _vb2_fop_release(file, NULL);
> +	clear_bit(VCAM_FLAG_IS_OPEN, &dev->flags);
> +
> +	kref_put(&dev->ref, vcam_release);
> +	return ret;
> +}
> +
> +static const struct v4l2_file_operations vcam_vdev_fops = {
> +	.owner = THIS_MODULE,
> +	.open = vcam_vdev_open,
> +	.release = vcam_vdev_close,
> +	.poll = vb2_fop_poll,
> +	.mmap = vb2_fop_mmap,
> +	.unlocked_ioctl = video_ioctl2,
> +};
> +
> +static int vcam_ioc_create_validate(struct vcam_ioc_create *config,
> +				    char *card_label)
> +{
> +	long len, i;
> +
> +	if (config->device_nr != 0)
> +		return -EINVAL;
> +	if (config->reserved)
> +		return -EINVAL;
> +	if (config->nr_frames > VCAM_MAX_FRAMES)
> +		return -E2BIG;
> +	if (config->nr_frames < VCAM_MIN_FRAMES)
> +		return -EINVAL;
> +	if (!config->frames)
> +		return -EINVAL;
> +
> +	memset(card_label, 0, VCAM_CARD_LABEL_MAX);
> +	len = strncpy_from_user(card_label,
> +				u64_to_user_ptr(config->device_name),
> +				VCAM_CARD_LABEL_MAX);
> +	if (len < 0)
> +		return -EFAULT;
> +	if (len >= VCAM_CARD_LABEL_MAX)
> +		return -E2BIG;
> +	if (!len)
> +		return -EINVAL;
> +	if (!isalnum((unsigned char)card_label[0]))
> +		return -EINVAL;
> +	for (i = 0; i < len; i++) {
> +		if (!isalnum((unsigned char)card_label[i]) &&
> +		    !isspace((unsigned char)card_label[i]))
> +			return -EINVAL;
> +	}
> +	if (!isalnum((unsigned char)card_label[len - 1]))
> +		return -EINVAL;
> +
> +	return len;
> +}
> +
> +static int vcam_vb2_queue_setup(struct vb2_queue *queue,
> +				unsigned int *nr_buffers,
> +				unsigned int *nr_planes, unsigned int sizes[],
> +				struct device *alloc_devs[])
> +{
> +	struct vcam *data = vb2_get_drv_priv(queue);
> +	unsigned int sizeimage = data->pix_format.sizeimage;
> +
> +	if (!sizeimage)
> +		return -EINVAL;
> +
> +	if (*nr_buffers < VCAM_MIN_FRAMES)
> +		*nr_buffers = VCAM_MIN_FRAMES;
> +
> +	if (*nr_planes)
> +		return sizes[0] < sizeimage ? -EINVAL : 0;
> +
> +	*nr_planes = 1;
> +	sizes[0] = sizeimage;
> +	return 0;
> +}
> +
> +static int vcam_vb2_buf_prepare(struct vb2_buffer *vb)
> +{
> +	struct vcam *data = vb2_get_drv_priv(vb->vb2_queue);
> +	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
> +	struct vcam_buf *buf = container_of(vbuf, struct vcam_buf, vb);
> +	unsigned int sizeimage = data->pix_format.sizeimage;
> +	unsigned int bytesused;
> +	void *vaddr;
> +
> +	if (vb2_plane_size(vb, 0) < sizeimage)
> +		return -EINVAL;
> +
> +	vbuf->field = data->pix_format.field;
> +	bytesused = vb2_get_plane_payload(vb, 0);
> +	if (V4L2_TYPE_IS_OUTPUT(vb->vb2_queue->type) && !bytesused)
> +		vb2_set_plane_payload(vb, 0, sizeimage);
> +
> +	buf->flags = VCAM_BUF_FLAG_MAPPABLE;
> +	if (vb->planes[0].dbuf) {
> +		vaddr = vb2_plane_vaddr(vb, 0);
> +		if (!vaddr)
> +			buf->flags &= ~VCAM_BUF_FLAG_MAPPABLE;
> +	}
> +	return 0;
> +}
> +
> +static void vcam_vb2_buf_queue(struct vb2_buffer *vb)
> +{
> +	struct vcam *data = vb2_get_drv_priv(vb->vb2_queue);
> +	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
> +	struct vcam_buf *buf;
> +	unsigned long flags;
> +
> +	buf = container_of(vbuf, struct vcam_buf, vb);
> +
> +	if (V4L2_TYPE_IS_OUTPUT(vb->vb2_queue->type)) {
> +		spin_lock_irqsave(&data->frame_lock, flags);
> +		list_add_tail(&buf->list, &data->output_list);
> +		spin_unlock_irqrestore(&data->frame_lock, flags);
> +	} else {
> +		spin_lock_irqsave(&data->frame_lock, flags);
> +		list_add_tail(&buf->list, &data->capture_list);
> +		spin_unlock_irqrestore(&data->frame_lock, flags);
> +	}
> +
> +	vcam_dequeue_frames(data);
> +}
> +
> +static int vcam_vb2_prepare_streaming(struct vb2_queue *vq)
> +{
> +	return 0;
> +}
> +
> +static int vcam_vb2_start_streaming(struct vb2_queue *vq, unsigned int count)
> +{
> +	struct vcam *data = vb2_get_drv_priv(vq);
> +
> +	if (V4L2_TYPE_IS_CAPTURE(vq->type)) {
> +		atomic_set(&data->sequence, 0);
> +		vcam_status_update_stream(data, true);
> +	}
> +
> +	vcam_dequeue_frames(data);
> +	return 0;
> +}
> +
> +static void vcam_vb2_stop_streaming(struct vb2_queue *vq)
> +{
> +	struct vcam *data = vb2_get_drv_priv(vq);
> +	struct vcam_buf *buf, *tmp;
> +	unsigned long flags;
> +	LIST_HEAD(done_list);
> +
> +	if (V4L2_TYPE_IS_CAPTURE(vq->type)) {
> +		vcam_status_update_stream(data, false);
> +		spin_lock_irqsave(&data->frame_lock, flags);
> +		list_splice_init(&data->capture_list, &done_list);
> +		list_splice_init(&data->output_list, &done_list);
> +		spin_unlock_irqrestore(&data->frame_lock, flags);
> +
> +		list_for_each_entry_safe(buf, tmp, &done_list, list)
> +			vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> +
> +		return;
> +	}
> +
> +	if (V4L2_TYPE_IS_OUTPUT(vq->type)) {
> +		spin_lock_irqsave(&data->frame_lock, flags);
> +		list_splice_init(&data->output_list, &done_list);
> +		list_splice_init(&data->capture_list, &done_list);
> +		spin_unlock_irqrestore(&data->frame_lock, flags);
> +
> +		list_for_each_entry_safe(buf, tmp, &done_list, list)
> +			vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> +	}
> +}
> +
> +static const struct vb2_ops vcam_vb2_ops = {
> +	.queue_setup = vcam_vb2_queue_setup,
> +	.buf_queue = vcam_vb2_buf_queue,
> +	.buf_prepare = vcam_vb2_buf_prepare,
> +	.prepare_streaming = vcam_vb2_prepare_streaming,
> +	.start_streaming = vcam_vb2_start_streaming,
> +	.stop_streaming = vcam_vb2_stop_streaming,
> +};
> +
> +static int vcam_ioc_create(struct file *file, struct vcam *dev,
> +			   struct vcam_ioc_create *config, char *card_label,
> +			   unsigned int len)
> +{
> +	struct v4l2_format try_fmt;
> +	struct video_device *vdev;
> +	struct vb2_queue *queue;
> +	struct v4l2_format fmt;
> +	long ret;
> +
> +	strscpy(dev->v4l2_dev.name, "vcam", sizeof(dev->v4l2_dev.name));
> +
> +	ret = v4l2_device_register(NULL, &dev->v4l2_dev);
> +	if (ret)
> +		return ret;
> +
> +	vdev = video_device_alloc();
> +	if (!vdev) {
> +		ret = -ENOMEM;
> +		goto err_unregister;
> +	}
> +
> +	dev->vdev = vdev;
> +	video_set_drvdata(vdev, dev);
> +	memcpy(vdev->name, card_label, len);
> +	vdev->name[len] = '\0';
> +	vdev->vfl_type = VFL_TYPE_VIDEO;
> +	vdev->fops = &vcam_vdev_fops;
> +	vdev->ioctl_ops = &vcam_ioctl_ops;
> +	vdev->release = &video_device_release;
> +	vdev->minor = -1;
> +	vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
> +	vdev->vfl_dir = VFL_DIR_RX;
> +
> +	mutex_init(&dev->lock);
> +	spin_lock_init(&dev->frame_lock);
> +	spin_lock_init(&dev->status_lock);
> +	INIT_LIST_HEAD(&dev->capture_list);
> +	INIT_LIST_HEAD(&dev->output_list);
> +	dev->status = VCAM_STATUS_IDLE;
> +	dev->output_memory = VB2_MEMORY_DMABUF;
> +	init_waitqueue_head(&dev->status_waitq);
> +
> +	dev->vdev->v4l2_dev = &dev->v4l2_dev;
> +	dev->vdev->queue = &dev->capture_queue;
> +	dev->vdev->lock = &dev->lock;
> +	dev->capture.capability = 0;
> +	dev->capture.capturemode = 0;
> +	dev->capture.extendedmode = 0;
> +	dev->capture.readbuffers = VCAM_MIN_FRAMES;
> +	dev->capture.timeperframe.numerator = 1;
> +	dev->capture.timeperframe.denominator = 30;
> +
> +	if (!IS_ENABLED(CONFIG_DMA_SHARED_BUFFER) ||
> +	    !vb2_vmalloc_memops.attach_dmabuf) {
> +		ret = -EOPNOTSUPP;
> +		goto err_unregister;
> +	}
> +
> +	fmt = (struct v4l2_format){
> +		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> +		.fmt.pix = { .width = config->width,
> +			     .height = config->height,
> +			     .pixelformat = config->pixelformat,
> +			     .colorspace = config->colorspace,
> +			     .bytesperline = config->bytesperline,
> +			     .field = V4L2_FIELD_NONE }
> +	};
> +
> +	try_fmt = fmt;
> +
> +	ret = vcam_set_format(dev, &try_fmt);
> +	if (ret)
> +		goto err_unregister;
> +
> +	if ((fmt.fmt.pix.width && try_fmt.fmt.pix.width != fmt.fmt.pix.width) ||
> +	    (fmt.fmt.pix.height &&
> +	     try_fmt.fmt.pix.height != fmt.fmt.pix.height) ||
> +	    try_fmt.fmt.pix.pixelformat != fmt.fmt.pix.pixelformat ||
> +	    (fmt.fmt.pix.colorspace != V4L2_COLORSPACE_DEFAULT &&
> +	     try_fmt.fmt.pix.colorspace != fmt.fmt.pix.colorspace) ||
> +	    (fmt.fmt.pix.bytesperline &&
> +	     try_fmt.fmt.pix.bytesperline != fmt.fmt.pix.bytesperline)) {
> +		ret = -EINVAL;
> +		goto err_unregister;
> +	}
> +
> +	dev->pix_format = try_fmt.fmt.pix;
> +
> +	queue = &dev->capture_queue;
> +	queue->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
> +	queue->io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
> +	queue->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
> +	queue->drv_priv = dev;
> +	queue->buf_struct_size = sizeof(struct vcam_buf);
> +	queue->ops = &vcam_vb2_ops;
> +	queue->mem_ops = &vb2_vmalloc_memops;
> +	queue->lock = &dev->lock;
> +	queue->dev = &dev->vdev->dev;
> +	ret = vb2_queue_init(queue);
> +	if (ret)
> +		goto err_unregister;
> +
> +	queue = &dev->output_queue;
> +	queue->type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> +	queue->io_modes = VB2_DMABUF;
> +	queue->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_COPY;
> +	queue->drv_priv = dev;
> +	queue->buf_struct_size = sizeof(struct vcam_buf);
> +	queue->ops = &vcam_vb2_ops;
> +	queue->mem_ops = &vb2_vmalloc_memops;
> +	queue->lock = &dev->lock;
> +	queue->dev = &dev->vdev->dev;
> +	ret = vb2_queue_init(queue);
> +	if (ret)
> +		goto err_capture_queue;
> +
> +	ret = vcam_ioc_alloc(file, dev, config->nr_frames,
> +			     u64_to_user_ptr(config->frames),
> +			     VB2_MEMORY_DMABUF);
> +	if (ret)
> +		goto err_output_queue;
> +
> +	ret = video_register_device(dev->vdev, VFL_TYPE_VIDEO, -1);
> +	if (ret < 0)
> +		goto err_output_queue;
> +
> +	config->device_nr = dev->vdev->num;
> +	return 0;
> +
> +err_output_queue:
> +	vb2_queue_release(&dev->output_queue);
> +
> +err_capture_queue:
> +	vb2_queue_release(&dev->capture_queue);
> +
> +err_unregister:
> +	if (dev->vdev)
> +		video_device_release(dev->vdev);
> +	v4l2_device_unregister(&dev->v4l2_dev);
> +	return ret;
> +}
> +
> +static long vcam_ioctl(struct file *file, unsigned int cmd, unsigned long parm)
> +{
> +	struct vcam *dev = file->private_data;
> +	char card_label[VCAM_CARD_LABEL_MAX];
> +	struct vcam_ioc_create config;
> +	long ret, len;
> +
> +	if (cmd != VCAM_IOC_CREATE) {
> +		if (!dev || !test_bit(VCAM_FLAG_CREATING, &dev->flags) ||
> +		    !test_bit(VCAM_FLAG_READY, &dev->flags) ||
> +		    dev->device_nr < 0)
> +			return -ENOTTY;
> +		return vcam_ioctl_common(file, cmd, parm);
> +	}
> +
> +	if (!dev)
> +		return -ENOTTY;
> +
> +	if (test_and_set_bit(VCAM_FLAG_CREATING, &dev->flags))
> +		return -EBUSY;
> +
> +	if (!parm) {
> +		ret = -EINVAL;
> +		goto err_clear;
> +	}
> +
> +	if (copy_from_user(&config, (void *)parm, sizeof(config))) {
> +		ret = -EFAULT;
> +		goto err_clear;
> +	}
> +
> +	len = vcam_ioc_create_validate(&config, card_label);
> +	if (len < 0) {
> +		ret = len;
> +		goto err_clear;
> +	}
> +
> +	ret = vcam_ioc_create(file, dev, &config, card_label, len);
> +	if (ret)
> +		goto err_clear;
> +
> +	if (copy_to_user((void *)parm, &config, sizeof(config))) {
> +		ret = -EFAULT;
> +		goto err_release;
> +	}
> +
> +	dev->device_nr = dev->vdev->num;
> +	snprintf(dev->v4l2_dev.name, sizeof(dev->v4l2_dev.name), "vcam-%d",
> +		 dev->device_nr);
> +	set_bit(VCAM_FLAG_READY, &dev->flags);
> +	return 0;
> +
> +err_release:
> +	__vcam_release(dev);
> +
> +err_clear:
> +	clear_bit(VCAM_FLAG_CREATING, &dev->flags);
> +	return ret;
> +}
> +
> +static const struct file_operations vcam_fops = {
> +	.owner = THIS_MODULE,
> +	.open = vcam_open,
> +	.unlocked_ioctl = vcam_ioctl,
> +#ifdef CONFIG_COMPAT
> +	.compat_ioctl = vcam_ioctl,
> +#endif
> +	.poll = vcam_poll,
> +	.mmap = vcam_mmap,
> +	.release = vcam_close,
> +	.llseek = noop_llseek,
> +};
> +
> +static struct miscdevice vcam_misc = {
> +	.minor = MISC_DYNAMIC_MINOR,
> +	.name = "vcam",
> +	.fops = &vcam_fops,
> +	.groups = vcam_attr_groups,
> +};
> +
> +module_misc_device(vcam_misc);
> diff --git a/include/uapi/linux/vcam.h b/include/uapi/linux/vcam.h
> new file mode 100644
> index 000000000000..aca0d1d32ee5
> --- /dev/null
> +++ b/include/uapi/linux/vcam.h
> @@ -0,0 +1,124 @@
> +/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */
> +/*
> + * Copyright (c) Jarkko Sakkinen 2025-2026
> + */
> +
> +#ifndef _UAPI_LINUX_VCAM_H
> +#define _UAPI_LINUX_VCAM_H
> +
> +#include <linux/types.h>
> +#include <linux/ioctl.h>
> +
> +#define VCAM_IOC_BASE 'v'
> +
> +/**
> + * DOC: vcam uAPI
> + *
> + * The ioctl API of /dev/vcam provides ioctls for creating DMA-BUF backed
> + * virtual capture devices, and pushing image frames for consumption.
> + *
> + * Frames are queued with %VCAM_IOC_QUEUE and recycled with %VCAM_IOC_DEQUEUE.
> + * Queueing without dequeuing eventually exhausts the output queue.
> + */
> +
> +/**
> + * enum vcam_status - Status bits
> + * @VCAM_STATUS_IDLE: Capture queue is not streaming.
> + * @VCAM_STATUS_STREAMING: Capture queue is streaming.
> + */
> +enum vcam_status {
> +	VCAM_STATUS_IDLE = 1U << 0,
> +	VCAM_STATUS_STREAMING = 1U << 1,
> +};
> +
> +/**
> + * struct vcam_ioc_create - Create a virtual camera device
> + * @device_name: (input) User pointer to device name string.
> + * @width: (input) Frame width in pixels. Must be non-zero.
> + * @height: (input) Frame height in pixels. Must be non-zero.
> + * @pixelformat: (input) Four CC format code.
> + * @colorspace: (input) V4L2 colorspace value.
> + * @bytesperline: (input) Bytes per line in the output format.
> + * @reserved: Reserved for future use. Must be set to zero.
> + * @device_nr: (output) Device number (must be 0 on input).
> + * @nr_frames: (input) Number of entries in @frames.
> + * @frames: (input/output) User pointer to an array of &struct vcam_frame.
> + */
> +struct vcam_ioc_create {
> +	__u64 device_name;
> +	__u32 width;
> +	__u32 height;
> +	__u32 pixelformat;
> +	__u32 colorspace;
> +	__u32 bytesperline;
> +	__u32 reserved;
> +	__u32 device_nr;
> +	__u32 nr_frames;
> +	__u64 frames;
> +};
> +
> +/**
> + * struct vcam_frame - a frame descriptor
> + * @index: Frame index assigned by the driver.
> + * @length: Frame size in bytes.
> + */
> +struct vcam_frame {
> +	__u32 index;
> +	__u32 length;
> +};
> +
> +/**
> + * struct vcam_ioc_queue - Produce an output buffer
> + * @fd: (input) DMA-BUF file descriptor.
> + * @index: (input) Buffer index for %VCAM_IOC_QUEUE.
> + * @length: (input) Payload length in bytes for %VCAM_IOC_QUEUE.
> + * @reserved: Reserved for future use. Must be set to zero.
> + * @timestamp: (input) Timestamp in nanoseconds for %VCAM_IOC_QUEUE.
> + */
> +struct vcam_ioc_queue {
> +	__u32 fd;
> +	__u32 index;
> +	__u32 length;
> +	__u32 reserved;
> +	__u64 timestamp;
> +};
> +
> +/**
> + * struct vcam_ioc_dequeue - Dequeue an output buffer
> + * @index: (output) Buffer index for %VCAM_IOC_DEQUEUE.
> + * @length: (output) Payload length in bytes for %VCAM_IOC_DEQUEUE.
> + * @timestamp: (output) Timestamp in nanoseconds for %VCAM_IOC_DEQUEUE.
> + */
> +struct vcam_ioc_dequeue {
> +	__u32 index;
> +	__u32 length;
> +	__u64 timestamp;
> +};
> +
> +/**
> + * struct vcam_ioc_wait - Wait for capture status
> + * @mask: (input) Mask of status bits to wait for.
> + * @status: (output) Current status bit mask.
> + */
> +struct vcam_ioc_wait {
> +	__u64 mask;
> +	__u64 status;
> +};
> +
> +/**
> + * DOC: vcam ioctls
> + *
> + * %VCAM_IOC_CREATE: Creates a virtual camera device and associates output
> + * buffers described by &struct vcam_frame with DMA-BUF file descriptors.
> + * %VCAM_IOC_QUEUE: Enqueues an output buffer for capture.
> + * %VCAM_IOC_DEQUEUE: Dequeues a consumed output buffer for reuse.
> + * %VCAM_IOC_STATUS: Reads the driver status bits.
> + * %VCAM_IOC_WAIT: Waits for the subset of status bits to activate.
> + */
> +#define VCAM_IOC_CREATE _IOWR(VCAM_IOC_BASE, 0x00, struct vcam_ioc_create)
> +#define VCAM_IOC_QUEUE _IOW(VCAM_IOC_BASE, 0x01, struct vcam_ioc_queue)
> +#define VCAM_IOC_DEQUEUE _IOR(VCAM_IOC_BASE, 0x02, struct vcam_ioc_dequeue)
> +#define VCAM_IOC_STATUS _IOR(VCAM_IOC_BASE, 0x03, __u64)
> +#define VCAM_IOC_WAIT _IOWR(VCAM_IOC_BASE, 0x04, struct vcam_ioc_wait)
> +
> +#endif /* _UAPI_LINUX_VCAM_H */

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Jarkko Sakkinen 5 days, 17 hours ago
On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
> Hi Jarkko,
> 
> Thank you for the patch.
> 
> On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> > vcam is a DMA-BUF backed virtual camera driver capable of creating video
> > capture devices to which data can be streamed through /dev/vcam after
> > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> > with VCAM_IOC_DEQUEUE.
> > 
> > Zero-copy semantics are supported for shared DMA-BUF between capture and
> > output.
> >
> > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> > ---
> > Early feedback e.g., is this completely in wrong direction? V4L2 world
> > is relatively alien world, and thus I need a sanity check ;-)
> 
> We already have multiple virtual drivers, including vivid and vimc.
> Could you please explain the rationale for yet another one, and why the
> new features it provides (if any) can't be added to existing drivers ?

There is a notable user base for v4l2-loopback. It is the defacto choice
for streaming phone cams.

The motivation here is to provide a service optimized for that use and
purpose. It's virtual but non-generic i.e. not aimed for testing/emulation.

> 
> >  .../driver-api/media/drivers/index.rst        |    1 +
> >  .../driver-api/media/drivers/vcam.rst         |   16 +
> >  MAINTAINERS                                   |    8 +
> >  drivers/media/Kconfig                         |   13 +
> >  drivers/media/Makefile                        |    1 +
> >  drivers/media/vcam.c                          | 1700 +++++++++++++++++
> >  include/uapi/linux/vcam.h                     |  124 ++
> >  7 files changed, 1863 insertions(+)
> >  create mode 100644 Documentation/driver-api/media/drivers/vcam.rst
> >  create mode 100644 drivers/media/vcam.c
> >  create mode 100644 include/uapi/linux/vcam.h
> > 
> > diff --git a/Documentation/driver-api/media/drivers/index.rst b/Documentation/driver-api/media/drivers/index.rst
> > index 7f6f3dcd5c90..211cafc9c070 100644
> > --- a/Documentation/driver-api/media/drivers/index.rst
> > +++ b/Documentation/driver-api/media/drivers/index.rst
> > @@ -27,6 +27,7 @@ Video4Linux (V4L) drivers
> >  	zoran
> >  	ccs/ccs
> >  	ipu6
> > +	vcam
> >  
> >  
> >  Digital TV drivers
> > diff --git a/Documentation/driver-api/media/drivers/vcam.rst b/Documentation/driver-api/media/drivers/vcam.rst
> > new file mode 100644
> > index 000000000000..b5a23144ebee
> > --- /dev/null
> > +++ b/Documentation/driver-api/media/drivers/vcam.rst
> > @@ -0,0 +1,16 @@
> > +.. SPDX-License-Identifier: GPL-2.0
> > +
> > +===========================
> > +vcam: Virtual Camera Driver
> > +===========================
> > +
> > +Theory of Operation
> > +-------------------
> > +
> > +.. kernel-doc:: drivers/media/vcam.c
> > +   :doc: Theory of Operation
> > +
> > +Driver uAPI
> > +-----------
> > +
> > +.. kernel-doc:: include/uapi/linux/vcam.h
> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index 6863d5fa07a1..b8444ff48716 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -27504,6 +27504,14 @@ S:	Maintained
> >  F:	drivers/media/common/videobuf2/*
> >  F:	include/media/videobuf2-*
> >  
> > +VCAM V4L2 DRIVER
> > +M:	Jarkko Sakkinen <jarkko@kernel.org>
> > +L:	linux-media@vger.kernel.org
> > +S:	Maintained
> > +T:	git git://git.kernel.org/pub/scm/linux/kernel/git/jarkko/linux-tpmdd.git
> > +F:	drivers/media/vcam.c
> > +F:	include/uapi/linux/vcam.h
> > +
> >  VIDTV VIRTUAL DIGITAL TV DRIVER
> >  M:	Daniel W. S. Almeida <dwlsalmeida@gmail.com>
> >  L:	linux-media@vger.kernel.org
> > diff --git a/drivers/media/Kconfig b/drivers/media/Kconfig
> > index 6abc9302cd84..f2f4b2ec9135 100644
> > --- a/drivers/media/Kconfig
> > +++ b/drivers/media/Kconfig
> > @@ -239,6 +239,19 @@ source "drivers/media/firewire/Kconfig"
> >  # Common driver options
> >  source "drivers/media/common/Kconfig"
> >  
> > +config VCAM
> > +	tristate "V4L2 virtual camera"
> > +	depends on VIDEO_DEV
> > +	default m
> > +	select VIDEOBUF2_VMALLOC
> > +	help
> > +	  Say Y here to enable a DMA-BUF backed virtual camera driver capable
> > +	  of creating video capture devices to which data can be streamed
> > +	  through /dev/vcam after calling VCAM_IOC_CREATE. Frames are pushed
> > +	  with VCAM_IOC_QUEUE and recycled with VCAM_IOC_DEQUEUE.
> > +
> > +	  When in doubt, say N.
> > +
> >  endmenu
> >  
> >  #
> > diff --git a/drivers/media/Makefile b/drivers/media/Makefile
> > index 20fac24e4f0f..d539fecbe498 100644
> > --- a/drivers/media/Makefile
> > +++ b/drivers/media/Makefile
> > @@ -32,3 +32,4 @@ obj-$(CONFIG_CEC_CORE) += cec/
> >  obj-y += common/ platform/ pci/ usb/ mmc/ firewire/ spi/ test-drivers/
> >  obj-$(CONFIG_VIDEO_DEV) += radio/
> >  
> > +obj-$(CONFIG_VCAM) += vcam.o
> > diff --git a/drivers/media/vcam.c b/drivers/media/vcam.c
> > new file mode 100644
> > index 000000000000..82f4351d0499
> > --- /dev/null
> > +++ b/drivers/media/vcam.c
> > @@ -0,0 +1,1700 @@
> > +// SPDX-License-Identifier: GPL-2.0-only
> > +/*
> > + * Copyright (c) Jarkko Sakkinen 2025-2026
> > + *
> > + * Derived originally from v4l2loopback driver but is essentially a rewrite.
> > + */
> > +
> > +/**
> > + * DOC: Theory of Operation
> > + *
> > + * The driver exposes /dev/vcam for creating virtual capture devices via
> > + * %VCAM_IOC_CREATE. The ioctl registers a video capture node and associates
> > + * output buffers described by &struct vcam_frame with DMA-BUF file descriptors
> > + * supplied by the caller. This also keeps output buffers owned by the caller,
> > + * and accounted from the calling process.
> > + *
> > + * Frames are pushed to the capture device by queueing output buffers using
> > + * %VCAM_IOC_QUEUE, and recycling them with %VCAM_IOC_DEQUEUE. Queueing without
> > + * dequeuing eventually exhausts the output queue and stalls the producer.
> > + *
> > + * If both buffers reference the same DMA-BUF, the driver performs a zero-copy
> > + * transfer by propagating metadata. Otherwise, if both buffers are mappable,
> > + * the payload is copied into the capture buffer. When neither zero-copy nor a
> > + * CPU mapping is possible, the capture buffer completes with an error.
> > + */
> > +
> > +#include <linux/cleanup.h>
> > +#include <linux/bitops.h>
> > +#include <linux/atomic.h>
> > +#include <linux/ctype.h>
> > +#include <linux/compat.h>
> > +#include <linux/dma-buf.h>
> > +#include <linux/dma-mapping.h>
> > +#include <linux/fdtable.h>
> > +#include <linux/file.h>
> > +#include <linux/fs.h>
> > +#include <linux/limits.h>
> > +#include <linux/device.h>
> > +#include <linux/mm.h>
> > +#include <linux/module.h>
> > +#include <linux/miscdevice.h>
> > +#include <linux/poll.h>
> > +#include <linux/sched.h>
> > +#include <linux/time.h>
> > +#include <linux/time64.h>
> > +#include <linux/math64.h>
> > +#include <linux/minmax.h>
> > +#include <linux/slab.h>
> > +#include <linux/string.h>
> > +#include <linux/spinlock.h>
> > +#include <linux/sysfs.h>
> > +#include <linux/time.h>
> > +#include <linux/videodev2.h>
> > +#include <linux/wait.h>
> > +#include <media/v4l2-common.h>
> > +#include <media/v4l2-device.h>
> > +#include <media/v4l2-ioctl.h>
> > +#include <media/videobuf2-v4l2.h>
> > +#include <media/videobuf2-vmalloc.h>
> > +#include <uapi/linux/vcam.h>
> > +
> > +#undef pr_fmt
> > +#define pr_fmt(fmt) "vcam: " fmt
> > +
> > +MODULE_DESCRIPTION("V4L2 virtual camera driver");
> > +MODULE_LICENSE("GPL");
> > +
> > +#define VCAM_CARD_LABEL_MAX sizeof_field(struct video_device, name)
> > +#define VCAM_FPS_MIN 1
> > +#define VCAM_FPS_MAX 1000
> > +
> > +#define VCAM_MIN_WIDTH 2
> > +#define VCAM_MIN_HEIGHT 2
> > +#define VCAM_MAX_WIDTH 8192
> > +#define VCAM_MAX_HEIGHT 8192
> > +#define VCAM_DEFAULT_WIDTH 640
> > +#define VCAM_DEFAULT_HEIGHT 480
> > +
> > +#define VCAM_MAX_FORMATS 16
> > +#define VCAM_MIN_FRAMES 2
> > +#define VCAM_MAX_FRAMES 32
> > +
> > +#define VCAM_STATUS_MASK (VCAM_STATUS_IDLE | VCAM_STATUS_STREAMING)
> > +
> > +enum vcam_flags {
> > +	VCAM_FLAG_IS_OPEN = 0x01,
> > +	VCAM_FLAG_CREATING = 0x02,
> > +	VCAM_FLAG_READY = 0x04,
> > +};
> > +
> > +struct vcam_buf {
> > +	struct vb2_v4l2_buffer vb;
> > +	struct list_head list;
> > +	unsigned long flags;
> > +};
> > +
> > +enum vcam_buf_flags {
> > +	VCAM_BUF_FLAG_MAPPABLE = BIT(0),
> > +};
> > +
> > +struct vcam {
> > +	unsigned long flags;
> > +	int device_nr;
> > +	struct v4l2_device v4l2_dev;
> > +	struct video_device *vdev;
> > +	struct vb2_queue capture_queue;
> > +	struct vb2_queue output_queue;
> > +	struct v4l2_pix_format pix_format;
> > +	struct v4l2_captureparm capture;
> > +	atomic_t sequence;
> > +	struct list_head capture_list;
> > +	struct list_head output_list;
> > +	u64 status;
> > +	wait_queue_head_t status_waitq;
> > +	enum vb2_memory output_memory;
> > +
> > +	/* Protects status flags and wait queue updates. */
> > +	spinlock_t status_lock;
> > +
> > +	/* Shared lock for vdev and VB2 queues. */
> > +	struct mutex lock;
> > +
> > +	/* Protects capture_list and output_list. */
> > +	spinlock_t frame_lock;
> > +
> > +	/*
> > +	 * Maintains a shared reference between processes having either
> > +	 * /dev/vcam or /dev/videoX open.
> > +	 */
> > +	struct kref ref;
> > +};
> > +
> > +enum vcam_format_flags {
> > +	VCAM_PLANAR = BIT(0),
> > +	VCAM_COMPRESSED = BIT(1),
> > +};
> > +
> > +struct vcam_format {
> > +	int fourcc;
> > +	int depth;
> > +	int flags;
> > +};
> > +
> > +const struct vcam_format vcam_formats[] = {
> > +	{
> > +		.fourcc = V4L2_PIX_FMT_YUYV,
> > +		.depth = 16,
> > +		.flags = 0,
> > +	},
> > +	{
> > +		.fourcc = V4L2_PIX_FMT_NV12,
> > +		.depth = 12,
> > +		.flags = VCAM_PLANAR,
> > +	},
> > +	{
> > +		.fourcc = V4L2_PIX_FMT_MJPEG,
> > +		.depth = 32,
> > +		.flags = VCAM_COMPRESSED,
> > +	},
> > +};
> > +
> > +#define VCAM_NR_FORMATS ARRAY_SIZE(vcam_formats)
> > +
> > +static const struct vcam_format *vcam_find_format(int fourcc)
> > +{
> > +	unsigned int i;
> > +
> > +	for (i = 0; i < VCAM_NR_FORMATS; i++) {
> > +		if (vcam_formats[i].fourcc == fourcc)
> > +			return vcam_formats + i;
> > +	}
> > +
> > +	return NULL;
> > +}
> > +
> > +static void vcam_fmt_descr(char *dst, size_t dst_len, u32 format)
> > +{
> > +	snprintf(dst, dst_len, "[%c%c%c%c]", (format >> 0) & 0xFF,
> > +		 (format >> 8) & 0xFF, (format >> 16) & 0xFF,
> > +		 (format >> 24) & 0xFF);
> > +}
> > +
> > +static void vcam_fourcc_str(char *dst, u32 format)
> > +{
> > +	dst[0] = (format >> 0) & 0xFF;
> > +	dst[1] = (format >> 8) & 0xFF;
> > +	dst[2] = (format >> 16) & 0xFF;
> > +	dst[3] = (format >> 24) & 0xFF;
> > +	dst[4] = '\0';
> > +}
> > +
> > +static inline bool vcam_is_streaming(struct vcam *data)
> > +{
> > +	return vb2_is_streaming(&data->output_queue) ||
> > +	       vb2_is_streaming(&data->capture_queue);
> > +}
> > +
> > +static bool vcam_status_mask_ready(struct vcam *dev, u64 mask)
> > +{
> > +	unsigned long flags;
> > +	bool ready;
> > +
> > +	spin_lock_irqsave(&dev->status_lock, flags);
> > +	ready = (dev->status & mask) == mask;
> > +	spin_unlock_irqrestore(&dev->status_lock, flags);
> > +
> > +	return ready;
> > +}
> > +
> > +static void vcam_status_update_stream(struct vcam *dev, bool on)
> > +{
> > +	unsigned long flags;
> > +	u64 old_flags;
> > +	u64 new_flags;
> > +
> > +	spin_lock_irqsave(&dev->status_lock, flags);
> > +	old_flags = dev->status;
> > +	if (on) {
> > +		dev->status &= ~VCAM_STATUS_IDLE;
> > +		dev->status |= VCAM_STATUS_STREAMING;
> > +	} else {
> > +		dev->status &= ~VCAM_STATUS_STREAMING;
> > +		dev->status |= VCAM_STATUS_IDLE;
> > +	}
> > +	new_flags = dev->status;
> > +	spin_unlock_irqrestore(&dev->status_lock, flags);
> > +
> > +	if (new_flags != old_flags)
> > +		wake_up_interruptible(&dev->status_waitq);
> > +}
> > +
> > +static u64 vcam_status_read(struct vcam *dev)
> > +{
> > +	unsigned long flags;
> > +	u64 flags_snapshot;
> > +
> > +	spin_lock_irqsave(&dev->status_lock, flags);
> > +	flags_snapshot = dev->status;
> > +	spin_unlock_irqrestore(&dev->status_lock, flags);
> > +
> > +	return flags_snapshot;
> > +}
> > +
> > +static bool vcam_tpf_valid(const struct v4l2_fract *tpf)
> > +{
> > +	u64 min_den = (u64)tpf->numerator * VCAM_FPS_MIN;
> > +	u64 max_den = (u64)tpf->numerator * VCAM_FPS_MAX;
> > +
> > +	if (!tpf->numerator || !tpf->denominator)
> > +		return false;
> > +	if ((u64)tpf->denominator < min_den)
> > +		return false;
> > +	if ((u64)tpf->denominator > max_den)
> > +		return false;
> > +
> > +	return true;
> > +}
> > +
> > +static bool vcam_pix_format_eq(const struct v4l2_pix_format *src,
> > +			       const struct v4l2_pix_format *dest)
> > +{
> > +	return src->width == dest->width && src->height == dest->height &&
> > +	       src->pixelformat == dest->pixelformat;
> > +}
> > +
> > +static int vcam_set_format(struct vcam *dev, struct v4l2_format *fmt)
> > +{
> > +	struct v4l2_pix_format *pix = &fmt->fmt.pix;
> > +	const struct vcam_format *format;
> > +	u64 bytesperline;
> > +	u64 sizeimage;
> > +
> > +	if (V4L2_TYPE_IS_MULTIPLANAR(fmt->type))
> > +		return -EINVAL;
> > +
> > +	if (!pix->width)
> > +		pix->width = VCAM_DEFAULT_WIDTH;
> > +	if (!pix->height)
> > +		pix->height = VCAM_DEFAULT_HEIGHT;
> > +
> > +	pix->width = clamp(pix->width, VCAM_MIN_WIDTH, VCAM_MAX_WIDTH);
> > +	pix->height = clamp(pix->height, VCAM_MIN_HEIGHT, VCAM_MAX_HEIGHT);
> > +
> > +	format = vcam_find_format(pix->pixelformat);
> > +	if (!format) {
> > +		format = &vcam_formats[0];
> > +		pix->pixelformat = format->fourcc;
> > +	}
> > +
> > +	if (format->flags & VCAM_PLANAR) {
> > +		pix->bytesperline = pix->width;
> > +		sizeimage = ((u64)pix->width * pix->height * format->depth) >>
> > +			    3;
> > +	} else if (format->flags & VCAM_COMPRESSED) {
> > +		pix->bytesperline = 0;
> > +		sizeimage = ((u64)pix->width * pix->height * format->depth) >>
> > +			    3;
> > +	} else {
> > +		bytesperline = ((u64)pix->width * format->depth) >> 3;
> > +		if (bytesperline > U32_MAX)
> > +			return -EOVERFLOW;
> > +
> > +		pix->bytesperline = bytesperline;
> > +		sizeimage = (u64)pix->height * bytesperline;
> > +	}
> > +
> > +	if (sizeimage > U32_MAX)
> > +		return -EOVERFLOW;
> > +
> > +	pix->sizeimage = sizeimage;
> > +
> > +	if (pix->colorspace == V4L2_COLORSPACE_DEFAULT ||
> > +	    pix->colorspace > V4L2_COLORSPACE_DCI_P3)
> > +		pix->colorspace = V4L2_COLORSPACE_SRGB;
> > +	if (pix->field == V4L2_FIELD_ANY)
> > +		pix->field = V4L2_FIELD_NONE;
> > +
> > +	return 0;
> > +}
> > +
> > +static int vcam_vidioc_querycap(struct file *file, void *priv,
> > +				struct v4l2_capability *cap)
> > +{
> > +	__u32 capabilities = V4L2_CAP_STREAMING | V4L2_CAP_VIDEO_CAPTURE;
> > +	struct vcam *dev = video_drvdata(file);
> > +
> > +	cap->device_caps = capabilities;
> > +	cap->capabilities = capabilities | V4L2_CAP_DEVICE_CAPS;
> > +
> > +	strscpy(cap->driver, "vcam", sizeof(cap->driver));
> > +	strscpy(cap->card, dev->vdev->name, sizeof(cap->card));
> > +	snprintf(cap->bus_info, sizeof(cap->bus_info), "vcam:%d",
> > +		 dev->device_nr);
> > +
> > +	return 0;
> > +}
> > +
> > +static int vcam_enum_framesizes(struct vcam *dev, struct v4l2_frmsizeenum *argp)
> > +{
> > +	if (argp->index)
> > +		return -EINVAL;
> > +
> > +	if (vcam_is_streaming(dev)) {
> > +		if (argp->pixel_format != dev->pix_format.pixelformat)
> > +			return -EINVAL;
> > +
> > +		argp->type = V4L2_FRMSIZE_TYPE_DISCRETE;
> > +
> > +		argp->discrete.width = dev->pix_format.width;
> > +		argp->discrete.height = dev->pix_format.height;
> > +	} else {
> > +		if (!vcam_find_format(argp->pixel_format))
> > +			return -EINVAL;
> > +
> > +		argp->type = V4L2_FRMSIZE_TYPE_CONTINUOUS;
> > +
> > +		argp->stepwise.min_width = VCAM_MIN_WIDTH;
> > +		argp->stepwise.min_height = VCAM_MIN_HEIGHT;
> > +		argp->stepwise.max_width = VCAM_MAX_WIDTH;
> > +		argp->stepwise.max_height = VCAM_MAX_HEIGHT;
> > +		argp->stepwise.step_width = 1;
> > +		argp->stepwise.step_height = 1;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +static int vcam_enum_frameintervals(struct vcam *dev,
> > +				    struct v4l2_frmivalenum *argp)
> > +{
> > +	if (argp->index)
> > +		return -EINVAL;
> > +
> > +	if (vcam_is_streaming(dev)) {
> > +		if (argp->width != dev->pix_format.width ||
> > +		    argp->height != dev->pix_format.height ||
> > +		    argp->pixel_format != dev->pix_format.pixelformat)
> > +			return -EINVAL;
> > +
> > +		argp->type = V4L2_FRMIVAL_TYPE_DISCRETE;
> > +		argp->discrete = dev->capture.timeperframe;
> > +	} else {
> > +		if (argp->width < VCAM_MIN_WIDTH ||
> > +		    argp->width > VCAM_MAX_WIDTH ||
> > +		    argp->height < VCAM_MIN_HEIGHT ||
> > +		    argp->height > VCAM_MAX_HEIGHT ||
> > +		    !vcam_find_format(argp->pixel_format))
> > +			return -EINVAL;
> > +
> > +		argp->type = V4L2_FRMIVAL_TYPE_CONTINUOUS;
> > +		argp->stepwise.min.numerator = 1;
> > +		argp->stepwise.min.denominator = VCAM_FPS_MAX;
> > +		argp->stepwise.max.numerator = 1;
> > +		argp->stepwise.max.denominator = VCAM_FPS_MIN;
> > +		argp->stepwise.step.numerator = 1;
> > +		argp->stepwise.step.denominator = 1;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +static int vcam_vidioc_enum_framesizes(struct file *file, void *fh,
> > +				       struct v4l2_frmsizeenum *argp)
> > +{
> > +	struct vcam *dev = video_drvdata(file);
> > +
> > +	return vcam_enum_framesizes(dev, argp);
> > +}
> > +
> > +static int vcam_vidioc_enum_frameintervals(struct file *file, void *fh,
> > +					   struct v4l2_frmivalenum *argp)
> > +{
> > +	struct vcam *dev = video_drvdata(file);
> > +
> > +	return vcam_enum_frameintervals(dev, argp);
> > +}
> > +
> > +static int vcam_vidioc_enum_fmt_cap(struct file *file, void *fh,
> > +				    struct v4l2_fmtdesc *f)
> > +{
> > +	struct vcam *dev;
> > +
> > +	dev = video_drvdata(file);
> > +
> > +	if (vcam_is_streaming(dev)) {
> > +		const __u32 format = dev->pix_format.pixelformat;
> > +
> > +		if (f->index)
> > +			return -EINVAL;
> > +
> > +		f->pixelformat = dev->pix_format.pixelformat;
> > +		vcam_fmt_descr(f->description, sizeof(f->description), format);
> > +	} else {
> > +		if (f->index >= VCAM_NR_FORMATS)
> > +			return -EINVAL;
> > +
> > +		f->pixelformat = vcam_formats[f->index].fourcc;
> > +		vcam_fmt_descr(f->description, sizeof(f->description),
> > +			       f->pixelformat);
> > +	}
> > +	f->flags = 0;
> > +	return 0;
> > +}
> > +
> > +static int vcam_vidioc_g_fmt_vid_cap(struct file *file, void *priv,
> > +				     struct v4l2_format *fmt)
> > +{
> > +	struct vcam *dev;
> > +
> > +	dev = video_drvdata(file);
> > +
> > +	fmt->fmt.pix = dev->pix_format;
> > +	return 0;
> > +}
> > +
> > +static int vcam_vidioc_try_fmt_vid_cap(struct file *file, void *priv,
> > +				       struct v4l2_format *fmt)
> > +{
> > +	struct vcam *dev = video_drvdata(file);
> > +
> > +	if (!V4L2_TYPE_IS_CAPTURE(fmt->type))
> > +		return -EINVAL;
> > +
> > +	if (vcam_is_streaming(dev)) {
> > +		if (!vcam_pix_format_eq(&dev->pix_format, &fmt->fmt.pix))
> > +			return -EBUSY;
> > +
> > +		fmt->fmt.pix = dev->pix_format;
> > +	}
> > +
> > +	return vcam_set_format(dev, fmt);
> > +}
> > +
> > +static int vcam_vidioc_s_fmt_vid_cap(struct file *file, void *priv,
> > +				     struct v4l2_format *fmt)
> > +{
> > +	struct vcam *dev = video_drvdata(file);
> > +	struct v4l2_format try_fmt = *fmt;
> > +	int ret;
> > +
> > +	if (!V4L2_TYPE_IS_CAPTURE(fmt->type))
> > +		return -EINVAL;
> > +
> > +	if (vcam_is_streaming(dev)) {
> > +		if (!vcam_pix_format_eq(&dev->pix_format, &fmt->fmt.pix))
> > +			return -EBUSY;
> > +
> > +		fmt->fmt.pix = dev->pix_format;
> > +	}
> > +
> > +	ret = vcam_set_format(dev, &try_fmt);
> > +	if (ret)
> > +		return ret;
> > +
> > +	if (vb2_is_busy(&dev->output_queue) &&
> > +	    !vcam_pix_format_eq(&dev->pix_format, &try_fmt.fmt.pix))
> > +		return -EBUSY;
> > +
> > +	dev->pix_format = try_fmt.fmt.pix;
> > +	*fmt = try_fmt;
> > +	return 0;
> > +}
> > +
> > +static int vcam_ioc_reqbufs(struct file *file, struct vcam *dev,
> > +			    struct v4l2_requestbuffers *req)
> > +{
> > +	int ret = 0;
> > +
> > +	if (req->type != V4L2_BUF_TYPE_VIDEO_OUTPUT)
> > +		return -EINVAL;
> > +
> > +	scoped_guard(mutex, &dev->lock)
> > +	{
> > +		if (vb2_queue_is_busy(&dev->output_queue, file)) {
> > +			ret = -EBUSY;
> > +			break;
> > +		}
> > +
> > +		ret = vb2_reqbufs(&dev->output_queue, req);
> > +		if (!ret)
> > +			dev->output_queue.owner =
> > +				req->count ? file->private_data : NULL;
> > +	}
> > +	return ret;
> > +}
> > +
> > +static int vcam_ioc_querybuf(struct file *file, struct vcam *dev,
> > +			     struct v4l2_buffer *buf)
> > +{
> > +	int ret = 0;
> > +
> > +	if (buf->type != V4L2_BUF_TYPE_VIDEO_OUTPUT)
> > +		return -EINVAL;
> > +
> > +	scoped_guard(mutex, &dev->lock)
> > +		ret = vb2_querybuf(&dev->output_queue, buf);
> > +
> > +	return ret;
> > +}
> > +
> > +static ssize_t formats_show(struct device *dev, struct device_attribute *attr,
> > +			    char *buf)
> > +{
> > +	struct vcam_format_entry {
> > +		u32 fourcc;
> > +		char name[5];
> > +	};
> > +	struct vcam_format_entry formats[VCAM_MAX_FORMATS];
> > +	struct vcam_format_entry tmp;
> > +	unsigned int count =
> > +		min_t(unsigned int, VCAM_NR_FORMATS, VCAM_MAX_FORMATS);
> > +	size_t len = 0;
> > +	unsigned int i, j;
> > +
> > +	for (i = 0; i < count; i++) {
> > +		formats[i].fourcc = vcam_formats[i].fourcc;
> > +		vcam_fourcc_str(formats[i].name, formats[i].fourcc);
> > +	}
> > +
> > +	for (i = 1; i < count; i++) {
> > +		for (j = i; j > 0; j--) {
> > +			if (strcmp(formats[j - 1].name, formats[j].name) <= 0)
> > +				break;
> > +			tmp = formats[j - 1];
> > +			formats[j - 1] = formats[j];
> > +			formats[j] = tmp;
> > +		}
> > +	}
> > +
> > +	for (i = 0; i < count; i++)
> > +		len += sysfs_emit_at(buf, len, "%s%s", i ? " " : "",
> > +				     formats[i].name);
> > +
> > +	len += sysfs_emit_at(buf, len, "\n");
> > +	return len;
> > +}
> > +
> > +static ssize_t max_width_show(struct device *dev, struct device_attribute *attr,
> > +			      char *buf)
> > +{
> > +	return sysfs_emit(buf, "%u\n", VCAM_MAX_WIDTH);
> > +}
> > +
> > +static ssize_t max_height_show(struct device *dev,
> > +			       struct device_attribute *attr, char *buf)
> > +{
> > +	return sysfs_emit(buf, "%u\n", VCAM_MAX_HEIGHT);
> > +}
> > +
> > +static ssize_t max_frames_show(struct device *dev,
> > +			       struct device_attribute *attr, char *buf)
> > +{
> > +	return sysfs_emit(buf, "%u\n", VCAM_MAX_FRAMES);
> > +}
> > +
> > +static DEVICE_ATTR_RO(formats);
> > +static DEVICE_ATTR_RO(max_frames);
> > +static DEVICE_ATTR_RO(max_height);
> > +static DEVICE_ATTR_RO(max_width);
> > +
> > +static struct attribute *vcam_attrs[] = {
> > +	&dev_attr_formats.attr,
> > +	&dev_attr_max_frames.attr,
> > +	&dev_attr_max_height.attr,
> > +	&dev_attr_max_width.attr,
> > +	NULL,
> > +};
> > +
> > +static const struct attribute_group vcam_attr_group = {
> > +	.attrs = vcam_attrs,
> > +};
> > +
> > +static const struct attribute_group *vcam_attr_groups[] = {
> > +	&vcam_attr_group,
> > +	NULL,
> > +};
> > +
> > +static int vcam_ioc_alloc(struct file *file, struct vcam *dev, u32 nr_frames,
> > +			  void __user *frames_user, enum vb2_memory memory)
> > +{
> > +	struct v4l2_requestbuffers req = {
> > +		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> > +		.memory = memory,
> > +	};
> > +	struct v4l2_buffer buf;
> > +	struct vcam_frame *frames = NULL;
> > +	unsigned int i;
> > +	int ret;
> > +
> > +	if (memory == VB2_MEMORY_DMABUF &&
> > +	    !dev->output_queue.mem_ops->attach_dmabuf)
> > +		return -EOPNOTSUPP;
> > +
> > +	if (!frames_user)
> > +		return -EINVAL;
> > +
> > +	if (nr_frames) {
> > +		frames = kcalloc(nr_frames, sizeof(*frames), GFP_KERNEL);
> > +		if (!frames)
> > +			return -ENOMEM;
> > +	}
> > +
> > +	if (copy_from_user(frames, frames_user, nr_frames * sizeof(*frames))) {
> > +		ret = -EFAULT;
> > +		goto out_free;
> > +	}
> > +
> > +	req.count = nr_frames;
> > +	ret = vcam_ioc_reqbufs(file, dev, &req);
> > +	if (ret)
> > +		goto out_free;
> > +
> > +	if (req.count != nr_frames) {
> > +		struct v4l2_requestbuffers req_free = {
> > +			.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> > +			.memory = memory,
> > +			.count = 0,
> > +		};
> > +
> > +		vcam_ioc_reqbufs(file, dev, &req_free);
> > +		ret = -ENOMEM;
> > +		goto out_free;
> > +	}
> > +
> > +	dev->output_memory = memory;
> > +
> > +	for (i = 0; i < nr_frames; i++) {
> > +		memset(&buf, 0, sizeof(buf));
> > +		buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> > +		buf.memory = memory;
> > +		buf.index = i;
> > +
> > +		ret = vcam_ioc_querybuf(file, dev, &buf);
> > +		if (ret)
> > +			goto out_free_reqbufs;
> > +
> > +		frames[i].index = i;
> > +		frames[i].length = buf.length;
> > +	}
> > +
> > +	if (copy_to_user(frames_user, frames, nr_frames * sizeof(*frames)))
> > +		ret = -EFAULT;
> > +
> > +out_free_reqbufs:
> > +	if (ret) {
> > +		struct v4l2_requestbuffers req_free = {
> > +			.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> > +			.memory = memory,
> > +			.count = 0,
> > +		};
> > +
> > +		vcam_ioc_reqbufs(file, dev, &req_free);
> > +		dev->output_memory = VB2_MEMORY_DMABUF;
> > +	}
> > +out_free:
> > +	kfree(frames);
> > +	if (ret)
> > +		return ret;
> > +
> > +	return 0;
> > +}
> > +
> > +static int vcam_ioc_queue(struct file *file, struct vcam *dev,
> > +			  struct vcam_ioc_queue *queue)
> > +{
> > +	struct v4l2_buffer buf = {
> > +		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> > +		.memory = dev->output_memory,
> > +		.index = queue->index,
> > +		.bytesused = queue->length,
> > +	};
> > +	u32 remainder;
> > +	int ret;
> > +
> > +	if (queue->reserved)
> > +		return -EINVAL;
> > +
> > +	if (dev->output_memory == VB2_MEMORY_DMABUF) {
> > +		buf.m.fd = queue->fd;
> > +		buf.length = dev->pix_format.sizeimage;
> > +	}
> > +
> > +	buf.timestamp.tv_sec =
> > +		div_u64_rem(queue->timestamp, NSEC_PER_SEC, &remainder);
> > +	buf.timestamp.tv_usec = remainder / NSEC_PER_USEC;
> > +
> > +	scoped_guard(mutex, &dev->lock)
> > +	{
> > +		if (vb2_queue_is_busy(&dev->output_queue, file)) {
> > +			ret = -EBUSY;
> > +			break;
> > +		}
> > +
> > +		if (vb2_is_streaming(&dev->capture_queue) &&
> > +		    !vb2_is_streaming(&dev->output_queue)) {
> > +			ret = vb2_streamon(&dev->output_queue, buf.type);
> > +			if (ret)
> > +				break;
> > +		}
> > +
> > +		ret = vb2_qbuf(&dev->output_queue, dev->v4l2_dev.mdev, &buf);
> > +	}
> > +
> > +	return ret;
> > +}
> > +
> > +static int vcam_ioc_dequeue(struct file *file, struct vcam *dev,
> > +			    struct vcam_ioc_dequeue *queue)
> > +{
> > +	struct v4l2_buffer buf = {
> > +		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> > +		.memory = dev->output_memory,
> > +	};
> > +	int ret;
> > +
> > +	scoped_guard(mutex, &dev->lock)
> > +	{
> > +		if (vb2_queue_is_busy(&dev->output_queue, file)) {
> > +			ret = -EBUSY;
> > +			break;
> > +		}
> > +
> > +		ret = vb2_dqbuf(&dev->output_queue, &buf,
> > +				file->f_flags & O_NONBLOCK);
> > +	}
> > +	if (ret)
> > +		return ret;
> > +
> > +	queue->index = buf.index;
> > +	queue->length = buf.bytesused;
> > +	queue->timestamp = (u64)buf.timestamp.tv_sec * NSEC_PER_SEC +
> > +			   (u64)buf.timestamp.tv_usec * NSEC_PER_USEC;
> > +	return 0;
> > +}
> > +
> > +static int vcam_ioc_status(struct vcam *dev, __u64 *status)
> > +{
> > +	*status = vcam_status_read(dev);
> > +	return 0;
> > +}
> > +
> > +static int vcam_ioc_wait(struct vcam *dev, struct vcam_ioc_wait *wait)
> > +{
> > +	int ret;
> > +
> > +	if (!wait->mask)
> > +		return -EINVAL;
> > +	if (wait->mask & ~VCAM_STATUS_MASK)
> > +		return -EINVAL;
> > +
> > +	ret = wait_event_interruptible(dev->status_waitq,
> > +				       vcam_status_mask_ready(dev, wait->mask));
> > +	if (ret)
> > +		return ret;
> > +
> > +	wait->status = vcam_status_read(dev);
> > +	return 0;
> > +}
> > +
> > +static long vcam_output_ioctl_core(struct file *file, unsigned int cmd,
> > +				   void *arg)
> > +{
> > +	struct vcam *dev = file->private_data;
> > +	long ret = 0;
> > +
> > +	switch (cmd) {
> > +	case VCAM_IOC_QUEUE:
> > +		ret = vcam_ioc_queue(file, dev, arg);
> > +		break;
> > +	case VCAM_IOC_DEQUEUE:
> > +		ret = vcam_ioc_dequeue(file, dev, arg);
> > +		break;
> > +	case VCAM_IOC_STATUS:
> > +		ret = vcam_ioc_status(dev, arg);
> > +		break;
> > +	case VCAM_IOC_WAIT:
> > +		ret = vcam_ioc_wait(dev, arg);
> > +		break;
> > +	default:
> > +		ret = -EOPNOTSUPP;
> > +		break;
> > +	}
> > +
> > +	return ret;
> > +}
> > +
> > +static long vcam_ioctl_common(struct file *file, unsigned int cmd,
> > +			      unsigned long arg)
> > +{
> > +	void __user *argp = (void __user *)arg;
> > +	void *karg;
> > +	size_t size;
> > +	long ret;
> > +
> > +	switch (cmd) {
> > +	case VCAM_IOC_QUEUE:
> > +		size = sizeof(struct vcam_ioc_queue);
> > +		break;
> > +	case VCAM_IOC_DEQUEUE:
> > +		size = sizeof(struct vcam_ioc_dequeue);
> > +		break;
> > +	case VCAM_IOC_STATUS:
> > +		size = sizeof(__u64);
> > +		break;
> > +	case VCAM_IOC_WAIT:
> > +		size = sizeof(struct vcam_ioc_wait);
> > +		break;
> > +	default:
> > +		return -ENOTTY;
> > +	}
> > +
> > +	if (size > SZ_4K)
> > +		return -ENOTTY;
> > +
> > +	karg = kzalloc(size, GFP_KERNEL);
> > +	if (!karg)
> > +		return -ENOMEM;
> > +
> > +	if (copy_from_user(karg, argp, size)) {
> > +		ret = -EFAULT;
> > +		goto out_free;
> > +	}
> > +
> > +	ret = vcam_output_ioctl_core(file, cmd, karg);
> > +	if (ret)
> > +		goto out_free;
> > +
> > +	if (copy_to_user(argp, karg, size)) {
> > +		ret = -EFAULT;
> > +		goto out_free;
> > +	}
> > +
> > +	ret = 0;
> > +out_free:
> > +	kfree(karg);
> > +	return ret;
> > +}
> > +
> > +static void __vcam_release(struct vcam *dev)
> > +{
> > +	if (!dev->vdev)
> > +		return;
> > +
> > +	vb2_queue_release(&dev->output_queue);
> > +	vb2_queue_release(&dev->capture_queue);
> > +
> > +	if (video_is_registered(dev->vdev))
> > +		video_unregister_device(dev->vdev);
> > +	else
> > +		video_device_release(dev->vdev);
> > +
> > +	v4l2_device_unregister(&dev->v4l2_dev);
> > +
> > +	dev->vdev = NULL;
> > +	dev->device_nr = -1;
> > +}
> > +
> > +static void vcam_release(struct kref *ref)
> > +{
> > +	struct vcam *dev;
> > +
> > +	dev = container_of(ref, struct vcam, ref);
> > +
> > +	if (!test_bit(VCAM_FLAG_CREATING, &dev->flags) || dev->device_nr < 0) {
> > +		kfree(dev);
> > +		return;
> > +	}
> > +
> > +	__vcam_release(dev);
> > +	kfree(dev);
> > +}
> > +
> > +static int __vcam_close(struct inode *inode, struct file *file)
> > +{
> > +	struct vcam *dev = file->private_data;
> > +
> > +	if (dev->vdev && video_is_registered(dev->vdev))
> > +		video_unregister_device(dev->vdev);
> > +
> > +	vb2_queue_release(&dev->output_queue);
> > +
> > +	dev->output_memory = VB2_MEMORY_DMABUF;
> > +
> > +	kref_put(&dev->ref, vcam_release);
> > +	return 0;
> > +}
> > +
> > +static int vcam_open(struct inode *inode, struct file *file)
> > +{
> > +	struct vcam *dev;
> > +	int ret = nonseekable_open(inode, file);
> > +
> > +	if (ret)
> > +		return ret;
> > +
> > +	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
> > +	if (!dev)
> > +		return -ENOMEM;
> > +
> > +	kref_init(&dev->ref);
> > +	dev->device_nr = -1;
> > +	file->private_data = dev;
> > +	return 0;
> > +}
> > +
> > +static int vcam_close(struct inode *inode, struct file *file)
> > +{
> > +	struct vcam *dev = file->private_data;
> > +	int ret = 0;
> > +
> > +	if (!dev)
> > +		return 0;
> > +
> > +	if (test_bit(VCAM_FLAG_CREATING, &dev->flags) && dev->device_nr >= 0)
> > +		ret = __vcam_close(inode, file);
> > +	else
> > +		kref_put(&dev->ref, vcam_release);
> > +
> > +	file->private_data = NULL;
> > +	return ret;
> > +}
> > +
> > +static __poll_t vcam_poll(struct file *file, struct poll_table_struct *pts)
> > +{
> > +	struct vcam *dev = file->private_data;
> > +
> > +	if (!dev || !test_bit(VCAM_FLAG_CREATING, &dev->flags) ||
> > +	    !test_bit(VCAM_FLAG_READY, &dev->flags) || dev->device_nr < 0)
> > +		return POLLERR;
> > +
> > +	return vb2_core_poll(&dev->output_queue, file, pts);
> > +}
> > +
> > +static int vcam_mmap(struct file *file, struct vm_area_struct *vma)
> > +{
> > +	struct vcam *dev = file->private_data;
> > +
> > +	if (!dev || !test_bit(VCAM_FLAG_CREATING, &dev->flags) ||
> > +	    !test_bit(VCAM_FLAG_READY, &dev->flags) || dev->device_nr < 0)
> > +		return -ENOTTY;
> > +
> > +	return vb2_mmap(&dev->output_queue, vma);
> > +}
> > +
> > +static int vcam_vidioc_g_parm(struct file *file, void *priv,
> > +			      struct v4l2_streamparm *parm)
> > +{
> > +	struct vcam *dev;
> > +
> > +	if (parm->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> > +		return -EINVAL;
> > +
> > +	dev = video_drvdata(file);
> > +	parm->parm.capture = dev->capture;
> > +	return 0;
> > +}
> > +
> > +static int vcam_vidioc_s_parm(struct file *file, void *priv,
> > +			      struct v4l2_streamparm *parm)
> > +{
> > +	struct v4l2_fract *tpf = &parm->parm.capture.timeperframe;
> > +	struct vcam *dev = video_drvdata(file);
> > +
> > +	if (!vcam_tpf_valid(tpf))
> > +		return -EINVAL;
> > +
> > +	if (parm->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> > +		return -EINVAL;
> > +
> > +	dev->capture.timeperframe = *tpf;
> > +	parm->parm.capture = dev->capture;
> > +	return 0;
> > +}
> > +
> > +static int vcam_vidioc_enum_input(struct file *file, void *fh,
> > +				  struct v4l2_input *inp)
> > +{
> > +	struct vcam *dev;
> > +	__u32 index = inp->index;
> > +
> > +	if (index != 0)
> > +		return -EINVAL;
> > +
> > +	memset(inp, 0, sizeof(*inp));
> > +
> > +	inp->index = index;
> > +	strscpy(inp->name, "vcam", sizeof(inp->name));
> > +	inp->type = V4L2_INPUT_TYPE_CAMERA;
> > +	inp->audioset = 0;
> > +	inp->tuner = 0;
> > +	inp->status = 0;
> > +
> > +	dev = video_drvdata(file);
> > +	if (!vb2_is_streaming(&dev->output_queue))
> > +		inp->status |= V4L2_IN_ST_NO_SIGNAL;
> > +
> > +	return 0;
> > +}
> > +
> > +static int vcam_vidioc_g_input(struct file *file, void *fh, unsigned int *i)
> > +{
> > +	*i = 0;
> > +	return 0;
> > +}
> > +
> > +static int vcam_vidioc_s_input(struct file *file, void *fh, unsigned int i)
> > +{
> > +	if (i == 0)
> > +		return 0;
> > +
> > +	return -EINVAL;
> > +}
> > +
> > +static int vcam_vidioc_streamon(struct file *file, void *fh,
> > +				enum v4l2_buf_type type)
> > +{
> > +	struct vcam *dev = video_drvdata(file);
> > +	int ret;
> > +
> > +	if (type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> > +		return -EINVAL;
> > +
> > +	if (vb2_queue_is_busy(&dev->capture_queue, file))
> > +		return -EBUSY;
> > +
> > +	ret = vb2_streamon(&dev->capture_queue, type);
> > +	if (ret)
> > +		return ret;
> > +
> > +	if (vb2_get_num_buffers(&dev->output_queue)) {
> > +		ret = vb2_streamon(&dev->output_queue,
> > +				   V4L2_BUF_TYPE_VIDEO_OUTPUT);
> > +		if (ret) {
> > +			vb2_streamoff(&dev->capture_queue, type);
> > +			return ret;
> > +		}
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +static int vcam_vidioc_streamoff(struct file *file, void *fh,
> > +				 enum v4l2_buf_type type)
> > +{
> > +	struct vcam *dev = video_drvdata(file);
> > +	int ret;
> > +
> > +	if (type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
> > +		return -EINVAL;
> > +
> > +	if (vb2_queue_is_busy(&dev->capture_queue, file))
> > +		return -EBUSY;
> > +
> > +	ret = vb2_streamoff(&dev->capture_queue, type);
> > +	if (ret)
> > +		return ret;
> > +
> > +	if (vb2_get_num_buffers(&dev->output_queue))
> > +		vb2_streamoff(&dev->output_queue, V4L2_BUF_TYPE_VIDEO_OUTPUT);
> > +
> > +	return 0;
> > +}
> > +
> > +static const struct v4l2_ioctl_ops vcam_ioctl_ops = {
> > +	.vidioc_querycap = &vcam_vidioc_querycap,
> > +	.vidioc_enum_framesizes = &vcam_vidioc_enum_framesizes,
> > +	.vidioc_enum_frameintervals = &vcam_vidioc_enum_frameintervals,
> > +	.vidioc_enum_input = &vcam_vidioc_enum_input,
> > +	.vidioc_g_input = &vcam_vidioc_g_input,
> > +	.vidioc_s_input = &vcam_vidioc_s_input,
> > +	.vidioc_enum_fmt_vid_cap = &vcam_vidioc_enum_fmt_cap,
> > +	.vidioc_g_fmt_vid_cap = &vcam_vidioc_g_fmt_vid_cap,
> > +	.vidioc_s_fmt_vid_cap = &vcam_vidioc_s_fmt_vid_cap,
> > +	.vidioc_try_fmt_vid_cap = &vcam_vidioc_try_fmt_vid_cap,
> > +	.vidioc_g_parm = &vcam_vidioc_g_parm,
> > +	.vidioc_s_parm = &vcam_vidioc_s_parm,
> > +
> > +	.vidioc_reqbufs = &vb2_ioctl_reqbufs,
> > +	.vidioc_create_bufs = &vb2_ioctl_create_bufs,
> > +	.vidioc_prepare_buf = &vb2_ioctl_prepare_buf,
> > +	.vidioc_querybuf = &vb2_ioctl_querybuf,
> > +	.vidioc_qbuf = &vb2_ioctl_qbuf,
> > +	.vidioc_dqbuf = &vb2_ioctl_dqbuf,
> > +	.vidioc_expbuf = &vb2_ioctl_expbuf,
> > +	.vidioc_streamon = &vcam_vidioc_streamon,
> > +	.vidioc_streamoff = &vcam_vidioc_streamoff,
> > +};
> > +
> > +static enum vb2_buffer_state vcam_buf_fill(struct vcam *dev,
> > +					   struct vcam_buf *buf,
> > +					   const void *src, u32 src_len,
> > +					   u64 timestamp)
> > +{
> > +	struct vb2_buffer *vb = &buf->vb.vb2_buf;
> > +	u32 sequence;
> > +	void *dst;
> > +
> > +	dst = vb2_plane_vaddr(vb, 0);
> > +	if (!dst)
> > +		return VB2_BUF_STATE_ERROR;
> > +
> > +	if (!src_len || src_len > dev->pix_format.sizeimage)
> > +		src_len = dev->pix_format.sizeimage;
> > +
> > +	if (!src)
> > +		return VB2_BUF_STATE_ERROR;
> > +
> > +	memcpy(dst, src, src_len);
> > +
> > +	sequence = (u32)(atomic_inc_return(&dev->sequence) - 1);
> > +
> > +	vb->timestamp = timestamp ? timestamp : ktime_get_ns();
> > +	buf->vb.sequence = sequence;
> > +	buf->vb.field = dev->pix_format.field;
> > +	vb2_set_plane_payload(vb, 0, src_len);
> > +
> > +	return VB2_BUF_STATE_DONE;
> > +}
> > +
> > +static bool vcam_buf_flip(struct vcam *dev, struct vb2_buffer *out_vb,
> > +			  struct vcam_buf *cap_buf, u32 bytesused)
> > +{
> > +	struct vb2_buffer *cap_vb = &cap_buf->vb.vb2_buf;
> > +	u32 sequence;
> > +
> > +	if (!out_vb->planes[0].dbuf || !cap_vb->planes[0].dbuf)
> > +		return false;
> > +
> > +	if (out_vb->planes[0].dbuf != cap_vb->planes[0].dbuf)
> > +		return false;
> > +
> > +	if (!bytesused)
> > +		bytesused = dev->pix_format.sizeimage;
> > +	if (bytesused > vb2_plane_size(cap_vb, 0))
> > +		bytesused = vb2_plane_size(cap_vb, 0);
> > +
> > +	sequence = (u32)(atomic_inc_return(&dev->sequence) - 1);
> > +
> > +	cap_vb->timestamp = out_vb->timestamp ? out_vb->timestamp :
> > +						ktime_get_ns();
> > +	cap_buf->vb.sequence = sequence;
> > +	cap_buf->vb.field = dev->pix_format.field;
> > +	vb2_set_plane_payload(cap_vb, 0, bytesused);
> > +
> > +	return true;
> > +}
> > +
> > +static bool vcam_buf_pair_dequeue(struct vcam *dev, struct vcam_buf **out_buf,
> > +				  struct vcam_buf **cap_buf)
> > +{
> > +	unsigned long flags;
> > +	bool dequeued = false;
> > +
> > +	spin_lock_irqsave(&dev->frame_lock, flags);
> > +	if (!list_empty(&dev->output_list) && !list_empty(&dev->capture_list)) {
> > +		*out_buf = list_first_entry(&dev->output_list, struct vcam_buf,
> > +					    list);
> > +		list_del(&(*out_buf)->list);
> > +		*cap_buf = list_first_entry(&dev->capture_list, struct vcam_buf,
> > +					    list);
> > +		list_del(&(*cap_buf)->list);
> > +		dequeued = true;
> > +	}
> > +	spin_unlock_irqrestore(&dev->frame_lock, flags);
> > +	return dequeued;
> > +}
> > +
> > +static void vcam_dequeue_frames(struct vcam *data)
> > +{
> > +	const struct vcam_format *format;
> > +	enum vb2_buffer_state cap_state;
> > +	struct vcam_buf *cap_buf;
> > +	struct vcam_buf *out_buf;
> > +	struct vb2_buffer *vb;
> > +	bool zero_copy;
> > +	u32 bytesused;
> > +	void *src;
> > +
> > +	if (!vcam_is_streaming(data))
> > +		return;
> > +
> > +	format = vcam_find_format(data->pix_format.pixelformat);
> > +	while (vcam_buf_pair_dequeue(data, &out_buf, &cap_buf)) {
> > +		cap_state = VB2_BUF_STATE_DONE;
> > +		vb = &out_buf->vb.vb2_buf;
> > +		bytesused = vb2_get_plane_payload(vb, 0);
> > +		if (!bytesused || bytesused > data->pix_format.sizeimage)
> > +			bytesused = data->pix_format.sizeimage;
> > +
> > +		if (bytesused < data->pix_format.sizeimage &&
> > +		    (!format || !(format->flags & VCAM_COMPRESSED))) {
> > +			cap_state = VB2_BUF_STATE_ERROR;
> > +			goto out_done;
> > +		}
> > +
> > +		zero_copy = vcam_buf_flip(data, vb, cap_buf, bytesused);
> > +		if (!zero_copy &&
> > +		    (!(out_buf->flags & VCAM_BUF_FLAG_MAPPABLE) ||
> > +		     !(cap_buf->flags & VCAM_BUF_FLAG_MAPPABLE))) {
> > +			dev_dbg(&data->vdev->dev,
> > +				"unshared unmappable capture and output");
> > +			cap_state = VB2_BUF_STATE_ERROR;
> > +			goto out_done;
> > +		}
> > +		if (!zero_copy) {
> > +			src = vb2_plane_vaddr(vb, 0);
> > +			if (!src) {
> > +				cap_state = VB2_BUF_STATE_ERROR;
> > +				goto out_done;
> > +			}
> > +
> > +			cap_state = vcam_buf_fill(data, cap_buf, src, bytesused,
> > +						  vb->timestamp);
> > +		}
> > +out_done:
> > +		vb2_buffer_done(&cap_buf->vb.vb2_buf, cap_state);
> > +
> > +		if (cap_state == VB2_BUF_STATE_ERROR)
> > +			vb2_buffer_done(vb, VB2_BUF_STATE_ERROR);
> > +		else
> > +			vb2_buffer_done(vb, VB2_BUF_STATE_DONE);
> > +	}
> > +}
> > +
> > +static int vcam_vdev_open(struct file *file)
> > +{
> > +	struct vcam *dev;
> > +	int ret;
> > +
> > +	dev = video_drvdata(file);
> > +	if (test_and_set_bit(VCAM_FLAG_IS_OPEN, &dev->flags))
> > +		return -EBUSY;
> > +	if (dev->device_nr < 0 || !test_bit(VCAM_FLAG_READY, &dev->flags)) {
> > +		clear_bit(VCAM_FLAG_IS_OPEN, &dev->flags);
> > +		return -ENODEV;
> > +	}
> > +
> > +	ret = v4l2_fh_open(file);
> > +	if (ret) {
> > +		clear_bit(VCAM_FLAG_IS_OPEN, &dev->flags);
> > +		return ret;
> > +	}
> > +
> > +	kref_get(&dev->ref);
> > +	return 0;
> > +}
> > +
> > +static int vcam_vdev_close(struct file *file)
> > +{
> > +	struct vcam *dev;
> > +	int ret;
> > +
> > +	dev = video_drvdata(file);
> > +	ret = _vb2_fop_release(file, NULL);
> > +	clear_bit(VCAM_FLAG_IS_OPEN, &dev->flags);
> > +
> > +	kref_put(&dev->ref, vcam_release);
> > +	return ret;
> > +}
> > +
> > +static const struct v4l2_file_operations vcam_vdev_fops = {
> > +	.owner = THIS_MODULE,
> > +	.open = vcam_vdev_open,
> > +	.release = vcam_vdev_close,
> > +	.poll = vb2_fop_poll,
> > +	.mmap = vb2_fop_mmap,
> > +	.unlocked_ioctl = video_ioctl2,
> > +};
> > +
> > +static int vcam_ioc_create_validate(struct vcam_ioc_create *config,
> > +				    char *card_label)
> > +{
> > +	long len, i;
> > +
> > +	if (config->device_nr != 0)
> > +		return -EINVAL;
> > +	if (config->reserved)
> > +		return -EINVAL;
> > +	if (config->nr_frames > VCAM_MAX_FRAMES)
> > +		return -E2BIG;
> > +	if (config->nr_frames < VCAM_MIN_FRAMES)
> > +		return -EINVAL;
> > +	if (!config->frames)
> > +		return -EINVAL;
> > +
> > +	memset(card_label, 0, VCAM_CARD_LABEL_MAX);
> > +	len = strncpy_from_user(card_label,
> > +				u64_to_user_ptr(config->device_name),
> > +				VCAM_CARD_LABEL_MAX);
> > +	if (len < 0)
> > +		return -EFAULT;
> > +	if (len >= VCAM_CARD_LABEL_MAX)
> > +		return -E2BIG;
> > +	if (!len)
> > +		return -EINVAL;
> > +	if (!isalnum((unsigned char)card_label[0]))
> > +		return -EINVAL;
> > +	for (i = 0; i < len; i++) {
> > +		if (!isalnum((unsigned char)card_label[i]) &&
> > +		    !isspace((unsigned char)card_label[i]))
> > +			return -EINVAL;
> > +	}
> > +	if (!isalnum((unsigned char)card_label[len - 1]))
> > +		return -EINVAL;
> > +
> > +	return len;
> > +}
> > +
> > +static int vcam_vb2_queue_setup(struct vb2_queue *queue,
> > +				unsigned int *nr_buffers,
> > +				unsigned int *nr_planes, unsigned int sizes[],
> > +				struct device *alloc_devs[])
> > +{
> > +	struct vcam *data = vb2_get_drv_priv(queue);
> > +	unsigned int sizeimage = data->pix_format.sizeimage;
> > +
> > +	if (!sizeimage)
> > +		return -EINVAL;
> > +
> > +	if (*nr_buffers < VCAM_MIN_FRAMES)
> > +		*nr_buffers = VCAM_MIN_FRAMES;
> > +
> > +	if (*nr_planes)
> > +		return sizes[0] < sizeimage ? -EINVAL : 0;
> > +
> > +	*nr_planes = 1;
> > +	sizes[0] = sizeimage;
> > +	return 0;
> > +}
> > +
> > +static int vcam_vb2_buf_prepare(struct vb2_buffer *vb)
> > +{
> > +	struct vcam *data = vb2_get_drv_priv(vb->vb2_queue);
> > +	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
> > +	struct vcam_buf *buf = container_of(vbuf, struct vcam_buf, vb);
> > +	unsigned int sizeimage = data->pix_format.sizeimage;
> > +	unsigned int bytesused;
> > +	void *vaddr;
> > +
> > +	if (vb2_plane_size(vb, 0) < sizeimage)
> > +		return -EINVAL;
> > +
> > +	vbuf->field = data->pix_format.field;
> > +	bytesused = vb2_get_plane_payload(vb, 0);
> > +	if (V4L2_TYPE_IS_OUTPUT(vb->vb2_queue->type) && !bytesused)
> > +		vb2_set_plane_payload(vb, 0, sizeimage);
> > +
> > +	buf->flags = VCAM_BUF_FLAG_MAPPABLE;
> > +	if (vb->planes[0].dbuf) {
> > +		vaddr = vb2_plane_vaddr(vb, 0);
> > +		if (!vaddr)
> > +			buf->flags &= ~VCAM_BUF_FLAG_MAPPABLE;
> > +	}
> > +	return 0;
> > +}
> > +
> > +static void vcam_vb2_buf_queue(struct vb2_buffer *vb)
> > +{
> > +	struct vcam *data = vb2_get_drv_priv(vb->vb2_queue);
> > +	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
> > +	struct vcam_buf *buf;
> > +	unsigned long flags;
> > +
> > +	buf = container_of(vbuf, struct vcam_buf, vb);
> > +
> > +	if (V4L2_TYPE_IS_OUTPUT(vb->vb2_queue->type)) {
> > +		spin_lock_irqsave(&data->frame_lock, flags);
> > +		list_add_tail(&buf->list, &data->output_list);
> > +		spin_unlock_irqrestore(&data->frame_lock, flags);
> > +	} else {
> > +		spin_lock_irqsave(&data->frame_lock, flags);
> > +		list_add_tail(&buf->list, &data->capture_list);
> > +		spin_unlock_irqrestore(&data->frame_lock, flags);
> > +	}
> > +
> > +	vcam_dequeue_frames(data);
> > +}
> > +
> > +static int vcam_vb2_prepare_streaming(struct vb2_queue *vq)
> > +{
> > +	return 0;
> > +}
> > +
> > +static int vcam_vb2_start_streaming(struct vb2_queue *vq, unsigned int count)
> > +{
> > +	struct vcam *data = vb2_get_drv_priv(vq);
> > +
> > +	if (V4L2_TYPE_IS_CAPTURE(vq->type)) {
> > +		atomic_set(&data->sequence, 0);
> > +		vcam_status_update_stream(data, true);
> > +	}
> > +
> > +	vcam_dequeue_frames(data);
> > +	return 0;
> > +}
> > +
> > +static void vcam_vb2_stop_streaming(struct vb2_queue *vq)
> > +{
> > +	struct vcam *data = vb2_get_drv_priv(vq);
> > +	struct vcam_buf *buf, *tmp;
> > +	unsigned long flags;
> > +	LIST_HEAD(done_list);
> > +
> > +	if (V4L2_TYPE_IS_CAPTURE(vq->type)) {
> > +		vcam_status_update_stream(data, false);
> > +		spin_lock_irqsave(&data->frame_lock, flags);
> > +		list_splice_init(&data->capture_list, &done_list);
> > +		list_splice_init(&data->output_list, &done_list);
> > +		spin_unlock_irqrestore(&data->frame_lock, flags);
> > +
> > +		list_for_each_entry_safe(buf, tmp, &done_list, list)
> > +			vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> > +
> > +		return;
> > +	}
> > +
> > +	if (V4L2_TYPE_IS_OUTPUT(vq->type)) {
> > +		spin_lock_irqsave(&data->frame_lock, flags);
> > +		list_splice_init(&data->output_list, &done_list);
> > +		list_splice_init(&data->capture_list, &done_list);
> > +		spin_unlock_irqrestore(&data->frame_lock, flags);
> > +
> > +		list_for_each_entry_safe(buf, tmp, &done_list, list)
> > +			vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
> > +	}
> > +}
> > +
> > +static const struct vb2_ops vcam_vb2_ops = {
> > +	.queue_setup = vcam_vb2_queue_setup,
> > +	.buf_queue = vcam_vb2_buf_queue,
> > +	.buf_prepare = vcam_vb2_buf_prepare,
> > +	.prepare_streaming = vcam_vb2_prepare_streaming,
> > +	.start_streaming = vcam_vb2_start_streaming,
> > +	.stop_streaming = vcam_vb2_stop_streaming,
> > +};
> > +
> > +static int vcam_ioc_create(struct file *file, struct vcam *dev,
> > +			   struct vcam_ioc_create *config, char *card_label,
> > +			   unsigned int len)
> > +{
> > +	struct v4l2_format try_fmt;
> > +	struct video_device *vdev;
> > +	struct vb2_queue *queue;
> > +	struct v4l2_format fmt;
> > +	long ret;
> > +
> > +	strscpy(dev->v4l2_dev.name, "vcam", sizeof(dev->v4l2_dev.name));
> > +
> > +	ret = v4l2_device_register(NULL, &dev->v4l2_dev);
> > +	if (ret)
> > +		return ret;
> > +
> > +	vdev = video_device_alloc();
> > +	if (!vdev) {
> > +		ret = -ENOMEM;
> > +		goto err_unregister;
> > +	}
> > +
> > +	dev->vdev = vdev;
> > +	video_set_drvdata(vdev, dev);
> > +	memcpy(vdev->name, card_label, len);
> > +	vdev->name[len] = '\0';
> > +	vdev->vfl_type = VFL_TYPE_VIDEO;
> > +	vdev->fops = &vcam_vdev_fops;
> > +	vdev->ioctl_ops = &vcam_ioctl_ops;
> > +	vdev->release = &video_device_release;
> > +	vdev->minor = -1;
> > +	vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
> > +	vdev->vfl_dir = VFL_DIR_RX;
> > +
> > +	mutex_init(&dev->lock);
> > +	spin_lock_init(&dev->frame_lock);
> > +	spin_lock_init(&dev->status_lock);
> > +	INIT_LIST_HEAD(&dev->capture_list);
> > +	INIT_LIST_HEAD(&dev->output_list);
> > +	dev->status = VCAM_STATUS_IDLE;
> > +	dev->output_memory = VB2_MEMORY_DMABUF;
> > +	init_waitqueue_head(&dev->status_waitq);
> > +
> > +	dev->vdev->v4l2_dev = &dev->v4l2_dev;
> > +	dev->vdev->queue = &dev->capture_queue;
> > +	dev->vdev->lock = &dev->lock;
> > +	dev->capture.capability = 0;
> > +	dev->capture.capturemode = 0;
> > +	dev->capture.extendedmode = 0;
> > +	dev->capture.readbuffers = VCAM_MIN_FRAMES;
> > +	dev->capture.timeperframe.numerator = 1;
> > +	dev->capture.timeperframe.denominator = 30;
> > +
> > +	if (!IS_ENABLED(CONFIG_DMA_SHARED_BUFFER) ||
> > +	    !vb2_vmalloc_memops.attach_dmabuf) {
> > +		ret = -EOPNOTSUPP;
> > +		goto err_unregister;
> > +	}
> > +
> > +	fmt = (struct v4l2_format){
> > +		.type = V4L2_BUF_TYPE_VIDEO_OUTPUT,
> > +		.fmt.pix = { .width = config->width,
> > +			     .height = config->height,
> > +			     .pixelformat = config->pixelformat,
> > +			     .colorspace = config->colorspace,
> > +			     .bytesperline = config->bytesperline,
> > +			     .field = V4L2_FIELD_NONE }
> > +	};
> > +
> > +	try_fmt = fmt;
> > +
> > +	ret = vcam_set_format(dev, &try_fmt);
> > +	if (ret)
> > +		goto err_unregister;
> > +
> > +	if ((fmt.fmt.pix.width && try_fmt.fmt.pix.width != fmt.fmt.pix.width) ||
> > +	    (fmt.fmt.pix.height &&
> > +	     try_fmt.fmt.pix.height != fmt.fmt.pix.height) ||
> > +	    try_fmt.fmt.pix.pixelformat != fmt.fmt.pix.pixelformat ||
> > +	    (fmt.fmt.pix.colorspace != V4L2_COLORSPACE_DEFAULT &&
> > +	     try_fmt.fmt.pix.colorspace != fmt.fmt.pix.colorspace) ||
> > +	    (fmt.fmt.pix.bytesperline &&
> > +	     try_fmt.fmt.pix.bytesperline != fmt.fmt.pix.bytesperline)) {
> > +		ret = -EINVAL;
> > +		goto err_unregister;
> > +	}
> > +
> > +	dev->pix_format = try_fmt.fmt.pix;
> > +
> > +	queue = &dev->capture_queue;
> > +	queue->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
> > +	queue->io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
> > +	queue->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
> > +	queue->drv_priv = dev;
> > +	queue->buf_struct_size = sizeof(struct vcam_buf);
> > +	queue->ops = &vcam_vb2_ops;
> > +	queue->mem_ops = &vb2_vmalloc_memops;
> > +	queue->lock = &dev->lock;
> > +	queue->dev = &dev->vdev->dev;
> > +	ret = vb2_queue_init(queue);
> > +	if (ret)
> > +		goto err_unregister;
> > +
> > +	queue = &dev->output_queue;
> > +	queue->type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> > +	queue->io_modes = VB2_DMABUF;
> > +	queue->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_COPY;
> > +	queue->drv_priv = dev;
> > +	queue->buf_struct_size = sizeof(struct vcam_buf);
> > +	queue->ops = &vcam_vb2_ops;
> > +	queue->mem_ops = &vb2_vmalloc_memops;
> > +	queue->lock = &dev->lock;
> > +	queue->dev = &dev->vdev->dev;
> > +	ret = vb2_queue_init(queue);
> > +	if (ret)
> > +		goto err_capture_queue;
> > +
> > +	ret = vcam_ioc_alloc(file, dev, config->nr_frames,
> > +			     u64_to_user_ptr(config->frames),
> > +			     VB2_MEMORY_DMABUF);
> > +	if (ret)
> > +		goto err_output_queue;
> > +
> > +	ret = video_register_device(dev->vdev, VFL_TYPE_VIDEO, -1);
> > +	if (ret < 0)
> > +		goto err_output_queue;
> > +
> > +	config->device_nr = dev->vdev->num;
> > +	return 0;
> > +
> > +err_output_queue:
> > +	vb2_queue_release(&dev->output_queue);
> > +
> > +err_capture_queue:
> > +	vb2_queue_release(&dev->capture_queue);
> > +
> > +err_unregister:
> > +	if (dev->vdev)
> > +		video_device_release(dev->vdev);
> > +	v4l2_device_unregister(&dev->v4l2_dev);
> > +	return ret;
> > +}
> > +
> > +static long vcam_ioctl(struct file *file, unsigned int cmd, unsigned long parm)
> > +{
> > +	struct vcam *dev = file->private_data;
> > +	char card_label[VCAM_CARD_LABEL_MAX];
> > +	struct vcam_ioc_create config;
> > +	long ret, len;
> > +
> > +	if (cmd != VCAM_IOC_CREATE) {
> > +		if (!dev || !test_bit(VCAM_FLAG_CREATING, &dev->flags) ||
> > +		    !test_bit(VCAM_FLAG_READY, &dev->flags) ||
> > +		    dev->device_nr < 0)
> > +			return -ENOTTY;
> > +		return vcam_ioctl_common(file, cmd, parm);
> > +	}
> > +
> > +	if (!dev)
> > +		return -ENOTTY;
> > +
> > +	if (test_and_set_bit(VCAM_FLAG_CREATING, &dev->flags))
> > +		return -EBUSY;
> > +
> > +	if (!parm) {
> > +		ret = -EINVAL;
> > +		goto err_clear;
> > +	}
> > +
> > +	if (copy_from_user(&config, (void *)parm, sizeof(config))) {
> > +		ret = -EFAULT;
> > +		goto err_clear;
> > +	}
> > +
> > +	len = vcam_ioc_create_validate(&config, card_label);
> > +	if (len < 0) {
> > +		ret = len;
> > +		goto err_clear;
> > +	}
> > +
> > +	ret = vcam_ioc_create(file, dev, &config, card_label, len);
> > +	if (ret)
> > +		goto err_clear;
> > +
> > +	if (copy_to_user((void *)parm, &config, sizeof(config))) {
> > +		ret = -EFAULT;
> > +		goto err_release;
> > +	}
> > +
> > +	dev->device_nr = dev->vdev->num;
> > +	snprintf(dev->v4l2_dev.name, sizeof(dev->v4l2_dev.name), "vcam-%d",
> > +		 dev->device_nr);
> > +	set_bit(VCAM_FLAG_READY, &dev->flags);
> > +	return 0;
> > +
> > +err_release:
> > +	__vcam_release(dev);
> > +
> > +err_clear:
> > +	clear_bit(VCAM_FLAG_CREATING, &dev->flags);
> > +	return ret;
> > +}
> > +
> > +static const struct file_operations vcam_fops = {
> > +	.owner = THIS_MODULE,
> > +	.open = vcam_open,
> > +	.unlocked_ioctl = vcam_ioctl,
> > +#ifdef CONFIG_COMPAT
> > +	.compat_ioctl = vcam_ioctl,
> > +#endif
> > +	.poll = vcam_poll,
> > +	.mmap = vcam_mmap,
> > +	.release = vcam_close,
> > +	.llseek = noop_llseek,
> > +};
> > +
> > +static struct miscdevice vcam_misc = {
> > +	.minor = MISC_DYNAMIC_MINOR,
> > +	.name = "vcam",
> > +	.fops = &vcam_fops,
> > +	.groups = vcam_attr_groups,
> > +};
> > +
> > +module_misc_device(vcam_misc);
> > diff --git a/include/uapi/linux/vcam.h b/include/uapi/linux/vcam.h
> > new file mode 100644
> > index 000000000000..aca0d1d32ee5
> > --- /dev/null
> > +++ b/include/uapi/linux/vcam.h
> > @@ -0,0 +1,124 @@
> > +/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */
> > +/*
> > + * Copyright (c) Jarkko Sakkinen 2025-2026
> > + */
> > +
> > +#ifndef _UAPI_LINUX_VCAM_H
> > +#define _UAPI_LINUX_VCAM_H
> > +
> > +#include <linux/types.h>
> > +#include <linux/ioctl.h>
> > +
> > +#define VCAM_IOC_BASE 'v'
> > +
> > +/**
> > + * DOC: vcam uAPI
> > + *
> > + * The ioctl API of /dev/vcam provides ioctls for creating DMA-BUF backed
> > + * virtual capture devices, and pushing image frames for consumption.
> > + *
> > + * Frames are queued with %VCAM_IOC_QUEUE and recycled with %VCAM_IOC_DEQUEUE.
> > + * Queueing without dequeuing eventually exhausts the output queue.
> > + */
> > +
> > +/**
> > + * enum vcam_status - Status bits
> > + * @VCAM_STATUS_IDLE: Capture queue is not streaming.
> > + * @VCAM_STATUS_STREAMING: Capture queue is streaming.
> > + */
> > +enum vcam_status {
> > +	VCAM_STATUS_IDLE = 1U << 0,
> > +	VCAM_STATUS_STREAMING = 1U << 1,
> > +};
> > +
> > +/**
> > + * struct vcam_ioc_create - Create a virtual camera device
> > + * @device_name: (input) User pointer to device name string.
> > + * @width: (input) Frame width in pixels. Must be non-zero.
> > + * @height: (input) Frame height in pixels. Must be non-zero.
> > + * @pixelformat: (input) Four CC format code.
> > + * @colorspace: (input) V4L2 colorspace value.
> > + * @bytesperline: (input) Bytes per line in the output format.
> > + * @reserved: Reserved for future use. Must be set to zero.
> > + * @device_nr: (output) Device number (must be 0 on input).
> > + * @nr_frames: (input) Number of entries in @frames.
> > + * @frames: (input/output) User pointer to an array of &struct vcam_frame.
> > + */
> > +struct vcam_ioc_create {
> > +	__u64 device_name;
> > +	__u32 width;
> > +	__u32 height;
> > +	__u32 pixelformat;
> > +	__u32 colorspace;
> > +	__u32 bytesperline;
> > +	__u32 reserved;
> > +	__u32 device_nr;
> > +	__u32 nr_frames;
> > +	__u64 frames;
> > +};
> > +
> > +/**
> > + * struct vcam_frame - a frame descriptor
> > + * @index: Frame index assigned by the driver.
> > + * @length: Frame size in bytes.
> > + */
> > +struct vcam_frame {
> > +	__u32 index;
> > +	__u32 length;
> > +};
> > +
> > +/**
> > + * struct vcam_ioc_queue - Produce an output buffer
> > + * @fd: (input) DMA-BUF file descriptor.
> > + * @index: (input) Buffer index for %VCAM_IOC_QUEUE.
> > + * @length: (input) Payload length in bytes for %VCAM_IOC_QUEUE.
> > + * @reserved: Reserved for future use. Must be set to zero.
> > + * @timestamp: (input) Timestamp in nanoseconds for %VCAM_IOC_QUEUE.
> > + */
> > +struct vcam_ioc_queue {
> > +	__u32 fd;
> > +	__u32 index;
> > +	__u32 length;
> > +	__u32 reserved;
> > +	__u64 timestamp;
> > +};
> > +
> > +/**
> > + * struct vcam_ioc_dequeue - Dequeue an output buffer
> > + * @index: (output) Buffer index for %VCAM_IOC_DEQUEUE.
> > + * @length: (output) Payload length in bytes for %VCAM_IOC_DEQUEUE.
> > + * @timestamp: (output) Timestamp in nanoseconds for %VCAM_IOC_DEQUEUE.
> > + */
> > +struct vcam_ioc_dequeue {
> > +	__u32 index;
> > +	__u32 length;
> > +	__u64 timestamp;
> > +};
> > +
> > +/**
> > + * struct vcam_ioc_wait - Wait for capture status
> > + * @mask: (input) Mask of status bits to wait for.
> > + * @status: (output) Current status bit mask.
> > + */
> > +struct vcam_ioc_wait {
> > +	__u64 mask;
> > +	__u64 status;
> > +};
> > +
> > +/**
> > + * DOC: vcam ioctls
> > + *
> > + * %VCAM_IOC_CREATE: Creates a virtual camera device and associates output
> > + * buffers described by &struct vcam_frame with DMA-BUF file descriptors.
> > + * %VCAM_IOC_QUEUE: Enqueues an output buffer for capture.
> > + * %VCAM_IOC_DEQUEUE: Dequeues a consumed output buffer for reuse.
> > + * %VCAM_IOC_STATUS: Reads the driver status bits.
> > + * %VCAM_IOC_WAIT: Waits for the subset of status bits to activate.
> > + */
> > +#define VCAM_IOC_CREATE _IOWR(VCAM_IOC_BASE, 0x00, struct vcam_ioc_create)
> > +#define VCAM_IOC_QUEUE _IOW(VCAM_IOC_BASE, 0x01, struct vcam_ioc_queue)
> > +#define VCAM_IOC_DEQUEUE _IOR(VCAM_IOC_BASE, 0x02, struct vcam_ioc_dequeue)
> > +#define VCAM_IOC_STATUS _IOR(VCAM_IOC_BASE, 0x03, __u64)
> > +#define VCAM_IOC_WAIT _IOWR(VCAM_IOC_BASE, 0x04, struct vcam_ioc_wait)
> > +
> > +#endif /* _UAPI_LINUX_VCAM_H */
> 
> -- 
> Regards,
> 
> Laurent Pinchart

BR, Jarkko
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 5 days, 15 hours ago
On Sun, Feb 01, 2026 at 09:04:00PM +0200, Jarkko Sakkinen wrote:
> On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
> > On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> > > vcam is a DMA-BUF backed virtual camera driver capable of creating video
> > > capture devices to which data can be streamed through /dev/vcam after
> > > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> > > with VCAM_IOC_DEQUEUE.
> > > 
> > > Zero-copy semantics are supported for shared DMA-BUF between capture and
> > > output.
> > >
> > > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> > > ---
> > > Early feedback e.g., is this completely in wrong direction? V4L2 world
> > > is relatively alien world, and thus I need a sanity check ;-)
> > 
> > We already have multiple virtual drivers, including vivid and vimc.
> > Could you please explain the rationale for yet another one, and why the
> > new features it provides (if any) can't be added to existing drivers ?
> 
> There is a notable user base for v4l2-loopback. It is the defacto choice
> for streaming phone cams.

This will then likely face the same hurdles as v4l2-loopback, the main
one being that camera support should be upstreamed with proper drivers
instead of a closed-source userspace daemon.

For phone cameras, the way forward upstream is libcamera. Until kernel
drivers for ISPs are available, the soft ISP is a stop-gap solution. It
recently gained GPU acceleration support (with work to improve image
quality with additional algorithms ongoing).

> The motivation here is to provide a service optimized for that use and
> purpose. It's virtual but non-generic i.e. not aimed for testing/emulation.
> 
> > >  .../driver-api/media/drivers/index.rst        |    1 +
> > >  .../driver-api/media/drivers/vcam.rst         |   16 +
> > >  MAINTAINERS                                   |    8 +
> > >  drivers/media/Kconfig                         |   13 +
> > >  drivers/media/Makefile                        |    1 +
> > >  drivers/media/vcam.c                          | 1700 +++++++++++++++++
> > >  include/uapi/linux/vcam.h                     |  124 ++
> > >  7 files changed, 1863 insertions(+)
> > >  create mode 100644 Documentation/driver-api/media/drivers/vcam.rst
> > >  create mode 100644 drivers/media/vcam.c
> > >  create mode 100644 include/uapi/linux/vcam.h

[snip]

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Jani Nikula 4 days, 2 hours ago
On Sun, 01 Feb 2026, Laurent Pinchart <laurent.pinchart@ideasonboard.com> wrote:
> On Sun, Feb 01, 2026 at 09:04:00PM +0200, Jarkko Sakkinen wrote:
>> On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
>> > On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
>> > > vcam is a DMA-BUF backed virtual camera driver capable of creating video
>> > > capture devices to which data can be streamed through /dev/vcam after
>> > > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
>> > > with VCAM_IOC_DEQUEUE.
>> > > 
>> > > Zero-copy semantics are supported for shared DMA-BUF between capture and
>> > > output.
>> > >
>> > > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
>> > > ---
>> > > Early feedback e.g., is this completely in wrong direction? V4L2 world
>> > > is relatively alien world, and thus I need a sanity check ;-)
>> > 
>> > We already have multiple virtual drivers, including vivid and vimc.
>> > Could you please explain the rationale for yet another one, and why the
>> > new features it provides (if any) can't be added to existing drivers ?
>> 
>> There is a notable user base for v4l2-loopback. It is the defacto choice
>> for streaming phone cams.
>
> This will then likely face the same hurdles as v4l2-loopback, the main
> one being that camera support should be upstreamed with proper drivers
> instead of a closed-source userspace daemon.

My use case:

Input screen capture and webcam into OBS Studio, output the combined
scene into virtual device, and input that virtual device into an
application that is designed to work with video devices like that, and
is not aware of anything fancier. For example, a web based meeting
software. [1]

There's nothing proprietary or closed-source here. AFAICT using
v4l2-loopback is currently the only method suggested or supported by OBS
Studio, or the plethora of apps that only really understand the video
devices.

I don't want to use that out-of-tree module from distro DKMS or
whatever. Please enlighten me (and apparently a lot of other folks) if
there's a better option that can be made to work out of the box. And it
pretty much has to be as simple as 'apt install v4l2loopback-dkms'.


BR,
Jani.


[1] https://obsproject.com/kb/virtual-camera-guide


-- 
Jani Nikula, Intel
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 3 days, 20 hours ago
On Tue, Feb 03, 2026 at 11:50:23AM +0200, Jani Nikula wrote:
> On Sun, 01 Feb 2026, Laurent Pinchart <laurent.pinchart@ideasonboard.com> wrote:
> > On Sun, Feb 01, 2026 at 09:04:00PM +0200, Jarkko Sakkinen wrote:
> >> On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
> >> > On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> >> > > vcam is a DMA-BUF backed virtual camera driver capable of creating video
> >> > > capture devices to which data can be streamed through /dev/vcam after
> >> > > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> >> > > with VCAM_IOC_DEQUEUE.
> >> > > 
> >> > > Zero-copy semantics are supported for shared DMA-BUF between capture and
> >> > > output.
> >> > >
> >> > > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> >> > > ---
> >> > > Early feedback e.g., is this completely in wrong direction? V4L2 world
> >> > > is relatively alien world, and thus I need a sanity check ;-)
> >> > 
> >> > We already have multiple virtual drivers, including vivid and vimc.
> >> > Could you please explain the rationale for yet another one, and why the
> >> > new features it provides (if any) can't be added to existing drivers ?
> >> 
> >> There is a notable user base for v4l2-loopback. It is the defacto choice
> >> for streaming phone cams.
> >
> > This will then likely face the same hurdles as v4l2-loopback, the main
> > one being that camera support should be upstreamed with proper drivers
> > instead of a closed-source userspace daemon.
> 
> My use case:
> 
> Input screen capture and webcam into OBS Studio, output the combined
> scene into virtual device, and input that virtual device into an
> application that is designed to work with video devices like that, and
> is not aware of anything fancier. For example, a web based meeting
> software. [1]
> 
> There's nothing proprietary or closed-source here. AFAICT using
> v4l2-loopback is currently the only method suggested or supported by OBS
> Studio, or the plethora of apps that only really understand the video
> devices.
> 
> I don't want to use that out-of-tree module from distro DKMS or
> whatever. Please enlighten me (and apparently a lot of other folks) if
> there's a better option that can be made to work out of the box. And it
> pretty much has to be as simple as 'apt install v4l2loopback-dkms'.

I'd say it should be as simple as 'apt install obs-studio' :-)

OBS has recently gained PipeWire support, but as a source, not a sink.
To cover your use case, we would need to extend OBS with PipeWire sink
support. The video stream that it would then provide to PipeWire would
be available in applications, including web browsers for video
conferencing.

Yes, this will require work on OBS, but the proposed virtual camera
driver does as well as the API on the sink side uses custom ioctls.
v4l2loopback support in OBS won't work out of the box with it. If OBS is
to be extended anyway, it should be done with PipeWire support as that's
where the ecosystem is moving.

PipeWire also includes pw-v4l2, a wrapper script with an LD_PRELOAD-able
library that emulates the V4L2 API. It helps with applications that
require a V4L2 capture device, until they gain native PipeWire support.

> [1] https://obsproject.com/kb/virtual-camera-guide

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Jarkko Sakkinen 5 days, 15 hours ago
On Sun, Feb 01, 2026 at 10:06:49PM +0200, Laurent Pinchart wrote:
> On Sun, Feb 01, 2026 at 09:04:00PM +0200, Jarkko Sakkinen wrote:
> > On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
> > > On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> > > > vcam is a DMA-BUF backed virtual camera driver capable of creating video
> > > > capture devices to which data can be streamed through /dev/vcam after
> > > > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> > > > with VCAM_IOC_DEQUEUE.
> > > > 
> > > > Zero-copy semantics are supported for shared DMA-BUF between capture and
> > > > output.
> > > >
> > > > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> > > > ---
> > > > Early feedback e.g., is this completely in wrong direction? V4L2 world
> > > > is relatively alien world, and thus I need a sanity check ;-)
> > > 
> > > We already have multiple virtual drivers, including vivid and vimc.
> > > Could you please explain the rationale for yet another one, and why the
> > > new features it provides (if any) can't be added to existing drivers ?
> > 
> > There is a notable user base for v4l2-loopback. It is the defacto choice
> > for streaming phone cams.
> 
> This will then likely face the same hurdles as v4l2-loopback, the main
> one being that camera support should be upstreamed with proper drivers
> instead of a closed-source userspace daemon.
> 
> For phone cameras, the way forward upstream is libcamera. Until kernel
> drivers for ISPs are available, the soft ISP is a stop-gap solution. It
> recently gained GPU acceleration support (with work to improve image
> quality with additional algorithms ongoing).

That might have some weight as a pro but the unarguable con is that at
the same time this policy retains a base of tainted kernels in the wild.

Not saying that this weight more but it is important to remark this
fact.

> 
> > The motivation here is to provide a service optimized for that use and
> > purpose. It's virtual but non-generic i.e. not aimed for testing/emulation.
> > 
> > > >  .../driver-api/media/drivers/index.rst        |    1 +
> > > >  .../driver-api/media/drivers/vcam.rst         |   16 +
> > > >  MAINTAINERS                                   |    8 +
> > > >  drivers/media/Kconfig                         |   13 +
> > > >  drivers/media/Makefile                        |    1 +
> > > >  drivers/media/vcam.c                          | 1700 +++++++++++++++++
> > > >  include/uapi/linux/vcam.h                     |  124 ++
> > > >  7 files changed, 1863 insertions(+)
> > > >  create mode 100644 Documentation/driver-api/media/drivers/vcam.rst
> > > >  create mode 100644 drivers/media/vcam.c
> > > >  create mode 100644 include/uapi/linux/vcam.h
> 
> [snip]
> 
> -- 
> Regards,
> 
> Laurent Pinchart

BR, Jarkko
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 5 days, 15 hours ago
On Sun, Feb 01, 2026 at 10:35:06PM +0200, Jarkko Sakkinen wrote:
> On Sun, Feb 01, 2026 at 10:06:49PM +0200, Laurent Pinchart wrote:
> > On Sun, Feb 01, 2026 at 09:04:00PM +0200, Jarkko Sakkinen wrote:
> > > On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
> > > > On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> > > > > vcam is a DMA-BUF backed virtual camera driver capable of creating video
> > > > > capture devices to which data can be streamed through /dev/vcam after
> > > > > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> > > > > with VCAM_IOC_DEQUEUE.
> > > > > 
> > > > > Zero-copy semantics are supported for shared DMA-BUF between capture and
> > > > > output.
> > > > >
> > > > > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> > > > > ---
> > > > > Early feedback e.g., is this completely in wrong direction? V4L2 world
> > > > > is relatively alien world, and thus I need a sanity check ;-)
> > > > 
> > > > We already have multiple virtual drivers, including vivid and vimc.
> > > > Could you please explain the rationale for yet another one, and why the
> > > > new features it provides (if any) can't be added to existing drivers ?
> > > 
> > > There is a notable user base for v4l2-loopback. It is the defacto choice
> > > for streaming phone cams.
> > 
> > This will then likely face the same hurdles as v4l2-loopback, the main
> > one being that camera support should be upstreamed with proper drivers
> > instead of a closed-source userspace daemon.
> > 
> > For phone cameras, the way forward upstream is libcamera. Until kernel
> > drivers for ISPs are available, the soft ISP is a stop-gap solution. It
> > recently gained GPU acceleration support (with work to improve image
> > quality with additional algorithms ongoing).
> 
> That might have some weight as a pro but the unarguable con is that at
> the same time this policy retains a base of tainted kernels in the wild.

Do you mean tainted by the out-of-tree v4l2loopback module ? Won't those
systems be equally tainted by out-of-tree camera drivers then ? With
libcamera and the soft ISP you can run a 100% mainline stack.

> Not saying that this weight more but it is important to remark this
> fact.
> 
> > > The motivation here is to provide a service optimized for that use and
> > > purpose. It's virtual but non-generic i.e. not aimed for testing/emulation.
> > > 
> > > > >  .../driver-api/media/drivers/index.rst        |    1 +
> > > > >  .../driver-api/media/drivers/vcam.rst         |   16 +
> > > > >  MAINTAINERS                                   |    8 +
> > > > >  drivers/media/Kconfig                         |   13 +
> > > > >  drivers/media/Makefile                        |    1 +
> > > > >  drivers/media/vcam.c                          | 1700 +++++++++++++++++
> > > > >  include/uapi/linux/vcam.h                     |  124 ++
> > > > >  7 files changed, 1863 insertions(+)
> > > > >  create mode 100644 Documentation/driver-api/media/drivers/vcam.rst
> > > > >  create mode 100644 drivers/media/vcam.c
> > > > >  create mode 100644 include/uapi/linux/vcam.h
> > 
> > [snip]

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Jarkko Sakkinen 5 days, 14 hours ago
On Sun, Feb 01, 2026 at 11:01:44PM +0200, Laurent Pinchart wrote:
> On Sun, Feb 01, 2026 at 10:35:06PM +0200, Jarkko Sakkinen wrote:
> > On Sun, Feb 01, 2026 at 10:06:49PM +0200, Laurent Pinchart wrote:
> > > On Sun, Feb 01, 2026 at 09:04:00PM +0200, Jarkko Sakkinen wrote:
> > > > On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
> > > > > On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> > > > > > vcam is a DMA-BUF backed virtual camera driver capable of creating video
> > > > > > capture devices to which data can be streamed through /dev/vcam after
> > > > > > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> > > > > > with VCAM_IOC_DEQUEUE.
> > > > > > 
> > > > > > Zero-copy semantics are supported for shared DMA-BUF between capture and
> > > > > > output.
> > > > > >
> > > > > > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> > > > > > ---
> > > > > > Early feedback e.g., is this completely in wrong direction? V4L2 world
> > > > > > is relatively alien world, and thus I need a sanity check ;-)
> > > > > 
> > > > > We already have multiple virtual drivers, including vivid and vimc.
> > > > > Could you please explain the rationale for yet another one, and why the
> > > > > new features it provides (if any) can't be added to existing drivers ?
> > > > 
> > > > There is a notable user base for v4l2-loopback. It is the defacto choice
> > > > for streaming phone cams.
> > > 
> > > This will then likely face the same hurdles as v4l2-loopback, the main
> > > one being that camera support should be upstreamed with proper drivers
> > > instead of a closed-source userspace daemon.
> > > 
> > > For phone cameras, the way forward upstream is libcamera. Until kernel
> > > drivers for ISPs are available, the soft ISP is a stop-gap solution. It
> > > recently gained GPU acceleration support (with work to improve image
> > > quality with additional algorithms ongoing).
> > 
> > That might have some weight as a pro but the unarguable con is that at
> > the same time this policy retains a base of tainted kernels in the wild.
> 
> Do you mean tainted by the out-of-tree v4l2loopback module ? Won't those
> systems be equally tainted by out-of-tree camera drivers then ? With
> libcamera and the soft ISP you can run a 100% mainline stack.

A camera driver could also manage a network stream, not necessarily
some piece of proprietary hardware.

That said I don't have enough knowledge of the industry to say anything
about how properietary risk would change i.e., not really arguing against
that.

Just want to emphasis that while disagreeing in some level I'm not
downplaying totally legit arguments :-)

BR, Jarkko
Re: [RFC PATCH] media: Virtual camera driver
Posted by Jarkko Sakkinen 5 days, 15 hours ago
On Sun, Feb 01, 2026 at 10:35:12PM +0200, Jarkko Sakkinen wrote:
> On Sun, Feb 01, 2026 at 10:06:49PM +0200, Laurent Pinchart wrote:
> > On Sun, Feb 01, 2026 at 09:04:00PM +0200, Jarkko Sakkinen wrote:
> > > On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
> > > > On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> > > > > vcam is a DMA-BUF backed virtual camera driver capable of creating video
> > > > > capture devices to which data can be streamed through /dev/vcam after
> > > > > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> > > > > with VCAM_IOC_DEQUEUE.
> > > > > 
> > > > > Zero-copy semantics are supported for shared DMA-BUF between capture and
> > > > > output.
> > > > >
> > > > > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> > > > > ---
> > > > > Early feedback e.g., is this completely in wrong direction? V4L2 world
> > > > > is relatively alien world, and thus I need a sanity check ;-)
> > > > 
> > > > We already have multiple virtual drivers, including vivid and vimc.
> > > > Could you please explain the rationale for yet another one, and why the
> > > > new features it provides (if any) can't be added to existing drivers ?
> > > 
> > > There is a notable user base for v4l2-loopback. It is the defacto choice
> > > for streaming phone cams.
> > 
> > This will then likely face the same hurdles as v4l2-loopback, the main
> > one being that camera support should be upstreamed with proper drivers
> > instead of a closed-source userspace daemon.
> > 
> > For phone cameras, the way forward upstream is libcamera. Until kernel
> > drivers for ISPs are available, the soft ISP is a stop-gap solution. It
> > recently gained GPU acceleration support (with work to improve image
> > quality with additional algorithms ongoing).
> 
> That might have some weight as a pro but the unarguable con is that at
> the same time this policy retains a base of tainted kernels in the wild.
> 
> Not saying that this weight more but it is important to remark this
> fact.

It's widely packaged for different distributions and even embedded build
systems forming across the board tained ecosystem. And this has been
ongoing for years. Suggesting PipeWire as "a fix" for all possible
situations is not "a solution".

BR, Jarkko
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 5 days, 14 hours ago
On Sun, Feb 01, 2026 at 10:54:34PM +0200, Jarkko Sakkinen wrote:
> On Sun, Feb 01, 2026 at 10:35:12PM +0200, Jarkko Sakkinen wrote:
> > On Sun, Feb 01, 2026 at 10:06:49PM +0200, Laurent Pinchart wrote:
> > > On Sun, Feb 01, 2026 at 09:04:00PM +0200, Jarkko Sakkinen wrote:
> > > > On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
> > > > > On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> > > > > > vcam is a DMA-BUF backed virtual camera driver capable of creating video
> > > > > > capture devices to which data can be streamed through /dev/vcam after
> > > > > > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> > > > > > with VCAM_IOC_DEQUEUE.
> > > > > > 
> > > > > > Zero-copy semantics are supported for shared DMA-BUF between capture and
> > > > > > output.
> > > > > >
> > > > > > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> > > > > > ---
> > > > > > Early feedback e.g., is this completely in wrong direction? V4L2 world
> > > > > > is relatively alien world, and thus I need a sanity check ;-)
> > > > > 
> > > > > We already have multiple virtual drivers, including vivid and vimc.
> > > > > Could you please explain the rationale for yet another one, and why the
> > > > > new features it provides (if any) can't be added to existing drivers ?
> > > > 
> > > > There is a notable user base for v4l2-loopback. It is the defacto choice
> > > > for streaming phone cams.
> > > 
> > > This will then likely face the same hurdles as v4l2-loopback, the main
> > > one being that camera support should be upstreamed with proper drivers
> > > instead of a closed-source userspace daemon.
> > > 
> > > For phone cameras, the way forward upstream is libcamera. Until kernel
> > > drivers for ISPs are available, the soft ISP is a stop-gap solution. It
> > > recently gained GPU acceleration support (with work to improve image
> > > quality with additional algorithms ongoing).
> > 
> > That might have some weight as a pro but the unarguable con is that at
> > the same time this policy retains a base of tainted kernels in the wild.
> > 
> > Not saying that this weight more but it is important to remark this
> > fact.
> 
> It's widely packaged for different distributions and even embedded build
> systems forming across the board tained ecosystem. And this has been
> ongoing for years. Suggesting PipeWire as "a fix" for all possible
> situations is not "a solution".

PipeWire may not solve all of the world's problems, but it's part of a
clean solution for this particular issue. The fact that everybody relied
on an out-of-tree kernel module instead of designing a better stack is
not a reason to merge v4l2loopback upstream now that we have a better
option that is actively developed.

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Jarkko Sakkinen 5 days, 10 hours ago
On Sun, Feb 01, 2026 at 11:09:52PM +0200, Laurent Pinchart wrote:
> On Sun, Feb 01, 2026 at 10:54:34PM +0200, Jarkko Sakkinen wrote:
> > On Sun, Feb 01, 2026 at 10:35:12PM +0200, Jarkko Sakkinen wrote:
> > > On Sun, Feb 01, 2026 at 10:06:49PM +0200, Laurent Pinchart wrote:
> > > > On Sun, Feb 01, 2026 at 09:04:00PM +0200, Jarkko Sakkinen wrote:
> > > > > On Sun, Feb 01, 2026 at 08:20:11PM +0200, Laurent Pinchart wrote:
> > > > > > On Sun, Feb 01, 2026 at 03:33:38PM +0200, Jarkko Sakkinen wrote:
> > > > > > > vcam is a DMA-BUF backed virtual camera driver capable of creating video
> > > > > > > capture devices to which data can be streamed through /dev/vcam after
> > > > > > > calling VCAM_IOC_CREATE. Frames are pushed with VCAM_IOC_QUEUE and recycled
> > > > > > > with VCAM_IOC_DEQUEUE.
> > > > > > > 
> > > > > > > Zero-copy semantics are supported for shared DMA-BUF between capture and
> > > > > > > output.
> > > > > > >
> > > > > > > Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
> > > > > > > ---
> > > > > > > Early feedback e.g., is this completely in wrong direction? V4L2 world
> > > > > > > is relatively alien world, and thus I need a sanity check ;-)
> > > > > > 
> > > > > > We already have multiple virtual drivers, including vivid and vimc.
> > > > > > Could you please explain the rationale for yet another one, and why the
> > > > > > new features it provides (if any) can't be added to existing drivers ?
> > > > > 
> > > > > There is a notable user base for v4l2-loopback. It is the defacto choice
> > > > > for streaming phone cams.
> > > > 
> > > > This will then likely face the same hurdles as v4l2-loopback, the main
> > > > one being that camera support should be upstreamed with proper drivers
> > > > instead of a closed-source userspace daemon.
> > > > 
> > > > For phone cameras, the way forward upstream is libcamera. Until kernel
> > > > drivers for ISPs are available, the soft ISP is a stop-gap solution. It
> > > > recently gained GPU acceleration support (with work to improve image
> > > > quality with additional algorithms ongoing).
> > > 
> > > That might have some weight as a pro but the unarguable con is that at
> > > the same time this policy retains a base of tainted kernels in the wild.
> > > 
> > > Not saying that this weight more but it is important to remark this
> > > fact.
> > 
> > It's widely packaged for different distributions and even embedded build
> > systems forming across the board tained ecosystem. And this has been
> > ongoing for years. Suggesting PipeWire as "a fix" for all possible
> > situations is not "a solution".
> 
> PipeWire may not solve all of the world's problems, but it's part of a
> clean solution for this particular issue. The fact that everybody relied
> on an out-of-tree kernel module instead of designing a better stack is
> not a reason to merge v4l2loopback upstream now that we have a better
> option that is actively developed.

Nobody (at least not in this thread) requested to merge v4l2-loopback
upstream. Let's use correct terminology in order not convolute the
discussion further, thank you.

> 
> -- 
> Regards,
> 
> Laurent Pinchart

BR, Jarkko
Re: [RFC PATCH] media: Virtual camera driver
Posted by Oleksandr Natalenko 5 days, 15 hours ago
Hello.

On neděle 1. února 2026 21:06:49, středoevropský standardní čas Laurent Pinchart wrote:
> > There is a notable user base for v4l2-loopback. It is the defacto choice
> > for streaming phone cams.
> 
> This will then likely face the same hurdles as v4l2-loopback, the main
> one being that camera support should be upstreamed with proper drivers
> instead of a closed-source userspace daemon.
> 
> For phone cameras, the way forward upstream is libcamera. Until kernel
> drivers for ISPs are available, the soft ISP is a stop-gap solution. It
> recently gained GPU acceleration support (with work to improve image
> quality with additional algorithms ongoing).

My use-case for v4l2loopback is to stream a webcam from one machine to another (with the help of ffmpeg). Is this covered by something other than v4l2loopback now?

Thank you.

-- 
Oleksandr Natalenko, MSE
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 5 days, 15 hours ago
On Sun, Feb 01, 2026 at 09:14:54PM +0100, Oleksandr Natalenko wrote:
> On neděle 1. února 2026 21:06:49, středoevropský standardní čas Laurent Pinchart wrote:
> > > There is a notable user base for v4l2-loopback. It is the defacto choice
> > > for streaming phone cams.
> > 
> > This will then likely face the same hurdles as v4l2-loopback, the main
> > one being that camera support should be upstreamed with proper drivers
> > instead of a closed-source userspace daemon.
> > 
> > For phone cameras, the way forward upstream is libcamera. Until kernel
> > drivers for ISPs are available, the soft ISP is a stop-gap solution. It
> > recently gained GPU acceleration support (with work to improve image
> > quality with additional algorithms ongoing).
> 
> My use-case for v4l2loopback is to stream a webcam from one machine to
> another (with the help of ffmpeg). Is this covered by something other
> than v4l2loopback now?

On the transmitting side I assume you don't use v4l2loopback. On the
receiving side, the recommened option is PipeWire.

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Oleksandr Natalenko 5 days, 15 hours ago
On neděle 1. února 2026 21:22:00, středoevropský standardní čas Laurent Pinchart wrote:
> > My use-case for v4l2loopback is to stream a webcam from one machine to
> > another (with the help of ffmpeg). Is this covered by something other
> > than v4l2loopback now?
> 
> On the transmitting side I assume you don't use v4l2loopback. On the
> receiving side, the recommened option is PipeWire.

Yes, v4l2loopback is on the receiving side. Would you please be able to share a manual for solving this with PipeWire only?

Thank you.

-- 
Oleksandr Natalenko, MSE
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 5 days, 15 hours ago
On Sun, Feb 01, 2026 at 09:27:30PM +0100, Oleksandr Natalenko wrote:
> On neděle 1. února 2026 21:22:00, středoevropský standardní čas Laurent Pinchart wrote:
> > > My use-case for v4l2loopback is to stream a webcam from one machine to
> > > another (with the help of ffmpeg). Is this covered by something other
> > > than v4l2loopback now?
> > 
> > On the transmitting side I assume you don't use v4l2loopback. On the
> > receiving side, the recommened option is PipeWire.
> 
> Yes, v4l2loopback is on the receiving side. Would you please be able
> to share a manual for solving this with PipeWire only?

The basic idea is that you need an application that receives data over
the network and feeds it into PipeWire, the same way you would do with
v4l2loopback. GStreamer should be an easy option, using the
gstpipewiresink element. I haven't tested that personally though, so I
don't have detailed instructions.

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Mauro Carvalho Chehab 5 days, 15 hours ago
On Sun, 01 Feb 2026 21:14:54 +0100
Oleksandr Natalenko <oleksandr@natalenko.name> wrote:

> Hello.
> 
> On neděle 1. února 2026 21:06:49, středoevropský standardní čas Laurent Pinchart wrote:
> > > There is a notable user base for v4l2-loopback. It is the defacto choice
> > > for streaming phone cams.  
> > 
> > This will then likely face the same hurdles as v4l2-loopback, the main
> > one being that camera support should be upstreamed with proper drivers
> > instead of a closed-source userspace daemon.
> > 
> > For phone cameras, the way forward upstream is libcamera. Until kernel
> > drivers for ISPs are available, the soft ISP is a stop-gap solution. It
> > recently gained GPU acceleration support (with work to improve image
> > quality with additional algorithms ongoing).  
> 
> My use-case for v4l2loopback is to stream a webcam from one machine to another (with the help of ffmpeg). Is this covered by something other than v4l2loopback now?

Using a kernel driver for something like that is a bad idea and may end
causing dead lock problems. You may also have performance issues and
high network traffic. The best solution for it is to use a proper
userspace tool, like obs:

	https://obsproject.com/kb/linux-installation

Thanks,
Mauro
Re: [RFC PATCH] media: Virtual camera driver
Posted by Oleksandr Natalenko 5 days, 15 hours ago
On neděle 1. února 2026 21:21:33, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > My use-case for v4l2loopback is to stream a webcam from one machine to another (with the help of ffmpeg). Is this covered by something other than v4l2loopback now?
> 
> Using a kernel driver for something like that is a bad idea and may end
> causing dead lock problems. You may also have performance issues and
> high network traffic. The best solution for it is to use a proper
> userspace tool, like obs:
> 
> 	https://obsproject.com/kb/linux-installation

Ignoring the fact I've never had any performance issues, and I don't care much about how big the traffic is in my isolated VLAN dedicated to this specific task, the OBS solution still uses v4l2loopback under the hood. Could you please tell me what do I miss in this regard?

Thank you.

-- 
Oleksandr Natalenko, MSE
Re: [RFC PATCH] media: Virtual camera driver
Posted by Mauro Carvalho Chehab 5 days, 12 hours ago
On Sun, 01 Feb 2026 21:26:24 +0100
Oleksandr Natalenko <oleksandr@natalenko.name> wrote:

> On neděle 1. února 2026 21:21:33, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > > My use-case for v4l2loopback is to stream a webcam from one machine to another (with the help of ffmpeg). Is this covered by something other than v4l2loopback now?  
> > 
> > Using a kernel driver for something like that is a bad idea and may end
> > causing dead lock problems. You may also have performance issues and
> > high network traffic. The best solution for it is to use a proper
> > userspace tool, like obs:
> > 
> > 	https://obsproject.com/kb/linux-installation  
> 
> Ignoring the fact I've never had any performance issues, and I don't care much about how big the traffic is in my isolated VLAN dedicated to this specific task, the OBS solution still uses v4l2loopback under the hood. Could you please tell me what do I miss in this regard?

No. At the machine with the camera, obs can read from a V4L2 input,
generate a mpeg TS stream, and listen to a UDP port (for instance). 

At the remote machine, you can just pass the URL to ffmpeg.

No need to use OOT kernel drivers. 

Btw, there are other solutions that work the same way, like
motioneye.

Thanks,
Mauro
Re: [RFC PATCH] media: Virtual camera driver
Posted by Oleksandr Natalenko 5 days, 12 hours ago
On pondělí 2. února 2026 0:17:20, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> No. At the machine with the camera, obs can read from a V4L2 input,
> generate a mpeg TS stream, and listen to a UDP port (for instance). 
> 
> At the remote machine, you can just pass the URL to ffmpeg.

I can't, I have to feed the stream into Firefox somehow for it to see the stream as a virtual webcam.

> No need to use OOT kernel drivers. 
> 
> Btw, there are other solutions that work the same way, like
> motioneye.

-- 
Oleksandr Natalenko, MSE
Re: [RFC PATCH] media: Virtual camera driver
Posted by Mauro Carvalho Chehab 5 days, 11 hours ago
On Mon, 02 Feb 2026 00:25:37 +0100
Oleksandr Natalenko <oleksandr@natalenko.name> wrote:

> Date: Mon, 02 Feb 2026 00:25:37 +0100
> From: Oleksandr Natalenko <oleksandr@natalenko.name>
> To: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
> Cc: Laurent Pinchart <laurent.pinchart@ideasonboard.com>, Jarkko Sakkinen <jarkko@kernel.org>, linux-media@vger.kernel.org, jani.nikula@linux.intel.com, anisse@astier.eu, Mauro Carvalho Chehab <mchehab@kernel.org>, Hans Verkuil <hverkuil@kernel.org>, Sakari Ailus <sakari.ailus@linux.intel.com>, Jacopo Mondi <jacopo.mondi@ideasonboard.com>, Ricardo Ribalda <ribalda@chromium.org>, open list <linux-kernel@vger.kernel.org>
> Message-ID: <13939245.uLZWGnKmhe@natalenko.name>
> 
> On pondělí 2. února 2026 0:17:20, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > No. At the machine with the camera, obs can read from a V4L2 input,
> > generate a mpeg TS stream, and listen to a UDP port (for instance). 
> > 
> > At the remote machine, you can just pass the URL to ffmpeg.  
> 
> I can't, I have to feed the stream into Firefox somehow for it to see the stream as a virtual webcam.

Motioneye could be used on such scenario. It has a proper web
interface, allows multiple users to watch, has login control accepts
multiple cameras.

Thanks,
Mauro
Re: [RFC PATCH] media: Virtual camera driver
Posted by Gergo Koteles 5 days ago
On Mon, 2026-02-02 at 02:02 +0100, Mauro Carvalho Chehab wrote:
> On Mon, 02 Feb 2026 00:25:37 +0100
> Oleksandr Natalenko <oleksandr@natalenko.name> wrote:
> 
> > Date: Mon, 02 Feb 2026 00:25:37 +0100
> > From: Oleksandr Natalenko <oleksandr@natalenko.name>
> > To: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
> > Cc: Laurent Pinchart <laurent.pinchart@ideasonboard.com>, Jarkko Sakkinen <jarkko@kernel.org>, linux-media@vger.kernel.org, jani.nikula@linux.intel.com, anisse@astier.eu, Mauro Carvalho Chehab <mchehab@kernel.org>, Hans Verkuil <hverkuil@kernel.org>, Sakari Ailus <sakari.ailus@linux.intel.com>, Jacopo Mondi <jacopo.mondi@ideasonboard.com>, Ricardo Ribalda <ribalda@chromium.org>, open list <linux-kernel@vger.kernel.org>
> > Message-ID: <13939245.uLZWGnKmhe@natalenko.name>
> > 
> > On pondělí 2. února 2026 0:17:20, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > > No. At the machine with the camera, obs can read from a V4L2 input,
> > > generate a mpeg TS stream, and listen to a UDP port (for instance). 
> > > 
> > > At the remote machine, you can just pass the URL to ffmpeg.  
> > 
> > I can't, I have to feed the stream into Firefox somehow for it to see the stream as a virtual webcam.
> 
> Motioneye could be used on such scenario. It has a proper web
> interface, allows multiple users to watch, has login control accepts
> multiple cameras.
> 

WebRTC in browsers isn't that bad. Firefox and Chrome also have
PipeWire video support. 

If I understand correctly, it would be more forward-thinking to develop
virtual camera support in PipeWire rather than in the kernel.

Best Regards,
Gergo Koteles
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 5 days ago
On Mon, Feb 02, 2026 at 12:36:44PM +0100, Gergo Koteles wrote:
> On Mon, 2026-02-02 at 02:02 +0100, Mauro Carvalho Chehab wrote:
> > On Mon, 02 Feb 2026 00:25:37 +0100
> > Oleksandr Natalenko <oleksandr@natalenko.name> wrote:
> > 
> > > Date: Mon, 02 Feb 2026 00:25:37 +0100
> > > From: Oleksandr Natalenko <oleksandr@natalenko.name>
> > > To: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
> > > Cc: Laurent Pinchart <laurent.pinchart@ideasonboard.com>, Jarkko Sakkinen <jarkko@kernel.org>, linux-media@vger.kernel.org, jani.nikula@linux.intel.com, anisse@astier.eu, Mauro Carvalho Chehab <mchehab@kernel.org>, Hans Verkuil <hverkuil@kernel.org>, Sakari Ailus <sakari.ailus@linux.intel.com>, Jacopo Mondi <jacopo.mondi@ideasonboard.com>, Ricardo Ribalda <ribalda@chromium.org>, open list <linux-kernel@vger.kernel.org>
> > > Message-ID: <13939245.uLZWGnKmhe@natalenko.name>
> > > 
> > > On pondělí 2. února 2026 0:17:20, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > > > No. At the machine with the camera, obs can read from a V4L2 input,
> > > > generate a mpeg TS stream, and listen to a UDP port (for instance). 
> > > > 
> > > > At the remote machine, you can just pass the URL to ffmpeg.  
> > > 
> > > I can't, I have to feed the stream into Firefox somehow for it to see the stream as a virtual webcam.
> > 
> > Motioneye could be used on such scenario. It has a proper web
> > interface, allows multiple users to watch, has login control accepts
> > multiple cameras.
> > 
> 
> WebRTC in browsers isn't that bad. Firefox and Chrome also have
> PipeWire video support. 
> 
> If I understand correctly, it would be more forward-thinking to develop
> virtual camera support in PipeWire rather than in the kernel.

I don't think there's even a need for development in PipeWire

$ gst-launch-1.0 \
	videotestsrc ! \
	video/x-raw,format=YUY2 ! \
	pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera"

This gives me a virtual camera in Firefox. Extending the GStreamer
pipeline to get the video stream from the network should be quite
trivial.

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Oleksandr Natalenko 5 days ago
On pondělí 2. února 2026 12:40:12, středoevropský standardní čas Laurent Pinchart wrote:
> > If I understand correctly, it would be more forward-thinking to develop
> > virtual camera support in PipeWire rather than in the kernel.
> 
> I don't think there's even a need for development in PipeWire
> 
> $ gst-launch-1.0 \
> 	videotestsrc ! \
> 	video/x-raw,format=YUY2 ! \
> 	pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera"
> 
> This gives me a virtual camera in Firefox. Extending the GStreamer
> pipeline to get the video stream from the network should be quite
> trivial.

So far, I came up with this:

* sender:

$ gst-launch-1.0 pipewiresrc path=<webcam_id> ! image/jpeg, width=1280, height=720, framerate=24/1 ! rndbuffersize max=1400 ! udpsink host=<receiver_host> port=<receiver_port>

* receiver:

$ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! image/jpeg, width=1280, height=720, framerate=24/1 ! jpegparse ! jpegdec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam

Please let me know if I do something dumb here. Trial and error to make this work took a couple of hours for me, but it seems to provide what I need.

Thank you.

-- 
Oleksandr Natalenko, MSE
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 4 days, 10 hours ago
Hi Oleksandr,

(Cc'ing Nicolas Dufresne)

On Mon, Feb 02, 2026 at 12:45:15PM +0100, Oleksandr Natalenko wrote:
> On pondělí 2. února 2026 12:40:12, středoevropský standardní čas Laurent Pinchart wrote:
> > > If I understand correctly, it would be more forward-thinking to develop
> > > virtual camera support in PipeWire rather than in the kernel.
> > 
> > I don't think there's even a need for development in PipeWire
> > 
> > $ gst-launch-1.0 \
> > 	videotestsrc ! \
> > 	video/x-raw,format=YUY2 ! \
> > 	pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera"
> > 
> > This gives me a virtual camera in Firefox. Extending the GStreamer
> > pipeline to get the video stream from the network should be quite
> > trivial.
> 
> So far, I came up with this:
> 
> * sender:
> 
> $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! image/jpeg, width=1280, height=720, framerate=24/1 ! rndbuffersize max=1400 ! udpsink host=<receiver_host> port=<receiver_port>
>
> * receiver:
> 
> $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! image/jpeg, width=1280, height=720, framerate=24/1 ! jpegparse ! jpegdec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
>
> Please let me know if I do something dumb here. Trial and error to
> make this work took a couple of hours for me, but it seems to provide
> what I need.

There's nothing dumb at all, especially given that it works :-) I have
been able to reproduce it locally (using a different pipeline on the
sender side).

I compared your pipelines with another JPEG-over-UDP setup I used a
while ago, which used an rtpjpegpay element before udpsink on the sender
side to encapsulate the payload in RTP packets, and an rtpjpegdepay
element on the receiver side after udpsrc. This helps the receiver
synchronize with the sender if the sender is started first. The full
pipelines are

* Sender:

gst-launch-1.0 \
	v4l2src ! \
	video/x-raw,pixelformat=YUYV,size=640x480 ! \
	jpegenc ! \
	rtpjpegpay ! \
	udpsink host=192.168.10.200 port=8000

* Receiver:

gst-launch-1.0 \
	udpsrc port=8000 ! \
	application/x-rtp,encoding-name=JPEG,payload=26 ! \
	rtpjpegdepay ! \
	jpegdec ! \
	video/x-raw,pixelformat=YUYV,size=640x480 ! \
	queue ! \
	pipewiresink mode=provide \
	       stream-properties="properties,media.class=Video/Source,media.role=Camera" \
	       client-name="Remote Camera"

Unfortunatley this doesn't work, when the pipewire client connects to
the stream on the receiver side I get

ERROR: from element /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0: stream error: no more input formats

Nicolas, would you have any wisdom to share about this and tell me if I
did something dumb ? :-) There's no hurry.

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Oleksandr Natalenko 3 days, 21 hours ago
On úterý 3. února 2026 2:23:13, středoevropský standardní čas Laurent Pinchart wrote:
> Hi Oleksandr,
> 
> (Cc'ing Nicolas Dufresne)
> 
> On Mon, Feb 02, 2026 at 12:45:15PM +0100, Oleksandr Natalenko wrote:
> > On pondělí 2. února 2026 12:40:12, středoevropský standardní čas Laurent Pinchart wrote:
> > > > If I understand correctly, it would be more forward-thinking to develop
> > > > virtual camera support in PipeWire rather than in the kernel.
> > > 
> > > I don't think there's even a need for development in PipeWire
> > > 
> > > $ gst-launch-1.0 \
> > > 	videotestsrc ! \
> > > 	video/x-raw,format=YUY2 ! \
> > > 	pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera"
> > > 
> > > This gives me a virtual camera in Firefox. Extending the GStreamer
> > > pipeline to get the video stream from the network should be quite
> > > trivial.
> > 
> > So far, I came up with this:
> > 
> > * sender:
> > 
> > $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! image/jpeg, width=1280, height=720, framerate=24/1 ! rndbuffersize max=1400 ! udpsink host=<receiver_host> port=<receiver_port>
> >
> > * receiver:
> > 
> > $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! image/jpeg, width=1280, height=720, framerate=24/1 ! jpegparse ! jpegdec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
> >
> > Please let me know if I do something dumb here. Trial and error to
> > make this work took a couple of hours for me, but it seems to provide
> > what I need.
> 
> There's nothing dumb at all, especially given that it works :-) I have
> been able to reproduce it locally (using a different pipeline on the
> sender side).
> 
> I compared your pipelines with another JPEG-over-UDP setup I used a
> while ago, which used an rtpjpegpay element before udpsink on the sender
> side to encapsulate the payload in RTP packets, and an rtpjpegdepay
> element on the receiver side after udpsrc. This helps the receiver
> synchronize with the sender if the sender is started first. The full
> pipelines are
> 
> * Sender:
> 
> gst-launch-1.0 \
> 	v4l2src ! \
> 	video/x-raw,pixelformat=YUYV,size=640x480 ! \
> 	jpegenc ! \
> 	rtpjpegpay ! \
> 	udpsink host=192.168.10.200 port=8000
> 
> * Receiver:
> 
> gst-launch-1.0 \
> 	udpsrc port=8000 ! \
> 	application/x-rtp,encoding-name=JPEG,payload=26 ! \
> 	rtpjpegdepay ! \
> 	jpegdec ! \
> 	video/x-raw,pixelformat=YUYV,size=640x480 ! \
> 	queue ! \
> 	pipewiresink mode=provide \
> 	       stream-properties="properties,media.class=Video/Source,media.role=Camera" \
> 	       client-name="Remote Camera"
> 
> Unfortunatley this doesn't work, when the pipewire client connects to
> the stream on the receiver side I get
> 
> ERROR: from element /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0: stream error: no more input formats
> 
> Nicolas, would you have any wisdom to share about this and tell me if I
> did something dumb ? :-) There's no hurry.

Just to share my current state of affairs:

* sender:

$ gst-launch-1.0 pipewiresrc path=<webcam_id> ! video/x-h264, width=1280, height=720, framerate=24/1 ! rtph264pay ! rtpstreampay ! udpsink host=<receiver_host> port=<receiver_port>

* receiver:

$ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! application/x-rtp-stream,encoding-name=H264 ! rtpstreamdepay ! application/x-rtp,encoding-name=H264 ! rtph264depay ! h264parse ! openh264dec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam

I chose H.264 because of much lower (tenfold) traffic comparing to MJPEG, wrapped this into RTP, opted in for OpenH264 decoder because I read it was handling low latency streams better than avdec_h264, and tested this setup with both Firefox and Chromium, and it actually worked pretty reliably, so I'm impressed now.

The only issue I have with this thing is that once a tab with meeting in the browser is closed, the whole receiver pipeline stops gracefully because "PipeWire link to remote node was destroyed". I didn't find a way to tell the pipeline to just restart, so in fact I had to wrap it into a Python script with Gst.parse_launch() and friends, and add error message parsing to restart the pipeline inside the script.

Leaving this in public, because it's a straightforward and potentially widely used setup, yet there's little to no info on how to do it properly, and the knowledge is scattered across random posts of varying age.

-- 
Oleksandr Natalenko, MSE
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 3 days, 21 hours ago
Hi Oleksandr,

On Tue, Feb 03, 2026 at 03:38:06PM +0100, Oleksandr Natalenko wrote:
> On úterý 3. února 2026 2:23:13, středoevropský standardní čas Laurent Pinchart wrote:
> > Hi Oleksandr,
> > 
> > (Cc'ing Nicolas Dufresne)
> > 
> > On Mon, Feb 02, 2026 at 12:45:15PM +0100, Oleksandr Natalenko wrote:
> > > On pondělí 2. února 2026 12:40:12, středoevropský standardní čas Laurent Pinchart wrote:
> > > > > If I understand correctly, it would be more forward-thinking to develop
> > > > > virtual camera support in PipeWire rather than in the kernel.
> > > > 
> > > > I don't think there's even a need for development in PipeWire
> > > > 
> > > > $ gst-launch-1.0 \
> > > > 	videotestsrc ! \
> > > > 	video/x-raw,format=YUY2 ! \
> > > > 	pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera"
> > > > 
> > > > This gives me a virtual camera in Firefox. Extending the GStreamer
> > > > pipeline to get the video stream from the network should be quite
> > > > trivial.
> > > 
> > > So far, I came up with this:
> > > 
> > > * sender:
> > > 
> > > $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! image/jpeg, width=1280, height=720, framerate=24/1 ! rndbuffersize max=1400 ! udpsink host=<receiver_host> port=<receiver_port>
> > >
> > > * receiver:
> > > 
> > > $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! image/jpeg, width=1280, height=720, framerate=24/1 ! jpegparse ! jpegdec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
> > >
> > > Please let me know if I do something dumb here. Trial and error to
> > > make this work took a couple of hours for me, but it seems to provide
> > > what I need.
> > 
> > There's nothing dumb at all, especially given that it works :-) I have
> > been able to reproduce it locally (using a different pipeline on the
> > sender side).
> > 
> > I compared your pipelines with another JPEG-over-UDP setup I used a
> > while ago, which used an rtpjpegpay element before udpsink on the sender
> > side to encapsulate the payload in RTP packets, and an rtpjpegdepay
> > element on the receiver side after udpsrc. This helps the receiver
> > synchronize with the sender if the sender is started first. The full
> > pipelines are
> > 
> > * Sender:
> > 
> > gst-launch-1.0 \
> > 	v4l2src ! \
> > 	video/x-raw,pixelformat=YUYV,size=640x480 ! \
> > 	jpegenc ! \
> > 	rtpjpegpay ! \
> > 	udpsink host=192.168.10.200 port=8000
> > 
> > * Receiver:
> > 
> > gst-launch-1.0 \
> > 	udpsrc port=8000 ! \
> > 	application/x-rtp,encoding-name=JPEG,payload=26 ! \
> > 	rtpjpegdepay ! \
> > 	jpegdec ! \
> > 	video/x-raw,pixelformat=YUYV,size=640x480 ! \
> > 	queue ! \
> > 	pipewiresink mode=provide \
> > 	       stream-properties="properties,media.class=Video/Source,media.role=Camera" \
> > 	       client-name="Remote Camera"
> > 
> > Unfortunatley this doesn't work, when the pipewire client connects to
> > the stream on the receiver side I get
> > 
> > ERROR: from element /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0: stream error: no more input formats
> > 
> > Nicolas, would you have any wisdom to share about this and tell me if I
> > did something dumb ? :-) There's no hurry.
> 
> Just to share my current state of affairs:
> 
> * sender:
> 
> $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! video/x-h264, width=1280, height=720, framerate=24/1 ! rtph264pay ! rtpstreampay ! udpsink host=<receiver_host> port=<receiver_port>
> 
> * receiver:
> 
> $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! application/x-rtp-stream,encoding-name=H264 ! rtpstreamdepay ! application/x-rtp,encoding-name=H264 ! rtph264depay ! h264parse ! openh264dec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
> 
> I chose H.264 because of much lower (tenfold) traffic comparing to
> MJPEG, wrapped this into RTP, opted in for OpenH264 decoder because I
> read it was handling low latency streams better than avdec_h264, and
> tested this setup with both Firefox and Chromium, and it actually
> worked pretty reliably, so I'm impressed now.

Thank you for the update. I'll give this a try.

> The only issue I have with this thing is that once a tab with meeting
> in the browser is closed, the whole receiver pipeline stops gracefully
> because "PipeWire link to remote node was destroyed". I didn't find a
> way to tell the pipeline to just restart, so in fact I had to wrap it
> into a Python script with Gst.parse_launch() and friends, and add
> error message parsing to restart the pipeline inside the script.

I've seen that too but didn't investigate yet.

> Leaving this in public, because it's a straightforward and potentially
> widely used setup, yet there's little to no info on how to do it
> properly, and the knowledge is scattered across random posts of
> varying age.

I'm writing a blog post on this topic, I'll reply with a link when I'll
be done.

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 3 days, 15 hours ago
On Tue, Feb 03, 2026 at 04:53:41PM +0200, Laurent Pinchart wrote:
> On Tue, Feb 03, 2026 at 03:38:06PM +0100, Oleksandr Natalenko wrote:
> > On úterý 3. února 2026 2:23:13, středoevropský standardní čas Laurent Pinchart wrote:
> > > On Mon, Feb 02, 2026 at 12:45:15PM +0100, Oleksandr Natalenko wrote:
> > > > On pondělí 2. února 2026 12:40:12, středoevropský standardní čas Laurent Pinchart wrote:
> > > > > > If I understand correctly, it would be more forward-thinking to develop
> > > > > > virtual camera support in PipeWire rather than in the kernel.
> > > > > 
> > > > > I don't think there's even a need for development in PipeWire
> > > > > 
> > > > > $ gst-launch-1.0 \
> > > > > 	videotestsrc ! \
> > > > > 	video/x-raw,format=YUY2 ! \
> > > > > 	pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera"
> > > > > 
> > > > > This gives me a virtual camera in Firefox. Extending the GStreamer
> > > > > pipeline to get the video stream from the network should be quite
> > > > > trivial.
> > > > 
> > > > So far, I came up with this:
> > > > 
> > > > * sender:
> > > > 
> > > > $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! image/jpeg, width=1280, height=720, framerate=24/1 ! rndbuffersize max=1400 ! udpsink host=<receiver_host> port=<receiver_port>
> > > >
> > > > * receiver:
> > > > 
> > > > $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! image/jpeg, width=1280, height=720, framerate=24/1 ! jpegparse ! jpegdec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
> > > >
> > > > Please let me know if I do something dumb here. Trial and error to
> > > > make this work took a couple of hours for me, but it seems to provide
> > > > what I need.
> > > 
> > > There's nothing dumb at all, especially given that it works :-) I have
> > > been able to reproduce it locally (using a different pipeline on the
> > > sender side).
> > > 
> > > I compared your pipelines with another JPEG-over-UDP setup I used a
> > > while ago, which used an rtpjpegpay element before udpsink on the sender
> > > side to encapsulate the payload in RTP packets, and an rtpjpegdepay
> > > element on the receiver side after udpsrc. This helps the receiver
> > > synchronize with the sender if the sender is started first. The full
> > > pipelines are
> > > 
> > > * Sender:
> > > 
> > > gst-launch-1.0 \
> > > 	v4l2src ! \
> > > 	video/x-raw,pixelformat=YUYV,size=640x480 ! \
> > > 	jpegenc ! \
> > > 	rtpjpegpay ! \
> > > 	udpsink host=192.168.10.200 port=8000
> > > 
> > > * Receiver:
> > > 
> > > gst-launch-1.0 \
> > > 	udpsrc port=8000 ! \
> > > 	application/x-rtp,encoding-name=JPEG,payload=26 ! \
> > > 	rtpjpegdepay ! \
> > > 	jpegdec ! \
> > > 	video/x-raw,pixelformat=YUYV,size=640x480 ! \
> > > 	queue ! \
> > > 	pipewiresink mode=provide \
> > > 	       stream-properties="properties,media.class=Video/Source,media.role=Camera" \
> > > 	       client-name="Remote Camera"
> > > 
> > > Unfortunatley this doesn't work, when the pipewire client connects to
> > > the stream on the receiver side I get
> > > 
> > > ERROR: from element /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0: stream error: no more input formats
> > > 
> > > Nicolas, would you have any wisdom to share about this and tell me if I
> > > did something dumb ? :-) There's no hurry.
> > 
> > Just to share my current state of affairs:
> > 
> > * sender:
> > 
> > $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! video/x-h264, width=1280, height=720, framerate=24/1 ! rtph264pay ! rtpstreampay ! udpsink host=<receiver_host> port=<receiver_port>
> > 
> > * receiver:
> > 
> > $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! application/x-rtp-stream,encoding-name=H264 ! rtpstreamdepay ! application/x-rtp,encoding-name=H264 ! rtph264depay ! h264parse ! openh264dec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
> > 
> > I chose H.264 because of much lower (tenfold) traffic comparing to
> > MJPEG, wrapped this into RTP, opted in for OpenH264 decoder because I
> > read it was handling low latency streams better than avdec_h264, and
> > tested this setup with both Firefox and Chromium, and it actually
> > worked pretty reliably, so I'm impressed now.
> 
> Thank you for the update. I'll give this a try.
> 
> > The only issue I have with this thing is that once a tab with meeting
> > in the browser is closed, the whole receiver pipeline stops gracefully
> > because "PipeWire link to remote node was destroyed". I didn't find a
> > way to tell the pipeline to just restart, so in fact I had to wrap it
> > into a Python script with Gst.parse_launch() and friends, and add
> > error message parsing to restart the pipeline inside the script.
> 
> I've seen that too but didn't investigate yet.
> 
> > Leaving this in public, because it's a straightforward and potentially
> > widely used setup, yet there's little to no info on how to do it
> > properly, and the knowledge is scattered across random posts of
> > varying age.
> 
> I'm writing a blog post on this topic, I'll reply with a link when I'll
> be done.

https://www.ideasonboard.com/news/pipewire-is-the-new-v4l2loopback/

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 3 days, 15 hours ago
On Tue, Feb 03, 2026 at 04:53:41PM +0200, Laurent Pinchart wrote:
> On Tue, Feb 03, 2026 at 03:38:06PM +0100, Oleksandr Natalenko wrote:
> > On úterý 3. února 2026 2:23:13, středoevropský standardní čas Laurent Pinchart wrote:
> > > Hi Oleksandr,
> > > 
> > > (Cc'ing Nicolas Dufresne)
> > > 
> > > On Mon, Feb 02, 2026 at 12:45:15PM +0100, Oleksandr Natalenko wrote:
> > > > On pondělí 2. února 2026 12:40:12, středoevropský standardní čas Laurent Pinchart wrote:
> > > > > > If I understand correctly, it would be more forward-thinking to develop
> > > > > > virtual camera support in PipeWire rather than in the kernel.
> > > > > 
> > > > > I don't think there's even a need for development in PipeWire
> > > > > 
> > > > > $ gst-launch-1.0 \
> > > > > 	videotestsrc ! \
> > > > > 	video/x-raw,format=YUY2 ! \
> > > > > 	pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera"
> > > > > 
> > > > > This gives me a virtual camera in Firefox. Extending the GStreamer
> > > > > pipeline to get the video stream from the network should be quite
> > > > > trivial.
> > > > 
> > > > So far, I came up with this:
> > > > 
> > > > * sender:
> > > > 
> > > > $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! image/jpeg, width=1280, height=720, framerate=24/1 ! rndbuffersize max=1400 ! udpsink host=<receiver_host> port=<receiver_port>
> > > >
> > > > * receiver:
> > > > 
> > > > $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! image/jpeg, width=1280, height=720, framerate=24/1 ! jpegparse ! jpegdec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
> > > >
> > > > Please let me know if I do something dumb here. Trial and error to
> > > > make this work took a couple of hours for me, but it seems to provide
> > > > what I need.
> > > 
> > > There's nothing dumb at all, especially given that it works :-) I have
> > > been able to reproduce it locally (using a different pipeline on the
> > > sender side).
> > > 
> > > I compared your pipelines with another JPEG-over-UDP setup I used a
> > > while ago, which used an rtpjpegpay element before udpsink on the sender
> > > side to encapsulate the payload in RTP packets, and an rtpjpegdepay
> > > element on the receiver side after udpsrc. This helps the receiver
> > > synchronize with the sender if the sender is started first. The full
> > > pipelines are
> > > 
> > > * Sender:
> > > 
> > > gst-launch-1.0 \
> > > 	v4l2src ! \
> > > 	video/x-raw,pixelformat=YUYV,size=640x480 ! \
> > > 	jpegenc ! \
> > > 	rtpjpegpay ! \
> > > 	udpsink host=192.168.10.200 port=8000
> > > 
> > > * Receiver:
> > > 
> > > gst-launch-1.0 \
> > > 	udpsrc port=8000 ! \
> > > 	application/x-rtp,encoding-name=JPEG,payload=26 ! \
> > > 	rtpjpegdepay ! \
> > > 	jpegdec ! \
> > > 	video/x-raw,pixelformat=YUYV,size=640x480 ! \
> > > 	queue ! \
> > > 	pipewiresink mode=provide \
> > > 	       stream-properties="properties,media.class=Video/Source,media.role=Camera" \
> > > 	       client-name="Remote Camera"
> > > 
> > > Unfortunatley this doesn't work, when the pipewire client connects to
> > > the stream on the receiver side I get
> > > 
> > > ERROR: from element /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0: stream error: no more input formats
> > > 
> > > Nicolas, would you have any wisdom to share about this and tell me if I
> > > did something dumb ? :-) There's no hurry.
> > 
> > Just to share my current state of affairs:
> > 
> > * sender:
> > 
> > $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! video/x-h264, width=1280, height=720, framerate=24/1 ! rtph264pay ! rtpstreampay ! udpsink host=<receiver_host> port=<receiver_port>
> > 
> > * receiver:
> > 
> > $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! application/x-rtp-stream,encoding-name=H264 ! rtpstreamdepay ! application/x-rtp,encoding-name=H264 ! rtph264depay ! h264parse ! openh264dec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
> > 
> > I chose H.264 because of much lower (tenfold) traffic comparing to
> > MJPEG, wrapped this into RTP, opted in for OpenH264 decoder because I
> > read it was handling low latency streams better than avdec_h264, and
> > tested this setup with both Firefox and Chromium, and it actually
> > worked pretty reliably, so I'm impressed now.
> 
> Thank you for the update. I'll give this a try.

I've tried those, but as soon as Firefox is connecting, I get

0:00:03.569028999 131465 0x7f85cc002030 DEBUG           pipewiresink gstpipewiresink.c:692:on_state_changed:<pipewiresink0> got stream state "error" (-1)
0:00:03.569060959 131465 0x7f85cc002030 DEBUG           pipewiresink gstpipewiresink.c:692:on_state_changed:<pipewiresink0> got stream state "error" (-1)
0:00:03.569070767 131465 0x7f85cc002030 WARN            pipewiresink gstpipewiresink.c:710:on_state_changed:<pipewiresink0> error: stream error: no more input formats
ERROR: from element /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0: stream error: no more input formats
Additional debug info:
../pipewire-1.4.9/src/gst/gstpipewiresink.c(710): on_state_changed (): /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0

Maybe I should try the pipewire master branch.

> > The only issue I have with this thing is that once a tab with meeting
> > in the browser is closed, the whole receiver pipeline stops gracefully
> > because "PipeWire link to remote node was destroyed". I didn't find a
> > way to tell the pipeline to just restart, so in fact I had to wrap it
> > into a Python script with Gst.parse_launch() and friends, and add
> > error message parsing to restart the pipeline inside the script.
> 
> I've seen that too but didn't investigate yet.
> 
> > Leaving this in public, because it's a straightforward and potentially
> > widely used setup, yet there's little to no info on how to do it
> > properly, and the knowledge is scattered across random posts of
> > varying age.
> 
> I'm writing a blog post on this topic, I'll reply with a link when I'll
> be done.

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Oleksandr Natalenko 3 days, 14 hours ago
On úterý 3. února 2026 21:36:48, středoevropský standardní čas Laurent Pinchart wrote:
> On Tue, Feb 03, 2026 at 04:53:41PM +0200, Laurent Pinchart wrote:
> > On Tue, Feb 03, 2026 at 03:38:06PM +0100, Oleksandr Natalenko wrote:
> > > 
> > > Just to share my current state of affairs:
> > > 
> > > * sender:
> > > 
> > > $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! video/x-h264, width=1280, height=720, framerate=24/1 ! rtph264pay ! rtpstreampay ! udpsink host=<receiver_host> port=<receiver_port>
> > > 
> > > * receiver:
> > > 
> > > $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! application/x-rtp-stream,encoding-name=H264 ! rtpstreamdepay ! application/x-rtp,encoding-name=H264 ! rtph264depay ! h264parse ! openh264dec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
> > > 
> > > I chose H.264 because of much lower (tenfold) traffic comparing to
> > > MJPEG, wrapped this into RTP, opted in for OpenH264 decoder because I
> > > read it was handling low latency streams better than avdec_h264, and
> > > tested this setup with both Firefox and Chromium, and it actually
> > > worked pretty reliably, so I'm impressed now.
> > 
> > Thank you for the update. I'll give this a try.
> 
> I've tried those, but as soon as Firefox is connecting, I get
> 
> 0:00:03.569028999 131465 0x7f85cc002030 DEBUG           pipewiresink gstpipewiresink.c:692:on_state_changed:<pipewiresink0> got stream state "error" (-1)
> 0:00:03.569060959 131465 0x7f85cc002030 DEBUG           pipewiresink gstpipewiresink.c:692:on_state_changed:<pipewiresink0> got stream state "error" (-1)
> 0:00:03.569070767 131465 0x7f85cc002030 WARN            pipewiresink gstpipewiresink.c:710:on_state_changed:<pipewiresink0> error: stream error: no more input formats
> ERROR: from element /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0: stream error: no more input formats
> Additional debug info:
> ../pipewire-1.4.9/src/gst/gstpipewiresink.c(710): on_state_changed (): /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0
> 
> Maybe I should try the pipewire master branch.

I'd be glad to help, but my understanding of this "stream error: no more input formats" boils down only to the absence of caps identifiers (like those "application/x-rtp-stream"blahblah), which is not the case here. Also, Firefox behaves differently from Chrome when using sites like webcamtests, and also when using google meet. I may speculate Firefox more eagerly closes link to a pipewire node, but I have no knowledge of this at all.

That said, "pretty reliably" doesn't mean without issues whatsoever, and I manage to crash both Chrome and Firefox from time to time while experimenting with pipelines. Probably because I occasionally unintentionally feed some crap into the pipewire sink.

FWIW, I'm using 1:1.4.10-2 pipewire packages from Arch.

-- 
Oleksandr Natalenko, MSE
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 3 days, 14 hours ago
On Tue, Feb 03, 2026 at 10:39:41PM +0100, Oleksandr Natalenko wrote:
> On úterý 3. února 2026 21:36:48, středoevropský standardní čas Laurent Pinchart wrote:
> > On Tue, Feb 03, 2026 at 04:53:41PM +0200, Laurent Pinchart wrote:
> > > On Tue, Feb 03, 2026 at 03:38:06PM +0100, Oleksandr Natalenko wrote:
> > > > 
> > > > Just to share my current state of affairs:
> > > > 
> > > > * sender:
> > > > 
> > > > $ gst-launch-1.0 pipewiresrc path=<webcam_id> ! video/x-h264, width=1280, height=720, framerate=24/1 ! rtph264pay ! rtpstreampay ! udpsink host=<receiver_host> port=<receiver_port>
> > > > 
> > > > * receiver:
> > > > 
> > > > $ gst-launch-1.0 udpsrc address=<receiver_host> port=<receiver_port> ! queue ! application/x-rtp-stream,encoding-name=H264 ! rtpstreamdepay ! application/x-rtp,encoding-name=H264 ! rtph264depay ! h264parse ! openh264dec ! pipewiresink mode=provide stream-properties="properties,media.class=Video/Source,media.role=Camera" client-name=VirtualCam
> > > > 
> > > > I chose H.264 because of much lower (tenfold) traffic comparing to
> > > > MJPEG, wrapped this into RTP, opted in for OpenH264 decoder because I
> > > > read it was handling low latency streams better than avdec_h264, and
> > > > tested this setup with both Firefox and Chromium, and it actually
> > > > worked pretty reliably, so I'm impressed now.
> > > 
> > > Thank you for the update. I'll give this a try.
> > 
> > I've tried those, but as soon as Firefox is connecting, I get
> > 
> > 0:00:03.569028999 131465 0x7f85cc002030 DEBUG           pipewiresink gstpipewiresink.c:692:on_state_changed:<pipewiresink0> got stream state "error" (-1)
> > 0:00:03.569060959 131465 0x7f85cc002030 DEBUG           pipewiresink gstpipewiresink.c:692:on_state_changed:<pipewiresink0> got stream state "error" (-1)
> > 0:00:03.569070767 131465 0x7f85cc002030 WARN            pipewiresink gstpipewiresink.c:710:on_state_changed:<pipewiresink0> error: stream error: no more input formats
> > ERROR: from element /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0: stream error: no more input formats
> > Additional debug info:
> > ../pipewire-1.4.9/src/gst/gstpipewiresink.c(710): on_state_changed (): /GstPipeline:pipeline0/GstPipeWireSink:pipewiresink0
> > 
> > Maybe I should try the pipewire master branch.
> 
> I'd be glad to help, but my understanding of this "stream error: no
> more input formats" boils down only to the absence of caps identifiers
> (like those "application/x-rtp-stream"blahblah), which is not the case
> here. Also, Firefox behaves differently from Chrome when using sites
> like webcamtests, and also when using google meet. I may speculate
> Firefox more eagerly closes link to a pipewire node, but I have no
> knowledge of this at all.

No worries. I'll try to find time to investigate.

> That said, "pretty reliably" doesn't mean without issues whatsoever,
> and I manage to crash both Chrome and Firefox from time to time while
> experimenting with pipelines. Probably because I occasionally
> unintentionally feed some crap into the pipewire sink.

I wouldn't dare claiming it's perfect :-) But the projects are actively
developed, so I have good hopes issues will be fixed quickly (as long as
they're reported of course).

> FWIW, I'm using 1:1.4.10-2 pipewire packages from Arch.

I'm running 1.4.9 from Gentoo, I think I should upgrade.

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Jarkko Sakkinen 5 days, 2 hours ago
On Mon, Feb 02, 2026 at 02:02:14AM +0100, Mauro Carvalho Chehab wrote:
> On Mon, 02 Feb 2026 00:25:37 +0100
> Oleksandr Natalenko <oleksandr@natalenko.name> wrote:
> 
> > Date: Mon, 02 Feb 2026 00:25:37 +0100
> > From: Oleksandr Natalenko <oleksandr@natalenko.name>
> > To: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
> > Cc: Laurent Pinchart <laurent.pinchart@ideasonboard.com>, Jarkko Sakkinen <jarkko@kernel.org>, linux-media@vger.kernel.org, jani.nikula@linux.intel.com, anisse@astier.eu, Mauro Carvalho Chehab <mchehab@kernel.org>, Hans Verkuil <hverkuil@kernel.org>, Sakari Ailus <sakari.ailus@linux.intel.com>, Jacopo Mondi <jacopo.mondi@ideasonboard.com>, Ricardo Ribalda <ribalda@chromium.org>, open list <linux-kernel@vger.kernel.org>
> > Message-ID: <13939245.uLZWGnKmhe@natalenko.name>
> > 
> > On pondělí 2. února 2026 0:17:20, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > > No. At the machine with the camera, obs can read from a V4L2 input,
> > > generate a mpeg TS stream, and listen to a UDP port (for instance). 
> > > 
> > > At the remote machine, you can just pass the URL to ffmpeg.  
> > 
> > I can't, I have to feed the stream into Firefox somehow for it to see the stream as a virtual webcam.
> 
> Motioneye could be used on such scenario. It has a proper web
> interface, allows multiple users to watch, has login control accepts
> multiple cameras.

When proposed workarounds move in the area of motion detection systems
it feels like there was a competion who invents the most impractical
solution for a practical real-world problem out in the wild.

> 
> Thanks,
> Mauro

BR, Jarkko
Re: [RFC PATCH] media: Virtual camera driver
Posted by Jarkko Sakkinen 5 days, 2 hours ago
On Mon, Feb 02, 2026 at 11:05:52AM +0200, Jarkko Sakkinen wrote:
> On Mon, Feb 02, 2026 at 02:02:14AM +0100, Mauro Carvalho Chehab wrote:
> > On Mon, 02 Feb 2026 00:25:37 +0100
> > Oleksandr Natalenko <oleksandr@natalenko.name> wrote:
> > 
> > > Date: Mon, 02 Feb 2026 00:25:37 +0100
> > > From: Oleksandr Natalenko <oleksandr@natalenko.name>
> > > To: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
> > > Cc: Laurent Pinchart <laurent.pinchart@ideasonboard.com>, Jarkko Sakkinen <jarkko@kernel.org>, linux-media@vger.kernel.org, jani.nikula@linux.intel.com, anisse@astier.eu, Mauro Carvalho Chehab <mchehab@kernel.org>, Hans Verkuil <hverkuil@kernel.org>, Sakari Ailus <sakari.ailus@linux.intel.com>, Jacopo Mondi <jacopo.mondi@ideasonboard.com>, Ricardo Ribalda <ribalda@chromium.org>, open list <linux-kernel@vger.kernel.org>
> > > Message-ID: <13939245.uLZWGnKmhe@natalenko.name>
> > > 
> > > On pondělí 2. února 2026 0:17:20, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > > > No. At the machine with the camera, obs can read from a V4L2 input,
> > > > generate a mpeg TS stream, and listen to a UDP port (for instance). 
> > > > 
> > > > At the remote machine, you can just pass the URL to ffmpeg.  
> > > 
> > > I can't, I have to feed the stream into Firefox somehow for it to see the stream as a virtual webcam.
> > 
> > Motioneye could be used on such scenario. It has a proper web
> > interface, allows multiple users to watch, has login control accepts
> > multiple cameras.
> 
> When proposed workarounds move in the area of motion detection systems
> it feels like there was a competion who invents the most impractical
> solution for a practical real-world problem out in the wild.

A trivial Google search shows that the scope of the issue, which is
also (on emphasis) a security issue, and net effect that is caused
of not addressing it properly.

> 
> > 
> > Thanks,
> > Mauro


BR, Jarkko
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 5 days ago
On Mon, Feb 02, 2026 at 11:19:03AM +0200, Jarkko Sakkinen wrote:
> On Mon, Feb 02, 2026 at 11:05:52AM +0200, Jarkko Sakkinen wrote:
> > On Mon, Feb 02, 2026 at 02:02:14AM +0100, Mauro Carvalho Chehab wrote:
> > > On Mon, 02 Feb 2026 00:25:37 +0100 Oleksandr Natalenko wrote:
> > > > On pondělí 2. února 2026 0:17:20, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > > > > No. At the machine with the camera, obs can read from a V4L2 input,
> > > > > generate a mpeg TS stream, and listen to a UDP port (for instance). 
> > > > > 
> > > > > At the remote machine, you can just pass the URL to ffmpeg.  
> > > > 
> > > > I can't, I have to feed the stream into Firefox somehow for it to see the stream as a virtual webcam.
> > > 
> > > Motioneye could be used on such scenario. It has a proper web
> > > interface, allows multiple users to watch, has login control accepts
> > > multiple cameras.
> > 
> > When proposed workarounds move in the area of motion detection systems
> > it feels like there was a competion who invents the most impractical
> > solution for a practical real-world problem out in the wild.

I'm also not quite sure how motioneye would be related. Please see my
reply to Gergo in this mail thread for a proposed solution that is (in
my opinion) not a workaround.

> A trivial Google search shows that the scope of the issue, which is
> also (on emphasis) a security issue, and net effect that is caused
> of not addressing it properly.

How is it a security issue ?

-- 
Regards,

Laurent Pinchart
Re: [RFC PATCH] media: Virtual camera driver
Posted by Oleksandr Natalenko 5 days, 4 hours ago
On pondělí 2. února 2026 2:02:14, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > On pondělí 2. února 2026 0:17:20, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > > No. At the machine with the camera, obs can read from a V4L2 input,
> > > generate a mpeg TS stream, and listen to a UDP port (for instance). 
> > > 
> > > At the remote machine, you can just pass the URL to ffmpeg.  
> > 
> > I can't, I have to feed the stream into Firefox somehow for it to see the stream as a virtual webcam.
> 
> Motioneye could be used on such scenario. It has a proper web
> interface, allows multiple users to watch, has login control accepts
> multiple cameras.

I still don't get it how this will help with using a webcam from one machine in google meet on another machine, sorry.

-- 
Oleksandr Natalenko, MSE
Re: [RFC PATCH] media: Virtual camera driver
Posted by Laurent Pinchart 5 days ago
On Mon, Feb 02, 2026 at 08:16:36AM +0100, Oleksandr Natalenko wrote:
> On pondělí 2. února 2026 2:02:14, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > > On pondělí 2. února 2026 0:17:20, středoevropský standardní čas Mauro Carvalho Chehab wrote:
> > > > No. At the machine with the camera, obs can read from a V4L2 input,
> > > > generate a mpeg TS stream, and listen to a UDP port (for instance). 
> > > > 
> > > > At the remote machine, you can just pass the URL to ffmpeg.  
> > > 
> > > I can't, I have to feed the stream into Firefox somehow for it to
> > > see the stream as a virtual webcam.
> > 
> > Motioneye could be used on such scenario. It has a proper web
> > interface, allows multiple users to watch, has login control accepts
> > multiple cameras.
> 
> I still don't get it how this will help with using a webcam from one
> machine in google meet on another machine, sorry.

I don't get it either :-)

Please see my reply to Gergo in this mail thread for an example of how
to create a virtual video source for PipeWire using GStreamer.

-- 
Regards,

Laurent Pinchart