// SPDX-License-Identifier: GPL-2.0
/*
 * V4L2 Capture ISI subdev driver for i.MX8QXP/QM platform
 *
 * ISI is a Image Sensor Interface of i.MX8QXP/QM platform, which
 * used to process image from camera sensor to memory or DC
 *
 * Copyright (c) 2019 NXP Semiconductor
 */

#include <linux/device.h>
#include <linux/dma-mapping.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/media-bus-format.h>
#include <linux/minmax.h>
#include <linux/pm_runtime.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/videodev2.h>

#include <media/media-entity.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/v4l2-subdev.h>
#include <media/videobuf2-core.h>
#include <media/videobuf2-dma-contig.h>
#include <media/videobuf2-v4l2.h>

#include "imx8-isi-core.h"
#include "imx8-isi-regs.h"

/* Keep the first entry matching MXC_ISI_DEF_PIXEL_FORMAT */
static const struct mxc_isi_format_info mxc_isi_formats[] = {
	/* YUV formats */
	{
		.mbus_code	= MEDIA_BUS_FMT_YUV8_1X24,
		.fourcc		= V4L2_PIX_FMT_YUYV,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_OUT
				| MXC_ISI_VIDEO_M2M_CAP,
		.isi_in_format	= CHNL_MEM_RD_CTRL_IMG_TYPE_YUV422_1P8P,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_YUV422_1P8P,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_YUV,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_YUV8_1X24,
		.fourcc		= V4L2_PIX_FMT_YUVA32,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_YUV444_1P8,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 32 },
		.encoding	= MXC_ISI_ENC_YUV,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_YUV8_1X24,
		.fourcc		= V4L2_PIX_FMT_NV12,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_YUV420_2P8P,
		.color_planes	= 2,
		.mem_planes	= 1,
		.depth		= { 8, 16 },
		.hsub		= 2,
		.vsub		= 2,
		.encoding	= MXC_ISI_ENC_YUV,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_YUV8_1X24,
		.fourcc		= V4L2_PIX_FMT_NV12M,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_YUV420_2P8P,
		.mem_planes	= 2,
		.color_planes	= 2,
		.depth		= { 8, 16 },
		.hsub		= 2,
		.vsub		= 2,
		.encoding	= MXC_ISI_ENC_YUV,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_YUV8_1X24,
		.fourcc		= V4L2_PIX_FMT_NV16,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_YUV422_2P8P,
		.color_planes	= 2,
		.mem_planes	= 1,
		.depth		= { 8, 16 },
		.hsub		= 2,
		.vsub		= 1,
		.encoding	= MXC_ISI_ENC_YUV,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_YUV8_1X24,
		.fourcc		= V4L2_PIX_FMT_NV16M,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_YUV422_2P8P,
		.mem_planes	= 2,
		.color_planes	= 2,
		.depth		= { 8, 16 },
		.hsub		= 2,
		.vsub		= 1,
		.encoding	= MXC_ISI_ENC_YUV,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_YUV8_1X24,
		.fourcc		= V4L2_PIX_FMT_YUV444M,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_YUV444_3P8P,
		.mem_planes	= 3,
		.color_planes	= 3,
		.depth		= { 8, 8, 8 },
		.hsub		= 1,
		.vsub		= 1,
		.encoding	= MXC_ISI_ENC_YUV,
	},
	/* RGB formats */
	{
		.mbus_code	= MEDIA_BUS_FMT_RGB888_1X24,
		.fourcc		= V4L2_PIX_FMT_RGB565,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_OUT
				| MXC_ISI_VIDEO_M2M_CAP,
		.isi_in_format	= CHNL_MEM_RD_CTRL_IMG_TYPE_RGB565,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RGB565,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RGB,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_RGB888_1X24,
		.fourcc		= V4L2_PIX_FMT_RGB24,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_OUT
				| MXC_ISI_VIDEO_M2M_CAP,
		.isi_in_format	= CHNL_MEM_RD_CTRL_IMG_TYPE_BGR8P,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_BGR888P,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 24 },
		.encoding	= MXC_ISI_ENC_RGB,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_RGB888_1X24,
		.fourcc		= V4L2_PIX_FMT_BGR24,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_OUT
				| MXC_ISI_VIDEO_M2M_CAP,
		.isi_in_format	= CHNL_MEM_RD_CTRL_IMG_TYPE_RGB8P,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RGB888P,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 24 },
		.encoding	= MXC_ISI_ENC_RGB,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_RGB888_1X24,
		.fourcc		= V4L2_PIX_FMT_XBGR32,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_OUT
				| MXC_ISI_VIDEO_M2M_CAP,
		.isi_in_format	= CHNL_MEM_RD_CTRL_IMG_TYPE_XBGR8,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_XRGB888,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 32 },
		.encoding	= MXC_ISI_ENC_RGB,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_RGB888_1X24,
		.fourcc		= V4L2_PIX_FMT_ABGR32,
		.type		= MXC_ISI_VIDEO_CAP | MXC_ISI_VIDEO_M2M_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_ARGB8888,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 32 },
		.encoding	= MXC_ISI_ENC_RGB,
	},
	/*
	 * RAW formats
	 *
	 * The ISI shifts the 10-bit and 12-bit formats left by 6 and 4 bits
	 * when using CHNL_IMG_CTRL_FORMAT_RAW10 or MXC_ISI_OUT_FMT_RAW12
	 * respectively, to align the bits to the left and pad with zeros in
	 * the LSBs. The corresponding V4L2 formats are however right-aligned,
	 * we have to use CHNL_IMG_CTRL_FORMAT_RAW16 to avoid the left shift.
	 */
	{
		.mbus_code	= MEDIA_BUS_FMT_Y8_1X8,
		.fourcc		= V4L2_PIX_FMT_GREY,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW8,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 8 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_Y10_1X10,
		.fourcc		= V4L2_PIX_FMT_Y10,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_Y12_1X12,
		.fourcc		= V4L2_PIX_FMT_Y12,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_Y14_1X14,
		.fourcc		= V4L2_PIX_FMT_Y14,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SBGGR8_1X8,
		.fourcc		= V4L2_PIX_FMT_SBGGR8,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW8,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 8 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SGBRG8_1X8,
		.fourcc		= V4L2_PIX_FMT_SGBRG8,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW8,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 8 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SGRBG8_1X8,
		.fourcc		= V4L2_PIX_FMT_SGRBG8,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW8,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 8 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SRGGB8_1X8,
		.fourcc		= V4L2_PIX_FMT_SRGGB8,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW8,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 8 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SBGGR10_1X10,
		.fourcc		= V4L2_PIX_FMT_SBGGR10,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SGBRG10_1X10,
		.fourcc		= V4L2_PIX_FMT_SGBRG10,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SGRBG10_1X10,
		.fourcc		= V4L2_PIX_FMT_SGRBG10,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SRGGB10_1X10,
		.fourcc		= V4L2_PIX_FMT_SRGGB10,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SBGGR12_1X12,
		.fourcc		= V4L2_PIX_FMT_SBGGR12,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SGBRG12_1X12,
		.fourcc		= V4L2_PIX_FMT_SGBRG12,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SGRBG12_1X12,
		.fourcc		= V4L2_PIX_FMT_SGRBG12,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SRGGB12_1X12,
		.fourcc		= V4L2_PIX_FMT_SRGGB12,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SBGGR14_1X14,
		.fourcc		= V4L2_PIX_FMT_SBGGR14,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SGBRG14_1X14,
		.fourcc		= V4L2_PIX_FMT_SGBRG14,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SGRBG14_1X14,
		.fourcc		= V4L2_PIX_FMT_SGRBG14,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	}, {
		.mbus_code	= MEDIA_BUS_FMT_SRGGB14_1X14,
		.fourcc		= V4L2_PIX_FMT_SRGGB14,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW16,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 16 },
		.encoding	= MXC_ISI_ENC_RAW,
	},
	/* JPEG */
	{
		.mbus_code	= MEDIA_BUS_FMT_JPEG_1X8,
		.fourcc		= V4L2_PIX_FMT_MJPEG,
		.type		= MXC_ISI_VIDEO_CAP,
		.isi_out_format	= CHNL_IMG_CTRL_FORMAT_RAW8,
		.mem_planes	= 1,
		.color_planes	= 1,
		.depth		= { 8 },
		.encoding	= MXC_ISI_ENC_RAW,
	}
};

