// SPDX-License-Identifier: GPL-2.0
/*
 * Support for Clovertrail PNW Camera Imaging ISP subsystem.
 *
 * Copyright (c) 2012 Intel Corporation. All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 *
 */

/*
 * This file implements loadable acceleration firmware API,
 * including ioctls to map and unmap acceleration parameters and buffers.
 */

#include <linux/init.h>
#include <media/v4l2-event.h>

#include "hmm.h"

#include "atomisp_acc.h"
#include "atomisp_internal.h"
#include "atomisp_compat.h"
#include "atomisp_cmd.h"

#include "ia_css.h"

static const struct {
	unsigned int flag;
	enum ia_css_pipe_id pipe_id;
} acc_flag_to_pipe[] = {
	{ ATOMISP_ACC_FW_LOAD_FL_PREVIEW, IA_CSS_PIPE_ID_PREVIEW },
	{ ATOMISP_ACC_FW_LOAD_FL_COPY, IA_CSS_PIPE_ID_COPY },
	{ ATOMISP_ACC_FW_LOAD_FL_VIDEO, IA_CSS_PIPE_ID_VIDEO },
	{ ATOMISP_ACC_FW_LOAD_FL_CAPTURE, IA_CSS_PIPE_ID_CAPTURE },
	{ ATOMISP_ACC_FW_LOAD_FL_ACC, IA_CSS_PIPE_ID_ACC }
};

/*
 * Allocate struct atomisp_acc_fw along with space for firmware.
 * The returned struct atomisp_acc_fw is cleared (firmware region is not).
 */
static struct atomisp_acc_fw *acc_alloc_fw(unsigned int fw_size)
{
	struct atomisp_acc_fw *acc_fw;

	acc_fw = kzalloc(sizeof(*acc_fw), GFP_KERNEL);
	if (!acc_fw)
		return NULL;

	acc_fw->fw = vmalloc(fw_size);
	if (!acc_fw->fw) {
		kfree(acc_fw);
		return NULL;
	}

	return acc_fw;
}

static void acc_free_fw(struct atomisp_acc_fw *acc_fw)
{
	vfree(acc_fw->fw);
	kfree(acc_fw);
}

static struct atomisp_acc_fw *
acc_get_fw(struct atomisp_sub_device *asd, unsigned int handle)
{
	struct atomisp_acc_fw *acc_fw;

	list_for_each_entry(acc_fw, &asd->acc.fw, list)
	if (acc_fw->handle == handle)
		return acc_fw;

	return NULL;
}

static struct atomisp_map *acc_get_map(struct atomisp_sub_device *asd,
				       unsigned long css_ptr, size_t length)
{
	struct atomisp_map *atomisp_map;

	list_for_each_entry(atomisp_map, &asd->acc.memory_maps, list) {
		if (atomisp_map->ptr == css_ptr &&
		    atomisp_map->length == length)
			return atomisp_map;
	}
	return NULL;
}

static int acc_stop_acceleration(struct atomisp_sub_device *asd)
{
	int ret;

	ret = atomisp_css_stop_acc_pipe(asd);
	atomisp_css_destroy_acc_pipe(asd);

	return ret;
}

void atomisp_acc_cleanup(struct atomisp_device *isp)
{
	int i;

	for (i = 0; i < isp->num_of_streams; i++)
		ida_destroy(&isp->asd[i].acc.ida);
}

void atomisp_acc_release(struct atomisp_sub_device *asd)
{
	struct atomisp_acc_fw *acc_fw, *ta;
	struct atomisp_map *atomisp_map, *tm;

	/* Stop acceleration if already running */
	if (asd->acc.pipeline)
		acc_stop_acceleration(asd);

	/* Unload all loaded acceleration binaries */
	list_for_each_entry_safe(acc_fw, ta, &asd->acc.fw, list) {
		list_del(&acc_fw->list);
		ida_free(&asd->acc.ida, acc_fw->handle);
		acc_free_fw(acc_fw);
	}

	/* Free all mapped memory blocks */
	list_for_each_entry_safe(atomisp_map, tm, &asd->acc.memory_maps, list) {
		list_del(&atomisp_map->list);
		hmm_free(atomisp_map->ptr);
		kfree(atomisp_map);
	}
}

