* [PATCH v2 1/9] drm/rockchip: vop2: Take the UV plane address from the second GEM object
2026-11-12 8:33 [PATCH v2 0/9] drm/rockchip: vop2: RK3588 clocking, sync polarity, AXI port and window fixes Arun Iyengar via B4 Relay
@ 2026-11-12 8:33 ` Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 2/9] drm/rockchip: vop2: Don't use uninitialised dividers on RK3588 Arun Iyengar via B4 Relay
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-12 8:33 UTC (permalink / raw)
To: Sandy Huang, Heiko Stübner, Andy Yan, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Sascha Hauer
Cc: Piotr Zalewski, Chaoyi Chen, dri-devel, linux-rockchip,
linux-arm-kernel, linux-kernel, Arun Iyengar, stable
From: Arun Iyengar <aiyengar1990@gmail.com>
vop2_plane_atomic_update() computes the address of the chroma plane of a
multi-planar YUV framebuffer from fb->obj[0], the object of the luma
plane, plus the offset of the chroma plane. A framebuffer may have its
planes in different GEM objects though (DRM_IOCTL_MODE_ADDFB2 with two
handles, as with an NV12M buffer from a V4L2 decoder imported as two
DMA-BUFs). The window then fetches the chroma plane from the luma buffer,
at an offset meant for another buffer, which shows a wrong picture or
reads past the end of the luma buffer.
Use fb->obj[1] for the chroma plane. Single plane YUV formats like
DRM_FORMAT_YUV420_8BIT have no second object and keep using fb->obj[0].
This is also what the vendor kernel does.
Fixes: 604be85547ce ("drm/rockchip: Add VOP2 driver")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-206550@kernel.org/
Cc: stable@vger.kernel.org
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
index 76d151b8bc52..ea8d837d661f 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
@@ -1369,7 +1369,9 @@ static void vop2_plane_atomic_update(struct drm_plane *plane,
if ((pstate->rotation & DRM_MODE_REFLECT_Y) && !afbc_en)
offset += fb->pitches[1] * ((pstate->src_h >> 16) - 2) / vsub;
- rk_obj = to_rockchip_obj(fb->obj[0]);
+ /* Single plane formats like YUV420_8BIT have no second object */
+ if (fb->format->num_planes > 1)
+ rk_obj = to_rockchip_obj(fb->obj[1]);
uv_mst = rk_obj->dma_addr + offset + fb->offsets[1];
}
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v2 2/9] drm/rockchip: vop2: Don't use uninitialised dividers on RK3588
2026-11-12 8:33 [PATCH v2 0/9] drm/rockchip: vop2: RK3588 clocking, sync polarity, AXI port and window fixes Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 1/9] drm/rockchip: vop2: Take the UV plane address from the second GEM object Arun Iyengar via B4 Relay
@ 2026-11-12 8:33 ` Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 3/9] drm/rockchip: vop2: Fix the HDMI sync polarity " Arun Iyengar via B4 Relay
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-12 8:33 UTC (permalink / raw)
To: Sandy Huang, Heiko Stübner, Andy Yan, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Sascha Hauer
Cc: Piotr Zalewski, Chaoyi Chen, dri-devel, linux-rockchip,
linux-arm-kernel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
rk3588_calc_cru_cfg() converts all four dividers to their register
encoding with ilog2(), but only sets the ones the interface uses:
dclk_out_div is left uninitialised for HDMI, eDP and DPI, and
if_pixclk_div/if_dclk_div for DP and DPI. The garbage is then written to
RK3588_VP_CLK_CTRL; on an Orange Pi 5 the dclk_out divider of the HDMI
video port reads back as 3 (/8) in every mode.
Start all dividers at 1. dclk_out only feeds DP and MIPI (and HDMI with
DSC, which is not supported), so for the other interfaces keep it at the
dclk_core rate, as the vendor kernel does for HDMI with DSC, instead of
letting it run at the full DCLK rate.
Fixes: 5a028e8f062f ("drm/rockchip: vop2: Add support for rk3588")
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/rockchip/rockchip_vop2_reg.c | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
index c44648e1bc0d..659fd0a3ab63 100644
--- a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
+++ b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
@@ -1579,6 +1579,15 @@ static unsigned long rk3588_calc_cru_cfg(struct vop2_video_port *vp, int id,
unsigned long if_pixclk_rate;
int K = 1;
+ /*
+ * Not every interface uses every divider: dclk_out only feeds DP and
+ * MIPI, the interface dividers are unused for DP. Start from 1 so that
+ * no divider is left uninitialised when its value is converted below.
+ */
+ *dclk_out_div = 1;
+ *if_pixclk_div = 1;
+ *if_dclk_div = 1;
+
if (vop2_output_if_is_hdmi(id)) {
/*
* K = 2: dclk_core = if_pixclk_rate > if_dclk_rate
@@ -1648,6 +1657,9 @@ static unsigned long rk3588_calc_cru_cfg(struct vop2_video_port *vp, int id,
}
*dclk_core_div = dclk_rate / dclk_core_rate;
+ /* An unused dclk_out must not run faster than dclk_core */
+ if (!vop2_output_if_is_dp(id) && !vop2_output_if_is_mipi(id))
+ *dclk_out_div = *dclk_core_div;
*if_pixclk_div = ilog2(*if_pixclk_div);
*if_dclk_div = ilog2(*if_dclk_div);
*dclk_core_div = ilog2(*dclk_core_div);
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v2 3/9] drm/rockchip: vop2: Fix the HDMI sync polarity on RK3588
2026-11-12 8:33 [PATCH v2 0/9] drm/rockchip: vop2: RK3588 clocking, sync polarity, AXI port and window fixes Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 1/9] drm/rockchip: vop2: Take the UV plane address from the second GEM object Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 2/9] drm/rockchip: vop2: Don't use uninitialised dividers on RK3588 Arun Iyengar via B4 Relay
@ 2026-11-12 8:33 ` Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 4/9] drm/rockchip: vop2: Don't accept AFBC YTR for RGB565 " Arun Iyengar via B4 Relay
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-12 8:33 UTC (permalink / raw)
To: Sandy Huang, Heiko Stübner, Andy Yan, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Sascha Hauer
Cc: Piotr Zalewski, Chaoyi Chen, dri-devel, linux-rockchip,
linux-arm-kernel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
rk3588_set_intf_mux() passes the POLFLAG bits of the mode to
rk3588_get_hdmi_pol(), which decodes them as DRM mode flags: the
VSYNC_POSITIVE bit is taken for DRM_MODE_FLAG_NHSYNC and POLFLAG_DCLK_INV
for DRM_MODE_FLAG_NVSYNC. As a result GRF_VO1_CON0 marks the HSYNC of
every +HSync/+VSync mode (e.g. all 1080p, 2160p CTA modes) as negative
and both syncs of -HSync/-VSync modes (e.g. 720x480, 640x480) as
positive.
The bits in GRF_VO1_CON0 are set for a negative sync; that is how the
vendor kernel programs them (vop2_get_hdmi_pol(), called with the mode
flags, sets them for DRM_MODE_FLAG_NHSYNC/NVSYNC on RK3588). Decode the
POLFLAG bits accordingly.
Fixes: 5a028e8f062f ("drm/rockchip: vop2: Add support for rk3588")
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/rockchip/rockchip_vop2_reg.c | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
index 659fd0a3ab63..7c48b440cb29 100644
--- a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
+++ b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
@@ -1687,12 +1687,16 @@ static u32 rk3588_get_mipi_port_mux(int vp_id)
return 0;
}
-static u32 rk3588_get_hdmi_pol(u32 flags)
+/*
+ * The HDMI sync polarity bits in GRF_VO1_CON0 are set for an active low
+ * (negative) sync.
+ */
+static u32 rk3588_get_hdmi_pol(u32 polflags)
{
u32 val;
- val = (flags & DRM_MODE_FLAG_NHSYNC) ? BIT(HSYNC_POSITIVE) : 0;
- val |= (flags & DRM_MODE_FLAG_NVSYNC) ? BIT(VSYNC_POSITIVE) : 0;
+ val = (polflags & BIT(HSYNC_POSITIVE)) ? 0 : BIT(HSYNC_POSITIVE);
+ val |= (polflags & BIT(VSYNC_POSITIVE)) ? 0 : BIT(VSYNC_POSITIVE);
return val;
}
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v2 4/9] drm/rockchip: vop2: Don't accept AFBC YTR for RGB565 on RK3588
2026-11-12 8:33 [PATCH v2 0/9] drm/rockchip: vop2: RK3588 clocking, sync polarity, AXI port and window fixes Arun Iyengar via B4 Relay
` (2 preceding siblings ...)
2026-11-12 8:33 ` [PATCH v2 3/9] drm/rockchip: vop2: Fix the HDMI sync polarity " Arun Iyengar via B4 Relay
@ 2026-11-12 8:33 ` Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 5/9] drm/rockchip: vop2: Balance window fetches across both AXI ports " Arun Iyengar via B4 Relay
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-12 8:33 UTC (permalink / raw)
To: Sandy Huang, Heiko Stübner, Andy Yan, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Sascha Hauer
Cc: Piotr Zalewski, Chaoyi Chen, dri-devel, linux-rockchip,
linux-arm-kernel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
The Cluster windows of the RK3588 VOP2 advertise every AFBC modifier
of format_modifiers_afbc for RGB565 and BGR565, including the ones
with the lossless colour transform (YTR). Scanning out an RGB565
buffer rendered by the Mali-G610 (panfrost) with
AFBC(16x16 | SPARSE | YTR) gives a black picture and POST_BUF_EMPTY
on the video port, on every Cluster window, while the same buffer
rendered with AFBC(16x16 | SPARSE) displays correctly, as do 32 bpp
formats with YTR.
YTR on RGB565 is also a questionable pairing to start with:
Documentation/gpu/afbc.rst requires the R, G, B component order when
YTR is used, with component 0 in the least significant bits, which
DRM_FORMAT_RGB565 (B in the low bits) doesn't have, and the Arm
Mali-D71 (komeda) only accepts AFBC for BGR565.
Stop advertising and accepting YTR for the two 16 bpp RGB formats on
RK3588, so that userspace picks a modifier the decoder handles. The
BGR565 case could not be tested (Mesa can't import BGR565 dma-bufs),
it's rejected as well because the decoder is configured identically
for both formats, apart from the R/B swap after decoding.
Fixes: 5a028e8f062f ("drm/rockchip: vop2: Add support for rk3588")
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
index ea8d837d661f..7c157f5c1734 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
@@ -404,6 +404,18 @@ static bool rockchip_vop2_mod_supported(struct drm_plane *plane, u32 format,
if (vop2_convert_afbc_format(format) == VOP2_AFBC_FMT_INVALID)
return false;
+ /*
+ * The AFBC decoder of the RK3588 Cluster windows can't decode 16 bpp
+ * RGB that uses the lossless colour transform: the picture stays
+ * black and the video port reports POST_BUF_EMPTY, while the same
+ * buffers without YTR display fine.
+ */
+ if (vop2->version == VOP_VERSION_RK3588 && (modifier & AFBC_FORMAT_MOD_YTR) &&
+ (format == DRM_FORMAT_RGB565 || format == DRM_FORMAT_BGR565)) {
+ drm_dbg_kms(vop2->drm, "No AFBC YTR support for 16 bpp RGB\n");
+ return false;
+ }
+
/* Different windows have different format/modifier support */
for (i = 0; i < plane->modifier_count; i++) {
if (plane->modifiers[i] == modifier)
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v2 5/9] drm/rockchip: vop2: Balance window fetches across both AXI ports on RK3588
2026-11-12 8:33 [PATCH v2 0/9] drm/rockchip: vop2: RK3588 clocking, sync polarity, AXI port and window fixes Arun Iyengar via B4 Relay
` (3 preceding siblings ...)
2026-11-12 8:33 ` [PATCH v2 4/9] drm/rockchip: vop2: Don't accept AFBC YTR for RGB565 " Arun Iyengar via B4 Relay
@ 2026-11-12 8:33 ` Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 6/9] drm/rockchip: vop2: Report POST_BUF_EMPTY at most once per frame Arun Iyengar via B4 Relay
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-12 8:33 UTC (permalink / raw)
To: Sandy Huang, Heiko Stübner, Andy Yan, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Sascha Hauer
Cc: Piotr Zalewski, Chaoyi Chen, dri-devel, linux-rockchip,
linux-arm-kernel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
The RK3588 VOP2 fetches through two AXI read ports. Every window has a
fixed default port in rk3588_vop_win_data: Cluster0/1 and Esmart0/1 use
AXI0, Cluster2/3 and Esmart2/3 use AXI1. The port is chosen by a per
window register (CLUSTER_CTRL[13] / ESMART_AXI_CTRL[1]), though, and it
is programmed on every plane update.
One port can't sustain two full screen 4K@120Hz layers. With the
console on Cluster0 (the primary plane of video port 0) and a video on
Esmart0 (its overlay plane), both on AXI0, HDMI 3840x2160@120 reports
POST_BUF_EMPTY on about half of all lines (~120000 per second) and the
picture is scrambled. The VOP read rate measured with the DDR PMU drops
from the expected 6.3 GB/s to 3.2 GB/s: the fetch collapses instead of
merely saturating. Measured at 4K@120Hz, XR24 primary plane:
Cluster0 + Esmart0 (AXI0 + AXI0) ~120k POST_BUF_EMPTY/s
Cluster2 + Esmart2 (AXI1 + AXI1) ~100k/s
Esmart0 + Esmart1 (AXI0 + AXI0) ~90k/s (XR24 both)
Cluster0 + Esmart2 (AXI0 + AXI1) 0, 9.2 GB/s read
Cluster0 + Cluster2 (AXI0 + AXI1) 0, 8.6 GB/s read
Moving Esmart0 to AXI1 at runtime (with no other change) makes the
first case clean. Raising ACLK_VOP from 750 to 850 MHz, or changing
the NoC QoS priority or mode of the VOP ports, makes no difference,
and neither does the buffer placement (both planes scanning out the
same buffer fail the same way). So the limit is per port, and the
fixed assignment puts both layers of the most common configuration on
the same port while the other one sits idle. Lower modes are affected
too: the same port also carries Cluster1 for a second display, and
under DDR contention from other masters a 4K@60Hz port with the
console, an overlay and a second display underflows for short bursts.
Choose the port of a window when it starts fetching instead. The atomic
check assigns the ports from the whole new state and keeps the
assignment in a private object for the next check. The port of a window
is also stored in its plane state, and that is what the plane update
programs: commits on different video ports aren't ordered against each
other, so the private state of a commit may already be replaced and
freed when its planes are updated. A window that keeps fetching keeps
its port, so an active layer is never moved. The windows
that start fetching, the busiest first, each get the port that leaves
the busier port least loaded, preferring the default one on a tie. The
load of a window is its estimated fetch rate: source size x bits per
pixel of all its planes x refresh rate.
A window must not share an AXI read ID with another window on the same
port. The IDs in the table are unique per default port, and mirrored
between the two ports (Cluster0/Cluster2, Esmart0/Esmart2, ...), so a
window can always use the port its mirror isn't on. A window that is
switched off keeps fetching until the next frame start of its video
port, so its read IDs stay in use for a window that starts on another
video port in the same commit, and until the commit that switched it
off is done.
This is only enabled for RK3588, where it was measured.
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 330 ++++++++++++++++++++++++++-
drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 42 ++++
drivers/gpu/drm/rockchip/rockchip_vop2_reg.c | 3 +-
3 files changed, 370 insertions(+), 5 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
index 7c157f5c1734..684fa46924e7 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
@@ -23,6 +23,7 @@
#include <drm/drm.h>
#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_state_helper.h>
#include <drm/drm_atomic_uapi.h>
#include <drm/drm_blend.h>
#include <drm/drm_crtc.h>
@@ -1231,6 +1232,314 @@ static int vop2_plane_atomic_check(struct drm_plane *plane,
return 0;
}
+static bool vop2_win_axi_ids_clash(const struct vop2_win *a, const struct vop2_win *b)
+{
+ u8 ida[] = { a->data->axi_yrgb_r_id, a->data->axi_uv_r_id };
+ u8 idb[] = { b->data->axi_yrgb_r_id, b->data->axi_uv_r_id };
+ int i, j;
+
+ for (i = 0; i < ARRAY_SIZE(ida); i++)
+ for (j = 0; j < ARRAY_SIZE(idb); j++)
+ if (ida[i] == idb[j])
+ return true;
+
+ return false;
+}
+
+/*
+ * The VOP2 on RK3588 has two AXI read ports. Each window has a default port
+ * (&vop2_win_data.axi_bus_id), but the port is selected by a per window
+ * register, so any window can fetch through either one as long as its read
+ * IDs are unique on that port.
+ *
+ * A single port can't sustain much more than one full screen 4K@120Hz
+ * layer: two such layers on the same port (e.g. Cluster0 and Esmart0, the
+ * default primary and overlay planes of video port 0) make the window
+ * line buffers run dry, the fetch rate collapses and the video port
+ * reports POST_BUF_EMPTY on most lines, which scrambles the picture. The
+ * same layers fetched through different ports are fine. Raising ACLK or
+ * the NoC priority of the VOP doesn't help, the limit is per port.
+ *
+ * So the atomic check assigns a port to every window that starts fetching,
+ * from the whole new state: a window that keeps fetching keeps its port,
+ * and the new ones, the busiest first, get the port that leaves the busier
+ * of the two ports least loaded, preferring the default one. A window
+ * never gets a port on which another window may still fetch with one of
+ * its read IDs.
+ *
+ * The assignment of all windows is kept in a private object for the next
+ * atomic check. The plane update takes the port of its window from the
+ * plane state instead: commits on different video ports aren't ordered
+ * against each other, so a later one may have replaced and freed the
+ * private state by the time the planes of this one are updated.
+ */
+#define to_vop2_axi_state(s) container_of(s, struct vop2_axi_state, base)
+#define to_vop2_plane_state(s) container_of(s, struct vop2_plane_state, base)
+
+static struct drm_private_state *vop2_axi_duplicate_state(struct drm_private_obj *obj)
+{
+ struct vop2 *vop2 = container_of(obj, struct vop2, axi_obj);
+ struct vop2_axi_state *state;
+
+ state = kmemdup(to_vop2_axi_state(obj->state),
+ struct_size(state, win, vop2->registered_num_wins),
+ GFP_KERNEL);
+ if (!state)
+ return NULL;
+
+ __drm_atomic_helper_private_obj_duplicate_state(obj, &state->base);
+
+ return &state->base;
+}
+
+static void vop2_axi_destroy_state(struct drm_private_obj *obj,
+ struct drm_private_state *state)
+{
+ kfree(to_vop2_axi_state(state));
+}
+
+static struct drm_private_state *vop2_axi_create_state(struct drm_private_obj *obj)
+{
+ struct vop2 *vop2 = container_of(obj, struct vop2, axi_obj);
+ struct vop2_axi_state *state;
+ int i;
+
+ state = kzalloc(struct_size(state, win, vop2->registered_num_wins),
+ GFP_KERNEL);
+ if (!state)
+ return ERR_PTR(-ENOMEM);
+
+ for (i = 0; i < vop2->registered_num_wins; i++)
+ state->win[i].bus = vop2->win[i].data->axi_bus_id;
+
+ __drm_atomic_helper_private_obj_create_state(obj, &state->base);
+
+ return &state->base;
+}
+
+static const struct drm_private_state_funcs vop2_axi_state_funcs = {
+ .atomic_create_state = vop2_axi_create_state,
+ .atomic_duplicate_state = vop2_axi_duplicate_state,
+ .atomic_destroy_state = vop2_axi_destroy_state,
+};
+
+/* Bytes per second the window fetches, from all planes of the framebuffer */
+static u64 vop2_plane_axi_load(const struct drm_plane_state *pstate,
+ const struct drm_display_mode *mode)
+{
+ const struct drm_format_info *info = pstate->fb->format;
+ u64 pixels = (u64)(drm_rect_width(&pstate->src) >> 16) *
+ (drm_rect_height(&pstate->src) >> 16) *
+ drm_mode_vrefresh(mode);
+ u32 bits = 0;
+ int i;
+
+ /* Single plane formats like YUV420_8BIT have no cpp */
+ if (info->num_planes == 1)
+ return div_u64(pixels * vop2_get_bpp(info), 8);
+
+ for (i = 0; i < info->num_planes; i++)
+ bits += drm_format_info_bpp(info, i) /
+ (i ? info->hsub * info->vsub : 1);
+
+ return div_u64(pixels * bits, 8);
+}
+
+/* The CRTC state of the plane if its window fetches in the new state */
+static const struct drm_crtc_state *
+vop2_plane_fetch_crtc_state(struct drm_atomic_commit *state,
+ const struct drm_plane_state *pstate)
+{
+ const struct drm_crtc_state *cstate;
+
+ if (!pstate->crtc || !pstate->fb || !pstate->visible)
+ return NULL;
+
+ cstate = drm_atomic_get_new_crtc_state(state, pstate->crtc);
+
+ return cstate && cstate->active ? cstate : NULL;
+}
+
+/*
+ * Whether a window that doesn't fetch in the new state may still fetch while
+ * a window starts on @crtc. The window registers are latched at the frame
+ * start of their video port: a window switched off in this commit stops
+ * together with the new one only on the same video port, and one switched
+ * off in an earlier commit once that commit is done.
+ */
+static int vop2_win_may_fetch(struct drm_atomic_commit *state,
+ struct vop2_win *win, const struct drm_crtc *crtc,
+ bool *fetching)
+{
+ const struct drm_plane_state *old = drm_atomic_get_old_plane_state(state, &win->base);
+ struct drm_crtc_commit *commit;
+ int ret;
+
+ if (old) {
+ *fetching = old->crtc && old->crtc != crtc;
+ return 0;
+ }
+
+ ret = drm_modeset_lock(&win->base.mutex, state->acquire_ctx);
+ if (ret)
+ return ret;
+
+ commit = win->base.state->commit;
+ *fetching = commit && !try_wait_for_completion(&commit->flip_done);
+
+ return 0;
+}
+
+static u8 vop2_axi_pick_bus(u8 def, const u64 *bus_load, const bool *clash, u64 load)
+{
+ if (!clash[def ^ 1] &&
+ (clash[def] || max(bus_load[def ^ 1] + load, bus_load[def]) <
+ max(bus_load[def] + load, bus_load[def ^ 1])))
+ return def ^ 1;
+
+ return def;
+}
+
+static int vop2_axi_check(struct vop2 *vop2, struct drm_atomic_commit *state)
+{
+ unsigned int n = vop2->registered_num_wins;
+ const struct vop2_axi_state *old;
+ struct drm_private_state *priv;
+ struct vop2_axi_state *new;
+ unsigned long start = 0;
+ bool changed = false;
+ unsigned int i, j;
+ int ret;
+
+ if (!(vop2->data->feature & VOP2_FEATURE_AXI_BALANCE))
+ return 0;
+
+ for (i = 0; i < n; i++)
+ if (drm_atomic_get_new_plane_state(state, &vop2->win[i].base))
+ changed = true;
+
+ if (!changed)
+ return 0;
+
+ priv = drm_atomic_get_private_obj_state(state, &vop2->axi_obj);
+ if (IS_ERR(priv))
+ return PTR_ERR(priv);
+
+ new = to_vop2_axi_state(priv);
+ old = to_vop2_axi_state(drm_atomic_get_old_private_obj_state(state,
+ &vop2->axi_obj));
+
+ for (i = 0; i < n; i++) {
+ const struct drm_plane_state *pstate;
+ const struct drm_crtc_state *cstate;
+
+ new->win[i] = old->win[i];
+
+ pstate = drm_atomic_get_new_plane_state(state, &vop2->win[i].base);
+ if (!pstate)
+ continue;
+
+ cstate = vop2_plane_fetch_crtc_state(state, pstate);
+ new->win[i].load = cstate ? vop2_plane_axi_load(pstate, &cstate->adjusted_mode) : 0;
+ new->win[i].active = cstate && old->win[i].active;
+ if (cstate && !old->win[i].active)
+ start |= BIT(i);
+ }
+
+ while (start) {
+ u64 bus_load[VOP2_SYS_AXI_BUS_NUM] = { };
+ bool clash[VOP2_SYS_AXI_BUS_NUM] = { };
+ const struct drm_plane_state *pstate;
+ struct vop2_win *win;
+ unsigned int k = n;
+ u8 bus;
+
+ for_each_set_bit(j, &start, n)
+ if (k == n || new->win[j].load > new->win[k].load)
+ k = j;
+
+ win = &vop2->win[k];
+ pstate = drm_atomic_get_new_plane_state(state, &win->base);
+
+ for (j = 0; j < n; j++) {
+ struct vop2_win *w = &vop2->win[j];
+ bool fetching = new->win[j].active;
+
+ if (j == k || (start & BIT(j)))
+ continue;
+
+ if (fetching)
+ bus_load[new->win[j].bus] += new->win[j].load;
+
+ if (!vop2_win_axi_ids_clash(win, w))
+ continue;
+
+ if (!fetching) {
+ ret = vop2_win_may_fetch(state, w, pstate->crtc, &fetching);
+ if (ret)
+ return ret;
+ }
+
+ if (fetching)
+ clash[new->win[j].bus] = true;
+ }
+
+ bus = vop2_axi_pick_bus(win->data->axi_bus_id, bus_load, clash,
+ new->win[k].load);
+ if (clash[bus]) {
+ drm_dbg_kms(vop2->drm, "%s: no AXI port with free read IDs\n",
+ win->data->name);
+ return -EINVAL;
+ }
+
+ new->win[k].bus = bus;
+ new->win[k].active = true;
+ start &= ~BIT(k);
+ }
+
+ /* The plane update programs the port from the plane state */
+ for (i = 0; i < n; i++) {
+ struct drm_plane_state *pstate;
+
+ pstate = drm_atomic_get_new_plane_state(state, &vop2->win[i].base);
+ if (pstate)
+ to_vop2_plane_state(pstate)->axi_bus = new->win[i].bus;
+ }
+
+ return 0;
+}
+
+static struct drm_plane_state *vop2_plane_create_state(struct drm_plane *plane)
+{
+ struct vop2_plane_state *vpstate;
+
+ vpstate = kzalloc_obj(*vpstate);
+ if (!vpstate)
+ return ERR_PTR(-ENOMEM);
+
+ __drm_atomic_helper_plane_state_init(&vpstate->base, plane);
+ vpstate->axi_bus = to_vop2_win(plane)->data->axi_bus_id;
+
+ return &vpstate->base;
+}
+
+static struct drm_plane_state *vop2_plane_duplicate_state(struct drm_plane *plane)
+{
+ struct vop2_plane_state *vpstate;
+
+ if (WARN_ON(!plane->state))
+ return NULL;
+
+ vpstate = kmemdup(to_vop2_plane_state(plane->state), sizeof(*vpstate),
+ GFP_KERNEL);
+ if (!vpstate)
+ return NULL;
+
+ __drm_atomic_helper_plane_duplicate_state(plane, &vpstate->base);
+
+ return &vpstate->base;
+}
+
static void vop2_plane_atomic_disable(struct drm_plane *plane,
struct drm_atomic_commit *state)
{
@@ -1418,7 +1727,8 @@ static void vop2_plane_atomic_update(struct drm_plane *plane,
afbc_en ? "AFBC" : "", &yrgb_mst);
if (vop2->version > VOP_VERSION_RK3568) {
- vop2_win_write(win, VOP2_WIN_AXI_BUS_ID, win->data->axi_bus_id);
+ vop2_win_write(win, VOP2_WIN_AXI_BUS_ID,
+ to_vop2_plane_state(pstate)->axi_bus);
vop2_win_write(win, VOP2_WIN_AXI_YRGB_R_ID, win->data->axi_yrgb_r_id);
vop2_win_write(win, VOP2_WIN_AXI_UV_R_ID, win->data->axi_uv_r_id);
}
@@ -1565,8 +1875,8 @@ static const struct drm_plane_funcs vop2_plane_funcs = {
.update_plane = drm_atomic_helper_update_plane,
.disable_plane = drm_atomic_helper_disable_plane,
.destroy = drm_plane_cleanup,
- .atomic_create_state = drm_atomic_helper_plane_create_state,
- .atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state,
+ .atomic_create_state = vop2_plane_create_state,
+ .atomic_duplicate_state = vop2_plane_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_plane_destroy_state,
.format_mod_supported = rockchip_vop2_mod_supported,
};
@@ -2086,7 +2396,7 @@ static int vop2_crtc_atomic_check(struct drm_crtc *crtc,
if (nplanes > vp->nlayers)
return -EINVAL;
- return 0;
+ return vop2_axi_check(vp->vop2, state);
}
static void vop2_crtc_atomic_begin(struct drm_crtc *crtc,
@@ -3016,6 +3326,13 @@ static int vop2_bind(struct device *dev, struct device *master, void *data)
if (ret)
return ret;
+ if (vop2_data->feature & VOP2_FEATURE_AXI_BALANCE) {
+ ret = drm_atomic_private_obj_init(drm, &vop2->axi_obj,
+ &vop2_axi_state_funcs);
+ if (ret)
+ goto err_crtcs;
+ }
+
if (vop2->version >= VOP_VERSION_RK3576) {
struct drm_crtc *crtc;
@@ -3060,6 +3377,8 @@ static int vop2_bind(struct device *dev, struct device *master, void *data)
return 0;
err_crtcs:
+ if (vop2->axi_obj.state)
+ drm_atomic_private_obj_fini(&vop2->axi_obj);
vop2_destroy_crtcs(vop2);
return ret;
@@ -3074,6 +3393,9 @@ static void vop2_unbind(struct device *dev, struct device *master, void *data)
if (vop2->rgb)
rockchip_rgb_fini(vop2->rgb);
+ if (vop2->axi_obj.state)
+ drm_atomic_private_obj_fini(&vop2->axi_obj);
+
vop2_destroy_crtcs(vop2);
}
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
index 1c2d7d97a6c2..48a852fec2bf 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
@@ -8,6 +8,7 @@
#define _ROCKCHIP_DRM_VOP2_H
#include <linux/regmap.h>
+#include <drm/drm_atomic.h>
#include <drm/drm_modes.h>
#include <dt-bindings/soc/rockchip,vop2.h>
#include "rockchip_drm_drv.h"
@@ -29,6 +30,7 @@
#define VOP2_FEATURE_HAS_VO1_GRF BIT(2)
#define VOP2_FEATURE_HAS_VOP_GRF BIT(3)
#define VOP2_FEATURE_HAS_SYS_PMU BIT(4)
+#define VOP2_FEATURE_AXI_BALANCE BIT(5)
#define WIN_FEATURE_AFBDC BIT(0)
#define WIN_FEATURE_CLUSTER BIT(1)
@@ -221,6 +223,40 @@ struct vop2_win {
enum drm_plane_type type;
};
+/**
+ * struct vop2_axi_win - AXI read port use of a window
+ * @load: estimated fetch rate in bytes/s, 0 if the window doesn't fetch
+ * @bus: AXI read port the window fetches through, or last fetched through
+ * @active: the window fetches
+ */
+struct vop2_axi_win {
+ u64 load;
+ u8 bus;
+ bool active;
+};
+
+/**
+ * struct vop2_axi_state - AXI read port assignment of all windows
+ * @base: private object state
+ * @win: one entry per window, indexed like &vop2.win
+ */
+struct vop2_axi_state {
+ struct drm_private_state base;
+ struct vop2_axi_win win[];
+};
+
+/**
+ * struct vop2_plane_state - plane state of a window
+ * @base: DRM plane state
+ * @axi_bus: AXI read port the window fetches through, the default one of
+ * the window unless the atomic check assigned one
+ * (VOP2_FEATURE_AXI_BALANCE)
+ */
+struct vop2_plane_state {
+ struct drm_plane_state base;
+ u8 axi_bus;
+};
+
struct vop2_video_port_data {
unsigned int id;
u32 feature;
@@ -356,6 +392,12 @@ struct vop2 {
*/
struct mutex ovl_lock;
+ /**
+ * @axi_obj: AXI read port assignment of the windows (&struct
+ * vop2_axi_state), with VOP2_FEATURE_AXI_BALANCE only
+ */
+ struct drm_private_obj axi_obj;
+
/* must be put at the end of the struct */
struct vop2_win win[];
};
diff --git a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
index 7c48b440cb29..83203d18865b 100644
--- a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
+++ b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
@@ -2577,7 +2577,8 @@ static const struct vop2_data rk3576_vop = {
static const struct vop2_data rk3588_vop = {
.version = VOP_VERSION_RK3588,
.feature = VOP2_FEATURE_HAS_SYS_GRF | VOP2_FEATURE_HAS_VO1_GRF |
- VOP2_FEATURE_HAS_VOP_GRF | VOP2_FEATURE_HAS_SYS_PMU,
+ VOP2_FEATURE_HAS_VOP_GRF | VOP2_FEATURE_HAS_SYS_PMU |
+ VOP2_FEATURE_AXI_BALANCE,
.nr_vps = 4,
.max_input = { 4096, 4320 },
.max_output = { 4096, 4320 },
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v2 6/9] drm/rockchip: vop2: Report POST_BUF_EMPTY at most once per frame
2026-11-12 8:33 [PATCH v2 0/9] drm/rockchip: vop2: RK3588 clocking, sync polarity, AXI port and window fixes Arun Iyengar via B4 Relay
` (4 preceding siblings ...)
2026-11-12 8:33 ` [PATCH v2 5/9] drm/rockchip: vop2: Balance window fetches across both AXI ports " Arun Iyengar via B4 Relay
@ 2026-11-12 8:33 ` Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 7/9] drm/rockchip: vop2: Find the first mixer of a video port with Esmart3 too Arun Iyengar via B4 Relay
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-12 8:33 UTC (permalink / raw)
To: Sandy Huang, Heiko Stübner, Andy Yan, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Sascha Hauer
Cc: Piotr Zalewski, Chaoyi Chen, dri-devel, linux-rockchip,
linux-arm-kernel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
POST_BUF_EMPTY is raised for every line on which the post processing
stage of a video port finds no data. The interrupt handler only
rate-limits the error message and leaves the interrupt enabled, so an
underflowing video port keeps the CPU in the interrupt handler 50000 to
300000 times per second, for as long as the configuration that causes
the underflow stays in place (e.g. 120000 interrupts per second for
3840x2160@120 with two full screen layers on one AXI port).
Mask POST_BUF_EMPTY when it fires and unmask it at the next frame start.
The frame start (FS) interrupt is enabled for that frame even when
vblank reporting is off, and disabled again once POST_BUF_EMPTY is
re-armed or the video port is switched off. VP_INT_EN is write-masked,
so masking POST_BUF_EMPTY leaves FS alone; FS only needs enabling here
while vblank reporting has it off. A new per video port lock
serialises the interrupt enable bookkeeping between the interrupt
handler and the vblank callbacks. The error is still reported, now at
most once per frame and video port.
Fixes: 604be85547ce ("drm/rockchip: Add VOP2 driver")
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 91 ++++++++++++++++++++++++++--
drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 10 +++
2 files changed, 97 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
index 684fa46924e7..c59e270739b6 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
@@ -842,6 +842,70 @@ static void vop2_crtc_disable_irq(struct vop2_video_port *vp, u32 irq)
vop2_writel(vop2, RK3568_VP_INT_EN(vp->id), irq << 16);
}
+/*
+ * POST_BUF_EMPTY is raised for every line the post processing stage finds
+ * no data for, so an underflowing video port can raise it 100000 times per
+ * second. Report it at most once per frame: mask it when it fires and
+ * unmask it at the next frame start, using the FS interrupt even when
+ * vblank reporting is off.
+ */
+/* Forget a pending re-arm, called with vp->irq_lock held */
+static void vop2_crtc_unmask_post_buf_empty(struct vop2_video_port *vp)
+{
+ if (vp->post_buf_empty_masked && !vp->vblank_enabled)
+ vop2_crtc_disable_irq(vp, VP_INT_FS_FIELD);
+ vp->post_buf_empty_masked = false;
+}
+
+static void vop2_crtc_enable_post_buf_empty_irq(struct vop2_video_port *vp)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&vp->irq_lock, flags);
+ vop2_crtc_unmask_post_buf_empty(vp);
+ vop2_crtc_enable_irq(vp, VP_INT_POST_BUF_EMPTY);
+ spin_unlock_irqrestore(&vp->irq_lock, flags);
+}
+
+static void vop2_crtc_disable_post_buf_empty_irq(struct vop2_video_port *vp)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&vp->irq_lock, flags);
+ vop2_crtc_disable_irq(vp, VP_INT_POST_BUF_EMPTY);
+ vop2_crtc_unmask_post_buf_empty(vp);
+ spin_unlock_irqrestore(&vp->irq_lock, flags);
+}
+
+static void vop2_crtc_handle_post_buf_empty(struct vop2_video_port *vp)
+{
+ spin_lock(&vp->irq_lock);
+ vop2_crtc_disable_irq(vp, VP_INT_POST_BUF_EMPTY);
+ vp->post_buf_empty_masked = true;
+ /*
+ * POST_BUF_EMPTY is unmasked again by the FS interrupt at the next
+ * frame start, so FS must be enabled. With vblank reporting on it
+ * is already, and a pending FS may carry a vblank event. Otherwise
+ * enable it here, clearing the FS status of the current frame so
+ * that FS fires at the next frame start only.
+ */
+ if (!vp->vblank_enabled)
+ vop2_crtc_enable_irq(vp, VP_INT_FS_FIELD);
+ spin_unlock(&vp->irq_lock);
+}
+
+static void vop2_crtc_rearm_post_buf_empty(struct vop2_video_port *vp)
+{
+ spin_lock(&vp->irq_lock);
+ if (vp->post_buf_empty_masked) {
+ vp->post_buf_empty_masked = false;
+ vop2_crtc_enable_irq(vp, VP_INT_POST_BUF_EMPTY);
+ if (!vp->vblank_enabled)
+ vop2_crtc_disable_irq(vp, VP_INT_FS_FIELD);
+ }
+ spin_unlock(&vp->irq_lock);
+}
+
static int vop2_core_clks_prepare_enable(struct vop2 *vop2)
{
int ret;
@@ -1095,6 +1159,7 @@ static void vop2_crtc_atomic_disable(struct drm_crtc *crtc,
drm_info(vop2->drm, "wait for vp%d dsp_hold timeout\n", vp->id);
vop2_crtc_disable_irq(vp, VP_INT_DSP_HOLD_VALID);
+ vop2_crtc_disable_post_buf_empty_irq(vp);
if (vp->dclk_src)
clk_set_parent(vp->dclk, vp->dclk_src);
@@ -1884,8 +1949,14 @@ static const struct drm_plane_funcs vop2_plane_funcs = {
static int vop2_crtc_enable_vblank(struct drm_crtc *crtc)
{
struct vop2_video_port *vp = to_vop2_video_port(crtc);
+ unsigned long flags;
- vop2_crtc_enable_irq(vp, VP_INT_FS_FIELD);
+ spin_lock_irqsave(&vp->irq_lock, flags);
+ vp->vblank_enabled = true;
+ /* FS is enabled already while POST_BUF_EMPTY waits to be re-armed */
+ if (!vp->post_buf_empty_masked)
+ vop2_crtc_enable_irq(vp, VP_INT_FS_FIELD);
+ spin_unlock_irqrestore(&vp->irq_lock, flags);
return 0;
}
@@ -1893,8 +1964,13 @@ static int vop2_crtc_enable_vblank(struct drm_crtc *crtc)
static void vop2_crtc_disable_vblank(struct drm_crtc *crtc)
{
struct vop2_video_port *vp = to_vop2_video_port(crtc);
+ unsigned long flags;
- vop2_crtc_disable_irq(vp, VP_INT_FS_FIELD);
+ spin_lock_irqsave(&vp->irq_lock, flags);
+ vp->vblank_enabled = false;
+ if (!vp->post_buf_empty_masked)
+ vop2_crtc_disable_irq(vp, VP_INT_FS_FIELD);
+ spin_unlock_irqrestore(&vp->irq_lock, flags);
}
static enum drm_mode_status vop2_crtc_mode_valid(struct drm_crtc *crtc,
@@ -2193,7 +2269,7 @@ static void vop2_crtc_atomic_enable(struct drm_crtc *crtc,
vcstate->yuv_overlay = is_yuv_output(vcstate->bus_format);
- vop2_crtc_enable_irq(vp, VP_INT_POST_BUF_EMPTY);
+ vop2_crtc_enable_post_buf_empty_irq(vp);
polflags = 0;
if (vcstate->bus_flags & DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE)
@@ -2342,6 +2418,7 @@ static void vop2_crtc_atomic_enable(struct drm_crtc *crtc,
return;
err_put_enable_count:
+ vop2_crtc_disable_post_buf_empty_irq(vp);
if (!--vop2->enable_count)
vop2_disable(vop2);
@@ -2751,6 +2828,7 @@ static irqreturn_t rk3576_vp_isr(int irq, void *data)
}
if (irqs & VP_INT_FS_FIELD) {
+ vop2_crtc_rearm_post_buf_empty(vp);
drm_crtc_handle_vblank(crtc);
spin_lock(&crtc->dev->event_lock);
if (vp->event) {
@@ -2768,6 +2846,7 @@ static irqreturn_t rk3576_vp_isr(int irq, void *data)
}
if (irqs & VP_INT_POST_BUF_EMPTY) {
+ vop2_crtc_handle_post_buf_empty(vp);
drm_err_ratelimited(vop2->drm, "POST_BUF_EMPTY irq err at vp%d\n", vp->id);
ret = IRQ_HANDLED;
}
@@ -2807,6 +2886,7 @@ static irqreturn_t vop2_isr(int irq, void *data)
}
if (irqs & VP_INT_FS_FIELD) {
+ vop2_crtc_rearm_post_buf_empty(vp);
drm_crtc_handle_vblank(crtc);
spin_lock(&crtc->dev->event_lock);
if (vp->event) {
@@ -2824,6 +2904,7 @@ static irqreturn_t vop2_isr(int irq, void *data)
}
if (irqs & VP_INT_POST_BUF_EMPTY) {
+ vop2_crtc_handle_post_buf_empty(vp);
drm_err_ratelimited(vop2->drm,
"POST_BUF_EMPTY irq err at vp%d\n",
vp->id);
@@ -3200,7 +3281,7 @@ static int vop2_bind(struct device *dev, struct device *master, void *data)
struct vop2 *vop2;
struct resource *res;
size_t alloc_size;
- int ret;
+ int ret, i;
vop2_data = of_device_get_match_data(dev);
if (!vop2_data)
@@ -3317,6 +3398,8 @@ static int vop2_bind(struct device *dev, struct device *master, void *data)
mutex_init(&vop2->vop2_lock);
mutex_init(&vop2->ovl_lock);
+ for (i = 0; i < ARRAY_SIZE(vop2->vps); i++)
+ spin_lock_init(&vop2->vps[i].irq_lock);
ret = devm_request_irq(dev, vop2->irq, vop2_isr, IRQF_SHARED, dev_name(dev), vop2);
if (ret)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
index 48a852fec2bf..ccbfbdb8d246 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
@@ -297,6 +297,16 @@ struct vop2_video_port {
struct drm_pending_vblank_event *event;
unsigned int nlayers;
+
+ /** @irq_lock: protects @vblank_enabled and @post_buf_empty_masked */
+ spinlock_t irq_lock;
+ /** @vblank_enabled: the FS interrupt is enabled for vblank reporting */
+ bool vblank_enabled;
+ /**
+ * @post_buf_empty_masked: POST_BUF_EMPTY is masked until the next
+ * frame start
+ */
+ bool post_buf_empty_masked;
};
/**
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v2 7/9] drm/rockchip: vop2: Find the first mixer of a video port with Esmart3 too
2026-11-12 8:33 [PATCH v2 0/9] drm/rockchip: vop2: RK3588 clocking, sync polarity, AXI port and window fixes Arun Iyengar via B4 Relay
` (5 preceding siblings ...)
2026-11-12 8:33 ` [PATCH v2 6/9] drm/rockchip: vop2: Report POST_BUF_EMPTY at most once per frame Arun Iyengar via B4 Relay
@ 2026-11-12 8:33 ` Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 8/9] drm/rockchip: vop2: Keep the other LUT_PORT_SEL fields on gamma LUT updates Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 9/9] drm/rockchip: vop2: Fix the AFBC offsets of a source that starts inside a superblock Arun Iyengar via B4 Relay
8 siblings, 0 replies; 10+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-12 8:33 UTC (permalink / raw)
To: Sandy Huang, Heiko Stübner, Andy Yan, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Sascha Hauer
Cc: Piotr Zalewski, Chaoyi Chen, dri-devel, linux-rockchip,
linux-arm-kernel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
vop2_find_start_mixer_id_for_vp() looks for the first layer that holds a
window of the video port, but for_each_set_bit() takes the number of bits
to search, and ROCKCHIP_VOP2_ESMART3 is the highest window id, so Esmart3
is never found. A video port whose bottom layer shows Esmart3 then starts
its alpha setup at the mixer of a higher layer, or, if Esmart3 is its
only window, past all layers, and programs mixers of other video ports.
Add ROCKCHIP_VOP2_MAX_LAYER_ID after the last window id and search up to
it.
Fixes: d3fe9aa49585 ("drm/rockchip: vop2: Use OVL_LAYER_SEL configuration instead of use win_mask calculate used layers")
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 1 +
drivers/gpu/drm/rockchip/rockchip_vop2_reg.c | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
index ccbfbdb8d246..290990632388 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h
@@ -857,6 +857,7 @@ enum vop2_layer_phy_id {
ROCKCHIP_VOP2_CLUSTER3,
ROCKCHIP_VOP2_ESMART2,
ROCKCHIP_VOP2_ESMART3,
+ ROCKCHIP_VOP2_MAX_LAYER_ID,
ROCKCHIP_VOP2_PHY_ID_INVALID = -1,
};
diff --git a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
index 83203d18865b..088c1f3a972f 100644
--- a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
+++ b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c
@@ -1889,7 +1889,7 @@ static int vop2_find_start_mixer_id_for_vp(struct vop2_video_port *vp)
for (i = 0; i < 31; i += 4) {
match = false;
- for_each_set_bit(phys_id, &win_mask, ROCKCHIP_VOP2_ESMART3) {
+ for_each_set_bit(phys_id, &win_mask, ROCKCHIP_VOP2_MAX_LAYER_ID) {
win = vop2_find_win_by_phys_id(vop2, phys_id);
if (win->data->layer_sel_id[vp->id] == ((layer_sel >> i) & 0xf)) {
match = true;
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v2 8/9] drm/rockchip: vop2: Keep the other LUT_PORT_SEL fields on gamma LUT updates
2026-11-12 8:33 [PATCH v2 0/9] drm/rockchip: vop2: RK3588 clocking, sync polarity, AXI port and window fixes Arun Iyengar via B4 Relay
` (6 preceding siblings ...)
2026-11-12 8:33 ` [PATCH v2 7/9] drm/rockchip: vop2: Find the first mixer of a video port with Esmart3 too Arun Iyengar via B4 Relay
@ 2026-11-12 8:33 ` Arun Iyengar via B4 Relay
2026-11-12 8:33 ` [PATCH v2 9/9] drm/rockchip: vop2: Fix the AFBC offsets of a source that starts inside a superblock Arun Iyengar via B4 Relay
8 siblings, 0 replies; 10+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-12 8:33 UTC (permalink / raw)
To: Sandy Huang, Heiko Stübner, Andy Yan, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Sascha Hauer
Cc: Piotr Zalewski, Chaoyi Chen, dri-devel, linux-rockchip,
linux-arm-kernel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
On RK3588 the gamma LUT of a video port is written through a window that
GAMMA_AHB_WRITE_SEL in SYS_CTRL_LUT_PORT_SEL selects. The register also
holds the dclk source selection of video ports 0 and 2 and the enable of
the video port 0/1 merge for modes wider than 4096 pixels, which writing
the whole register clears.
Only update the write select field.
Fixes: 4f537776340d ("rockchip/drm: vop2: add support for gamma LUT")
Reviewed-by: Chaoyi Chen <chaoyi.chen@rock-chips.com>
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
index c59e270739b6..259e30ebb9cb 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
@@ -2015,8 +2015,9 @@ static void vop2_crtc_atomic_set_gamma_seamless(struct vop2 *vop2,
struct vop2_video_port *vp,
struct drm_crtc *crtc)
{
- vop2_writel(vop2, RK3568_LUT_PORT_SEL,
- FIELD_PREP(RK3588_LUT_PORT_SEL__GAMMA_AHB_WRITE_SEL, vp->id));
+ regmap_update_bits(vop2->map, RK3568_LUT_PORT_SEL,
+ RK3588_LUT_PORT_SEL__GAMMA_AHB_WRITE_SEL,
+ FIELD_PREP(RK3588_LUT_PORT_SEL__GAMMA_AHB_WRITE_SEL, vp->id));
vop2_vp_dsp_lut_enable(vp);
vop2_crtc_write_gamma_lut(vop2, crtc);
vop2_vp_dsp_lut_update_enable(vp);
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread* [PATCH v2 9/9] drm/rockchip: vop2: Fix the AFBC offsets of a source that starts inside a superblock
2026-11-12 8:33 [PATCH v2 0/9] drm/rockchip: vop2: RK3588 clocking, sync polarity, AXI port and window fixes Arun Iyengar via B4 Relay
` (7 preceding siblings ...)
2026-11-12 8:33 ` [PATCH v2 8/9] drm/rockchip: vop2: Keep the other LUT_PORT_SEL fields on gamma LUT updates Arun Iyengar via B4 Relay
@ 2026-11-12 8:33 ` Arun Iyengar via B4 Relay
8 siblings, 0 replies; 10+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-12 8:33 UTC (permalink / raw)
To: Sandy Huang, Heiko Stübner, Andy Yan, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
Sascha Hauer
Cc: Piotr Zalewski, Chaoyi Chen, dri-devel, linux-rockchip,
linux-arm-kernel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
The AFBC decoder of a window takes the position of the source in the
framebuffer (AFBC_PIC_OFFSET), the number of superblock columns to
decode (AFBC_TILE_NUM) and the offset of the first pixel inside its
superblock (the transform offset). For a source that doesn't start at
x = 0:
- The tile number only counts the superblocks of the source width, one
short when the source starts inside a superblock and ends inside
another one.
- The transform offset is the whole x position, truncated to the 8 bit
field, instead of the position in the superblock (16 pixels wide, or
32 with AFBC_FORMAT_MOD_BLOCK_SIZE_32x8 on RK3576).
Count the superblock columns from the first one the source touches, and
take the x offset in the superblock, like the vendor kernel does for the
not mirrored cases. The X mirrored and rotated cases are left as they are.
Fixes: 604be85547ce ("drm/rockchip: Add VOP2 driver")
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
index 259e30ebb9cb..0e16b665ccac 100644
--- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
+++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c
@@ -439,7 +439,7 @@ static bool vop2_half_block_enable(struct drm_plane_state *pstate)
}
static u32 vop2_afbc_transform_offset(struct drm_plane_state *pstate,
- bool afbc_half_block_en)
+ bool afbc_half_block_en, u32 block_w)
{
struct drm_rect *src = &pstate->src;
struct drm_framebuffer *fb = pstate->fb;
@@ -487,7 +487,7 @@ static u32 vop2_afbc_transform_offset(struct drm_plane_state *pstate,
ty = act_yoffset;
break;
case DRM_MODE_REFLECT_Y:
- tx = act_xoffset;
+ tx = act_xoffset % block_w;
ty = bottom_crop_line_num - act_yoffset;
break;
case DRM_MODE_ROTATE_90:
@@ -499,7 +499,7 @@ static u32 vop2_afbc_transform_offset(struct drm_plane_state *pstate,
ty = vir_width - width - act_xoffset;
break;
case 0:
- tx = act_xoffset;
+ tx = act_xoffset % block_w;
ty = act_yoffset;
break;
}
@@ -1816,7 +1816,8 @@ static void vop2_plane_atomic_update(struct drm_plane *plane,
if (fb->modifier & AFBC_FORMAT_MOD_YTR)
afbc_format |= (1 << 4);
- afbc_tile_num = ALIGN(src_w, block_w) / block_w;
+ /* Superblock columns the source covers, the first one partly */
+ afbc_tile_num = ALIGN(src_w + (src->x1 >> 16) % block_w, block_w) / block_w;
/*
* AFBC pic_vir_width is count by pixel, this is different
@@ -1867,7 +1868,7 @@ static void vop2_plane_atomic_update(struct drm_plane *plane,
vop2_win_write(win, VOP2_WIN_AFBC_PLD_OFFSET, yrgb_mst);
}
- transform_offset = vop2_afbc_transform_offset(pstate, half_block_en);
+ transform_offset = vop2_afbc_transform_offset(pstate, half_block_en, block_w);
vop2_win_write(win, VOP2_WIN_AFBC_HDR_PTR, yrgb_mst);
vop2_win_write(win, VOP2_WIN_AFBC_PIC_SIZE, act_info);
vop2_win_write(win, VOP2_WIN_TRANSFORM_OFFSET, transform_offset);
--
2.55.0
^ permalink raw reply [flat|nested] 10+ messages in thread