From 4e9ac951507079f31582d58ce59d968e2af93963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Spr=C3=A5ng?= Date: Mon, 6 Jul 2026 17:43:32 +0200 Subject: [PATCH 1/3] Fix dynamic slice adjustment for frames with extreme aspect ratios When rate control is disabled (RC_OFF_MODE), dynamic slice adjustment in DynamicAdjustSlicing() defaults the minimum macroblock requirement per slice (iMinimalMbNum) to the frame's macroblock width (1 row per slice). For frames with extreme aspect ratios (such as very wide resolutions), requiring 1 macroblock row per slice across multiple slices can exceed the total number of macroblocks in the frame. This change ensures robust macroblock distribution and load balancing by: - Falling back iMinimalMbNum to 1 macroblock per slice when row-based heuristics exceed frame capacity in RC_OFF_MODE. - Skipping adjustment and logging a warning when the requested number of slices equals or exceeds the total macroblocks in the frame. - Adding a capacity guard in GomValidCheckSliceMbNum() for rate-controlled encoding modes. - Adding a unit test (DynamicAdjustSlicingExtremeAspectRatio) to verify dynamic slice adjustment and load balancing on wide aspect ratio frames. --- .../core/src/slice_multi_threading.cpp | 18 +++++ .../core/src/svc_enc_slice_segment.cpp | 6 ++ test/api/encoder_test.cpp | 75 +++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/codec/encoder/core/src/slice_multi_threading.cpp b/codec/encoder/core/src/slice_multi_threading.cpp index 09ea2c08c7..7fd99929d6 100644 --- a/codec/encoder/core/src/slice_multi_threading.cpp +++ b/codec/encoder/core/src/slice_multi_threading.cpp @@ -197,6 +197,24 @@ void DynamicAdjustSlicing (sWelsEncCtx* pCtx, return; } iMinimalMbNum = iNumMbInEachGom; + } else { + // If the number of requested slices equals or exceeds the total macroblocks + // in the frame, each slice cannot be assigned even 1 macroblock. In this + // case, do not adjust slice sizes. + if (kiCountSliceNum >= kiCountNumMb) { + WelsLog(&(pCtx->sLogCtx), WELS_LOG_WARNING, + "[MT] DynamicAdjustSlicing(), requested slice number (%d) equals " + "or exceeds total macroblocks (%d), do not adjust", + kiCountSliceNum, kiCountNumMb); + return; + } else if (iMinimalMbNum * kiCountSliceNum >= kiCountNumMb) { + // When rate control is off, iMinimalMbNum defaults to 1 macroblock row + // (iMbWidth). For extreme aspect ratios (such as very wide resolutions), + // requiring 1 row per slice may exceed total frame capacity. Fall back to + // 1 macroblock per slice to ensure valid upper bounds and allow proper + // load balancing across slices. + iMinimalMbNum = 1; + } } if (kiCountSliceNum < 2 || (kiCountSliceNum & 0x01)) // we need suppose uiSliceNum is even for multiple threading diff --git a/codec/encoder/core/src/svc_enc_slice_segment.cpp b/codec/encoder/core/src/svc_enc_slice_segment.cpp index 5a9bf739eb..a920de7f30 100644 --- a/codec/encoder/core/src/svc_enc_slice_segment.cpp +++ b/codec/encoder/core/src/svc_enc_slice_segment.cpp @@ -281,6 +281,12 @@ bool GomValidCheckSliceMbNum (const int32_t kiMbWidth, const int32_t kiMbHeight, int32_t iCurNumMbAssigning = 0; iMinimalMbNum = iGomSize; + // Ensure that the minimum macroblock requirement across all slices does not + // exceed total frame capacity, preventing negative calculations for remaining + // macroblocks. + if (iMinimalMbNum * static_cast(kuiSliceNum) > kiMbNumInFrame) { + return false; + } while (uiSliceIdx + 1 < kuiSliceNum) { iMaximalMbNum = iNumMbLeft - (kuiSliceNum - uiSliceIdx - 1) * iMinimalMbNum; // get maximal num_mb in left parts // make sure one GOM at least in each slice for safe diff --git a/test/api/encoder_test.cpp b/test/api/encoder_test.cpp index ebe4a0390e..f1a81a7877 100644 --- a/test/api/encoder_test.cpp +++ b/test/api/encoder_test.cpp @@ -259,3 +259,78 @@ TEST_F(EncoderInitTest, VeryLargeSlices) { ASSERT_EQ(0, rv); } } + +// This test verifies dynamic slice adjustment when encoding frames with extreme +// aspect ratios (very wide resolution) in RC_OFF_MODE using multiple +// slices/threads. It ensures that macroblocks are correctly distributed across +// slices without producing negative slice limits or invalid memory access when +// slice encoding speeds vary. +TEST_F(EncoderInitTest, DynamicAdjustSlicingExtremeAspectRatio) { + SEncParamExt param; + encoder_->GetDefaultParams(¶m); + + param.iUsageType = CAMERA_VIDEO_REAL_TIME; + // Extreme wide aspect ratio: frame height is only 2 macroblock rows (32 + // pixels), which tests slicing behavior when row-based heuristics exceed + // frame dimensions. + param.iPicWidth = 32752; + param.iPicHeight = 32; + param.fMaxFrameRate = 30.0f; + param.iSpatialLayerNum = 1; + param.iMultipleThreadIdc = 4; + param.iRCMode = RC_OFF_MODE; + // Enable load balancing and high complexity mode to trigger dynamic slice + // adjustments. + param.bUseLoadBalancing = true; + param.iComplexityMode = HIGH_COMPLEXITY; + param.iMinQp = 0; + param.iMaxQp = 51; + + param.sSpatialLayers[0].iVideoWidth = param.iPicWidth; + param.sSpatialLayers[0].iVideoHeight = param.iPicHeight; + param.sSpatialLayers[0].fFrameRate = param.fMaxFrameRate; + param.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_FIXEDSLCNUM_SLICE; + param.sSpatialLayers[0].sSliceArgument.uiSliceNum = 4; + param.sSpatialLayers[0].iDLayerQp = 24; + + int rv = encoder_->InitializeExt(¶m); + ASSERT_EQ(0, rv); + + int frameSize = param.iPicWidth * param.iPicHeight * 3 / 2; + BufferedData buf; + buf.SetLength(frameSize); + ASSERT_EQ(buf.Length(), (size_t)frameSize); + + SFrameBSInfo info; + memset(&info, 0, sizeof(SFrameBSInfo)); + + SSourcePicture pic; + memset(&pic, 0, sizeof(SSourcePicture)); + pic.iPicWidth = param.iPicWidth; + pic.iPicHeight = param.iPicHeight; + pic.iColorFormat = videoFormatI420; + pic.iStride[0] = pic.iPicWidth; + pic.iStride[1] = pic.iStride[2] = pic.iPicWidth >> 1; + pic.pData[0] = buf.data(); + pic.pData[1] = pic.pData[0] + param.iPicWidth * param.iPicHeight; + pic.pData[2] = pic.pData[1] + (param.iPicWidth * param.iPicHeight >> 2); + + for (int i = 0; i < 10; i++) { + // Generate varying random noise in Slices 1..3 while keeping Slice 0 + // completely flat (zeros). This creates a significant encoding speed + // differential, causing the encoder to assign a larger proportion of + // macroblocks to Slice 0 during dynamic load balancing. + for (int idx = 0; idx < frameSize; idx++) { + buf.data()[idx] = rand() % 256; + } + for (int y = 0; y < 16; y++) { + memset(pic.pData[0] + y * pic.iStride[0], 0, 16368); + } + for (int y = 0; y < 8; y++) { + memset(pic.pData[1] + y * pic.iStride[1], 0, 8184); + memset(pic.pData[2] + y * pic.iStride[2], 0, 8184); + } + rv = encoder_->EncodeFrame(&pic, &info); + ASSERT_EQ(0, rv); + } +} From edc8ea8a3169876a0483ee2f9e9d543f5b234e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Spr=C3=A5ng?= Date: Thu, 9 Jul 2026 15:12:28 +0200 Subject: [PATCH 2/3] Reduce frame size in dynamic slicing test to prevent buffer overflow --- test/api/encoder_test.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/api/encoder_test.cpp b/test/api/encoder_test.cpp index f1a81a7877..af4867a1eb 100644 --- a/test/api/encoder_test.cpp +++ b/test/api/encoder_test.cpp @@ -273,7 +273,7 @@ TEST_F(EncoderInitTest, DynamicAdjustSlicingExtremeAspectRatio) { // Extreme wide aspect ratio: frame height is only 2 macroblock rows (32 // pixels), which tests slicing behavior when row-based heuristics exceed // frame dimensions. - param.iPicWidth = 32752; + param.iPicWidth = 8192; param.iPicHeight = 32; param.fMaxFrameRate = 30.0f; param.iSpatialLayerNum = 1; @@ -324,11 +324,11 @@ TEST_F(EncoderInitTest, DynamicAdjustSlicingExtremeAspectRatio) { buf.data()[idx] = rand() % 256; } for (int y = 0; y < 16; y++) { - memset(pic.pData[0] + y * pic.iStride[0], 0, 16368); + memset(pic.pData[0] + y * pic.iStride[0], 0, 4096); } for (int y = 0; y < 8; y++) { - memset(pic.pData[1] + y * pic.iStride[1], 0, 8184); - memset(pic.pData[2] + y * pic.iStride[2], 0, 8184); + memset(pic.pData[1] + y * pic.iStride[1], 0, 2048); + memset(pic.pData[2] + y * pic.iStride[2], 0, 2048); } rv = encoder_->EncodeFrame(&pic, &info); ASSERT_EQ(0, rv); From 43ca267959bf6059ea090d27e8e7a5ec6cf0aa10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Spr=C3=A5ng?= Date: Thu, 9 Jul 2026 15:33:02 +0200 Subject: [PATCH 3/3] Allow slices to expand to full thread buffer size under dynamic load balancing --- codec/encoder/core/src/svc_encode_slice.cpp | 1 + test/api/encoder_test.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/codec/encoder/core/src/svc_encode_slice.cpp b/codec/encoder/core/src/svc_encode_slice.cpp index 196f7d09ba..db550a157b 100644 --- a/codec/encoder/core/src/svc_encode_slice.cpp +++ b/codec/encoder/core/src/svc_encode_slice.cpp @@ -1026,6 +1026,7 @@ int32_t InitOneSliceInThread (sWelsEncCtx* pCtx, pSlice->sSliceBs.uiBsPos = 0; pSlice->sSliceBs.iNalIndex = 0; pSlice->sSliceBs.pBsBuffer = pCtx->pSliceThreading->pThreadBsBuffer[kiSlcBuffIdx]; + pSlice->sSliceBs.uiSize = pCtx->iFrameBsSize; return ENC_RETURN_SUCCESS; } diff --git a/test/api/encoder_test.cpp b/test/api/encoder_test.cpp index af4867a1eb..f1a81a7877 100644 --- a/test/api/encoder_test.cpp +++ b/test/api/encoder_test.cpp @@ -273,7 +273,7 @@ TEST_F(EncoderInitTest, DynamicAdjustSlicingExtremeAspectRatio) { // Extreme wide aspect ratio: frame height is only 2 macroblock rows (32 // pixels), which tests slicing behavior when row-based heuristics exceed // frame dimensions. - param.iPicWidth = 8192; + param.iPicWidth = 32752; param.iPicHeight = 32; param.fMaxFrameRate = 30.0f; param.iSpatialLayerNum = 1; @@ -324,11 +324,11 @@ TEST_F(EncoderInitTest, DynamicAdjustSlicingExtremeAspectRatio) { buf.data()[idx] = rand() % 256; } for (int y = 0; y < 16; y++) { - memset(pic.pData[0] + y * pic.iStride[0], 0, 4096); + memset(pic.pData[0] + y * pic.iStride[0], 0, 16368); } for (int y = 0; y < 8; y++) { - memset(pic.pData[1] + y * pic.iStride[1], 0, 2048); - memset(pic.pData[2] + y * pic.iStride[2], 0, 2048); + memset(pic.pData[1] + y * pic.iStride[1], 0, 8184); + memset(pic.pData[2] + y * pic.iStride[2], 0, 8184); } rv = encoder_->EncodeFrame(&pic, &info); ASSERT_EQ(0, rv);