int atomisp_acc_load_to_pipe(struct atomisp_sub_device *asd,
			     struct atomisp_acc_fw_load_to_pipe *user_fw)
{
	static const unsigned int pipeline_flags =
	    ATOMISP_ACC_FW_LOAD_FL_PREVIEW | ATOMISP_ACC_FW_LOAD_FL_COPY |
	    ATOMISP_ACC_FW_LOAD_FL_VIDEO |
	    ATOMISP_ACC_FW_LOAD_FL_CAPTURE | ATOMISP_ACC_FW_LOAD_FL_ACC;

	struct atomisp_acc_fw *acc_fw;
	int handle;

	if (!user_fw->data || user_fw->size < sizeof(*acc_fw->fw))
		return -EINVAL;

	/* Binary has to be enabled at least for one pipeline */
	if (!(user_fw->flags & pipeline_flags))
		return -EINVAL;

	/* We do not support other flags yet */
	if (user_fw->flags & ~pipeline_flags)
		return -EINVAL;

	if (user_fw->type < ATOMISP_ACC_FW_LOAD_TYPE_OUTPUT ||
	    user_fw->type > ATOMISP_ACC_FW_LOAD_TYPE_STANDALONE)
		return -EINVAL;

	if (asd->acc.pipeline || asd->acc.extension_mode)
		return -EBUSY;

	acc_fw = acc_alloc_fw(user_fw->size);
	if (!acc_fw)
		return -ENOMEM;

	if (copy_from_user(acc_fw->fw, user_fw->data, user_fw->size)) {
		acc_free_fw(acc_fw);
		return -EFAULT;
	}

	handle = ida_alloc(&asd->acc.ida, GFP_KERNEL);
	if (handle < 0) {
		acc_free_fw(acc_fw);
		return -ENOSPC;
	}

	user_fw->fw_handle = handle;
	acc_fw->handle = handle;
	acc_fw->flags = user_fw->flags;
	acc_fw->type = user_fw->type;
	acc_fw->fw->handle = handle;

	/*
	 * correct isp firmware type in order ISP firmware can be appended
	 * to correct pipe properly
	 */
	if (acc_fw->fw->type == ia_css_isp_firmware) {
		static const int type_to_css[] = {
			[ATOMISP_ACC_FW_LOAD_TYPE_OUTPUT] =
			IA_CSS_ACC_OUTPUT,
			[ATOMISP_ACC_FW_LOAD_TYPE_VIEWFINDER] =
			IA_CSS_ACC_VIEWFINDER,
			[ATOMISP_ACC_FW_LOAD_TYPE_STANDALONE] =
			IA_CSS_ACC_STANDALONE,
		};
		acc_fw->fw->info.isp.type = type_to_css[acc_fw->type];
	}

	list_add_tail(&acc_fw->list, &asd->acc.fw);
	return 0;
}

int atomisp_acc_load(struct atomisp_sub_device *asd,
		     struct atomisp_acc_fw_load *user_fw)
{
	struct atomisp_acc_fw_load_to_pipe ltp = {0};
	int r;

	ltp.flags = ATOMISP_ACC_FW_LOAD_FL_ACC;
	ltp.type = ATOMISP_ACC_FW_LOAD_TYPE_STANDALONE;
	ltp.size = user_fw->size;
	ltp.data = user_fw->data;
	r = atomisp_acc_load_to_pipe(asd, &ltp);
	user_fw->fw_handle = ltp.fw_handle;
	return r;
}

int atomisp_acc_unload(struct atomisp_sub_device *asd, unsigned int *handle)
{
	struct atomisp_acc_fw *acc_fw;

	if (asd->acc.pipeline || asd->acc.extension_mode)
		return -EBUSY;

	acc_fw = acc_get_fw(asd, *handle);
	if (!acc_fw)
		return -EINVAL;

	list_del(&acc_fw->list);
	ida_free(&asd->acc.ida, acc_fw->handle);
	acc_free_fw(acc_fw);

	return 0;
}

