Devicetree
 help / color / mirror / Atom feed
From: Cody Kang via B4 Relay <devnull+codykang.hk.gmail.com@kernel.org>
To: David Airlie <airlied@gmail.com>, Simona Vetter <simona@ffwll.ch>,
	 Maarten Lankhorst <maarten.lankhorst@linux.intel.com>,
	 Maxime Ripard <mripard@kernel.org>,
	Thomas Zimmermann <tzimmermann@suse.de>,
	 Rob Herring <robh@kernel.org>,
	Krzysztof Kozlowski <krzk+dt@kernel.org>,
	 Conor Dooley <conor+dt@kernel.org>, Yixun Lan <dlan@kernel.org>,
	 Vinod Koul <vkoul@kernel.org>,
	Neil Armstrong <neil.armstrong@linaro.org>,
	 Haylen Chu <heylenay@4d2.org>,
	Michael Turquette <mturquette@baylibre.com>,
	 Stephen Boyd <sboyd@kernel.org>,
	Brian Masney <bmasney@redhat.com>,
	 Philipp Zabel <p.zabel@pengutronix.de>,
	Paul Walmsley <pjw@kernel.org>,
	 Palmer Dabbelt <palmer@dabbelt.com>,
	Albert Ou <aou@eecs.berkeley.edu>,
	 Alexandre Ghiti <alex@ghiti.fr>
Cc: dri-devel@lists.freedesktop.org, linux-riscv@lists.infradead.org,
	 devicetree@vger.kernel.org, spacemit@lists.linux.dev,
	 linux-kernel@vger.kernel.org, linux-phy@lists.infradead.org,
	 linux-clk@vger.kernel.org, Cody Kang <codykang.hk@gmail.com>
