diff --git a/drivers/media/platform/Kconfig b/drivers/media/platform/Kconfig
index f1056ceaf5a8c57cac1d23c0e9e5595f80e9b16c..d9f39b228e2bce273d9a789918495aabfb94cb78 100644
--- a/drivers/media/platform/Kconfig
+++ b/drivers/media/platform/Kconfig
@@ -82,6 +82,7 @@ source "drivers/media/platform/st/Kconfig"
 source "drivers/media/platform/sunxi/Kconfig"
 source "drivers/media/platform/ti/Kconfig"
 source "drivers/media/platform/via/Kconfig"
+source "drivers/media/platform/virtio/Kconfig"
 source "drivers/media/platform/xilinx/Kconfig"
 
 endif # MEDIA_PLATFORM_DRIVERS
diff --git a/drivers/media/platform/Makefile b/drivers/media/platform/Makefile
index a881e97bae950c2430bf0914d84460e9e2578b0c..6094f377cf3ecc41220539bf155634778c63cdeb 100644
--- a/drivers/media/platform/Makefile
+++ b/drivers/media/platform/Makefile
@@ -25,6 +25,7 @@ obj-y += st/
 obj-y += sunxi/
 obj-y += ti/
 obj-y += via/
+obj-y += virtio/
 obj-y += xilinx/
 
 # Please place here only ancillary drivers that aren't SoC-specific
