From 3012139028a3b0fa83165782616e59a33037974d Mon Sep 17 00:00:00 2001 From: Tsung-Hsiang Chang Date: Sun, 2 Aug 2026 00:03:36 -0700 Subject: [PATCH] gitadora: add native dual fullscreen for Arena (#846) ## Link to GitHub Issue or related Pull Request, if one exists Fixed https://github.com/spice2x/spice2x.github.io/issues/740 ## Description of change - Enable native D3D9 adapter-group dual fullscreen for the GITADORA Arena model when the two-screen MAIN + SMALL configuration is selected without windowed mode. - Present MAIN and SMALL on their respective monitor heads while keeping the invisible LEFT and RIGHT targets offscreen. - Scope the new fake-swap-chain query behavior to the hidden GITADORA two-head targets, preserving existing behavior for other games and configurations. The existing borderless-windowed two-screen path could not consistently keep input and game timing synchronized and also reduced rendering performance. Native dual fullscreen avoids that windowed composition path. ## Testing - Manually tested GITADORA Arena with separate MAIN and SMALL monitors in fullscreen mode. Windowed mode still works if the user chooses to use that. - Confirmed both displays render correctly, touch input works on the SMALL screen, gameplay input stays synchronized, and gameplay holds a steady 60 FPS. - Built `spicetools_spice64` at commit `e4a98e9` with the repository Docker toolchain. - Confirmed no new compiler warnings. - Confirmed the forbidden static DLL import check passes. - Confirmed the Windows 7 DLL compatibility check reports `All DLLs OK!`. --------- Co-authored-by: vgod <428979+vgod@users.noreply.github.com> --- src/spice2x/CMakeLists.txt | 1 + src/spice2x/games/gitadora/gitadora.cpp | 213 ++++-- src/spice2x/games/gitadora/gitadora.h | 1 + .../graphics/backends/d3d9/d3d9_backend.cpp | 247 +++++- .../graphics/backends/d3d9/d3d9_backend.h | 2 + .../graphics/backends/d3d9/d3d9_device.cpp | 338 ++++++++- .../graphics/backends/d3d9/d3d9_device.h | 76 +- .../backends/d3d9/d3d9_fake_swapchain.cpp | 86 ++- .../backends/d3d9/d3d9_fake_swapchain.h | 11 +- .../graphics/backends/d3d9/d3d9_gfdm.cpp | 715 ++++++++++++++++++ .../hooks/graphics/backends/d3d9/d3d9_gfdm.h | 78 ++ .../graphics/backends/d3d9/d3d9_swapchain.cpp | 39 +- .../graphics/backends/d3d9/d3d9_swapchain.h | 17 +- src/spice2x/hooks/graphics/graphics.cpp | 136 +++- src/spice2x/hooks/graphics/graphics.h | 9 + src/spice2x/launcher/options.cpp | 4 +- 16 files changed, 1870 insertions(+), 103 deletions(-) create mode 100644 src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp create mode 100644 src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 50562af..5711010 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -536,6 +536,7 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/graphics/nvenc_hook.cpp hooks/graphics/backends/d3d9/d3d9_backend.cpp hooks/graphics/backends/d3d9/d3d9_device.cpp + hooks/graphics/backends/d3d9/d3d9_gfdm.cpp hooks/graphics/backends/d3d9/d3d9_live2d.cpp hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_swapchain.cpp diff --git a/src/spice2x/games/gitadora/gitadora.cpp b/src/spice2x/games/gitadora/gitadora.cpp index 5a6c3ca..1758044 100644 --- a/src/spice2x/games/gitadora/gitadora.cpp +++ b/src/spice2x/games/gitadora/gitadora.cpp @@ -2,12 +2,14 @@ #include "asio.h" #include "handle.h" #include "bi2x_hook.h" +#include #include +#include "cfg/configurator.h" +#include "cfg/screen_resize.h" + #include #include - -#include "cfg/configurator.h" #include "hooks/audio/audio.h" #include "hooks/audio/mme.h" #include "hooks/graphics/graphics.h" @@ -35,6 +37,7 @@ namespace games::gitadora { std::optional SUBSCREEN_OVERLAY_SIZE; std::optional PICK_ALGO = socd::SocdAlgorithm::PreferRecent; std::optional ARENA_WINDOW_COUNT = std::nullopt; + bool ARENA_TWO_HEAD_EXCLUSIVE = false; std::optional ASIO_DRIVER = std::nullopt; bool ALLOW_REALTEK_AUDIO = false; @@ -271,9 +274,21 @@ namespace games::gitadora { break; case 2: if (!GRAPHICS_WINDOWED) { - log_fatal( + if (D3D9_ADAPTER.has_value()) { + log_fatal( + "gitadora", + "arena model: fullscreen two-window mode cannot use -monitor; " + "use -w for borderless windows instead"); + } + ARENA_TWO_HEAD_EXCLUSIVE = true; + log_info( "gitadora", - "arena model: 2-window mode is not supported in fullscreen, choose 1 or 4"); + "arena model: native two-head fullscreen adapter-group mode " + "(MAIN + SMALL; LEFT/RIGHT are virtual)"); + } else { + log_info( + "gitadora", + "arena model: two-window mode uses windowed rendering"); } log_info("gitadora", "arena model: two-window mode"); GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true; @@ -299,10 +314,18 @@ namespace games::gitadora { static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr; static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr; - // cached primary real monitor: its path + source/target mode entries. - // modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained. + // Cached real monitor paths and their source/target mode entries. modeInfoIdx values are + // renumbered so the cache is self-contained. The normal single-head emulation keeps only + // MAIN; the two-head adapter-group mode keeps MAIN and SMALL as real heads. static DISPLAYCONFIG_PATH_INFO real_primary_path = {}; static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target + static DISPLAYCONFIG_PATH_INFO real_small_path = {}; + static DISPLAYCONFIG_MODE_INFO real_small_modes[2] = {}; // [0]=source, [1]=target + static bool real_small_path_available = false; + + static bool is_two_head_exclusive() { + return ARENA_TWO_HEAD_EXCLUSIVE && !GRAPHICS_WINDOWED; + } // fake monitors appended after the real ones. the game classifies monitors // by outputTechnology + connectorInstance: @@ -326,12 +349,31 @@ namespace games::gitadora { // adapter to a swap chain role via its DisplayConfig connector instance, // so the entries here must be listed in the same order the wrapper enumerates // them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small. - static constexpr FakeMonitor FAKE_MONITORS[] = { + // Normal single-head emulation: every non-MAIN role is fake. + static constexpr FakeMonitor FAKE_MONITORS_SINGLE_HEAD[] = { { 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0) { 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1) { 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch) }; - static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast(std::size(FAKE_MONITORS)); + + // Two-head fullscreen: adapters 0/1 are the real MAIN/SMALL group heads. + // Keep only LEFT/RIGHT fake, with their adapter ids matching the D3D9 wrapper. + static constexpr FakeMonitor FAKE_MONITORS_TWO_HEAD[] = { + { 2, 1080, 1920, -200000, -200000, 0 }, // left (DP connector instance 0) + { 3, 1080, 1920, -300000, -300000, 1 }, // right (DP connector instance 1) + }; + + static std::span get_fake_monitors() { + if (is_two_head_exclusive()) { + return FAKE_MONITORS_TWO_HEAD; + } + + return FAKE_MONITORS_SINGLE_HEAD; + } + + static UINT32 real_monitor_count() { + return is_two_head_exclusive() ? 2 : 1; + } // call QueryDisplayConfig once, keep only the primary monitor's path and its // two referenced modes (source + target). modeInfoIdx values are rewritten to @@ -369,13 +411,55 @@ namespace games::gitadora { log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found"); } + if (primary->targetInfo.modeInfoIdx >= all_modes.size()) { + log_fatal("gitadora", "cache_primary_monitor_info: primary target mode is missing"); + } + real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx]; real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx]; real_primary_path = *primary; real_primary_path.sourceInfo.modeInfoIdx = 0; real_primary_path.targetInfo.modeInfoIdx = 1; - log_info("gitadora", "cache_primary_monitor_info: cached primary monitor"); + real_small_path_available = false; + if (is_two_head_exclusive()) { + if (all_paths.size() != 2) { + log_fatal( + "gitadora", + "two-head fullscreen mode requires exactly two active display paths, found {}", + all_paths.size()); + } + + const auto secondary = std::find_if(all_paths.begin(), all_paths.end(), + [&](const auto &p) { + return &p != &*primary; + }); + if (secondary == all_paths.end() || + secondary->sourceInfo.modeInfoIdx >= all_modes.size() || + secondary->targetInfo.modeInfoIdx >= all_modes.size()) { + log_fatal("gitadora", "could not identify the physical SMALL monitor"); + } + const LUID primary_adapter = primary->sourceInfo.adapterId; + const LUID secondary_adapter = secondary->sourceInfo.adapterId; + const bool same_adapter = primary_adapter.HighPart == secondary_adapter.HighPart && + primary_adapter.LowPart == secondary_adapter.LowPart; + if (!same_adapter) { + log_fatal( + "gitadora", + "MAIN and SMALL must use the same display adapter"); + } + + real_small_modes[0] = all_modes[secondary->sourceInfo.modeInfoIdx]; + real_small_modes[1] = all_modes[secondary->targetInfo.modeInfoIdx]; + real_small_path = *secondary; + real_small_path.sourceInfo.modeInfoIdx = 2; + real_small_path.targetInfo.modeInfoIdx = 3; + real_small_path_available = true; + + log_info("gitadora", "cache_primary_monitor_info: cached real MAIN and SMALL monitors"); + } else { + log_info("gitadora", "cache_primary_monitor_info: cached primary monitor"); + } } static @@ -390,33 +474,36 @@ namespace games::gitadora { static std::once_flag populate_once; std::call_once(populate_once, cache_primary_monitor_info); - // always report exactly 1 real + N fake monitors - *pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT; - *pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2; + const auto fake_monitors = get_fake_monitors(); + const auto fake_count = static_cast(fake_monitors.size()); + const UINT32 real_count = real_monitor_count(); + *pNumPathArrayElements = real_count + fake_count; + *pNumModeInfoArrayElements = (real_count + fake_count) * 2; log_info( "gitadora", - "GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)", - FAKE_MONITOR_COUNT); + "GetDisplayConfigBufferSizes: {} real path(s) + {} fake monitor(s)", + real_count, + fake_count); return ERROR_SUCCESS; } - // write fake monitor i into the caller's path/mode arrays. layout (single-monitor - // assumption): index 0 in both arrays holds the cached primary real monitor, so - // fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1). + // Write a fake monitor after the real paths. The cache is packed as [source, target] + // mode pairs, so each path consumes two mode entries. static void insert_fake_monitor( DISPLAYCONFIG_PATH_INFO *paths, DISPLAYCONFIG_MODE_INFO *modes, - UINT32 i) + const FakeMonitor &m, + UINT32 path_index, + UINT32 source_mode_index) { - const FakeMonitor &m = FAKE_MONITORS[i]; - const UINT32 src_idx = 2 + i * 2; + const UINT32 src_idx = source_mode_index; const UINT32 tgt_idx = src_idx + 1; const LUID adapter_id { .LowPart = static_cast(-m.id), .HighPart = -m.id }; const UINT32 uid = static_cast(-m.id); - paths[1 + i] = { + paths[path_index] = { .sourceInfo = { .adapterId = adapter_id, .id = uid, @@ -474,21 +561,35 @@ namespace games::gitadora { DISPLAYCONFIG_MODE_INFO* modeInfoArray, DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId) { - // copy cached primary real monitor into caller buffers at index 0 + // Copy cached real monitor paths into the caller buffers. pathArray[0] = real_primary_path; modeInfoArray[0] = real_primary_modes[0]; modeInfoArray[1] = real_primary_modes[1]; - *numPathArrayElements = 1 + FAKE_MONITOR_COUNT; - *numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2; + if (is_two_head_exclusive() && real_small_path_available) { + pathArray[1] = real_small_path; + modeInfoArray[2] = real_small_modes[0]; + modeInfoArray[3] = real_small_modes[1]; + } + + const auto fake_monitors = get_fake_monitors(); + const auto fake_count = static_cast(fake_monitors.size()); + const UINT32 real_count = real_monitor_count(); + *numPathArrayElements = real_count + fake_count; + *numModeInfoArrayElements = (real_count + fake_count) * 2; if (currentTopologyId != nullptr) { *currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND; } log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes"); - // append fake monitors after the real one - for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) { - insert_fake_monitor(pathArray, modeInfoArray, i); + // Append fake monitors after the real path(s). + for (UINT32 i = 0; i < fake_count; i++) { + insert_fake_monitor( + pathArray, + modeInfoArray, + fake_monitors[i], + real_count + i, + real_count * 2 + i * 2); } return ERROR_SUCCESS; @@ -519,7 +620,8 @@ namespace games::gitadora { const auto targetName = reinterpret_cast(requestPacket); const LONG fake_id = -id; UINT32 conn_inst = 0xff; - for (const auto& f : FAKE_MONITORS) { + const auto fake_monitors = get_fake_monitors(); + for (const auto &f : fake_monitors) { if (f.id == fake_id) { conn_inst = f.connector_instance; break; @@ -542,18 +644,27 @@ namespace games::gitadora { return ret; } - // override the cached primary real monitor target info to look like HDMI/0 + // Override MAIN to HDMI/0 and retag the real second head as the DP/2 SMALL + // display expected by the cabinet software in two-head fullscreen mode. const auto targetName = reinterpret_cast(requestPacket); - const auto& target = real_primary_path.targetInfo; - if (target.id == targetName->header.id && - target.adapterId.HighPart == targetName->header.adapterId.HighPart && - target.adapterId.LowPart == targetName->header.adapterId.LowPart) - { + const auto target_matches = [&](const DISPLAYCONFIG_PATH_TARGET_INFO &target) { + return target.id == targetName->header.id && + target.adapterId.HighPart == targetName->header.adapterId.HighPart && + target.adapterId.LowPart == targetName->header.adapterId.LowPart; + }; + if (target_matches(real_primary_path.targetInfo)) { targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI; targetName->connectorInstance = 0; log_info("gitadora", "overriding primary monitor (id={}) to pretend to be HDMI", targetName->header.id); + } else if (is_two_head_exclusive() && real_small_path_available && + target_matches(real_small_path.targetInfo)) { + targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL; + targetName->connectorInstance = 2; + log_info("gitadora", + "overriding secondary monitor (id={}) to pretend to be SMALL DP/2", + targetName->header.id); } return ret; } @@ -651,7 +762,8 @@ namespace games::gitadora { hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true; } - // monitor/touch hooks (windowed or full screen) + // Single-window mode needs touch injection for its overlay. Two-head fullscreen + // mode uses the real SMALL output, so it only needs the display-topology shim. if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) { // enable touch hook for subscreen overlay const auto native_touch_ready = !wintouchemu::FORCE && @@ -661,25 +773,28 @@ namespace games::gitadora { wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE); } + } #if !SPICE_XP - if (!GRAPHICS_WINDOWED) { - // monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors - // (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology - GetDisplayConfigBufferSizes_orig = - detour::iat_try("GetDisplayConfigBufferSizes", - GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE); - QueryDisplayConfig_orig = - detour::iat_try("QueryDisplayConfig", - QueryDisplayConfig_hook, avs::game::DLL_INSTANCE); - DisplayConfigGetDeviceInfo_orig = - detour::iat_try("DisplayConfigGetDeviceInfo", - DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE); + if (!GRAPHICS_WINDOWED && + (GRAPHICS_PREVENT_SECONDARY_WINDOWS || ARENA_TWO_HEAD_EXCLUSIVE)) { + if (ARENA_TWO_HEAD_EXCLUSIVE) { + log_info( + "gitadora", + "exposing physical MAIN/SMALL and virtual LEFT/RIGHT"); } -#endif - + GetDisplayConfigBufferSizes_orig = + detour::iat_try("GetDisplayConfigBufferSizes", + GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE); + QueryDisplayConfig_orig = + detour::iat_try("QueryDisplayConfig", + QueryDisplayConfig_hook, avs::game::DLL_INSTANCE); + DisplayConfigGetDeviceInfo_orig = + detour::iat_try("DisplayConfigGetDeviceInfo", + DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE); } +#endif } // window patch diff --git a/src/spice2x/games/gitadora/gitadora.h b/src/spice2x/games/gitadora/gitadora.h index e910af8..8e4120a 100644 --- a/src/spice2x/games/gitadora/gitadora.h +++ b/src/spice2x/games/gitadora/gitadora.h @@ -19,6 +19,7 @@ namespace games::gitadora { extern std::optional SUBSCREEN_OVERLAY_SIZE; extern std::optional PICK_ALGO; extern std::optional ARENA_WINDOW_COUNT; + extern bool ARENA_TWO_HEAD_EXCLUSIVE; extern std::optional ASIO_DRIVER; extern bool ALLOW_REALTEK_AUDIO; diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index 5decc73..370c5c8 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -113,7 +113,10 @@ static Direct3DCreate9On12Ex_t Direct3DCreate9On12Ex_orig = nullptr; static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false; static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr; -static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params); +static void graphics_d3d9_ldj_init_sub_screen( + IDirect3DDevice9Ex *device, + D3DPRESENT_PARAMETERS *present_params, + UINT gfdm_small_swapchain); static std::string behavior2s(DWORD behavior_flags) { FLAGS_START(behavior_flags); @@ -409,7 +412,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::QueryInterface( return E_POINTER; } - if (riid == IID_WrappedIDirect3D9 || + if ((riid == IID_IUnknown && gfdm_two_head_exclusive()) || + riid == IID_WrappedIDirect3D9 || riid == IID_IDirect3D9 || riid == IID_IDirect3D9Ex) { @@ -467,6 +471,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() { UINT result = pReal->GetAdapterCount(); + if (gfdm_two_head_exclusive()) { + if (result == 2) { + FAKE_SUBSCREEN_ADAPTER = true; + return GFDM_LOGICAL_HEAD_COUNT; + } + log_warning( + "graphics::d3d9", + "two-head mode requires two native adapters; found {}", + result); + return result; + } + if (!FAKE_SUBSCREEN_ADAPTER) { if (games::popn::is_pikapika_model() && result == 1) { FAKE_SUBSCREEN_ADAPTER = true; @@ -493,7 +509,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier( D3DADAPTER_IDENTIFIER9 *pIdentifier) { - if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) { + if (is_fake_subscreen_adapter(Adapter) && pIdentifier) { *pIdentifier = {}; const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter); strcpy(pIdentifier->DeviceName, adapter_name.c_str()); @@ -509,7 +525,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier( } UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) { - if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) { + if (is_fake_subscreen_adapter(Adapter)) { log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter); return 1; } @@ -523,13 +539,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes( UINT Mode, D3DDISPLAYMODE *pMode) { - if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) { + if (is_fake_subscreen_adapter(Adapter)) { log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter); if (Mode == 0 && pMode) { - pMode->Width = 1280; - pMode->Height = 800; - pMode->RefreshRate = 60; - pMode->Format = D3DFMT_X8R8G8B8; + get_fake_subscreen_display_mode(Adapter, pMode); return S_OK; } else { return D3DERR_INVALIDCALL; @@ -611,6 +624,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes( } HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE *pMode) { + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) { + if (pMode == nullptr) { + return D3DERR_INVALIDCALL; + } + get_fake_subscreen_display_mode(Adapter, pMode); + return D3D_OK; + } + CHECK_RESULT(pReal->GetAdapterDisplayMode(Adapter, pMode)); } @@ -621,6 +642,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceType( D3DFORMAT BackBufferFormat, BOOL bWindowed) { + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(iAdapter)) { + return D3D_OK; + } + CHECK_RESULT(pReal->CheckDeviceType(iAdapter, DevType, DisplayFormat, BackBufferFormat, bWindowed)); } @@ -632,7 +657,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat( D3DRESOURCETYPE RType, D3DFORMAT CheckFormat) { - if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) { + if (is_fake_subscreen_adapter(Adapter)) { log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter); return D3D_OK; } @@ -648,6 +673,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceMultiSampleType( D3DMULTISAMPLE_TYPE MultiSampleType, DWORD *pQualityLevels) { + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) { + if (pQualityLevels != nullptr) { + *pQualityLevels = 1; + } + return D3D_OK; + } + CHECK_RESULT(pReal->CheckDeviceMultiSampleType( Adapter, DeviceType, @@ -664,6 +696,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDepthStencilMatch( D3DFORMAT RenderTargetFormat, D3DFORMAT DepthStencilFormat) { + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) { + return D3D_OK; + } + CHECK_RESULT(pReal->CheckDepthStencilMatch( Adapter, DeviceType, @@ -678,6 +714,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormatConversion( D3DFORMAT SourceFormat, D3DFORMAT TargetFormat) { + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) { + return D3D_OK; + } + CHECK_RESULT(pReal->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat)); } @@ -689,6 +729,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT return D3DERR_INVALIDCALL; } + if (gfdm_two_head_exclusive() && Adapter < GFDM_LOGICAL_HEAD_COUNT) { + HRESULT ret = this->pReal->GetDeviceCaps( + Adapter >= 2 ? 0 : Adapter, + DeviceType, + pCaps); + if (SUCCEEDED(ret)) { + pCaps->NumberOfAdaptersInGroup = + Adapter == 0 ? GFDM_LOGICAL_HEAD_COUNT : 0; + pCaps->MasterAdapterOrdinal = 0; + pCaps->AdapterOrdinalInGroup = Adapter; + } + return ret; + } + // SDVX uses `NumberOfAdaptersInGroup` to allocate a vector and the Microsoft documentation states: // "The value will be 0 for a subordinate adapter of a multihead card. Each card can have at most one // master, but may have many subordinates." Therefore, this must point to the master adapter. @@ -741,6 +795,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT } HMONITOR STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterMonitor(UINT Adapter) { + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) { + return pReal->GetAdapterMonitor(0); + } + return pReal->GetAdapterMonitor(Adapter); } @@ -861,7 +919,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice( "either use -graphics-force-refresh option or change the desktop resolution beforehand."); } - if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) { + if (!GRAPHICS_WINDOWED && num_adapters >= 2 + && GRAPHICS_FORCE_REFRESH_SUB.has_value()) { log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)", pPresentationParameters[1].FullScreen_RefreshRateInHz, GRAPHICS_FORCE_REFRESH_SUB.value()); @@ -902,7 +961,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice( } else if (!D3D9_DEVICE_HOOK_DISABLE) { graphics_hook_window(hFocusWindow, pPresentationParameters); - *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface); + *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9( + hFocusWindow, + *ppReturnedDeviceInterface); } // return result @@ -916,6 +977,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice( UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCountEx(UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter) { assert(is_d3d9ex); + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) { + return 1; + } + return static_cast(pReal)->GetAdapterModeCountEx(Adapter, pFilter); } @@ -926,6 +991,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModesEx( D3DDISPLAYMODEEX *pMode) { assert(is_d3d9ex); + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) { + if (Mode != 0 || pMode == nullptr) { + return D3DERR_INVALIDCALL; + } + get_fake_subscreen_display_mode_ex(Adapter, pMode); + return D3D_OK; + } + CHECK_RESULT(static_cast(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode)); } @@ -935,6 +1008,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayModeEx( D3DDISPLAYROTATION *pRotation) { assert(is_d3d9ex); + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) { + if (pMode == nullptr) { + return D3DERR_INVALIDCALL; + } + get_fake_subscreen_display_mode_ex(Adapter, pMode); + if (pRotation != nullptr) { + *pRotation = D3DDISPLAYROTATION_IDENTITY; + } + return D3D_OK; + } + CHECK_RESULT(static_cast(pReal)->GetAdapterDisplayModeEx(Adapter, pMode, pRotation)); } @@ -963,6 +1047,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx( return D3DERR_INVALIDCALL; } + if (gfdm_two_head_exclusive() && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) { + log_warning( + "graphics::d3d9", + "-forceressub is unavailable; SMALL must remain {}x{}", + GFDM_SMALL_WIDTH, + GFDM_SMALL_HEIGHT); + return D3DERR_INVALIDCALL; + } DWORD orig_behavior_flags = BehaviorFlags; size_t num_adapters = 1; @@ -989,6 +1081,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx( if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) { num_adapters = device_caps.NumberOfAdaptersInGroup; } + if (gfdm_two_head_exclusive()) { + num_adapters = GFDM_LOGICAL_HEAD_COUNT; + } } for (size_t i = 0; i < num_adapters; i++) { @@ -1105,7 +1200,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx( } } - if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) { + if (!GRAPHICS_WINDOWED && !gfdm_two_head_exclusive() + && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) { log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)", pPresentationParameters[1].FullScreen_RefreshRateInHz, GRAPHICS_FORCE_REFRESH_SUB.value()); @@ -1131,16 +1227,93 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx( pPresentationParameters->BackBufferCount = GRAPHICS_FORCE_VSYNC_BUFFER.value(); } + GfdmTwoHeadDeviceState gfdm_parameters( + pPresentationParameters, + pFullscreenDisplayMode); + if (gfdm_two_head_exclusive()) { + if (!(BehaviorFlags & D3DCREATE_ADAPTERGROUP_DEVICE)) { + log_warning( + "graphics::d3d9", + "CreateDeviceEx did not request an adapter-group device"); + return D3DERR_NOTAVAILABLE; + } + HRESULT select = graphics_d3d9_gfdm_select_two_head_group_parameters( + pPresentationParameters, + pFullscreenDisplayMode, + gfdm_parameters.native_presentation_parameters.data(), + gfdm_parameters.native_fullscreen_display_modes.data(), + &gfdm_parameters.logical_small_swapchain, + "CreateDeviceEx"); + if (FAILED(select)) { + return select; + } + if (GRAPHICS_FORCE_REFRESH_SUB.has_value()) { + gfdm_parameters.native_presentation_parameters[1] + .FullScreen_RefreshRateInHz = + GRAPHICS_FORCE_REFRESH_SUB.value(); + if (pFullscreenDisplayMode != nullptr) { + gfdm_parameters.native_fullscreen_display_modes[1].RefreshRate = + GRAPHICS_FORCE_REFRESH_SUB.value(); + } + } + HRESULT remap = graphics_d3d9_gfdm_remap_two_head_group_parameters( + gfdm_parameters.native_presentation_parameters.data(), + pFullscreenDisplayMode != nullptr + ? gfdm_parameters.native_fullscreen_display_modes.data() + : nullptr, + "CreateDeviceEx"); + if (FAILED(remap)) { + return remap; + } + if (pFullscreenDisplayMode != nullptr) { + graphics_d3d9_gfdm_align_two_head_refresh_to_desktop( + static_cast(this->pReal), + Adapter, + gfdm_parameters.native_presentation_parameters.data(), + gfdm_parameters.native_fullscreen_display_modes.data(), + "CreateDeviceEx"); + } + if (!graphics_gitadora_prepare_two_head_device_window( + gfdm_parameters.native_presentation_parameters[1].hDeviceWindow, + this->pReal->GetAdapterMonitor(Adapter + 1), + gfdm_parameters.native_presentation_parameters[1].BackBufferWidth, + gfdm_parameters.native_presentation_parameters[1].BackBufferHeight)) + { + return D3DERR_INVALIDCALL; + } + HRESULT validation = validate_gfdm_two_head_exclusive( + this->pReal, + Adapter, + DeviceType, + BehaviorFlags, + gfdm_parameters.native_presentation_parameters.data()); + if (FAILED(validation)) { + return validation; + } + gfdm_parameters.use_native_parameters(); + } + // call original HRESULT result = static_cast(this->pReal)->CreateDeviceEx( Adapter, DeviceType, hFocusWindow, BehaviorFlags, - pPresentationParameters, - pFullscreenDisplayMode, + gfdm_parameters.presentation_parameters, + gfdm_parameters.fullscreen_display_modes, ppReturnedDeviceInterface); + if (SUCCEEDED(result) && gfdm_two_head_exclusive()) { + pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0]; + pPresentationParameters[gfdm_parameters.logical_small_swapchain] = + gfdm_parameters.presentation_parameters[1]; + if (pFullscreenDisplayMode != nullptr) { + pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0]; + pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] = + gfdm_parameters.fullscreen_display_modes[1]; + } + } + // check for error if (result != D3D_OK) { @@ -1151,17 +1324,28 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx( } else if (!D3D9_DEVICE_HOOK_DISABLE) { graphics_hook_window(hFocusWindow, pPresentationParameters); - *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface); + *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9( + hFocusWindow, + *ppReturnedDeviceInterface, + gfdm_parameters.logical_small_swapchain, + gfdm_two_head_exclusive() ? static_cast(this) : nullptr, + gfdm_two_head_exclusive() ? pPresentationParameters : nullptr); // initialize sub screen if the game requested a multi-head context if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) && (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) { - UINT i = 1; - if (games::gitadora::is_arena_model()) { - i = 2; - } - graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]); + UINT i = games::gitadora::is_arena_model() + ? gfdm_parameters.logical_small_swapchain + : 1; + D3DPRESENT_PARAMETERS *subscreen_parameters = + gfdm_two_head_exclusive() + ? &gfdm_parameters.presentation_parameters[1] + : &pPresentationParameters[i]; + graphics_d3d9_ldj_init_sub_screen( + *ppReturnedDeviceInterface, + subscreen_parameters, + i); } } @@ -1170,6 +1354,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx( HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *pLUID) { assert(is_d3d9ex); + if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) { + if (pLUID == nullptr) { + return D3DERR_INVALIDCALL; + } + pLUID->LowPart = static_cast(-static_cast(Adapter)); + pLUID->HighPart = -static_cast(Adapter); + return D3D_OK; + } CHECK_RESULT(static_cast(pReal)->GetAdapterLUID(Adapter, pLUID)); } @@ -1178,7 +1370,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID * // The sub screen swap chain should be created if: // - Running windowed with `NumberOfAdaptersInGroup >= 2` (game expects implicit swap chain to exist) // - Running fullscreen with `NumberOfAdaptersInGroup < 2` (overridden `GetDeviceCaps` structure) -static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params) { +static void graphics_d3d9_ldj_init_sub_screen( + IDirect3DDevice9Ex *device, + D3DPRESENT_PARAMETERS *present_params, + UINT gfdm_small_swapchain) +{ D3DCAPS9 caps {}; HRESULT hr = device->GetDeviceCaps(&caps); if (FAILED(hr)) { @@ -1212,10 +1408,9 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE } } else { - int swapchain = 1; - if (games::gitadora::is_arena_model()) { - swapchain = 2; - } + const int swapchain = games::gitadora::is_arena_model() + ? static_cast(gfdm_small_swapchain) + : 1; hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN); if (FAILED(hr)) { diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h index 9f957e3..c8902e8 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h @@ -2,6 +2,8 @@ #include +#include "d3d9_gfdm.h" + // {EEE9CCF6-53D6-4326-9AE5-60921B3DB394} static const GUID IID_WrappedIDirect3D9 = { 0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 } diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp index 45f72ba..1f7e31f 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp @@ -41,7 +41,6 @@ constexpr bool CUSTOM_RESET = false; constexpr D3DFORMAT D3DFMT_DF24 = static_cast(MAKEFOURCC('D', 'F', '2', '4')); - auto format_as(D3DPOOL f) { return fmt::underlying(f); } @@ -85,7 +84,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::QueryInterface( return E_POINTER; } - if (riid == IID_WrappedIDirect3DDevice9 || + if ((riid == IID_IUnknown && is_gfdm_two_head_exclusive()) || + riid == IID_WrappedIDirect3DDevice9 || riid == IID_IDirect3DDevice9 || riid == IID_IDirect3DDevice9Ex) { @@ -202,6 +202,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDirect3D( IDirect3D9 **ppD3D9) { WRAP_VERBOSE; + if (is_gfdm_two_head_exclusive() && gfdm_parent_d3d != nullptr) { + if (ppD3D9 == nullptr) { + return D3DERR_INVALIDCALL; + } + gfdm_parent_d3d->AddRef(); + *ppD3D9 = gfdm_parent_d3d; + return D3D_OK; + } + CHECK_RESULT(pReal->GetDirect3D(ppD3D9)); } @@ -209,7 +218,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDeviceCaps( D3DCAPS9 *pCaps) { WRAP_VERBOSE; - CHECK_RESULT(pReal->GetDeviceCaps(pCaps)); + HRESULT result = pReal->GetDeviceCaps(pCaps); + if (SUCCEEDED(result) && is_gfdm_two_head_exclusive() && pCaps != nullptr) { + pCaps->NumberOfAdaptersInGroup = GFDM_LOGICAL_HEAD_COUNT; + pCaps->MasterAdapterOrdinal = 0; + pCaps->AdapterOrdinalInGroup = 0; + } + CHECK_RESULT(result); } HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode( @@ -217,6 +232,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode( D3DDISPLAYMODE *pMode) { WRAP_VERBOSE; + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_side_swapchain(iSwapChain)) + { + return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetDisplayMode(pMode); + } + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_small_swapchain(iSwapChain)) + { + CHECK_RESULT(pReal->GetDisplayMode(GFDM_NATIVE_SMALL_SWAPCHAIN, pMode)); + } + CHECK_RESULT(pReal->GetDisplayMode(iSwapChain, pMode)); } @@ -258,6 +284,51 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain( { WRAP_VERBOSE; + if (pPresentationParameters == nullptr || ppSwapChain == nullptr) { + return D3DERR_INVALIDCALL; + } + + if (is_gfdm_two_head_exclusive()) { + if (pPresentationParameters->BackBufferWidth == 800) { + return GetSwapChain(gfdm_logical_small_swapchain, ppSwapChain); + } + if (pPresentationParameters->BackBufferWidth != 1080) { + log_warning( + "graphics::d3d9", + "two-head exclusive: rejecting unexpected additional swap chain {}x{}", + pPresentationParameters->BackBufferWidth, + pPresentationParameters->BackBufferHeight); + return D3DERR_INVALIDCALL; + } + + size_t index = 3; + for (UINT logical_swapchain = 1; + logical_swapchain < GFDM_LOGICAL_HEAD_COUNT; + logical_swapchain++) + { + if (!is_gfdm_logical_side_swapchain(logical_swapchain)) { + continue; + } + const size_t candidate = + gfdm_hidden_side_swapchain_slot(logical_swapchain); + if (fake_sub_swapchain[candidate] == nullptr) { + index = candidate; + break; + } + } + if (index == 3) { + return D3DERR_OUTOFVIDEOMEMORY; + } + fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9( + this, + pPresentationParameters, + is_d3d9ex, + true); + fake_sub_swapchain[index]->AddRef(); + *ppSwapChain = static_cast(fake_sub_swapchain[index]); + return D3D_OK; + } + int index = 0; bool create_swap_chain = false; bool create_fake_swap_chain = false; @@ -271,6 +342,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain( // SMALL (subscreen) create_swap_chain = true; index = 0; + } else if (pPresentationParameters->BackBufferWidth == 1080) { // LEFT/RIGHT create_swap_chain = true; @@ -335,6 +407,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain( { WRAP_VERBOSE_FMT("GetSwapChain({})", iSwapChain); + if (ppSwapChain == nullptr) { + return D3DERR_INVALIDCALL; + } + if (iSwapChain == 0) { if (!main_swapchain) { HRESULT ret = pReal->GetSwapChain(iSwapChain, ppSwapChain); @@ -344,7 +420,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain( return ret; } - main_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain); + main_swapchain = new WrappedIDirect3DSwapChain9( + this, + *ppSwapChain, + is_gfdm_two_head_exclusive() + ? WrappedIDirect3DSwapChain9::NativeGroupHead::Main + : WrappedIDirect3DSwapChain9::NativeGroupHead::Unknown); } main_swapchain->AddRef(); @@ -354,6 +435,39 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain( return D3D_OK; } + if (is_gfdm_two_head_exclusive()) { + if (is_gfdm_logical_small_swapchain(iSwapChain)) { + if (!sub_swapchain[0]) { + IDirect3DSwapChain9 *real_swapchain = nullptr; + HRESULT ret = pReal->GetSwapChain( + GFDM_NATIVE_SMALL_SWAPCHAIN, + &real_swapchain); + if (FAILED(ret)) { + return ret; + } + sub_swapchain[0] = new WrappedIDirect3DSwapChain9( + this, + real_swapchain, + WrappedIDirect3DSwapChain9::NativeGroupHead::Small); + sub_swapchain[0]->should_run_hooks = false; + } + + sub_swapchain[0]->AddRef(); + *ppSwapChain = static_cast(sub_swapchain[0]); + graphics_screens_register(iSwapChain); + return D3D_OK; + } + if (is_gfdm_logical_side_swapchain(iSwapChain)) { + FakeIDirect3DSwapChain9 *swapchain = + ensure_gfdm_hidden_side_swapchain(iSwapChain); + swapchain->AddRef(); + *ppSwapChain = static_cast(swapchain); + graphics_screens_register(iSwapChain); + return D3D_OK; + } + return D3DERR_INVALIDCALL; + } + bool swap = false; if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) { swap = true; @@ -390,6 +504,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain( UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() { WRAP_VERBOSE; + if (is_gfdm_two_head_exclusive()) { + return GFDM_LOGICAL_HEAD_COUNT; + } + UINT n = pReal->GetNumberOfSwapChains(); if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) { @@ -453,6 +571,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer( IDirect3DSurface9 **ppBackBuffer) { WRAP_VERBOSE; + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_side_swapchain(iSwapChain)) + { + return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetBackBuffer( + iBackBuffer, + Type, + ppBackBuffer); + } + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_small_swapchain(iSwapChain)) + { + CHECK_RESULT(pReal->GetBackBuffer( + GFDM_NATIVE_SMALL_SWAPCHAIN, + iBackBuffer, + Type, + ppBackBuffer)); + } + CHECK_RESULT(pReal->GetBackBuffer(iSwapChain, iBackBuffer, Type, ppBackBuffer)); } @@ -461,6 +597,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetRasterStatus( D3DRASTER_STATUS *pRasterStatus) { WRAP_DEBUG; + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_side_swapchain(iSwapChain)) + { + return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetRasterStatus( + pRasterStatus); + } + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_small_swapchain(iSwapChain)) + { + CHECK_RESULT(pReal->GetRasterStatus( + GFDM_NATIVE_SMALL_SWAPCHAIN, + pRasterStatus)); + } + CHECK_RESULT(pReal->GetRasterStatus(iSwapChain, pRasterStatus)); } @@ -477,6 +627,18 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetGammaRamp( const D3DGAMMARAMP *pRamp) { WRAP_DEBUG; + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_side_swapchain(iSwapChain)) + { + return; + } + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_small_swapchain(iSwapChain)) + { + pReal->SetGammaRamp(GFDM_NATIVE_SMALL_SWAPCHAIN, Flags, pRamp); + return; + } + pReal->SetGammaRamp(iSwapChain, Flags, pRamp); } @@ -485,6 +647,21 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGammaRamp( D3DGAMMARAMP *pRamp) { WRAP_DEBUG; + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_side_swapchain(iSwapChain)) + { + if (pRamp != nullptr) { + *pRamp = {}; + } + return; + } + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_small_swapchain(iSwapChain)) + { + pReal->GetGammaRamp(GFDM_NATIVE_SMALL_SWAPCHAIN, pRamp); + return; + } + pReal->GetGammaRamp(iSwapChain, pRamp); } @@ -685,6 +862,19 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetFrontBufferData( IDirect3DSurface9 *pDestSurface) { WRAP_DEBUG; + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_side_swapchain(iSwapChain)) + { + return D3DERR_INVALIDCALL; + } + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_small_swapchain(iSwapChain)) + { + CHECK_RESULT(pReal->GetFrontBufferData( + GFDM_NATIVE_SMALL_SWAPCHAIN, + pDestSurface)); + } + CHECK_RESULT(pReal->GetFrontBufferData(iSwapChain, pDestSurface)); } @@ -1779,6 +1969,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::WaitForVBlank( WRAP_DEBUG; assert(is_d3d9ex); + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_side_swapchain(iSwapChain)) + { + return D3D_OK; + } + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_small_swapchain(iSwapChain)) + { + CHECK_RESULT(static_cast(pReal)->WaitForVBlank( + GFDM_NATIVE_SMALL_SWAPCHAIN)); + } + CHECK_RESULT(static_cast(pReal)->WaitForVBlank(iSwapChain)); } @@ -1882,6 +2084,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx( log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::ResetEx"); + if (is_gfdm_two_head_exclusive()) { + gfdm_disarm_present_mode_recovery(); + if (pPresentationParameters == nullptr) { + return D3DERR_INVALIDCALL; + } + release_gfdm_hidden_side_swapchains(); + } + if (GRAPHICS_WINDOWED) { if (pPresentationParameters) { pPresentationParameters->Windowed = true; @@ -1898,13 +2108,115 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx( } } + GfdmTwoHeadDeviceState gfdm_parameters( + pPresentationParameters, + pFullscreenDisplayMode, + gfdm_logical_small_swapchain); + if (is_gfdm_two_head_exclusive()) { + HRESULT result = graphics_d3d9_gfdm_select_two_head_group_parameters( + pPresentationParameters, + pFullscreenDisplayMode, + gfdm_parameters.native_presentation_parameters.data(), + pFullscreenDisplayMode != nullptr + ? gfdm_parameters.native_fullscreen_display_modes.data() + : nullptr, + &gfdm_parameters.logical_small_swapchain, + "ResetEx"); + if (FAILED(result)) { + return result; + } + set_gfdm_logical_group_parameters(pPresentationParameters); + gfdm_parameters.use_native_parameters(); + if (gfdm_parameters.native_presentation_parameters[0].Windowed + || gfdm_parameters.native_presentation_parameters[1].Windowed) + { + return D3DERR_INVALIDCALL; + } + if (GRAPHICS_FORCE_REFRESH_SUB.has_value()) { + gfdm_parameters.native_presentation_parameters[1] + .FullScreen_RefreshRateInHz = + GRAPHICS_FORCE_REFRESH_SUB.value(); + if (gfdm_parameters.fullscreen_display_modes != nullptr) { + gfdm_parameters.fullscreen_display_modes[1].RefreshRate = + GRAPHICS_FORCE_REFRESH_SUB.value(); + } + } + result = graphics_d3d9_gfdm_remap_two_head_group_parameters( + gfdm_parameters.native_presentation_parameters.data(), + gfdm_parameters.fullscreen_display_modes, + "ResetEx"); + if (FAILED(result)) { + return result; + } + if (!graphics_gitadora_prepare_two_head_device_window( + gfdm_parameters.native_presentation_parameters[1].hDeviceWindow, + nullptr, + gfdm_parameters.native_presentation_parameters[1].BackBufferWidth, + gfdm_parameters.native_presentation_parameters[1].BackBufferHeight)) + { + return D3DERR_INVALIDCALL; + } + if (gfdm_parameters.fullscreen_display_modes != nullptr) { + IDirect3D9 *raw_d3d = nullptr; + IDirect3D9Ex *raw_d3d_ex = nullptr; + D3DDEVICE_CREATION_PARAMETERS creation {}; + HRESULT raw_result = pReal->GetDirect3D(&raw_d3d); + if (SUCCEEDED(raw_result) && raw_d3d != nullptr) { + raw_result = raw_d3d->QueryInterface(IID_PPV_ARGS(&raw_d3d_ex)); + } + if (SUCCEEDED(raw_result)) { + raw_result = pReal->GetCreationParameters(&creation); + } + if (SUCCEEDED(raw_result) && raw_d3d_ex != nullptr) { + graphics_d3d9_gfdm_align_two_head_refresh_to_desktop( + raw_d3d_ex, + creation.AdapterOrdinal, + gfdm_parameters.native_presentation_parameters.data(), + gfdm_parameters.fullscreen_display_modes, + "ResetEx"); + } + if (raw_d3d_ex != nullptr) { + raw_d3d_ex->Release(); + } + if (raw_d3d != nullptr) { + raw_d3d->Release(); + } + gfdm_parameters.recovery_parameters = + gfdm_parameters.native_presentation_parameters; + gfdm_parameters.recovery_modes = + gfdm_parameters.native_fullscreen_display_modes; + gfdm_parameters.recovery_candidate = true; + } + } + // reset overlay if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) { overlay::OVERLAY->reset_invalidate(); } HRESULT res = static_cast(pReal)->ResetEx( - pPresentationParameters, pFullscreenDisplayMode); + gfdm_parameters.presentation_parameters, + gfdm_parameters.fullscreen_display_modes); + + if (is_gfdm_two_head_exclusive() + && SUCCEEDED(res) + && gfdm_parameters.recovery_candidate) + { + gfdm_arm_present_mode_recovery( + gfdm_parameters.recovery_parameters.data(), + gfdm_parameters.recovery_modes.data()); + } + if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) { + gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain; + pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0]; + pPresentationParameters[gfdm_parameters.logical_small_swapchain] = + gfdm_parameters.presentation_parameters[1]; + if (pFullscreenDisplayMode != nullptr) { + pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0]; + pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] = + gfdm_parameters.fullscreen_display_modes[1]; + } + } // recreate overlay if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) { @@ -1922,6 +2234,22 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayModeEx( WRAP_DEBUG; assert(is_d3d9ex); + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_side_swapchain(iSwapChain)) + { + return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetDisplayModeEx( + pMode, + pRotation); + } + if (is_gfdm_two_head_exclusive() + && is_gfdm_logical_small_swapchain(iSwapChain)) + { + CHECK_RESULT(static_cast(pReal)->GetDisplayModeEx( + GFDM_NATIVE_SMALL_SWAPCHAIN, + pMode, + pRotation)); + } + CHECK_RESULT(static_cast(pReal)->GetDisplayModeEx( iSwapChain, pMode, pRotation)); } diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h index 964d8b6..13a4157 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include @@ -40,23 +42,54 @@ static const GUID IID_WrappedIDirect3DDevice9 = { void SurfaceHook(IDirect3DDevice9 *pReal); struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { - explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig) - : hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) { + explicit WrappedIDirect3DDevice9( + HWND hFocusWindow, + IDirect3DDevice9 *orig, + UINT gfdm_logical_small_swapchain = 2, + IDirect3D9 *gfdm_parent_d3d = nullptr, + const D3DPRESENT_PARAMETERS *gfdm_logical_group_parameters = nullptr) + : hFocusWindow(hFocusWindow), + pReal(orig), + is_d3d9ex(false), + gfdm_logical_small_swapchain(gfdm_logical_small_swapchain), + gfdm_parent_d3d(gfdm_parent_d3d) { + if (this->gfdm_parent_d3d != nullptr) { + this->gfdm_parent_d3d->AddRef(); + } IDirect3DDevice9Ex *device = nullptr; // attempt to upgrade handle if (SUCCEEDED(this->QueryInterface(IID_PPV_ARGS(&device))) && device != nullptr) { device->Release(); } + set_gfdm_logical_group_parameters(gfdm_logical_group_parameters); } - explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9Ex *orig) - : hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(true) {} + explicit WrappedIDirect3DDevice9( + HWND hFocusWindow, + IDirect3DDevice9Ex *orig, + UINT gfdm_logical_small_swapchain = 2, + IDirect3D9 *gfdm_parent_d3d = nullptr, + const D3DPRESENT_PARAMETERS *gfdm_logical_group_parameters = nullptr) + : hFocusWindow(hFocusWindow), + pReal(orig), + is_d3d9ex(true), + gfdm_logical_small_swapchain(gfdm_logical_small_swapchain), + gfdm_parent_d3d(gfdm_parent_d3d) { + if (this->gfdm_parent_d3d != nullptr) { + this->gfdm_parent_d3d->AddRef(); + } + set_gfdm_logical_group_parameters(gfdm_logical_group_parameters); + } WrappedIDirect3DDevice9(const WrappedIDirect3DDevice9 &) = delete; WrappedIDirect3DDevice9 &operator=(const WrappedIDirect3DDevice9 &) = delete; - virtual ~WrappedIDirect3DDevice9() = default; + virtual ~WrappedIDirect3DDevice9() { + if (gfdm_parent_d3d != nullptr) { + gfdm_parent_d3d->Release(); + } + } #pragma region IUnknown virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override; @@ -201,6 +234,27 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override; #pragma endregion + bool is_gfdm_two_head_exclusive() const; + bool is_gfdm_logical_small_swapchain(UINT swapchain) const; + bool is_gfdm_logical_side_swapchain(UINT swapchain) const; + size_t gfdm_hidden_side_swapchain_slot(UINT swapchain) const; + void set_gfdm_logical_group_parameters( + const D3DPRESENT_PARAMETERS *presentation_parameters); + FakeIDirect3DSwapChain9 *ensure_gfdm_hidden_side_swapchain(UINT iSwapChain); + void release_gfdm_hidden_side_swapchains(); + + enum class GfdmPresentModeRecoveryResult { + NotAttempted, + ReadyToRetry, + Failed, + }; + void gfdm_disarm_present_mode_recovery(); + void gfdm_arm_present_mode_recovery( + const D3DPRESENT_PARAMETERS *native_presentation_parameters, + const D3DDISPLAYMODEEX *native_fullscreen_display_modes); + GfdmPresentModeRecoveryResult gfdm_recover_present_mode_change( + HRESULT *failure_result); + HWND const hFocusWindow; IDirect3DDevice9 *pReal; bool is_d3d9ex = false; @@ -211,5 +265,17 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr }; FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr }; + UINT gfdm_logical_small_swapchain = 2; + std::array gfdm_logical_group_parameters {}; + bool gfdm_logical_group_parameters_valid = false; + IDirect3D9 *gfdm_parent_d3d = nullptr; + + std::mutex gfdm_recovery_mutex; + std::array gfdm_recovery_parameters {}; + std::array gfdm_recovery_modes {}; + bool gfdm_recovery_armed = false; + std::atomic_bool gfdm_recovery_consumed {false}; + const DWORD gfdm_device_creation_thread_id = GetCurrentThreadId(); + IDirect3DVertexShader9 *vertex_shader = nullptr; }; diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp index 57695e4..a9ae01e 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp @@ -59,7 +59,12 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::Present(const RECT *pSourceRe log_misc("graphics::d3d9", "FakeIDirect3DSwapChain9::Present"); }); - return D3DERR_INVALIDCALL; + if (!is_hidden) { + return D3DERR_INVALIDCALL; + } + + present_count.fetch_add(1, std::memory_order_relaxed); + return D3D_OK; } HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) { WRAP_VERBOSE; @@ -86,11 +91,32 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffe } HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) { WRAP_VERBOSE; - return D3DERR_INVALIDCALL; + if (!is_hidden) { + return D3DERR_INVALIDCALL; + } + + if (pRasterStatus == nullptr) { + return D3DERR_INVALIDCALL; + } + + *pRasterStatus = {}; + return D3D_OK; } HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayMode(D3DDISPLAYMODE *pMode) { WRAP_VERBOSE; - return D3DERR_INVALIDCALL; + if (!is_hidden) { + return D3DERR_INVALIDCALL; + } + + if (pMode == nullptr) { + return D3DERR_INVALIDCALL; + } + + pMode->Width = present_params.BackBufferWidth; + pMode->Height = present_params.BackBufferHeight; + pMode->RefreshRate = present_params.FullScreen_RefreshRateInHz; + pMode->Format = present_params.BackBufferFormat; + return D3D_OK; } HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDevice(IDirect3DDevice9 **ppDevice) { WRAP_VERBOSE; @@ -108,17 +134,46 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentParameters( D3DPRESENT_PARAMETERS *pPresentationParameters) { WRAP_VERBOSE; - return D3DERR_INVALIDCALL; + if (!is_hidden) { + return D3DERR_INVALIDCALL; + } + + if (pPresentationParameters == nullptr) { + return D3DERR_INVALIDCALL; + } + + *pPresentationParameters = present_params; + return D3D_OK; } // IDirect3DSwapChain9Ex HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetLastPresentCount(UINT *pLastPresentCount) { assert(is_d3d9ex); - return D3DERR_INVALIDCALL; + if (!is_hidden) { + return D3DERR_INVALIDCALL; + } + + if (pLastPresentCount == nullptr) { + return D3DERR_INVALIDCALL; + } + + *pLastPresentCount = present_count.load(std::memory_order_relaxed); + return D3D_OK; } HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentStats(D3DPRESENTSTATS *pPresentationStatistics) { assert(is_d3d9ex); - return D3DERR_INVALIDCALL; + if (!is_hidden) { + return D3DERR_INVALIDCALL; + } + + if (pPresentationStatistics == nullptr) { + return D3DERR_INVALIDCALL; + } + + *pPresentationStatistics = {}; + pPresentationStatistics->PresentCount = + present_count.load(std::memory_order_relaxed); + return D3D_OK; } HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) @@ -126,5 +181,22 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMO WRAP_VERBOSE; assert(is_d3d9ex); - return D3DERR_INVALIDCALL; + if (!is_hidden) { + return D3DERR_INVALIDCALL; + } + + if (pMode == nullptr) { + return D3DERR_INVALIDCALL; + } + + pMode->Size = sizeof(*pMode); + pMode->Width = present_params.BackBufferWidth; + pMode->Height = present_params.BackBufferHeight; + pMode->RefreshRate = present_params.FullScreen_RefreshRateInHz; + pMode->Format = present_params.BackBufferFormat; + pMode->ScanLineOrdering = D3DSCANLINEORDERING_PROGRESSIVE; + if (pRotation != nullptr) { + *pRotation = D3DDISPLAYROTATION_IDENTITY; + } + return D3D_OK; } diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_fake_swapchain.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_fake_swapchain.h index 499025e..c24c73a 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_fake_swapchain.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_fake_swapchain.h @@ -6,14 +6,19 @@ #endif #include +#include #include #include "util/logging.h" struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex { - FakeIDirect3DSwapChain9(IDirect3DDevice9 *pDev, D3DPRESENT_PARAMETERS *present_params, bool is_d3d9ex) : - pDev(pDev), is_d3d9ex(is_d3d9ex) + FakeIDirect3DSwapChain9( + IDirect3DDevice9 *pDev, + D3DPRESENT_PARAMETERS *present_params, + bool is_d3d9ex, + bool is_hidden = false) : + pDev(pDev), is_d3d9ex(is_d3d9ex), is_hidden(is_hidden) { // copy presentation parameters memcpy(&this->present_params, present_params, sizeof(this->present_params)); @@ -78,8 +83,10 @@ struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex { IDirect3DDevice9 *const pDev; bool is_d3d9ex; + bool is_hidden; std::atomic ref_cnt = 1; + std::atomic present_count = 0; D3DPRESENT_PARAMETERS present_params {}; std::vector render_targets; diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp new file mode 100644 index 0000000..2f696b7 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp @@ -0,0 +1,715 @@ +#include "d3d9_gfdm.h" + +#include +#include +#include + +#include "games/gitadora/gitadora.h" +#include "hooks/graphics/graphics.h" +#include "util/logging.h" + +#include "d3d9_device.h" + +bool gfdm_two_head_exclusive() { + return games::gitadora::is_arena_model() + && games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE + && !GRAPHICS_WINDOWED; +} + +HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters( + const D3DPRESENT_PARAMETERS *logical_presentation_parameters, + const D3DDISPLAYMODEEX *logical_fullscreen_display_modes, + D3DPRESENT_PARAMETERS *native_presentation_parameters, + D3DDISPLAYMODEEX *native_fullscreen_display_modes, + UINT *logical_small_swapchain, + const char *operation) +{ + if (!gfdm_two_head_exclusive() + || logical_presentation_parameters == nullptr + || native_presentation_parameters == nullptr + || logical_small_swapchain == nullptr) + { + return D3DERR_INVALIDCALL; + } + + UINT small_slot = GFDM_LOGICAL_HEAD_COUNT; + for (UINT i = 1; i < GFDM_LOGICAL_HEAD_COUNT; i++) { + const auto ¶ms = logical_presentation_parameters[i]; + if (params.BackBufferWidth != GFDM_SMALL_WIDTH + || params.BackBufferHeight != GFDM_SMALL_HEIGHT) + { + continue; + } + if (small_slot != GFDM_LOGICAL_HEAD_COUNT) { + log_warning( + "graphics::d3d9", + "two-head exclusive: {} found multiple SMALL slots: {} and {}", + operation, + small_slot, + i); + return D3DERR_INVALIDCALL; + } + small_slot = i; + } + + if (small_slot == GFDM_LOGICAL_HEAD_COUNT) { + log_warning( + "graphics::d3d9", + "two-head exclusive: {} could not find an 800x1280 logical SMALL descriptor", + operation); + return D3DERR_INVALIDCALL; + } + + if (logical_fullscreen_display_modes != nullptr + && (logical_fullscreen_display_modes[small_slot].Width != GFDM_SMALL_WIDTH + || logical_fullscreen_display_modes[small_slot].Height != GFDM_SMALL_HEIGHT)) + { + log_warning( + "graphics::d3d9", + "two-head exclusive: {} SMALL slot {} mode is {}x{}, expected {}x{}", + operation, + small_slot, + logical_fullscreen_display_modes[small_slot].Width, + logical_fullscreen_display_modes[small_slot].Height, + GFDM_SMALL_WIDTH, + GFDM_SMALL_HEIGHT); + return D3DERR_INVALIDCALL; + } + + native_presentation_parameters[0] = logical_presentation_parameters[0]; + native_presentation_parameters[1] = logical_presentation_parameters[small_slot]; + if (logical_fullscreen_display_modes != nullptr + && native_fullscreen_display_modes != nullptr) + { + native_fullscreen_display_modes[0] = logical_fullscreen_display_modes[0]; + native_fullscreen_display_modes[1] = logical_fullscreen_display_modes[small_slot]; + } + + log_info( + "graphics::d3d9", + "two-head exclusive: {} selected logical SMALL slot {} -> native group head 1{}", + operation, + small_slot, + logical_presentation_parameters[small_slot].hDeviceWindow == GFDM_SUBSCREEN_WINDOW + ? " (named SMALL host)" + : ""); + *logical_small_swapchain = small_slot; + return D3D_OK; +} + +bool is_fake_subscreen_adapter(UINT adapter) { + // MAIN and SMALL remain native D3D heads; LEFT/RIGHT are virtual. + if (gfdm_two_head_exclusive()) { + return adapter >= 2 && adapter < GFDM_LOGICAL_HEAD_COUNT; + } + + return FAKE_SUBSCREEN_ADAPTER && adapter > 0; +} + +void get_fake_subscreen_display_mode( + UINT adapter, + D3DDISPLAYMODE *mode) +{ + (void) adapter; + if (gfdm_two_head_exclusive()) { + mode->Width = GFDM_SIDE_WIDTH; + mode->Height = GFDM_SIDE_HEIGHT; + } else { + mode->Width = 1280; + mode->Height = 800; + } + mode->RefreshRate = 60; + mode->Format = D3DFMT_X8R8G8B8; +} + +void get_fake_subscreen_display_mode_ex( + UINT adapter, + D3DDISPLAYMODEEX *mode) +{ + D3DDISPLAYMODE legacy_mode {}; + get_fake_subscreen_display_mode(adapter, &legacy_mode); + + mode->Size = sizeof(*mode); + mode->Width = legacy_mode.Width; + mode->Height = legacy_mode.Height; + mode->RefreshRate = legacy_mode.RefreshRate; + mode->Format = legacy_mode.Format; + mode->ScanLineOrdering = D3DSCANLINEORDERING_PROGRESSIVE; +} + +HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters( + D3DPRESENT_PARAMETERS *presentation_parameters, + D3DDISPLAYMODEEX *fullscreen_display_modes, + const char *operation) +{ + if (!gfdm_two_head_exclusive()) { + return D3D_OK; + } + if (presentation_parameters == nullptr) { + return D3DERR_INVALIDCALL; + } + + auto &secondary = presentation_parameters[1]; + if (secondary.BackBufferWidth != GFDM_SMALL_WIDTH + || secondary.BackBufferHeight != GFDM_SMALL_HEIGHT) + { + log_warning( + "graphics::d3d9", + "two-head exclusive: {} expected physical SMALL {}x{} at group slot 1, got {}x{}", + operation, + GFDM_SMALL_WIDTH, + GFDM_SMALL_HEIGHT, + secondary.BackBufferWidth, + secondary.BackBufferHeight); + return D3DERR_INVALIDCALL; + } + + if (GFDM_SUBSCREEN_WINDOW == nullptr || !IsWindow(GFDM_SUBSCREEN_WINDOW)) { + log_warning( + "graphics::d3d9", + "two-head exclusive: {} cannot use the named SMALL window as physical group head 1", + operation); + return D3DERR_INVALIDCALL; + } + secondary.hDeviceWindow = GFDM_SUBSCREEN_WINDOW; + + if (fullscreen_display_modes != nullptr + && (fullscreen_display_modes[1].Width != GFDM_SMALL_WIDTH + || fullscreen_display_modes[1].Height != GFDM_SMALL_HEIGHT)) + { + log_warning( + "graphics::d3d9", + "two-head exclusive: {} SMALL head mode is {}x{}, expected 800x1280", + operation, + fullscreen_display_modes[1].Width, + fullscreen_display_modes[1].Height); + return D3DERR_INVALIDCALL; + } + + return D3D_OK; +} + +void graphics_d3d9_gfdm_align_two_head_refresh_to_desktop( + IDirect3D9Ex *d3d, + UINT master_adapter, + D3DPRESENT_PARAMETERS *presentation_parameters, + D3DDISPLAYMODEEX *fullscreen_display_modes, + const char *operation) +{ + if (d3d == nullptr + || presentation_parameters == nullptr + || fullscreen_display_modes == nullptr) + { + return; + } + + for (UINT head = 0; head < 2; head++) { + const bool explicitly_forced = + (head == 0 && GRAPHICS_FORCE_REFRESH > 0) + || (head == 1 && GRAPHICS_FORCE_REFRESH_SUB.has_value()); + if (explicitly_forced) { + continue; + } + + D3DDISPLAYMODEEX desktop_mode {}; + desktop_mode.Size = sizeof(desktop_mode); + D3DDISPLAYROTATION rotation = D3DDISPLAYROTATION_IDENTITY; + const HRESULT result = d3d->GetAdapterDisplayModeEx( + master_adapter + head, + &desktop_mode, + &rotation); + if (FAILED(result)) { + log_warning( + "graphics::d3d9", + "two-head exclusive: {} could not query native head {} desktop refresh, hr={}", + operation, + head, + FMT_HRESULT(result)); + continue; + } + + const auto ¶meters = presentation_parameters[head]; + if (desktop_mode.Width != parameters.BackBufferWidth + || desktop_mode.Height != parameters.BackBufferHeight + || desktop_mode.Format != parameters.BackBufferFormat + || desktop_mode.RefreshRate == 0) + { + log_warning( + "graphics::d3d9", + "two-head exclusive: {} head {} mode differs from the desktop", + operation, + head); + continue; + } + + if (parameters.FullScreen_RefreshRateInHz != desktop_mode.RefreshRate + || fullscreen_display_modes[head].RefreshRate != desktop_mode.RefreshRate) + { + log_info( + "graphics::d3d9", + "two-head exclusive: {} head {} refresh {} / {} -> {} (rotation {})", + operation, + head, + parameters.FullScreen_RefreshRateInHz, + fullscreen_display_modes[head].RefreshRate, + desktop_mode.RefreshRate, + static_cast(rotation)); + presentation_parameters[head].FullScreen_RefreshRateInHz = + desktop_mode.RefreshRate; + fullscreen_display_modes[head].RefreshRate = + desktop_mode.RefreshRate; + } + } +} + +HRESULT validate_gfdm_two_head_exclusive( + IDirect3D9 *d3d, + UINT adapter, + D3DDEVTYPE device_type, + DWORD behavior_flags, + const D3DPRESENT_PARAMETERS *presentation_parameters) +{ + if (!(behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) { + log_warning( + "graphics::d3d9", + "two-head mode requires D3DCREATE_ADAPTERGROUP_DEVICE"); + return D3DERR_NOTAVAILABLE; + } + if (adapter != 0) { + log_warning( + "graphics::d3d9", + "two-head exclusive mode currently requires master adapter 0, game requested {}", + adapter); + return D3DERR_NOTAVAILABLE; + } + if (presentation_parameters == nullptr) { + return D3DERR_INVALIDCALL; + } + const UINT native_adapter_count = d3d->GetAdapterCount(); + if (native_adapter_count != 2) { + log_warning( + "graphics::d3d9", + "two-head mode requires two native D3D9 adapters; found {}", + native_adapter_count); + return D3DERR_NOTAVAILABLE; + } + + D3DCAPS9 caps {}; + const HRESULT caps_result = d3d->GetDeviceCaps(adapter, device_type, &caps); + if (FAILED(caps_result)) { + log_warning( + "graphics::d3d9", + "two-head exclusive mode could not query native adapter caps, hr={}", + FMT_HRESULT(caps_result)); + return caps_result; + } + if (caps.MasterAdapterOrdinal != adapter + || caps.AdapterOrdinalInGroup != 0 + || caps.NumberOfAdaptersInGroup != 2) + { + log_warning( + "graphics::d3d9", + "invalid adapter group: master={}, index={}, size={}", + caps.MasterAdapterOrdinal, + caps.AdapterOrdinalInGroup, + caps.NumberOfAdaptersInGroup); + return D3DERR_NOTAVAILABLE; + } + + const auto &main = presentation_parameters[0]; + const auto &small_params = presentation_parameters[1]; + if (main.Windowed || small_params.Windowed) { + log_warning( + "graphics::d3d9", + "two-head exclusive mode requires both group heads to be fullscreen"); + return D3DERR_INVALIDCALL; + } + if (small_params.BackBufferWidth != GFDM_SMALL_WIDTH + || small_params.BackBufferHeight != GFDM_SMALL_HEIGHT) + { + log_warning( + "graphics::d3d9", + "SMALL head must be {}x{}; got {}x{}", + GFDM_SMALL_WIDTH, + GFDM_SMALL_HEIGHT, + small_params.BackBufferWidth, + small_params.BackBufferHeight); + return D3DERR_INVALIDCALL; + } + if (main.EnableAutoDepthStencil != small_params.EnableAutoDepthStencil) { + log_warning( + "graphics::d3d9", + "two-head mode requires matching automatic depth-stencil settings"); + return D3DERR_INVALIDCALL; + } + if (main.EnableAutoDepthStencil + && (main.AutoDepthStencilFormat != small_params.AutoDepthStencilFormat + || main.BackBufferWidth != small_params.BackBufferWidth + || main.BackBufferHeight != small_params.BackBufferHeight + || main.BackBufferFormat != small_params.BackBufferFormat)) + { + log_warning( + "graphics::d3d9", + "two-head mode cannot use unequal sizes with automatic depth-stencil"); + return D3DERR_INVALIDCALL; + } + + return D3D_OK; +} + +static bool gfdm_find_alternate_small_mode( + IDirect3D9Ex *d3d, + UINT adapter, + const D3DDISPLAYMODEEX &desired, + D3DDISPLAYMODEEX *alternate) +{ + if (d3d == nullptr || alternate == nullptr) { + return false; + } + + D3DDISPLAYMODEFILTER filter {}; + filter.Size = sizeof(filter); + filter.Format = desired.Format; + filter.ScanLineOrdering = D3DSCANLINEORDERING_UNKNOWN; + const UINT count = d3d->GetAdapterModeCountEx(adapter, &filter); + const bool portrait = desired.Width < desired.Height; + + for (UINT index = 0; index < count; index++) { + D3DDISPLAYMODEEX mode {}; + mode.Size = sizeof(mode); + if (FAILED(d3d->EnumAdapterModesEx(adapter, &filter, index, &mode)) + || mode.Width != desired.Width + || mode.Height != desired.Height + || mode.Format != desired.Format + || (mode.RefreshRate == desired.RefreshRate + && mode.ScanLineOrdering == desired.ScanLineOrdering)) + { + continue; + } + *alternate = mode; + return true; + } + + for (int preserve_orientation = 1; preserve_orientation >= 0; preserve_orientation--) { + for (int preserve_refresh = 1; preserve_refresh >= 0; preserve_refresh--) { + for (UINT index = 0; index < count; index++) { + D3DDISPLAYMODEEX mode {}; + mode.Size = sizeof(mode); + if (FAILED(d3d->EnumAdapterModesEx(adapter, &filter, index, &mode)) + || (mode.Width == desired.Width && mode.Height == desired.Height) + || mode.Format != desired.Format) + { + continue; + } + if (preserve_orientation && (mode.Width < mode.Height) != portrait) { + continue; + } + if (preserve_refresh && mode.RefreshRate != desired.RefreshRate) { + continue; + } + *alternate = mode; + return true; + } + } + } + return false; +} + +static bool gfdm_wait_for_small_mode( + HWND window, + const D3DDISPLAYMODEEX &expected) +{ + const HMONITOR monitor = + window != nullptr ? MonitorFromWindow(window, MONITOR_DEFAULTTONULL) : nullptr; + MONITORINFOEXA monitor_info {}; + monitor_info.cbSize = sizeof(monitor_info); + if (monitor == nullptr || !GetMonitorInfoA(monitor, &monitor_info)) { + return false; + } + + for (UINT poll = 0; poll < 50; poll++) { + DEVMODEA current {}; + current.dmSize = sizeof(current); + if (EnumDisplaySettingsExA( + monitor_info.szDevice, + ENUM_CURRENT_SETTINGS, + ¤t, + 0) + && current.dmPelsWidth == expected.Width + && current.dmPelsHeight == expected.Height + && (expected.RefreshRate == 0 + || current.dmDisplayFrequency == expected.RefreshRate)) + { + return true; + } + Sleep(10); + } + return false; +} + +HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode( + IDirect3D9Ex *d3d, + IDirect3DDevice9Ex *device, + UINT master_adapter, + const D3DPRESENT_PARAMETERS *desired_parameters, + const D3DDISPLAYMODEEX *desired_modes) +{ + if (d3d == nullptr + || device == nullptr + || desired_parameters == nullptr + || desired_modes == nullptr) + { + return D3DERR_INVALIDCALL; + } + + D3DDISPLAYMODEEX alternate_small {}; + if (!gfdm_find_alternate_small_mode( + d3d, + master_adapter + 1, + desired_modes[1], + &alternate_small)) + { + log_warning("graphics::d3d9", "no alternate SMALL mode is available for recovery"); + return D3DERR_NOTAVAILABLE; + } + + D3DPRESENT_PARAMETERS temporary_parameters[2] { + desired_parameters[0], + desired_parameters[1]}; + D3DDISPLAYMODEEX temporary_modes[2] { + desired_modes[0], + desired_modes[1]}; + temporary_parameters[1].BackBufferWidth = alternate_small.Width; + temporary_parameters[1].BackBufferHeight = alternate_small.Height; + temporary_parameters[1].BackBufferFormat = alternate_small.Format; + temporary_parameters[1].FullScreen_RefreshRateInHz = alternate_small.RefreshRate; + temporary_modes[1] = alternate_small; + + HRESULT temporary_result = device->ResetEx(temporary_parameters, temporary_modes); + const bool temporary_settled = + temporary_result == D3D_OK + && gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, alternate_small); + + D3DPRESENT_PARAMETERS restore_parameters[2] { + desired_parameters[0], + desired_parameters[1]}; + D3DDISPLAYMODEEX restore_modes[2] { + desired_modes[0], + desired_modes[1]}; + HRESULT restore_result = device->ResetEx(restore_parameters, restore_modes); + const bool restore_settled = + restore_result == D3D_OK + && gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, desired_modes[1]); + + if (temporary_result != D3D_OK) { + return temporary_result; + } + if (restore_result != D3D_OK) { + return restore_result; + } + if (!temporary_settled || !restore_settled) { + return D3DERR_NOTAVAILABLE; + } + return device->CheckDeviceState(desired_parameters[0].hDeviceWindow) == D3D_OK + ? D3D_OK + : D3DERR_NOTAVAILABLE; +} + +bool WrappedIDirect3DDevice9::is_gfdm_two_head_exclusive() const { + return ::gfdm_two_head_exclusive(); +} + +bool WrappedIDirect3DDevice9::is_gfdm_logical_small_swapchain( + UINT swapchain) const +{ + return swapchain == gfdm_logical_small_swapchain; +} + +bool WrappedIDirect3DDevice9::is_gfdm_logical_side_swapchain( + UINT swapchain) const +{ + return swapchain > 0 + && swapchain < GFDM_LOGICAL_HEAD_COUNT + && swapchain != gfdm_logical_small_swapchain; +} + +size_t WrappedIDirect3DDevice9::gfdm_hidden_side_swapchain_slot( + UINT swapchain) const +{ + assert(is_gfdm_logical_side_swapchain(swapchain)); + return swapchain < gfdm_logical_small_swapchain + ? swapchain + : swapchain - 1; +} + +void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters( + const D3DPRESENT_PARAMETERS *presentation_parameters) +{ + if (presentation_parameters == nullptr) { + gfdm_logical_group_parameters_valid = false; + return; + } + std::copy_n( + presentation_parameters, + gfdm_logical_group_parameters.size(), + gfdm_logical_group_parameters.begin()); + gfdm_logical_group_parameters_valid = true; +} + +FakeIDirect3DSwapChain9 * +WrappedIDirect3DDevice9::ensure_gfdm_hidden_side_swapchain(UINT iSwapChain) { + assert(is_gfdm_logical_side_swapchain(iSwapChain)); + + const size_t index = gfdm_hidden_side_swapchain_slot(iSwapChain); + if (fake_sub_swapchain[index] == nullptr) { + D3DPRESENT_PARAMETERS params {}; + if (gfdm_logical_group_parameters_valid) { + params = gfdm_logical_group_parameters[iSwapChain]; + } else { + params.BackBufferWidth = 1080; + params.BackBufferHeight = 1920; + params.BackBufferFormat = D3DFMT_X8R8G8B8; + params.BackBufferCount = 1; + params.MultiSampleType = D3DMULTISAMPLE_NONE; + params.SwapEffect = D3DSWAPEFFECT_DISCARD; + params.Windowed = FALSE; + params.FullScreen_RefreshRateInHz = 60; + params.EnableAutoDepthStencil = FALSE; + params.PresentationInterval = D3DPRESENT_INTERVAL_ONE; + } + fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9( + this, + ¶ms, + is_d3d9ex, + true); + } + return fake_sub_swapchain[index]; +} + +void WrappedIDirect3DDevice9::release_gfdm_hidden_side_swapchains() { + for (UINT i = 1; i < GFDM_LOGICAL_HEAD_COUNT; i++) { + if (!is_gfdm_logical_side_swapchain(i)) { + continue; + } + const size_t index = gfdm_hidden_side_swapchain_slot(i); + if (fake_sub_swapchain[index] != nullptr) { + fake_sub_swapchain[index]->Release(); + fake_sub_swapchain[index] = nullptr; + } + } +} + +void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() { + std::lock_guard lock(gfdm_recovery_mutex); + gfdm_recovery_armed = false; + gfdm_recovery_consumed.store(true, std::memory_order_release); +} + +void WrappedIDirect3DDevice9::gfdm_arm_present_mode_recovery( + const D3DPRESENT_PARAMETERS *native_presentation_parameters, + const D3DDISPLAYMODEEX *native_fullscreen_display_modes) +{ + if (native_presentation_parameters == nullptr + || native_fullscreen_display_modes == nullptr) + { + return; + } + + std::lock_guard lock(gfdm_recovery_mutex); + gfdm_recovery_parameters[0] = native_presentation_parameters[0]; + gfdm_recovery_parameters[1] = native_presentation_parameters[1]; + gfdm_recovery_modes[0] = native_fullscreen_display_modes[0]; + gfdm_recovery_modes[1] = native_fullscreen_display_modes[1]; + if (gfdm_recovery_modes[0].Size == 0) { + gfdm_recovery_modes[0].Size = sizeof(D3DDISPLAYMODEEX); + } + if (gfdm_recovery_modes[1].Size == 0) { + gfdm_recovery_modes[1].Size = sizeof(D3DDISPLAYMODEEX); + } + gfdm_recovery_armed = + !gfdm_recovery_parameters[0].Windowed + && !gfdm_recovery_parameters[1].Windowed + && IsWindow(gfdm_recovery_parameters[1].hDeviceWindow); + gfdm_recovery_consumed.store( + !gfdm_recovery_armed, + std::memory_order_release); +} + +WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult +WrappedIDirect3DDevice9::gfdm_recover_present_mode_change( + HRESULT *failure_result) +{ + if (failure_result != nullptr) { + *failure_result = D3D_OK; + } + if (!is_gfdm_two_head_exclusive() || !is_d3d9ex + || gfdm_recovery_consumed.exchange( + true, + std::memory_order_acq_rel)) + { + return GfdmPresentModeRecoveryResult::NotAttempted; + } + if (GetCurrentThreadId() != gfdm_device_creation_thread_id) { + if (failure_result != nullptr) { + *failure_result = D3DERR_INVALIDCALL; + } + return GfdmPresentModeRecoveryResult::Failed; + } + + std::array parameters {}; + std::array modes {}; + { + std::lock_guard lock(gfdm_recovery_mutex); + if (!gfdm_recovery_armed) { + return GfdmPresentModeRecoveryResult::NotAttempted; + } + parameters = gfdm_recovery_parameters; + modes = gfdm_recovery_modes; + } + + IDirect3D9 *raw_d3d = nullptr; + HRESULT result = pReal->GetDirect3D(&raw_d3d); + IDirect3D9Ex *raw_d3d_ex = nullptr; + if (SUCCEEDED(result)) { + result = raw_d3d != nullptr + ? raw_d3d->QueryInterface(IID_PPV_ARGS(&raw_d3d_ex)) + : E_FAIL; + } + D3DDEVICE_CREATION_PARAMETERS creation {}; + if (SUCCEEDED(result)) { + result = pReal->GetCreationParameters(&creation); + } + if (FAILED(result) || raw_d3d_ex == nullptr) { + if (raw_d3d_ex != nullptr) { + raw_d3d_ex->Release(); + } + if (raw_d3d != nullptr) { + raw_d3d->Release(); + } + if (failure_result != nullptr) { + *failure_result = result; + } + return GfdmPresentModeRecoveryResult::Failed; + } + + result = graphics_d3d9_gfdm_recover_two_head_present_mode( + raw_d3d_ex, + static_cast(pReal), + creation.AdapterOrdinal, + parameters.data(), + modes.data()); + raw_d3d_ex->Release(); + raw_d3d->Release(); + + if (result == D3DERR_NOTAVAILABLE) { + return GfdmPresentModeRecoveryResult::NotAttempted; + } + if (FAILED(result)) { + if (failure_result != nullptr) { + *failure_result = result; + } + return GfdmPresentModeRecoveryResult::Failed; + } + return GfdmPresentModeRecoveryResult::ReadyToRetry; +} + diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h new file mode 100644 index 0000000..1791621 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include + +inline constexpr UINT GFDM_SIDE_WIDTH = 1080; +inline constexpr UINT GFDM_SIDE_HEIGHT = 1920; +inline constexpr UINT GFDM_SMALL_WIDTH = 800; +inline constexpr UINT GFDM_SMALL_HEIGHT = 1280; +inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4; +inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1; + +struct GfdmTwoHeadDeviceState { + explicit GfdmTwoHeadDeviceState( + D3DPRESENT_PARAMETERS *presentation_parameters, + D3DDISPLAYMODEEX *fullscreen_display_modes, + UINT logical_small_swapchain = 2) + : presentation_parameters(presentation_parameters), + fullscreen_display_modes(fullscreen_display_modes), + logical_small_swapchain(logical_small_swapchain) {} + + void use_native_parameters() { + presentation_parameters = native_presentation_parameters.data(); + fullscreen_display_modes = source_fullscreen_display_modes != nullptr + ? native_fullscreen_display_modes.data() + : nullptr; + } + + std::array native_presentation_parameters {}; + std::array native_fullscreen_display_modes {}; + std::array recovery_parameters {}; + std::array recovery_modes {}; + D3DPRESENT_PARAMETERS *presentation_parameters; + D3DDISPLAYMODEEX *fullscreen_display_modes; + D3DDISPLAYMODEEX *const source_fullscreen_display_modes = fullscreen_display_modes; + UINT logical_small_swapchain; + bool recovery_candidate = false; +}; + +bool gfdm_two_head_exclusive(); +bool is_fake_subscreen_adapter(UINT adapter); +void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode); +void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode); + +HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters( + const D3DPRESENT_PARAMETERS *logical_presentation_parameters, + const D3DDISPLAYMODEEX *logical_fullscreen_display_modes, + D3DPRESENT_PARAMETERS *native_presentation_parameters, + D3DDISPLAYMODEEX *native_fullscreen_display_modes, + UINT *logical_small_swapchain, + const char *operation); + +HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters( + D3DPRESENT_PARAMETERS *presentation_parameters, + D3DDISPLAYMODEEX *fullscreen_display_modes, + const char *operation); + +void graphics_d3d9_gfdm_align_two_head_refresh_to_desktop( + IDirect3D9Ex *d3d, + UINT master_adapter, + D3DPRESENT_PARAMETERS *presentation_parameters, + D3DDISPLAYMODEEX *fullscreen_display_modes, + const char *operation); + +HRESULT validate_gfdm_two_head_exclusive( + IDirect3D9 *d3d, + UINT adapter, + D3DDEVTYPE device_type, + DWORD behavior_flags, + const D3DPRESENT_PARAMETERS *presentation_parameters); + +HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode( + IDirect3D9Ex *d3d, + IDirect3DDevice9Ex *device, + UINT master_adapter, + const D3DPRESENT_PARAMETERS *desired_parameters, + const D3DDISPLAYMODEEX *desired_modes); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp index 8544ad8..7fb28ea 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp @@ -26,7 +26,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid return E_POINTER; } - if (//riid == __uuidof(IUnknown) || Ignore IUnknown, it's often queried to test object equality between different interfaces + if ((riid == IID_IUnknown && pDev->is_gfdm_two_head_exclusive()) || riid == IID_IDirect3DSwapChain9 || riid == IID_IDirect3DSwapChain9Ex) { @@ -89,7 +89,42 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev); } - CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags)); + HRESULT result = pReal->Present( + pSourceRect, + pDestRect, + hDestWindowOverride, + pDirtyRegion, + dwFlags); + + // Some drivers report S_PRESENT_MODE_CHANGED after Windows moves the portrait SMALL + // head into the requested exclusive mode, leaving both group heads black. Toggle SMALL + // through another supported mode and restore it once, then retry the interrupted MAIN + // present. Recovery is restricted to the creation thread and consumes its armed state, + // so subsequent presents cannot repeat the mode switch. + if (pDev->is_gfdm_two_head_exclusive() + && native_group_head == NativeGroupHead::Main + && result == S_PRESENT_MODE_CHANGED) + { + HRESULT recovery_failure = D3D_OK; + const auto recovery = pDev->gfdm_recover_present_mode_change( + &recovery_failure); + if (recovery + == WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult::ReadyToRetry) + { + result = pReal->Present( + pSourceRect, + pDestRect, + hDestWindowOverride, + pDirtyRegion, + dwFlags); + } else if (recovery + == WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult::Failed + && FAILED(recovery_failure)) + { + result = recovery_failure; + } + } + CHECK_RESULT(result); } HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) { CHECK_RESULT(pReal->GetFrontBufferData(pDestSurface)); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.h index e039901..ab013cf 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.h @@ -5,8 +5,13 @@ interface WrappedIDirect3DDevice9; struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex { - WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9 *orig) : - pDev(dev), pReal(orig), is_d3d9ex(false) + enum class NativeGroupHead { Unknown, Main, Small }; + + WrappedIDirect3DSwapChain9( + WrappedIDirect3DDevice9 *dev, + IDirect3DSwapChain9 *orig, + NativeGroupHead native_group_head = NativeGroupHead::Unknown) : + pDev(dev), pReal(orig), is_d3d9ex(false), native_group_head(native_group_head) { IDirect3DSwapChain9Ex *swapchain = nullptr; @@ -16,8 +21,11 @@ struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex { } } - WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9Ex *orig) : - pDev(dev), pReal(orig), is_d3d9ex(true) + WrappedIDirect3DSwapChain9( + WrappedIDirect3DDevice9 *dev, + IDirect3DSwapChain9Ex *orig, + NativeGroupHead native_group_head = NativeGroupHead::Unknown) : + pDev(dev), pReal(orig), is_d3d9ex(true), native_group_head(native_group_head) { } @@ -53,6 +61,7 @@ struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex { IDirect3DSwapChain9 *pReal; bool is_d3d9ex = false; + NativeGroupHead native_group_head = NativeGroupHead::Unknown; bool should_run_hooks = true; }; diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 9e28607..07896eb 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -44,6 +44,8 @@ HWND SDVX_SUBSCREEN_WINDOW = nullptr; HWND GFDM_SUBSCREEN_WINDOW = nullptr; static HWND GFDM_LEFT_WINDOW = nullptr; static HWND GFDM_RIGHT_WINDOW = nullptr; +static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr; +static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr; HWND POPN_SUBSCREEN_WINDOW = nullptr; bool FAKE_SUBSCREEN_ADAPTER = false; @@ -183,6 +185,13 @@ static bool is_gfdm_known_window(HWND hWnd) { return gitadora_window_name_for_hwnd(hWnd) != nullptr; } +static bool is_gfdm_two_head_small_window(HWND hWnd) { + return games::gitadora::is_arena_model() && + games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE && + hWnd != nullptr && + hWnd == GFDM_TWO_HEAD_SMALL_WINDOW; +} + static bool gitadora_should_block_game_window_placement(HWND hWnd) { if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) { return false; @@ -204,6 +213,95 @@ static void gitadora_remember_window(HWND hWnd, const std::string &window_name) } } +bool graphics_gitadora_prepare_two_head_device_window( + HWND hWnd, HMONITOR target_monitor, UINT desired_width, UINT desired_height) { + if (hWnd == nullptr || !IsWindow(hWnd)) { + log_warning( + "graphics", + "two-head exclusive: native secondary device window is invalid: {}", + fmt::ptr(hWnd)); + return false; + } + if (ShowWindow_orig == nullptr || SetWindowLongA_orig == nullptr || + SetWindowPos_orig == nullptr) { + log_warning( + "graphics", + "two-head exclusive: window hooks are not initialized for {}", + fmt::ptr(hWnd)); + return false; + } + + RECT rect_before {}; + GetWindowRect(hWnd, &rect_before); + const DWORD style_before = static_cast(GetWindowLongA(hWnd, GWL_STYLE)); + const BOOL visible_before = IsWindowVisible(hWnd); + + if (target_monitor != nullptr) { + GFDM_TWO_HEAD_SMALL_MONITOR = target_monitor; + } + const HMONITOR monitor = target_monitor != nullptr + ? target_monitor + : (GFDM_TWO_HEAD_SMALL_MONITOR != nullptr + ? GFDM_TWO_HEAD_SMALL_MONITOR + : MonitorFromWindow(hWnd, MONITOR_DEFAULTTONULL)); + MONITORINFO monitor_info {}; + monitor_info.cbSize = sizeof(monitor_info); + if (monitor == nullptr || !GetMonitorInfoA(monitor, &monitor_info)) { + log_warning( + "graphics", + "two-head exclusive: could not find the SMALL monitor for {}", + fmt::ptr(hWnd)); + return false; + } + + // Arena requests D3DCREATE_NOWINDOWCHANGES, so place its SMALL window on + // the second head before D3D9 takes ownership of it. + const DWORD fullscreen_style = (style_before & ~WS_OVERLAPPEDWINDOW) | WS_POPUP; + const bool style_changed = fullscreen_style != style_before; + if (style_changed) { + SetWindowLongA_orig(hWnd, GWL_STYLE, static_cast(fullscreen_style)); + } + const RECT &monitor_rect = monitor_info.rcMonitor; + const int host_width = desired_width != 0 + ? static_cast(desired_width) + : monitor_rect.right - monitor_rect.left; + const int host_height = desired_height != 0 + ? static_cast(desired_height) + : monitor_rect.bottom - monitor_rect.top; + const bool rect_changed = rect_before.left != monitor_rect.left || + rect_before.top != monitor_rect.top || + rect_before.right != monitor_rect.left + host_width || + rect_before.bottom != monitor_rect.top + host_height; + if ((style_changed || rect_changed) && !SetWindowPos_orig( + hWnd, + HWND_TOP, + monitor_rect.left, + monitor_rect.top, + host_width, + host_height, + SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED)) { + log_warning( + "graphics", + "two-head exclusive: failed to place SMALL window, error={}", + GetLastError()); + return false; + } + + if (!visible_before) { + ShowWindow_orig(hWnd, SW_SHOWNOACTIVATE); + } + if (MonitorFromWindow(hWnd, MONITOR_DEFAULTTONULL) != monitor) { + log_warning( + "graphics", + "two-head exclusive: SMALL window {} is on the wrong monitor", + fmt::ptr(hWnd)); + return false; + } + + GFDM_TWO_HEAD_SMALL_WINDOW = hWnd; + return true; +} + static bool gitadora_should_allow_small_resize() { return GRAPHICS_WINDOWED && games::gitadora::is_arena_model() && @@ -618,9 +716,16 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC } // only hook touch window if multiple windows are allowed - if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { + if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") { gitadora_remember_window(result, gfdm_window_name); - graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); + } + if (is_gfdm_sub_window && + ((GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) || + games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE)) { + gitadora_remember_window(result, gfdm_window_name); + if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { + graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); + } } if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { gitadora_force_window_style(result); @@ -767,6 +872,10 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei nHeight, bRepaint); + if (is_gfdm_two_head_small_window(hWnd)) { + return TRUE; + } + // sound voltex windowed mode adjustments if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) { RECT rect {}; @@ -874,6 +983,11 @@ static HCURSOR WINAPI SetCursor_hook(HCURSOR hCursor) { static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) { + if (is_gfdm_two_head_small_window(hWnd) && + (nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE)) { + return GetWindowLongA(hWnd, nIndex); + } + // DDR window style fix if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) { dwNewLong |= WS_OVERLAPPEDWINDOW; @@ -894,6 +1008,11 @@ static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) { static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) { + if (is_gfdm_two_head_small_window(hWnd) && + (nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE)) { + return GetWindowLongW(hWnd, nIndex); + } + // DDR overlapped window fix if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) { dwNewLong |= WS_OVERLAPPEDWINDOW; @@ -915,6 +1034,12 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) { static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags) { + if (is_gfdm_two_head_small_window(hWnd) && + ((uFlags & SWP_HIDEWINDOW) || + (uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) { + return TRUE; + } + // windowed mode adjustments if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) { return TRUE; @@ -939,6 +1064,13 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter, } static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) { + if (is_gfdm_two_head_small_window(hWnd) && + (nCmdShow == SW_HIDE || nCmdShow == SW_MINIMIZE || + nCmdShow == SW_SHOWMINIMIZED || nCmdShow == SW_SHOWMINNOACTIVE || + nCmdShow == SW_FORCEMINIMIZE)) { + return TRUE; + } + if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS && hWnd != GRAPHICS_HOOKED_WINDOW) { diff --git a/src/spice2x/hooks/graphics/graphics.h b/src/spice2x/hooks/graphics/graphics.h index 9c7cffe..22663bd 100644 --- a/src/spice2x/hooks/graphics/graphics.h +++ b/src/spice2x/hooks/graphics/graphics.h @@ -119,6 +119,15 @@ extern bool D3D9_DEVICE_HOOK_DISABLE; void graphics_init(); void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters); +// The native GITADORA two-head D3D9 group uses the game's named SMALL +// device window for the native physical SMALL head. The game requests +// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized +// to the requested native SMALL mode before the native fullscreen device is +// created. During the adapter-group startup warmup, the monitor can briefly +// report a different mode; preserve its origin but not that temporary size. +bool graphics_gitadora_prepare_two_head_device_window( + HWND hWnd, HMONITOR target_monitor = nullptr, UINT desired_width = 0, + UINT desired_height = 0); void graphics_add_wnd_proc(WNDPROC wndProc); void graphics_remove_wnd_proc(WNDPROC wndProc); void graphics_hook_subscreen_window(HWND hWnd); diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index d451c26..69e743d 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -1299,7 +1299,9 @@ static const std::vector OPTION_DEFINITIONS = { .name = "gdalayout", .desc = "For Arena Model: select how many windows to create.\n\n" "1 window: main only; use the subscreen overlay for SMALL.\n\n" - "2 windows: main + SMALL. Note: currently only works for windowed mode, broken for fullscreen.\n\n" + "2 windows: main + SMALL. Without -w, this uses native D3D9 adapter-group fullscreen " + "with physical MAIN/SMALL and virtual LEFT/RIGHT. It requires two active displays on " + "one adapter group. With -w, it uses borderless windows instead.\n\n" "4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the " "right resolution; see wiki for details.", .type = OptionType::Enum,