int atomisp_acc_start(struct atomisp_sub_device *asd, unsigned int *handle)
{
	struct atomisp_device *isp = asd->isp;
	struct atomisp_acc_fw *acc_fw;
	int ret;
	unsigned int nbin;

	if (asd->acc.pipeline || asd->acc.extension_mode)
		return -EBUSY;

	/* Invalidate caches. FIXME: should flush only necessary buffers */
	wbinvd();

	ret = atomisp_css_create_acc_pipe(asd);
	if (ret)
		return ret;

	nbin = 0;
	list_for_each_entry(acc_fw, &asd->acc.fw, list) {
		if (*handle != 0 && *handle != acc_fw->handle)
			continue;

		if (acc_fw->type != ATOMISP_ACC_FW_LOAD_TYPE_STANDALONE)
			continue;

		/* Add the binary into the pipeline */
		ret = atomisp_css_load_acc_binary(asd, acc_fw->fw, nbin);
		if (ret < 0) {
			dev_err(isp->dev, "acc_load_binary failed\n");
			goto err_stage;
		}

		ret = atomisp_css_set_acc_parameters(acc_fw);
		if (ret < 0) {
			dev_err(isp->dev, "acc_set_parameters failed\n");
			goto err_stage;
		}
		nbin++;
	}
	if (nbin < 1) {
		/* Refuse creating pipelines with no binaries */
		dev_err(isp->dev, "%s: no acc binary available\n", __func__);
		ret = -EINVAL;
		goto err_stage;
	}

	ret = atomisp_css_start_acc_pipe(asd);
	if (ret) {
		dev_err(isp->dev, "%s: atomisp_acc_start_acc_pipe failed\n",
			__func__);
		goto err_stage;
	}

	return 0;

err_stage:
	atomisp_css_destroy_acc_pipe(asd);
	return ret;
}

int atomisp_acc_wait(struct atomisp_sub_device *asd, unsigned int *handle)
{
	struct atomisp_device *isp = asd->isp;
	int ret;

	if (!asd->acc.pipeline)
		return -ENOENT;

	if (*handle && !acc_get_fw(asd, *handle))
		return -EINVAL;

	ret = atomisp_css_wait_acc_finish(asd);
	if (acc_stop_acceleration(asd) == -EIO) {
		atomisp_reset(isp);
		return -EINVAL;
	}

	return ret;
}

void atomisp_acc_done(struct atomisp_sub_device *asd, unsigned int handle)
{
	struct v4l2_event event = { 0 };

	event.type = V4L2_EVENT_ATOMISP_ACC_COMPLETE;
	event.u.frame_sync.frame_sequence = atomic_read(&asd->sequence);
	event.id = handle;

	v4l2_event_queue(asd->subdev.devnode, &event);
}

int atomisp_acc_map(struct atomisp_sub_device *asd, struct atomisp_acc_map *map)
{
	struct atomisp_map *atomisp_map;
	ia_css_ptr cssptr;
	int pgnr;

	if (map->css_ptr)
		return -EINVAL;

	if (asd->acc.pipeline)
		return -EBUSY;

	if (map->user_ptr) {
		/* Buffer to map must be page-aligned */
		if ((unsigned long)map->user_ptr & ~PAGE_MASK) {
			dev_err(asd->isp->dev,
				"%s: mapped buffer address %p is not page aligned\n",
				__func__, map->user_ptr);
			return -EINVAL;
		}

		pgnr = DIV_ROUND_UP(map->length, PAGE_SIZE);
		if (pgnr < ((PAGE_ALIGN(map->length)) >> PAGE_SHIFT)) {
			dev_err(asd->isp->dev,
				"user space memory size is less than the expected size..\n");
			return -ENOMEM;
		} else if (pgnr > ((PAGE_ALIGN(map->length)) >> PAGE_SHIFT)) {
			dev_err(asd->isp->dev,
				"user space memory size is large than the expected size..\n");
			return -ENOMEM;
		}

		cssptr = hmm_alloc(map->length, HMM_BO_USER, 0, map->user_ptr,
				   map->flags & ATOMISP_MAP_FLAG_CACHED);

	} else {
		/* Allocate private buffer. */
		cssptr = hmm_alloc(map->length, HMM_BO_PRIVATE, 0, NULL,
				   map->flags & ATOMISP_MAP_FLAG_CACHED);
	}

	if (!cssptr)
		return -ENOMEM;

	atomisp_map = kmalloc(sizeof(*atomisp_map), GFP_KERNEL);
	if (!atomisp_map) {
		hmm_free(cssptr);
		return -ENOMEM;
	}
	atomisp_map->ptr = cssptr;
	atomisp_map->length = map->length;
	list_add(&atomisp_map->list, &asd->acc.memory_maps);

	dev_dbg(asd->isp->dev, "%s: userptr %p, css_address 0x%x, size %d\n",
		__func__, map->user_ptr, cssptr, map->length);
	map->css_ptr = cssptr;
	return 0;
}