Subject: [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend
Date: Sat, 25 Jul 2026 00:51:18 -0400	[thread overview]
Message-ID: <20260725-k3-display-v1-9-6de34d80e86c@gmail.com> (raw)
In-Reply-To: <20260725-k3-display-v1-0-6de34d80e86c@gmail.com>

From: Cody Kang <codykang.hk@gmail.com>

Add the register-level programming of one Saturn instance: clock and
reset bring-up, scene control and timing generator setup, RDMA channel
and composer layer programming, FBC line-buffer budgeting, interrupt
dispatch and the cfg-ready/vsync handshake that paces commits. The
backend is exposed as an ops table and hardware descriptor to be
aggregated by the KMS layer in an upcoming patch.

Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
 drivers/gpu/drm/spacemit/dpu/dpu_saturn.c          | 483 +++++++++++
 drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c      | 896 +++++++++++++++++++++
 drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c       | 152 ++++
 drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.h       |  21 +
 drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h |  52 ++
 5 files changed, 1604 insertions(+)

diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
new file mode 100644
index 000000000000..8d5f13bc5373
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
@@ -0,0 +1,483 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/module.h>
+#include <linux/mfd/syscon.h>
+#include <linux/pm_qos.h>
+#include <linux/regmap.h>
+#include <linux/of.h>
+#include <linux/of_graph.h>
+#include <linux/types.h>
+#include <linux/math64.h>
+#include <drm/drm_print.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_gem.h>
+#include "dpu_saturn.h"
+#include "saturn_fbcmem.h"
+#include "../spacemit_cmdlist.h"
+#include "../spacemit_dmmu.h"
+#include "../spacemit_dpu_reg.h"
+#include "../spacemit_drm.h"
+#include <video/display_timing.h>
+#include "../spacemit_dpu_ids.h"
+#include "saturn_regs/ops_hee.h"
+
+#define CREATE_TRACE_POINTS
+#include "dpu_trace.h"
+
+#define TOTAL_RDMA_MEMSIZE	(68 * 1024)
+static const struct dpu_format_id primary_fmts[] = {
+	{ DRM_FORMAT_XRGB8888, 8, 32 }, /* RDMA_FMT_XRGB_8888 */
+	{ DRM_FORMAT_ARGB8888, 4, 32 }, /* RDMA_FMT_ARGB_8888 */
+	{ DRM_FORMAT_XBGR8888, 9, 32 }, /* RDMA_FMT_XBGR_8888 */
+	{ DRM_FORMAT_ABGR8888, 5, 32 }, /* RDMA_FMT_ABGR_8888 */
+	{ DRM_FORMAT_RGBX8888, 10, 32 }, /* RDMA_FMT_RGBX_8888 */
+	{ DRM_FORMAT_RGBA8888, 6, 32 }, /* RDMA_FMT_RGBA_8888 */
+	{ DRM_FORMAT_BGRX8888, 11, 32 }, /* RDMA_FMT_BGRX_8888 */
+	{ DRM_FORMAT_BGRA8888, 7, 32 }, /* RDMA_FMT_BGRA_8888 */
+	{ DRM_FORMAT_ABGR2101010, 1, 32 }, /* RDMA_FMT_ABGR_2101010 */
+	{ DRM_FORMAT_RGB888, 12, 24 }, /* RDMA_FMT_RGB_888 */
+	{ DRM_FORMAT_BGR888, 13, 24 }, /* RDMA_FMT_BGR_888 */
+	{ DRM_FORMAT_RGB565, 22, 16 }, /* RDMA_FMT_RGB_565 */
+	{ DRM_FORMAT_BGR565, 23, 16 }, /* RDMA_FMT_BGR_565 */
+};
+
+static const struct spacemit_hw_rdma saturn_hee_rdmas[] = {
+	{FORMAT_RGB},
+	{FORMAT_RGB},
+	{FORMAT_RGB},
+	{FORMAT_RGB},
+};
+
+static const u32 saturn_hee_fbcmem_sizes[] = {
+	89600, /* 87.5k */
+	15360, /* 16k */
+};
+
+const struct spacemit_hw_device k3_saturn_dpu = {
+	.plane_nums = 1,
+	.crtc_nums = 2,
+	.rdma_nums = ARRAY_SIZE(saturn_hee_rdmas),
+	.rdmas = saturn_hee_rdmas,
+	.n_formats = ARRAY_SIZE(primary_fmts),
+	.formats = primary_fmts,
+	.n_fbcmems = ARRAY_SIZE(saturn_hee_fbcmem_sizes),
+	.fbcmem_sizes = saturn_hee_fbcmem_sizes,
+	.enable_vsync = saturn_hee_enable_vsync,
+	.enable_cfg_irq = saturn_hee_enable_cfg_irq,
+	.cfg_ready = saturn_hee_cfg_ready,
+	.sw_start = saturn_hee_sw_start,
+	.irq_enable = saturn_hee_irq_enable,
+	.dpu_init = saturn_hee_dpu_init,
+	.plane_update_hw_channel = saturn_hee_plane_update_hw_channel,
+	.plane_disable_hw_channel = saturn_hee_plane_disable_hw_channel,
+	.conf_dpuctrl = saturn_hee_conf_dpuctrl,
+	.get_cfg_rdy = saturn_hee_get_cfg_rdy,
+	.get_int_sts = saturn_hee_get_int_sts,
+	.get_irq_bit = saturn_hee_get_irq_bit,
+	.clr_int_sts = saturn_hee_clr_int_sts,
+	.enable_cmdlist = saturn_hee_enable_cmdlist,
+	.cfg_cmdlist = saturn_hee_cfg_cmdlist,
+	.rdma_dmmu = saturn_hee_rdma_dmmu,
+	.get_cl_rdma_buf = saturn_hee_get_cl_rdma_buf,
+	.cmdlist_fill_data_row = saturn_hee_cmdlist_fill_data_row,
+	.cmdlist_fill_conf_row = saturn_hee_cmdlist_fill_conf_row,
+	.crtc_cmdlist = saturn_hee_crtc_cmdlist,
+	.cmdlist_dump_node = saturn_hee_cmdlist_dump_node,
+};
+
+void saturn_enable_irq_mask(struct spacemit_crtc *a_crtc, bool enable,
+			    u32 offset, u32 mask)
+{
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	if (!hwdev->base) {
+		drm_err(a_crtc->crtc.dev, "hwdev->base is NULL\n");
+		return;
+	}
+
+	if (!a_crtc->clocks_on)
+		return;
+
+	regmap_update_bits(hwdev->regmap, offset, mask, enable ? mask : 0);
+}
+
+int dpu_parse_clocks(struct device *dev, struct dpu_clk_context *clk_ctx)
+{
+	clk_ctx->pxclk = devm_clk_get(dev, "pxclk");
+	if (IS_ERR(clk_ctx->pxclk))
+		return dev_err_probe(dev, PTR_ERR(clk_ctx->pxclk),
+				     "failed to get pxclk\n");
+
+	clk_ctx->mclk = devm_clk_get(dev, "mclk");
+	if (IS_ERR(clk_ctx->mclk))
+		return dev_err_probe(dev, PTR_ERR(clk_ctx->mclk),
+				     "failed to get mclk\n");
+
+	clk_ctx->escclk = devm_clk_get(dev, "escclk");
+	if (IS_ERR(clk_ctx->escclk))
+		return dev_err_probe(dev, PTR_ERR(clk_ctx->escclk),
+				     "failed to get escclk\n");
+
+	clk_ctx->aclk = devm_clk_get(dev, "aclk");
+	if (IS_ERR(clk_ctx->aclk))
+		return dev_err_probe(dev, PTR_ERR(clk_ctx->aclk),
+				     "failed to get aclk\n");
+
+	clk_ctx->dscclk = devm_clk_get(dev, "dscclk");
+	if (IS_ERR(clk_ctx->dscclk))
+		return dev_err_probe(dev, PTR_ERR(clk_ctx->dscclk),
+				     "failed to get dscclk\n");
+
+	return 0;
+}
+
+static int dpu_enable_clocks(struct spacemit_crtc *a_crtc)
+{
+	struct dpu_clk_context *clk_ctx = &a_crtc->clk_ctx;
+	struct drm_crtc *crtc = &a_crtc->crtc;
+	struct drm_display_mode *mode = &crtc->mode;
+	u64 clk_val;
+	u64 set_clk_val;
+	int ret;
+
+	ret = clk_prepare_enable(clk_ctx->pxclk);
+	if (ret)
+		goto err;
+	set_clk_val = mode->clock * 1000;
+	if (set_clk_val) {
+		set_clk_val = clk_round_rate(clk_ctx->pxclk, set_clk_val);
+		clk_val = clk_get_rate(clk_ctx->pxclk);
+		if (clk_val != set_clk_val) {
+			clk_set_rate(clk_ctx->pxclk, set_clk_val);
+			drm_dbg(crtc->dev, "pxclk=%lld\n", clk_val);
+		}
+	}
+
+	ret = clk_prepare_enable(clk_ctx->mclk);
+	if (ret)
+		goto err_pxclk;
+	clk_val = clk_get_rate(clk_ctx->mclk);
+	if (clk_val != DPU_MCLK_DEFAULT) {
+		clk_val = clk_round_rate(clk_ctx->mclk, DPU_MCLK_DEFAULT);
+		clk_set_rate(clk_ctx->mclk, clk_val);
+		drm_dbg(crtc->dev, "mclk=%lld\n", clk_val);
+	}
+
+	/* The escape clock only has to run for the "esc" reset to propagate. */
+	ret = clk_prepare_enable(clk_ctx->escclk);
+	if (ret)
+		goto err_mclk;
+
+	ret = clk_prepare_enable(clk_ctx->aclk);
+	if (ret)
+		goto err_escclk;
+	clk_val = clk_get_rate(clk_ctx->aclk);
+	if (clk_val != a_crtc->aclk) {
+		set_clk_val = clk_round_rate(clk_ctx->aclk, a_crtc->aclk);
+		if (set_clk_val != clk_val) {
+			clk_set_rate(clk_ctx->aclk, set_clk_val);
+			drm_dbg(crtc->dev, "aclk %llu -> %llu\n", clk_val,
+				set_clk_val);
+		}
+	}
+
+	ret = clk_prepare_enable(clk_ctx->dscclk);
+	if (ret)
+		goto err_aclk;
+
+	trace_dpu_enable_clocks(a_crtc->dev_id);
+	return 0;
+
+err_aclk:
+	clk_disable_unprepare(clk_ctx->aclk);
+err_escclk:
+	clk_disable_unprepare(clk_ctx->escclk);
+err_mclk:
+	clk_disable_unprepare(clk_ctx->mclk);
+err_pxclk:
+	clk_disable_unprepare(clk_ctx->pxclk);
+err:
+	drm_err(crtc->dev, "failed to enable DPU clocks: %d\n", ret);
+	return ret;
+}
+
+static int dpu_disable_clocks(struct spacemit_crtc *a_crtc)
+{
+	struct dpu_clk_context *clk_ctx = &a_crtc->clk_ctx;
+
+	trace_dpu_disable_clocks(a_crtc->dev_id);
+
+	clk_disable_unprepare(clk_ctx->pxclk);
+	clk_disable_unprepare(clk_ctx->mclk);
+	clk_disable_unprepare(clk_ctx->escclk);
+
+	clk_disable_unprepare(clk_ctx->aclk);
+	clk_disable_unprepare(clk_ctx->dscclk);
+
+	return 0;
+}
+
+u8 spacemit_plane_hw_get_format_id(u32 format)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(primary_fmts); i++) {
+		if (primary_fmts[i].format == format)
+			return primary_fmts[i].id;
+	}
+
+	return SPACEMIT_DPU_INVALID_FORMAT_ID;
+}
+
+static void saturn_enable_vsync(struct spacemit_crtc *a_crtc, bool enable)
+{
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	trace_saturn_enable_vsync("vsync", enable);
+	hwdev->enable_vsync(a_crtc, hwdev, enable);
+}
+
+void spacemit_cfg_rdy_timer_handler(struct timer_list *t)
+{
+	struct spacemit_crtc *a_crtc = timer_container_of(a_crtc, t,
+							  cfg_rdy_timer);
+	struct drm_crtc *crtc = &a_crtc->crtc;
+	struct drm_device *drm = crtc->dev;
+	struct drm_pending_vblank_event *event;
+	unsigned long flags;
+
+	drm_err_ratelimited(drm, "CFG_RDY handshake timeout\n");
+	trace_saturn_ctrl_cfg_ready_timer(a_crtc->dev_id);
+
+	a_crtc->flip_done = true;
+
+	spin_lock_irqsave(&drm->event_lock, flags);
+	event = crtc->state->event;
+	if (event) {
+		crtc->state->event = NULL;
+		drm_crtc_send_vblank_event(crtc, event);
+	}
+	spin_unlock_irqrestore(&drm->event_lock, flags);
+
+	drm_crtc_vblank_put(crtc);
+}
+
+static void saturn_ctrl_cfg_ready(struct spacemit_crtc *a_crtc, bool enable)
+{
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	trace_saturn_ctrl_cfg_ready(a_crtc->dev_id, enable);
+	mod_timer(&a_crtc->cfg_rdy_timer, jiffies + msecs_to_jiffies(3000));
+	hwdev->cfg_ready(a_crtc, hwdev);
+}
+
+static void saturn_ctrl_sw_start(struct spacemit_crtc *a_crtc, bool enable)
+{
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	trace_saturn_ctrl_sw_start(a_crtc->dev_id, enable);
+
+	hwdev->sw_start(a_crtc, hwdev);
+}
+
+static int dpu_init(struct spacemit_crtc *a_crtc)
+{
+	unsigned int timeout = 1000;
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	if (!a_crtc->clocks_on)
+		return 0;
+
+	while (timeout) {
+		if (hwdev->get_cfg_rdy(a_crtc, hwdev) == 0)
+			break;
+		usleep_range(100, 200);
+		timeout--;
+	}
+	if (timeout == 0)
+		drm_err(a_crtc->crtc.dev, "wait for cfg ready timed out\n");
+
+	hwdev->dpu_init(a_crtc);
+
+	return 0;
+}
+
+static void dpu_uninit(struct spacemit_crtc *a_crtc)
+{
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	if (!a_crtc->clocks_on)
+		return;
+
+	trace_dpu_uninit(a_crtc->dev_id);
+	hwdev->irq_enable(a_crtc, false);
+}
+
+static inline void dpu_isr_vblank(struct spacemit_crtc *a_crtc, bool *flip)
+{
+	struct drm_crtc *crtc = &a_crtc->crtc;
+	struct drm_device *drm = crtc->dev;
+	struct drm_pending_vblank_event *event;
+
+	drm_crtc_handle_vblank(crtc);
+
+	if (*flip)
+		return;
+	*flip = true;
+
+	/*
+	 * event_lock serialises this against the commit arming
+	 * crtc->state->event.
+	 */
+	spin_lock(&drm->event_lock);
+	event = crtc->state->event;
+	if (event) {
+		/*
+		 * Consume it exactly once, even against a concurrent
+		 * commit_hw_done().
+		 */
+		crtc->state->event = NULL;
+		drm_crtc_send_vblank_event(crtc, event);
+	}
+	spin_unlock(&drm->event_lock);
+
+	drm_crtc_vblank_put(crtc);
+}
+
+u32 saturn_conf_dpuctrl_rdma(struct spacemit_crtc *a_crtc)
+{
+	struct drm_crtc *crtc = &a_crtc->crtc;
+	struct drm_plane *plane;
+	u32 rdma_en = 0;
+
+	drm_atomic_crtc_for_each_plane(plane, crtc) {
+		u32 rdma_id = to_spacemit_plane_state(plane->state)->rdma_id;
+
+		if (rdma_id != RDMA_INVALID_ID)
+			rdma_en |= (1 << rdma_id);
+	}
+
+	trace_dpuctrl("rdma_en", rdma_en);
+
+	return rdma_en;
+}
+
+static u32 dpu_online_isr(struct spacemit_crtc *a_crtc)
+{
+	u32 irq_raw, irq_bit, irq_ur_bit;
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+	int dev_id = a_crtc->dev_id;
+
+	trace_dpu_isr(a_crtc->dev_id);
+
+	irq_raw = hwdev->get_int_sts(hwdev, dev_id);
+	trace_dpu_isr_status("ONLINE", irq_raw);
+	irq_bit = hwdev->get_irq_bit(INT_UNDERRUN, dev_id);
+	irq_ur_bit = irq_raw & irq_bit;
+	if (irq_ur_bit && !a_crtc->ur_reported) {
+		hwdev->clr_int_sts(a_crtc, irq_ur_bit, dev_id);
+		trace_dpu_isr_status("underrun", irq_ur_bit);
+		drm_err_ratelimited(a_crtc->crtc.dev, "underrun\n");
+		a_crtc->ur_reported = true;
+	}
+	irq_bit = hwdev->get_irq_bit(INT_CFG_RDY, dev_id);
+	if (irq_raw & irq_bit) {
+		hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
+		trace_dpu_isr_status("cfg_rdy_clr", irq_raw & irq_bit);
+		/*
+		 * A fired watchdog already sent the event and put this flip's
+		 * vblank reference; re-arming the vsync path would put twice.
+		 */
+		if (timer_delete(&a_crtc->cfg_rdy_timer))
+			a_crtc->flip_done = false;
+		a_crtc->ur_reported = false;
+		if (hwdev->enable_cfg_irq)
+			hwdev->enable_cfg_irq(a_crtc, hwdev, false);
+	}
+	irq_bit = hwdev->get_irq_bit(INT_VSYNC, dev_id);
+	if (irq_raw & irq_bit) {
+		hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
+		trace_dpu_isr_status("vsync", irq_raw & irq_bit);
+		dpu_isr_vblank(a_crtc, &a_crtc->flip_done);
+	}
+	irq_bit = hwdev->get_irq_bit(INT_REST, dev_id) | irq_ur_bit;
+	if (irq_raw & irq_bit)
+		hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
+
+	irq_bit = hwdev->get_irq_bit(INT_VSYNC_UPDATE, dev_id);
+	if (irq_raw & irq_bit)
+		hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
+
+	return 0;
+}
+
+static void dpu_run(struct drm_crtc *crtc,
+		    struct drm_crtc_state *old_state)
+{
+	struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	trace_dpu_run(a_crtc->dev_id);
+
+	if (hwdev->conf_dpuctrl)
+		hwdev->conf_dpuctrl(crtc, old_state);
+
+	/* flush all config register writes before CFG_RDY is raised */
+	mb();
+
+	drm_crtc_vblank_get(crtc);
+	if (hwdev->enable_cfg_irq)
+		hwdev->enable_cfg_irq(a_crtc, hwdev, true);
+
+	saturn_ctrl_cfg_ready(a_crtc, true);
+
+	if (unlikely(a_crtc->is_1st_f)) {
+		drm_dbg(a_crtc->crtc.dev, "DPU start\n");
+		a_crtc->is_1st_f = false;
+		saturn_ctrl_sw_start(a_crtc, true);
+	}
+	dma_rmb();
+}
+
+static void dpu_enable_vsync(struct spacemit_crtc *a_crtc)
+{
+	saturn_enable_vsync(a_crtc, true);
+}
+
+static void dpu_disable_vsync(struct spacemit_crtc *a_crtc)
+{
+	saturn_enable_vsync(a_crtc, false);
+}
+
+const struct dpu_core_ops dpu_saturn_ops = {
+	.init = dpu_init,
+	.uninit = dpu_uninit,
+	.run = dpu_run,
+	.online_isr = dpu_online_isr,
+	.enable_clk = dpu_enable_clocks,
+	.disable_clk = dpu_disable_clocks,
+	.enable_vsync = dpu_enable_vsync,
+	.disable_vsync = dpu_disable_vsync,
+	.cal_layer_fbcmem_size = saturn_cal_layer_fbcmem_size,
+	.adjust_rdma_fbcmem = saturn_adjust_rdma_fbcmem,
+};
diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
new file mode 100644
index 000000000000..c23e4ca36d26
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
@@ -0,0 +1,896 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/module.h>
+#include <linux/mfd/syscon.h>
+#include <linux/pm_qos.h>
+#include <linux/regmap.h>
+#include <drm/drm_print.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_blend.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem.h>
+#include "dpu_saturn.h"
+#include "saturn_fbcmem.h"
+#include "../spacemit_cmdlist.h"
+#include "../spacemit_dmmu.h"
+#include "../spacemit_dpu_reg.h"
+#include <video/display_timing.h>
+#include "../spacemit_dpu_ids.h"
+#include "saturn_regs/ops_hee.h"
+#include "saturn_regs/reg_map_hee.h"
+
+#include "dpu_trace.h"
+
+#define SATURN_WRITE_TBU_REGS(hwdev, base, reg_id, cl_tbu) \
+do {\
+	dpu_write(hwdev, MMU_TBU_REG, base, tbu_base_addr##reg_id##_low, \
+		  tbu->ttb_pa[reg_id] & 0xFFFFFFFF, cl_tbu); \
+	dpu_write(hwdev, MMU_TBU_REG, base, tbu_base_addr##reg_id##_high, \
+		  (tbu->ttb_pa[reg_id] >> 32) & 0xFFF, cl_tbu); \
+	dpu_write(hwdev, MMU_TBU_REG, base, tbu_va##reg_id, \
+		 (tbu->tbu_va[reg_id] >> 12), cl_tbu); \
+	dpu_write(hwdev, MMU_TBU_REG, base, \
+		  tbu_size##reg_id, tbu->ttb_size[reg_id], cl_tbu); \
+	dpu_write(hwdev, MMU_TBU_REG, base, \
+		  vsync_update_en, 1, cl_tbu); \
+} while (0)
+
+#define SATURN_WRITE_RDMA_ADDR_REG(hwdev, reg_id, rdma_id, addr, cl_rdma) \
+do { \
+	if (hwdev) { \
+		dpu_write(hwdev, RDMA_PATH_X_REG, RDMA_BASE_ADDR[rdma_id], \
+			  base_addr##reg_id##_low_ly0, (addr) & 0xFFFFFFFF, \
+			  cl_rdma); \
+		dpu_write(hwdev, RDMA_PATH_X_REG, RDMA_BASE_ADDR[rdma_id], \
+			  base_addr##reg_id##_high_ly0, ((addr) >> 32) & 0xFFF, \
+			  cl_rdma); \
+	} \
+} while (0)
+
+static const u32 RDMA_BASE_ADDR[] = {
+	RDMA0_BASE_ADDR,
+	RDMA1_BASE_ADDR,
+	RDMA2_BASE_ADDR,
+	RDMA3_BASE_ADDR,
+};
+
+static const u32 CMP_BASE_ADDR[] = {
+	CMP0_BASE_ADDR,
+	CMP1_BASE_ADDR,
+	CMP2_BASE_ADDR,
+};
+
+static const u32 TMG_BASE_ADDR[] = {
+	0,
+	TMG0_BASE_ADDR,
+	0,
+};
+
+static const u32 MMU_TBU_BASE_ADDR_ARRAY[] = {
+	TBU0_ADDR, TBU1_ADDR, TBU2_ADDR, TBU3_ADDR,
+	TBU4_ADDR, TBU5_ADDR, TBU6_ADDR, TBU7_ADDR,
+};
+
+void saturn_hee_enable_vsync(struct spacemit_crtc *a_crtc, struct spacemit_hw_device *hwdev,
+			     bool enable)
+{
+	u32 offset = DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK;
+	u32 mask = saturn_hee_get_irq_bit(INT_VSYNC, a_crtc->dev_id);
+
+	saturn_enable_irq_mask(a_crtc, enable, offset, mask);
+}
+
+void saturn_hee_enable_cfg_irq(struct spacemit_crtc *a_crtc, struct spacemit_hw_device *hwdev,
+			       bool enable)
+{
+	u32 offset = DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK;
+	u32 mask = saturn_hee_get_irq_bit(INT_CFG_RDY, a_crtc->dev_id);
+
+	saturn_enable_irq_mask(a_crtc, enable, offset, mask);
+}
+
+void saturn_hee_cfg_ready(struct spacemit_crtc *a_crtc,
+			  struct spacemit_hw_device *hwdev)
+{
+	u32 base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+
+	saturn_enable_irq_mask(a_crtc, true,
+		DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK,
+		saturn_hee_get_irq_bit(INT_UNDERRUN, a_crtc->dev_id));
+	a_crtc->ur_mask_armed = true;
+	dpu_write(hwdev, DPU_CTL_REG, base, both_cfg_rdy, 1);
+}
+
+void saturn_hee_sw_start(struct spacemit_crtc *a_crtc,
+			 struct spacemit_hw_device *hwdev)
+{
+	u32 base;
+
+	base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+	dpu_write_reg(hwdev, DPU_CTL_REG, base, sw_start, 1);
+}
+
+static void saturn_init_tmg(struct spacemit_crtc *a_crtc)
+{
+	u32 base = 0;
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+	struct drm_crtc *crtc = &a_crtc->crtc;
+	struct drm_display_mode *mode = &crtc->mode;
+	u16 vfp, vbp, vsync, hfp, hbp, hsync;
+	void __iomem *tmg_addr;
+	u32 value;
+
+	/* dev_id names the composer type, not the DPU instance: use TMG0. */
+	tmg_addr = hwdev->base + TMG0_BASE_ADDR;
+
+	hsync = mode->hsync_end - mode->hsync_start;
+	hbp = mode->htotal - mode->hsync_end;
+	hfp = mode->hsync_start - mode->hdisplay;
+	vsync = mode->vsync_end - mode->vsync_start;
+	vbp = mode->vtotal - mode->vsync_end;
+	vfp = mode->vsync_start - mode->vdisplay;
+	trace_drm_display_mode_info(mode);
+
+	base = TMG_BASE_ADDR[a_crtc->dev_id];
+	if (base) {
+		dpu_write(hwdev, TMG_REG, base, disp_ready_man_en, 0);
+		dpu_write(hwdev, TMG_REG, base, background_r, 0);
+		dpu_write(hwdev, TMG_REG, base, background_g, 0x0);
+		dpu_write(hwdev, TMG_REG, base, background_b, 0xff);
+		dpu_write(hwdev, TMG_REG, base, eof_1st_ln_dly_num, vfp - 7);
+		dpu_write(hwdev, TMG_REG, base, eof_2nd_ln_dly_num, vfp - 6);
+		dpu_write(hwdev, TMG_REG, base, split_en, 0);
+		dpu_write(hwdev, TMG_REG, base, cmd_screen, 0);
+		dpu_write(hwdev, TMG_REG, base, cmd_wait_en, 0);
+		dpu_write(hwdev, TMG_REG, base, cmd_wait_te, 0);
+		dpu_write(hwdev, TMG_REG, base, sof_pre_ln_num, 0);
+		dpu_write(hwdev, TMG_REG, base, hfp, hfp);
+		dpu_write(hwdev, TMG_REG, base, hbp, hbp);
+		dpu_write(hwdev, TMG_REG, base, vfp, vfp);
+		dpu_write(hwdev, TMG_REG, base, vbp, vbp);
+		dpu_write(hwdev, TMG_REG, base, hsync_width, hsync);
+		dpu_write(hwdev, TMG_REG, base, vsync_width, vsync);
+		dpu_write(hwdev, TMG_REG, base, hsp, 1);
+		dpu_write(hwdev, TMG_REG, base, vsp, 1);
+		dpu_write(hwdev, TMG_REG, base, h_active, mode->hdisplay);
+		dpu_write(hwdev, TMG_REG, base, v_active, mode->vdisplay);
+		dpu_write(hwdev, TMG_REG, base, fm_timing_en, 1);
+		dpu_write(hwdev, TMG_REG, base, user, a_crtc->out_format);
+	}
+
+	value = readl(tmg_addr + 0x14);
+	value &= 0xFFFF0000;
+	value |= vfp;
+	writel(value, tmg_addr + 0x14);
+
+	/* align vsync with hsync */
+	value = readl(tmg_addr + 0x3c);
+	value |= 0x01;
+	writel(value, tmg_addr + 0x3c);
+}
+
+static void saturn_init_regs(struct spacemit_crtc *a_crtc)
+{
+	u32 base = 0;
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	base = CMP_BASE_ADDR[a_crtc->dev_id];
+	dpu_write(hwdev, CMPS_X_REG, base, bg_color_b, 0x0);
+	dpu_write(hwdev, CMPS_X_REG, base, bg_color_r, 0x0);
+	dpu_write(hwdev, CMPS_X_REG, base, bg_color_g, 0x0);
+	dpu_write(hwdev, CMPS_X_REG, base, bg_color_a, 0xFF);
+	dpu_write(hwdev, CMPS_X_REG, base, module_enable, 1);
+	dpu_write(hwdev, CMPS_X_REG, base, layer00_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer01_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer02_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer03_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer04_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer05_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer06_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer07_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer08_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer09_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer10_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer11_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer12_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer13_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer14_en, 0);
+	dpu_write(hwdev, CMPS_X_REG, base, layer15_en, 0);
+
+	base = DPU_TOP_BASE_ADDR;
+
+	base = EE_ADDR;
+	dpu_write(hwdev, EE_REG, base, m_benable, 0);
+
+	saturn_init_tmg(a_crtc);
+}
+
+static void saturn_setup_dma_top(struct spacemit_crtc *a_crtc)
+{
+	u32 base = DMA_TOP_BASE_ADDR;
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	dpu_write(hwdev, DMA_TOP_REG, base, image_rr_ratio, 0x10);
+	dpu_write(hwdev, DMA_TOP_REG, base, pixel_num_th, 4);
+
+	dpu_write(hwdev, DMA_TOP_REG, base, rdma_timeout_limit, 0xFFFE);
+	dpu_write(hwdev, DMA_TOP_REG, base, wdma_timeout_limit, 0xFFFF);
+}
+
+static void saturn_setup_mmu_top(struct spacemit_crtc *a_crtc)
+{
+	unsigned int rd_outs_num = 0;
+	u32 base = MMU_TOP_BASE_ADDR;
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	rd_outs_num = RD_OUTS_NUM / 2;
+
+	dpu_write(hwdev, MMU_TOP_REG, base, rdma_timelimit, RDMA_TIMELIMIT);
+
+	dpu_write(hwdev, MMU_TOP_REG, base, sram0_tlb_axi_port_sel, 0);
+	dpu_write(hwdev, MMU_TOP_REG, base, sram1_tlb_axi_port_sel, 0);
+	dpu_write(hwdev, MMU_TOP_REG, base, dmac0_rd_outs_num, rd_outs_num);
+	dpu_write(hwdev, MMU_TOP_REG, base, dmac1_rd_outs_num, rd_outs_num);
+}
+
+void saturn_hee_irq_enable(struct spacemit_crtc *a_crtc, bool enable)
+{
+	u32 offset = DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK;
+	u32 mask;
+
+	trace_saturn_irq_enable("irq online", enable);
+	mask = saturn_hee_get_irq_bit(INT_UNDERRUN, a_crtc->dev_id) |
+	       saturn_hee_get_irq_bit(INT_CFG_RDY, a_crtc->dev_id);
+	if (enable) {
+		struct spacemit_drm_private *priv =
+				a_crtc->crtc.dev->dev_private;
+
+		/*
+		 * Ack first: a surviving status bit would fire as the mask
+		 * opens.
+		 */
+		regmap_write(priv->hwdev->regmap,
+			     DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_STS, mask);
+	}
+	saturn_enable_irq_mask(a_crtc, enable, offset, mask);
+	a_crtc->ur_mask_armed = true;
+}
+
+void saturn_hee_dpu_init(struct spacemit_crtc *a_crtc)
+{
+	saturn_init_regs(a_crtc);
+	saturn_setup_dma_top(a_crtc);
+	saturn_setup_mmu_top(a_crtc);
+	saturn_hee_irq_enable(a_crtc, true);
+}
+
+#define SATURN_WRITE_COMPOSER_LAYER(id_name) \
+do {\
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _colorkey_en,	\
+		  0, cl_cmp);					\
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _blend_mode,	\
+		  blend_mode, cl_cmp);				\
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _blend_sel,	\
+		  alpha_sel, cl_cmp);				\
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _layer_alpha,	\
+		  alpha, cl_cmp);					\
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _alpha_ratio,	\
+		  0, cl_cmp);					\
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _area_left,	\
+		  crtc_x, cl_cmp);					\
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _area_top,	\
+		  crtc_y, cl_cmp);					\
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _area_right,	\
+		  crtc_x + crtc_w - 1, cl_cmp);			\
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _area_bottom,	\
+		  crtc_y + crtc_h - 1, cl_cmp);			\
+	if (unlikely(solid_en)) { \
+		dpu_write(hwdev, CMPS_X_REG, base,				\
+			  layer ## id_name ## _solid_color_r,			\
+			  solid_r, cl_cmp);			\
+		dpu_write(hwdev, CMPS_X_REG, base,				\
+			  layer ## id_name ## _solid_color_g,			\
+			  solid_g, cl_cmp);			\
+		dpu_write(hwdev, CMPS_X_REG, base,				\
+			  layer ## id_name ## _solid_color_b,			\
+			  solid_b, cl_cmp);			\
+		dpu_write(hwdev, CMPS_X_REG, base,				\
+			  layer ## id_name ## _solid_color_a,			\
+			  solid_a, cl_cmp);			\
+		dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _solid_en,	\
+			  1, cl_cmp);				\
+	} else { \
+		dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _solid_en,	\
+			  0, cl_cmp);				\
+		dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _dma_id,	\
+			  rdma_id, cl_cmp);			\
+	} \
+	dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _en, 1, cl_cmp); \
+} while (0)
+
+static void saturn_write_fbcmem_regs(struct spacemit_hw_device *hwdev,
+				     struct drm_plane_state *state, u32 rdma_id,
+				     u32 base, struct cmdlist_regs *cl_rdma)
+{
+	struct drm_crtc_state *crtc_state = state->crtc->state;
+	const struct spacemit_crtc_rdma *rdmas =
+			to_spacemit_crtc_state(crtc_state)->rdmas;
+	u32 size  = rdmas[rdma_id].fbcmem.size;
+	u32 start = rdmas[rdma_id].fbcmem.start;
+	bool map   = rdmas[rdma_id].fbcmem.map;
+
+	dpu_write_word(hwdev, base, 31, map << 28 | start << 16 | size,
+		       cl_rdma);
+}
+
+
+void saturn_hee_plane_update_hw_channel(struct drm_plane *plane)
+{
+	struct drm_plane_state *state = plane->state;
+	struct spacemit_crtc *a_crtc = to_spacemit_crtc(state->crtc);
+	struct drm_framebuffer *fb = plane->state->fb;
+	struct spacemit_plane_state *spacemit_plane_state =
+			to_spacemit_plane_state(state);
+	struct cmdlist_regs *cl_cmp = NULL;
+	u32 rdma_id = spacemit_plane_state->rdma_id;
+	u8 alpha = state->alpha >> 8;
+	u16 pixel_alpha = state->pixel_blend_mode;
+	u32 src_w, src_h, src_x, src_y;
+	u32 crtc_w, crtc_h, crtc_x, crtc_y;
+	u32 alpha_sel, blend_mode = 0;
+	struct drm_display_mode *mode = &state->crtc->mode;
+
+	u32 base;
+	bool solid_en = false;
+	u32 solid_a, solid_r, solid_g, solid_b;
+	struct spacemit_drm_private *priv = plane->dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+	struct cmdlist_regs *cl_rdma = a_crtc->cl_rdma;
+
+	trace_spacemit_plane_update_hw_channel("rdma_id", rdma_id);
+
+	src_w = state->src_w >> 16;
+	src_h = state->src_h >> 16;
+	src_x = state->src_x >> 16;
+	src_y = state->src_y >> 16;
+
+	crtc_w = state->crtc_w;
+	crtc_h = state->crtc_h;
+	crtc_x = state->crtc_x;
+	crtc_y = state->crtc_y;
+
+	drm_dbg(plane->dev, "crtc_x %u crtc_y %u\n", crtc_x, crtc_y);
+
+	if (rdma_id == RDMA_INVALID_ID)
+		solid_en = true;
+
+	trace_dpu_plane_info(state, fb, rdma_id, alpha, state->rotation);
+
+	/* For solid color both src_w and src_h are 0 */
+	if (!solid_en) {
+		base = RDMA_BASE_ADDR[rdma_id];
+		/* linear scanout only: the FBC decode path is not exposed */
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, layer_mode, 0, cl_rdma);
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, layer_cmpsr_id,
+			  a_crtc->dev_id, cl_rdma);
+		/*
+		 * force online-mode routing whatever the bootloader left behind
+		 */
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, is_offline, 0, cl_rdma);
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, is_two_layers, 0,
+			  cl_rdma);
+
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, img_width_ly0,
+			  fb->width, cl_rdma);
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, img_height_ly0,
+			  fb->height, cl_rdma);
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, bbox_start_x_ly0, src_x,
+			  cl_rdma);
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, bbox_start_y_ly0, src_y,
+			  cl_rdma);
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, bbox_end_x_ly0,
+			  src_x + src_w - 1, cl_rdma);
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, bbox_end_y_ly0,
+			  src_y + src_h - 1, cl_rdma);
+
+		saturn_write_fbcmem_regs(hwdev, state, rdma_id, base, cl_rdma);
+
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, rot_mode_ly0, 0,
+			  cl_rdma);
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, uv_swap, 0, cl_rdma);
+		dpu_write(hwdev, RDMA_PATH_X_REG, base, pixel_format,
+			  spacemit_plane_state->format, cl_rdma);
+	} else {
+		/* Constant-fill layer: zero-size source, transparent black. */
+		solid_r = 0;
+		solid_g = 0;
+		solid_b = 0;
+		solid_a = 0;
+	}
+
+	switch (pixel_alpha) {
+	case DRM_MODE_BLEND_COVERAGE:
+		blend_mode = 0x0;
+		break;
+	case DRM_MODE_BLEND_PREMULTI:
+		blend_mode = 0x1;
+		break;
+	case DRM_MODE_BLEND_PIXEL_NONE:
+		blend_mode = 0x0;
+		break;
+	default:
+		drm_err(plane->dev, "unsupported blend mode for pixel alpha\n");
+	}
+
+	if (state->fb->format && state->fb->format->has_alpha &&
+	    pixel_alpha != DRM_MODE_BLEND_PIXEL_NONE) {
+		if (alpha != 0xff)
+			alpha_sel = 0x2;
+		else
+			alpha_sel = 0x1;
+	} else {
+		blend_mode = 0x0;
+		alpha_sel = 0x0;
+	}
+
+	cl_cmp = alloc_cmdlist_regs(CMPS_X_REG);
+
+	base = CMP_BASE_ADDR[a_crtc->dev_id];
+	dpu_write(hwdev, CMPS_X_REG, base, dst_w, mode->hdisplay, cl_cmp);
+	dpu_write(hwdev, CMPS_X_REG, base, dst_h, mode->vdisplay, cl_cmp);
+
+	SATURN_WRITE_COMPOSER_LAYER(00);
+	cmdlist_regs_packing(crtc_to_cl(plane->state->crtc), CMDLIST_MOD_COMP,
+			     cl_cmp);
+	free_cmdlist_regs(cl_cmp);
+}
+
+void saturn_hee_plane_disable_hw_channel(struct drm_plane *plane,
+					 struct drm_plane_state *old_state)
+{
+	u8 dev_id = to_spacemit_crtc(old_state->crtc)->dev_id;
+	u32 base = CMP_BASE_ADDR[dev_id];
+	u32 rdma_id = to_spacemit_plane_state(old_state)->rdma_id;
+	struct spacemit_drm_private *priv = plane->dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+	struct cmdlist_regs *cl_cmp = NULL;
+
+	trace_spacemit_plane_disable_hw_channel(0, rdma_id);
+	cl_cmp = alloc_cmdlist_regs(CMPS_X_REG);
+	dpu_write(hwdev, CMPS_X_REG, base, layer00_en, 0, cl_cmp);
+
+	cmdlist_regs_packing(crtc_to_cl(old_state->crtc), CMDLIST_MOD_COMP,
+			     cl_cmp);
+	free_cmdlist_regs(cl_cmp);
+}
+
+void saturn_hee_conf_dpuctrl(struct drm_crtc *crtc,
+			     struct drm_crtc_state *old_state)
+{
+	u32 rdma_en = 0;
+	u32 base;
+	u32 pp_base = POST_PIPE_ADDR;
+	struct spacemit_drm_private *priv = crtc->dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+	struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+	struct drm_display_mode *mode = &crtc->mode;
+	struct drm_crtc_state *crtc_state = a_crtc->crtc.state;
+	struct spacemit_crtc_state *spacemit_crtc_state =
+			to_spacemit_crtc_state(crtc_state);
+	struct cmdlist *cl = NULL;
+	struct cmdlist_regs *pp_cl = NULL;
+
+	base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+	rdma_en = saturn_conf_dpuctrl_rdma(a_crtc);
+
+	pp_cl = alloc_cmdlist_regs(POSTPIPE_REG);
+	/*
+	 * postpipe should be configured no matter whether pq function is on or
+	 * off
+	 */
+	dpu_write_word(hwdev, pp_base, 0, 0, pp_cl);
+	dpu_write(hwdev, POSTPIPE_REG, pp_base, m_inwidth, mode->hdisplay,
+		  pp_cl);
+	dpu_write(hwdev, POSTPIPE_REG, pp_base, m_inheight, mode->vdisplay,
+		  pp_cl);
+
+	cmdlist_regs_packing(crtc_to_cl(crtc), CMDLIST_MOD_COMP, pp_cl);
+	free_cmdlist_regs(pp_cl);
+
+	cmdlist_sort_by_group(crtc);
+	cmdlist_atomic_commit(crtc, old_state);
+
+	dpu_write(hwdev, DPU_CTL_REG, base, nml_scl_en, 0);
+	dpu_write(hwdev, DPU_CTL_REG, base, nml_rch_en, rdma_en);
+	dpu_write(hwdev, DPU_CTL_REG, base, timing_inter0, 8);
+	dpu_write(hwdev, DPU_CTL_REG, base, timing_inter1, 8);
+	dpu_write(hwdev, DPU_CTL_REG, base, video_mod, 1);
+	dpu_write(hwdev, DPU_CTL_REG, base, dbg_mod, 0);
+
+	dpu_write(hwdev, DPU_CTL_REG, base, nml_outctl_en, 0x1);
+	dpu_write(hwdev, DPU_CTL_REG, base, nml_frm_timing_en, 2);
+	cl = &spacemit_crtc_state->cl;
+	if (cl->va) {
+		crtc_cmdlist_sort_by_group(a_crtc);
+		crtc_cmdlist_atomic_commit(a_crtc);
+	}
+}
+
+u32 saturn_hee_get_cfg_rdy(struct spacemit_crtc *a_crtc,
+			   struct spacemit_hw_device *hwdev)
+{
+	u32 base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+
+	return dpu_read_word(hwdev, base, 2);
+}
+
+u32 saturn_hee_get_int_sts(struct spacemit_hw_device *hwdev, int dev_id)
+{
+	/* Both K3 DPU instances are wired as COMPOSER1 (see master_bind). */
+	if (dev_id != COMPOSER1)
+		return 0;
+
+	return dpu_read_word(hwdev, DPU_INT_BASE_ADDR, 12);
+}
+
+static const struct spacemit_crtc_irq_bitfield {
+	enum spacemit_dpu_irq irq_id;
+	u32 irq_bit;
+} irq_list[] = {
+	[INT_UNDERRUN] = {INT_UNDERRUN, DPU_INT_FRM_TIMING_UNFLOW},
+	[INT_CFG_RDY] = {INT_CFG_RDY, DPU_INT_CFG_RDY_CLR},
+	[INT_VSYNC] = {INT_VSYNC, DPU_INT_FRM_TIMING_VSYNC},
+	[INT_EOF] = {INT_EOF, DPU_INT_FRM_TIMING_EOF},
+	[INT_REST] = {INT_REST, DPU_REST_INT_BITS},
+};
+
+u32 saturn_hee_get_irq_bit(enum spacemit_dpu_irq irq_id, int dev_id)
+{
+	if (irq_id >= ARRAY_SIZE(irq_list))
+		return 0;
+
+	if (dev_id == COMPOSER1 || dev_id == COMPOSER2)
+		return irq_list[irq_id].irq_bit;
+	else
+		return 0;
+}
+
+void saturn_hee_clr_int_sts(struct spacemit_crtc *a_crtc, u32 data, int dev_id)
+{
+	struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+	struct spacemit_hw_device *hwdev = priv->hwdev;
+
+	/* Both K3 DPU instances are wired as COMPOSER1 (see master_bind). */
+	if (dev_id != COMPOSER1)
+		return;
+
+	regmap_write(hwdev->regmap, DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_STS,
+		     data);
+	if ((saturn_hee_get_irq_bit(INT_UNDERRUN, dev_id) & data) &&
+	    a_crtc->ur_mask_armed) {
+		saturn_enable_irq_mask(a_crtc, false,
+			DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK,
+			saturn_hee_get_irq_bit(INT_UNDERRUN, dev_id));
+		a_crtc->ur_mask_armed = false;
+	}
+}
+
+void saturn_hee_enable_cmdlist(struct spacemit_crtc *a_crtc, struct spacemit_hw_device *hwdev,
+			       int id, bool enable)
+{
+	u32 val = 0;
+	u32 base = 0;
+	struct drm_crtc *crtc = &a_crtc->crtc;
+	struct drm_crtc_state *crtc_state = crtc->state;
+	struct spacemit_crtc_state *spacemit_state =
+			to_spacemit_crtc_state(crtc_state);
+	struct spacemit_crtc_rdma *rdmas = spacemit_state->rdmas;
+
+	base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+
+	dpu_write_reg_arr(hwdev, DPU_CTL_TOP_REG, DPU_CTRL_BASE_ADDR,
+			  dpu_ctl_top_reg_0, id, cmdlist_rch_en,
+			  enable ? 1 : 0);
+	val = dpu_read_reg(hwdev, DPU_CTL_REG, base, nml_rch_vrt_reuse);
+	if (enable && rdmas[id].use_cnt > 1)
+		val |= (1 << id);
+	else
+		val &= ~(1 << id);
+
+	dpu_write(hwdev, DPU_CTL_REG, base, nml_rch_vrt_reuse, val);
+}
+
+void saturn_hee_cfg_cmdlist(struct spacemit_hw_device *hwdev, int id, u32 chy,
+			    u32 addrl, u32 addrh)
+{
+	dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR,
+			  cmdlist_reg_48, id, cmdlist_ch_y_first, chy);
+	dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR,
+			  cmdlist_reg_0, id, cmdlist_ch_start_addrl, addrl);
+	dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR,
+			  cmdlist_reg_16, id, cmdlist_ch_start_addrh, addrh);
+}
+
+void saturn_hee_rdma_dmmu(struct spacemit_hw_device *hwdev, u8 tbu_id, struct tbu_instance *tbu,
+			  struct drm_framebuffer *fb, u32 val, struct cmdlist_regs *cl_rdma,
+			  struct drm_plane *plane)
+{
+	u32 base;
+	u8 rdma_id = tbu_id / 2;
+	struct cmdlist_regs *cl_tbu = NULL;
+
+	SATURN_WRITE_RDMA_ADDR_REG(hwdev, 0, rdma_id, tbu->tbu_va[0], cl_rdma);
+	SATURN_WRITE_RDMA_ADDR_REG(hwdev, 1, rdma_id, tbu->tbu_va[1], cl_rdma);
+	SATURN_WRITE_RDMA_ADDR_REG(hwdev, 2, rdma_id, tbu->tbu_va[2], cl_rdma);
+
+	base = RDMA_BASE_ADDR[rdma_id];
+	dpu_write(hwdev, RDMA_PATH_X_REG, base, rdma_stride0_layer0,
+		  fb->pitches[0], cl_rdma);
+	dpu_write(hwdev, RDMA_PATH_X_REG, base, rdma_stride1_layer0,
+		  fb->pitches[1], cl_rdma);
+
+	cl_tbu = alloc_cmdlist_regs(MMU_TBU_REG);
+	base = MMU_TBU_BASE_ADDR_ARRAY[tbu_id];
+	SATURN_WRITE_TBU_REGS(hwdev, base, 0, cl_tbu);
+	SATURN_WRITE_TBU_REGS(hwdev, base, 1, cl_tbu);
+	SATURN_WRITE_TBU_REGS(hwdev, base, 2, cl_tbu);
+
+	dpu_write_word(hwdev, base, 0, val, cl_tbu);
+	cmdlist_regs_packing(plane_to_cl(plane), CMDLIST_MOD_DMMU, cl_tbu);
+	free_cmdlist_regs(cl_tbu);
+}
+
+static u32 saturn_get_cl_chy_addr(void)
+{
+	return CMDLIST_BASE_ADDR + CMDLIST_CH_Y;
+}
+
+static u32 saturn_get_cl_start_cmps_y_addr(void)
+{
+	return DPU_CTRL_BASE_ADDR + CMDLIST_CH_START_CMPS_Y;
+}
+
+static u32 saturn_get_cl_cfg_rdy_addr(void)
+{
+	return DPU_CTRL_BASE_ADDR + CMDLIST_CFG_READY;
+}
+
+int saturn_hee_get_cl_rdma_buf(struct spacemit_crtc *a_crtc)
+{
+	a_crtc->cl_rdma = alloc_cmdlist_regs(RDMA_PATH_X_REG);
+	if (!a_crtc->cl_rdma)
+		return -ENOMEM;
+	else
+		return 0;
+}
+
+struct cmdlist_header {
+	u64 next_list_addr : 39;
+	/* reserved */
+	u32: 1;
+	u32 nod_len : 16;
+	/* 1: the last cmdlist node, 2: pending node, otherwise 0 */
+	u32 nod_type : 2;
+	u32 next_nod_secu : 1;
+	u64 wait_event_low    : 5;
+	u64 wait_event    : 59;
+	/* reserved */
+	u32: 5;
+};
+
+struct cmdlist_row {
+	u32 module_cfg_addr   : 19;
+	u32 module_cfg_strobe : 12;
+	/* the last row tag = 1, else tag = 0. */
+	u32 row_eof_tag       : 1;
+	u32 module_regs[3];
+};
+
+/* DMA stream layout consumed by the cmdlist engine; sizes are load-bearing. */
+static_assert(sizeof(struct cmdlist_header) == 16);
+static_assert(sizeof(struct cmdlist_row) == 16);
+
+#define CL_HEADER_SZ sizeof(struct cmdlist_header)
+#define CL_ROW_SZ    sizeof(struct cmdlist_row)
+
+#define CMDLIST_RDMA_CFG_RDY(ch) BIT(ch)
+#define CMDLIST_CMPS_CFG_RDY(ch) BIT((ch) + 20)
+
+#define CMDLIST_WAIT_EVENT_BIT_RDMA_RELOAD BIT(4)
+#define CMDLIST_WAIT_EVENT_BIT_CMPS_RELOAD BIT(6)
+
+void saturn_hee_cmdlist_fill_data_row(struct cmdlist *cl, u32 strobe,
+				      u32 offset, u32 value[])
+{
+	struct cmdlist_row *row;
+	u8 i;
+
+	/*
+	 * packing counts rows blindly; drop the row rather than the buffer end
+	 */
+	if (WARN_ONCE(CL_HEADER_SZ + (cl->nod_len + 1) * CL_ROW_SZ > cl->size,
+		      "cmdlist buffer full (rows %u)\n", cl->nod_len))
+		return;
+
+	row = (struct cmdlist_row *)((char *)cl->va + CL_HEADER_SZ) + cl->nod_len;
+	row->module_cfg_addr = offset >> 2;
+	row->module_cfg_strobe = strobe;
+
+	for (i = 0; i < CMDLIST_ROW_REGS; i++)
+		if (strobe & CMDLIST_REG_STROBE(i))
+			row->module_regs[i] = value[i];
+}
+
+void saturn_hee_cmdlist_fill_conf_row(struct cmdlist *cl, struct spacemit_hw_device *hwdev,
+				      u8 dev_id)
+{
+	struct cmdlist_header *header;
+	struct cmdlist_row *row;
+	struct spacemit_plane_state *spacemit_pstate = NULL;
+	u8 rch_id = 0;
+	u32 wait_event_bits = 0, cfg_rdy_bits = 0;
+	int cur_crtc_y = -1, cur_crtc_h = -1, next_crtc_y = -1;
+
+	/*
+	 * CRTC-level cmdlists live inside a crtc_state; only planes have one.
+	 */
+	if (cl->type == CMDLIST_PLANE) {
+		spacemit_pstate = cl_to_spacemit_pstate(cl);
+		rch_id = spacemit_pstate->rdma_id;
+	}
+
+	/* At most three rows are appended below; bail before overrunning. */
+	if (WARN_ONCE(CL_HEADER_SZ + (cl->nod_len + 3) * CL_ROW_SZ > cl->size,
+		      "cmdlist buffer full (type %u, rows %u)\n",
+		      cl->type, cl->nod_len))
+		return;
+
+	header = (struct cmdlist_header *)(cl->va);
+	row = (struct cmdlist_row *)((char *)header + CL_HEADER_SZ + cl->nod_len * CL_ROW_SZ);
+	if (cl->next) {
+		if (dev_id == CMDLIST_CMP_INVALID) {
+			trace_u64_data("rch_id", rch_id);
+			row->module_cfg_addr = ((saturn_get_cl_chy_addr() + rch_id * 4) >> 2);
+		} else {
+			trace_u64_data("dev_id", dev_id);
+			row->module_cfg_addr = ((saturn_get_cl_chy_addr() + (5 + dev_id) * 4) >> 2);
+		}
+		row->module_cfg_strobe = CMDLIST_REG_STROBE(0);
+		if (cl->cmdlist_ch_y_other != CMDLIST_SENTINEL_POISON)
+			next_crtc_y = cl->cmdlist_ch_y_other;
+		else if (cl->next->type == CMDLIST_PLANE)
+			next_crtc_y = cl_to_spacemit_pstate(cl->next)->state.crtc_y;
+		else
+			next_crtc_y = 0;
+		row->module_regs[0] = next_crtc_y;
+		cl->nod_len++;
+		row++;
+	}
+
+	if (cl->type == CMDLIST_PLANE) {
+		if (cl->rch_start_cmps_y != CMDLIST_SENTINEL_POISON)
+			cur_crtc_y = cl->rch_start_cmps_y;
+		else
+			cur_crtc_y = cl_to_spacemit_pstate(cl)->state.crtc_y;
+		cur_crtc_h = cl_to_spacemit_pstate(cl)->state.crtc_h;
+		/* the cmdlist nodes don't belong to the same plane */
+		if (next_crtc_y != -1 && cl_to_spacemit_pstate(cl->next) != spacemit_pstate &&
+		    (cur_crtc_y + cur_crtc_h + 24) > next_crtc_y) {
+			drm_err_ratelimited(spacemit_pstate->state.crtc->dev,
+				"invalid gap: cur_y=%d cur_h=%d next_y=%d\n",
+				cur_crtc_y, cur_crtc_h, next_crtc_y);
+		}
+		row->module_cfg_addr = (saturn_get_cl_start_cmps_y_addr() + rch_id * 4) >> 2;
+		row->module_cfg_strobe = CMDLIST_REG_STROBE(0);
+		row->module_regs[0] =
+				(cur_crtc_y >= 24) ? (cur_crtc_y - 24) : 0;
+		cl->nod_len++;
+		row++;
+	}
+
+	if (cl->mode_mask & CMDLIST_MOD_RDMA) {
+		wait_event_bits |= CMDLIST_WAIT_EVENT_BIT_RDMA_RELOAD;
+		cfg_rdy_bits |= CMDLIST_RDMA_CFG_RDY(rch_id);
+	}
+	if (cl->mode_mask & CMDLIST_MOD_COMP) {
+		wait_event_bits |= CMDLIST_WAIT_EVENT_BIT_CMPS_RELOAD;
+		cfg_rdy_bits |= CMDLIST_CMPS_CFG_RDY(dev_id);
+	}
+	row->module_cfg_addr = (saturn_get_cl_cfg_rdy_addr()) >> 2;
+	row->module_cfg_strobe = CMDLIST_REG_STROBE(0);
+	row->module_regs[0] = cfg_rdy_bits;
+	row->row_eof_tag = 1;
+	cl->nod_len++;
+	row++;
+
+	if (cl->next)
+		header->next_list_addr = cl->next->pa;
+	else
+		header->nod_type = 1;
+	header->nod_len = cl->nod_len + 1; /* include header */
+	header->wait_event_low = wait_event_bits & 0x1F;
+	header->wait_event = wait_event_bits >> 5;
+}
+
+void saturn_hee_crtc_cmdlist(struct cmdlist *cl, struct spacemit_hw_device *hwdev,
+			     struct spacemit_drm_private *priv, u8 crtc_id, u8 dev_id)
+{
+	u32 val = 0;
+
+	dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR, cmdlist_reg_48,
+			  hwdev->rdma_nums + 2 + crtc_id, cmdlist_ch_y_first,
+			  val);
+
+	val = (lower_32_bits(priv->cmdlist_groups[hwdev->rdma_nums + crtc_id]->pa) &
+	       CMDLIST_ADDRL_ALIGN_MASK) >> CMDLIST_ADDRL_ALIGN_BITS;
+	dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR, cmdlist_reg_0,
+			  hwdev->rdma_nums + 2 + crtc_id,
+			  cmdlist_ch_start_addrl, val);
+	val = upper_32_bits(priv->cmdlist_groups[hwdev->rdma_nums + crtc_id]->pa);
+	dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR, cmdlist_reg_16,
+			  hwdev->rdma_nums + 2 + crtc_id,
+			  cmdlist_ch_start_addrh, val);
+	dpu_write_reg_arr(hwdev, DPU_CTL_TOP_REG, DPU_CTRL_BASE_ADDR,
+			  dpu_ctl_top_reg_10, dev_id, cmdlist_cmps_top_en, 1);
+	dpu_write_reg_arr(hwdev, DPU_CTL_TOP_REG, DPU_CTRL_BASE_ADDR,
+			  dpu_ctl_top_reg_10, dev_id, cmdlist_cmps_other_en, 1);
+
+	priv->cmdlist_groups[hwdev->rdma_nums + crtc_id] = NULL;
+}
+
+#define DUMP_BUF_LEN	100
+void saturn_hee_cmdlist_dump_node(struct cmdlist *cl)
+{
+	struct cmdlist_header *header = (struct cmdlist_header *)(cl->va);
+	ssize_t ret = 0;
+	char tmp[DUMP_BUF_LEN] = {0x0};
+
+	ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "header: ");
+	ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "next_addr:0x%llx ",
+			 (unsigned long long)header->next_list_addr);
+	ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "rows:%d ",
+			 header->nod_len);
+	ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "type:%d ",
+			 header->nod_type);
+	ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "event_low:0x%llx ",
+			 (unsigned long long)header->wait_event_low);
+	ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "event:0x%llx ",
+			 (unsigned long long)header->wait_event);
+	trace_plat_cmdlist_dump_node(tmp);
+
+	for (int i = 0; i < header->nod_len - 1; i++) {
+		struct cmdlist_row *row = (struct cmdlist_row *)((char *)header + CL_HEADER_SZ) + i;
+		u32 addr = row->module_cfg_addr << 2;
+
+		ret = 0;
+		ret += scnprintf(tmp + ret, sizeof(tmp) - ret,
+				 "row[%4d:tag:%d]: ", i, row->row_eof_tag);
+
+		for (int j = 0; j < CMDLIST_ROW_REGS; j++) {
+			if (row->module_cfg_strobe & CMDLIST_REG_STROBE(j))
+				ret += scnprintf(tmp + ret, sizeof(tmp) - ret,
+						 "[%#10x:%#10x]", addr + j * 4,
+						 row->module_regs[j]);
+			else
+				ret += scnprintf(tmp + ret, sizeof(tmp) - ret,
+						 "%#11x:%#11x", addr + j * 4,
+						 row->module_regs[j]);
+		}
+		trace_plat_cmdlist_dump_node(tmp);
+	}
+}
diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c
new file mode 100644
index 000000000000..144716ec9b80
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/stddef.h>
+#include <linux/export.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_mode.h>
+#include <drm/drm_fourcc.h>
+#include "../spacemit_cmdlist.h"
+#include "saturn_fbcmem.h"
+
+int get_raw_data_plane_rdma_mem_size(u32 drm_4cc_fmt, u32 plane_crop_width,
+				     u32 *output_mem_size)
+{
+	u32 ret_mem_size = 0;
+	const struct drm_format_info *info = NULL;
+
+	info = drm_format_info(drm_4cc_fmt);
+	if (info->cpp[0] == 0 || info->num_planes != 1 || info->is_yuv)
+		return -EINVAL;
+
+	/* 64-byte rounding is a dpu hardware request */
+	ret_mem_size = roundup(plane_crop_width * info->cpp[0], 64);
+	ret_mem_size = ret_mem_size / 32;
+
+	if (output_mem_size)
+		*output_mem_size = ret_mem_size;
+
+	return 0;
+}
+
+int saturn_cal_layer_fbcmem_size(struct drm_plane *plane,
+				 struct drm_plane_state *state)
+{
+	struct spacemit_plane_state *pstate = to_spacemit_plane_state(state);
+	u32 drm_4cc_fmt = pstate->state.fb->format->format;
+	u32 crop_w = pstate->state.src_w >> 16;
+
+	/* Only linear sizing: the compressed path is not exposed. */
+	return get_raw_data_plane_rdma_mem_size(drm_4cc_fmt, crop_w,
+						&pstate->fbcmem_size);
+}
+
+/* Even RDMA (0, 2, ...): sole owner of the front of its fbc memory bank. */
+static int saturn_fbcmem_place_even(struct spacemit_hw_device *hwdev,
+				    struct spacemit_crtc_rdma *rdmas,
+				    u32 *fbc_mems_left, u8 index)
+{
+	u32 cur_rdma_fbcmem_size = rdmas[index].fbcmem.size;
+	u32 pri_fbcmem_size = hwdev->fbcmem_sizes[index / 2] / FBCMEM_UNIT;
+
+	if (cur_rdma_fbcmem_size > pri_fbcmem_size)
+		return -ENOSPC;
+
+	/* The preceding odd RDMA may not already share this bank. */
+	if (index > 0 && rdmas[index - 1].fbcmem.map)
+		return -ENOSPC;
+
+	rdmas[index].fbcmem.start = 0;
+	rdmas[index].fbcmem.map = true;
+	fbc_mems_left[index / 2] -= cur_rdma_fbcmem_size;
+
+	return 0;
+}
+
+/* Odd RDMA takes what is left of its own bank, spilling into the next. */
+static int saturn_fbcmem_place_odd(struct spacemit_hw_device *hwdev,
+				   struct spacemit_crtc_rdma *rdmas,
+				   u32 *fbc_mems_left, u8 index)
+{
+	u32 cur_rdma_fbcmem_size = rdmas[index].fbcmem.size;
+	u32 pri_fbcmem_size = hwdev->fbcmem_sizes[index / 2] / FBCMEM_UNIT;
+	u8 rdma_nums = hwdev->rdma_nums;
+	u8 sec_fbcmem_index;
+	u32 sec_fbcmem_size;
+
+	if (cur_rdma_fbcmem_size <= fbc_mems_left[index / 2]) { /* not share fbc mem */
+		rdmas[index].fbcmem.map = false;
+		rdmas[index].fbcmem.start =
+				pri_fbcmem_size - fbc_mems_left[index / 2];
+		rdmas[index].fbcmem.size = fbc_mems_left[index / 2]; /* use all the mem left */
+		fbc_mems_left[index / 2] = 0;
+		return 0;
+	}
+
+	/* Need to share the next bank's fbc mem. */
+	sec_fbcmem_index = (index / 2 + 1) % (rdma_nums / 2);
+	sec_fbcmem_size = hwdev->fbcmem_sizes[sec_fbcmem_index] / FBCMEM_UNIT;
+	if (cur_rdma_fbcmem_size > fbc_mems_left[index / 2] + sec_fbcmem_size)
+		return -ENOSPC;
+
+	if (index == rdma_nums - 1) { /* last rdma id */
+		/* bank 0 is already in use */
+		if (fbc_mems_left[0] != hwdev->fbcmem_sizes[0] / FBCMEM_UNIT)
+			return -ENOSPC;
+
+		rdmas[index].fbcmem.size =
+				fbc_mems_left[index / 2] + sec_fbcmem_size;
+		cur_rdma_fbcmem_size = rdmas[index].fbcmem.size;
+	}
+	fbc_mems_left[sec_fbcmem_index] -= (cur_rdma_fbcmem_size - fbc_mems_left[index / 2]);
+	rdmas[index].fbcmem.start = pri_fbcmem_size - fbc_mems_left[index / 2];
+	rdmas[index].fbcmem.map = true;
+	fbc_mems_left[index / 2] = 0;
+
+	return 0;
+}
+
+int saturn_adjust_rdma_fbcmem(struct spacemit_hw_device *hwdev,
+			      struct spacemit_crtc_rdma *rdmas)
+{
+	int ret = -ENOSPC;
+	u8 index = 0;
+	u32 *fbc_mems_left = NULL;
+	u8 rdma_nums = hwdev->rdma_nums;
+
+	fbc_mems_left = kzalloc_objs(*fbc_mems_left, rdma_nums / 2);
+	if (!fbc_mems_left) {
+		ret = -ENOMEM;
+		goto free;
+	}
+	for (index = 0; index < rdma_nums / 2; index++)
+		fbc_mems_left[index] = hwdev->fbcmem_sizes[index] / FBCMEM_UNIT;
+
+	for (index = 0; index < rdma_nums; index++) {
+		int err;
+
+		if (rdmas[index].fbcmem.size == 0)
+			continue;
+
+		if (index % 2 == 0)
+			err = saturn_fbcmem_place_even(hwdev, rdmas,
+						       fbc_mems_left, index);
+		else
+			err = saturn_fbcmem_place_odd(hwdev, rdmas,
+						      fbc_mems_left, index);
+		if (err) {
+			ret = err;
+			goto free;
+		}
+	}
+	ret = 0;
+
+free:
+	kfree(fbc_mems_left);
+
+	return ret;
+}
diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.h b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.h
new file mode 100644
index 000000000000..a4998dbc682f
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _SATURN_FBCMEM_H_
+#define _SATURN_FBCMEM_H_
+
+#include <drm/drm_plane.h>
+#include "../spacemit_crtc.h"
+#include "../spacemit_dpu_reg.h"
+#include "dpu_saturn.h"
+
+#define FBCMEM_UNIT	(32) /* fbcmem is 32 bytes per unit */
+
+int get_raw_data_plane_rdma_mem_size(u32 drm_4cc_fmt, u32 plane_crop_width,
+				     u32 *output_mem_size);
+
+int saturn_cal_layer_fbcmem_size(struct drm_plane *plane,
+				 struct drm_plane_state *state);
+
+int saturn_adjust_rdma_fbcmem(struct spacemit_hw_device *hwdev,
+			      struct spacemit_crtc_rdma *rdmas);
+
+#endif
diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h b/drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h
new file mode 100644
index 000000000000..7f5f01966377
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef _SATURN_hee_OPS_H_
+#define _SATURN_hee_OPS_H_
+
+#include "../../spacemit_crtc.h"
+
+void saturn_hee_enable_vsync(struct spacemit_crtc *a_crtc,
+			     struct spacemit_hw_device *hwdev, bool enable);
+void saturn_hee_enable_cfg_irq(struct spacemit_crtc *a_crtc,
+			       struct spacemit_hw_device *hwdev, bool enable);
+void saturn_hee_cfg_ready(struct spacemit_crtc *a_crtc,
+			  struct spacemit_hw_device *hwdev);
+void saturn_hee_sw_start(struct spacemit_crtc *a_crtc,
+			 struct spacemit_hw_device *hwdev);
+void saturn_hee_dpu_init(struct spacemit_crtc *a_crtc);
+void saturn_hee_plane_update_hw_channel(struct drm_plane *plane);
+void saturn_hee_plane_disable_hw_channel(struct drm_plane *plane,
+					 struct drm_plane_state *old_state);
+void saturn_hee_irq_enable(struct spacemit_crtc *a_crtc, bool enable);
+void saturn_hee_conf_dpuctrl(struct drm_crtc *crtc,
+			     struct drm_crtc_state *old_state);
+u32 saturn_hee_get_cfg_rdy(struct spacemit_crtc *a_crtc,
+			   struct spacemit_hw_device *hwdev);
+u32 saturn_hee_get_int_sts(struct spacemit_hw_device *hwdev, int dev_id);
+u32 saturn_hee_get_irq_bit(enum spacemit_dpu_irq irq_id, int dev_id);
+void saturn_hee_clr_int_sts(struct spacemit_crtc *a_crtc, u32 data, int dev_id);
+void saturn_hee_enable_cmdlist(struct spacemit_crtc *a_crtc,
+			       struct spacemit_hw_device *hwdev, int id,
+			       bool enable);
+void saturn_hee_cfg_cmdlist(struct spacemit_hw_device *hwdev, int id, u32 chy,
+			    u32 addrl, u32 addrh);
+void saturn_hee_rdma_dmmu(struct spacemit_hw_device *hwdev, u8 tbu_id,
+			  struct tbu_instance *tbu, struct drm_framebuffer *fb,
+			  u32 val, struct cmdlist_regs *cl_rdma,
+			  struct drm_plane *plane);
+int saturn_hee_get_cl_rdma_buf(struct spacemit_crtc *a_crtc);
+void saturn_hee_cmdlist_fill_data_row(struct cmdlist *cl, u32 strobe,
+				      u32 offset, u32 value[]);
+void saturn_hee_cmdlist_fill_conf_row(struct cmdlist *cl,
+				      struct spacemit_hw_device *hwdev,
+				      u8 dev_id);
+void saturn_hee_crtc_cmdlist(struct cmdlist *cl,
+			     struct spacemit_hw_device *hwdev,
+			     struct spacemit_drm_private *priv, u8 crtc_id,
+			     u8 dev_id);
+void saturn_hee_cmdlist_dump_node(struct cmdlist *cl);
+#endif

