Commit d91f567b for openh264

commit d91f567beb58dd979c380418b32674ac4ce8dc03
Author: Erik Språng <sprang@google.com>
Date:   Tue Jul 14 07:56:39 2026 +0200

    Fix dynamic slice adjustment for frames with extreme aspect ratios (#3960)

    * 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.

    * Reduce frame size in dynamic slicing test to prevent buffer overflow

    * Allow slices to expand to full thread buffer size under dynamic load balancing

    ---------

    Co-authored-by: Erik Språng <sprang@webrtc.org>

diff --git a/codec/encoder/core/src/slice_multi_threading.cpp b/codec/encoder/core/src/slice_multi_threading.cpp
index 09ea2c08..7fd99929 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 5a9bf739..a920de7f 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<int32_t>(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/codec/encoder/core/src/svc_encode_slice.cpp b/codec/encoder/core/src/svc_encode_slice.cpp
index 196f7d09..db550a15 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 ebe4a039..f1a81a78 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(&param);
+
+  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(&param);
+  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);
+  }
+}