PR #25529 opened by Forgejo_Fairy
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25529
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25529.patch

HEVC can return frames decoded before `avcodec_flush_buffers()` because its 
flush callback clears the DPB but leaves the output FIFO populated. This occurs 
during final draining and at sequence boundaries with single-thread decoding, 
and also affects IMM5's inner HEVC decoder.

Drain the FIFO during `hevc_decode_flush()`. Add an API FATE test using the 
existing `hevc-conformance/BUMPING_A_ericsson_1.bit` sample: flush during 
draining, record receive-without-input, restart the input, and compare the 
first output picture with the retained original first picture. Expected results 
are stored in `tests/ref/fate/api-hevc-flush`.

The regression was verified at 4b9adb35b609; its immediate parent 79afc45c03ab 
passes the same reproduction.

Validation on AArch64 software decoding:

- The new API test detects the failure on unmodified master and passes with the 
patch.
- The issue attachment restarts at PTS 0 with the patch, instead of stale PTS 
1967.
- IMM5 wrapping of the same HEVC sample and a midstream closed-GOP case pass 
with the patch.
- One-thread and four-thread checks pass.
- `make -j8 fate-api-hevc-flush fate-hevc SAMPLES=/opt/fate-suite` passes: the 
new API test plus 185 enabled HEVC tests.
- `git diff --check` passes.

Addresses the HEVC/IMM5 portion of #25447. The independent SVQ3, libxevd, and 
JPEG XL flush failures require separate fixes. Hardware decoding was not tested.


>From b4f565d1808c4c5972782db771ddb4737e3c762c Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Tue, 27 Oct 2026 15:38:28 +0000
Subject: [PATCH] avcodec/hevc: discard queued output frames on flush

Clear the output FIFO as well as the DPB when flushing. Otherwise frames queued 
while draining or starting a new sequence can be returned after 
avcodec_flush_buffers(), including through the IMM5 wrapper.

Add an API FATE test that flushes during draining, checks for pending output, 
and compares the first frame after restarting with the original first frame. 
Use an existing HEVC conformance sample.

Fixes: 4b9adb35b609b7acb591be0459cde07550fd4ebe

Addresses the HEVC/IMM5 portion of #25447.

Assisted-by: Fairy
---
 libavcodec/hevc/hevcdec.c          |   1 +
 tests/api/Makefile                 |   1 +
 tests/api/api-decoder-flush-test.c | 173 +++++++++++++++++++++++++++++
 tests/fate/api.mak                 |   4 +
 tests/ref/fate/api-hevc-flush      |   4 +
 5 files changed, 183 insertions(+)
 create mode 100644 tests/api/api-decoder-flush-test.c
 create mode 100644 tests/ref/fate/api-hevc-flush

