PR #24977 opened by Kilian (lusoris)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24977
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24977.patch

# Summary of changes

hevc_vaapi produces corrupt streams when ROI side data is used with `-rc_mode 
CQP`. This PR enables `cu_qp_delta` in the PPS for that case.

**Why.** In CQP mode `vaapi_encode_h265.c` writes the PPS with 
`cu_qp_delta_enabled_flag = 0` (line 273, and the feature query at 327-329 is 
skipped for CQP). However, `vaapi_encode.c:2046-2050` still allows ROI in CQP 
mode, and the ROI buffer is sent every frame. A driver that applies the ROI 
quantises those blocks at a different QP that the stream cannot signal. The 
decoder then uses the slice QP everywhere, and the error spreads through 
prediction to the whole picture. iHD also reports `cu_qp_delta` as 
`VA_FEATURE_REQUIRED` on `VAEntrypointEncSliceLP`, which FFmpeg ignores in CQP 
mode.

**What changes.** In CQP mode, `cu_qp_delta` is now enabled when ROI is 
allowed, or when the driver reports the feature as required. Non-CQP modes are 
unchanged.

**How it was verified.** Tested on an Arc A380 (DG2, iHD 26.3.5, low-power 
entrypoint) with 60 frames at 640x360, `-rc_mode CQP -qp 30`, and 
`addroi=x=0:y=0:w=iw:h=ih/2:qoffset=-0.15`:

| | bytes | PSNR-Y | ROI half | other half |
| --- | ---: | ---: | ---: | ---: |
| master, no ROI | 81459 | 41.02 | 52.51 | 38.17 |
| master, ROI | 84077 | 13.81 | 24.77 | 10.98 |
| this PR, no ROI | 81577 | 41.02 | 52.51 | 38.17 |
| this PR, ROI | 85443 | 41.12 | 54.83 | 38.20 |

- The same result holds for qoffset −0.3, −0.1 and +0.1, for a full-frame ROI 
and for a 64x64 ROI.
- The libavcodec HEVC decoder and libde265 give identical PSNR on all four 
streams.
- h264_vaapi with the same ROI was already correct.
- `make fate-source` passes. FATE has no VA-API encode coverage.

**For the reviewer.**

- Without ROI side data, CQP streams on drivers that advertise ROI now carry 
`cu_qp_delta` syntax. That cost 0.14% here, and the decoded pictures are 
bit-identical to master.
- The `VA_FEATURE_REQUIRED` clause can be dropped if you prefer a ROI-only 
change. On iHD both conditions hold.

Reproducer:

```sh
ffmpeg -f lavfi -i 
"gradients=size=640x180:x0=0:y0=0:x1=0:y1=180:c0=0x060608:c1=0x2a3038:nb_colors=2:rate=25:seed=1"
 \
  -f lavfi -i "mandelbrot=size=640x180:rate=25" \
  -filter_complex "[0:v][1:v]vstack,format=yuv420p" -frames:v 60 -f rawvideo 
src.yuv
ffmpeg -init_hw_device vaapi=va:/dev/dri/renderD128 -filter_hw_device va \
  -f rawvideo -pix_fmt yuv420p -s 640x360 -framerate 25 -i src.yuv \
  -vf "format=nv12,addroi=x=0:y=0:w=iw:h=ih/2:qoffset=-0.15,hwupload" \
  -c:v hevc_vaapi -rc_mode CQP -qp 30 roi.hevc
ffmpeg -i roi.hevc -f rawvideo -pix_fmt yuv420p -s 640x360 -i src.yuv -lavfi 
psnr -f null -
```



From 4cd3a47efd3ec4b6a4b5a13615311aebd4209723 Mon Sep 17 00:00:00 2001
From: lusoris <[email protected]>
Date: Fri, 9 Oct 2026 19:47:34 +0200
Subject: [PATCH] avcodec/vaapi_encode_h265: enable cu_qp_delta for ROI in CQP
 mode

In constant-QP mode the PPS is written with cu_qp_delta_enabled_flag
set to 0, but ROI side data is still passed to the driver: ROI is
allowed in CQP mode even without roi_rc_qp_delta_support. A driver that
applies the ROI changes the QP of the affected CUs, and the stream has
no syntax to signal that change, so a decoder reconstructs every CU
with the slice QP. The mismatch then spreads through prediction to the
rest of the picture.

On an Arc A380 (DG2, iHD 26.3.5, VAEntrypointEncSliceLP) with
-rc_mode CQP -qp 30 and one addroi region (top half, qoffset -0.15),
PSNR-Y of the decoded stream drops from 41.02 dB without ROI to
13.81 dB with it, and the half outside the ROI is damaged too. The
same ROI on h264_vaapi is fine, since H.264 always carries mb_qp_delta.

Enable cu_qp_delta in CQP mode when ROI is allowed. Also enable it when
the driver reports the feature as VA_FEATURE_REQUIRED, which iHD does
for HEVC on VAEntrypointEncSliceLP.

With this change the same command gives 41.12 dB, with the ROI half at
54.83 dB against 52.51 dB without ROI. Without ROI the decoded frames
are bit-identical to before and the stream grows by 0.14% (81459 to
81577 bytes for 60 frames at 640x360).

Signed-off-by: lusoris <[email protected]>
---
 libavcodec/vaapi_encode_h265.c | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/libavcodec/vaapi_encode_h265.c b/libavcodec/vaapi_encode_h265.c
index baf0d77c8a..1b69b6d136 100644
--- a/libavcodec/vaapi_encode_h265.c
+++ b/libavcodec/vaapi_encode_h265.c
@@ -270,7 +270,10 @@ static int 
vaapi_encode_h265_init_sequence_params(AVCodecContext *avctx)
 
     // priv->unit_opts.tier already set
     // priv->unit_opts.fixed_qp_idr already set
-    priv->unit_opts.cu_qp_delta_enabled_flag = (ctx->va_rc_mode != VA_RC_CQP);
+    // ROI in constant-QP mode changes the QP of individual CUs, which the
+    // bitstream can only signal with cu_qp_delta.
+    priv->unit_opts.cu_qp_delta_enabled_flag = ctx->va_rc_mode != VA_RC_CQP ||
+                                               base_ctx->roi_allowed;
     priv->unit_opts.tile_rows = ctx->tile_rows;
     priv->unit_opts.tile_cols = ctx->tile_cols;
     priv->unit_opts.nb_slices = ctx->nb_slices;
@@ -324,7 +327,8 @@ static int 
vaapi_encode_h265_init_sequence_params(AVCodecContext *avctx)
     // update pps setting according to queried result
     if (priv->va_features) {
         VAConfigAttribValEncHEVCFeatures features = { .value = 
priv->va_features };
-        if (ctx->va_rc_mode != VA_RC_CQP)
+        if (pps->cu_qp_delta_enabled_flag ||
+            features.bits.cu_qp_delta == VA_FEATURE_REQUIRED)
             pps->cu_qp_delta_enabled_flag =
                 !!features.bits.cu_qp_delta;
 
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to