Contributors: 2
Author Tokens Token Proportion Commits Commit Proportion
Nicolas Frattaroli 4727 99.77% 1 50.00%
Brian Masney 11 0.23% 1 50.00%
Total 4738 2


// SPDX-License-Identifier: GPL-2.0-only
/*
 * Driver for MediaTek MFlexGraphics Devices
 *
 * Copyright (C) 2025, Collabora Ltd.
 */

#include <linux/completion.h>
#include <linux/clk.h>
#include <linux/clk-provider.h>
#include <linux/container_of.h>
#include <linux/iopoll.h>
#include <linux/mailbox_client.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_platform.h>
#include <linux/of_reserved_mem.h>
#include <linux/overflow.h>
#include <linux/platform_device.h>
#include <linux/pm_domain.h>
#include <linux/pm_opp.h>
#include <linux/regulator/consumer.h>
#include <linux/units.h>

#define GPR_LP_STATE		0x0028
#define   EB_ON_SUSPEND		0x0
#define   EB_ON_RESUME		0x1
#define GPR_IPI_MAGIC		0x34

#define RPC_PWR_CON		0x0504
#define   PWR_ACK_M		GENMASK(31, 30)
#define RPC_DUMMY_REG_2		0x0658
#define RPC_GHPM_CFG0_CON	0x0800
#define   GHPM_ENABLE_M		BIT(0)
#define   GHPM_ON_SEQ_M		BIT(2)
#define RPC_GHPM_RO0_CON	0x09A4
#define   GHPM_STATE_M		GENMASK(7, 0)
#define   GHPM_PWR_STATE_M	BIT(16)

#define GF_REG_MAGIC			0x0000
#define GF_REG_GPU_OPP_IDX		0x0004
#define GF_REG_STK_OPP_IDX		0x0008
#define GF_REG_GPU_OPP_NUM		0x000c
#define GF_REG_STK_OPP_NUM		0x0010
#define GF_REG_GPU_OPP_SNUM		0x0014
#define GF_REG_STK_OPP_SNUM		0x0018
#define GF_REG_POWER_COUNT		0x001c
#define GF_REG_BUCK_COUNT		0x0020
#define GF_REG_MTCMOS_COUNT		0x0024
#define GF_REG_CG_COUNT			0x0028 /* CG = Clock Gate? */
#define GF_REG_ACTIVE_COUNT		0x002C
#define GF_REG_TEMP_RAW			0x0030
#define GF_REG_TEMP_NORM_GPU		0x0034
#define GF_REG_TEMP_HIGH_GPU		0x0038
#define GF_REG_TEMP_NORM_STK		0x003C
#define GF_REG_TEMP_HIGH_STK		0x0040
#define GF_REG_FREQ_CUR_GPU		0x0044
#define GF_REG_FREQ_CUR_STK		0x0048
#define GF_REG_FREQ_OUT_GPU		0x004C /* Guess: actual achieved freq */
#define GF_REG_FREQ_OUT_STK		0x0050 /* Guess: actual achieved freq */
#define GF_REG_FREQ_METER_GPU		0x0054 /* Seems unused, always 0 */
#define GF_REG_FREQ_METER_STK		0x0058 /* Seems unused, always 0 */
#define GF_REG_VOLT_CUR_GPU		0x005C /* in tens of microvolts */
#define GF_REG_VOLT_CUR_STK		0x0060 /* in tens of microvolts */
#define GF_REG_VOLT_CUR_GPU_SRAM	0x0064
#define GF_REG_VOLT_CUR_STK_SRAM	0x0068
#define GF_REG_VOLT_CUR_GPU_REG		0x006C /* Seems unused, always 0 */
#define GF_REG_VOLT_CUR_STK_REG		0x0070 /* Seems unused, always 0 */
#define GF_REG_VOLT_CUR_GPU_REG_SRAM	0x0074
#define GF_REG_VOLT_CUR_STK_REG_SRAM	0x0078
#define GF_REG_PWR_CUR_GPU		0x007C /* in milliwatts */
#define GF_REG_PWR_CUR_STK		0x0080 /* in milliwatts */
#define GF_REG_PWR_MAX_GPU		0x0084 /* in milliwatts */
#define GF_REG_PWR_MAX_STK		0x0088 /* in milliwatts */
#define GF_REG_PWR_MIN_GPU		0x008C /* in milliwatts */
#define GF_REG_PWR_MIN_STK		0x0090 /* in milliwatts */
#define GF_REG_LEAKAGE_RT_GPU		0x0094 /* Unknown */
#define GF_REG_LEAKAGE_RT_STK		0x0098 /* Unknown */
#define GF_REG_LEAKAGE_RT_SRAM		0x009C /* Unknown */
#define GF_REG_LEAKAGE_HT_GPU		0x00A0 /* Unknown */
#define GF_REG_LEAKAGE_HT_STK		0x00A4 /* Unknown */
#define GF_REG_LEAKAGE_HT_SRAM		0x00A8 /* Unknown */
#define GF_REG_VOLT_DAC_LOW_GPU		0x00AC /* Seems unused, always 0 */
#define GF_REG_VOLT_DAC_LOW_STK		0x00B0 /* Seems unused, always 0 */
#define GF_REG_OPP_CUR_CEIL		0x00B4
#define GF_REG_OPP_CUR_FLOOR		0x00B8
#define GF_REG_OPP_CUR_LIMITER_CEIL	0x00BC
#define GF_REG_OPP_CUR_LIMITER_FLOOR	0x00C0
#define GF_REG_OPP_PRIORITY_CEIL	0x00C4
#define GF_REG_OPP_PRIORITY_FLOOR	0x00C8
#define GF_REG_PWR_CTL			0x00CC
#define GF_REG_ACTIVE_SLEEP_CTL		0x00D0
#define GF_REG_DVFS_STATE		0x00D4
#define GF_REG_SHADER_PRESENT		0x00D8
#define GF_REG_ASENSOR_ENABLE		0x00DC
#define GF_REG_AGING_LOAD		0x00E0
#define GF_REG_AGING_MARGIN		0x00E4
#define GF_REG_AVS_ENABLE		0x00E8
#define GF_REG_AVS_MARGIN		0x00EC
#define GF_REG_CHIP_TYPE		0x00F0
#define GF_REG_SB_VERSION		0x00F4
#define GF_REG_PTP_VERSION		0x00F8
#define GF_REG_DBG_VERSION		0x00FC
#define GF_REG_KDBG_VERSION		0x0100
#define GF_REG_GPM1_MODE		0x0104
#define GF_REG_GPM3_MODE		0x0108
#define GF_REG_DFD_MODE			0x010C
#define GF_REG_DUAL_BUCK		0x0110
#define GF_REG_SEGMENT_ID		0x0114
#define GF_REG_POWER_TIME_H		0x0118
#define GF_REG_POWER_TIME_L		0x011C
#define GF_REG_PWR_STATUS		0x0120
#define GF_REG_STRESS_TEST		0x0124
#define GF_REG_TEST_MODE		0x0128
#define GF_REG_IPS_MODE			0x012C
#define GF_REG_TEMP_COMP_MODE		0x0130
#define GF_REG_HT_TEMP_COMP_MODE	0x0134
#define GF_REG_PWR_TRACKER_MODE		0x0138
#define GF_REG_OPP_TABLE_GPU		0x0314
#define GF_REG_OPP_TABLE_STK		0x09A4
#define GF_REG_OPP_TABLE_GPU_S		0x1034
#define GF_REG_OPP_TABLE_STK_S		0x16c4
#define GF_REG_LIMIT_TABLE		0x1d54
#define GF_REG_GPM3_TABLE		0x223C

