Files
spice2x.github.io/src/spice2x/hooks/libraryhook.cpp
T
bicarus d0db547dd0 sdvx: implement NVAPI for non-NVIDIA GPUs (#816)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

In summary: for SDVX VM mode, this PR eliminates the need for `Note FPS
Target 120Hz` / `Game FPS Target 120Hz` patches typically used for AMD
GPU via synthetic responses provided via a fake NVAPI layer.

Before this change, playing on AMD GPU required patches to force the
game to render at 60Hz (See #107 for relevant discussion). Basically, if
NVAPI query fails, the game shows

`I:NvDisplayConfig: GetMainDisplayRefreshRate = 60.000hz (DUMMY)`

the game renders at 120Hz but the notes render at 60Hz, which would look
stuttery.

After this change, even without a real `nvapi64.dll`, spice will provide
a stub implementation, just enough to convince the game that the main
display is 120Hz, which causes notes to render at 120Hz as well.

This can be overridden by `-graphics-force-refresh` option.

Important note: if user has the `Note FPS Target 60Hz` patch but running
the game at 120Hz, before this change the notes would render at 60Hz,
but after this they will be at 120Hz. I don't know why anyone would
intentionally lower the notes framerate, so this is not going to be
fixed. If the user wants to run the game at 60Hz they can do so via
`-graphics-force-refresh`.

Another change - `-nonvapi` will always force down this synthetic nvapi
path, even if the system has the real `nvapi64.dll`. Even on NVDIA GPU
this option can be used to force this.

## Testing
2026-07-19 23:43:14 -07:00

142 lines
4.1 KiB
C++

#include "libraryhook.h"
#include "external/robin_hood.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
static bool LHOOK_ENABLED = false;
static robin_hood::unordered_map<std::string, HMODULE> LIBRARIES_A;
static robin_hood::unordered_map<std::wstring, HMODULE> LIBRARIES_W;
static robin_hood::unordered_map<std::string, FARPROC> PROCS;
static decltype(LoadLibraryA) *LoadLibraryA_orig = nullptr;
static decltype(LoadLibraryW) *LoadLibraryW_orig = nullptr;
static decltype(LoadLibraryExW) *LoadLibraryExW_orig = nullptr;
static decltype(GetModuleHandleA) *GetModuleHandleA_orig = nullptr;
static decltype(GetModuleHandleW) *GetModuleHandleW_orig = nullptr;
static decltype(GetProcAddress) *GetProcAddress_orig = nullptr;
static HMODULE WINAPI LoadLibraryA_hook(LPCTSTR lpFileName) {
// check hooks
if (lpFileName) {
auto module = LIBRARIES_A.find(lpFileName);
if (module != LIBRARIES_A.end()) {
return module->second;
}
}
// fallback
return LoadLibraryA_orig(lpFileName);
}
static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
// check hooks
if (lpFileName) {
auto module = LIBRARIES_W.find(lpFileName);
if (module != LIBRARIES_W.end()) {
return module->second;
}
}
// fallback
return LoadLibraryW_orig(lpFileName);
}
static HMODULE WINAPI LoadLibraryExW_hook(LPCWSTR lpFileName, HANDLE hFile, DWORD dwFlags) {
// check hooks
if (lpFileName) {
auto module = LIBRARIES_W.find(lpFileName);
if (module != LIBRARIES_W.end()) {
return module->second;
}
}
// fallback
return LoadLibraryExW_orig(lpFileName, hFile, dwFlags);
}
static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
// check hooks
if (lpModuleName) {
auto module = LIBRARIES_A.find(lpModuleName);
if (module != LIBRARIES_A.end()) {
return module->second;
}
}
// fallback
return GetModuleHandleA_orig(lpModuleName);
}
static HMODULE WINAPI GetModuleHandleW_hook(LPCWSTR lpModuleName) {
// check hooks
if (lpModuleName) {
auto module = LIBRARIES_W.find(lpModuleName);
if (module != LIBRARIES_W.end()) {
return module->second;
}
}
// fallback
return GetModuleHandleW_orig(lpModuleName);
}
static FARPROC WINAPI GetProcAddress_hook(HMODULE hModule, LPCSTR lpProcName) {
// check for ordinal
if (reinterpret_cast<uintptr_t>(lpProcName) <= UINT16_MAX) {
// fallback
return GetProcAddress_orig(hModule, lpProcName);
}
// check hooks
if (lpProcName) {
auto proc = PROCS.find(lpProcName);
if (proc != PROCS.end()) {
return proc->second;
}
}
// fallback
return GetProcAddress_orig(hModule, lpProcName);
}
void libraryhook_enable(HMODULE module) {
log_info("libraryhook", "LibraryHook Attach");
if (LHOOK_ENABLED) {
return;
}
// detour
detour::trampoline_try("kernel32.dll", "LoadLibraryA", LoadLibraryA_hook, &LoadLibraryA_orig);
detour::trampoline_try("kernel32.dll", "LoadLibraryW", LoadLibraryW_hook, &LoadLibraryW_orig);
detour::trampoline_try("kernel32.dll", "LoadLibraryExW", LoadLibraryExW_hook, &LoadLibraryExW_orig);
detour::trampoline_try("kernel32.dll", "GetModuleHandleA", GetModuleHandleA_hook, &GetModuleHandleA_orig);
detour::trampoline_try("kernel32.dll", "GetModuleHandleW", GetModuleHandleW_hook, &GetModuleHandleW_orig);
detour::trampoline_try("kernel32.dll", "GetProcAddress", GetProcAddress_hook, &GetProcAddress_orig);
// set enabled
LHOOK_ENABLED = true;
}
void libraryhook_hook_library(std::string library_name, HMODULE library_address) {
// add library to list
LIBRARIES_W.insert_or_assign(s2ws(library_name), library_address);
LIBRARIES_A.insert_or_assign(std::move(library_name), library_address);
}
void libraryhook_hook_proc(std::string proc_name, FARPROC proc_address) {
// add proc to list
PROCS.insert_or_assign(std::move(proc_name), proc_address);
}