From 7888fc0b3d07151ddefe8e388c557554c3679745 Mon Sep 17 00:00:00 2001
From: Dmitry Osipenko <dmitry.osipenko@collabora.com>
Date: Sun, 28 Aug 2022 22:51:30 +0300
Subject: [PATCH] virtio: Add virtio-camera

Signed-off-by: Dmitry Osipenko <dmitry.osipenko@collabora.com>
---
 hw/virtio/Kconfig                             |   5 +
 hw/virtio/meson.build                         |   5 +
 hw/virtio/virtio-camera.c                     | 668 ++++++++++++++++++
 hw/virtio/virtio.c                            |   3 +-
 include/hw/virtio/virtio-camera.h             |  56 ++
 .../standard-headers/linux/virtio_camera.h    |  84 +++
 include/standard-headers/linux/virtio_ids.h   |   1 +
 meson.build                                   |   4 +
 8 files changed, 825 insertions(+), 1 deletion(-)
 create mode 100644 hw/virtio/virtio-camera.c
 create mode 100644 include/hw/virtio/virtio-camera.h
 create mode 100644 include/standard-headers/linux/virtio_camera.h

diff --git a/hw/virtio/Kconfig b/hw/virtio/Kconfig
index e9ecae1f50a..a966325a878 100644
--- a/hw/virtio/Kconfig
+++ b/hw/virtio/Kconfig
@@ -30,6 +30,11 @@ config VIRTIO_BALLOON
     default y
     depends on VIRTIO
 
+config VIRTIO_CAMERA
+    bool
+    default y
+    depends on VIRTIO
+
 config VIRTIO_CRYPTO
     bool
     default y
diff --git a/hw/virtio/meson.build b/hw/virtio/meson.build
index 7e8877fd64e..07b3e3c20c8 100644
--- a/hw/virtio/meson.build
+++ b/hw/virtio/meson.build
@@ -30,6 +30,11 @@ virtio_ss.add(when: 'CONFIG_VIRTIO_MEM', if_true: files('virtio-mem.c'))
 virtio_ss.add(when: 'CONFIG_VHOST_USER_I2C', if_true: files('vhost-user-i2c.c'))
 virtio_ss.add(when: 'CONFIG_VHOST_USER_RNG', if_true: files('vhost-user-rng.c'))
 
+if libv4l2.found()
+  virtio_ss.add(declare_dependency(dependencies: [libv4l2]))
+  virtio_ss.add(when: 'CONFIG_VIRTIO_CAMERA', if_true: files('virtio-camera.c'))
+endif
+
 virtio_pci_ss = ss.source_set()
 virtio_pci_ss.add(when: 'CONFIG_VHOST_VSOCK', if_true: files('vhost-vsock-pci.c'))
 virtio_pci_ss.add(when: 'CONFIG_VHOST_USER_VSOCK', if_true: files('vhost-user-vsock-pci.c'))
