Skip to content

av1: decode frames smaller than the sequence maximum at their real size - #460

Open
yevhen-sychov wants to merge 1 commit into
elFarto:masterfrom
yevhen-sychov:fix/av1-frame-size-override
Open

yevhen-sychov wants to merge 1 commit into
elFarto:masterfrom
yevhen-sychov:fix/av1-frame-size-override

Conversation

@yevhen-sychov

@yevhen-sychov yevhen-sychov commented Sep 15, 2026

Copy link
Copy Markdown

av1: decode frames smaller than the sequence maximum at their real size

What you see

With some AV1 streams, hardware decode through this driver starts out fine and
then falls apart: a few seconds after each keyframe, moving parts of the picture
turn into broken blocks, and the damage keeps spreading the more things move
until the next keyframe arrives. Static content can look almost normal, which
makes it easy to mistake for a network or encoder problem.

We hit it in Chromium WebRTC loopback and screen sharing with a hardware AV1
encoder, but it is not Chromium-specific: ffmpeg -hwaccel vaapi shows exactly
the same corruption on the same files, while libdav1d and NVIDIA's own Vulkan
Video decoder (same GPU) decode them perfectly.

Which streams are affected

AV1 lets a stream declare a maximum frame size in its sequence header and then
code individual frames smaller than that (frame_size_override_flag). Hardware
encoders do this all the time. NVENC, for example, works in 16-pixel-aligned
blocks, so for 1080p content it declares a 1920x1088 maximum and codes every
frame at 1920x1080 (and 3840x2176 vs 3840x2160 for 4K).

Streams whose frames are coded at the full maximum size (for example most
software-encoded content) are not affected, which is probably why this has
gone unnoticed.

Why it happens

Players correctly create the VA context and surfaces at the sequence maximum.
The driver then assumed every frame was that size as well:

  1. copyAV1PicParam told NVDEC the current frame was the context size
    (1920x1088), and gave every reference frame the size of its surface
    (also 1920x1088), instead of the sizes the frames were really coded at
    (1920x1080). The first frames after a keyframe still come out right, but
    prediction runs against the wrong frame geometry, so small errors appear
    wherever there is motion and compound frame after frame.

  2. Once the right sizes are passed, a second issue shows up: NVDEC does not
    crop a smaller frame out of the decoder output, it stretches it to fill
    the display area the decoder was created with. A 1080-line frame would come
    out scaled to 1088 lines.

The first point was found by recording the exact CUVIDAV1PICPARAMS this
driver and ffmpeg's own NVDEC AV1 hwaccel send for the same stream: the only
fields that differed were the frame and reference sizes.

What the patch changes

  • Real sizes in the picture parameters. The current frame gets its coded
    size; that size is remembered on the surface the frame is decoded into, and
    each reference frame is described with its remembered size instead of its
    surface size. The picture parameters now match ffmpeg's NVDEC hwaccel.

  • Display area follows the frame size. When a frame's size differs from the
    decoder's display area, the display area is changed with
    cuvidReconfigureDecoder. The frame is placed unscaled in the top-left of the
    unchanged, surface-sized target, so the existing copy into the surface keeps
    working as before.

    Two details worth knowing when reviewing:

    • NVDEC refuses a reconfigure before the decoder has decoded anything, so the
      first change is applied right after the first picture is decoded and before
      that picture is handed to the resolve thread (the display area takes effect
      when a frame is mapped, not when it is decoded).
    • If the size changes again later in a stream, frames already queued for
      output are allowed to finish before the display area changes, so they are
      not cropped with the new size.

Streams whose frames are all coded at the maximum size never request a display
area change and take exactly the same path as before.

How it was tested

RTX GPU, NVIDIA driver 610.57.04, direct backend. Each stream was decoded
through this driver with ffmpeg -hwaccel vaapi and compared frame by frame
against libdav1d:

Stream Before After
1920x1080 frames, 1920x1088 sequence maximum 59 / 150 frames bit-exact 150 / 150
3840x2160 frames, 3840x2176 sequence maximum 30 / 90 90 / 90
AV1 frames equal to the sequence maximum (av1_nvenc) 150 / 150 150 / 150
H.264 decode byte-identical to before

No reconfigure failures or CUDA errors were logged. Also confirmed by eye in a
Chromium WebRTC AV1 loopback with heavy motion: before, the picture degraded
into blocks within seconds; after, it stays clean.

Not covered: a stream whose frame size changes in the middle of playback (no
such sample was available). The code path for it is described above.

Reproducing it yourself

You need a stream whose frames are smaller than the sequence maximum. The
simplest way to check an existing file:

ffmpeg -i input.ivf -c:v copy -bsf:v trace_headers -f null - 2>&1 \
  | grep -E "max_frame_height_minus_1|frame_size_override_flag|frame_height_minus_1" | head

frame_size_override_flag = 1 with frame_height_minus_1 smaller than
max_frame_height_minus_1 means the stream is affected.

Then compare the hardware decode with libdav1d (1080p example):

ffmpeg -c:v libdav1d -i input.ivf -f rawvideo -pix_fmt yuv420p dav1d.yuv
LIBVA_DRIVER_NAME=nvidia ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 \
  -hwaccel_output_format vaapi -i input.ivf \
  -vf 'hwdownload,format=nv12' -f rawvideo -pix_fmt yuv420p vaapi.yuv
ffmpeg -f rawvideo -pix_fmt yuv420p -s 1920x1080 -i vaapi.yuv \
       -f rawvideo -pix_fmt yuv420p -s 1920x1080 -i dav1d.yuv \
       -lavfi psnr=stats_file=psnr.txt -f null -
grep -c 'psnr_y:inf' psnr.txt   # frames that match exactly

Without the patch the count stops matching roughly 30 frames after every
keyframe; with it, every frame matches.

🤖 Generated with Claude Code

AV1 lets a stream declare a maximum frame size in its sequence header
and then code individual frames smaller than that (frame_size_override_flag).
Hardware encoders do this routinely: NVENC, for example, declares a
1920x1088 maximum for 1080p content (and 3840x2176 for 2160p) because it
works in 16-pixel-aligned blocks, but codes every frame at 1920x1080.
Players create the VA context and surfaces at the maximum size, which is
correct, but the driver then assumed every frame was that size too.

Two things went wrong because of that:

 * copyAV1PicParam told NVDEC the frame was the context size, and gave
   every reference frame the size of its surface, instead of the size the
   frame and its references were actually coded at. The first frames after
   a keyframe still decode correctly, but prediction is done against the
   wrong frame geometry, so small errors appear wherever there is motion
   and keep compounding until the next keyframe. On screen this looks like
   a picture that starts out fine and turns into an ever-growing smear of
   broken blocks the more things move. It is very visible in Chromium
   WebRTC calls and screen shares that use a hardware AV1 encoder.

 * Once the right sizes are passed, NVDEC does not crop the smaller frame
   out of the surface-sized decoder output: it stretches it to fill the
   display area the decoder was created with, so a 1080-line frame would
   come out scaled to 1088 lines.

The fix:

 * Pass the real coded frame size for the current frame, remember that
   size on the surface it was decoded into, and pass each reference
   frame's remembered size rather than its surface size. With this the
   picture parameters match what ffmpeg's own NVDEC AV1 hwaccel sends for
   the same stream.

 * Keep the decoder's display area in step with the frame size using
   cuvidReconfigureDecoder, placing the frame unscaled at the top-left of
   the unchanged surface-sized target, so the existing copy into the
   surface does not need to change. NVDEC refuses a reconfigure before the
   decoder has decoded anything, so the first one is applied right after
   the first picture is decoded and before it is handed to the resolve
   thread. If the size changes again later, frames that are already queued
   for output are allowed to finish first, since the display area is
   applied when a frame is mapped.

Streams whose frames are all coded at the maximum size never request a
display area change, so they take exactly the same path as before.

Tested on an RTX GPU with driver 610.57.04 by decoding through this
driver with ffmpeg (-hwaccel vaapi) and comparing frame by frame against
libdav1d:

 * 1920x1080 frames in a 1920x1088 sequence: before, 59 of 150 frames
   bit-exact (every frame from the 31st after a keyframe was corrupt);
   after, 150 of 150.
 * 3840x2160 frames in a 3840x2176 sequence: before, 30 of 90; after,
   90 of 90.
 * An AV1 stream whose frames match the sequence maximum (ffmpeg
   av1_nvenc): 150 of 150 both before and after.
 * H.264 decode output is byte-identical before and after.

NVIDIA's Vulkan Video AV1 decoder on the same GPU also decodes the
affected streams bit-exactly, which is what pointed at the driver rather
than the hardware.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JyWW73TaBFHr9CoqHoWqdx
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