mirror of
https://github.com/ittiam-systems/libhevc.git
synced 2026-08-14 06:42:34 +07:00
500 lines
15 KiB
C++
500 lines
15 KiB
C++
/******************************************************************************
|
|
*
|
|
* Copyright (C) 2026 Ittiam Systems Pvt Ltd, Bangalore
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at:
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
******************************************************************************/
|
|
|
|
#include "DecHelper.h"
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
|
|
#include "BitsFile.h"
|
|
#include "Md5Wrapper.h"
|
|
#include "RawFile.h"
|
|
|
|
// Include internal libhevc decoder headers here inside the .cpp file to prevent
|
|
// public header pollution
|
|
// clang-format off
|
|
extern "C" {
|
|
#include "ihevc_typedefs.h"
|
|
#include "iv.h"
|
|
#include "ivd.h"
|
|
#include "ihevcd_cxa.h"
|
|
}
|
|
// clang-format on
|
|
|
|
using ::testing::Eq;
|
|
|
|
constexpr size_t kInputBufSize = 4096 * 4096;
|
|
|
|
// Standard aligned memory allocators for libhevc callbacks
|
|
static void* alignedAlloc(void* pv_ctxt, int32_t alignment, int32_t size) {
|
|
(void)pv_ctxt;
|
|
#if defined(_WIN32)
|
|
return _aligned_malloc(size, alignment);
|
|
#else
|
|
void* buf = nullptr;
|
|
if (0 != posix_memalign(&buf, alignment, size)) {
|
|
return nullptr;
|
|
}
|
|
return buf;
|
|
#endif
|
|
}
|
|
|
|
static void alignedFree(void* pv_ctxt, void* pv_buf) {
|
|
(void)pv_ctxt;
|
|
#if defined(_WIN32)
|
|
_aligned_free(pv_buf);
|
|
#else
|
|
free(pv_buf);
|
|
#endif
|
|
}
|
|
|
|
static IV_ARCH_T getDefaultArch() {
|
|
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || \
|
|
defined(_M_IX86)
|
|
return ARCH_X86_SSE42;
|
|
#elif defined(__arm__) || defined(_M_ARM)
|
|
return ARCH_ARM_A9Q;
|
|
#elif defined(__aarch64__) || defined(_M_ARM64)
|
|
return ARCH_ARMV8_GENERIC;
|
|
#else
|
|
return ARCH_ARM_NONEON;
|
|
#endif
|
|
}
|
|
|
|
// Maps high-level Arch enum to stable native Ittiam API architecture values
|
|
static IV_ARCH_T getNativeArch(libhevc::test::Arch arch) {
|
|
switch (arch) {
|
|
case libhevc::test::Arch::defaultArch:
|
|
return getDefaultArch();
|
|
case libhevc::test::Arch::generic:
|
|
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || \
|
|
defined(_M_IX86)
|
|
return ARCH_X86_GENERIC;
|
|
#else // defined(__arm__) || defined(_M_ARM) || defined(__aarch64__) ||
|
|
// defined(_M_ARM64)
|
|
return ARCH_ARM_NONEON;
|
|
#endif
|
|
case libhevc::test::Arch::armv7:
|
|
return ARCH_ARM_A9Q;
|
|
case libhevc::test::Arch::armv8:
|
|
return ARCH_ARMV8_GENERIC;
|
|
case libhevc::test::Arch::x86_sse42:
|
|
return ARCH_X86_SSE42;
|
|
case libhevc::test::Arch::x86_avx2:
|
|
return ARCH_X86_AVX2;
|
|
}
|
|
return ARCH_ARM_NONEON;
|
|
}
|
|
|
|
// Helper to set decoder configuration parameters and decode mode
|
|
static bool setDecoderConfig(void* codec, IVD_VIDEO_DECODE_MODE_T decMode,
|
|
size_t dispWd) {
|
|
ihevcd_cxa_ctl_set_config_ip_t setConfigIp = {};
|
|
ihevcd_cxa_ctl_set_config_op_t setConfigOp = {};
|
|
setConfigIp.s_ivd_ctl_set_config_ip_t.e_cmd = IVD_CMD_VIDEO_CTL;
|
|
setConfigIp.s_ivd_ctl_set_config_ip_t.e_sub_cmd = IVD_CMD_CTL_SETPARAMS;
|
|
setConfigIp.s_ivd_ctl_set_config_ip_t.u4_disp_wd = dispWd;
|
|
setConfigIp.s_ivd_ctl_set_config_ip_t.e_frm_skip_mode = IVD_SKIP_NONE;
|
|
setConfigIp.s_ivd_ctl_set_config_ip_t.e_frm_out_mode = IVD_DISPLAY_FRAME_OUT;
|
|
setConfigIp.s_ivd_ctl_set_config_ip_t.e_vid_dec_mode = decMode;
|
|
setConfigIp.s_ivd_ctl_set_config_ip_t.u4_size =
|
|
sizeof(ihevcd_cxa_ctl_set_config_ip_t);
|
|
setConfigOp.s_ivd_ctl_set_config_op_t.u4_size =
|
|
sizeof(ihevcd_cxa_ctl_set_config_op_t);
|
|
|
|
IV_API_CALL_STATUS_T ret = ihevcd_cxa_api_function(
|
|
static_cast<iv_obj_t*>(codec), &setConfigIp, &setConfigOp);
|
|
return (ret == IV_SUCCESS);
|
|
}
|
|
|
|
std::unique_ptr<DecHelper> DecHelper::Builder::build() {
|
|
std::unique_ptr<DecHelper> helper(new DecHelper());
|
|
helper->mFormat = mFormat;
|
|
helper->mCores = mCores;
|
|
helper->mArch = mArch;
|
|
helper->mInputFilePath = mInputFilePath;
|
|
helper->mOutputFilePath = mOutputFilePath;
|
|
helper->mRefMd5Path = mRefMd5Path;
|
|
|
|
// Load and parse expected reference MD5s if the path is provided
|
|
if (mRefMd5Path.has_value()) {
|
|
std::ifstream md5File(*mRefMd5Path);
|
|
if (!md5File.is_open()) {
|
|
return nullptr;
|
|
}
|
|
std::string line;
|
|
while (std::getline(md5File, line)) {
|
|
if (!line.empty()) {
|
|
std::stringstream ss(line);
|
|
std::string md5;
|
|
ss >> md5;
|
|
if (!md5.empty()) {
|
|
helper->mRefMd5s.push_back(md5);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Open input bitstream file
|
|
if (!mInputFilePath.has_value() ||
|
|
!helper->mBitsFile.open(*mInputFilePath, true)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Open output YUV file if path is provided
|
|
if (mOutputFilePath.has_value()) {
|
|
if (!helper->mOutFile.open(*mOutputFilePath, false)) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
// Initialize native decoder (allocates mInputBuf internally)
|
|
if (!helper->initDecoder()) {
|
|
return nullptr;
|
|
}
|
|
return helper;
|
|
}
|
|
|
|
bool DecHelper::initDecoder() {
|
|
ihevcd_cxa_create_ip_t createIp = {};
|
|
ihevcd_cxa_create_op_t createOp = {};
|
|
|
|
createIp.s_ivd_create_ip_t.e_cmd = IVD_CMD_CREATE;
|
|
createIp.s_ivd_create_ip_t.u4_share_disp_buf =
|
|
0; // Non-sharing mode is safer for tests
|
|
|
|
IV_COLOR_FORMAT_T colorFmt = IV_YUV_420P;
|
|
switch (mFormat) {
|
|
case libhevc::test::Format::yuv400p:
|
|
colorFmt = IV_GRAY;
|
|
break;
|
|
case libhevc::test::Format::yuv420p:
|
|
colorFmt = IV_YUV_420P;
|
|
break;
|
|
case libhevc::test::Format::yuv420sp_uv:
|
|
colorFmt = IV_YUV_420SP_UV;
|
|
break;
|
|
case libhevc::test::Format::yuv420sp_vu:
|
|
colorFmt = IV_YUV_420SP_VU;
|
|
break;
|
|
case libhevc::test::Format::yuv422p:
|
|
colorFmt = IV_YUV_422P;
|
|
break;
|
|
case libhevc::test::Format::yuv444p:
|
|
colorFmt = IV_YUV_444P;
|
|
break;
|
|
}
|
|
createIp.s_ivd_create_ip_t.e_output_format = colorFmt;
|
|
createIp.s_ivd_create_ip_t.pf_aligned_alloc = alignedAlloc;
|
|
createIp.s_ivd_create_ip_t.pf_aligned_free = alignedFree;
|
|
createIp.s_ivd_create_ip_t.pv_mem_ctxt = nullptr;
|
|
createIp.s_ivd_create_ip_t.u4_size = sizeof(ihevcd_cxa_create_ip_t);
|
|
createOp.s_ivd_create_op_t.u4_size = sizeof(ihevcd_cxa_create_op_t);
|
|
|
|
createIp.u4_enable_frame_info = 0;
|
|
createIp.u4_enable_yuv_formats = 15; // Supports all chroma formats
|
|
createIp.u4_keep_threads_active = 1;
|
|
|
|
IV_API_CALL_STATUS_T ret =
|
|
ihevcd_cxa_api_function(nullptr, &createIp, &createOp);
|
|
if (ret != IV_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
mCodec = createOp.s_ivd_create_op_t.pv_handle;
|
|
if (mCodec) {
|
|
iv_obj_t* ps_handle = static_cast<iv_obj_t*>(mCodec);
|
|
ps_handle->pv_fxns = (void*)&ihevcd_cxa_api_function;
|
|
ps_handle->u4_size = sizeof(iv_obj_t);
|
|
}
|
|
|
|
// Set number of cores
|
|
ihevcd_cxa_ctl_set_num_cores_ip_t setCoresIp = {};
|
|
ihevcd_cxa_ctl_set_num_cores_op_t setCoresOp = {};
|
|
setCoresIp.e_cmd = IVD_CMD_VIDEO_CTL;
|
|
setCoresIp.e_sub_cmd = static_cast<IVD_CONTROL_API_COMMAND_TYPE_T>(
|
|
IHEVCD_CXA_CMD_CTL_SET_NUM_CORES);
|
|
setCoresIp.u4_num_cores = mCores;
|
|
setCoresIp.u4_size = sizeof(ihevcd_cxa_ctl_set_num_cores_ip_t);
|
|
setCoresOp.u4_size = sizeof(ihevcd_cxa_ctl_set_num_cores_op_t);
|
|
|
|
ret = ihevcd_cxa_api_function(static_cast<iv_obj_t*>(mCodec), &setCoresIp,
|
|
&setCoresOp);
|
|
if (ret != IV_SUCCESS) {
|
|
deinitDecoder();
|
|
return false;
|
|
}
|
|
|
|
// Set processor architecture using local mapper helper
|
|
ihevcd_cxa_ctl_set_processor_ip_t setProcIp = {};
|
|
ihevcd_cxa_ctl_set_processor_op_t setProcOp = {};
|
|
setProcIp.e_cmd = IVD_CMD_VIDEO_CTL;
|
|
setProcIp.e_sub_cmd = static_cast<IVD_CONTROL_API_COMMAND_TYPE_T>(
|
|
IHEVCD_CXA_CMD_CTL_SET_PROCESSOR);
|
|
setProcIp.u4_arch = getNativeArch(mArch);
|
|
setProcIp.u4_soc = 0;
|
|
setProcIp.u4_size = sizeof(ihevcd_cxa_ctl_set_processor_ip_t);
|
|
setProcOp.u4_size = sizeof(ihevcd_cxa_ctl_set_processor_op_t);
|
|
|
|
ret = ihevcd_cxa_api_function(static_cast<iv_obj_t*>(mCodec), &setProcIp,
|
|
&setProcOp);
|
|
if (ret != IV_SUCCESS) {
|
|
deinitDecoder();
|
|
return false;
|
|
}
|
|
|
|
// Initialize to header decode mode using config helper
|
|
if (!setDecoderConfig(mCodec, IVD_DECODE_HEADER, 0)) {
|
|
deinitDecoder();
|
|
return false;
|
|
}
|
|
|
|
// Allocate chunk-sized input bitstream buffer
|
|
if (!mInputBuf.allocBuffer(1024 * 1024)) {
|
|
deinitDecoder();
|
|
return false;
|
|
}
|
|
|
|
mHeaderDecoded = false;
|
|
mInFlushMode = false;
|
|
return true;
|
|
}
|
|
|
|
void DecHelper::deinitDecoder() {
|
|
if (mCodec) {
|
|
ihevcd_cxa_delete_ip_t deleteIp = {};
|
|
ihevcd_cxa_delete_op_t deleteOp = {};
|
|
deleteIp.s_ivd_delete_ip_t.e_cmd = IVD_CMD_DELETE;
|
|
deleteIp.s_ivd_delete_ip_t.u4_size = sizeof(ihevcd_cxa_delete_ip_t);
|
|
deleteOp.s_ivd_delete_op_t.u4_size = sizeof(ihevcd_cxa_delete_op_t);
|
|
|
|
ihevcd_cxa_api_function(static_cast<iv_obj_t*>(mCodec), &deleteIp,
|
|
&deleteOp);
|
|
mCodec = nullptr;
|
|
}
|
|
}
|
|
|
|
bool DecHelper::decodeHeader(size_t& bytesConsumed) {
|
|
bytesConsumed = 0;
|
|
if (!mCodec) {
|
|
return false;
|
|
}
|
|
|
|
ihevcd_cxa_video_decode_ip_t decodeIp = {};
|
|
ihevcd_cxa_video_decode_op_t decodeOp = {};
|
|
|
|
decodeIp.s_ivd_video_decode_ip_t.e_cmd = IVD_CMD_VIDEO_DECODE;
|
|
decodeIp.s_ivd_video_decode_ip_t.u4_ts = 0;
|
|
decodeIp.s_ivd_video_decode_ip_t.u4_num_Bytes = mInputBuf.size();
|
|
decodeIp.s_ivd_video_decode_ip_t.pv_stream_buffer = mInputBuf.data();
|
|
decodeIp.s_ivd_video_decode_ip_t.u4_size =
|
|
sizeof(ihevcd_cxa_video_decode_ip_t);
|
|
decodeOp.s_ivd_video_decode_op_t.u4_size =
|
|
sizeof(ihevcd_cxa_video_decode_op_t);
|
|
|
|
IV_API_CALL_STATUS_T ret = ihevcd_cxa_api_function(
|
|
static_cast<iv_obj_t*>(mCodec), &decodeIp, &decodeOp);
|
|
|
|
bytesConsumed = decodeOp.s_ivd_video_decode_op_t.u4_num_bytes_consumed;
|
|
|
|
if (ret == IV_SUCCESS) {
|
|
mWidth = decodeOp.s_ivd_video_decode_op_t.u4_pic_wd;
|
|
mHeight = decodeOp.s_ivd_video_decode_op_t.u4_pic_ht;
|
|
mHeaderDecoded = true;
|
|
|
|
// Allocate the output reconstructed YUV frame buffer
|
|
if (!mOutputBuf.allocBuffer(mWidth, mHeight, mFormat)) {
|
|
return false;
|
|
}
|
|
|
|
// Transition decoder to frame decode mode using config helper
|
|
return setDecoderConfig(mCodec, IVD_DECODE_FRAME, 0);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool DecHelper::decodeFrame(size_t& bytesConsumed, bool& frameReady,
|
|
size_t& frameIndex, bool noInput) {
|
|
bytesConsumed = 0;
|
|
frameReady = false;
|
|
if (!mCodec) {
|
|
return false;
|
|
}
|
|
|
|
ihevcd_cxa_video_decode_ip_t decodeIp = {};
|
|
ihevcd_cxa_video_decode_op_t decodeOp = {};
|
|
|
|
decodeIp.s_ivd_video_decode_ip_t.e_cmd = IVD_CMD_VIDEO_DECODE;
|
|
decodeIp.s_ivd_video_decode_ip_t.u4_ts = 0;
|
|
if (noInput) {
|
|
decodeIp.s_ivd_video_decode_ip_t.pv_stream_buffer = nullptr;
|
|
decodeIp.s_ivd_video_decode_ip_t.u4_num_Bytes = 0;
|
|
} else {
|
|
decodeIp.s_ivd_video_decode_ip_t.pv_stream_buffer =
|
|
static_cast<uint8_t*>(mInputBuf.data()) + mInputBuf.offset();
|
|
decodeIp.s_ivd_video_decode_ip_t.u4_num_Bytes =
|
|
mInputBuf.size() - mInputBuf.offset();
|
|
}
|
|
decodeIp.s_ivd_video_decode_ip_t.u4_size =
|
|
sizeof(ihevcd_cxa_video_decode_ip_t);
|
|
decodeOp.s_ivd_video_decode_op_t.u4_size =
|
|
sizeof(ihevcd_cxa_video_decode_op_t);
|
|
|
|
// Map destination output buffers for reconstruction
|
|
decodeIp.s_ivd_video_decode_ip_t.s_out_buffer.u4_num_bufs =
|
|
mOutputBuf.numPlanes();
|
|
for (size_t i = 0; i < mOutputBuf.numPlanes(); ++i) {
|
|
decodeIp.s_ivd_video_decode_ip_t.s_out_buffer.pu1_bufs[i] =
|
|
mOutputBuf.planeData(i);
|
|
decodeIp.s_ivd_video_decode_ip_t.s_out_buffer.u4_min_out_buf_size[i] =
|
|
mOutputBuf.stride(i) * mOutputBuf.planeHeight(i);
|
|
}
|
|
|
|
IV_API_CALL_STATUS_T ret = ihevcd_cxa_api_function(
|
|
static_cast<iv_obj_t*>(mCodec), &decodeIp, &decodeOp);
|
|
if (ret != IV_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
bytesConsumed = decodeOp.s_ivd_video_decode_op_t.u4_num_bytes_consumed;
|
|
|
|
// Check if a frame has been decoded and is ready for display
|
|
if (decodeOp.s_ivd_video_decode_op_t.u4_output_present == 1) {
|
|
const iv_yuv_buf_t& dispBuf =
|
|
decodeOp.s_ivd_video_decode_op_t.s_disp_frm_buf;
|
|
|
|
frameReady = true;
|
|
|
|
// Process the decoded frame directly inside decodeFrame
|
|
processDecodedFrame(mOutputBuf, frameIndex);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DecHelper::flushDecoder(size_t& frameIndex) {
|
|
if (!mCodec || !mHeaderDecoded) {
|
|
return false;
|
|
}
|
|
|
|
// Put decoder in flush mode if not already done
|
|
if (!mInFlushMode) {
|
|
ivd_ctl_flush_ip_t flushIp = {};
|
|
ivd_ctl_flush_op_t flushOp = {};
|
|
flushIp.e_cmd = IVD_CMD_VIDEO_CTL;
|
|
flushIp.e_sub_cmd = IVD_CMD_CTL_FLUSH;
|
|
flushIp.u4_size = sizeof(ivd_ctl_flush_ip_t);
|
|
flushOp.u4_size = sizeof(ivd_ctl_flush_op_t);
|
|
|
|
IV_API_CALL_STATUS_T ret = ihevcd_cxa_api_function(
|
|
static_cast<iv_obj_t*>(mCodec), &flushIp, &flushOp);
|
|
if (ret != IV_SUCCESS) {
|
|
return false;
|
|
}
|
|
mInFlushMode = true;
|
|
}
|
|
|
|
// Loop internally and delegate to decodeFrame with noInput = true
|
|
while (true) {
|
|
size_t consumed = 0;
|
|
bool frameReady = false;
|
|
if (!decodeFrame(consumed, frameReady, frameIndex, true)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string DecHelper::computeFrameMd5(const RawBuf& buf) {
|
|
Md5Wrapper md5;
|
|
md5.init();
|
|
|
|
size_t planesCount = buf.numPlanes();
|
|
for (size_t p = 0; p < planesCount; ++p) {
|
|
const uint8_t* planePtr = buf.planeData(p);
|
|
size_t planeW = buf.planeWidth(p);
|
|
size_t planeH = buf.planeHeight(p);
|
|
size_t planeStride = buf.stride(p);
|
|
|
|
for (size_t r = 0; r < planeH; ++r) {
|
|
md5.update(planePtr + (r * planeStride), planeW);
|
|
}
|
|
}
|
|
|
|
return md5.finalize();
|
|
}
|
|
|
|
void DecHelper::processDecodedFrame(RawBuf& buf, size_t& frameIndex) {
|
|
if (mOutputFilePath.has_value()) {
|
|
mOutFile.write(buf);
|
|
}
|
|
|
|
if (frameIndex < mRefMd5s.size()) {
|
|
std::string computedMd5 = computeFrameMd5(buf);
|
|
EXPECT_THAT(computedMd5, Eq(mRefMd5s[frameIndex]))
|
|
<< "MD5 mismatch at decoded frame " << frameIndex << " of file "
|
|
<< *mInputFilePath;
|
|
}
|
|
frameIndex++;
|
|
}
|
|
|
|
bool DecHelper::decodeFile() {
|
|
size_t frameIndex = 0;
|
|
|
|
size_t inputFileOffset = 0;
|
|
size_t inputFrameSize = mInputBuf.capacity();
|
|
while (!mBitsFile.isEof()) {
|
|
size_t bytesRead = mBitsFile.read(mInputBuf, inputFrameSize);
|
|
if (bytesRead == 0) break;
|
|
|
|
size_t consumed = 0;
|
|
if (!mHeaderDecoded) {
|
|
bool header_ret = decodeHeader(consumed);
|
|
if (!header_ret) {
|
|
if (consumed == 0) {
|
|
return false;
|
|
}
|
|
} else {
|
|
inputFrameSize = mWidth * mHeight * 3;
|
|
}
|
|
} else {
|
|
bool frameReady = false;
|
|
|
|
decodeFrame(consumed, frameReady, frameIndex);
|
|
if (consumed == 0 && !frameReady) {
|
|
break;
|
|
}
|
|
}
|
|
inputFileOffset += consumed;
|
|
mBitsFile.seek(inputFileOffset);
|
|
}
|
|
|
|
// Flush remaining frames (invokes processDecodedFrame internally for all
|
|
// remaining frames)
|
|
flushDecoder(frameIndex);
|
|
|
|
return true;
|
|
}
|