Skip to content

drm/vc4: Fix plane scaling edge cases that corrupt other planes - #7702

Open
popcornmix wants to merge 18 commits into
raspberrypi:rpi-6.18.yfrom
popcornmix:issue7701
Open

popcornmix wants to merge 18 commits into
raspberrypi:rpi-6.18.yfrom
popcornmix:issue7701

Conversation

@popcornmix

@popcornmix popcornmix commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Started as a fix for #7701 (a 1x1 source scaled up corrupts other planes), then widened after a writeback-based test of every plane format across ~950 scaling cases each (tiny sources, around the TPZ/PPF switch, subsampled formats where luma and chroma scale differently, large up/downscales, odd and fractional source positions), with a second plane placed so that its LBM follows the plane under test.

Fixes, by symptom:

  • Other planes corrupted (LBM overrun): 1x1 RGB source upscaled (vc4: scaling a 1-pixel plane up on the HVS corrupts neighboring planes #7701); tiny or odd-width YUV sources; fractional source position or size; YUV444 at any scale on BCM2712.
  • Plane drawn wrong: TPZ upscale of YUV, or of a fractional source position, showed the wrong colour; YUV vertical downscales over 3x were shifted and miscoloured; a fractional source at 1:1 drew outside its CRTC rectangle; unscaled YUV444 on BCM2835/BCM2711 drew at double width.
  • Register field overflows (VC4_SET_FIELD() warnings, garbage output): horizontal TPZ downscales over 32x, and large vertical line skips, are now capped or rejected; YUV444 over 4095 pixels wide is rejected on BCM2711.
  • Fractional sources and reflection: a pixel beyond a fractional source rectangle was fetched and filtered into the right/bottom edge; with REFLECT_Y a fractional source started one line too high. Both now render exactly.
  • Legacy async page flips dropped the plane's source crop (and only half-handled reflection), showing the new fb from its origin.
  • BCM2835: chroma was misplaced by ~3 source pixels at a horizontal PPF downscale of exactly 1.5.
  • YUV sources smaller than one chroma sample (1 pixel wide or, for 4:2:0, 1 line tall) are rejected: they render the wrong colour everywhere and corrupt other planes on BCM2712.
  • BCM2712 no longer advertises T-tiled or SAND64 modifiers, which it can't scan out.

Results, cases with corruption / wrong interior / drawn outside the rectangle:

before after
Pi 5 (D0), 49 formats, 46,550 cases (before = with the #7701/#7700 fixes only) 2,010 / 1,958 / 138 0 / 0 / 0
Pi 4, 43 formats 227 / 1,280 / 223 0 / 0 / 0
Pi 3, 10 formats 116 / 382 / 77 0 / 137 small (≤14 levels, test tolerance) / 0

Final passes on the complete series, adding X/Y reflection (35,574 cases on Pi 5 and Pi 4, 7,260 on Pi 3) and seeded random size/offset/fraction/reflection combinations (98,000 on Pi 5, 49,000 on Pi 4, 3,000 on Pi 3): no other-plane corruption, nothing drawn outside the CRTC rectangle, no wrong interiors beyond a few levels of rounding on Pi 3, no kernel warnings. The last two commits (async page flip, small YUV rejection) came after those passes and were checked directly: an async page flip of a cropped, reflected primary plane now scans out the right crop (writeback capture, Pi 5 and Pi 3).

No kernel warnings with the fixes (the unmodified kernel warns on every overflowing case). The full runs were on a 7.3 kernel. This 6.18 branch was built and given quick plain and reflected runs plus the async test on Pi 5 (D0), clean.

Still open: YUV444 is scanned out as 3-plane 4:2:2 with the width doubled, so the luma fetch runs past the source and the last source pixel at the right edge blends with whatever follows it (up to ~36 levels on large upscales). There is no 4:4:4 3-plane mode to avoid that.

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: raspberrypi#7701
Signed-off-by: Dom Cobley <popcornmix@gmail.com>
@popcornmix

Copy link
Copy Markdown
Collaborator Author

Also tested on Pi 5 (C1) with a 6.18.57 kernel: before the fix, the neighbouring plane had 460,160 corrupt pixels; after, none.

@popcornmix

Copy link
Copy Markdown
Collaborator Author

We have some additional YUV bugs, which I'm just narrowing in on.

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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
__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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
@popcornmix popcornmix changed the title drm/vc4: Size the vc6 LBM for H-TPZ from the destination width drm/vc4: Fix plane scaling edge cases that corrupt other planes Oct 21, 2026
@popcornmix
popcornmix marked this pull request as draft October 21, 2026 20:07
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
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 <popcornmix@gmail.com>
@popcornmix

Copy link
Copy Markdown
Collaborator Author

vc4 plane format/scaling test

What it does: It scans out a test plane (A) through the writeback connector and checks every pixel of the result. A has four coloured quadrants inside a guard colour, and is scaled to its destination rectangle. A second scaled plane (B) is placed so that its line buffer follows A's. Each case reports:

  • in: wrong pixels inside A, away from edges and colour boundaries;
  • edge: wrong pixels near A's edges;
  • out: pixels drawn outside A's destination rectangle;
  • B: corruption of plane B, which shows A overran its line buffer.

Expected colours come from the same format at 1:1, so the YUV matrix doesn't matter.

suite.py runs every supported format (RGB, YUV planar and semi-planar, 10/12/16-bit, SAND128/256, P030) through about 950 cases each. They cover 1×1 and tiny sources, the TPZ/PPF switch-over, subsampled formats where luma and chroma use different scalers, very large up- and downscales, and odd or fractional source offsets. It can also run X/Y reflection and seeded random cases.

How to run (on the Pi console with no desktop or Kodi running, since it needs DRM master):
sudo apt install libdrm-dev
gcc -O2 -o hvsfmt hvsfmt.c -I/usr/include/libdrm -ldrm -lm
python3 suite.py --out results.tsv # ~40 min on Pi 5; --quick for ~2 min
python3 analyse.py results.tsv # failures grouped by kind, format, scaler
Options for suite.py:

  • --formats NV12,YUV420 to restrict formats;
  • --rot for reflection cases;
  • --random N --seed S for random cases.

To run a single case:
printf "ref NV12 linear\nNV12 linear 2 2 600 720 0 0 0 0 1\n" | sudo ./hvsfmt
The fields are format, modifier, source w/h, destination w/h, x/y offset, x/y fraction (1/65536), dump capture.

asynctest.c checks that a legacy async page flip keeps a cropped and reflected plane's source offset (sudo ./asynctest 0..3).

vc4-plane-test.zip

@popcornmix
popcornmix marked this pull request as ready for review October 22, 2026 13:19
@popcornmix

Copy link
Copy Markdown
Collaborator Author

@6by9 (sorry!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant