Compare commits

...

12 Commits

Author SHA1 Message Date
bicarus baa550037a api: sdvx tape led (#857)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #852

## Description of change
Add SDVX valk cab tape LED output over API

## Testing
tested with custom python script over api
2026-08-07 00:38:17 -07:00
bicarus b53447bed5 popn: fix subscreen redraw option causing graphical glitches (#854)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Old behavior: Forced redraw presented the subscreen every main frame,
even when the game already presented it, causing duplicate presents and
tearing in popn (was fine in sdvx)

New behavior: Forced redraw acts as a fallback, presenting only when the
game skips or fails a subscreen update.

## Testing
Popn - no more glitching
Nabla - no regression
2026-08-06 04:14:31 -07:00
bicarus 4959a58de3 iidx: don't hook legacy camera unless requested (#856)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #855 

## Description of change
Legacy camera hooks are specifically for IIDX 25/26 and they only
perform redirection of device discovery (emulates USB vendor/device ID
and USB port). It has very limited use since most cameras are
practically unusable in IIDX 25/26. Don't enable it, unless explicitly
requested by the user.

New truth table for `-iidxcabcams` :

| Mode | `-iidx` off (cab setup) | `-iidx` on |
|---|---|---|
| `auto` | Native cameras enabled (same as `on`) | Cameras disabled
(same as `off`) |
| `off` | Cameras disabled | Cameras disabled |
| `on` | Native cameras enabled | Native cameras enabled |
| `legacy` | Native cameras enabled | IIDX 25/26 discovery emulation
enabled |

## Testing
2026-08-06 03:44:52 -07:00
bicarus 9141ff453b lang: detect forced UTF-8 ACP, try to opt-out (#853)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Fixes broken Japanese text when Windows `Use Unicode UTF-8 for worldwide
language support` setting is enabled. This setting changes the system
ACP to UTF-8 (65001), causing legacy Shift-JIS lead-byte checks to fail.

Windows 11: requests the legacy process code page through the manifest.

Windows 10: detects ACP 65001 (UTF-8), warns the user via deferred log,
and applies compatibility hook (only for popn pika model for now)

Windows 7 and below - UTF-8 option doesn't exist.

## Testing
2026-08-04 09:04:29 -07:00
bicarus f955ab984c iidx: allow main window to receive touches in windows TDJ subscreen mode (#850)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
This allows API touches to be recognized even if the main window is in
focus.

## Testing
2026-08-02 16:46:33 -07:00
bicarus-dev 435b9ce9aa output device name in message 2026-08-02 12:29:21 -07:00
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
45 changed files with 2727 additions and 234 deletions
+3
View File
@@ -332,6 +332,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
api/modules/control.cpp
api/modules/touch.cpp
api/modules/iidx.cpp
api/modules/sdvx.cpp
api/serial.cpp
api/modules/drs.cpp
api/modules/lcd.cpp
@@ -411,6 +412,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/sdvx/sdvx_live2d.cpp
games/sdvx/io.cpp
games/sdvx/camera.cpp
games/jb/bi2x_hook.cpp
games/jb/jb.cpp
games/jb/jb_touch.cpp
games/jb/io.cpp
@@ -535,6 +537,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/graphics/nvenc_hook.cpp
hooks/graphics/backends/d3d9/d3d9_backend.cpp
hooks/graphics/backends/d3d9/d3d9_device.cpp
hooks/graphics/backends/d3d9/d3d9_gfdm.cpp
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
+14
View File
@@ -267,6 +267,20 @@ which also means that your hex edits are applicable directly.
- `Side Panel Right Inner`
- `Side Panel Right`
#### SDVX
- tapeled_get(name: str, ...)
- returns a list containing a dict of the current tape LED states. The dict keys are:
- `Title`
- `Upper Left Speaker`
- `Upper Right Speaker`
- `Left Wing`
- `Right Wing`
- `Control Panel`
- `Lower Left Speaker`
- `Lower Right Speaker`
- `Woofer`
- `V Unit`
#### LCD
- info()
- returns information about the serial LCD controller some games use
+1 -1
View File
@@ -239,7 +239,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
// SDVX Old cabinet mode
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
answer_type = 1;
if (avs::game::is_model("L44"))
if (avs::game::is_model({ "L44", "T44" }))
answer_type = 2;
// check answer type
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <atomic>
#include <cstdint>
namespace api {
extern std::atomic_uint32_t CLIENT_COUNT;
inline bool has_clients() {
return CLIENT_COUNT.load(std::memory_order_relaxed) > 0;
}
}
+9
View File
@@ -5,6 +5,7 @@
#include <utility>
#include "client.h"
#include "cfg/configurator.h"
#include "external/rapidjson/document.h"
#include "util/crypt.h"
@@ -28,6 +29,7 @@
#include "modules/lcd.h"
#include "modules/lights.h"
#include "modules/memory.h"
#include "modules/sdvx.h"
#include "modules/touch.h"
#include "modules/resize.h"
#include "request.h"
@@ -36,6 +38,8 @@
using namespace rapidjson;
using namespace api;
std::atomic_uint32_t api::CLIENT_COUNT = 0;
Controller::Controller(unsigned short port, std::string password, bool pretty)
: port(port), password(std::move(password)), pretty(pretty)
{
@@ -411,8 +415,11 @@ void Controller::init_state(api::ClientState *state) {
state->modules.push_back(new modules::LCD());
state->modules.push_back(new modules::Lights());
state->modules.push_back(new modules::Memory());
state->modules.push_back(new modules::SDVX());
state->modules.push_back(new modules::Touch());
state->modules.push_back(new modules::Resize());
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
}
void Controller::free_state(api::ClientState *state) {
@@ -424,6 +431,8 @@ void Controller::free_state(api::ClientState *state) {
// free cipher
delete state->cipher;
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
}
void Controller::free_socket() {
+4 -2
View File
@@ -106,14 +106,16 @@ namespace api::modules {
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
// Create an array for the light state
Value light_state(kArrayType);
light_state.Reserve(mapping.data.capacity() * 3, res.doc()->GetAllocator());
light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator());
}
// Can't use StringRef here, turns some strings partially into null bytes for some reason
// can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
}
+67
View File
@@ -0,0 +1,67 @@
#include "sdvx.h"
#include <functional>
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
SDVX::SDVX() : Module("sdvx") {
functions["tapeled_get"] = std::bind(&SDVX::tapeled_get, this, _1, _2);
for (auto &light : games::sdvx::TAPELED_MAPPING) {
lights_by_names.emplace(light.lightName, light);
}
}
/**
* tapeled_get()
* tapeled_get(name: str, ...)
*/
void SDVX::tapeled_get(Request &req, Response &res) {
Value response_object(kObjectType);
// all tape leds
if (req.params.Size() == 0) {
// iterate through each device and dump its lights data into the response
for (const auto &mapping : games::sdvx::TAPELED_MAPPING) {
copy_tapeled_data(res, response_object, mapping);
}
} else {
// specified light names
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsString()) {
error_type(res, "name", "string");
return;
}
const auto name = param.GetString();
if (const auto &it = lights_by_names.find(name); it != lights_by_names.end()) {
copy_tapeled_data(res, response_object, it->second.get());
}
}
}
res.add_data(response_object);
}
void SDVX::copy_tapeled_data(Response &res, Value &response_object,
const tapeledutils::tape_led &mapping)
{
Value light_state(kArrayType);
light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator());
}
// can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <functional>
#include <string>
#include "api/module.h"
#include "api/request.h"
#include "external/robin_hood.h"
#include "games/sdvx/sdvx.h"
namespace api::modules {
class SDVX : public Module {
public:
SDVX();
private:
robin_hood::unordered_map<std::string, std::reference_wrapper<tapeledutils::tape_led>> lights_by_names;
void tapeled_get(Request &req, Response &res);
void copy_tapeled_data(Response &res, rapidjson::Value &response_object,
const tapeledutils::tape_led &mapping);
};
}
@@ -7,6 +7,7 @@ from .coin import *
from .control import *
from .exceptions import *
from .iidx import *
from .sdvx import *
from .info import *
from .keypads import *
from .lights import *
@@ -0,0 +1,12 @@
from .connection import Connection
from .request import Request
def sdvx_tapeled_get(con: Connection, *light_names):
req = Request("sdvx", "tapeled_get")
for light_name in light_names:
req.add_param(light_name)
res = con.request(req)
return res.get_data()
+1
View File
@@ -1114,6 +1114,7 @@ namespace avs {
#endif
// jubeat
{"jubeat.dll", 0x2000000},
{"jubeat2019.dll", 0x10000000},
// MUSECA
{"museca.dll", 0xC000000},
+1
View File
@@ -591,6 +591,7 @@ namespace avs {
// for proper reporting of Omnimix and other song packs
if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
_stricmp(EA3_MODEL, "L44") == 0 ||
_stricmp(EA3_MODEL, "T44") == 0 ||
_stricmp(EA3_MODEL, "M39") == 0 ||
_stricmp(EA3_MODEL, "KFC") == 0)
{
+1
View File
@@ -6,6 +6,7 @@
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<dpiAware>true</dpiAware>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">Legacy</activeCodePage>
</asmv3:windowsSettings>
</asmv3:application>
<dependency>
+211 -49
View File
@@ -2,12 +2,14 @@
#include "asio.h"
#include "handle.h"
#include "bi2x_hook.h"
#include <span>
#include <unordered_map>
#include "cfg/configurator.h"
#include "cfg/screen_resize.h"
#include <ks.h>
#include <ksmedia.h>
#include "cfg/configurator.h"
#include "hooks/audio/audio.h"
#include "hooks/audio/mme.h"
#include "hooks/graphics/graphics.h"
@@ -29,12 +31,14 @@ namespace games::gitadora {
// settings
bool TWOCHANNEL = false;
bool DISABLE_FRAME_LIMITER = false;
std::optional<unsigned int> CAB_TYPE = std::nullopt;
bool P1_LEFTY = false;
bool P2_LEFTY = false;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
bool ALLOW_REALTEK_AUDIO = false;
@@ -62,6 +66,46 @@ namespace games::gitadora {
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
/*
@@ -271,9 +315,21 @@ namespace games::gitadora {
break;
case 2:
if (!GRAPHICS_WINDOWED) {
log_fatal(
if (D3D9_ADAPTER.has_value()) {
log_fatal(
"gitadora",
"arena model: fullscreen two-window mode cannot use -monitor; "
"use -w for borderless windows instead");
}
ARENA_TWO_HEAD_EXCLUSIVE = true;
log_info(
"gitadora",
"arena model: 2-window mode is not supported in fullscreen, choose 1 or 4");
"arena model: native two-head fullscreen adapter-group mode "
"(MAIN + SMALL; LEFT/RIGHT are virtual)");
} else {
log_info(
"gitadora",
"arena model: two-window mode uses windowed rendering");
}
log_info("gitadora", "arena model: two-window mode");
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
@@ -299,10 +355,18 @@ namespace games::gitadora {
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
// cached primary real monitor: its path + source/target mode entries.
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
// Cached real monitor paths and their source/target mode entries. modeInfoIdx values are
// renumbered so the cache is self-contained. The normal single-head emulation keeps only
// MAIN; the two-head adapter-group mode keeps MAIN and SMALL as real heads.
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
static DISPLAYCONFIG_PATH_INFO real_small_path = {};
static DISPLAYCONFIG_MODE_INFO real_small_modes[2] = {}; // [0]=source, [1]=target
static bool real_small_path_available = false;
static bool is_two_head_exclusive() {
return ARENA_TWO_HEAD_EXCLUSIVE && !GRAPHICS_WINDOWED;
}
// fake monitors appended after the real ones. the game classifies monitors
// by outputTechnology + connectorInstance:
@@ -326,12 +390,31 @@ namespace games::gitadora {
// adapter to a swap chain role via its DisplayConfig connector instance,
// so the entries here must be listed in the same order the wrapper enumerates
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
static constexpr FakeMonitor FAKE_MONITORS[] = {
// Normal single-head emulation: every non-MAIN role is fake.
static constexpr FakeMonitor FAKE_MONITORS_SINGLE_HEAD[] = {
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
};
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<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
// two referenced modes (source + target). modeInfoIdx values are rewritten to
@@ -369,13 +452,55 @@ namespace games::gitadora {
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
}
if (primary->targetInfo.modeInfoIdx >= all_modes.size()) {
log_fatal("gitadora", "cache_primary_monitor_info: primary target mode is missing");
}
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
real_primary_path = *primary;
real_primary_path.sourceInfo.modeInfoIdx = 0;
real_primary_path.targetInfo.modeInfoIdx = 1;
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
real_small_path_available = false;
if (is_two_head_exclusive()) {
if (all_paths.size() != 2) {
log_fatal(
"gitadora",
"two-head fullscreen mode requires exactly two active display paths, found {}",
all_paths.size());
}
const auto secondary = std::find_if(all_paths.begin(), all_paths.end(),
[&](const auto &p) {
return &p != &*primary;
});
if (secondary == all_paths.end() ||
secondary->sourceInfo.modeInfoIdx >= all_modes.size() ||
secondary->targetInfo.modeInfoIdx >= all_modes.size()) {
log_fatal("gitadora", "could not identify the physical SMALL monitor");
}
const LUID primary_adapter = primary->sourceInfo.adapterId;
const LUID secondary_adapter = secondary->sourceInfo.adapterId;
const bool same_adapter = primary_adapter.HighPart == secondary_adapter.HighPart &&
primary_adapter.LowPart == secondary_adapter.LowPart;
if (!same_adapter) {
log_fatal(
"gitadora",
"MAIN and SMALL must use the same display adapter");
}
real_small_modes[0] = all_modes[secondary->sourceInfo.modeInfoIdx];
real_small_modes[1] = all_modes[secondary->targetInfo.modeInfoIdx];
real_small_path = *secondary;
real_small_path.sourceInfo.modeInfoIdx = 2;
real_small_path.targetInfo.modeInfoIdx = 3;
real_small_path_available = true;
log_info("gitadora", "cache_primary_monitor_info: cached real MAIN and SMALL monitors");
} else {
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
}
}
static
@@ -390,33 +515,36 @@ namespace games::gitadora {
static std::once_flag populate_once;
std::call_once(populate_once, cache_primary_monitor_info);
// always report exactly 1 real + N fake monitors
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
const auto fake_monitors = get_fake_monitors();
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
const UINT32 real_count = real_monitor_count();
*pNumPathArrayElements = real_count + fake_count;
*pNumModeInfoArrayElements = (real_count + fake_count) * 2;
log_info(
"gitadora",
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
FAKE_MONITOR_COUNT);
"GetDisplayConfigBufferSizes: {} real path(s) + {} fake monitor(s)",
real_count,
fake_count);
return ERROR_SUCCESS;
}
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
// assumption): index 0 in both arrays holds the cached primary real monitor, so
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
// Write a fake monitor after the real paths. The cache is packed as [source, target]
// mode pairs, so each path consumes two mode entries.
static void insert_fake_monitor(
DISPLAYCONFIG_PATH_INFO *paths,
DISPLAYCONFIG_MODE_INFO *modes,
UINT32 i)
const FakeMonitor &m,
UINT32 path_index,
UINT32 source_mode_index)
{
const FakeMonitor &m = FAKE_MONITORS[i];
const UINT32 src_idx = 2 + i * 2;
const UINT32 src_idx = source_mode_index;
const UINT32 tgt_idx = src_idx + 1;
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
const UINT32 uid = static_cast<UINT32>(-m.id);
paths[1 + i] = {
paths[path_index] = {
.sourceInfo = {
.adapterId = adapter_id,
.id = uid,
@@ -474,21 +602,35 @@ namespace games::gitadora {
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
{
// copy cached primary real monitor into caller buffers at index 0
// Copy cached real monitor paths into the caller buffers.
pathArray[0] = real_primary_path;
modeInfoArray[0] = real_primary_modes[0];
modeInfoArray[1] = real_primary_modes[1];
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
if (is_two_head_exclusive() && real_small_path_available) {
pathArray[1] = real_small_path;
modeInfoArray[2] = real_small_modes[0];
modeInfoArray[3] = real_small_modes[1];
}
const auto fake_monitors = get_fake_monitors();
const auto fake_count = static_cast<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) {
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
}
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
// append fake monitors after the real one
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
insert_fake_monitor(pathArray, modeInfoArray, i);
// Append fake monitors after the real path(s).
for (UINT32 i = 0; i < fake_count; i++) {
insert_fake_monitor(
pathArray,
modeInfoArray,
fake_monitors[i],
real_count + i,
real_count * 2 + i * 2);
}
return ERROR_SUCCESS;
@@ -519,7 +661,8 @@ namespace games::gitadora {
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const LONG fake_id = -id;
UINT32 conn_inst = 0xff;
for (const auto& f : FAKE_MONITORS) {
const auto fake_monitors = get_fake_monitors();
for (const auto &f : fake_monitors) {
if (f.id == fake_id) {
conn_inst = f.connector_instance;
break;
@@ -542,18 +685,27 @@ namespace games::gitadora {
return ret;
}
// override the cached primary real monitor target info to look like HDMI/0
// Override MAIN to HDMI/0 and retag the real second head as the DP/2 SMALL
// display expected by the cabinet software in two-head fullscreen mode.
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const auto& target = real_primary_path.targetInfo;
if (target.id == targetName->header.id &&
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
{
const auto target_matches = [&](const DISPLAYCONFIG_PATH_TARGET_INFO &target) {
return target.id == targetName->header.id &&
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
target.adapterId.LowPart == targetName->header.adapterId.LowPart;
};
if (target_matches(real_primary_path.targetInfo)) {
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
targetName->connectorInstance = 0;
log_info("gitadora",
"overriding primary monitor (id={}) to pretend to be HDMI",
targetName->header.id);
} else if (is_two_head_exclusive() && real_small_path_available &&
target_matches(real_small_path.targetInfo)) {
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
targetName->connectorInstance = 2;
log_info("gitadora",
"overriding secondary monitor (id={}) to pretend to be SMALL DP/2",
targetName->header.id);
}
return ret;
}
@@ -576,6 +728,12 @@ namespace games::gitadora {
HMODULE system_module = libutils::try_module("libsystem.dll");
// 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(
sharepj_module, "eam_network_detected_ip_change"));
detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc(
@@ -651,7 +809,8 @@ namespace games::gitadora {
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
}
// monitor/touch hooks (windowed or full screen)
// Single-window mode needs touch injection for its overlay. Two-head fullscreen
// mode uses the real SMALL output, so it only needs the display-topology shim.
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay
const auto native_touch_ready = !wintouchemu::FORCE &&
@@ -661,25 +820,28 @@ namespace games::gitadora {
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
}
}
#if !SPICE_XP
if (!GRAPHICS_WINDOWED) {
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
GetDisplayConfigBufferSizes_orig =
detour::iat_try("GetDisplayConfigBufferSizes",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
QueryDisplayConfig_orig =
detour::iat_try("QueryDisplayConfig",
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
DisplayConfigGetDeviceInfo_orig =
detour::iat_try("DisplayConfigGetDeviceInfo",
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
if (!GRAPHICS_WINDOWED &&
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || ARENA_TWO_HEAD_EXCLUSIVE)) {
if (ARENA_TWO_HEAD_EXCLUSIVE) {
log_info(
"gitadora",
"exposing physical MAIN/SMALL and virtual LEFT/RIGHT");
}
#endif
GetDisplayConfigBufferSizes_orig =
detour::iat_try("GetDisplayConfigBufferSizes",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
QueryDisplayConfig_orig =
detour::iat_try("QueryDisplayConfig",
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
DisplayConfigGetDeviceInfo_orig =
detour::iat_try("DisplayConfigGetDeviceInfo",
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
}
#endif
}
// window patch
+2
View File
@@ -13,12 +13,14 @@ namespace games::gitadora {
// settings
extern bool TWOCHANNEL;
extern bool DISABLE_FRAME_LIMITER;
extern std::optional<unsigned int> CAB_TYPE;
extern bool P1_LEFTY;
extern bool P2_LEFTY;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
extern std::optional<std::string> ASIO_DRIVER;
extern bool ALLOW_REALTEK_AUDIO;
+7 -4
View File
@@ -3,6 +3,7 @@
#if SPICE64
#include <cstdint>
#include "api/client.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -443,10 +444,12 @@ namespace games::iidx {
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
for (unsigned int i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
if (api::has_clients()) {
for (size_t i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
}
}
+6 -18
View File
@@ -58,7 +58,7 @@ namespace games::iidx {
// settings
bool FLIP_CAMS = false;
std::optional<bool> DISABLE_CAMS;
cab_camera_access_mode CAB_CAMERA_ACCESS = cab_camera_access_mode::automatic;
bool TDJ_CAMERA = false;
bool TDJ_CAMERA_PREFER_16_9 = true;
bool TDJ_MODE = false;
@@ -424,10 +424,7 @@ namespace games::iidx {
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
// check if cam hook should be enabled
if (!DISABLE_CAMS.has_value()) {
log_fatal("iidx", "assertion failure - DISABLE_CAMS not set during attach");
}
if (!DISABLE_CAMS.value()) {
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::legacy) {
init_legacy_camera_hook(FLIP_CAMS);
}
@@ -454,19 +451,10 @@ namespace games::iidx {
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
}
// check for cab camera access for the second time (first time was in launcher.cpp)
// this time, we are inside -iidx module hook, which means the user is likely NOT on a cab
// therefore, start with cams OFF by default, and allow user to forcibly override to ON
if (!games::iidx::DISABLE_CAMS.has_value()) {
games::iidx::DISABLE_CAMS = true;
if (options->at(launcher::Options::IIDXCabCamAccess).is_active() &&
options->at(launcher::Options::IIDXCabCamAccess).value_text() == "on") {
games::iidx::DISABLE_CAMS = false;
}
if (games::iidx::DISABLE_CAMS.value()) {
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
// auto with iidx module means turn off camera (non-cab use)
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::automatic) {
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
// windowed subscreen, enabled by default, unless turned off by user
+8 -1
View File
@@ -11,6 +11,13 @@
namespace games::iidx {
enum class cab_camera_access_mode {
automatic,
off,
on,
legacy,
};
enum class iidx_aio_emulation_state {
unknown,
bi2a_com2,
@@ -20,7 +27,7 @@ namespace games::iidx {
// settings
extern bool FLIP_CAMS;
extern std::optional<bool> DISABLE_CAMS;
extern cab_camera_access_mode CAB_CAMERA_ACCESS;
extern bool TDJ_CAMERA;
extern bool TDJ_CAMERA_PREFER_16_9;
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
+1
View File
@@ -133,6 +133,7 @@ namespace games {
buttons_help.insert({ jb, jb::get_buttons_help() });
lights.insert({ jb, jb::get_lights() });
file_hints[jb].push_back({"jubeat.dll"});
file_hints[jb].push_back({"jubeat2019.dll"});
// mga
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 <filesystem>
#include "avs/game.h"
#include "bi2x_hook.h"
#include "cfg/configurator.h"
#include "util/logging.h"
#include "util/detour.h"
@@ -10,6 +12,8 @@
namespace games::jb {
#if !SPICE64
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
static BOOL __stdcall network_addr_is_changed() {
return 0;
@@ -31,6 +35,8 @@ namespace games::jb {
return 0;
}
#endif
JBGame::JBGame() : Game("Jubeat") {
}
@@ -60,8 +66,18 @@ namespace games::jb {
void JBGame::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();
#if !SPICE64
// enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll");
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
@@ -75,6 +91,8 @@ namespace games::jb {
network, "network_get_network_check_info"));
detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc(
network, "network_get_dhcp_result"));
#endif
}
void JBGame::detach() {
+40 -26
View File
@@ -20,6 +20,8 @@
// (6, 8) in landscape.
#define JB_BUTTON_SIZE 160
#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
// 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 bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true;
static bool IS_T44 = false;
static std::atomic_uint16_t TOUCH_STATE = 0;
// fixed-size contact view used by the debug overlay
@@ -85,35 +88,45 @@ namespace games::jb {
// --- touch geometry ------------------------------------------------------
// 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_T44_BUTTON_GAPS[3] = {
JB_T44_BUTTON_GAP,
JB_T44_BUTTON_GAP,
JB_T44_BUTTON_GAP,
};
struct AxisGeometry {
int size;
int button[4]; // left/top edge of each button
};
// 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 {};
g.size = size;
g.button[0] = first;
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;
}
// button edges for the current orientation
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
if (IS_PORTRAIT) {
gx = axis_geometry(8);
gy = axis_geometry(602);
if (IS_T44) {
gx = axis_geometry(37, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS);
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 {
gx = axis_geometry(6);
gy = axis_geometry(8);
gx = axis_geometry(6, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
gy = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
}
}
// distance from `p` to a button along one axis (0 when inside)
static int axis_distance(int p, int button) {
int end = button + JB_BUTTON_SIZE - 1;
static int axis_distance(int p, int button, int size) {
int end = button + size - 1;
if (p < button) {
return button - p;
}
@@ -131,8 +144,8 @@ namespace games::jb {
int best_dist = 0;
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c]);
int dy = axis_distance(py, gy.button[r]);
int dx = axis_distance(px, gx.button[c], gx.size);
int dy = axis_distance(py, gy.button[r], gy.size);
int dist = dx * dx + dy * dy;
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
best_dist = dist;
@@ -145,7 +158,8 @@ namespace games::jb {
// detection reach for the current algorithm (0 = register only inside a button)
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
@@ -162,8 +176,8 @@ namespace games::jb {
}
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c]);
int dy = axis_distance(py, gy.button[r]);
int dx = axis_distance(px, gx.button[c], gx.size);
int dy = axis_distance(py, gy.button[r], gy.size);
if (dx * dx + dy * dy <= radius * radius) {
state |= uint16_t(1) << (r * 4 + c);
}
@@ -184,11 +198,15 @@ namespace games::jb {
// one-time touch window attach
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
// 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();
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
wnd = FindWindowBeginsWith(avs::game::MODEL);
if (!string_begins_with(GetActiveWindowTitle(), window_title)) {
wnd = FindWindowBeginsWith(window_title);
}
if (!wnd) {
log_warning("jubeat", "could not find window handle for touch");
@@ -197,10 +215,6 @@ namespace games::jb {
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));
// let the rawinput stack correct the aspect ratio
@@ -232,7 +246,7 @@ namespace games::jb {
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
auto offset = IS_PORTRAIT ? 580 : 0;
@@ -311,7 +325,7 @@ namespace games::jb {
for (int c = 0; c < 4; c++) {
cells[r * 4 + c] = {
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) {
int c = index % 4;
int r = index / 4;
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py,
(gx.button[c] + JB_BUTTON_SIZE / 2) - px);
double mid = std::atan2((gy.button[r] + gy.size / 2) - py,
(gx.button[c] + gx.size / 2) - px);
const double quarter = 3.14159265358979323846 / 2.0;
const int segments = 16;
POINT arc[segments + 1];
@@ -425,7 +439,7 @@ namespace games::jb {
}
// 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 {};
if (!legacy) {
touch_geometry(gx, gy);
+13 -26
View File
@@ -3,6 +3,7 @@
#if SPICE64
#include <cstdint>
#include "api/client.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -349,43 +350,29 @@ namespace games::sdvx {
* 9 - v unit - 258 bytes - 86 colors
*
* data is stored in RGB order, 3 bytes per color
*
* TODO: expose this data via API
*/
// data mapping
static struct TapeLedMapping {
size_t data_size;
int index_r, index_g, index_b;
TapeLedMapping(size_t data_size, int index_r, int index_g, int index_b)
: data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {}
} mapping[] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B },
};
// check index bounds
if (tapeledutils::is_enabled() && index < std::size(mapping)) {
auto &map = mapping[index];
if (tapeledutils::is_enabled() && index < std::size(TAPELED_MAPPING)) {
auto &map = TAPELED_MAPPING[index];
const auto data_size = map.data.size();
// pick a color to use
const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size);
const auto rgb = tapeledutils::pick_color_from_led_tape(data, data_size);
// program the lights into API
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
if (api::has_clients()) {
for (size_t i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
}
}
if (This != custom_node) {
+13
View File
@@ -61,6 +61,19 @@ namespace games::sdvx {
static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr;
tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B, "Title" },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B, "Upper Left Speaker" },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B, "Upper Right Speaker" },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B, "Left Wing" },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B, "Right Wing" },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B, "Control Panel" },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B, "Lower Left Speaker" },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B, "Lower Right Speaker" },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B, "Woofer" },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B, "V Unit" },
};
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (lpSubKey != nullptr &&
phkResult != nullptr &&
+4
View File
@@ -6,6 +6,7 @@
#include "avs/game.h"
#include "games/game.h"
#include "util/tapeled.h"
namespace games::sdvx {
@@ -25,6 +26,9 @@ namespace games::sdvx {
// states
extern bool SHOW_VM_MONITOR_WARNING;
constexpr int SDVX_TAPELED_TOTAL = 10;
extern tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL];
static inline bool is_valkyrie_model() {
return (
avs::game::is_model("KFC") &&
@@ -1,5 +1,6 @@
#include "d3d9_backend.h"
#include <atomic>
#include <cassert>
#include <memory>
#include <thread>
@@ -113,7 +114,16 @@ static Direct3DCreate9On12Ex_t Direct3DCreate9On12Ex_orig = nullptr;
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr;
static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params);
// main and subscreen presents may occur on different threads.
static std::atomic_bool SUBSCREEN_PRESENTED_SINCE_LAST_MAIN = false;
// do not mistake the fallback Present below for a game-originated Present.
static thread_local bool SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = false;
static void graphics_d3d9_ldj_init_sub_screen(
IDirect3DDevice9Ex *device,
D3DPRESENT_PARAMETERS *present_params,
UINT gfdm_small_swapchain);
static std::string behavior2s(DWORD behavior_flags) {
FLAGS_START(behavior_flags);
@@ -409,7 +419,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::QueryInterface(
return E_POINTER;
}
if (riid == IID_WrappedIDirect3D9 ||
if ((riid == IID_IUnknown && gfdm_two_head_exclusive()) ||
riid == IID_WrappedIDirect3D9 ||
riid == IID_IDirect3D9 ||
riid == IID_IDirect3D9Ex)
{
@@ -467,6 +478,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
UINT result = pReal->GetAdapterCount();
if (gfdm_two_head_exclusive()) {
if (result == 2) {
FAKE_SUBSCREEN_ADAPTER = true;
return GFDM_LOGICAL_HEAD_COUNT;
}
log_warning(
"graphics::d3d9",
"two-head mode requires two native adapters; found {}",
result);
return result;
}
if (!FAKE_SUBSCREEN_ADAPTER) {
if (games::popn::is_pikapika_model() && result == 1) {
FAKE_SUBSCREEN_ADAPTER = true;
@@ -493,7 +516,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
D3DADAPTER_IDENTIFIER9 *pIdentifier)
{
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) {
if (is_fake_subscreen_adapter(Adapter) && pIdentifier) {
*pIdentifier = {};
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
@@ -509,7 +532,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
}
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
if (is_fake_subscreen_adapter(Adapter)) {
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
return 1;
}
@@ -523,13 +546,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
UINT Mode,
D3DDISPLAYMODE *pMode)
{
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
if (is_fake_subscreen_adapter(Adapter)) {
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
if (Mode == 0 && pMode) {
pMode->Width = 1280;
pMode->Height = 800;
pMode->RefreshRate = 60;
pMode->Format = D3DFMT_X8R8G8B8;
get_fake_subscreen_display_mode(Adapter, pMode);
return S_OK;
} else {
return D3DERR_INVALIDCALL;
@@ -611,6 +631,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE *pMode) {
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
if (pMode == nullptr) {
return D3DERR_INVALIDCALL;
}
get_fake_subscreen_display_mode(Adapter, pMode);
return D3D_OK;
}
CHECK_RESULT(pReal->GetAdapterDisplayMode(Adapter, pMode));
}
@@ -621,6 +649,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceType(
D3DFORMAT BackBufferFormat,
BOOL bWindowed)
{
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(iAdapter)) {
return D3D_OK;
}
CHECK_RESULT(pReal->CheckDeviceType(iAdapter, DevType, DisplayFormat, BackBufferFormat, bWindowed));
}
@@ -632,7 +664,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
D3DRESOURCETYPE RType,
D3DFORMAT CheckFormat)
{
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
if (is_fake_subscreen_adapter(Adapter)) {
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
return D3D_OK;
}
@@ -648,6 +680,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceMultiSampleType(
D3DMULTISAMPLE_TYPE MultiSampleType,
DWORD *pQualityLevels)
{
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
if (pQualityLevels != nullptr) {
*pQualityLevels = 1;
}
return D3D_OK;
}
CHECK_RESULT(pReal->CheckDeviceMultiSampleType(
Adapter,
DeviceType,
@@ -664,6 +703,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDepthStencilMatch(
D3DFORMAT RenderTargetFormat,
D3DFORMAT DepthStencilFormat)
{
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
return D3D_OK;
}
CHECK_RESULT(pReal->CheckDepthStencilMatch(
Adapter,
DeviceType,
@@ -678,6 +721,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormatConversion(
D3DFORMAT SourceFormat,
D3DFORMAT TargetFormat)
{
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
return D3D_OK;
}
CHECK_RESULT(pReal->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat));
}
@@ -689,6 +736,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
return D3DERR_INVALIDCALL;
}
if (gfdm_two_head_exclusive() && Adapter < GFDM_LOGICAL_HEAD_COUNT) {
HRESULT ret = this->pReal->GetDeviceCaps(
Adapter >= 2 ? 0 : Adapter,
DeviceType,
pCaps);
if (SUCCEEDED(ret)) {
pCaps->NumberOfAdaptersInGroup =
Adapter == 0 ? GFDM_LOGICAL_HEAD_COUNT : 0;
pCaps->MasterAdapterOrdinal = 0;
pCaps->AdapterOrdinalInGroup = Adapter;
}
return ret;
}
// SDVX uses `NumberOfAdaptersInGroup` to allocate a vector and the Microsoft documentation states:
// "The value will be 0 for a subordinate adapter of a multihead card. Each card can have at most one
// master, but may have many subordinates." Therefore, this must point to the master adapter.
@@ -741,6 +802,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
}
HMONITOR STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterMonitor(UINT Adapter) {
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
return pReal->GetAdapterMonitor(0);
}
return pReal->GetAdapterMonitor(Adapter);
}
@@ -861,7 +926,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
"either use -graphics-force-refresh option or change the desktop resolution beforehand.");
}
if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
if (!GRAPHICS_WINDOWED && num_adapters >= 2
&& GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
pPresentationParameters[1].FullScreen_RefreshRateInHz,
GRAPHICS_FORCE_REFRESH_SUB.value());
@@ -902,7 +968,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
hFocusWindow,
*ppReturnedDeviceInterface);
}
// return result
@@ -916,6 +984,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCountEx(UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter) {
assert(is_d3d9ex);
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
return 1;
}
return static_cast<IDirect3D9Ex *>(pReal)->GetAdapterModeCountEx(Adapter, pFilter);
}
@@ -926,6 +998,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModesEx(
D3DDISPLAYMODEEX *pMode)
{
assert(is_d3d9ex);
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
if (Mode != 0 || pMode == nullptr) {
return D3DERR_INVALIDCALL;
}
get_fake_subscreen_display_mode_ex(Adapter, pMode);
return D3D_OK;
}
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode));
}
@@ -935,6 +1015,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayModeEx(
D3DDISPLAYROTATION *pRotation)
{
assert(is_d3d9ex);
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
if (pMode == nullptr) {
return D3DERR_INVALIDCALL;
}
get_fake_subscreen_display_mode_ex(Adapter, pMode);
if (pRotation != nullptr) {
*pRotation = D3DDISPLAYROTATION_IDENTITY;
}
return D3D_OK;
}
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterDisplayModeEx(Adapter, pMode, pRotation));
}
@@ -963,6 +1054,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
return D3DERR_INVALIDCALL;
}
if (gfdm_two_head_exclusive() && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
log_warning(
"graphics::d3d9",
"-forceressub is unavailable; SMALL must remain {}x{}",
GFDM_SMALL_WIDTH,
GFDM_SMALL_HEIGHT);
return D3DERR_INVALIDCALL;
}
DWORD orig_behavior_flags = BehaviorFlags;
size_t num_adapters = 1;
@@ -989,6 +1088,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) {
num_adapters = device_caps.NumberOfAdaptersInGroup;
}
if (gfdm_two_head_exclusive()) {
num_adapters = GFDM_LOGICAL_HEAD_COUNT;
}
}
for (size_t i = 0; i < num_adapters; i++) {
@@ -1105,7 +1207,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
}
}
if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
if (!GRAPHICS_WINDOWED && !gfdm_two_head_exclusive()
&& num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
pPresentationParameters[1].FullScreen_RefreshRateInHz,
GRAPHICS_FORCE_REFRESH_SUB.value());
@@ -1131,16 +1234,93 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
pPresentationParameters->BackBufferCount = GRAPHICS_FORCE_VSYNC_BUFFER.value();
}
GfdmTwoHeadDeviceState gfdm_parameters(
pPresentationParameters,
pFullscreenDisplayMode);
if (gfdm_two_head_exclusive()) {
if (!(BehaviorFlags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
log_warning(
"graphics::d3d9",
"CreateDeviceEx did not request an adapter-group device");
return D3DERR_NOTAVAILABLE;
}
HRESULT select = graphics_d3d9_gfdm_select_two_head_group_parameters(
pPresentationParameters,
pFullscreenDisplayMode,
gfdm_parameters.native_presentation_parameters.data(),
gfdm_parameters.native_fullscreen_display_modes.data(),
&gfdm_parameters.logical_small_swapchain,
"CreateDeviceEx");
if (FAILED(select)) {
return select;
}
if (GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
gfdm_parameters.native_presentation_parameters[1]
.FullScreen_RefreshRateInHz =
GRAPHICS_FORCE_REFRESH_SUB.value();
if (pFullscreenDisplayMode != nullptr) {
gfdm_parameters.native_fullscreen_display_modes[1].RefreshRate =
GRAPHICS_FORCE_REFRESH_SUB.value();
}
}
HRESULT remap = graphics_d3d9_gfdm_remap_two_head_group_parameters(
gfdm_parameters.native_presentation_parameters.data(),
pFullscreenDisplayMode != nullptr
? gfdm_parameters.native_fullscreen_display_modes.data()
: nullptr,
"CreateDeviceEx");
if (FAILED(remap)) {
return remap;
}
if (pFullscreenDisplayMode != nullptr) {
graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
static_cast<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
HRESULT result = static_cast<IDirect3D9Ex *>(this->pReal)->CreateDeviceEx(
Adapter,
DeviceType,
hFocusWindow,
BehaviorFlags,
pPresentationParameters,
pFullscreenDisplayMode,
gfdm_parameters.presentation_parameters,
gfdm_parameters.fullscreen_display_modes,
ppReturnedDeviceInterface);
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.presentation_parameters[1];
if (pFullscreenDisplayMode != nullptr) {
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.fullscreen_display_modes[1];
}
}
// check for error
if (result != D3D_OK) {
@@ -1151,17 +1331,28 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
hFocusWindow,
*ppReturnedDeviceInterface,
gfdm_parameters.logical_small_swapchain,
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
gfdm_two_head_exclusive() ? pPresentationParameters : nullptr);
// initialize sub screen if the game requested a multi-head context
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
UINT i = 1;
if (games::gitadora::is_arena_model()) {
i = 2;
}
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]);
UINT i = games::gitadora::is_arena_model()
? gfdm_parameters.logical_small_swapchain
: 1;
D3DPRESENT_PARAMETERS *subscreen_parameters =
gfdm_two_head_exclusive()
? &gfdm_parameters.presentation_parameters[1]
: &pPresentationParameters[i];
graphics_d3d9_ldj_init_sub_screen(
*ppReturnedDeviceInterface,
subscreen_parameters,
i);
}
}
@@ -1170,6 +1361,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *pLUID) {
assert(is_d3d9ex);
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
if (pLUID == nullptr) {
return D3DERR_INVALIDCALL;
}
pLUID->LowPart = static_cast<DWORD>(-static_cast<LONG>(Adapter));
pLUID->HighPart = -static_cast<LONG>(Adapter);
return D3D_OK;
}
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterLUID(Adapter, pLUID));
}
@@ -1178,7 +1377,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *
// The sub screen swap chain should be created if:
// - Running windowed with `NumberOfAdaptersInGroup >= 2` (game expects implicit swap chain to exist)
// - Running fullscreen with `NumberOfAdaptersInGroup < 2` (overridden `GetDeviceCaps` structure)
static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params) {
static void graphics_d3d9_ldj_init_sub_screen(
IDirect3DDevice9Ex *device,
D3DPRESENT_PARAMETERS *present_params,
UINT gfdm_small_swapchain)
{
D3DCAPS9 caps {};
HRESULT hr = device->GetDeviceCaps(&caps);
if (FAILED(hr)) {
@@ -1212,10 +1415,9 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE
}
} else {
int swapchain = 1;
if (games::gitadora::is_arena_model()) {
swapchain = 2;
}
const int swapchain = games::gitadora::is_arena_model()
? static_cast<int>(gfdm_small_swapchain)
: 1;
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
if (FAILED(hr)) {
@@ -1250,6 +1452,12 @@ IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen() {
return surface;
}
void graphics_d3d9_notify_subscreen_present() {
if (SUBSCREEN_FORCE_REDRAW && !SUBSCREEN_FORCE_REDRAW_IN_PROGRESS) {
SUBSCREEN_PRESENTED_SINCE_LAST_MAIN.store(true, std::memory_order_relaxed);
}
}
static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
// iidx/sdvx
int swapchain = 1;
@@ -1277,8 +1485,16 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
//
// early versions of popn HC needs this as well, but not on by default as it can cause
// graphical glitches on some GPUs
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) {
//
// treat forced redraw as a fallback so it does not duplicate a successful game present.
const bool force_redraw = SUBSCREEN_FORCE_REDRAW &&
!SUBSCREEN_PRESENTED_SINCE_LAST_MAIN.exchange(false, std::memory_order_relaxed);
if (GRAPHICS_WINDOWED || force_redraw) {
SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = true;
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = false;
}
}
}
@@ -2,6 +2,8 @@
#include <d3d9.h>
#include "d3d9_gfdm.h"
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
static const GUID IID_WrappedIDirect3D9 = {
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
@@ -13,6 +15,8 @@ void graphics_d3d9_on_present(
IDirect3DDevice9 *device,
IDirect3DDevice9 *wrapped_device);
void graphics_d3d9_notify_subscreen_present();
IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen();
struct WrappedIDirect3D9 : IDirect3D9Ex {
@@ -41,7 +41,6 @@
constexpr bool CUSTOM_RESET = false;
constexpr D3DFORMAT D3DFMT_DF24 = static_cast<D3DFORMAT>(MAKEFOURCC('D', 'F', '2', '4'));
auto format_as(D3DPOOL f) {
return fmt::underlying(f);
}
@@ -85,7 +84,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::QueryInterface(
return E_POINTER;
}
if (riid == IID_WrappedIDirect3DDevice9 ||
if ((riid == IID_IUnknown && is_gfdm_two_head_exclusive()) ||
riid == IID_WrappedIDirect3DDevice9 ||
riid == IID_IDirect3DDevice9 ||
riid == IID_IDirect3DDevice9Ex)
{
@@ -134,6 +134,10 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
this->main_swapchain->Release();
this->main_swapchain = nullptr;
}
if (this->implicit_sub_swapchain) {
this->implicit_sub_swapchain->Release();
this->implicit_sub_swapchain = nullptr;
}
for (auto &sc : this->sub_swapchain) {
if (sc) {
sc->Release();
@@ -202,6 +206,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDirect3D(
IDirect3D9 **ppD3D9)
{
WRAP_VERBOSE;
if (is_gfdm_two_head_exclusive() && gfdm_parent_d3d != nullptr) {
if (ppD3D9 == nullptr) {
return D3DERR_INVALIDCALL;
}
gfdm_parent_d3d->AddRef();
*ppD3D9 = gfdm_parent_d3d;
return D3D_OK;
}
CHECK_RESULT(pReal->GetDirect3D(ppD3D9));
}
@@ -209,7 +222,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDeviceCaps(
D3DCAPS9 *pCaps)
{
WRAP_VERBOSE;
CHECK_RESULT(pReal->GetDeviceCaps(pCaps));
HRESULT result = pReal->GetDeviceCaps(pCaps);
if (SUCCEEDED(result) && is_gfdm_two_head_exclusive() && pCaps != nullptr) {
pCaps->NumberOfAdaptersInGroup = GFDM_LOGICAL_HEAD_COUNT;
pCaps->MasterAdapterOrdinal = 0;
pCaps->AdapterOrdinalInGroup = 0;
}
CHECK_RESULT(result);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
@@ -217,6 +236,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
D3DDISPLAYMODE *pMode)
{
WRAP_VERBOSE;
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_side_swapchain(iSwapChain))
{
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetDisplayMode(pMode);
}
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
CHECK_RESULT(pReal->GetDisplayMode(GFDM_NATIVE_SMALL_SWAPCHAIN, pMode));
}
CHECK_RESULT(pReal->GetDisplayMode(iSwapChain, pMode));
}
@@ -258,6 +288,51 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
{
WRAP_VERBOSE;
if (pPresentationParameters == nullptr || ppSwapChain == nullptr) {
return D3DERR_INVALIDCALL;
}
if (is_gfdm_two_head_exclusive()) {
if (pPresentationParameters->BackBufferWidth == 800) {
return GetSwapChain(gfdm_logical_small_swapchain, ppSwapChain);
}
if (pPresentationParameters->BackBufferWidth != 1080) {
log_warning(
"graphics::d3d9",
"two-head exclusive: rejecting unexpected additional swap chain {}x{}",
pPresentationParameters->BackBufferWidth,
pPresentationParameters->BackBufferHeight);
return D3DERR_INVALIDCALL;
}
size_t index = 3;
for (UINT logical_swapchain = 1;
logical_swapchain < GFDM_LOGICAL_HEAD_COUNT;
logical_swapchain++)
{
if (!is_gfdm_logical_side_swapchain(logical_swapchain)) {
continue;
}
const size_t candidate =
gfdm_hidden_side_swapchain_slot(logical_swapchain);
if (fake_sub_swapchain[candidate] == nullptr) {
index = candidate;
break;
}
}
if (index == 3) {
return D3DERR_OUTOFVIDEOMEMORY;
}
fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(
this,
pPresentationParameters,
is_d3d9ex,
true);
fake_sub_swapchain[index]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
return D3D_OK;
}
int index = 0;
bool create_swap_chain = false;
bool create_fake_swap_chain = false;
@@ -271,6 +346,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
// SMALL (subscreen)
create_swap_chain = true;
index = 0;
} else if (pPresentationParameters->BackBufferWidth == 1080) {
// LEFT/RIGHT
create_swap_chain = true;
@@ -335,6 +411,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
{
WRAP_VERBOSE_FMT("GetSwapChain({})", iSwapChain);
if (ppSwapChain == nullptr) {
return D3DERR_INVALIDCALL;
}
if (iSwapChain == 0) {
if (!main_swapchain) {
HRESULT ret = pReal->GetSwapChain(iSwapChain, ppSwapChain);
@@ -344,7 +424,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
return ret;
}
main_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
main_swapchain = new WrappedIDirect3DSwapChain9(
this,
*ppSwapChain,
is_gfdm_two_head_exclusive()
? WrappedIDirect3DSwapChain9::NativeGroupHead::Main
: WrappedIDirect3DSwapChain9::NativeGroupHead::Unknown);
}
main_swapchain->AddRef();
@@ -354,6 +439,39 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
return D3D_OK;
}
if (is_gfdm_two_head_exclusive()) {
if (is_gfdm_logical_small_swapchain(iSwapChain)) {
if (!sub_swapchain[0]) {
IDirect3DSwapChain9 *real_swapchain = nullptr;
HRESULT ret = pReal->GetSwapChain(
GFDM_NATIVE_SMALL_SWAPCHAIN,
&real_swapchain);
if (FAILED(ret)) {
return ret;
}
sub_swapchain[0] = new WrappedIDirect3DSwapChain9(
this,
real_swapchain,
WrappedIDirect3DSwapChain9::NativeGroupHead::Small);
sub_swapchain[0]->should_run_hooks = false;
}
sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<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;
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
swap = true;
@@ -373,6 +491,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
fake_sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
graphics_screens_register(iSwapChain);
return D3D_OK;
} else if (SUBSCREEN_FORCE_REDRAW) {
// store implicit sub swap chain
if (!implicit_sub_swapchain) {
IDirect3DSwapChain9 *real_swapchain = nullptr;
HRESULT ret = pReal->GetSwapChain(iSwapChain, &real_swapchain);
if (FAILED(ret)) {
return ret;
}
implicit_sub_swapchain = new WrappedIDirect3DSwapChain9(this, real_swapchain);
implicit_sub_swapchain->should_run_hooks = false;
}
implicit_sub_swapchain->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(implicit_sub_swapchain);
graphics_screens_register(iSwapChain);
return D3D_OK;
}
@@ -390,6 +526,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
WRAP_VERBOSE;
if (is_gfdm_two_head_exclusive()) {
return GFDM_LOGICAL_HEAD_COUNT;
}
UINT n = pReal->GetNumberOfSwapChains();
if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
@@ -453,6 +593,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
IDirect3DSurface9 **ppBackBuffer)
{
WRAP_VERBOSE;
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_side_swapchain(iSwapChain))
{
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetBackBuffer(
iBackBuffer,
Type,
ppBackBuffer);
}
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
CHECK_RESULT(pReal->GetBackBuffer(
GFDM_NATIVE_SMALL_SWAPCHAIN,
iBackBuffer,
Type,
ppBackBuffer));
}
CHECK_RESULT(pReal->GetBackBuffer(iSwapChain, iBackBuffer, Type, ppBackBuffer));
}
@@ -461,6 +619,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetRasterStatus(
D3DRASTER_STATUS *pRasterStatus)
{
WRAP_DEBUG;
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_side_swapchain(iSwapChain))
{
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetRasterStatus(
pRasterStatus);
}
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
CHECK_RESULT(pReal->GetRasterStatus(
GFDM_NATIVE_SMALL_SWAPCHAIN,
pRasterStatus));
}
CHECK_RESULT(pReal->GetRasterStatus(iSwapChain, pRasterStatus));
}
@@ -477,6 +649,18 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetGammaRamp(
const D3DGAMMARAMP *pRamp)
{
WRAP_DEBUG;
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_side_swapchain(iSwapChain))
{
return;
}
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
pReal->SetGammaRamp(GFDM_NATIVE_SMALL_SWAPCHAIN, Flags, pRamp);
return;
}
pReal->SetGammaRamp(iSwapChain, Flags, pRamp);
}
@@ -485,6 +669,21 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGammaRamp(
D3DGAMMARAMP *pRamp)
{
WRAP_DEBUG;
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_side_swapchain(iSwapChain))
{
if (pRamp != nullptr) {
*pRamp = {};
}
return;
}
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
pReal->GetGammaRamp(GFDM_NATIVE_SMALL_SWAPCHAIN, pRamp);
return;
}
pReal->GetGammaRamp(iSwapChain, pRamp);
}
@@ -685,6 +884,19 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetFrontBufferData(
IDirect3DSurface9 *pDestSurface)
{
WRAP_DEBUG;
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_side_swapchain(iSwapChain))
{
return D3DERR_INVALIDCALL;
}
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
CHECK_RESULT(pReal->GetFrontBufferData(
GFDM_NATIVE_SMALL_SWAPCHAIN,
pDestSurface));
}
CHECK_RESULT(pReal->GetFrontBufferData(iSwapChain, pDestSurface));
}
@@ -1049,7 +1261,36 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetViewport(
D3DVIEWPORT9 *pViewport)
{
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(
@@ -1779,6 +2020,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::WaitForVBlank(
WRAP_DEBUG;
assert(is_d3d9ex);
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_side_swapchain(iSwapChain))
{
return D3D_OK;
}
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(
GFDM_NATIVE_SMALL_SWAPCHAIN));
}
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(iSwapChain));
}
@@ -1882,6 +2135,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::ResetEx");
if (is_gfdm_two_head_exclusive()) {
gfdm_disarm_present_mode_recovery();
if (pPresentationParameters == nullptr) {
return D3DERR_INVALIDCALL;
}
release_gfdm_hidden_side_swapchains();
}
if (GRAPHICS_WINDOWED) {
if (pPresentationParameters) {
pPresentationParameters->Windowed = true;
@@ -1898,13 +2159,115 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
}
}
GfdmTwoHeadDeviceState gfdm_parameters(
pPresentationParameters,
pFullscreenDisplayMode,
gfdm_logical_small_swapchain);
if (is_gfdm_two_head_exclusive()) {
HRESULT result = graphics_d3d9_gfdm_select_two_head_group_parameters(
pPresentationParameters,
pFullscreenDisplayMode,
gfdm_parameters.native_presentation_parameters.data(),
pFullscreenDisplayMode != nullptr
? gfdm_parameters.native_fullscreen_display_modes.data()
: nullptr,
&gfdm_parameters.logical_small_swapchain,
"ResetEx");
if (FAILED(result)) {
return result;
}
set_gfdm_logical_group_parameters(pPresentationParameters);
gfdm_parameters.use_native_parameters();
if (gfdm_parameters.native_presentation_parameters[0].Windowed
|| gfdm_parameters.native_presentation_parameters[1].Windowed)
{
return D3DERR_INVALIDCALL;
}
if (GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
gfdm_parameters.native_presentation_parameters[1]
.FullScreen_RefreshRateInHz =
GRAPHICS_FORCE_REFRESH_SUB.value();
if (gfdm_parameters.fullscreen_display_modes != nullptr) {
gfdm_parameters.fullscreen_display_modes[1].RefreshRate =
GRAPHICS_FORCE_REFRESH_SUB.value();
}
}
result = graphics_d3d9_gfdm_remap_two_head_group_parameters(
gfdm_parameters.native_presentation_parameters.data(),
gfdm_parameters.fullscreen_display_modes,
"ResetEx");
if (FAILED(result)) {
return result;
}
if (!graphics_gitadora_prepare_two_head_device_window(
gfdm_parameters.native_presentation_parameters[1].hDeviceWindow,
nullptr,
gfdm_parameters.native_presentation_parameters[1].BackBufferWidth,
gfdm_parameters.native_presentation_parameters[1].BackBufferHeight))
{
return D3DERR_INVALIDCALL;
}
if (gfdm_parameters.fullscreen_display_modes != nullptr) {
IDirect3D9 *raw_d3d = nullptr;
IDirect3D9Ex *raw_d3d_ex = nullptr;
D3DDEVICE_CREATION_PARAMETERS creation {};
HRESULT raw_result = pReal->GetDirect3D(&raw_d3d);
if (SUCCEEDED(raw_result) && raw_d3d != nullptr) {
raw_result = raw_d3d->QueryInterface(IID_PPV_ARGS(&raw_d3d_ex));
}
if (SUCCEEDED(raw_result)) {
raw_result = pReal->GetCreationParameters(&creation);
}
if (SUCCEEDED(raw_result) && raw_d3d_ex != nullptr) {
graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
raw_d3d_ex,
creation.AdapterOrdinal,
gfdm_parameters.native_presentation_parameters.data(),
gfdm_parameters.fullscreen_display_modes,
"ResetEx");
}
if (raw_d3d_ex != nullptr) {
raw_d3d_ex->Release();
}
if (raw_d3d != nullptr) {
raw_d3d->Release();
}
gfdm_parameters.recovery_parameters =
gfdm_parameters.native_presentation_parameters;
gfdm_parameters.recovery_modes =
gfdm_parameters.native_fullscreen_display_modes;
gfdm_parameters.recovery_candidate = true;
}
}
// reset overlay
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
overlay::OVERLAY->reset_invalidate();
}
HRESULT res = static_cast<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
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) {
@@ -1922,6 +2285,22 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayModeEx(
WRAP_DEBUG;
assert(is_d3d9ex);
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_side_swapchain(iSwapChain))
{
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetDisplayModeEx(
pMode,
pRotation);
}
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
GFDM_NATIVE_SMALL_SWAPCHAIN,
pMode,
pRotation));
}
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
iSwapChain, pMode, pRotation));
}
@@ -1,6 +1,8 @@
#pragma once
#include <array>
#include <atomic>
#include <mutex>
#include <initguid.h>
#include <d3d9.h>
@@ -40,23 +42,54 @@ static const GUID IID_WrappedIDirect3DDevice9 = {
void SurfaceHook(IDirect3DDevice9 *pReal);
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig)
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) {
explicit WrappedIDirect3DDevice9(
HWND hFocusWindow,
IDirect3DDevice9 *orig,
UINT gfdm_logical_small_swapchain = 2,
IDirect3D9 *gfdm_parent_d3d = nullptr,
const D3DPRESENT_PARAMETERS *gfdm_logical_group_parameters = nullptr)
: hFocusWindow(hFocusWindow),
pReal(orig),
is_d3d9ex(false),
gfdm_logical_small_swapchain(gfdm_logical_small_swapchain),
gfdm_parent_d3d(gfdm_parent_d3d) {
if (this->gfdm_parent_d3d != nullptr) {
this->gfdm_parent_d3d->AddRef();
}
IDirect3DDevice9Ex *device = nullptr;
// attempt to upgrade handle
if (SUCCEEDED(this->QueryInterface(IID_PPV_ARGS(&device))) && device != nullptr) {
device->Release();
}
set_gfdm_logical_group_parameters(gfdm_logical_group_parameters);
}
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9Ex *orig)
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(true) {}
explicit WrappedIDirect3DDevice9(
HWND hFocusWindow,
IDirect3DDevice9Ex *orig,
UINT gfdm_logical_small_swapchain = 2,
IDirect3D9 *gfdm_parent_d3d = nullptr,
const D3DPRESENT_PARAMETERS *gfdm_logical_group_parameters = nullptr)
: hFocusWindow(hFocusWindow),
pReal(orig),
is_d3d9ex(true),
gfdm_logical_small_swapchain(gfdm_logical_small_swapchain),
gfdm_parent_d3d(gfdm_parent_d3d) {
if (this->gfdm_parent_d3d != nullptr) {
this->gfdm_parent_d3d->AddRef();
}
set_gfdm_logical_group_parameters(gfdm_logical_group_parameters);
}
WrappedIDirect3DDevice9(const WrappedIDirect3DDevice9 &) = delete;
WrappedIDirect3DDevice9 &operator=(const WrappedIDirect3DDevice9 &) = delete;
virtual ~WrappedIDirect3DDevice9() = default;
virtual ~WrappedIDirect3DDevice9() {
if (gfdm_parent_d3d != nullptr) {
gfdm_parent_d3d->Release();
}
}
#pragma region IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
@@ -201,6 +234,27 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
#pragma endregion
bool is_gfdm_two_head_exclusive() const;
bool is_gfdm_logical_small_swapchain(UINT swapchain) const;
bool is_gfdm_logical_side_swapchain(UINT swapchain) const;
size_t gfdm_hidden_side_swapchain_slot(UINT swapchain) const;
void set_gfdm_logical_group_parameters(
const D3DPRESENT_PARAMETERS *presentation_parameters);
FakeIDirect3DSwapChain9 *ensure_gfdm_hidden_side_swapchain(UINT iSwapChain);
void release_gfdm_hidden_side_swapchains();
enum class GfdmPresentModeRecoveryResult {
NotAttempted,
ReadyToRetry,
Failed,
};
void gfdm_disarm_present_mode_recovery();
void gfdm_arm_present_mode_recovery(
const D3DPRESENT_PARAMETERS *native_presentation_parameters,
const D3DDISPLAYMODEEX *native_fullscreen_display_modes);
GfdmPresentModeRecoveryResult gfdm_recover_present_mode_change(
HRESULT *failure_result);
HWND const hFocusWindow;
IDirect3DDevice9 *pReal;
bool is_d3d9ex = false;
@@ -208,8 +262,21 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::atomic_ulong refs = 1;
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *implicit_sub_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *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;
};
@@ -59,7 +59,12 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::Present(const RECT *pSourceRe
log_misc("graphics::d3d9", "FakeIDirect3DSwapChain9::Present");
});
return D3DERR_INVALIDCALL;
if (!is_hidden) {
return D3DERR_INVALIDCALL;
}
present_count.fetch_add(1, std::memory_order_relaxed);
return D3D_OK;
}
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
WRAP_VERBOSE;
@@ -86,11 +91,32 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffe
}
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
WRAP_VERBOSE;
return D3DERR_INVALIDCALL;
if (!is_hidden) {
return D3DERR_INVALIDCALL;
}
if (pRasterStatus == nullptr) {
return D3DERR_INVALIDCALL;
}
*pRasterStatus = {};
return D3D_OK;
}
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayMode(D3DDISPLAYMODE *pMode) {
WRAP_VERBOSE;
return D3DERR_INVALIDCALL;
if (!is_hidden) {
return D3DERR_INVALIDCALL;
}
if (pMode == nullptr) {
return D3DERR_INVALIDCALL;
}
pMode->Width = present_params.BackBufferWidth;
pMode->Height = present_params.BackBufferHeight;
pMode->RefreshRate = present_params.FullScreen_RefreshRateInHz;
pMode->Format = present_params.BackBufferFormat;
return D3D_OK;
}
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDevice(IDirect3DDevice9 **ppDevice) {
WRAP_VERBOSE;
@@ -108,17 +134,46 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentParameters(
D3DPRESENT_PARAMETERS *pPresentationParameters)
{
WRAP_VERBOSE;
return D3DERR_INVALIDCALL;
if (!is_hidden) {
return D3DERR_INVALIDCALL;
}
if (pPresentationParameters == nullptr) {
return D3DERR_INVALIDCALL;
}
*pPresentationParameters = present_params;
return D3D_OK;
}
// IDirect3DSwapChain9Ex
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetLastPresentCount(UINT *pLastPresentCount) {
assert(is_d3d9ex);
return D3DERR_INVALIDCALL;
if (!is_hidden) {
return D3DERR_INVALIDCALL;
}
if (pLastPresentCount == nullptr) {
return D3DERR_INVALIDCALL;
}
*pLastPresentCount = present_count.load(std::memory_order_relaxed);
return D3D_OK;
}
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentStats(D3DPRESENTSTATS *pPresentationStatistics) {
assert(is_d3d9ex);
return D3DERR_INVALIDCALL;
if (!is_hidden) {
return D3DERR_INVALIDCALL;
}
if (pPresentationStatistics == nullptr) {
return D3DERR_INVALIDCALL;
}
*pPresentationStatistics = {};
pPresentationStatistics->PresentCount =
present_count.load(std::memory_order_relaxed);
return D3D_OK;
}
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMODEEX *pMode,
D3DDISPLAYROTATION *pRotation)
@@ -126,5 +181,22 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMO
WRAP_VERBOSE;
assert(is_d3d9ex);
return D3DERR_INVALIDCALL;
if (!is_hidden) {
return D3DERR_INVALIDCALL;
}
if (pMode == nullptr) {
return D3DERR_INVALIDCALL;
}
pMode->Size = sizeof(*pMode);
pMode->Width = present_params.BackBufferWidth;
pMode->Height = present_params.BackBufferHeight;
pMode->RefreshRate = present_params.FullScreen_RefreshRateInHz;
pMode->Format = present_params.BackBufferFormat;
pMode->ScanLineOrdering = D3DSCANLINEORDERING_PROGRESSIVE;
if (pRotation != nullptr) {
*pRotation = D3DDISPLAYROTATION_IDENTITY;
}
return D3D_OK;
}
@@ -6,14 +6,19 @@
#endif
#include <atomic>
#include <vector>
#include <d3d9.h>
#include "util/logging.h"
struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
FakeIDirect3DSwapChain9(IDirect3DDevice9 *pDev, D3DPRESENT_PARAMETERS *present_params, bool is_d3d9ex) :
pDev(pDev), is_d3d9ex(is_d3d9ex)
FakeIDirect3DSwapChain9(
IDirect3DDevice9 *pDev,
D3DPRESENT_PARAMETERS *present_params,
bool is_d3d9ex,
bool is_hidden = false) :
pDev(pDev), is_d3d9ex(is_d3d9ex), is_hidden(is_hidden)
{
// copy presentation parameters
memcpy(&this->present_params, present_params, sizeof(this->present_params));
@@ -78,8 +83,10 @@ struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
IDirect3DDevice9 *const pDev;
bool is_d3d9ex;
bool is_hidden;
std::atomic<ULONG> ref_cnt = 1;
std::atomic<UINT> present_count = 0;
D3DPRESENT_PARAMETERS present_params {};
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;
}
if (//riid == __uuidof(IUnknown) || Ignore IUnknown, it's often queried to test object equality between different interfaces
if ((riid == IID_IUnknown && pDev->is_gfdm_two_head_exclusive()) ||
riid == IID_IDirect3DSwapChain9 ||
riid == IID_IDirect3DSwapChain9Ex)
{
@@ -89,7 +89,48 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
}
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags));
HRESULT result = pReal->Present(
pSourceRect,
pDestRect,
hDestWindowOverride,
pDirtyRegion,
dwFlags);
// a successful game subscreen present suppresses the next forced fallback present.
if (SUCCEEDED(result) &&
(this == pDev->implicit_sub_swapchain || this == pDev->sub_swapchain[0])) {
graphics_d3d9_notify_subscreen_present();
}
// Some drivers report S_PRESENT_MODE_CHANGED after Windows moves the portrait SMALL
// head into the requested exclusive mode, leaving both group heads black. Toggle SMALL
// through another supported mode and restore it once, then retry the interrupted MAIN
// present. Recovery is restricted to the creation thread and consumes its armed state,
// so subsequent presents cannot repeat the mode switch.
if (pDev->is_gfdm_two_head_exclusive()
&& native_group_head == NativeGroupHead::Main
&& result == S_PRESENT_MODE_CHANGED)
{
HRESULT recovery_failure = D3D_OK;
const auto recovery = pDev->gfdm_recover_present_mode_change(
&recovery_failure);
if (recovery
== WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult::ReadyToRetry)
{
result = pReal->Present(
pSourceRect,
pDestRect,
hDestWindowOverride,
pDirtyRegion,
dwFlags);
} else if (recovery
== WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult::Failed
&& FAILED(recovery_failure))
{
result = recovery_failure;
}
}
CHECK_RESULT(result);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
CHECK_RESULT(pReal->GetFrontBufferData(pDestSurface));
@@ -5,8 +5,13 @@
interface WrappedIDirect3DDevice9;
struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9 *orig) :
pDev(dev), pReal(orig), is_d3d9ex(false)
enum class NativeGroupHead { Unknown, Main, Small };
WrappedIDirect3DSwapChain9(
WrappedIDirect3DDevice9 *dev,
IDirect3DSwapChain9 *orig,
NativeGroupHead native_group_head = NativeGroupHead::Unknown) :
pDev(dev), pReal(orig), is_d3d9ex(false), native_group_head(native_group_head)
{
IDirect3DSwapChain9Ex *swapchain = nullptr;
@@ -16,8 +21,11 @@ struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
}
}
WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9Ex *orig) :
pDev(dev), pReal(orig), is_d3d9ex(true)
WrappedIDirect3DSwapChain9(
WrappedIDirect3DDevice9 *dev,
IDirect3DSwapChain9Ex *orig,
NativeGroupHead native_group_head = NativeGroupHead::Unknown) :
pDev(dev), pReal(orig), is_d3d9ex(true), native_group_head(native_group_head)
{
}
@@ -53,6 +61,7 @@ struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
IDirect3DSwapChain9 *pReal;
bool is_d3d9ex = false;
NativeGroupHead native_group_head = NativeGroupHead::Unknown;
bool should_run_hooks = true;
};
+135 -2
View File
@@ -17,6 +17,7 @@
#include "games/ddr/ddr.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "games/sdvx/sdvx.h"
#include "games/popn/popn.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
@@ -44,6 +45,8 @@ HWND SDVX_SUBSCREEN_WINDOW = nullptr;
HWND GFDM_SUBSCREEN_WINDOW = nullptr;
static HWND GFDM_LEFT_WINDOW = nullptr;
static HWND GFDM_RIGHT_WINDOW = nullptr;
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
HWND POPN_SUBSCREEN_WINDOW = nullptr;
bool FAKE_SUBSCREEN_ADAPTER = false;
@@ -183,6 +186,13 @@ static bool is_gfdm_known_window(HWND hWnd) {
return gitadora_window_name_for_hwnd(hWnd) != nullptr;
}
static bool is_gfdm_two_head_small_window(HWND hWnd) {
return games::gitadora::is_arena_model() &&
games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE &&
hWnd != nullptr &&
hWnd == GFDM_TWO_HEAD_SMALL_WINDOW;
}
static bool gitadora_should_block_game_window_placement(HWND hWnd) {
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
return false;
@@ -204,6 +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() {
return GRAPHICS_WINDOWED &&
games::gitadora::is_arena_model() &&
@@ -618,9 +717,16 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
}
// only hook touch window if multiple windows are allowed
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
gitadora_remember_window(result, gfdm_window_name);
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
}
if (is_gfdm_sub_window &&
((GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) ||
games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE)) {
gitadora_remember_window(result, gfdm_window_name);
if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
}
}
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_force_window_style(result);
@@ -767,6 +873,10 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
nHeight,
bRepaint);
if (is_gfdm_two_head_small_window(hWnd)) {
return TRUE;
}
// sound voltex windowed mode adjustments
if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) {
RECT rect {};
@@ -874,6 +984,11 @@ static HCURSOR WINAPI SetCursor_hook(HCURSOR hCursor) {
static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
if (is_gfdm_two_head_small_window(hWnd) &&
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE)) {
return GetWindowLongA(hWnd, nIndex);
}
// DDR window style fix
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
dwNewLong |= WS_OVERLAPPEDWINDOW;
@@ -894,6 +1009,11 @@ static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
if (is_gfdm_two_head_small_window(hWnd) &&
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE)) {
return GetWindowLongW(hWnd, nIndex);
}
// DDR overlapped window fix
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
dwNewLong |= WS_OVERLAPPEDWINDOW;
@@ -915,6 +1035,12 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
int X, int Y, int cx, int cy, UINT uFlags) {
if (is_gfdm_two_head_small_window(hWnd) &&
((uFlags & SWP_HIDEWINDOW) ||
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
return TRUE;
}
// windowed mode adjustments
if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) {
return TRUE;
@@ -939,6 +1065,13 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
}
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
if (is_gfdm_two_head_small_window(hWnd) &&
(nCmdShow == SW_HIDE || nCmdShow == SW_MINIMIZE ||
nCmdShow == SW_SHOWMINIMIZED || nCmdShow == SW_SHOWMINNOACTIVE ||
nCmdShow == SW_FORCEMINIMIZE)) {
return TRUE;
}
if (games::gitadora::is_arena_model() &&
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
hWnd != GRAPHICS_HOOKED_WINDOW) {
+9
View File
@@ -119,6 +119,15 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
void graphics_init();
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
// The native GITADORA two-head D3D9 group uses the game's named SMALL
// device window for the native physical SMALL head. The game requests
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
// to the requested native SMALL mode before the native fullscreen device is
// created. During the adapter-group startup warmup, the monitor can briefly
// report a different mode; preserve its origin but not that temporary size.
bool graphics_gitadora_prepare_two_head_device_window(
HWND hWnd, HMONITOR target_monitor = nullptr, UINT desired_width = 0,
UINT desired_height = 0);
void graphics_add_wnd_proc(WNDPROC wndProc);
void graphics_remove_wnd_proc(WNDPROC wndProc);
void graphics_hook_subscreen_window(HWND hWnd);
+49 -3
View File
@@ -13,7 +13,9 @@
#include "avs/game.h"
#include "games/iidx/iidx.h"
#include "games/gitadora/gitadora.h"
#include "games/popn/popn.h"
#include "games/sdvx/sdvx.h"
#include "util/deferlog.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -29,6 +31,7 @@ static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
#ifdef SPICE64
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_orig = nullptr;
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
@@ -182,9 +185,30 @@ static BOOL WINAPI IsDBCSLeadByte_hook (
BYTE TestChar
)
{
if (IsDBCSLeadByteEx_orig) {
return IsDBCSLeadByteEx_orig(CODEPAGE_SHIFT_JIS, TestChar);
}
return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar);
}
static BOOL WINAPI IsDBCSLeadByteEx_hook(
UINT CodePage,
BYTE TestChar)
{
switch (CodePage) {
case CP_ACP:
case CP_THREAD_ACP:
CodePage = CODEPAGE_SHIFT_JIS;
break;
default:
break;
}
return IsDBCSLeadByteEx_orig(CodePage, TestChar);
}
static
int
WINAPI
@@ -250,6 +274,18 @@ GetLocaleInfoA_hook(
void hooks::lang::early_init() {
log_info("hooks::lang", "early initialization");
const auto native_code_page = GetACP();
if (native_code_page == CP_UTF8) {
log_warning(
"hooks::lang",
"Windows is using UTF-8 as the system code page; "
"some games may render text incorrectly or behave unexpectedly");
deferredlogs::defer_error_messages({
"Windows is using UTF-8 as the system code page",
" some games may render text incorrectly or behave unexpectedly"});
}
// hooking these two functions fixes the jubeat mojibake
detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig);
detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);
@@ -296,8 +332,9 @@ void hooks::lang::early_init() {
&GetLocaleInfoA_orig);
}
// for TDJ subscreen search keyboard
if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) {
// for TDJ subscreen search keyboard and T44 narrow-string handling
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
avs::game::is_model("T44")) {
log_info("hooks::lang", "hooking IsDBCSLeadByte");
detour::trampoline_try(
"kernel32.dll",
@@ -306,7 +343,7 @@ void hooks::lang::early_init() {
&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");
detour::trampoline_try(
"kernel32.dll",
@@ -314,6 +351,15 @@ void hooks::lang::early_init() {
WideCharToMultiByte_hook,
&WideCharToMultiByte_orig);
}
if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) {
detour::trampoline_try(
"kernel32.dll",
"IsDBCSLeadByteEx",
IsDBCSLeadByteEx_hook,
&IsDBCSLeadByteEx_orig);
}
#endif
}
+35 -29
View File
@@ -5,7 +5,6 @@
#include <cmath>
#include <cstdlib>
#include <assert.h>
#include <shlwapi.h>
#include <windows.h>
#include <cfg/configurator.h>
@@ -524,22 +523,25 @@ int main_implementation(int argc, char *argv[]) {
games::iidx::FLIP_CAMS = true;
}
// IIDX CONNECT_CAMERA logic here for cases where user is running without -iidx module
// for now, we assume that user may be running on a cab
// (games::iidx::DISABLE_CAMS starts out as false unless user overrides)
// we will check again in IIDX module with a different default
assert(!games::iidx::DISABLE_CAMS.has_value());
if (options[launcher::Options::IIDXDisableCameras].value_bool()) {
games::iidx::DISABLE_CAMS = true;
// Resolve the IIDX camera policy here so it also applies without the -iidx module.
const auto &cab_camera_access = options[launcher::Options::IIDXCabCamAccess];
if (cab_camera_access.is_active()) {
const auto value = cab_camera_access.value_text();
if (value == "off") {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
} else if (value == "on") {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::on;
} else if (value == "legacy") {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::legacy;
}
}
if (options[launcher::Options::IIDXCabCamAccess].is_active() &&
options[launcher::Options::IIDXCabCamAccess].value_text() == "off") {
games::iidx::DISABLE_CAMS = true;
if (options[launcher::Options::IIDXDisableCameras].value_bool()) {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
}
if (options[launcher::Options::IIDXCamHook].value_bool()) {
games::iidx::TDJ_CAMERA = true;
// Disable legacy behaviour to avoid conflict
games::iidx::DISABLE_CAMS = true;
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
}
// CONNECT_CAMERA env var will be set once logging is enabled
@@ -644,6 +646,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::LoadGitaDoraModule].value_bool()) {
attach_gitadora = true;
}
if (options[launcher::Options::GitaDoraDisableFrameLimiter].value_bool()) {
games::gitadora::DISABLE_FRAME_LIMITER = true;
}
if (options[launcher::Options::GitaDoraCabinetType].is_active()) {
games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32();
}
@@ -1702,31 +1707,25 @@ int main_implementation(int argc, char *argv[]) {
}
}
// SDVXFullscreenLandscape disabled due to it messing with in-game camera angle
// FullscreenOrientationFlip continues to live on, however.
if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !cfg::CONFIGURATOR_STANDALONE) {
log_fatal("launcher", "-sdvxlandscape removed due to a camera bug in SDVX!");
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::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) {
GRAPHICS_FS_ORIENTATION_SWAP = true;
}
// for cab usage - set environment variables (outside of -iidx module)
if (games::iidx::DISABLE_CAMS.has_value() &&
games::iidx::DISABLE_CAMS.value() &&
// apply an explicit off outside of the -iidx module so it also works on cabinets.
if (games::iidx::CAB_CAMERA_ACCESS == games::iidx::cab_camera_access_mode::off &&
!cfg::CONFIGURATOR_STANDALONE) {
log_misc("launcher::iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
if (games::iidx::SOUND_OUTPUT_DEVICE.has_value() &&
games::iidx::SOUND_OUTPUT_DEVICE.value() != "auto" &&
!cfg::CONFIGURATOR_STANDALONE) {
@@ -1855,6 +1854,13 @@ int main_implementation(int argc, char *argv[]) {
break;
}
if (check_dll("jubeat2019.dll")) {
avs::game::DLL_NAME = "jubeat2019.dll";
attach_io = true;
attach_jb = true;
break;
}
// RB
if (check_dll("reflecbeat.dll")) {
avs::game::DLL_NAME = "reflecbeat.dll";
@@ -2283,7 +2289,7 @@ int main_implementation(int argc, char *argv[]) {
games.push_back(new games::sdvx::SDVXGame());
}
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());
}
if (attach_nostalgia) {
+32 -15
View File
@@ -330,9 +330,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc =
"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"
"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 "
"with Live2D!",
"Strongly consider combining this with -forceres option to render at monitor native resolution.\n\n"
"For SDVX, use the dedicated -sdvxlandscape option.",
.type = OptionType::Bool,
.hidden = true,
.category = "Full Screen Settings"
@@ -641,17 +640,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// IIDXCabCamAccess
.title = "IIDX Use Official AC Cams",
.title = "IIDX Official AC Camera Access",
.name = "iidxcabcams",
.desc = "For IIDX25+, allow direct access to cameras from real arcade cabinets. "
"Only turn this on if you have OFFICIAL arcade cameras connected to the correct USB ports. Default: auto.",
.desc = "Controls how the game accesses USB cameras for IIDX 25+.\n\n"
"auto (default): use [off] when the IIDX module is enabled; otherwise, use [on].\n\n"
"on: game discovers and directly accesses cameras; requires official cameras on correct USB ports.\n\n"
"legacy: for IIDX 25/26 only; allow camera access with emulated discovery.\n\n"
"off: prevent game from accessing USB cameras.",
.type = OptionType::Enum,
.game_name = "Beatmania IIDX",
.category = "Cab Peripherals",
.elements = {
{"auto", ""},
{"on", ""},
{"legacy", ""},
{"off", ""},
{"on", ""}
},
},
{
@@ -978,7 +981,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.display_name = "sdvxsubredraw",
.aliases= "sdvxsubredraw",
.desc = "Check if submonitor in fullscreen mode doesn't update every frame; "
"this option forces subscreen to redraw every frame.",
"this option presents the subscreen when the game does not.",
.type = OptionType::Bool,
.game_name = "Sound Voltex",
.category = "Advanced Game Options",
@@ -1172,7 +1175,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
.name = "popnsubredraw",
.desc = "Check if submonitor in fullscreen mode appears stuck; "
"this option forces subscreen to redraw every frame.",
"this option presents the subscreen when the game does not.",
.type = OptionType::Bool,
.game_name = "Pop'n Music",
.category = "Advanced Game Options",
@@ -1203,6 +1206,19 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Options",
.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",
.name = "gdcabtype",
@@ -1299,7 +1315,9 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "gdalayout",
.desc = "For Arena Model: select how many windows to create.\n\n"
"1 window: main only; use the subscreen overlay for SMALL.\n\n"
"2 windows: main + SMALL. Note: currently only works for windowed mode, broken for fullscreen.\n\n"
"2 windows: main + SMALL. Without -w, this uses native D3D9 adapter-group fullscreen "
"with physical MAIN/SMALL and virtual LEFT/RIGHT. It requires two active displays on "
"one adapter group. With -w, it uses borderless windows instead.\n\n"
"4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the "
"right resolution; see wiki for details.",
.type = OptionType::Enum,
@@ -2165,7 +2183,6 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{"25", "+25 dB"},
{"30", "+30 dB"},
},
.quick_setting_category = "Audio",
},
{
// AudioResample
@@ -3012,17 +3029,17 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// SDVXFullscreenLandscape
.title = "(DISABLED) SDVX Landscape Mode (SDVX5+)",
.title = "SDVX Full Screen Landscape Mode (SDVX5+, EXPERIMENTAL)",
.name = "sdvxlandscape",
.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"
"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.",
.type = OptionType::Bool,
.hidden = true,
.game_name = "Sound Voltex",
.category = "Game Options",
.quick_setting_category = "Game",
},
{
// spice2x_EnableSMXStage
+1
View File
@@ -114,6 +114,7 @@ namespace launcher {
LoadHelloPopnMusicModule,
LoadGitaDoraModule,
GitaDoraTwoChannelAudio,
GitaDoraDisableFrameLimiter,
GitaDoraCabinetType,
GitaDoraLefty,
GitaDoraWailHold,
+2 -1
View File
@@ -673,7 +673,8 @@ void eamuse_autodetect_game() {
eamuse_set_game("Beatmania IIDX");
else if (avs::game::is_model("J44") ||
avs::game::is_model("K44") ||
avs::game::is_model("L44"))
avs::game::is_model("L44") ||
avs::game::is_model("T44"))
eamuse_set_game("Jubeat");
else if (avs::game::is_model("KDM"))
eamuse_set_game("Dance Evolution");
+2 -1
View File
@@ -660,7 +660,8 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
if (logical_max > field_max) {
log_info(
"rawinput",
"value cap {} LogicalMax exceeds bit width, fixing it up: {} -> {}",
"device {}: value cap {} LogicalMax exceeds bit width, fixing it up: {} -> {}",
new_device.name,
value_cap_num,
value_caps.LogicalMax,
field_max
+2 -3
View File
@@ -394,11 +394,10 @@ namespace nativetouch::inject {
// preserve native registration and attach mouse injection to the touch window
static BOOL WINAPI RegisterTouchWindowHook(HWND window, ULONG flags) {
// for TDJ in windowed subscreen mode, the main game window is
// target for delivering touch messages, not the subscreen
// TDJ handles touches on the main window, including contacts that the
// dedicated subscreen forwards there
if (GRAPHICS_IIDX_WSUB && window != TDJ_SUBSCREEN_WINDOW) {
touch_delivery_window.store(window, std::memory_order_release);
return TRUE;
}
// call original