// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2014-2016, 2018, 2020 The Linux Foundation. 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 and
 * only 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.
 */

#define VIDC_DBG_LABEL "venus_boot"

#include <asm/dma-iommu.h>
#include <asm/page.h>
#include <linux/clk.h>
#include <linux/delay.h>
#include <linux/dma-mapping.h>
#include <linux/err.h>
#include <linux/io.h>
#include <linux/iommu.h>
#include <linux/iopoll.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/regulator/consumer.h>
#include <linux/sizes.h>
#include <linux/slab.h>
#include <soc/qcom/subsystem_notif.h>
#include <soc/qcom/subsystem_restart.h>
#include "msm_vidc_debug.h"
#include "vidc_hfi_io.h"
#include "venus_boot.h"

/* VENUS WRAPPER registers */
#define VENUS_WRAPPER_VBIF_SS_SEC_CPA_START_ADDR_v1 \
				(VIDC_WRAPPER_BASE_OFFS + 0x1018)
#define VENUS_WRAPPER_VBIF_SS_SEC_CPA_END_ADDR_v1 \
				(VIDC_WRAPPER_BASE_OFFS + 0x101C)
#define VENUS_WRAPPER_VBIF_SS_SEC_FW_START_ADDR_v1 \
				(VIDC_WRAPPER_BASE_OFFS + 0x1020)
#define VENUS_WRAPPER_VBIF_SS_SEC_FW_END_ADDR_v1 \
				(VIDC_WRAPPER_BASE_OFFS + 0x1024)

#define VENUS_WRAPPER_VBIF_SS_SEC_CPA_START_ADDR_v2 \
				(VIDC_WRAPPER_BASE_OFFS + 0x1020)
#define VENUS_WRAPPER_VBIF_SS_SEC_CPA_END_ADDR_v2 \
				(VIDC_WRAPPER_BASE_OFFS + 0x1024)
#define VENUS_WRAPPER_VBIF_SS_SEC_FW_START_ADDR_v2 \
				(VIDC_WRAPPER_BASE_OFFS + 0x1028)
#define VENUS_WRAPPER_VBIF_SS_SEC_FW_END_ADDR_v2 \
				(VIDC_WRAPPER_BASE_OFFS + 0x102C)

#define VENUS_WRAPPER_SW_RESET	(VIDC_WRAPPER_BASE_OFFS + 0x3000)

/* VENUS VBIF registers */
#define VENUS_VBIF_CLKON_FORCE_ON			BIT(0)

#define VENUS_VBIF_ADDR_TRANS_EN  (VIDC_VBIF_BASE_OFFS + 0x1000)
#define VENUS_VBIF_AT_OLD_BASE    (VIDC_VBIF_BASE_OFFS + 0x1004)
#define VENUS_VBIF_AT_OLD_HIGH    (VIDC_VBIF_BASE_OFFS + 0x1008)
#define VENUS_VBIF_AT_NEW_BASE    (VIDC_VBIF_BASE_OFFS + 0x1010)
#define VENUS_VBIF_AT_NEW_HIGH    (VIDC_VBIF_BASE_OFFS + 0x1018)


/* Poll interval in uS */
#define POLL_INTERVAL_US				50

#define VENUS_REGION_SIZE				0x00500000

static struct {
	struct msm_vidc_platform_resources *resources;
	struct regulator *gdsc;
	const char *reg_name;
	void __iomem *reg_base;
	struct device *iommu_ctx_bank_dev;
	struct iommu_domain *domain;
	dma_addr_t fw_iova;
	bool is_booted;
	bool hw_ver_checked;
	u32 fw_sz;
	u32 hw_ver_major;
	u32 hw_ver_minor;
	void *venus_notif_hdle;
} *venus_data = NULL;

/* Get venus clocks and set rates for rate-settable clocks */
static int venus_clock_setup(void)
{
	int i, rc = 0;
	unsigned long rate;
	struct msm_vidc_platform_resources *res = venus_data->resources;
	struct clock_info *cl;

	for (i = 0; i < res->clock_set.count; i++) {
		cl = &res->clock_set.clock_tbl[i];
		/* Make sure rate-settable clocks' rates are set */
		if (!clk_get_rate(cl->clk) && cl->count) {
			rate = clk_round_rate(cl->clk, 0);
			rc = clk_set_rate(cl->clk, rate);
			if (rc) {
				dprintk(VIDC_ERR,
						"Failed to set clock rate %lu %s: %d\n",
						rate, cl->name, rc);
				break;
			}
		}
	}

	return rc;
}

