Compare commits

...

6 Commits

Author SHA1 Message Date
bicarus 1ec7528dac gitadora: option to disable frame limiter (#848)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
There is a patch for GITADORA series that fixes frame pacing on modern
Windows. Turn it into a signature patch and embed it into the game.

## Testing
should work for all 64-bit gitadora versions, pre-GWD
2026-08-02 03:35:07 -07:00
bicarus-dev e7822aad8e remove volume boost from quick options 2026-08-02 01:04:11 -07:00
bicarus-dev 8140e83e46 mark -sdvxlandscape as experimental 2026-08-02 01:01:09 -07:00
bicarus cc0899290e sdvx: re-enable landscape mode (#847)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Un-deprecate SDVX landscape mode. The game calls `GetViewport` to figure
out the camera position, so if we return a sane value the game goes back
to rendering correctly.

## Testing
Tested nabla only
2026-08-02 00:50:04 -07:00
Tsung-Hsiang Chang 3012139028 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>
2026-08-02 00:03:36 -07:00
bicarus c590b87cff jb: add t44 support (#845)
With help from certain sea creature.
2026-08-01 01:20:31 -07:00
28 changed files with 2400 additions and 155 deletions
+2
View File
@@ -411,6 +411,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/sdvx/sdvx_live2d.cpp games/sdvx/sdvx_live2d.cpp
games/sdvx/io.cpp games/sdvx/io.cpp
games/sdvx/camera.cpp games/sdvx/camera.cpp
games/jb/bi2x_hook.cpp
games/jb/jb.cpp games/jb/jb.cpp
games/jb/jb_touch.cpp games/jb/jb_touch.cpp
games/jb/io.cpp games/jb/io.cpp
@@ -535,6 +536,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/graphics/nvenc_hook.cpp hooks/graphics/nvenc_hook.cpp
hooks/graphics/backends/d3d9/d3d9_backend.cpp hooks/graphics/backends/d3d9/d3d9_backend.cpp
hooks/graphics/backends/d3d9/d3d9_device.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_live2d.cpp
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
+1 -1
View File
@@ -239,7 +239,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
// SDVX Old cabinet mode // SDVX Old cabinet mode
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H') if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
answer_type = 1; answer_type = 1;
if (avs::game::is_model("L44")) if (avs::game::is_model({ "L44", "T44" }))
answer_type = 2; answer_type = 2;
// check answer type // check answer type
+1
View File
@@ -1114,6 +1114,7 @@ namespace avs {
#endif #endif
// jubeat // jubeat
{"jubeat.dll", 0x2000000}, {"jubeat.dll", 0x2000000},
{"jubeat2019.dll", 0x10000000},
// MUSECA // MUSECA
{"museca.dll", 0xC000000}, {"museca.dll", 0xC000000},
+1
View File
@@ -591,6 +591,7 @@ namespace avs {
// for proper reporting of Omnimix and other song packs // for proper reporting of Omnimix and other song packs
if (_stricmp(EA3_MODEL, "LDJ") == 0 || if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
_stricmp(EA3_MODEL, "L44") == 0 || _stricmp(EA3_MODEL, "L44") == 0 ||
_stricmp(EA3_MODEL, "T44") == 0 ||
_stricmp(EA3_MODEL, "M39") == 0 || _stricmp(EA3_MODEL, "M39") == 0 ||
_stricmp(EA3_MODEL, "KFC") == 0) _stricmp(EA3_MODEL, "KFC") == 0)
{ {
+211 -49
View File
@@ -2,12 +2,14 @@
#include "asio.h" #include "asio.h"
#include "handle.h" #include "handle.h"
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include <span>
#include <unordered_map> #include <unordered_map>
#include "cfg/configurator.h"
#include "cfg/screen_resize.h"
#include <ks.h> #include <ks.h>
#include <ksmedia.h> #include <ksmedia.h>
#include "cfg/configurator.h"
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/audio/mme.h" #include "hooks/audio/mme.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
@@ -29,12 +31,14 @@ namespace games::gitadora {
// settings // settings
bool TWOCHANNEL = false; bool TWOCHANNEL = false;
bool DISABLE_FRAME_LIMITER = false;
std::optional<unsigned int> CAB_TYPE = std::nullopt; std::optional<unsigned int> CAB_TYPE = std::nullopt;
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;
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent; std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt; std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
std::optional<std::string> ASIO_DRIVER = std::nullopt; std::optional<std::string> ASIO_DRIVER = std::nullopt;
bool ALLOW_REALTEK_AUDIO = false; bool ALLOW_REALTEK_AUDIO = false;
@@ -62,6 +66,46 @@ namespace games::gitadora {
return CreateDirectoryA(lpPathName, lpSecurityAttributes); return CreateDirectoryA(lpPathName, lpSecurityAttributes);
} }
// libshare-pj paces mainloop with separate 12 ms and 16 ms waits. these waits
// interfere with the game's normal display synchronization on modern Windows
// and can hold a nominal 60 FPS game near 58 FPS. locate the instruction
// sequences at runtime so the fix does not depend on per-version file offsets.
static void disable_mainloop_frame_limiter(HMODULE sharepj_module) {
if (!sharepj_module) {
return;
}
// both limiters have the same shape, only the millisecond target xx differs
// (0Ch for the 12 ms limiter, 10h for the 16 ms one):
//
// 48 83 F8 xx: cmp rax, xx; compare elapsed frame time with the target
// 73 10: jae +10h; skip the wait once the target has elapsed
// B9 xx 00 00 00: mov ecx, xx; load the target
// 48 2B C8: sub rcx, rax; calculate the remaining wait time
// 74 06: je +6h; skip the following six-byte Sleep call if no wait remains
//
// changing jae (73h) to jmp (EBh) makes each block always take its existing
// skip path, which bypasses only the associated Sleep call and leaves the 10h
// branch displacement and every other wait untouched.
const auto limiter_12ms_disabled = replace_pattern(
sharepj_module,
"4883F80C7310B90C000000482BC87406",
"????????EB??????????????????????", 0, 0);
const auto limiter_16ms_disabled = replace_pattern(
sharepj_module,
"4883F8107310B910000000482BC87406",
"????????EB??????????????????????", 0, 0);
if (!limiter_12ms_disabled || !limiter_16ms_disabled) {
log_fatal(
"gitadora",
"failed to disable libshare-pj mainloop frame limiter (-gdnoframelimiter), ensure patch is not already applied");
return;
}
log_info("gitadora", "successfully disabled libshare-pj mainloop frame limiter (-gdnoframelimiter)");
}
#endif #endif
/* /*
@@ -271,9 +315,21 @@ namespace games::gitadora {
break; break;
case 2: case 2:
if (!GRAPHICS_WINDOWED) { 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", "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"); log_info("gitadora", "arena model: two-window mode");
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true; GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
@@ -299,10 +355,18 @@ namespace games::gitadora {
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr; static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr; static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
// cached primary real monitor: its path + source/target mode entries. // Cached real monitor paths and their source/target mode entries. modeInfoIdx values are
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained. // 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_PATH_INFO real_primary_path = {};
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target 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 // fake monitors appended after the real ones. the game classifies monitors
// by outputTechnology + connectorInstance: // by outputTechnology + connectorInstance:
@@ -326,12 +390,31 @@ namespace games::gitadora {
// adapter to a swap chain role via its DisplayConfig connector instance, // 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 // 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. // 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) { 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1) { 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch) { 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));
// 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<const FakeMonitor> 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 // call QueryDisplayConfig once, keep only the primary monitor's path and its
// two referenced modes (source + target). modeInfoIdx values are rewritten to // two referenced modes (source + target). modeInfoIdx values are rewritten to
@@ -369,13 +452,55 @@ namespace games::gitadora {
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found"); 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[0] = all_modes[primary->sourceInfo.modeInfoIdx];
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx]; real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
real_primary_path = *primary; real_primary_path = *primary;
real_primary_path.sourceInfo.modeInfoIdx = 0; real_primary_path.sourceInfo.modeInfoIdx = 0;
real_primary_path.targetInfo.modeInfoIdx = 1; 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 static
@@ -390,33 +515,36 @@ namespace games::gitadora {
static std::once_flag populate_once; static std::once_flag populate_once;
std::call_once(populate_once, cache_primary_monitor_info); std::call_once(populate_once, cache_primary_monitor_info);
// always report exactly 1 real + N fake monitors const auto fake_monitors = get_fake_monitors();
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT; const auto fake_count = static_cast<UINT32>(fake_monitors.size());
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2; const UINT32 real_count = real_monitor_count();
*pNumPathArrayElements = real_count + fake_count;
*pNumModeInfoArrayElements = (real_count + fake_count) * 2;
log_info( log_info(
"gitadora", "gitadora",
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)", "GetDisplayConfigBufferSizes: {} real path(s) + {} fake monitor(s)",
FAKE_MONITOR_COUNT); real_count,
fake_count);
return ERROR_SUCCESS; return ERROR_SUCCESS;
} }
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor // Write a fake monitor after the real paths. The cache is packed as [source, target]
// assumption): index 0 in both arrays holds the cached primary real monitor, so // mode pairs, so each path consumes two mode entries.
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
static void insert_fake_monitor( static void insert_fake_monitor(
DISPLAYCONFIG_PATH_INFO *paths, DISPLAYCONFIG_PATH_INFO *paths,
DISPLAYCONFIG_MODE_INFO *modes, 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 = source_mode_index;
const UINT32 src_idx = 2 + i * 2;
const UINT32 tgt_idx = src_idx + 1; const UINT32 tgt_idx = src_idx + 1;
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id }; const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
const UINT32 uid = static_cast<UINT32>(-m.id); const UINT32 uid = static_cast<UINT32>(-m.id);
paths[1 + i] = { paths[path_index] = {
.sourceInfo = { .sourceInfo = {
.adapterId = adapter_id, .adapterId = adapter_id,
.id = uid, .id = uid,
@@ -474,21 +602,35 @@ namespace games::gitadora {
DISPLAYCONFIG_MODE_INFO* modeInfoArray, DISPLAYCONFIG_MODE_INFO* modeInfoArray,
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId) 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; pathArray[0] = real_primary_path;
modeInfoArray[0] = real_primary_modes[0]; modeInfoArray[0] = real_primary_modes[0];
modeInfoArray[1] = real_primary_modes[1]; modeInfoArray[1] = real_primary_modes[1];
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT; if (is_two_head_exclusive() && real_small_path_available) {
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2; 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<UINT32>(fake_monitors.size());
const UINT32 real_count = real_monitor_count();
*numPathArrayElements = real_count + fake_count;
*numModeInfoArrayElements = (real_count + fake_count) * 2;
if (currentTopologyId != nullptr) { if (currentTopologyId != nullptr) {
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND; *currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
} }
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes"); log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
// append fake monitors after the real one // Append fake monitors after the real path(s).
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) { for (UINT32 i = 0; i < fake_count; i++) {
insert_fake_monitor(pathArray, modeInfoArray, i); insert_fake_monitor(
pathArray,
modeInfoArray,
fake_monitors[i],
real_count + i,
real_count * 2 + i * 2);
} }
return ERROR_SUCCESS; return ERROR_SUCCESS;
@@ -519,7 +661,8 @@ namespace games::gitadora {
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket); const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const LONG fake_id = -id; const LONG fake_id = -id;
UINT32 conn_inst = 0xff; 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) { if (f.id == fake_id) {
conn_inst = f.connector_instance; conn_inst = f.connector_instance;
break; break;
@@ -542,18 +685,27 @@ namespace games::gitadora {
return ret; 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<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket); const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const auto& target = real_primary_path.targetInfo; const auto target_matches = [&](const DISPLAYCONFIG_PATH_TARGET_INFO &target) {
if (target.id == targetName->header.id && return target.id == targetName->header.id &&
target.adapterId.HighPart == targetName->header.adapterId.HighPart && target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
target.adapterId.LowPart == targetName->header.adapterId.LowPart) target.adapterId.LowPart == targetName->header.adapterId.LowPart;
{ };
if (target_matches(real_primary_path.targetInfo)) {
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI; targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
targetName->connectorInstance = 0; targetName->connectorInstance = 0;
log_info("gitadora", log_info("gitadora",
"overriding primary monitor (id={}) to pretend to be HDMI", "overriding primary monitor (id={}) to pretend to be HDMI",
targetName->header.id); 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; return ret;
} }
@@ -576,6 +728,12 @@ namespace games::gitadora {
HMODULE system_module = libutils::try_module("libsystem.dll"); HMODULE system_module = libutils::try_module("libsystem.dll");
// patches // patches
#ifdef SPICE64
if (DISABLE_FRAME_LIMITER && !is_arena_model()) {
disable_mainloop_frame_limiter(sharepj_module);
}
#endif
detour::inline_hook((void *) eam_network_detected_ip_change, libutils::try_proc( detour::inline_hook((void *) eam_network_detected_ip_change, libutils::try_proc(
sharepj_module, "eam_network_detected_ip_change")); sharepj_module, "eam_network_detected_ip_change"));
detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc( detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc(
@@ -651,7 +809,8 @@ namespace games::gitadora {
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true; 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) { if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay // enable touch hook for subscreen overlay
const auto native_touch_ready = !wintouchemu::FORCE && const auto native_touch_ready = !wintouchemu::FORCE &&
@@ -661,25 +820,28 @@ namespace games::gitadora {
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 !SPICE_XP
if (!GRAPHICS_WINDOWED) { if (!GRAPHICS_WINDOWED &&
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors (GRAPHICS_PREVENT_SECONDARY_WINDOWS || ARENA_TWO_HEAD_EXCLUSIVE)) {
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology if (ARENA_TWO_HEAD_EXCLUSIVE) {
GetDisplayConfigBufferSizes_orig = log_info(
detour::iat_try("GetDisplayConfigBufferSizes", "gitadora",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE); "exposing physical MAIN/SMALL and virtual LEFT/RIGHT");
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 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 // window patch
+2
View File
@@ -13,12 +13,14 @@ namespace games::gitadora {
// settings // settings
extern bool TWOCHANNEL; extern bool TWOCHANNEL;
extern bool DISABLE_FRAME_LIMITER;
extern std::optional<unsigned int> CAB_TYPE; extern std::optional<unsigned int> CAB_TYPE;
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;
extern std::optional<socd::SocdAlgorithm> PICK_ALGO; extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
extern std::optional<uint8_t> ARENA_WINDOW_COUNT; extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
extern std::optional<std::string> ASIO_DRIVER; extern std::optional<std::string> ASIO_DRIVER;
extern bool ALLOW_REALTEK_AUDIO; extern bool ALLOW_REALTEK_AUDIO;
+1
View File
@@ -133,6 +133,7 @@ namespace games {
buttons_help.insert({ jb, jb::get_buttons_help() }); buttons_help.insert({ jb, jb::get_buttons_help() });
lights.insert({ jb, jb::get_lights() }); lights.insert({ jb, jb::get_lights() });
file_hints[jb].push_back({"jubeat.dll"}); file_hints[jb].push_back({"jubeat.dll"});
file_hints[jb].push_back({"jubeat2019.dll"});
// mga // mga
const std::string mga("Metal Gear"); const std::string mga("Metal Gear");
+336
View File
@@ -0,0 +1,336 @@
#include "bi2x_hook.h"
#if SPICE64
#include <array>
#include <cstdint>
#include <cstring>
#include "games/io.h"
#include "games/jb/jb_touch.h"
#include "io.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/detour.h"
#include "util/logging.h"
namespace games::jb {
struct AIO_SCI_COMM {
std::array<uint8_t, 0x100> data;
};
struct AIO_NMGR_IOB2;
struct AIO_NMGR_IOB2_VTABLE {
std::array<void *, 10> unused;
void (__fastcall *begin_manage)(AIO_NMGR_IOB2 *node_mgr);
};
struct AIO_NMGR_IOB2 {
AIO_NMGR_IOB2_VTABLE *vtable;
std::array<uint8_t, 0x78> data;
};
struct AIO_IOB2_BI2X_T44 {
std::array<uint8_t, 0x80> data;
};
struct AIO_IOB2_BI2X_T44_DEVSTATUS {
std::array<uint8_t, 0x140> data;
};
struct AIO_IOB2_BI2X_WRFIRM {
uint8_t data;
};
static_assert(sizeof(AIO_NMGR_IOB2) == 0x80);
static_assert(sizeof(AIO_IOB2_BI2X_T44_DEVSTATUS) == 0x140);
using aioIob2Bi2xT44_Create_t = AIO_IOB2_BI2X_T44 *(__fastcall *)(AIO_NMGR_IOB2 *node_mgr,
uint32_t device_id);
using aioIob2Bi2xT44_GetDeviceStatus_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
AIO_IOB2_BI2X_T44_DEVSTATUS *status);
using aioIob2Bi2xT44_IoReset_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t reset);
using aioIob2Bi2xT44_SetWatchDogTimer_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint8_t count);
using aioIob2Bi2xT44_ControlCoinBlocker_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
uint32_t slot, bool open);
using aioIob2Bi2xT44_AddCounter_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
uint32_t counter, uint32_t count);
using aioIob2Bi2xT44_SetIccrLed_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t color);
using aioIob2Bi2xT44_SetTapeLedData_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
uint32_t tape, const void *data);
using aioIob2Bi2x_OpenSciUsbCdc_t = AIO_SCI_COMM *(__fastcall *)(uint32_t serial_number);
using aioIob2Bi2x_CreateWriteFirmContext_t = AIO_IOB2_BI2X_WRFIRM *(__fastcall *)(
uint32_t serial_number, uint32_t iob_mask);
using aioIob2Bi2x_DestroyWriteFirmContext_t = void (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context);
using aioIob2Bi2x_WriteFirmGetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context);
using aioIob2Bi2x_WriteFirmIsCompleted_t = bool (__fastcall *)(int32_t state);
using aioIob2Bi2x_WriteFirmIsError_t = bool (__fastcall *)(int32_t state);
using aioNMgrIob2_Create_t = AIO_NMGR_IOB2 *(__fastcall *)(AIO_SCI_COMM *sci, uint32_t mode);
using aioSci_Destroy_t = void (__fastcall *)(AIO_SCI_COMM *sci);
using aioNodeMgr_Destroy_t = void (__fastcall *)(AIO_NMGR_IOB2 *node_mgr);
using aioNodeCtl_Destroy_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node);
using aioNodeCtl_UpdateDevicesStatus_t = void (__fastcall *)();
static aioIob2Bi2xT44_Create_t aioIob2Bi2xT44_Create_orig = nullptr;
static aioIob2Bi2xT44_GetDeviceStatus_t aioIob2Bi2xT44_GetDeviceStatus_orig = nullptr;
static aioIob2Bi2xT44_IoReset_t aioIob2Bi2xT44_IoReset_orig = nullptr;
static aioIob2Bi2xT44_SetWatchDogTimer_t aioIob2Bi2xT44_SetWatchDogTimer_orig = nullptr;
static aioIob2Bi2xT44_ControlCoinBlocker_t aioIob2Bi2xT44_ControlCoinBlocker_orig = nullptr;
static aioIob2Bi2xT44_AddCounter_t aioIob2Bi2xT44_AddCounter_orig = nullptr;
static aioIob2Bi2xT44_SetIccrLed_t aioIob2Bi2xT44_SetIccrLed_orig = nullptr;
static aioIob2Bi2xT44_SetTapeLedData_t aioIob2Bi2xT44_SetTapeLedData_orig = nullptr;
static aioIob2Bi2x_OpenSciUsbCdc_t aioIob2Bi2x_OpenSciUsbCdc_orig = nullptr;
static aioIob2Bi2x_CreateWriteFirmContext_t aioIob2Bi2x_CreateWriteFirmContext_orig = nullptr;
static aioIob2Bi2x_DestroyWriteFirmContext_t aioIob2Bi2x_DestroyWriteFirmContext_orig = nullptr;
static aioIob2Bi2x_WriteFirmGetState_t aioIob2Bi2x_WriteFirmGetState_orig = nullptr;
static aioIob2Bi2x_WriteFirmIsCompleted_t aioIob2Bi2x_WriteFirmIsCompleted_orig = nullptr;
static aioIob2Bi2x_WriteFirmIsError_t aioIob2Bi2x_WriteFirmIsError_orig = nullptr;
static aioNMgrIob2_Create_t aioNMgrIob2_Create_orig = nullptr;
static aioSci_Destroy_t aioSci_Destroy_orig = nullptr;
static aioNodeMgr_Destroy_t aioNodeMgr_Destroy_orig = nullptr;
static aioNodeCtl_Destroy_t aioNodeCtl_Destroy_orig = nullptr;
static aioNodeCtl_UpdateDevicesStatus_t aioNodeCtl_UpdateDevicesStatus_orig = nullptr;
static AIO_SCI_COMM *aio_sci_comm = nullptr;
static AIO_NMGR_IOB2 *aio_node_mgr = nullptr;
static AIO_IOB2_BI2X_T44 *aio_t44 = nullptr;
static AIO_IOB2_BI2X_WRFIRM *aio_write_firm = nullptr;
static uint8_t input_counter = 0;
static void __fastcall aioNMgrIob_BeginManage(AIO_NMGR_IOB2 *) {
}
static AIO_NMGR_IOB2_VTABLE aio_node_mgr_vtable {
{},
aioNMgrIob_BeginManage,
};
static AIO_SCI_COMM *__fastcall aioIob2Bi2x_OpenSciUsbCdc(uint32_t) {
aio_sci_comm = new AIO_SCI_COMM {};
return aio_sci_comm;
}
static AIO_NMGR_IOB2 *__fastcall aioNMgrIob2_Create(AIO_SCI_COMM *sci, uint32_t mode) {
if (sci != aio_sci_comm) {
return aioNMgrIob2_Create_orig(sci, mode);
}
aio_node_mgr = new AIO_NMGR_IOB2 {};
aio_node_mgr->vtable = &aio_node_mgr_vtable;
return aio_node_mgr;
}
static AIO_IOB2_BI2X_T44 *__fastcall aioIob2Bi2xT44_Create(
AIO_NMGR_IOB2 *node_mgr, uint32_t device_id) {
if (node_mgr != aio_node_mgr) {
return aioIob2Bi2xT44_Create_orig(node_mgr, device_id);
}
log_info("jb::bi2x", "T44 node created");
aio_t44 = new AIO_IOB2_BI2X_T44 {};
return aio_t44;
}
static void __fastcall aioIob2Bi2xT44_GetDeviceStatus(
AIO_IOB2_BI2X_T44 *node, AIO_IOB2_BI2X_T44_DEVSTATUS *status) {
if (node != aio_t44) {
return aioIob2Bi2xT44_GetDeviceStatus_orig(node, status);
}
RI_MGR->devices_flush_output();
std::memset(status, 0, sizeof(*status));
status->data[0x00] = input_counter;
status->data[0x03] = input_counter++;
status->data[0x0A] = static_cast<uint8_t>(eamuse_coin_get_stock());
games::jb::touch_update();
const auto touched = games::jb::touch_state();
auto &buttons = get_buttons();
status->data[0x04] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]) ? 0xFF : 0;
status->data[0x05] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]) ? 0xFF : 0;
status->data[0x06] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]) ? 0xFF : 0;
static constexpr size_t PANEL_ORDER[16] = {
3, 7, 11, 15,
2, 6, 10, 14,
1, 5, 9, 13,
0, 4, 8, 12,
};
for (size_t status_index = 0; status_index < std::size(PANEL_ORDER); status_index++) {
const auto panel_index = PANEL_ORDER[status_index];
if (touched[panel_index] || GameAPI::Buttons::getState(
RI_MGR, buttons[Buttons::Button1 + panel_index])) {
status->data[0x0F + status_index] = 0xFF;
}
}
}
static void __fastcall aioIob2Bi2xT44_IoReset(AIO_IOB2_BI2X_T44 *node, uint32_t reset) {
if (node != aio_t44) {
return aioIob2Bi2xT44_IoReset_orig(node, reset);
}
}
static void __fastcall aioIob2Bi2xT44_SetWatchDogTimer(AIO_IOB2_BI2X_T44 *node, uint8_t count) {
if (node != aio_t44) {
return aioIob2Bi2xT44_SetWatchDogTimer_orig(node, count);
}
}
static void __fastcall aioIob2Bi2xT44_ControlCoinBlocker(
AIO_IOB2_BI2X_T44 *node, uint32_t slot, bool open) {
if (node != aio_t44) {
return aioIob2Bi2xT44_ControlCoinBlocker_orig(node, slot, open);
}
eamuse_coin_set_block(!open);
}
static void __fastcall aioIob2Bi2xT44_AddCounter(
AIO_IOB2_BI2X_T44 *node, uint32_t counter, uint32_t count) {
if (node != aio_t44) {
return aioIob2Bi2xT44_AddCounter_orig(node, counter, count);
}
}
static void __fastcall aioIob2Bi2xT44_SetIccrLed(AIO_IOB2_BI2X_T44 *node, uint32_t color) {
if (node != aio_t44) {
return aioIob2Bi2xT44_SetIccrLed_orig(node, color);
}
}
static void __fastcall aioIob2Bi2xT44_SetTapeLedData(
AIO_IOB2_BI2X_T44 *node, uint32_t tape, const void *data) {
if (node != aio_t44) {
return aioIob2Bi2xT44_SetTapeLedData_orig(node, tape, data);
}
}
static AIO_IOB2_BI2X_WRFIRM *__fastcall aioIob2Bi2x_CreateWriteFirmContext(uint32_t, uint32_t) {
aio_write_firm = new AIO_IOB2_BI2X_WRFIRM {};
return aio_write_firm;
}
static void __fastcall aioIob2Bi2x_DestroyWriteFirmContext(AIO_IOB2_BI2X_WRFIRM *context) {
if (context != aio_write_firm) {
return aioIob2Bi2x_DestroyWriteFirmContext_orig(context);
}
delete aio_write_firm;
aio_write_firm = nullptr;
}
static int32_t __fastcall aioIob2Bi2x_WriteFirmGetState(AIO_IOB2_BI2X_WRFIRM *context) {
if (context != aio_write_firm) {
return aioIob2Bi2x_WriteFirmGetState_orig(context);
}
return 8;
}
static bool __fastcall aioIob2Bi2x_WriteFirmIsCompleted(int32_t state) {
if (aio_write_firm) {
return true;
}
return aioIob2Bi2x_WriteFirmIsCompleted_orig(state);
}
static bool __fastcall aioIob2Bi2x_WriteFirmIsError(int32_t state) {
if (aio_write_firm) {
return false;
}
return aioIob2Bi2x_WriteFirmIsError_orig(state);
}
static void __fastcall aioSci_Destroy(AIO_SCI_COMM *sci) {
if (sci != aio_sci_comm) {
return aioSci_Destroy_orig(sci);
}
delete aio_sci_comm;
aio_sci_comm = nullptr;
}
static void __fastcall aioNodeMgr_Destroy(AIO_NMGR_IOB2 *node_mgr) {
if (node_mgr != aio_node_mgr) {
return aioNodeMgr_Destroy_orig(node_mgr);
}
delete aio_node_mgr;
aio_node_mgr = nullptr;
}
static void __fastcall aioNodeCtl_Destroy(AIO_IOB2_BI2X_T44 *node) {
if (node != aio_t44) {
return aioNodeCtl_Destroy_orig(node);
}
delete aio_t44;
aio_t44 = nullptr;
}
static void __fastcall aioNodeCtl_UpdateDevicesStatus() {
}
void bi2x_hook_init() {
static bool initialized = false;
if (initialized) {
return;
}
initialized = true;
log_info("jb::bi2x", "initializing T44 hooks");
const auto libaio_iob2_video = "libaio-iob2_video.dll";
detour::trampoline_try(libaio_iob2_video, "aioIob2Bi2xT44_Create",
aioIob2Bi2xT44_Create, &aioIob2Bi2xT44_Create_orig);
detour::trampoline_try(libaio_iob2_video,
"?GetDeviceStatus@AIO_IOB2_BI2X_T44@@QEBAXAEAUDEVSTATUS@1@@Z",
aioIob2Bi2xT44_GetDeviceStatus, &aioIob2Bi2xT44_GetDeviceStatus_orig);
detour::trampoline_try(libaio_iob2_video, "?IoReset@AIO_IOB2_BI2X_T44@@QEAAXI@Z",
aioIob2Bi2xT44_IoReset, &aioIob2Bi2xT44_IoReset_orig);
detour::trampoline_try(libaio_iob2_video,
"?SetWatchDogTimer@AIO_IOB2_BI2X_T44@@QEAAXE@Z",
aioIob2Bi2xT44_SetWatchDogTimer, &aioIob2Bi2xT44_SetWatchDogTimer_orig);
detour::trampoline_try(libaio_iob2_video,
"?ControlCoinBlocker@AIO_IOB2_BI2X_T44@@QEAAXI_N@Z",
aioIob2Bi2xT44_ControlCoinBlocker, &aioIob2Bi2xT44_ControlCoinBlocker_orig);
detour::trampoline_try(libaio_iob2_video, "?AddCounter@AIO_IOB2_BI2X_T44@@QEAAXII@Z",
aioIob2Bi2xT44_AddCounter, &aioIob2Bi2xT44_AddCounter_orig);
detour::trampoline_try(libaio_iob2_video, "?SetIccrLed@AIO_IOB2_BI2X_T44@@QEAAXI@Z",
aioIob2Bi2xT44_SetIccrLed, &aioIob2Bi2xT44_SetIccrLed_orig);
detour::trampoline_try(libaio_iob2_video,
"?SetTapeLedData@AIO_IOB2_BI2X_T44@@QEAAXIPEBX@Z",
aioIob2Bi2xT44_SetTapeLedData, &aioIob2Bi2xT44_SetTapeLedData_orig);
const auto libaio_iob = "libaio-iob.dll";
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_OpenSciUsbCdc",
aioIob2Bi2x_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_CreateWriteFirmContext",
aioIob2Bi2x_CreateWriteFirmContext, &aioIob2Bi2x_CreateWriteFirmContext_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_DestroyWriteFirmContext",
aioIob2Bi2x_DestroyWriteFirmContext, &aioIob2Bi2x_DestroyWriteFirmContext_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmGetState",
aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsCompleted",
aioIob2Bi2x_WriteFirmIsCompleted, &aioIob2Bi2x_WriteFirmIsCompleted_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsError",
aioIob2Bi2x_WriteFirmIsError, &aioIob2Bi2x_WriteFirmIsError_orig);
detour::trampoline_try(libaio_iob, "aioNMgrIob2_Create",
aioNMgrIob2_Create, &aioNMgrIob2_Create_orig);
const auto libaio = "libaio.dll";
detour::trampoline_try(libaio, "aioSci_Destroy", aioSci_Destroy, &aioSci_Destroy_orig);
detour::trampoline_try(libaio, "aioNodeMgr_Destroy",
aioNodeMgr_Destroy, &aioNodeMgr_Destroy_orig);
detour::trampoline_try(libaio, "aioNodeCtl_Destroy",
aioNodeCtl_Destroy, &aioNodeCtl_Destroy_orig);
detour::trampoline_try(libaio, "aioNodeCtl_UpdateDevicesStatus",
aioNodeCtl_UpdateDevicesStatus, &aioNodeCtl_UpdateDevicesStatus_orig);
}
}
#endif
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#if SPICE64
namespace games::jb {
void bi2x_hook_init();
}
#endif
+18
View File
@@ -3,6 +3,8 @@
#include <windows.h> #include <windows.h>
#include <filesystem> #include <filesystem>
#include "avs/game.h"
#include "bi2x_hook.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/detour.h" #include "util/detour.h"
@@ -10,6 +12,8 @@
namespace games::jb { namespace games::jb {
#if !SPICE64
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN) // fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
static BOOL __stdcall network_addr_is_changed() { static BOOL __stdcall network_addr_is_changed() {
return 0; return 0;
@@ -31,6 +35,8 @@ namespace games::jb {
return 0; return 0;
} }
#endif
JBGame::JBGame() : Game("Jubeat") { JBGame::JBGame() : Game("Jubeat") {
} }
@@ -60,8 +66,18 @@ namespace games::jb {
void JBGame::attach() { void JBGame::attach() {
Game::attach(); Game::attach();
#if SPICE64
if (avs::game::DLL_NAME == "jubeat2019.dll") {
libutils::load_library("libaio.dll");
libutils::load_library("libaio-iob.dll");
libutils::load_library("libaio-iob2_video.dll");
bi2x_hook_init();
}
#endif
touch_attach(); touch_attach();
#if !SPICE64
// enable debug logging of gftools // enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll"); HMODULE gftools = libutils::try_module("gftools.dll");
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc( detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
@@ -75,6 +91,8 @@ namespace games::jb {
network, "network_get_network_check_info")); network, "network_get_network_check_info"));
detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc( detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc(
network, "network_get_dhcp_result")); network, "network_get_dhcp_result"));
#endif
} }
void JBGame::detach() { void JBGame::detach() {
+40 -26
View File
@@ -20,6 +20,8 @@
// (6, 8) in landscape. // (6, 8) in landscape.
#define JB_BUTTON_SIZE 160 #define JB_BUTTON_SIZE 160
#define JB_MAX_BUTTON_GAP 38 #define JB_MAX_BUTTON_GAP 38
#define JB_T44_BUTTON_SIZE 224
#define JB_T44_BUTTON_GAP 33
// improved and plus modes use this reach around each button. must be >= the // improved and plus modes use this reach around each button. must be >= the
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button. // diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
@@ -41,6 +43,7 @@ namespace games::jb {
static std::atomic_bool TOUCH_ENABLE = false; static std::atomic_bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false; static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true; static bool IS_PORTRAIT = true;
static bool IS_T44 = false;
static std::atomic_uint16_t TOUCH_STATE = 0; static std::atomic_uint16_t TOUCH_STATE = 0;
// fixed-size contact view used by the debug overlay // fixed-size contact view used by the debug overlay
@@ -85,35 +88,45 @@ namespace games::jb {
// --- touch geometry ------------------------------------------------------ // --- touch geometry ------------------------------------------------------
// gaps between the four buttons along one axis (the middle gap is 1px wider) // gaps between the four buttons along one axis (the middle gap is 1px wider)
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 }; static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
static const int JB_T44_BUTTON_GAPS[3] = {
JB_T44_BUTTON_GAP,
JB_T44_BUTTON_GAP,
JB_T44_BUTTON_GAP,
};
struct AxisGeometry { struct AxisGeometry {
int size;
int button[4]; // left/top edge of each button int button[4]; // left/top edge of each button
}; };
// left/top edges of the four buttons along one axis, starting at `first` // left/top edges of the four buttons along one axis, starting at `first`
static AxisGeometry axis_geometry(int first) { static AxisGeometry axis_geometry(int first, int size, const int gaps[3]) {
AxisGeometry g {}; AxisGeometry g {};
g.size = size;
g.button[0] = first; g.button[0] = first;
for (int i = 1; i < 4; i++) { for (int i = 1; i < 4; i++) {
g.button[i] = g.button[i - 1] + JB_BUTTON_SIZE + JB_BUTTON_GAPS[i - 1]; g.button[i] = g.button[i - 1] + size + gaps[i - 1];
} }
return g; return g;
} }
// button edges for the current orientation // button edges for the current orientation
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) { static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
if (IS_PORTRAIT) { if (IS_T44) {
gx = axis_geometry(8); gx = axis_geometry(37, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS);
gy = axis_geometry(602); gy = axis_geometry(864, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS);
} else if (IS_PORTRAIT) {
gx = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
gy = axis_geometry(602, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
} else { } else {
gx = axis_geometry(6); gx = axis_geometry(6, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
gy = axis_geometry(8); gy = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
} }
} }
// distance from `p` to a button along one axis (0 when inside) // distance from `p` to a button along one axis (0 when inside)
static int axis_distance(int p, int button) { static int axis_distance(int p, int button, int size) {
int end = button + JB_BUTTON_SIZE - 1; int end = button + size - 1;
if (p < button) { if (p < button) {
return button - p; return button - p;
} }
@@ -131,8 +144,8 @@ namespace games::jb {
int best_dist = 0; int best_dist = 0;
for (int r = 0; r < 4; r++) { for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) { for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c]); int dx = axis_distance(px, gx.button[c], gx.size);
int dy = axis_distance(py, gy.button[r]); int dy = axis_distance(py, gy.button[r], gy.size);
int dist = dx * dx + dy * dy; int dist = dx * dx + dy * dy;
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) { if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
best_dist = dist; best_dist = dist;
@@ -145,7 +158,8 @@ namespace games::jb {
// detection reach for the current algorithm (0 = register only inside a button) // detection reach for the current algorithm (0 = register only inside a button)
static int touch_radius() { static int touch_radius() {
return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_TOUCH_RADIUS; return TOUCH_ALGORITHM == AcAccurate || (IS_T44 && TOUCH_ALGORITHM == Legacy) ?
0 : JB_TOUCH_RADIUS;
} }
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or // mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
@@ -162,8 +176,8 @@ namespace games::jb {
} }
for (int r = 0; r < 4; r++) { for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) { for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c]); int dx = axis_distance(px, gx.button[c], gx.size);
int dy = axis_distance(py, gy.button[r]); int dy = axis_distance(py, gy.button[r], gy.size);
if (dx * dx + dy * dy <= radius * radius) { if (dx * dx + dy * dy <= radius * radius) {
state |= uint16_t(1) << (r * 4 + c); state |= uint16_t(1) << (r * 4 + c);
} }
@@ -184,11 +198,15 @@ namespace games::jb {
// one-time touch window attach // one-time touch window attach
if (!TOUCH_ATTACHED) { if (!TOUCH_ATTACHED) {
IS_T44 = avs::game::is_model("T44");
IS_PORTRAIT = IS_T44 || avs::game::is_model("L44");
// find the game window: prefer the foreground window, else search by // find the game window: prefer the foreground window, else search by
// title (the model name prefixes the window title in every version) // title (T44 uses a fixed title instead of the model prefix)
const char *window_title = IS_T44 ? "jubeat 10 main" : avs::game::MODEL;
HWND wnd = GetForegroundWindow(); HWND wnd = GetForegroundWindow();
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) { if (!string_begins_with(GetActiveWindowTitle(), window_title)) {
wnd = FindWindowBeginsWith(avs::game::MODEL); wnd = FindWindowBeginsWith(window_title);
} }
if (!wnd) { if (!wnd) {
log_warning("jubeat", "could not find window handle for touch"); log_warning("jubeat", "could not find window handle for touch");
@@ -197,10 +215,6 @@ namespace games::jb {
return; return;
} }
// only the L44 model runs in portrait; set this before starting the
// touch-window thread so the renderer only observes the final value
IS_PORTRAIT = avs::game::is_model("L44");
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd)); log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
// let the rawinput stack correct the aspect ratio // let the rawinput stack correct the aspect ratio
@@ -232,7 +246,7 @@ namespace games::jb {
return !touch_matured(tp, now_ms, threshold_ms); return !touch_matured(tp, now_ms, threshold_ms);
}); });
if (TOUCH_ALGORITHM == Legacy) { if (TOUCH_ALGORITHM == Legacy && !IS_T44) {
// legacy: evenly divide the play area into a 4x4 grid // legacy: evenly divide the play area into a 4x4 grid
auto offset = IS_PORTRAIT ? 580 : 0; auto offset = IS_PORTRAIT ? 580 : 0;
@@ -311,7 +325,7 @@ namespace games::jb {
for (int c = 0; c < 4; c++) { for (int c = 0; c < 4; c++) {
cells[r * 4 + c] = { cells[r * 4 + c] = {
gx.button[c], gy.button[r], gx.button[c], gy.button[r],
gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE gx.button[c] + gx.size, gy.button[r] + gy.size
}; };
} }
} }
@@ -352,8 +366,8 @@ namespace games::jb {
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) { const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
int c = index % 4; int c = index % 4;
int r = index / 4; int r = index / 4;
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py, double mid = std::atan2((gy.button[r] + gy.size / 2) - py,
(gx.button[c] + JB_BUTTON_SIZE / 2) - px); (gx.button[c] + gx.size / 2) - px);
const double quarter = 3.14159265358979323846 / 2.0; const double quarter = 3.14159265358979323846 / 2.0;
const int segments = 16; const int segments = 16;
POINT arc[segments + 1]; POINT arc[segments + 1];
@@ -425,7 +439,7 @@ namespace games::jb {
} }
// legacy divides the field evenly; the other algorithms use button squares // legacy divides the field evenly; the other algorithms use button squares
bool legacy = (TOUCH_ALGORITHM == Legacy); bool legacy = TOUCH_ALGORITHM == Legacy && !IS_T44;
AxisGeometry gx {}, gy {}; AxisGeometry gx {}, gy {};
if (!legacy) { if (!legacy) {
touch_geometry(gx, gy); touch_geometry(gx, gy);
@@ -113,7 +113,10 @@ static Direct3DCreate9On12Ex_t Direct3DCreate9On12Ex_orig = nullptr;
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false; static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr; 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) { static std::string behavior2s(DWORD behavior_flags) {
FLAGS_START(behavior_flags); FLAGS_START(behavior_flags);
@@ -409,7 +412,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::QueryInterface(
return E_POINTER; return E_POINTER;
} }
if (riid == IID_WrappedIDirect3D9 || if ((riid == IID_IUnknown && gfdm_two_head_exclusive()) ||
riid == IID_WrappedIDirect3D9 ||
riid == IID_IDirect3D9 || riid == IID_IDirect3D9 ||
riid == IID_IDirect3D9Ex) riid == IID_IDirect3D9Ex)
{ {
@@ -467,6 +471,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() { UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
UINT result = pReal->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 (!FAKE_SUBSCREEN_ADAPTER) {
if (games::popn::is_pikapika_model() && result == 1) { if (games::popn::is_pikapika_model() && result == 1) {
FAKE_SUBSCREEN_ADAPTER = true; FAKE_SUBSCREEN_ADAPTER = true;
@@ -493,7 +509,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
D3DADAPTER_IDENTIFIER9 *pIdentifier) D3DADAPTER_IDENTIFIER9 *pIdentifier)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) { if (is_fake_subscreen_adapter(Adapter) && pIdentifier) {
*pIdentifier = {}; *pIdentifier = {};
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter); const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
strcpy(pIdentifier->DeviceName, adapter_name.c_str()); strcpy(pIdentifier->DeviceName, adapter_name.c_str());
@@ -509,7 +525,7 @@ 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 > 0) { if (is_fake_subscreen_adapter(Adapter)) {
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter); log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
return 1; return 1;
} }
@@ -523,13 +539,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
UINT Mode, UINT Mode,
D3DDISPLAYMODE *pMode) 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); log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
if (Mode == 0 && pMode) { if (Mode == 0 && pMode) {
pMode->Width = 1280; get_fake_subscreen_display_mode(Adapter, pMode);
pMode->Height = 800;
pMode->RefreshRate = 60;
pMode->Format = D3DFMT_X8R8G8B8;
return S_OK; return S_OK;
} else { } else {
return D3DERR_INVALIDCALL; return D3DERR_INVALIDCALL;
@@ -611,6 +624,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
} }
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE *pMode) { 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)); CHECK_RESULT(pReal->GetAdapterDisplayMode(Adapter, pMode));
} }
@@ -621,6 +642,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceType(
D3DFORMAT BackBufferFormat, D3DFORMAT BackBufferFormat,
BOOL bWindowed) BOOL bWindowed)
{ {
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(iAdapter)) {
return D3D_OK;
}
CHECK_RESULT(pReal->CheckDeviceType(iAdapter, DevType, DisplayFormat, BackBufferFormat, bWindowed)); CHECK_RESULT(pReal->CheckDeviceType(iAdapter, DevType, DisplayFormat, BackBufferFormat, bWindowed));
} }
@@ -632,7 +657,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
D3DRESOURCETYPE RType, D3DRESOURCETYPE RType,
D3DFORMAT CheckFormat) 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); log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
return D3D_OK; return D3D_OK;
} }
@@ -648,6 +673,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceMultiSampleType(
D3DMULTISAMPLE_TYPE MultiSampleType, D3DMULTISAMPLE_TYPE MultiSampleType,
DWORD *pQualityLevels) DWORD *pQualityLevels)
{ {
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
if (pQualityLevels != nullptr) {
*pQualityLevels = 1;
}
return D3D_OK;
}
CHECK_RESULT(pReal->CheckDeviceMultiSampleType( CHECK_RESULT(pReal->CheckDeviceMultiSampleType(
Adapter, Adapter,
DeviceType, DeviceType,
@@ -664,6 +696,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDepthStencilMatch(
D3DFORMAT RenderTargetFormat, D3DFORMAT RenderTargetFormat,
D3DFORMAT DepthStencilFormat) D3DFORMAT DepthStencilFormat)
{ {
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
return D3D_OK;
}
CHECK_RESULT(pReal->CheckDepthStencilMatch( CHECK_RESULT(pReal->CheckDepthStencilMatch(
Adapter, Adapter,
DeviceType, DeviceType,
@@ -678,6 +714,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormatConversion(
D3DFORMAT SourceFormat, D3DFORMAT SourceFormat,
D3DFORMAT TargetFormat) D3DFORMAT TargetFormat)
{ {
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
return D3D_OK;
}
CHECK_RESULT(pReal->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat)); CHECK_RESULT(pReal->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat));
} }
@@ -689,6 +729,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
return D3DERR_INVALIDCALL; 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: // 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 // "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. // 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) { HMONITOR STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterMonitor(UINT Adapter) {
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
return pReal->GetAdapterMonitor(0);
}
return pReal->GetAdapterMonitor(Adapter); return pReal->GetAdapterMonitor(Adapter);
} }
@@ -861,7 +919,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
"either use -graphics-force-refresh option or change the desktop resolution beforehand."); "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)", log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
pPresentationParameters[1].FullScreen_RefreshRateInHz, pPresentationParameters[1].FullScreen_RefreshRateInHz,
GRAPHICS_FORCE_REFRESH_SUB.value()); GRAPHICS_FORCE_REFRESH_SUB.value());
@@ -902,7 +961,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
} else if (!D3D9_DEVICE_HOOK_DISABLE) { } else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters); graphics_hook_window(hFocusWindow, pPresentationParameters);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface); *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
hFocusWindow,
*ppReturnedDeviceInterface);
} }
// return result // return result
@@ -916,6 +977,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCountEx(UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter) { UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCountEx(UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter) {
assert(is_d3d9ex); assert(is_d3d9ex);
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
return 1;
}
return static_cast<IDirect3D9Ex *>(pReal)->GetAdapterModeCountEx(Adapter, pFilter); return static_cast<IDirect3D9Ex *>(pReal)->GetAdapterModeCountEx(Adapter, pFilter);
} }
@@ -926,6 +991,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModesEx(
D3DDISPLAYMODEEX *pMode) D3DDISPLAYMODEEX *pMode)
{ {
assert(is_d3d9ex); 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<IDirect3D9Ex *>(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode)); CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode));
} }
@@ -935,6 +1008,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayModeEx(
D3DDISPLAYROTATION *pRotation) D3DDISPLAYROTATION *pRotation)
{ {
assert(is_d3d9ex); 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<IDirect3D9Ex *>(pReal)->GetAdapterDisplayModeEx(Adapter, pMode, pRotation)); CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterDisplayModeEx(Adapter, pMode, pRotation));
} }
@@ -963,6 +1047,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
return D3DERR_INVALIDCALL; 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; DWORD orig_behavior_flags = BehaviorFlags;
size_t num_adapters = 1; size_t num_adapters = 1;
@@ -989,6 +1081,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) { if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) {
num_adapters = device_caps.NumberOfAdaptersInGroup; 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++) { 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)", log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
pPresentationParameters[1].FullScreen_RefreshRateInHz, pPresentationParameters[1].FullScreen_RefreshRateInHz,
GRAPHICS_FORCE_REFRESH_SUB.value()); GRAPHICS_FORCE_REFRESH_SUB.value());
@@ -1131,16 +1227,93 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
pPresentationParameters->BackBufferCount = GRAPHICS_FORCE_VSYNC_BUFFER.value(); 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<IDirect3D9Ex *>(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 // call original
HRESULT result = static_cast<IDirect3D9Ex *>(this->pReal)->CreateDeviceEx( HRESULT result = static_cast<IDirect3D9Ex *>(this->pReal)->CreateDeviceEx(
Adapter, Adapter,
DeviceType, DeviceType,
hFocusWindow, hFocusWindow,
BehaviorFlags, BehaviorFlags,
pPresentationParameters, gfdm_parameters.presentation_parameters,
pFullscreenDisplayMode, gfdm_parameters.fullscreen_display_modes,
ppReturnedDeviceInterface); 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 // check for error
if (result != D3D_OK) { if (result != D3D_OK) {
@@ -1151,17 +1324,28 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
} else if (!D3D9_DEVICE_HOOK_DISABLE) { } else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters); 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<IDirect3D9 *>(this) : nullptr,
gfdm_two_head_exclusive() ? pPresentationParameters : nullptr);
// initialize sub screen if the game requested a multi-head context // initialize sub screen if the game requested a multi-head context
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) && if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) { (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
UINT i = 1; UINT i = games::gitadora::is_arena_model()
if (games::gitadora::is_arena_model()) { ? gfdm_parameters.logical_small_swapchain
i = 2; : 1;
} D3DPRESENT_PARAMETERS *subscreen_parameters =
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]); 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) { HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *pLUID) {
assert(is_d3d9ex); assert(is_d3d9ex);
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
if (pLUID == nullptr) {
return D3DERR_INVALIDCALL;
}
pLUID->LowPart = static_cast<DWORD>(-static_cast<LONG>(Adapter));
pLUID->HighPart = -static_cast<LONG>(Adapter);
return D3D_OK;
}
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterLUID(Adapter, pLUID)); CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterLUID(Adapter, pLUID));
} }
@@ -1178,7 +1370,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *
// The sub screen swap chain should be created if: // The sub screen swap chain should be created if:
// - Running windowed with `NumberOfAdaptersInGroup >= 2` (game expects implicit swap chain to exist) // - Running windowed with `NumberOfAdaptersInGroup >= 2` (game expects implicit swap chain to exist)
// - Running fullscreen with `NumberOfAdaptersInGroup < 2` (overridden `GetDeviceCaps` structure) // - 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 {}; D3DCAPS9 caps {};
HRESULT hr = device->GetDeviceCaps(&caps); HRESULT hr = device->GetDeviceCaps(&caps);
if (FAILED(hr)) { if (FAILED(hr)) {
@@ -1212,10 +1408,9 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE
} }
} else { } else {
int swapchain = 1; const int swapchain = games::gitadora::is_arena_model()
if (games::gitadora::is_arena_model()) { ? static_cast<int>(gfdm_small_swapchain)
swapchain = 2; : 1;
}
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN); hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
if (FAILED(hr)) { if (FAILED(hr)) {
@@ -2,6 +2,8 @@
#include <d3d9.h> #include <d3d9.h>
#include "d3d9_gfdm.h"
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394} // {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
static const GUID IID_WrappedIDirect3D9 = { static const GUID IID_WrappedIDirect3D9 = {
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 } 0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
@@ -41,7 +41,6 @@
constexpr bool CUSTOM_RESET = false; constexpr bool CUSTOM_RESET = false;
constexpr D3DFORMAT D3DFMT_DF24 = static_cast<D3DFORMAT>(MAKEFOURCC('D', 'F', '2', '4')); constexpr D3DFORMAT D3DFMT_DF24 = static_cast<D3DFORMAT>(MAKEFOURCC('D', 'F', '2', '4'));
auto format_as(D3DPOOL f) { auto format_as(D3DPOOL f) {
return fmt::underlying(f); return fmt::underlying(f);
} }
@@ -85,7 +84,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::QueryInterface(
return E_POINTER; return E_POINTER;
} }
if (riid == IID_WrappedIDirect3DDevice9 || if ((riid == IID_IUnknown && is_gfdm_two_head_exclusive()) ||
riid == IID_WrappedIDirect3DDevice9 ||
riid == IID_IDirect3DDevice9 || riid == IID_IDirect3DDevice9 ||
riid == IID_IDirect3DDevice9Ex) riid == IID_IDirect3DDevice9Ex)
{ {
@@ -202,6 +202,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDirect3D(
IDirect3D9 **ppD3D9) IDirect3D9 **ppD3D9)
{ {
WRAP_VERBOSE; 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)); CHECK_RESULT(pReal->GetDirect3D(ppD3D9));
} }
@@ -209,7 +218,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDeviceCaps(
D3DCAPS9 *pCaps) D3DCAPS9 *pCaps)
{ {
WRAP_VERBOSE; 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( HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
@@ -217,6 +232,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
D3DDISPLAYMODE *pMode) D3DDISPLAYMODE *pMode)
{ {
WRAP_VERBOSE; 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)); CHECK_RESULT(pReal->GetDisplayMode(iSwapChain, pMode));
} }
@@ -258,6 +284,51 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
{ {
WRAP_VERBOSE; 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<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
return D3D_OK;
}
int index = 0; int index = 0;
bool create_swap_chain = false; bool create_swap_chain = false;
bool create_fake_swap_chain = false; bool create_fake_swap_chain = false;
@@ -271,6 +342,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
// SMALL (subscreen) // SMALL (subscreen)
create_swap_chain = true; create_swap_chain = true;
index = 0; index = 0;
} else if (pPresentationParameters->BackBufferWidth == 1080) { } else if (pPresentationParameters->BackBufferWidth == 1080) {
// LEFT/RIGHT // LEFT/RIGHT
create_swap_chain = true; create_swap_chain = true;
@@ -335,6 +407,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
{ {
WRAP_VERBOSE_FMT("GetSwapChain({})", iSwapChain); WRAP_VERBOSE_FMT("GetSwapChain({})", iSwapChain);
if (ppSwapChain == nullptr) {
return D3DERR_INVALIDCALL;
}
if (iSwapChain == 0) { if (iSwapChain == 0) {
if (!main_swapchain) { if (!main_swapchain) {
HRESULT ret = pReal->GetSwapChain(iSwapChain, ppSwapChain); HRESULT ret = pReal->GetSwapChain(iSwapChain, ppSwapChain);
@@ -344,7 +420,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
return ret; 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(); main_swapchain->AddRef();
@@ -354,6 +435,39 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
return D3D_OK; 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<IDirect3DSwapChain9 *>(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<IDirect3DSwapChain9 *>(swapchain);
graphics_screens_register(iSwapChain);
return D3D_OK;
}
return D3DERR_INVALIDCALL;
}
bool swap = false; bool swap = false;
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) { if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
swap = true; swap = true;
@@ -390,6 +504,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() { UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
WRAP_VERBOSE; WRAP_VERBOSE;
if (is_gfdm_two_head_exclusive()) {
return GFDM_LOGICAL_HEAD_COUNT;
}
UINT n = pReal->GetNumberOfSwapChains(); UINT n = pReal->GetNumberOfSwapChains();
if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) { if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
@@ -453,6 +571,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
IDirect3DSurface9 **ppBackBuffer) IDirect3DSurface9 **ppBackBuffer)
{ {
WRAP_VERBOSE; 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)); CHECK_RESULT(pReal->GetBackBuffer(iSwapChain, iBackBuffer, Type, ppBackBuffer));
} }
@@ -461,6 +597,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetRasterStatus(
D3DRASTER_STATUS *pRasterStatus) D3DRASTER_STATUS *pRasterStatus)
{ {
WRAP_DEBUG; 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)); CHECK_RESULT(pReal->GetRasterStatus(iSwapChain, pRasterStatus));
} }
@@ -477,6 +627,18 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetGammaRamp(
const D3DGAMMARAMP *pRamp) const D3DGAMMARAMP *pRamp)
{ {
WRAP_DEBUG; 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); pReal->SetGammaRamp(iSwapChain, Flags, pRamp);
} }
@@ -485,6 +647,21 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGammaRamp(
D3DGAMMARAMP *pRamp) D3DGAMMARAMP *pRamp)
{ {
WRAP_DEBUG; 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); pReal->GetGammaRamp(iSwapChain, pRamp);
} }
@@ -685,6 +862,19 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetFrontBufferData(
IDirect3DSurface9 *pDestSurface) IDirect3DSurface9 *pDestSurface)
{ {
WRAP_DEBUG; 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)); CHECK_RESULT(pReal->GetFrontBufferData(iSwapChain, pDestSurface));
} }
@@ -1049,7 +1239,36 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetViewport(
D3DVIEWPORT9 *pViewport) D3DVIEWPORT9 *pViewport)
{ {
WRAP_DEBUG; WRAP_DEBUG;
CHECK_RESULT(pReal->GetViewport(pViewport)); const auto result = pReal->GetViewport(pViewport);
// SDVX landscape mode
// without this, the game calculates the camera angle incorrectly and
// ends up with zoomed out view of the lanes
const auto landscape_width = GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT;
const auto landscape_height = GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH;
if (SUCCEEDED(result) &&
GRAPHICS_FS_ORIENTATION_SWAP &&
avs::game::is_model("KFC") &&
pViewport != nullptr &&
pViewport->Width == landscape_width &&
pViewport->Height == landscape_height) {
static std::once_flag log_once;
std::call_once(log_once, [pViewport]() {
log_info(
"graphics::d3d9",
"SDVX landscape viewport fix: {}x{} => {}x{}",
pViewport->Width,
pViewport->Height,
pViewport->Height,
pViewport->Width);
});
std::swap(pViewport->Width, pViewport->Height);
}
return result;
} }
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetMaterial( HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetMaterial(
@@ -1779,6 +1998,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::WaitForVBlank(
WRAP_DEBUG; WRAP_DEBUG;
assert(is_d3d9ex); 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<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(
GFDM_NATIVE_SMALL_SWAPCHAIN));
}
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(iSwapChain)); CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(iSwapChain));
} }
@@ -1882,6 +2113,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
log_misc("graphics::d3d9", "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 (GRAPHICS_WINDOWED) {
if (pPresentationParameters) { if (pPresentationParameters) {
pPresentationParameters->Windowed = true; pPresentationParameters->Windowed = true;
@@ -1898,13 +2137,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 // reset overlay
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) { if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
overlay::OVERLAY->reset_invalidate(); overlay::OVERLAY->reset_invalidate();
} }
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx( HRESULT res = static_cast<IDirect3DDevice9Ex *>(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 // recreate overlay
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) { if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) {
@@ -1922,6 +2263,22 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayModeEx(
WRAP_DEBUG; WRAP_DEBUG;
assert(is_d3d9ex); 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<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
GFDM_NATIVE_SMALL_SWAPCHAIN,
pMode,
pRotation));
}
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx( CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
iSwapChain, pMode, pRotation)); iSwapChain, pMode, pRotation));
} }
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include <array>
#include <atomic> #include <atomic>
#include <mutex>
#include <initguid.h> #include <initguid.h>
#include <d3d9.h> #include <d3d9.h>
@@ -40,23 +42,54 @@ static const GUID IID_WrappedIDirect3DDevice9 = {
void SurfaceHook(IDirect3DDevice9 *pReal); void SurfaceHook(IDirect3DDevice9 *pReal);
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig) explicit WrappedIDirect3DDevice9(
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) { 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; IDirect3DDevice9Ex *device = nullptr;
// attempt to upgrade handle // attempt to upgrade handle
if (SUCCEEDED(this->QueryInterface(IID_PPV_ARGS(&device))) && device != nullptr) { if (SUCCEEDED(this->QueryInterface(IID_PPV_ARGS(&device))) && device != nullptr) {
device->Release(); device->Release();
} }
set_gfdm_logical_group_parameters(gfdm_logical_group_parameters);
} }
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9Ex *orig) explicit WrappedIDirect3DDevice9(
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(true) {} 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(const WrappedIDirect3DDevice9 &) = delete;
WrappedIDirect3DDevice9 &operator=(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 #pragma region IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override; 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; virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
#pragma endregion #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; HWND const hFocusWindow;
IDirect3DDevice9 *pReal; IDirect3DDevice9 *pReal;
bool is_d3d9ex = false; bool is_d3d9ex = false;
@@ -211,5 +265,17 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr }; WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr }; FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
UINT gfdm_logical_small_swapchain = 2;
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
bool gfdm_logical_group_parameters_valid = false;
IDirect3D9 *gfdm_parent_d3d = nullptr;
std::mutex gfdm_recovery_mutex;
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
std::array<D3DDISPLAYMODEEX, 2> 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; IDirect3DVertexShader9 *vertex_shader = nullptr;
}; };
@@ -59,7 +59,12 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::Present(const RECT *pSourceRe
log_misc("graphics::d3d9", "FakeIDirect3DSwapChain9::Present"); 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) { HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
WRAP_VERBOSE; WRAP_VERBOSE;
@@ -86,11 +91,32 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffe
} }
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) { HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
WRAP_VERBOSE; 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) { HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayMode(D3DDISPLAYMODE *pMode) {
WRAP_VERBOSE; 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) { HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDevice(IDirect3DDevice9 **ppDevice) {
WRAP_VERBOSE; WRAP_VERBOSE;
@@ -108,17 +134,46 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentParameters(
D3DPRESENT_PARAMETERS *pPresentationParameters) D3DPRESENT_PARAMETERS *pPresentationParameters)
{ {
WRAP_VERBOSE; WRAP_VERBOSE;
return D3DERR_INVALIDCALL; if (!is_hidden) {
return D3DERR_INVALIDCALL;
}
if (pPresentationParameters == nullptr) {
return D3DERR_INVALIDCALL;
}
*pPresentationParameters = present_params;
return D3D_OK;
} }
// IDirect3DSwapChain9Ex // IDirect3DSwapChain9Ex
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetLastPresentCount(UINT *pLastPresentCount) { HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetLastPresentCount(UINT *pLastPresentCount) {
assert(is_d3d9ex); 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) { HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentStats(D3DPRESENTSTATS *pPresentationStatistics) {
assert(is_d3d9ex); 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, HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMODEEX *pMode,
D3DDISPLAYROTATION *pRotation) D3DDISPLAYROTATION *pRotation)
@@ -126,5 +181,22 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMO
WRAP_VERBOSE; WRAP_VERBOSE;
assert(is_d3d9ex); 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;
} }
@@ -6,14 +6,19 @@
#endif #endif
#include <atomic> #include <atomic>
#include <vector>
#include <d3d9.h> #include <d3d9.h>
#include "util/logging.h" #include "util/logging.h"
struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex { struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
FakeIDirect3DSwapChain9(IDirect3DDevice9 *pDev, D3DPRESENT_PARAMETERS *present_params, bool is_d3d9ex) : FakeIDirect3DSwapChain9(
pDev(pDev), is_d3d9ex(is_d3d9ex) 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 // copy presentation parameters
memcpy(&this->present_params, present_params, sizeof(this->present_params)); memcpy(&this->present_params, present_params, sizeof(this->present_params));
@@ -78,8 +83,10 @@ struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
IDirect3DDevice9 *const pDev; IDirect3DDevice9 *const pDev;
bool is_d3d9ex; bool is_d3d9ex;
bool is_hidden;
std::atomic<ULONG> ref_cnt = 1; std::atomic<ULONG> ref_cnt = 1;
std::atomic<UINT> present_count = 0;
D3DPRESENT_PARAMETERS present_params {}; D3DPRESENT_PARAMETERS present_params {};
std::vector<IDirect3DSurface9 *> render_targets; std::vector<IDirect3DSurface9 *> render_targets;
@@ -0,0 +1,715 @@
#include "d3d9_gfdm.h"
#include <algorithm>
#include <cassert>
#include <mutex>
#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 &params = 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 &parameters = 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<UINT>(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,
&current,
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,
&params,
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<std::mutex> 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<std::mutex> 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<D3DPRESENT_PARAMETERS, 2> parameters {};
std::array<D3DDISPLAYMODEEX, 2> modes {};
{
std::lock_guard<std::mutex> 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<IDirect3DDevice9Ex *>(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;
}
@@ -0,0 +1,78 @@
#pragma once
#include <array>
#include <d3d9.h>
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<D3DPRESENT_PARAMETERS, 2> native_presentation_parameters {};
std::array<D3DDISPLAYMODEEX, 2> native_fullscreen_display_modes {};
std::array<D3DPRESENT_PARAMETERS, 2> recovery_parameters {};
std::array<D3DDISPLAYMODEEX, 2> 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);
@@ -26,7 +26,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid
return E_POINTER; 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_IDirect3DSwapChain9 ||
riid == IID_IDirect3DSwapChain9Ex) riid == IID_IDirect3DSwapChain9Ex)
{ {
@@ -89,7 +89,42 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev); 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) { HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
CHECK_RESULT(pReal->GetFrontBufferData(pDestSurface)); CHECK_RESULT(pReal->GetFrontBufferData(pDestSurface));
@@ -5,8 +5,13 @@
interface WrappedIDirect3DDevice9; interface WrappedIDirect3DDevice9;
struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex { struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9 *orig) : enum class NativeGroupHead { Unknown, Main, Small };
pDev(dev), pReal(orig), is_d3d9ex(false)
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; IDirect3DSwapChain9Ex *swapchain = nullptr;
@@ -16,8 +21,11 @@ struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
} }
} }
WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9Ex *orig) : WrappedIDirect3DSwapChain9(
pDev(dev), pReal(orig), is_d3d9ex(true) 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; IDirect3DSwapChain9 *pReal;
bool is_d3d9ex = false; bool is_d3d9ex = false;
NativeGroupHead native_group_head = NativeGroupHead::Unknown;
bool should_run_hooks = true; bool should_run_hooks = true;
}; };
+135 -2
View File
@@ -17,6 +17,7 @@
#include "games/ddr/ddr.h" #include "games/ddr/ddr.h"
#include "games/gitadora/gitadora.h" #include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/sdvx/sdvx.h"
#include "games/popn/popn.h" #include "games/popn/popn.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h" #include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "hooks/graphics/backends/d3d11/d3d11_backend.h" #include "hooks/graphics/backends/d3d11/d3d11_backend.h"
@@ -44,6 +45,8 @@ HWND SDVX_SUBSCREEN_WINDOW = nullptr;
HWND GFDM_SUBSCREEN_WINDOW = nullptr; HWND GFDM_SUBSCREEN_WINDOW = nullptr;
static HWND GFDM_LEFT_WINDOW = nullptr; static HWND GFDM_LEFT_WINDOW = nullptr;
static HWND GFDM_RIGHT_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; HWND POPN_SUBSCREEN_WINDOW = nullptr;
bool FAKE_SUBSCREEN_ADAPTER = false; bool FAKE_SUBSCREEN_ADAPTER = false;
@@ -183,6 +186,13 @@ static bool is_gfdm_known_window(HWND hWnd) {
return gitadora_window_name_for_hwnd(hWnd) != nullptr; 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) { static bool gitadora_should_block_game_window_placement(HWND hWnd) {
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) { if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
return false; return false;
@@ -204,6 +214,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<DWORD>(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<LONG>(fullscreen_style));
}
const RECT &monitor_rect = monitor_info.rcMonitor;
const int host_width = desired_width != 0
? static_cast<int>(desired_width)
: monitor_rect.right - monitor_rect.left;
const int host_height = desired_height != 0
? static_cast<int>(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() { static bool gitadora_should_allow_small_resize() {
return GRAPHICS_WINDOWED && return GRAPHICS_WINDOWED &&
games::gitadora::is_arena_model() && games::gitadora::is_arena_model() &&
@@ -618,9 +717,16 @@ 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 && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
gitadora_remember_window(result, gfdm_window_name); 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) { if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_force_window_style(result); gitadora_force_window_style(result);
@@ -767,6 +873,10 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
nHeight, nHeight,
bRepaint); bRepaint);
if (is_gfdm_two_head_small_window(hWnd)) {
return TRUE;
}
// sound voltex windowed mode adjustments // sound voltex windowed mode adjustments
if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) { if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) {
RECT rect {}; RECT rect {};
@@ -874,6 +984,11 @@ static HCURSOR WINAPI SetCursor_hook(HCURSOR hCursor) {
static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) { 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 // DDR window style fix
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) { if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
dwNewLong |= WS_OVERLAPPEDWINDOW; dwNewLong |= WS_OVERLAPPEDWINDOW;
@@ -894,6 +1009,11 @@ static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
static LONG WINAPI SetWindowLongW_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 // DDR overlapped window fix
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) { if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
dwNewLong |= WS_OVERLAPPEDWINDOW; dwNewLong |= WS_OVERLAPPEDWINDOW;
@@ -915,6 +1035,12 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter, static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
int X, int Y, int cx, int cy, UINT uFlags) { 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 // windowed mode adjustments
if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) { if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) {
return TRUE; return TRUE;
@@ -939,6 +1065,13 @@ 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 (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() && if (games::gitadora::is_arena_model() &&
GRAPHICS_PREVENT_SECONDARY_WINDOWS && GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
hWnd != GRAPHICS_HOOKED_WINDOW) { hWnd != GRAPHICS_HOOKED_WINDOW) {
+9
View File
@@ -119,6 +119,15 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
void graphics_init(); void graphics_init();
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters); 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_add_wnd_proc(WNDPROC wndProc);
void graphics_remove_wnd_proc(WNDPROC wndProc); void graphics_remove_wnd_proc(WNDPROC wndProc);
void graphics_hook_subscreen_window(HWND hWnd); void graphics_hook_subscreen_window(HWND hWnd);
+4 -3
View File
@@ -296,8 +296,9 @@ void hooks::lang::early_init() {
&GetLocaleInfoA_orig); &GetLocaleInfoA_orig);
} }
// for TDJ subscreen search keyboard // for TDJ subscreen search keyboard and T44 narrow-string handling
if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) { if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
avs::game::is_model("T44")) {
log_info("hooks::lang", "hooking IsDBCSLeadByte"); log_info("hooks::lang", "hooking IsDBCSLeadByte");
detour::trampoline_try( detour::trampoline_try(
"kernel32.dll", "kernel32.dll",
@@ -306,7 +307,7 @@ void hooks::lang::early_init() {
&IsDBCSLeadByte_orig); &IsDBCSLeadByte_orig);
} }
if (games::gitadora::is_arena_model()) { if (games::gitadora::is_arena_model() || avs::game::is_model("T44")) {
log_info("hooks::lang", "hooking WideCharToMultiByte"); log_info("hooks::lang", "hooking WideCharToMultiByte");
detour::trampoline_try( detour::trampoline_try(
"kernel32.dll", "kernel32.dll",
+17 -12
View File
@@ -644,6 +644,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::LoadGitaDoraModule].value_bool()) { if (options[launcher::Options::LoadGitaDoraModule].value_bool()) {
attach_gitadora = true; attach_gitadora = true;
} }
if (options[launcher::Options::GitaDoraDisableFrameLimiter].value_bool()) {
games::gitadora::DISABLE_FRAME_LIMITER = true;
}
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();
} }
@@ -1702,18 +1705,13 @@ int main_implementation(int argc, char *argv[]) {
} }
} }
// SDVXFullscreenLandscape disabled due to it messing with in-game camera angle if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !GRAPHICS_WINDOWED) {
// FullscreenOrientationFlip continues to live on, however. #if SPICE64
if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !cfg::CONFIGURATOR_STANDALONE) { GRAPHICS_FS_ORIENTATION_SWAP = true;
log_fatal("launcher", "-sdvxlandscape removed due to a camera bug in SDVX!"); #else
log_warning("launcher", "-sdvxlandscape is not supported in 32-bit SDVX, ignoring...");
#endif
} }
// if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !GRAPHICS_WINDOWED) {
// #if SPICE64
// GRAPHICS_FS_ORIENTATION_SWAP = true;
// #else
// log_warning("launcher", "-sdvxlandscape is not supported in 32-bit SDVX, ignoring...");
// #endif
// }
if (options[launcher::Options::FullscreenOrientationFlip].value_bool() && !GRAPHICS_WINDOWED) { if (options[launcher::Options::FullscreenOrientationFlip].value_bool() && !GRAPHICS_WINDOWED) {
GRAPHICS_FS_ORIENTATION_SWAP = true; GRAPHICS_FS_ORIENTATION_SWAP = true;
@@ -1855,6 +1853,13 @@ int main_implementation(int argc, char *argv[]) {
break; break;
} }
if (check_dll("jubeat2019.dll")) {
avs::game::DLL_NAME = "jubeat2019.dll";
attach_io = true;
attach_jb = true;
break;
}
// RB // RB
if (check_dll("reflecbeat.dll")) { if (check_dll("reflecbeat.dll")) {
avs::game::DLL_NAME = "reflecbeat.dll"; avs::game::DLL_NAME = "reflecbeat.dll";
@@ -2283,7 +2288,7 @@ int main_implementation(int argc, char *argv[]) {
games.push_back(new games::sdvx::SDVXGame()); games.push_back(new games::sdvx::SDVXGame());
} }
if (attach_jb) { if (attach_jb) {
avs::core::set_default_heap_size("jubeat.dll"); avs::core::set_default_heap_size(avs::game::DLL_NAME);
games.push_back(new games::jb::JBGame()); games.push_back(new games::jb::JBGame());
} }
if (attach_nostalgia) { if (attach_nostalgia) {
+22 -9
View File
@@ -330,9 +330,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = .desc =
"Allows you to play portrait games in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n" "Allows you to play portrait games in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
"Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n" "Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n"
"Strongly consider combining this with -forceres option to render at monitor native resolution\n\n" "Strongly consider combining this with -forceres option to render at monitor native resolution.\n\n"
"WARNING: for SDVX, this messes with camera angle for note lanes, causing it to be zoomed out, and causes performance issues " "For SDVX, use the dedicated -sdvxlandscape option.",
"with Live2D!",
.type = OptionType::Bool, .type = OptionType::Bool,
.hidden = true, .hidden = true,
.category = "Full Screen Settings" .category = "Full Screen Settings"
@@ -1203,6 +1202,19 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Options", .category = "Game Options",
.quick_setting_category = "Game", .quick_setting_category = "Game",
}, },
{
.title = "GitaDora Disable Frame Limiter (EXPERIMENTAL)",
.name = "gdnoframelimiter",
.desc = "Only for GITADORA to GALAXY WAVE; not for XG series or Arena Model.\n\n"
"Game engine's built-in frame limiter can result in unstable frame pacing "
"(around 56-58 FPS) on modern Windows. Turning this on disables that and "
"forces the game to rely on V-Sync instead, resulting in a stable FPS.\n\n"
"Ensure your monitors are set to 60Hz.",
.type = OptionType::Bool,
.game_name = "GitaDora",
.category = "Game Options",
.quick_setting_category = "Game",
},
{ {
.title = "GitaDora Cabinet Type", .title = "GitaDora Cabinet Type",
.name = "gdcabtype", .name = "gdcabtype",
@@ -1299,7 +1311,9 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "gdalayout", .name = "gdalayout",
.desc = "For Arena Model: select how many windows to create.\n\n" .desc = "For Arena Model: select how many windows to create.\n\n"
"1 window: main only; use the subscreen overlay for SMALL.\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 " "4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the "
"right resolution; see wiki for details.", "right resolution; see wiki for details.",
.type = OptionType::Enum, .type = OptionType::Enum,
@@ -2165,7 +2179,6 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{"25", "+25 dB"}, {"25", "+25 dB"},
{"30", "+30 dB"}, {"30", "+30 dB"},
}, },
.quick_setting_category = "Audio",
}, },
{ {
// AudioResample // AudioResample
@@ -3012,17 +3025,17 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// SDVXFullscreenLandscape // SDVXFullscreenLandscape
.title = "(DISABLED) SDVX Landscape Mode (SDVX5+)", .title = "SDVX Full Screen Landscape Mode (SDVX5+, EXPERIMENTAL)",
.name = "sdvxlandscape", .name = "sdvxlandscape",
.desc = .desc =
"Option HIDDEN and DISABLED due to it affecting in-game camera angle for displaying lanes.\n\n"
"Allows you to play in landscape by transposing resolution and applying image scaling.\n\n" "Allows you to play in landscape by transposing resolution and applying image scaling.\n\n"
"Works only for SDVX5 and above! This is identical to -forceresswap.\n\n" "Corrects the in-game perspective camera for the transposed display aspect ratio.\n\n"
"Works only for SDVX5 and above.\n\n"
"Will launch at 1080p by default; strongly consider combining this with -forceres option to render at monitor native resolution.", "Will launch at 1080p by default; strongly consider combining this with -forceres option to render at monitor native resolution.",
.type = OptionType::Bool, .type = OptionType::Bool,
.hidden = true,
.game_name = "Sound Voltex", .game_name = "Sound Voltex",
.category = "Game Options", .category = "Game Options",
.quick_setting_category = "Game",
}, },
{ {
// spice2x_EnableSMXStage // spice2x_EnableSMXStage
+1
View File
@@ -114,6 +114,7 @@ namespace launcher {
LoadHelloPopnMusicModule, LoadHelloPopnMusicModule,
LoadGitaDoraModule, LoadGitaDoraModule,
GitaDoraTwoChannelAudio, GitaDoraTwoChannelAudio,
GitaDoraDisableFrameLimiter,
GitaDoraCabinetType, GitaDoraCabinetType,
GitaDoraLefty, GitaDoraLefty,
GitaDoraWailHold, GitaDoraWailHold,
+2 -1
View File
@@ -673,7 +673,8 @@ void eamuse_autodetect_game() {
eamuse_set_game("Beatmania IIDX"); eamuse_set_game("Beatmania IIDX");
else if (avs::game::is_model("J44") || else if (avs::game::is_model("J44") ||
avs::game::is_model("K44") || avs::game::is_model("K44") ||
avs::game::is_model("L44")) avs::game::is_model("L44") ||
avs::game::is_model("T44"))
eamuse_set_game("Jubeat"); eamuse_set_game("Jubeat");
else if (avs::game::is_model("KDM")) else if (avs::game::is_model("KDM"))
eamuse_set_game("Dance Evolution"); eamuse_set_game("Dance Evolution");