int atomisp_acc_unmap(struct atomisp_sub_device *asd,
		      struct atomisp_acc_map *map)
{
	struct atomisp_map *atomisp_map;

	if (asd->acc.pipeline)
		return -EBUSY;

	atomisp_map = acc_get_map(asd, map->css_ptr, map->length);
	if (!atomisp_map)
		return -EINVAL;

	list_del(&atomisp_map->list);
	hmm_free(atomisp_map->ptr);
	kfree(atomisp_map);
	return 0;
}

int atomisp_acc_s_mapped_arg(struct atomisp_sub_device *asd,
			     struct atomisp_acc_s_mapped_arg *arg)
{
	struct atomisp_acc_fw *acc_fw;

	if (arg->memory >= ATOMISP_ACC_NR_MEMORY)
		return -EINVAL;

	if (asd->acc.pipeline)
		return -EBUSY;

	acc_fw = acc_get_fw(asd, arg->fw_handle);
	if (!acc_fw)
		return -EINVAL;

	if (arg->css_ptr != 0 || arg->length != 0) {
		/* Unless the parameter is cleared, check that it exists */
		if (!acc_get_map(asd, arg->css_ptr, arg->length))
			return -EINVAL;
	}

	acc_fw->args[arg->memory].length = arg->length;
	acc_fw->args[arg->memory].css_ptr = arg->css_ptr;

	dev_dbg(asd->isp->dev, "%s: mem %d, address %p, size %ld\n",
		__func__, arg->memory, (void *)arg->css_ptr,
		(unsigned long)arg->length);
	return 0;
}

/*
 * Appends the loaded acceleration binary extensions to the
 * current ISP mode. Must be called just before sh_css_start().
 */
int atomisp_acc_load_extensions(struct atomisp_sub_device *asd)
{
	struct atomisp_acc_fw *acc_fw;
	bool ext_loaded = false;
	bool continuous = asd->continuous_mode->val &&
			  asd->run_mode->val == ATOMISP_RUN_MODE_PREVIEW;
	int ret = 0, i = -1;
	struct atomisp_device *isp = asd->isp;

	if (asd->acc.pipeline || asd->acc.extension_mode)
		return -EBUSY;

	/* Invalidate caches. FIXME: should flush only necessary buffers */
	wbinvd();

	list_for_each_entry(acc_fw, &asd->acc.fw, list) {
		if (acc_fw->type != ATOMISP_ACC_FW_LOAD_TYPE_OUTPUT &&
		    acc_fw->type != ATOMISP_ACC_FW_LOAD_TYPE_VIEWFINDER)
			continue;

		for (i = 0; i < ARRAY_SIZE(acc_flag_to_pipe); i++) {
			/* QoS (ACC pipe) acceleration stages are currently
			 * allowed only in continuous mode. Skip them for
			 * all other modes. */
			if (!continuous &&
			    acc_flag_to_pipe[i].flag ==
			    ATOMISP_ACC_FW_LOAD_FL_ACC)
				continue;

			if (acc_fw->flags & acc_flag_to_pipe[i].flag) {
				ret = atomisp_css_load_acc_extension(asd,
								     acc_fw->fw,
								     acc_flag_to_pipe[i].pipe_id,
								     acc_fw->type);
				if (ret)
					goto error;

				ext_loaded = true;
			}
		}

		ret = atomisp_css_set_acc_parameters(acc_fw);
		if (ret < 0)
			goto error;
	}

	if (!ext_loaded)
		return ret;

	ret = atomisp_css_update_stream(asd);
	if (ret) {
		dev_err(isp->dev, "%s: update stream failed.\n", __func__);
		goto error;
	}

	asd->acc.extension_mode = true;
	return 0;

error:
	while (--i >= 0) {
		if (acc_fw->flags & acc_flag_to_pipe[i].flag) {
			atomisp_css_unload_acc_extension(asd, acc_fw->fw,
							 acc_flag_to_pipe[i].pipe_id);
		}
	}

	list_for_each_entry_continue_reverse(acc_fw, &asd->acc.fw, list) {
		if (acc_fw->type != ATOMISP_ACC_FW_LOAD_TYPE_OUTPUT &&
		    acc_fw->type != ATOMISP_ACC_FW_LOAD_TYPE_VIEWFINDER)
			continue;

		for (i = ARRAY_SIZE(acc_flag_to_pipe) - 1; i >= 0; i--) {
			if (!continuous &&
			    acc_flag_to_pipe[i].flag ==
			    ATOMISP_ACC_FW_LOAD_FL_ACC)
				continue;
			if (acc_fw->flags & acc_flag_to_pipe[i].flag) {
				atomisp_css_unload_acc_extension(asd,
								 acc_fw->fw,
								 acc_flag_to_pipe[i].pipe_id);
			}
		}
	}
	return ret;
}