#define MFG_MT8196_E2_ID		0x101
#define GPUEB_SLEEP_MAGIC		0x55667788UL
#define GPUEB_MEM_MAGIC			0xBABADADAUL

#define GPUEB_TIMEOUT_US		10000UL
#define GPUEB_POLL_US			50

#define MAX_OPP_NUM			70

#define GPUEB_MBOX_MAX_RX_SIZE		32 /* in bytes */

/*
 * This enum is part of the ABI of the GPUEB firmware. Don't change the
 * numbering, as you would wreak havoc.
 */
enum mtk_mfg_ipi_cmd {
	CMD_INIT_SHARED_MEM		= 0,
	CMD_GET_FREQ_BY_IDX		= 1,
	CMD_GET_POWER_BY_IDX		= 2,
	CMD_GET_OPPIDX_BY_FREQ		= 3,
	CMD_GET_LEAKAGE_POWER		= 4,
	CMD_SET_LIMIT			= 5,
	CMD_POWER_CONTROL		= 6,
	CMD_ACTIVE_SLEEP_CONTROL	= 7,
	CMD_COMMIT			= 8,
	CMD_DUAL_COMMIT			= 9,
	CMD_PDCA_CONFIG			= 10,
	CMD_UPDATE_DEBUG_OPP_INFO	= 11,
	CMD_SWITCH_LIMIT		= 12,
	CMD_FIX_TARGET_OPPIDX		= 13,
	CMD_FIX_DUAL_TARGET_OPPIDX	= 14,
	CMD_FIX_CUSTOM_FREQ_VOLT	= 15,
	CMD_FIX_DUAL_CUSTOM_FREQ_VOLT	= 16,
	CMD_SET_MFGSYS_CONFIG		= 17,
	CMD_MSSV_COMMIT			= 18,
	CMD_NUM				= 19,
};

/*
 * This struct is part of the ABI of the GPUEB firmware. Changing it, or
 * reordering fields in it, will break things, so don't do it. Thank you.
 */
struct __packed mtk_mfg_ipi_msg {
	__le32 magic;
	__le32 cmd;
	__le32 target;
	/*
	 * Downstream relies on the compiler to implicitly add the following
	 * padding, as it declares the struct as non-packed.
	 */
	__le32 reserved;
	union {
		s32 __bitwise oppidx;
		s32 __bitwise return_value;
		__le32 freq;
		__le32 volt;
		__le32 power;
		__le32 power_state;
		__le32 mode;
		__le32 value;
		struct {
			__le64 base;
			__le32 size;
		} shared_mem;
		struct {
			__le32 freq;
			__le32 volt;
		} custom;
		struct {
			__le32 limiter;
			s32 __bitwise ceiling_info;
			s32 __bitwise floor_info;
		} set_limit;
		struct {
			__le32 target;
			__le32 val;
		} mfg_cfg;
		struct {
			__le32 target;
			__le32 val;
		} mssv;
		struct {
			s32 __bitwise gpu_oppidx;
			s32 __bitwise stack_oppidx;
		} dual_commit;
		struct {
			__le32 fgpu;
			__le32 vgpu;
			__le32 fstack;
			__le32 vstack;
		} dual_custom;
	} u;
};