const struct mxc_isi_format_info *
mxc_isi_format_by_fourcc(u32 fourcc, enum mxc_isi_video_type type)
{
	unsigned int i;

	for (i = 0; i < ARRAY_SIZE(mxc_isi_formats); i++) {
		const struct mxc_isi_format_info *fmt = &mxc_isi_formats[i];

		if (fmt->fourcc == fourcc && fmt->type & type)
			return fmt;
	}

	return NULL;
}

const struct mxc_isi_format_info *
mxc_isi_format_enum(unsigned int index, enum mxc_isi_video_type type)
{
	unsigned int i;

	for (i = 0; i < ARRAY_SIZE(mxc_isi_formats); i++) {
		const struct mxc_isi_format_info *fmt = &mxc_isi_formats[i];

		if (!(fmt->type & type))
			continue;

		if (!index)
			return fmt;

		index--;
	}

	return NULL;
}

const struct mxc_isi_format_info *
mxc_isi_format_try(struct mxc_isi_pipe *pipe, struct v4l2_pix_format_mplane *pix,
		   enum mxc_isi_video_type type)
{
	const struct mxc_isi_format_info *fmt;
	unsigned int max_width;
	unsigned int i;

	max_width = pipe->id == pipe->isi->pdata->num_channels - 1
		  ? MXC_ISI_MAX_WIDTH_UNCHAINED
		  : MXC_ISI_MAX_WIDTH_CHAINED;

	fmt = mxc_isi_format_by_fourcc(pix->pixelformat, type);
	if (!fmt)
		fmt = &mxc_isi_formats[0];

	pix->width = clamp(pix->width, MXC_ISI_MIN_WIDTH, max_width);
	pix->height = clamp(pix->height, MXC_ISI_MIN_HEIGHT, MXC_ISI_MAX_HEIGHT);
	pix->pixelformat = fmt->fourcc;
	pix->field = V4L2_FIELD_NONE;

	if (pix->colorspace == V4L2_COLORSPACE_DEFAULT) {
		pix->colorspace = MXC_ISI_DEF_COLOR_SPACE;
		pix->ycbcr_enc = MXC_ISI_DEF_YCBCR_ENC;
		pix->quantization = MXC_ISI_DEF_QUANTIZATION;
		pix->xfer_func = MXC_ISI_DEF_XFER_FUNC;
	}

	if (pix->ycbcr_enc == V4L2_YCBCR_ENC_DEFAULT)
		pix->ycbcr_enc = V4L2_MAP_YCBCR_ENC_DEFAULT(pix->colorspace);
	if (pix->quantization == V4L2_QUANTIZATION_DEFAULT) {
		bool is_rgb = fmt->encoding == MXC_ISI_ENC_RGB;

		pix->quantization =
			V4L2_MAP_QUANTIZATION_DEFAULT(is_rgb, pix->colorspace,
						      pix->ycbcr_enc);
	}
	if (pix->xfer_func == V4L2_XFER_FUNC_DEFAULT)
		pix->xfer_func = V4L2_MAP_XFER_FUNC_DEFAULT(pix->colorspace);

	pix->num_planes = fmt->mem_planes;

	for (i = 0; i < fmt->color_planes; ++i) {
		struct v4l2_plane_pix_format *plane = &pix->plane_fmt[i];
		unsigned int bpl;

		/* The pitch must be identical for all planes. */
		if (i == 0)
			bpl = clamp(plane->bytesperline,
				    pix->width * fmt->depth[0] / 8,
				    65535U);
		else
			bpl = pix->plane_fmt[0].bytesperline;

		plane->bytesperline = bpl;

		plane->sizeimage = plane->bytesperline * pix->height;
		if (i >= 1)
			plane->sizeimage /= fmt->vsub;
	}