diff --git a/hw/virtio/virtio-camera.c b/hw/virtio/virtio-camera.c
new file mode 100644
index 00000000000..65fad97d0a7
--- /dev/null
+++ b/hw/virtio/virtio-camera.c
@@ -0,0 +1,668 @@
+/*
+ * Virtio camera Support
+ *
+  * Copyright © 2022 Collabora, Ltd.
+ *
+ * Authors:
+ *    Dmitry Osipenko <dmitry.osipenko@collabora.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 or
+ * (at your option) any later version.  See the COPYING file in the
+ * top-level directory.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/iov.h"
+#include "qemu/main-loop.h"
+#include "qemu/module.h"
+#include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "sysemu/dma.h"
+
+#include "hw/virtio/virtio.h"
+#include "hw/virtio/virtio-access.h"
+#include "hw/virtio/virtio-camera.h"
+#include "standard-headers/linux/virtio_ids.h"
+
+#include "hw/qdev-properties.h"
+
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+
+#include <linux/videodev2.h>
+#include <libv4l2.h>
+
+struct virtio_camera_test_command {
+	uint64_t command;
+};
+
+static int virtio_camera_v4l_ioctl(int fh, int request, void *arg)
+{
+    int ret;
+
+    do {
+        ret = ioctl(fh, request, arg);
+    } while (ret == -1 && (errno == EINTR || errno == EAGAIN));
+
+    return ret;
+}
+
+static void virtio_camera_destroy_v4l_buffers(VirtIOCamera *vcam)
+{
+    struct v4l2_requestbuffers req = {};
+    unsigned int i;
+
+    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    req.memory = V4L2_MEMORY_MMAP;
+    req.count = 0;
+
+    virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_REQBUFS, &req);
+
+    for (i = 0; i < VIRTIO_CAMERA_NUM_V4L2_BUFFERS; i++) {
+        if (!vcam->v4l2.buffer_data[i])
+            continue;
+
+        v4l2_munmap(vcam->v4l2.buffer_data[i], vcam->v4l2.buffer_size[i]);
+        vcam->v4l2.buffer_data[i] = NULL;
+        vcam->v4l2.buffer_size[i] = 0;
+    }
+}
+
+static int virtio_camera_alloc_v4l_buffers(VirtIOCamera *vcam)
+{
+    struct v4l2_requestbuffers req = {};
+    unsigned int i;
+    void *data;
+    int err;
+
+    req.count = VIRTIO_CAMERA_NUM_V4L2_BUFFERS;
+    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    req.memory = V4L2_MEMORY_MMAP;
+
+    err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_REQBUFS, &req);
+    if (err)
+        return err;
+
+    for (i = 0; i < VIRTIO_CAMERA_NUM_V4L2_BUFFERS; i++) {
+        struct v4l2_buffer buffer = {};
+
+        buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        buffer.memory = V4L2_MEMORY_MMAP;
+        buffer.index = i;
+
+        err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_QUERYBUF, &buffer);
+        if (err) {
+            virtio_camera_destroy_v4l_buffers(vcam);
+            return err;
+        }
+
+        data = v4l2_mmap(NULL, buffer.length, PROT_READ, MAP_SHARED,
+                         vcam->v4l2.fd, buffer.m.offset);
+        if (data == MAP_FAILED) {
+            virtio_camera_destroy_v4l_buffers(vcam);
+            return -ENOMEM;
+        }
+
+        vcam->v4l2.buffer_data[i] = data;
+        vcam->v4l2.buffer_size[i] = buffer.length;
+    }
+
+    return 0;
+}
+
+static int virtio_camera_enqueue_v4l_buffers(VirtIOCamera *vcam)
+{
+    unsigned int i;
+    int err;
+
+    for (i = 0; i < VIRTIO_CAMERA_NUM_V4L2_BUFFERS; i++) {
+        struct v4l2_buffer buffer = {};
+
+        buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        buffer.memory = V4L2_MEMORY_MMAP;
+        buffer.index = i;
+
+        err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_QBUF, &buffer);
+        if (err)
+            return err;
+    }
+
+    return 0;
+}
+
+static struct virtio_camera_mem_buffer *
+virtio_camera_mem_buf_by_uuid(VirtIOCamera *vcam, void *uuid)
+{
+    struct virtio_camera_mem_buffer *mem_buf;
+
+    QTAILQ_FOREACH(mem_buf, &vcam->buflist, node) {
+        if (!memcmp(mem_buf->uuid.data, uuid, sizeof(mem_buf->uuid.data)))
+            return mem_buf;
+    }
+
+    return NULL;
+}
+
+static void virtio_camera_push_resp(VirtIOCamera *vcam,
+                                    VirtQueueElement **elemp,
+                                    struct virtio_camera_op_ctrl_req *resp)
+{
+    VirtQueueElement *elem = *elemp;
+
+    if (iov_from_buf(elem->in_sg, elem->in_num, 0,
+                     resp, sizeof(*resp)) != sizeof(*resp)) {
+        virtio_error(VIRTIO_DEVICE(vcam), "virtio-camera invalid response size");
+        virtqueue_detach_element(vcam->ctrl_vq, elem, 0);
+    } else {
+        virtqueue_push(vcam->ctrl_vq, elem, sizeof(*resp));
+    }
+
+    virtio_notify(VIRTIO_DEVICE(vcam), vcam->ctrl_vq);
+
+    g_free(elem);
+    *elemp = NULL;
+}
+
+static void virtio_camera_v4l_event(void *opaque)
+{
+    struct virtio_camera_mem_buffer *mem_buf;
+    VirtIOCamera *vcam = opaque;
+    int err;
+
+    if (!vcam->streaming)
+        return;
+
+    while (true) {
+        struct virtio_camera_op_ctrl_req resp = {};
+        struct v4l2_buffer buffer = {};
+
+        buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        buffer.index = vcam->v4l2.queue_buffer_index;
+        buffer.memory = V4L2_MEMORY_MMAP;
+
+        err = ioctl(vcam->v4l2.fd, VIDIOC_DQBUF, &buffer);
+        if (err)
+            break;
+
+        mem_buf = QTAILQ_FIRST(&vcam->capturelist);
+        if (mem_buf) {
+            iov_from_buf(mem_buf->iov, mem_buf->num_entries, 0,
+                         vcam->v4l2.buffer_data[buffer.index],
+                         buffer.bytesused);
+
+            QTAILQ_REMOVE(&vcam->capturelist, mem_buf, capture_node);
+            resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_OK_NODATA);
+            virtio_camera_push_resp(vcam, &mem_buf->capture_elem, &resp);
+        }
+
+        virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_QBUF, &buffer);
+
+        vcam->v4l2.queue_buffer_index = (vcam->v4l2.queue_buffer_index + 1) %
+                                        VIRTIO_CAMERA_NUM_V4L2_BUFFERS;
+    }
+}
+
+static void virtio_camera_ctrl_handle(VirtIODevice *vdev, VirtQueue *vq)
+{
+    VirtIOCamera *vcam = VIRTIO_CAMERA(vdev);
+    VirtQueueElement *elem = NULL;
+
+    while (virtio_queue_ready(vq)) {
+        enum v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        struct virtio_camera_mem_buffer *mem_buf, *mem_buf_tmp;
+        struct virtio_camera_mem_entry *ents;
+        struct virtio_camera_op_ctrl_req ctrl = {};
+        struct virtio_camera_op_ctrl_req resp = {};
+        struct v4l2_frmsizeenum frmsize = {};
+        struct v4l2_fmtdesc format_desc = {};
+        struct v4l2_format format = {};
+        uint64_t num_entries, size;
+        int v4l_err = 0;
+        unsigned int i;
+        uint32_t cmd;
+        hwaddr hwlen;
+
+        elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
+        if (!elem)
+            break;
+
+        if (elem->out_num < 1 || elem->in_num < 1) {
+            virtio_error(vdev, "%s: ctrl missing headers", __func__);
+            virtqueue_detach_element(vq, elem, 0);
+            break;
+        }
+
+        if (iov_to_buf(elem->out_sg, elem->out_num, 0,
+                       &ctrl, sizeof(ctrl)) != sizeof(ctrl)) {
+            virtio_error(vdev, "%s: virtio-camera invalid control size", __func__);
+            virtqueue_detach_element(vq, elem, 0);
+            break;
+        }
+
+        format.type = buf_type;
+        format.fmt.pix.field = V4L2_FIELD_NONE;
+
+        cmd = le32_to_cpu(ctrl.header.cmd);
+        resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_OK_NODATA);
+
+        switch (cmd) {
+        case VIRTIO_CAMERA_CMD_GET_FORMAT:
+            v4l_err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_G_FMT, &format);
+            if (v4l_err)
+                break;
+
+            resp.u.format.size.width     = cpu_to_le32(format.fmt.pix.width);
+            resp.u.format.size.height    = cpu_to_le32(format.fmt.pix.height);
+            resp.u.format.size.stride    = cpu_to_le32(format.fmt.pix.bytesperline);
+            resp.u.format.pixelformat    = cpu_to_le32(format.fmt.pix.pixelformat);
+            resp.u.format.size.sizeimage = cpu_to_le32(format.fmt.pix.sizeimage);
+            break;
+
+        case VIRTIO_CAMERA_CMD_TRY_FORMAT:
+            format.fmt.pix.width        = le32_to_cpu(ctrl.u.format.size.width);
+            format.fmt.pix.height       = le32_to_cpu(ctrl.u.format.size.height);
+            format.fmt.pix.bytesperline = le32_to_cpu(ctrl.u.format.size.stride);
+            format.fmt.pix.pixelformat  = le32_to_cpu(ctrl.u.format.pixelformat);
+
+            v4l_err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_TRY_FMT, &format);
+            if (v4l_err)
+                break;
+
+            resp.u.format.size.width     = cpu_to_le32(format.fmt.pix.width);
+            resp.u.format.size.height    = cpu_to_le32(format.fmt.pix.height);
+            resp.u.format.size.stride    = cpu_to_le32(format.fmt.pix.bytesperline);
+            resp.u.format.pixelformat    = cpu_to_le32(format.fmt.pix.pixelformat);
+            resp.u.format.size.sizeimage = cpu_to_le32(format.fmt.pix.sizeimage);
+            break;
+
+        case VIRTIO_CAMERA_CMD_SET_FORMAT:
+            format.fmt.pix.width        = le32_to_cpu(ctrl.u.format.size.width);
+            format.fmt.pix.height       = le32_to_cpu(ctrl.u.format.size.height);
+            format.fmt.pix.bytesperline = le32_to_cpu(ctrl.u.format.size.stride);
+            format.fmt.pix.pixelformat  = le32_to_cpu(ctrl.u.format.pixelformat);
+
+            v4l_err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_S_FMT,
+                                              &format);
+            if (v4l_err)
+                break;
+
+            resp.u.format.size.width     = cpu_to_le32(format.fmt.pix.width);
+            resp.u.format.size.height    = cpu_to_le32(format.fmt.pix.height);
+            resp.u.format.size.stride    = cpu_to_le32(format.fmt.pix.bytesperline);
+            resp.u.format.pixelformat    = cpu_to_le32(format.fmt.pix.pixelformat);
+            resp.u.format.size.sizeimage = cpu_to_le32(format.fmt.pix.sizeimage);
+            break;
+
+        case VIRTIO_CAMERA_CMD_ENUM_FORMAT:
+            format_desc.index = le32_to_cpu(ctrl.header.index);
+            format_desc.type  = buf_type;
+
+            v4l_err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_ENUM_FMT,
+                                              &format_desc);
+            if (v4l_err)
+                break;
+
+            resp.u.format.pixelformat = cpu_to_le32(format_desc.pixelformat);
+            break;
+
+        case VIRTIO_CAMERA_CMD_ENUM_SIZE:
+            frmsize.index        = le32_to_cpu(ctrl.header.index);
+            frmsize.pixel_format = le32_to_cpu(ctrl.u.format.pixelformat);
+
+            v4l_err = virtio_camera_v4l_ioctl(vcam->v4l2.fd,
+                                              VIDIOC_ENUM_FRAMESIZES,
+                                              &frmsize);
+            if (v4l_err)
+                break;
+
+            switch (frmsize.type) {
+            case V4L2_FRMSIZE_TYPE_DISCRETE:
+                resp.u.format.size.min_width   = cpu_to_le32(frmsize.discrete.width);
+                resp.u.format.size.min_height  = cpu_to_le32(frmsize.discrete.height);
+                resp.u.format.size.max_width   = resp.u.format.size.min_width;
+                resp.u.format.size.max_height  = resp.u.format.size.min_height;
+                break;
+
+            case V4L2_FRMSIZE_TYPE_CONTINUOUS:
+                resp.u.format.size.min_width   = cpu_to_le32(frmsize.stepwise.min_width);
+                resp.u.format.size.min_height  = cpu_to_le32(frmsize.stepwise.min_height);
+                resp.u.format.size.max_width   = cpu_to_le32(frmsize.stepwise.max_width);
+                resp.u.format.size.max_height  = cpu_to_le32(frmsize.stepwise.max_height);
+                resp.u.format.size.step_width  = cpu_to_le32(1);
+                resp.u.format.size.step_height = cpu_to_le32(1);
+                break;
+
+            case V4L2_FRMSIZE_TYPE_STEPWISE:
+                resp.u.format.size.min_width   = cpu_to_le32(frmsize.stepwise.min_width);
+                resp.u.format.size.min_height  = cpu_to_le32(frmsize.stepwise.min_height);
+                resp.u.format.size.max_width   = cpu_to_le32(frmsize.stepwise.max_width);
+                resp.u.format.size.max_height  = cpu_to_le32(frmsize.stepwise.max_height);
+                resp.u.format.size.step_width  = cpu_to_le32(frmsize.stepwise.step_width);
+                resp.u.format.size.step_height = cpu_to_le32(frmsize.stepwise.step_height);
+                break;
+            }
+
+            break;
+
+        case VIRTIO_CAMERA_CMD_CREATE_BUFFER:
+            num_entries = le32_to_cpu(ctrl.u.buffer.num_entries);
+
+            ents = g_new(typeof(*ents), num_entries);
+            if (!ents) {
+                resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_OUT_OF_MEMORY);
+                break;
+            }
+
+            size = sizeof(*ents) * num_entries;
+            if (iov_to_buf(elem->out_sg, elem->out_num,
+                           sizeof(ctrl), ents, size) != size) {
+                resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_UNSPEC);
+                g_free(ents);
+                break;
+            }
+
+            size = sizeof(*mem_buf) + sizeof(struct iovec) * num_entries;
+            if (size > G_MAXSIZE) {
+                resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_OUT_OF_MEMORY);
+                g_free(ents);
+                break;
+            }
+
+            mem_buf = g_malloc0(size);
+            if (!mem_buf) {
+                resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_OUT_OF_MEMORY);
+                g_free(ents);
+                break;
+            }
+
+            for (i = 0; i < num_entries; i++) {
+                uint64_t addr   = le64_to_cpu(ents[i].addr);
+                uint32_t length = le32_to_cpu(ents[i].length);
+
+                hwlen = length;
+                mem_buf->iov[i].iov_base = dma_memory_map(vdev->dma_as, addr, &hwlen,
+                                                          DMA_DIRECTION_FROM_DEVICE,
+                                                          MEMTXATTRS_UNSPECIFIED);
+
+                if (!mem_buf->iov[i].iov_base) {
+                    while (i--)
+                        dma_memory_unmap(vdev->dma_as,
+                                         mem_buf->iov[i].iov_base,
+                                         mem_buf->iov[i].iov_len,
+                                         DMA_DIRECTION_FROM_DEVICE,
+                                         mem_buf->iov[i].iov_len);
+
+                    resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_UNSPEC);
+                    g_free(mem_buf);
+                    g_free(ents);
+                    break;
+                }
+
+                mem_buf->iov[i].iov_len = length;
+            }
+
+            g_free(ents);
+
+            mem_buf->num_entries = num_entries;
+            qemu_uuid_generate(&mem_buf->uuid);
+
+            QEMU_BUILD_BUG_ON(sizeof(mem_buf->uuid.data) !=
+                              sizeof(resp.u.buffer.uuid));
+
+            memcpy(resp.u.buffer.uuid, mem_buf->uuid.data,
+                   sizeof(mem_buf->uuid));
+
+            QTAILQ_INSERT_TAIL(&vcam->buflist, mem_buf, node);
+            break;
+
+        case VIRTIO_CAMERA_CMD_DESTROY_BUFFER:
+            mem_buf = virtio_camera_mem_buf_by_uuid(vcam, ctrl.u.buffer.uuid);
+            if (!mem_buf) {
+                resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_UNSPEC);
+                break;
+            }
+
+            if (mem_buf->capture_elem) {
+                QTAILQ_REMOVE(&vcam->capturelist, mem_buf, capture_node);
+                resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_UNSPEC);
+                virtio_camera_push_resp(vcam, &mem_buf->capture_elem, &resp);
+            }
+
+            while (mem_buf->num_entries--)
+                dma_memory_unmap(vdev->dma_as,
+                                 mem_buf->iov[mem_buf->num_entries].iov_base,
+                                 mem_buf->iov[mem_buf->num_entries].iov_len,
+                                 DMA_DIRECTION_FROM_DEVICE,
+                                 mem_buf->iov[mem_buf->num_entries].iov_len);
+
+            resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_OK_NODATA);
+            QTAILQ_REMOVE(&vcam->buflist, mem_buf, node);
+            g_free(mem_buf);
+            break;
+
+        case VIRTIO_CAMERA_CMD_ENQUEUE_BUFFER:
+            mem_buf = virtio_camera_mem_buf_by_uuid(vcam, ctrl.u.buffer.uuid);
+            if (!mem_buf) {
+                resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_UNSPEC);
+                break;
+            }
+
+            if (mem_buf->capture_elem) {
+                resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_BUSY);
+                break;
+            }
+
+            QTAILQ_INSERT_TAIL(&vcam->capturelist, mem_buf, capture_node);
+            mem_buf->capture_elem = elem;
+            elem = NULL;
+            break;
+
+        case VIRTIO_CAMERA_CMD_STREAM_ON:
+            if (vcam->streaming)
+                break;
+
+            v4l_err = virtio_camera_alloc_v4l_buffers(vcam);
+            if (v4l_err)
+                break;
+
+            v4l_err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_STREAMON,
+                                              &buf_type);
+            if (v4l_err) {
+                virtio_camera_destroy_v4l_buffers(vcam);
+                break;
+            }
+
+            v4l_err = virtio_camera_enqueue_v4l_buffers(vcam);
+            if (v4l_err) {
+                virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_STREAMOFF,
+                                        &buf_type);
+                virtio_camera_destroy_v4l_buffers(vcam);
+                break;
+            }
+
+            qemu_set_fd_handler(vcam->v4l2.fd, virtio_camera_v4l_event, NULL,
+                                vcam);
+
+            vcam->streaming = true;
+            break;
+
+        case VIRTIO_CAMERA_CMD_STREAM_OFF:
+            if (!vcam->streaming)
+                break;
+
+            v4l_err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_STREAMOFF,
+                                              &buf_type);
+            if (v4l_err)
+                break;
+
+            virtio_camera_destroy_v4l_buffers(vcam);
+
+            qemu_set_fd_handler(vcam->v4l2.fd, NULL, NULL, vcam);
+
+            QTAILQ_FOREACH_SAFE(mem_buf, &vcam->capturelist, capture_node,
+                                mem_buf_tmp) {
+                QTAILQ_REMOVE(&vcam->capturelist, mem_buf, capture_node);
+                resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_UNSPEC);
+                virtio_camera_push_resp(vcam, &mem_buf->capture_elem, &resp);
+            }
+
+            resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_OK_NODATA);
+            vcam->streaming = false;
+            break;
+
+        default:
+            virtio_error(vdev, "%s: invalid command: %u", __func__, cmd);
+            break;
+        }
+
+        if (v4l_err) {
+            fprintf(stderr, "%s: v4l2 error, command %u: %s\n",
+                    __func__, cmd, strerror(errno));
+
+            resp.header.cmd = cpu_to_le32(VIRTIO_CAMERA_CMD_RESP_ERR_UNSPEC);
+        }
+
+        if (elem)
+            virtio_camera_push_resp(vcam, &elem, &resp);
+    }
+
+    g_free(elem);
+}
+
+static int virtio_camera_v4l_init(VirtIOCamera *vcam, Error **errp)
+{
+    struct v4l2_format format = {};
+    struct v4l2_capability cap;
+    int err;
+
+    format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    vcam->v4l2.fd = v4l2_open(vcam->v4l2.dev_path, O_RDWR | O_NONBLOCK, 0);
+    if (vcam->v4l2.fd < 0) {
+        error_setg(errp, "failed to open V4L2 device: %s\n", strerror(errno));
+        return vcam->v4l2.fd;
+    }
+
+    /* whoever sets the format first gets the exclusive control */
+    err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_G_FMT, &format);
+    if (err) {
+        error_setg(errp, "failed to get V4L2 format: %s\n", strerror(errno));
+        return err;
+    }
+
+    /* take exclusive control over the camera device */
+    err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_S_FMT, &format);
+    if (err) {
+        error_setg(errp, "failed to set V4L2 format: %s\n", strerror(errno));
+        return err;
+    }
+
+    err = virtio_camera_v4l_ioctl(vcam->v4l2.fd, VIDIOC_QUERYCAP, &cap);
+    if (err) {
+        error_setg(errp, "failed to get V4L2 capability: %s\n", strerror(errno));
+        return err;
+    }
+
+    strncpy((char*)vcam->config.name, (char*)cap.card,
+            sizeof(vcam->config.name) - 1);
+
+    return 0;
+}
+
+static void virtio_camera_v4l_deinit(VirtIOCamera *vcam)
+{
+    close(vcam->v4l2.fd);
+}
+
+static void virtio_camera_device_realize(DeviceState *dev, Error **errp)
+{
+    VirtIODevice *vdev = VIRTIO_DEVICE(dev);
+    VirtIOCamera *vcam = VIRTIO_CAMERA(dev);
+    int err;
+
+    QTAILQ_INIT(&vcam->buflist);
+    QTAILQ_INIT(&vcam->capturelist);
+
+    err = virtio_camera_v4l_init(vcam, errp);
+    if (err)
+        return;
+
+    virtio_init(vdev, VIRTIO_ID_CAMERA, sizeof(vcam->config));
+    vcam->ctrl_vq = virtio_add_queue(vdev, 32, virtio_camera_ctrl_handle);
+}
+
+static void virtio_camera_device_unrealize(DeviceState *dev)
+{
+    VirtIODevice *vdev = VIRTIO_DEVICE(dev);
+    VirtIOCamera *vcam = VIRTIO_CAMERA(dev);
+
+    virtio_camera_v4l_deinit(vcam);
+    virtio_delete_queue(vcam->ctrl_vq);
+    virtio_cleanup(vdev);
+}
+
+static void virtio_camera_get_config(VirtIODevice *vdev, uint8_t *config)
+{
+    VirtIOCamera *vcam = VIRTIO_CAMERA(vdev);
+
+    memcpy(config, &vcam->config, sizeof(vcam->config));
+}
+
+static uint64_t virtio_camera_get_features(VirtIODevice *vdev,
+                                           uint64_t features,
+                                           Error **errp)
+{
+    return features;
+}
+
+static void virtio_camera_reset(VirtIODevice *vdev)
+{
+}
+
+static const VMStateDescription vmstate_virtio_camera = {
+    .name = "virtio-camera",
+    .unmigratable = 1,
+    .minimum_version_id = 1,
+    .version_id = 1,
+    .fields = (VMStateField[]) {
+        VMSTATE_VIRTIO_DEVICE,
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static Property virtio_camera_properties[] = {
+    DEFINE_PROP_STRING("v4ldev", VirtIOCamera, v4l2.dev_path),
+    DEFINE_PROP_END_OF_LIST(),
+};
+
+static void virtio_camera_class_init(ObjectClass *klass, void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);
+
+    dc->vmsd = &vmstate_virtio_camera;
+
+    vdc->realize = virtio_camera_device_realize;
+    vdc->unrealize = virtio_camera_device_unrealize;
+
+    vdc->get_config = virtio_camera_get_config;
+    vdc->get_features = virtio_camera_get_features;
+    vdc->reset = virtio_camera_reset;
+
+    device_class_set_props(dc, virtio_camera_properties);
+}
+
+static const TypeInfo virtio_camera_info = {
+    .name = TYPE_VIRTIO_CAMERA,
+    .parent = TYPE_VIRTIO_DEVICE,
+    .instance_size = sizeof(VirtIOCamera),
+    .class_init = virtio_camera_class_init,
+};
+module_obj(TYPE_VIRTIO_CAMERA);
+
+static void virtio_camera_register_types(void)
+{
+    type_register_static(&virtio_camera_info);
+}
+
+type_init(virtio_camera_register_types)
diff --git a/hw/virtio/virtio.c b/hw/virtio/virtio.c
index 5d607aeaa05..e782b6b0824 100644
--- a/hw/virtio/virtio.c
+++ b/hw/virtio/virtio.c
@@ -171,7 +171,8 @@ const char *virtio_device_names[] = {
     [VIRTIO_ID_PARAM_SERV] = "virtio-param-serv",
     [VIRTIO_ID_AUDIO_POLICY] = "virtio-audio-pol",
     [VIRTIO_ID_BT] = "virtio-bluetooth",
-    [VIRTIO_ID_GPIO] = "virtio-gpio"
+    [VIRTIO_ID_GPIO] = "virtio-gpio",
+    [VIRTIO_ID_CAMERA] = "virtio-camera",
 };
 
 static const char *virtio_id_to_name(uint16_t device_id)
diff --git a/include/hw/virtio/virtio-camera.h b/include/hw/virtio/virtio-camera.h
new file mode 100644
index 00000000000..f1fb93cc7f5
--- /dev/null
+++ b/include/hw/virtio/virtio-camera.h
@@ -0,0 +1,56 @@
+/*
+ * Virtio Camera Device
+ *
+ * Copyright © 2022 Collabora, Ltd.
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2.
+ * See the COPYING file in the top-level directory.
+ */
+
+#ifndef QEMU_VIRTIO_CAMERA_H
+#define QEMU_VIRTIO_CAMERA_H
+
+#include "qemu/queue.h"
+#include "qemu/uuid.h"
+#include "hw/virtio/virtio.h"
+#include "qom/object.h"
+
+#include "standard-headers/linux/virtio_camera.h"
+
+#define TYPE_VIRTIO_CAMERA "virtio-camera-device"
+OBJECT_DECLARE_SIMPLE_TYPE(VirtIOCamera, VIRTIO_CAMERA)
+
+#define VIRTIO_CAMERA_GET_PARENT_CLASS(obj) \
+        OBJECT_GET_PARENT_CLASS(obj, TYPE_VIRTIO_CAMERA)
+
+#define VIRTIO_CAMERA_NUM_V4L2_BUFFERS  3
+
+struct virtio_camera_v4l2 {
+    void *buffer_data[VIRTIO_CAMERA_NUM_V4L2_BUFFERS];
+    size_t buffer_size[VIRTIO_CAMERA_NUM_V4L2_BUFFERS];
+    unsigned int queue_buffer_index;
+    char *dev_path;
+    int fd;
+};
+
+struct virtio_camera_mem_buffer {
+    QTAILQ_ENTRY(virtio_camera_mem_buffer) node;
+    QTAILQ_ENTRY(virtio_camera_mem_buffer) capture_node;
+    QemuUUID uuid;
+    VirtQueueElement *capture_elem;
+    unsigned int num_entries;
+    struct iovec iov[];
+};
+
+struct VirtIOCamera {
+    VirtIODevice parent_obj;
+    VirtQueue *ctrl_vq;
+    struct virtio_camera_v4l2 v4l2;
+    struct virtio_camera_config config;
+    QTAILQ_HEAD(, virtio_camera_mem_buffer) buflist;
+    QTAILQ_HEAD(, virtio_camera_mem_buffer) capturelist;
+    uint64_t mem_buf_uuid;
+    bool streaming;
+};
+
+#endif
diff --git a/include/standard-headers/linux/virtio_camera.h b/include/standard-headers/linux/virtio_camera.h
new file mode 100644
index 00000000000..704dd62c787
--- /dev/null
+++ b/include/standard-headers/linux/virtio_camera.h
@@ -0,0 +1,84 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later WITH Linux-syscall-note */
+/*
+ * Virtio Camera Device
+ *
+ * Copyright © 2022 Collabora, Ltd.
+ */
+
+#ifndef _LINUX_VIRTIO_CAMERA_H
+#define _LINUX_VIRTIO_CAMERA_H
+
+#include "standard-headers/linux/types.h"
+
+enum virtio_camera_ctrl_type {
+	VIRTIO_CAMERA_CMD_GET_FORMAT = 0x1,
+	VIRTIO_CAMERA_CMD_SET_FORMAT,
+	VIRTIO_CAMERA_CMD_TRY_FORMAT,
+	VIRTIO_CAMERA_CMD_ENUM_FORMAT,
+	VIRTIO_CAMERA_CMD_ENUM_SIZE,
+	VIRTIO_CAMERA_CMD_CREATE_BUFFER,
+	VIRTIO_CAMERA_CMD_DESTROY_BUFFER,
+	VIRTIO_CAMERA_CMD_ENQUEUE_BUFFER,
+	VIRTIO_CAMERA_CMD_STREAM_ON,
+	VIRTIO_CAMERA_CMD_STREAM_OFF,
+
+	VIRTIO_CAMERA_CMD_RESP_OK_NODATA = 0x100,
+
+	VIRTIO_CAMERA_CMD_RESP_ERR_UNSPEC = 0x200,
+	VIRTIO_CAMERA_CMD_RESP_ERR_BUSY = 0x201,
+	VIRTIO_CAMERA_CMD_RESP_ERR_OUT_OF_MEMORY = 0x202,
+};
+
+struct virtio_camera_config {
+	uint8_t name[256];
+};
+
+struct virtio_camera_mem_entry {
+	uint64_t addr;
+	uint32_t length;
+};
+
+struct virtio_camera_ctrl_hdr {
+	uint32_t cmd;
+	uint32_t index;
+};
+
+struct virtio_camera_format_size {
+	union {
+		uint32_t min_width;
+		uint32_t width;
+	};
+	uint32_t max_width;
+	uint32_t step_width;
+
+	union {
+		uint32_t min_height;
+		uint32_t height;
+	};
+	uint32_t max_height;
+	uint32_t step_height;
+	uint32_t stride;
+	uint32_t sizeimage;
+};
+
+struct virtio_camera_req_format {
+	uint32_t pixelformat;
+	struct virtio_camera_format_size size;
+};
+
+struct virtio_camera_req_buffer {
+	uint32_t num_entries;
+	uint8_t uuid[16];
+};
+
+struct virtio_camera_op_ctrl_req {
+	struct virtio_camera_ctrl_hdr header;
+
+	union {
+		struct virtio_camera_req_format format;
+		struct virtio_camera_req_buffer buffer;
+		uint64_t padding[3];
+	} u;
+};
+
+#endif
diff --git a/include/standard-headers/linux/virtio_ids.h b/include/standard-headers/linux/virtio_ids.h
index 80d76b75bcc..39b2b5344da 100644
--- a/include/standard-headers/linux/virtio_ids.h
+++ b/include/standard-headers/linux/virtio_ids.h
@@ -68,6 +68,7 @@
 #define VIRTIO_ID_AUDIO_POLICY		39 /* virtio audio policy */
 #define VIRTIO_ID_BT			40 /* virtio bluetooth */
 #define VIRTIO_ID_GPIO			41 /* virtio gpio */
+#define VIRTIO_ID_CAMERA		42 /* virtio camera */
 
 /*
  * Virtio Transitional IDs
diff --git a/meson.build b/meson.build
index 20fddbd707c..33b16be57f9 100644
--- a/meson.build
+++ b/meson.build
@@ -2782,6 +2782,9 @@ config_host_data.set('CONFIG_CAPSTONE', capstone.found())
 config_host_data.set('CONFIG_FDT', fdt.found())
 config_host_data.set('CONFIG_SLIRP', slirp.found())
 
+libv4l2 = dependency('libv4l2', required: true, method: 'pkg-config',
+                     kwargs: static_kwargs)
+
 #####################
 # Generated sources #
 #####################
@@ -3981,6 +3984,7 @@ summary_info += {'capstone':          capstone}
 summary_info += {'libpmem support':   libpmem}
 summary_info += {'libdaxctl support': libdaxctl}
 summary_info += {'libudev':           libudev}
+summary_info += {'libv4l2 support':   libv4l2}
 # Dummy dependency, keep .found()
 summary_info += {'FUSE lseek':        fuse_lseek.found()}
 summary_info += {'selinux':           selinux}
-- 
GitLab