void atomisp_acc_unload_extensions(struct atomisp_sub_device *asd)
{
	struct atomisp_acc_fw *acc_fw;
	int i;

	if (!asd->acc.extension_mode)
		return;

	list_for_each_entry_reverse(acc_fw, &asd->acc.fw, list) {
		if (acc_fw->type != ATOMISP_ACC_FW_LOAD_TYPE_OUTPUT &&
		    acc_fw->type != ATOMISP_ACC_FW_LOAD_TYPE_VIEWFINDER)
			continue;

		for (i = ARRAY_SIZE(acc_flag_to_pipe) - 1; i >= 0; i--) {
			if (acc_fw->flags & acc_flag_to_pipe[i].flag) {
				atomisp_css_unload_acc_extension(asd,
								 acc_fw->fw,
								 acc_flag_to_pipe[i].pipe_id);
			}
		}
	}

	asd->acc.extension_mode = false;
}

int atomisp_acc_set_state(struct atomisp_sub_device *asd,
			  struct atomisp_acc_state *arg)
{
	struct atomisp_acc_fw *acc_fw;
	bool enable = (arg->flags & ATOMISP_STATE_FLAG_ENABLE) != 0;
	struct ia_css_pipe *pipe;
	int r;
	int i;

	if (!asd->acc.extension_mode)
		return -EBUSY;

	if (arg->flags & ~ATOMISP_STATE_FLAG_ENABLE)
		return -EINVAL;

	acc_fw = acc_get_fw(asd, arg->fw_handle);
	if (!acc_fw)
		return -EINVAL;

	if (enable)
		wbinvd();

	for (i = 0; i < ARRAY_SIZE(acc_flag_to_pipe); i++) {
		if (acc_fw->flags & acc_flag_to_pipe[i].flag) {
			pipe = asd->stream_env[ATOMISP_INPUT_STREAM_GENERAL].
			       pipes[acc_flag_to_pipe[i].pipe_id];
			r = ia_css_pipe_set_qos_ext_state(pipe, acc_fw->handle,
							  enable);
			if (r)
				return -EBADRQC;
		}
	}

	if (enable)
		acc_fw->flags |= ATOMISP_ACC_FW_LOAD_FL_ENABLE;
	else
		acc_fw->flags &= ~ATOMISP_ACC_FW_LOAD_FL_ENABLE;

	return 0;
}

int atomisp_acc_get_state(struct atomisp_sub_device *asd,
			  struct atomisp_acc_state *arg)
{
	struct atomisp_acc_fw *acc_fw;

	if (!asd->acc.extension_mode)
		return -EBUSY;

	acc_fw = acc_get_fw(asd, arg->fw_handle);
	if (!acc_fw)
		return -EINVAL;

	arg->flags = acc_fw->flags;

	return 0;
}
