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]