struct __packed mtk_mfg_ipi_sleep_msg {
	__le32 event;
	__le32 state;
	__le32 magic;
};

/**
 * struct mtk_mfg_opp_entry - OPP table entry from firmware
 * @freq_khz: The operating point's frequency in kilohertz
 * @voltage_core: The operating point's core voltage in tens of microvolts
 * @voltage_sram: The operating point's SRAM voltage in tens of microvolts
 * @posdiv: exponent of base 2 for PLL frequency divisor used for this OPP
 * @voltage_margin: Number of tens of microvolts the voltage can be undershot
 * @power_mw: estimate of power usage at this operating point, in milliwatts
 *
 * This struct is part of the ABI with the EB firmware. Do not change it.
 */
struct __packed mtk_mfg_opp_entry {
	__le32 freq_khz;
	__le32 voltage_core;
	__le32 voltage_sram;
	__le32 posdiv;
	__le32 voltage_margin;
	__le32 power_mw;
};

struct mtk_mfg_mbox {
	struct mbox_client cl;
	struct completion rx_done;
	struct mtk_mfg *mfg;
	struct mbox_chan *ch;
	void *rx_data;
};

struct mtk_mfg {
	struct generic_pm_domain pd;
	struct platform_device *pdev;
	struct clk *clk_eb;
	struct clk_bulk_data *gpu_clks;
	struct clk_hw clk_core_hw;
	struct clk_hw clk_stack_hw;
	struct regulator_bulk_data *gpu_regs;
	void __iomem *rpc;
	void __iomem *gpr;
	void __iomem *shared_mem;
	phys_addr_t shared_mem_phys;
	unsigned int shared_mem_size;
	u16 ghpm_en_reg;
	u32 ipi_magic;
	unsigned short num_gpu_opps;
	unsigned short num_stack_opps;
	struct dev_pm_opp_data *gpu_opps;
	struct dev_pm_opp_data *stack_opps;
	struct mtk_mfg_mbox *gf_mbox;
	struct mtk_mfg_mbox *slp_mbox;
	const struct mtk_mfg_variant *variant;
};

struct mtk_mfg_variant {
	const char *const *clk_names;
	unsigned int num_clks;
	const char *const *regulator_names;
	unsigned int num_regulators;
	/** @turbo_below: opp indices below this value are considered turbo */
	unsigned int turbo_below;
	int (*init)(struct mtk_mfg *mfg);
};

static inline struct mtk_mfg *mtk_mfg_from_genpd(struct generic_pm_domain *pd)
{
	return container_of(pd, struct mtk_mfg, pd);
}

static inline void mtk_mfg_update_reg_bits(void __iomem *addr, u32 mask, u32 val)
{
	writel((readl(addr) & ~mask) | (val & mask), addr);
}

static inline bool mtk_mfg_is_powered_on(struct mtk_mfg *mfg)
{
	return (readl(mfg->rpc + RPC_PWR_CON) & PWR_ACK_M) == PWR_ACK_M;
}

static unsigned long mtk_mfg_recalc_rate_gpu(struct clk_hw *hw,
					     unsigned long parent_rate)
{
	struct mtk_mfg *mfg = container_of(hw, struct mtk_mfg, clk_core_hw);

	return readl(mfg->shared_mem + GF_REG_FREQ_OUT_GPU) * HZ_PER_KHZ;
}

static int mtk_mfg_determine_rate(struct clk_hw *hw,
				  struct clk_rate_request *req)
{
	/*
	 * The determine_rate callback needs to be implemented to avoid returning
	 * the current clock frequency, rather than something even remotely
	 * close to the frequency that was asked for.
	 *
	 * Instead of writing considerable amounts of possibly slow code just to
	 * somehow figure out which of the three PLLs to round for, or even to
	 * do a search through one of two OPP tables in order to find the closest
	 * OPP of a frequency, just return the rate as-is. This avoids devfreq
	 * "rounding" a request for the lowest frequency to the possibly very
	 * high current frequency, breaking the powersave governor in the process.
	 */

	return 0;
}

static unsigned long mtk_mfg_recalc_rate_stack(struct clk_hw *hw,
					       unsigned long parent_rate)
{
	struct mtk_mfg *mfg = container_of(hw, struct mtk_mfg, clk_stack_hw);

	return readl(mfg->shared_mem + GF_REG_FREQ_OUT_STK) * HZ_PER_KHZ;
}

static const struct clk_ops mtk_mfg_clk_gpu_ops = {
	.recalc_rate = mtk_mfg_recalc_rate_gpu,
	.determine_rate = mtk_mfg_determine_rate,
};

static const struct clk_ops mtk_mfg_clk_stack_ops = {
	.recalc_rate = mtk_mfg_recalc_rate_stack,
	.determine_rate = mtk_mfg_determine_rate,
};

static const struct clk_init_data mtk_mfg_clk_gpu_init = {
	.name = "gpu-core",
	.ops = &mtk_mfg_clk_gpu_ops,
	.flags = CLK_GET_RATE_NOCACHE,
};

