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]

Reply via email to