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]
