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

Calling avcodec_flush_buffers() currently leaves the ANSI decoder's picture, 
cursor, rendition attributes, font and parser state intact. The sequence in 
#25318 produces 449 differing pixels compared with a freshly opened decoder.

Add a flush callback that releases the retained frame and resets 
rendering/parser state through a helper shared with initialization. Save the 
initial dimensions and restore them before allocating the next picture, so an 
earlier screen-mode change does not survive the flush. Frames retained by the 
caller remain unchanged, and the public frame counter is preserved.

Add a sample-free FATE API test covering picture/cursor/colour state, saved 
cursor, partial escape sequences, music state, font and screen-mode changes, 
custom initial dimensions, repeated flushes, flushing before input, and 
resuming after EOF. The test prints actual results for comparison with 
tests/ref/fate/api-ansi-flush.

Validation on AArch64 Linux: all 12 API cases match fresh decoders after the 
fix; the test detects the failures against unmodified release/9.0 libraries. 
fate-api-ansi-flush, fate-ansi and fate-ansi256 pass with GCC 13.3. The API 
executable and its libavcodec/libavutil dependencies also pass with Clang 18.1 
AddressSanitizer and UndefinedBehaviorSanitizer instrumentation, without 
diagnostics.

Fixes #25318.


>From b5e898401020d666d4b2dbd5ea7f8b23ec3fe48f Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Thu, 22 Oct 2026 17:11:04 +0000
Subject: [PATCH] avcodec/ansi: reset decoder state on flush

Reset the screen, cursor, saved cursor, rendition attributes, font and parser 
state when avcodec_flush_buffers() is called. Restore the initial dimensions 
before allocating the next frame, including when an escape sequence previously 
changed the screen mode. Keep frames retained by the caller intact.

Add a sample-free FATE API test comparing flushed and fresh decoders, including 
incomplete escape sequences, screen modes, custom dimensions, repeated flushes 
and draining.

Fixes #25318.

Assisted-by: Fairy
---
 libavcodec/ansi.c               |  45 ++++++++---
 tests/api/Makefile              |   1 +
 tests/api/api-ansi-flush-test.c | 137 ++++++++++++++++++++++++++++++++
 tests/fate/api.mak              |   4 +
 tests/ref/fate/api-ansi-flush   |  12 +++
 5 files changed, 189 insertions(+), 10 deletions(-)
 create mode 100644 tests/api/api-ansi-flush-test.c
 create mode 100644 tests/ref/fate/api-ansi-flush