static const struct clk_init_data mtk_mfg_clk_stack_init = {
	.name = "gpu-stack",
	.ops = &mtk_mfg_clk_stack_ops,
	.flags = CLK_GET_RATE_NOCACHE,
};

static int mtk_mfg_eb_on(struct mtk_mfg *mfg)
{
	struct device *dev = &mfg->pdev->dev;
	u32 val;
	int ret;

	/*
	 * If MFG is already on from e.g. the bootloader, skip doing the
	 * power-on sequence, as it wouldn't work without powering it off first.
	 */
	if (mtk_mfg_is_powered_on(mfg))
		return 0;

	ret = readl_poll_timeout(mfg->rpc + RPC_GHPM_RO0_CON, val,
				 !(val & (GHPM_PWR_STATE_M | GHPM_STATE_M)),
				 GPUEB_POLL_US, GPUEB_TIMEOUT_US);
	if (ret) {
		dev_err(dev, "timed out waiting for EB to power on\n");
		return ret;
	}

	mtk_mfg_update_reg_bits(mfg->rpc + mfg->ghpm_en_reg, GHPM_ENABLE_M,
				GHPM_ENABLE_M);

	mtk_mfg_update_reg_bits(mfg->rpc + RPC_GHPM_CFG0_CON, GHPM_ON_SEQ_M, 0);
	mtk_mfg_update_reg_bits(mfg->rpc + RPC_GHPM_CFG0_CON, GHPM_ON_SEQ_M,
				GHPM_ON_SEQ_M);

	mtk_mfg_update_reg_bits(mfg->rpc + mfg->ghpm_en_reg, GHPM_ENABLE_M, 0);


	ret = readl_poll_timeout(mfg->rpc + RPC_PWR_CON, val,
				 (val & PWR_ACK_M) == PWR_ACK_M,
				 GPUEB_POLL_US, GPUEB_TIMEOUT_US);
	if (ret) {
		dev_err(dev, "timed out waiting for EB power ack, val = 0x%X\n",
			val);
		return ret;
	}

	ret = readl_poll_timeout(mfg->gpr + GPR_LP_STATE, val,
				 (val == EB_ON_RESUME),
				 GPUEB_POLL_US, GPUEB_TIMEOUT_US);
	if (ret) {
		dev_err(dev, "timed out waiting for EB to resume, status = 0x%X\n", val);
		return ret;
	}

	return 0;
}

static int mtk_mfg_eb_off(struct mtk_mfg *mfg)
{
	struct device *dev = &mfg->pdev->dev;
	struct mtk_mfg_ipi_sleep_msg msg = {
		.event = 0,
		.state = 0,
		.magic = GPUEB_SLEEP_MAGIC
	};
	u32 val;
	int ret;

	ret = mbox_send_message(mfg->slp_mbox->ch, &msg);
	if (ret < 0) {
		dev_err(dev, "Cannot send sleep command: %pe\n", ERR_PTR(ret));
		return ret;
	}

	ret = readl_poll_timeout(mfg->rpc + RPC_PWR_CON, val,
				 !(val & PWR_ACK_M), GPUEB_POLL_US,
				 GPUEB_TIMEOUT_US);

	if (ret) {
		dev_err(dev, "Timed out waiting for EB to power off, val=0x%08X\n", val);
		return ret;
	}

	return 0;
}

/**
 * mtk_mfg_send_ipi - synchronously send an IPI message on the gpufreq channel
 * @mfg: pointer to this driver instance's private &struct mtk_mfg
 * @msg: pointer to a message to send; will have magic filled and response assigned
 *
 * Send an IPI message on the gpufreq channel, and wait for a response. Once a
 * response is received, assign a pointer to the response buffer (valid until
 * next response is received) to @msg.
 *
 * Returns 0 on success, negative errno on failure.
 */
static int mtk_mfg_send_ipi(struct mtk_mfg *mfg, struct mtk_mfg_ipi_msg *msg)
{
	struct device *dev = &mfg->pdev->dev;
	unsigned long wait;
	int ret;

	msg->magic = mfg->ipi_magic;

	ret = mbox_send_message(mfg->gf_mbox->ch, msg);
	if (ret < 0) {
		dev_err(dev, "Cannot send GPUFreq IPI command: %pe\n", ERR_PTR(ret));
		return ret;
	}

	wait = wait_for_completion_timeout(&mfg->gf_mbox->rx_done, msecs_to_jiffies(500));
	if (!wait)
		return -ETIMEDOUT;

	msg = mfg->gf_mbox->rx_data;

	if (msg->u.return_value < 0) {
		dev_err(dev, "IPI return: %d\n", msg->u.return_value);
		return -EPROTO;
	}

	return 0;
}

static int mtk_mfg_init_shared_mem(struct mtk_mfg *mfg)
{
	struct device *dev = &mfg->pdev->dev;
	struct mtk_mfg_ipi_msg msg = {};
	int ret;

	dev_dbg(dev, "clearing GPUEB shared memory, 0x%X bytes\n", mfg->shared_mem_size);
	memset_io(mfg->shared_mem, 0, mfg->shared_mem_size);

	msg.cmd = CMD_INIT_SHARED_MEM;
	msg.u.shared_mem.base = mfg->shared_mem_phys;
	msg.u.shared_mem.size = mfg->shared_mem_size;

	ret = mtk_mfg_send_ipi(mfg, &msg);
	if (ret)
		return ret;

	if (readl(mfg->shared_mem + GF_REG_MAGIC) != GPUEB_MEM_MAGIC) {
		dev_err(dev, "EB did not initialise shared memory correctly\n");
		return -EIO;
	}

	return 0;
}

