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]
