From d0db547dd09aa420ae9cce33d450237e0417a163 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:43:14 -0700 Subject: [PATCH] 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 --- src/spice2x/CMakeLists.txt | 1 + src/spice2x/games/sdvx/sdvx.cpp | 18 +- src/spice2x/hooks/graphics/nvapi_hook.cpp | 67 ++- src/spice2x/hooks/graphics/nvapi_impl.cpp | 480 ++++++++++++++++++++++ src/spice2x/hooks/graphics/nvapi_impl.h | 14 + src/spice2x/hooks/libraryhook.cpp | 16 + 6 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 src/spice2x/hooks/graphics/nvapi_impl.cpp create mode 100644 src/spice2x/hooks/graphics/nvapi_impl.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 64e329d..4f890a6 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -528,6 +528,7 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/devicehook.cpp hooks/graphics/graphics.cpp hooks/graphics/graphics_windowed.cpp + hooks/graphics/nvapi_impl.cpp hooks/graphics/nvapi_hook.cpp hooks/graphics/nvenc_hook.cpp hooks/graphics/backends/d3d9/d3d9_backend.cpp diff --git a/src/spice2x/games/sdvx/sdvx.cpp b/src/spice2x/games/sdvx/sdvx.cpp index 6cc7975..650e9a0 100644 --- a/src/spice2x/games/sdvx/sdvx.cpp +++ b/src/spice2x/games/sdvx/sdvx.cpp @@ -8,6 +8,9 @@ #include "hooks/graphics/graphics.h" #include "hooks/devicehook.h" #include "hooks/libraryhook.h" +#ifdef SPICE64 +#include "hooks/graphics/nvapi_impl.h" +#endif #include "hooks/graphics/nvapi_hook.h" #include "hooks/powrprof.h" #include "hooks/sleephook.h" @@ -428,11 +431,24 @@ namespace games::sdvx { if (aio != nullptr) { // enable 9on12 for AMD - if (!libutils::try_library("nvapi64.dll")) { + const auto nvapi = libutils::try_library("nvapi64.dll"); + if (nvapi == nullptr) { log_info( "sdvx", "nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled"); GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true; + } + + if (nvapi_hook::BYPASS_NVAPI || nvapi == nullptr) { + const uint32_t main_refresh_hz = GRAPHICS_FORCE_REFRESH > 0 ? + GRAPHICS_FORCE_REFRESH : (is_valkyrie_model() ? 120 : 60); + const uint32_t sub_refresh_hz = GRAPHICS_FORCE_REFRESH_SUB.value_or(60); + if (!nvapi_impl::initialize( + avs::game::DLL_INSTANCE, + main_refresh_hz, + sub_refresh_hz)) { + log_warning("sdvx", "failed to initialize synthetic NVAPI"); + } } else { // don't let nvapi mess with display settings nvapi_hook::initialize(avs::game::DLL_INSTANCE); diff --git a/src/spice2x/hooks/graphics/nvapi_hook.cpp b/src/spice2x/hooks/graphics/nvapi_hook.cpp index ab22fe6..f5dec43 100644 --- a/src/spice2x/hooks/graphics/nvapi_hook.cpp +++ b/src/spice2x/hooks/graphics/nvapi_hook.cpp @@ -1,5 +1,6 @@ #include "external/nvapi/nvapi.h" #include "external/nvapi/NvApiDriverSettings.h" +#include "avs/game.h" #include "hooks/libraryhook.h" #include "util/detour.h" #include "util/libutils.h" @@ -12,14 +13,21 @@ namespace nvapi_hook { bool BYPASS_NVAPI = false; typedef uintptr_t *(*NvAPI_QueryInterface_t)(unsigned int); + static NvAPI_QueryInterface_t NvAPI_QueryInterface_orig = nullptr; + static decltype(NvAPI_GPU_GetConnectedDisplayIds) *NvAPI_GPU_GetConnectedDisplayIds_orig = nullptr; + static decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *NvAPI_DISP_GetGDIPrimaryDisplayId_orig = nullptr; static uintptr_t* __cdecl NvAPI_QueryInterface_hook(unsigned int func_code); + static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook( + NvPhysicalGpuHandle hPhysicalGpu, + NV_GPU_DISPLAYIDS *pDisplayIds, + NvU32 *pDisplayIdCount, + NvU32 flags); static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook( NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags); void initialize(HINSTANCE dll) { - #ifdef SPICE64 std::string nvapi_dll = "nvapi64.dll"; #else @@ -40,6 +48,18 @@ namespace nvapi_hook { return nullptr; } + // spoof display connector types only for SDVX + if (func_code == 0x0078DBA2 && avs::game::is_model("KFC")) { + NvAPI_GPU_GetConnectedDisplayIds_orig = + reinterpret_cast( + NvAPI_QueryInterface_orig(func_code)); + NvAPI_DISP_GetGDIPrimaryDisplayId_orig = + reinterpret_cast( + NvAPI_QueryInterface_orig(0x1E9D8A31)); + log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_GPU_GetConnectedDisplayIds) - hooked"); + return (uintptr_t *)NvAPI_GPU_GetConnectedDisplayIds_hook; + } + // NvAPI_DISP_SetDisplayConfig if (func_code == 0x5D8CF8DE) { log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_DISP_SetDisplayConfig) - hooked"); @@ -51,8 +71,53 @@ namespace nvapi_hook { return NvAPI_QueryInterface_orig(func_code); } + NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook( + NvPhysicalGpuHandle hPhysicalGpu, + NV_GPU_DISPLAYIDS *pDisplayIds, + NvU32 *pDisplayIdCount, + NvU32 flags) { + + if (NvAPI_GPU_GetConnectedDisplayIds_orig == nullptr) { + return NVAPI_NO_IMPLEMENTATION; + } + + const NvAPI_Status status = NvAPI_GPU_GetConnectedDisplayIds_orig( + hPhysicalGpu, + pDisplayIds, + pDisplayIdCount, + flags); + + if (status != NVAPI_OK || pDisplayIds == nullptr || pDisplayIdCount == nullptr || + NvAPI_DISP_GetGDIPrimaryDisplayId_orig == nullptr) { + return status; + } + + NvU32 primary_display_id = 0; + if (NvAPI_DISP_GetGDIPrimaryDisplayId_orig(&primary_display_id) != NVAPI_OK) { + return status; + } + + // report the primary display adapter as DP and all other displays as HDMI + // (the game expects this since the VM cab is like that) + // + // this doesn't actually affect gameplay; only how + // NvDisplayConfig: MainDisplay={}hz / SubDisplay={}hz + // gets printed. + // + // for the actual value that matters for gameplay, it's this: + // NvDisplayConfig: GetMainDisplayRefreshRate = {}hz + for (NvU32 index = 0; index < *pDisplayIdCount; index++) { + auto &display = pDisplayIds[index]; + display.connectorType = display.displayId == primary_display_id ? + NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI; + } + + return status; + } + NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook( NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags) { + // do not let the game apply its emulated cabinet topology to Windows log_misc("nvapi_hook", "NvAPI_DISP_SetDisplayConfig_hook - do nothing and return"); return NVAPI_OK; } diff --git a/src/spice2x/hooks/graphics/nvapi_impl.cpp b/src/spice2x/hooks/graphics/nvapi_impl.cpp new file mode 100644 index 0000000..dbe6b08 --- /dev/null +++ b/src/spice2x/hooks/graphics/nvapi_impl.cpp @@ -0,0 +1,480 @@ +#include "nvapi_impl.h" + +#ifdef SPICE64 + +#include +#include + +#include + +#include "external/nvapi/nvapi.h" +#include "hooks/libraryhook.h" +#include "util/logging.h" +#include "util/sysutils.h" + +namespace nvapi_impl { + +namespace { + +constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828; +constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F; +constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E; +constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F; +constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2; +constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31; +constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8; +constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE; + +constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll"; + +struct SyntheticDisplay { + NvU32 display_id; + NvU32 width; + NvU32 height; + NvU32 color_depth; + NvS32 x; + NvS32 y; + NvU32 refresh_rate_1k; + NV_ROTATE rotation; + bool primary; +}; + +static bool provider_initialized = false; +static bool nvapi_initialized = false; +static int gpu_handle_storage = 0; +// snapshot of the Win32 display state exposed through synthetic NVAPI +static std::vector displays; + +static NvPhysicalGpuHandle get_gpu_handle() { + return reinterpret_cast(&gpu_handle_storage); +} + +static NV_ROTATE get_rotation(DWORD orientation) { + switch (orientation) { + case DMDO_90: + return NV_ROTATE_90; + case DMDO_180: + return NV_ROTATE_180; + case DMDO_270: + return NV_ROTATE_270; + default: + return NV_ROTATE_0; + } +} + +static std::vector enumerate_displays( + uint32_t main_refresh_hz, + uint32_t sub_refresh_hz) { + + std::vector result; + + // reuse the active monitor list, then read live modes after -mainmonitor changes + for (const auto &monitor : sysutils::enumerate_monitors()) { + DEVMODEA mode {}; + mode.dmSize = sizeof(mode); + if (!EnumDisplaySettingsExA( + monitor.display_name.c_str(), + ENUM_CURRENT_SETTINGS, + &mode, + 0)) { + continue; + } + + const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0; + result.push_back({ + .display_id = 0, + .width = mode.dmPelsWidth, + .height = mode.dmPelsHeight, + .color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32, + .x = mode.dmPosition.x, + .y = mode.dmPosition.y, + .refresh_rate_1k = 0, + .rotation = get_rotation(mode.dmDisplayOrientation), + .primary = primary, + }); + } + + std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) { + return left.primary && !right.primary; + }); + + if (result.size() > 2) { + result.resize(2); + } + + if (result.empty()) { + result.push_back({ + .display_id = 0, + .width = 1920, + .height = 1080, + .color_depth = 32, + .x = 0, + .y = 0, + .refresh_rate_1k = 0, + .rotation = NV_ROTATE_0, + .primary = true, + }); + } + + for (size_t index = 0; index < result.size(); index++) { + auto &display = result[index]; + display.primary = index == 0; + display.display_id = 0x80000000u | static_cast(index + 1); + const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz; + display.refresh_rate_1k = refresh_hz * 1000; + } + + return result; +} + +// initializes NVAPI for the calling process. +// marks the synthetic provider initialized without contacting a driver. +static NvAPI_Status __cdecl NvAPI_Initialize_impl() { + log_misc("nvapi_impl", "NvAPI_Initialize"); + nvapi_initialized = true; + return NVAPI_OK; +} + +// initializes NVAPI with additional client flags. +// accepts the flags and marks the synthetic provider initialized. +static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) { + log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags); + nvapi_initialized = true; + return NVAPI_OK; +} + +// releases NVAPI state held for the calling process. +// clears the synthetic initialization state while leaving the provider installed. +static NvAPI_Status __cdecl NvAPI_Unload_impl() { + log_misc("nvapi_impl", "NvAPI_Unload"); + nvapi_initialized = false; + return NVAPI_OK; +} + +// enumerates physical GPU handles managed by the NVIDIA driver. +// returns one stable synthetic GPU containing all exposed displays. +static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl( + NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS], + NvU32 *gpu_count) { + + log_misc( + "nvapi_impl", + "NvAPI_EnumPhysicalGPUs(handles={}, count={})", + fmt::ptr(gpu_handles), + fmt::ptr(gpu_count)); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + if (gpu_handles == nullptr || gpu_count == nullptr) { + return NVAPI_INVALID_ARGUMENT; + } + + gpu_handles[0] = get_gpu_handle(); + *gpu_count = 1; + log_misc( + "nvapi_impl", + "NvAPI_EnumPhysicalGPUs - gpu={}, count={}", + fmt::ptr(gpu_handles[0]), + *gpu_count); + return NVAPI_OK; +} + +// returns connected display descriptors for a physical GPU. +// exposes the monitor snapshot as DP primary and HDMI secondary displays. +static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl( + NvPhysicalGpuHandle gpu_handle, + NV_GPU_DISPLAYIDS *display_ids, + NvU32 *display_id_count, + NvU32 flags) { + + const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0; + log_misc( + "nvapi_impl", + "NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})", + fmt::ptr(gpu_handle), + fmt::ptr(display_ids), + input_count, + flags); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + if (gpu_handle != get_gpu_handle()) { + return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE; + } + if (display_id_count == nullptr) { + return NVAPI_INVALID_ARGUMENT; + } + + const NvU32 required_count = static_cast(displays.size()); + if (display_ids == nullptr) { + *display_id_count = required_count; + log_misc( + "nvapi_impl", + "NvAPI_GPU_GetConnectedDisplayIds - required_count={}", + required_count); + return NVAPI_OK; + } + + const NvU32 capacity = *display_id_count; + *display_id_count = required_count; + if (capacity < required_count) { + return NVAPI_INSUFFICIENT_BUFFER; + } + + for (NvU32 index = 0; index < required_count; index++) { + const auto &source = displays[index]; + auto &destination = display_ids[index]; + destination = {}; + destination.version = NV_GPU_DISPLAYIDS_VER; + destination.connectorType = source.primary ? + NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI; + destination.displayId = source.display_id; + destination.isActive = 1; + destination.isOSVisible = 1; + destination.isConnected = 1; + destination.isPhysicallyConnected = 1; + } + + log_misc( + "nvapi_impl", + "NvAPI_GPU_GetConnectedDisplayIds - returned_count={}", + required_count); + return NVAPI_OK; +} + +// returns the NVAPI display ID associated with the Windows GDI primary. +// returns the first synthetic display, ordered from the live desktop origin. +static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) { + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})", + fmt::ptr(display_id)); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + if (display_id == nullptr || displays.empty()) { + return NVAPI_INVALID_ARGUMENT; + } + + *display_id = displays.front().display_id; + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}", + *display_id); + return NVAPI_OK; +} + +static void fill_source_mode( + NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination, + const SyntheticDisplay &source) { + + if (destination == nullptr) { + return; + } + + *destination = {}; + destination->resolution.width = source.width; + destination->resolution.height = source.height; + destination->resolution.colorDepth = source.color_depth; + destination->colorFormat = NV_FORMAT_A8R8G8B8; + destination->position.x = source.x; + destination->position.y = source.y; + destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE; + destination->bGDIPrimary = source.primary ? 1 : 0; +} + +static NvAPI_Status fill_target( + NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination, + const SyntheticDisplay &source, + NvU32 target_id) { + + if (destination == nullptr) { + return NVAPI_OK; + } + + auto *details = destination->details; + destination->displayId = source.display_id; + destination->targetId = target_id; + + if (details == nullptr) { + return NVAPI_OK; + } + if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) { + return NVAPI_INCOMPATIBLE_STRUCT_VERSION; + } + + *details = {}; + details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER; + details->rotation = source.rotation; + details->scaling = NV_SCALING_DEFAULT; + details->refreshRate1K = source.refresh_rate_1k; + details->timingOverride = NV_TIMING_OVERRIDE_CURRENT; + return NVAPI_OK; +} + +// retrieves the current global display topology through NVAPI's three-pass contract. +// fills caller-owned buffers from the synthetic monitor snapshot and configured rates. +static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl( + NvU32 *path_info_count, + NV_DISPLAYCONFIG_PATH_INFO *path_info) { + + const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0; + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetDisplayConfig(count={}, paths={})", + input_count, + fmt::ptr(path_info)); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + if (path_info_count == nullptr) { + return NVAPI_INVALID_ARGUMENT; + } + + const NvU32 required_count = static_cast(displays.size()); + if (path_info == nullptr) { + *path_info_count = required_count; + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetDisplayConfig - required_count={}", + required_count); + return NVAPI_OK; + } + + const NvU32 capacity = *path_info_count; + *path_info_count = required_count; + if (capacity < required_count) { + return NVAPI_INSUFFICIENT_BUFFER; + } + + for (NvU32 index = 0; index < required_count; index++) { + auto &path = path_info[index]; + if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) { + return NVAPI_INCOMPATIBLE_STRUCT_VERSION; + } + if (path.targetInfo != nullptr && path.targetInfoCount < 1) { + return NVAPI_INSUFFICIENT_BUFFER; + } + + const auto &display = displays[index]; + path.sourceId = index; + path.targetInfoCount = 1; + path.IsNonNVIDIAAdapter = 0; + path.pOSAdapterID = nullptr; + fill_source_mode(path.sourceModeInfo, display); + + const NvAPI_Status status = fill_target(path.targetInfo, display, index); + if (status != NVAPI_OK) { + return status; + } + } + + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetDisplayConfig - returned_count={}", + required_count); + return NVAPI_OK; +} + +// applies a supplied global display topology through the NVIDIA driver. +// accepts the cabinet topology without making any changes to Windows. +static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl( + NvU32 path_info_count, + NV_DISPLAYCONFIG_PATH_INFO *path_info, + NvU32 flags) { + + log_misc( + "nvapi_impl", + "NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})", + path_info_count, + fmt::ptr(path_info), + flags); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + + log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success"); + return NVAPI_OK; +} + +template +static uintptr_t *query_result(T function) { + return reinterpret_cast(function); +} + +// resolves an NVAPI function ID to its implementation address. +// exposes only the synthetic entry points used by KFC and rejects all others. +static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) { + uintptr_t *result = nullptr; + switch (function_id) { + case NVAPI_INITIALIZE_ID: + result = query_result(NvAPI_Initialize_impl); + break; + case NVAPI_INITIALIZE_EX_ID: + result = query_result(NvAPI_InitializeEx_impl); + break; + case NVAPI_UNLOAD_ID: + result = query_result(NvAPI_Unload_impl); + break; + case NVAPI_ENUM_PHYSICAL_GPUS_ID: + result = query_result(NvAPI_EnumPhysicalGPUs_impl); + break; + case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID: + result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl); + break; + case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID: + result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl); + break; + case NVAPI_DISP_GET_DISPLAY_CONFIG_ID: + result = query_result(NvAPI_DISP_GetDisplayConfig_impl); + break; + case NVAPI_DISP_SET_DISPLAY_CONFIG_ID: + result = query_result(NvAPI_DISP_SetDisplayConfig_impl); + break; + default: + break; + } + + log_misc( + "nvapi_impl", + "NvAPI_QueryInterface(0x{:x}) - {}", + function_id, + result != nullptr ? "implemented" : "unsupported"); + return result; +} + +} + +bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) { + if (provider_initialized) { + return true; + } + if (dll == nullptr) { + log_warning("nvapi_impl", "invalid synthetic module handle"); + return false; + } + + displays = enumerate_displays(main_refresh_hz, sub_refresh_hz); + libraryhook_hook_library(NVAPI_DLL_NAME_A, dll); + libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl); + libraryhook_enable(); + + provider_initialized = true; + log_info( + "nvapi_impl", + "synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz", + NVAPI_DLL_NAME_A, + displays.size(), + main_refresh_hz, + sub_refresh_hz); + return true; +} + +} + +#endif diff --git a/src/spice2x/hooks/graphics/nvapi_impl.h b/src/spice2x/hooks/graphics/nvapi_impl.h new file mode 100644 index 0000000..a43c9bc --- /dev/null +++ b/src/spice2x/hooks/graphics/nvapi_impl.h @@ -0,0 +1,14 @@ +#pragma once + +#ifdef SPICE64 + +#include +#include + +namespace nvapi_impl { + +bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz); + +} + +#endif diff --git a/src/spice2x/hooks/libraryhook.cpp b/src/spice2x/hooks/libraryhook.cpp index e981586..ec72fbe 100644 --- a/src/spice2x/hooks/libraryhook.cpp +++ b/src/spice2x/hooks/libraryhook.cpp @@ -12,6 +12,7 @@ static robin_hood::unordered_map 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; @@ -44,6 +45,20 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) { 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 @@ -103,6 +118,7 @@ void libraryhook_enable(HMODULE module) { // 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);