	/*
	 * For single-planar pixel formats with multiple color planes,
	 * concatenate the size of all planes and clear all planes but the
	 * first one.
	 */
	if (fmt->color_planes != fmt->mem_planes) {
		for (i = 1; i < fmt->color_planes; ++i) {
			struct v4l2_plane_pix_format *plane = &pix->plane_fmt[i];

			pix->plane_fmt[0].sizeimage += plane->sizeimage;
			plane->bytesperline = 0;
			plane->sizeimage = 0;
		}
	}

	return fmt;
}

/* -----------------------------------------------------------------------------
 * videobuf2 queue operations
 */

static void mxc_isi_video_frame_write_done(struct mxc_isi_pipe *pipe,
					   u32 status)
{
	struct mxc_isi_video *video = &pipe->video;
	struct device *dev = pipe->isi->dev;
	struct mxc_isi_buffer *next_buf;
	struct mxc_isi_buffer *buf;
	enum mxc_isi_buf_id buf_id;

	spin_lock(&video->buf_lock);

	/*
	 * The ISI hardware handles buffers using a ping-pong mechanism with
	 * two sets of destination addresses (with shadow registers to allow
	 * programming addresses for all planes atomically) named BUF1 and
	 * BUF2. Addresses can be loaded and copied to shadow registers at any
	 * at any time.
	 *
	 * The hardware keeps track of which buffer is being written to and
	 * automatically switches to the other buffer at frame end, copying the
	 * corresponding address to another set of shadow registers that track
	 * the address being written to. The active buffer tracking bits are
	 * accessible through the CHNL_STS register.
	 *
	 *  BUF1        BUF2  |   Event   | Action
	 *                    |           |
	 *                    |           | Program initial buffers
	 *                    |           | B0 in BUF1, B1 in BUF2
	 *                    | Start ISI |
	 * +----+             |           |
	 * | B0 |             |           |
	 * +----+             |           |
	 *             +----+ | FRM IRQ 0 | B0 complete, BUF2 now active
	 *             | B1 | |           | Program B2 in BUF1
	 *             +----+ |           |
	 * +----+             | FRM IRQ 1 | B1 complete, BUF1 now active
	 * | B2 |             |           | Program B3 in BUF2
	 * +----+             |           |
	 *             +----+ | FRM IRQ 2 | B2 complete, BUF2 now active
	 *             | B3 | |           | Program B4 in BUF1
	 *             +----+ |           |
	 * +----+             | FRM IRQ 3 | B3 complete, BUF1 now active
	 * | B4 |             |           | Program B5 in BUF2
	 * +----+             |           |
	 *        ...         |           |
	 *
	 * Races between address programming and buffer switching can be
	 * detected by checking if a frame end interrupt occurred after
	 * programming the addresses.
	 *
	 * As none of the shadow registers are accessible, races can occur
	 * between address programming and buffer switching. It is possible to
	 * detect the race condition by checking if a frame end interrupt
	 * occurred after programming the addresses, but impossible to
	 * determine if the race has been won or lost.
	 *
	 * In addition to this, we need to use discard buffers if no pending
	 * buffers are available. To simplify handling of discard buffer, we
	 * need to allocate three of them, as two can be active concurrently
	 * and we need to still be able to get hold of a next buffer. The logic
	 * could be improved to use two buffers only, but as all discard
	 * buffers share the same memory, an additional buffer is cheap.
	 */

	/* Check which buffer has just completed. */
	buf_id = pipe->isi->pdata->buf_active_reverse
	       ? (status & CHNL_STS_BUF1_ACTIVE ? MXC_ISI_BUF2 : MXC_ISI_BUF1)
	       : (status & CHNL_STS_BUF1_ACTIVE ? MXC_ISI_BUF1 : MXC_ISI_BUF2);

	buf = list_first_entry_or_null(&video->out_active,
				       struct mxc_isi_buffer, list);

	/* Safety check, this should really never happen. */
	if (!buf) {
		dev_warn(dev, "trying to access empty active list\n");
		goto done;
	}

	/*
	 * If the buffer that has completed doesn't match the buffer on the
	 * front of the active list, it means we have lost one frame end
	 * interrupt (or possibly a large odd number of interrupts, although
	 * quite unlikely).
	 *
	 * For instance, if IRQ1 is lost and we handle IRQ2, both B1 and B2
	 * have been completed, but B3 hasn't been programmed, BUF2 still
	 * addresses B1 and the ISI is now writing in B1 instead of B3. We
	 * can't complete B2 as that would result in out-of-order completion.
	 *
	 * The only option is to ignore this interrupt and try again. When IRQ3
	 * will be handled, we will complete B1 and be in sync again.
	 */
	if (buf->id != buf_id) {
		dev_dbg(dev, "buffer ID mismatch (expected %u, got %u), skipping\n",
			buf->id, buf_id);

		/*
		 * Increment the frame count by two to account for the missed
		 * and the ignored interrupts.
		 */
		video->frame_count += 2;
		goto done;
	}

	/* Pick the next buffer and queue it to the hardware. */
	next_buf = list_first_entry_or_null(&video->out_pending,
					    struct mxc_isi_buffer, list);
	if (!next_buf) {
		next_buf = list_first_entry_or_null(&video->out_discard,
						    struct mxc_isi_buffer, list);

		/* Safety check, this should never happen. */
		if (!next_buf) {
			dev_warn(dev, "trying to access empty discard list\n");
			goto done;
		}
	}

	mxc_isi_channel_set_outbuf(pipe, next_buf->dma_addrs, buf_id);
	next_buf->id = buf_id;

	/*
	 * Check if we have raced with the end of frame interrupt. If so, we
	 * can't tell if the ISI has recorded the new address, or is still
	 * using the previous buffer. We must assume the latter as that is the
	 * worst case.
	 *
	 * For instance, if we are handling IRQ1 and now detect the FRM
	 * interrupt, assume B2 has completed and the ISI has switched to BUF2
	 * using B1 just before we programmed B3. Unlike in the previous race
	 * condition, B3 has been programmed and will be written to the next
	 * time the ISI switches to BUF2. We can however handle this exactly as
	 * the first race condition, as we'll program B3 (still at the head of
	 * the pending list) when handling IRQ3.
	 */
	status = mxc_isi_channel_irq_status(pipe, false);
	if (status & CHNL_STS_FRM_STRD) {
		dev_dbg(dev, "raced with frame end interrupt\n");
		video->frame_count += 2;
		goto done;
	}

	/*
	 * The next buffer has been queued successfully, move it to the active
	 * list, and complete the current buffer.
	 */
	list_move_tail(&next_buf->list, &video->out_active);

	if (!buf->discard) {
		list_del_init(&buf->list);
		buf->v4l2_buf.sequence = video->frame_count;
		buf->v4l2_buf.vb2_buf.timestamp = ktime_get_ns();
		vb2_buffer_done(&buf->v4l2_buf.vb2_buf, VB2_BUF_STATE_DONE);
	} else {
		list_move_tail(&buf->list, &video->out_discard);
	}

	video->frame_count++;

done:
	spin_unlock(&video->buf_lock);
}

