+};
+MODULE_DEVICE_TABLE(of, rzv2h_ivc_of_match);
+
+static struct platform_driver rzv2h_ivc_driver = {
+ .driver = {
+ .name = "rzv2h-ivc",
+ .of_match_table = rzv2h_ivc_of_match,
+ .pm = &rzv2h_ivc_pm_ops,
+ },
+ .probe = rzv2h_ivc_probe,
+ .remove = rzv2h_ivc_remove,
+};
+
+module_platform_driver(rzv2h_ivc_driver);
+
+MODULE_AUTHOR("Daniel Scally <dan.scally@xxxxxxxxxxxxxxxx>");
+MODULE_DESCRIPTION("Renesas RZ/V2H Input Video Control Block driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/media/platform/renesas/rzv2h-ivc/rzv2h-ivc-subdev.c b/drivers/media/platform/renesas/rzv2h-ivc/rzv2h-ivc-subdev.c
new file mode 100644
index 0000000000000000000000000000000000000000..eb2913153d406fbad2491bb36e1c5ea754bea6f2
--- /dev/null
+++ b/drivers/media/platform/renesas/rzv2h-ivc/rzv2h-ivc-subdev.c
@@ -0,0 +1,376 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Renesas RZ/V2H Input Video Control Block driver
+ *
+ * Copyright (C) 2025 Ideas on Board Oy
+ */
+
+#include "rzv2h-ivc.h"
+
+#include <linux/media.h>
+#include <linux/media-bus-format.h>
+#include <linux/v4l2-mediabus.h>
+
+#include <media/v4l2-async.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-dev.h>
+#include <media/v4l2-event.h>
+
+#define RZV2H_IVC_N_INPUTS_PER_OUTPUT 6
+
+/*
+ * We support 8/10/12/14/16/20 bit input in any bayer order, but the output
+ * format is fixed at 20-bits with the same order as the input.
+ */
+static const struct {
+ u32 inputs[RZV2H_IVC_N_INPUTS_PER_OUTPUT];
+ u32 output;
+} rzv2h_ivc_formats[] = {
+ {
+ .inputs = {
+ MEDIA_BUS_FMT_SBGGR8_1X8,
+ MEDIA_BUS_FMT_SBGGR10_1X10,
+ MEDIA_BUS_FMT_SBGGR12_1X12,
+ MEDIA_BUS_FMT_SBGGR14_1X14,
+ MEDIA_BUS_FMT_SBGGR16_1X16,
+ MEDIA_BUS_FMT_SBGGR20_1X20,
+ },
+ .output = MEDIA_BUS_FMT_SBGGR20_1X20
+ },
+ {
+ .inputs = {
+ MEDIA_BUS_FMT_SGBRG8_1X8,
+ MEDIA_BUS_FMT_SGBRG10_1X10,
+ MEDIA_BUS_FMT_SGBRG12_1X12,
+ MEDIA_BUS_FMT_SGBRG14_1X14,
+ MEDIA_BUS_FMT_SGBRG16_1X16,
+ MEDIA_BUS_FMT_SGBRG20_1X20,
+ },
+ .output = MEDIA_BUS_FMT_SGBRG20_1X20
+ },
+ {
+ .inputs = {
+ MEDIA_BUS_FMT_SGRBG8_1X8,
+ MEDIA_BUS_FMT_SGRBG10_1X10,
+ MEDIA_BUS_FMT_SGRBG12_1X12,
+ MEDIA_BUS_FMT_SGRBG14_1X14,
+ MEDIA_BUS_FMT_SGRBG16_1X16,
+ MEDIA_BUS_FMT_SGRBG20_1X20,
+ },
+ .output = MEDIA_BUS_FMT_SGRBG20_1X20
+ },
+ {
+ .inputs = {
+ MEDIA_BUS_FMT_SRGGB8_1X8,
+ MEDIA_BUS_FMT_SRGGB10_1X10,
+ MEDIA_BUS_FMT_SRGGB12_1X12,
+ MEDIA_BUS_FMT_SRGGB14_1X14,
+ MEDIA_BUS_FMT_SRGGB16_1X16,
+ MEDIA_BUS_FMT_SRGGB20_1X20,
+ },
+ .output = MEDIA_BUS_FMT_SRGGB20_1X20
+ },
+};
+
+static u32 rzv2h_ivc_get_mbus_output_from_input(u32 mbus_code)
+{
+ unsigned int i, j;
+
+ for (i = 0; i < ARRAY_SIZE(rzv2h_ivc_formats); i++) {
+ for (j = 0; j < RZV2H_IVC_N_INPUTS_PER_OUTPUT; j++) {
+ if (rzv2h_ivc_formats[i].inputs[j] == mbus_code)
+ return rzv2h_ivc_formats[i].output;
+ }
+ }
+
+ return 0;
+}
+
+static int rzv2h_ivc_enum_mbus_code(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_mbus_code_enum *code)
+{
+ const struct v4l2_mbus_framefmt *fmt;
+ unsigned int order_index;
+ unsigned int index;
+
+ /*
+ * On the source pad, only the 20-bit format corresponding to the sink
+ * pad format's bayer order is supported.
+ */
+ if (code->pad == RZV2H_IVC_SUBDEV_SOURCE_PAD) {
+ if (code->index)
+ return -EINVAL;
+
+ fmt = v4l2_subdev_state_get_format(state,
+ RZV2H_IVC_SUBDEV_SINK_PAD);
+ code->code = rzv2h_ivc_get_mbus_output_from_input(fmt->code);
+
+ return 0;
+ }
+
+ if (code->index >= ARRAY_SIZE(rzv2h_ivc_formats) *
+ RZV2H_IVC_N_INPUTS_PER_OUTPUT)
+ return -EINVAL;
+
+ order_index = code->index / RZV2H_IVC_N_INPUTS_PER_OUTPUT;
+ index = code->index % RZV2H_IVC_N_INPUTS_PER_OUTPUT;
+
+ code->code = rzv2h_ivc_formats[order_index].inputs[index];
+
+ return 0;
+}
+
+static int rzv2h_ivc_enum_frame_size(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_frame_size_enum *fse)
+{
+ const struct v4l2_mbus_framefmt *fmt;
+
+ if (fse->index > 0)
+ return -EINVAL;
+
+ if (fse->pad == RZV2H_IVC_SUBDEV_SOURCE_PAD) {
+ fmt = v4l2_subdev_state_get_format(state,
+ RZV2H_IVC_SUBDEV_SINK_PAD);
+
+ if (fse->code != rzv2h_ivc_get_mbus_output_from_input(fmt->code))
+ return -EINVAL;
+
+ fse->min_width = fmt->width;
+ fse->max_width = fmt->width;
+ fse->min_height = fmt->height;
+ fse->max_height = fmt->height;
+
+ return 0;
+ }
+
+ if (!rzv2h_ivc_get_mbus_output_from_input(fse->code))
+ return -EINVAL;
+
+ fse->min_width = RZV2H_IVC_MIN_WIDTH;
+ fse->max_width = RZV2H_IVC_MAX_WIDTH;
+ fse->min_height = RZV2H_IVC_MIN_HEIGHT;
+ fse->max_height = RZV2H_IVC_MAX_HEIGHT;
+
+ return 0;
+}
+
+static int rzv2h_ivc_set_fmt(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state,
+ struct v4l2_subdev_format *format)
+{
+ struct v4l2_mbus_framefmt *fmt = &format->format;
+ struct v4l2_mbus_framefmt *src_fmt, *sink_fmt;
+
+ if (format->pad == RZV2H_IVC_SUBDEV_SOURCE_PAD)
+ return v4l2_subdev_get_fmt(sd, state, format);
+
+ sink_fmt = v4l2_subdev_state_get_format(state,
+ RZV2H_IVC_SUBDEV_SINK_PAD);
+
+ sink_fmt->code = rzv2h_ivc_get_mbus_output_from_input(fmt->code) ?
+ fmt->code : rzv2h_ivc_formats[0].inputs[0];
+
+ sink_fmt->width = clamp(fmt->width, RZV2H_IVC_MIN_WIDTH,
+ RZV2H_IVC_MAX_WIDTH);
+ sink_fmt->height = clamp(fmt->height, RZV2H_IVC_MIN_HEIGHT,
+ RZV2H_IVC_MAX_HEIGHT);
+
+ *fmt = *sink_fmt;
+
+ src_fmt = v4l2_subdev_state_get_format(state,
+ RZV2H_IVC_SUBDEV_SOURCE_PAD);
+ *src_fmt = *sink_fmt;
+ src_fmt->code = rzv2h_ivc_get_mbus_output_from_input(sink_fmt->code);
+
+ return 0;
+}
+
+static int rzv2h_ivc_enable_streams(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state, u32 pad,
+ u64 streams_mask)
+{
+ /*
+ * We have a single source pad, which has a single stream. V4L2 core has
+ * already validated those things. The actual power-on and programming
+ * of registers will be done through the video device's .vidioc_streamon
+ * so there's nothing to actually do here...
+ */
+
+ return 0;
+}
+
+static int rzv2h_ivc_disable_streams(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state, u32 pad,
+ u64 streams_mask)
+{
+ return 0;
+}
+
+static const struct v4l2_subdev_pad_ops rzv2h_ivc_pad_ops = {
+ .enum_mbus_code = rzv2h_ivc_enum_mbus_code,
+ .enum_frame_size = rzv2h_ivc_enum_frame_size,
+ .get_fmt = v4l2_subdev_get_fmt,
+ .set_fmt = rzv2h_ivc_set_fmt,
+ .enable_streams = rzv2h_ivc_enable_streams,
+ .disable_streams = rzv2h_ivc_disable_streams,
+};
+
+static const struct v4l2_subdev_core_ops rzv2h_ivc_core_ops = {
+ .subscribe_event = v4l2_ctrl_subdev_subscribe_event,
+ .unsubscribe_event = v4l2_event_subdev_unsubscribe,
+};
+
+static const struct v4l2_subdev_ops rzv2h_ivc_subdev_ops = {
+ .core = &rzv2h_ivc_core_ops,
+ .pad = &rzv2h_ivc_pad_ops,
+};
+
+static int rzv2h_ivc_init_state(struct v4l2_subdev *sd,
+ struct v4l2_subdev_state *state)
+{
+ struct v4l2_mbus_framefmt *sink_fmt, *src_fmt;
+
+ sink_fmt = v4l2_subdev_state_get_format(state,
+ RZV2H_IVC_SUBDEV_SINK_PAD);
+ sink_fmt->width = RZV2H_IVC_DEFAULT_WIDTH;
+ sink_fmt->height = RZV2H_IVC_DEFAULT_HEIGHT;
+ sink_fmt->field = V4L2_FIELD_NONE;
+ sink_fmt->code = MEDIA_BUS_FMT_SRGGB16_1X16;
+ sink_fmt->colorspace = V4L2_COLORSPACE_RAW;
+ sink_fmt->xfer_func = V4L2_MAP_XFER_FUNC_DEFAULT(sink_fmt->colorspace);
+ sink_fmt->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(sink_fmt->colorspace);
+ sink_fmt->quantization = V4L2_MAP_QUANTIZATION_DEFAULT(
+ true, sink_fmt->colorspace, sink_fmt->ycbcr_enc);
+
+ src_fmt = v4l2_subdev_state_get_format(state,
+ RZV2H_IVC_SUBDEV_SOURCE_PAD);
+
+ *src_fmt = *sink_fmt;
+ src_fmt->code = MEDIA_BUS_FMT_SRGGB20_1X20;
+
+ return 0;
+}
+
+static int rzv2h_ivc_registered(struct v4l2_subdev *sd)
+{
+ struct rzv2h_ivc *ivc = container_of(sd, struct rzv2h_ivc, subdev.sd);
+
+ return rzv2h_ivc_init_vdev(ivc, sd->v4l2_dev);
+}
+
+static const struct v4l2_subdev_internal_ops rzv2h_ivc_subdev_internal_ops = {
+ .init_state = rzv2h_ivc_init_state,
+ .registered = rzv2h_ivc_registered,
+};
+
+static int rzv2h_ivc_link_validate(struct media_link *link)
+{
+ struct video_device *vdev =
+ media_entity_to_video_device(link->source->entity);
+ struct rzv2h_ivc *ivc = video_get_drvdata(vdev);
+ struct v4l2_subdev *sd =
+ media_entity_to_v4l2_subdev(link->sink->entity);
+ const struct rzv2h_ivc_format *fmt;
+ const struct v4l2_pix_format_mplane *pix;
+ struct v4l2_subdev_state *state;
+ struct v4l2_mbus_framefmt *mf;
+ unsigned int i;
+ int ret = 0;
+
+ state = v4l2_subdev_lock_and_get_active_state(sd);
+ mf = v4l2_subdev_state_get_format(state, link->sink->index);
+
+ pix = &ivc->format.pix;
+ fmt = ivc->format.fmt;
+
+ if (mf->width != pix->width || mf->height != pix->height) {
+ dev_dbg(ivc->dev,
+ "link '%s':%u -> '%s':%u not valid: %ux%u != %ux%u\n",
+ link->source->entity->name, link->source->index,
+ link->sink->entity->name, link->sink->index,
+ mf->width, mf->height,
+ pix->width, pix->height);
+ ret = -EPIPE;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(fmt->mbus_codes); i++)
+ if (mf->code == fmt->mbus_codes[i])
+ break;
+
+ if (i == ARRAY_SIZE(fmt->mbus_codes)) {
+ dev_dbg(ivc->dev,
+ "link '%s':%u -> '%s':%u not valid: pixel format %p4cc cannot produce mbus_code 0x%04x\n",
+ link->source->entity->name, link->source->index,
+ link->sink->entity->name, link->sink->index,
+ &pix->pixelformat, mf->code);
+ ret = -EPIPE;
+ }
+
+ v4l2_subdev_unlock_state(state);
+
+ return ret;
+}
+
+static const struct media_entity_operations rzv2h_ivc_media_ops = {
+ .link_validate = rzv2h_ivc_link_validate,
+};
+
+int rzv2h_ivc_initialise_subdevice(struct rzv2h_ivc *ivc)
+{
+ struct v4l2_subdev *sd;
+ int ret;
+
+ /* Initialise subdevice */
+ sd = &ivc->subdev.sd;
+ sd->dev = ivc->dev;
+ v4l2_subdev_init(sd, &rzv2h_ivc_subdev_ops);
+ sd->flags |= V4L2_SUBDEV_FL_HAS_DEVNODE | V4L2_SUBDEV_FL_HAS_EVENTS;
+ sd->entity.function = MEDIA_ENT_F_PROC_VIDEO_PIXEL_FORMATTER;
+ sd->internal_ops = &rzv2h_ivc_subdev_internal_ops;
+ sd->entity.ops = &rzv2h_ivc_media_ops;
+
+ ivc->subdev.pads[RZV2H_IVC_SUBDEV_SINK_PAD].flags = MEDIA_PAD_FL_SINK;
+ ivc->subdev.pads[RZV2H_IVC_SUBDEV_SOURCE_PAD].flags = MEDIA_PAD_FL_SOURCE;
+
+ snprintf(sd->name, sizeof(sd->name), "rzv2h ivc block");
+
+ ret = media_entity_pads_init(&sd->entity, RZV2H_IVC_NUM_SUBDEV_PADS,
+ ivc->subdev.pads);
+ if (ret) {
+ dev_err(ivc->dev, "failed to initialise media entity\n");
+ return ret;
+ }
+
+ ret = v4l2_subdev_init_finalize(sd);
+ if (ret) {
+ dev_err(ivc->dev, "failed to finalize subdev init\n");
+ goto err_cleanup_subdev_entity;
+ }
+
+ ret = v4l2_async_register_subdev(sd);
+ if (ret) {
+ dev_err(ivc->dev, "failed to register subdevice\n");
+ goto err_cleanup_subdev;
+ }
+
+ return 0;
+
+err_cleanup_subdev:
+ v4l2_subdev_cleanup(sd);
+err_cleanup_subdev_entity:
+ media_entity_cleanup(&sd->entity);
+
+ return ret;
+}
+
+void rzv2h_ivc_deinit_subdevice(struct rzv2h_ivc *ivc)
+{
+ struct v4l2_subdev *sd = &ivc->subdev.sd;
+
+ v4l2_subdev_cleanup(sd);
+ media_entity_remove_links(&sd->entity);
+ v4l2_async_unregister_subdev(sd);
+ media_entity_cleanup(&sd->entity);
+}
diff --git a/drivers/media/platform/renesas/rzv2h-ivc/rzv2h-ivc-video.c b/drivers/media/platform/renesas/rzv2h-ivc/rzv2h-ivc-video.c
new file mode 100644
index 0000000000000000000000000000000000000000..005a5700b5e2351b1e7ba5d99539ce4468f3db8b
--- /dev/null
+++ b/drivers/media/platform/renesas/rzv2h-ivc/rzv2h-ivc-video.c
@@ -0,0 +1,568 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Renesas RZ/V2H Input Video Control Block driver
+ *
+ * Copyright (C) 2025 Ideas on Board Oy
+ */
+
+#include "rzv2h-ivc.h"
+
+#include <linux/cleanup.h>
+#include <linux/iopoll.h>
+#include <linux/media-bus-format.h>
+#include <linux/minmax.h>
+#include <linux/mutex.h>
+#include <linux/pm_runtime.h>
+
+#include <media/mipi-csi2.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-dev.h>
+#include <media/v4l2-event.h>
+#include <media/v4l2-fh.h>
+#include <media/v4l2-ioctl.h>
+#include <media/videobuf2-dma-contig.h>
+
+#define RZV2H_IVC_FIXED_HBLANK 0x20
+#define RZV2H_IVC_MIN_VBLANK(hts) max(0x1b, 15 + (120501 / (hts)))
+
+struct rzv2h_ivc_buf {
+ struct vb2_v4l2_buffer vb;
+ struct list_head queue;
+ dma_addr_t addr;
+};
+
+#define to_rzv2h_ivc_buf(vbuf) \
+ container_of(vbuf, struct rzv2h_ivc_buf, vb)
+
+static const struct rzv2h_ivc_format rzv2h_ivc_formats[] = {
+ {
+ .fourcc = V4L2_PIX_FMT_SBGGR8,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SBGGR8_1X8,
+ },
+ .dtype = MIPI_CSI2_DT_RAW8,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_SGBRG8,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SGBRG8_1X8,
+ },
+ .dtype = MIPI_CSI2_DT_RAW8,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_SGRBG8,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SGRBG8_1X8,
+ },
+ .dtype = MIPI_CSI2_DT_RAW8,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_SRGGB8,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SRGGB8_1X8,
+ },
+ .dtype = MIPI_CSI2_DT_RAW8,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_RAW_CRU10,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SBGGR10_1X10,
+ MEDIA_BUS_FMT_SGBRG10_1X10,
+ MEDIA_BUS_FMT_SGRBG10_1X10,
+ MEDIA_BUS_FMT_SRGGB10_1X10
+ },
+ .dtype = MIPI_CSI2_DT_RAW10,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_RAW_CRU12,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SBGGR12_1X12,
+ MEDIA_BUS_FMT_SGBRG12_1X12,
+ MEDIA_BUS_FMT_SGRBG12_1X12,
+ MEDIA_BUS_FMT_SRGGB12_1X12
+ },
+ .dtype = MIPI_CSI2_DT_RAW12,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_RAW_CRU14,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SBGGR14_1X14,
+ MEDIA_BUS_FMT_SGBRG14_1X14,
+ MEDIA_BUS_FMT_SGRBG14_1X14,
+ MEDIA_BUS_FMT_SRGGB14_1X14
+ },
+ .dtype = MIPI_CSI2_DT_RAW14,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_SBGGR16,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SBGGR16_1X16,
+ },
+ .dtype = MIPI_CSI2_DT_RAW16,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_SGBRG16,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SGBRG16_1X16,
+ },
+ .dtype = MIPI_CSI2_DT_RAW16,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_SGRBG16,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SGRBG16_1X16,
+ },
+ .dtype = MIPI_CSI2_DT_RAW16,
+ },
+ {
+ .fourcc = V4L2_PIX_FMT_SRGGB16,
+ .mbus_codes = {
+ MEDIA_BUS_FMT_SRGGB16_1X16,
+ },
+ .dtype = MIPI_CSI2_DT_RAW16,
+ },
+};
+
+static void rzv2h_ivc_transfer_buffer(struct work_struct *work)
+{
+ struct rzv2h_ivc *ivc = container_of(work, struct rzv2h_ivc,
+ buffers.work);
+ struct rzv2h_ivc_buf *buf;
+
+ scoped_guard(spinlock, &ivc->buffers.lock) {
+ if (ivc->buffers.curr) {
+ ivc->buffers.curr->vb.sequence = ivc->buffers.sequence++;
+ vb2_buffer_done(&ivc->buffers.curr->vb.vb2_buf,
+ VB2_BUF_STATE_DONE);
+ ivc->buffers.curr = NULL;
+ }
+
+ buf = list_first_entry_or_null(&ivc->buffers.queue,
+ struct rzv2h_ivc_buf, queue);
+ }
+
+ if (!buf)
+ return;
+
+ list_del(&buf->queue);
+
+ ivc->buffers.curr = buf;
+ buf->addr = vb2_dma_contig_plane_dma_addr(&buf->vb.vb2_buf, 0);
+ rzv2h_ivc_write(ivc, RZV2H_IVC_REG_AXIRX_SADDL_P0, buf->addr);
+
+ scoped_guard(spinlock_irqsave, &ivc->spinlock) {
+ ivc->vvalid_ifp = 2;
+ }
+ rzv2h_ivc_write(ivc, RZV2H_IVC_REG_FM_FRCON, 0x1);
+}
+
+static int rzv2h_ivc_pipeline_started(struct media_entity *entity)
+{
+ struct video_device *vdev = media_entity_to_video_device(entity);
+ struct rzv2h_ivc *ivc = video_get_drvdata(vdev);
+
+ guard(spinlock)(&ivc->buffers.lock);
+
+ if (list_empty(&ivc->buffers.queue)) {
+ /*
+ * The driver waits for interrupts to send a new frame and
+ * tracks their receipt in the vvalid_ifp variable. .buf_queue()
+ * will queue work if vvalid_ifp == 0 to trigger a new frame (an
+ * event that normally would only occur if no buffer was ready
+ * when the interrupt arrived). If there are no buffers in the
+ * queue yet, we set vvalid_ifp to zero so that the next queue
+ * will trigger the work.
+ */
+ scoped_guard(spinlock_irqsave, &ivc->spinlock) {
+ ivc->vvalid_ifp = 0;
+ }
+ } else {
+ queue_work(ivc->buffers.async_wq, &ivc->buffers.work);
+ }
+
+ return 0;
+}
+
+static void rzv2h_ivc_pipeline_stopped(struct media_entity *entity)
+{
+ struct video_device *vdev = media_entity_to_video_device(entity);
+ struct rzv2h_ivc *ivc = video_get_drvdata(vdev);
+ u32 val = 0;
+
+ rzv2h_ivc_write(ivc, RZV2H_IVC_REG_FM_STOP, 0x1);
+ readl_poll_timeout(ivc->base + RZV2H_IVC_REG_FM_STOP,
+ val, !val, 10 * USEC_PER_MSEC, 250 * USEC_PER_MSEC);
+}
+
+static const struct media_entity_operations rzv2h_ivc_media_ops = {
+ .pipeline_started = rzv2h_ivc_pipeline_started,
+ .pipeline_stopped = rzv2h_ivc_pipeline_stopped,
+};
+
+static int rzv2h_ivc_queue_setup(struct vb2_queue *q, unsigned int *num_buffers,
+ unsigned int *num_planes, unsigned int sizes[],
+ struct device *alloc_devs[])
+{
+ struct rzv2h_ivc *ivc = vb2_get_drv_priv(q);
+
+ if (*num_planes && *num_planes > 1)
+ return -EINVAL;
+
+ if (sizes[0] && sizes[0] < ivc->format.pix.plane_fmt[0].sizeimage)
+ return -EINVAL;
+
+ *num_planes = 1;
+
+ if (!sizes[0])
+ sizes[0] = ivc->format.pix.plane_fmt[0].sizeimage;
+
+ return 0;
+}
+
+static void rzv2h_ivc_buf_queue(struct vb2_buffer *vb)
+{
+ struct rzv2h_ivc *ivc = vb2_get_drv_priv(vb->vb2_queue);
+ struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
+ struct rzv2h_ivc_buf *buf = to_rzv2h_ivc_buf(vbuf);
+
+ scoped_guard(spinlock, &ivc->buffers.lock) {
+ list_add_tail(&buf->queue, &ivc->buffers.queue);
+ }
+
+ scoped_guard(spinlock_irqsave, &ivc->spinlock) {
+ if (vb2_is_streaming(vb->vb2_queue) && !ivc->vvalid_ifp)
+ queue_work(ivc->buffers.async_wq, &ivc->buffers.work);
+ }
+}
+
+static void rzv2h_ivc_format_configure(struct rzv2h_ivc *ivc)
+{
+ const struct rzv2h_ivc_format *fmt = ivc->format.fmt;
+ struct v4l2_pix_format_mplane *pix = &ivc->format.pix;
+ unsigned int vblank;
+ unsigned int hts;
+
+ /* Currently only CRU packed pixel formats are supported */
+ rzv2h_ivc_write(ivc, RZV2H_IVC_REG_AXIRX_PXFMT,
+ RZV2H_IVC_INPUT_FMT_CRU_PACKED);
+
+ rzv2h_ivc_update_bits(ivc, RZV2H_IVC_REG_AXIRX_PXFMT,
+ RZV2H_IVC_PXFMT_DTYPE, fmt->dtype);
+
+ rzv2h_ivc_write(ivc, RZV2H_IVC_REG_AXIRX_HSIZE, pix->width);
+ rzv2h_ivc_write(ivc, RZV2H_IVC_REG_AXIRX_VSIZE, pix->height);
+ rzv2h_ivc_write(ivc, RZV2H_IVC_REG_AXIRX_STRD,
+ pix->plane_fmt[0].bytesperline);
+
+ /*
+ * The ISP has minimum vertical blanking requirements that must be
+ * adhered to by the IVC. The minimum is a function of the Iridix blocks
+ * clocking requirements and the width of the image and horizontal
+ * blanking, but if we assume the worst case then it boils down to the
+ * below (plus one to the numerator to ensure the answer is rounded up)
+ */
+
+ hts = pix->width + RZV2H_IVC_FIXED_HBLANK;
+ vblank = RZV2H_IVC_MIN_VBLANK(hts);
+
+ rzv2h_ivc_write(ivc, RZV2H_IVC_REG_AXIRX_BLANK,
+ RZV2H_IVC_VBLANK(vblank));
+}
+
+static void rzv2h_ivc_return_buffers(struct rzv2h_ivc *ivc,
+ enum vb2_buffer_state state)
+{
+ struct rzv2h_ivc_buf *buf, *tmp;
+
+ guard(spinlock)(&ivc->buffers.lock);
+
+ if (ivc->buffers.curr) {
+ vb2_buffer_done(&ivc->buffers.curr->vb.vb2_buf, state);
+ ivc->buffers.curr = NULL;
+ }
+
+ list_for_each_entry_safe(buf, tmp, &ivc->buffers.queue, queue) {
+ list_del(&buf->queue);
+ vb2_buffer_done(&buf->vb.vb2_buf, state);
+ }
+}
+
+static int rzv2h_ivc_start_streaming(struct vb2_queue *q, unsigned int count)
+{
+ struct rzv2h_ivc *ivc = vb2_get_drv_priv(q);
+ int ret;
+
+ ivc->buffers.sequence = 0;
+ ivc->vvalid_ifp = 2;
+
+ ret = pm_runtime_resume_and_get(ivc->dev);
+ if (ret)
+ goto err_return_buffers;
+
+ ret = video_device_pipeline_alloc_start(&ivc->vdev.dev);
+ if (ret) {
+ dev_err(ivc->dev, "failed to start media pipeline\n");
+ goto err_pm_runtime_put;
+ }
+
+ rzv2h_ivc_format_configure(ivc);
+
+ ret = video_device_pipeline_started(&ivc->vdev.dev);
+ if (ret < 0)
+ goto err_stop_pipeline;
+
+ return 0;
+
+err_stop_pipeline:
+ video_device_pipeline_stop(&ivc->vdev.dev);
+err_pm_runtime_put:
+ pm_runtime_put(ivc->dev);
+err_return_buffers:
+ rzv2h_ivc_return_buffers(ivc, VB2_BUF_STATE_QUEUED);
+
+ return ret;
+}
+
+static void rzv2h_ivc_stop_streaming(struct vb2_queue *q)
+{
+ struct rzv2h_ivc *ivc = vb2_get_drv_priv(q);
+
+ video_device_pipeline_stopped(&ivc->vdev.dev);
+ rzv2h_ivc_return_buffers(ivc, VB2_BUF_STATE_ERROR);
+ video_device_pipeline_stop(&ivc->vdev.dev);
+ pm_runtime_mark_last_busy(ivc->dev);
+ pm_runtime_put_autosuspend(ivc->dev);
+}
+
+static const struct vb2_ops rzv2h_ivc_vb2_ops = {
+ .queue_setup = &rzv2h_ivc_queue_setup,
+ .buf_queue = &rzv2h_ivc_buf_queue,
+ .wait_prepare = vb2_ops_wait_prepare,
+ .wait_finish = vb2_ops_wait_finish,
+ .start_streaming = &rzv2h_ivc_start_streaming,
+ .stop_streaming = &rzv2h_ivc_stop_streaming,
+};
+
+static const struct rzv2h_ivc_format *
+rzv2h_ivc_format_from_pixelformat(u32 fourcc)
+{
+ for (unsigned int i = 0; i < ARRAY_SIZE(rzv2h_ivc_formats); i++)
+ if (fourcc == rzv2h_ivc_formats[i].fourcc)
+ return &rzv2h_ivc_formats[i];
+
+ return &rzv2h_ivc_formats[0];
+}
+
+static int rzv2h_ivc_enum_fmt_vid_out(struct file *file, void *fh,
+ struct v4l2_fmtdesc *f)
+{
+ if (f->index >= ARRAY_SIZE(rzv2h_ivc_formats))
+ return -EINVAL;
+
+ f->pixelformat = rzv2h_ivc_formats[f->index].fourcc;
+ return 0;
+}
+
+static int rzv2h_ivc_g_fmt_vid_out(struct file *file, void *fh,
+ struct v4l2_format *f)
+{
+ struct rzv2h_ivc *ivc = video_drvdata(file);
+
+ f->fmt.pix_mp = ivc->format.pix;
+
+ return 0;
+}
+
+static void rzv2h_ivc_try_fmt(struct v4l2_pix_format_mplane *pix,
+ const struct rzv2h_ivc_format *fmt)
+{
+ pix->pixelformat = fmt->fourcc;
+
+ pix->width = clamp(pix->width, RZV2H_IVC_MIN_WIDTH,
+ RZV2H_IVC_MAX_WIDTH);
+ pix->height = clamp(pix->height, RZV2H_IVC_MIN_HEIGHT,
+ RZV2H_IVC_MAX_HEIGHT);
+
+ pix->field = V4L2_FIELD_NONE;
+ pix->colorspace = V4L2_COLORSPACE_RAW;
+ pix->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(pix->colorspace);
+ pix->quantization = V4L2_MAP_QUANTIZATION_DEFAULT(true,
+ pix->colorspace,
+ pix->ycbcr_enc);
+
+ v4l2_fill_pixfmt_mp(pix, pix->pixelformat, pix->width, pix->height);
+}
+
+static void rzv2h_ivc_set_format(struct rzv2h_ivc *ivc,
+ struct v4l2_pix_format_mplane *pix)
+{
+ const struct rzv2h_ivc_format *fmt;
+
+ fmt = rzv2h_ivc_format_from_pixelformat(pix->pixelformat);
+
+ rzv2h_ivc_try_fmt(pix, fmt);
+ ivc->format.pix = *pix;
+ ivc->format.fmt = fmt;
+}
+
+static int rzv2h_ivc_s_fmt_vid_out(struct file *file, void *fh,
+ struct v4l2_format *f)
+{
+ struct rzv2h_ivc *ivc = video_drvdata(file);
+ struct v4l2_pix_format_mplane *pix = &f->fmt.pix_mp;
+
+ if (vb2_is_busy(&ivc->vdev.vb2q))
+ return -EBUSY;
+
+ rzv2h_ivc_set_format(ivc, pix);
+
+ return 0;
+}
+
+static int rzv2h_ivc_try_fmt_vid_out(struct file *file, void *fh,
+ struct v4l2_format *f)
+{
+ const struct rzv2h_ivc_format *fmt;
+
+ fmt = rzv2h_ivc_format_from_pixelformat(f->fmt.pix.pixelformat);
+ rzv2h_ivc_try_fmt(&f->fmt.pix_mp, fmt);
+
+ return 0;
+}
+
+static int rzv2h_ivc_querycap(struct file *file, void *fh,
+ struct v4l2_capability *cap)
+{
+ strscpy(cap->driver, "rzv2h-ivc", sizeof(cap->driver));
+ strscpy(cap->card, "Renesas Input Video Control", sizeof(cap->card));
+
+ return 0;
+}
+
+static const struct v4l2_ioctl_ops rzv2h_ivc_v4l2_ioctl_ops = {
+ .vidioc_reqbufs = vb2_ioctl_reqbufs,
+ .vidioc_querybuf = vb2_ioctl_querybuf,
+ .vidioc_create_bufs = vb2_ioctl_create_bufs,
+ .vidioc_qbuf = vb2_ioctl_qbuf,
+ .vidioc_expbuf = vb2_ioctl_expbuf,
+ .vidioc_dqbuf = vb2_ioctl_dqbuf,
+ .vidioc_prepare_buf = vb2_ioctl_prepare_buf,
+ .vidioc_streamon = vb2_ioctl_streamon,
+ .vidioc_streamoff = vb2_ioctl_streamoff,
+ .vidioc_enum_fmt_vid_out = rzv2h_ivc_enum_fmt_vid_out,
+ .vidioc_g_fmt_vid_out_mplane = rzv2h_ivc_g_fmt_vid_out,
+ .vidioc_s_fmt_vid_out_mplane = rzv2h_ivc_s_fmt_vid_out,
+ .vidioc_try_fmt_vid_out_mplane = rzv2h_ivc_try_fmt_vid_out,
+ .vidioc_querycap = rzv2h_ivc_querycap,
+ .vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
+ .vidioc_unsubscribe_event = v4l2_event_unsubscribe,
+};
+
+static const struct v4l2_file_operations rzv2h_ivc_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,
+};
+
+int rzv2h_ivc_init_vdev(struct rzv2h_ivc *ivc, struct v4l2_device *v4l2_dev)
+{
+ struct v4l2_pix_format_mplane pix = { };
+ struct video_device *vdev;
+ struct vb2_queue *vb2q;
+ int ret;
+
+ spin_lock_init(&ivc->buffers.lock);
+ INIT_LIST_HEAD(&ivc->buffers.queue);
+ INIT_WORK(&ivc->buffers.work, rzv2h_ivc_transfer_buffer);
+
+ ivc->buffers.async_wq = alloc_workqueue("rzv2h-ivc", 0, 0);
+ if (!ivc->buffers.async_wq)
+ return -EINVAL;
+
+ /* Initialise vb2 queue */
+ vb2q = &ivc->vdev.vb2q;
+ vb2q->type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
+ vb2q->io_modes = VB2_MMAP | VB2_DMABUF;
+ vb2q->drv_priv = ivc;
+ vb2q->mem_ops = &vb2_dma_contig_memops;
+ vb2q->ops = &rzv2h_ivc_vb2_ops;
+ vb2q->buf_struct_size = sizeof(struct rzv2h_ivc_buf);
+ vb2q->min_queued_buffers = 0;
+ vb2q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
+ vb2q->lock = &ivc->lock;
+ vb2q->dev = ivc->dev;
+
+ ret = vb2_queue_init(vb2q);
+ if (ret) {
+ dev_err(ivc->dev, "vb2 queue init failed\n");
+ goto err_destroy_workqueue;
+ }
+
+ /* Initialise Video Device */
+ vdev = &ivc->vdev.dev;
+ strscpy(vdev->name, "rzv2h-ivc", sizeof(vdev->name));
+ vdev->release = video_device_release_empty;
+ vdev->fops = &rzv2h_ivc_v4l2_fops;
+ vdev->ioctl_ops = &rzv2h_ivc_v4l2_ioctl_ops;
+ vdev->lock = &ivc->lock;
+ vdev->v4l2_dev = v4l2_dev;
+ vdev->queue = vb2q;
+ vdev->device_caps = V4L2_CAP_VIDEO_OUTPUT_MPLANE | V4L2_CAP_STREAMING;
+ vdev->vfl_dir = VFL_DIR_TX;
+ video_set_drvdata(vdev, ivc);
+
+ pix.pixelformat = V4L2_PIX_FMT_SRGGB16;
+ pix.width = RZV2H_IVC_DEFAULT_WIDTH;
+ pix.height = RZV2H_IVC_DEFAULT_HEIGHT;
+ rzv2h_ivc_set_format(ivc, &pix);
+
+ ivc->vdev.pad.flags = MEDIA_PAD_FL_SOURCE;
+ ivc->vdev.dev.entity.ops = &rzv2h_ivc_media_ops;
+ ret = media_entity_pads_init(&ivc->vdev.dev.entity, 1, &ivc->vdev.pad);
+ if (ret)
+ goto err_release_vb2q;
+
+ ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
+ if (ret) {
+ dev_err(ivc->dev, "failed to register IVC video device\n");
+ goto err_cleanup_vdev_entity;
+ }
+
+ ret = media_create_pad_link(&vdev->entity, 0, &ivc->subdev.sd.entity,
+ RZV2H_IVC_SUBDEV_SINK_PAD,
+ MEDIA_LNK_FL_ENABLED |
+ MEDIA_LNK_FL_IMMUTABLE);
+ if (ret) {
+ dev_err(ivc->dev, "failed to create media link\n");
+ goto err_unregister_vdev;
+ }
+
+ return 0;
+
+err_unregister_vdev:
+ video_unregister_device(vdev);
+err_cleanup_vdev_entity:
+ media_entity_cleanup(&vdev->entity);
+err_release_vb2q:
+ vb2_queue_release(vb2q);
+err_destroy_workqueue:
+ destroy_workqueue(ivc->buffers.async_wq);
+
+ return ret;
+}
+
+void rzv2h_deinit_video_dev_and_queue(struct rzv2h_ivc *ivc)
+{
+ struct video_device *vdev = &ivc->vdev.dev;
+ struct vb2_queue *vb2q = &ivc->vdev.vb2q;
+
+ if (!ivc->sched)
+ return;
+
+ vb2_video_unregister_device(vdev);
+ media_entity_cleanup(&vdev->entity);
+ vb2_queue_release(vb2q);
+}
diff --git a/drivers/media/platform/renesas/rzv2h-ivc/rzv2h-ivc.h b/drivers/media/platform/renesas/rzv2h-ivc/rzv2h-ivc.h
new file mode 100644
index 0000000000000000000000000000000000000000..709c6a9398fe2484c2acb03d443d58ea4e153a66
--- /dev/null
+++ b/drivers/media/platform/renesas/rzv2h-ivc/rzv2h-ivc.h
@@ -0,0 +1,131 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Renesas RZ/V2H Input Video Control Block driver
+ *
+ * Copyright (C) 2025 Ideas on Board Oy
+ */
+
+#include <linux/clk.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/reset.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+#include <linux/videodev2.h>
+#include <linux/workqueue.h>
+
+#include <media/media-entity.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-subdev.h>
+#include <media/videobuf2-core.h>
+#include <media/videobuf2-v4l2.h>
+
+#define RZV2H_IVC_REG_AXIRX_PLNUM 0x0000
+#define RZV2H_IVC_ONE_EXPOSURE 0x00
+#define RZV2H_IVC_TWO_EXPOSURE 0x01
+#define RZV2H_IVC_REG_AXIRX_PXFMT 0x0004
+#define RZV2H_IVC_INPUT_FMT_MIPI (0 << 16)
+#define RZV2H_IVC_INPUT_FMT_CRU_PACKED (1 << 16)
+#define RZV2H_IVC_PXFMT_DTYPE GENMASK(7, 0)
+#define RZV2H_IVC_REG_AXIRX_SADDL_P0 0x0010
+#define RZV2H_IVC_REG_AXIRX_SADDH_P0 0x0014
+#define RZV2H_IVC_REG_AXIRX_SADDL_P1 0x0018
+#define RZV2H_IVC_REG_AXIRX_SADDH_P1 0x001c
+#define RZV2H_IVC_REG_AXIRX_HSIZE 0x0020
+#define RZV2H_IVC_REG_AXIRX_VSIZE 0x0024
+#define RZV2H_IVC_REG_AXIRX_BLANK 0x0028
+#define RZV2H_IVC_VBLANK(x) ((x) << 16)
+#define RZV2H_IVC_REG_AXIRX_STRD 0x0030
+#define RZV2H_IVC_REG_AXIRX_ISSU 0x0040
+#define RZV2H_IVC_REG_AXIRX_ERACT 0x0048
+#define RZV2H_IVC_REG_FM_CONTEXT 0x0100
+#define RZV2H_IVC_SOFTWARE_CFG 0x00
+#define RZV2H_IVC_SINGLE_CONTEXT_SW_HW_CFG BIT(0)
+#define RZV2H_IVC_MULTI_CONTEXT_SW_HW_CFG BIT(1)
+#define RZV2H_IVC_REG_FM_MCON 0x0104
+#define RZV2H_IVC_REG_FM_FRCON 0x0108
+#define RZV2H_IVC_REG_FM_STOP 0x010c
+#define RZV2H_IVC_REG_FM_INT_EN 0x0120
+#define RZV2H_IVC_VVAL_IFPE BIT(0)
+#define RZV2H_IVC_REG_FM_INT_STA 0x0124
+#define RZV2H_IVC_REG_AXIRX_FIFOCAP0 0x0208
+#define RZV2H_IVC_REG_CORE_CAPCON 0x020c
+#define RZV2H_IVC_REG_CORE_FIFOCAP0 0x0228
+#define RZV2H_IVC_REG_CORE_FIFOCAP1 0x022c
+
+#define RZV2H_IVC_MIN_WIDTH 640
+#define RZV2H_IVC_MAX_WIDTH 4096
+#define RZV2H_IVC_MIN_HEIGHT 480
+#define RZV2H_IVC_MAX_HEIGHT 4096
+#define RZV2H_IVC_DEFAULT_WIDTH 1920
+#define RZV2H_IVC_DEFAULT_HEIGHT 1080
+
+#define RZV2H_IVC_NUM_HW_RESOURCES 3
+
+struct device;
+
+enum rzv2h_ivc_subdev_pads {
+ RZV2H_IVC_SUBDEV_SINK_PAD,
+ RZV2H_IVC_SUBDEV_SOURCE_PAD,
+ RZV2H_IVC_NUM_SUBDEV_PADS
+};
+
+struct rzv2h_ivc_format {
+ u32 fourcc;
+ /*
+ * The CRU packed pixel formats are bayer-order agnostic, so each could
+ * support any one of the 4 possible media bus formats.
+ */
+ u32 mbus_codes[4];
+ u8 dtype;
+};
+
+struct rzv2h_ivc {
+ struct device *dev;
+ void __iomem *base;
+ struct clk_bulk_data clks[RZV2H_IVC_NUM_HW_RESOURCES];
+ struct reset_control_bulk_data resets[RZV2H_IVC_NUM_HW_RESOURCES];
+ int irqnum;
+ u8 vvalid_ifp;
+
+ struct {
+ struct video_device dev;
+ struct vb2_queue vb2q;
+ struct media_pad pad;
+ } vdev;
+
+ struct {
+ struct v4l2_subdev sd;
+ struct media_pad pads[RZV2H_IVC_NUM_SUBDEV_PADS];
+ } subdev;
+
+ struct {
+ /* Spinlock to guard buffer queue */
+ spinlock_t lock;
+ struct workqueue_struct *async_wq;
+ struct work_struct work;
+ struct list_head queue;
+ struct rzv2h_ivc_buf *curr;
+ unsigned int sequence;
+ } buffers;
+
+ struct media_job_scheduler *sched;