gitadora: (arena model) booting fullscreen with one monitor (#696)

## Link to GitHub Issue or related Pull Request, if one exists
#477 

## Description of change
By default the game needs 4 monitors.

With the new option to disable subscreens, you can now boot fullscreen
with a single 4K monitor, with subscreen overlay enabled. Side monitors
are not shown in the overlay; only the touch screen is emulated.

## Future work

- [ ] custom resolution (-forceres) doesn't work properly (hooking
ResetEx does allow the game to boot but will render incorrectly;
probably needs hex edits)
- [x] enable this automatically if the user has fewer than 4 monitors
- [ ] ability to boot with a 4k main screen + small subscreen (with no
side screens which do nothing gameplay wise), but this whole display
emulation is a nightmare to work with & test thoroughly so it'll have to
be in the future.


## Testing
Checklist:

- [x] fs on 1 monitor with option set
- [x] fs on 2 monitor with option set
- [x] windowed - default (4 windows, no overlay)
- [x] windowed - with option set (1 window, overlay)
- [x] fs with 4 monitors?
- [x] check popn hc for regression
This commit is contained in:
bicarus
2026-05-18 15:28:12 -07:00
committed by GitHub
parent e9dcc5717e
commit f69e40fa26
13 changed files with 425 additions and 69 deletions
+307 -5
View File
@@ -13,6 +13,8 @@
#include "util/logging.h" #include "util/logging.h"
#include "util/sigscan.h" #include "util/sigscan.h"
#include "util/socd_cleaner.h" #include "util/socd_cleaner.h"
#include "util/sysutils.h"
#include "util/utils.h"
#include "hooks/setupapihook.h" #include "hooks/setupapihook.h"
namespace games::gitadora { namespace games::gitadora {
@@ -20,7 +22,6 @@ namespace games::gitadora {
// settings // settings
bool TWOCHANNEL = false; bool TWOCHANNEL = false;
std::optional<unsigned int> CAB_TYPE = std::nullopt; std::optional<unsigned int> CAB_TYPE = std::nullopt;
bool ARENA_SINGLE_WINDOW = false;
bool P1_LEFTY = false; bool P1_LEFTY = false;
bool P2_LEFTY = false; bool P2_LEFTY = false;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
@@ -223,8 +224,7 @@ namespace games::gitadora {
// arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay // arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay
// force scaling to make things usable // force scaling to make things usable
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model() && if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model() && !cfg::CONFIGURATOR_STANDALONE) {
GRAPHICS_WINDOWED && !cfg::CONFIGURATOR_STANDALONE) {
overlay::UI_SCALE_PERCENT = 250; overlay::UI_SCALE_PERCENT = 250;
} }
@@ -237,9 +237,292 @@ namespace games::gitadora {
} else { } else {
log_info("gitadora", "guitar pick SOCD algorithm: legacy"); log_info("gitadora", "guitar pick SOCD algorithm: legacy");
} }
#if SPICE64 && !SPICE_XP
if (is_arena_model() && !GRAPHICS_WINDOWED && !GRAPHICS_FORCE_SINGLE_ADAPTER) {
const auto &monitors = sysutils::enumerate_monitors();
const size_t active_count = monitors.size();
log_info("gitadora", "arena model: detected {} active monitor(s)", active_count);
if (active_count < 4) {
log_info("gitadora", "arena model: enable single monitor mode due to insufficient monitors");
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
} }
} }
#endif
}
}
#if SPICE64 && !SPICE_XP
static decltype(GetDisplayConfigBufferSizes) *GetDisplayConfigBufferSizes_orig = nullptr;
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.
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
// fake monitors appended after the real ones. the game classifies monitors
// by outputTechnology + connectorInstance:
// HDMI -> main 4k monitor (real primary)
// DP connInstance 0 -> left
// DP connInstance 1 -> right
// DP connInstance 2 -> small (sub/touch)
// ids are negated on the fake monitor headers so they can be distinguished
// from real ones in DisplayConfigGetDeviceInfo.
struct FakeMonitor {
LONG id;
int width;
int height;
int offset_x;
int offset_y;
UINT32 connector_instance;
};
// ORDERING MATTERS: the d3d9 wrapper (FAKE_SUBSCREEN_ADAPTER) hands out
// adapters as "\\.\DISPLAY_SPICE_FAKE_{N}" for N=1,2,3. The game maps each
// 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[] = {
{ 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<UINT32>(std::size(FAKE_MONITORS));
// call QueryDisplayConfig once, keep only the primary monitor's path and its
// two referenced modes (source + target). modeInfoIdx values are rewritten to
// 0 and 1 so the cache is self-consistent.
static void cache_primary_monitor_info() {
UINT32 path_count = 0;
UINT32 mode_count = 0;
if (GetDisplayConfigBufferSizes_orig(
QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: GetDisplayConfigBufferSizes failed");
}
std::vector<DISPLAYCONFIG_PATH_INFO> all_paths(path_count);
std::vector<DISPLAYCONFIG_MODE_INFO> all_modes(mode_count);
if (QueryDisplayConfig_orig(
QDC_ONLY_ACTIVE_PATHS,
&path_count, all_paths.data(),
&mode_count, all_modes.data(),
nullptr) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: QueryDisplayConfig failed");
}
all_paths.resize(path_count);
all_modes.resize(mode_count);
// pick the primary monitor: source mode at (0, 0)
auto primary = std::find_if(all_paths.begin(), all_paths.end(),
[&](const auto &p) {
const auto idx = p.sourceInfo.modeInfoIdx;
return idx < all_modes.size() &&
all_modes[idx].infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE &&
all_modes[idx].sourceMode.position.x == 0 &&
all_modes[idx].sourceMode.position.y == 0;
});
if (primary == all_paths.end()) {
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
}
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");
}
static
LONG
WINAPI
GetDisplayConfigBufferSizes_hook(
UINT32 Flags,
UINT32 *pNumPathArrayElements,
UINT32 *pNumModeInfoArrayElements)
{
// populate cached primary real monitor on the first call
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;
log_info(
"gitadora",
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
FAKE_MONITOR_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).
static void insert_fake_monitor(
DISPLAYCONFIG_PATH_INFO *paths,
DISPLAYCONFIG_MODE_INFO *modes,
UINT32 i)
{
const FakeMonitor &m = FAKE_MONITORS[i];
const UINT32 src_idx = 2 + i * 2;
const UINT32 tgt_idx = src_idx + 1;
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
const UINT32 uid = static_cast<UINT32>(-m.id);
paths[1 + i] = {
.sourceInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = src_idx,
.statusFlags = DISPLAYCONFIG_SOURCE_IN_USE,
},
.targetInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = tgt_idx,
.outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL,
.rotation = DISPLAYCONFIG_ROTATION_IDENTITY,
.scaling = DISPLAYCONFIG_SCALING_IDENTITY,
.refreshRate = { .Numerator = 60000, .Denominator = 1000 },
.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
.targetAvailable = TRUE,
.statusFlags = DISPLAYCONFIG_TARGET_IN_USE,
},
.flags = DISPLAYCONFIG_PATH_ACTIVE,
};
modes[src_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE,
.id = uid,
.adapterId = adapter_id,
.sourceMode = {
.width = static_cast<UINT32>(m.width),
.height = static_cast<UINT32>(m.height),
.pixelFormat = DISPLAYCONFIG_PIXELFORMAT_32BPP,
.position = { .x = m.offset_x, .y = m.offset_y },
},
};
modes[tgt_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_TARGET,
.id = uid,
.adapterId = adapter_id,
.targetMode = {},
};
log_misc(
"gitadora",
"inserted fake monitor: id={}, width={}, height={}, offset_x={}, offset_y={}",
m.id, m.width, m.height, m.offset_x, m.offset_y);
}
static
LONG
WINAPI
QueryDisplayConfig_hook(
UINT32 flags,
UINT32* numPathArrayElements,
DISPLAYCONFIG_PATH_INFO* pathArray,
UINT32* numModeInfoArrayElements,
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
{
// copy cached primary real monitor into caller buffers at index 0
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 (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);
}
return ERROR_SUCCESS;
}
static
LONG
WINAPI
DisplayConfigGetDeviceInfo_hook(DISPLAYCONFIG_DEVICE_INFO_HEADER* requestPacket)
{
if (requestPacket == nullptr) {
return DisplayConfigGetDeviceInfo_orig(requestPacket);
}
// handle fake monitors (negative id) directly without calling orig
const auto id = static_cast<int>(requestPacket->id);
if (id < 0) {
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
const auto sourceName = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
// name must match WrappedIDirect3D9::GetAdapterIdentifier
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", -id);
wcscpy(sourceName->viewGdiDeviceName, s2ws(adapter_name).c_str());
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake source id={} name={}", id, adapter_name);
return ERROR_SUCCESS;
}
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const LONG fake_id = -id;
UINT32 conn_inst = 0xff;
for (const auto& f : FAKE_MONITORS) {
if (f.id == fake_id) {
conn_inst = f.connector_instance;
break;
}
}
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
targetName->connectorInstance = conn_inst;
wcscpy(targetName->monitorFriendlyDeviceName, L"Spice Fake Monitor");
wcscpy(targetName->monitorDevicePath, L"\\\\?\\SpiceFakeMonitor");
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake target id={} -> DP connInst {}",
id, targetName->connectorInstance);
return ERROR_SUCCESS;
}
}
const auto ret = DisplayConfigGetDeviceInfo_orig(requestPacket);
if (ret != ERROR_SUCCESS ||
requestPacket->type != DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
return ret;
}
// override the cached primary real monitor target info to look like HDMI/0
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(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)
{
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
targetName->connectorInstance = 0;
log_info("gitadora",
"overriding primary monitor (id={}) to pretend to be HDMI",
targetName->header.id);
}
return ret;
}
#endif
void GitaDoraGame::attach() { void GitaDoraGame::attach() {
Game::attach(); Game::attach();
@@ -297,11 +580,30 @@ namespace games::gitadora {
// volume change prevention // volume change prevention
hooks::audio::mme::init(avs::game::DLL_INSTANCE); hooks::audio::mme::init(avs::game::DLL_INSTANCE);
// touch hook // monitor/touch hooks (windowed or full screen)
if (ARENA_SINGLE_WINDOW) { if (GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW) {
// enable touch hook for subscreen overlay
wintouchemu::FORCE = true; wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE); 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);
}
#endif
} }
return; return;
} }
-1
View File
@@ -11,7 +11,6 @@ namespace games::gitadora {
// settings // settings
extern bool TWOCHANNEL; extern bool TWOCHANNEL;
extern std::optional<unsigned int> CAB_TYPE; extern std::optional<unsigned int> CAB_TYPE;
extern bool ARENA_SINGLE_WINDOW;
extern bool P1_LEFTY; extern bool P1_LEFTY;
extern bool P2_LEFTY; extern bool P2_LEFTY;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
+1 -1
View File
@@ -261,7 +261,7 @@ namespace games::popn {
} else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) { } else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket); const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
// value must match WrappedIDirect3D9::GetAdapterIdentifier // value must match WrappedIDirect3D9::GetAdapterIdentifier
wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE"); wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE_1");
} else { } else {
log_fatal( log_fatal(
"popn", "popn",
@@ -465,15 +465,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() { UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
UINT result = pReal->GetAdapterCount(); UINT result = pReal->GetAdapterCount();
if (!FAKE_SUBSCREEN_ADAPTER && games::popn::is_pikapika_model() && result == 1) { if (!FAKE_SUBSCREEN_ADAPTER) {
if (games::popn::is_pikapika_model() && result == 1) {
FAKE_SUBSCREEN_ADAPTER = true; FAKE_SUBSCREEN_ADAPTER = true;
log_info( log_info(
"graphics::d3d9", "graphics::d3d9",
"GetAdapterCount returned 1, popn needs 2 adapters - enabliing fake submonitor mode"); "GetAdapterCount returned {}, popn needs 2 adapters - enabling fake submonitor mode", result);
}
if (FAKE_SUBSCREEN_ADAPTER) {
return 2; return 2;
} else if (games::gitadora::is_arena_model() && !GRAPHICS_WINDOWED && result < 4) {
FAKE_SUBSCREEN_ADAPTER = true;
log_info(
"graphics::d3d9",
"GetAdapterCount returned {}, gfdm needs 4 adapters - enabling fake submonitor mode", result);
return 4;
}
} }
return result; return result;
@@ -485,12 +491,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
D3DADAPTER_IDENTIFIER9 *pIdentifier) D3DADAPTER_IDENTIFIER9 *pIdentifier)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1 && pIdentifier) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) {
*pIdentifier = {}; *pIdentifier = {};
strcpy(pIdentifier->DeviceName, "\\\\.\\DISPLAY_SPICE_FAKE"); const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
log_misc( log_misc(
"graphics::d3d9", "graphics::d3d9",
"GetAdapterIdentifier called for fake subscreen adapter 1: {}", "GetAdapterIdentifier called for fake subscreen adapter {}: {}",
Adapter,
pIdentifier->DeviceName); pIdentifier->DeviceName);
return S_OK; return S_OK;
} }
@@ -499,8 +507,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
} }
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) { UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter"); log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
return 1; return 1;
} }
@@ -513,8 +521,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
UINT Mode, UINT Mode,
D3DDISPLAYMODE *pMode) D3DDISPLAYMODE *pMode)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter"); log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
if (Mode == 0 && pMode) { if (Mode == 0 && pMode) {
pMode->Width = 1280; pMode->Width = 1280;
pMode->Height = 800; pMode->Height = 800;
@@ -622,8 +630,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
D3DRESOURCETYPE RType, D3DRESOURCETYPE RType,
D3DFORMAT CheckFormat) D3DFORMAT CheckFormat)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter"); log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
return D3D_OK; return D3D_OK;
} }
@@ -1143,10 +1151,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface); *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
// initialize sub screen if IIDX/SDVX requested a multi-head context // initialize sub screen if the game requested a multi-head context
if (avs::game::is_model({"LDJ", "KFC", "M39"}) && if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) { (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[1]);
UINT i = 1;
if (games::gitadora::is_arena_model()) {
i = 2;
}
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]);
} }
} }
@@ -1196,7 +1209,13 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE
log_info("graphics::d3d9", "created additional swap chain for windowed mode"); log_info("graphics::d3d9", "created additional swap chain for windowed mode");
} }
} else { } else {
hr = device->GetSwapChain(1, &SUB_SWAP_CHAIN);
int swapchain = 1;
if (games::gitadora::is_arena_model()) {
swapchain = 2;
}
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
if (FAILED(hr)) { if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr)); log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr));
} else { } else {
@@ -1233,7 +1252,7 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
// iidx/sdvx // iidx/sdvx
int swapchain = 1; int swapchain = 1;
if (games::gitadora::is_arena_model()) { if (games::gitadora::is_arena_model()) {
swapchain = 3; swapchain = 2;
} }
if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) { if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) {
@@ -1452,8 +1471,9 @@ void graphics_d3d9_on_present(
// for IIDX TDJ / SDVX UFC, handle subscreen // for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm = games::sdvx::is_valkyrie_model(); const bool is_vm = games::sdvx::is_valkyrie_model();
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE; const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
const bool is_gfdm_arena = const bool is_gfdm_arena = games::gitadora::is_arena_model() &&
games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW; (GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW);
const bool is_pika = games::popn::is_pikapika_model(); const bool is_pika = games::popn::is_pikapika_model();
if (is_vm || is_tdj || is_gfdm_arena || is_pika) { if (is_vm || is_tdj || is_gfdm_arena || is_pika) {
graphics_d3d9_ldj_on_present(wrapped_device); graphics_d3d9_ldj_on_present(wrapped_device);
@@ -129,13 +129,17 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
this->main_swapchain->Release(); this->main_swapchain->Release();
this->main_swapchain = nullptr; this->main_swapchain = nullptr;
} }
if (this->sub_swapchain) { for (auto &sc : this->sub_swapchain) {
this->sub_swapchain->Release(); if (sc) {
this->sub_swapchain = nullptr; sc->Release();
sc = nullptr;
}
}
for (auto &sc : this->fake_sub_swapchain) {
if (sc) {
sc->Release();
sc = nullptr;
} }
if (this->fake_sub_swapchain) {
this->fake_sub_swapchain->Release();
this->fake_sub_swapchain = nullptr;
} }
if (overlay::ENABLED) { if (overlay::ENABLED) {
@@ -250,27 +254,47 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
WRAP_VERBOSE; WRAP_VERBOSE;
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain); HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
int index = 0;
bool create_swap_chain = false; bool create_swap_chain = false;
if (avs::game::is_model({"LDJ", "KFC", "M39"})) { if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
create_swap_chain = true; create_swap_chain = true;
} else if (games::gitadora::is_arena_model() && } else if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW && (GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW)) {
pPresentationParameters->BackBufferWidth == 800) { if (pPresentationParameters->BackBufferWidth == 800) {
// SMALL (subscreen)
create_swap_chain = true; create_swap_chain = true;
index = 0;
} else if (pPresentationParameters->BackBufferWidth == 1080) {
// LEFT/RIGHT
create_swap_chain = true;
index = 1;
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
index = 2;
}
} else {
log_warning("graphics::d3d9", "unknown swap chain detected in CreateAdditionalSwapChain");
}
} }
if (create_swap_chain) { if (create_swap_chain) {
if (SUCCEEDED(hr) && !sub_swapchain) { log_misc(
sub_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain); "graphics::d3d9",
sub_swapchain->should_run_hooks = false; "CreateAdditionalSwapChain called for swap chain {}, creating swap chain",
} else if (FAILED(hr) && !fake_sub_swapchain) { index);
if (SUCCEEDED(hr) && !sub_swapchain[index]) {
sub_swapchain[index] = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
sub_swapchain[index]->should_run_hooks = false;
} else if (FAILED(hr) && !fake_sub_swapchain[index]) {
log_warning("graphics::d3d9", log_warning("graphics::d3d9",
"failed to create sub swap chain, hr={}, using fake swap chain", "failed to create sub swap chain, hr={}, using fake swap chain",
FMT_HRESULT(hr)); FMT_HRESULT(hr));
fake_sub_swapchain = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false); fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
fake_sub_swapchain->AddRef(); fake_sub_swapchain[index]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
return D3D_OK; return D3D_OK;
} }
@@ -308,22 +332,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) { if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
swap = true; swap = true;
} }
if (games::gitadora::is_arena_model() && if (games::gitadora::is_arena_model() && iSwapChain == 2) {
games::gitadora::ARENA_SINGLE_WINDOW &&
iSwapChain == 3) {
swap = true; swap = true;
} }
if (swap) { if (swap) {
if (sub_swapchain) { if (sub_swapchain[0]) {
sub_swapchain->AddRef(); sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain[0]);
graphics_screens_register(iSwapChain); graphics_screens_register(iSwapChain);
return D3D_OK; return D3D_OK;
} else if (fake_sub_swapchain) { } else if (fake_sub_swapchain[0]) {
fake_sub_swapchain->AddRef(); fake_sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
graphics_screens_register(iSwapChain); graphics_screens_register(iSwapChain);
return D3D_OK; return D3D_OK;
@@ -344,7 +366,7 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
UINT n = pReal->GetNumberOfSwapChains(); UINT n = pReal->GetNumberOfSwapChains();
if (sub_swapchain && avs::game::is_model({"LDJ", "KFC", "M39"})) { if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
n += 1; n += 1;
} }
@@ -204,7 +204,8 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::atomic_ulong refs = 1; std::atomic_ulong refs = 1;
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr; WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *sub_swapchain = nullptr; WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
FakeIDirect3DSwapChain9 *fake_sub_swapchain = nullptr; FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
IDirect3DVertexShader9 *vertex_shader = nullptr; IDirect3DVertexShader9 *vertex_shader = nullptr;
}; };
+2 -2
View File
@@ -421,7 +421,7 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
// only hook touch window if multiple windows are allowed // only hook touch window if multiple windows are allowed
if (is_gfdm_sub_window && !games::gitadora::ARENA_SINGLE_WINDOW) { if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOW) {
GFDM_SUBSCREEN_WINDOW = result; GFDM_SUBSCREEN_WINDOW = result;
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
} }
@@ -702,7 +702,7 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) { static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
if (games::gitadora::is_arena_model() && if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW && GRAPHICS_PREVENT_SECONDARY_WINDOW &&
hWnd != GRAPHICS_HOOKED_WINDOW) { hWnd != GRAPHICS_HOOKED_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd)); log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true; return true;
+5 -2
View File
@@ -644,8 +644,11 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::GitaDoraCabinetType].is_active()) { if (options[launcher::Options::GitaDoraCabinetType].is_active()) {
games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32(); games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32();
} }
if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool() && GRAPHICS_WINDOWED) { if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool()) {
games::gitadora::ARENA_SINGLE_WINDOW = true; // for full screen
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
// for windowed
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
} }
if (options[launcher::Options::GitaDoraWailHold].is_active()) { if (options[launcher::Options::GitaDoraWailHold].is_active()) {
socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32(); socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32();
+5 -2
View File
@@ -1056,9 +1056,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// GitaDoraArenaSingleWindow // GitaDoraArenaSingleWindow
.title = "GitaDora Arena Single Window (EXPERIMENTAL)", .title = "GitaDora Arena Disable Subscreens (EXPERIMENTAL)",
.name = "gdaonewindow", .name = "gdaonewindow",
.desc = "For Arena Model, when combined with windowed mode, only show the main game window; enables subscreen overlay.", .desc = "For Arena Model:\n\n"
"Windowed mode: instead of 4 windows, create 1 window.\n\n"
"Fullscreen mode: instead of requiring 4 monitors, use only the primary monitor. WARNING: requires 4K monitor.\n\n"
"To access the subscreen, use the subscreen overlay.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "GitaDora", .game_name = "GitaDora",
.category = "Game Options", .category = "Game Options",
+1 -1
View File
@@ -416,7 +416,7 @@ namespace wintouchemu {
// same as iidx case above // same as iidx case above
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen"); log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
USE_MOUSE = true; USE_MOUSE = true;
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) { } else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOW) {
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen"); log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
USE_MOUSE = true; USE_MOUSE = true;
} else { } else {
+1 -1
View File
@@ -415,7 +415,7 @@ void overlay::SpiceOverlay::init() {
window_sub = new overlay::windows::DRSDanceFloorDisplay(this); window_sub = new overlay::windows::DRSDanceFloorDisplay(this);
} else if (avs::game::is_model("KFC")) { } else if (avs::game::is_model("KFC")) {
window_sub = new overlay::windows::SDVXSubScreen(this); window_sub = new overlay::windows::SDVXSubScreen(this);
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) { } else if (games::gitadora::is_arena_model()) {
window_sub = new overlay::windows::GitaDoraSubScreen(this); window_sub = new overlay::windows::GitaDoraSubScreen(this);
} else if (games::popn::is_pikapika_model()) { } else if (games::popn::is_pikapika_model()) {
window_sub = new overlay::windows::PopnSubScreen(this); window_sub = new overlay::windows::PopnSubScreen(this);
+1 -1
View File
@@ -71,7 +71,7 @@ namespace overlay::windows {
sub = "Show Valkyrie Subscreen"; sub = "Show Valkyrie Subscreen";
} else if (avs::game::is_model("REC")) { } else if (avs::game::is_model("REC")) {
sub = "Show DRS Dance Floor"; sub = "Show DRS Dance Floor";
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) { } else if (games::gitadora::is_arena_model()) {
sub = "Show GITADORA Subscreen"; sub = "Show GITADORA Subscreen";
} else if (games::popn::is_pikapika_model()) { } else if (games::popn::is_pikapika_model()) {
sub = "Show Pop'n Subscreen"; sub = "Show Pop'n Subscreen";
+6
View File
@@ -11,6 +11,12 @@ namespace overlay::windows {
if (!games::gitadora::is_arena_model()) { if (!games::gitadora::is_arena_model()) {
this->disabled_message = "Requires Arena Model!"; this->disabled_message = "Requires Arena Model!";
} else if (!GRAPHICS_PREVENT_SECONDARY_WINDOW) {
if (GRAPHICS_WINDOWED) {
this->disabled_message = "Use the dedicated subscreen window!";
} else {
this->disabled_message = "Subscreen overlay disabled by user; enable single window mode for overlay.";
}
} }
this->resize_callback = keep_10_by_16; this->resize_callback = keep_10_by_16;