From 2042b629e961be6ce3140a146c524ae100a40ce7 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:00:47 -0700 Subject: [PATCH] popn: allow game to launch on PCs with only one monitor (#633) ## Link to GitHub Issue or related Pull Request, if one exists #618 ## Description of change The game insists on having two monitors. Hook various DX9 and Win32 monitor APIs to fake a monitor if the user only has one. This correctly convinces the game that it needs to draw two screens instead of just one. ## Testing WIP --- src/spice2x/games/popn/popn.cpp | 167 +++++++++++++++++- .../graphics/backends/d3d9/d3d9_backend.cpp | 53 +++++- src/spice2x/hooks/graphics/graphics.cpp | 1 + src/spice2x/hooks/graphics/graphics.h | 1 + src/spice2x/util/sysutils.cpp | 2 +- 5 files changed, 214 insertions(+), 10 deletions(-) diff --git a/src/spice2x/games/popn/popn.cpp b/src/spice2x/games/popn/popn.cpp index 19f3b71..b3dbea9 100644 --- a/src/spice2x/games/popn/popn.cpp +++ b/src/spice2x/games/popn/popn.cpp @@ -26,9 +26,121 @@ namespace games::popn { bool SHOW_PIKA_MONITOR_WARNING = false; #if SPICE64 && !SPICE_XP - + + const HMONITOR FAKE_MONITOR_HMONITOR = (HMONITOR)-1; + constexpr LONG FAKE_MONITOR_OFFSET_X_Y = -999999; + constexpr LONG FAKE_MONITOR_WIDTH = 1280; + constexpr LONG FAKE_MONITOR_HEIGHT = 800; + + static decltype(GetDisplayConfigBufferSizes) *GetDisplayConfigBufferSizes_orig = nullptr; + static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr; static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr; + static UINT32 pNumPathArrayElements_original = 0; + static UINT32 pNumModeInfoArrayElements_original = 0; + + static + LONG + WINAPI + GetDisplayConfigBufferSizes_hook( + UINT32 Flags, + UINT32 *pNumPathArrayElements, + UINT32 *pNumModeInfoArrayElements) + { + const auto ret = GetDisplayConfigBufferSizes_orig(Flags, pNumPathArrayElements, pNumModeInfoArrayElements); + if (FAKE_SUBSCREEN_ADAPTER) { + log_misc("popn", "GetDisplayConfigBufferSizes returning fake monitor paths and modes"); + pNumPathArrayElements_original = *pNumPathArrayElements; + pNumModeInfoArrayElements_original = *pNumModeInfoArrayElements; + *pNumPathArrayElements += 1; + *pNumModeInfoArrayElements += 2; + } + return ret; + } + + static + LONG + WINAPI + QueryDisplayConfig_hook( + UINT32 flags, + UINT32* numPathArrayElements, + DISPLAYCONFIG_PATH_INFO* pathArray, + UINT32* numModeInfoArrayElements, + DISPLAYCONFIG_MODE_INFO* modeInfoArray, + DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId) + { + + if (!FAKE_SUBSCREEN_ADAPTER) { + return QueryDisplayConfig_orig( + flags, + numPathArrayElements, pathArray, + numModeInfoArrayElements, modeInfoArray, + currentTopologyId); + } + + // call original to fill in real monitor info + UINT32 num_paths = pNumPathArrayElements_original; + UINT32 num_modes = pNumModeInfoArrayElements_original; + const auto ret = QueryDisplayConfig_orig( + flags, + &num_paths, pathArray, + &num_modes, modeInfoArray, + currentTopologyId); + + if (ret != ERROR_SUCCESS) { + log_warning("popn", "QueryDisplayConfig failed with error code {}", ret); + return ret; + } + + log_misc("popn", "QueryDisplayConfig returning fake monitor paths and modes"); + + // insert a fake path + DISPLAYCONFIG_PATH_INFO *path = &pathArray[pNumPathArrayElements_original]; + *path = {}; + path->flags = DISPLAYCONFIG_PATH_ACTIVE; + + path->sourceInfo.adapterId.HighPart = -1; + path->sourceInfo.adapterId.LowPart = -1; + path->sourceInfo.id = -1; + path->sourceInfo.modeInfoIdx = pNumModeInfoArrayElements_original; + path->sourceInfo.statusFlags = DISPLAYCONFIG_SOURCE_IN_USE; + + path->targetInfo.adapterId.HighPart = -1; + path->targetInfo.adapterId.LowPart = -1; + path->targetInfo.id = -2; + path->targetInfo.modeInfoIdx = pNumModeInfoArrayElements_original + 1; + path->targetInfo.outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI; + path->targetInfo.rotation = DISPLAYCONFIG_ROTATION_IDENTITY; + path->targetInfo.scaling = DISPLAYCONFIG_SCALING_IDENTITY; + path->targetInfo.refreshRate.Numerator = 60000; + path->targetInfo.refreshRate.Denominator = 1000; + path->targetInfo.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE; + path->targetInfo.targetAvailable = true; + path->targetInfo.statusFlags = DISPLAYCONFIG_TARGET_IN_USE; + + // insert fake mode source + DISPLAYCONFIG_MODE_INFO *mode_source = &modeInfoArray[pNumModeInfoArrayElements_original]; + *mode_source = {}; + mode_source->infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE; + mode_source->id = -1; + mode_source->adapterId.HighPart = -1; + mode_source->adapterId.LowPart = -1; + mode_source->sourceMode.width = FAKE_MONITOR_WIDTH; + mode_source->sourceMode.height = FAKE_MONITOR_HEIGHT; + mode_source->sourceMode.pixelFormat = DISPLAYCONFIG_PIXELFORMAT_32BPP; + mode_source->sourceMode.position.x = FAKE_MONITOR_OFFSET_X_Y; + mode_source->sourceMode.position.y = FAKE_MONITOR_OFFSET_X_Y; + + // insert fake mode target + DISPLAYCONFIG_MODE_INFO *mode_target = &modeInfoArray[pNumModeInfoArrayElements_original+1]; + *mode_target = {}; + mode_target->infoType = DISPLAYCONFIG_MODE_INFO_TYPE_TARGET; + mode_target->id = -2; + mode_target->adapterId.HighPart = -1; + mode_target->adapterId.LowPart = -1; + return ret; + } + static LONG WINAPI @@ -37,6 +149,43 @@ namespace games::popn { if (requestPacket == nullptr) { return DisplayConfigGetDeviceInfo_orig(requestPacket); } + + // fake monitor + if (FAKE_SUBSCREEN_ADAPTER && + (requestPacket->id == static_cast(-1) || + requestPacket->id == static_cast(-2)) && + requestPacket->adapterId.HighPart == static_cast(-1) && + requestPacket->adapterId.LowPart == static_cast(-1)) { + log_misc( + "popn", + "DisplayConfigGetDeviceInfo hook hit for fake monitor, type={}, size={}, id={}, luid={}/{}", + static_cast(requestPacket->type), + requestPacket->size, + requestPacket->id, + requestPacket->adapterId.HighPart, + requestPacket->adapterId.LowPart); + + if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) { + const auto target = reinterpret_cast(requestPacket); + target->flags.value = 0; + target->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI; + target->connectorInstance = 0; + wcscpy(target->monitorFriendlyDeviceName, L"Spice Fake Monitor"); + wcscpy(target->monitorDevicePath, L"\\\\?\\SpiceFakeMonitor"); + } else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) { + const auto source = reinterpret_cast(requestPacket); + // value must match WrappedIDirect3D9::GetAdapterIdentifier + wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE"); + } else { + log_fatal( + "popn", + "unexpected device info type {} for fake monitor", + static_cast(requestPacket->type)); + } + + return ERROR_SUCCESS; + } + const auto ret = DisplayConfigGetDeviceInfo_orig(requestPacket); log_misc( "popn", @@ -436,6 +585,12 @@ namespace games::popn { } // monitor hook + 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); @@ -472,10 +627,12 @@ namespace games::popn { // 00000080 0000000A 00000001 (button 8) // 00000100 0000000B 00000001 (button 9) // set third column to 0 and it will work with BIO2 - - wintouchemu::FORCE = true; - wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; - wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE); + + if (!GRAPHICS_WINDOWED) { + wintouchemu::FORCE = true; + wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; + wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE); + } #endif diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index 30883d5..084cef9 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -463,7 +463,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti } UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() { - return pReal->GetAdapterCount(); + UINT result = pReal->GetAdapterCount(); + + if (!FAKE_SUBSCREEN_ADAPTER && games::popn::is_pikapika_model() && result == 1) { + FAKE_SUBSCREEN_ADAPTER = true; + log_info( + "graphics::d3d9", + "GetAdapterCount returned 1, popn needs 2 adapters - enabliing fake submonitor mode"); + } + + if (FAKE_SUBSCREEN_ADAPTER) { + return 2; + } + + return result; } HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier( @@ -471,10 +484,26 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier( DWORD Flags, D3DADAPTER_IDENTIFIER9 *pIdentifier) { + + if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1 && pIdentifier) { + *pIdentifier = {}; + strcpy(pIdentifier->DeviceName, "\\\\.\\DISPLAY_SPICE_FAKE"); + log_misc( + "graphics::d3d9", + "GetAdapterIdentifier called for fake subscreen adapter 1: {}", + pIdentifier->DeviceName); + return S_OK; + } + CHECK_RESULT(pReal->GetAdapterIdentifier(Adapter, Flags, pIdentifier)); } UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) { + if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { + log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter"); + return 1; + } + return pReal->GetAdapterModeCount(Adapter, Format); } @@ -484,6 +513,19 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes( UINT Mode, D3DDISPLAYMODE *pMode) { + if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { + log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter"); + if (Mode == 0 && pMode) { + pMode->Width = 1280; + pMode->Height = 800; + pMode->RefreshRate = 60; + pMode->Format = D3DFMT_X8R8G8B8; + return S_OK; + } else { + return D3DERR_INVALIDCALL; + } + } + HRESULT ret = pReal->EnumAdapterModes(Adapter, Format, Mode, pMode); if (SUCCEEDED(ret) && pMode) { @@ -580,6 +622,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat( D3DRESOURCETYPE RType, D3DFORMAT CheckFormat) { + if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { + log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter"); + return D3D_OK; + } + CHECK_RESULT(pReal->CheckDeviceFormat(Adapter, DeviceType, AdapterFormat, Usage, RType, CheckFormat)); } @@ -677,9 +724,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT pCaps->NumberOfAdaptersInGroup = 1; } } else if (games::popn::is_pikapika_model()) { - if (GRAPHICS_WINDOWED) { - pCaps->NumberOfAdaptersInGroup = 2; - } + pCaps->NumberOfAdaptersInGroup = 2; } return ret; diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 57a50b9..3d54e17 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -40,6 +40,7 @@ HWND TDJ_SUBSCREEN_WINDOW = nullptr; HWND SDVX_SUBSCREEN_WINDOW = nullptr; HWND GFDM_SUBSCREEN_WINDOW = nullptr; HWND POPN_SUBSCREEN_WINDOW = nullptr; +bool FAKE_SUBSCREEN_ADAPTER = false; // icon static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON)); diff --git a/src/spice2x/hooks/graphics/graphics.h b/src/spice2x/hooks/graphics/graphics.h index 179584e..4a55e1a 100644 --- a/src/spice2x/hooks/graphics/graphics.h +++ b/src/spice2x/hooks/graphics/graphics.h @@ -67,6 +67,7 @@ extern HWND POPN_SUBSCREEN_WINDOW; extern HWND GFDM_SUBSCREEN_WINDOW; extern bool SUBSCREEN_FORCE_REDRAW; +extern bool FAKE_SUBSCREEN_ADAPTER; // settings extern std::string GRAPHICS_DEVICEID; diff --git a/src/spice2x/util/sysutils.cpp b/src/spice2x/util/sysutils.cpp index 6daf40e..4182508 100644 --- a/src/spice2x/util/sysutils.cpp +++ b/src/spice2x/util/sysutils.cpp @@ -559,7 +559,7 @@ namespace sysutils { // if there is one preferred by the user, use this one, and hide others log_info( "sysutils", - "returning second monitor, {} @ index {} (-sysutilssubmonitor)", + "returning second monitor, {} @ index {} (-iidxsubmonitor / -sdvxsubmonitor)", lpDisplayDevice->DeviceName, iDevNum); return result_orig;