static int mtk_mfg_power_control(struct mtk_mfg *mfg, bool enabled)
{
	struct mtk_mfg_ipi_msg msg = {};

	msg.cmd = CMD_POWER_CONTROL;
	msg.u.power_state = enabled ? 1 : 0;

	return mtk_mfg_send_ipi(mfg, &msg);
}

static int mtk_mfg_set_oppidx(struct mtk_mfg *mfg, unsigned int opp_idx)
{
	struct mtk_mfg_ipi_msg msg = {};
	int ret;

	if (opp_idx >= mfg->num_gpu_opps)
		return -EINVAL;

	msg.cmd = CMD_FIX_DUAL_TARGET_OPPIDX;
	msg.u.dual_commit.gpu_oppidx = opp_idx;
	msg.u.dual_commit.stack_oppidx = opp_idx;

	ret = mtk_mfg_send_ipi(mfg, &msg);
	if (ret) {
		dev_err(&mfg->pdev->dev, "Failed to set OPP %u: %pe\n",
			opp_idx, ERR_PTR(ret));
		return ret;
	}

	return 0;
}

static int mtk_mfg_read_opp_tables(struct mtk_mfg *mfg)
{
	struct device *dev = &mfg->pdev->dev;
	struct mtk_mfg_opp_entry e = {};
	unsigned int i;

	mfg->num_gpu_opps = readl(mfg->shared_mem + GF_REG_GPU_OPP_NUM);
	mfg->num_stack_opps = readl(mfg->shared_mem + GF_REG_STK_OPP_NUM);

	if (mfg->num_gpu_opps > MAX_OPP_NUM || mfg->num_gpu_opps == 0) {
		dev_err(dev, "GPU OPP count (%u) out of range %u >= count > 0\n",
			mfg->num_gpu_opps, MAX_OPP_NUM);
		return -EINVAL;
	}

	if (mfg->num_stack_opps && mfg->num_stack_opps > MAX_OPP_NUM) {
		dev_err(dev, "Stack OPP count (%u) out of range %u >= count >= 0\n",
			mfg->num_stack_opps, MAX_OPP_NUM);
		return -EINVAL;
	}

	mfg->gpu_opps = devm_kcalloc(dev, mfg->num_gpu_opps,
				     sizeof(struct dev_pm_opp_data), GFP_KERNEL);
	if (!mfg->gpu_opps)
		return -ENOMEM;

	if (mfg->num_stack_opps) {
		mfg->stack_opps = devm_kcalloc(dev, mfg->num_stack_opps,
					       sizeof(struct dev_pm_opp_data), GFP_KERNEL);
		if (!mfg->stack_opps)
			return -ENOMEM;
	}

	for (i = 0; i < mfg->num_gpu_opps; i++) {
		memcpy_fromio(&e, mfg->shared_mem + GF_REG_OPP_TABLE_GPU + i * sizeof(e),
			      sizeof(e));
		if (mem_is_zero(&e, sizeof(e))) {
			dev_err(dev, "ran into an empty GPU OPP at index %u\n",
				i);
			return -EINVAL;
		}
		mfg->gpu_opps[i].freq = e.freq_khz * HZ_PER_KHZ;
		mfg->gpu_opps[i].u_volt = e.voltage_core * 10;
		mfg->gpu_opps[i].level = i;
		if (i < mfg->variant->turbo_below)
			mfg->gpu_opps[i].turbo = true;
	}

	for (i = 0; i < mfg->num_stack_opps; i++) {
		memcpy_fromio(&e, mfg->shared_mem + GF_REG_OPP_TABLE_STK + i * sizeof(e),
			      sizeof(e));
		if (mem_is_zero(&e, sizeof(e))) {
			dev_err(dev, "ran into an empty Stack OPP at index %u\n",
				i);
			return -EINVAL;
		}
		mfg->stack_opps[i].freq = e.freq_khz * HZ_PER_KHZ;
		mfg->stack_opps[i].u_volt = e.voltage_core * 10;
		mfg->stack_opps[i].level = i;
		if (i < mfg->variant->turbo_below)
			mfg->stack_opps[i].turbo = true;
	}

	return 0;
}

static const char *const mtk_mfg_mt8196_clk_names[] = {
	"core",
	"stack0",
	"stack1",
};

static const char *const mtk_mfg_mt8196_regulators[] = {
	"core",
	"stack",
	"sram",
};

static int mtk_mfg_mt8196_init(struct mtk_mfg *mfg)
{
	void __iomem *e2_base;

	e2_base = devm_platform_ioremap_resource_byname(mfg->pdev, "hw-revision");
	if (IS_ERR(e2_base))
		return dev_err_probe(&mfg->pdev->dev, PTR_ERR(e2_base),
				     "Couldn't get hw-revision register\n");

	clk_prepare_enable(mfg->clk_eb);

	if (readl(e2_base) == MFG_MT8196_E2_ID)
		mfg->ghpm_en_reg = RPC_DUMMY_REG_2;
	else
		mfg->ghpm_en_reg = RPC_GHPM_CFG0_CON;

	clk_disable_unprepare(mfg->clk_eb);

	return 0;
}

