Commit 960414d6 for openh264
commit 960414d6fdff1b0d4b791cbaa8a42bc370666627
Author: BenzhengZhang <140143892+BenzhengZhang@users.noreply.github.com>
Date: Tue Sep 1 11:24:23 2026 +0800
decoder: snapshot per-thread pPreviousDecodedPictureInDpb before finish signal (#3994)
Co-authored-by: benzzhan <benzzhan@cisco.com>
diff --git a/codec/decoder/core/inc/decoder_context.h b/codec/decoder/core/inc/decoder_context.h
index fe825058..4d9adebc 100644
--- a/codec/decoder/core/inc/decoder_context.h
+++ b/codec/decoder/core/inc/decoder_context.h
@@ -544,6 +544,10 @@ typedef struct tagSWelsDecThreadCtx {
SWelsDecEvent sSliceDecodeStart;
SWelsDecEvent sSliceDecodeFinish;
int32_t iPicBuffIdx; //picBuff Index
+ // Per-thread snapshot of pPreviousDecodedPictureInDpb captured before sSliceDecodeFinish
+ // is signaled. Prevents concurrent workers from overwriting the shared pLastDecPicInfo
+ // field before BufferingReadyPicture() reads it.
+ PPicture pPreviousDecodedPictureInDpb;
} SWelsDecoderThreadCTX, *PWelsDecoderThreadCTX;
static inline void ResetActiveSPSForEachLayer (PWelsDecoderContext pCtx) {
diff --git a/codec/decoder/core/src/decoder_core.cpp b/codec/decoder/core/src/decoder_core.cpp
index eb73c456..5edf7990 100644
--- a/codec/decoder/core/src/decoder_core.cpp
+++ b/codec/decoder/core/src/decoder_core.cpp
@@ -2870,6 +2870,11 @@ int32_t DecodeCurrentAccessUnit (PWelsDecoderContext pCtx, uint8_t** ppDst, SBuf
}
pCtx->pLastDecPicInfo->pPreviousDecodedPictureInDpb = pCtx->pDec; //store latest decoded picture for EC
+ if (iThreadCount > 1) {
+ // Snapshot into per-thread slot; the shared pLastDecPicInfo field can be
+ // overwritten by another worker before BufferingReadyPicture() reads it.
+ pThreadCtx->pPreviousDecodedPictureInDpb = pCtx->pDec;
+ }
pCtx->bUsedAsRef = pCtx->uiNalRefIdc > 0;
if (iThreadCount <= 1) {
if (pCtx->bUsedAsRef) {
@@ -2964,6 +2969,9 @@ bool CheckAndFinishLastPic (PWelsDecoderContext pCtx, uint8_t** ppDst, SBufferIn
DecodeFrameConstruction (pCtx, ppDst, pDstInfo);
pCtx->pLastDecPicInfo->pPreviousDecodedPictureInDpb = pCtx->pDec; //save ECed pic for future use
+ if (pCtx->pThreadCtx != NULL && GetThreadCount (pCtx) > 1) {
+ ((PWelsDecoderThreadCTX)pCtx->pThreadCtx)->pPreviousDecodedPictureInDpb = pCtx->pDec;
+ }
if (pCtx->pLastDecPicInfo->sLastNalHdrExt.sNalUnitHeader.uiNalRefIdc > 0) {
if (MarkECFrameAsRef (pCtx) == ERR_INFO_INVALID_PTR) {
pCtx->iErrorCode |= dsRefListNullPtrs;
diff --git a/codec/decoder/plus/src/welsDecoderExt.cpp b/codec/decoder/plus/src/welsDecoderExt.cpp
index 818dd3cc..55f86e49 100644
--- a/codec/decoder/plus/src/welsDecoderExt.cpp
+++ b/codec/decoder/plus/src/welsDecoderExt.cpp
@@ -1010,8 +1010,13 @@ void CWelsDecoder::BufferingReadyPicture (PWelsDecoderContext pCtx, unsigned cha
m_sPictInfoList[i].iSeqNum = pCtx->iSeqNum;
m_sPictInfoList[i].uiDecodingTimeStamp = pCtx->uiDecodingTimeStamp;
if (pCtx->pLastDecPicInfo->pPreviousDecodedPictureInDpb != NULL) {
- m_sPictInfoList[i].iPicBuffIdx = pCtx->pLastDecPicInfo->pPreviousDecodedPictureInDpb->iPicBuffIdx;
- if (GetThreadCount (pCtx) <= 1) ++pCtx->pLastDecPicInfo->pPreviousDecodedPictureInDpb->iRefCount;
+ PPicture pPrevPic = (GetThreadCount (pCtx) > 1 && pCtx->pThreadCtx != NULL)
+ ? ((PWelsDecoderThreadCTX)pCtx->pThreadCtx)->pPreviousDecodedPictureInDpb
+ : pCtx->pLastDecPicInfo->pPreviousDecodedPictureInDpb;
+ if (pPrevPic != NULL) {
+ m_sPictInfoList[i].iPicBuffIdx = pPrevPic->iPicBuffIdx;
+ if (GetThreadCount (pCtx) <= 1) ++pPrevPic->iRefCount;
+ }
}
m_iLastBufferedIdx = i;
pDstInfo->iBufferStatus = 0;
diff --git a/test/api/thread_decoder_test.cpp b/test/api/thread_decoder_test.cpp
index 2db4c20f..bf04bade 100644
--- a/test/api/thread_decoder_test.cpp
+++ b/test/api/thread_decoder_test.cpp
@@ -327,3 +327,110 @@ static const FileParam kFileParamArray[] = {
INSTANTIATE_TEST_SUITE_P (ThreadDecodeFile, ThreadDecoderOutputTest,
::testing::ValuesIn (kFileParamArray));
+
+// Regression: threaded decode of BA_MW_D.264 must produce the same SHA1 output
+// as single-thread decode. Pre-fix, the shared pPreviousDecodedPictureInDpb
+// pointer was overwritten by concurrent workers before BufferingReadyPicture()
+// read it, causing output hash divergence and occasional SIGSEGV.
+class ThreadDecoderPreviousPicRaceTest : public ::testing::Test {
+ public:
+ struct HashCbk : public BaseThreadDecoderTest::Callback {
+ SHA1Context ctx;
+ HashCbk() { SHA1Reset (&ctx); }
+ void onDecodeFrame (const BaseThreadDecoderTest::Frame& frame) override {
+ UpdateHashFromPlane (&ctx, frame.y.data, frame.y.width, frame.y.height, frame.y.stride);
+ UpdateHashFromPlane (&ctx, frame.u.data, frame.u.width, frame.u.height, frame.u.stride);
+ UpdateHashFromPlane (&ctx, frame.v.data, frame.v.width, frame.v.height, frame.v.stride);
+ }
+ std::string Digest() {
+ unsigned char d[SHA_DIGEST_LENGTH];
+ SHA1Result (&ctx, d);
+ char buf[SHA_DIGEST_LENGTH * 2 + 1];
+ for (int i = 0; i < SHA_DIGEST_LENGTH; ++i)
+ std::snprintf (buf + i * 2, 3, "%02x", d[i]);
+ return std::string (buf);
+ }
+ };
+
+ static std::string DecodeFile (const char* path, int threads) {
+ long rv = 0;
+ ISVCDecoder* dec = NULL;
+ rv = WelsCreateDecoder (&dec);
+ if (rv != 0 || dec == NULL) return "";
+ SDecodingParam p;
+ std::memset (&p, 0, sizeof (p));
+ p.uiTargetDqLayer = UCHAR_MAX;
+ p.eEcActiveIdc = ERROR_CON_SLICE_COPY;
+ p.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_DEFAULT;
+ dec->SetOption (DECODER_OPTION_NUM_OF_THREADS, &threads);
+ if (dec->Initialize (&p) != 0) { WelsDestroyDecoder (dec); return ""; }
+
+ std::ifstream f (path, std::ios::binary);
+ if (!f.is_open()) { dec->Uninitialize(); WelsDestroyDecoder (dec); return ""; }
+ std::vector<uint8_t> bs ((std::istreambuf_iterator<char> (f)), std::istreambuf_iterator<char>());
+ f.close();
+
+ HashCbk cbk;
+ uint64_t ts = 0;
+ int32_t pos = 0;
+ const int32_t sz = static_cast<int32_t> (bs.size());
+ // dst/info must outlive each DecodeFrameNoDelay call: worker threads may
+ // write through ppDst after the call returns (stack-use-after-scope otherwise).
+ unsigned char* dst[3] = {NULL, NULL, NULL};
+ SBufferInfo info;
+ while (pos < sz) {
+ int32_t fsz = ReadFrameForHangRegression (bs.data(), sz, pos);
+ if (fsz <= 0) break;
+ std::memset (dst, 0, sizeof (dst));
+ std::memset (&info, 0, sizeof (info));
+ info.uiInBsTimeStamp = ++ts;
+ dec->DecodeFrameNoDelay (bs.data() + pos, fsz, dst, &info);
+ if (info.iBufferStatus == 1) {
+ BaseThreadDecoderTest::Frame fr;
+ fr.y = {info.pDst[0], info.UsrData.sSystemBuffer.iWidth, info.UsrData.sSystemBuffer.iHeight, info.UsrData.sSystemBuffer.iStride[0]};
+ fr.u = {info.pDst[1], info.UsrData.sSystemBuffer.iWidth/2, info.UsrData.sSystemBuffer.iHeight/2, info.UsrData.sSystemBuffer.iStride[1]};
+ fr.v = {info.pDst[2], info.UsrData.sSystemBuffer.iWidth/2, info.UsrData.sSystemBuffer.iHeight/2, info.UsrData.sSystemBuffer.iStride[1]};
+ cbk.onDecodeFrame (fr);
+ }
+ pos += fsz;
+ }
+ int32_t eos = 1;
+ dec->SetOption (DECODER_OPTION_END_OF_STREAM, &eos);
+ int32_t rem = 0;
+ dec->GetOption (DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER, &rem);
+ for (int i = 0; i < rem; ++i) {
+ std::memset (dst, 0, sizeof (dst));
+ std::memset (&info, 0, sizeof (info));
+ dec->FlushFrame (dst, &info);
+ if (info.iBufferStatus == 1) {
+ BaseThreadDecoderTest::Frame fr;
+ fr.y = {info.pDst[0], info.UsrData.sSystemBuffer.iWidth, info.UsrData.sSystemBuffer.iHeight, info.UsrData.sSystemBuffer.iStride[0]};
+ fr.u = {info.pDst[1], info.UsrData.sSystemBuffer.iWidth/2, info.UsrData.sSystemBuffer.iHeight/2, info.UsrData.sSystemBuffer.iStride[1]};
+ fr.v = {info.pDst[2], info.UsrData.sSystemBuffer.iWidth/2, info.UsrData.sSystemBuffer.iHeight/2, info.UsrData.sSystemBuffer.iStride[1]};
+ cbk.onDecodeFrame (fr);
+ }
+ }
+ dec->Uninitialize();
+ WelsDestroyDecoder (dec);
+ return cbk.Digest();
+ }
+};
+
+TEST_F (ThreadDecoderPreviousPicRaceTest, ThreadedOutputIsConsistent) {
+ const char* kFile = "res/BA_MW_D.264";
+ // Run the same stream three times with the same thread count.
+ // Pre-fix, the shared pPreviousDecodedPictureInDpb could be overwritten by a
+ // concurrent worker between the write and BufferingReadyPicture(), making
+ // multi-thread output non-deterministic. Post-fix all runs must agree.
+ std::string h1 = DecodeFile (kFile, 3);
+ std::string h2 = DecodeFile (kFile, 3);
+ std::string h3 = DecodeFile (kFile, 3);
+ ASSERT_FALSE (h1.empty()) << "3-thread decode produced no output (run 1)";
+ ASSERT_FALSE (h2.empty()) << "3-thread decode produced no output (run 2)";
+ ASSERT_FALSE (h3.empty()) << "3-thread decode produced no output (run 3)";
+ EXPECT_EQ (h1, h2)
+ << "3-thread decode is non-deterministic between run 1 and run 2";
+ EXPECT_EQ (h1, h3)
+ << "3-thread decode is non-deterministic between run 1 and run 3";
+}
+
diff --git a/test/decoder/DecUT_ErrorConcealment.cpp b/test/decoder/DecUT_ErrorConcealment.cpp
index 72d1d7b2..3fedda3f 100644
--- a/test/decoder/DecUT_ErrorConcealment.cpp
+++ b/test/decoder/DecUT_ErrorConcealment.cpp
@@ -327,7 +327,7 @@ TEST (ErrorConTest, DoErrorConFrameCopyResolutionMismatchFallsBackToFill) {
pECCtx->pCtx->pCurDqLayer->sLayerInfo.sNalHeaderExt.bIdrFlag = 0;
pECCtx->pCtx->pLastDecPicInfo->pPreviousDecodedPictureInDpb = &pECCtx->sSrcPic;
- pECCtx->sSrcPic.iWidthInPixel -= 16;
+ pECCtx->sSrcPic.iWidthInPixel += 16; // use += so value stays > 0 even when iMbWidth==1
memset (pECCtx->sWelsPic.pData[0], 7, pECCtx->iMbWidth * pECCtx->iMbHeight * 256 * 3 / 2);
DoErrorConFrameCopy (pECCtx->pCtx);
@@ -352,7 +352,7 @@ TEST (ErrorConTest, DoErrorConSliceCopyResolutionMismatchFallsBackToFill) {
pECCtx->pCtx->pLastDecPicInfo->pPreviousDecodedPictureInDpb = &pECCtx->sSrcPic;
memset (pECCtx->pMbCorrectlyDecodedFlag, 0, pECCtx->iMbWidth * pECCtx->iMbHeight * sizeof (bool));
- pECCtx->sSrcPic.iHeightInPixel -= 16;
+ pECCtx->sSrcPic.iHeightInPixel += 16; // use += so value stays > 0 even when iMbHeight==1
memset (pECCtx->sWelsPic.pData[0], 7, pECCtx->iMbWidth * pECCtx->iMbHeight * 256 * 3 / 2);
DoErrorConSliceCopy (pECCtx->pCtx);