static void mxc_isi_video_free_discard_buffers(struct mxc_isi_video *video)
{
	unsigned int i;

	for (i = 0; i < video->pix.num_planes; i++) {
		struct mxc_isi_dma_buffer *buf = &video->discard_buffer[i];

		if (!buf->addr)
			continue;

		dma_free_coherent(video->pipe->isi->dev, buf->size, buf->addr,
				  buf->dma);
		buf->addr = NULL;
	}
}

static int mxc_isi_video_alloc_discard_buffers(struct mxc_isi_video *video)
{
	unsigned int i, j;

	/* Allocate memory for each plane. */
	for (i = 0; i < video->pix.num_planes; i++) {
		struct mxc_isi_dma_buffer *buf = &video->discard_buffer[i];

		buf->size = PAGE_ALIGN(video->pix.plane_fmt[i].sizeimage);
		buf->addr = dma_alloc_coherent(video->pipe->isi->dev, buf->size,
					       &buf->dma, GFP_DMA | GFP_KERNEL);
		if (!buf->addr) {
			mxc_isi_video_free_discard_buffers(video);
			return -ENOMEM;
		}

		dev_dbg(video->pipe->isi->dev,
			"discard buffer plane %u: %zu bytes @%pad (CPU address %p)\n",
			i, buf->size, &buf->dma, buf->addr);
	}

	/* Fill the DMA addresses in the discard buffers. */
	for (i = 0; i < ARRAY_SIZE(video->buf_discard); ++i) {
		struct mxc_isi_buffer *buf = &video->buf_discard[i];

		buf->discard = true;

		for (j = 0; j < video->pix.num_planes; ++j)
			buf->dma_addrs[j] = video->discard_buffer[j].dma;
	}

	return 0;
}

static int mxc_isi_video_validate_format(struct mxc_isi_video *video)
{
	const struct v4l2_mbus_framefmt *format;
	const struct mxc_isi_format_info *info;
	struct v4l2_subdev_state *state;
	struct v4l2_subdev *sd = &video->pipe->sd;
	int ret = 0;

	state = v4l2_subdev_lock_and_get_active_state(sd);

	info = mxc_isi_format_by_fourcc(video->pix.pixelformat,
					MXC_ISI_VIDEO_CAP);
	format = v4l2_subdev_get_try_format(sd, state, MXC_ISI_PIPE_PAD_SOURCE);

	if (format->code != info->mbus_code ||
	    format->width != video->pix.width ||
	    format->height != video->pix.height) {
		dev_dbg(video->pipe->isi->dev,
			"%s: configuration mismatch, 0x%04x/%ux%u != 0x%04x/%ux%u\n",
			__func__, format->code, format->width, format->height,
			info->mbus_code, video->pix.width, video->pix.height);
		ret = -EINVAL;
	}

	v4l2_subdev_unlock_state(state);

	return ret;
}

static void mxc_isi_video_return_buffers(struct mxc_isi_video *video,
					 enum vb2_buffer_state state)
{
	struct mxc_isi_buffer *buf;

	spin_lock_irq(&video->buf_lock);

	while (!list_empty(&video->out_active)) {
		buf = list_first_entry(&video->out_active,
				       struct mxc_isi_buffer, list);
		list_del_init(&buf->list);
		if (buf->discard)
			continue;

		vb2_buffer_done(&buf->v4l2_buf.vb2_buf, state);
	}

	while (!list_empty(&video->out_pending)) {
		buf = list_first_entry(&video->out_pending,
				       struct mxc_isi_buffer, list);
		list_del_init(&buf->list);
		vb2_buffer_done(&buf->v4l2_buf.vb2_buf, state);
	}

	while (!list_empty(&video->out_discard)) {
		buf = list_first_entry(&video->out_discard,
				       struct mxc_isi_buffer, list);
		list_del_init(&buf->list);
	}

	INIT_LIST_HEAD(&video->out_active);
	INIT_LIST_HEAD(&video->out_pending);
	INIT_LIST_HEAD(&video->out_discard);

	spin_unlock_irq(&video->buf_lock);
}

static void mxc_isi_video_queue_first_buffers(struct mxc_isi_video *video)
{
	unsigned int discard;
	unsigned int i;

	lockdep_assert_held(&video->buf_lock);

	/*
	 * Queue two ISI channel output buffers. We are not guaranteed to have
	 * any buffer in the pending list when this function is called from the
	 * system resume handler. Use pending buffers as much as possible, and
	 * use discard buffers to fill the remaining slots.
	 */

	/* How many discard buffers do we need to queue first ? */
	discard = list_empty(&video->out_pending) ? 2
		: list_is_singular(&video->out_pending) ? 1
		: 0;

	for (i = 0; i < 2; ++i) {
		enum mxc_isi_buf_id buf_id = i == 0 ? MXC_ISI_BUF1
					   : MXC_ISI_BUF2;
		struct mxc_isi_buffer *buf;
		struct list_head *list;

		list = i < discard ? &video->out_discard : &video->out_pending;
		buf = list_first_entry(list, struct mxc_isi_buffer, list);

		mxc_isi_channel_set_outbuf(video->pipe, buf->dma_addrs, buf_id);
		buf->id = buf_id;
		list_move_tail(&buf->list, &video->out_active);
	}
}