diff --git a/drivers/media/platform/virtio/Kconfig b/drivers/media/platform/virtio/Kconfig
new file mode 100644
index 0000000000000000000000000000000000000000..173230b0bea49930699b5b009ebcf03b20934205
--- /dev/null
+++ b/drivers/media/platform/virtio/Kconfig
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+comment "VirtIO media platform drivers"
+
+config VIDEO_VIRTIO_CAMERA
+	tristate "VirtIO camera driver"
+	depends on V4L_PLATFORM_DRIVERS
+	depends on VIDEO_DEV
+	depends on VIRTIO
+	select VIDEOBUF2_DMA_SG
+	select VIRTIO_DMA_SHARED_BUFFER
+	help
+	  This driver provides support for VirtIO camera device. If you
+	  choose 'M' here, this module will be called virtio_camera.
diff --git a/drivers/media/platform/virtio/Makefile b/drivers/media/platform/virtio/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..0fb500b75f3751718dd5ebbb70a3921e2037ee70
--- /dev/null
+++ b/drivers/media/platform/virtio/Makefile
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_VIDEO_VIRTIO_CAMERA) += virtio-camera.o
diff --git a/drivers/media/platform/virtio/virtio-camera.c b/drivers/media/platform/virtio/virtio-camera.c
new file mode 100644
index 0000000000000000000000000000000000000000..5c99db4f2a1afe74a67761e8d7d72fed9ae2222a
--- /dev/null
+++ b/drivers/media/platform/virtio/virtio-camera.c
@@ -0,0 +1,597 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+ /*
+  * Driver for VirtIO camera device.
+  *
+  * Copyright © 2022 Collabora, Ltd.
+  */
+
+#include <linux/completion.h>
+#include <linux/err.h>
+#include <linux/module.h>
+#include <linux/virtio.h>
+#include <linux/virtio_config.h>
+#include <linux/virtio_camera.h>
+#include <linux/virtio_types.h>
+#include <linux/virtio_ids.h>
+#include <media/videobuf2-dma-sg.h>
+
+#include <media/media-device.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-event.h>
+#include <media/v4l2-ioctl.h>
+
+struct virtio_camera_ctrl_req {
+	struct virtio_camera_op_ctrl_req ctrl;
+	struct virtio_camera_op_ctrl_req resp;
+	struct completion completion;
+	struct vb2_buffer *vb;
+};
+
+struct virtio_camera {
+	struct v4l2_device v4l2_dev;
+	struct v4l2_m2m_dev *m2m;
+	struct media_device mdev;
+	struct video_device vdev;
+	struct mutex v4l2_lock;
+	struct vb2_queue vq;
+	struct virtqueue *vqx;
+	struct virtio_camera_ctrl_req req;
+	struct virtio_camera_config config;
+	struct v4l2_format f;
+};
+
+struct virtio_camera_buffer {
+	struct vb2_v4l2_buffer vb;
+	struct virtio_camera_ctrl_req req;
+	u8 uuid[16];
+};
+
+static const struct v4l2_file_operations vcam_v4l2_fops = {
+	.owner = THIS_MODULE,
+	.unlocked_ioctl = video_ioctl2,
+	.open = v4l2_fh_open,
+	.release = vb2_fop_release,
+	.poll = vb2_fop_poll,
+	.mmap = vb2_fop_mmap,
+	.read = vb2_fop_read,
+};
+
+static inline struct virtio_camera_buffer *
+vb_to_vcam_buf(struct vb2_buffer *vb)
+{
+	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
+
+	return container_of(vbuf, struct virtio_camera_buffer, vb);
+}
+
+static void virtio_camera_control_ack(struct virtqueue *vq)
+{
+	struct virtio_camera_ctrl_req *req;
+	unsigned int len;
+
+	while ((req = virtqueue_get_buf(vq, &len))) {
+		complete(&req->completion);
+
+		if (req->vb)
+			vb2_buffer_done(req->vb, VB2_BUF_STATE_DONE);
+	}
+}
+
+static void vcam_init_request(struct virtio_camera_ctrl_req *req)
+{
+	init_completion(&req->completion);
+}
+
+static int vcam_vq_request(struct virtio_camera *vcam,
+			   struct virtio_camera_ctrl_req *req,
+			   struct virtio_camera_mem_entry *ents,
+			   unsigned int num_ents,
+			   bool async)
+{
+	struct scatterlist vreq[3], *sgs[3];
+	unsigned int num_sgs = 0;
+	int ret;
+
+	memset(&req->resp, 0, sizeof(req->resp));
+
+	sg_init_one(&vreq[0], &req->ctrl, sizeof(req->ctrl));
+	sgs[num_sgs++] = &vreq[0];
+
+	if (ents) {
+		sg_init_one(&vreq[1], ents, sizeof(*ents) * num_ents);
+		sgs[num_sgs++] = &vreq[1];
+	}
+
+	sg_init_one(&vreq[2], &req->resp, sizeof(req->resp));
+	sgs[num_sgs++] = &vreq[2];
+
+	reinit_completion(&req->completion);
+
+	virtqueue_add_sgs(vcam->vqx, sgs, num_sgs - 1, 1, req, GFP_KERNEL);
+	virtqueue_kick(vcam->vqx);
+
+	if (async)
+		return 0;
+
+	wait_for_completion(&req->completion);
+
+	memset(&req->ctrl, 0, sizeof(req->ctrl));
+
+	switch (req->resp.header.cmd) {
+	case VIRTIO_CAMERA_CMD_RESP_OK_NODATA:
+		ret = 0;
+		break;
+
+	case VIRTIO_CAMERA_CMD_RESP_ERR_BUSY:
+		ret = -EBUSY;
+		break;
+
+	case VIRTIO_CAMERA_CMD_RESP_ERR_OUT_OF_MEMORY:
+		ret = -ENOMEM;
+		break;
+
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	return ret;
+}
+
+static int vcam_querycap(struct file *file, void *priv,
+			 struct v4l2_capability *cap)
+{
+	struct virtio_camera *vcam = video_drvdata(file);
+
+	strscpy(cap->bus_info, "platform:camera", sizeof(cap->bus_info));
+	strscpy(cap->driver, "virtio-camera", sizeof(cap->driver));
+	strscpy(cap->card, vcam->config.name, sizeof(cap->card));
+
+	return 0;
+}
+
+static int vcam_enum_fmt(struct file *file, void *fh, struct v4l2_fmtdesc *f)
+{
+	struct virtio_camera *vcam = video_drvdata(file);
+	int err;
+
+	vcam->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_ENUM_FORMAT;
+	vcam->req.ctrl.header.index = f->index;
+
+	err = vcam_vq_request(vcam, &vcam->req, NULL, 0, false);
+	if (err)
+		return err;
+
+	f->pixelformat = vcam->req.resp.u.format.pixelformat;
+
+	return 0;
+}
+
+static int vcam_enum_framesizes(struct file *file, void *fh,
+				struct v4l2_frmsizeenum *fsize)
+{
+	struct virtio_camera *vcam = video_drvdata(file);
+	struct virtio_camera_format_size *sz = &vcam->req.resp.u.format.size;
+	int err;
+
+	vcam->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_ENUM_SIZE;
+	vcam->req.ctrl.header.index = fsize->index;
+	vcam->req.ctrl.u.format.pixelformat = fsize->pixel_format;
+
+	err = vcam_vq_request(vcam, &vcam->req, NULL, 0, false);
+	if (err)
+		return err;
+
+	if (sz->min_width == sz->max_width && sz->min_height == sz->max_height) {
+		fsize->discrete.width = sz->width;
+		fsize->discrete.height = sz->height;
+		fsize->type = V4L2_FRMSIZE_TYPE_DISCRETE;
+	} else {
+		fsize->stepwise.min_width = sz->min_width;
+		fsize->stepwise.max_width = sz->max_width;
+		fsize->stepwise.min_height = sz->min_height;
+		fsize->stepwise.max_height = sz->max_height;
+		fsize->stepwise.step_width = sz->step_width;
+		fsize->stepwise.step_height = sz->max_height;
+
+		if (sz->step_width == 1 && sz->step_height == 1)
+			fsize->type = V4L2_FRMSIZE_TYPE_CONTINUOUS;
+		else
+			fsize->type = V4L2_FRMSIZE_TYPE_STEPWISE;
+	}
+
+	return 0;
+}
+
+static int vcam_g_fmt(struct file *file, void *fh, struct v4l2_format *f)
+{
+	struct virtio_camera *vcam = video_drvdata(file);
+	int err;
+
+	vcam->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_GET_FORMAT;
+
+	err = vcam_vq_request(vcam, &vcam->req, NULL, 0, false);
+	if (err)
+		return err;
+
+	f->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+
+	f->fmt.pix.flags = 0;
+	f->fmt.pix.field = V4L2_FIELD_NONE;
+	f->fmt.pix.width = vcam->req.resp.u.format.size.width;
+	f->fmt.pix.height = vcam->req.resp.u.format.size.height;
+	f->fmt.pix.pixelformat = vcam->req.resp.u.format.pixelformat;
+	f->fmt.pix.bytesperline = vcam->req.resp.u.format.size.stride;
+	f->fmt.pix.sizeimage = vcam->req.resp.u.format.size.sizeimage;
+
+	/* TODO */
+	f->fmt.pix.field = V4L2_FIELD_NONE;
+	f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
+
+	return 0;
+}
+
+static int vcam_s_fmt(struct file *file, void *fh, struct v4l2_format *f)
+{
+	struct virtio_camera *vcam = video_drvdata(file);
+	int err;
+
+	if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+		return -EINVAL;
+
+	vcam->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_SET_FORMAT;
+	vcam->req.ctrl.u.format.size.width = f->fmt.pix.width;
+	vcam->req.ctrl.u.format.size.height = f->fmt.pix.height;
+	vcam->req.ctrl.u.format.size.stride = f->fmt.pix.bytesperline;
+	vcam->req.ctrl.u.format.pixelformat = f->fmt.pix.pixelformat;
+
+	err = vcam_vq_request(vcam, &vcam->req, NULL, 0, false);
+	if (err)
+		return err;
+
+	f->fmt.pix.flags = 0;
+	f->fmt.pix.field = V4L2_FIELD_NONE;
+	f->fmt.pix.width = vcam->req.resp.u.format.size.width;
+	f->fmt.pix.height = vcam->req.resp.u.format.size.height;
+	f->fmt.pix.pixelformat = vcam->req.resp.u.format.pixelformat;
+	f->fmt.pix.bytesperline = vcam->req.resp.u.format.size.stride;
+	f->fmt.pix.sizeimage = vcam->req.resp.u.format.size.sizeimage;
+
+	/* TODO */
+	f->fmt.pix.field = V4L2_FIELD_NONE;
+	f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
+
+	vcam->f = *f;
+
+	return err;
+}
+
+static int vcam_try_fmt(struct file *file, void *fh, struct v4l2_format *f)
+{
+	struct virtio_camera *vcam = video_drvdata(file);
+	int err;
+
+	vcam->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_TRY_FORMAT;
+	vcam->req.ctrl.u.format.size.width = f->fmt.pix.width;
+	vcam->req.ctrl.u.format.size.height = f->fmt.pix.height;
+	vcam->req.ctrl.u.format.size.stride = f->fmt.pix.bytesperline;
+	vcam->req.ctrl.u.format.pixelformat = f->fmt.pix.pixelformat;
+
+	err = vcam_vq_request(vcam, &vcam->req, NULL, 0, false);
+	if (err)
+		return err;
+
+	f->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+
+	f->fmt.pix.flags = 0;
+	f->fmt.pix.field = V4L2_FIELD_NONE;
+	f->fmt.pix.width = vcam->req.resp.u.format.size.width;
+	f->fmt.pix.height = vcam->req.resp.u.format.size.height;
+	f->fmt.pix.pixelformat = vcam->req.resp.u.format.pixelformat;
+	f->fmt.pix.bytesperline = vcam->req.resp.u.format.size.stride;
+	f->fmt.pix.sizeimage = vcam->req.resp.u.format.size.sizeimage;
+
+	/* TODO */
+	f->fmt.pix.field = V4L2_FIELD_NONE;
+	f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
+
+	return 0;
+}
+
+static int vcam_enum_input(struct file *filp, void *p,
+			   struct v4l2_input *input)
+{
+	if (input->index > 0)
+		return -EINVAL;
+
+	strscpy(input->name, "virtio-camera0", sizeof(input->name));
+	input->type = V4L2_INPUT_TYPE_CAMERA;
+	input->std = V4L2_STD_UNKNOWN;
+	input->status = 0;
+
+	return 0;
+}
+
+static int vcam_g_input(struct file *filp, void *p, unsigned int *i)
+{
+	*i = 0;
+
+	return 0;
+}
+
+static int vcam_s_input(struct file *filp, void *p, unsigned int i)
+{
+	if (i)
+		return -EINVAL;
+
+	return 0;
+}
+
+static const struct v4l2_ioctl_ops vcam_ioctl_ops = {
+	.vidioc_querycap = vcam_querycap,
+	.vidioc_enum_fmt_vid_cap = vcam_enum_fmt,
+	.vidioc_enum_framesizes = vcam_enum_framesizes,
+	.vidioc_g_fmt_vid_cap = vcam_g_fmt,
+	.vidioc_s_fmt_vid_cap = vcam_s_fmt,
+	.vidioc_try_fmt_vid_cap = vcam_try_fmt,
+	.vidioc_reqbufs = vb2_ioctl_reqbufs,
+	.vidioc_querybuf = vb2_ioctl_querybuf,
+	.vidioc_qbuf = vb2_ioctl_qbuf,
+	.vidioc_expbuf = vb2_ioctl_expbuf,
+	.vidioc_dqbuf = vb2_ioctl_dqbuf,
+	.vidioc_create_bufs = vb2_ioctl_create_bufs,
+	.vidioc_prepare_buf = vb2_ioctl_prepare_buf,
+	.vidioc_streamon = vb2_ioctl_streamon,
+	.vidioc_streamoff = vb2_ioctl_streamoff,
+	.vidioc_enum_input = vcam_enum_input,
+	.vidioc_g_input = vcam_g_input,
+	.vidioc_s_input = vcam_s_input,
+};
+
+static int
+vcam_queue_setup(struct vb2_queue *vq,
+		 unsigned int *nbuffers, unsigned int *num_planes,
+		 unsigned int sizes[], struct device *alloc_devs[])
+
+{
+	struct virtio_camera *vcam = vb2_get_drv_priv(vq);
+	unsigned int size = vcam->f.fmt.pix.sizeimage;
+	int ret;
+
+	if (vq->num_buffers + *nbuffers < 2)
+		*nbuffers = 2 - vq->num_buffers;
+
+	if (*num_planes) {
+		ret = sizes[0] < size ? -EINVAL : 0;
+		return ret;
+	}
+
+	*num_planes = 1;
+	sizes[0] = size;
+
+	return 0;
+}
+
+static int vcam_buf_init(struct vb2_buffer *vb)
+{
+	struct virtio_camera *vcam = vb2_get_drv_priv(vb->vb2_queue);
+	struct virtio_camera_buffer *vbuf = vb_to_vcam_buf(vb);
+	struct virtio_camera_mem_entry *ents;
+	struct scatterlist *sg;
+	struct sg_table *sgt;
+	unsigned int i;
+	int err;
+
+	/* TODO */
+	if (WARN_ON(vb->num_planes != 1))
+		return -EINVAL;
+
+	sgt = vb2_dma_sg_plane_desc(vb, 0);
+	ents = kmalloc_array(sgt->nents, sizeof(*ents), GFP_KERNEL);
+	if (!ents)
+		return -ENOMEM;
+
+	for_each_sg(sgt->sgl, sg, sgt->nents, i) {
+		ents[i].addr = cpu_to_le64(sg_phys(sg));
+		ents[i].length = cpu_to_le32(sg->length);
+	}
+
+	vcam->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_CREATE_BUFFER;
+	vcam->req.ctrl.u.buffer.num_entries = sgt->nents;
+
+	err = vcam_vq_request(vcam, &vcam->req, ents, sgt->nents, false);
+	kfree(ents);
+	if (err)
+		return err;
+
+	memcpy(vbuf->uuid, vcam->req.resp.u.buffer.uuid, sizeof(vbuf->uuid));
+
+	vcam_init_request(&vbuf->req);
+	vbuf->req.vb = vb;
+
+	return 0;
+}
+
+static void vcam_buf_cleanup(struct vb2_buffer *vb)
+{
+	struct virtio_camera *vcam = vb2_get_drv_priv(vb->vb2_queue);
+	struct virtio_camera_buffer *vbuf = vb_to_vcam_buf(vb);
+
+	vcam->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_DESTROY_BUFFER;
+	memcpy(vcam->req.ctrl.u.buffer.uuid, vbuf->uuid, sizeof(vbuf->uuid));
+
+	vcam_vq_request(vcam, &vcam->req, NULL, 0, false);
+}
+
+static int vcam_buf_prepare(struct vb2_buffer *vb)
+{
+	struct virtio_camera *vcam = vb2_get_drv_priv(vb->vb2_queue);
+
+	vb2_set_plane_payload(vb, 0, vcam->f.fmt.pix.sizeimage);
+
+	return 0;
+}
+
+static void vcam_buf_queue(struct vb2_buffer *vb)
+{
+	struct virtio_camera *vcam = vb2_get_drv_priv(vb->vb2_queue);
+	struct virtio_camera_buffer *vbuf = vb_to_vcam_buf(vb);
+	int err;
+
+	vbuf->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_ENQUEUE_BUFFER;
+	memcpy(vbuf->req.ctrl.u.buffer.uuid, vbuf->uuid, sizeof(vbuf->uuid));
+
+	err = vcam_vq_request(vcam, &vbuf->req, NULL, 0, true);
+	if (err)
+		vb2_buffer_done(vb, VB2_BUF_STATE_ERROR);
+}
+
+static int vcam_start_streaming(struct vb2_queue *q, unsigned int count)
+{
+	struct virtio_camera *vcam = vb2_get_drv_priv(q);
+	int err;
+
+	vcam->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_STREAM_ON;
+
+	err = vcam_vq_request(vcam, &vcam->req, NULL, 0, false);
+	if (err)
+		return err;
+
+	return 0;
+}
+
+static void vcam_stop_streaming(struct vb2_queue *q)
+{
+	struct virtio_camera *vcam = vb2_get_drv_priv(q);
+
+	vcam->req.ctrl.header.cmd = VIRTIO_CAMERA_CMD_STREAM_OFF;
+
+	vcam_vq_request(vcam, &vcam->req, NULL, 0, false);
+
+	vb2_wait_for_all_buffers(q);
+}
+
+static const struct vb2_ops vcam_vb2_ops = {
+	.queue_setup = vcam_queue_setup,
+	.wait_prepare = vb2_ops_wait_prepare,
+	.wait_finish = vb2_ops_wait_finish,
+	.buf_init = vcam_buf_init,
+	.buf_queue = vcam_buf_queue,
+	.buf_cleanup = vcam_buf_cleanup,
+	.buf_prepare = vcam_buf_prepare,
+	.start_streaming = vcam_start_streaming,
+	.stop_streaming = vcam_stop_streaming,
+};
+
+static void delete_vqs(void *data)
+{
+	struct virtio_device *vdev = data;
+
+	vdev->config->del_vqs(vdev);
+}
+
+static int virtio_camera_probe(struct virtio_device *vdev)
+{
+	static vq_callback_t *callbacks[] = { virtio_camera_control_ack };
+	static const char * const names[] = { "control" };
+	struct virtio_camera *vcam;
+	struct virtqueue *vqs[1];
+	int err;
+
+	vcam = devm_kzalloc(&vdev->dev, sizeof(*vcam), GFP_KERNEL);
+	if (!vcam)
+		return -ENOMEM;
+
+	vdev->priv = vcam;
+	mutex_init(&vcam->v4l2_lock);
+	vcam_init_request(&vcam->req);
+	media_device_init(&vcam->mdev);
+	video_set_drvdata(&vcam->vdev, vcam);
+
+	vcam->vdev.queue = &vcam->vq;
+	vcam->vdev.lock = &vcam->v4l2_lock,
+	vcam->vdev.fops = &vcam_v4l2_fops,
+	vcam->vdev.vfl_dir = VFL_DIR_RX,
+	vcam->vdev.release = video_device_release_empty,
+	vcam->vdev.v4l2_dev = &vcam->v4l2_dev;
+	vcam->vdev.ioctl_ops = &vcam_ioctl_ops,
+	vcam->vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
+
+	vcam->vq.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+	vcam->vq.buf_struct_size = sizeof(struct virtio_camera_buffer);
+	vcam->vq.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
+	vcam->vq.io_modes = VB2_MMAP | VB2_DMABUF;
+	vcam->vq.mem_ops = &vb2_dma_sg_memops;
+	vcam->vq.lock = &vcam->v4l2_lock;
+	vcam->vq.min_buffers_needed = 1;
+	vcam->vq.gfp_flags = GFP_DMA32;
+	vcam->vq.ops = &vcam_vb2_ops;
+	vcam->vq.dev = vdev->dev.parent;
+	vcam->vq.drv_priv = vcam;
+
+	err = virtio_find_vqs(vdev, 1, vqs, callbacks, names, NULL);
+	if (err)
+		return dev_err_probe(&vdev->dev, err,
+				     "failed to find virt queue\n");
+
+	err = devm_add_action_or_reset(&vdev->dev, delete_vqs, vdev);
+	if (err)
+		return err;
+
+	vcam->vqx = vqs[0];
+
+	virtio_cread_bytes(vdev, 0, &vcam->config, sizeof(vcam->config));
+
+	err = vb2_queue_init(&vcam->vq);
+	if (err)
+		return dev_err_probe(&vdev->dev, err,
+				     "failed to initialize vb2 queue\n");
+
+	err = v4l2_device_register(&vdev->dev, &vcam->v4l2_dev);
+	if (err)
+		return dev_err_probe(&vdev->dev, err,
+				     "failed to register v4l2 device\n");
+
+	err = video_register_device(&vcam->vdev, VFL_TYPE_VIDEO, -1);
+	if (err) {
+		v4l2_device_unregister(&vcam->v4l2_dev);
+		return dev_err_probe(&vdev->dev, err,
+				     "failed to register video device\n");
+	}
+
+	return 0;
+}
+
+static void virtio_camera_remove(struct virtio_device *vdev)
+{
+	struct virtio_camera *vcam = vdev->priv;
+
+	video_unregister_device(&vcam->vdev);
+	v4l2_device_unregister(&vcam->v4l2_dev);
+	virtio_reset_device(vdev);
+}
+
+static const unsigned int features[] = {
+	/* none */
+};
+
+static const struct virtio_device_id id_table[] = {
+	{ VIRTIO_ID_CAMERA, VIRTIO_DEV_ANY_ID },
+	{},
+};
+MODULE_DEVICE_TABLE(virtio, id_table);
+
+static struct virtio_driver virtio_camera_driver = {
+	.feature_table_size = ARRAY_SIZE(features),
+	.feature_table = features,
+	.probe = virtio_camera_probe,
+	.remove = virtio_camera_remove,
+	.driver.name = "virtio-camera",
+	.id_table = id_table,
+};
+module_virtio_driver(virtio_camera_driver);
+
+MODULE_AUTHOR("Dmitry Osipenko <dmitry.osipenko@collabora.com>");
+MODULE_DESCRIPTION("virtio camera device driver");
+MODULE_LICENSE("GPL");
diff --git a/include/uapi/linux/virtio_camera.h b/include/uapi/linux/virtio_camera.h
new file mode 100644
index 0000000000000000000000000000000000000000..a7f45e7fa8778c7f66dbbcfc3f4f442c8c556dfb
--- /dev/null
+++ b/include/uapi/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 <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 {
+	__u8 name[256];
+};
+
+struct virtio_camera_mem_entry {
+	__le64 addr;
+	__le32 length;
+};
+
+struct virtio_camera_ctrl_hdr {
+	__le32 cmd;
+	__le32 index;
+};
+
+struct virtio_camera_format_size {
+	union {
+		__le32 min_width;
+		__le32 width;
+	};
+	__le32 max_width;
+	__le32 step_width;
+
+	union {
+		__le32 min_height;
+		__le32 height;
+	};
+	__le32 max_height;
+	__le32 step_height;
+	__le32 stride;
+	__le32 sizeimage;
+};
+
+struct virtio_camera_req_format {
+	__le32 pixelformat;
+	struct virtio_camera_format_size size;
+};
+
+struct virtio_camera_req_buffer {
+	__le32 num_entries;
+	__u8 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;
+		__le64 padding[3];
+	} u;
+};
+
+#endif
diff --git a/include/uapi/linux/virtio_ids.h b/include/uapi/linux/virtio_ids.h
index 7aa2eb76620508fdc915533f74973d76308d3ef5..5e3f9521e247e960ac4e4da9f5995561b935b7ad 100644
--- a/include/uapi/linux/virtio_ids.h
+++ b/include/uapi/linux/virtio_ids.h
@@ -68,6 +68,7 @@
 #define VIRTIO_ID_AUDIO_POLICY		39 /* virtio audio policy */
 #define VIRTIO_ID_BT			40 /* virtio bluetooth */
 #define VIRTIO_ID_GPIO			41 /* virtio gpio */
+#define VIRTIO_ID_CAMERA		42 /* virtio camera */
 
 /*
  * Virtio Transitional IDs