[PR] avcodec/frame_thread_encoder: force one thread for MJPEG rate control (PR #24079)

Théo Valette via ffmpeg-devel <[email protected]>
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <178645400236.59.7913126062400560332@29965ddac10e>
PR #24079 opened by Théo Valette (theovalette)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24079
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24079.patch

### Problem

Rate-controlled MJPEG encoding with explicit frame threading can produce different output across identical runs. Ten runs of the issue reproducer on the unmodified base produced ten distinct SHA-256 hashes and output sizes from 868,916 to 871,422 bytes.

### Root cause and fix

The generic frame-thread encoder gives each worker an independent persistent codec context, while workers acquire queued frames according to scheduling. MJPEG rate control keeps predictors, accumulated complexity, short-term quantizer state, wanted-bit state, and total encoded bits in those worker-local contexts, so a frame can see a different rate-control history depending on its worker assignment.

The existing safeguard only forced automatic thread selection to one and merely warned for an explicit multi-thread request. This change also forces explicit rate-controlled MJPEG frame-thread requests to one thread and warns about the fallback, preventing creation of the independent worker histories.

Explicit slice threading and constant-quantizer frame threading remain available and are excluded from the fallback.

### Tests

- `make -j"$(nproc)" fate-ffmpeg-mjpeg-cbr-frame-threading fate-vsynth2-mjpeg fate-libavcodec-huffman`: passed.
- The full issue reproducer repeated ten times with the patched default build produced the same 489,380-byte AVI and SHA-256 `caa7c184c6c363e54c4276190bf84b15a8d901140e3c5fcbf2dabb5e35dd223a` every time, matching the unmodified-base single-thread control.
- Five `-threads 4 -thread_type slice` controls and five `-threads 4 -q:v 2` controls remained deterministic and did not trigger the fallback.
- A standalone build configured with only ffmpeg, the MJPEG encoder, rawvideo decoder/demuxer, AVI muxer, file protocol, and scale filter succeeded; five reproducer runs with that build matched the deterministic result above.
- `git diff --check 03dc244a693ce639cebf82f7bae112fb75580919..37fe13f9a8ab1dd0aca790054d7e5a450821bd1f`: passed.

Fixes https://code.ffmpeg.org/FFmpeg/FFmpeg/issues/20964


From 37fe13f9a8ab1dd0aca790054d7e5a450821bd1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Th=C3=A9o=20Valette?=
 <[email protected]>
Date: Tue, 11 Aug 2026 15:04:30 +0200
Subject: [PATCH] avcodec/frame_thread_encoder: force one thread for MJPEG rate
 control
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Frame-threaded encoders use an independent codec context for every
worker. MJPEG rate-control state is context-local, so explicit frame
threading splits frames across independent histories and can make the
output depend on worker scheduling.

Extend the existing automatic-thread safeguard to explicit thread
counts. Keep slice threading and constant-quantizer frame threading
available. Add a FATE test for the forced serial result.

Fixes: #20964
Reported-by: James Almer <[email protected]>
Signed-off-by: Théo Valette <[email protected]>
---
 libavcodec/frame_thread_encoder.c               | 12 +++---------
 tests/fate/ffmpeg.mak                           |  8 ++++++++
 tests/ref/fate/ffmpeg-mjpeg-cbr-frame-threading |  1 +
 3 files changed, 12 insertions(+), 9 deletions(-)
 create mode 100644 tests/ref/fate/ffmpeg-mjpeg-cbr-frame-threading

diff --git a/libavcodec/frame_thread_encoder.c b/libavcodec/frame_thread_encoder.c
index 6af3db722e..97cce6f669 100644
--- a/libavcodec/frame_thread_encoder.c
+++ b/libavcodec/frame_thread_encoder.c
@@ -129,20 +129,14 @@ av_cold int ff_frame_thread_encoder_init(AVCodecContext *avctx)
        || !(avctx->codec->capabilities & AV_CODEC_CAP_FRAME_THREADS))
         return 0;
 
-    if(   !avctx->thread_count
+    if(   avctx->thread_count != 1
        && avctx->codec_id == AV_CODEC_ID_MJPEG
        && !(avctx->flags & AV_CODEC_FLAG_QSCALE)) {
-        av_log(avctx, AV_LOG_DEBUG,
+        av_log(avctx, avctx->thread_count > 1 ? AV_LOG_WARNING : AV_LOG_DEBUG,
                "Forcing thread count to 1 for MJPEG encoding, use -thread_type slice "
-               "or a constant quantizer if you want to use multiple cpu cores\n");
+               "or a constant quantizer if you want to use multiple CPU cores\n");
         avctx->thread_count = 1;
     }
-    if(   avctx->thread_count > 1
-       && avctx->codec_id == AV_CODEC_ID_MJPEG
-       && !(avctx->flags & AV_CODEC_FLAG_QSCALE))
-        av_log(avctx, AV_LOG_WARNING,
-               "MJPEG CBR encoding works badly with frame multi-threading, consider "
-               "using -threads 1, -thread_type slice or a constant quantizer.\n");
 
     if (avctx->codec_id == AV_CODEC_ID_HUFFYUV ||
         avctx->codec_id == AV_CODEC_ID_FFVHUFF) {
diff --git a/tests/fate/ffmpeg.mak b/tests/fate/ffmpeg.mak
index ad8e5c775f..e1140e34fb 100644
--- a/tests/fate/ffmpeg.mak
+++ b/tests/fate/ffmpeg.mak
@@ -16,6 +16,14 @@ fate-ffmpeg-filter_colorkey: CMD = framecrc -auto_conversion_filters -idct simpl
 FATE_FFMPEG-$(call FILTERFRAMECRC, COLOR) += fate-ffmpeg-lavfi
 fate-ffmpeg-lavfi: CMD = framecrc -lavfi color=d=1:r=5 -fflags +bitexact
 
+FATE_FFMPEG-$(call ENCMUX, MJPEG, AVI, RAWVIDEO_DEMUXER RAWVIDEO_DECODER \
+    SCALE_FILTER FILE_PROTOCOL) += fate-ffmpeg-mjpeg-cbr-frame-threading
+fate-ffmpeg-mjpeg-cbr-frame-threading: tests/data/vsynth2.yuv
+fate-ffmpeg-mjpeg-cbr-frame-threading: CMD = md5 -auto_conversion_filters \
+    -f rawvideo -s 352x288 \
+    -color_range mpeg -pix_fmt yuv420p -i $(TARGET_PATH)/tests/data/vsynth2.yuv \
+    -frames:v 8 -threads 4 -c:v mjpeg -flags +bitexact -fflags +bitexact -f avi
+
 FATE_FFMPEG-$(call ENCDEC2, MPEG4, RAWVIDEO, AVI, RAWVIDEO_DEMUXER FRAMECRC_MUXER) += fate-force_key_frames
 fate-force_key_frames: tests/data/vsynth1.yuv
 fate-force_key_frames: CMD = enc_dec \
diff --git a/tests/ref/fate/ffmpeg-mjpeg-cbr-frame-threading b/tests/ref/fate/ffmpeg-mjpeg-cbr-frame-threading
new file mode 100644
index 0000000000..701f8a3dfc
--- /dev/null
+++ b/tests/ref/fate/ffmpeg-mjpeg-cbr-frame-threading
@@ -0,0 +1 @@
+d5c9cf75673ace3f437fda8b60b9fdc7
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.