Commit e22b2731 for openh264
commit e22b2731a3cc116177612eaa3a3f7628dfc00a42
Author: Erik Språng <sprang@google.com>
Date: Tue Jul 21 10:36:31 2026 +0200
Fix asymmetric chroma plane stride handling in encoder preprocessing (#3961)
* Fix asymmetric chroma plane stride handling in encoder preprocessing
When preprocessing incoming I420 source frames, WelsMoveMemoryWrapper
previously read only the U chroma plane stride (iStride[1]) and used it as a
shared stride for copying both the U and V chroma planes. If an incoming
frame has distinct, asymmetric strides for the U and V chroma planes (for
example, when the U plane stride is larger than the V plane stride), copying
the V plane using the U plane's stride advances the source pointer incorrectly.
This change ensures correct memory advancement for each chroma plane by:
- Modifying WelsMoveMemory_c to accept separate destination and source
strides for the U and V chroma planes (iDstStrideU, iDstStrideV,
iSrcStrideU, iSrcStrideV).
- Updating WelsMoveMemoryWrapper to read and pass iStride[1] and iStride[2]
separately from the input SSourcePicture.
- Updating internal callers (DownsamplePadding and Padding) to pass their
respective U and V line sizes cleanly.
- Adding a unit test (CustomChromaPlaneStrides) to verify that distinct,
asymmetric chroma plane strides are handled correctly.
* Add chroma stride validation guards and strengthen unit test verification
---------
Co-authored-by: Erik Språng <sprang@webrtc.org>
diff --git a/codec/encoder/core/inc/wels_preprocess.h b/codec/encoder/core/inc/wels_preprocess.h
index 4d1cd8ef..7d4db94d 100644
--- a/codec/encoder/core/inc/wels_preprocess.h
+++ b/codec/encoder/core/inc/wels_preprocess.h
@@ -127,7 +127,7 @@ class CWelsPreProcess {
int32_t WelsPreprocessReset (sWelsEncCtx* pEncCtx, int32_t iWidth, int32_t iHeight);
int32_t AllocSpatialPictures (sWelsEncCtx* pCtx, SWelsSvcCodingParam* pParam);
void FreeSpatialPictures (sWelsEncCtx* pCtx);
- int32_t BuildSpatialPicList (sWelsEncCtx* pEncCtx, const SSourcePicture* kpSrcPic);
+ int32_t BuildSpatialPicList (sWelsEncCtx* pEncCtx, const SSourcePicture* kpSrcPic, int32_t* pSpatialNum);
int32_t AnalyzeSpatialPic (sWelsEncCtx* pEncCtx, const int32_t kiDIdx);
int32_t UpdateSpatialPictures (sWelsEncCtx* pEncCtx, SWelsSvcCodingParam* pParam, const int8_t iCurTid,
const int32_t d_idx);
@@ -157,7 +157,7 @@ class CWelsPreProcess {
int32_t InitLastSpatialPictures (sWelsEncCtx* pEncCtx);
private:
- int32_t SingleLayerPreprocess (sWelsEncCtx* pEncCtx, const SSourcePicture* kpSrc, Scaled_Picture* m_sScaledPicture);
+ int32_t SingleLayerPreprocess (sWelsEncCtx* pEncCtx, const SSourcePicture* kpSrc, Scaled_Picture* m_sScaledPicture, int32_t* pSpatialNum);
void BilateralDenoising (SPicture* pSrc, const int32_t iWidth, const int32_t iHeight);
@@ -175,7 +175,7 @@ class CWelsPreProcess {
int32_t ColorspaceConvert (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic, const SSourcePicture* kpSrc,
const int32_t kiWidth, const int32_t kiHeight);
- void WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic, const SSourcePicture* kpSrc,
+ int32_t WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic, const SSourcePicture* kpSrc,
const int32_t kiWidth, const int32_t kiHeight);
/*!
diff --git a/codec/encoder/core/src/encoder_ext.cpp b/codec/encoder/core/src/encoder_ext.cpp
index c67a6ad2..228a67dc 100644
--- a/codec/encoder/core/src/encoder_ext.cpp
+++ b/codec/encoder/core/src/encoder_ext.cpp
@@ -3486,10 +3486,10 @@ int32_t WelsEncoderEncodeExt (sWelsEncCtx* pCtx, SFrameBSInfo* pFbi, const SSour
pFbi->sLayerInfo[iNalIdx].iNalCount = 0;
}
// perform csc/denoise/downsample/padding, generate spatial layers
- iSpatialNum = pCtx->pVpp->BuildSpatialPicList (pCtx, pSrcPic);
- if (iSpatialNum == -1) {
- WelsLog (& (pCtx->sLogCtx), WELS_LOG_ERROR, "Failed in allocating memory in BuildSpatialPicList");
- return ENC_RETURN_MEMALLOCERR;
+ int32_t iRet = pCtx->pVpp->BuildSpatialPicList (pCtx, pSrcPic, &iSpatialNum);
+ if (iRet != ENC_RETURN_SUCCESS) {
+ WelsLog (& (pCtx->sLogCtx), WELS_LOG_ERROR, "Failed in BuildSpatialPicList, error=%d", iRet);
+ return iRet;
}
if (pCtx->pFuncList->pfRc.pfWelsUpdateMaxBrWindowStatus) {
diff --git a/codec/encoder/core/src/wels_preprocess.cpp b/codec/encoder/core/src/wels_preprocess.cpp
index e9e81fc8..d3376974 100644
--- a/codec/encoder/core/src/wels_preprocess.cpp
+++ b/codec/encoder/core/src/wels_preprocess.cpp
@@ -56,9 +56,11 @@ namespace WelsEnc {
int32_t WelsInitScaledPic (SWelsSvcCodingParam* pParam, Scaled_Picture* pScaledPic, CMemoryAlign* pMemoryAlign);
bool JudgeNeedOfScaling (SWelsSvcCodingParam* pParam, Scaled_Picture* pScaledPic);
void FreeScaledPic (Scaled_Picture* pScaledPic, CMemoryAlign* pMemoryAlign);
-void WelsMoveMemory_c (uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV, int32_t iDstStrideY, int32_t iDstStrideUV,
- uint8_t* pSrcY, uint8_t* pSrcU, uint8_t* pSrcV, int32_t iSrcStrideY, int32_t iSrcStrideUV, int32_t iWidth,
- int32_t iHeight);
+void WelsMoveMemory_c(uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV,
+ int32_t iDstStrideY, int32_t iDstStrideU,
+ int32_t iDstStrideV, uint8_t* pSrcY, uint8_t* pSrcU,
+ uint8_t* pSrcV, int32_t iSrcStrideY, int32_t iSrcStrideU,
+ int32_t iSrcStrideV, int32_t iWidth, int32_t iHeight);
//******* table definition ***********************************************************************//
const uint8_t g_kuiRefTemporalIdx[MAX_TEMPORAL_LEVEL][MAX_GOP_SIZE] = {
@@ -215,18 +217,18 @@ void CWelsPreProcess::FreeSpatialPictures (sWelsEncCtx* pCtx) {
}
}
-int32_t CWelsPreProcess::BuildSpatialPicList (sWelsEncCtx* pCtx, const SSourcePicture* kpSrcPic) {
+int32_t CWelsPreProcess::BuildSpatialPicList (sWelsEncCtx* pCtx, const SSourcePicture* kpSrcPic, int32_t* pSpatialNum) {
SWelsSvcCodingParam* pSvcParam = pCtx->pSvcParam;
- int32_t iSpatialNum = 0;
int32_t iWidth = ((kpSrcPic->iPicWidth >> 1) << 1);
int32_t iHeight = ((kpSrcPic->iPicHeight >> 1) << 1);
+ *pSpatialNum = 0;
if (!m_bInitDone) {
if (WelsPreprocessCreate() != 0)
- return -1;
+ return ENC_RETURN_MEMALLOCERR;
if (WelsPreprocessReset (pCtx, iWidth, iHeight) != 0)
- return -1;
+ return ENC_RETURN_MEMALLOCERR;
m_iAvaliableRefInSpatialPicList = pSvcParam->iNumRefFrame;
@@ -234,18 +236,21 @@ int32_t CWelsPreProcess::BuildSpatialPicList (sWelsEncCtx* pCtx, const SSourcePi
} else {
if ((iWidth != pSvcParam->SUsedPicRect.iWidth) || (iHeight != pSvcParam->SUsedPicRect.iHeight)) {
if (WelsPreprocessReset (pCtx, iWidth, iHeight) != 0)
- return -1;
+ return ENC_RETURN_MEMALLOCERR;
}
}
if (m_pInterfaceVp == NULL)
- return -1;
+ return ENC_RETURN_MEMALLOCERR;
pCtx->pVaa->bSceneChangeFlag = pCtx->pVaa->bIdrPeriodFlag = false;
- iSpatialNum = SingleLayerPreprocess (pCtx, kpSrcPic, &m_sScaledPicture);
+ int32_t iRet = SingleLayerPreprocess (pCtx, kpSrcPic, &m_sScaledPicture, pSpatialNum);
+ if (iRet != ENC_RETURN_SUCCESS) {
+ return iRet;
+ }
- return iSpatialNum;
+ return ENC_RETURN_SUCCESS;
}
SPicture* CWelsPreProcess::GetBestRefPic (EUsageType iUsageType, bool bSceneLtr, EWelsSliceType eSliceType,
@@ -344,7 +349,7 @@ int32_t CWelsPreProcess::UpdateSpatialPictures (sWelsEncCtx* pCtx, SWelsSvcCodin
* @return: exact number of spatial layers need to encoder indeed
*/
int32_t CWelsPreProcess::SingleLayerPreprocess (sWelsEncCtx* pCtx, const SSourcePicture* kpSrc,
- Scaled_Picture* pScaledPicture) {
+ Scaled_Picture* pScaledPicture, int32_t* pSpatialNum) {
SWelsSvcCodingParam* pSvcParam = pCtx->pSvcParam;
int8_t iDependencyId = pSvcParam->iSpatialLayerNum - 1;
@@ -377,10 +382,13 @@ int32_t CWelsPreProcess::SingleLayerPreprocess (sWelsEncCtx* pCtx, const SSource
}
}
+ *pSpatialNum = 0;
pSrcPic = pScaledPicture->pScaledInputPicture ? pScaledPicture->pScaledInputPicture : GetCurrentOrigFrame (
iDependencyId);
-
- WelsMoveMemoryWrapper (pSvcParam, pSrcPic, kpSrc, iSrcWidth, iSrcHeight);
+ int32_t iRet = WelsMoveMemoryWrapper (pSvcParam, pSrcPic, kpSrc, iSrcWidth, iSrcHeight);
+ if (iRet != ENC_RETURN_SUCCESS) {
+ return iRet;
+ }
if (pSvcParam->bEnableDenoise)
BilateralDenoising (pSrcPic, iSrcWidth, iSrcHeight);
@@ -471,8 +479,8 @@ int32_t CWelsPreProcess::SingleLayerPreprocess (sWelsEncCtx* pCtx, const SSource
}
}
- return iSpatialNum;
-
+ *pSpatialNum = iSpatialNum;
+ return ENC_RETURN_SUCCESS;
}
@@ -656,9 +664,11 @@ int32_t CWelsPreProcess::DownsamplePadding (SPicture* pSrc, SPicture* pDstPic,
if (iSrcWidth != iShrinkWidth || iSrcHeight != iShrinkHeight) {
iRet = m_pInterfaceVp->Process (iMethodIdx, &sSrcPixMap, &sDstPicMap);
} else {
- WelsMoveMemory_c (pDstPic->pData[0], pDstPic->pData[1], pDstPic->pData[2], pDstPic->iLineSize[0], pDstPic->iLineSize[1],
- pSrc->pData[0], pSrc->pData[1], pSrc->pData[2], pSrc->iLineSize[0], pSrc->iLineSize[1],
- iSrcWidth, iSrcHeight);
+ WelsMoveMemory_c(pDstPic->pData[0], pDstPic->pData[1], pDstPic->pData[2],
+ pDstPic->iLineSize[0], pDstPic->iLineSize[1],
+ pDstPic->iLineSize[2], pSrc->pData[0], pSrc->pData[1],
+ pSrc->pData[2], pSrc->iLineSize[0], pSrc->iLineSize[1],
+ pSrc->iLineSize[2], iSrcWidth, iSrcHeight);
}
} else {
memcpy (&sDstPicMap, &sSrcPixMap, sizeof (sDstPicMap)); // confirmed_safe_unsafe_usage
@@ -1371,9 +1381,11 @@ void* WelsMemset (void* p, int32_t val, uint32_t uiSize) {
}
//i420_to_i420_c
-void WelsMoveMemory_c (uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV, int32_t iDstStrideY, int32_t iDstStrideUV,
- uint8_t* pSrcY, uint8_t* pSrcU, uint8_t* pSrcV, int32_t iSrcStrideY, int32_t iSrcStrideUV, int32_t iWidth,
- int32_t iHeight) {
+void WelsMoveMemory_c(uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV,
+ int32_t iDstStrideY, int32_t iDstStrideU,
+ int32_t iDstStrideV, uint8_t* pSrcY, uint8_t* pSrcU,
+ uint8_t* pSrcV, int32_t iSrcStrideY, int32_t iSrcStrideU,
+ int32_t iSrcStrideV, int32_t iWidth, int32_t iHeight) {
int32_t iWidth2 = iWidth >> 1;
int32_t iHeight2 = iHeight >> 1;
int32_t j;
@@ -1387,18 +1399,18 @@ void WelsMoveMemory_c (uint8_t* pDstY, uint8_t* pDstU, uint8_t* pDstV, int32_t
for (j = iHeight2; j; j--) {
WelsMemcpy (pDstU, pSrcU, iWidth2);
WelsMemcpy (pDstV, pSrcV, iWidth2);
- pDstU += iDstStrideUV;
- pDstV += iDstStrideUV;
- pSrcU += iSrcStrideUV;
- pSrcV += iSrcStrideUV;
+ pDstU += iDstStrideU;
+ pDstV += iDstStrideV;
+ pSrcU += iSrcStrideU;
+ pSrcV += iSrcStrideV;
}
}
-void CWelsPreProcess::WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic,
+int32_t CWelsPreProcess::WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SPicture* pDstPic,
const SSourcePicture* kpSrc,
const int32_t kiTargetWidth, const int32_t kiTargetHeight) {
if (VIDEO_FORMAT_I420 != (kpSrc->iColorFormat & (~VIDEO_FORMAT_VFlip)))
- return;
+ return ENC_RETURN_INVALIDINPUT;
int32_t iSrcWidth = kpSrc->iPicWidth;
int32_t iSrcHeight = kpSrc->iPicHeight;
@@ -1423,40 +1435,47 @@ void CWelsPreProcess::WelsMoveMemoryWrapper (SWelsSvcCodingParam* pSvcParam, SP
uint8_t* pSrcU = kpSrc->pData[1] + iSrcOffset[1];
uint8_t* pSrcV = kpSrc->pData[2] + iSrcOffset[2];
const int32_t kiSrcStrideY = kpSrc->iStride[0];
- const int32_t kiSrcStrideUV = kpSrc->iStride[1];
+ const int32_t kiSrcStrideU = kpSrc->iStride[1];
+ const int32_t kiSrcStrideV = kpSrc->iStride[2];
uint8_t* pDstY = pDstPic->pData[0];
uint8_t* pDstU = pDstPic->pData[1];
uint8_t* pDstV = pDstPic->pData[2];
const int32_t kiDstStrideY = pDstPic->iLineSize[0];
- const int32_t kiDstStrideUV = pDstPic->iLineSize[1];
+ const int32_t kiDstStrideU = pDstPic->iLineSize[1];
+ const int32_t kiDstStrideV = pDstPic->iLineSize[2];
if (pSrcY) {
if (iSrcWidth <= 0 || iSrcHeight <= 0 || (iSrcWidth * iSrcHeight > (MAX_MBS_PER_FRAME << 8)))
- return;
- if (kiSrcTopOffsetY >= iSrcHeight || kiSrcLeftOffsetY >= iSrcWidth || iSrcWidth > kiSrcStrideY)
- return;
+ return ENC_RETURN_INVALIDINPUT;
+ if (kiSrcTopOffsetY >= iSrcHeight || kiSrcLeftOffsetY >= iSrcWidth || iSrcWidth > kiSrcStrideY
+ || (iSrcWidth >> 1) > kiSrcStrideU || (iSrcWidth >> 1) > kiSrcStrideV)
+ return ENC_RETURN_INVALIDINPUT;
}
if (pDstY) {
if (kiTargetWidth <= 0 || kiTargetHeight <= 0 || (kiTargetWidth * kiTargetHeight > (MAX_MBS_PER_FRAME << 8)))
- return;
- if (kiTargetWidth > kiDstStrideY)
- return;
+ return ENC_RETURN_INVALIDINPUT;
+ if (kiTargetWidth > kiDstStrideY || (kiTargetWidth >> 1) > kiDstStrideU || (kiTargetWidth >> 1) > kiDstStrideV)
+ return ENC_RETURN_INVALIDINPUT;
}
if (pSrcY == NULL || pSrcU == NULL || pSrcV == NULL || pDstY == NULL || pDstU == NULL || pDstV == NULL
|| (iSrcWidth & 1) || (iSrcHeight & 1)) {
+ return ENC_RETURN_INVALIDINPUT;
} else {
//i420_to_i420_c
- WelsMoveMemory_c (pDstY, pDstU, pDstV, kiDstStrideY, kiDstStrideUV,
- pSrcY, pSrcU, pSrcV, kiSrcStrideY, kiSrcStrideUV, iSrcWidth, iSrcHeight);
+ WelsMoveMemory_c(pDstY, pDstU, pDstV, kiDstStrideY, kiDstStrideU,
+ kiDstStrideV, pSrcY, pSrcU, pSrcV, kiSrcStrideY,
+ kiSrcStrideU, kiSrcStrideV, iSrcWidth, iSrcHeight);
//in VP Process
if (kiTargetWidth > iSrcWidth || kiTargetHeight > iSrcHeight) {
- Padding (pDstY, pDstU, pDstV, kiDstStrideY, kiDstStrideUV, iSrcWidth, kiTargetWidth, iSrcHeight, kiTargetHeight);
+ Padding(pDstY, pDstU, pDstV, kiDstStrideY, kiDstStrideU, iSrcWidth,
+ kiTargetWidth, iSrcHeight, kiTargetHeight);
}
}
+ return ENC_RETURN_SUCCESS;
}
bool CWelsPreProcess::GetSceneChangeFlag (ESceneChangeIdc eSceneChangeIdc) {
diff --git a/codec/encoder/plus/src/welsEncoderExt.cpp b/codec/encoder/plus/src/welsEncoderExt.cpp
index 7c85f232..aca94ecc 100644
--- a/codec/encoder/plus/src/welsEncoderExt.cpp
+++ b/codec/encoder/plus/src/welsEncoderExt.cpp
@@ -418,6 +418,10 @@ int CWelsH264SVCEncoder ::EncodeFrameInternal (const SSourcePicture* pSrcPic, S
kiEncoderReturn);
WelsUninitEncoderExt (&m_pEncContext);
return cmMallocMemeError;
+ } else if (kiEncoderReturn == ENC_RETURN_INVALIDINPUT) {
+ WelsLog (&m_pWelsTrace->m_sLogCtx, WELS_LOG_ERROR, "CWelsH264SVCEncoder::EncodeFrame() invalid input, err=%d",
+ kiEncoderReturn);
+ return cmUnsupportedData;
} else if ((kiEncoderReturn != ENC_RETURN_SUCCESS) && (kiEncoderReturn == ENC_RETURN_CORRECTED)) {
WelsLog (&m_pWelsTrace->m_sLogCtx, WELS_LOG_ERROR, "unexpected return(%d) from EncodeFrameInternal()!",
kiEncoderReturn);
diff --git a/test/api/encoder_test.cpp b/test/api/encoder_test.cpp
index 94c085ed..69e877b5 100644
--- a/test/api/encoder_test.cpp
+++ b/test/api/encoder_test.cpp
@@ -4,6 +4,41 @@
#include "utils/BufferedData.h"
#include <string>
#include <cstdlib>
+#include <cmath>
+
+static void GeneratePattern(uint8_t* yBuf, int yStride, uint8_t* uBuf, int uStride, uint8_t* vBuf, int vStride, int width, int height) {
+ for (int y = 0; y < height; ++y) {
+ for (int x = 0; x < width; ++x) {
+ if (x < yStride) {
+ yBuf[y * yStride + x] = (x * 4 + y * 4) % 256;
+ }
+ }
+ }
+ for (int y = 0; y < (height >> 1); ++y) {
+ for (int x = 0; x < (width >> 1); ++x) {
+ if (x < uStride) {
+ uBuf[y * uStride + x] = (x * 8) % 256;
+ }
+ if (x < vStride) {
+ vBuf[y * vStride + x] = (y * 8) % 256;
+ }
+ }
+ }
+}
+
+static double CalculatePlanePsnr(const uint8_t* ref, int refStride, const uint8_t* test, int testStride, int width, int height) {
+ double mse = 0;
+ int compareWidth = std::min(width, std::min(refStride, testStride));
+ for (int y = 0; y < height; ++y) {
+ for (int x = 0; x < compareWidth; ++x) {
+ double diff = ref[y * refStride + x] - test[y * testStride + x];
+ mse += diff * diff;
+ }
+ }
+ mse /= (compareWidth * height);
+ if (mse == 0) return 99.0;
+ return 10.0 * log10((255.0 * 255.0) / mse);
+}
static void UpdateHashFromFrame (const SFrameBSInfo& info, SHA1Context* ctx) {
for (int i = 0; i < info.iLayerNum; ++i) {
@@ -351,3 +386,176 @@ TEST_F(EncoderInitTest, ScreenContentScrollMotionVectorBounds) {
rv = encoder_->EncodeFrame(&pic, &info);
ASSERT_EQ(0, rv);
}
+
+// This test verifies that the encoder correctly handles frames with distinct,
+// asymmetric strides for the U and V chroma planes (e.g., when U stride is much
+// larger than V stride, or vice versa). It ensures that memory copy routines
+// advance each chroma plane pointer by its own stride without invalid memory
+// access.
+TEST_F(EncoderInitTest, CustomChromaPlaneStrides) {
+ SEncParamExt param;
+ encoder_->GetDefaultParams(¶m);
+
+ param.iUsageType = CAMERA_VIDEO_REAL_TIME;
+ param.iPicWidth = 64;
+ param.iPicHeight = 64;
+ param.fMaxFrameRate = 30.0f;
+ param.iSpatialLayerNum = 1;
+ param.iRCMode = RC_OFF_MODE;
+
+ param.sSpatialLayers[0].iVideoWidth = param.iPicWidth;
+ param.sSpatialLayers[0].iVideoHeight = param.iPicHeight;
+ param.sSpatialLayers[0].fFrameRate = param.fMaxFrameRate;
+ param.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SINGLE_SLICE;
+ param.sSpatialLayers[0].iDLayerQp = 0;
+
+ int rv = encoder_->InitializeExt(¶m);
+ ASSERT_EQ(0, rv);
+
+ // Initialize a decoder to verify correctness of the output
+ ISVCDecoder* decoder = nullptr;
+ rv = WelsCreateDecoder(&decoder);
+ ASSERT_EQ(0, rv);
+ ASSERT_TRUE(decoder != nullptr);
+
+ SDecodingParam decParam;
+ memset(&decParam, 0, sizeof(SDecodingParam));
+ decParam.uiTargetDqLayer = UCHAR_MAX;
+ decParam.eEcActiveIdc = ERROR_CON_SLICE_COPY;
+ decParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_DEFAULT;
+
+ rv = decoder->Initialize(&decParam);
+ ASSERT_EQ(0, rv);
+
+ SFrameBSInfo info;
+ memset(&info, 0, sizeof(SFrameBSInfo));
+
+ // Configure distinct strides: U stride is much larger than V stride.
+ int strideY = 64;
+ int strideU = 4096;
+ int strideV = 32;
+
+ // Generate a distinct pattern
+ std::vector<uint8_t> bufY(strideY * param.iPicHeight);
+ std::vector<uint8_t> bufU(strideU * (param.iPicHeight >> 1));
+ std::vector<uint8_t> bufV(strideV * (param.iPicHeight >> 1));
+ GeneratePattern(bufY.data(), strideY, bufU.data(), strideU, bufV.data(), strideV, param.iPicWidth, param.iPicHeight);
+
+ SSourcePicture pic;
+ memset(&pic, 0, sizeof(SSourcePicture));
+ pic.iPicWidth = param.iPicWidth;
+ pic.iPicHeight = param.iPicHeight;
+ pic.iColorFormat = videoFormatI420;
+ pic.iStride[0] = strideY;
+ pic.iStride[1] = strideU;
+ pic.iStride[2] = strideV;
+ pic.pData[0] = bufY.data();
+ pic.pData[1] = bufU.data();
+ pic.pData[2] = bufV.data();
+
+ rv = encoder_->EncodeFrame(&pic, &info);
+ ASSERT_EQ(0, rv);
+
+ // Calculate total bitstream size
+ int len = 0;
+ for (int i = 0; i < info.iLayerNum; ++i) {
+ const SLayerBSInfo& layerInfo = info.sLayerInfo[i];
+ for (int j = 0; j < layerInfo.iNalCount; ++j) {
+ len += layerInfo.pNalLengthInByte[j];
+ }
+ }
+ ASSERT_GT(len, 0);
+
+ // Decode the encoded frame
+ unsigned char* pData[3] = {nullptr};
+ SBufferInfo dstBufInfo;
+ memset(&dstBufInfo, 0, sizeof(SBufferInfo));
+
+ rv = decoder->DecodeFrame2(info.sLayerInfo[0].pBsBuf, len, pData, &dstBufInfo);
+ ASSERT_EQ(0, rv);
+
+ if (dstBufInfo.iBufferStatus == 0) {
+ rv = decoder->DecodeFrame2(nullptr, 0, pData, &dstBufInfo);
+ ASSERT_EQ(0, rv);
+ }
+
+ ASSERT_EQ(1, dstBufInfo.iBufferStatus);
+
+ // Verify that the decoded YUV content matches our original pattern (high PSNR)
+ int decodedWidthU = dstBufInfo.UsrData.sSystemBuffer.iWidth >> 1;
+ int decodedHeightU = dstBufInfo.UsrData.sSystemBuffer.iHeight >> 1;
+ int strideDecY = dstBufInfo.UsrData.sSystemBuffer.iStride[0];
+ int strideDecChroma = dstBufInfo.UsrData.sSystemBuffer.iStride[1];
+
+ uint8_t* decY = pData[0];
+ uint8_t* decU = pData[1];
+ uint8_t* decV = pData[2];
+
+ ASSERT_TRUE(decY != nullptr);
+ ASSERT_TRUE(decU != nullptr);
+ ASSERT_TRUE(decV != nullptr);
+
+ double psnrY = CalculatePlanePsnr(bufY.data(), strideY, decY, strideDecY, param.iPicWidth, param.iPicHeight);
+ double psnrU = CalculatePlanePsnr(bufU.data(), strideU, decU, strideDecChroma, decodedWidthU, decodedHeightU);
+ double psnrV = CalculatePlanePsnr(bufV.data(), strideV, decV, strideDecChroma, decodedWidthU, decodedHeightU);
+
+ // With lossless QP=0, PSNR should be extremely high (effectively identical)
+ EXPECT_GT(psnrY, 40.0);
+ EXPECT_GT(psnrU, 40.0);
+ EXPECT_GT(psnrV, 40.0);
+
+ WelsDestroyDecoder(decoder);
+}
+
+// This test verifies that the encoder safely returns early and avoids invalid
+// memory access (no crash / ASan error) when the input source picture has U or
+// V strides that are smaller than the required width/2.
+TEST_F(EncoderInitTest, CustomChromaPlaneStridesInvalidSrc) {
+ SEncParamExt param;
+ encoder_->GetDefaultParams(¶m);
+
+ param.iUsageType = CAMERA_VIDEO_REAL_TIME;
+ param.iPicWidth = 64;
+ param.iPicHeight = 64;
+ param.fMaxFrameRate = 30.0f;
+ param.iSpatialLayerNum = 1;
+ param.iRCMode = RC_OFF_MODE;
+
+ param.sSpatialLayers[0].iVideoWidth = param.iPicWidth;
+ param.sSpatialLayers[0].iVideoHeight = param.iPicHeight;
+ param.sSpatialLayers[0].fFrameRate = param.fMaxFrameRate;
+ param.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SINGLE_SLICE;
+ param.sSpatialLayers[0].iDLayerQp = 0;
+
+ int rv = encoder_->InitializeExt(¶m);
+ ASSERT_EQ(0, rv);
+
+ SFrameBSInfo info;
+ memset(&info, 0, sizeof(SFrameBSInfo));
+
+ // Configure invalid strides: U stride is 16, which is smaller than width/2 (32).
+ int strideY = 64;
+ int strideU = 16; // Invalid: must be >= 32
+ int strideV = 32;
+
+ // Generate pattern (alternative pattern is not strictly needed but we'll use a simple fill)
+ std::vector<uint8_t> bufY(strideY * param.iPicHeight, 128);
+ std::vector<uint8_t> bufU(strideU * (param.iPicHeight >> 1), 100);
+ std::vector<uint8_t> bufV(strideV * (param.iPicHeight >> 1), 200);
+
+ SSourcePicture pic;
+ memset(&pic, 0, sizeof(SSourcePicture));
+ pic.iPicWidth = param.iPicWidth;
+ pic.iPicHeight = param.iPicHeight;
+ pic.iColorFormat = videoFormatI420;
+ pic.iStride[0] = strideY;
+ pic.iStride[1] = strideU;
+ pic.iStride[2] = strideV;
+ pic.pData[0] = bufY.data();
+ pic.pData[1] = bufU.data();
+ pic.pData[2] = bufV.data();
+
+ // This should reject the frame and return cmUnsupportedData
+ rv = encoder_->EncodeFrame(&pic, &info);
+ ASSERT_EQ(cmUnsupportedData, rv);
+}