Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Daniel Scally 1551 100.00% 1 100.00%
Total 1551 1


// SPDX-License-Identifier: GPL-2.0
/*
 * Renesas RZ/V2H(P) 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);
}