From 654c1c7ef77e6f34b188b9ceb2c7171eef2611d5 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 10:43:07 +0000 Subject: [PATCH 01/18] drm/vc4: Size the vc6 LBM for H-TPZ from the destination width A 1 pixel source is scaled with TPZ, which cannot upscale and instead repeats the last pixel out to the destination size. H-TPZ runs before the vertical scaler, so the lines written to the LBM are crtc_w wide. vc4_lbm_channel_size() sized the LBM from min(src_w, crtc_w), so a 1x1 source upscaled to a large rectangle got a single LBM line and overran into the LBM of whichever plane was allocated after it, corrupting both. The vc4/vc5 path already uses crtc_w for H-TPZ. Fixes: https://github.com/raspberrypi/linux/issues/7701 Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index b8cdabf029fa28..cff9310f3ee868 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -842,6 +842,7 @@ static unsigned int vc4_lbm_components(const struct drm_plane_state *state, static unsigned int vc4_lbm_channel_size(const struct drm_plane_state *state, unsigned int channel) { + const struct vc4_plane_state *vc4_state = to_vc4_plane_state(state); const struct drm_format_info *info = state->fb->format; unsigned int components, words, wpc; unsigned int width, lines; @@ -849,9 +850,13 @@ static unsigned int vc4_lbm_channel_size(const struct drm_plane_state *state, /* LBM is meant to use the smaller of source or dest width, but there * is a issue with UV scaling that the size required for the second * channel is based on the source width only. + * H-TPZ runs before the vertical scaler and, when upscaling a 1 pixel + * source, repeats that pixel out to crtc_w, so it needs crtc_w. */ if (info->hsub > 1 && channel == 1) width = state->src_w >> 16; + else if (vc4_state->x_scaling[channel] == VC4_SCALING_TPZ) + width = state->crtc_w; else width = min(state->src_w >> 16, state->crtc_w); width = round_up(width / info->hsub, 4); From a21de0061fe49fdee3ff2b596f5b1bb48563deff Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:44 +0000 Subject: [PATCH 02/18] drm/vc4: Only use the TPZ upscale for RGB at an integer position A source of 1 pixel or less is scaled with TPZ, which can't upscale and instead repeats the last pixel fetched. That goes wrong in two cases: - YUV: a 2 pixel wide NV12 source has a 1 pixel chroma channel, so chroma uses TPZ while luma uses PPF, and a 1 pixel source puts luma on TPZ. Either way the plane is rendered the wrong colour, and the chroma LBM, sized from the source width, is overrun into the next plane. Seen on BCM2835, BCM2711 and BCM2712. - A fractional source position: the mode_set functions fetch an extra pixel at each partial end, so the pixel repeated across the plane is the one beyond the source rectangle. Use PPF for a TPZ upscale in those cases, as larger sources already do. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index cff9310f3ee868..5f2dbe3ed96956 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -650,6 +650,22 @@ static int vc4_plane_setup_clipping_and_scaling(struct drm_plane_state *state) vc4_state->y_scaling[1] = VC4_SCALING_NONE; } + /* + * The 1 pixel TPZ upscale renders YUV wrongly, and with a fractional + * start it repeats the extra pixel fetched beyond the source, so use + * PPF for those. + */ + for (int i = 0; i < 2; i++) { + if (vc4_state->x_scaling[i] == VC4_SCALING_TPZ && + ((u64)vc4_state->crtc_w << 16) > vc4_state->src_w[i] && + (vc4_state->is_yuv || (vc4_state->src_x & 0xffff))) + vc4_state->x_scaling[i] = VC4_SCALING_PPF; + if (vc4_state->y_scaling[i] == VC4_SCALING_TPZ && + ((u64)vc4_state->crtc_h << 16) > vc4_state->src_h[i] && + (vc4_state->is_yuv || (vc4_state->src_y & 0xffff))) + vc4_state->y_scaling[i] = VC4_SCALING_PPF; + } + return 0; } From e362a784897b6518786fee5dad1c20852db189c9 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:51 +0000 Subject: [PATCH 03/18] drm/vc4: Reject a horizontal downscale that overflows the TPZ scale The TPZ scale field is 21 bits of 16.16, so a horizontal downscale of more than 32x doesn't fit. VC4_SET_FIELD() warned and masked it, and the plane was scanned out the wrong colour. Unlike the vertical direction, there is no line skipping to bring the ratio down, so reject it in atomic check. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 5f2dbe3ed96956..53fccb18797f5f 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -666,6 +666,12 @@ static int vc4_plane_setup_clipping_and_scaling(struct drm_plane_state *state) vc4_state->y_scaling[i] = VC4_SCALING_PPF; } + for (int i = 0; i < 2; i++) + if (vc4_state->x_scaling[i] == VC4_SCALING_TPZ && + vc4_state->src_w[i] / vc4_state->crtc_w > + FIELD_MAX(SCALER_TPZ0_SCALE_MASK)) + return -EINVAL; + return 0; } From ef3e223090a398b01e066832386d2e3fca45daf3 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:51 +0000 Subject: [PATCH 04/18] drm/vc4: Don't treat a fractional source as unscaled vc4_get_scaling_mode() returned VC4_SCALING_NONE whenever the integer part of the source size matched the destination, and ignored the source position. With a fractional source position or size, the mode_set functions fetch an extra pixel at each partial end, and without scaling those were all output, so the plane was drawn up to 2 pixels wider and taller than its CRTC rectangle. Only report no scaling for an exact match, and use PPF when the source starts part way into a pixel. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 53fccb18797f5f..8c7ae5ac9efa52 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -328,7 +328,7 @@ static const struct hvs_format *vc4_get_hvs_format(u32 drm_format) static enum vc4_scaling_mode vc4_get_scaling_mode(u32 src, u32 dst) { - if (dst == src >> 16) + if (dst << 16 == src) return VC4_SCALING_NONE; if (src <= (1 << 16)) @@ -614,6 +614,12 @@ static int vc4_plane_setup_clipping_and_scaling(struct drm_plane_state *state) vc4_state->y_scaling[0] = vc4_get_scaling_mode(vc4_state->src_h[0], vc4_state->crtc_h); + /* A fractional start fetches an extra pixel, which needs scaling out */ + if (vc4_state->x_scaling[0] == VC4_SCALING_NONE && (vc4_state->src_x & 0xffff)) + vc4_state->x_scaling[0] = VC4_SCALING_PPF; + if (vc4_state->y_scaling[0] == VC4_SCALING_NONE && (vc4_state->src_y & 0xffff)) + vc4_state->y_scaling[0] = VC4_SCALING_PPF; + vc4_state->is_unity = (vc4_state->x_scaling[0] == VC4_SCALING_NONE && vc4_state->y_scaling[0] == VC4_SCALING_NONE); From 477f4824cae1410af49d17a559706656b4da2d99 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:51 +0000 Subject: [PATCH 05/18] drm/vc4: Size the LBM from the pixels actually fetched The mode_set functions fetch an extra pixel at each partial end of the source, but the LBM was sized from the integer source width. A plane with a fractional source position or size that is vertically scaled overran its LBM into the next plane's, corrupting both. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 8c7ae5ac9efa52..706a4c6ad1210a 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -781,6 +781,18 @@ static void vc4_write_ppf(struct vc4_plane_state *vc4_state, u32 src, u32 dst, VC4_SET_FIELD(phase, SCALER_PPF_IPHASE)); } +/* Source pixels fetched per line: the mode_set functions add one per partial end */ +static u32 vc4_lbm_src_w(const struct vc4_plane_state *vc4_state) +{ + u32 w = vc4_state->src_w[0] >> 16; + + if (vc4_state->src_x & 0xffff) + w++; + if ((vc4_state->src_x + vc4_state->src_w[0]) & 0xffff) + w++; + return w; +} + static u32 __vc4_lbm_size(struct drm_plane_state *state) { struct vc4_plane_state *vc4_state = to_vc4_plane_state(state); @@ -803,7 +815,7 @@ static u32 __vc4_lbm_size(struct drm_plane_state *state) if (vc4_state->x_scaling[0] == VC4_SCALING_TPZ) pix_per_line = vc4_state->crtc_w; else - pix_per_line = vc4_state->src_w[0] >> 16; + pix_per_line = vc4_lbm_src_w(vc4_state); if (!vc4_state->is_yuv) { if (vc4_state->y_scaling[0] == VC4_SCALING_TPZ) @@ -873,7 +885,7 @@ static unsigned int vc4_lbm_channel_size(const struct drm_plane_state *state, const struct vc4_plane_state *vc4_state = to_vc4_plane_state(state); const struct drm_format_info *info = state->fb->format; unsigned int components, words, wpc; - unsigned int width, lines; + unsigned int width, lines, src_w = vc4_lbm_src_w(vc4_state); /* LBM is meant to use the smaller of source or dest width, but there * is a issue with UV scaling that the size required for the second @@ -882,11 +894,11 @@ static unsigned int vc4_lbm_channel_size(const struct drm_plane_state *state, * source, repeats that pixel out to crtc_w, so it needs crtc_w. */ if (info->hsub > 1 && channel == 1) - width = state->src_w >> 16; + width = src_w; else if (vc4_state->x_scaling[channel] == VC4_SCALING_TPZ) width = state->crtc_w; else - width = min(state->src_w >> 16, state->crtc_w); + width = min(src_w, state->crtc_w); width = round_up(width / info->hsub, 4); wpc = vc4_lbm_words_per_component(state, channel); From 87ea4a79814678b6d097f7021b6229df41bb16f1 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:51 +0000 Subject: [PATCH 06/18] drm/vc4: hvs6: Round up the LBM width of subsampled channels vc4_lbm_channel_size() divided the width by the horizontal subsampling, rounding down. A 1 pixel wide YUV source got no LBM at all, and odd widths, such as 33 pixels upscaled, got a line too short, either of which corrupts the next plane's LBM. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 706a4c6ad1210a..c7ff62484f4c28 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -899,7 +899,7 @@ static unsigned int vc4_lbm_channel_size(const struct drm_plane_state *state, width = state->crtc_w; else width = min(src_w, state->crtc_w); - width = round_up(width / info->hsub, 4); + width = round_up(DIV_ROUND_UP(width, info->hsub), 4); wpc = vc4_lbm_words_per_component(state, channel); if (!wpc) From 4903c34f490b0b990251d95a4013b8535a95d83e Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:52 +0000 Subject: [PATCH 07/18] drm/vc4: hvs6: Allocate the LBM for YUV444 chroma __vc6_lbm_size() only took the chroma channel into account when it was horizontally subsampled, so YUV444 got an LBM for luma only: none at all at 1:1, where only chroma is scaled. The width is also programmed doubled for YUV444, and the hardware uses an LBM for that doubled width. Size both channels for all YUV formats, and double the width for YUV444. Without this a YUV444 plane corrupts the plane after it in the LBM even when unscaled. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index c7ff62484f4c28..d06c61c6f2fb5b 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -899,6 +899,9 @@ static unsigned int vc4_lbm_channel_size(const struct drm_plane_state *state, width = state->crtc_w; else width = min(src_w, state->crtc_w); + /* vc6_plane_mode_set() programs double the width for YUV444 */ + if (info->format == DRM_FORMAT_YUV444 || info->format == DRM_FORMAT_YVU444) + width *= 2; width = round_up(DIV_ROUND_UP(width, info->hsub), 4); wpc = vc4_lbm_words_per_component(state, channel); @@ -920,7 +923,7 @@ static unsigned int __vc6_lbm_size(const struct drm_plane_state *state) { const struct drm_format_info *info = state->fb->format; - if (info->hsub > 1) + if (info->is_yuv) return max(vc4_lbm_channel_size(state, 0), vc4_lbm_channel_size(state, 1)); else From 6dda2f2689106bd01ff8acb1d8eca7cf96d44c44 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:52 +0000 Subject: [PATCH 08/18] drm/vc4: Fix unscaled YUV444 on BCM2835 and BCM2711 vc4_plane_mode_set() programs double the width for YUV444 so that all of the chroma is fetched, but when the plane wasn't horizontally scaled the luma was output at that doubled width too, drawing the plane twice as wide as its CRTC rectangle. Enable the luma scaler at 1:1 for YUV444, as vc6_plane_mode_set() already does. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index d06c61c6f2fb5b..919bf5443d17ba 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -1434,6 +1434,15 @@ static int vc4_plane_mode_set(struct drm_plane *plane, if (ret) return ret; + /* The doubled YUV444 width below has to be scaled back for the luma */ + if ((fb->format->format == DRM_FORMAT_YUV444 || + fb->format->format == DRM_FORMAT_YVU444) && + vc4_state->x_scaling[0] == VC4_SCALING_NONE) { + vc4_state->x_scaling[0] = VC4_SCALING_PPF; + vc4_state->is_unity = false; + vc4_state->is_yuv444_unity = true; + } + if (!vc4_state->src_w[0] || !vc4_state->src_h[0] || !vc4_state->crtc_w || !vc4_state->crtc_h) { /* 0 source size probably means the plane is offscreen */ From f4c05a237f18814f86805472704883ba5bb6491c Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:52 +0000 Subject: [PATCH 09/18] drm/vc4: hvs6: Reject YUV sources narrower than a chroma pixel On BCM2712 a YUV source less than one chroma pixel wide that is scaled corrupts both its own output and that of the plane after it in the LBM, however large an LBM it is given. Reject it in atomic check. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 919bf5443d17ba..f640edefca6b43 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -624,6 +624,11 @@ static int vc4_plane_setup_clipping_and_scaling(struct drm_plane_state *state) vc4_state->y_scaling[0] == VC4_SCALING_NONE); if (num_planes > 1) { + /* The HVS6 corrupts other planes if a chroma line is under a pixel */ + if (to_vc4_dev(state->plane->dev)->gen >= VC4_GEN_6_C && + (vc4_state->src_w[0] >> 16) < h_subsample) + return -EINVAL; + vc4_state->is_yuv = true; vc4_state->src_w[1] = vc4_state->src_w[0] / h_subsample; From f9cc84fc790d9b42264a76243d3308390a1d56a0 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:52 +0000 Subject: [PATCH 10/18] drm/vc4: Multiply the chroma pitch by the line skip When skipping lines for a large vertical downscale, the luma pitch was multiplied by vdownsample but the chroma pitch of planar formats was divided by it, so chroma was read from the wrong lines. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index f640edefca6b43..507fc7d2770ac8 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -1837,7 +1837,7 @@ static int vc4_plane_mode_set(struct drm_plane *plane, if (hvs_format != HVS_PIXEL_FORMAT_H264 && hvs_format != HVS_PIXEL_FORMAT_YCBCR_10BIT) vc4_dlist_write(vc4_state, - VC4_SET_FIELD(fb->pitches[i] / + VC4_SET_FIELD(fb->pitches[i] * vc4_state->vdownsample, SCALER_SRC_PITCH)); else From c4920b2e82ce9938d42bf139ecc90235b9548ed6 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:52 +0000 Subject: [PATCH 11/18] drm/vc4: Keep the vertical line skip within the hardware limits A vertical TPZ downscale of more than 3x skips source lines, through the pitch for linear buffers or a 3 bit line skip field for SAND, but the skip wasn't limited to what those hold: - A large linear pitch times the skip overflowed the 16 bit (BCM2835, BCM2711) or 17 bit (BCM2712) pitch field, so the wrong lines were read. On BCM2712 VC4_SET_FIELD() warned about it. - More than 8 overflowed the SAND line skip field. - T-tiled buffers have no line pitch to multiply. - Chroma lines are skipped too, so for YUV a skip that turned the chroma into an upscale left TPZ unable to scale it, and the plane was rendered the wrong colour and shifted. Factor the calculation out of the two mode_set functions, cap the skip to the field and the chroma ratio, and reject a vertical TPZ scale that then overflows the scale field. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 69 +++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 507fc7d2770ac8..132e0b1a2c996a 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -1407,6 +1407,47 @@ static u32 vc4_hvs6_get_alpha_mask_mode(struct drm_plane_state *state) return VC4_SET_FIELD(SCALER6_CTL0_ALPHA_MASK_NONE, SCALER6_CTL0_ALPHA_MASK); } +/* + * Skips source lines for a vertical TPZ downscale of more than x3, to limit + * SDRAM bandwidth. The skip is applied through the pitch or the tile line + * skip field, so it is capped to what those hold, and to keep the chroma + * downscaling. + */ +static int vc4_plane_set_vdownsample(struct drm_plane_state *state, + u32 max_pitch, u32 max_tile_skip) +{ + struct vc4_plane_state *vc4_state = to_vc4_plane_state(state); + struct drm_framebuffer *fb = state->fb; + u64 mod = fourcc_mod_broadcom_mod(fb->modifier); + u32 vds = 1, max; + + if (vc4_state->y_scaling[0] == VC4_SCALING_TPZ && + vc4_state->src_h[0] / vc4_state->crtc_h > (3 << 16)) { + vds = ((vc4_state->src_h[0] / (vc4_state->crtc_h * 3)) >> 16) + 1; + + if (mod == DRM_FORMAT_MOD_BROADCOM_SAND64 || + mod == DRM_FORMAT_MOD_BROADCOM_SAND128 || + mod == DRM_FORMAT_MOD_BROADCOM_SAND256) + max = max_tile_skip; + else if (mod == DRM_FORMAT_MOD_LINEAR) + max = max_pitch / fb->pitches[0]; + else + max = 1; + if (fb->format->num_planes > 1) + max = min(max, (vc4_state->src_h[1] / vc4_state->crtc_h) >> 16); + vds = clamp_t(u32, max, 1, vds); + } + vc4_state->vdownsample = vds; + + for (int i = 0; i < 2; i++) + if (vc4_state->y_scaling[i] == VC4_SCALING_TPZ && + vc4_state->src_h[i] / vds / vc4_state->crtc_h > + FIELD_MAX(SCALER_TPZ0_SCALE_MASK)) + return -EINVAL; + + return 0; +} + /* Writes out a full display list for an active plane to the plane's * private dlist state. */ @@ -1455,16 +1496,10 @@ static int vc4_plane_mode_set(struct drm_plane *plane, return 0; } - if (vc4_state->y_scaling[0] == VC4_SCALING_TPZ && - vc4_state->src_h[0] / vc4_state->crtc_h > (3 << 16)) { - /* Downscaling by more than x3. Reduce the number of lines read - * to avoid exceeding SDRAM bandwidth. - */ - vc4_state->vdownsample = ((vc4_state->src_h[0] / - (vc4_state->crtc_h * 3)) >> 16) + 1; - } else { - vc4_state->vdownsample = 1; - } + ret = vc4_plane_set_vdownsample(state, FIELD_MAX(SCALER_SRC_PITCH_MASK), + FIELD_MAX(SCALER_TILE_SKIP_0_MASK) + 1); + if (ret) + return ret; width = vc4_state->src_w[0] >> 16; height = vc4_state->src_h[0] >> 16; @@ -2055,16 +2090,10 @@ static int vc6_plane_mode_set(struct drm_plane *plane, width = vc4_state->src_w[0] >> 16; height = vc4_state->src_h[0] >> 16; - if (vc4_state->y_scaling[0] == VC4_SCALING_TPZ && - vc4_state->src_h[0] / vc4_state->crtc_h > (3 << 16)) { - /* Downscaling by more than x3. Reduce the number of lines read - * to avoid exceeding SDRAM bandwidth. - */ - vc4_state->vdownsample = ((vc4_state->src_h[0] / - (vc4_state->crtc_h * 3)) >> 16) + 1; - } else { - vc4_state->vdownsample = 1; - } + ret = vc4_plane_set_vdownsample(state, FIELD_MAX(SCALER6_PTR2_PITCH_MASK), + FIELD_MAX(SCALER6_PTR2_TILE_LSKIP_MASK) + 1); + if (ret) + return ret; /* SCL1 is used for Cb/Cr scaling of planar formats. For RGB * and 4:4:4, scl1 should be set to scl0 so both channels of From 944939ad638f96d3ff959d435ffe6e2868bf8bbb Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 16:47:52 +0000 Subject: [PATCH 12/18] drm/vc4: hvs6: Don't advertise T-tiled or SAND64 modifiers vc6_plane_mode_set() only handles linear, SAND128 and SAND256 buffers, and rejects anything else, but the planes advertised T-tiled and SAND64 too. A client using them got a framebuffer that every commit rejected. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 132e0b1a2c996a..bed26f09b3d2dc 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -2792,6 +2792,14 @@ static bool vc4_format_mod_supported(struct drm_plane *plane, uint32_t format, uint64_t modifier) { + struct vc4_dev *vc4 = to_vc4_dev(plane->dev); + + /* vc6_plane_mode_set() only handles linear, SAND128 and SAND256 */ + if (vc4->gen >= VC4_GEN_6_C && + (modifier == DRM_FORMAT_MOD_BROADCOM_VC4_T_TILED || + fourcc_mod_broadcom_mod(modifier) == DRM_FORMAT_MOD_BROADCOM_SAND64)) + return false; + /* Support T_TILING for RGB formats only. */ switch (format) { case DRM_FORMAT_XRGB8888: From 86d64a78720c5953e3fdc7b1977ad571a38b7990 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 17:07:53 +0000 Subject: [PATCH 13/18] drm/vc4: Reject YUV444 sources too wide for the doubled width vc4_plane_mode_set() programs double the source width for YUV444, which for a 4096 pixel wide source overflows the 13 bit POS2 width field on BCM2711 (12 bit on BCM2835). VC4_SET_FIELD() warned and the width was truncated. Reject such planes in atomic check. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index bed26f09b3d2dc..75052418f76be0 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -1738,6 +1738,10 @@ static int vc4_plane_mode_set(struct drm_plane *plane, if (format->drm == DRM_FORMAT_YUV444 || format->drm == DRM_FORMAT_YVU444) width <<= 1; + if (width > (vc4->gen >= VC4_GEN_5 ? FIELD_MAX(SCALER5_POS2_WIDTH_MASK) : + FIELD_MAX(SCALER_POS2_WIDTH_MASK))) + return -EINVAL; + /* Don't waste cycles mixing with plane alpha if the set alpha * is opaque or there is no per-pixel alpha information. * In any case we use the alpha property value as the fixed alpha. From f57675d3e4db79cecea995b679bbf9331e01adef Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 23:26:25 +0000 Subject: [PATCH 14/18] drm/vc4: Don't fetch a pixel beyond a fractional source rectangle The mode_set functions added a pixel to the fetch for each end of the source rectangle that wasn't pixel aligned. When both ends are partial and their fractions add up to less than a pixel, that is one more than the source covers, so the pixel beyond the right or bottom edge was fetched and filtered into the edge of the plane. With a 2x2 source at (16.5, 16.5) scaled to 400x400 the right and bottom edges picked up the colour of the pixels outside the source. Fetch the pixels the source rectangle actually covers, and size the LBM from the same count. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 56 +++++++++------------------------ 1 file changed, 14 insertions(+), 42 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 75052418f76be0..fd546f1d49e970 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -786,16 +786,16 @@ static void vc4_write_ppf(struct vc4_plane_state *vc4_state, u32 src, u32 dst, VC4_SET_FIELD(phase, SCALER_PPF_IPHASE)); } -/* Source pixels fetched per line: the mode_set functions add one per partial end */ -static u32 vc4_lbm_src_w(const struct vc4_plane_state *vc4_state) +/* Number of whole pixels covered by a 16.16 source start and size */ +static u32 vc4_src_span(u32 start, u32 size) { - u32 w = vc4_state->src_w[0] >> 16; + return ((start + size + 0xffff) >> 16) - (start >> 16); +} - if (vc4_state->src_x & 0xffff) - w++; - if ((vc4_state->src_x + vc4_state->src_w[0]) & 0xffff) - w++; - return w; +/* Source pixels fetched per line */ +static u32 vc4_lbm_src_w(const struct vc4_plane_state *vc4_state) +{ + return vc4_src_span(vc4_state->src_x, vc4_state->src_w[0]); } static u32 __vc4_lbm_size(struct drm_plane_state *state) @@ -1714,23 +1714,9 @@ static int vc4_plane_mode_set(struct drm_plane *plane, return -EINVAL; } - /* fetch an extra pixel if we don't actually line up with the left edge. */ - if ((vc4_state->src_x & 0xffff) && vc4_state->src_x < (state->fb->width << 16)) - width++; - - /* same for the right side */ - if (((vc4_state->src_x + vc4_state->src_w[0]) & 0xffff) && - vc4_state->src_x + vc4_state->src_w[0] < (state->fb->width << 16)) - width++; - - /* now for the top */ - if ((vc4_state->src_y & 0xffff) && vc4_state->src_y < (state->fb->height << 16)) - height++; - - /* and the bottom */ - if (((vc4_state->src_y + vc4_state->src_h[0]) & 0xffff) && - vc4_state->src_y + vc4_state->src_h[0] < (state->fb->height << 16)) - height++; + /* Fetch every pixel the source rectangle touches, including partial ones */ + width = vc4_src_span(vc4_state->src_x, vc4_state->src_w[0]); + height = vc4_src_span(vc4_state->src_y, vc4_state->src_h[0]); /* For YUV444 the hardware wants double the width, otherwise it doesn't * fetch full width of chroma @@ -2125,23 +2111,9 @@ static int vc6_plane_mode_set(struct drm_plane *plane, src_x = vc4_state->src_x >> 16; - /* fetch an extra pixel if we don't actually line up with the left edge. */ - if ((vc4_state->src_x & 0xffff) && vc4_state->src_x < (state->fb->width << 16)) - width++; - - /* same for the right side */ - if (((vc4_state->src_x + vc4_state->src_w[0]) & 0xffff) && - vc4_state->src_x + vc4_state->src_w[0] < (state->fb->width << 16)) - width++; - - /* now for the top */ - if ((vc4_state->src_y & 0xffff) && vc4_state->src_y < (state->fb->height << 16)) - height++; - - /* and the bottom */ - if (((vc4_state->src_y + vc4_state->src_h[0]) & 0xffff) && - vc4_state->src_y + vc4_state->src_h[0] < (state->fb->height << 16)) - height++; + /* Fetch every pixel the source rectangle touches, including partial ones */ + width = vc4_src_span(vc4_state->src_x, vc4_state->src_w[0]); + height = vc4_src_span(vc4_state->src_y, vc4_state->src_h[0]); switch (base_format_mod) { case DRM_FORMAT_MOD_LINEAR: From 366dc8e19af96e0f0f2435e7c05f4e330a89a8df Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 21 Oct 2026 23:35:57 +0000 Subject: [PATCH 15/18] drm/vc4: Avoid a chroma PPF downscale of exactly 1.5 on BCM2835 On BCM2835 the chroma of a planar YUV plane is placed about 3 source pixels to the left when its horizontal PPF downscale is exactly 1.5, for example NV12 300 pixels wide scaled to 100, where luma uses TPZ and chroma sits on the 2/3 PPF limit. 1.495 and TPZ at 1.51 are both correct, as is the same scale on BCM2711 and BCM2712, and vertically. Use TPZ for chroma at that limit on BCM2835. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index fd546f1d49e970..3497df285094f4 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -637,6 +637,11 @@ static int vc4_plane_setup_clipping_and_scaling(struct drm_plane_state *state) vc4_state->x_scaling[1] = vc4_get_scaling_mode(vc4_state->src_w[1], vc4_state->crtc_w); + /* The BCM2835 HVS misplaces chroma with a PPF downscale of exactly 1.5 */ + if (to_vc4_dev(state->plane->dev)->gen == VC4_GEN_4 && + vc4_state->x_scaling[1] == VC4_SCALING_PPF && + (u64)vc4_state->src_w[1] * 2 >= ((u64)vc4_state->crtc_w * 3) << 16) + vc4_state->x_scaling[1] = VC4_SCALING_TPZ; vc4_state->y_scaling[1] = vc4_get_scaling_mode(vc4_state->src_h[1], vc4_state->crtc_h); From 15a0d9bbdd80d1cbfd131faa49ad61a39b13a6fa Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Thu, 22 Oct 2026 06:03:02 +0000 Subject: [PATCH 16/18] drm/vc4: Point at the last fetched line for a fractional Y reflection With DRM_MODE_REFLECT_Y the scan starts at the last line of the source and works up, but that line was found from the integer source height, before the extra line fetched for a partial pixel at the top or bottom was added. A vertically reflected plane with a fractional source started one line too high, so its edge showed the line above the source rectangle and lost the last line. Work out the fetched width and height before they are used. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 3497df285094f4..8607ff40db4ed7 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -1506,8 +1506,9 @@ static int vc4_plane_mode_set(struct drm_plane *plane, if (ret) return ret; - width = vc4_state->src_w[0] >> 16; - height = vc4_state->src_h[0] >> 16; + /* Fetch every pixel the source rectangle touches, including partial ones */ + width = vc4_src_span(vc4_state->src_x, vc4_state->src_w[0]); + height = vc4_src_span(vc4_state->src_y, vc4_state->src_h[0]); /* SCL1 is used for Cb/Cr scaling of planar formats. For RGB * and 4:4:4, scl1 should be set to scl0 so both channels of @@ -1719,9 +1720,6 @@ static int vc4_plane_mode_set(struct drm_plane *plane, return -EINVAL; } - /* Fetch every pixel the source rectangle touches, including partial ones */ - width = vc4_src_span(vc4_state->src_x, vc4_state->src_w[0]); - height = vc4_src_span(vc4_state->src_y, vc4_state->src_h[0]); /* For YUV444 the hardware wants double the width, otherwise it doesn't * fetch full width of chroma @@ -2082,8 +2080,9 @@ static int vc6_plane_mode_set(struct drm_plane *plane, return 0; } - width = vc4_state->src_w[0] >> 16; - height = vc4_state->src_h[0] >> 16; + /* Fetch every pixel the source rectangle touches, including partial ones */ + width = vc4_src_span(vc4_state->src_x, vc4_state->src_w[0]); + height = vc4_src_span(vc4_state->src_y, vc4_state->src_h[0]); ret = vc4_plane_set_vdownsample(state, FIELD_MAX(SCALER6_PTR2_PITCH_MASK), FIELD_MAX(SCALER6_PTR2_TILE_LSKIP_MASK) + 1); @@ -2116,9 +2115,6 @@ static int vc6_plane_mode_set(struct drm_plane *plane, src_x = vc4_state->src_x >> 16; - /* Fetch every pixel the source rectangle touches, including partial ones */ - width = vc4_src_span(vc4_state->src_x, vc4_state->src_w[0]); - height = vc4_src_span(vc4_state->src_y, vc4_state->src_h[0]); switch (base_format_mod) { case DRM_FORMAT_MOD_LINEAR: From 040ddb5c931d39abc3c849ff98c8f8c75884b2a4 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Thu, 22 Oct 2026 09:44:00 +0000 Subject: [PATCH 17/18] drm/vc4: Keep the source offset for an async page flip vc4_plane_async_set_fb() pointed the plane at the start of the new framebuffer, adjusted only for a Y reflection, so the plane's source crop was dropped: a primary plane scanning out from (64,48) of its framebuffer showed the new one from (0,0) after a legacy async page flip. Record the offset the mode_set functions computed for the source position, reflection and tiling, and apply it to the new framebuffer. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_drv.h | 2 ++ drivers/gpu/drm/vc4/vc4_plane.c | 16 +++++----------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_drv.h b/drivers/gpu/drm/vc4/vc4_drv.h index 2ed82b3d8c3658..94ef15768f94d2 100644 --- a/drivers/gpu/drm/vc4/vc4_drv.h +++ b/drivers/gpu/drm/vc4/vc4_drv.h @@ -442,6 +442,8 @@ struct vc4_plane_state { u32 pos0_offset; u32 pos2_offset; u32 ptr0_offset[DRM_FORMAT_MAX_PLANES]; + /* Offset of the first pixel scanned out from the start of the fb */ + u32 ptr0_src_offset; u32 lbm_offset; /* Offset where the plane's dlist was last stored in the diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 8607ff40db4ed7..2617c7840528d6 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -1842,6 +1842,7 @@ static int vc4_plane_mode_set(struct drm_plane *plane, * The pointers may be any byte address. */ vc4_state->ptr0_offset[0] = vc4_state->dlist_count; + vc4_state->ptr0_src_offset = offsets[0]; for (i = 0; i < num_planes; i++) { struct drm_gem_dma_object *bo = drm_fb_dma_get_gem_obj(fb, i); @@ -2308,6 +2309,8 @@ static int vc6_plane_mode_set(struct drm_plane *plane, /* Pointer Word 0 */ vc4_state->ptr0_offset[i] = vc4_state->dlist_count; + if (!i) + vc4_state->ptr0_src_offset = offsets[idx]; vc4_dlist_write(vc4_state, (rotation & DRM_MODE_REFLECT_Y ? SCALER6_PTR0_VFLIP : 0) | /* @@ -2541,22 +2544,13 @@ void vc4_plane_async_set_fb(struct drm_plane *plane, struct drm_framebuffer *fb) struct vc4_plane_state *vc4_state = to_vc4_plane_state(state); struct drm_gem_dma_object *bo = drm_fb_dma_get_gem_obj(fb, 0); struct vc4_dev *vc4 = to_vc4_dev(plane->dev); - dma_addr_t dma_addr = bo->dma_addr + fb->offsets[0]; - unsigned int rotation; + dma_addr_t dma_addr = bo->dma_addr + fb->offsets[0] + + vc4_state->ptr0_src_offset; int idx; if (!drm_dev_enter(plane->dev, &idx)) return; - rotation = drm_rotation_simplify(state->rotation, - DRM_MODE_ROTATE_0 | - DRM_MODE_REFLECT_X | - DRM_MODE_REFLECT_Y); - - /* We must point to the last line when Y reflection is enabled. */ - if (rotation & DRM_MODE_REFLECT_Y) - dma_addr += fb->pitches[0] * ((vc4_state->src_h[0] >> 16) - 1); - /* We're skipping the address adjustment for negative origin, * because this is only called on the primary plane. */ From dcadce2061b6003e6af7fb7dd3787e04fdee7068 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Thu, 22 Oct 2026 09:45:53 +0000 Subject: [PATCH 18/18] drm/vc4: Reject YUV sources smaller than a chroma sample A YUV source less than one chroma sample wide or tall is rendered the wrong colour on BCM2835, BCM2711 and BCM2712 (a 4:2:0 source 1 line tall picks up the next chroma line, and 1 pixel wide is wrong throughout). BCM2712 already rejected the 1 pixel wide case, which also corrupts the next plane's LBM. Reject both directions on all generations. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_plane.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_plane.c b/drivers/gpu/drm/vc4/vc4_plane.c index 2617c7840528d6..9c2e76d060aa60 100644 --- a/drivers/gpu/drm/vc4/vc4_plane.c +++ b/drivers/gpu/drm/vc4/vc4_plane.c @@ -624,9 +624,12 @@ static int vc4_plane_setup_clipping_and_scaling(struct drm_plane_state *state) vc4_state->y_scaling[0] == VC4_SCALING_NONE); if (num_planes > 1) { - /* The HVS6 corrupts other planes if a chroma line is under a pixel */ - if (to_vc4_dev(state->plane->dev)->gen >= VC4_GEN_6_C && - (vc4_state->src_w[0] >> 16) < h_subsample) + /* + * A source smaller than one chroma sample is rendered the wrong + * colour, and on BCM2712 one that narrow corrupts other planes. + */ + if ((vc4_state->src_w[0] >> 16) < h_subsample || + (vc4_state->src_h[0] >> 16) < v_subsample) return -EINVAL; vc4_state->is_yuv = true;