PR #25369 opened by Forgejo_Fairy URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25369 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25369.patch
Fixes #25320. An interrupted CAVLC macroblock can leave residual coefficients in a slice context. Clearing `H264SliceContext.mb` for every slice context during `avcodec_flush_buffers()` prevents those values from affecting the next stream decoded by the same context. Adds two public API FATE regression cases adapted from Pu Cheng's reproducer. On unmodified master they report 1 differing frame for `BA1_Sony_D.jsv` and 30 for `BA_MW_D.264`; with this change both report zero. Tested on AArch64 with the two new FATE tests and the existing H.264 conformance tests for both samples. >From c9b5510924d7db67a7a8350d0b1d2569ff003506 Mon Sep 17 00:00:00 2001 From: Michael Niedermayer <[email protected]> Date: Thu, 22 Oct 2026 17:35:52 +0000 Subject: [PATCH] avcodec/h264: clear pending coefficients on flush An interrupted CAVLC macroblock can leave residual coefficients in a slice context. The next stream decoded with the same context then incorporates them after avcodec_flush_buffers(). Clear the coefficient buffer for every slice context on flush and cover the reuse sequence with two conformance samples. Fixes #25320. Assisted-by: Fairy --- libavcodec/h264dec.c | 4 + tests/api/Makefile | 1 + tests/api/api-h264-flush-coeffs-test.c | 208 ++++++++++++++++++++++ tests/fate/api.mak | 5 + tests/ref/fate/api-h264-flush-coeffs-mw | 2 + tests/ref/fate/api-h264-flush-coeffs-sony | 2 + 6 files changed, 222 insertions(+) create mode 100644 tests/api/api-h264-flush-coeffs-test.c create mode 100644 tests/ref/fate/api-h264-flush-coeffs-mw create mode 100644 tests/ref/fate/api-h264-flush-coeffs-sony diff --git a/libavcodec/h264dec.c b/libavcodec/h264dec.c index 597bdae548a..9096e6ba7d2 100644 --- a/libavcodec/h264dec.c +++ b/libavcodec/h264dec.c @@ -484,6 +484,10 @@ static av_cold void h264_decode_flush(AVCodecContext *avctx) ff_h264_flush_change(h); ff_h264_sei_uninit(&h->sei); + /* A damaged slice may leave coefficients in an incomplete macroblock. */ + for (i = 0; i < h->nb_slice_ctx; i++) + memset(h->slice_ctx[i].mb, 0, sizeof(h->slice_ctx[i].mb)); + for (i = 0; i < H264_MAX_PICTURE_COUNT; i++) ff_h264_unref_picture(&h->DPB[i]); h->cur_pic_ptr = NULL; diff --git a/tests/api/Makefile b/tests/api/Makefile index e51dc6c5837..2852aa668e1 100644 --- a/tests/api/Makefile +++ b/tests/api/Makefile @@ -1,6 +1,7 @@ APITESTPROGS-$(call ALLYES, LIBASTCENC_ENCODER LIBASTCENC_DECODER) += api-astc-profile APITESTPROGS-$(call ENCDEC, FLAC, FLAC) += api-flac APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264 +APITESTPROGS-$(call ALLYES, H264_DECODER H264_PARSER) += api-h264-flush-coeffs APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264-slice APITESTPROGS-$(CONFIG_MOV_MUXER) += api-movenc APITESTPROGS-$(CONFIG_MOVTEXT_ENCODER) += api-movtext diff --git a/tests/api/api-h264-flush-coeffs-test.c b/tests/api/api-h264-flush-coeffs-test.c new file mode 100644 index 00000000000..f1615a99d2d --- /dev/null +++ b/tests/api/api-h264-flush-coeffs-test.c @@ -0,0 +1,208 @@ +/* + * H.264 decoder flush regression test, based on the reproducer by Pu Cheng + * in FFmpeg issue #25320. + * + * Without the flush fix, residual coefficients of an incomplete macroblock + * survive avcodec_flush_buffers() and are added to the next stream. + * + * Usage: h264_partial_flush <stream> <bytes> + * e.g. FATE h264-conformance/BA1_Sony_D.jsv + * + * Decoder R receives the first access unit of <stream> cut to <bytes> bytes + * (as after an interrupted transfer), is flushed, then decodes the whole + * <stream>. Decoder F is newly opened and decodes only <stream>. + */ +#include <libavcodec/avcodec.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* ---- helpers: split an Annex B file into access units, hash pictures ---- */ + +#define MAX_FRAMES 4096 + +typedef struct AUList { AVPacket **au; int n; } AUList; + +static int load_aus(const char *path, AUList *l) +{ + FILE *fp = fopen(path, "rb"); + const AVCodec *c = avcodec_find_decoder(AV_CODEC_ID_H264); + AVCodecParserContext *pc = av_parser_init(AV_CODEC_ID_H264); + AVCodecContext *ctx = avcodec_alloc_context3(c); + uint8_t *buf; + long size; + int off = 0; + + if (!fp || !pc || !ctx) + return -1; + fseek(fp, 0, SEEK_END); + size = ftell(fp); + fseek(fp, 0, SEEK_SET); + buf = av_mallocz(size + AV_INPUT_BUFFER_PADDING_SIZE); + if (fread(buf, 1, size, fp) != (size_t)size) + return -1; + fclose(fp); + l->au = NULL; + l->n = 0; + for (;;) { + uint8_t *data; + int data_size, eof = off >= size; + int used = av_parser_parse2(pc, ctx, &data, &data_size, + eof ? NULL : buf + off, eof ? 0 : size - off, + AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); + off += used; + if (data_size) { + AVPacket *p = av_packet_alloc(); + av_new_packet(p, data_size); + memcpy(p->data, data, data_size); + l->au = realloc(l->au, (l->n + 1) * sizeof(*l->au)); + l->au[l->n++] = p; + } else if (eof) + break; + } + av_parser_close(pc); + avcodec_free_context(&ctx); + av_free(buf); + return l->n; +} + +static void free_aus(AUList *l) +{ + for (int i = 0; i < l->n; i++) + av_packet_free(&l->au[i]); + free(l->au); + l->au = NULL; + l->n = 0; +} + +static AVCodecContext *open_h264(void) +{ + const AVCodec *c = avcodec_find_decoder(AV_CODEC_ID_H264); + AVCodecContext *ctx = avcodec_alloc_context3(c); + if (!ctx) + return NULL; + ctx->thread_count = 1; + if (avcodec_open2(ctx, c, NULL) < 0) + return NULL; + return ctx; +} + +static uint64_t hash_frame(const AVFrame *f) +{ + uint64_t h = 1469598103934665603ULL; + for (int p = 0; p < 3 && f->data[p]; p++) { + int w = p ? AV_CEIL_RSHIFT(f->width, 1) : f->width; + int hh = p ? AV_CEIL_RSHIFT(f->height, 1) : f->height; + for (int y = 0; y < hh; y++) + for (int x = 0; x < w; x++) + h = (h ^ f->data[p][y * f->linesize[p] + x]) * 1099511628211ULL; + } + return h; +} + +/* send every AU, then drain to EOF; returns the number of frames. + * If keep is set, a reference to frame number keep_idx is stored in it. */ +static int decode_keep(AVCodecContext *ctx, const AUList *l, uint64_t *hashes, int max, + int keep_idx, AVFrame *keep) +{ + AVFrame *f = av_frame_alloc(); + int n = 0; + for (int i = 0; i <= l->n; i++) { + avcodec_send_packet(ctx, i < l->n ? l->au[i] : NULL); + while (avcodec_receive_frame(ctx, f) >= 0) { + if (hashes && n < max) + hashes[n] = hash_frame(f); + if (keep && n == keep_idx) + av_frame_ref(keep, f); + n++; + av_frame_unref(f); + } + } + av_frame_free(&f); + return n; +} + +static int decode_all(AVCodecContext *ctx, const AUList *l, uint64_t *hashes, int max) +{ + return decode_keep(ctx, l, hashes, max, -1, NULL); +} + +/* ---- end of helpers ---- */ + +int main(int argc, char **argv) +{ + AUList s; + AVCodecContext *r, *f; + AVPacket *cut = av_packet_alloc(); + AVFrame *fr = av_frame_alloc(), *ff = av_frame_alloc(); + uint64_t hr[MAX_FRAMES], hf[MAX_FRAMES]; + int nr, nf, first = -1, last = -1, ndiff = 0, ret; + + setvbuf(stdout, NULL, _IONBF, 0); + if (argc < 3 || load_aus(argv[1], &s) <= 0) + return 2; + av_new_packet(cut, FFMIN(atoi(argv[2]), s.au[0]->size)); + memcpy(cut->data, s.au[0]->data, cut->size); + + r = open_h264(); + f = open_h264(); + ret = avcodec_send_packet(r, cut); + printf("R sent the first access unit cut to %d of %d bytes: send %d", + cut->size, s.au[0]->size, ret); + ret = avcodec_receive_frame(r, fr); + printf(", receive %s\n", ret == AVERROR(EAGAIN) ? "EAGAIN" : ret < 0 ? "error" : "frame"); + av_frame_unref(fr); + avcodec_flush_buffers(r); + + nr = decode_all(r, &s, hr, MAX_FRAMES); + nf = decode_all(f, &s, hf, MAX_FRAMES); + for (int i = 0; i < FFMIN(FFMIN(nr, nf), MAX_FRAMES); i++) + if (hr[i] != hf[i]) { + ndiff++; + if (first < 0) + first = i; + last = i; + } + printf("R %d frames, F %d frames; frames that differ: %d", nr, nf, ndiff); + if (ndiff == 1) + printf(" (frame %d)", first); + else if (ndiff) + printf(" (frames %d to %d)", first, last); + printf("\n"); + + if (first >= 0) { + /* decode again, keep the first differing frame and locate the damage */ + AVCodecContext *r2 = open_h264(), *f2 = open_h264(); + int mbw, minmb = -1, nmb = 0; + avcodec_send_packet(r2, cut); + avcodec_receive_frame(r2, fr); + av_frame_unref(fr); + avcodec_flush_buffers(r2); + decode_keep(r2, &s, NULL, 0, first, fr); + decode_keep(f2, &s, NULL, 0, first, ff); + mbw = (fr->width + 15) / 16; + for (int my = 0; my < (fr->height + 15) / 16; my++) + for (int mx = 0; mx < mbw; mx++) { + int d = 0; + for (int y = my * 16; y < FFMIN(my * 16 + 16, fr->height); y++) + for (int x = mx * 16; x < FFMIN(mx * 16 + 16, fr->width); x++) + d |= fr->data[0][y * fr->linesize[0] + x] != ff->data[0][y * ff->linesize[0] + x]; + if (d) { + nmb++; + if (minmb < 0) + minmb = my * mbw + mx; + } + } + printf("frame %d: luma differs in %d of %d macroblocks, the first is macroblock %d\n", + first, nmb, mbw * ((fr->height + 15) / 16), minmb); + avcodec_free_context(&r2); + avcodec_free_context(&f2); + } + avcodec_free_context(&r); + avcodec_free_context(&f); + av_packet_free(&cut); + av_frame_free(&fr); + av_frame_free(&ff); + free_aus(&s); + return 0; +} diff --git a/tests/fate/api.mak b/tests/fate/api.mak index ca836a40e3d..6a02c2c5405 100644 --- a/tests/fate/api.mak +++ b/tests/fate/api.mak @@ -30,6 +30,11 @@ FATE_API_SAMPLES_LIBAVFORMAT-$(call DEMDEC, H264, H264, H264_PARSER) += fate-api fate-api-h264: $(APITESTSDIR)/api-h264-test$(EXESUF) fate-api-h264: CMD = run $(APITESTSDIR)/api-h264-test$(EXESUF) $(TARGET_SAMPLES)/h264-conformance/SVA_NL2_E.264 +FATE_API_SAMPLES_LIBAVFORMAT-$(call ALLYES, H264_DECODER H264_PARSER) += fate-api-h264-flush-coeffs-sony fate-api-h264-flush-coeffs-mw +fate-api-h264-flush-coeffs-sony fate-api-h264-flush-coeffs-mw: $(APITESTSDIR)/api-h264-flush-coeffs-test$(EXESUF) +fate-api-h264-flush-coeffs-sony: CMD = run $(APITESTSDIR)/api-h264-flush-coeffs-test$(EXESUF) $(TARGET_SAMPLES)/h264-conformance/BA1_Sony_D.jsv 1000 +fate-api-h264-flush-coeffs-mw: CMD = run $(APITESTSDIR)/api-h264-flush-coeffs-test$(EXESUF) $(TARGET_SAMPLES)/h264-conformance/BA_MW_D.264 1000 + FATE_API_SAMPLES_LIBAVFORMAT-$(call DEMDEC, H264, H264) += fate-api-h264-slice fate-api-h264-slice: $(APITESTSDIR)/api-h264-slice-test$(EXESUF) fate-api-h264-slice: CMD = run $(APITESTSDIR)/api-h264-slice-test$(EXESUF) 2 $(TARGET_SAMPLES)/h264/crew_cif.nal diff --git a/tests/ref/fate/api-h264-flush-coeffs-mw b/tests/ref/fate/api-h264-flush-coeffs-mw new file mode 100644 index 00000000000..5b671863123 --- /dev/null +++ b/tests/ref/fate/api-h264-flush-coeffs-mw @@ -0,0 +1,2 @@ +R sent the first access unit cut to 1000 of 2384 bytes: send 0, receive frame +R 100 frames, F 100 frames; frames that differ: 0 diff --git a/tests/ref/fate/api-h264-flush-coeffs-sony b/tests/ref/fate/api-h264-flush-coeffs-sony new file mode 100644 index 00000000000..27b3ae97947 --- /dev/null +++ b/tests/ref/fate/api-h264-flush-coeffs-sony @@ -0,0 +1,2 @@ +R sent the first access unit cut to 1000 of 3184 bytes: send 0, receive frame +R 17 frames, F 17 frames; frames that differ: 0 -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
