From nobody Sat Feb 7 13:41:11 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9F9924A3C; Sun, 1 Feb 2026 13:33:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1769952830; cv=none; b=XgS7qdNjKS308sI3BMiJvL4efxq7nG7zKItwsaKjLm9Csx01VRYQtrXzILVBbyZnivAM/vg+ajrTf8FtXbq7RBX3XR8oAB/qQ4J/FFkrojBEuZpJgsbE7vuobpVEG4CPI77FRI/X7NVaThjwKATfOTza7eehFcIEzYuA4jcCW3E= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1769952830; c=relaxed/simple; bh=tibKO0EUCxsq8kO2Af4wymXjFE4ndZkZmJaWlLe4aos=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=iFvCZjTcqgAbgWpGwz3+rxrb33aPam8JKkRIRyxAIEaBudziMhHkPKGlep1dC91BpacPcv6rDDC9UbO2QClDNf71/zofawP2Gb7Vl4stiBmrberm+0MgllKtEGgc/XcmnUx1FVkxhp7pxukM5O6ZvZfM7bZdU0dw/+knDJgSOD8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=ZDt3RFnZ; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="ZDt3RFnZ" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 54FC7C4CEF7; Sun, 1 Feb 2026 13:33:49 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1769952830; bh=tibKO0EUCxsq8kO2Af4wymXjFE4ndZkZmJaWlLe4aos=; h=From:To:Cc:Subject:Date:From; b=ZDt3RFnZoPfglRNrOqQ9IrXuLTN7IPyY/QDweV91EfNXLy8wOe+0Of5HizhoSRDMx 838xHnASFHtTaywhSZf5VBNfol2fGcaxE2Ugt0OFV2MXF1EfWHPhVI/EG8uUFSDODZ 4DAbknxrYqiL1pClhNK6ZB9+8Qiyz9uwDrD03299dMs3ussSoS8FQtwttw8+xKUAsn +Pc+aNhLvlUMOsgRFc40rdPrExpnxXAI2LYsz17Yuk0eGTUXuwCRYjARaVMcMhoIYO puaLNi9HC9YirH04rb5Lr2p8lDJC/2/KHR/zmZfjFx80ihRD5QPC3dKfbP2gB120hr w5EfGE3NAIncw== From: Jarkko Sakkinen To: linux-media@vger.kernel.org Cc: jani.nikula@linux.intel.com, anisse@astier.eu, oleksandr@natalenko.name, Jarkko Sakkinen , Mauro Carvalho Chehab , Hans Verkuil , Laurent Pinchart , Sakari Ailus , Jacopo Mondi , Ricardo Ribalda , linux-kernel@vger.kernel.org (open list) Subject: [RFC PATCH] media: Virtual camera driver Date: Sun, 1 Feb 2026 15:33:38 +0200 Message-ID: <20260201133342.335680-1-jarkko@kernel.org> X-Mailer: git-send-email 2.52.0 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 --- 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/Documentati= on/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 =20 =20 Digital TV drivers diff --git a/Documentation/driver-api/media/drivers/vcam.rst b/Documentatio= n/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 + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D +vcam: Virtual Camera Driver +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D + +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-* =20 +VCAM V4L2 DRIVER +M: Jarkko Sakkinen +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 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" =20 +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 =20 # 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) +=3D cec/ obj-y +=3D common/ platform/ pci/ usb/ mmc/ firewire/ spi/ test-drivers/ obj-$(CONFIG_VIDEO_DEV) +=3D radio/ =20 +obj-$(CONFIG_VCAM) +=3D 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 rewrit= e. + */ + +/** + * 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 associat= es + * output buffers described by &struct vcam_frame with DMA-BUF file descri= ptors + * supplied by the caller. This also keeps output buffers owned by the cal= ler, + * 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 wi= thout + * 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 mappab= le, + * the payload is copied into the capture buffer. When neither zero-copy n= or a + * CPU mapping is possible, the capture buffer completes with an error. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 =3D 0x01, + VCAM_FLAG_CREATING =3D 0x02, + VCAM_FLAG_READY =3D 0x04, +}; + +struct vcam_buf { + struct vb2_v4l2_buffer vb; + struct list_head list; + unsigned long flags; +}; + +enum vcam_buf_flags { + VCAM_BUF_FLAG_MAPPABLE =3D 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 =3D BIT(0), + VCAM_COMPRESSED =3D BIT(1), +}; + +struct vcam_format { + int fourcc; + int depth; + int flags; +}; + +const struct vcam_format vcam_formats[] =3D { + { + .fourcc =3D V4L2_PIX_FMT_YUYV, + .depth =3D 16, + .flags =3D 0, + }, + { + .fourcc =3D V4L2_PIX_FMT_NV12, + .depth =3D 12, + .flags =3D VCAM_PLANAR, + }, + { + .fourcc =3D V4L2_PIX_FMT_MJPEG, + .depth =3D 32, + .flags =3D 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 =3D 0; i < VCAM_NR_FORMATS; i++) { + if (vcam_formats[i].fourcc =3D=3D 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] =3D (format >> 0) & 0xFF; + dst[1] =3D (format >> 8) & 0xFF; + dst[2] =3D (format >> 16) & 0xFF; + dst[3] =3D (format >> 24) & 0xFF; + dst[4] =3D '\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 =3D (dev->status & mask) =3D=3D 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 =3D dev->status; + if (on) { + dev->status &=3D ~VCAM_STATUS_IDLE; + dev->status |=3D VCAM_STATUS_STREAMING; + } else { + dev->status &=3D ~VCAM_STATUS_STREAMING; + dev->status |=3D VCAM_STATUS_IDLE; + } + new_flags =3D dev->status; + spin_unlock_irqrestore(&dev->status_lock, flags); + + if (new_flags !=3D 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 =3D 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 =3D (u64)tpf->numerator * VCAM_FPS_MIN; + u64 max_den =3D (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 =3D=3D dest->width && src->height =3D=3D dest->height && + src->pixelformat =3D=3D dest->pixelformat; +} + +static int vcam_set_format(struct vcam *dev, struct v4l2_format *fmt) +{ + struct v4l2_pix_format *pix =3D &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 =3D VCAM_DEFAULT_WIDTH; + if (!pix->height) + pix->height =3D VCAM_DEFAULT_HEIGHT; + + pix->width =3D clamp(pix->width, VCAM_MIN_WIDTH, VCAM_MAX_WIDTH); + pix->height =3D clamp(pix->height, VCAM_MIN_HEIGHT, VCAM_MAX_HEIGHT); + + format =3D vcam_find_format(pix->pixelformat); + if (!format) { + format =3D &vcam_formats[0]; + pix->pixelformat =3D format->fourcc; + } + + if (format->flags & VCAM_PLANAR) { + pix->bytesperline =3D pix->width; + sizeimage =3D ((u64)pix->width * pix->height * format->depth) >> + 3; + } else if (format->flags & VCAM_COMPRESSED) { + pix->bytesperline =3D 0; + sizeimage =3D ((u64)pix->width * pix->height * format->depth) >> + 3; + } else { + bytesperline =3D ((u64)pix->width * format->depth) >> 3; + if (bytesperline > U32_MAX) + return -EOVERFLOW; + + pix->bytesperline =3D bytesperline; + sizeimage =3D (u64)pix->height * bytesperline; + } + + if (sizeimage > U32_MAX) + return -EOVERFLOW; + + pix->sizeimage =3D sizeimage; + + if (pix->colorspace =3D=3D V4L2_COLORSPACE_DEFAULT || + pix->colorspace > V4L2_COLORSPACE_DCI_P3) + pix->colorspace =3D V4L2_COLORSPACE_SRGB; + if (pix->field =3D=3D V4L2_FIELD_ANY) + pix->field =3D V4L2_FIELD_NONE; + + return 0; +} + +static int vcam_vidioc_querycap(struct file *file, void *priv, + struct v4l2_capability *cap) +{ + __u32 capabilities =3D V4L2_CAP_STREAMING | V4L2_CAP_VIDEO_CAPTURE; + struct vcam *dev =3D video_drvdata(file); + + cap->device_caps =3D capabilities; + cap->capabilities =3D 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 !=3D dev->pix_format.pixelformat) + return -EINVAL; + + argp->type =3D V4L2_FRMSIZE_TYPE_DISCRETE; + + argp->discrete.width =3D dev->pix_format.width; + argp->discrete.height =3D dev->pix_format.height; + } else { + if (!vcam_find_format(argp->pixel_format)) + return -EINVAL; + + argp->type =3D V4L2_FRMSIZE_TYPE_CONTINUOUS; + + argp->stepwise.min_width =3D VCAM_MIN_WIDTH; + argp->stepwise.min_height =3D VCAM_MIN_HEIGHT; + argp->stepwise.max_width =3D VCAM_MAX_WIDTH; + argp->stepwise.max_height =3D VCAM_MAX_HEIGHT; + argp->stepwise.step_width =3D 1; + argp->stepwise.step_height =3D 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 !=3D dev->pix_format.width || + argp->height !=3D dev->pix_format.height || + argp->pixel_format !=3D dev->pix_format.pixelformat) + return -EINVAL; + + argp->type =3D V4L2_FRMIVAL_TYPE_DISCRETE; + argp->discrete =3D 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 =3D V4L2_FRMIVAL_TYPE_CONTINUOUS; + argp->stepwise.min.numerator =3D 1; + argp->stepwise.min.denominator =3D VCAM_FPS_MAX; + argp->stepwise.max.numerator =3D 1; + argp->stepwise.max.denominator =3D VCAM_FPS_MIN; + argp->stepwise.step.numerator =3D 1; + argp->stepwise.step.denominator =3D 1; + } + + return 0; +} + +static int vcam_vidioc_enum_framesizes(struct file *file, void *fh, + struct v4l2_frmsizeenum *argp) +{ + struct vcam *dev =3D 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 =3D 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 =3D video_drvdata(file); + + if (vcam_is_streaming(dev)) { + const __u32 format =3D dev->pix_format.pixelformat; + + if (f->index) + return -EINVAL; + + f->pixelformat =3D dev->pix_format.pixelformat; + vcam_fmt_descr(f->description, sizeof(f->description), format); + } else { + if (f->index >=3D VCAM_NR_FORMATS) + return -EINVAL; + + f->pixelformat =3D vcam_formats[f->index].fourcc; + vcam_fmt_descr(f->description, sizeof(f->description), + f->pixelformat); + } + f->flags =3D 0; + return 0; +} + +static int vcam_vidioc_g_fmt_vid_cap(struct file *file, void *priv, + struct v4l2_format *fmt) +{ + struct vcam *dev; + + dev =3D video_drvdata(file); + + fmt->fmt.pix =3D 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 =3D 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 =3D 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 =3D video_drvdata(file); + struct v4l2_format try_fmt =3D *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 =3D dev->pix_format; + } + + ret =3D 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 =3D try_fmt.fmt.pix; + *fmt =3D try_fmt; + return 0; +} + +static int vcam_ioc_reqbufs(struct file *file, struct vcam *dev, + struct v4l2_requestbuffers *req) +{ + int ret =3D 0; + + if (req->type !=3D V4L2_BUF_TYPE_VIDEO_OUTPUT) + return -EINVAL; + + scoped_guard(mutex, &dev->lock) + { + if (vb2_queue_is_busy(&dev->output_queue, file)) { + ret =3D -EBUSY; + break; + } + + ret =3D vb2_reqbufs(&dev->output_queue, req); + if (!ret) + dev->output_queue.owner =3D + 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 =3D 0; + + if (buf->type !=3D V4L2_BUF_TYPE_VIDEO_OUTPUT) + return -EINVAL; + + scoped_guard(mutex, &dev->lock) + ret =3D vb2_querybuf(&dev->output_queue, buf); + + return ret; +} + +static ssize_t formats_show(struct device *dev, struct device_attribute *a= ttr, + 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 =3D + min_t(unsigned int, VCAM_NR_FORMATS, VCAM_MAX_FORMATS); + size_t len =3D 0; + unsigned int i, j; + + for (i =3D 0; i < count; i++) { + formats[i].fourcc =3D vcam_formats[i].fourcc; + vcam_fourcc_str(formats[i].name, formats[i].fourcc); + } + + for (i =3D 1; i < count; i++) { + for (j =3D i; j > 0; j--) { + if (strcmp(formats[j - 1].name, formats[j].name) <=3D 0) + break; + tmp =3D formats[j - 1]; + formats[j - 1] =3D formats[j]; + formats[j] =3D tmp; + } + } + + for (i =3D 0; i < count; i++) + len +=3D sysfs_emit_at(buf, len, "%s%s", i ? " " : "", + formats[i].name); + + len +=3D 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[] =3D { + &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 =3D { + .attrs =3D vcam_attrs, +}; + +static const struct attribute_group *vcam_attr_groups[] =3D { + &vcam_attr_group, + NULL, +}; + +static int vcam_ioc_alloc(struct file *file, struct vcam *dev, u32 nr_fram= es, + void __user *frames_user, enum vb2_memory memory) +{ + struct v4l2_requestbuffers req =3D { + .type =3D V4L2_BUF_TYPE_VIDEO_OUTPUT, + .memory =3D memory, + }; + struct v4l2_buffer buf; + struct vcam_frame *frames =3D NULL; + unsigned int i; + int ret; + + if (memory =3D=3D VB2_MEMORY_DMABUF && + !dev->output_queue.mem_ops->attach_dmabuf) + return -EOPNOTSUPP; + + if (!frames_user) + return -EINVAL; + + if (nr_frames) { + frames =3D kcalloc(nr_frames, sizeof(*frames), GFP_KERNEL); + if (!frames) + return -ENOMEM; + } + + if (copy_from_user(frames, frames_user, nr_frames * sizeof(*frames))) { + ret =3D -EFAULT; + goto out_free; + } + + req.count =3D nr_frames; + ret =3D vcam_ioc_reqbufs(file, dev, &req); + if (ret) + goto out_free; + + if (req.count !=3D nr_frames) { + struct v4l2_requestbuffers req_free =3D { + .type =3D V4L2_BUF_TYPE_VIDEO_OUTPUT, + .memory =3D memory, + .count =3D 0, + }; + + vcam_ioc_reqbufs(file, dev, &req_free); + ret =3D -ENOMEM; + goto out_free; + } + + dev->output_memory =3D memory; + + for (i =3D 0; i < nr_frames; i++) { + memset(&buf, 0, sizeof(buf)); + buf.type =3D V4L2_BUF_TYPE_VIDEO_OUTPUT; + buf.memory =3D memory; + buf.index =3D i; + + ret =3D vcam_ioc_querybuf(file, dev, &buf); + if (ret) + goto out_free_reqbufs; + + frames[i].index =3D i; + frames[i].length =3D buf.length; + } + + if (copy_to_user(frames_user, frames, nr_frames * sizeof(*frames))) + ret =3D -EFAULT; + +out_free_reqbufs: + if (ret) { + struct v4l2_requestbuffers req_free =3D { + .type =3D V4L2_BUF_TYPE_VIDEO_OUTPUT, + .memory =3D memory, + .count =3D 0, + }; + + vcam_ioc_reqbufs(file, dev, &req_free); + dev->output_memory =3D 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 =3D { + .type =3D V4L2_BUF_TYPE_VIDEO_OUTPUT, + .memory =3D dev->output_memory, + .index =3D queue->index, + .bytesused =3D queue->length, + }; + u32 remainder; + int ret; + + if (queue->reserved) + return -EINVAL; + + if (dev->output_memory =3D=3D VB2_MEMORY_DMABUF) { + buf.m.fd =3D queue->fd; + buf.length =3D dev->pix_format.sizeimage; + } + + buf.timestamp.tv_sec =3D + div_u64_rem(queue->timestamp, NSEC_PER_SEC, &remainder); + buf.timestamp.tv_usec =3D remainder / NSEC_PER_USEC; + + scoped_guard(mutex, &dev->lock) + { + if (vb2_queue_is_busy(&dev->output_queue, file)) { + ret =3D -EBUSY; + break; + } + + if (vb2_is_streaming(&dev->capture_queue) && + !vb2_is_streaming(&dev->output_queue)) { + ret =3D vb2_streamon(&dev->output_queue, buf.type); + if (ret) + break; + } + + ret =3D 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 =3D { + .type =3D V4L2_BUF_TYPE_VIDEO_OUTPUT, + .memory =3D dev->output_memory, + }; + int ret; + + scoped_guard(mutex, &dev->lock) + { + if (vb2_queue_is_busy(&dev->output_queue, file)) { + ret =3D -EBUSY; + break; + } + + ret =3D vb2_dqbuf(&dev->output_queue, &buf, + file->f_flags & O_NONBLOCK); + } + if (ret) + return ret; + + queue->index =3D buf.index; + queue->length =3D buf.bytesused; + queue->timestamp =3D (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 =3D 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 =3D wait_event_interruptible(dev->status_waitq, + vcam_status_mask_ready(dev, wait->mask)); + if (ret) + return ret; + + wait->status =3D vcam_status_read(dev); + return 0; +} + +static long vcam_output_ioctl_core(struct file *file, unsigned int cmd, + void *arg) +{ + struct vcam *dev =3D file->private_data; + long ret =3D 0; + + switch (cmd) { + case VCAM_IOC_QUEUE: + ret =3D vcam_ioc_queue(file, dev, arg); + break; + case VCAM_IOC_DEQUEUE: + ret =3D vcam_ioc_dequeue(file, dev, arg); + break; + case VCAM_IOC_STATUS: + ret =3D vcam_ioc_status(dev, arg); + break; + case VCAM_IOC_WAIT: + ret =3D vcam_ioc_wait(dev, arg); + break; + default: + ret =3D -EOPNOTSUPP; + break; + } + + return ret; +} + +static long vcam_ioctl_common(struct file *file, unsigned int cmd, + unsigned long arg) +{ + void __user *argp =3D (void __user *)arg; + void *karg; + size_t size; + long ret; + + switch (cmd) { + case VCAM_IOC_QUEUE: + size =3D sizeof(struct vcam_ioc_queue); + break; + case VCAM_IOC_DEQUEUE: + size =3D sizeof(struct vcam_ioc_dequeue); + break; + case VCAM_IOC_STATUS: + size =3D sizeof(__u64); + break; + case VCAM_IOC_WAIT: + size =3D sizeof(struct vcam_ioc_wait); + break; + default: + return -ENOTTY; + } + + if (size > SZ_4K) + return -ENOTTY; + + karg =3D kzalloc(size, GFP_KERNEL); + if (!karg) + return -ENOMEM; + + if (copy_from_user(karg, argp, size)) { + ret =3D -EFAULT; + goto out_free; + } + + ret =3D vcam_output_ioctl_core(file, cmd, karg); + if (ret) + goto out_free; + + if (copy_to_user(argp, karg, size)) { + ret =3D -EFAULT; + goto out_free; + } + + ret =3D 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 =3D NULL; + dev->device_nr =3D -1; +} + +static void vcam_release(struct kref *ref) +{ + struct vcam *dev; + + dev =3D 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 =3D 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 =3D 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 =3D nonseekable_open(inode, file); + + if (ret) + return ret; + + dev =3D kzalloc(sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + + kref_init(&dev->ref); + dev->device_nr =3D -1; + file->private_data =3D dev; + return 0; +} + +static int vcam_close(struct inode *inode, struct file *file) +{ + struct vcam *dev =3D file->private_data; + int ret =3D 0; + + if (!dev) + return 0; + + if (test_bit(VCAM_FLAG_CREATING, &dev->flags) && dev->device_nr >=3D 0) + ret =3D __vcam_close(inode, file); + else + kref_put(&dev->ref, vcam_release); + + file->private_data =3D NULL; + return ret; +} + +static __poll_t vcam_poll(struct file *file, struct poll_table_struct *pts) +{ + struct vcam *dev =3D 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 =3D 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 !=3D V4L2_BUF_TYPE_VIDEO_CAPTURE) + return -EINVAL; + + dev =3D video_drvdata(file); + parm->parm.capture =3D dev->capture; + return 0; +} + +static int vcam_vidioc_s_parm(struct file *file, void *priv, + struct v4l2_streamparm *parm) +{ + struct v4l2_fract *tpf =3D &parm->parm.capture.timeperframe; + struct vcam *dev =3D video_drvdata(file); + + if (!vcam_tpf_valid(tpf)) + return -EINVAL; + + if (parm->type !=3D V4L2_BUF_TYPE_VIDEO_CAPTURE) + return -EINVAL; + + dev->capture.timeperframe =3D *tpf; + parm->parm.capture =3D dev->capture; + return 0; +} + +static int vcam_vidioc_enum_input(struct file *file, void *fh, + struct v4l2_input *inp) +{ + struct vcam *dev; + __u32 index =3D inp->index; + + if (index !=3D 0) + return -EINVAL; + + memset(inp, 0, sizeof(*inp)); + + inp->index =3D index; + strscpy(inp->name, "vcam", sizeof(inp->name)); + inp->type =3D V4L2_INPUT_TYPE_CAMERA; + inp->audioset =3D 0; + inp->tuner =3D 0; + inp->status =3D 0; + + dev =3D video_drvdata(file); + if (!vb2_is_streaming(&dev->output_queue)) + inp->status |=3D V4L2_IN_ST_NO_SIGNAL; + + return 0; +} + +static int vcam_vidioc_g_input(struct file *file, void *fh, unsigned int *= i) +{ + *i =3D 0; + return 0; +} + +static int vcam_vidioc_s_input(struct file *file, void *fh, unsigned int i) +{ + if (i =3D=3D 0) + return 0; + + return -EINVAL; +} + +static int vcam_vidioc_streamon(struct file *file, void *fh, + enum v4l2_buf_type type) +{ + struct vcam *dev =3D video_drvdata(file); + int ret; + + if (type !=3D V4L2_BUF_TYPE_VIDEO_CAPTURE) + return -EINVAL; + + if (vb2_queue_is_busy(&dev->capture_queue, file)) + return -EBUSY; + + ret =3D vb2_streamon(&dev->capture_queue, type); + if (ret) + return ret; + + if (vb2_get_num_buffers(&dev->output_queue)) { + ret =3D 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 =3D video_drvdata(file); + int ret; + + if (type !=3D V4L2_BUF_TYPE_VIDEO_CAPTURE) + return -EINVAL; + + if (vb2_queue_is_busy(&dev->capture_queue, file)) + return -EBUSY; + + ret =3D 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 =3D { + .vidioc_querycap =3D &vcam_vidioc_querycap, + .vidioc_enum_framesizes =3D &vcam_vidioc_enum_framesizes, + .vidioc_enum_frameintervals =3D &vcam_vidioc_enum_frameintervals, + .vidioc_enum_input =3D &vcam_vidioc_enum_input, + .vidioc_g_input =3D &vcam_vidioc_g_input, + .vidioc_s_input =3D &vcam_vidioc_s_input, + .vidioc_enum_fmt_vid_cap =3D &vcam_vidioc_enum_fmt_cap, + .vidioc_g_fmt_vid_cap =3D &vcam_vidioc_g_fmt_vid_cap, + .vidioc_s_fmt_vid_cap =3D &vcam_vidioc_s_fmt_vid_cap, + .vidioc_try_fmt_vid_cap =3D &vcam_vidioc_try_fmt_vid_cap, + .vidioc_g_parm =3D &vcam_vidioc_g_parm, + .vidioc_s_parm =3D &vcam_vidioc_s_parm, + + .vidioc_reqbufs =3D &vb2_ioctl_reqbufs, + .vidioc_create_bufs =3D &vb2_ioctl_create_bufs, + .vidioc_prepare_buf =3D &vb2_ioctl_prepare_buf, + .vidioc_querybuf =3D &vb2_ioctl_querybuf, + .vidioc_qbuf =3D &vb2_ioctl_qbuf, + .vidioc_dqbuf =3D &vb2_ioctl_dqbuf, + .vidioc_expbuf =3D &vb2_ioctl_expbuf, + .vidioc_streamon =3D &vcam_vidioc_streamon, + .vidioc_streamoff =3D &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 =3D &buf->vb.vb2_buf; + u32 sequence; + void *dst; + + dst =3D vb2_plane_vaddr(vb, 0); + if (!dst) + return VB2_BUF_STATE_ERROR; + + if (!src_len || src_len > dev->pix_format.sizeimage) + src_len =3D dev->pix_format.sizeimage; + + if (!src) + return VB2_BUF_STATE_ERROR; + + memcpy(dst, src, src_len); + + sequence =3D (u32)(atomic_inc_return(&dev->sequence) - 1); + + vb->timestamp =3D timestamp ? timestamp : ktime_get_ns(); + buf->vb.sequence =3D sequence; + buf->vb.field =3D 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 =3D &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 !=3D cap_vb->planes[0].dbuf) + return false; + + if (!bytesused) + bytesused =3D dev->pix_format.sizeimage; + if (bytesused > vb2_plane_size(cap_vb, 0)) + bytesused =3D vb2_plane_size(cap_vb, 0); + + sequence =3D (u32)(atomic_inc_return(&dev->sequence) - 1); + + cap_vb->timestamp =3D out_vb->timestamp ? out_vb->timestamp : + ktime_get_ns(); + cap_buf->vb.sequence =3D sequence; + cap_buf->vb.field =3D 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 =3D false; + + spin_lock_irqsave(&dev->frame_lock, flags); + if (!list_empty(&dev->output_list) && !list_empty(&dev->capture_list)) { + *out_buf =3D list_first_entry(&dev->output_list, struct vcam_buf, + list); + list_del(&(*out_buf)->list); + *cap_buf =3D list_first_entry(&dev->capture_list, struct vcam_buf, + list); + list_del(&(*cap_buf)->list); + dequeued =3D 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 =3D vcam_find_format(data->pix_format.pixelformat); + while (vcam_buf_pair_dequeue(data, &out_buf, &cap_buf)) { + cap_state =3D VB2_BUF_STATE_DONE; + vb =3D &out_buf->vb.vb2_buf; + bytesused =3D vb2_get_plane_payload(vb, 0); + if (!bytesused || bytesused > data->pix_format.sizeimage) + bytesused =3D data->pix_format.sizeimage; + + if (bytesused < data->pix_format.sizeimage && + (!format || !(format->flags & VCAM_COMPRESSED))) { + cap_state =3D VB2_BUF_STATE_ERROR; + goto out_done; + } + + zero_copy =3D 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 =3D VB2_BUF_STATE_ERROR; + goto out_done; + } + if (!zero_copy) { + src =3D vb2_plane_vaddr(vb, 0); + if (!src) { + cap_state =3D VB2_BUF_STATE_ERROR; + goto out_done; + } + + cap_state =3D 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 =3D=3D 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 =3D 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 =3D 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 =3D video_drvdata(file); + ret =3D _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 =3D { + .owner =3D THIS_MODULE, + .open =3D vcam_vdev_open, + .release =3D vcam_vdev_close, + .poll =3D vb2_fop_poll, + .mmap =3D vb2_fop_mmap, + .unlocked_ioctl =3D video_ioctl2, +}; + +static int vcam_ioc_create_validate(struct vcam_ioc_create *config, + char *card_label) +{ + long len, i; + + if (config->device_nr !=3D 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 =3D strncpy_from_user(card_label, + u64_to_user_ptr(config->device_name), + VCAM_CARD_LABEL_MAX); + if (len < 0) + return -EFAULT; + if (len >=3D VCAM_CARD_LABEL_MAX) + return -E2BIG; + if (!len) + return -EINVAL; + if (!isalnum((unsigned char)card_label[0])) + return -EINVAL; + for (i =3D 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 =3D vb2_get_drv_priv(queue); + unsigned int sizeimage =3D data->pix_format.sizeimage; + + if (!sizeimage) + return -EINVAL; + + if (*nr_buffers < VCAM_MIN_FRAMES) + *nr_buffers =3D VCAM_MIN_FRAMES; + + if (*nr_planes) + return sizes[0] < sizeimage ? -EINVAL : 0; + + *nr_planes =3D 1; + sizes[0] =3D sizeimage; + return 0; +} + +static int vcam_vb2_buf_prepare(struct vb2_buffer *vb) +{ + struct vcam *data =3D vb2_get_drv_priv(vb->vb2_queue); + struct vb2_v4l2_buffer *vbuf =3D to_vb2_v4l2_buffer(vb); + struct vcam_buf *buf =3D container_of(vbuf, struct vcam_buf, vb); + unsigned int sizeimage =3D data->pix_format.sizeimage; + unsigned int bytesused; + void *vaddr; + + if (vb2_plane_size(vb, 0) < sizeimage) + return -EINVAL; + + vbuf->field =3D data->pix_format.field; + bytesused =3D 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 =3D VCAM_BUF_FLAG_MAPPABLE; + if (vb->planes[0].dbuf) { + vaddr =3D vb2_plane_vaddr(vb, 0); + if (!vaddr) + buf->flags &=3D ~VCAM_BUF_FLAG_MAPPABLE; + } + return 0; +} + +static void vcam_vb2_buf_queue(struct vb2_buffer *vb) +{ + struct vcam *data =3D vb2_get_drv_priv(vb->vb2_queue); + struct vb2_v4l2_buffer *vbuf =3D to_vb2_v4l2_buffer(vb); + struct vcam_buf *buf; + unsigned long flags; + + buf =3D 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 cou= nt) +{ + struct vcam *data =3D 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 =3D 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 =3D { + .queue_setup =3D vcam_vb2_queue_setup, + .buf_queue =3D vcam_vb2_buf_queue, + .buf_prepare =3D vcam_vb2_buf_prepare, + .prepare_streaming =3D vcam_vb2_prepare_streaming, + .start_streaming =3D vcam_vb2_start_streaming, + .stop_streaming =3D 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 =3D v4l2_device_register(NULL, &dev->v4l2_dev); + if (ret) + return ret; + + vdev =3D video_device_alloc(); + if (!vdev) { + ret =3D -ENOMEM; + goto err_unregister; + } + + dev->vdev =3D vdev; + video_set_drvdata(vdev, dev); + memcpy(vdev->name, card_label, len); + vdev->name[len] =3D '\0'; + vdev->vfl_type =3D VFL_TYPE_VIDEO; + vdev->fops =3D &vcam_vdev_fops; + vdev->ioctl_ops =3D &vcam_ioctl_ops; + vdev->release =3D &video_device_release; + vdev->minor =3D -1; + vdev->device_caps =3D V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING; + vdev->vfl_dir =3D 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 =3D VCAM_STATUS_IDLE; + dev->output_memory =3D VB2_MEMORY_DMABUF; + init_waitqueue_head(&dev->status_waitq); + + dev->vdev->v4l2_dev =3D &dev->v4l2_dev; + dev->vdev->queue =3D &dev->capture_queue; + dev->vdev->lock =3D &dev->lock; + dev->capture.capability =3D 0; + dev->capture.capturemode =3D 0; + dev->capture.extendedmode =3D 0; + dev->capture.readbuffers =3D VCAM_MIN_FRAMES; + dev->capture.timeperframe.numerator =3D 1; + dev->capture.timeperframe.denominator =3D 30; + + if (!IS_ENABLED(CONFIG_DMA_SHARED_BUFFER) || + !vb2_vmalloc_memops.attach_dmabuf) { + ret =3D -EOPNOTSUPP; + goto err_unregister; + } + + fmt =3D (struct v4l2_format){ + .type =3D V4L2_BUF_TYPE_VIDEO_OUTPUT, + .fmt.pix =3D { .width =3D config->width, + .height =3D config->height, + .pixelformat =3D config->pixelformat, + .colorspace =3D config->colorspace, + .bytesperline =3D config->bytesperline, + .field =3D V4L2_FIELD_NONE } + }; + + try_fmt =3D fmt; + + ret =3D vcam_set_format(dev, &try_fmt); + if (ret) + goto err_unregister; + + if ((fmt.fmt.pix.width && try_fmt.fmt.pix.width !=3D fmt.fmt.pix.width) || + (fmt.fmt.pix.height && + try_fmt.fmt.pix.height !=3D fmt.fmt.pix.height) || + try_fmt.fmt.pix.pixelformat !=3D fmt.fmt.pix.pixelformat || + (fmt.fmt.pix.colorspace !=3D V4L2_COLORSPACE_DEFAULT && + try_fmt.fmt.pix.colorspace !=3D fmt.fmt.pix.colorspace) || + (fmt.fmt.pix.bytesperline && + try_fmt.fmt.pix.bytesperline !=3D fmt.fmt.pix.bytesperline)) { + ret =3D -EINVAL; + goto err_unregister; + } + + dev->pix_format =3D try_fmt.fmt.pix; + + queue =3D &dev->capture_queue; + queue->type =3D V4L2_BUF_TYPE_VIDEO_CAPTURE; + queue->io_modes =3D VB2_MMAP | VB2_USERPTR | VB2_DMABUF; + queue->timestamp_flags =3D V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC; + queue->drv_priv =3D dev; + queue->buf_struct_size =3D sizeof(struct vcam_buf); + queue->ops =3D &vcam_vb2_ops; + queue->mem_ops =3D &vb2_vmalloc_memops; + queue->lock =3D &dev->lock; + queue->dev =3D &dev->vdev->dev; + ret =3D vb2_queue_init(queue); + if (ret) + goto err_unregister; + + queue =3D &dev->output_queue; + queue->type =3D V4L2_BUF_TYPE_VIDEO_OUTPUT; + queue->io_modes =3D VB2_DMABUF; + queue->timestamp_flags =3D V4L2_BUF_FLAG_TIMESTAMP_COPY; + queue->drv_priv =3D dev; + queue->buf_struct_size =3D sizeof(struct vcam_buf); + queue->ops =3D &vcam_vb2_ops; + queue->mem_ops =3D &vb2_vmalloc_memops; + queue->lock =3D &dev->lock; + queue->dev =3D &dev->vdev->dev; + ret =3D vb2_queue_init(queue); + if (ret) + goto err_capture_queue; + + ret =3D vcam_ioc_alloc(file, dev, config->nr_frames, + u64_to_user_ptr(config->frames), + VB2_MEMORY_DMABUF); + if (ret) + goto err_output_queue; + + ret =3D video_register_device(dev->vdev, VFL_TYPE_VIDEO, -1); + if (ret < 0) + goto err_output_queue; + + config->device_nr =3D 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 =3D file->private_data; + char card_label[VCAM_CARD_LABEL_MAX]; + struct vcam_ioc_create config; + long ret, len; + + if (cmd !=3D 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 =3D -EINVAL; + goto err_clear; + } + + if (copy_from_user(&config, (void *)parm, sizeof(config))) { + ret =3D -EFAULT; + goto err_clear; + } + + len =3D vcam_ioc_create_validate(&config, card_label); + if (len < 0) { + ret =3D len; + goto err_clear; + } + + ret =3D vcam_ioc_create(file, dev, &config, card_label, len); + if (ret) + goto err_clear; + + if (copy_to_user((void *)parm, &config, sizeof(config))) { + ret =3D -EFAULT; + goto err_release; + } + + dev->device_nr =3D 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 =3D { + .owner =3D THIS_MODULE, + .open =3D vcam_open, + .unlocked_ioctl =3D vcam_ioctl, +#ifdef CONFIG_COMPAT + .compat_ioctl =3D vcam_ioctl, +#endif + .poll =3D vcam_poll, + .mmap =3D vcam_mmap, + .release =3D vcam_close, + .llseek =3D noop_llseek, +}; + +static struct miscdevice vcam_misc =3D { + .minor =3D MISC_DYNAMIC_MINOR, + .name =3D "vcam", + .fops =3D &vcam_fops, + .groups =3D 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 +#include + +#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_DEQU= EUE. + * 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 =3D 1U << 0, + VCAM_STATUS_STREAMING =3D 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 */ --=20 2.52.0