static const struct mtk_mfg_variant mtk_mfg_mt8196_variant = {
	.clk_names = mtk_mfg_mt8196_clk_names,
	.num_clks = ARRAY_SIZE(mtk_mfg_mt8196_clk_names),
	.regulator_names = mtk_mfg_mt8196_regulators,
	.num_regulators = ARRAY_SIZE(mtk_mfg_mt8196_regulators),
	.turbo_below = 7,
	.init = mtk_mfg_mt8196_init,
};

static void mtk_mfg_mbox_rx_callback(struct mbox_client *cl, void *mssg)
{
	struct mtk_mfg_mbox *mb = container_of(cl, struct mtk_mfg_mbox, cl);

	if (mb->rx_data)
		mb->rx_data = memcpy(mb->rx_data, mssg, GPUEB_MBOX_MAX_RX_SIZE);
	complete(&mb->rx_done);
}

static int mtk_mfg_attach_dev(struct generic_pm_domain *pd, struct device *dev)
{
	struct mtk_mfg *mfg = mtk_mfg_from_genpd(pd);
	struct dev_pm_opp_data *so = mfg->stack_opps;
	struct dev_pm_opp_data *go = mfg->gpu_opps;
	struct dev_pm_opp_data *prev_o;
	struct dev_pm_opp_data *o;
	int i, ret;

	for (i = mfg->num_gpu_opps - 1; i >= 0; i--) {
		/*
		 * Adding the lower of the two OPPs avoids gaps of indices in
		 * situations where the GPU OPPs are duplicated a couple of
		 * times when only the Stack OPP is being lowered at that index.
		 */
		if (i >= mfg->num_stack_opps || go[i].freq < so[i].freq)
			o = &go[i];
		else
			o = &so[i];

		/*
		 * Skip indices where both GPU and Stack OPPs are equal. Nominally,
		 * OPP core shouldn't care about dupes, but not doing so will cause
		 * dev_pm_opp_find_freq_ceil_indexed to -ERANGE later down the line.
		 */
		if (prev_o && prev_o->freq == o->freq)
			continue;

		ret = dev_pm_opp_add_dynamic(dev, o);
		if (ret) {
			dev_err(dev, "Failed to add OPP level %u from PD %s: %pe\n",
				o->level, pd->name, ERR_PTR(ret));
			dev_pm_opp_remove_all_dynamic(dev);
			return ret;
		}
		prev_o = o;
	}

	return 0;
}

static void mtk_mfg_detach_dev(struct generic_pm_domain *pd, struct device *dev)
{
	dev_pm_opp_remove_all_dynamic(dev);
}

static int mtk_mfg_set_performance(struct generic_pm_domain *pd,
				   unsigned int state)
{
	struct mtk_mfg *mfg = mtk_mfg_from_genpd(pd);

	/*
	 * pmdomain core intentionally sets a performance state before turning
	 * a domain on, and after turning it off. For the GPUEB however, it's
	 * only possible to act on performance requests when the GPUEB is
	 * powered on. To do this, return cleanly without taking action, and
	 * defer setting what pmdomain core set in mtk_mfg_power_on.
	 */
	if (mfg->pd.status != GENPD_STATE_ON)
		return 0;

	return mtk_mfg_set_oppidx(mfg, state);
}

static int mtk_mfg_power_on(struct generic_pm_domain *pd)
{
	struct mtk_mfg *mfg = mtk_mfg_from_genpd(pd);
	int ret;

	ret = regulator_bulk_enable(mfg->variant->num_regulators,
				    mfg->gpu_regs);
	if (ret)
		return ret;

	ret = clk_prepare_enable(mfg->clk_eb);
	if (ret)
		goto err_disable_regulators;

	ret = clk_bulk_prepare_enable(mfg->variant->num_clks, mfg->gpu_clks);
	if (ret)
		goto err_disable_eb_clk;

	ret = mtk_mfg_eb_on(mfg);
	if (ret)
		goto err_disable_clks;

	mfg->ipi_magic = readl(mfg->gpr + GPR_IPI_MAGIC);

	ret = mtk_mfg_power_control(mfg, true);
	if (ret)
		goto err_eb_off;

	/* Don't try to set a OPP in probe before OPPs have been read from EB */
	if (mfg->gpu_opps) {
		/* The aforementioned deferred setting of pmdomain's state */
		ret = mtk_mfg_set_oppidx(mfg, pd->performance_state);
		if (ret)
			dev_warn(&mfg->pdev->dev, "Failed to set oppidx in %s\n", __func__);
	}

	return 0;

err_eb_off:
	mtk_mfg_eb_off(mfg);
err_disable_clks:
	clk_bulk_disable_unprepare(mfg->variant->num_clks, mfg->gpu_clks);
err_disable_eb_clk:
	clk_disable_unprepare(mfg->clk_eb);
err_disable_regulators:
	regulator_bulk_disable(mfg->variant->num_regulators, mfg->gpu_regs);

	return ret;
}