static int venus_clock_prepare_enable(void)
{
	int i, rc = 0;
	struct msm_vidc_platform_resources *res = venus_data->resources;
	struct clock_info *cl;

	for (i = 0; i < res->clock_set.count; i++) {
		cl = &res->clock_set.clock_tbl[i];
		rc = clk_prepare_enable(cl->clk);
		if (rc) {
			dprintk(VIDC_ERR, "failed to enable %s\n", cl->name);
			for (i--; i >= 0; i--) {
				cl = &res->clock_set.clock_tbl[i];
				clk_disable_unprepare(cl->clk);
			}
			return rc;
		}
	}

	return rc;
}

static void venus_clock_disable_unprepare(void)
{
	int i;
	struct msm_vidc_platform_resources *res = venus_data->resources;
	struct clock_info *cl;

	for (i = 0; i < res->clock_set.count; i++) {
		cl = &res->clock_set.clock_tbl[i];
		clk_disable_unprepare(cl->clk);
	}
}

static int venus_setup_cb(struct device *dev,
				u32 size)
{
	venus_data->domain = iommu_get_domain_for_dev(dev);
	if (IS_ERR_OR_NULL(venus_data->domain)) {
		dprintk(VIDC_ERR, "%s: failed to create mapping for %s\n",
		__func__, dev_name(dev));
		return -ENODEV;
	}
	dprintk(VIDC_DBG,
		"%s Attached device %pK and created domain %pK for %s\n",
		__func__, dev, venus_data->domain, dev_name(dev));
	return 0;
}

static int pil_venus_mem_setup(size_t size)
{
	int rc = 0;

	if (!venus_data->domain) {
		size = round_up(size, SZ_4K);
		rc = venus_setup_cb(venus_data->iommu_ctx_bank_dev, size);
		if (rc) {
			dprintk(VIDC_ERR,
			"%s: Failed to setup context bank for venus : %s\n",
			 __func__,
			dev_name(venus_data->iommu_ctx_bank_dev));
			return rc;
		}
		venus_data->fw_sz = size;
	}

	return rc;
}

static int pil_venus_auth_and_reset(void)
{
	int rc;

	phys_addr_t fw_bias = venus_data->resources->firmware_base;
	void __iomem *reg_base = venus_data->reg_base;
	u32 ver;
	bool iommu_present = is_iommu_present(venus_data->resources);
	struct device *dev = venus_data->iommu_ctx_bank_dev;

	if (!fw_bias) {
		dprintk(VIDC_ERR, "FW bias is not valid\n");
		return -EINVAL;
	}
	venus_data->fw_iova = (dma_addr_t)NULL;
	/* Get Venus version number */
	if (!venus_data->hw_ver_checked) {
		ver = readl_relaxed(reg_base + VIDC_WRAPPER_HW_VERSION);
		venus_data->hw_ver_minor = (ver & 0x0FFF0000) >> 16;
		venus_data->hw_ver_major = (ver & 0xF0000000) >> 28;
		venus_data->hw_ver_checked = true;
	}

	if (iommu_present) {
		u32 cpa_start_addr, cpa_end_addr, fw_start_addr, fw_end_addr;
		/* Get the cpa and fw start/end addr based on Venus version */
		if (venus_data->hw_ver_major == 0x1 &&
				venus_data->hw_ver_minor <= 1) {
			cpa_start_addr =
				VENUS_WRAPPER_VBIF_SS_SEC_CPA_START_ADDR_v1;
			cpa_end_addr =
				VENUS_WRAPPER_VBIF_SS_SEC_CPA_END_ADDR_v1;
			fw_start_addr =
				VENUS_WRAPPER_VBIF_SS_SEC_FW_START_ADDR_v1;
			fw_end_addr =
				VENUS_WRAPPER_VBIF_SS_SEC_FW_END_ADDR_v1;
		} else {
			cpa_start_addr =
				VENUS_WRAPPER_VBIF_SS_SEC_CPA_START_ADDR_v2;
			cpa_end_addr =
				VENUS_WRAPPER_VBIF_SS_SEC_CPA_END_ADDR_v2;
			fw_start_addr =
				VENUS_WRAPPER_VBIF_SS_SEC_FW_START_ADDR_v2;
			fw_end_addr =
				VENUS_WRAPPER_VBIF_SS_SEC_FW_END_ADDR_v2;
		}

		/* Program CPA start and end address */
		writel_relaxed(0, reg_base + cpa_start_addr);
		writel_relaxed(venus_data->fw_sz, reg_base + cpa_end_addr);

		/* Program FW start and end address */
		writel_relaxed(0, reg_base + fw_start_addr);
		writel_relaxed(venus_data->fw_sz, reg_base + fw_end_addr);
	} else {
		rc = regulator_enable(venus_data->gdsc);
		if (rc) {
			dprintk(VIDC_ERR, "GDSC enable failed\n");
			goto err;
		}

		rc = venus_clock_prepare_enable();
		if (rc) {
			dprintk(VIDC_ERR, "Clock prepare and enable failed\n");
			regulator_disable(venus_data->gdsc);
			goto err;
		}

		writel_relaxed(0, reg_base + VENUS_VBIF_AT_OLD_BASE);
		writel_relaxed(VENUS_REGION_SIZE,
				reg_base + VENUS_VBIF_AT_OLD_HIGH);
		writel_relaxed(fw_bias, reg_base + VENUS_VBIF_AT_NEW_BASE);
		writel_relaxed(fw_bias + VENUS_REGION_SIZE,
				reg_base + VENUS_VBIF_AT_NEW_HIGH);
		writel_relaxed(0x7F007F, reg_base + VENUS_VBIF_ADDR_TRANS_EN);
		venus_clock_disable_unprepare();
		regulator_disable(venus_data->gdsc);
	}
	/* Make sure all register writes are committed. */
	mb();

	/*
	 * Need to wait 10 cycles of internal clocks before bringing ARM9
	 * out of reset.
	 */
	udelay(1);

	if (iommu_present) {
		phys_addr_t pa = fw_bias;

		dprintk(VIDC_DBG, "Attached and created mapping for %s\n",
				dev_name(dev));

		/* Map virtual addr space 0 - fw_sz to fw phys addr space */
		rc = iommu_map(venus_data->domain,
			venus_data->fw_iova, pa, venus_data->fw_sz,
			IOMMU_READ|IOMMU_WRITE|IOMMU_PRIV);
		if (!rc) {
			dprintk(VIDC_DBG,
				"%s - Successfully mapped and performed test translation!\n",
				dev_name(dev));
		}

		if (rc || (venus_data->fw_iova != 0)) {
			dprintk(VIDC_ERR, "%s - Failed to setup IOMMU\n",
					dev_name(dev));
			goto err;
		}
	}
	/* Bring Arm9 out of reset */
	writel_relaxed(0, reg_base + VENUS_WRAPPER_SW_RESET);

	venus_data->is_booted = true;
	return 0;

err:
	return rc;
}

static int pil_venus_shutdown(void)
{
	void __iomem *reg_base = venus_data->reg_base;
	u32 reg;
	int rc;

	if (!venus_data->is_booted)
		return 0;

	/* Assert the reset to ARM9 */
	reg = readl_relaxed(reg_base + VENUS_WRAPPER_SW_RESET);
	reg |= BIT(4);
	writel_relaxed(reg, reg_base + VENUS_WRAPPER_SW_RESET);

	/* Make sure reset is asserted before the mapping is removed */
	mb();

	if (is_iommu_present(venus_data->resources)) {
		iommu_unmap(venus_data->domain, venus_data->fw_iova,
			venus_data->fw_sz);
	}
	/*
	 * Force the VBIF clk to be on to avoid AXI bridge halt ack failure
	 * for certain Venus version.
	 */
	if (venus_data->hw_ver_major == 0x1 &&
				(venus_data->hw_ver_minor == 0x2 ||
				venus_data->hw_ver_minor == 0x3)) {
		reg = readl_relaxed(reg_base + VIDC_VENUS_VBIF_CLK_ON);
		reg |= VENUS_VBIF_CLKON_FORCE_ON;
		writel_relaxed(reg, reg_base + VIDC_VENUS_VBIF_CLK_ON);
	}

	/* Halt AXI and AXI OCMEM VBIF Access */
	reg = readl_relaxed(reg_base + VENUS_VBIF_AXI_HALT_CTRL0);
	reg |= VENUS_VBIF_AXI_HALT_CTRL0_HALT_REQ;
	writel_relaxed(reg, reg_base + VENUS_VBIF_AXI_HALT_CTRL0);

	/* Request for AXI bus port halt */
	rc = readl_poll_timeout(reg_base + VENUS_VBIF_AXI_HALT_CTRL1,
			reg, reg & VENUS_VBIF_AXI_HALT_CTRL1_HALT_ACK,
			POLL_INTERVAL_US,
			VENUS_VBIF_AXI_HALT_ACK_TIMEOUT_US);
	if (rc)
		dprintk(VIDC_ERR, "Port halt timeout\n");

	venus_data->is_booted = false;

	return 0;
}

