All of lore.kernel.org
 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



-- 
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy

WARNING: multiple messages have this Message-ID (diff)
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



WARNING: multiple messages have this Message-ID (diff)
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



_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv

WARNING: multiple messages have this Message-ID (diff)
From: Cody Kang <codykang.hk@gmail.com>
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>

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: 88+ 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 ` Cody Kang
2026-07-25  4:51 ` Cody Kang via B4 Relay
2026-07-25  4:51 ` 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   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` 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   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` 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   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` 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   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` 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  4:51   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  5:02   ` sashiko-bot
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   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` 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   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` 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  4:51   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  5:04   ` sashiko-bot
2026-07-25  5:04     ` sashiko-bot
2026-07-25  4:51 ` Cody Kang via B4 Relay [this message]
2026-07-25  4:51   ` [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  5:12   ` sashiko-bot
2026-07-25  5:12     ` 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  4:51   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  5:04   ` sashiko-bot
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  4:51   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  5:05   ` sashiko-bot
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  4:51   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  5:04   ` sashiko-bot
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   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` 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  4:51   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  5:08   ` sashiko-bot
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  4:51   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  5:12   ` sashiko-bot
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   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` 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
2026-07-25  4:51   ` Cody Kang
2026-07-25  4:51   ` Cody Kang via B4 Relay
2026-07-25  4:51   ` 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.