static int mtk_mfg_power_off(struct generic_pm_domain *pd)
{
	struct mtk_mfg *mfg = mtk_mfg_from_genpd(pd);
	struct device *dev = &mfg->pdev->dev;
	int ret;

	ret = mtk_mfg_power_control(mfg, false);
	if (ret) {
		dev_err(dev, "power_control failed: %pe\n", ERR_PTR(ret));
		return ret;
	}

	ret = mtk_mfg_eb_off(mfg);
	if (ret) {
		dev_err(dev, "eb_off failed: %pe\n", ERR_PTR(ret));
		return ret;
	}

	clk_bulk_disable_unprepare(mfg->variant->num_clks, mfg->gpu_clks);
	clk_disable_unprepare(mfg->clk_eb);
	ret = regulator_bulk_disable(mfg->variant->num_regulators, mfg->gpu_regs);
	if (ret) {
		dev_err(dev, "Disabling regulators failed: %pe\n", ERR_PTR(ret));
		return ret;
	}

	return 0;
}

static int mtk_mfg_init_mbox(struct mtk_mfg *mfg)
{
	struct device *dev = &mfg->pdev->dev;
	struct mtk_mfg_mbox *gf;
	struct mtk_mfg_mbox *slp;

	gf = devm_kzalloc(dev, sizeof(*gf), GFP_KERNEL);
	if (!gf)
		return -ENOMEM;

	gf->rx_data = devm_kzalloc(dev, GPUEB_MBOX_MAX_RX_SIZE, GFP_KERNEL);
	if (!gf->rx_data)
		return -ENOMEM;

	gf->mfg = mfg;
	init_completion(&gf->rx_done);
	gf->cl.dev = dev;
	gf->cl.rx_callback = mtk_mfg_mbox_rx_callback;
	gf->cl.tx_tout = GPUEB_TIMEOUT_US / USEC_PER_MSEC;
	gf->ch = mbox_request_channel_byname(&gf->cl, "gpufreq");
	if (IS_ERR(gf->ch))
		return PTR_ERR(gf->ch);

	mfg->gf_mbox = gf;

	slp = devm_kzalloc(dev, sizeof(*slp), GFP_KERNEL);
	if (!slp)
		return -ENOMEM;

	slp->mfg = mfg;
	init_completion(&slp->rx_done);
	slp->cl.dev = dev;
	slp->cl.tx_tout = GPUEB_TIMEOUT_US / USEC_PER_MSEC;
	slp->cl.tx_block = true;
	slp->ch = mbox_request_channel_byname(&slp->cl, "sleep");
	if (IS_ERR(slp->ch)) {
		mbox_free_channel(gf->ch);
		return PTR_ERR(slp->ch);
	}

	mfg->slp_mbox = slp;

	return 0;
}

static int mtk_mfg_init_clk_provider(struct mtk_mfg *mfg)
{
	struct device *dev = &mfg->pdev->dev;
	struct clk_hw_onecell_data *clk_data;
	int ret;

	clk_data = devm_kzalloc(dev, struct_size(clk_data, hws, 2), GFP_KERNEL);
	if (!clk_data)
		return -ENOMEM;

	clk_data->num = 2;

	mfg->clk_core_hw.init = &mtk_mfg_clk_gpu_init;
	mfg->clk_stack_hw.init = &mtk_mfg_clk_stack_init;

	ret = devm_clk_hw_register(dev, &mfg->clk_core_hw);
	if (ret)
		return dev_err_probe(dev, ret, "Couldn't register GPU core clock\n");

	ret = devm_clk_hw_register(dev, &mfg->clk_stack_hw);
	if (ret)
		return dev_err_probe(dev, ret, "Couldn't register GPU stack clock\n");

	clk_data->hws[0] = &mfg->clk_core_hw;
	clk_data->hws[1] = &mfg->clk_stack_hw;

	ret = devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, clk_data);
	if (ret)
		return dev_err_probe(dev, ret, "Couldn't register clock provider\n");

	return 0;
}