static int venus_notifier_cb(struct notifier_block *this, unsigned long code,
							void *ss_handle)
{
	struct notif_data *data = (struct notif_data *)ss_handle;
	static bool venus_data_set;
	int ret;

	if (!data->no_auth)
		return NOTIFY_DONE;

	if (!venus_data_set) {
		ret = venus_clock_setup();
		if (ret)
			return ret;

		ret = of_property_read_string(data->pdev->dev.of_node,
				"qcom,proxy-reg-names", &venus_data->reg_name);
		if (ret)
			return ret;

		venus_data->gdsc = devm_regulator_get(
				&data->pdev->dev, venus_data->reg_name);
		if (IS_ERR(venus_data->gdsc)) {
			dprintk(VIDC_ERR, "Failed to get Venus GDSC\n");
			return -ENODEV;
		}

		venus_data_set = true;
	}

	if (code != SUBSYS_AFTER_POWERUP && code != SUBSYS_AFTER_SHUTDOWN)
		return NOTIFY_DONE;

	ret = regulator_enable(venus_data->gdsc);
	if (ret) {
		dprintk(VIDC_ERR, "GDSC enable failed\n");
		return ret;
	}

	ret = venus_clock_prepare_enable();
	if (ret) {
		dprintk(VIDC_ERR, "Clock prepare and enable failed\n");
		goto err_clks;
	}

	if (code == SUBSYS_AFTER_POWERUP) {
		if (is_iommu_present(venus_data->resources))
			pil_venus_mem_setup(VENUS_REGION_SIZE);
		pil_venus_auth_and_reset();
	} else if (code == SUBSYS_AFTER_SHUTDOWN)
		pil_venus_shutdown();

	venus_clock_disable_unprepare();
	regulator_disable(venus_data->gdsc);

	return NOTIFY_DONE;
err_clks:
	regulator_disable(venus_data->gdsc);
	return ret;
}

static struct notifier_block venus_notifier = {
	.notifier_call = venus_notifier_cb,
};

int venus_boot_init(struct msm_vidc_platform_resources *res,
			struct context_bank_info *cb)
{
	int rc = 0;

	if (!res || !cb) {
		dprintk(VIDC_ERR, "Invalid platform resource handle\n");
		return -EINVAL;
	}
	venus_data = kzalloc(sizeof(*venus_data), GFP_KERNEL);
	if (!venus_data)
		return -ENOMEM;

	venus_data->resources = res;
	venus_data->iommu_ctx_bank_dev = cb->dev;
	if (!venus_data->iommu_ctx_bank_dev) {
		dprintk(VIDC_ERR, "Invalid venus context bank device\n");
		return -ENODEV;
	}
	venus_data->reg_base = ioremap_nocache(res->register_base,
			(unsigned long)res->register_size);
	if (!venus_data->reg_base) {
		dprintk(VIDC_ERR,
				"could not map reg addr %pa of size %d\n",
				&res->register_base, res->register_size);
		rc = -ENOMEM;
		goto err_ioremap_fail;
	}
	venus_data->venus_notif_hdle = subsys_notif_register_notifier("venus",
							&venus_notifier);
	if (IS_ERR(venus_data->venus_notif_hdle)) {
		dprintk(VIDC_ERR, "register event notification failed\n");
		rc = PTR_ERR(venus_data->venus_notif_hdle);
		goto err_subsys_notif;
	}

	return rc;

err_subsys_notif:
err_ioremap_fail:
	kfree(venus_data);
	return rc;
}

void venus_boot_deinit(void)
{
	venus_data->resources = NULL;
	subsys_notif_unregister_notifier(venus_data->venus_notif_hdle,
			&venus_notifier);
	kfree(venus_data);
}
