av1: decode frames smaller than the sequence maximum at their real size - #460
Open
yevhen-sychov wants to merge 1 commit into
Open
yevhen-sychov wants to merge 1 commit into
yevhen-sychov wants to merge 1 commit into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 vaapishows exactlythe 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). Hardwareencoders 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:
copyAV1PicParamtold 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.
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
CUVIDAV1PICPARAMSthisdriver 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 theunchanged, surface-sized target, so the existing copy into the surface keeps
working as before.
Two details worth knowing when reviewing:
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).
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 vaapiand compared frame by frameagainst libdav1d:
av1_nvenc)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:
frame_size_override_flag = 1withframe_height_minus_1smaller thanmax_frame_height_minus_1means the stream is affected.Then compare the hardware decode with libdav1d (1080p example):
Without the patch the count stops matching roughly 30 frames after every
keyframe; with it, every frame matches.
🤖 Generated with Claude Code