static inline struct mxc_isi_buffer *to_isi_buffer(struct vb2_v4l2_buffer *v4l2_buf)
{
	return container_of(v4l2_buf, struct mxc_isi_buffer, v4l2_buf);
}

int mxc_isi_video_queue_setup(const struct v4l2_pix_format_mplane *format,
			      const struct mxc_isi_format_info *info,
			      unsigned int *num_buffers,
			      unsigned int *num_planes, unsigned int sizes[])
{
	unsigned int i;

	if (*num_planes) {
		if (*num_planes != info->mem_planes)
			return -EINVAL;

		for (i = 0; i < info->mem_planes; ++i) {
			if (sizes[i] < format->plane_fmt[i].sizeimage)
				return -EINVAL;
		}

		return 0;
	}

	*num_planes = info->mem_planes;

	for (i = 0; i < info->mem_planes; ++i)
		sizes[i] = format->plane_fmt[i].sizeimage;

	return 0;
}

void mxc_isi_video_buffer_init(struct vb2_buffer *vb2, dma_addr_t dma_addrs[3],
			       const struct mxc_isi_format_info *info,
			       const struct v4l2_pix_format_mplane *pix)
{
	unsigned int i;

	for (i = 0; i < info->mem_planes; ++i)
		dma_addrs[i] = vb2_dma_contig_plane_dma_addr(vb2, i);

	/*
	 * For single-planar pixel formats with multiple color planes, split
	 * the buffer into color planes.
	 */
	if (info->color_planes != info->mem_planes) {
		unsigned int size = pix->plane_fmt[0].bytesperline * pix->height;

		for (i = 1; i < info->color_planes; ++i) {
			unsigned int vsub = i > 1 ? info->vsub : 1;

			dma_addrs[i] = dma_addrs[i - 1] + size / vsub;
		}
	}
}

int mxc_isi_video_buffer_prepare(struct mxc_isi_dev *isi, struct vb2_buffer *vb2,
				 const struct mxc_isi_format_info *info,
				 const struct v4l2_pix_format_mplane *pix)
{
	unsigned int i;

	for (i = 0; i < info->mem_planes; i++) {
		unsigned long size = pix->plane_fmt[i].sizeimage;

		if (vb2_plane_size(vb2, i) < size) {
			dev_err(isi->dev, "User buffer too small (%ld < %ld)\n",
				vb2_plane_size(vb2, i), size);
			return -EINVAL;
		}

		vb2_set_plane_payload(vb2, i, size);
	}

	return 0;
}

static int mxc_isi_vb2_queue_setup(struct vb2_queue *q,
				   unsigned int *num_buffers,
				   unsigned int *num_planes,
				   unsigned int sizes[],
				   struct device *alloc_devs[])
{
	struct mxc_isi_video *video = vb2_get_drv_priv(q);

	return mxc_isi_video_queue_setup(&video->pix, video->fmtinfo,
					 num_buffers, num_planes, sizes);
}

static int mxc_isi_vb2_buffer_init(struct vb2_buffer *vb2)
{
	struct mxc_isi_buffer *buf = to_isi_buffer(to_vb2_v4l2_buffer(vb2));
	struct mxc_isi_video *video = vb2_get_drv_priv(vb2->vb2_queue);

	mxc_isi_video_buffer_init(vb2, buf->dma_addrs, video->fmtinfo,
				  &video->pix);

	return 0;
}

static int mxc_isi_vb2_buffer_prepare(struct vb2_buffer *vb2)
{
	struct mxc_isi_video *video = vb2_get_drv_priv(vb2->vb2_queue);

	return mxc_isi_video_buffer_prepare(video->pipe->isi, vb2,
					    video->fmtinfo, &video->pix);
}

static void mxc_isi_vb2_buffer_queue(struct vb2_buffer *vb2)
{
	struct vb2_v4l2_buffer *v4l2_buf = to_vb2_v4l2_buffer(vb2);
	struct mxc_isi_buffer *buf = to_isi_buffer(v4l2_buf);
	struct mxc_isi_video *video = vb2_get_drv_priv(vb2->vb2_queue);

	spin_lock_irq(&video->buf_lock);
	list_add_tail(&buf->list, &video->out_pending);
	spin_unlock_irq(&video->buf_lock);
}

static void mxc_isi_video_init_channel(struct mxc_isi_video *video)
{
	struct mxc_isi_pipe *pipe = video->pipe;

	mxc_isi_channel_get(pipe);

	mutex_lock(video->ctrls.handler.lock);
	mxc_isi_channel_set_alpha(pipe, video->ctrls.alpha);
	mxc_isi_channel_set_flip(pipe, video->ctrls.hflip, video->ctrls.vflip);
	mutex_unlock(video->ctrls.handler.lock);

	mxc_isi_channel_set_output_format(pipe, video->fmtinfo, &video->pix);
}

static int mxc_isi_vb2_start_streaming(struct vb2_queue *q, unsigned int count)
{
	struct mxc_isi_video *video = vb2_get_drv_priv(q);
	unsigned int i;
	int ret;

	/* Initialize the ISI channel. */
	mxc_isi_video_init_channel(video);

	spin_lock_irq(&video->buf_lock);

	/* Add the discard buffers to the out_discard list. */
	for (i = 0; i < ARRAY_SIZE(video->buf_discard); ++i) {
		struct mxc_isi_buffer *buf = &video->buf_discard[i];

		list_add_tail(&buf->list, &video->out_discard);
	}

	/* Queue the first buffers. */
	mxc_isi_video_queue_first_buffers(video);

	/* Clear frame count */
	video->frame_count = 0;

	spin_unlock_irq(&video->buf_lock);

	ret = mxc_isi_pipe_enable(video->pipe);
	if (ret)
		goto error;

	return 0;

error:
	mxc_isi_channel_put(video->pipe);
	mxc_isi_video_return_buffers(video, VB2_BUF_STATE_QUEUED);
	return ret;
}

