Compare commits

...

13 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
bicarus 5cabd026ae misc: various performance clean up (#844)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

- Replace unnecessary precision timers with standard sleeps to reduce
wakeups and background CPU usage.
- Reuse buffers in raw input, touchscreen, and HID output paths to
eliminate steady-state allocations.
- Pre-index HID button groups and correctly process batched HID reports.

No functional changes.

## Testing
2026-07-28 21:05:32 -07:00
bicarus 7ccd938f9b overlay: mouse-as-touch needs to check if subscreen window is active (#842)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #840 

## Description of change
Currently, for subscreen games, mouse events are still delivered even
when the subscreen overlay window is not visible.

Change this so that the subscreen overlay must be active and under the
mouse cursor for the mouse-to-touch transformation to occur. Applies to
both native and wintouchemu.

## Testing
Needs to test everything again..

iidx:

- [ ]  full screen with overlay
- [ ]  single window with overlay
- [ ]  two window

Test: mouse, poke, api, real touch screen

sdvx:

- [ ] full screen with overlay
- [ ] windowed

popn

- [x] full screen with overlay
- [x] window with overlay
- [x] dedicated window

gitadora

- [x] single window with overlay
- [x] dedicated sub window

nostalgia

- [x] fullscreen
- [x] windowed

test: poke

wintouchemu
- [ ] Do all of the above again with wintouchemu
2026-07-28 01:14:47 -07:00
bicarus 88a210e82e iidx: camera hook improvements (#841)
## Link to GitHub Issue or related Pull Request, if one exists
#201

## Description of change

- Adds MJPEG camera support using Media Foundation decoding.
- Moves camera capture to asynchronous Source Reader callbacks.
- Supports native NV12 and YUY2 capture, preferring target-sized native
formats over MJPEG when possible.
- Improves capture and rendering performance through callback-paced
reads, bulk frame uploads, optimized row-based flips, and lazy
allocation of flip textures.
- Makes runtime media-type changes interrupt pending reads and safely
flush and reconfigure the Source Reader.
- Improves automatic mode selection based on aspect ratio, proximity to
1280x720, frame rate, and capture format.
- Hardens camera shutdown, flush handling, media-type changes, and
repeated capture failures.

## Testing
Tested with two cameras in tdj
2026-07-27 08:42:46 -07:00
bicarus 74e619df37 sdvx: fix check for enable_console in avs_config.xml (#838)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #657 

## Description of change
Workaround for SDVX4 was applied too broadly and caused sdvx 1/2/3 to
not boot, depending on contents of avs-config.xml.

`property_search_safe` throws a fatal error if the node is not present.
What we wanted to do was to check for presence.

## Testing
sanity checked sdvx 1/2/3/4
2026-07-26 16:16:25 -07:00
bicarus 10a97b9c63 signal: dump exception context, log dump creation failure (#837)
More diagnostics info for game crashes.
2026-07-26 15:25:24 -07:00
bicarus 159043803c imgui: fix crash when launching from UNC path (network shares) (#836)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
ImGui filebrowser extension crashes due to MinGW quirk about UNC path
handling.

## Testing
2026-07-26 04:30:06 -07:00
bicarus bd2fbfcb67 touch: restore wintouchemu, fall back to it when native touch hooks fail (#834)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #833

## Description of change
Last couple PRs - such as #820 #827 #828 - made the native touch hook &
touch injection using `InjectTouchInput` the default path, since it
performs much more reliably with both real touch screens and mouse (or
any other synthetic source).

However, user has reported that WINE lacks `InjectTouchInput` which
means this won't work.

As a fix, revive the old wintouchemu code. Native touch is still the
default, but under following circumstances:

1. if `-touchemuforce` is set, or
2. if any of the required Windows touch APIs are unavailable

then we fail over from native touch to wintouchemu code. 

For Linux, condition #2 would be hit during init, and gracefully switch
over.

Caveat: the poke code for IIDX and Nost will continue to require native
touch, I do not want to maintain two paths for this. This means that
iidx poke will stop working on Linux, unfortunately.

## Testing
Tested on Windows with `-touchemuforce` set. This is mostly reverting
Linux code path back to where we were last release, so this should just
work with wine.
2026-07-26 04:29:45 -07:00
68 changed files with 4363 additions and 590 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
+4 -4
View File
@@ -1,13 +1,14 @@
#include "keypads.h" #include "keypads.h"
#include <chrono>
#include <functional> #include <functional>
#include <thread>
#include <windows.h> #include <windows.h>
#include "avs/game.h" #include "avs/game.h"
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/precise_timer.h"
using namespace std::placeholders; using namespace std::placeholders;
using namespace rapidjson; using namespace rapidjson;
@@ -59,7 +60,6 @@ namespace api::modules {
// get params // get params
auto keypad = req.params[0].GetUint(); auto keypad = req.params[0].GetUint();
auto input = std::string(req.params[1].GetString()); auto input = std::string(req.params[1].GetString());
timeutils::PreciseSleepTimer timer;
// process all chars // process all chars
for (auto c : input) { for (auto c : input) {
@@ -93,11 +93,11 @@ namespace api::modules {
// set // set
eamuse_set_keypad_overrides(keypad, state); eamuse_set_keypad_overrides(keypad, state);
timer.sleep(sleep_time); std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
// unset // unset
eamuse_set_keypad_overrides(keypad, 0); eamuse_set_keypad_overrides(keypad, 0);
timer.sleep(sleep_time); std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
} }
} }
+3 -3
View File
@@ -1,11 +1,12 @@
#include "controller.h" #include "controller.h"
#include "serial.h" #include "serial.h"
#include <chrono>
#include <string> #include <string>
#include <thread>
#include <utility> #include <utility>
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
@@ -17,7 +18,6 @@ namespace api {
controller->init_state(this->state); controller->init_state(this->state);
this->thread = new std::thread([this] () { this->thread = new std::thread([this] () {
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud); log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
timeutils::PreciseSleepTimer timer;
// read buffer // read buffer
uint8_t read_buffer[16*1024]; uint8_t read_buffer[16*1024];
@@ -162,7 +162,7 @@ namespace api {
// slow down on reconnect // slow down on reconnect
if (this->running) { if (this->running) {
timer.sleep(retry_time); std::this_thread::sleep_for(std::chrono::milliseconds(retry_time));
} }
} }
}); });
+2 -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},
@@ -1924,7 +1925,7 @@ namespace avs {
#if !SPICE64 #if !SPICE64
// sdvx4 bad log config fix // sdvx4 bad log config fix
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
property_search_safe(config, config_node, "/log/enable_console")) { property_search(config, config_node, "/log/enable_console")) {
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>"); log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
property_search_remove_safe(config, config_node, "/log/enable_console"); property_search_remove_safe(config, config_node, "/log/enable_console");
} }
+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)
{ {
+9 -1
View File
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd) inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
{ {
pwd_ = absolute(pwd); // spice2x
// avoid MinGW std::filesystem::absolute() duplicating UNC server/share
// prefixes, which makes directory_iterator() throw during startup
// spice2x
const auto &nativePwd = pwd.native();
const bool hasUncPrefix = nativePwd.size() >= 2 &&
(nativePwd[0] == '\\' || nativePwd[0] == '/') &&
(nativePwd[1] == '\\' || nativePwd[1] == '/');
pwd_ = hasUncPrefix ? pwd : absolute(pwd);
UpdateFileRecords(); UpdateFileRecords();
selectedFilenames_.clear(); selectedFilenames_.clear();
(*inputNameBuf_)[0] = '\0'; (*inputNameBuf_)[0] = '\0';
+206 -37
View File
@@ -2,15 +2,18 @@
#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"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "util/cpuutils.h" #include "util/cpuutils.h"
@@ -28,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;
@@ -61,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
/* /*
@@ -270,9 +315,21 @@ namespace games::gitadora {
break; break;
case 2: case 2:
if (!GRAPHICS_WINDOWED) { if (!GRAPHICS_WINDOWED) {
if (D3D9_ADAPTER.has_value()) {
log_fatal( log_fatal(
"gitadora", "gitadora",
"arena model: 2-window mode is not supported in fullscreen, choose 1 or 4"); "arena model: fullscreen two-window mode cannot use -monitor; "
"use -w for borderless windows instead");
}
ARENA_TWO_HEAD_EXCLUSIVE = true;
log_info(
"gitadora",
"arena model: 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;
@@ -298,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:
@@ -325,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
@@ -368,14 +452,56 @@ 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;
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"); log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
} }
}
static static
LONG LONG
@@ -389,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,
@@ -473,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;
@@ -518,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;
@@ -541,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;
} }
@@ -575,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(
@@ -650,16 +809,28 @@ 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 &&
nativetouch::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
if (!native_touch_ready) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
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) {
log_info(
"gitadora",
"exposing physical MAIN/SMALL and virtual LEFT/RIGHT");
}
GetDisplayConfigBufferSizes_orig = GetDisplayConfigBufferSizes_orig =
detour::iat_try("GetDisplayConfigBufferSizes", detour::iat_try("GetDisplayConfigBufferSizes",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE); GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
@@ -671,8 +842,6 @@ namespace games::gitadora {
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE); DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
} }
#endif #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;
+10 -8
View File
@@ -534,7 +534,7 @@ namespace games::iidx {
if (mediaTypePointer && mediaTypePointer->IsString()) { if (mediaTypePointer && mediaTypePointer->IsString()) {
std::string mediaType = mediaTypePointer->GetString(); std::string mediaType = mediaTypePointer->GetString();
if (mediaType.length() > 0) { if (mediaType.length() > 0) {
camera->m_selectedMediaTypeDescription = mediaType; camera->SetSelectedMediaTypeDescription(mediaType);
camera->m_useAutoMediaType = false; camera->m_useAutoMediaType = false;
} else { } else {
camera->m_useAutoMediaType = true; camera->m_useAutoMediaType = true;
@@ -547,7 +547,7 @@ namespace games::iidx {
std::string drawModeString = drawModePointer->GetString(); std::string drawModeString = drawModePointer->GetString();
for (int j = 0; j < DRAW_MODE_SIZE; j++) { for (int j = 0; j < DRAW_MODE_SIZE; j++) {
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) { if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
camera->m_drawMode = (LocalCameraDrawMode) j; camera->m_drawMode.store((LocalCameraDrawMode) j);
break; break;
} }
} }
@@ -556,12 +556,12 @@ namespace games::iidx {
// Flip // Flip
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc); auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) { if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
camera->m_flipHorizontal = flipHorizontalPointer->GetBool(); camera->m_flipHorizontal.store(flipHorizontalPointer->GetBool());
} }
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc); auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
if (flipVerticalPointer && flipVerticalPointer->IsBool()) { if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
camera->m_flipVertical = flipVerticalPointer->GetBool(); camera->m_flipVertical.store(flipVerticalPointer->GetBool());
} }
// Allow manual control // Allow manual control
@@ -625,15 +625,17 @@ namespace games::iidx {
if (camera->m_useAutoMediaType) { if (camera->m_useAutoMediaType) {
rapidjson::Pointer(root + "MediaType").Set(doc, ""); rapidjson::Pointer(root + "MediaType").Set(doc, "");
} else { } else {
rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription); const auto mediaTypeDescription = camera->GetSelectedMediaTypeDescription();
rapidjson::Pointer(root + "MediaType").Set(doc, mediaTypeDescription);
} }
// Draw Mode // Draw Mode
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[camera->m_drawMode]); const auto drawMode = camera->m_drawMode.load();
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[drawMode]);
// Flip // Flip
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal); rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal.load());
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical); rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical.load());
// Manual control // Manual control
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl); rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
+19
View File
@@ -22,6 +22,7 @@
#include "touch/touch.h" #include "touch/touch.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/wintouchemu.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "util/fileutils.h" #include "util/fileutils.h"
@@ -40,6 +41,7 @@
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include "ezusb.h" #include "ezusb.h"
#include "io.h" #include "io.h"
#include "poke.h"
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr; static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr; static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
@@ -62,6 +64,8 @@ namespace games::iidx {
bool TDJ_MODE = false; bool TDJ_MODE = false;
bool FORCE_720P = false; bool FORCE_720P = false;
bool DISABLE_ESPEC_IO = false; bool DISABLE_ESPEC_IO = false;
bool NATIVE_TOUCH = true;
bool ENABLE_POKE = false;
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt; std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt; std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt; std::optional<std::string> ASIO_DRIVER = std::nullopt;
@@ -348,7 +352,16 @@ namespace games::iidx {
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook // need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
devicehook_init(avs::core::DLL_INSTANCE); devicehook_init(avs::core::DLL_INSTANCE);
NATIVE_TOUCH = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
if (!NATIVE_TOUCH) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
"beatmania IIDX",
"main",
avs::game::DLL_INSTANCE);
}
} }
// insert BI2X hooks // insert BI2X hooks
@@ -525,6 +538,12 @@ namespace games::iidx {
} }
} }
void IIDXGame::post_attach() {
if (ENABLE_POKE) {
poke::enable();
}
}
void IIDXGame::detach() { void IIDXGame::detach() {
Game::detach(); Game::detach();
+3
View File
@@ -28,6 +28,8 @@ namespace games::iidx {
extern bool TDJ_MODE; extern bool TDJ_MODE;
extern bool FORCE_720P; extern bool FORCE_720P;
extern bool DISABLE_ESPEC_IO; extern bool DISABLE_ESPEC_IO;
extern bool NATIVE_TOUCH;
extern bool ENABLE_POKE;
extern std::optional<std::string> SOUND_OUTPUT_DEVICE; extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
extern std::optional<std::string> ASIO_DRIVER; extern std::optional<std::string> ASIO_DRIVER;
extern uint8_t DIGITAL_TT_SENS; extern uint8_t DIGITAL_TT_SENS;
@@ -52,6 +54,7 @@ namespace games::iidx {
virtual void attach() override; virtual void attach() override;
virtual void pre_attach() override; virtual void pre_attach() override;
virtual void post_attach() override;
virtual void detach() override; virtual void detach() override;
private: private:
File diff suppressed because it is too large Load Diff
+36 -11
View File
@@ -2,6 +2,7 @@
#if SPICE64 && !SPICE_XP #if SPICE64 && !SPICE_XP
#include <atomic>
#include <d3d9.h> #include <d3d9.h>
#include <dxva2api.h> #include <dxva2api.h>
#include <mutex> #include <mutex>
@@ -66,6 +67,8 @@ extern std::string CAMERA_CONTROL_LABELS[];
extern std::string DRAW_MODE_LABELS[]; extern std::string DRAW_MODE_LABELS[];
namespace games::iidx { namespace games::iidx {
class IIDXCameraSourceReaderCallback;
namespace Camera { namespace Camera {
struct PlayVideoCamera { struct PlayVideoCamera {
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) { IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
@@ -87,7 +90,7 @@ namespace games::iidx {
class IIDXLocalCamera { class IIDXLocalCamera {
protected: protected:
virtual ~IIDXLocalCamera() {}; virtual ~IIDXLocalCamera();
LONG m_nRefCount; LONG m_nRefCount;
CRITICAL_SECTION m_critsec; CRITICAL_SECTION m_critsec;
@@ -101,9 +104,14 @@ namespace games::iidx {
// For reading frames from Camera // For reading frames from Camera
IMFMediaSource *m_pSource = nullptr; IMFMediaSource *m_pSource = nullptr;
IMFSourceReader *m_pSourceReader = nullptr; IMFSourceReader *m_pSourceReader = nullptr;
IMFSourceReaderEx *m_pSourceReaderEx = nullptr;
IIDXCameraSourceReaderCallback *m_pSourceReaderCallback = nullptr;
std::mutex m_mediaTypeMutex;
IMFMediaType *m_pendingMediaType = nullptr;
int m_selectedMediaTypeIndex = 0;
std::string m_selectedMediaTypeDescription = "";
// Camera Format information // Camera Format information
double m_frameRate = 0;
LONG m_cameraWidth; LONG m_cameraWidth;
LONG m_cameraHeight; LONG m_cameraHeight;
@@ -129,6 +137,12 @@ namespace games::iidx {
LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr; LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr;
IDirect3DSurface9 *m_pConversionSurf = nullptr; IDirect3DSurface9 *m_pConversionSurf = nullptr;
// upload surface for decoded camera frames returned in system memory
IDirect3DSurface9 *m_pDecodedSurf = nullptr;
GUID m_decodedSubtype = GUID_NULL;
GUID m_outputSubtype = GUID_NULL;
bool m_drawErrorLogged = false;
// Texture for custom transform (e.g. horizontal flip) // Texture for custom transform (e.g. horizontal flip)
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr; LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
IDirect3DSurface9 *m_pTransformSurf = nullptr; IDirect3DSurface9 *m_pTransformSurf = nullptr;
@@ -153,21 +167,19 @@ namespace games::iidx {
BOOL m_initialized = false; BOOL m_initialized = false;
// True if all the setup steps succeeded // True if all the setup steps succeeded
BOOL m_active = false; std::atomic_bool m_active = false;
// Media type select // Media type select
std::vector<MediaTypeInfo> m_mediaTypeInfos = {}; std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
int m_selectedMediaTypeIndex = 0;
bool m_useAutoMediaType = true; bool m_useAutoMediaType = true;
IMFMediaType *m_pAutoMediaType = nullptr; IMFMediaType *m_pAutoMediaType = nullptr;
std::string m_selectedMediaTypeDescription = "";
bool m_allowManualControl = false; bool m_allowManualControl = false;
LocalCameraDrawMode m_drawMode = DrawModeCrop4_3; std::atomic<LocalCameraDrawMode> m_drawMode = DrawModeCrop4_3;
// Render processing // Render processing
bool m_flipHorizontal = false; std::atomic_bool m_flipHorizontal = false;
bool m_flipVertical = false; std::atomic_bool m_flipVertical = false;
IIDXLocalCamera( IIDXLocalCamera(
std::string name, std::string name,
@@ -184,11 +196,14 @@ namespace games::iidx {
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp); HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
HRESULT SetCameraControlProp(int index, long value, long flags); HRESULT SetCameraControlProp(int index, long value, long flags);
HRESULT ResetCameraControlProps(); HRESULT ResetCameraControlProps();
HRESULT FlushDrawCommands();
std::string GetName(); std::string GetName();
std::string GetFriendlyName(); std::string GetFriendlyName();
std::string GetSymLink(); std::string GetSymLink();
int GetSelectedMediaTypeIndex();
std::string GetSelectedMediaTypeDescription();
void SetSelectedMediaTypeDescription(const std::string &description);
HRESULT ChangeMediaType(IMFMediaType *pType); HRESULT ChangeMediaType(IMFMediaType *pType);
void RequestMediaType(IMFMediaType *pType);
HRESULT StartCapture(); HRESULT StartCapture();
void UpdateDrawRect(); void UpdateDrawRect();
@@ -197,12 +212,22 @@ namespace games::iidx {
void CreateThread(); void CreateThread();
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType); MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
std::string GetVideoFormatName(GUID subtype); std::string GetVideoFormatName(GUID subtype);
HRESULT TryMediaType(IMFMediaType *pType, UINT32 *pBestWidth, double *pBestFrameRate); static bool CompareMediaTypes(const MediaTypeInfo &a, const MediaTypeInfo &b);
bool MatchesPreferredAspect(const MediaTypeInfo &info) const;
static bool IsBetterAutoType(const MediaTypeInfo &candidate, const MediaTypeInfo &current);
IMFMediaType *FindBestNativeAutoType(bool requirePreferredAspect) const;
IMFMediaType *FindBestAutoType(const GUID &subtype, bool requirePreferredAspect) const;
HRESULT ValidateMediaType(IMFMediaType *pType);
HRESULT InitTargetTexture(); HRESULT InitTargetTexture();
HRESULT EnsureTransformTextures();
HRESULT InitCameraControl(); HRESULT InitCameraControl();
void SetSelectedMediaType(int index, const std::string &description);
bool HasPendingMediaType();
HRESULT ApplyPendingMediaType();
HRESULT UploadDecodedSample(IMFMediaBuffer *pSrcBuffer);
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer); HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
HRESULT ReadSample(); HRESULT ReadSample();
LPDIRECT3DTEXTURE9 Render(); HRESULT Render();
}; };
} }
+16
View File
@@ -15,6 +15,10 @@ namespace games::iidx {
_In_ UINT32 cInitialSize _In_ UINT32 cInitialSize
); );
typedef HRESULT (__stdcall * MFCreateMediaType_t)(
_Out_ IMFMediaType** ppMFType
);
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)( typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
_In_ IMFAttributes* pAttributes, _In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
@@ -35,6 +39,7 @@ namespace games::iidx {
); );
static MFCreateAttributes_t MFCreateAttributes = nullptr; static MFCreateAttributes_t MFCreateAttributes = nullptr;
static MFCreateMediaType_t MFCreateMediaType = nullptr;
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr; static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr; static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
static MFGetService_t MFGetService = nullptr; static MFGetService_t MFGetService = nullptr;
@@ -66,6 +71,12 @@ namespace games::iidx {
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook"); log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
} }
MFCreateMediaType = (MFCreateMediaType_t)
libutils::get_proc(mfplat_dll, "MFCreateMediaType");
if (!MFCreateMediaType) {
log_fatal("mf_wrappers", "MFCreateMediaType failed to hook");
}
MFEnumDeviceSources = (MFEnumDeviceSources_t) MFEnumDeviceSources = (MFEnumDeviceSources_t)
libutils::get_proc(mf_dll, "MFEnumDeviceSources"); libutils::get_proc(mf_dll, "MFEnumDeviceSources");
if (!MFEnumDeviceSources) { if (!MFEnumDeviceSources) {
@@ -92,6 +103,11 @@ namespace games::iidx {
return MFCreateAttributes(ppMFAttributes, cInitialSize); return MFCreateAttributes(ppMFAttributes, cInitialSize);
} }
HRESULT WrappedMFCreateMediaType (
_Out_ IMFMediaType** ppMFType) {
return MFCreateMediaType(ppMFType);
}
HRESULT WrappedMFEnumDeviceSources ( HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes, _In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
+3
View File
@@ -12,6 +12,9 @@ namespace games::iidx {
_Out_ IMFAttributes** ppMFAttributes, _Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize); _In_ UINT32 cInitialSize);
HRESULT WrappedMFCreateMediaType (
_Out_ IMFMediaType** ppMFType);
HRESULT WrappedMFEnumDeviceSources ( HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes, _In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
+8 -4
View File
@@ -1,16 +1,17 @@
#include "poke.h" #include "poke.h"
#include <chrono>
#include <thread> #include <thread>
#include "windows.h" #include "windows.h"
#include "games/io.h" #include "games/io.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "touch/native/inject.h" #include "touch/native/inject.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
namespace games::iidx::poke { namespace games::iidx::poke {
@@ -116,11 +117,14 @@ namespace games::iidx::poke {
if (THREAD) if (THREAD)
return; return;
if (!games::iidx::NATIVE_TOUCH) {
log_warning("poke", "keypad touch emulation requires native touch; disabled");
return;
}
// create new thread // create new thread
THREAD_RUNNING = true; THREAD_RUNNING = true;
THREAD = new std::thread([] { THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
// log // log
log_info("poke", "enabled"); log_info("poke", "enabled");
@@ -192,7 +196,7 @@ namespace games::iidx::poke {
} }
// slow down // slow down
timer.sleep(50); std::this_thread::sleep_for(std::chrono::milliseconds(50));
} }
if (!touch_points.empty()) { if (!touch_points.empty()) {
+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);
+7
View File
@@ -2,6 +2,7 @@
#include "poke.h" #include "poke.h"
#include "touch_mode.h" #include "touch_mode.h"
#include "hooks/setupapihook.h" #include "hooks/setupapihook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "avs/game.h" #include "avs/game.h"
@@ -41,11 +42,17 @@ namespace games::nost {
setupapihook_init(avs::game::DLL_INSTANCE); setupapihook_init(avs::game::DLL_INSTANCE);
setupapihook_add(touch_settings); setupapihook_add(touch_settings);
const auto native_touch_ready = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
if (!native_touch_ready) {
wintouchemu::FORCE = true;
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
} else {
if (ENABLE_TOUCH_MODE) { if (ENABLE_TOUCH_MODE) {
touch_mode::enable(); touch_mode::enable();
} }
} }
}
void NostGame::post_attach() { void NostGame::post_attach() {
if (ENABLE_POKE) { if (ENABLE_POKE) {
+8 -4
View File
@@ -1,6 +1,7 @@
#include "poke.h" #include "poke.h"
#include <atomic> #include <atomic>
#include <chrono>
#include <optional> #include <optional>
#include <thread> #include <thread>
@@ -9,8 +10,8 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "touch/native/inject.h" #include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
namespace games::nost::poke { namespace games::nost::poke {
@@ -59,11 +60,14 @@ namespace games::nost::poke {
return; return;
} }
if (!nativetouch::is_hooked()) {
log_warning("poke", "Nostalgia poke requires native touch; disabled");
return;
}
// create new thread // create new thread
THREAD_RUNNING = true; THREAD_RUNNING = true;
THREAD = new std::thread([] { THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
const int swipe_anim_total_frames = 6; const int swipe_anim_total_frames = 6;
const int swipe_anim_y = 300; const int swipe_anim_y = 300;
@@ -188,7 +192,7 @@ namespace games::nost::poke {
} }
// slow down // slow down
timer.sleep(30); std::this_thread::sleep_for(std::chrono::milliseconds(30));
} }
if (contact_active) { if (contact_active) {
+13
View File
@@ -16,6 +16,7 @@
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "launcher/logger.h" #include "launcher/logger.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/wintouchemu.h"
#include "util/sysutils.h" #include "util/sysutils.h"
#include "io.h" #include "io.h"
#include "util/deferlog.h" #include "util/deferlog.h"
@@ -24,6 +25,7 @@
namespace games::popn { namespace games::popn {
bool SHOW_PIKA_MONITOR_WARNING = false; bool SHOW_PIKA_MONITOR_WARNING = false;
bool NATIVE_TOUCH = true;
#if SPICE64 && !SPICE_XP #if SPICE64 && !SPICE_XP
@@ -713,7 +715,18 @@ namespace games::popn {
// 00000100 0000000B 00000001 (button 9) // 00000100 0000000B 00000001 (button 9)
// set third column to 0 and it will work with BIO2 // set third column to 0 and it will work with BIO2
NATIVE_TOUCH = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
if (!NATIVE_TOUCH) {
wintouchemu::FORCE = true;
if (!GRAPHICS_WINDOWED || GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
"",
"Main Screen",
avs::game::DLL_INSTANCE);
}
}
sysutils::hook_EnumDisplayDevicesA(); sysutils::hook_EnumDisplayDevicesA();
+1
View File
@@ -22,6 +22,7 @@ namespace games::popn {
#endif #endif
extern bool SHOW_PIKA_MONITOR_WARNING; extern bool SHOW_PIKA_MONITOR_WARNING;
extern bool NATIVE_TOUCH;
class POPNGame : public games::Game { class POPNGame : public games::Game {
public: public:
+12
View File
@@ -26,6 +26,7 @@
#include "util/libutils.h" #include "util/libutils.h"
#include "util/sysutils.h" #include "util/sysutils.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include "camera.h" #include "camera.h"
@@ -453,7 +454,18 @@ namespace games::sdvx {
} }
if (is_valkyrie_model()) { if (is_valkyrie_model()) {
const auto native_touch_ready = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
if (!native_touch_ready) {
wintouchemu::FORCE = true;
if (!GRAPHICS_WINDOWED) {
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
"SOUND VOLTEX",
"Main Screen",
avs::game::DLL_INSTANCE);
}
}
// insert BI2X hooks // insert BI2X hooks
bi2x_hook_init(); bi2x_hook_init();
@@ -2,10 +2,10 @@
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <thread>
#include "hooks/audio/util.h" #include "hooks/audio/util.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
NullDiscardBackend::~NullDiscardBackend() { NullDiscardBackend::~NullDiscardBackend() {
this->running = false; this->running = false;
@@ -125,8 +125,6 @@ HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
void NullDiscardBackend::pace_loop() { void NullDiscardBackend::pace_loop() {
using namespace std::chrono; using namespace std::chrono;
timeutils::PreciseSleepTimer timer;
// audio is discarded, so timing precision and drift do not matter; just wake the // audio is discarded, so timing precision and drift do not matter; just wake the
// game once per buffer period to keep its render thread from blocking on the event. // game once per buffer period to keep its render thread from blocking on the event.
const auto period = duration_cast<steady_clock::duration>( const auto period = duration_cast<steady_clock::duration>(
@@ -136,6 +134,6 @@ void NullDiscardBackend::pace_loop() {
if (this->relay_handle) { if (this->relay_handle) {
SetEvent(this->relay_handle); SetEvent(this->relay_handle);
} }
timer.sleep(period); std::this_thread::sleep_for(period);
} }
} }
@@ -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");
}); });
if (!is_hidden) {
return D3DERR_INVALIDCALL; 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;
if (!is_hidden) {
return D3DERR_INVALIDCALL; 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;
if (!is_hidden) {
return D3DERR_INVALIDCALL; 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;
if (!is_hidden) {
return D3DERR_INVALIDCALL; 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);
if (!is_hidden) {
return D3DERR_INVALIDCALL; 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);
if (!is_hidden) {
return D3DERR_INVALIDCALL; 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);
if (!is_hidden) {
return D3DERR_INVALIDCALL; 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;
}; };
+158 -3
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"
@@ -28,6 +29,7 @@
#include "util/logging.h" #include "util/logging.h"
#include "util/fileutils.h" #include "util/fileutils.h"
#include "util/utils.h" #include "util/utils.h"
#include "misc/wintouchemu.h"
#include "touch/native/inject.h" #include "touch/native/inject.h"
#include "util/time.h" #include "util/time.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
@@ -43,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;
@@ -182,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;
@@ -203,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() &&
@@ -307,6 +407,22 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
} }
} }
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
// drop mouse inputs since only wintouches should be used
switch (uMsg) {
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
return true;
}
}
// window resize // window resize
graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam); graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam);
@@ -601,10 +717,17 @@ 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);
}
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); 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);
} }
@@ -750,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 {};
@@ -786,7 +913,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
nWidth = rect.right - rect.left; nWidth = rect.right - rect.left;
nHeight = rect.bottom - rect.top; nHeight = rect.bottom - rect.top;
if (games::iidx::NATIVE_TOUCH) {
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW); nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
} else {
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
}
} else { } else {
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead // Existing behaviour: suppress subscreen window and prompt user to use overlay instead
log_info( log_info(
@@ -853,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;
@@ -873,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;
@@ -894,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;
@@ -918,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) {
@@ -1160,8 +1314,9 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc)); SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
const bool native_touch_overlay = const bool native_touch_overlay =
(games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) || (games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
(games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS); (games::popn::NATIVE_TOUCH &&
games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
if (native_touch_overlay) { if (native_touch_overlay) {
nativetouch::inject::register_and_attach_window(hWnd); nativetouch::inject::register_and_attach_window(hWnd);
} }
+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",
+19 -14
View File
@@ -38,10 +38,10 @@
#include "games/gitadora/gitadora.h" #include "games/gitadora/gitadora.h"
#include "games/hpm/hpm.h" #include "games/hpm/hpm.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/iidx/poke.h"
#ifdef SPICE64 #ifdef SPICE64
#include "games/iidx/camera.h" #include "games/iidx/camera.h"
#endif #endif
#include "games/iidx/poke.h"
#include "games/jb/jb.h" #include "games/jb/jb.h"
#include "games/mga/mga.h" #include "games/mga/mga.h"
#include "games/nost/nost.h" #include "games/nost/nost.h"
@@ -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;
@@ -1783,7 +1781,7 @@ int main_implementation(int argc, char *argv[]) {
// enable subscreen touch emulation // enable subscreen touch emulation
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) { if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
games::iidx::poke::enable(); games::iidx::ENABLE_POKE = true;
} }
if (options[launcher::Options::NostalgiaPoke].is_active()) { if (options[launcher::Options::NostalgiaPoke].is_active()) {
games::nost::ENABLE_POKE = true; games::nost::ENABLE_POKE = 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) {
+24 -11
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,
@@ -1791,10 +1805,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Touch Parameters", .category = "Touch Parameters",
}, },
{ {
.title = "Force Touch Emulation", .title = "Force Legacy Touch Emulation",
.name = "touchemuforce", .name = "touchemuforce",
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. " .desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
"This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.", "Do not enable this unless you have trouble with using a mouse to emulate touch input.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Touch Parameters", .category = "Touch Parameters",
}, },
@@ -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,
+401 -45
View File
@@ -1,5 +1,6 @@
#include "signal.h" #include "signal.h"
#include <algorithm>
#include <future> #include <future>
#include <exception> #include <exception>
@@ -25,6 +26,7 @@
#endif #endif
static decltype(MiniDumpWriteDump) *MiniDumpWriteDump_local = nullptr; static decltype(MiniDumpWriteDump) *MiniDumpWriteDump_local = nullptr;
static LONG EXCEPTION_HANDLER_ACTIVE = 0;
namespace launcher::signal { namespace launcher::signal {
@@ -47,7 +49,7 @@ static std::string control_code(DWORD dwCtrlType) {
} }
} }
static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) { static std::string exception_code(const struct _EXCEPTION_RECORD *ExceptionRecord) {
switch (ExceptionRecord->ExceptionCode) { switch (ExceptionRecord->ExceptionCode) {
V(EXCEPTION_ACCESS_VIOLATION); V(EXCEPTION_ACCESS_VIOLATION);
V(EXCEPTION_ARRAY_BOUNDS_EXCEEDED); V(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
@@ -77,6 +79,388 @@ static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
#undef V #undef V
static const char *access_operation(ULONG_PTR operation) {
switch (operation) {
case 0:
return "read";
case 1:
return "write";
case 8:
return "execute";
default:
return "unknown";
}
}
static const char *memory_state(DWORD state) {
switch (state) {
case MEM_COMMIT:
return "committed";
case MEM_FREE:
return "free";
case MEM_RESERVE:
return "reserved";
default:
return "unknown";
}
}
static const char *memory_type(DWORD type) {
switch (type) {
case MEM_IMAGE:
return "image";
case MEM_MAPPED:
return "mapped";
case MEM_PRIVATE:
return "private";
default:
return "none";
}
}
static const char *memory_protection(DWORD protection) {
switch (protection & 0xff) {
case PAGE_EXECUTE:
return "execute";
case PAGE_EXECUTE_READ:
return "execute-read";
case PAGE_EXECUTE_READWRITE:
return "execute-read-write";
case PAGE_EXECUTE_WRITECOPY:
return "execute-write-copy";
case PAGE_NOACCESS:
return "no-access";
case PAGE_READONLY:
return "read-only";
case PAGE_READWRITE:
return "read-write";
case PAGE_WRITECOPY:
return "write-copy";
default:
return "none";
}
}
static void log_exception_parameters(const struct _EXCEPTION_RECORD *record) {
const auto parameter_count = record->NumberParameters <= EXCEPTION_MAXIMUM_PARAMETERS ?
record->NumberParameters : EXCEPTION_MAXIMUM_PARAMETERS;
log_warning("signal", "exception flags: 0x{:08x}, parameters: {}",
record->ExceptionFlags, parameter_count);
for (DWORD parameter = 0; parameter < parameter_count; parameter++) {
#ifdef _WIN64
log_warning("signal", "exception parameter[{}]: {:016x}",
parameter, record->ExceptionInformation[parameter]);
#else
log_warning("signal", "exception parameter[{}]: {:08x}",
parameter, record->ExceptionInformation[parameter]);
#endif
}
}
static bool query_memory(const void *address, MEMORY_BASIC_INFORMATION *memory) {
return VirtualQuery(address, memory, sizeof(*memory)) == sizeof(*memory);
}
static void log_memory_information(const char *label, const MEMORY_BASIC_INFORMATION &memory) {
log_warning("signal",
"{}: base={}, size=0x{:x}, allocation_base={}, state={} (0x{:x}), "
"protect={} (0x{:x}), type={} (0x{:x})",
label,
fmt::ptr(memory.BaseAddress),
memory.RegionSize,
fmt::ptr(memory.AllocationBase),
memory_state(memory.State),
memory.State,
memory_protection(memory.Protect),
memory.Protect,
memory_type(memory.Type),
memory.Type);
}
static void log_memory_region(const char *label, const void *address) {
MEMORY_BASIC_INFORMATION memory {};
if (!query_memory(address, &memory)) {
log_warning("signal", "{}: VirtualQuery failed for {}: 0x{:08x}",
label, fmt::ptr(address), GetLastError());
return;
}
log_memory_information(label, memory);
}
static void log_adjacent_memory_regions(const void *address) {
MEMORY_BASIC_INFORMATION current {};
if (!query_memory(address, &current)) {
return;
}
const auto current_base = reinterpret_cast<uintptr_t>(current.BaseAddress);
if (current_base > 0) {
MEMORY_BASIC_INFORMATION previous {};
if (query_memory(reinterpret_cast<const void *>(current_base - 1), &previous)) {
log_memory_information("fault memory previous", previous);
}
}
const auto current_end = current_base + current.RegionSize;
if (current_end > current_base) {
MEMORY_BASIC_INFORMATION next {};
if (query_memory(reinterpret_cast<const void *>(current_end), &next)) {
log_memory_information("fault memory next", next);
}
}
}
static void log_exception_module(const void *address) {
MEMORY_BASIC_INFORMATION memory {};
if (!query_memory(address, &memory) || memory.Type != MEM_IMAGE || memory.AllocationBase == nullptr) {
log_warning("signal", "exception module: unavailable");
return;
}
char module_path[MAX_PATH] {};
const auto module = static_cast<HMODULE>(memory.AllocationBase);
const auto module_base = reinterpret_cast<uintptr_t>(memory.AllocationBase);
const auto exception_address = reinterpret_cast<uintptr_t>(address);
const auto module_length = GetModuleFileNameA(module, module_path, sizeof(module_path));
if (module_length > 0) {
const std::string module_name(module_path, std::min<size_t>(module_length, sizeof(module_path)));
log_warning("signal", "exception module: '{}' base={}, rva=0x{:x}",
module_name, fmt::ptr(memory.AllocationBase), exception_address - module_base);
} else {
log_warning("signal", "exception module: base={}, rva=0x{:x}",
fmt::ptr(memory.AllocationBase), exception_address - module_base);
}
}
static void log_instruction_bytes(const void *address) {
MEMORY_BASIC_INFORMATION memory {};
if (!query_memory(address, &memory) ||
memory.State != MEM_COMMIT ||
(memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0) {
log_warning("signal", "instruction bytes: unavailable");
return;
}
constexpr size_t max_instruction_bytes = 16;
const auto region_end = reinterpret_cast<uintptr_t>(memory.BaseAddress) + memory.RegionSize;
const auto instruction_address = reinterpret_cast<uintptr_t>(address);
const auto available = region_end > instruction_address ? region_end - instruction_address : 0;
const auto byte_count = std::min(max_instruction_bytes, static_cast<size_t>(available));
uint8_t bytes[max_instruction_bytes] {};
SIZE_T bytes_read = 0;
if (byte_count == 0 ||
!ReadProcessMemory(GetCurrentProcess(), address, bytes, byte_count, &bytes_read)) {
log_warning("signal", "instruction bytes: read failed at {}: 0x{:08x}",
fmt::ptr(address), GetLastError());
return;
}
std::string byte_string;
byte_string.reserve(bytes_read * 3);
for (size_t index = 0; index < bytes_read; index++) {
fmt::format_to(std::back_inserter(byte_string), "{:02x}{}",
bytes[index], index + 1 < bytes_read ? " " : "");
}
log_warning("signal", "instruction bytes: {}", byte_string);
}
static void log_stack_context(const CONTEXT *context) {
const auto *tib = reinterpret_cast<const NT_TIB *>(NtCurrentTeb());
const auto stack_base = reinterpret_cast<uintptr_t>(tib->StackBase);
const auto stack_limit = reinterpret_cast<uintptr_t>(tib->StackLimit);
#ifdef _WIN64
const auto stack_pointer = static_cast<uintptr_t>(context->Rsp);
#else
const auto stack_pointer = static_cast<uintptr_t>(context->Esp);
#endif
const auto stack_used = stack_base > stack_pointer ? stack_base - stack_pointer : 0;
const auto stack_remaining = stack_pointer > stack_limit ? stack_pointer - stack_limit : 0;
log_warning("signal", "stack: base={}, limit={}, pointer={}, used=0x{:x}, remaining=0x{:x}",
fmt::ptr(tib->StackBase),
fmt::ptr(tib->StackLimit),
fmt::ptr(reinterpret_cast<const void *>(stack_pointer)),
stack_used,
stack_remaining);
}
static void log_exception_context(struct _EXCEPTION_POINTERS *ExceptionInfo, DWORD last_error) {
const auto *record = ExceptionInfo->ExceptionRecord;
const auto *context = ExceptionInfo->ContextRecord;
log_warning("signal", "thread id: {}", GetCurrentThreadId());
log_warning("signal", "thread last error (GetLastError()) at exception: 0x{:08x}", last_error);
log_warning("signal", "exception address: {}", fmt::ptr(record->ExceptionAddress));
log_exception_module(record->ExceptionAddress);
log_instruction_bytes(record->ExceptionAddress);
log_exception_parameters(record);
if ((record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION ||
record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR) &&
record->NumberParameters >= 2) {
const auto operation = record->ExceptionInformation[0];
const auto address = reinterpret_cast<const void *>(record->ExceptionInformation[1]);
log_warning("signal", "invalid memory access: {} at {}",
access_operation(operation), fmt::ptr(address));
log_memory_region("fault memory", address);
log_adjacent_memory_regions(address);
}
if (record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR && record->NumberParameters >= 3) {
log_warning("signal", "in-page error status: 0x{:08x}",
static_cast<DWORD>(record->ExceptionInformation[2]));
}
if (context == nullptr) {
return;
}
log_stack_context(context);
#ifdef _WIN64
log_warning("signal", "registers: rax={:016x} rbx={:016x} rcx={:016x} rdx={:016x}",
context->Rax, context->Rbx, context->Rcx, context->Rdx);
log_warning("signal", "registers: rsi={:016x} rdi={:016x} rbp={:016x} rsp={:016x}",
context->Rsi, context->Rdi, context->Rbp, context->Rsp);
log_warning("signal", "registers: r8={:016x} r9={:016x} r10={:016x} r11={:016x}",
context->R8, context->R9, context->R10, context->R11);
log_warning("signal", "registers: r12={:016x} r13={:016x} r14={:016x} r15={:016x}",
context->R12, context->R13, context->R14, context->R15);
log_warning("signal", "registers: rip={:016x} eflags={:08x}",
context->Rip, context->EFlags);
#else
log_warning("signal", "registers: eax={:08x} ebx={:08x} ecx={:08x} edx={:08x}",
context->Eax, context->Ebx, context->Ecx, context->Edx);
log_warning("signal", "registers: esi={:08x} edi={:08x} ebp={:08x} esp={:08x}",
context->Esi, context->Edi, context->Ebp, context->Esp);
log_warning("signal", "registers: eip={:08x} eflags={:08x}",
context->Eip, context->EFlags);
#endif
}
static void write_minidump(struct _EXCEPTION_POINTERS *ExceptionInfo) {
if (MiniDumpWriteDump_local == nullptr) {
log_warning("signal", "minidump creation function not available, skipping");
return;
}
HANDLE minidump_file = CreateFileA(
"minidump.dmp",
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (minidump_file == INVALID_HANDLE_VALUE) {
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
GetLastError());
return;
}
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
ExceptionParam.ThreadId = GetCurrentThreadId();
ExceptionParam.ExceptionPointers = ExceptionInfo;
ExceptionParam.ClientPointers = FALSE;
constexpr auto extended_type = static_cast<MINIDUMP_TYPE>(
MiniDumpWithUnloadedModules |
MiniDumpWithIndirectlyReferencedMemory |
MiniDumpWithProcessThreadData |
MiniDumpWithFullMemoryInfo |
MiniDumpWithThreadInfo |
MiniDumpIgnoreInaccessibleMemory);
auto written_type = extended_type;
auto minidump_written = MiniDumpWriteDump_local(
GetCurrentProcess(),
GetCurrentProcessId(),
minidump_file,
extended_type,
&ExceptionParam,
nullptr,
nullptr);
auto minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
if (!minidump_written) {
log_warning("signal", "failed to write extended minidump: 0x{:08x}; retrying MiniDumpNormal",
minidump_error);
LARGE_INTEGER file_start {};
if (SetFilePointerEx(minidump_file, file_start, nullptr, FILE_BEGIN) &&
SetEndOfFile(minidump_file)) {
written_type = MiniDumpNormal;
minidump_written = MiniDumpWriteDump_local(
GetCurrentProcess(),
GetCurrentProcessId(),
minidump_file,
MiniDumpNormal,
&ExceptionParam,
nullptr,
nullptr);
minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
} else {
minidump_error = GetLastError();
}
}
LARGE_INTEGER minidump_size {};
const auto minidump_size_available = minidump_written &&
GetFileSizeEx(minidump_file, &minidump_size);
CloseHandle(minidump_file);
if (minidump_written) {
if (minidump_size_available) {
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x}, size={} bytes)",
static_cast<unsigned>(written_type), minidump_size.QuadPart);
} else {
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x})",
static_cast<unsigned>(written_type));
}
} else {
log_warning("signal", "failed to write 'minidump.dmp': 0x{:08x}", minidump_error);
}
}
static void log_exception_chain(const struct _EXCEPTION_RECORD *record) {
constexpr size_t max_exception_chain_depth = 8;
auto *record_cause = record->ExceptionRecord;
size_t depth = 0;
while (record_cause != nullptr && depth < max_exception_chain_depth) {
struct _EXCEPTION_RECORD cause {};
SIZE_T bytes_read = 0;
if (!ReadProcessMemory(
GetCurrentProcess(),
record_cause,
&cause,
sizeof(cause),
&bytes_read) ||
bytes_read != sizeof(cause)) {
log_warning("signal", "failed to read exception chain record {} at {}: 0x{:08x}",
depth, fmt::ptr(record_cause), GetLastError());
return;
}
log_warning("signal", "caused by: {} at {}",
exception_code(&cause), fmt::ptr(cause.ExceptionAddress));
log_exception_parameters(&cause);
record_cause = cause.ExceptionRecord;
depth++;
}
if (record_cause != nullptr) {
log_warning("signal", "exception chain truncated after {} records", max_exception_chain_depth);
}
}
static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) { static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
log_info("signal", "console ctrl handler called: {}", control_code(dwCtrlType)); log_info("signal", "console ctrl handler called: {}", control_code(dwCtrlType));
@@ -90,15 +474,23 @@ static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
} }
static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) { static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) {
const auto last_error = GetLastError();
// ignore signal if disabled or no exception info provided // ignore signal if disabled or no exception info provided
if (!launcher::signal::DISABLE && ExceptionInfo != nullptr) { if (!launcher::signal::DISABLE &&
ExceptionInfo != nullptr &&
ExceptionInfo->ExceptionRecord != nullptr &&
InterlockedCompareExchange(&EXCEPTION_HANDLER_ACTIVE, 1, 0) == 0) {
// get exception record // get exception record
struct _EXCEPTION_RECORD *ExceptionRecord = ExceptionInfo->ExceptionRecord; struct _EXCEPTION_RECORD *ExceptionRecord = ExceptionInfo->ExceptionRecord;
// print signal // print signal
log_warning("signal", "exception raised: {}", exception_code(ExceptionRecord)); log_warning("signal", "exception raised: {}", exception_code(ExceptionRecord));
log_exception_context(ExceptionInfo, last_error);
// write the minidump before other diagnostics inspect potentially damaged state
write_minidump(ExceptionInfo);
switch (ExceptionRecord->ExceptionCode) { switch (ExceptionRecord->ExceptionCode) {
case EXCEPTION_ILLEGAL_INSTRUCTION: case EXCEPTION_ILLEGAL_INSTRUCTION:
@@ -128,12 +520,11 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
// (e.g., ACIO init hang due to RTSS) // (e.g., ACIO init hang due to RTSS)
deferredlogs::dump_to_logger(true); deferredlogs::dump_to_logger(true);
// walk the exception chain // dump memory information before inspecting potentially damaged exception and stack data
struct _EXCEPTION_RECORD *record_cause = ExceptionRecord->ExceptionRecord; memutils::show_available_memory();
while (record_cause != nullptr) {
log_warning("signal", "caused by: {}", exception_code(record_cause)); // walk the exception chain with bounded, checked reads
record_cause = record_cause->ExceptionRecord; log_exception_chain(ExceptionRecord);
}
// print stacktrace // print stacktrace
StackWalker sw; StackWalker sw;
@@ -142,47 +533,12 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
log_warning("signal", "failed to print callstack"); log_warning("signal", "failed to print callstack");
} }
if (MiniDumpWriteDump_local != nullptr) {
HANDLE minidump_file = CreateFileA(
"minidump.dmp",
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (minidump_file != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
ExceptionParam.ThreadId = GetCurrentThreadId();
ExceptionParam.ExceptionPointers = ExceptionInfo;
ExceptionParam.ClientPointers = FALSE;
MiniDumpWriteDump_local(
GetCurrentProcess(),
GetCurrentProcessId(),
minidump_file,
MiniDumpNormal,
&ExceptionParam,
nullptr,
nullptr);
CloseHandle(minidump_file);
} else {
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
GetLastError());
}
} else {
log_warning("signal", "minidump creation function not available, skipping");
}
// dump memory information
memutils::show_available_memory();
// this will stall all UI threads for this process // this will stall all UI threads for this process
show_popup_for_crash(); show_popup_for_crash();
log_fatal("signal", "end"); log_fatal("signal", "end");
InterlockedExchange(&EXCEPTION_HANDLER_ACTIVE, 0);
} }
return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_CONTINUE_SEARCH;
+9 -10
View File
@@ -1,6 +1,7 @@
#include "eamuse.h" #include "eamuse.h"
#include <atomic> #include <atomic>
#include <chrono>
#include <fstream> #include <fstream>
#include <thread> #include <thread>
@@ -10,7 +11,6 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/time.h" #include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
@@ -335,7 +335,6 @@ void eamuse_coin_start_thread() {
COIN_INPUT_THREAD = new std::thread([]() { COIN_INPUT_THREAD = new std::thread([]() {
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game()); auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
static bool COIN_INPUT_KEY_STATE = false; static bool COIN_INPUT_KEY_STATE = false;
timeutils::PreciseSleepTimer timer;
while (COIN_INPUT_THREAD_ACTIVE) { while (COIN_INPUT_THREAD_ACTIVE) {
// check input key // check input key
@@ -355,7 +354,7 @@ void eamuse_coin_start_thread() {
} }
// once every two frames // once every two frames
timer.sleep(1000 / 30); std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 30));
} }
}); });
} }
@@ -409,7 +408,6 @@ void eamuse_pin_macro_start_thread() {
size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()}; size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()};
std::optional<uint8_t> active_unit = std::nullopt; std::optional<uint8_t> active_unit = std::nullopt;
timeutils::PreciseSleepTimer timer;
while (PIN_MACRO_THREAD_ACTIVE) { while (PIN_MACRO_THREAD_ACTIVE) {
// wait for key press or auto-trigger // wait for key press or auto-trigger
@@ -438,7 +436,7 @@ void eamuse_pin_macro_start_thread() {
} }
if (!active_unit.has_value()) { if (!active_unit.has_value()) {
timer.sleep(20); std::this_thread::sleep_for(std::chrono::milliseconds(20));
continue; continue;
} }
} }
@@ -454,22 +452,22 @@ void eamuse_pin_macro_start_thread() {
eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]); eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]);
} }
pin_index[unit]++; pin_index[unit]++;
timer.sleep(100); std::this_thread::sleep_for(std::chrono::milliseconds(100));
// clear // clear
eamuse_set_keypad_overrides(unit, 0); eamuse_set_keypad_overrides(unit, 0);
timer.sleep(50); std::this_thread::sleep_for(std::chrono::milliseconds(50));
// end of PIN // end of PIN
if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) { if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) {
active_unit = std::nullopt; active_unit = std::nullopt;
timer.sleep(120); std::this_thread::sleep_for(std::chrono::milliseconds(120));
} }
continue; continue;
} }
timer.sleep(200); std::this_thread::sleep_for(std::chrono::milliseconds(200));
} }
}); });
} }
@@ -675,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");
+239 -6
View File
@@ -4,19 +4,44 @@
#include "wintouchemu.h" #include "wintouchemu.h"
#include <chrono>
#include <algorithm>
#include <optional>
#include <thread>
#include "cfg/screen_resize.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "games/popn/popn.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/windows/generic_sub.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
#include "rawinput/touch.h"
#include "avs/game.h"
namespace wintouchemu { namespace wintouchemu {
typedef struct {
POINT pos;
bool last_button_pressed;
DWORD touch_event;
} mouse_state_t;
// settings // settings
bool FORCE = false; bool FORCE = false;
bool INJECT_MOUSE_AS_WM_TOUCH = false;
bool LOG_FPS = false;
bool ADD_TOUCH_FLAG_PRIMARY = false; bool ADD_TOUCH_FLAG_PRIMARY = false;
// state
double last_touch_event = 0.0;
static inline bool is_emu_enabled() { static inline bool is_emu_enabled() {
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR; return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
} }
@@ -56,13 +81,16 @@ namespace wintouchemu {
// state // state
BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int); BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int);
bool USE_MOUSE = false;
std::vector<TouchEvent> TOUCH_EVENTS; std::vector<TouchEvent> TOUCH_EVENTS;
std::vector<TouchPoint> TOUCH_POINTS; std::vector<TouchPoint> TOUCH_POINTS;
HMODULE HOOKED_MODULE = nullptr; HMODULE HOOKED_MODULE = nullptr;
std::string WINDOW_TITLE_START = ""; std::string WINDOW_TITLE_START = "";
bool INITIALIZED = false; std::optional<std::string> WINDOW_TITLE_END = std::nullopt;
volatile bool INITIALIZED = false;
mouse_state_t mouse_state;
void hook(const char *window_title, HMODULE module) { void hook(const char *window_title, HMODULE module, int delay_in_s) {
// hooks // hooks
auto system_metrics_hook = detour::iat_try( auto system_metrics_hook = detour::iat_try(
@@ -81,9 +109,32 @@ namespace wintouchemu {
// set module and title // set module and title
HOOKED_MODULE = module; HOOKED_MODULE = module;
WINDOW_TITLE_START = window_title; WINDOW_TITLE_START = window_title;
if (0 < delay_in_s) {
// some games crash when touch events are injected too early during boot
std::thread t([&]() {
log_misc("wintouchemu", "defer initialization until later (with delay of {}s)", delay_in_s);
std::this_thread::sleep_for(std::chrono::seconds(delay_in_s));
log_misc("wintouchemu", "initializing", delay_in_s);
INITIALIZED = true;
});
t.detach();
} else {
log_misc("wintouchemu", "initializing"); log_misc("wintouchemu", "initializing");
INITIALIZED = true; INITIALIZED = true;
} }
}
void hook_title_ends(const char *window_title_start, const char *window_title_end, HMODULE module) {
hook(window_title_start, module);
WINDOW_TITLE_END = window_title_end;
}
static void flip_touch_points(PTOUCHINPUT point) {
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
}
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) { static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
@@ -94,6 +145,7 @@ namespace wintouchemu {
// set touch inputs // set touch inputs
bool result = false; bool result = false;
bool mouse_used = false;
for (UINT input = 0; input < cInputs; input++) { for (UINT input = 0; input < cInputs; input++) {
auto *touch_input = &pInputs[input]; auto *touch_input = &pInputs[input];
@@ -126,7 +178,13 @@ namespace wintouchemu {
// log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y)); // log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
if (overlay::OVERLAY) { if (GRAPHICS_IIDX_WSUB) {
// touch was received on subscreen window.
RECT clientRect {};
GetClientRect(TDJ_SUBSCREEN_WINDOW, &clientRect);
x = (float) x / clientRect.right * SPICETOUCH_TOUCH_WIDTH + SPICETOUCH_TOUCH_X;
y = (float) y / clientRect.bottom * SPICETOUCH_TOUCH_HEIGHT + SPICETOUCH_TOUCH_Y;
} else if (overlay::OVERLAY) {
// touch was received on global coords // touch was received on global coords
valid = overlay::OVERLAY->transform_touch_point(&x, &y); valid = overlay::OVERLAY->transform_touch_point(&x, &y);
} else { } else {
@@ -172,10 +230,89 @@ namespace wintouchemu {
touch_input->cxContact = 0; touch_input->cxContact = 0;
touch_input->cyContact = 0; touch_input->cyContact = 0;
} else { if (avs::game::is_model("KFC") &&
rawinput::touch::DISPLAY_INITIALIZED &&
rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
flip_touch_points(touch_input);
}
} else if (USE_MOUSE && !mouse_used) {
// disable further mouse inputs this call
mouse_used = true;
if (mouse_state.touch_event) {
result = true;
touch_input->x = mouse_state.pos.x;
touch_input->y = mouse_state.pos.y;
if (GRAPHICS_WINDOWED) {
touch_input->x -= SPICETOUCH_TOUCH_X;
touch_input->y -= SPICETOUCH_TOUCH_Y;
}
// log_misc(
// "wintouchemu",
// "mouse state ({}, {}) event={}",
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
auto valid = true;
if (overlay::OVERLAY) {
valid = overlay::OVERLAY->transform_touch_point(
&touch_input->x, &touch_input->y);
}
// touch inputs require 100x precision per pixel
touch_input->x *= 100;
touch_input->y *= 100;
touch_input->hSource = hTouchInput;
touch_input->dwID = 0;
touch_input->dwFlags = 0;
switch (mouse_state.touch_event) {
case TOUCHEVENTF_DOWN:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
}
break;
case TOUCHEVENTF_MOVE:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
}
break;
case TOUCHEVENTF_UP:
// don't check valid so that this touch ID can be released
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_UP;
break;
}
touch_input->dwMask = 0;
touch_input->dwTime = 0;
touch_input->dwExtraInfo = 0;
touch_input->cxContact = 0;
touch_input->cyContact = 0;
// reset it since the event was consumed & propagated as touch
mouse_state.touch_event = 0;
}
} else if (!GRAPHICS_IIDX_WSUB) {
/*
* For some reason, Nostalgia won't show an active touch point unless a move event
* triggers in the same frame. To work around this, we just supply a fake move
* event if we didn't update the same pointer ID in the same call.
*/
// beatstream requires a MOVE for active points in each update
// add one for every active point without a matching input event
// find touch point which has no associated input event // find touch point which has no associated input event
TouchPoint *touch_point = nullptr; TouchPoint *touch_point = nullptr;
for (auto &tp : TOUCH_POINTS) { for (auto &tp : TOUCH_POINTS) {
@@ -243,6 +380,21 @@ namespace wintouchemu {
title = get_window_title(hWnd); title = get_window_title(hWnd);
} }
// if a window title end is set, check to see if it matches
if (WINDOW_TITLE_END.has_value() && !string_ends_with(title.c_str(), WINDOW_TITLE_END.value().c_str())) {
hWnd = nullptr;
title = "";
for (auto &window : find_windows_beginning_with(WINDOW_TITLE_START)) {
auto check_title = get_window_title(window);
if (string_ends_with(check_title.c_str(), WINDOW_TITLE_END.value().c_str())) {
hWnd = std::move(window);
title = std::move(check_title);
break;
}
}
}
// check window // check window
if (hWnd == nullptr) { if (hWnd == nullptr) {
return; return;
@@ -250,13 +402,39 @@ namespace wintouchemu {
// check if windowed // check if windowed
if (GRAPHICS_WINDOWED) { if (GRAPHICS_WINDOWED) {
if (GRAPHICS_IIDX_WSUB) {
// no handling is needed here
// graphics::MoveWindow_hook will attach hook to windowed subscreen
log_info("wintouchemu", "attach touch hook to windowed subscreen for TDJ");
USE_MOUSE = false;
} else if (avs::game::is_model("LDJ") && !GENERIC_SUB_WINDOW_FULLSIZE) {
// overlay subscreen in IIDX
// use mouse position as ImGui overlay will block the touch window
log_info("wintouchemu", "use mouse cursor API for ldj overlay subscreen");
USE_MOUSE = true;
} else if (games::popn::is_pikapika_model()) {
// same as iidx case above
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
USE_MOUSE = true;
} else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
USE_MOUSE = true;
} else {
// create touch window - create overlay if not yet existing at this point // create touch window - create overlay if not yet existing at this point
log_info("wintouchemu", "create touch window relative to main game window"); log_info("wintouchemu", "create touch window relative to main game window");
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY); touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
USE_MOUSE = false;
}
} else if (INJECT_MOUSE_AS_WM_TOUCH) {
log_info(
"wintouchemu",
"using raw mouse cursor API in full screen and injecting them as WM_TOUCH events");
USE_MOUSE = true;
} else { } else {
log_info("wintouchemu", "activating DirectX hooks"); log_info("wintouchemu", "activating DirectX hooks");
// mouse position based input only // mouse position based input only
touch_attach_dx_hook(); touch_attach_dx_hook();
USE_MOUSE = false;
} }
// hooks // hooks
@@ -267,6 +445,8 @@ namespace wintouchemu {
} }
} }
const auto now = get_performance_milliseconds();
// update touch events // update touch events
if (hWnd != nullptr) { if (hWnd != nullptr) {
@@ -286,11 +466,64 @@ namespace wintouchemu {
// check if new events are available // check if new events are available
if (event_count > 0) { if (event_count > 0) {
last_touch_event = now;
// send fake event to make the game update it's touch inputs // send fake event to make the game update it's touch inputs
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC); auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook); wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook);
} }
// update frame logging
if (LOG_FPS) {
static int log_frames = 0;
static uint64_t log_time = 0;
log_frames++;
if (log_time < get_system_seconds()) {
if (log_time > 0) {
log_info("wintouchemu", "polling at {} touch frames per second", log_frames);
}
log_frames = 0;
log_time = get_system_seconds();
}
}
}
// send separate WM_TOUCH event for mouse
// this must be separate from actual touch events because some games will ignore the return
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to
// send empty events when the mouse button is not clicked/released
if (hWnd != nullptr && USE_MOUSE) {
bool button_pressed = get_async_primary_mouse();
// while the subscreen overlay cannot accept the mouse, treat the button as
// released; this ends any active touch and prevents new mouse touch contacts
if (overlay::OVERLAY &&
overlay::OVERLAY->has_subscreen_touch_transform() &&
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
button_pressed = false;
}
// if there was a touch event in the last 500 ms, don't insert new button presses
if (button_pressed && (now - last_touch_event) < 500) {
button_pressed = false;
}
// figure out what kind of touch event to simulate
if (button_pressed && !mouse_state.last_button_pressed) {
mouse_state.touch_event = TOUCHEVENTF_DOWN;
} else if (button_pressed && mouse_state.last_button_pressed) {
mouse_state.touch_event = TOUCHEVENTF_MOVE;
} else if (!button_pressed && mouse_state.last_button_pressed) {
mouse_state.touch_event = TOUCHEVENTF_UP;
}
mouse_state.last_button_pressed = button_pressed;
if (mouse_state.touch_event) {
GetCursorPos(&mouse_state.pos);
// send fake event to make the game update it's touch inputs
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
wndProc(hWnd, WM_TOUCH, MAKEWORD(1, 0), (LPARAM) GetTouchInputInfoHook);
}
} }
} }
} }
+7 -1
View File
@@ -6,8 +6,14 @@ namespace wintouchemu {
// settings // settings
extern bool FORCE; extern bool FORCE;
extern bool INJECT_MOUSE_AS_WM_TOUCH;
extern bool LOG_FPS;
extern bool ADD_TOUCH_FLAG_PRIMARY; extern bool ADD_TOUCH_FLAG_PRIMARY;
void hook(const char *window_title, HMODULE module = nullptr); void hook(const char *window_title, HMODULE module = nullptr, int delay_in_s=0);
void hook_title_ends(
const char *window_title_start,
const char *window_title_ends,
HMODULE module = nullptr);
void update(); void update();
} }
+11
View File
@@ -908,6 +908,17 @@ bool overlay::SpiceOverlay::get_active() {
return this->active; return this->active;
} }
bool overlay::SpiceOverlay::is_subscreen_overlay_visible() {
return this->get_active() &&
this->window_sub != nullptr &&
this->window_sub->get_active();
}
bool overlay::SpiceOverlay::accepts_subscreen_mouse_input() {
return this->is_subscreen_overlay_visible() &&
this->window_sub->is_mouse_hovered();
}
bool overlay::SpiceOverlay::has_focus() { bool overlay::SpiceOverlay::has_focus() {
return this->get_active() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow); return this->get_active() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow);
} }
+9 -7
View File
@@ -109,6 +109,7 @@ namespace overlay {
void show_main_menu(); void show_main_menu();
void set_active(bool active); void set_active(bool active);
bool get_active(); bool get_active();
bool accepts_subscreen_mouse_input();
bool has_focus(); bool has_focus();
bool hotkeys_triggered(); bool hotkeys_triggered();
@@ -143,20 +144,20 @@ namespace overlay {
return this->hWnd; return this->hWnd;
} }
bool can_transform_touch_input() { bool has_subscreen_touch_transform() {
return (this->subscreen_mouse_handler != nullptr); return (this->subscreen_touch_transform != nullptr);
} }
bool transform_touch_point(LONG *x, LONG *y) { bool transform_touch_point(LONG *x, LONG *y) {
if (this->get_active() && this->subscreen_mouse_handler) { if (this->get_active() && this->subscreen_touch_transform) {
return this->subscreen_mouse_handler(x, y); return this->subscreen_touch_transform(x, y);
} else { } else {
return true; return true;
} }
} }
void set_subscreen_mouse_handler(const std::function<bool(LONG *, LONG *)> &f) { void set_subscreen_touch_transform(const std::function<bool(LONG *, LONG *)> &f) {
this->subscreen_mouse_handler = f; this->subscreen_touch_transform = f;
} }
// renderer // renderer
@@ -203,7 +204,7 @@ namespace overlay {
std::vector<std::unique_ptr<Window>> windows; std::vector<std::unique_ptr<Window>> windows;
std::function<bool(LONG *, LONG *)> subscreen_mouse_handler = nullptr; std::function<bool(LONG *, LONG *)> subscreen_touch_transform = nullptr;
bool active = false; bool active = false;
bool toggle_down = false; bool toggle_down = false;
@@ -216,6 +217,7 @@ namespace overlay {
// so render() never runs without a matching NewFrame. // so render() never runs without a matching NewFrame.
bool has_pending_frame = false; bool has_pending_frame = false;
bool is_subscreen_overlay_visible();
void init(); void init();
void add_font(const char* font, ImFontConfig* config, const ImWchar* glyphs); void add_font(const char* font, ImFontConfig* config, const ImWchar* glyphs);
}; };
+18 -2
View File
@@ -69,6 +69,7 @@ void overlay::Window::build() {
// check if active // check if active
if (!this->active) { if (!this->active) {
this->mouse_hovered = false;
return; return;
} }
@@ -106,10 +107,18 @@ void overlay::Window::build() {
// top spot. suppress that so only an explicit focus request (see below) // top spot. suppress that so only an explicit focus request (see below)
// decides what comes to the front. // decides what comes to the front.
const std::string window_id = this->title + "###" + to_string(this); const std::string window_id = this->title + "###" + to_string(this);
if (ImGui::Begin( const bool build_contents = ImGui::Begin(
window_id.c_str(), window_id.c_str(),
&this->active, &this->active,
this->flags | ImGuiWindowFlags_NoFocusOnAppearing)) { this->flags | ImGuiWindowFlags_NoFocusOnAppearing);
// keep the window hovered while its invisible input button is held during a drag
this->mouse_hovered =
ImGui::IsWindowHovered(
ImGuiHoveredFlags_RootAndChildWindows |
ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
if (build_contents) {
// window attributes - init_pos / init_size are only honored once // window attributes - init_pos / init_size are only honored once
// (ImGuiCond_Once), so compute them a single time instead of every frame // (ImGuiCond_Once), so compute them a single time instead of every frame
@@ -154,6 +163,9 @@ void overlay::Window::build() {
} else { } else {
// raw content does not have an ImGui window to hover
this->mouse_hovered = false;
// add raw content // add raw content
this->build_content(); this->build_content();
} }
@@ -201,3 +213,7 @@ bool overlay::Window::get_active() {
// now it depends on us // now it depends on us
return this->active; return this->active;
} }
bool overlay::Window::is_mouse_hovered() {
return this->mouse_hovered;
}
+2
View File
@@ -24,6 +24,7 @@ namespace overlay {
void toggle_active(); void toggle_active();
void set_active(bool active); void set_active(bool active);
bool get_active(); bool get_active();
bool is_mouse_hovered();
// raise this window to the top of the z-order on its next build. only // raise this window to the top of the z-order on its next build. only
// call in response to an explicit user action (keyboard toggle, UI // call in response to an explicit user action (keyboard toggle, UI
@@ -37,6 +38,7 @@ namespace overlay {
// set when the window transitions to visible so build() raises it to the // set when the window transitions to visible so build() raises it to the
// top of the z-order on the next frame // top of the z-order on the next frame
bool request_focus = false; bool request_focus = false;
bool mouse_hovered = false;
std::vector<Window*> children; std::vector<Window*> children;
// settings // settings
+26 -10
View File
@@ -82,9 +82,14 @@ namespace overlay::windows {
ImGui::Text("Rendering"); ImGui::Text("Rendering");
// Media Type Selector // Media Type Selector
int selectedMediaTypeIndex = selectedCamera->m_selectedMediaTypeIndex; int selectedMediaTypeIndex = selectedCamera->GetSelectedMediaTypeIndex();
auto numMediaTypes = selectedCamera->m_mediaTypeInfos.size(); auto numMediaTypes = selectedCamera->m_mediaTypeInfos.size();
if (numMediaTypes == 0) {
ImGui::TextColored(ImVec4(1.f, 1.f, 0.f, 1.f), "%s", "No supported media types");
return;
}
auto selectedMediaTypeIndexChanged = ImGui::BeginCombo( auto selectedMediaTypeIndexChanged = ImGui::BeginCombo(
"Media Type", selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).description.c_str() "Media Type", selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).description.c_str()
); );
@@ -103,9 +108,9 @@ namespace overlay::windows {
ImGui::EndCombo(); ImGui::EndCombo();
} }
if (selectedMediaTypeIndexChanged && selectedMediaTypeIndex != selectedCamera->m_selectedMediaTypeIndex) { if (selectedMediaTypeIndexChanged && selectedMediaTypeIndex != selectedCamera->GetSelectedMediaTypeIndex()) {
selectedCamera->m_useAutoMediaType = false; selectedCamera->m_useAutoMediaType = false;
selectedCamera->ChangeMediaType(selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).p_mediaType); selectedCamera->RequestMediaType(selectedCamera->m_mediaTypeInfos.at(selectedMediaTypeIndex).p_mediaType);
} }
// Auto media type // Auto media type
@@ -114,13 +119,14 @@ namespace overlay::windows {
if (ImGui::Checkbox("Auto##MediaType", &isAutoMediaType)) { if (ImGui::Checkbox("Auto##MediaType", &isAutoMediaType)) {
selectedCamera->m_useAutoMediaType = isAutoMediaType; selectedCamera->m_useAutoMediaType = isAutoMediaType;
if (isAutoMediaType) { if (isAutoMediaType) {
selectedCamera->ChangeMediaType(selectedCamera->m_pAutoMediaType); selectedCamera->RequestMediaType(selectedCamera->m_pAutoMediaType);
} }
} }
// Draw mode // Draw mode
int selectedDrawModeIndex = selectedCamera->m_drawMode; const auto drawMode = selectedCamera->m_drawMode.load();
if (ImGui::BeginCombo("Draw Mode", DRAW_MODE_LABELS[selectedCamera->m_drawMode].c_str())) { int selectedDrawModeIndex = drawMode;
if (ImGui::BeginCombo("Draw Mode", DRAW_MODE_LABELS[drawMode].c_str())) {
for (size_t i = 0; i < DRAW_MODE_SIZE; i++) { for (size_t i = 0; i < DRAW_MODE_SIZE; i++) {
const bool is_selected = (selectedDrawModeIndex == (int) i); const bool is_selected = (selectedDrawModeIndex == (int) i);
if (ImGui::Selectable(DRAW_MODE_LABELS[i].c_str(), is_selected)) { if (ImGui::Selectable(DRAW_MODE_LABELS[i].c_str(), is_selected)) {
@@ -142,15 +148,25 @@ namespace overlay::windows {
"Letterbox to 4:3: Like Letterbox, but target 4:3" "Letterbox to 4:3: Like Letterbox, but target 4:3"
); );
if (selectedDrawModeIndex != selectedCamera->m_drawMode) { if (selectedDrawModeIndex != selectedCamera->m_drawMode.load()) {
selectedCamera->m_drawMode = (LocalCameraDrawMode)selectedDrawModeIndex; selectedCamera->m_drawMode.store((LocalCameraDrawMode)selectedDrawModeIndex);
selectedCamera->UpdateDrawRect(); selectedCamera->UpdateDrawRect();
} }
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::Checkbox("Horizontal Flip", &selectedCamera->m_flipHorizontal); bool flipHorizontal = selectedCamera->m_flipHorizontal.load();
if (ImGui::Checkbox("Horizontal Flip", &flipHorizontal)) {
selectedCamera->m_flipHorizontal.store(flipHorizontal);
}
ImGui::SameLine(); ImGui::SameLine();
ImGui::Checkbox("Vertical Flip", &selectedCamera->m_flipVertical); bool flipVertical = selectedCamera->m_flipVertical.load();
if (ImGui::Checkbox("Vertical Flip", &flipVertical)) {
selectedCamera->m_flipVertical.store(flipVertical);
}
ImGui::SameLine();
ImGui::HelpMarker(
"Flips are done by the CPU and can be very expensive, potentially causing frame drops! "
"Prefer to use camera's built-in flip if available.");
// Camera control parameters // Camera control parameters
ImGui::Separator(); ImGui::Separator();
+1 -1
View File
@@ -42,7 +42,7 @@ namespace overlay::windows {
this->texture_width = 0; this->texture_width = 0;
this->texture_height = 0; this->texture_height = 0;
overlay->set_subscreen_mouse_handler([this](LONG *x, LONG *y) -> bool { overlay->set_subscreen_touch_transform([this](LONG *x, LONG *y) -> bool {
// convert to normalized form (relative window coordinates 0.f-1.f) // convert to normalized form (relative window coordinates 0.f-1.f)
ImVec2 xy; ImVec2 xy;
+12 -5
View File
@@ -4,7 +4,6 @@
#include <thread> #include <thread>
#include <mutex> #include <mutex>
#include <vector> #include <vector>
#include <map>
#include <windows.h> #include <windows.h>
@@ -103,6 +102,13 @@ namespace rawinput {
std::vector<HIDTouchPoint> touch_points; std::vector<HIDTouchPoint> touch_points;
}; };
struct HIDButtonInputGroup {
USAGE usage_page;
USHORT link_collection;
std::vector<size_t> cap_indices;
std::vector<USAGE> usages;
};
struct DeviceHIDInfo { struct DeviceHIDInfo {
HANDLE handle; HANDLE handle;
_HIDP_CAPS caps; _HIDP_CAPS caps;
@@ -125,11 +131,12 @@ namespace rawinput {
std::vector<std::vector<bool>> button_states; std::vector<std::vector<bool>> button_states;
std::vector<std::vector<double>> button_up, button_down; std::vector<std::vector<double>> button_up, button_down;
std::vector<std::vector<uint8_t>> button_report_states;
std::vector<std::vector<bool>> button_output_states; std::vector<std::vector<bool>> button_output_states;
std::vector<HIDButtonInputGroup> button_input_groups;
// key: usage page, link collection std::vector<CHAR> output_report;
// value: number of buttons for that key (combine ranges and nonranges) std::vector<USAGE> button_output_usages;
std::map<std::pair<USAGE, ULONG>, ULONG> button_usage_pages; std::vector<USAGE> button_output_usages_off;
std::vector<float> value_states; std::vector<float> value_states;
std::vector<LONG> value_states_raw; std::vector<LONG> value_states_raw;
+86 -57
View File
@@ -1,6 +1,10 @@
#include "rawinput.h" #include "rawinput.h"
#include <algorithm>
#include <chrono>
#include <cstdarg> #include <cstdarg>
#include <map>
#include <thread>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -9,7 +13,6 @@
#include "util/logging.h" #include "util/logging.h"
#include "external/robin_hood.h" #include "external/robin_hood.h"
#include "util/precise_timer.h"
#include "util/time.h" #include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
@@ -169,9 +172,8 @@ void rawinput::RawInputManager::input_hwnd_create() {
}); });
// wait for window creation being done // wait for window creation being done
timeutils::PreciseSleepTimer timer;
while (!this->input_hwnd) { while (!this->input_hwnd) {
timer.sleep(1); std::this_thread::sleep_for(std::chrono::milliseconds(1));
} }
} }
@@ -439,7 +441,9 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
std::vector<std::string> button_caps_names; std::vector<std::string> button_caps_names;
std::vector<std::vector<bool>> button_states; std::vector<std::vector<bool>> button_states;
std::vector<std::vector<double>> button_up, button_down; std::vector<std::vector<double>> button_up, button_down;
std::map<std::pair<USAGE, ULONG>, ULONG> button_usage_pages; std::vector<std::vector<uint8_t>> button_report_states;
std::vector<HIDButtonInputGroup> button_input_groups;
std::map<std::pair<USAGE, ULONG>, size_t> button_input_group_indices;
for (int button_cap_num = 0; button_cap_num < button_cap_length; button_cap_num++) { for (int button_cap_num = 0; button_cap_num < button_cap_length; button_cap_num++) {
auto &button_caps = button_cap_data[button_cap_num]; auto &button_caps = button_cap_data[button_cap_num];
@@ -458,7 +462,7 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1; int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1;
// ignore bad ranges reported by bad devices // ignore bad ranges reported by bad devices
if (button_count >= 0xffff) { if (button_count <= 0 || button_count >= 0xffff) {
log_warning("rawinput", "skipping bad button cap range for device {}, range [{}, {}]", log_warning("rawinput", "skipping bad button cap range for device {}, range [{}, {}]",
device_name, device_name,
button_caps.Range.UsageMin, button_caps.Range.UsageMin,
@@ -467,11 +471,30 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
} }
// fill vectors // fill vectors
const size_t cap_index = button_caps_list.size();
button_caps_list.emplace_back(button_caps); button_caps_list.emplace_back(button_caps);
button_states.emplace_back(std::vector<bool>(static_cast<unsigned int>(button_count), false)); button_states.emplace_back(std::vector<bool>(static_cast<unsigned int>(button_count), false));
button_up.emplace_back(std::vector<double>(static_cast<unsigned int>(button_count), 0.0)); button_up.emplace_back(std::vector<double>(static_cast<unsigned int>(button_count), 0.0));
button_down.emplace_back(std::vector<double>(static_cast<unsigned int>(button_count), 0.0)); button_down.emplace_back(std::vector<double>(static_cast<unsigned int>(button_count), 0.0));
button_usage_pages[std::make_pair(button_caps.UsagePage, button_caps.LinkCollection)] += button_count; button_report_states.emplace_back(static_cast<size_t>(button_count), 0);
const auto group_key = std::make_pair(
button_caps.UsagePage,
static_cast<ULONG>(button_caps.LinkCollection));
auto [group_it, inserted] = button_input_group_indices.try_emplace(
group_key,
button_input_groups.size());
if (inserted) {
button_input_groups.emplace_back(HIDButtonInputGroup {
.usage_page = button_caps.UsagePage,
.link_collection = button_caps.LinkCollection,
.cap_indices = {},
.usages = {},
});
}
auto &input_group = button_input_groups[group_it->second];
input_group.cap_indices.push_back(cap_index);
input_group.usages.resize(input_group.usages.size() + button_count);
// names // names
for (USAGE usg = button_caps.Range.UsageMin; usg <= button_caps.Range.UsageMax; usg++) { for (USAGE usg = button_caps.Range.UsageMin; usg <= button_caps.Range.UsageMax; usg++) {
@@ -512,7 +535,7 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1; int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1;
// ignore bad ranges reported by bad devices // ignore bad ranges reported by bad devices
if (button_count >= 0xffff) { if (button_count <= 0 || button_count >= 0xffff) {
log_warning("rawinput", "skipping bad button output cap range for device {}, range [{}, {}]", log_warning("rawinput", "skipping bad button output cap range for device {}, range [{}, {}]",
device_name, device_name,
button_caps.Range.UsageMin, button_caps.Range.UsageMin,
@@ -800,8 +823,18 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
new_device.hidInfo->button_states = std::move(button_states); new_device.hidInfo->button_states = std::move(button_states);
new_device.hidInfo->button_up = std::move(button_up); new_device.hidInfo->button_up = std::move(button_up);
new_device.hidInfo->button_down = std::move(button_down); new_device.hidInfo->button_down = std::move(button_down);
new_device.hidInfo->button_report_states = std::move(button_report_states);
new_device.hidInfo->button_output_states = std::move(button_output_states); new_device.hidInfo->button_output_states = std::move(button_output_states);
new_device.hidInfo->button_usage_pages = std::move(button_usage_pages); new_device.hidInfo->button_input_groups = std::move(button_input_groups);
new_device.hidInfo->output_report.resize(caps.OutputReportByteLength);
size_t max_button_output_count = 0;
for (const auto &button_output_state : new_device.hidInfo->button_output_states) {
if (button_output_state.size() > max_button_output_count) {
max_button_output_count = button_output_state.size();
}
}
new_device.hidInfo->button_output_usages.reserve(max_button_output_count);
new_device.hidInfo->button_output_usages_off.reserve(max_button_output_count);
new_device.hidInfo->value_states = std::move(value_states); new_device.hidInfo->value_states = std::move(value_states);
new_device.hidInfo->value_states_raw = std::move(value_states_raw); new_device.hidInfo->value_states_raw = std::move(value_states_raw);
new_device.hidInfo->value_output_states = std::move(value_output_states); new_device.hidInfo->value_output_states = std::move(value_output_states);
@@ -1573,11 +1606,17 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
if (!data_size) { if (!data_size) {
break; break;
} }
std::shared_ptr<RAWINPUT> data((RAWINPUT *) new char[data_size]{}); thread_local std::vector<RAWINPUT> data_buffer;
const size_t data_count =
(data_size + sizeof(RAWINPUT) - 1) / sizeof(RAWINPUT);
if (data_buffer.size() < data_count) {
data_buffer.resize(data_count);
}
auto data = data_buffer.data();
if (GetRawInputData( if (GetRawInputData(
(HRAWINPUT) lParam, (HRAWINPUT) lParam,
RID_INPUT, RID_INPUT,
data.get(), data,
&data_size, &data_size,
sizeof(RAWINPUTHEADER)) != data_size) { sizeof(RAWINPUTHEADER)) != data_size) {
break; break;
@@ -1791,17 +1830,13 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
+ (size_t) hid_report_index * data_hid.dwSizeHid; + (size_t) hid_report_index * data_hid.dwSizeHid;
// parse reports // parse reports
for (const auto &pair : device.hidInfo->button_usage_pages) { for (auto &input_group : device.hidInfo->button_input_groups) {
const auto usage_page = pair.first.first; auto &usages = input_group.usages;
const auto link_collection = pair.first.second; ULONG usages_length = static_cast<ULONG>(usages.size());
const auto button_count = pair.second;
ULONG usages_length = button_count;
std::vector<USAGE> usages(static_cast<size_t>(usages_length));
if (HidP_GetUsages( if (HidP_GetUsages(
HidP_Input, HidP_Input,
usage_page, input_group.usage_page,
link_collection, input_group.link_collection,
usages.data(), usages.data(),
&usages_length, &usages_length,
reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()), reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()),
@@ -1819,29 +1854,22 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
// log_info( // log_info(
// "rawinput", // "rawinput",
// "processing HID input for device {}, usage page {:x} and link collection {:x} with {} buttons, got {} reports", // "processing HID input for device {}, usage page {:x} and link collection {:x} with {} buttons, got {} reports",
// device.desc, // device.desc, input_group.usage_page,
// usage_page, link_collection, button_count, usages_length); // input_group.link_collection, usages.size(), usages_length);
// buttons // buttons
for (size_t cap_num = 0; cap_num < device.hidInfo->button_caps_list.size(); cap_num++) { for (const size_t cap_num : input_group.cap_indices) {
auto &button_caps = device.hidInfo->button_caps_list[cap_num]; auto &button_caps = device.hidInfo->button_caps_list[cap_num];
auto &button_states = device.hidInfo->button_states[cap_num]; auto &button_states = device.hidInfo->button_states[cap_num];
auto &button_down = device.hidInfo->button_down[cap_num]; auto &button_down = device.hidInfo->button_down[cap_num];
auto &button_up = device.hidInfo->button_up[cap_num]; auto &button_up = device.hidInfo->button_up[cap_num];
auto &new_states = device.hidInfo->button_report_states[cap_num];
// is this the right usage page and link collection?
if (button_caps.UsagePage != usage_page || button_caps.LinkCollection != link_collection) {
continue;
}
// get button count // get button count
int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1; int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1;
if (button_count <= 0) {
continue;
}
// update buttons // update buttons
std::vector<bool> new_states(button_count); std::fill(new_states.begin(), new_states.end(), 0);
for (ULONG usage_num = 0; usage_num < usages_length; usage_num++) { for (ULONG usage_num = 0; usage_num < usages_length; usage_num++) {
if (usages[usage_num] < button_caps.Range.UsageMin || if (usages[usage_num] < button_caps.Range.UsageMin ||
usages[usage_num] > button_caps.Range.UsageMax) { usages[usage_num] > button_caps.Range.UsageMax) {
@@ -1852,17 +1880,18 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
// guard against some buggy device sending an event for a usage below UsageMin // guard against some buggy device sending an event for a usage below UsageMin
if (usage < button_count) { if (usage < button_count) {
new_states[usage] = true; new_states[usage] = 1;
} }
} }
for (int button_num = 0; button_num < button_count; button_num++) { for (int button_num = 0; button_num < button_count; button_num++) {
if (!new_states[button_num] && button_states[button_num]) { const bool new_state = new_states[button_num] != 0;
if (!new_state && button_states[button_num]) {
device.updated = true; device.updated = true;
button_states[button_num] = new_states[button_num]; button_states[button_num] = new_state;
button_down[button_num] = input_time; button_down[button_num] = input_time;
} else if (new_states[button_num] && !button_states[button_num]) { } else if (new_state && !button_states[button_num]) {
device.updated = true; device.updated = true;
button_states[button_num] = new_states[button_num]; button_states[button_num] = new_state;
button_up[button_num] = input_time; button_up[button_num] = input_time;
} }
} }
@@ -2017,8 +2046,10 @@ void rawinput::RawInputManager::device_write_output(Device *device, bool only_up
switch (hid->driver) { switch (hid->driver) {
case HIDDriver::Default: { case HIDDriver::Default: {
// allocate report auto &report_data = hid->output_report;
CHAR *report_data = new CHAR[hid->caps.OutputReportByteLength] {}; auto &usage_list = hid->button_output_usages;
auto &usage_off_list = hid->button_output_usages_off;
std::fill(report_data.begin(), report_data.end(), 0);
// set buttons // set buttons
for (size_t cap_no = 0; cap_no < hid->button_output_caps_list.size(); cap_no++) { for (size_t cap_no = 0; cap_no < hid->button_output_caps_list.size(); cap_no++) {
@@ -2026,10 +2057,8 @@ void rawinput::RawInputManager::device_write_output(Device *device, bool only_up
auto &button_state_list = hid->button_output_states[cap_no]; auto &button_state_list = hid->button_output_states[cap_no];
// determine which buttons to turn on // determine which buttons to turn on
std::vector<USAGE> usage_list; usage_list.clear();
std::vector<USAGE> usage_off_list; usage_off_list.clear();
usage_list.reserve(button_state_list.size());
usage_off_list.reserve(button_state_list.size());
for (size_t state_no = 0; state_no < button_state_list.size(); state_no++) { for (size_t state_no = 0; state_no < button_state_list.size(); state_no++) {
if (button_state_list[state_no]) { if (button_state_list[state_no]) {
usage_list.push_back(button_cap.Range.UsageMin + (USAGE) state_no); usage_list.push_back(button_cap.Range.UsageMin + (USAGE) state_no);
@@ -2044,15 +2073,18 @@ void rawinput::RawInputManager::device_write_output(Device *device, bool only_up
HidP_Output, HidP_Output,
button_cap.UsagePage, button_cap.UsagePage,
button_cap.LinkCollection, button_cap.LinkCollection,
&usage_list[0], usage_list.data(),
&usage_list_length, &usage_list_length,
reinterpret_cast<PHIDP_PREPARSED_DATA>(hid->preparsed_data.get()), reinterpret_cast<PHIDP_PREPARSED_DATA>(hid->preparsed_data.get()),
report_data, report_data.data(),
hid->caps.OutputReportByteLength) == HIDP_STATUS_INCOMPATIBLE_REPORT_ID) { hid->caps.OutputReportByteLength) == HIDP_STATUS_INCOMPATIBLE_REPORT_ID) {
// flush report // flush report
HidD_SetOutputReport(hid->handle, report_data, hid->caps.OutputReportByteLength); HidD_SetOutputReport(
memset(report_data, 0, hid->caps.OutputReportByteLength); hid->handle,
report_data.data(),
hid->caps.OutputReportByteLength);
std::fill(report_data.begin(), report_data.end(), 0);
} }
// clear the buttons // clear the buttons
@@ -2061,22 +2093,22 @@ void rawinput::RawInputManager::device_write_output(Device *device, bool only_up
HidP_Output, HidP_Output,
button_cap.UsagePage, button_cap.UsagePage,
button_cap.LinkCollection, button_cap.LinkCollection,
&usage_off_list[0], usage_off_list.data(),
&usage_off_list_length, &usage_off_list_length,
reinterpret_cast<PHIDP_PREPARSED_DATA>(hid->preparsed_data.get()), reinterpret_cast<PHIDP_PREPARSED_DATA>(hid->preparsed_data.get()),
report_data, report_data.data(),
hid->caps.OutputReportByteLength) == HIDP_STATUS_INCOMPATIBLE_REPORT_ID) { hid->caps.OutputReportByteLength) == HIDP_STATUS_INCOMPATIBLE_REPORT_ID) {
// flush report // flush report
DWORD written_bytes = 0; DWORD written_bytes = 0;
WriteFile( WriteFile(
hid->handle, hid->handle,
reinterpret_cast<void *>(report_data), report_data.data(),
hid->caps.OutputReportByteLength, hid->caps.OutputReportByteLength,
&written_bytes, &written_bytes,
nullptr nullptr
); );
memset(report_data, 0, hid->caps.OutputReportByteLength); std::fill(report_data.begin(), report_data.end(), 0);
} }
} }
@@ -2105,19 +2137,19 @@ void rawinput::RawInputManager::device_write_output(Device *device, bool only_up
value_cap.NotRange.Usage, value_cap.NotRange.Usage,
static_cast<ULONG>(usage_value), static_cast<ULONG>(usage_value),
reinterpret_cast<PHIDP_PREPARSED_DATA>(hid->preparsed_data.get()), reinterpret_cast<PHIDP_PREPARSED_DATA>(hid->preparsed_data.get()),
report_data, report_data.data(),
hid->caps.OutputReportByteLength) == HIDP_STATUS_INCOMPATIBLE_REPORT_ID) { hid->caps.OutputReportByteLength) == HIDP_STATUS_INCOMPATIBLE_REPORT_ID) {
// flush report // flush report
DWORD written_bytes = 0; DWORD written_bytes = 0;
WriteFile( WriteFile(
hid->handle, hid->handle,
reinterpret_cast<void *>(report_data), report_data.data(),
hid->caps.OutputReportByteLength, hid->caps.OutputReportByteLength,
&written_bytes, &written_bytes,
nullptr nullptr
); );
memset(report_data, 0, hid->caps.OutputReportByteLength); std::fill(report_data.begin(), report_data.end(), 0);
} }
} }
@@ -2151,15 +2183,12 @@ void rawinput::RawInputManager::device_write_output(Device *device, bool only_up
DWORD written_bytes = 0; DWORD written_bytes = 0;
WriteFile( WriteFile(
hid->handle, hid->handle,
reinterpret_cast<void *>(report_data), report_data.data(),
hid->caps.OutputReportByteLength, hid->caps.OutputReportByteLength,
&written_bytes, &written_bytes,
nullptr nullptr
); );
// delete report
delete[] report_data;
break; break;
} }
case HIDDriver::PacDrive: { case HIDDriver::PacDrive: {
+14 -5
View File
@@ -319,7 +319,8 @@ namespace rawinput::touch {
} }
// iterate all input data and get touch points // iterate all input data and get touch points
std::vector<HIDTouchPoint> touch_points; thread_local std::vector<HIDTouchPoint> touch_points;
touch_points.clear();
touch_points.reserve(touch.elements_x.size()); touch_points.reserve(touch.elements_x.size());
for (size_t i = 0; i < touch.elements_x.size(); i++) { for (size_t i = 0; i < touch.elements_x.size(); i++) {
@@ -424,9 +425,15 @@ namespace rawinput::touch {
} }
// process touch points // process touch points
std::vector<DWORD> touch_removes; thread_local std::vector<DWORD> touch_removes;
std::vector<TouchPoint> touch_writes; thread_local std::vector<TouchPoint> touch_writes;
std::vector<DWORD> touch_modifications; thread_local std::vector<DWORD> touch_modifications;
touch_removes.clear();
touch_writes.clear();
touch_modifications.clear();
touch_removes.reserve(touch.touch_points.size() + touch_points.size());
touch_writes.reserve(touch_points.size());
touch_modifications.reserve(touch_points.size());
// drop every touch point with the given id (marking it as released) // drop every touch point with the given id (marking it as released)
auto remove_touch_point = [&] (DWORD id) { auto remove_touch_point = [&] (DWORD id) {
@@ -581,7 +588,9 @@ namespace rawinput::touch {
auto deadline = get_system_milliseconds() - 50; auto deadline = get_system_milliseconds() - 50;
// check touch points // check touch points
std::vector<DWORD> touch_removes; thread_local std::vector<DWORD> touch_removes;
touch_removes.clear();
touch_removes.reserve(touch.touch_points.size());
auto touch_it = touch.touch_points.begin(); auto touch_it = touch.touch_points.begin();
while (touch_it != touch.touch_points.end()) { while (touch_it != touch.touch_points.end()) {
auto &hid_tp = *touch_it; auto &hid_tp = *touch_it;
+7 -11
View File
@@ -1,13 +1,13 @@
#include "reader.h" #include "reader.h"
#include <chrono>
#include <cstring>
#include <filesystem> #include <filesystem>
#include <thread> #include <thread>
#include <cstring>
#include <vector> #include <vector>
#include "util/logging.h" #include "util/logging.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "structuredmessage.h" #include "structuredmessage.h"
@@ -218,8 +218,6 @@ bool Reader::set_comm_state(DWORD BaudRate) {
} }
bool Reader::wait_for_handshake() { bool Reader::wait_for_handshake() {
timeutils::PreciseSleepTimer timer;
// baud rates // baud rates
DWORD baud_rates[] = { CBR_57600, CBR_38400, CBR_19200, CBR_9600 }; DWORD baud_rates[] = { CBR_57600, CBR_38400, CBR_19200, CBR_9600 };
@@ -270,7 +268,7 @@ bool Reader::wait_for_handshake() {
} }
// sleep // sleep
timer.sleep(50); std::this_thread::sleep_for(std::chrono::milliseconds(50));
} }
log_warning("reader", "{}: no handshake received for {} baud", this->port, baud_rates[i]); log_warning("reader", "{}: no handshake received for {} baud", this->port, baud_rates[i]);
@@ -407,7 +405,6 @@ void start_reader_thread(const std::string &port, int id) {
READER_THREAD_RUNNING = true; READER_THREAD_RUNNING = true;
READER_THREADS.push_back(new std::thread([port, id]() { READER_THREADS.push_back(new std::thread([port, id]() {
log_info("reader", "{}: starting reader thread", port); log_info("reader", "{}: starting reader thread", port);
timeutils::PreciseSleepTimer timer;
while (READER_THREAD_RUNNING) { while (READER_THREAD_RUNNING) {
@@ -445,22 +442,21 @@ void start_reader_thread(const std::string &port, int id) {
} }
if (did_read_card) { if (did_read_card) {
timer.sleep(2500); std::this_thread::sleep_for(std::chrono::milliseconds(2500));
} }
timer.sleep(20); std::this_thread::sleep_for(std::chrono::milliseconds(20));
} }
} }
// sleep between reader connection retries // sleep between reader connection retries
if (READER_THREAD_RUNNING) if (READER_THREAD_RUNNING)
timer.sleep(5000); std::this_thread::sleep_for(std::chrono::milliseconds(5000));
} }
})); }));
// wait for thread to start // wait for thread to start
timeutils::PreciseSleepTimer timer; std::this_thread::sleep_for(std::chrono::milliseconds(10));
timer.sleep(10);
} }
void stop_reader_thread() { void stop_reader_thread() {
+34 -16
View File
@@ -338,9 +338,7 @@ namespace nativetouch::inject {
// attach mouse injection without replacing the window's existing procedure // attach mouse injection without replacing the window's existing procedure
static void attach_window_impl(HWND window, bool register_touch) { static void attach_window_impl(HWND window, bool register_touch) {
initialize_touch_injection(); if (!initialize_touch_injection()) {
if (!touch_injection_available()) {
if (register_touch) { if (register_touch) {
log_warning( log_warning(
"touch::native", "touch::native",
@@ -422,7 +420,7 @@ namespace nativetouch::inject {
} }
// load and initialize Windows 8 touch injection without a static API dependency // load and initialize Windows 8 touch injection without a static API dependency
void initialize_touch_injection() { bool initialize_touch_injection() {
std::call_once(initialization_once, [] { std::call_once(initialization_once, [] {
initialize_synthetic_touch(); initialize_synthetic_touch();
contact_refresh_message = contact_refresh_message =
@@ -433,17 +431,26 @@ namespace nativetouch::inject {
} }
// load all APIs from user32 without adding static imports // load all APIs from user32 without adding static imports
//
// note that these are expected to be present in Windows 8 and above;
// however, on WINE, touch implementation remains in Windows 7 era
// and therefore the hooks below will fail, hence the fallback to
// legacy wintouchemu code
const auto user32 = libutils::load_library("user32.dll"); const auto user32 = libutils::load_library("user32.dll");
InitializeTouchInjection_ptr = libutils::try_proc<decltype(InitializeTouchInjection_ptr)>( InitializeTouchInjection_ptr = libutils::try_proc<decltype(InitializeTouchInjection_ptr)>(
user32, "InitializeTouchInjection"); user32, "InitializeTouchInjection");
if (InitializeTouchInjection_ptr == nullptr) {
log_warning(
"touch::native", "InitializeTouchInjection unavailable; mouse touch injection disabled");
return;
}
InjectTouchInput_ptr = libutils::try_proc<decltype(InjectTouchInput_ptr)>( InjectTouchInput_ptr = libutils::try_proc<decltype(InjectTouchInput_ptr)>(
user32, "InjectTouchInput"); user32, "InjectTouchInput");
if (InjectTouchInput_ptr == nullptr) {
if (InitializeTouchInjection_ptr == nullptr ||
InjectTouchInput_ptr == nullptr) {
clear_touch_injection_functions(); clear_touch_injection_functions();
log_warning( log_warning(
"touch::native", "touch injection API unavailable; mouse touch injection disabled"); "touch::native", "InjectTouchInput unavailable; mouse touch injection disabled");
return; return;
} }
@@ -457,20 +464,31 @@ namespace nativetouch::inject {
log_misc("touch::native", "mouse touch injection initialized"); log_misc("touch::native", "mouse touch injection initialized");
}); });
} return InitializeTouchInjection_ptr != nullptr && InjectTouchInput_ptr != nullptr;
bool touch_injection_available() {
return InjectTouchInput_ptr != nullptr;
} }
// install injection support for touch windows registered by the game module // install injection support for touch windows registered by the game module
void hook(HMODULE module) { bool hook_available(HMODULE module) {
initialize_touch_injection(); if (detour::iat_find("RegisterTouchWindow", module) == nullptr) {
log_warning("touch::native", "RegisterTouchWindow unavailable");
return false;
}
return true;
}
bool hook(HMODULE module) {
if (!initialize_touch_injection()) {
return false;
}
RegisterTouchWindow_orig = detour::iat_try( RegisterTouchWindow_orig = detour::iat_try(
"RegisterTouchWindow", RegisterTouchWindowHook, module); "RegisterTouchWindow", RegisterTouchWindowHook, module);
if (RegisterTouchWindow_orig != nullptr) { if (RegisterTouchWindow_orig == nullptr) {
log_misc("touch::native", "RegisterTouchWindow hooked"); log_warning("touch::native", "failed to hook RegisterTouchWindow");
return false;
} }
log_misc("touch::native", "RegisterTouchWindow hooked");
return true;
} }
} }
+2 -1
View File
@@ -7,7 +7,8 @@ struct tagTOUCHINPUT;
namespace nativetouch::inject { namespace nativetouch::inject {
void attach_window(HWND window); void attach_window(HWND window);
void register_and_attach_window(HWND window); void register_and_attach_window(HWND window);
void hook(HMODULE module); bool hook_available(HMODULE module);
bool hook(HMODULE module);
bool inject_synthetic_touch(POINT position, bool down); bool inject_synthetic_touch(POINT position, bool down);
bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down); bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down);
bool transform_touch_input(tagTOUCHINPUT *point); bool transform_touch_input(tagTOUCHINPUT *point);
+1 -2
View File
@@ -10,9 +10,8 @@ namespace nativetouch::inject {
Synthetic, Synthetic,
}; };
void initialize_touch_injection(); bool initialize_touch_injection();
void initialize_synthetic_touch(); void initialize_synthetic_touch();
bool touch_injection_available();
void refresh_contact_lifetime(); void refresh_contact_lifetime();
bool contact_is_active(); bool contact_is_active();
+2 -2
View File
@@ -41,7 +41,7 @@ namespace nativetouch::inject {
} }
POINT transformed = *position; POINT transformed = *position;
return transform::screen_to_game(window, &transformed); return transform::mouse_to_game(window, &transformed);
} }
// release the active injected contact and its window capture // release the active injected contact and its window capture
@@ -108,7 +108,7 @@ namespace nativetouch::inject {
} }
POINT transformed = position; POINT transformed = position;
if (!transform::screen_to_game(window, &transformed)) { if (!transform::mouse_to_game(window, &transformed)) {
end_mouse_contact(window); end_mouse_contact(window);
return; return;
} }
@@ -113,11 +113,12 @@ namespace nativetouch::inject {
} }
static HWND prepare_synthetic_touch() { static HWND prepare_synthetic_touch() {
initialize_touch_injection(); if (!initialize_touch_injection()) {
return nullptr;
}
const auto window = get_injection_window(); const auto window = get_injection_window();
if (!touch_injection_available() || window == nullptr || if (window == nullptr || synthetic_touch_message == 0) {
synthetic_touch_message == 0) {
return nullptr; return nullptr;
} }
return window; return window;
+40 -7
View File
@@ -223,25 +223,58 @@ namespace nativetouch {
} }
} }
void hook(HMODULE module) { static bool hook_prerequisites_available(HMODULE module) {
if (detour::iat_find("GetTouchInputInfo", module) == nullptr) {
log_warning("touch::native", "GetTouchInputInfo unavailable");
return false;
}
if (settings::EMULATE_DIGITIZER &&
detour::iat_find("GetSystemMetrics", module) == nullptr) {
log_warning("touch::native", "GetSystemMetrics unavailable");
return false;
}
return true;
}
bool hook(HMODULE module) {
native_touch_hooked = false;
initialize_game_settings(); initialize_game_settings();
inject::hook(module); // check if the OS supports touch API (Win7+) and injection API (requires Win8+)
if (!hook_prerequisites_available(module)) {
return false;
}
if (!inject::hook_available(module)) {
return false;
}
native_touch_hooked = true; // try hooking injection API first since they require the highest OS level
// (WINE specifically did not implement this in 2026)
if (!inject::hook(module)) {
return false;
}
// GetSystemMetrics
if (settings::EMULATE_DIGITIZER) { if (settings::EMULATE_DIGITIZER) {
GetSystemMetrics_orig = detour::iat_try( GetSystemMetrics_orig = detour::iat_try(
"GetSystemMetrics", GetSystemMetricsHook, module); "GetSystemMetrics", GetSystemMetricsHook, module);
if (GetSystemMetrics_orig != nullptr) { if (GetSystemMetrics_orig == nullptr) {
log_warning("touch::native", "failed to hook GetSystemMetrics");
return false;
}
log_misc("touch::native", "GetSystemMetrics hooked"); log_misc("touch::native", "GetSystemMetrics hooked");
} }
}
// GetTouchInputInfo
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module); GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
if (GetTouchInputInfo_orig != nullptr) { if (GetTouchInputInfo_orig == nullptr) {
log_misc("touch::native", "GetTouchInputInfo hooked"); log_warning("touch::native", "failed to hook GetTouchInputInfo");
return false;
} }
log_misc("touch::native", "GetTouchInputInfo hooked");
native_touch_hooked = true;
return true;
} }
} }
+1 -1
View File
@@ -15,7 +15,7 @@ namespace nativetouch {
using TouchInputFilter = bool (*)(const NativeTouchEvent &event); using TouchInputFilter = bool (*)(const NativeTouchEvent &event);
void hook(HMODULE module); bool hook(HMODULE module);
void refresh_contact_lifetime(); void refresh_contact_lifetime();
void set_input_filter(TouchInputFilter filter); void set_input_filter(TouchInputFilter filter);
+20 -1
View File
@@ -68,7 +68,7 @@ namespace nativetouch::transform {
static bool has_active_overlay_transform() { static bool has_active_overlay_transform() {
return overlay::OVERLAY != nullptr && return overlay::OVERLAY != nullptr &&
overlay::OVERLAY->get_active() && overlay::OVERLAY->get_active() &&
overlay::OVERLAY->can_transform_touch_input(); overlay::OVERLAY->has_subscreen_touch_transform();
} }
static bool transform_overlay_touch_position(POINT *position) { static bool transform_overlay_touch_position(POINT *position) {
@@ -120,6 +120,25 @@ namespace nativetouch::transform {
return transform_overlay_touch_position(position); return transform_overlay_touch_position(position);
} }
bool mouse_to_game(HWND window, POINT *position) {
// exception: iidx tdj dedicated subscreen window is allowed
if (is_tdj_dedicated_subscreen(window)) {
return screen_to_game(window, position);
}
// if this game has a subscreen overlay that can transform touch input
// but the window is hidden or not under the cursor, reject mouse-as-touch
// (e.g., iidx/sdvx are rejected here, but nostalgia is allowed)
if (overlay::OVERLAY != nullptr &&
overlay::OVERLAY->has_subscreen_touch_transform() &&
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
return false;
}
return screen_to_game(window, position);
}
// route hardware screen coordinates through dedicated or overlay mapping and report the result // route hardware screen coordinates through dedicated or overlay mapping and report the result
Result hardware_to_game(POINT *position) { Result hardware_to_game(POINT *position) {
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW); const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
+1
View File
@@ -12,5 +12,6 @@ namespace nativetouch::transform {
bool is_tdj_dedicated_subscreen(HWND window); bool is_tdj_dedicated_subscreen(HWND window);
bool game_to_screen(HWND window, POINT *position); bool game_to_screen(HWND window, POINT *position);
bool screen_to_game(HWND window, POINT *position); bool screen_to_game(HWND window, POINT *position);
bool mouse_to_game(HWND window, POINT *position);
Result hardware_to_game(POINT *position); Result hardware_to_game(POINT *position);
} }
+2 -2
View File
@@ -944,7 +944,7 @@ void touch_get_points(std::vector<TouchPoint> &touch_points, bool overlay) {
if (!overlay && if (!overlay &&
overlay::OVERLAY && overlay::OVERLAY &&
overlay::OVERLAY->get_active() && overlay::OVERLAY->get_active() &&
!overlay::OVERLAY->can_transform_touch_input() && !overlay::OVERLAY->has_subscreen_touch_transform() &&
ImGui::GetIO().WantCaptureMouse) { ImGui::GetIO().WantCaptureMouse) {
return; return;
@@ -971,7 +971,7 @@ void touch_get_events(std::vector<TouchEvent> &touch_events, bool overlay) {
if (!overlay && if (!overlay &&
overlay::OVERLAY && overlay::OVERLAY &&
overlay::OVERLAY->get_active() && overlay::OVERLAY->get_active() &&
!overlay::OVERLAY->can_transform_touch_input() && !overlay::OVERLAY->has_subscreen_touch_transform() &&
ImGui::GetIO().WantCaptureMouse) { ImGui::GetIO().WantCaptureMouse) {
TOUCH_EVENTS.reset(); TOUCH_EVENTS.reset();