diff --git a/libavcodec/hevc/hevcdec.c b/libavcodec/hevc/hevcdec.c
index f1bd5622913..a01fa854d60 100644
--- a/libavcodec/hevc/hevcdec.c
+++ b/libavcodec/hevc/hevcdec.c
@@ -4465,6 +4465,7 @@ static av_cold void hevc_decode_flush(AVCodecContext 
*avctx)
 {
     HEVCContext *s = avctx->priv_data;
     ff_hevc_flush_dpb(s);
+    av_container_fifo_drain(s->output_fifo, 
av_container_fifo_can_read(s->output_fifo));
     ff_hevc_reset_sei(&s->sei);
     ff_dovi_ctx_flush(&s->dovi_ctx);
     av_buffer_unref(&s->rpu_buf);
diff --git a/tests/api/Makefile b/tests/api/Makefile
index 7da5f6da32b..0c03d69a4e5 100644
--- a/tests/api/Makefile
+++ b/tests/api/Makefile
@@ -11,6 +11,7 @@ APITESTPROGS-$(HAVE_THREADS) += api-threadmessage
 APITESTPROGS-$(CONFIG_AVCODEC) += api-enc-info
 APITESTPROGS-$(CONFIG_PCM_S16LE_DECODER) += api-skip-samples
 APITESTPROGS-$(call ALLYES, H261_ENCODER H261_PARSER) += api-enc-parser
+APITESTPROGS-$(call DEMDEC, HEVC, HEVC, HEVC_PARSER) += api-decoder-flush
 APITESTPROGS += $(APITESTPROGS-yes)
 
 APITESTOBJS  := $(APITESTOBJS:%=$(APITESTSDIR)%) 
$(APITESTPROGS:%=$(APITESTSDIR)/%-test.o)
diff --git a/tests/api/api-decoder-flush-test.c 
b/tests/api/api-decoder-flush-test.c
new file mode 100644
index 00000000000..322600244d4
--- /dev/null
+++ b/tests/api/api-decoder-flush-test.c
@@ -0,0 +1,173 @@
+/*
+ * Exercise flushing a decoder with delayed output, then replay the input.
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libavcodec/avcodec.h"
+#include "libavformat/avformat.h"
+#include "libavutil/imgutils.h"
+#include "libavutil/mem.h"
+
+static int open_input(AVFormatContext **fmt, const char *name)
+{
+    int ret = avformat_open_input(fmt, name, NULL, NULL);
+    if (ret < 0)
+        return ret;
+    ret = avformat_find_stream_info(*fmt, NULL);
+    if (ret < 0)
+        return ret;
+    return av_find_best_stream(*fmt, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
+}
+
+static int next_frame(AVFormatContext *fmt, AVCodecContext *dec, AVPacket *pkt,
+                      AVFrame *frame, int stream, int *draining)
+{
+    int ret;
+
+    while ((ret = avcodec_receive_frame(dec, frame)) == AVERROR(EAGAIN)) {
+        if (*draining)
+            return AVERROR_BUG;
+        do {
+            av_packet_unref(pkt);
+            ret = av_read_frame(fmt, pkt);
+        } while (ret >= 0 && pkt->stream_index != stream);
+        if (ret < 0 && ret != AVERROR_EOF)
+            return ret;
+        *draining = ret == AVERROR_EOF;
+        ret = avcodec_send_packet(dec, *draining ? NULL : pkt);
+        av_packet_unref(pkt);
+        if (ret < 0)
+            return ret;
+    }
+    return ret;
+}
+
+static int compare_frames(const AVFrame *a, const AVFrame *b)
+{
+    uint8_t *buf_a = NULL, *buf_b = NULL;
+    int ret, size;
+
+    if (a->width != b->width || a->height != b->height || a->format != 
b->format) {
+        puts("first frame: different geometry or format");
+        return 0;
+    }
+    size = av_image_get_buffer_size(a->format, a->width, a->height, 1);
+    if (size < 0)
+        return size;
+    buf_a = av_malloc(size);
+    buf_b = av_malloc(size);
+    if (!buf_a || !buf_b) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+    ret = av_image_copy_to_buffer(buf_a, size, (const uint8_t *const *)a->data,
+                                  a->linesize, a->format, a->width, a->height, 
1);
+    if (ret < 0)
+        goto end;
+    ret = av_image_copy_to_buffer(buf_b, size, (const uint8_t *const *)b->data,
+                                  b->linesize, b->format, b->width, b->height, 
1);
+    if (ret < 0)
+        goto end;
+    printf("first frame pixels equal: %d\n", !memcmp(buf_a, buf_b, size));
+    printf("first frame pts equal: %d\n", a->pts == b->pts);
+    ret = 0;
+end:
+    av_free(buf_a);
+    av_free(buf_b);
+    return ret;
+}
+
+int main(int argc, char **argv)
+{
+    AVFormatContext *fmt = NULL;
+    AVCodecContext *dec = NULL;
+    AVPacket *pkt = av_packet_alloc();
+    AVFrame *frame = av_frame_alloc(), *first = av_frame_alloc();
+    const AVCodec *codec;
+    int ret = AVERROR(ENOMEM), stream, draining = 0, count = 0, limit;
+
+    if (argc != 4) {
+        fprintf(stderr, "Usage: %s input threads frames-before-flush (0: 
drain)\n", argv[0]);
+        ret = AVERROR(EINVAL);
+        goto end;
+    }
+    if (!pkt || !frame || !first)
+        goto end;
+    limit = atoi(argv[3]);
+    stream = open_input(&fmt, argv[1]);
+    if (stream < 0) {
+        ret = stream;
+        goto end;
+    }
+    codec = avcodec_find_decoder(fmt->streams[stream]->codecpar->codec_id);
+    if (!codec) {
+        ret = AVERROR_DECODER_NOT_FOUND;
+        goto end;
+    }
+    dec = avcodec_alloc_context3(codec);
+    if (!dec)
+        goto end;
+    ret = avcodec_parameters_to_context(dec, fmt->streams[stream]->codecpar);
+    if (ret < 0)
+        goto end;
+    dec->thread_count = atoi(argv[2]);
+    dec->pkt_timebase = fmt->streams[stream]->time_base;
+    ret = avcodec_open2(dec, codec, NULL);
+    if (ret < 0)
+        goto end;
+    do {
+        ret = next_frame(fmt, dec, pkt, frame, stream, &draining);
+        if (ret < 0)
+            goto end;
+        if (!count) {
+            ret = av_frame_ref(first, frame);
+            if (ret < 0)
+                goto end;
+        }
+        count++;
+    } while (limit ? count < limit : !draining);
+    printf("frames before flush: %d; draining: %d\n", count, draining);
+    avcodec_flush_buffers(dec);
+    ret = avcodec_receive_frame(dec, frame);
+    printf("receive after flush: %s\n", ret == AVERROR(EAGAIN) ? "EAGAIN" :
+           ret == AVERROR_EOF ? "EOF" : ret < 0 ? "error" : "frame");
+    avformat_close_input(&fmt);
+    stream = open_input(&fmt, argv[1]);
+    if (stream < 0) {
+        ret = stream;
+        goto end;
+    }
+    draining = 0;
+    ret = next_frame(fmt, dec, pkt, frame, stream, &draining);
+    if (ret < 0)
+        goto end;
+    ret = compare_frames(first, frame);
+end:
+    if (ret < 0)
+        fprintf(stderr, "Error: %s\n", av_err2str(ret));
+    av_frame_free(&frame);
+    av_frame_free(&first);
+    av_packet_free(&pkt);
+    avcodec_free_context(&dec);
+    avformat_close_input(&fmt);
+    return ret < 0;
+}
diff --git a/tests/fate/api.mak b/tests/fate/api.mak
index 7b90e3dd3b8..927d4c07553 100644
--- a/tests/fate/api.mak
+++ b/tests/fate/api.mak
@@ -1,3 +1,7 @@
+FATE_API_SAMPLES_LIBAVFORMAT-$(call DEMDEC, HEVC, HEVC, HEVC_PARSER) += 
fate-api-hevc-flush
+fate-api-hevc-flush: $(APITESTSDIR)/api-decoder-flush-test$(EXESUF)
+fate-api-hevc-flush: CMD = run $(APITESTSDIR)/api-decoder-flush-test$(EXESUF) 
$(TARGET_SAMPLES)/hevc-conformance/BUMPING_A_ericsson_1.bit 1 0
+
 FATE_API_LIBAVCODEC-$(call ALLYES, LIBASTCENC_ENCODER LIBASTCENC_DECODER) += 
fate-api-astc-profile
 fate-api-astc-profile: $(APITESTSDIR)/api-astc-profile-test$(EXESUF)
 fate-api-astc-profile: CMD = run $(APITESTSDIR)/api-astc-profile-test$(EXESUF)
diff --git a/tests/ref/fate/api-hevc-flush b/tests/ref/fate/api-hevc-flush
new file mode 100644
index 00000000000..4bdef017701
--- /dev/null
+++ b/tests/ref/fate/api-hevc-flush
@@ -0,0 +1,4 @@
+frames before flush: 46; draining: 1
+receive after flush: EAGAIN
+first frame pixels equal: 1
+first frame pts equal: 1
-- 
2.54.0

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

Reply via email to