diff --git a/libavcodec/ansi.c b/libavcodec/ansi.c
index 4ff015e2c0a..545fc1a1838 100644
--- a/libavcodec/ansi.c
+++ b/libavcodec/ansi.c
@@ -64,6 +64,8 @@ typedef struct AnsiContext {
     int fg;               /**< foreground color */
     int bg;               /**< background color */
     int first_frame;
+    int initial_width;
+    int initial_height;
 
     /* ansi parser state machine */
     enum {
@@ -77,16 +79,26 @@ typedef struct AnsiContext {
     int nb_args;          /**< number of arguments (may exceed MAX_NB_ARGS) */
 } AnsiContext;
 
+static void reset_state(AnsiContext *s)
+{
+    s->x = s->y = s->sx = s->sy = 0;
+    s->font        = avpriv_vga16_font_get();
+    s->font_height = 16;
+    s->attributes  = 0;
+    s->fg          = DEFAULT_FG_COLOR;
+    s->bg          = DEFAULT_BG_COLOR;
+    s->first_frame = 0;
+    s->state       = STATE_NORMAL;
+    s->nb_args     = 0;
+    memset(s->args, 0, sizeof(s->args));
+}
+
 static av_cold int decode_init(AVCodecContext *avctx)
 {
     AnsiContext *s = avctx->priv_data;
     avctx->pix_fmt = AV_PIX_FMT_PAL8;
 
-    /* defaults */
-    s->font        = avpriv_vga16_font_get();
-    s->font_height = 16;
-    s->fg          = DEFAULT_FG_COLOR;
-    s->bg          = DEFAULT_BG_COLOR;
+    reset_state(s);
 
     if (!avctx->width || !avctx->height) {
         int ret = ff_set_dimensions(avctx, 80 << 3, 25 << 4);
@@ -97,6 +109,9 @@ static av_cold int decode_init(AVCodecContext *avctx)
         return AVERROR(EINVAL);
     }
 
+    s->initial_width  = avctx->width;
+    s->initial_height = avctx->height;
+
     s->frame = av_frame_alloc();
     if (!s->frame)
         return AVERROR(ENOMEM);
@@ -362,13 +377,14 @@ static int decode_frame(AVCodecContext *avctx, AVFrame 
*rframe,
     const uint8_t *buf_end   = buf+buf_size;
     int ret, i, count;
 
+    if (!s->first_frame) {
+        /* Discard any screen mode selected before the flush. */
+        ret = ff_set_dimensions(avctx, s->initial_width, s->initial_height);
+        if (ret < 0)
+            return ret;
+    }
     if ((ret = ff_reget_buffer(avctx, s->frame, 0)) < 0)
         return ret;
-    if (!avctx->frame_num) {
-        for (i=0; i<avctx->height; i++)
-            memset(s->frame->data[0]+ i*s->frame->linesize[0], 0, 
avctx->width);
-        memset(s->frame->data[1], 0, AVPALETTE_SIZE);
-    }
 
     s->frame->pict_type           = AV_PICTURE_TYPE_I;
     set_palette((uint32_t *)s->frame->data[1]);
@@ -466,6 +482,14 @@ static int decode_frame(AVCodecContext *avctx, AVFrame 
*rframe,
     return buf_size;
 }
 
+static void decode_flush(AVCodecContext *avctx)
+{
+    AnsiContext *s = avctx->priv_data;
+
+    av_frame_unref(s->frame);
+    reset_state(s);
+}
+
 static av_cold int decode_close(AVCodecContext *avctx)
 {
     AnsiContext *s = avctx->priv_data;
@@ -487,6 +511,7 @@ const FFCodec ff_ansi_decoder = {
     .priv_data_size = sizeof(AnsiContext),
     .init           = decode_init,
     .close          = decode_close,
+    .flush          = decode_flush,
     FF_CODEC_DECODE_CB(decode_frame),
     .p.capabilities = AV_CODEC_CAP_DR1,
     .defaults       = ansi_defaults,
diff --git a/tests/api/Makefile b/tests/api/Makefile
index e51dc6c5837..9de5cbf256f 100644
--- a/tests/api/Makefile
+++ b/tests/api/Makefile
@@ -1,3 +1,4 @@
+APITESTPROGS-$(CONFIG_ANSI_DECODER) += api-ansi-flush
 APITESTPROGS-$(call ALLYES, LIBASTCENC_ENCODER LIBASTCENC_DECODER) += 
api-astc-profile
 APITESTPROGS-$(call ENCDEC, FLAC, FLAC) += api-flac
 APITESTPROGS-$(call DEMDEC, H264, H264) += api-h264
diff --git a/tests/api/api-ansi-flush-test.c b/tests/api/api-ansi-flush-test.c
new file mode 100644
index 00000000000..99afafc4f7b
--- /dev/null
+++ b/tests/api/api-ansi-flush-test.c
@@ -0,0 +1,137 @@
+/*
+ * ANSI decoder flush API test
+ *
+ * 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
+ */
+
+/* Compare a flushed decoder with a fresh decoder, retaining the old frame. */
+
+#include <stdio.h>
+#include <string.h>
+#include "libavcodec/avcodec.h"
+#include "libavutil/adler32.h"
+#include "libavutil/common.h"
+
+static int decode_text(AVCodecContext *ctx, const char *text, AVFrame *frame)
+{
+    AVPacket *pkt = av_packet_alloc();
+    int ret;
+    if (!pkt)
+        return AVERROR(ENOMEM);
+    ret = av_new_packet(pkt, strlen(text));
+    if (ret >= 0) {
+        memcpy(pkt->data, text, pkt->size);
+        ret = avcodec_send_packet(ctx, pkt);
+    }
+    av_packet_free(&pkt);
+    if (ret < 0)
+        return ret;
+    av_frame_unref(frame);
+    return avcodec_receive_frame(ctx, frame);
+}
+
+static unsigned checksum(const AVFrame *frame)
+{
+    unsigned sum = 1;
+    for (int y = 0; y < frame->height; y++)
+        sum = av_adler32_update(sum, frame->data[0] + y * frame->linesize[0], 
frame->width);
+    return av_adler32_update(sum, frame->data[1], AVPALETTE_SIZE);
+}
+
+static int run_case(const char *name, const char *before, const char *after,
+                    int width, int height, int drain)
+{
+    const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_ANSI);
+    AVCodecContext *r = avcodec_alloc_context3(codec);
+    AVCodecContext *f = avcodec_alloc_context3(codec);
+    AVFrame *old = av_frame_alloc(), *fr = av_frame_alloc(), *ff = 
av_frame_alloc();
+    unsigned old_sum = 0;
+    int ret = AVERROR(ENOMEM), diff = 0, pal_diff = 0, eof = 0;
+
+    if (!r || !f || !old || !fr || !ff)
+        goto end;
+    r->width = f->width = width;
+    r->height = f->height = height;
+    if ((ret = avcodec_open2(r, codec, NULL)) < 0 ||
+        (ret = avcodec_open2(f, codec, NULL)) < 0)
+        goto end;
+    if (before) {
+        if ((ret = decode_text(r, before, old)) < 0)
+            goto end;
+        old_sum = checksum(old);
+    }
+    if (drain) {
+        if ((ret = avcodec_send_packet(r, NULL)) < 0)
+            goto end;
+        ret = avcodec_receive_frame(r, fr);
+        eof = ret == AVERROR_EOF;
+        if (ret < 0 && !eof)
+            goto end;
+    }
+    /* Flushing twice must be harmless, including before the first packet. */
+    avcodec_flush_buffers(r);
+    avcodec_flush_buffers(r);
+    if ((ret = decode_text(r, after, fr)) < 0 ||
+        (ret = decode_text(f, after, ff)) < 0)
+        goto end;
+    for (int y = 0; y < FFMIN(fr->height, ff->height); y++)
+        for (int x = 0; x < FFMIN(fr->width, ff->width); x++)
+            diff += fr->data[0][y * fr->linesize[0] + x] !=
+                    ff->data[0][y * ff->linesize[0] + x];
+    for (int i = 0; i < AVPALETTE_SIZE; i++)
+        pal_diff += fr->data[1][i] != ff->data[1][i];
+    printf("%s: reused=%dx%d fresh=%dx%d pixels=%d palette=%d retained=%d 
eof=%d\n",
+           name, fr->width, fr->height, ff->width, ff->height, diff, pal_diff,
+           before ? old_sum == checksum(old) : 1, eof);
+end:
+    avcodec_free_context(&r);
+    avcodec_free_context(&f);
+    av_frame_free(&old);
+    av_frame_free(&fr);
+    av_frame_free(&ff);
+    return ret < 0 ? ret : 0;
+}
+
+int main(void)
+{
+    static const struct {
+        const char *name, *before, *after;
+        int width, height, drain;
+    } cases[] = {
+        { "picture-cursor-sgr", "\033[1;31mHELLO WORLD\033[5;10H", "B" },
+        { "saved-cursor", "\033[5;10H\033[s", "\033[uB" },
+        { "background", "\033[44mA", "B" },
+        { "escape", "\033", "B" },
+        { "csi", "\033[1;31", "B" },
+        { "csi-arguments", "\033[1;31;44;5", "B" },
+        { "music", "\033[Mmusic", "B" },
+        { "font-mode6", "\033[6;0hA", "B" },
+        { "font-mode0", "\033[0;0hA", "B" },
+        { "custom-size", "\033[0;0hA", "B", 320, 160 },
+        { "before-input", NULL, "B" },
+        { "after-eof", "\033[1;31mA", "B", 0, 0, 1 },
+    };
+    for (int i = 0; i < FF_ARRAY_ELEMS(cases); i++) {
+        int ret = run_case(cases[i].name, cases[i].before, cases[i].after,
+                           cases[i].width, cases[i].height, cases[i].drain);
+        if (ret < 0) {
+            fprintf(stderr, "%s: error %d\n", cases[i].name, ret);
+            return 1;
+        }
+    }
+    return 0;
+}
diff --git a/tests/fate/api.mak b/tests/fate/api.mak
index ca836a40e3d..d6ec3ed4e14 100644
--- a/tests/fate/api.mak
+++ b/tests/fate/api.mak
@@ -1,3 +1,7 @@
+FATE_API_LIBAVCODEC-$(CONFIG_ANSI_DECODER) += fate-api-ansi-flush
+fate-api-ansi-flush: $(APITESTSDIR)/api-ansi-flush-test$(EXESUF)
+fate-api-ansi-flush: CMD = run $(APITESTSDIR)/api-ansi-flush-test$(EXESUF)
+
 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-ansi-flush b/tests/ref/fate/api-ansi-flush
new file mode 100644
index 00000000000..0ba2bce86dc
--- /dev/null
+++ b/tests/ref/fate/api-ansi-flush
@@ -0,0 +1,12 @@
+picture-cursor-sgr: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 
eof=0
+saved-cursor: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=0
+background: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=0
+escape: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=0
+csi: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=0
+csi-arguments: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=0
+music: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=0
+font-mode6: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=0
+font-mode0: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=0
+custom-size: reused=320x160 fresh=320x160 pixels=0 palette=0 retained=1 eof=0
+before-input: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=0
+after-eof: reused=640x400 fresh=640x400 pixels=0 palette=0 retained=1 eof=1
-- 
2.54.0

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

Reply via email to