PR #25833 opened by luoye-cpu
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25833
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25833.patch
The AV1 decoder exports film grain parameters for hardware-accelerated
output as well, but the exported chroma subsampling is derived from the
hardware pixel format and is wrong on those paths.
# Summary of changes
**What happens now.** `set_output_frame()` exports the film grain
parameters of the current frame as `AV_FRAME_DATA_FILM_GRAIN_PARAMS` side
data whenever `AV_CODEC_EXPORT_DATA_FILM_GRAIN` is set. This runs for
hardware-accelerated output too: `av1_receive_frame_internal()` calls
`set_output_frame()` after the hwaccel end-frame callback when the frame
is shown, and the `show_existing_frame` branch calls it directly. The
shared export path is not an accident: it is what
2a8c6d30b9 ("avcodec/av1dec: remove the coding bias from exported
chroma grain params") already relies on, a correction to the shared
exporter that reaches every decode path at once.
`export_film_grain()` reads the chroma subsampling it exports from
`frame->format`. On these paths the frame is a hardware frame, so
`frame->format` is `AV_PIX_FMT_CUDA` (or another hardware format), which
does not describe the underlying chroma layout. The CUDA and CUARRAY
pixfmt descriptors have no chroma dimensions at all — `log2_chroma_w/h`
are zero-initialised — so on a 4:2:0 stream the exported parameters
carry `subsampling_x/y = 0/0` (4:4:4) instead of `1/1`.
The consequence is visible at the consumer. `av_film_grain_params_select()`
matches an AV1 entry against the frame it is attached to by an exact
chroma subsampling and bit depth comparison. For a 4:2:0 stream:
| frame | exported entry (before) | frame descriptor | select() |
|---|---|---|---|
| CUDA frame | subsampling 0/0, bit depth 0 | log2_chroma 0/0, depth 0 | MATCH
(a spurious match of two empty descriptors) |
| downloaded yuv420p frame (the real consumption point, after hwdownload) |
subsampling 0/0, bit depth 0 | log2_chroma 1/1, depth 8 | **NULL — the
parameters are lost** |
Only a 4:4:4 stream happens to escape detection, because its correct
exported values 0/0 coincide with what the CUDA descriptor derives.
**The patch.** `export_film_grain()` reads the descriptor of
`avctx->sw_pix_fmt` — the software format the decoder negotiated —
instead of `frame->format`. On the plain software path both hold the
same value, so only the hardware-accelerated output changes: the
exported subsampling is now the stream's actual subsampling, and after
a `hwdownload` the entry is selected again.
Sequence-header changes are handled: `get_pixel_format()`'s fast path
is only taken when the newly computed software format equals the
current `sw_pix_fmt`, so any actual format change goes through
`ff_get_format()` and updates it.
This is the same field qsvdec reads when exporting the film grain
chroma subsampling (Kilian Hagemann, avcodec/qsvdec: export the film
grain chroma subsampling), which fixed the same contract for the QSV
path; the exported side data contract is shared, the decode paths are
not, so the two patches are complementary rather than duplicates.
## Testing
Verified as follows (MSYS2 mingw64, gcc 16.2.0, nv-codec-headers 13.1,
GeForce RTX 4060 Laptop GPU, CUDA device 0):
- Built with `--enable-nvdec --enable-decoder=av1,av1_nvdec,libdav1d
--enable-hwaccel=av1_nvdec` on the current master tip; the build
compiles and links cleanly.
- A film-grain AV1 stream (320x240, 4:2:0 8-bit, 8 frames dumped), with
a probe that walks the raw `frame->side_data[]` array, dumps every
`AVFilmGrainParams` payload, calls `av_film_grain_params_select()`,
and, on the hardware path, also dumps and selects on a software
`yuv420p` frame obtained by reading the frame's NV12 surface directly
and splitting it to the planar layout. That read is an equivalent
parameter-level test of the soft-frame matching behaviour, not the
real `av_hwframe_transfer_data()` download path (which fails on this
build, as declared under Limitations below):
| build | hw frame: count / subsampling / select | software yuv420p frame (NV12
surface read): count / subsampling / select |
|---|---|---|
| master tip, without the patch | 1 / 0/0 / MATCH (spurious) | 1 / 0/0 /
**NULL** |
| with the patch | 1 / 1/1 / NULL (no chroma layout on a CUDA descriptor —
expected) | 1 / 1/1 / **MATCH** |
| libdav1d, same stream | 1 / 1/1 / MATCH | — |
The exported 1/1 matches the stream's ground truth, and matches
libdav1d frame by frame on the subsampling fields for the same
stream.
- `tools/patcheck` reports no issues (no changelog entry, as for any
small change); `git diff --check` is clean; `git mailinfo` parses the
message: single Signed-off-by, no CR characters, no overlong lines.
**Limitations.** The hardware verification covers the 4:2:0 8-bit
stream, at parameter level (side data integrity and the selector), for
the first 8 dumped frames. On this build `av_hwframe_transfer_data()`
fails with `CUDA_ERROR_INVALID_VALUE` on NVDEC output frames
independently of this patch (the NVDEC surface is NV12 while the frame
is described as planar yuv420p, an upstream download-path issue), so
the software-frame numbers above were produced by reading the same
NV12 surface directly and splitting it to yuv420p before re-running
the census and the selector. That surface read is an equivalent
parameter-level test only; the real download path was not exercised.
Other subsampling formats, monochrome, and an image-level synthesis
comparison are not covered.
## API
No new side data type and no new public API. The change is internal to
`libavcodec/av1dec.c`.
Based on master `bcbbfb78b4`.
From 6286310907c84ec661baff3d7d95eeadfae8b77a Mon Sep 17 00:00:00 2001
From: luoye-cpu <[email protected]>
Date: Sat, 7 Nov 2026 18:53:17 +0800
Subject: [PATCH] avcodec/av1dec: use sw_pix_fmt for film grain subsampling
The AV1 decoder exports film grain parameters from
set_output_frame() for hardware-accelerated output as well.
On these paths, frame->format is a hardware pixel format
and does not describe the underlying chroma layout. The
CUDA and CUARRAY descriptors have no chroma dimensions at
all, so the exported subsampling was 0/0 regardless of the
stream, and av_film_grain_params_select(), which requires
an exact chroma subsampling match for AV1, never matched
the exported entry on a 4:2:0 stream.
Use avctx->sw_pix_fmt when exporting the chroma
subsampling parameters. On the plain software path
sw_pix_fmt holds the same value as frame->format, so only
the hardware-accelerated output changes behaviour.
Signed-off-by: luoye-cpu <[email protected]>
---
libavcodec/av1dec.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/libavcodec/av1dec.c b/libavcodec/av1dec.c
index 315828e18a4..e6512cd004d 100644
--- a/libavcodec/av1dec.c
+++ b/libavcodec/av1dec.c
@@ -1072,7 +1072,9 @@ static int export_film_grain(AVCodecContext *avctx,
AVFrame *frame)
{
AV1DecContext *s = avctx->priv_data;
const AV1RawFilmGrainParams *film_grain = &s->cur_frame.film_grain;
- const AVPixFmtDescriptor *pixdesc = av_pix_fmt_desc_get(frame->format);
+ /* Use the negotiated software format for chroma subsampling,
+ * since frame->format may be a hardware pixel format. */
+ const AVPixFmtDescriptor *pixdesc = av_pix_fmt_desc_get(avctx->sw_pix_fmt);
AVFilmGrainParams *fgp;
AVFilmGrainAOMParams *aom;
--
2.54.0
_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]