static void mxc_isi_vb2_stop_streaming(struct vb2_queue *q)
{
	struct mxc_isi_video *video = vb2_get_drv_priv(q);

	mxc_isi_pipe_disable(video->pipe);
	mxc_isi_channel_put(video->pipe);

	mxc_isi_video_return_buffers(video, VB2_BUF_STATE_ERROR);
}

static const struct vb2_ops mxc_isi_vb2_qops = {
	.queue_setup		= mxc_isi_vb2_queue_setup,
	.buf_init		= mxc_isi_vb2_buffer_init,
	.buf_prepare		= mxc_isi_vb2_buffer_prepare,
	.buf_queue		= mxc_isi_vb2_buffer_queue,
	.wait_prepare		= vb2_ops_wait_prepare,
	.wait_finish		= vb2_ops_wait_finish,
	.start_streaming	= mxc_isi_vb2_start_streaming,
	.stop_streaming		= mxc_isi_vb2_stop_streaming,
};

/* -----------------------------------------------------------------------------
 * V4L2 controls
 */

static inline struct mxc_isi_video *ctrl_to_isi_video(struct v4l2_ctrl *ctrl)
{
	return container_of(ctrl->handler, struct mxc_isi_video, ctrls.handler);
}

static int mxc_isi_video_s_ctrl(struct v4l2_ctrl *ctrl)
{
	struct mxc_isi_video *video = ctrl_to_isi_video(ctrl);

	switch (ctrl->id) {
	case V4L2_CID_ALPHA_COMPONENT:
		video->ctrls.alpha = ctrl->val;
		break;
	case V4L2_CID_VFLIP:
		video->ctrls.vflip = ctrl->val;
		break;
	case V4L2_CID_HFLIP:
		video->ctrls.hflip = ctrl->val;
		break;
	}

	return 0;
}

static const struct v4l2_ctrl_ops mxc_isi_video_ctrl_ops = {
	.s_ctrl = mxc_isi_video_s_ctrl,
};

static int mxc_isi_video_ctrls_create(struct mxc_isi_video *video)
{
	struct v4l2_ctrl_handler *handler = &video->ctrls.handler;
	int ret;

	v4l2_ctrl_handler_init(handler, 3);

	v4l2_ctrl_new_std(handler, &mxc_isi_video_ctrl_ops,
			  V4L2_CID_ALPHA_COMPONENT, 0, 255, 1, 0);

	v4l2_ctrl_new_std(handler, &mxc_isi_video_ctrl_ops,
			  V4L2_CID_VFLIP, 0, 1, 1, 0);

	v4l2_ctrl_new_std(handler, &mxc_isi_video_ctrl_ops,
			  V4L2_CID_HFLIP, 0, 1, 1, 0);

	if (handler->error) {
		ret = handler->error;
		v4l2_ctrl_handler_free(handler);
		return ret;
	}

	video->vdev.ctrl_handler = handler;

	return 0;
}

static void mxc_isi_video_ctrls_delete(struct mxc_isi_video *video)
{
	v4l2_ctrl_handler_free(&video->ctrls.handler);
}

/* -----------------------------------------------------------------------------
 * V4L2 ioctls
 */

static int mxc_isi_video_querycap(struct file *file, void *priv,
				  struct v4l2_capability *cap)
{
	strscpy(cap->driver, MXC_ISI_DRIVER_NAME, sizeof(cap->driver));
	strscpy(cap->card, MXC_ISI_CAPTURE, sizeof(cap->card));

	return 0;
}

static int mxc_isi_video_enum_fmt(struct file *file, void *priv,
				  struct v4l2_fmtdesc *f)
{
	const struct mxc_isi_format_info *fmt;
	unsigned int index = f->index;
	unsigned int i;

	if (f->mbus_code) {
		/*
		 * If a media bus code is specified, only enumerate formats
		 * compatible with it.
		 */
		for (i = 0; i < ARRAY_SIZE(mxc_isi_formats); i++) {
			fmt = &mxc_isi_formats[i];
			if (fmt->mbus_code != f->mbus_code)
				continue;

			if (index == 0)
				break;

			index--;
		}

		if (i == ARRAY_SIZE(mxc_isi_formats))
			return -EINVAL;
	} else {
		/* Otherwise, enumerate all formatS. */
		if (f->index >= ARRAY_SIZE(mxc_isi_formats))
			return -EINVAL;

		fmt = &mxc_isi_formats[f->index];
	}

	f->pixelformat = fmt->fourcc;
	f->flags |= V4L2_FMT_FLAG_CSC_COLORSPACE | V4L2_FMT_FLAG_CSC_YCBCR_ENC
		 |  V4L2_FMT_FLAG_CSC_QUANTIZATION | V4L2_FMT_FLAG_CSC_XFER_FUNC;

	return 0;
}

static int mxc_isi_video_g_fmt(struct file *file, void *fh,
			       struct v4l2_format *f)
{
	struct mxc_isi_video *video = video_drvdata(file);

	f->fmt.pix_mp = video->pix;

	return 0;
}

static int mxc_isi_video_try_fmt(struct file *file, void *fh,
				 struct v4l2_format *f)
{
	struct mxc_isi_video *video = video_drvdata(file);

	mxc_isi_format_try(video->pipe, &f->fmt.pix_mp, MXC_ISI_VIDEO_CAP);
	return 0;
}

static int mxc_isi_video_s_fmt(struct file *file, void *priv,
			       struct v4l2_format *f)
{
	struct mxc_isi_video *video = video_drvdata(file);
	struct v4l2_pix_format_mplane *pix = &f->fmt.pix_mp;

	if (vb2_is_busy(&video->vb2_q))
		return -EBUSY;

	video->fmtinfo = mxc_isi_format_try(video->pipe, pix, MXC_ISI_VIDEO_CAP);
	video->pix = *pix;

	return 0;
}

static int mxc_isi_video_streamon(struct file *file, void *priv,
				  enum v4l2_buf_type type)
{
	struct mxc_isi_video *video = video_drvdata(file);
	struct media_device *mdev = &video->pipe->isi->media_dev;
	struct media_pipeline *pipe;
	int ret;

	if (vb2_queue_is_busy(&video->vb2_q, file))
		return -EBUSY;

	/*
	 * Get a pipeline for the video node and start it. This must be done
	 * here and not in the queue .start_streaming() handler, so that
	 * pipeline start errors can be reported from VIDIOC_STREAMON and not
	 * delayed until subsequent VIDIOC_QBUF calls.
	 */
	mutex_lock(&mdev->graph_mutex);

	ret = mxc_isi_pipe_acquire(video->pipe, &mxc_isi_video_frame_write_done);
	if (ret) {
		mutex_unlock(&mdev->graph_mutex);
		return ret;
	}

	pipe = media_entity_pipeline(&video->vdev.entity) ? : &video->pipe->pipe;

	ret = __video_device_pipeline_start(&video->vdev, pipe);
	if (ret) {
		mutex_unlock(&mdev->graph_mutex);
		goto err_release;
	}

	mutex_unlock(&mdev->graph_mutex);

	/* Verify that the video format matches the output of the subdev. */
	ret = mxc_isi_video_validate_format(video);
	if (ret)
		goto err_stop;

	/* Allocate buffers for discard operation. */
	ret = mxc_isi_video_alloc_discard_buffers(video);
	if (ret)
		goto err_stop;

	ret = vb2_streamon(&video->vb2_q, type);
	if (ret)
		goto err_free;

	video->is_streaming = true;

	return 0;

err_free:
	mxc_isi_video_free_discard_buffers(video);
err_stop:
	video_device_pipeline_stop(&video->vdev);
err_release:
	mxc_isi_pipe_release(video->pipe);
	return ret;
}

static void mxc_isi_video_cleanup_streaming(struct mxc_isi_video *video)
{
	lockdep_assert_held(&video->lock);

	if (!video->is_streaming)
		return;

	mxc_isi_video_free_discard_buffers(video);
	video_device_pipeline_stop(&video->vdev);
	mxc_isi_pipe_release(video->pipe);

	video->is_streaming = false;
}

static int mxc_isi_video_streamoff(struct file *file, void *priv,
				   enum v4l2_buf_type type)
{
	struct mxc_isi_video *video = video_drvdata(file);
	int ret;

	ret = vb2_ioctl_streamoff(file, priv, type);
	if (ret)
		return ret;

	mxc_isi_video_cleanup_streaming(video);

	return 0;
}

static int mxc_isi_video_enum_framesizes(struct file *file, void *priv,
					 struct v4l2_frmsizeenum *fsize)
{
	struct mxc_isi_video *video = video_drvdata(file);
	const struct mxc_isi_format_info *info;
	unsigned int max_width;
	unsigned int h_align;
	unsigned int v_align;

	if (fsize->index)
		return -EINVAL;

	info = mxc_isi_format_by_fourcc(fsize->pixel_format, MXC_ISI_VIDEO_CAP);
	if (!info)
		return -EINVAL;

	h_align = max_t(unsigned int, info->hsub, 1);
	v_align = max_t(unsigned int, info->vsub, 1);

	max_width = video->pipe->id == video->pipe->isi->pdata->num_channels - 1
		  ? MXC_ISI_MAX_WIDTH_UNCHAINED
		  : MXC_ISI_MAX_WIDTH_CHAINED;

	fsize->type = V4L2_FRMSIZE_TYPE_STEPWISE;
	fsize->stepwise.min_width = ALIGN(MXC_ISI_MIN_WIDTH, h_align);
	fsize->stepwise.min_height = ALIGN(MXC_ISI_MIN_HEIGHT, v_align);
	fsize->stepwise.max_width = ALIGN_DOWN(max_width, h_align);
	fsize->stepwise.max_height = ALIGN_DOWN(MXC_ISI_MAX_HEIGHT, v_align);
	fsize->stepwise.step_width = h_align;
	fsize->stepwise.step_height = v_align;

	/*
	 * The width can be further restricted due to line buffer sharing
	 * between pipelines when scaling, but we have no way to know here if
	 * the scaler will be used.
	 */

	return 0;
}

static const struct v4l2_ioctl_ops mxc_isi_video_ioctl_ops = {
	.vidioc_querycap		= mxc_isi_video_querycap,

	.vidioc_enum_fmt_vid_cap	= mxc_isi_video_enum_fmt,
	.vidioc_try_fmt_vid_cap_mplane	= mxc_isi_video_try_fmt,
	.vidioc_s_fmt_vid_cap_mplane	= mxc_isi_video_s_fmt,
	.vidioc_g_fmt_vid_cap_mplane	= mxc_isi_video_g_fmt,

	.vidioc_reqbufs			= vb2_ioctl_reqbufs,
	.vidioc_querybuf		= vb2_ioctl_querybuf,
	.vidioc_qbuf			= vb2_ioctl_qbuf,
	.vidioc_dqbuf			= vb2_ioctl_dqbuf,
	.vidioc_expbuf			= vb2_ioctl_expbuf,
	.vidioc_prepare_buf		= vb2_ioctl_prepare_buf,
	.vidioc_create_bufs		= vb2_ioctl_create_bufs,

	.vidioc_streamon		= mxc_isi_video_streamon,
	.vidioc_streamoff		= mxc_isi_video_streamoff,

	.vidioc_enum_framesizes		= mxc_isi_video_enum_framesizes,

	.vidioc_subscribe_event		= v4l2_ctrl_subscribe_event,
	.vidioc_unsubscribe_event	= v4l2_event_unsubscribe,
};

/* -----------------------------------------------------------------------------
 * Video device file operations
 */

static int mxc_isi_video_open(struct file *file)
{
	struct mxc_isi_video *video = video_drvdata(file);
	int ret;

	ret = v4l2_fh_open(file);
	if (ret)
		return ret;

	ret = pm_runtime_resume_and_get(video->pipe->isi->dev);
	if (ret) {
		v4l2_fh_release(file);
		return ret;
	}

	return 0;
}

static int mxc_isi_video_release(struct file *file)
{
	struct mxc_isi_video *video = video_drvdata(file);
	int ret;

	ret = vb2_fop_release(file);
	if (ret)
		dev_err(video->pipe->isi->dev, "%s fail\n", __func__);

	mutex_lock(&video->lock);
	mxc_isi_video_cleanup_streaming(video);
	mutex_unlock(&video->lock);

	pm_runtime_put(video->pipe->isi->dev);
	return ret;
}

static const struct v4l2_file_operations mxc_isi_video_fops = {
	.owner		= THIS_MODULE,
	.open		= mxc_isi_video_open,
	.release	= mxc_isi_video_release,
	.poll		= vb2_fop_poll,
	.unlocked_ioctl	= video_ioctl2,
	.mmap		= vb2_fop_mmap,
};

/* -----------------------------------------------------------------------------
 * Suspend & resume
 */

void mxc_isi_video_suspend(struct mxc_isi_pipe *pipe)
{
	struct mxc_isi_video *video = &pipe->video;

	if (!video->is_streaming)
		return;

	mxc_isi_pipe_disable(pipe);
	mxc_isi_channel_put(pipe);

	spin_lock_irq(&video->buf_lock);

	/*
	 * Move the active buffers back to the pending or discard list. We must
	 * iterate the active list backward and move the buffers to the head of
	 * the pending list to preserve the buffer queueing order.
	 */
	while (!list_empty(&video->out_active)) {
		struct mxc_isi_buffer *buf =
			list_last_entry(&video->out_active,
					struct mxc_isi_buffer, list);

		if (buf->discard)
			list_move(&buf->list, &video->out_discard);
		else
			list_move(&buf->list, &video->out_pending);
	}

	spin_unlock_irq(&video->buf_lock);
}

int mxc_isi_video_resume(struct mxc_isi_pipe *pipe)
{
	struct mxc_isi_video *video = &pipe->video;

	if (!video->is_streaming)
		return 0;

	mxc_isi_video_init_channel(video);

	spin_lock_irq(&video->buf_lock);
	mxc_isi_video_queue_first_buffers(video);
	spin_unlock_irq(&video->buf_lock);

	return mxc_isi_pipe_enable(pipe);
}

/* -----------------------------------------------------------------------------
 * Registration
 */

int mxc_isi_video_register(struct mxc_isi_pipe *pipe,
			   struct v4l2_device *v4l2_dev)
{
	struct mxc_isi_video *video = &pipe->video;
	struct v4l2_pix_format_mplane *pix = &video->pix;
	struct video_device *vdev = &video->vdev;
	struct vb2_queue *q = &video->vb2_q;
	int ret = -ENOMEM;

	video->pipe = pipe;

	mutex_init(&video->lock);
	spin_lock_init(&video->buf_lock);

	pix->width = MXC_ISI_DEF_WIDTH;
	pix->height = MXC_ISI_DEF_HEIGHT;
	pix->pixelformat = MXC_ISI_DEF_PIXEL_FORMAT;
	pix->colorspace = MXC_ISI_DEF_COLOR_SPACE;
	pix->ycbcr_enc = MXC_ISI_DEF_YCBCR_ENC;
	pix->quantization = MXC_ISI_DEF_QUANTIZATION;
	pix->xfer_func = MXC_ISI_DEF_XFER_FUNC;
	video->fmtinfo = mxc_isi_format_try(video->pipe, pix, MXC_ISI_VIDEO_CAP);

	memset(vdev, 0, sizeof(*vdev));
	snprintf(vdev->name, sizeof(vdev->name), "mxc_isi.%d.capture", pipe->id);

	vdev->fops	= &mxc_isi_video_fops;
	vdev->ioctl_ops	= &mxc_isi_video_ioctl_ops;
	vdev->v4l2_dev	= v4l2_dev;
	vdev->minor	= -1;
	vdev->release	= video_device_release_empty;
	vdev->queue	= q;
	vdev->lock	= &video->lock;

	vdev->device_caps = V4L2_CAP_STREAMING | V4L2_CAP_VIDEO_CAPTURE_MPLANE
			  | V4L2_CAP_IO_MC;
	video_set_drvdata(vdev, video);

	INIT_LIST_HEAD(&video->out_pending);
	INIT_LIST_HEAD(&video->out_active);
	INIT_LIST_HEAD(&video->out_discard);

	memset(q, 0, sizeof(*q));
	q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
	q->io_modes = VB2_MMAP | VB2_DMABUF;
	q->drv_priv = video;
	q->ops = &mxc_isi_vb2_qops;
	q->mem_ops = &vb2_dma_contig_memops;
	q->buf_struct_size = sizeof(struct mxc_isi_buffer);
	q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
	q->min_buffers_needed = 2;
	q->lock = &video->lock;
	q->dev = pipe->isi->dev;

	ret = vb2_queue_init(q);
	if (ret)
		goto err_free_ctx;

	video->pad.flags = MEDIA_PAD_FL_SINK;
	vdev->entity.function = MEDIA_ENT_F_PROC_VIDEO_SCALER;
	ret = media_entity_pads_init(&vdev->entity, 1, &video->pad);
	if (ret)
		goto err_free_ctx;

	ret = mxc_isi_video_ctrls_create(video);
	if (ret)
		goto err_me_cleanup;

	ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
	if (ret)
		goto err_ctrl_free;

	ret = media_create_pad_link(&pipe->sd.entity,
				    MXC_ISI_PIPE_PAD_SOURCE,
				    &vdev->entity, 0,
				    MEDIA_LNK_FL_IMMUTABLE |
				    MEDIA_LNK_FL_ENABLED);
	if (ret)
		goto err_video_unreg;

	return 0;

err_video_unreg:
	video_unregister_device(vdev);
err_ctrl_free:
	mxc_isi_video_ctrls_delete(video);
err_me_cleanup:
	media_entity_cleanup(&vdev->entity);
err_free_ctx:
	return ret;
}

void mxc_isi_video_unregister(struct mxc_isi_pipe *pipe)
{
	struct mxc_isi_video *video = &pipe->video;
	struct video_device *vdev = &video->vdev;

	mutex_lock(&video->lock);

	if (video_is_registered(vdev)) {
		video_unregister_device(vdev);
		mxc_isi_video_ctrls_delete(video);
		media_entity_cleanup(&vdev->entity);
	}

	mutex_unlock(&video->lock);
}