-- 
2.43.0



  parent reply	other threads:[~2026-07-25  4:51 UTC|newest]

Thread overview: 26+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-25  4:51 [PATCH RESEND 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver Cody Kang via B4 Relay
2026-07-25  5:02   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 07/17] drm/spacemit: add Saturn DPU register model Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU Cody Kang via B4 Relay
2026-07-25  5:04   ` sashiko-bot
2026-07-25  4:51 ` Cody Kang via B4 Relay [this message]
2026-07-25  5:12   ` [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline Cody Kang via B4 Relay
2026-07-25  5:04   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver Cody Kang via B4 Relay
2026-07-25  5:05   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver Cody Kang via B4 Relay
2026-07-25  5:04   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 13/17] MAINTAINERS: add SpacemiT K3 display driver entry Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes Cody Kang via B4 Relay
2026-07-25  5:08   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output Cody Kang via B4 Relay
2026-07-25  5:12   ` sashiko-bot
2026-07-25  4:51 ` [PATCH RESEND 16/17] riscv: dts: spacemit: k3-com260-ifx: " Cody Kang via B4 Relay
2026-07-25  4:51 ` [PATCH RESEND 17/17] riscv: defconfig: spacemit: k3: enable display driver Cody Kang via B4 Relay

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260725-k3-display-v1-9-6de34d80e86c@gmail.com \
    --to=devnull+codykang.hk.gmail.com@kernel.org \
    --cc=airlied@gmail.com \
    --cc=alex@ghiti.fr \
    --cc=aou@eecs.berkeley.edu \
    --cc=bmasney@redhat.com \
    --cc=codykang.hk@gmail.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=dlan@kernel.org \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=heylenay@4d2.org \
    --cc=krzk+dt@kernel.org \
    --cc=linux-clk@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-phy@lists.infradead.org \
    --cc=linux-riscv@lists.infradead.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=mripard@kernel.org \
    --cc=mturquette@baylibre.com \
    --cc=neil.armstrong@linaro.org \
    --cc=p.zabel@pengutronix.de \
    --cc=palmer@dabbelt.com \
    --cc=pjw@kernel.org \
    --cc=robh@kernel.org \
    --cc=sboyd@kernel.org \
    --cc=simona@ffwll.ch \
    --cc=spacemit@lists.linux.dev \
    --cc=tzimmermann@suse.de \
    --cc=vkoul@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox