Files
spice2x-r3d/src/spice2x/hooks/graphics/nvenc_hook.cpp
T

291 lines
12 KiB
C++

#include <d3d9.h>
#include <optional>
#include "avs/game.h"
#include "external/nvenc/nvEncodeAPI.h"
#include "hooks/libraryhook.h"
#include "hooks/graphics/backends/d3d9/d3d9_device.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/utils.h"
#include "nvenc_hook.h"
#ifdef SPICE64
typedef NVENCSTATUS(NVENCAPI *NvEncodeAPICreateInstance_Type)(NV_ENCODE_API_FUNCTION_LIST*);
static NvEncodeAPICreateInstance_Type NvEncodeAPICreateInstance_orig = nullptr;
static PNVENCOPENENCODESESSIONEX nvEncOpenEncodeSessionEx_orig = nullptr;
static PNVENCINITIALIZEENCODER nvEncInitializeEncoder_orig = nullptr;
static PNVENCGETENCODEPRESETCONFIG nvEncGetEncodePresetConfig_orig = nullptr;
static PNVENCGETENCODEPRESETCONFIGEX nvEncGetEncodePresetConfigEx_orig = nullptr;
static BOOL initialized = false;
static BOOL new_preset_guids = false;
namespace nvenc_hook {
bool FORCE_DISABLE;
std::optional<std::string> VIDEO_CQP_STRING_OVERRIDE;
std::optional<NV_ENC_QP> VIDEO_CQP_OVERRIDE;
void parse_qcp_params();
void dump_init_params(NV_ENC_INITIALIZE_PARAMS* encode) {
log_misc("nvenc_hook", "NV_ENC_INITIALIZE_PARAMS:");
log_misc("nvenc_hook", " version: {:x}", encode->version);
log_misc("nvenc_hook", " encodeGUID: {}", guid2s(encode->encodeGUID));
log_misc("nvenc_hook", " presetGUID: {}", guid2s(encode->presetGUID));
log_misc("nvenc_hook", " encodeWidth: {}", encode->encodeWidth);
log_misc("nvenc_hook", " encodeHeight: {}", encode->encodeHeight);
log_misc("nvenc_hook", " darWidth: {}", encode->darWidth);
log_misc("nvenc_hook", " darHeight: {}", encode->darHeight);
log_misc("nvenc_hook", " frameRateNum: {}", encode->frameRateNum);
log_misc("nvenc_hook", " frameRateDen: {}", encode->frameRateDen);
log_misc("nvenc_hook", " enableEncodeAsync: {}", encode->enableEncodeAsync);
log_misc("nvenc_hook", " enablePTD: {}", encode->enablePTD);
log_misc("nvenc_hook", " maxEncodeWidth: {}", encode->maxEncodeWidth);
log_misc("nvenc_hook", " maxEncodeHeight: {}", encode->maxEncodeHeight);
log_misc("nvenc_hook", " tuningInfo: {}", static_cast<uint32_t>(encode->tuningInfo));
log_misc("nvenc_hook", " bufferFormat: {}", static_cast<uint32_t>(encode->bufferFormat));
log_misc("nvenc_hook", " encodeConfig.version: {:x}", encode->encodeConfig->version);
log_misc("nvenc_hook", " encodeConfig.profileGUID: {}", guid2s(encode->encodeConfig->profileGUID));
log_misc("nvenc_hook", " encodeConfig.gopLength: {}", encode->encodeConfig->gopLength);
log_misc("nvenc_hook", " encodeConfig.frameIntervalP: {}", encode->encodeConfig->frameIntervalP);
log_misc("nvenc_hook", " encodeConfig.monoChromeEncoding: {}", encode->encodeConfig->monoChromeEncoding);
const auto rc = &encode->encodeConfig->rcParams;
const auto h264 = &encode->encodeConfig->encodeCodecConfig.h264Config;
log_misc("nvenc_hook", " encodeConfig.encodeCodecConfig.h264Config.sliceMode: {}", h264->sliceMode);
log_misc("nvenc_hook", " encodeConfig.encodeCodecConfig.h264Config.sliceModeData: {}", h264->sliceModeData);
log_misc("nvenc_hook", " encodeConfig.rcParams.version: {:x}", rc->version);
log_misc("nvenc_hook", " encodeConfig.rcParams.rateControlMode: {}", static_cast<uint32_t>(rc->rateControlMode));
}
NVENCSTATUS NVENCAPI nvEncOpenEncodeSessionEx_hook(
NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS *openSessionExParams,
void **encoder
) {
WrappedIDirect3DDevice9 *wrappedDevice;
try {
wrappedDevice = (WrappedIDirect3DDevice9*)openSessionExParams->device;
// log_misc("nvenc_hook",
// "nvEncOpenEncodeSessionEx hook hit (wrapped: {}) (real: {})",
// fmt::ptr(wrappedDevice),
// fmt::ptr(wrappedDevice->pReal)
// );
openSessionExParams->device = wrappedDevice->pReal;
} catch (const std::exception &ex) {}
return nvEncOpenEncodeSessionEx_orig(openSessionExParams, encoder);
}
NVENCSTATUS NVENCAPI nvEncInitializeEncoder_hook(
void* encoder,
NV_ENC_INITIALIZE_PARAMS* createEncodeParams
) {
try {
// check input params
if (createEncodeParams == nullptr || createEncodeParams->encodeConfig == nullptr) {
log_warning("nvenc_hook", "nvEncInitializeEncoder called with invalid params");
goto done;
}
const auto rc = &createEncodeParams->encodeConfig->rcParams;
log_misc("nvenc_hook", "nvEncInitializeEncoder hook hit with expected params");
dump_init_params(createEncodeParams);
if (new_preset_guids) {
log_misc("nvenc_hook", "swapping out encoder preset for newer NVENC SDK");
memcpy(&createEncodeParams->presetGUID, &NV_ENC_PRESET_P4_GUID, sizeof(GUID));
createEncodeParams->tuningInfo = NV_ENC_TUNING_INFO_HIGH_QUALITY;
}
// cqp p/b/i override
if (rc->rateControlMode == NV_ENC_PARAMS_RC_CONSTQP) {
// print out most relevant video quality settings
// note: NvEncoder.cpp sample uses {28, 31, 25} (and that's what some hex edits modify)
// but bm2dx later adds 8 to each value, before calling this routine
log_misc(
"nvenc_hook", "nvEncInitializeEncoder: original constQP p={}, b={}, i={}",
rc->constQP.qpInterP, rc->constQP.qpInterB, rc->constQP.qpIntra);
// video quality override
if (VIDEO_CQP_OVERRIDE.has_value()) {
rc->constQP = VIDEO_CQP_OVERRIDE.value();
log_misc(
"nvenc_hook", "nvEncInitializeEncoder: user overriden constQP p={}, b={}, i={}",
rc->constQP.qpInterP, rc->constQP.qpInterB, rc->constQP.qpIntra);
}
} else {
log_warning(
"nvenc_hook",
"nvEncInitializeEncoder: unexpected rateControlMode, expected: NV_ENC_PARAMS_RC_CONSTQP, actual: {}",
static_cast<uint32_t>(rc->rateControlMode));
}
} catch (const std::exception &ex) {}
done:
const auto status = nvEncInitializeEncoder_orig(encoder, createEncodeParams);
log_misc(
"nvenc_hook",
"nvEncInitializeEncoder returned 0x{:x}",
static_cast<uint32_t>(status));
return status;
}
NVENCSTATUS NVENCAPI nvEncGetEncodePresetConfig_hook (
void* encoder, GUID encodeGUID, GUID presetGUID, NV_ENC_PRESET_CONFIG* presetConfig) {
// IIDX32 calls this with
// presetGUID = {34DBA71D-A77B-4B8F-9C3E-B6D5DA24C012} (NV_ENC_PRESET_HQ_GUID) (for h264)
// this preset is deprecated according to NVIDIA
const auto status = nvEncGetEncodePresetConfig_orig(
encoder, encodeGUID, presetGUID, presetConfig);
log_misc(
"nvenc_hook",
"NvEncGetEncodePresetConfig called with encodeGUID = {}, presetGUID = {}. and returned 0x{:x}",
guid2s(encodeGUID),
guid2s(presetGUID),
static_cast<uint32_t>(status));
if (status == NV_ENC_SUCCESS) {
return status;
}
// in NVIDIA driver 591.44 released in December 2025,
// NvEncGetEncodePresetConfig started to fail with NV_ENC_ERR_UNSUPPORTED_PARAM and
// eventually cause a crash
//
// IIDX32 calls this with
// presetGUID = {34DBA71D-A77B-4B8F-9C3E-B6D5DA24C012} (NV_ENC_PRESET_HQ_GUID) (for h264)
// this preset is deprecated according to NVIDIA
// https://docs.nvidia.com/video-technologies/video-codec-sdk/13.0/deprecation-notices/index.html
//
// references:
// https://github.com/NVIDIA/video-sdk-samples/tree/aa3544dcea2fe63122e4feb83bf805ea40e58dbe/Samples/NvCodec/NvEncoder
// https://forums.developer.nvidia.com/t/drivers-591-44-broke-nvenc-getencodepresetconfig-no-longer-works/353613
// https://docs.nvidia.com/video-technologies/video-codec-sdk/11.1/nvenc-preset-migration-guide/index.html
new_preset_guids = true;
const auto status_ex =
nvEncGetEncodePresetConfigEx_orig(
encoder,
encodeGUID,
NV_ENC_PRESET_P4_GUID,
NV_ENC_TUNING_INFO_HIGH_QUALITY,
presetConfig);
log_misc(
"nvenc_hook",
"called NvEncGetEncodePresetConfigEx instead; returned 0x{:x}",
static_cast<uint32_t>(status_ex));
return status_ex;
}
NVENCSTATUS NVENCAPI NvEncodeAPICreateInstance_hook(NV_ENCODE_API_FUNCTION_LIST *pFunctionList) {
if (FORCE_DISABLE) {
return NV_ENC_ERR_NO_ENCODE_DEVICE;
}
// log_misc("nvenc_hook", "NvEncodeAPICreateInstance hook hit");
auto status = NvEncodeAPICreateInstance_orig(pFunctionList);
// The game will call NvEncodeAPICreateInstance multiple times
// Using a flag to avoid creating trampoline repeatedly
if (!initialized) {
// hook functions
detour::trampoline_try(
pFunctionList->nvEncOpenEncodeSessionEx,
nvEncOpenEncodeSessionEx_hook,
&nvEncOpenEncodeSessionEx_orig);
detour::trampoline_try(
pFunctionList->nvEncInitializeEncoder,
nvEncInitializeEncoder_hook,
&nvEncInitializeEncoder_orig);
detour::trampoline_try(
pFunctionList->nvEncGetEncodePresetConfig,
nvEncGetEncodePresetConfig_hook,
&nvEncGetEncodePresetConfig_orig);
nvEncGetEncodePresetConfigEx_orig = pFunctionList->nvEncGetEncodePresetConfigEx;
log_misc("nvenc_hook", "NvEncodeAPICreateInstance_hook called and functions hooked");
initialized = true;
}
return status;
}
void initialize() {
HMODULE nvenc = libutils::try_library("nvEncodeAPI64.dll");
if (nvenc == nullptr) {
return;
}
bool success = detour::trampoline_try(
(NvEncodeAPICreateInstance_Type)libutils::try_proc(nvenc, "NvEncodeAPICreateInstance"),
NvEncodeAPICreateInstance_hook,
&NvEncodeAPICreateInstance_orig
);
if (success) {
log_misc("nvenc_hook", "created hook for NvEncodeAPICreateInstance");
} else {
log_warning("nvenc_hook", "failed to hook NvEncodeAPICreateInstance");
}
parse_qcp_params();
}
void parse_qcp_params() {
if (!VIDEO_CQP_STRING_OVERRIDE.has_value() || VIDEO_CQP_STRING_OVERRIDE.value().empty()) {
return;
}
const auto remove_spaces = [](const char& c) {
return c == ' ';
};
auto s = VIDEO_CQP_STRING_OVERRIDE.value();
s.erase(std::remove_if(s.begin(), s.end(), remove_spaces), s.end());
int cqp = -1;
NV_ENC_QP nvenc_qp;
// try 3-param format first, and then fall back to single parameter format
if (sscanf(
s.c_str(),
"%i,%i,%i",
(int*)&(nvenc_qp.qpInterP),
(int*)&(nvenc_qp.qpInterB),
(int*)&(nvenc_qp.qpIntra)) == 3) {
VIDEO_CQP_OVERRIDE = nvenc_qp;
} else if (sscanf(s.c_str(), "%i", &cqp) == 1) {
nvenc_qp.qpInterP = cqp;
nvenc_qp.qpInterB = cqp;
nvenc_qp.qpIntra = cqp;
VIDEO_CQP_OVERRIDE = nvenc_qp;
} else {
log_fatal("nvenc_hook", "failed to parse -iidxreccqp");
}
log_info("nvenc_hook", "parsed NVENC ConstQP params: p={}, b={}, i={} (-iidxreccqp)",
nvenc_qp.qpInterP, nvenc_qp.qpInterB, nvenc_qp.qpIntra);
}
}
#else
namespace nvenc_hook {
void initialize() {
return;
}
}
#endif