static int mtk_mfg_probe(struct platform_device *pdev)
{
	struct mtk_mfg *mfg;
	struct device *dev = &pdev->dev;
	const struct mtk_mfg_variant *data = of_device_get_match_data(dev);
	struct resource res;
	int ret, i;

	mfg = devm_kzalloc(dev, sizeof(*mfg), GFP_KERNEL);
	if (!mfg)
		return -ENOMEM;

	mfg->pdev = pdev;
	mfg->variant = data;

	dev_set_drvdata(dev, mfg);

	mfg->gpr = devm_platform_ioremap_resource(pdev, 0);
	if (IS_ERR(mfg->gpr))
		return dev_err_probe(dev, PTR_ERR(mfg->gpr),
				     "Couldn't retrieve GPR MMIO registers\n");

	mfg->rpc = devm_platform_ioremap_resource(pdev, 1);
	if (IS_ERR(mfg->rpc))
		return dev_err_probe(dev, PTR_ERR(mfg->rpc),
				     "Couldn't retrieve RPC MMIO registers\n");

	mfg->clk_eb = devm_clk_get(dev, "eb");
	if (IS_ERR(mfg->clk_eb))
		return dev_err_probe(dev, PTR_ERR(mfg->clk_eb),
				     "Couldn't get 'eb' clock\n");

	mfg->gpu_clks = devm_kcalloc(dev, data->num_clks, sizeof(*mfg->gpu_clks),
				     GFP_KERNEL);
	if (!mfg->gpu_clks)
		return -ENOMEM;

	for (i = 0; i < data->num_clks; i++)
		mfg->gpu_clks[i].id = data->clk_names[i];

	ret = devm_clk_bulk_get(dev, data->num_clks, mfg->gpu_clks);
	if (ret)
		return dev_err_probe(dev, ret, "Couldn't get GPU clocks\n");

	mfg->gpu_regs = devm_kcalloc(dev, data->num_regulators,
				     sizeof(*mfg->gpu_regs), GFP_KERNEL);
	if (!mfg->gpu_regs)
		return -ENOMEM;

	for (i = 0; i < data->num_regulators; i++)
		mfg->gpu_regs[i].supply = data->regulator_names[i];

	ret = devm_regulator_bulk_get(dev, data->num_regulators, mfg->gpu_regs);
	if (ret)
		return dev_err_probe(dev, ret, "Couldn't get GPU regulators\n");

	ret = of_reserved_mem_region_to_resource(dev->of_node, 0, &res);
	if (ret)
		return dev_err_probe(dev, ret, "Couldn't get GPUEB shared memory\n");

	mfg->shared_mem = devm_ioremap(dev, res.start, resource_size(&res));
	if (!mfg->shared_mem)
		return dev_err_probe(dev, -ENOMEM, "Can't ioremap GPUEB shared memory\n");
	mfg->shared_mem_size = resource_size(&res);
	mfg->shared_mem_phys = res.start;

	if (data->init) {
		ret = data->init(mfg);
		if (ret)
			return dev_err_probe(dev, ret, "Variant init failed\n");
	}

	mfg->pd.name = dev_name(dev);
	mfg->pd.attach_dev = mtk_mfg_attach_dev;
	mfg->pd.detach_dev = mtk_mfg_detach_dev;
	mfg->pd.power_off = mtk_mfg_power_off;
	mfg->pd.power_on = mtk_mfg_power_on;
	mfg->pd.set_performance_state = mtk_mfg_set_performance;
	mfg->pd.flags = GENPD_FLAG_OPP_TABLE_FW;

	ret = pm_genpd_init(&mfg->pd, NULL, false);
	if (ret)
		return dev_err_probe(dev, ret, "Failed to initialise power domain\n");

	ret = mtk_mfg_init_mbox(mfg);
	if (ret) {
		dev_err_probe(dev, ret, "Couldn't initialise mailbox\n");
		goto err_remove_genpd;
	}

	ret = mtk_mfg_power_on(&mfg->pd);
	if (ret) {
		dev_err_probe(dev, ret, "Failed to power on MFG\n");
		goto err_free_mbox;
	}

	ret = mtk_mfg_init_shared_mem(mfg);
	if (ret) {
		dev_err_probe(dev, ret, "Couldn't initialize EB shared memory\n");
		goto err_power_off;
	}

	ret = mtk_mfg_read_opp_tables(mfg);
	if (ret) {
		dev_err_probe(dev, ret, "Error reading OPP tables from EB\n");
		goto err_power_off;
	}

	ret = mtk_mfg_init_clk_provider(mfg);
	if (ret)
		goto err_power_off;

	ret = of_genpd_add_provider_simple(dev->of_node, &mfg->pd);
	if (ret) {
		dev_err_probe(dev, ret, "Failed to add pmdomain provider\n");
		goto err_power_off;
	}

	return 0;

err_power_off:
	mtk_mfg_power_off(&mfg->pd);
err_free_mbox:
	mbox_free_channel(mfg->slp_mbox->ch);
	mfg->slp_mbox->ch = NULL;
	mbox_free_channel(mfg->gf_mbox->ch);
	mfg->gf_mbox->ch = NULL;
err_remove_genpd:
	pm_genpd_remove(&mfg->pd);

	return ret;
}

static const struct of_device_id mtk_mfg_of_match[] = {
	{ .compatible = "mediatek,mt8196-gpufreq", .data = &mtk_mfg_mt8196_variant },
	{}
};
MODULE_DEVICE_TABLE(of, mtk_mfg_of_match);

static void mtk_mfg_remove(struct platform_device *pdev)
{
	struct mtk_mfg *mfg = dev_get_drvdata(&pdev->dev);

	if (mtk_mfg_is_powered_on(mfg))
		mtk_mfg_power_off(&mfg->pd);

	of_genpd_del_provider(pdev->dev.of_node);
	pm_genpd_remove(&mfg->pd);

	mbox_free_channel(mfg->gf_mbox->ch);
	mfg->gf_mbox->ch = NULL;

	mbox_free_channel(mfg->slp_mbox->ch);
	mfg->slp_mbox->ch = NULL;
}

static struct platform_driver mtk_mfg_driver = {
	.driver = {
		.name = "mtk-mfg-pmdomain",
		.of_match_table = mtk_mfg_of_match,
		.suppress_bind_attrs = true,
	},
	.probe = mtk_mfg_probe,
	.remove = mtk_mfg_remove,
};
module_platform_driver(mtk_mfg_driver);

MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
MODULE_DESCRIPTION("MediaTek MFlexGraphics Power Domain Driver");
MODULE_LICENSE("GPL");