mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-09-30 22:14:55 -07:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adf4cccd4a | |||
| bf8e194685 | |||
| a2e508208c | |||
| f5888609a8 | |||
| c6cd72c528 | |||
| 82e0c053d0 | |||
| 3b29227dbc | |||
| ce2f93d234 | |||
| 13a171f199 | |||
| 3da352488e | |||
| 2386047c2f | |||
| baa550037a | |||
| b53447bed5 | |||
| 4959a58de3 | |||
| 9141ff453b | |||
| f955ab984c | |||
| 435b9ce9aa | |||
| 1ec7528dac | |||
| e7822aad8e | |||
| 8140e83e46 | |||
| cc0899290e | |||
| 3012139028 | |||
| c590b87cff | |||
| 5cabd026ae | |||
| 7ccd938f9b | |||
| 88a210e82e | |||
| 74e619df37 | |||
| 10a97b9c63 | |||
| 159043803c | |||
| bd2fbfcb67 | |||
| 64600826b7 | |||
| 80deb4bbbd | |||
| 00c8bb8ce6 | |||
| 09ed8b948c | |||
| e2daefbb6a | |||
| 39bf17aafe | |||
| 4836cd94ab | |||
| 9d41ca2515 | |||
| 18421ffbfe | |||
| 3f0921d983 | |||
| ed7318c270 | |||
| 623e1e3998 | |||
| 558e3d0cb3 | |||
| d0db547dd0 | |||
| be4bb00390 | |||
| a8cc85531c | |||
| 2dea0d27bf | |||
| 61a0220c1a | |||
| c080bbe301 | |||
| e1d1b39567 |
@@ -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,12 +412,14 @@ 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
|
||||
games/nost/nost.cpp
|
||||
games/nost/io.cpp
|
||||
games/nost/poke.cpp
|
||||
games/nost/touch_mode.cpp
|
||||
games/gitadora/gitadora.cpp
|
||||
games/gitadora/asio.cpp
|
||||
games/gitadora/io.cpp
|
||||
@@ -502,6 +505,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/audio/buffer.cpp
|
||||
hooks/audio/mme.cpp
|
||||
hooks/audio/util.cpp
|
||||
hooks/audio/xact.cpp
|
||||
hooks/audio/backends/dsound/dsound_backend.cpp
|
||||
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
|
||||
hooks/audio/backends/mmdevice/device.cpp
|
||||
@@ -528,10 +532,13 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/devicehook.cpp
|
||||
hooks/graphics/graphics.cpp
|
||||
hooks/graphics/graphics_windowed.cpp
|
||||
hooks/graphics/nvapi_impl.cpp
|
||||
hooks/graphics/nvapi_hook.cpp
|
||||
hooks/graphics/nvenc_hook.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_screenshot.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
|
||||
@@ -571,7 +578,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
misc/device.cpp
|
||||
misc/eamuse.cpp
|
||||
misc/extdev.cpp
|
||||
misc/nativetouchhook.cpp
|
||||
misc/hotkeys.cpp
|
||||
misc/sciunit.cpp
|
||||
misc/sde.cpp
|
||||
misc/wintouchemu.cpp
|
||||
@@ -609,17 +616,26 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
overlay/windows/keypad.cpp
|
||||
overlay/windows/log.cpp
|
||||
overlay/windows/midi.cpp
|
||||
overlay/windows/nostalgia_touch_piano.cpp
|
||||
overlay/windows/obs.cpp
|
||||
overlay/windows/obs_websocket.cpp
|
||||
overlay/windows/patch_manager.cpp
|
||||
overlay/windows/popn_sub.cpp
|
||||
overlay/windows/wnd_manager.cpp
|
||||
|
||||
# patcher
|
||||
patcher/config.cpp
|
||||
patcher/lifecycle.cpp
|
||||
patcher/loader.cpp
|
||||
patcher/loader_group.cpp
|
||||
patcher/runtime.cpp
|
||||
|
||||
# external
|
||||
external/easywsclient/easywsclient.cpp
|
||||
|
||||
# rawinput
|
||||
rawinput/rawinput.cpp
|
||||
rawinput/rawinput_handles.cpp
|
||||
rawinput/midi.cpp
|
||||
rawinput/sextet.cpp
|
||||
rawinput/piuio.cpp
|
||||
@@ -648,6 +664,11 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
|
||||
# touch
|
||||
touch/gdi_overlay.cpp
|
||||
touch/native/inject.cpp
|
||||
touch/native/inject_mouse.cpp
|
||||
touch/native/inject_synthetic.cpp
|
||||
touch/native/nativetouchhook.cpp
|
||||
touch/native/transform.cpp
|
||||
touch/touch.cpp
|
||||
touch/touch_gestures.cpp
|
||||
touch/win7.cpp
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
// static stuff
|
||||
static int ACIO_WARMUP = 0;
|
||||
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
|
||||
// flush device output
|
||||
RI_MGR->devices_flush_output();
|
||||
|
||||
// update wintouchemu
|
||||
// update touch emulation
|
||||
wintouchemu::update();
|
||||
nativetouch::refresh_contact_lifetime();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "games/nost/io.h"
|
||||
#include "games/nost/nost.h"
|
||||
#include "games/nost/touch_mode.h"
|
||||
#include "util/logging.h"
|
||||
#include "avs/game.h"
|
||||
|
||||
@@ -16,6 +17,7 @@ using namespace GameAPI;
|
||||
// static stuff
|
||||
static uint8_t STATUS_BUFFER[277];
|
||||
static bool STATUS_BUFFER_FREEZE = false;
|
||||
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
|
||||
|
||||
/*
|
||||
* Implementations
|
||||
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
|
||||
};
|
||||
|
||||
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
|
||||
|
||||
// iterate pairs of keys
|
||||
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) {
|
||||
|
||||
@@ -257,6 +261,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
if (button0 > 0) {
|
||||
state0 = button0;
|
||||
}
|
||||
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2))) &&
|
||||
state0 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||
state0 = NOST_TOUCH_PIANO_VELOCITY;
|
||||
}
|
||||
|
||||
const auto button1 = panb_get_button_velocity(
|
||||
buttons.at(button_mapping[key_pair * 2 + 1]),
|
||||
@@ -267,6 +275,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
if (button1 > 0) {
|
||||
state1 = button1;
|
||||
}
|
||||
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2 + 1))) &&
|
||||
state1 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||
state1 = NOST_TOUCH_PIANO_VELOCITY;
|
||||
}
|
||||
|
||||
// build value
|
||||
uint8_t value = 0;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#include "keypads.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
@@ -59,7 +60,6 @@ namespace api::modules {
|
||||
// get params
|
||||
auto keypad = req.params[0].GetUint();
|
||||
auto input = std::string(req.params[1].GetString());
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// process all chars
|
||||
for (auto c : input) {
|
||||
@@ -93,11 +93,11 @@ namespace api::modules {
|
||||
|
||||
// set
|
||||
eamuse_set_keypad_overrides(keypad, state);
|
||||
timer.sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
|
||||
// unset
|
||||
eamuse_set_keypad_overrides(keypad, 0);
|
||||
timer.sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ¶m : 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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "touch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include "external/rapidjson/document.h"
|
||||
@@ -8,7 +10,11 @@
|
||||
#include "misc/eamuse.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "touch/native/transform.h"
|
||||
#include "util/utils.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
@@ -17,6 +23,54 @@ using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
static bool inject_native_touch(
|
||||
const std::vector<TouchPoint> &touch_points, int canvas_w, int canvas_h) {
|
||||
if (touch_points.empty()) {
|
||||
// no active touches remain; release the synthetic contact
|
||||
return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false);
|
||||
}
|
||||
|
||||
// only a single synthetic contact is supported, so just use the first touch point
|
||||
const auto &touch = touch_points.front();
|
||||
POINT position { touch.x, touch.y };
|
||||
if (canvas_w > 0 && canvas_h > 0) {
|
||||
return nativetouch::inject::inject_synthetic_touch_from_canvas(
|
||||
position,
|
||||
{ canvas_w, canvas_h },
|
||||
true);
|
||||
}
|
||||
return nativetouch::inject::inject_synthetic_touch(position, true);
|
||||
}
|
||||
|
||||
// map API coordinates onto the touch space SDVX reads, which depends on how it is displayed
|
||||
static void sdvx_touch_errata(
|
||||
int &x, int &y, bool use_native, int canvas_w, int canvas_h) {
|
||||
|
||||
// windowed coordinates already match the sub screen window they land on
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// landscape mode: native injection hands the game these coordinates
|
||||
// unchanged, so apply the rotation the touchscreen gets, while wintouchemu instead
|
||||
// rotates them later through the subscreen overlay
|
||||
if (GRAPHICS_FS_ORIENTATION_SWAP) {
|
||||
if (use_native) {
|
||||
POINT position { x, y };
|
||||
if (nativetouch::transform::sdvx_landscape_rotate(&position, canvas_w, canvas_h)) {
|
||||
x = position.x;
|
||||
y = position.y;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// rotate into the portrait touch space
|
||||
const int x_raw = x;
|
||||
x = canvas_w - y;
|
||||
y = x_raw;
|
||||
}
|
||||
|
||||
Touch::Touch() : Module("touch") {
|
||||
is_sdvx = avs::game::is_model("KFC");
|
||||
|
||||
@@ -26,6 +80,32 @@ namespace api::modules {
|
||||
is_tdj_fhd = false;
|
||||
}
|
||||
|
||||
use_native = nativetouch::is_hooked();
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata)
|
||||
native_canvas_w = 0;
|
||||
native_canvas_h = 0;
|
||||
if (is_sdvx) {
|
||||
// windowed API coordinates land on the sub screen window as-is; fullscreen
|
||||
// coordinates are rotated into the game's touch space by apply_touch_errata
|
||||
const bool landscape_coordinates =
|
||||
GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP;
|
||||
native_canvas_w = landscape_coordinates ? 1920 : 1080;
|
||||
native_canvas_h = landscape_coordinates ? 1080 : 1920;
|
||||
} else if (avs::game::is_model("LDJ")) {
|
||||
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
||||
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
||||
native_canvas_h = is_tdj_fhd ? 1080 : 720;
|
||||
} else if (avs::game::is_model("M39")) {
|
||||
// pop'n music API touch surface
|
||||
native_canvas_w = 1280;
|
||||
native_canvas_h = 800;
|
||||
} else if (games::gitadora::is_arena_model()) {
|
||||
// GITADORA arena SMALL subscreen, either in its own window or in the overlay
|
||||
native_canvas_w = games::gitadora::ARENA_SUBSCREEN_WIDTH;
|
||||
native_canvas_h = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
|
||||
}
|
||||
|
||||
functions["read"] = std::bind(&Touch::read, this, _1, _2);
|
||||
functions["write"] = std::bind(&Touch::write, this, _1, _2);
|
||||
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
|
||||
@@ -99,7 +179,11 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// apply touch points
|
||||
touch_write_points(&touch_points);
|
||||
if (use_native) {
|
||||
write_native(touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,23 +207,54 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// remove all IDs
|
||||
touch_remove_points(&touch_point_ids);
|
||||
if (use_native) {
|
||||
write_reset_native(touch_point_ids);
|
||||
} else {
|
||||
touch_remove_points(&touch_point_ids);
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_native(const std::vector<TouchPoint> &touch_points) {
|
||||
if (touch_points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
const auto touch_id = touch_points.front().id;
|
||||
if (native_touch_id && *native_touch_id != touch_id) {
|
||||
if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
return;
|
||||
}
|
||||
native_touch_id.reset();
|
||||
}
|
||||
if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id = touch_id;
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_reset_native(const std::vector<DWORD> &touch_point_ids) {
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
if (!native_touch_id ||
|
||||
std::find(
|
||||
touch_point_ids.begin(),
|
||||
touch_point_ids.end(),
|
||||
*native_touch_id) == touch_point_ids.end()) {
|
||||
return;
|
||||
}
|
||||
if (inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::apply_touch_errata(int &x, int &y) {
|
||||
int x_raw = x;
|
||||
int y_raw = y;
|
||||
|
||||
if (is_tdj_fhd) {
|
||||
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p)
|
||||
// we don't know what screen is being shown on the companion and the API doesn't specify
|
||||
// the target of the touch events so just assume it's the sub screen
|
||||
x = x_raw * 1920 / 1280;
|
||||
y = y_raw * 1080 / 720;
|
||||
x = x * 1920 / 1280;
|
||||
y = y * 1080 / 720;
|
||||
} else if (is_sdvx) {
|
||||
// for exceed gear, they are both 1080p screens, but need to apply transformation
|
||||
x = 1080 - y_raw;
|
||||
y = x_raw;
|
||||
sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
#include "touch/touch.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
@@ -13,11 +17,21 @@ namespace api::modules {
|
||||
private:
|
||||
bool is_sdvx;
|
||||
bool is_tdj_fhd;
|
||||
bool use_native;
|
||||
std::mutex native_touch_mutex;
|
||||
std::optional<uint32_t> native_touch_id;
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata);
|
||||
// used to normalize before mapping into the native injection window
|
||||
int native_canvas_w;
|
||||
int native_canvas_h;
|
||||
|
||||
// function definitions
|
||||
void read(Request &req, Response &res);
|
||||
void write(Request &req, Response &res);
|
||||
void write_reset(Request &req, Response &res);
|
||||
void write_native(const std::vector<TouchPoint> &touch_points);
|
||||
void write_reset_native(const std::vector<DWORD> &touch_point_ids);
|
||||
void apply_touch_errata(int &x, int &y);
|
||||
|
||||
};
|
||||
|
||||
@@ -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,11 +1,12 @@
|
||||
#include "controller.h"
|
||||
#include "serial.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
|
||||
@@ -17,7 +18,6 @@ namespace api {
|
||||
controller->init_state(this->state);
|
||||
this->thread = new std::thread([this] () {
|
||||
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// read buffer
|
||||
uint8_t read_buffer[16*1024];
|
||||
@@ -162,7 +162,7 @@ namespace api {
|
||||
|
||||
// slow down on reconnect
|
||||
if (this->running) {
|
||||
timer.sleep(retry_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_time));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1114,6 +1114,7 @@ namespace avs {
|
||||
#endif
|
||||
// jubeat
|
||||
{"jubeat.dll", 0x2000000},
|
||||
{"jubeat2019.dll", 0x10000000},
|
||||
|
||||
// MUSECA
|
||||
{"museca.dll", 0xC000000},
|
||||
@@ -1924,7 +1925,7 @@ namespace avs {
|
||||
#if !SPICE64
|
||||
// sdvx4 bad log config fix
|
||||
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
||||
property_search_safe(config, config_node, "/log/enable_console")) {
|
||||
property_search(config, config_node, "/log/enable_console")) {
|
||||
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
|
||||
property_search_remove_safe(config, config_node, "/log/enable_console");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
+66
-11
@@ -3,7 +3,9 @@
|
||||
#include <cassert>
|
||||
#include <optional>
|
||||
|
||||
#include "games/io.h"
|
||||
#include "launcher/superexit.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "rawinput/piuio.h"
|
||||
#include "util/time.h"
|
||||
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
|
||||
return sorted;
|
||||
}
|
||||
|
||||
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
|
||||
namespace GameAPI::Buttons {
|
||||
static State get_button_state(
|
||||
rawinput::RawInputManager *manager,
|
||||
Button &button,
|
||||
bool check_alts,
|
||||
bool check_modifiers);
|
||||
|
||||
static bool modifiers_pressed(rawinput::RawInputManager *manager, Button &button);
|
||||
}
|
||||
|
||||
bool GameAPI::Buttons::modifiers_pressed(rawinput::RawInputManager *manager, Button &button) {
|
||||
const auto modifier_mask = button.getModifierMask();
|
||||
if (modifier_mask == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(eamuse_get_game());
|
||||
if (!modifier_buttons) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
|
||||
if ((modifier_mask & (UINT8_C(1) << index)) != 0 &&
|
||||
(index >= modifier_buttons->size() ||
|
||||
get_button_state(manager, modifier_buttons->at(index), true, false) !=
|
||||
GameAPI::Buttons::BUTTON_PRESSED)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
GameAPI::Buttons::State GameAPI::Buttons::get_button_state(
|
||||
rawinput::RawInputManager *manager,
|
||||
Button &_button,
|
||||
bool check_alts,
|
||||
bool check_modifiers) {
|
||||
|
||||
// check override
|
||||
if (_button.override_enabled) {
|
||||
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
std::optional<bool> window_has_focus;
|
||||
while (true) {
|
||||
|
||||
// skip bindings whose required modifiers are not held
|
||||
//
|
||||
// note that modifiers cannot process MIDI as the logic is written
|
||||
// below, since MIDI buttons are event-based (on event, off event, etc)
|
||||
// and cannot be correctly handled by a simple early return
|
||||
// there is no explicit check for MIDI here, but the UI should have
|
||||
// prevented it
|
||||
if (check_modifiers && !modifiers_pressed(manager, *current_button)) {
|
||||
button_count++;
|
||||
if (!alternatives || alternatives->empty() ||
|
||||
button_count - 1 >= alternatives->size()) {
|
||||
return BUTTON_NOT_PRESSED;
|
||||
}
|
||||
current_button = &alternatives->at(button_count - 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// naive behavior
|
||||
if (current_button->isNaive()) {
|
||||
GameAPI::Buttons::State state;
|
||||
@@ -469,6 +524,10 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
}
|
||||
}
|
||||
|
||||
Buttons::State Buttons::getState(rawinput::RawInputManager *manager, Button &button, bool check_alts) {
|
||||
return get_button_state(manager, button, check_alts, true);
|
||||
}
|
||||
|
||||
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
|
||||
if (manager) {
|
||||
return getState(manager.get(), button, check_alts);
|
||||
@@ -484,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
|
||||
return button.override_velocity;
|
||||
}
|
||||
|
||||
// naive behavior
|
||||
if (button.isNaive()) {
|
||||
if (button.getInvert()) {
|
||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
|
||||
} else {
|
||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
// get button state
|
||||
Buttons::State button_state = Buttons::getState(manager, button, false);
|
||||
const auto button_state = Buttons::getState(manager, button, false);
|
||||
|
||||
// naive bindings report their digital state as full or zero velocity
|
||||
if (button.isNaive()) {
|
||||
return button_state == Buttons::BUTTON_PRESSED ? 1.f : 0.f;
|
||||
}
|
||||
|
||||
// check if button isn't being pressed
|
||||
if (button_state != Buttons::BUTTON_PRESSED) {
|
||||
|
||||
@@ -299,7 +299,27 @@ std::string Button::getMidiNoteString() {
|
||||
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
|
||||
}
|
||||
|
||||
std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
|
||||
std::string Button::getDisplayString(rawinput::RawInputManager *manager) {
|
||||
auto binding_display = this->getBindingDisplayString(manager);
|
||||
auto modifiers = this->getModifierMask();
|
||||
if (binding_display.empty() || modifiers == 0) {
|
||||
return binding_display;
|
||||
}
|
||||
|
||||
std::string modifier_display;
|
||||
for (unsigned int index = 1; modifiers != 0; index++, modifiers >>= 1) {
|
||||
if ((modifiers & UINT8_C(1)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!modifier_display.empty()) {
|
||||
modifier_display += "+";
|
||||
}
|
||||
modifier_display += fmt::format("Mod{}", index);
|
||||
}
|
||||
return modifier_display + "+" + binding_display;
|
||||
}
|
||||
|
||||
std::string Button::getBindingDisplayString(rawinput::RawInputManager *manager) {
|
||||
|
||||
// get VKey string
|
||||
auto vKey = (uint16_t) this->getVKey();
|
||||
|
||||
@@ -49,6 +49,7 @@ private:
|
||||
std::string name;
|
||||
std::string device_identifier = "";
|
||||
unsigned short vKey = INVALID_VKEY;
|
||||
uint8_t modifier_mask = 0;
|
||||
|
||||
// default bindings are always naive
|
||||
unsigned short vKey_default = INVALID_VKEY;
|
||||
@@ -65,6 +66,7 @@ private:
|
||||
unsigned short velocity_threshold = 0;
|
||||
|
||||
std::string getMidiNoteString();
|
||||
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
|
||||
|
||||
public:
|
||||
std::string getVKeyString();
|
||||
@@ -111,6 +113,7 @@ public:
|
||||
device_identifier = "";
|
||||
analog_type = BAT_NONE;
|
||||
bat_threshold = 0;
|
||||
modifier_mask = 0;
|
||||
}
|
||||
|
||||
std::string getDisplayString(rawinput::RawInputManager* manager);
|
||||
@@ -123,6 +126,14 @@ public:
|
||||
return this->name;
|
||||
}
|
||||
|
||||
inline uint8_t getModifierMask() const {
|
||||
return this->modifier_mask;
|
||||
}
|
||||
|
||||
inline void setModifierMask(uint8_t new_modifier_mask) {
|
||||
this->modifier_mask = new_modifier_mask & UINT8_C(0x0F);
|
||||
}
|
||||
|
||||
inline const std::string &getDeviceIdentifier() const {
|
||||
return this->device_identifier;
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
|
||||
gameButtonsNode->DeleteChild(gameButtonNode);
|
||||
gameButtonNode = this->configFile.NewElement("button");
|
||||
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
|
||||
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
|
||||
gameButtonNode->SetAttribute("invert", invert);
|
||||
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
} else {
|
||||
button->setVKey(static_cast<unsigned short int>(vKey));
|
||||
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
button->setBatThreshold(bat_threshold);
|
||||
button->setVelocityThreshold(velocity_threshold);
|
||||
button->setInvert(invert);
|
||||
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button->setDeviceIdentifier(devid);
|
||||
}
|
||||
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
@@ -552,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", button.getInvert());
|
||||
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
|
||||
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("velocity_threshold", 0);
|
||||
gameButtonNode->SetAttribute("invert", false);
|
||||
gameButtonNode->SetAttribute("devid", "");
|
||||
gameButtonNode->SetAttribute("modifiers", 0);
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
}
|
||||
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
|
||||
@@ -971,6 +980,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
|
||||
// find alternative
|
||||
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
alt.setBatThreshold(bat_threshold);
|
||||
alt.setVelocityThreshold(velocity_threshold);
|
||||
alt.setInvert(invert);
|
||||
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
alt.setDeviceIdentifier(std::string(devid));
|
||||
}
|
||||
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
button.setBatThreshold(bat_threshold);
|
||||
button.setVelocityThreshold(velocity_threshold);
|
||||
button.setInvert(invert);
|
||||
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button.setDeviceIdentifier(devid);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace overlay::windows {
|
||||
el->SetAttribute("debounce_down", entry.debounce_down);
|
||||
el->SetAttribute("bat_threshold", entry.bat_threshold);
|
||||
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
|
||||
el->SetAttribute("modifiers", entry.modifier_mask);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
|
||||
@@ -54,6 +55,10 @@ namespace overlay::windows {
|
||||
el->QueryIntAttribute("bat_threshold", &bat);
|
||||
entry.bat_threshold = bat;
|
||||
|
||||
unsigned int modifier_mask = 0;
|
||||
el->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
entry.modifier_mask = static_cast<uint8_t>(modifier_mask & UINT8_C(0x0F));
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -160,6 +165,28 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// modifier buttons
|
||||
auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons");
|
||||
if (mod_el) {
|
||||
auto *btn_el = mod_el->FirstChildElement("button");
|
||||
while (btn_el) {
|
||||
const char *btn_name = btn_el->Attribute("name");
|
||||
std::string btn_name_str = btn_name ? btn_name : "";
|
||||
|
||||
TemplateButtonBinding *binding = nullptr;
|
||||
for (auto &b : tmpl.modifier_buttons) {
|
||||
if (b.name == btn_name_str) { binding = &b; break; }
|
||||
}
|
||||
if (!binding) {
|
||||
tmpl.modifier_buttons.push_back(
|
||||
{btn_name_str, read_button_entry(btn_el), {}});
|
||||
} else {
|
||||
binding->alternatives.push_back(read_button_entry(btn_el));
|
||||
}
|
||||
btn_el = btn_el->NextSiblingElement("button");
|
||||
}
|
||||
}
|
||||
|
||||
// keypad buttons
|
||||
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
|
||||
if (kp_el) {
|
||||
@@ -244,6 +271,7 @@ namespace overlay::windows {
|
||||
bool save_user_template(
|
||||
const ControllerTemplate &tmpl,
|
||||
const bool save_buttons,
|
||||
const bool save_modifiers,
|
||||
const bool save_keypads,
|
||||
const bool save_analogs,
|
||||
const bool save_lights) {
|
||||
@@ -300,6 +328,24 @@ namespace overlay::windows {
|
||||
}
|
||||
tmpl_el->InsertEndChild(buttons_el);
|
||||
|
||||
// modifier buttons - skip unbound entries
|
||||
if (!tmpl.modifier_buttons.empty()) {
|
||||
auto *mod_el = doc.NewElement("modifier_buttons");
|
||||
if (save_modifiers) {
|
||||
for (auto &btn : tmpl.modifier_buttons) {
|
||||
if (!btn.primary.is_unbound()) {
|
||||
write_button_entry(doc, mod_el, btn.name, btn.primary);
|
||||
}
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (!alt.is_unbound()) {
|
||||
write_button_entry(doc, mod_el, btn.name, alt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpl_el->InsertEndChild(mod_el);
|
||||
}
|
||||
|
||||
// keypad buttons — skip unbound entries
|
||||
if (!tmpl.keypad_buttons.empty()) {
|
||||
auto *kp_el = doc.NewElement("keypad_buttons");
|
||||
|
||||
+9
-1
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
|
||||
|
||||
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
|
||||
{
|
||||
pwd_ = absolute(pwd);
|
||||
// spice2x
|
||||
// avoid MinGW std::filesystem::absolute() duplicating UNC server/share
|
||||
// prefixes, which makes directory_iterator() throw during startup
|
||||
// spice2x
|
||||
const auto &nativePwd = pwd.native();
|
||||
const bool hasUncPrefix = nativePwd.size() >= 2 &&
|
||||
(nativePwd[0] == '\\' || nativePwd[0] == '/') &&
|
||||
(nativePwd[1] == '\\' || nativePwd[1] == '/');
|
||||
pwd_ = hasUncPrefix ? pwd : absolute(pwd);
|
||||
UpdateFileRecords();
|
||||
selectedFilenames_.clear();
|
||||
(*inputNameBuf_)[0] = '\0';
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/detour.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "trackball.h"
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
#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"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/cpuutils.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/detour.h"
|
||||
@@ -28,14 +31,17 @@ 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;
|
||||
bool NATIVE_TOUCH = false;
|
||||
|
||||
/*
|
||||
* Prevent GitaDora from creating folders on F drive
|
||||
@@ -61,6 +67,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
|
||||
|
||||
/*
|
||||
@@ -270,9 +316,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;
|
||||
@@ -298,10 +356,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:
|
||||
@@ -325,12 +391,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
|
||||
@@ -368,13 +453,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
|
||||
@@ -389,33 +516,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,
|
||||
@@ -473,21 +603,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;
|
||||
@@ -518,7 +662,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;
|
||||
@@ -541,18 +686,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;
|
||||
}
|
||||
@@ -575,6 +729,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(
|
||||
@@ -650,31 +810,38 @@ namespace games::gitadora {
|
||||
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
||||
}
|
||||
|
||||
// monitor/touch hooks (windowed or full screen)
|
||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
// enable touch hook for subscreen overlay
|
||||
// touch injection drives mouse-as-touch and API touch for the subscreen,
|
||||
// no matter whether it is drawn by the overlay (single-window mode) or by
|
||||
// the dedicated SMALL window
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
// the legacy fallback can only feed the subscreen overlay
|
||||
wintouchemu::FORCE = true;
|
||||
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
|
||||
|
||||
@@ -13,14 +13,21 @@ 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;
|
||||
extern bool NATIVE_TOUCH;
|
||||
|
||||
// arena SMALL subscreen (touch panel) resolution
|
||||
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
|
||||
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
|
||||
|
||||
class GitaDoraGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@ namespace games::iidx {
|
||||
if (mediaTypePointer && mediaTypePointer->IsString()) {
|
||||
std::string mediaType = mediaTypePointer->GetString();
|
||||
if (mediaType.length() > 0) {
|
||||
camera->m_selectedMediaTypeDescription = mediaType;
|
||||
camera->SetSelectedMediaTypeDescription(mediaType);
|
||||
camera->m_useAutoMediaType = false;
|
||||
} else {
|
||||
camera->m_useAutoMediaType = true;
|
||||
@@ -547,7 +547,7 @@ namespace games::iidx {
|
||||
std::string drawModeString = drawModePointer->GetString();
|
||||
for (int j = 0; j < DRAW_MODE_SIZE; j++) {
|
||||
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
|
||||
camera->m_drawMode = (LocalCameraDrawMode) j;
|
||||
camera->m_drawMode.store((LocalCameraDrawMode) j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -556,12 +556,12 @@ namespace games::iidx {
|
||||
// Flip
|
||||
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
|
||||
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
|
||||
camera->m_flipHorizontal = flipHorizontalPointer->GetBool();
|
||||
camera->m_flipHorizontal.store(flipHorizontalPointer->GetBool());
|
||||
}
|
||||
|
||||
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
|
||||
if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
|
||||
camera->m_flipVertical = flipVerticalPointer->GetBool();
|
||||
camera->m_flipVertical.store(flipVerticalPointer->GetBool());
|
||||
}
|
||||
|
||||
// Allow manual control
|
||||
@@ -625,15 +625,17 @@ namespace games::iidx {
|
||||
if (camera->m_useAutoMediaType) {
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, "");
|
||||
} else {
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription);
|
||||
const auto mediaTypeDescription = camera->GetSelectedMediaTypeDescription();
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, mediaTypeDescription);
|
||||
}
|
||||
|
||||
// Draw Mode
|
||||
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[camera->m_drawMode]);
|
||||
const auto drawMode = camera->m_drawMode.load();
|
||||
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[drawMode]);
|
||||
|
||||
// Flip
|
||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal);
|
||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical);
|
||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal.load());
|
||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical.load());
|
||||
|
||||
// Manual control
|
||||
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
#include "hooks/sleephook.h"
|
||||
#include "launcher/options.h"
|
||||
#include "touch/touch.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/fileutils.h"
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "bi2x_hook.h"
|
||||
#include "ezusb.h"
|
||||
#include "io.h"
|
||||
#include "poke.h"
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
@@ -57,13 +58,14 @@ 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;
|
||||
bool FORCE_720P = false;
|
||||
bool DISABLE_ESPEC_IO = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
bool NATIVE_TOUCH = true;
|
||||
bool ENABLE_POKE = false;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||
@@ -350,12 +352,15 @@ namespace games::iidx {
|
||||
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
|
||||
devicehook_init(avs::core::DLL_INSTANCE);
|
||||
|
||||
if (NATIVE_TOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
|
||||
wintouchemu::hook_title_ends(
|
||||
"beatmania IIDX",
|
||||
"main",
|
||||
avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,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);
|
||||
}
|
||||
|
||||
@@ -449,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
|
||||
@@ -533,6 +526,12 @@ namespace games::iidx {
|
||||
}
|
||||
}
|
||||
|
||||
void IIDXGame::post_attach() {
|
||||
if (ENABLE_POKE) {
|
||||
poke::enable();
|
||||
}
|
||||
}
|
||||
|
||||
void IIDXGame::detach() {
|
||||
Game::detach();
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -29,6 +36,7 @@ namespace games::iidx {
|
||||
extern bool FORCE_720P;
|
||||
extern bool DISABLE_ESPEC_IO;
|
||||
extern bool NATIVE_TOUCH;
|
||||
extern bool ENABLE_POKE;
|
||||
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern uint8_t DIGITAL_TT_SENS;
|
||||
@@ -53,6 +61,7 @@ namespace games::iidx {
|
||||
|
||||
virtual void attach() override;
|
||||
virtual void pre_attach() override;
|
||||
virtual void post_attach() override;
|
||||
virtual void detach() override;
|
||||
|
||||
private:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
#include <atomic>
|
||||
#include <d3d9.h>
|
||||
#include <dxva2api.h>
|
||||
#include <mutex>
|
||||
@@ -66,6 +67,8 @@ extern std::string CAMERA_CONTROL_LABELS[];
|
||||
extern std::string DRAW_MODE_LABELS[];
|
||||
|
||||
namespace games::iidx {
|
||||
class IIDXCameraSourceReaderCallback;
|
||||
|
||||
namespace Camera {
|
||||
struct PlayVideoCamera {
|
||||
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
|
||||
@@ -87,7 +90,7 @@ namespace games::iidx {
|
||||
|
||||
class IIDXLocalCamera {
|
||||
protected:
|
||||
virtual ~IIDXLocalCamera() {};
|
||||
virtual ~IIDXLocalCamera();
|
||||
|
||||
LONG m_nRefCount;
|
||||
CRITICAL_SECTION m_critsec;
|
||||
@@ -101,9 +104,14 @@ namespace games::iidx {
|
||||
// For reading frames from Camera
|
||||
IMFMediaSource *m_pSource = nullptr;
|
||||
IMFSourceReader *m_pSourceReader = nullptr;
|
||||
IMFSourceReaderEx *m_pSourceReaderEx = nullptr;
|
||||
IIDXCameraSourceReaderCallback *m_pSourceReaderCallback = nullptr;
|
||||
std::mutex m_mediaTypeMutex;
|
||||
IMFMediaType *m_pendingMediaType = nullptr;
|
||||
int m_selectedMediaTypeIndex = 0;
|
||||
std::string m_selectedMediaTypeDescription = "";
|
||||
|
||||
// Camera Format information
|
||||
double m_frameRate = 0;
|
||||
LONG m_cameraWidth;
|
||||
LONG m_cameraHeight;
|
||||
|
||||
@@ -129,6 +137,12 @@ namespace games::iidx {
|
||||
LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr;
|
||||
IDirect3DSurface9 *m_pConversionSurf = nullptr;
|
||||
|
||||
// upload surface for decoded camera frames returned in system memory
|
||||
IDirect3DSurface9 *m_pDecodedSurf = nullptr;
|
||||
GUID m_decodedSubtype = GUID_NULL;
|
||||
GUID m_outputSubtype = GUID_NULL;
|
||||
bool m_drawErrorLogged = false;
|
||||
|
||||
// Texture for custom transform (e.g. horizontal flip)
|
||||
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
|
||||
IDirect3DSurface9 *m_pTransformSurf = nullptr;
|
||||
@@ -153,21 +167,19 @@ namespace games::iidx {
|
||||
BOOL m_initialized = false;
|
||||
|
||||
// True if all the setup steps succeeded
|
||||
BOOL m_active = false;
|
||||
std::atomic_bool m_active = false;
|
||||
|
||||
// Media type select
|
||||
std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
|
||||
int m_selectedMediaTypeIndex = 0;
|
||||
bool m_useAutoMediaType = true;
|
||||
IMFMediaType *m_pAutoMediaType = nullptr;
|
||||
std::string m_selectedMediaTypeDescription = "";
|
||||
bool m_allowManualControl = false;
|
||||
|
||||
LocalCameraDrawMode m_drawMode = DrawModeCrop4_3;
|
||||
std::atomic<LocalCameraDrawMode> m_drawMode = DrawModeCrop4_3;
|
||||
|
||||
// Render processing
|
||||
bool m_flipHorizontal = false;
|
||||
bool m_flipVertical = false;
|
||||
std::atomic_bool m_flipHorizontal = false;
|
||||
std::atomic_bool m_flipVertical = false;
|
||||
|
||||
IIDXLocalCamera(
|
||||
std::string name,
|
||||
@@ -184,11 +196,14 @@ namespace games::iidx {
|
||||
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
|
||||
HRESULT SetCameraControlProp(int index, long value, long flags);
|
||||
HRESULT ResetCameraControlProps();
|
||||
HRESULT FlushDrawCommands();
|
||||
std::string GetName();
|
||||
std::string GetFriendlyName();
|
||||
std::string GetSymLink();
|
||||
int GetSelectedMediaTypeIndex();
|
||||
std::string GetSelectedMediaTypeDescription();
|
||||
void SetSelectedMediaTypeDescription(const std::string &description);
|
||||
HRESULT ChangeMediaType(IMFMediaType *pType);
|
||||
void RequestMediaType(IMFMediaType *pType);
|
||||
HRESULT StartCapture();
|
||||
void UpdateDrawRect();
|
||||
|
||||
@@ -197,12 +212,22 @@ namespace games::iidx {
|
||||
void CreateThread();
|
||||
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
|
||||
std::string GetVideoFormatName(GUID subtype);
|
||||
HRESULT TryMediaType(IMFMediaType *pType, UINT32 *pBestWidth, double *pBestFrameRate);
|
||||
static bool CompareMediaTypes(const MediaTypeInfo &a, const MediaTypeInfo &b);
|
||||
bool MatchesPreferredAspect(const MediaTypeInfo &info) const;
|
||||
static bool IsBetterAutoType(const MediaTypeInfo &candidate, const MediaTypeInfo ¤t);
|
||||
IMFMediaType *FindBestNativeAutoType(bool requirePreferredAspect) const;
|
||||
IMFMediaType *FindBestAutoType(const GUID &subtype, bool requirePreferredAspect) const;
|
||||
HRESULT ValidateMediaType(IMFMediaType *pType);
|
||||
HRESULT InitTargetTexture();
|
||||
HRESULT EnsureTransformTextures();
|
||||
HRESULT InitCameraControl();
|
||||
void SetSelectedMediaType(int index, const std::string &description);
|
||||
bool HasPendingMediaType();
|
||||
HRESULT ApplyPendingMediaType();
|
||||
HRESULT UploadDecodedSample(IMFMediaBuffer *pSrcBuffer);
|
||||
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
|
||||
HRESULT ReadSample();
|
||||
LPDIRECT3DTEXTURE9 Render();
|
||||
HRESULT Render();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ namespace games::iidx {
|
||||
_In_ UINT32 cInitialSize
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFCreateMediaType_t)(
|
||||
_Out_ IMFMediaType** ppMFType
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
@@ -35,6 +39,7 @@ namespace games::iidx {
|
||||
);
|
||||
|
||||
static MFCreateAttributes_t MFCreateAttributes = nullptr;
|
||||
static MFCreateMediaType_t MFCreateMediaType = nullptr;
|
||||
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
|
||||
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
|
||||
static MFGetService_t MFGetService = nullptr;
|
||||
@@ -66,6 +71,12 @@ namespace games::iidx {
|
||||
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
|
||||
}
|
||||
|
||||
MFCreateMediaType = (MFCreateMediaType_t)
|
||||
libutils::get_proc(mfplat_dll, "MFCreateMediaType");
|
||||
if (!MFCreateMediaType) {
|
||||
log_fatal("mf_wrappers", "MFCreateMediaType failed to hook");
|
||||
}
|
||||
|
||||
MFEnumDeviceSources = (MFEnumDeviceSources_t)
|
||||
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
|
||||
if (!MFEnumDeviceSources) {
|
||||
@@ -92,6 +103,11 @@ namespace games::iidx {
|
||||
return MFCreateAttributes(ppMFAttributes, cInitialSize);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFCreateMediaType (
|
||||
_Out_ IMFMediaType** ppMFType) {
|
||||
return MFCreateMediaType(ppMFType);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFEnumDeviceSources (
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace games::iidx {
|
||||
_Out_ IMFAttributes** ppMFAttributes,
|
||||
_In_ UINT32 cInitialSize);
|
||||
|
||||
HRESULT WrappedMFCreateMediaType (
|
||||
_Out_ IMFMediaType** ppMFType);
|
||||
|
||||
HRESULT WrappedMFEnumDeviceSources (
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
|
||||
+26
-147
@@ -1,31 +1,17 @@
|
||||
#include "poke.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "windows.h"
|
||||
#include "cfg/screen_resize.h"
|
||||
#include "games/io.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/shutdown.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/windows/generic_sub.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/touch.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
#define POKE_NATIVE_TOUCH 0
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
static HINSTANCE USER32_INSTANCE = nullptr;
|
||||
typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
|
||||
typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
|
||||
static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
|
||||
static InjectTouchInput_t pInjectTouchInput = nullptr;
|
||||
#endif
|
||||
|
||||
namespace games::iidx::poke {
|
||||
|
||||
@@ -116,93 +102,29 @@ namespace games::iidx::poke {
|
||||
{"1/D", 50},
|
||||
};
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
void initialize_native_touch_library() {
|
||||
if (USER32_INSTANCE == nullptr) {
|
||||
USER32_INSTANCE = libutils::load_library("user32.dll");
|
||||
}
|
||||
|
||||
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
|
||||
USER32_INSTANCE, "InitializeTouchInjection");
|
||||
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
|
||||
USER32_INSTANCE, "InjectTouchInput");
|
||||
}
|
||||
|
||||
void emulate_native_touch(TouchPoint tp, bool is_down) {
|
||||
if (pInjectTouchInput == nullptr) {
|
||||
static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
|
||||
if (touch_points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINTER_TOUCH_INFO contact;
|
||||
BOOL bRet = TRUE;
|
||||
const int contact_offset = 2;
|
||||
|
||||
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
|
||||
|
||||
contact.pointerInfo.pointerType = PT_TOUCH;
|
||||
contact.pointerInfo.pointerId = 0;
|
||||
contact.pointerInfo.ptPixelLocation.x = tp.x;
|
||||
contact.pointerInfo.ptPixelLocation.y = tp.y;
|
||||
if (is_down) {
|
||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
|
||||
} else {
|
||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
|
||||
}
|
||||
|
||||
contact.pointerInfo.dwTime = 0;
|
||||
contact.pointerInfo.PerformanceCount = 0;
|
||||
|
||||
contact.touchFlags = TOUCH_FLAG_NONE;
|
||||
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
|
||||
contact.orientation = 0;
|
||||
contact.pressure = 1024;
|
||||
|
||||
contact.rcContact.top = tp.y - contact_offset;
|
||||
contact.rcContact.bottom = tp.y + contact_offset;
|
||||
contact.rcContact.left = tp.x - contact_offset;
|
||||
contact.rcContact.right = tp.x + contact_offset;
|
||||
|
||||
bRet = InjectTouchInput(1, &contact);
|
||||
if (!bRet) {
|
||||
log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
|
||||
}
|
||||
const auto &touch = touch_points.front();
|
||||
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
|
||||
}
|
||||
|
||||
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
int i = 0;
|
||||
for (auto &touch : *touch_points) {
|
||||
emulate_native_touch(touch, true);
|
||||
}
|
||||
}
|
||||
|
||||
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
for (auto &touch : *touch_points) {
|
||||
emulate_native_touch(touch, false);
|
||||
}
|
||||
touch_points->clear();
|
||||
}
|
||||
#endif
|
||||
|
||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
std::vector<DWORD> touch_ids;
|
||||
for (auto &touch : *touch_points) {
|
||||
touch_ids.emplace_back(touch.id);
|
||||
}
|
||||
touch_remove_points(&touch_ids);
|
||||
touch_points->clear();
|
||||
}
|
||||
|
||||
void enable() {
|
||||
|
||||
// check if already running
|
||||
if (THREAD)
|
||||
return;
|
||||
|
||||
if (!games::iidx::NATIVE_TOUCH) {
|
||||
log_warning("poke", "keypad touch emulation requires native touch; disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// create new thread
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// log
|
||||
log_info("poke", "enabled");
|
||||
|
||||
@@ -210,35 +132,13 @@ namespace games::iidx::poke {
|
||||
std::vector<TouchPoint> touch_points;
|
||||
std::vector<uint16_t> last_keypad_state = {0, 0};
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
bool use_native = games::iidx::NATIVE_TOUCH;
|
||||
|
||||
if (use_native) {
|
||||
initialize_native_touch_library();
|
||||
|
||||
if (pInitializeTouchInjection != nullptr) {
|
||||
pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
bool use_native = false;
|
||||
#endif
|
||||
|
||||
// set variable to false to stop
|
||||
while (THREAD_RUNNING) {
|
||||
|
||||
// clean up touch from last frame
|
||||
if (touch_points.size() > 0) {
|
||||
#if POKE_NATIVE_TOUCH
|
||||
if (use_native) {
|
||||
clear_native_touch_points(&touch_points);
|
||||
} else {
|
||||
clear_touch_points(&touch_points);
|
||||
}
|
||||
#else
|
||||
clear_touch_points(&touch_points);
|
||||
#endif
|
||||
inject_native_touch_points(touch_points, false);
|
||||
touch_points.clear();
|
||||
}
|
||||
|
||||
for (int unit = 0; unit < 2; unit++) {
|
||||
@@ -263,32 +163,15 @@ namespace games::iidx::poke {
|
||||
|
||||
float x = x_iter->second / 1920.0;
|
||||
float y = y_iter->second / 1080.0;
|
||||
if (use_native) {
|
||||
x *= rawinput::TOUCHSCREEN_RANGE_X;
|
||||
y *= rawinput::TOUCHSCREEN_RANGE_Y;
|
||||
} else if (GRAPHICS_IIDX_WSUB) {
|
||||
// Scale to windowed subscreen
|
||||
x *= GRAPHICS_WSUB_WIDTH;
|
||||
y *= GRAPHICS_WSUB_HEIGHT;
|
||||
} else if (GENERIC_SUB_WINDOW_FULLSIZE || !overlay::OVERLAY->get_active()) {
|
||||
// Overlay is not present, scale to main screen
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
x *= SPICETOUCH_TOUCH_WIDTH;
|
||||
y *= SPICETOUCH_TOUCH_HEIGHT;
|
||||
} else {
|
||||
x *= ImGui::GetIO().DisplaySize.x;
|
||||
y *= ImGui::GetIO().DisplaySize.y;
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||
continue;
|
||||
}
|
||||
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
|
||||
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
|
||||
} else {
|
||||
// Overlay subscreen
|
||||
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
|
||||
y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
|
||||
|
||||
// Scale to window size ratio
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
|
||||
y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
|
||||
}
|
||||
x = x_iter->second;
|
||||
y = y_iter->second;
|
||||
}
|
||||
|
||||
TouchPoint tp {
|
||||
@@ -309,19 +192,15 @@ namespace games::iidx::poke {
|
||||
} // for all units
|
||||
|
||||
if (touch_points.size() > 0) {
|
||||
#if POKE_NATIVE_TOUCH
|
||||
if (use_native) {
|
||||
emulate_native_touch_points(&touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
#else
|
||||
touch_write_points(&touch_points);
|
||||
#endif
|
||||
inject_native_touch_points(touch_points, true);
|
||||
}
|
||||
|
||||
// slow down
|
||||
timer.sleep(50);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
|
||||
if (!touch_points.empty()) {
|
||||
inject_native_touch_points(touch_points, false);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace games {
|
||||
static bool IO_INITIALIZED = false;
|
||||
static std::vector<std::string> games;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button> &> buttons;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_modifiers;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_keypads;
|
||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
|
||||
static robin_hood::unordered_map<std::string, std::string> buttons_help;
|
||||
@@ -132,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");
|
||||
@@ -365,6 +367,37 @@ namespace games {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static std::vector<Button> gen_buttons_modifiers(const std::string &game) {
|
||||
auto modifier_buttons = GameAPI::Buttons::getButtons(game);
|
||||
const std::vector<std::string> names {
|
||||
"Modifier 1",
|
||||
"Modifier 2",
|
||||
"Modifier 3",
|
||||
"Modifier 4",
|
||||
};
|
||||
modifier_buttons = GameAPI::Buttons::sortButtons(modifier_buttons, names);
|
||||
for (auto &modifier : modifier_buttons) {
|
||||
modifier.setModifierMask(0);
|
||||
for (auto &alternative : modifier.getAlternatives()) {
|
||||
alternative.setModifierMask(0);
|
||||
}
|
||||
}
|
||||
return modifier_buttons;
|
||||
}
|
||||
|
||||
std::vector<Button> *get_buttons_modifiers(const std::string &game) {
|
||||
initialize();
|
||||
auto it = buttons_modifiers.find(game);
|
||||
if (it == buttons_modifiers.end()) {
|
||||
if (game.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
buttons_modifiers[game] = gen_buttons_modifiers(game);
|
||||
return &buttons_modifiers[game];
|
||||
}
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
static std::vector<Button> gen_buttons_keypads(const std::string &game) {
|
||||
auto buttons = GameAPI::Buttons::getButtons(game);
|
||||
std::vector<std::string> names;
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
|
||||
namespace games {
|
||||
|
||||
namespace ModifierButtons {
|
||||
enum {
|
||||
Modifier1,
|
||||
Modifier2,
|
||||
Modifier3,
|
||||
Modifier4,
|
||||
Size,
|
||||
};
|
||||
}
|
||||
|
||||
namespace OverlayButtons {
|
||||
enum {
|
||||
Screenshot,
|
||||
@@ -59,6 +69,7 @@ namespace games {
|
||||
|
||||
const std::vector<std::string> &get_games();
|
||||
std::vector<Button> *get_buttons(const std::string &game);
|
||||
std::vector<Button> *get_buttons_modifiers(const std::string &game);
|
||||
std::string get_buttons_help(const std::string &game);
|
||||
std::string get_analogs_help(const std::string &game);
|
||||
std::vector<Button> *get_buttons_keypads(const std::string &game);
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#if SPICE64
|
||||
|
||||
namespace games::jb {
|
||||
void bi2x_hook_init();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,16 +215,13 @@ 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));
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
// let the rawinput stack correct the aspect ratio
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
if (GRAPHICS_SHOW_CURSOR) {
|
||||
ShowCursor(TRUE);
|
||||
}
|
||||
@@ -231,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;
|
||||
@@ -310,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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -351,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];
|
||||
@@ -424,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);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/execexe.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "bi2a_hook.h"
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
// enable touch functions - set version to windows 7
|
||||
// mingw otherwise doesn't load touch stuff
|
||||
#define _WIN32_WINNT 0x0601
|
||||
|
||||
#include "nost.h"
|
||||
#include "poke.h"
|
||||
#include "touch_mode.h"
|
||||
#include "hooks/setupapihook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "avs/game.h"
|
||||
|
||||
namespace games::nost {
|
||||
|
||||
bool ENABLE_POKE = false;
|
||||
bool ENABLE_TOUCH_MODE = false;
|
||||
|
||||
NostGame::NostGame() : Game("Nostalgia") {
|
||||
}
|
||||
@@ -43,9 +42,16 @@ namespace games::nost {
|
||||
setupapihook_init(avs::game::DLL_INSTANCE);
|
||||
setupapihook_add(touch_settings);
|
||||
|
||||
// custom touch
|
||||
// nostalgia crashes if you inject touch events too early
|
||||
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
||||
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!native_touch_ready) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
||||
} else {
|
||||
if (ENABLE_TOUCH_MODE) {
|
||||
touch_mode::enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NostGame::post_attach() {
|
||||
@@ -55,6 +61,9 @@ namespace games::nost {
|
||||
}
|
||||
|
||||
void NostGame::detach() {
|
||||
if (ENABLE_TOUCH_MODE) {
|
||||
touch_mode::disable();
|
||||
}
|
||||
if (ENABLE_POKE) {
|
||||
poke::disable();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
namespace games::nost {
|
||||
|
||||
extern bool ENABLE_POKE;
|
||||
extern bool ENABLE_TOUCH_MODE;
|
||||
|
||||
class NostGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
#include "poke.h"
|
||||
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "games/nost/io.h"
|
||||
#include "cfg/api.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
namespace games::nost::poke {
|
||||
|
||||
static std::thread *THREAD = nullptr;
|
||||
static volatile bool THREAD_RUNNING = false;
|
||||
|
||||
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
|
||||
static std::atomic<bool> THREAD_RUNNING { false };
|
||||
|
||||
// positions use game-client pixels, matching the legacy client-sized touch window
|
||||
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_KEYPAD {
|
||||
{EAM_IO_KEYPAD_0, {760, 440}},
|
||||
{EAM_IO_KEYPAD_1, {760, 385}},
|
||||
{EAM_IO_KEYPAD_2, {820, 385}},
|
||||
@@ -31,7 +34,8 @@ namespace games::nost::poke {
|
||||
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
|
||||
};
|
||||
|
||||
static const std::unordered_map<games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE {
|
||||
static const std::unordered_map<
|
||||
games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE_BUTTONS {
|
||||
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
|
||||
{games::nost::Buttons::Button::PokeBack, {340, 100}},
|
||||
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
|
||||
@@ -46,13 +50,8 @@ namespace games::nost::poke {
|
||||
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
|
||||
};
|
||||
|
||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
||||
std::vector<DWORD> touch_ids;
|
||||
for (auto &touch : *touch_points) {
|
||||
touch_ids.emplace_back(touch.id);
|
||||
}
|
||||
touch_remove_points(&touch_ids);
|
||||
touch_points->clear();
|
||||
static bool inject_poke_position(POINT position, bool down) {
|
||||
return nativetouch::inject::inject_synthetic_touch(position, down);
|
||||
}
|
||||
|
||||
void enable() {
|
||||
@@ -61,126 +60,143 @@ namespace games::nost::poke {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nativetouch::is_hooked()) {
|
||||
log_warning("poke", "Nostalgia poke requires native touch; disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// create new thread
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
|
||||
|
||||
const int swipe_anim_total_frames = 6;
|
||||
const int swipe_anim_y = 300;
|
||||
|
||||
const int swipe_next_page_x_begin = 1120;
|
||||
const int swipe_next_page_x_end = 1120-120;
|
||||
const int swipe_next_page_x_end = 1120 - 120;
|
||||
|
||||
const int swipe_prev_page_x_begin = 280-120;
|
||||
const int swipe_prev_page_x_begin = 280 - 120;
|
||||
const int swipe_prev_page_x_end = 280;
|
||||
|
||||
int next_page_anim_index = -1;
|
||||
int prev_page_anim_index = -1;
|
||||
std::vector<TouchPoint> touch_points;
|
||||
|
||||
bool touch_release = false;
|
||||
bool contact_active = false;
|
||||
bool release_pending = false;
|
||||
POINT last_position {};
|
||||
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
|
||||
uint16_t last_keypad_state = 0;
|
||||
|
||||
// log
|
||||
log_info("poke", "enabled");
|
||||
|
||||
// set variable to false to stop
|
||||
while (THREAD_RUNNING) {
|
||||
// clean up touch from last frame
|
||||
if (!touch_points.empty()) {
|
||||
if (touch_release) {
|
||||
clear_touch_points(&touch_points);
|
||||
touch_release = false;
|
||||
} else {
|
||||
touch_points.clear();
|
||||
if (release_pending) {
|
||||
if (contact_active) {
|
||||
inject_poke_position(last_position, false);
|
||||
}
|
||||
contact_active = false;
|
||||
release_pending = false;
|
||||
}
|
||||
|
||||
auto &buttons = games::nost::get_buttons();
|
||||
std::optional<POINT> touch_position;
|
||||
bool release_after_touch = false;
|
||||
|
||||
const auto button_triggered = [&](games::nost::Buttons::Button button) {
|
||||
const auto pressed =
|
||||
GameAPI::Buttons::getState(RI_MGR, buttons.at(button));
|
||||
const auto triggered = pressed && !button_states[button];
|
||||
button_states[button] = pressed;
|
||||
return triggered;
|
||||
};
|
||||
|
||||
std::optional<POINT> triggered_touch_position;
|
||||
for (const auto& it : NOST_POKE_BUTTONS) {
|
||||
if (button_triggered(it.first) && !triggered_touch_position.has_value()) {
|
||||
triggered_touch_position = POINT {
|
||||
it.second.first,
|
||||
it.second.second,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const auto next_triggered =
|
||||
button_triggered(games::nost::Buttons::PokeNextPage);
|
||||
const auto prev_triggered =
|
||||
button_triggered(games::nost::Buttons::PokePrevPage);
|
||||
|
||||
const auto keypad_state = eamuse_get_keypad_state(0);
|
||||
if (!triggered_touch_position.has_value()) {
|
||||
for (const auto& it : NOST_POKE_KEYPAD) {
|
||||
const auto mask = static_cast<uint16_t>(1 << it.first);
|
||||
if ((keypad_state & mask) && !(last_keypad_state & mask)) {
|
||||
triggered_touch_position = POINT {
|
||||
it.second.first,
|
||||
it.second.second,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
last_keypad_state = keypad_state;
|
||||
|
||||
if (0 <= next_page_anim_index) {
|
||||
const auto delta =
|
||||
(swipe_next_page_x_end - swipe_next_page_x_begin)
|
||||
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
|
||||
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = (LONG)swipe_next_page_x_begin + delta,
|
||||
.y = (LONG)swipe_anim_y,
|
||||
.mouse = true,
|
||||
touch_position = POINT {
|
||||
swipe_next_page_x_begin + delta,
|
||||
swipe_anim_y,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
next_page_anim_index--;
|
||||
if (next_page_anim_index < 0) {
|
||||
touch_release = true;
|
||||
release_after_touch = true;
|
||||
}
|
||||
} else if (0 <= prev_page_anim_index) {
|
||||
const auto delta =
|
||||
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
|
||||
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
|
||||
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = (LONG)swipe_prev_page_x_begin + delta,
|
||||
.y = (LONG)swipe_anim_y,
|
||||
.mouse = true,
|
||||
touch_position = POINT {
|
||||
swipe_prev_page_x_begin + delta,
|
||||
swipe_anim_y,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
prev_page_anim_index--;
|
||||
if (prev_page_anim_index < 0) {
|
||||
touch_release = true;
|
||||
release_after_touch = true;
|
||||
}
|
||||
} else {
|
||||
for (const auto& it : NOST_POKE) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) {
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = it.second.first,
|
||||
.y = it.second.second,
|
||||
.mouse = true,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
touch_release = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!touch_release) {
|
||||
const auto state = eamuse_get_keypad_state(0);
|
||||
if (state) {
|
||||
for (const auto& it : NOST_POKE_NUM) {
|
||||
if (state & (1 << it.first)) {
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = it.second.first,
|
||||
.y = it.second.second,
|
||||
.mouse = true,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
touch_release = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
touch_position = triggered_touch_position;
|
||||
release_after_touch = touch_position.has_value();
|
||||
|
||||
// start animations for next frame
|
||||
if (!touch_release) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) {
|
||||
if (!touch_position.has_value()) {
|
||||
if (next_triggered) {
|
||||
next_page_anim_index = swipe_anim_total_frames;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokePrevPage))) {
|
||||
|
||||
if (prev_triggered) {
|
||||
prev_page_anim_index = swipe_anim_total_frames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!touch_points.empty()) {
|
||||
touch_write_points(&touch_points);
|
||||
if (touch_position.has_value() &&
|
||||
inject_poke_position(*touch_position, true)) {
|
||||
contact_active = true;
|
||||
last_position = *touch_position;
|
||||
}
|
||||
if (release_after_touch && contact_active) {
|
||||
release_pending = true;
|
||||
}
|
||||
|
||||
// slow down
|
||||
timer.sleep(30);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||
}
|
||||
|
||||
if (contact_active) {
|
||||
inject_poke_position(last_position, false);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
#include "touch_mode.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::nost::touch_mode {
|
||||
|
||||
// native contact positions feed piano input directly. native events reach the game
|
||||
// in nav mode and are suppressed in piano mode. mode-button contacts are always
|
||||
// suppressed and change that routing after release.
|
||||
static constexpr LONG PIANO_LEFT_GAP = 11;
|
||||
static constexpr LONG PIANO_RIGHT_GAP = 10;
|
||||
static constexpr uint32_t PIANO_KEY_COUNT = 28;
|
||||
|
||||
struct TouchGeometry {
|
||||
HWND window = nullptr;
|
||||
RECT mode_button {};
|
||||
LONG client_width = 0;
|
||||
LONG client_height = 0;
|
||||
|
||||
bool valid() const {
|
||||
return window != nullptr && client_width > 0 && client_height > 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct NativeContact {
|
||||
POINT position {};
|
||||
|
||||
// position contains game-client coordinates
|
||||
bool client_position_valid = false;
|
||||
|
||||
// down began on the mode switch button; remains true through up
|
||||
bool mode_button = false;
|
||||
};
|
||||
|
||||
static std::atomic_bool accept_events { false };
|
||||
static std::atomic<Mode> current_mode_state { Mode::Nav };
|
||||
static std::mutex state_mutex;
|
||||
static TouchGeometry touch_geometry;
|
||||
|
||||
// native contacts are kept by ID so each contact contributes exactly one position
|
||||
static std::unordered_map<DWORD, NativeContact> active_contacts;
|
||||
|
||||
// a hardware button release requests one change after all contacts are released
|
||||
static bool mode_change_pending = false;
|
||||
|
||||
static void reset_state_locked() {
|
||||
current_mode_state.store(Mode::Nav, std::memory_order_release);
|
||||
touch_geometry = {};
|
||||
active_contacts.clear();
|
||||
mode_change_pending = false;
|
||||
}
|
||||
|
||||
// hardware contacts arrive in screen coordinates
|
||||
static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) {
|
||||
if (!touch_geometry.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
POINT position { event.x, event.y };
|
||||
if (!ScreenToClient(touch_geometry.window, &position)) {
|
||||
return false;
|
||||
}
|
||||
return PtInRect(&touch_geometry.mode_button, position) != FALSE;
|
||||
}
|
||||
|
||||
static bool update_touch_state(const nativetouch::NativeTouchEvent &event) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
|
||||
// first, process down / move events
|
||||
if (event.down || event.move) {
|
||||
auto contact = active_contacts.try_emplace(event.id).first;
|
||||
|
||||
// keep track of IDs that began as a down on the mode switch button
|
||||
if (event.down) {
|
||||
contact->second.mode_button = native_touch_in_button(event);
|
||||
}
|
||||
|
||||
// check for valid position
|
||||
POINT position { event.x, event.y };
|
||||
if (touch_geometry.window != nullptr &&
|
||||
ScreenToClient(touch_geometry.window, &position)) {
|
||||
contact->second.position = position;
|
||||
contact->second.client_position_valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
const auto contact = active_contacts.find(event.id);
|
||||
const bool mode_button_contact = contact != active_contacts.end() &&
|
||||
contact->second.mode_button;
|
||||
|
||||
// process up events
|
||||
if (event.up) {
|
||||
active_contacts.erase(event.id);
|
||||
|
||||
// if a contact that began down event on the mode switch button has
|
||||
// been released, a mode switch is now pending
|
||||
if (mode_button_contact) {
|
||||
mode_change_pending = true;
|
||||
}
|
||||
|
||||
// apply the change on the final hardware up. switching earlier would
|
||||
// split another contact's down and up events across different modes
|
||||
if (mode_change_pending && active_contacts.empty()) {
|
||||
mode_change_pending = false;
|
||||
const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav;
|
||||
current_mode_state.store(next_mode, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
return mode_button_contact;
|
||||
}
|
||||
|
||||
// install the Nostalgia-specific native touch interception
|
||||
void enable() {
|
||||
if (accept_events.exchange(true, std::memory_order_acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
reset_state_locked();
|
||||
}
|
||||
|
||||
nativetouch::set_input_filter(filter_native_touch);
|
||||
log_info("nost::touch", "enabled");
|
||||
}
|
||||
|
||||
void disable() {
|
||||
if (!accept_events.exchange(false, std::memory_order_acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nativetouch::set_input_filter(nullptr);
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
reset_state_locked();
|
||||
}
|
||||
|
||||
bool enabled() {
|
||||
return accept_events.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
Mode current_mode() {
|
||||
return current_mode_state.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
// publish the rendered overlay button rectangle in game-client pixels
|
||||
void publish_button_bounds(HWND window, const RECT &client_bounds) {
|
||||
TouchGeometry next {};
|
||||
RECT client_rect {};
|
||||
if (window != nullptr && GetClientRect(window, &client_rect) &&
|
||||
client_rect.right > 0 && client_rect.bottom > 0) {
|
||||
next.window = window;
|
||||
next.mode_button = client_bounds;
|
||||
next.client_width = client_rect.right;
|
||||
next.client_height = client_rect.bottom;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
touch_geometry = next;
|
||||
}
|
||||
|
||||
// return the active 28-key piano bitfield for the PANB input update
|
||||
uint32_t piano_key_state() {
|
||||
if (!enabled() || current_mode() != Mode::Piano) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (current_mode() != Mode::Piano || !touch_geometry.valid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t state = 0;
|
||||
for (const auto &contact : active_contacts) {
|
||||
// invalid position or mode-button contact; ignore these contacts
|
||||
if (!contact.second.client_position_valid || contact.second.mode_button) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &position = contact.second.position;
|
||||
|
||||
// outside the client area or on the mode button; ignore these contacts
|
||||
if (position.x < 0 || position.x >= touch_geometry.client_width ||
|
||||
position.y < 0 || position.y >= touch_geometry.client_height ||
|
||||
PtInRect(&touch_geometry.mode_button, position)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// divide the inset width evenly; touches in either side gap clamp to
|
||||
// the nearest outer key so the physical screen edges remain playable
|
||||
const auto piano_width =
|
||||
touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP;
|
||||
uint32_t key = 0;
|
||||
if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) {
|
||||
key = PIANO_KEY_COUNT - 1;
|
||||
} else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) {
|
||||
key = static_cast<uint32_t>(
|
||||
(position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width);
|
||||
}
|
||||
state |= UINT32_C(1) << key;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
// update native contacts and report whether this event should be hidden from the game
|
||||
bool filter_native_touch(const nativetouch::NativeTouchEvent &event) {
|
||||
// synthetic events are outside this hardware-only feature
|
||||
if (!enabled() || event.synthetic) {
|
||||
// false leaves the event visible to the game
|
||||
return false;
|
||||
}
|
||||
|
||||
// snapshot routing before an up event can commit a pending mode switch
|
||||
const bool piano_mode_before_update = current_mode() == Mode::Piano;
|
||||
|
||||
// update the contact lifetime and commit any pending switch when safe
|
||||
const bool mode_button_contact = update_touch_state(event);
|
||||
|
||||
// hide every event in a contact that began on the mode switch button
|
||||
if (mode_button_contact) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// piano mode consumes hardware events; nav mode forwards them to the game
|
||||
return piano_mode_before_update;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
namespace games::nost::touch_mode {
|
||||
|
||||
// nav mode forwards contacts to the game; piano mode converts them into piano keys
|
||||
enum class Mode {
|
||||
Nav,
|
||||
Piano,
|
||||
};
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
bool enabled();
|
||||
|
||||
Mode current_mode();
|
||||
|
||||
void publish_button_bounds(HWND window, const RECT &client_bounds);
|
||||
|
||||
uint32_t piano_key_state();
|
||||
|
||||
bool filter_native_touch(const nativetouch::NativeTouchEvent &event);
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "util/unity_player.h"
|
||||
#include "util/execexe.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/socd_cleaner.h"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "acio/icca/icca.h"
|
||||
#include "io.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/libutils.h"
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "launcher/logger.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/sysutils.h"
|
||||
#include "io.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
namespace games::popn {
|
||||
|
||||
bool SHOW_PIKA_MONITOR_WARNING = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
bool NATIVE_TOUCH = true;
|
||||
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
@@ -715,13 +715,16 @@ namespace games::popn {
|
||||
// 00000100 0000000B 00000001 (button 9)
|
||||
// set third column to 0 and it will work with BIO2
|
||||
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
if (NATIVE_TOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
wintouchemu::FORCE = true;
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH) {
|
||||
wintouchemu::FORCE = true;
|
||||
if (!GRAPHICS_WINDOWED || GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE);
|
||||
wintouchemu::hook_title_ends(
|
||||
"",
|
||||
"Main Screen",
|
||||
avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
||||
}
|
||||
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
// create touch window
|
||||
touch_create_wnd(wnd);
|
||||
|
||||
@@ -94,9 +97,6 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
ShowWindow(wnd, SW_SHOW);
|
||||
}
|
||||
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
} else {
|
||||
|
||||
// fallback to dx hook
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "hooks/devicehook.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#ifdef SPICE64
|
||||
#include "hooks/graphics/nvapi_impl.h"
|
||||
#endif
|
||||
#include "hooks/graphics/nvapi_hook.h"
|
||||
#include "hooks/powrprof.h"
|
||||
#include "hooks/sleephook.h"
|
||||
@@ -23,8 +26,8 @@
|
||||
#include "util/libutils.h"
|
||||
#include "util/sysutils.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "camera.h"
|
||||
#include "io.h"
|
||||
@@ -46,7 +49,6 @@ namespace games::sdvx {
|
||||
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
|
||||
|
||||
// settings
|
||||
bool NATIVETOUCH = false;
|
||||
uint8_t DIGITAL_KNOB_SENS = 16;
|
||||
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
|
||||
bool ENABLE_COM_PORT_SCAN_HOOK = false;
|
||||
@@ -59,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 &&
|
||||
@@ -428,28 +443,41 @@ namespace games::sdvx {
|
||||
if (aio != nullptr) {
|
||||
|
||||
// enable 9on12 for AMD
|
||||
if (!libutils::try_library("nvapi64.dll")) {
|
||||
const auto nvapi = libutils::try_library("nvapi64.dll");
|
||||
if (nvapi == nullptr) {
|
||||
log_info(
|
||||
"sdvx",
|
||||
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
|
||||
GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true;
|
||||
}
|
||||
|
||||
if (nvapi_hook::BYPASS_NVAPI || nvapi == nullptr) {
|
||||
const uint32_t main_refresh_hz = GRAPHICS_FORCE_REFRESH > 0 ?
|
||||
GRAPHICS_FORCE_REFRESH : (is_valkyrie_model() ? 120 : 60);
|
||||
const uint32_t sub_refresh_hz = GRAPHICS_FORCE_REFRESH_SUB.value_or(60);
|
||||
if (!nvapi_impl::initialize(
|
||||
avs::game::DLL_INSTANCE,
|
||||
main_refresh_hz,
|
||||
sub_refresh_hz)) {
|
||||
log_warning("sdvx", "failed to initialize synthetic NVAPI");
|
||||
}
|
||||
} else {
|
||||
// don't let nvapi mess with display settings
|
||||
nvapi_hook::initialize(avs::game::DLL_INSTANCE);
|
||||
}
|
||||
|
||||
if (is_valkyrie_model()) {
|
||||
if (NATIVETOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
|
||||
// hook touch window
|
||||
// in windowed mode, game can accept mouse input on the second screen
|
||||
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!native_touch_ready) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends(
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends(
|
||||
"SOUND VOLTEX",
|
||||
"Main Screen",
|
||||
avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
// insert BI2X hooks
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "games/game.h"
|
||||
#include "util/tapeled.h"
|
||||
|
||||
namespace games::sdvx {
|
||||
|
||||
@@ -18,7 +19,6 @@ namespace games::sdvx {
|
||||
};
|
||||
|
||||
// settings
|
||||
extern bool NATIVETOUCH;
|
||||
extern uint8_t DIGITAL_KNOB_SENS;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern SdvxOverlayPosition OVERLAY_POS;
|
||||
@@ -26,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") &&
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <audioclient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/audio/backends/mmdevice/device_enumerator.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "audio_private.h"
|
||||
#include "acm.h"
|
||||
#include "asio_proxy.h"
|
||||
#include "xact.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
||||
@@ -53,9 +55,39 @@ namespace hooks::audio {
|
||||
std::string ASIO_DRIVER_NAME = "";
|
||||
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
||||
|
||||
// private globals
|
||||
IAudioClient *CLIENT = nullptr;
|
||||
std::mutex INITIALIZE_LOCK; // for asio
|
||||
|
||||
IAudioClient *CLIENT = nullptr;
|
||||
static std::mutex CLIENT_LOCK;
|
||||
static bool CLIENT_STOPPING = false;
|
||||
|
||||
void set_active_client(IAudioClient *client, const char *source) {
|
||||
IAudioClient *previous_client = nullptr;
|
||||
{
|
||||
std::lock_guard lock(CLIENT_LOCK);
|
||||
|
||||
if (CLIENT_STOPPING || CLIENT == client) {
|
||||
return;
|
||||
}
|
||||
if (client) {
|
||||
client->AddRef();
|
||||
}
|
||||
|
||||
previous_client = CLIENT;
|
||||
CLIENT = client;
|
||||
}
|
||||
|
||||
if (previous_client) {
|
||||
previous_client->Release();
|
||||
}
|
||||
|
||||
if (client) {
|
||||
log_info(
|
||||
"audio",
|
||||
"active client selected by {} after successful initialization",
|
||||
source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
@@ -129,15 +161,24 @@ namespace hooks::audio {
|
||||
|
||||
// general hooks
|
||||
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
|
||||
if (avs::game::is_model("PAN")) {
|
||||
hooks::audio::xact::init();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
log_info("audio", "stopping");
|
||||
if (CLIENT) {
|
||||
CLIENT->Stop();
|
||||
CLIENT->Release();
|
||||
IAudioClient *client = nullptr;
|
||||
{
|
||||
std::lock_guard lock(CLIENT_LOCK);
|
||||
CLIENT_STOPPING = true;
|
||||
client = CLIENT;
|
||||
CLIENT = nullptr;
|
||||
}
|
||||
if (client) {
|
||||
client->Stop();
|
||||
client->Release();
|
||||
}
|
||||
stop_low_latency();
|
||||
|
||||
// release the ASIO drivers we kept pinned for the process lifetime now that we are
|
||||
|
||||
@@ -14,4 +14,6 @@ namespace hooks::audio {
|
||||
extern std::mutex INITIALIZE_LOCK;
|
||||
extern bool VOLUME_HOOK_ENABLED;
|
||||
extern LowLatencyAudioClient *LOW_LATENCY_CLIENT;
|
||||
|
||||
void set_active_client(IAudioClient *client, const char *source);
|
||||
}
|
||||
|
||||
@@ -95,11 +95,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
||||
}
|
||||
std::lock_guard initialize_guard(hooks::audio::INITIALIZE_LOCK, std::adopt_lock);
|
||||
|
||||
// release old audio client if initialized
|
||||
if (hooks::audio::CLIENT) {
|
||||
hooks::audio::CLIENT->Release();
|
||||
}
|
||||
|
||||
IAudioClient *client = nullptr;
|
||||
if (iid == IID_IAudioClient) {
|
||||
client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface));
|
||||
@@ -107,9 +102,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
||||
client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface));
|
||||
}
|
||||
*ppInterface = client;
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
} else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) {
|
||||
*ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface));
|
||||
|
||||
@@ -153,19 +153,9 @@ HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
|
||||
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
|
||||
|
||||
if (iid == IID_IAudioClient) {
|
||||
|
||||
// release any previously persisted client
|
||||
if (hooks::audio::CLIENT != nullptr) {
|
||||
hooks::audio::CLIENT->Release();
|
||||
}
|
||||
|
||||
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
|
||||
*ppInterface = client;
|
||||
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "hooks/audio/util.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
NullDiscardBackend::~NullDiscardBackend() {
|
||||
this->running = false;
|
||||
@@ -125,8 +125,6 @@ HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
|
||||
void NullDiscardBackend::pace_loop() {
|
||||
using namespace std::chrono;
|
||||
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// audio is discarded, so timing precision and drift do not matter; just wake the
|
||||
// game once per buffer period to keep its render thread from blocking on the event.
|
||||
const auto period = duration_cast<steady_clock::duration>(
|
||||
@@ -136,6 +134,6 @@ void NullDiscardBackend::pace_loop() {
|
||||
if (this->relay_handle) {
|
||||
SetEvent(this->relay_handle);
|
||||
}
|
||||
timer.sleep(period);
|
||||
std::this_thread::sleep_for(period);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +302,7 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
device_format->nChannels * (device_format->wBitsPerSample / 8));
|
||||
}
|
||||
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::Initialize");
|
||||
return ret;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
@@ -651,5 +652,6 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
|
||||
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
|
||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||
copy_wave_format(&this->device_format, pFormat);
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::InitializeSharedAudioStream");
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -75,13 +75,17 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
|
||||
|
||||
CHECK_RESULT(this->backend->on_initialize(
|
||||
HRESULT ret = this->backend->on_initialize(
|
||||
&ShareMode,
|
||||
&StreamFlags,
|
||||
&hnsBufferDuration,
|
||||
&hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid));
|
||||
AudioSessionGuid);
|
||||
if (SUCCEEDED(ret)) {
|
||||
hooks::audio::set_active_client(this, "DummyIAudioClient::Initialize");
|
||||
}
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
static std::once_flag printed;
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
#include "xact.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <mmreg.h>
|
||||
#include <objbase.h>
|
||||
|
||||
#include "util/deferlog.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace hooks::audio::xact {
|
||||
|
||||
// XAudio 2.7 is a COM API. Newer Windows SDKs expose a different IXAudio2
|
||||
// layout, so keep this proxy pinned to the legacy ABI used by libxact.
|
||||
struct XAudio2DeviceDetails {
|
||||
WCHAR device_id[256];
|
||||
WCHAR display_name[256];
|
||||
DWORD role;
|
||||
WAVEFORMATEXTENSIBLE output_format;
|
||||
};
|
||||
|
||||
struct XAudio2EffectChain {
|
||||
UINT32 effect_count;
|
||||
const void *effect_descriptors;
|
||||
};
|
||||
|
||||
struct IXAudio2_27 {
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) = 0;
|
||||
virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
|
||||
virtual ULONG STDMETHODCALLTYPE Release() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDeviceDetails(
|
||||
UINT32 device_index,
|
||||
XAudio2DeviceDetails *device_details) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) = 0;
|
||||
virtual void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CreateSourceVoice(
|
||||
void **source_voice,
|
||||
const WAVEFORMATEX *source_format,
|
||||
UINT32 flags,
|
||||
float max_frequency_ratio,
|
||||
void *callback,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
|
||||
void **submix_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 processing_stage,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
|
||||
void **mastering_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 device_index,
|
||||
const XAudio2EffectChain *effect_chain) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE StartEngine() = 0;
|
||||
virtual void STDMETHODCALLTYPE StopEngine() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) = 0;
|
||||
virtual void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) = 0;
|
||||
virtual void STDMETHODCALLTYPE SetDebugConfiguration(
|
||||
const void *debug_configuration,
|
||||
void *reserved) = 0;
|
||||
};
|
||||
|
||||
// XAudio2 2.7 COM class and interface.
|
||||
DEFINE_GUID(CLSID_XAudio2_7_LEGACY,
|
||||
0x5a508685, 0xa254, 0x4fba,
|
||||
0x9b, 0x82, 0x9a, 0x24, 0xb0, 0x03, 0x06, 0xaf);
|
||||
DEFINE_GUID(IID_IXAudio2_7_LEGACY,
|
||||
0x8bcf1f58, 0x9fe7, 0x4583,
|
||||
0x8a, 0xc6, 0xe2, 0xad, 0xc4, 0x65, 0xc8, 0xbb);
|
||||
|
||||
static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr;
|
||||
|
||||
using CreateFX_t = HRESULT (WINAPI *)(REFCLSID, IUnknown **, const void *, UINT32);
|
||||
static CreateFX_t CreateFX_orig = nullptr;
|
||||
|
||||
static std::string describe_wave_format(const WAVEFORMATEX *format) {
|
||||
if (format == nullptr) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
DWORD channel_mask = 0;
|
||||
if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||
format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
channel_mask = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format)->dwChannelMask;
|
||||
}
|
||||
|
||||
return fmt::format(
|
||||
"tag=0x{:04x}, channels={}, rate={} Hz, bits={}, valid_block={} B, avg={} B/s, mask=0x{:08x}",
|
||||
format->wFormatTag,
|
||||
format->nChannels,
|
||||
format->nSamplesPerSec,
|
||||
format->wBitsPerSample,
|
||||
format->nBlockAlign,
|
||||
format->nAvgBytesPerSec,
|
||||
channel_mask);
|
||||
}
|
||||
|
||||
template <size_t Size>
|
||||
static std::string narrow_fixed(const WCHAR (&value)[Size]) {
|
||||
size_t length = 0;
|
||||
while (length < Size && value[length] != L'\0') {
|
||||
length++;
|
||||
}
|
||||
return ws2s(std::wstring(value, length));
|
||||
}
|
||||
|
||||
class WrappedXAudio2 final : public IXAudio2_27 {
|
||||
public:
|
||||
explicit WrappedXAudio2(IXAudio2_27 *real) : real(real) {
|
||||
log_info("audio::xaudio2", "wrapping IXAudio2 2.7 engine {}", static_cast<void *>(real));
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override {
|
||||
if (object == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IXAudio2_7_LEGACY)) {
|
||||
*object = this;
|
||||
AddRef();
|
||||
log_info("audio::xaudio2", "IXAudio2::QueryInterface({}) -> proxy", guid2s(riid));
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const auto result = real->QueryInterface(riid, object);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::QueryInterface({}) -> {}, object={}",
|
||||
guid2s(riid),
|
||||
FMT_HRESULT(result),
|
||||
object != nullptr ? *object : nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE AddRef() override {
|
||||
return ++ref_count;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE Release() override {
|
||||
const auto remaining = --ref_count;
|
||||
if (remaining == 0) {
|
||||
log_info("audio::xaudio2", "destroying IXAudio2 2.7 proxy");
|
||||
real->Release();
|
||||
delete this;
|
||||
}
|
||||
return remaining;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) override {
|
||||
const auto result = real->GetDeviceCount(device_count);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::GetDeviceCount -> {}, count={}",
|
||||
FMT_HRESULT(result),
|
||||
SUCCEEDED(result) && device_count != nullptr ? *device_count : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE GetDeviceDetails(
|
||||
UINT32 device_index,
|
||||
XAudio2DeviceDetails *device_details) override {
|
||||
const auto result = real->GetDeviceDetails(device_index, device_details);
|
||||
if (SUCCEEDED(result) && device_details != nullptr) {
|
||||
const auto device_name = narrow_fixed(device_details->display_name);
|
||||
if (!device_details_logged.exchange(true, std::memory_order_relaxed)) {
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::GetDeviceDetails({}) -> {}, id='{}', name='{}', role=0x{:08x}, {}",
|
||||
device_index,
|
||||
FMT_HRESULT(result),
|
||||
narrow_fixed(device_details->device_id),
|
||||
device_name,
|
||||
device_details->role,
|
||||
describe_wave_format(&device_details->output_format.Format));
|
||||
}
|
||||
const auto channels = device_details->output_format.Format.nChannels;
|
||||
if (channels != 2 && channels != 6 &&
|
||||
!channel_warning_logged.exchange(true, std::memory_order_relaxed)) {
|
||||
log_warning(
|
||||
"audio::xaudio2",
|
||||
"output device '{}' has {} channels; Nostalgia requires stereo or 5.1 output",
|
||||
device_name,
|
||||
channels);
|
||||
deferredlogs::defer_error_messages({
|
||||
"unsupported audio output channel count detected!",
|
||||
fmt::format(" device: {}", device_name),
|
||||
fmt::format(" detected {} channels; Nostalgia requires 2 (stereo) or 6 (5.1)", channels),
|
||||
" * configure the default Windows playback device for stereo or 5.1 output",
|
||||
" * disable 7.1 surround sound or spatial audio for this device",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log_warning(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::GetDeviceDetails({}) -> {}",
|
||||
device_index,
|
||||
FMT_HRESULT(result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) override {
|
||||
const auto result = real->Initialize(flags, processor);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::Initialize(flags=0x{:08x}, processor=0x{:08x}) -> {}",
|
||||
flags,
|
||||
processor,
|
||||
FMT_HRESULT(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) override {
|
||||
const auto result = real->RegisterForCallbacks(callback);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::RegisterForCallbacks({}) -> {}",
|
||||
callback,
|
||||
FMT_HRESULT(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) override {
|
||||
log_info("audio::xaudio2", "IXAudio2::UnregisterForCallbacks({})", callback);
|
||||
real->UnregisterForCallbacks(callback);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CreateSourceVoice(
|
||||
void **source_voice,
|
||||
const WAVEFORMATEX *source_format,
|
||||
UINT32 flags,
|
||||
float max_frequency_ratio,
|
||||
void *callback,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) override {
|
||||
return real->CreateSourceVoice(
|
||||
source_voice,
|
||||
source_format,
|
||||
flags,
|
||||
max_frequency_ratio,
|
||||
callback,
|
||||
send_list,
|
||||
effect_chain);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
|
||||
void **submix_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 processing_stage,
|
||||
const void *send_list,
|
||||
const XAudio2EffectChain *effect_chain) override {
|
||||
return real->CreateSubmixVoice(
|
||||
submix_voice,
|
||||
input_channels,
|
||||
input_sample_rate,
|
||||
flags,
|
||||
processing_stage,
|
||||
send_list,
|
||||
effect_chain);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
|
||||
void **mastering_voice,
|
||||
UINT32 input_channels,
|
||||
UINT32 input_sample_rate,
|
||||
UINT32 flags,
|
||||
UINT32 device_index,
|
||||
const XAudio2EffectChain *effect_chain) override {
|
||||
const auto result = real->CreateMasteringVoice(
|
||||
mastering_voice,
|
||||
input_channels,
|
||||
input_sample_rate,
|
||||
flags,
|
||||
device_index,
|
||||
effect_chain);
|
||||
log_info(
|
||||
"audio::xaudio2",
|
||||
"IXAudio2::CreateMasteringVoice(channels={}, rate={} Hz, flags=0x{:08x}, device={}, effects={}) -> {}, voice={}",
|
||||
input_channels,
|
||||
input_sample_rate,
|
||||
flags,
|
||||
device_index,
|
||||
effect_chain != nullptr ? effect_chain->effect_count : 0,
|
||||
FMT_HRESULT(result),
|
||||
mastering_voice != nullptr ? *mastering_voice : nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE StartEngine() override {
|
||||
const auto result = real->StartEngine();
|
||||
log_info("audio::xaudio2", "IXAudio2::StartEngine -> {}", FMT_HRESULT(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE StopEngine() override {
|
||||
log_info("audio::xaudio2", "IXAudio2::StopEngine");
|
||||
real->StopEngine();
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) override {
|
||||
return real->CommitChanges(operation_set);
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) override {
|
||||
real->GetPerformanceData(performance_data);
|
||||
}
|
||||
|
||||
void STDMETHODCALLTYPE SetDebugConfiguration(
|
||||
const void *debug_configuration,
|
||||
void *reserved) override {
|
||||
log_info("audio::xaudio2", "IXAudio2::SetDebugConfiguration({})", debug_configuration);
|
||||
real->SetDebugConfiguration(debug_configuration, reserved);
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<ULONG> ref_count = 1;
|
||||
std::atomic_bool device_details_logged = false;
|
||||
std::atomic_bool channel_warning_logged = false;
|
||||
IXAudio2_27 *real;
|
||||
};
|
||||
|
||||
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
REFCLSID clsid,
|
||||
LPUNKNOWN outer,
|
||||
DWORD class_context,
|
||||
REFIID iid,
|
||||
LPVOID *object) {
|
||||
const auto result = CoCreateInstance_orig(clsid, outer, class_context, iid, object);
|
||||
log_info(
|
||||
"audio::xact",
|
||||
"CoCreateInstance(clsid={}, iid={}, context=0x{:08x}) -> {}, object={}",
|
||||
guid2s(clsid),
|
||||
guid2s(iid),
|
||||
class_context,
|
||||
FMT_HRESULT(result),
|
||||
object != nullptr ? *object : nullptr);
|
||||
|
||||
if (SUCCEEDED(result) && object != nullptr && *object != nullptr &&
|
||||
IsEqualCLSID(clsid, CLSID_XAudio2_7_LEGACY) &&
|
||||
IsEqualIID(iid, IID_IXAudio2_7_LEGACY)) {
|
||||
*object = static_cast<IXAudio2_27 *>(
|
||||
new WrappedXAudio2(static_cast<IXAudio2_27 *>(*object)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static HRESULT WINAPI CreateFX_hook(
|
||||
REFCLSID clsid,
|
||||
IUnknown **effect,
|
||||
const void *init_data,
|
||||
UINT32 init_data_size) {
|
||||
const auto result = CreateFX_orig(clsid, effect, init_data, init_data_size);
|
||||
log_info(
|
||||
"audio::xapofx",
|
||||
"CreateFX(clsid={}, init_data={}, size={}) -> {}, effect={}",
|
||||
guid2s(clsid),
|
||||
init_data,
|
||||
init_data_size,
|
||||
FMT_HRESULT(result),
|
||||
effect != nullptr ? static_cast<void *>(*effect) : nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
void init() {
|
||||
const auto libxact = GetModuleHandleW(L"libxact.dll");
|
||||
if (libxact == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoCreateInstance_orig = detour::iat_try(
|
||||
"CoCreateInstance", CoCreateInstance_hook, libxact);
|
||||
CreateFX_orig = detour::iat_try("CreateFX", CreateFX_hook, libxact);
|
||||
|
||||
log_info(
|
||||
"audio::xact",
|
||||
"libxact hooks installed: CoCreateInstance={}, CreateFX={}",
|
||||
CoCreateInstance_orig != nullptr,
|
||||
CreateFX_orig != nullptr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace hooks::audio::xact {
|
||||
void init();
|
||||
}
|
||||
@@ -23,10 +23,7 @@
|
||||
#include "external/imgui/backends/imgui_impl_dx11.h"
|
||||
#include "overlay/imgui/impl_spice.h"
|
||||
|
||||
#include "games/io.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -108,8 +105,62 @@ Present1_t Present1_orig = nullptr;
|
||||
bool g_swapchain_hooked = false;
|
||||
bool g_swapchain1_hooked = false;
|
||||
|
||||
// sub-screens / IME helpers are usually child or zero-sized windows.
|
||||
// visibility isn't checked - the game may present before showing the window.
|
||||
bool looks_like_game_window(HWND hwnd) {
|
||||
RECT client {};
|
||||
return GetAncestor(hwnd, GA_ROOT) == hwnd
|
||||
&& GetClientRect(hwnd, &client)
|
||||
&& client.right > client.left
|
||||
&& client.bottom > client.top;
|
||||
}
|
||||
|
||||
// only the main game window; ignore sub-screens / IME helpers.
|
||||
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
|
||||
DXGI_SWAP_CHAIN_DESC desc {};
|
||||
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HWND main = d3d11_hooks::main_hwnd();
|
||||
if (!main) {
|
||||
// no creation hook recorded a window, so fall back to the presenting one;
|
||||
// the choice is permanent, so require a plausible game window
|
||||
if (!looks_like_game_window(desc.OutputWindow)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"graphics::d3d11",
|
||||
"try to notemain hwnd from swapchain present: 0x{:x}",
|
||||
(uintptr_t)desc.OutputWindow);
|
||||
|
||||
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
|
||||
|
||||
// it may have been ignored, or another thread may have won the slot
|
||||
main = d3d11_hooks::main_hwnd();
|
||||
}
|
||||
return desc.OutputWindow == main;
|
||||
}
|
||||
|
||||
// checks are ordered cheapest first, since this runs on every present
|
||||
void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||
if (!swapchain || overlay::OVERLAY) {
|
||||
if (!swapchain) {
|
||||
return;
|
||||
}
|
||||
|
||||
// overlay is disabled by user
|
||||
if (!overlay::ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// overlay is already enabled and attached
|
||||
if (overlay::OVERLAY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore sub windows
|
||||
if (!is_main_game_swapchain(swapchain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,12 +169,6 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only attach to the main game window; ignore sub-screens / IME helpers.
|
||||
HWND main = d3d11_hooks::main_hwnd();
|
||||
if (main && desc.OutputWindow != main) {
|
||||
return;
|
||||
}
|
||||
|
||||
// theme the native title bar; first present is the only reliable point for
|
||||
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
|
||||
set_window_dark_titlebar(desc.OutputWindow);
|
||||
@@ -149,43 +194,42 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||
device->Release();
|
||||
}
|
||||
|
||||
// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour).
|
||||
void poll_screenshot_hotkey() {
|
||||
static bool s_down = false;
|
||||
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||
const bool pressed = buttons
|
||||
&& (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered())
|
||||
&& GameAPI::Buttons::getState(RI_MGR,
|
||||
buttons->at(games::OverlayButtons::Screenshot));
|
||||
if (pressed && !s_down) {
|
||||
graphics_screenshot_trigger();
|
||||
}
|
||||
s_down = pressed;
|
||||
}
|
||||
|
||||
void pump_overlay(IDXGISwapChain *swapchain) {
|
||||
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) {
|
||||
// screenshots have to keep working with the overlay disabled, so they are not gated on it
|
||||
void pump_frame(IDXGISwapChain *swapchain) {
|
||||
const bool has_overlay =
|
||||
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
||||
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
poll_screenshot_hotkey();
|
||||
graphics_poll_screenshot_hotkey();
|
||||
|
||||
// size imgui to the backbuffer (not window client). dxgi may upscale
|
||||
// a small backbuffer into a larger client rect; without this override
|
||||
// imgui would draw past the RTV and the mouse mapping would be off.
|
||||
DXGI_SWAP_CHAIN_DESC desc {};
|
||||
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
|
||||
ImGui_ImplSpice_SetDisplaySizeOverride(
|
||||
(float) desc.BufferDesc.Width,
|
||||
(float) desc.BufferDesc.Height);
|
||||
// before the overlay render so the screenshot excludes it
|
||||
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||
d3d11_hooks::try_screenshot(swapchain);
|
||||
}
|
||||
|
||||
overlay::OVERLAY->update();
|
||||
overlay::OVERLAY->new_frame();
|
||||
overlay::OVERLAY->render();
|
||||
if (has_overlay) {
|
||||
|
||||
// after overlay render so toasts/menus end up in the saved image.
|
||||
d3d11_hooks::try_screenshot(swapchain);
|
||||
// size imgui to the backbuffer (not window client). dxgi may upscale
|
||||
// a small backbuffer into a larger client rect; without this override
|
||||
// imgui would draw past the RTV and the mouse mapping would be off.
|
||||
DXGI_SWAP_CHAIN_DESC desc {};
|
||||
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
|
||||
ImGui_ImplSpice_SetDisplaySizeOverride(
|
||||
(float) desc.BufferDesc.Width,
|
||||
(float) desc.BufferDesc.Height);
|
||||
}
|
||||
|
||||
overlay::OVERLAY->update();
|
||||
overlay::OVERLAY->new_frame();
|
||||
overlay::OVERLAY->render();
|
||||
}
|
||||
|
||||
// after the overlay render so the screenshot includes toasts / menus
|
||||
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||
d3d11_hooks::try_screenshot(swapchain);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
@@ -194,8 +238,11 @@ void pump_overlay(IDXGISwapChain *swapchain) {
|
||||
HRESULT STDMETHODCALLTYPE Present_hook(
|
||||
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
|
||||
{
|
||||
try_create_overlay(swapchain);
|
||||
pump_overlay(swapchain);
|
||||
// a test present doesn't display anything; don't pick a window or take a screenshot off it
|
||||
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||
try_create_overlay(swapchain);
|
||||
pump_frame(swapchain);
|
||||
}
|
||||
return Present_orig(swapchain, SyncInterval, Flags);
|
||||
}
|
||||
|
||||
@@ -203,8 +250,10 @@ HRESULT STDMETHODCALLTYPE Present1_hook(
|
||||
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
|
||||
const DXGI_PRESENT_PARAMETERS *pParams)
|
||||
{
|
||||
try_create_overlay(swapchain);
|
||||
pump_overlay(swapchain);
|
||||
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||
try_create_overlay(swapchain);
|
||||
pump_frame(swapchain);
|
||||
}
|
||||
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
#include "d3d9_backend.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <external/robin_hood.h>
|
||||
|
||||
#include <d3d9.h>
|
||||
#ifdef __GNUC__
|
||||
#include <d3dx9tex.h>
|
||||
#endif
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "cfg/screen_resize.h"
|
||||
@@ -18,28 +15,23 @@
|
||||
#include "games/popn/popn.h"
|
||||
#include "games/sdvx/sdvx.h"
|
||||
#include "games/mfc/mfc.h"
|
||||
#include "games/io.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "launcher/options.h"
|
||||
#include "launcher/signal.h"
|
||||
#include "launcher/shutdown.h"
|
||||
#include "misc/clipboard.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/flags_helper.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/memutils.h"
|
||||
#include "util/threadpool.h"
|
||||
|
||||
#include "d3d9_device.h"
|
||||
#include "d3d9_screenshot.h"
|
||||
|
||||
#ifdef min
|
||||
#undef min
|
||||
@@ -63,19 +55,6 @@
|
||||
return __ret; \
|
||||
} while (0)
|
||||
|
||||
#ifdef __GNUC__
|
||||
typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t;
|
||||
#else
|
||||
#define D3DXIFF_PNG ((DWORD) 3)
|
||||
|
||||
typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)(
|
||||
LPCSTR pDestFile,
|
||||
DWORD DestFormat,
|
||||
LPDIRECT3DSURFACE9 pSrcSurface,
|
||||
CONST PALETTEENTRY *pSrcPalette,
|
||||
CONST RECT *pSrcRect);
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 9 on 12
|
||||
*/
|
||||
@@ -99,8 +78,6 @@ typedef IDirect3D9* (WINAPI *Direct3DCreate9On12_t)(
|
||||
static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr;
|
||||
static char D3D9_DIRECT3D_CREATE9_CONTENTS[16];
|
||||
|
||||
static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false;
|
||||
|
||||
// settings
|
||||
std::optional<UINT> D3D9_ADAPTER = std::nullopt;
|
||||
DWORD D3D9_BEHAVIOR_DISABLE = 0;
|
||||
@@ -113,7 +90,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 +395,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 +454,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 +492,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 +508,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 +522,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 +607,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 +625,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 +640,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 +656,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 +679,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 +697,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 +712,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 +778,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 +902,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 +944,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 +960,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 +974,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 +991,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 +1030,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 +1064,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 +1183,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 +1210,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 +1307,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 +1337,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 +1353,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 +1391,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 +1428,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,188 +1461,24 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void save_capture(
|
||||
int screen,
|
||||
D3DFORMAT format,
|
||||
UINT width,
|
||||
UINT height,
|
||||
IDirect3DSurface9 *surface) {
|
||||
HRESULT hr;
|
||||
|
||||
// lock surface to be able to access the data
|
||||
D3DLOCKED_RECT finished_copy {};
|
||||
hr = surface->LockRect(&finished_copy, nullptr, 0);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
|
||||
graphics_capture_skip(screen);
|
||||
return;
|
||||
}
|
||||
|
||||
// copy pixel data
|
||||
size_t pitch = finished_copy.Pitch;
|
||||
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
|
||||
auto pixels = new uint8_t[width * height * 3];
|
||||
for (size_t row = 0; row < height; row++) {
|
||||
size_t offset_pixels = 0;
|
||||
size_t offset_row = row * width * 3;
|
||||
switch (format) {
|
||||
case D3DFMT_R8G8B8: {
|
||||
for (size_t offset = 0; offset < pitch; offset += 3) {
|
||||
auto cell = data + row * pitch + offset;
|
||||
auto pixel = &pixels[offset_row + offset_pixels];
|
||||
pixel[0] = cell[0];
|
||||
pixel[1] = cell[1];
|
||||
pixel[2] = cell[2];
|
||||
offset_pixels += 3;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case D3DFMT_X8R8G8B8:
|
||||
case D3DFMT_A8R8G8B8: {
|
||||
for (size_t offset = 0; offset < pitch; offset += 4) {
|
||||
auto cell = data + row * pitch + offset;
|
||||
auto pixel = &pixels[offset_row + offset_pixels];
|
||||
pixel[0] = cell[2];
|
||||
pixel[1] = cell[1];
|
||||
pixel[2] = cell[0];
|
||||
offset_pixels += 3;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case D3DFMT_X8B8G8R8:
|
||||
case D3DFMT_A8B8G8R8: {
|
||||
for (size_t offset = 0; offset < pitch; offset += 4) {
|
||||
auto cell = data + row * pitch + offset;
|
||||
auto pixel = &pixels[offset_row + offset_pixels];
|
||||
pixel[0] = cell[0];
|
||||
pixel[1] = cell[1];
|
||||
pixel[2] = cell[2];
|
||||
offset_pixels += 3;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (size_t offset = 0; offset < width; offset++) {
|
||||
auto pixel = &pixels[offset_row + offset_pixels];
|
||||
pixel[0] = 0;
|
||||
pixel[1] = 0;
|
||||
pixel[2] = 0;
|
||||
offset_pixels += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unlock surface
|
||||
hr = surface->UnlockRect();
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
|
||||
graphics_capture_skip(screen);
|
||||
return;
|
||||
}
|
||||
|
||||
// enqueue
|
||||
graphics_capture_enqueue(screen, pixels, width, height);
|
||||
}
|
||||
|
||||
static void save_screenshot(const std::string &file_path, UINT height, IDirect3DSurface9 *surface) {
|
||||
HRESULT hr;
|
||||
|
||||
D3DLOCKED_RECT finished_copy {};
|
||||
hr = surface->LockRect(&finished_copy, nullptr, 0);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
|
||||
return;
|
||||
}
|
||||
|
||||
// set alpha channel to 255
|
||||
{
|
||||
auto pitch = finished_copy.Pitch;
|
||||
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
|
||||
|
||||
for (size_t i = 0; i < height; i++) {
|
||||
for (int j = 3; j < pitch; j += 4) {
|
||||
data[i * pitch + j] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr = surface->UnlockRect();
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
|
||||
return;
|
||||
}
|
||||
|
||||
// lazy load function
|
||||
static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr;
|
||||
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
||||
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>("D3DXSaveSurfaceToFileA");
|
||||
|
||||
// check if function was not found, likely because d3dx9 is not loaded
|
||||
if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
||||
ATTEMPTED_D3DX9_LOAD_LIBRARY = true;
|
||||
|
||||
for (size_t i = 43; i >= 24; i--) {
|
||||
auto lib_name = fmt::format("d3dx9_{}.dll", i);
|
||||
auto d3dx9 = libutils::try_library(lib_name);
|
||||
|
||||
// Check if library was not found
|
||||
if (d3dx9 == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>(
|
||||
d3dx9, "D3DXSaveSurfaceToFileA");
|
||||
|
||||
// Check if function was not found
|
||||
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
||||
FreeLibrary(d3dx9);
|
||||
d3dx9 = nullptr;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
log_info("graphics::d3d9", "found surface save function in '{}'", lib_name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (D3DXSaveSurfaceToFileA_ptr != nullptr) {
|
||||
|
||||
// save to file
|
||||
log_info("graphics::d3d9", "saving screenshot to {}", file_path);
|
||||
auto hr = D3DXSaveSurfaceToFileA_ptr(file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9", "Failed to save screenshot");
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Error,
|
||||
"Screenshot failed to save");
|
||||
return;
|
||||
}
|
||||
|
||||
// save to clipboard
|
||||
clipboard::copy_image(file_path);
|
||||
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
|
||||
} else {
|
||||
log_warning("graphics::d3d9", "Direct3D save helper function not available");
|
||||
}
|
||||
}
|
||||
|
||||
void graphics_d3d9_on_present(
|
||||
HWND hFocusWindow,
|
||||
IDirect3DDevice9 *device,
|
||||
IDirect3DDevice9 *wrapped_device) {
|
||||
WrappedIDirect3DDevice9 *wrapped_device) {
|
||||
|
||||
// image resize / orientation swap. run here (the present path) rather than from `EndScene`,
|
||||
// which may fire several times per frame on multi-pass / render-to-texture games. this is the
|
||||
@@ -1469,6 +1489,13 @@ void graphics_d3d9_on_present(
|
||||
SurfaceHook(device);
|
||||
}
|
||||
|
||||
graphics_poll_screenshot_hotkey();
|
||||
|
||||
// before the overlay render so the screenshot excludes it
|
||||
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||
graphics_d3d9_process_screenshot(device, wrapped_device);
|
||||
}
|
||||
|
||||
// Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never
|
||||
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
|
||||
// `dx9osd.dll` for pfreepanic.
|
||||
@@ -1488,6 +1515,15 @@ void graphics_d3d9_on_present(
|
||||
device->EndScene();
|
||||
}
|
||||
|
||||
// after the overlay render so the screenshot includes toasts / menus
|
||||
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||
graphics_d3d9_process_screenshot(device, wrapped_device);
|
||||
}
|
||||
|
||||
// API capture always includes the overlay; it must run before the subscreen present
|
||||
// below, which leaves the arena SMALL back buffer black
|
||||
graphics_d3d9_process_capture(device, wrapped_device);
|
||||
|
||||
// for IIDX TDJ / SDVX UFC, handle subscreen
|
||||
const bool is_vm = games::sdvx::is_valkyrie_model();
|
||||
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
|
||||
@@ -1501,136 +1537,6 @@ void graphics_d3d9_on_present(
|
||||
if (is_mfc) {
|
||||
wintouchemu::update();
|
||||
}
|
||||
|
||||
// check screenshot key
|
||||
static bool trigger_last = false;
|
||||
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||
if (buttons && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
|
||||
GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::Screenshot)))
|
||||
{
|
||||
if (!trigger_last) {
|
||||
graphics_screenshot_trigger();
|
||||
}
|
||||
trigger_last = true;
|
||||
} else {
|
||||
trigger_last = false;
|
||||
}
|
||||
|
||||
// process pending screenshot
|
||||
bool screenshot = false;
|
||||
bool capture = false;
|
||||
int capture_screen = 0;
|
||||
if ((screenshot = graphics_screenshot_consume())
|
||||
|| ((capture = graphics_capture_consume(&capture_screen)))) {
|
||||
HRESULT hr = S_OK;
|
||||
|
||||
// TODO: verify capture_screen is a valid swapchain
|
||||
|
||||
// get back buffer
|
||||
IDirect3DSurface9 *buffer = nullptr;
|
||||
if (SUB_SWAP_CHAIN != nullptr && capture_screen & 1) {
|
||||
hr = SUB_SWAP_CHAIN->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
||||
} else {
|
||||
hr = device->GetBackBuffer(capture_screen, 0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
||||
}
|
||||
if (FAILED(hr) || buffer == nullptr) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to get back buffer, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
if (capture) {
|
||||
graphics_capture_skip(capture_screen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
D3DSURFACE_DESC desc {};
|
||||
hr = buffer->GetDesc(&desc);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to acquire back buffer descriptor, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
buffer->Release();
|
||||
if (capture) {
|
||||
graphics_capture_skip(capture_screen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: cache render targets
|
||||
IDirect3DSurface9 *temp_surface = nullptr;
|
||||
hr = device->CreateRenderTarget(
|
||||
desc.Width, desc.Height, desc.Format, desc.MultiSampleType,
|
||||
desc.MultiSampleQuality, TRUE, &temp_surface, nullptr);
|
||||
if (FAILED(hr) || temp_surface == nullptr) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to acquire temporary surface, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
buffer->Release();
|
||||
if (capture) {
|
||||
graphics_capture_skip(capture_screen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
hr = device->StretchRect(buffer, nullptr, temp_surface, nullptr, D3DTEXF_NONE);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to copy back buffer contents, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
temp_surface->Release();
|
||||
buffer->Release();
|
||||
if (capture) {
|
||||
graphics_capture_skip(capture_screen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// release original back buffer reference
|
||||
buffer->Release();
|
||||
|
||||
// function for storing the surface
|
||||
auto surface_process = [=]() {
|
||||
|
||||
// capture
|
||||
if (capture) {
|
||||
save_capture(capture_screen, desc.Format, desc.Width, desc.Height, temp_surface);
|
||||
}
|
||||
|
||||
// screenshot
|
||||
if (screenshot) {
|
||||
|
||||
// check where we can save it
|
||||
auto file_path = graphics_screenshot_genpath();
|
||||
if (!file_path.empty()) {
|
||||
|
||||
// write to file
|
||||
save_screenshot(file_path, desc.Height, temp_surface);
|
||||
}
|
||||
}
|
||||
|
||||
// release surface
|
||||
temp_surface->Release();
|
||||
};
|
||||
|
||||
// list of games that crash when running the screenshot processor on another thread
|
||||
static const robin_hood::unordered_set<std::string> THREAD_BAN {
|
||||
"JMA",
|
||||
#ifndef SPICE64
|
||||
"KFC",
|
||||
#endif
|
||||
"KMA",
|
||||
"KLP",
|
||||
"LMA",
|
||||
};
|
||||
|
||||
// run the save operation on another thread for supported games
|
||||
if (THREAD_BAN.contains(avs::game::MODEL)) {
|
||||
surface_process();
|
||||
} else {
|
||||
static auto pool = ThreadPool(2);
|
||||
pool.add(surface_process);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <d3d9.h>
|
||||
|
||||
#include "d3d9_gfdm.h"
|
||||
|
||||
struct WrappedIDirect3DDevice9;
|
||||
|
||||
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
|
||||
static const GUID IID_WrappedIDirect3D9 = {
|
||||
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
|
||||
@@ -11,7 +15,9 @@ void graphics_d3d9_init();
|
||||
void graphics_d3d9_on_present(
|
||||
HWND hFocusWindow,
|
||||
IDirect3DDevice9 *device,
|
||||
IDirect3DDevice9 *wrapped_device);
|
||||
WrappedIDirect3DDevice9 *wrapped_device);
|
||||
|
||||
void graphics_d3d9_notify_subscreen_present();
|
||||
|
||||
IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen();
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
#include "shaders/vertex_shader.h"
|
||||
#endif
|
||||
|
||||
// maps arena's cached additional swap chains (SMALL, LEFT, RIGHT) to screen numbers.
|
||||
// MAIN is the implicit swap chain, is not in those slots, and is always screen 0.
|
||||
// screen 1 is the subscreen for every other game, so SMALL takes that number here too.
|
||||
static constexpr int GFDM_ARENA_SLOT_SCREENS[] { 1, 2, 3 };
|
||||
|
||||
#define CHECK_RESULT_FMT(x, fmt, ...) \
|
||||
HRESULT __ret = (x); \
|
||||
if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \
|
||||
@@ -41,7 +46,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 +89,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 +139,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 +211,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 +227,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 +241,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,22 +293,73 @@ 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;
|
||||
bool arena_slot = false;
|
||||
if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||
create_swap_chain = true;
|
||||
|
||||
} else if (games::gitadora::is_arena_model() &&
|
||||
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) {
|
||||
(GRAPHICS_SCREENSHOT_SUBSCREENS ||
|
||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS ||
|
||||
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) {
|
||||
|
||||
if (pPresentationParameters->BackBufferWidth == 800) {
|
||||
// SMALL (subscreen)
|
||||
create_swap_chain = true;
|
||||
arena_slot = true;
|
||||
index = 0;
|
||||
|
||||
} else if (pPresentationParameters->BackBufferWidth == 1080) {
|
||||
// LEFT/RIGHT
|
||||
create_swap_chain = true;
|
||||
arena_slot = true;
|
||||
index = 1;
|
||||
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
|
||||
index = 2;
|
||||
@@ -285,6 +371,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
|
||||
}
|
||||
}
|
||||
|
||||
// the api lists screens from this registry, so arena heads need their logical numbers in it
|
||||
if (arena_slot) {
|
||||
graphics_screens_register(GFDM_ARENA_SLOT_SCREENS[index]);
|
||||
}
|
||||
|
||||
if (create_fake_swap_chain) {
|
||||
if (!fake_sub_swapchain[index]) {
|
||||
log_info(
|
||||
@@ -335,6 +426,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 +439,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 +454,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 +506,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 +541,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"})) {
|
||||
@@ -399,6 +554,64 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
|
||||
return n;
|
||||
}
|
||||
|
||||
void WrappedIDirect3DDevice9::get_screenshot_screens(std::vector<int> &screens) const {
|
||||
if (games::gitadora::is_arena_model()) {
|
||||
screens.push_back(0);
|
||||
|
||||
// every head the game renders into, whether or not it reaches a display
|
||||
for (int slot = 0; slot < 3; slot++) {
|
||||
if (sub_swapchain[slot] != nullptr || fake_sub_swapchain[slot] != nullptr) {
|
||||
screens.push_back(GFDM_ARENA_SLOT_SCREENS[slot]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
graphics_screens_get(screens);
|
||||
|
||||
// the sub screen is only registered once the game asks for it by index
|
||||
if (sub_swapchain[0] != nullptr &&
|
||||
avs::game::is_model({"LDJ", "KFC", "M39"}) &&
|
||||
std::find(screens.begin(), screens.end(), 1) == screens.end())
|
||||
{
|
||||
screens.push_back(1);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT WrappedIDirect3DDevice9::get_screenshot_swap_chain(
|
||||
UINT iSwapChain,
|
||||
IDirect3DSwapChain9 **ppSwapChain)
|
||||
{
|
||||
if (ppSwapChain == nullptr) {
|
||||
return D3DERR_INVALIDCALL;
|
||||
}
|
||||
|
||||
// the game numbers the two-head SMALL head itself; keep screen 1 meaning SMALL
|
||||
if (games::gitadora::is_arena_model() && is_gfdm_two_head_exclusive() && iSwapChain == 1) {
|
||||
return GetSwapChain(gfdm_logical_small_swapchain, ppSwapChain);
|
||||
}
|
||||
|
||||
if (games::gitadora::is_arena_model()) {
|
||||
for (int slot = 0; slot < 3; slot++) {
|
||||
if (GFDM_ARENA_SLOT_SCREENS[slot] != (int) iSwapChain) {
|
||||
continue;
|
||||
}
|
||||
if (sub_swapchain[slot] != nullptr) {
|
||||
sub_swapchain[slot]->AddRef();
|
||||
*ppSwapChain = sub_swapchain[slot];
|
||||
return D3D_OK;
|
||||
}
|
||||
if (fake_sub_swapchain[slot] != nullptr) {
|
||||
fake_sub_swapchain[slot]->AddRef();
|
||||
*ppSwapChain = fake_sub_swapchain[slot];
|
||||
return D3D_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GetSwapChain(iSwapChain, ppSwapChain);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
|
||||
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
||||
{
|
||||
@@ -453,6 +666,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 +692,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 +722,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 +742,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 +957,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 +1334,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 +2093,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 +2208,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 +2232,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 +2358,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,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include <initguid.h>
|
||||
#include <d3d9.h>
|
||||
@@ -40,23 +43,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 +235,31 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
|
||||
#pragma endregion
|
||||
|
||||
// logical screens the game draws, and the swap chain each one lives on
|
||||
void get_screenshot_screens(std::vector<int> &screens) const;
|
||||
HRESULT get_screenshot_swap_chain(UINT iSwapChain, IDirect3DSwapChain9 **ppSwapChain);
|
||||
|
||||
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 +267,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 ¶ms = 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 ¶meters = 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,
|
||||
¤t,
|
||||
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,
|
||||
¶ms,
|
||||
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,80 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <d3d9.h>
|
||||
|
||||
#include "games/gitadora/gitadora.h"
|
||||
|
||||
inline constexpr UINT GFDM_SIDE_WIDTH = 1080;
|
||||
inline constexpr UINT GFDM_SIDE_HEIGHT = 1920;
|
||||
inline constexpr UINT GFDM_SMALL_WIDTH = games::gitadora::ARENA_SUBSCREEN_WIDTH;
|
||||
inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
|
||||
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);
|
||||
@@ -0,0 +1,470 @@
|
||||
#include "d3d9_screenshot.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <external/robin_hood.h>
|
||||
|
||||
#ifdef __GNUC__
|
||||
#include <d3dx9tex.h>
|
||||
#endif
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/clipboard.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/threadpool.h"
|
||||
|
||||
#include "d3d9_device.h"
|
||||
|
||||
#ifdef __GNUC__
|
||||
typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t;
|
||||
#else
|
||||
#define D3DXIFF_PNG ((DWORD) 3)
|
||||
|
||||
typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)(
|
||||
LPCSTR pDestFile,
|
||||
DWORD DestFormat,
|
||||
LPDIRECT3DSURFACE9 pSrcSurface,
|
||||
CONST PALETTEENTRY *pSrcPalette,
|
||||
CONST RECT *pSrcRect);
|
||||
#endif
|
||||
|
||||
static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false;
|
||||
|
||||
// genpath picks free filenames by probing the disk, so only one save may run at a time
|
||||
static std::mutex SCREENSHOT_SAVE_M;
|
||||
|
||||
namespace {
|
||||
|
||||
enum class ImageRequestKind {
|
||||
Screenshot,
|
||||
Capture,
|
||||
};
|
||||
|
||||
struct ImageRequest {
|
||||
ImageRequestKind kind;
|
||||
int screen;
|
||||
};
|
||||
|
||||
struct SurfaceReleaser {
|
||||
void operator()(IDirect3DSurface9 *surface) const {
|
||||
surface->Release();
|
||||
}
|
||||
};
|
||||
|
||||
using SurfacePtr = std::unique_ptr<IDirect3DSurface9, SurfaceReleaser>;
|
||||
|
||||
struct BackbufferCopy {
|
||||
int screen {};
|
||||
D3DSURFACE_DESC desc {};
|
||||
SurfacePtr surface;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void save_capture(
|
||||
int screen,
|
||||
D3DFORMAT format,
|
||||
UINT width,
|
||||
UINT height,
|
||||
IDirect3DSurface9 *surface) {
|
||||
HRESULT hr;
|
||||
|
||||
// lock surface to be able to access the data
|
||||
D3DLOCKED_RECT finished_copy {};
|
||||
hr = surface->LockRect(&finished_copy, nullptr, 0);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
|
||||
graphics_capture_skip(screen);
|
||||
return;
|
||||
}
|
||||
|
||||
// normalize supported D3D formats to packed RGB for API capture
|
||||
size_t pitch = finished_copy.Pitch;
|
||||
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
|
||||
auto pixels = std::unique_ptr<uint8_t[]>(new uint8_t[width * height * 3]);
|
||||
for (size_t row = 0; row < height; row++) {
|
||||
size_t offset_row = row * width * 3;
|
||||
switch (format) {
|
||||
case D3DFMT_R8G8B8: {
|
||||
for (size_t column = 0; column < width; column++) {
|
||||
auto cell = data + row * pitch + column * 3;
|
||||
auto pixel = &pixels[offset_row + column * 3];
|
||||
pixel[0] = cell[0];
|
||||
pixel[1] = cell[1];
|
||||
pixel[2] = cell[2];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case D3DFMT_X8R8G8B8:
|
||||
case D3DFMT_A8R8G8B8: {
|
||||
for (size_t column = 0; column < width; column++) {
|
||||
auto cell = data + row * pitch + column * 4;
|
||||
auto pixel = &pixels[offset_row + column * 3];
|
||||
pixel[0] = cell[2];
|
||||
pixel[1] = cell[1];
|
||||
pixel[2] = cell[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case D3DFMT_X8B8G8R8:
|
||||
case D3DFMT_A8B8G8R8: {
|
||||
for (size_t column = 0; column < width; column++) {
|
||||
auto cell = data + row * pitch + column * 4;
|
||||
auto pixel = &pixels[offset_row + column * 3];
|
||||
pixel[0] = cell[0];
|
||||
pixel[1] = cell[1];
|
||||
pixel[2] = cell[2];
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (size_t column = 0; column < width; column++) {
|
||||
auto pixel = &pixels[offset_row + column * 3];
|
||||
pixel[0] = 0;
|
||||
pixel[1] = 0;
|
||||
pixel[2] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unlock surface
|
||||
hr = surface->UnlockRect();
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
|
||||
graphics_capture_skip(screen);
|
||||
return;
|
||||
}
|
||||
|
||||
// enqueue
|
||||
graphics_capture_enqueue(screen, pixels.release(), width, height);
|
||||
}
|
||||
|
||||
static bool save_screenshot(
|
||||
const std::string &file_path,
|
||||
D3DFORMAT format,
|
||||
UINT width,
|
||||
UINT height,
|
||||
IDirect3DSurface9 *surface) {
|
||||
// 32-bit XRGB and ARGB surfaces use byte 3 as alpha; force opaque PNG output
|
||||
if (format == D3DFMT_X8R8G8B8 || format == D3DFMT_A8R8G8B8 ||
|
||||
format == D3DFMT_X8B8G8R8 || format == D3DFMT_A8B8G8R8) {
|
||||
|
||||
D3DLOCKED_RECT finished_copy {};
|
||||
HRESULT hr = surface->LockRect(&finished_copy, nullptr, 0);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t pitch = finished_copy.Pitch;
|
||||
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
|
||||
for (size_t row = 0; row < height; row++) {
|
||||
for (size_t column = 0; column < width; column++) {
|
||||
data[row * pitch + column * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
hr = surface->UnlockRect();
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// lazy load function
|
||||
static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr;
|
||||
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
||||
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>("D3DXSaveSurfaceToFileA");
|
||||
|
||||
// check if function was not found, likely because d3dx9 is not loaded
|
||||
if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
||||
ATTEMPTED_D3DX9_LOAD_LIBRARY = true;
|
||||
|
||||
// prefer the newest installed helper while supporting older D3DX9 runtimes
|
||||
for (size_t i = 43; i >= 24; i--) {
|
||||
auto lib_name = fmt::format("d3dx9_{}.dll", i);
|
||||
auto d3dx9 = libutils::try_library(lib_name);
|
||||
|
||||
// Check if library was not found
|
||||
if (d3dx9 == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>(
|
||||
d3dx9, "D3DXSaveSurfaceToFileA");
|
||||
|
||||
// Check if function was not found
|
||||
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
||||
FreeLibrary(d3dx9);
|
||||
d3dx9 = nullptr;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
log_info("graphics::d3d9", "found surface save function in '{}'", lib_name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
|
||||
log_warning("graphics::d3d9", "Direct3D save helper function not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
// save to file
|
||||
log_info("graphics::d3d9", "saving screenshot to {}", file_path);
|
||||
const HRESULT save_result = D3DXSaveSurfaceToFileA_ptr(
|
||||
file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr);
|
||||
|
||||
if (FAILED(save_result)) {
|
||||
log_warning("graphics::d3d9", "Failed to save screenshot");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// screen 0 keeps the plain name so existing tooling and the clipboard copy are unaffected
|
||||
static std::string screenshot_path_for_screen(const std::string &primary_path, int screen) {
|
||||
if (screen == 0) {
|
||||
return primary_path;
|
||||
}
|
||||
|
||||
const std::filesystem::path path(primary_path);
|
||||
return (path.parent_path() /
|
||||
fmt::format("{}_{}{}", path.stem().string(), screen, path.extension().string()))
|
||||
.string();
|
||||
}
|
||||
|
||||
static std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
||||
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
|
||||
|
||||
IDirect3DSurface9 *buffer = nullptr;
|
||||
HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
||||
if (FAILED(hr) || buffer == nullptr) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to get back buffer for screen {}, hr={}",
|
||||
screen,
|
||||
FMT_HRESULT(hr));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
D3DSURFACE_DESC desc {};
|
||||
hr = buffer->GetDesc(&desc);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to acquire back buffer descriptor, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
buffer->Release();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// TODO: cache render targets
|
||||
IDirect3DSurface9 *temp_surface = nullptr;
|
||||
hr = device->CreateRenderTarget(
|
||||
desc.Width, desc.Height, desc.Format, desc.MultiSampleType,
|
||||
desc.MultiSampleQuality, TRUE, &temp_surface, nullptr);
|
||||
if (FAILED(hr) || temp_surface == nullptr) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to acquire temporary surface, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
buffer->Release();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
hr = device->StretchRect(buffer, nullptr, temp_surface, nullptr, D3DTEXF_NONE);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to copy back buffer contents, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
temp_surface->Release();
|
||||
buffer->Release();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// release original back buffer reference
|
||||
buffer->Release();
|
||||
|
||||
return BackbufferCopy {
|
||||
.screen = screen,
|
||||
.desc = desc,
|
||||
.surface = SurfacePtr(temp_surface),
|
||||
};
|
||||
}
|
||||
|
||||
static void dispatch_surface_save(
|
||||
const ImageRequest &request,
|
||||
std::vector<BackbufferCopy> copies,
|
||||
size_t screen_count) {
|
||||
auto surface_process = [request, screen_count, copies = std::move(copies)]() {
|
||||
switch (request.kind) {
|
||||
case ImageRequestKind::Capture: {
|
||||
const auto © = copies.front();
|
||||
save_capture(
|
||||
request.screen,
|
||||
copy.desc.Format,
|
||||
copy.desc.Width,
|
||||
copy.desc.Height,
|
||||
copy.surface.get());
|
||||
break;
|
||||
}
|
||||
|
||||
case ImageRequestKind::Screenshot: {
|
||||
std::lock_guard<std::mutex> lock(SCREENSHOT_SAVE_M);
|
||||
|
||||
std::vector<int> screens;
|
||||
screens.reserve(copies.size());
|
||||
for (const auto © : copies) {
|
||||
screens.push_back(copy.screen);
|
||||
}
|
||||
|
||||
const auto base_path = graphics_screenshot_genpath(screens);
|
||||
if (base_path.empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// screens missing from copies already failed to be acquired
|
||||
size_t failed = screen_count - copies.size();
|
||||
std::string primary_path;
|
||||
std::string notify_path;
|
||||
for (const auto © : copies) {
|
||||
const auto path = screenshot_path_for_screen(base_path, copy.screen);
|
||||
if (!save_screenshot(
|
||||
path,
|
||||
copy.desc.Format,
|
||||
copy.desc.Width,
|
||||
copy.desc.Height,
|
||||
copy.surface.get())) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
if (notify_path.empty()) {
|
||||
notify_path = path;
|
||||
}
|
||||
if (copy.screen == 0) {
|
||||
primary_path = path;
|
||||
}
|
||||
}
|
||||
|
||||
// only the primary screen goes to the clipboard, but any saved file is a success
|
||||
if (!primary_path.empty()) {
|
||||
clipboard::copy_image(primary_path);
|
||||
}
|
||||
if (!notify_path.empty()) {
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
fmt::format("Screenshot saved: {}", fileutils::basename(notify_path)));
|
||||
} else {
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Error,
|
||||
"Screenshot failed to save");
|
||||
}
|
||||
|
||||
if (failed > 0) {
|
||||
log_warning("graphics::d3d9", "{} screenshot screen(s) missing", failed);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// list of games that crash when running the screenshot processor on another thread
|
||||
static const robin_hood::unordered_set<std::string> THREAD_BAN {
|
||||
"JMA",
|
||||
#ifndef SPICE64
|
||||
// KFC only crashes under threaded processing in 32-bit builds
|
||||
"KFC",
|
||||
#endif
|
||||
"KMA",
|
||||
"KLP",
|
||||
"LMA",
|
||||
};
|
||||
|
||||
// run the save operation on another thread for supported games
|
||||
if (THREAD_BAN.contains(avs::game::MODEL)) {
|
||||
surface_process();
|
||||
} else {
|
||||
static auto pool = ThreadPool(2);
|
||||
pool.add(std::move(surface_process));
|
||||
}
|
||||
}
|
||||
|
||||
static void process_image_request(
|
||||
IDirect3DDevice9 *device,
|
||||
WrappedIDirect3DDevice9 *wrapped_device,
|
||||
const ImageRequest &request) {
|
||||
const bool screenshot = request.kind == ImageRequestKind::Screenshot;
|
||||
|
||||
std::vector<int> screens { request.screen };
|
||||
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
|
||||
screens.clear();
|
||||
wrapped_device->get_screenshot_screens(screens);
|
||||
}
|
||||
|
||||
std::vector<BackbufferCopy> copies;
|
||||
copies.reserve(screens.size());
|
||||
for (const int screen : screens) {
|
||||
std::optional<BackbufferCopy> copy;
|
||||
|
||||
IDirect3DSwapChain9 *swap_chain = nullptr;
|
||||
HRESULT hr = wrapped_device->get_screenshot_swap_chain(screen, &swap_chain);
|
||||
if (FAILED(hr) || swap_chain == nullptr) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to get swap chain for screen {}, hr={}",
|
||||
screen,
|
||||
FMT_HRESULT(hr));
|
||||
} else {
|
||||
copy = acquire_backbuffer_copy(device, swap_chain, screen);
|
||||
swap_chain->Release();
|
||||
}
|
||||
|
||||
if (copy.has_value()) {
|
||||
copies.emplace_back(std::move(*copy));
|
||||
} else if (!screenshot) {
|
||||
graphics_capture_skip(request.screen);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (copies.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_surface_save(request, std::move(copies), screens.size());
|
||||
}
|
||||
|
||||
void graphics_d3d9_process_screenshot(
|
||||
IDirect3DDevice9 *device,
|
||||
WrappedIDirect3DDevice9 *wrapped_device) {
|
||||
if (graphics_screenshot_consume()) {
|
||||
process_image_request(device, wrapped_device, ImageRequest {
|
||||
.kind = ImageRequestKind::Screenshot,
|
||||
.screen = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void graphics_d3d9_process_capture(
|
||||
IDirect3DDevice9 *device,
|
||||
WrappedIDirect3DDevice9 *wrapped_device) {
|
||||
int screen = 0;
|
||||
if (graphics_capture_consume(&screen)) {
|
||||
process_image_request(device, wrapped_device, ImageRequest {
|
||||
.kind = ImageRequestKind::Capture,
|
||||
.screen = screen,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <d3d9.h>
|
||||
|
||||
struct WrappedIDirect3DDevice9;
|
||||
|
||||
void graphics_d3d9_process_screenshot(
|
||||
IDirect3DDevice9 *device,
|
||||
WrappedIDirect3DDevice9 *wrapped_device);
|
||||
|
||||
void graphics_d3d9_process_capture(
|
||||
IDirect3DDevice9 *device,
|
||||
WrappedIDirect3DDevice9 *wrapped_device);
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
#include "games/ddr/ddr.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "games/io.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"
|
||||
#include "launcher/shutdown.h"
|
||||
#include "misc/hotkeys.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/touch_gestures.h"
|
||||
@@ -29,6 +32,8 @@
|
||||
#include "util/fileutils.h"
|
||||
#include "util/utils.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/time.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
|
||||
@@ -43,6 +48,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;
|
||||
|
||||
@@ -112,6 +119,8 @@ uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT = 0;
|
||||
// settings
|
||||
std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146";
|
||||
std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots";
|
||||
bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = false;
|
||||
bool GRAPHICS_SCREENSHOT_SUBSCREENS = false;
|
||||
|
||||
static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr;
|
||||
static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr;
|
||||
@@ -182,23 +191,133 @@ 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;
|
||||
}
|
||||
|
||||
const auto window_name = gitadora_window_name_for_hwnd(hWnd);
|
||||
return window_name != nullptr && graphics_gitadora_has_window_monitor(window_name);
|
||||
return window_name != nullptr && graphics_gitadora_has_window_override(window_name);
|
||||
}
|
||||
|
||||
static void gitadora_remember_window(HWND hWnd, const std::string &window_name) {
|
||||
if (window_name == "LEFT") {
|
||||
if (window_name == "GITADORA") {
|
||||
GRAPHICS_HOOKED_WINDOW = hWnd;
|
||||
} else if (window_name == "LEFT") {
|
||||
GFDM_LEFT_WINDOW = hWnd;
|
||||
} else if (window_name == "RIGHT") {
|
||||
GFDM_RIGHT_WINDOW = hWnd;
|
||||
} else if (window_name == "SMALL") {
|
||||
GFDM_SUBSCREEN_WINDOW = hWnd;
|
||||
}
|
||||
|
||||
// touch belongs to the SMALL panel when it exists, otherwise to the main window
|
||||
// that draws the subscreen overlay
|
||||
const bool hosts_touch = window_name == "SMALL" ||
|
||||
(window_name == "GITADORA" && !graphics_gitadora_has_dedicated_subscreen());
|
||||
if (nativetouch::is_hooked() && hWnd != nullptr && hosts_touch) {
|
||||
nativetouch::inject::set_preferred_injection_window(hWnd);
|
||||
}
|
||||
}
|
||||
|
||||
bool graphics_gitadora_has_dedicated_subscreen() {
|
||||
return GFDM_SUBSCREEN_WINDOW != nullptr;
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -510,8 +629,10 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
||||
}
|
||||
}
|
||||
|
||||
const bool is_sdvx = avs::game::is_model("KFC");
|
||||
bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
|
||||
bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen");
|
||||
bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen");
|
||||
bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen");
|
||||
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
|
||||
const std::string gfdm_window_name = games::gitadora::is_arena_model()
|
||||
? gitadora_canonical_window_name(effective_window_name)
|
||||
@@ -589,6 +710,12 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
||||
hWndParent, hMenu, hInstance, lpParam);
|
||||
GRAPHICS_WINDOWS.push_back(result);
|
||||
|
||||
// remember these windows now because full capture happens later during D3D
|
||||
// initialization; placement calls before then can undo creation-time overrides
|
||||
if (result != nullptr && is_gfdm_window && !is_gfdm_sub_window) {
|
||||
gitadora_remember_window(result, gfdm_window_name);
|
||||
}
|
||||
|
||||
// theme the native title bar (dark/light)
|
||||
set_window_dark_titlebar(result);
|
||||
|
||||
@@ -608,13 +735,36 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
||||
graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW);
|
||||
}
|
||||
|
||||
// SDVX registers touch on both windows, so name the one synthetic touches must land on
|
||||
// instead of letting window creation order decide: the sub screen window when windowed,
|
||||
// the main window in fullscreen since the game reads it in primary-display coordinates
|
||||
if (nativetouch::is_hooked() &&
|
||||
result != nullptr &&
|
||||
(GRAPHICS_WINDOWED ? is_sdvx_sub_window : is_sdvx_main_window)) {
|
||||
log_misc(
|
||||
"graphics",
|
||||
"SDVX touch surface is {}, {}",
|
||||
fmt::ptr(result),
|
||||
window_name);
|
||||
nativetouch::inject::set_preferred_injection_window(result);
|
||||
}
|
||||
|
||||
// only hook touch window if multiple windows are allowed
|
||||
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
|
||||
gitadora_remember_window(result, gfdm_window_name);
|
||||
}
|
||||
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
if (is_gfdm_sub_window &&
|
||||
((GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) ||
|
||||
games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE)) {
|
||||
gitadora_remember_window(result, gfdm_window_name);
|
||||
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
||||
if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
||||
}
|
||||
|
||||
// the dedicated SMALL window is the touch panel; mouse and API touch target it
|
||||
if (nativetouch::is_hooked() && result != nullptr) {
|
||||
nativetouch::inject::register_and_attach_window(result);
|
||||
}
|
||||
}
|
||||
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
gitadora_force_window_style(result);
|
||||
@@ -761,6 +911,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 {};
|
||||
@@ -797,7 +951,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
||||
nWidth = rect.right - rect.left;
|
||||
nHeight = rect.bottom - rect.top;
|
||||
|
||||
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
||||
if (games::iidx::NATIVE_TOUCH) {
|
||||
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
|
||||
} else {
|
||||
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
||||
}
|
||||
} else {
|
||||
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
|
||||
log_info(
|
||||
@@ -864,6 +1022,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;
|
||||
@@ -884,6 +1047,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;
|
||||
@@ -905,6 +1073,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;
|
||||
@@ -929,6 +1103,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) {
|
||||
@@ -950,6 +1131,22 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (games::iidx::TDJ_MODE &&
|
||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||
hWnd == TDJ_SUBSCREEN_WINDOW) {
|
||||
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
||||
return true;
|
||||
}
|
||||
|
||||
// fullscreen SDVX keeps two adapters so the subscreen overlay can draw, so the game still
|
||||
// creates the sub window even when the user asked for it to be gone
|
||||
if (avs::game::is_model("KFC") &&
|
||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||
hWnd == SDVX_SUBSCREEN_WINDOW) {
|
||||
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
||||
return true;
|
||||
}
|
||||
|
||||
// call original
|
||||
return ShowWindow_orig(hWnd, nCmdShow);
|
||||
}
|
||||
@@ -1163,6 +1360,16 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
|
||||
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
|
||||
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
|
||||
|
||||
const bool native_touch_overlay =
|
||||
(games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
|
||||
(games::popn::NATIVE_TOUCH &&
|
||||
games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) ||
|
||||
(games::gitadora::NATIVE_TOUCH &&
|
||||
games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
|
||||
if (native_touch_overlay) {
|
||||
nativetouch::inject::register_and_attach_window(hWnd);
|
||||
}
|
||||
|
||||
// NOLEGACY causes WM_CHAR to be not received
|
||||
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
|
||||
if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
|
||||
@@ -1216,6 +1423,12 @@ void graphics_screens_get(std::vector<int> &screens) {
|
||||
screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end());
|
||||
}
|
||||
|
||||
void graphics_poll_screenshot_hotkey() {
|
||||
if (hotkeys::consume_screenshot()) {
|
||||
graphics_screenshot_trigger();
|
||||
}
|
||||
}
|
||||
|
||||
void graphics_screenshot_trigger() {
|
||||
GRAPHICS_SCREENSHOT_TRIGGER = true;
|
||||
}
|
||||
@@ -1364,7 +1577,7 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
|
||||
return success;
|
||||
}
|
||||
|
||||
std::string graphics_screenshot_genpath() {
|
||||
std::string graphics_screenshot_genpath(const std::vector<int> &screens) {
|
||||
|
||||
// verify dir path
|
||||
if (GRAPHICS_SCREENSHOT_DIR.empty()) {
|
||||
@@ -1389,11 +1602,21 @@ std::string graphics_screenshot_genpath() {
|
||||
auto tm_now = *std::gmtime(&t_now);
|
||||
auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d"));
|
||||
|
||||
// find next filename
|
||||
// find next filename; the whole set has to be free so one shot stays numbered together
|
||||
size_t id = 0;
|
||||
while (true) {
|
||||
auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id);
|
||||
if (!fileutils::file_exists(filepath)) {
|
||||
bool available = !fileutils::file_exists(filepath);
|
||||
for (const auto screen : screens) {
|
||||
if (!available) {
|
||||
break;
|
||||
}
|
||||
if (screen != 0) {
|
||||
available = !fileutils::file_exists(fmt::format(
|
||||
"{}\\{}_{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id, screen));
|
||||
}
|
||||
}
|
||||
if (available) {
|
||||
return filepath;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ extern std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
||||
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||
extern std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
|
||||
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
|
||||
|
||||
extern bool GRAPHICS_IIDX_WSUB;
|
||||
extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
|
||||
@@ -109,6 +111,8 @@ extern bool FAKE_SUBSCREEN_ADAPTER;
|
||||
// settings
|
||||
extern std::string GRAPHICS_DEVICEID;
|
||||
extern std::string GRAPHICS_SCREENSHOT_DIR;
|
||||
extern bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY;
|
||||
extern bool GRAPHICS_SCREENSHOT_SUBSCREENS;
|
||||
|
||||
// Direct3D 9 settings
|
||||
extern std::optional<UINT> D3D9_ADAPTER;
|
||||
@@ -117,12 +121,23 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
|
||||
|
||||
void graphics_init();
|
||||
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
|
||||
bool graphics_gitadora_has_dedicated_subscreen();
|
||||
// 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);
|
||||
void graphics_screens_register(int screen);
|
||||
void graphics_screens_unregister(int screen);
|
||||
void graphics_screens_get(std::vector<int> &screens);
|
||||
void graphics_poll_screenshot_hotkey();
|
||||
void graphics_screenshot_trigger();
|
||||
bool graphics_screenshot_consume();
|
||||
void graphics_capture_trigger(int screen);
|
||||
@@ -133,7 +148,8 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
|
||||
bool rgb = true, int quality = 80, bool downsample = true, int divide = 0,
|
||||
uint64_t *timestamp = nullptr,
|
||||
int *width = nullptr, int *height = nullptr);
|
||||
std::string graphics_screenshot_genpath();
|
||||
// the returned path is for screen 0; any extra screens only reserve their suffixed names
|
||||
std::string graphics_screenshot_genpath(const std::vector<int> &screens = {});
|
||||
|
||||
// graphics_windowed.cpp
|
||||
void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
|
||||
@@ -157,7 +173,7 @@ bool graphics_gitadora_apply_window_monitor(
|
||||
int &width,
|
||||
int &height,
|
||||
bool log_change);
|
||||
bool graphics_gitadora_has_window_monitor(const std::string &window_name);
|
||||
bool graphics_gitadora_has_window_override(const std::string &window_name);
|
||||
|
||||
void change_primary_monitor(const std::string &monitor_name);
|
||||
void update_monitor_on_boot(
|
||||
|
||||
@@ -30,6 +30,8 @@ std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
||||
std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||
std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||
std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||
std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
|
||||
std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
|
||||
|
||||
// IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed
|
||||
bool GRAPHICS_IIDX_WSUB = false;
|
||||
@@ -89,9 +91,12 @@ static const std::string &graphics_gitadora_monitor_for_window_name(
|
||||
return empty;
|
||||
}
|
||||
|
||||
bool graphics_gitadora_has_window_monitor(const std::string &window_name) {
|
||||
bool graphics_gitadora_has_window_override(const std::string &window_name) {
|
||||
const auto &monitor_name = graphics_gitadora_monitor_for_window_name(window_name);
|
||||
return !monitor_name.empty();
|
||||
return !monitor_name.empty() ||
|
||||
(window_name == "SMALL" &&
|
||||
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() ||
|
||||
GRAPHICS_GITADORA_SMALL_POS.has_value()));
|
||||
}
|
||||
|
||||
static bool graphics_monitor_rect_from_name(
|
||||
@@ -212,7 +217,11 @@ bool graphics_gitadora_apply_window_monitor(
|
||||
int &width,
|
||||
int &height,
|
||||
bool log_change) {
|
||||
return graphics_gitadora_apply_monitor_rect(
|
||||
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool applied = graphics_gitadora_apply_monitor_rect(
|
||||
graphics_gitadora_monitor_for_window_name(window_name),
|
||||
window_name,
|
||||
x,
|
||||
@@ -220,6 +229,42 @@ bool graphics_gitadora_apply_window_monitor(
|
||||
width,
|
||||
height,
|
||||
log_change);
|
||||
|
||||
if (window_name != "SMALL") {
|
||||
return applied;
|
||||
}
|
||||
|
||||
std::pair<uint32_t, uint32_t> result;
|
||||
if (GRAPHICS_GITADORA_SMALL_SIZE.has_value()) {
|
||||
if (parse_width_height(GRAPHICS_GITADORA_SMALL_SIZE.value(), result)) {
|
||||
width = result.first;
|
||||
height = result.second;
|
||||
applied = true;
|
||||
} else {
|
||||
log_fatal("graphics-windowed", "failed to parse -gdwsmallsize");
|
||||
}
|
||||
}
|
||||
if (GRAPHICS_GITADORA_SMALL_POS.has_value()) {
|
||||
if (parse_width_height(GRAPHICS_GITADORA_SMALL_POS.value(), result)) {
|
||||
x = result.first;
|
||||
y = result.second;
|
||||
applied = true;
|
||||
} else {
|
||||
log_fatal("graphics-windowed", "failed to parse -gdwsmallpos");
|
||||
}
|
||||
}
|
||||
|
||||
if (applied && log_change &&
|
||||
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() || GRAPHICS_GITADORA_SMALL_POS.has_value())) {
|
||||
log_info(
|
||||
"graphics-windowed",
|
||||
"GITADORA SMALL custom override: pos=({}, {}), size={}x{}",
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height);
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
void graphics_capture_initial_window(HWND hWnd) {
|
||||
@@ -254,7 +299,7 @@ void graphics_capture_initial_window(HWND hWnd) {
|
||||
cfg::SCREENRESIZE->init_client_width = client_w;
|
||||
cfg::SCREENRESIZE->init_client_height = client_h;
|
||||
cfg::SCREENRESIZE->init_client_aspect_ratio = (float)client_w / (float)client_h;
|
||||
log_debug(
|
||||
log_misc(
|
||||
"graphics-windowed",
|
||||
"[{}] graphics_capture_initial_window initial window size {}x{}, ratio {}",
|
||||
fmt::ptr(hWnd),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "external/nvapi/nvapi.h"
|
||||
#include "external/nvapi/NvApiDriverSettings.h"
|
||||
#include "avs/game.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
@@ -12,14 +13,21 @@ namespace nvapi_hook {
|
||||
bool BYPASS_NVAPI = false;
|
||||
|
||||
typedef uintptr_t *(*NvAPI_QueryInterface_t)(unsigned int);
|
||||
|
||||
static NvAPI_QueryInterface_t NvAPI_QueryInterface_orig = nullptr;
|
||||
static decltype(NvAPI_GPU_GetConnectedDisplayIds) *NvAPI_GPU_GetConnectedDisplayIds_orig = nullptr;
|
||||
static decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *NvAPI_DISP_GetGDIPrimaryDisplayId_orig = nullptr;
|
||||
|
||||
static uintptr_t* __cdecl NvAPI_QueryInterface_hook(unsigned int func_code);
|
||||
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
|
||||
NvPhysicalGpuHandle hPhysicalGpu,
|
||||
NV_GPU_DISPLAYIDS *pDisplayIds,
|
||||
NvU32 *pDisplayIdCount,
|
||||
NvU32 flags);
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
||||
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags);
|
||||
|
||||
void initialize(HINSTANCE dll) {
|
||||
|
||||
#ifdef SPICE64
|
||||
std::string nvapi_dll = "nvapi64.dll";
|
||||
#else
|
||||
@@ -40,6 +48,18 @@ namespace nvapi_hook {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// spoof display connector types only for SDVX
|
||||
if (func_code == 0x0078DBA2 && avs::game::is_model("KFC")) {
|
||||
NvAPI_GPU_GetConnectedDisplayIds_orig =
|
||||
reinterpret_cast<decltype(NvAPI_GPU_GetConnectedDisplayIds) *>(
|
||||
NvAPI_QueryInterface_orig(func_code));
|
||||
NvAPI_DISP_GetGDIPrimaryDisplayId_orig =
|
||||
reinterpret_cast<decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *>(
|
||||
NvAPI_QueryInterface_orig(0x1E9D8A31));
|
||||
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_GPU_GetConnectedDisplayIds) - hooked");
|
||||
return (uintptr_t *)NvAPI_GPU_GetConnectedDisplayIds_hook;
|
||||
}
|
||||
|
||||
// NvAPI_DISP_SetDisplayConfig
|
||||
if (func_code == 0x5D8CF8DE) {
|
||||
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_DISP_SetDisplayConfig) - hooked");
|
||||
@@ -51,8 +71,53 @@ namespace nvapi_hook {
|
||||
return NvAPI_QueryInterface_orig(func_code);
|
||||
}
|
||||
|
||||
NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
|
||||
NvPhysicalGpuHandle hPhysicalGpu,
|
||||
NV_GPU_DISPLAYIDS *pDisplayIds,
|
||||
NvU32 *pDisplayIdCount,
|
||||
NvU32 flags) {
|
||||
|
||||
if (NvAPI_GPU_GetConnectedDisplayIds_orig == nullptr) {
|
||||
return NVAPI_NO_IMPLEMENTATION;
|
||||
}
|
||||
|
||||
const NvAPI_Status status = NvAPI_GPU_GetConnectedDisplayIds_orig(
|
||||
hPhysicalGpu,
|
||||
pDisplayIds,
|
||||
pDisplayIdCount,
|
||||
flags);
|
||||
|
||||
if (status != NVAPI_OK || pDisplayIds == nullptr || pDisplayIdCount == nullptr ||
|
||||
NvAPI_DISP_GetGDIPrimaryDisplayId_orig == nullptr) {
|
||||
return status;
|
||||
}
|
||||
|
||||
NvU32 primary_display_id = 0;
|
||||
if (NvAPI_DISP_GetGDIPrimaryDisplayId_orig(&primary_display_id) != NVAPI_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// report the primary display adapter as DP and all other displays as HDMI
|
||||
// (the game expects this since the VM cab is like that)
|
||||
//
|
||||
// this doesn't actually affect gameplay; only how
|
||||
// NvDisplayConfig: MainDisplay={}hz / SubDisplay={}hz
|
||||
// gets printed.
|
||||
//
|
||||
// for the actual value that matters for gameplay, it's this:
|
||||
// NvDisplayConfig: GetMainDisplayRefreshRate = {}hz
|
||||
for (NvU32 index = 0; index < *pDisplayIdCount; index++) {
|
||||
auto &display = pDisplayIds[index];
|
||||
display.connectorType = display.displayId == primary_display_id ?
|
||||
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
||||
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags) {
|
||||
// do not let the game apply its emulated cabinet topology to Windows
|
||||
log_misc("nvapi_hook", "NvAPI_DISP_SetDisplayConfig_hook - do nothing and return");
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
#include "nvapi_impl.h"
|
||||
|
||||
#ifdef SPICE64
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "external/nvapi/nvapi.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/sysutils.h"
|
||||
|
||||
namespace nvapi_impl {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828;
|
||||
constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F;
|
||||
constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E;
|
||||
constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F;
|
||||
constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2;
|
||||
constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31;
|
||||
constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8;
|
||||
constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE;
|
||||
|
||||
constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll";
|
||||
|
||||
struct SyntheticDisplay {
|
||||
NvU32 display_id;
|
||||
NvU32 width;
|
||||
NvU32 height;
|
||||
NvU32 color_depth;
|
||||
NvS32 x;
|
||||
NvS32 y;
|
||||
NvU32 refresh_rate_1k;
|
||||
NV_ROTATE rotation;
|
||||
bool primary;
|
||||
};
|
||||
|
||||
static bool provider_initialized = false;
|
||||
static bool nvapi_initialized = false;
|
||||
static int gpu_handle_storage = 0;
|
||||
// snapshot of the Win32 display state exposed through synthetic NVAPI
|
||||
static std::vector<SyntheticDisplay> displays;
|
||||
|
||||
static NvPhysicalGpuHandle get_gpu_handle() {
|
||||
return reinterpret_cast<NvPhysicalGpuHandle>(&gpu_handle_storage);
|
||||
}
|
||||
|
||||
static NV_ROTATE get_rotation(DWORD orientation) {
|
||||
switch (orientation) {
|
||||
case DMDO_90:
|
||||
return NV_ROTATE_90;
|
||||
case DMDO_180:
|
||||
return NV_ROTATE_180;
|
||||
case DMDO_270:
|
||||
return NV_ROTATE_270;
|
||||
default:
|
||||
return NV_ROTATE_0;
|
||||
}
|
||||
}
|
||||
|
||||
static std::vector<SyntheticDisplay> enumerate_displays(
|
||||
uint32_t main_refresh_hz,
|
||||
uint32_t sub_refresh_hz) {
|
||||
|
||||
std::vector<SyntheticDisplay> result;
|
||||
|
||||
// reuse the active monitor list, then read live modes after -mainmonitor changes
|
||||
for (const auto &monitor : sysutils::enumerate_monitors()) {
|
||||
DEVMODEA mode {};
|
||||
mode.dmSize = sizeof(mode);
|
||||
if (!EnumDisplaySettingsExA(
|
||||
monitor.display_name.c_str(),
|
||||
ENUM_CURRENT_SETTINGS,
|
||||
&mode,
|
||||
0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0;
|
||||
result.push_back({
|
||||
.display_id = 0,
|
||||
.width = mode.dmPelsWidth,
|
||||
.height = mode.dmPelsHeight,
|
||||
.color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32,
|
||||
.x = mode.dmPosition.x,
|
||||
.y = mode.dmPosition.y,
|
||||
.refresh_rate_1k = 0,
|
||||
.rotation = get_rotation(mode.dmDisplayOrientation),
|
||||
.primary = primary,
|
||||
});
|
||||
}
|
||||
|
||||
std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) {
|
||||
return left.primary && !right.primary;
|
||||
});
|
||||
|
||||
if (result.size() > 2) {
|
||||
result.resize(2);
|
||||
}
|
||||
|
||||
if (result.empty()) {
|
||||
result.push_back({
|
||||
.display_id = 0,
|
||||
.width = 1920,
|
||||
.height = 1080,
|
||||
.color_depth = 32,
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.refresh_rate_1k = 0,
|
||||
.rotation = NV_ROTATE_0,
|
||||
.primary = true,
|
||||
});
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < result.size(); index++) {
|
||||
auto &display = result[index];
|
||||
display.primary = index == 0;
|
||||
display.display_id = 0x80000000u | static_cast<NvU32>(index + 1);
|
||||
const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz;
|
||||
display.refresh_rate_1k = refresh_hz * 1000;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// initializes NVAPI for the calling process.
|
||||
// marks the synthetic provider initialized without contacting a driver.
|
||||
static NvAPI_Status __cdecl NvAPI_Initialize_impl() {
|
||||
log_misc("nvapi_impl", "NvAPI_Initialize");
|
||||
nvapi_initialized = true;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// initializes NVAPI with additional client flags.
|
||||
// accepts the flags and marks the synthetic provider initialized.
|
||||
static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) {
|
||||
log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags);
|
||||
nvapi_initialized = true;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// releases NVAPI state held for the calling process.
|
||||
// clears the synthetic initialization state while leaving the provider installed.
|
||||
static NvAPI_Status __cdecl NvAPI_Unload_impl() {
|
||||
log_misc("nvapi_impl", "NvAPI_Unload");
|
||||
nvapi_initialized = false;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// enumerates physical GPU handles managed by the NVIDIA driver.
|
||||
// returns one stable synthetic GPU containing all exposed displays.
|
||||
static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl(
|
||||
NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS],
|
||||
NvU32 *gpu_count) {
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_EnumPhysicalGPUs(handles={}, count={})",
|
||||
fmt::ptr(gpu_handles),
|
||||
fmt::ptr(gpu_count));
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (gpu_handles == nullptr || gpu_count == nullptr) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
gpu_handles[0] = get_gpu_handle();
|
||||
*gpu_count = 1;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_EnumPhysicalGPUs - gpu={}, count={}",
|
||||
fmt::ptr(gpu_handles[0]),
|
||||
*gpu_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// returns connected display descriptors for a physical GPU.
|
||||
// exposes the monitor snapshot as DP primary and HDMI secondary displays.
|
||||
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl(
|
||||
NvPhysicalGpuHandle gpu_handle,
|
||||
NV_GPU_DISPLAYIDS *display_ids,
|
||||
NvU32 *display_id_count,
|
||||
NvU32 flags) {
|
||||
|
||||
const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})",
|
||||
fmt::ptr(gpu_handle),
|
||||
fmt::ptr(display_ids),
|
||||
input_count,
|
||||
flags);
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (gpu_handle != get_gpu_handle()) {
|
||||
return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE;
|
||||
}
|
||||
if (display_id_count == nullptr) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const NvU32 required_count = static_cast<NvU32>(displays.size());
|
||||
if (display_ids == nullptr) {
|
||||
*display_id_count = required_count;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_GPU_GetConnectedDisplayIds - required_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
const NvU32 capacity = *display_id_count;
|
||||
*display_id_count = required_count;
|
||||
if (capacity < required_count) {
|
||||
return NVAPI_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
for (NvU32 index = 0; index < required_count; index++) {
|
||||
const auto &source = displays[index];
|
||||
auto &destination = display_ids[index];
|
||||
destination = {};
|
||||
destination.version = NV_GPU_DISPLAYIDS_VER;
|
||||
destination.connectorType = source.primary ?
|
||||
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
|
||||
destination.displayId = source.display_id;
|
||||
destination.isActive = 1;
|
||||
destination.isOSVisible = 1;
|
||||
destination.isConnected = 1;
|
||||
destination.isPhysicallyConnected = 1;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_GPU_GetConnectedDisplayIds - returned_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// returns the NVAPI display ID associated with the Windows GDI primary.
|
||||
// returns the first synthetic display, ordered from the live desktop origin.
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) {
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})",
|
||||
fmt::ptr(display_id));
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (display_id == nullptr || displays.empty()) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
*display_id = displays.front().display_id;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}",
|
||||
*display_id);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
static void fill_source_mode(
|
||||
NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination,
|
||||
const SyntheticDisplay &source) {
|
||||
|
||||
if (destination == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
*destination = {};
|
||||
destination->resolution.width = source.width;
|
||||
destination->resolution.height = source.height;
|
||||
destination->resolution.colorDepth = source.color_depth;
|
||||
destination->colorFormat = NV_FORMAT_A8R8G8B8;
|
||||
destination->position.x = source.x;
|
||||
destination->position.y = source.y;
|
||||
destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE;
|
||||
destination->bGDIPrimary = source.primary ? 1 : 0;
|
||||
}
|
||||
|
||||
static NvAPI_Status fill_target(
|
||||
NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination,
|
||||
const SyntheticDisplay &source,
|
||||
NvU32 target_id) {
|
||||
|
||||
if (destination == nullptr) {
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
auto *details = destination->details;
|
||||
destination->displayId = source.display_id;
|
||||
destination->targetId = target_id;
|
||||
|
||||
if (details == nullptr) {
|
||||
return NVAPI_OK;
|
||||
}
|
||||
if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) {
|
||||
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
|
||||
}
|
||||
|
||||
*details = {};
|
||||
details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER;
|
||||
details->rotation = source.rotation;
|
||||
details->scaling = NV_SCALING_DEFAULT;
|
||||
details->refreshRate1K = source.refresh_rate_1k;
|
||||
details->timingOverride = NV_TIMING_OVERRIDE_CURRENT;
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// retrieves the current global display topology through NVAPI's three-pass contract.
|
||||
// fills caller-owned buffers from the synthetic monitor snapshot and configured rates.
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl(
|
||||
NvU32 *path_info_count,
|
||||
NV_DISPLAYCONFIG_PATH_INFO *path_info) {
|
||||
|
||||
const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetDisplayConfig(count={}, paths={})",
|
||||
input_count,
|
||||
fmt::ptr(path_info));
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
if (path_info_count == nullptr) {
|
||||
return NVAPI_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const NvU32 required_count = static_cast<NvU32>(displays.size());
|
||||
if (path_info == nullptr) {
|
||||
*path_info_count = required_count;
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetDisplayConfig - required_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
const NvU32 capacity = *path_info_count;
|
||||
*path_info_count = required_count;
|
||||
if (capacity < required_count) {
|
||||
return NVAPI_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
for (NvU32 index = 0; index < required_count; index++) {
|
||||
auto &path = path_info[index];
|
||||
if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) {
|
||||
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
|
||||
}
|
||||
if (path.targetInfo != nullptr && path.targetInfoCount < 1) {
|
||||
return NVAPI_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
const auto &display = displays[index];
|
||||
path.sourceId = index;
|
||||
path.targetInfoCount = 1;
|
||||
path.IsNonNVIDIAAdapter = 0;
|
||||
path.pOSAdapterID = nullptr;
|
||||
fill_source_mode(path.sourceModeInfo, display);
|
||||
|
||||
const NvAPI_Status status = fill_target(path.targetInfo, display, index);
|
||||
if (status != NVAPI_OK) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_GetDisplayConfig - returned_count={}",
|
||||
required_count);
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
// applies a supplied global display topology through the NVIDIA driver.
|
||||
// accepts the cabinet topology without making any changes to Windows.
|
||||
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl(
|
||||
NvU32 path_info_count,
|
||||
NV_DISPLAYCONFIG_PATH_INFO *path_info,
|
||||
NvU32 flags) {
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})",
|
||||
path_info_count,
|
||||
fmt::ptr(path_info),
|
||||
flags);
|
||||
|
||||
if (!nvapi_initialized) {
|
||||
return NVAPI_API_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success");
|
||||
return NVAPI_OK;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static uintptr_t *query_result(T function) {
|
||||
return reinterpret_cast<uintptr_t *>(function);
|
||||
}
|
||||
|
||||
// resolves an NVAPI function ID to its implementation address.
|
||||
// exposes only the synthetic entry points used by KFC and rejects all others.
|
||||
static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) {
|
||||
uintptr_t *result = nullptr;
|
||||
switch (function_id) {
|
||||
case NVAPI_INITIALIZE_ID:
|
||||
result = query_result(NvAPI_Initialize_impl);
|
||||
break;
|
||||
case NVAPI_INITIALIZE_EX_ID:
|
||||
result = query_result(NvAPI_InitializeEx_impl);
|
||||
break;
|
||||
case NVAPI_UNLOAD_ID:
|
||||
result = query_result(NvAPI_Unload_impl);
|
||||
break;
|
||||
case NVAPI_ENUM_PHYSICAL_GPUS_ID:
|
||||
result = query_result(NvAPI_EnumPhysicalGPUs_impl);
|
||||
break;
|
||||
case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID:
|
||||
result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl);
|
||||
break;
|
||||
case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID:
|
||||
result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl);
|
||||
break;
|
||||
case NVAPI_DISP_GET_DISPLAY_CONFIG_ID:
|
||||
result = query_result(NvAPI_DISP_GetDisplayConfig_impl);
|
||||
break;
|
||||
case NVAPI_DISP_SET_DISPLAY_CONFIG_ID:
|
||||
result = query_result(NvAPI_DISP_SetDisplayConfig_impl);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
log_misc(
|
||||
"nvapi_impl",
|
||||
"NvAPI_QueryInterface(0x{:x}) - {}",
|
||||
function_id,
|
||||
result != nullptr ? "implemented" : "unsupported");
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) {
|
||||
if (provider_initialized) {
|
||||
return true;
|
||||
}
|
||||
if (dll == nullptr) {
|
||||
log_warning("nvapi_impl", "invalid synthetic module handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
displays = enumerate_displays(main_refresh_hz, sub_refresh_hz);
|
||||
libraryhook_hook_library(NVAPI_DLL_NAME_A, dll);
|
||||
libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl);
|
||||
libraryhook_enable();
|
||||
|
||||
provider_initialized = true;
|
||||
log_info(
|
||||
"nvapi_impl",
|
||||
"synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz",
|
||||
NVAPI_DLL_NAME_A,
|
||||
displays.size(),
|
||||
main_refresh_hz,
|
||||
sub_refresh_hz);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef SPICE64
|
||||
|
||||
#include <cstdint>
|
||||
#include <windows.h>
|
||||
|
||||
namespace nvapi_impl {
|
||||
|
||||
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -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"
|
||||
@@ -24,12 +26,13 @@ constexpr UINT CODEPAGE_SHIFT_JIS = 932;
|
||||
static decltype(GetACP) *GetACP_orig = nullptr;
|
||||
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
|
||||
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
|
||||
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
|
||||
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
|
||||
|
||||
#ifdef SPICE64
|
||||
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
|
||||
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
|
||||
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
|
||||
static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_orig = nullptr;
|
||||
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
|
||||
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
|
||||
#endif
|
||||
@@ -182,9 +185,32 @@ 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);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static
|
||||
int
|
||||
WINAPI
|
||||
@@ -220,6 +246,8 @@ WideCharToMultiByte_hook(
|
||||
lpUsedDefaultChar);
|
||||
}
|
||||
|
||||
#ifdef SPICE64
|
||||
|
||||
int
|
||||
WINAPI
|
||||
GetLocaleInfoA_hook(
|
||||
@@ -250,6 +278,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 +336,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 +347,25 @@ void hooks::lang::early_init() {
|
||||
&IsDBCSLeadByte_orig);
|
||||
}
|
||||
|
||||
if (games::gitadora::is_arena_model()) {
|
||||
if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) {
|
||||
detour::trampoline_try(
|
||||
"kernel32.dll",
|
||||
"IsDBCSLeadByteEx",
|
||||
IsDBCSLeadByteEx_hook,
|
||||
&IsDBCSLeadByteEx_orig);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef SPICE64
|
||||
const auto hook_wide_char_to_multi_byte =
|
||||
games::gitadora::is_arena_model() || avs::game::is_model("T44");
|
||||
#else
|
||||
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
|
||||
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
|
||||
#endif
|
||||
|
||||
if (hook_wide_char_to_multi_byte) {
|
||||
log_info("hooks::lang", "hooking WideCharToMultiByte");
|
||||
detour::trampoline_try(
|
||||
"kernel32.dll",
|
||||
@@ -314,7 +373,6 @@ void hooks::lang::early_init() {
|
||||
WideCharToMultiByte_hook,
|
||||
&WideCharToMultiByte_orig);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ static robin_hood::unordered_map<std::string, FARPROC> PROCS;
|
||||
|
||||
static decltype(LoadLibraryA) *LoadLibraryA_orig = nullptr;
|
||||
static decltype(LoadLibraryW) *LoadLibraryW_orig = nullptr;
|
||||
static decltype(LoadLibraryExW) *LoadLibraryExW_orig = nullptr;
|
||||
static decltype(GetModuleHandleA) *GetModuleHandleA_orig = nullptr;
|
||||
static decltype(GetModuleHandleW) *GetModuleHandleW_orig = nullptr;
|
||||
static decltype(GetProcAddress) *GetProcAddress_orig = nullptr;
|
||||
@@ -44,6 +45,20 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
|
||||
return LoadLibraryW_orig(lpFileName);
|
||||
}
|
||||
|
||||
static HMODULE WINAPI LoadLibraryExW_hook(LPCWSTR lpFileName, HANDLE hFile, DWORD dwFlags) {
|
||||
|
||||
// check hooks
|
||||
if (lpFileName) {
|
||||
auto module = LIBRARIES_W.find(lpFileName);
|
||||
if (module != LIBRARIES_W.end()) {
|
||||
return module->second;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback
|
||||
return LoadLibraryExW_orig(lpFileName, hFile, dwFlags);
|
||||
}
|
||||
|
||||
static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
|
||||
|
||||
// check hooks
|
||||
@@ -103,6 +118,7 @@ void libraryhook_enable(HMODULE module) {
|
||||
// detour
|
||||
detour::trampoline_try("kernel32.dll", "LoadLibraryA", LoadLibraryA_hook, &LoadLibraryA_orig);
|
||||
detour::trampoline_try("kernel32.dll", "LoadLibraryW", LoadLibraryW_hook, &LoadLibraryW_orig);
|
||||
detour::trampoline_try("kernel32.dll", "LoadLibraryExW", LoadLibraryExW_hook, &LoadLibraryExW_orig);
|
||||
detour::trampoline_try("kernel32.dll", "GetModuleHandleA", GetModuleHandleA_hook, &GetModuleHandleA_orig);
|
||||
detour::trampoline_try("kernel32.dll", "GetModuleHandleW", GetModuleHandleW_hook, &GetModuleHandleW_orig);
|
||||
detour::trampoline_try("kernel32.dll", "GetProcAddress", GetProcAddress_hook, &GetProcAddress_orig);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <assert.h>
|
||||
#include <shlwapi.h>
|
||||
#include <windows.h>
|
||||
#include <cfg/configurator.h>
|
||||
@@ -38,10 +37,10 @@
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/hpm/hpm.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "games/iidx/poke.h"
|
||||
#ifdef SPICE64
|
||||
#include "games/iidx/camera.h"
|
||||
#endif
|
||||
#include "games/iidx/poke.h"
|
||||
#include "games/jb/jb.h"
|
||||
#include "games/mga/mga.h"
|
||||
#include "games/nost/nost.h"
|
||||
@@ -88,13 +87,13 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "launcher/logger.h"
|
||||
#include "launcher/signal.h"
|
||||
#include "launcher/superexit.h"
|
||||
#include "launcher/richpresence.h"
|
||||
#include "launcher/shutdown.h"
|
||||
#include "launcher/options.h"
|
||||
#include "misc/bt5api.h"
|
||||
#include "misc/device.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/hotkeys.h"
|
||||
#include "misc/extdev.h"
|
||||
#include "misc/ami2000.h"
|
||||
#include "misc/sciunit.h"
|
||||
@@ -102,7 +101,7 @@
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "overlay/windows/patch_manager.h"
|
||||
#include "patcher/patch_manager.h"
|
||||
#include "overlay/windows/iidx_seg.h"
|
||||
#include "overlay/windows/obs.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
@@ -379,6 +378,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
}
|
||||
if (options[launcher::Options::spice2x_IIDXNoSub].value_bool()) {
|
||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
|
||||
}
|
||||
if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) {
|
||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||
@@ -428,9 +428,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
|
||||
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
|
||||
D3D9_DEVICE_HOOK_DISABLE = true;
|
||||
// touch emulation gets disabled, might as well turn these on
|
||||
games::iidx::NATIVE_TOUCH = true;
|
||||
games::sdvx::NATIVETOUCH = true;
|
||||
// not strictly necessary as it will fail to init anyway, but cleaner to just disable it now
|
||||
overlay::ENABLED = false;
|
||||
// leaving these on without dx9hooks result in torn state and therefore failure to draw
|
||||
@@ -490,9 +487,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::LoadSoundVoltexModule].value_bool()) {
|
||||
attach_sdvx = true;
|
||||
}
|
||||
if (options[launcher::Options::SDVXNativeTouch].value_bool()) {
|
||||
games::sdvx::NATIVETOUCH = true;
|
||||
}
|
||||
if (options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].is_active()) {
|
||||
games::sdvx::DIGITAL_KNOB_SENS = (uint8_t)
|
||||
options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].value_uint32();
|
||||
@@ -529,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
|
||||
|
||||
@@ -593,9 +590,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::spice2x_IIDXNoESpec].value_bool()) {
|
||||
games::iidx::DISABLE_ESPEC_IO = true;
|
||||
}
|
||||
if (options[launcher::Options::spice2x_IIDXNativeTouch].value_bool()) {
|
||||
games::iidx::NATIVE_TOUCH = true;
|
||||
}
|
||||
// should come later since this will override a few settings
|
||||
if (options[launcher::Options::spice2x_IIDXWindowedTDJ].value_bool() ||
|
||||
(options[launcher::Options::IIDXTDJMode].value_bool() && GRAPHICS_WINDOWED)) {
|
||||
@@ -643,9 +637,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::PopnSubMonitorOverride].is_active()) {
|
||||
sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text();
|
||||
}
|
||||
if (options[launcher::Options::PopnNativeTouch].value_bool()) {
|
||||
games::popn::NATIVE_TOUCH = true;
|
||||
}
|
||||
if (options[launcher::Options::PopnSubRedraw].value_bool()) {
|
||||
SUBSCREEN_FORCE_REDRAW = true;
|
||||
}
|
||||
@@ -655,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();
|
||||
}
|
||||
@@ -889,7 +883,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
options[launcher::Options::ScreenResizeConfigPath].value_text();
|
||||
}
|
||||
if (options[launcher::Options::PatchManagerConfigPath].is_active()) {
|
||||
overlay::windows::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
||||
patcher::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
||||
options[launcher::Options::PatchManagerConfigPath].value_text();
|
||||
}
|
||||
if (options[launcher::Options::PathToAppConfig].is_active()) {
|
||||
@@ -1109,6 +1103,12 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::ScreenshotFolder].is_active()) {
|
||||
GRAPHICS_SCREENSHOT_DIR = options[launcher::Options::ScreenshotFolder].value_text();
|
||||
}
|
||||
if (options[launcher::Options::ScreenshotIncludeOverlay].value_bool()) {
|
||||
GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = true;
|
||||
}
|
||||
if (options[launcher::Options::ScreenshotSubscreens].value_bool()) {
|
||||
GRAPHICS_SCREENSHOT_SUBSCREENS = true;
|
||||
}
|
||||
if (options[launcher::Options::DisableColoredOutput].value_bool()) {
|
||||
logger::COLOR = false;
|
||||
}
|
||||
@@ -1286,6 +1286,14 @@ int main_implementation(int argc, char *argv[]) {
|
||||
GRAPHICS_GITADORA_SMALL_MONITOR =
|
||||
options[launcher::Options::GitaDoraWindowedSmallMonitor].value_text();
|
||||
}
|
||||
if (options[launcher::Options::GitaDoraWindowedSmallSize].is_active()) {
|
||||
GRAPHICS_GITADORA_SMALL_SIZE =
|
||||
options[launcher::Options::GitaDoraWindowedSmallSize].value_text();
|
||||
}
|
||||
if (options[launcher::Options::GitaDoraWindowedSmallPosition].is_active()) {
|
||||
GRAPHICS_GITADORA_SMALL_POS =
|
||||
options[launcher::Options::GitaDoraWindowedSmallPosition].value_text();
|
||||
}
|
||||
|
||||
// IIDX/SDVX Windowed Subscreen
|
||||
if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) {
|
||||
@@ -1705,31 +1713,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) {
|
||||
@@ -1781,15 +1783,23 @@ int main_implementation(int argc, char *argv[]) {
|
||||
nvapi::initialize();
|
||||
// add application profile to nvcp
|
||||
nvapi::set_profile_settings();
|
||||
// enable super exit
|
||||
superexit::enable();
|
||||
|
||||
// keep ALT+F4 available during lengthy non-standalone boot
|
||||
hotkeys::start();
|
||||
// enable subscreen touch emulation
|
||||
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
|
||||
games::iidx::poke::enable();
|
||||
games::iidx::ENABLE_POKE = true;
|
||||
}
|
||||
if (options[launcher::Options::NostalgiaPoke].is_active()) {
|
||||
games::nost::ENABLE_POKE = TRUE;
|
||||
games::nost::ENABLE_POKE = true;
|
||||
}
|
||||
if (options[launcher::Options::NostalgiaTouchMode].is_active()) {
|
||||
if (overlay::ENABLED) {
|
||||
games::nost::ENABLE_TOUCH_MODE = true;
|
||||
} else {
|
||||
log_warning(
|
||||
"launcher",
|
||||
"Nostalgia Touch Mode requires the global overlay; ignoring -nosttouch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1849,6 +1859,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";
|
||||
@@ -2277,7 +2294,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) {
|
||||
@@ -2440,6 +2457,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
|
||||
// initialize raw input
|
||||
RI_MGR = std::make_unique<rawinput::RawInputManager>();
|
||||
hotkeys::enable_raw_input();
|
||||
for (const auto &device : sextet_devices) {
|
||||
RI_MGR->sextet_register(device);
|
||||
}
|
||||
@@ -2462,6 +2480,9 @@ int main_implementation(int argc, char *argv[]) {
|
||||
log_misc("rawinput", "Analog mappings:");
|
||||
dump_analog_bindings();
|
||||
|
||||
// mappings are ready; begin screenshot and coin polling during late startup
|
||||
hotkeys::enable_input();
|
||||
|
||||
// for certain games, show cursor if no touch is available (must be called after RI_MGR is available)
|
||||
if (show_cursor_if_no_touch && !is_touch_available("launcher::main_implementation")) {
|
||||
GRAPHICS_SHOW_CURSOR = true;
|
||||
@@ -2517,10 +2538,8 @@ int main_implementation(int argc, char *argv[]) {
|
||||
// copy defaults to nvram
|
||||
avs::core::copy_defaults();
|
||||
|
||||
// prepare patches
|
||||
{
|
||||
overlay::windows::PatchManager patch_manager(nullptr, false);
|
||||
}
|
||||
// prepare patches (registers the DLL-load notification before the game DLL loads)
|
||||
patcher::init();
|
||||
|
||||
// load game
|
||||
avs::game::load_dll();
|
||||
@@ -2648,9 +2667,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
// apply patches
|
||||
{
|
||||
overlay::windows::PatchManager patch_manager(nullptr, true);
|
||||
}
|
||||
patcher::apply_patches_on_start();
|
||||
|
||||
// load AVS-EA3
|
||||
avs::ea3::boot(easrv_port, easrv_maint, easrv_smart);
|
||||
@@ -2695,9 +2712,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
|
||||
}
|
||||
|
||||
// start coin input thread
|
||||
eamuse_coin_start_thread();
|
||||
|
||||
// pin macro
|
||||
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
|
||||
eamuse_pin_macro_start_thread();
|
||||
@@ -2747,6 +2761,9 @@ int main_implementation(int argc, char *argv[]) {
|
||||
log_info("launcher", "calling game entry");
|
||||
avs::game::entry_main();
|
||||
|
||||
// stop screenshot and coin polling; mapped SuperExit and ALT+F4 remain active
|
||||
hotkeys::disable_input();
|
||||
|
||||
// clear presence
|
||||
richpresence::shutdown();
|
||||
|
||||
@@ -2784,9 +2801,6 @@ int main_implementation(int argc, char *argv[]) {
|
||||
// free api controller
|
||||
API_CONTROLLER.reset();
|
||||
|
||||
// stop coin input thread
|
||||
eamuse_coin_stop_thread();
|
||||
|
||||
eamuse_pin_macro_stop_thread();
|
||||
|
||||
// BT5API
|
||||
@@ -2797,6 +2811,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
sdk::fini_sdk_modules();
|
||||
|
||||
// stop raw input
|
||||
hotkeys::disable_raw_input();
|
||||
RI_MGR.reset();
|
||||
|
||||
// debug hook
|
||||
@@ -2829,8 +2844,8 @@ int main_implementation(int argc, char *argv[]) {
|
||||
// dispose crypt
|
||||
crypt::dispose();
|
||||
|
||||
// disable super exit
|
||||
superexit::disable();
|
||||
// end early/late ALT+F4 monitoring at the same teardown point as legacy SuperExit
|
||||
hotkeys::stop();
|
||||
|
||||
// disable poke
|
||||
games::iidx::poke::disable();
|
||||
|
||||
@@ -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", ""}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -962,13 +965,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Cab Peripherals",
|
||||
},
|
||||
{
|
||||
.title = "SDVX FS Subscreen Native Touch Handling",
|
||||
.title = "SDVX Native Touch (DEPRECATED - no longer needed)",
|
||||
.name = "sdvxnativetouch",
|
||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
||||
"Requires a touch screen to be connected as a secondary monitor. "
|
||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
||||
.desc = "This option does nothing.\n\n"
|
||||
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.game_name = "Sound Voltex",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
@@ -979,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",
|
||||
@@ -1159,13 +1161,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// PopnNativeTouch
|
||||
.title = "Pop'n Music PikaPika Native Touch Handling",
|
||||
.title = "Pop'n Music Native Touch (DEPRECATED - no longer needed)",
|
||||
.name = "popnnativetouch",
|
||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
||||
"Requires a touch screen to be connected as a secondary monitor. "
|
||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
||||
.desc = "This option does nothing.\n\n"
|
||||
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.game_name = "Pop'n Music",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
@@ -1174,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",
|
||||
@@ -1205,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",
|
||||
@@ -1301,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,
|
||||
@@ -1793,10 +1809,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Touch Parameters",
|
||||
},
|
||||
{
|
||||
.title = "Force Touch Emulation",
|
||||
.title = "Force Legacy Touch Emulation",
|
||||
.name = "touchemuforce",
|
||||
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
|
||||
"This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.",
|
||||
"Do not enable this unless you have trouble with using a mouse to emulate touch input.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "Touch Parameters",
|
||||
},
|
||||
@@ -2167,7 +2183,6 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
{"25", "+25 dB"},
|
||||
{"30", "+30 dB"},
|
||||
},
|
||||
.quick_setting_category = "Audio",
|
||||
},
|
||||
{
|
||||
// AudioResample
|
||||
@@ -2644,6 +2659,26 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Game Windowed Settings",
|
||||
.picker = OptionPickerType::Monitor,
|
||||
},
|
||||
{
|
||||
// GitaDoraWindowedSmallSize
|
||||
.title = "GitaDora Windowed SMALL Size",
|
||||
.name = "gdwsmallsize",
|
||||
.desc = "Size of the GITADORA Arena SMALL touch window. Defaults to (800,1280).",
|
||||
.type = OptionType::Text,
|
||||
.setting_name = "800,1280",
|
||||
.game_name = "GitaDora",
|
||||
.category = "Game Windowed Settings",
|
||||
},
|
||||
{
|
||||
// GitaDoraWindowedSmallPosition
|
||||
.title = "GitaDora Windowed SMALL Position",
|
||||
.name = "gdwsmallpos",
|
||||
.desc = "Initial position of the GITADORA Arena SMALL touch window. Defaults to (0,0).",
|
||||
.type = OptionType::Text,
|
||||
.setting_name = "0,0",
|
||||
.game_name = "GitaDora",
|
||||
.category = "Game Windowed Settings",
|
||||
},
|
||||
{
|
||||
// spice2x_IIDXWindowedSubscreenSize
|
||||
.title = "IIDX Windowed Subscreen Size",
|
||||
@@ -2870,15 +2905,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
},
|
||||
{
|
||||
// spice2x_IIDXNativeTouch
|
||||
.title = "IIDX TDJ Subscreen Native Touch Handling",
|
||||
.title = "IIDX Native Touch (DEPRECATED - no longer needed)",
|
||||
.name = "sp2x-iidxnativetouch",
|
||||
.display_name = "iidxnativetouch",
|
||||
.aliases= "iidxnativetouch",
|
||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
||||
"Requires a touch screen to be connected as a secondary monitor. "
|
||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
||||
.desc = "This option does nothing.\n\n"
|
||||
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||
.type = OptionType::Bool,
|
||||
.hidden = true,
|
||||
.game_name = "Beatmania IIDX",
|
||||
.category = "Advanced Game Options",
|
||||
},
|
||||
@@ -2995,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
|
||||
@@ -3132,6 +3166,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "Nostalgia",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// NostalgiaTouchMode
|
||||
.title = "Nostalgia Touch Piano",
|
||||
.name = "nosttouch",
|
||||
.desc =
|
||||
"Allows you to play the piano by touching the screen instead of a controller. "
|
||||
"Use the mode switch button to toggle between interacting with the menu and playing the piano.\n\n"
|
||||
"Velocity sensitivity is not supported. Touch targets will be pixel-perfect aligned with the "
|
||||
"judgement line, key beams, and notes.",
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "Nostalgia",
|
||||
.category = "Game Options",
|
||||
.quick_setting_category = "Game",
|
||||
},
|
||||
{
|
||||
// ForceBackBufferCount
|
||||
@@ -3348,6 +3397,22 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.category = "OBS Control",
|
||||
},
|
||||
{
|
||||
// ScreenshotIncludeOverlay
|
||||
.title = "Include Overlay in Screenshots",
|
||||
.name = "screenshotoverlay",
|
||||
.desc = "Includes Spice overlay in screenshots.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "General Overlay",
|
||||
},
|
||||
{
|
||||
// ScreenshotSubscreens
|
||||
.title = "Include Subscreens in Screenshots",
|
||||
.name = "screenshotsub",
|
||||
.desc = "Saves each subscreen as a separate PNG alongside the primary screenshot.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "General Overlay",
|
||||
},
|
||||
};
|
||||
|
||||
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
||||
|
||||
@@ -114,6 +114,7 @@ namespace launcher {
|
||||
LoadHelloPopnMusicModule,
|
||||
LoadGitaDoraModule,
|
||||
GitaDoraTwoChannelAudio,
|
||||
GitaDoraDisableFrameLimiter,
|
||||
GitaDoraCabinetType,
|
||||
GitaDoraLefty,
|
||||
GitaDoraWailHold,
|
||||
@@ -258,6 +259,8 @@ namespace launcher {
|
||||
GitaDoraWindowedLeftMonitor,
|
||||
GitaDoraWindowedRightMonitor,
|
||||
GitaDoraWindowedSmallMonitor,
|
||||
GitaDoraWindowedSmallSize,
|
||||
GitaDoraWindowedSmallPosition,
|
||||
spice2x_IIDXWindowedSubscreenSize,
|
||||
spice2x_IIDXWindowedSubscreenPosition,
|
||||
IIDXWindowedSubscreenBorderless,
|
||||
@@ -295,6 +298,7 @@ namespace launcher {
|
||||
DDRP4IOBufferMode,
|
||||
InputRequiresFocus,
|
||||
NostalgiaPoke,
|
||||
NostalgiaTouchMode,
|
||||
ForceBackBufferCount,
|
||||
SDVXWindowedSubscreenSize,
|
||||
SDVXWindowedSubscreenPosition,
|
||||
@@ -316,7 +320,9 @@ namespace launcher {
|
||||
OBSWebSocketHost,
|
||||
OBSWebSocketPort,
|
||||
OBSWebSocketPassword,
|
||||
OBSWebSocketDebug
|
||||
OBSWebSocketDebug,
|
||||
ScreenshotIncludeOverlay,
|
||||
ScreenshotSubscreens
|
||||
};
|
||||
|
||||
enum class OptionsCategory {
|
||||
|
||||
+401
-45
@@ -1,5 +1,6 @@
|
||||
#include "signal.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <future>
|
||||
#include <exception>
|
||||
|
||||
@@ -25,6 +26,7 @@
|
||||
#endif
|
||||
|
||||
static decltype(MiniDumpWriteDump) *MiniDumpWriteDump_local = nullptr;
|
||||
static LONG EXCEPTION_HANDLER_ACTIVE = 0;
|
||||
|
||||
namespace launcher::signal {
|
||||
|
||||
@@ -47,7 +49,7 @@ static std::string control_code(DWORD dwCtrlType) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||
static std::string exception_code(const struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||
switch (ExceptionRecord->ExceptionCode) {
|
||||
V(EXCEPTION_ACCESS_VIOLATION);
|
||||
V(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
|
||||
@@ -77,6 +79,388 @@ static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
|
||||
|
||||
#undef V
|
||||
|
||||
static const char *access_operation(ULONG_PTR operation) {
|
||||
switch (operation) {
|
||||
case 0:
|
||||
return "read";
|
||||
case 1:
|
||||
return "write";
|
||||
case 8:
|
||||
return "execute";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *memory_state(DWORD state) {
|
||||
switch (state) {
|
||||
case MEM_COMMIT:
|
||||
return "committed";
|
||||
case MEM_FREE:
|
||||
return "free";
|
||||
case MEM_RESERVE:
|
||||
return "reserved";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *memory_type(DWORD type) {
|
||||
switch (type) {
|
||||
case MEM_IMAGE:
|
||||
return "image";
|
||||
case MEM_MAPPED:
|
||||
return "mapped";
|
||||
case MEM_PRIVATE:
|
||||
return "private";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *memory_protection(DWORD protection) {
|
||||
switch (protection & 0xff) {
|
||||
case PAGE_EXECUTE:
|
||||
return "execute";
|
||||
case PAGE_EXECUTE_READ:
|
||||
return "execute-read";
|
||||
case PAGE_EXECUTE_READWRITE:
|
||||
return "execute-read-write";
|
||||
case PAGE_EXECUTE_WRITECOPY:
|
||||
return "execute-write-copy";
|
||||
case PAGE_NOACCESS:
|
||||
return "no-access";
|
||||
case PAGE_READONLY:
|
||||
return "read-only";
|
||||
case PAGE_READWRITE:
|
||||
return "read-write";
|
||||
case PAGE_WRITECOPY:
|
||||
return "write-copy";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
static void log_exception_parameters(const struct _EXCEPTION_RECORD *record) {
|
||||
const auto parameter_count = record->NumberParameters <= EXCEPTION_MAXIMUM_PARAMETERS ?
|
||||
record->NumberParameters : EXCEPTION_MAXIMUM_PARAMETERS;
|
||||
|
||||
log_warning("signal", "exception flags: 0x{:08x}, parameters: {}",
|
||||
record->ExceptionFlags, parameter_count);
|
||||
|
||||
for (DWORD parameter = 0; parameter < parameter_count; parameter++) {
|
||||
#ifdef _WIN64
|
||||
log_warning("signal", "exception parameter[{}]: {:016x}",
|
||||
parameter, record->ExceptionInformation[parameter]);
|
||||
#else
|
||||
log_warning("signal", "exception parameter[{}]: {:08x}",
|
||||
parameter, record->ExceptionInformation[parameter]);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static bool query_memory(const void *address, MEMORY_BASIC_INFORMATION *memory) {
|
||||
return VirtualQuery(address, memory, sizeof(*memory)) == sizeof(*memory);
|
||||
}
|
||||
|
||||
static void log_memory_information(const char *label, const MEMORY_BASIC_INFORMATION &memory) {
|
||||
log_warning("signal",
|
||||
"{}: base={}, size=0x{:x}, allocation_base={}, state={} (0x{:x}), "
|
||||
"protect={} (0x{:x}), type={} (0x{:x})",
|
||||
label,
|
||||
fmt::ptr(memory.BaseAddress),
|
||||
memory.RegionSize,
|
||||
fmt::ptr(memory.AllocationBase),
|
||||
memory_state(memory.State),
|
||||
memory.State,
|
||||
memory_protection(memory.Protect),
|
||||
memory.Protect,
|
||||
memory_type(memory.Type),
|
||||
memory.Type);
|
||||
}
|
||||
|
||||
static void log_memory_region(const char *label, const void *address) {
|
||||
MEMORY_BASIC_INFORMATION memory {};
|
||||
if (!query_memory(address, &memory)) {
|
||||
log_warning("signal", "{}: VirtualQuery failed for {}: 0x{:08x}",
|
||||
label, fmt::ptr(address), GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
log_memory_information(label, memory);
|
||||
}
|
||||
|
||||
static void log_adjacent_memory_regions(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION current {};
|
||||
if (!query_memory(address, ¤t)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto current_base = reinterpret_cast<uintptr_t>(current.BaseAddress);
|
||||
if (current_base > 0) {
|
||||
MEMORY_BASIC_INFORMATION previous {};
|
||||
if (query_memory(reinterpret_cast<const void *>(current_base - 1), &previous)) {
|
||||
log_memory_information("fault memory previous", previous);
|
||||
}
|
||||
}
|
||||
|
||||
const auto current_end = current_base + current.RegionSize;
|
||||
if (current_end > current_base) {
|
||||
MEMORY_BASIC_INFORMATION next {};
|
||||
if (query_memory(reinterpret_cast<const void *>(current_end), &next)) {
|
||||
log_memory_information("fault memory next", next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void log_exception_module(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION memory {};
|
||||
if (!query_memory(address, &memory) || memory.Type != MEM_IMAGE || memory.AllocationBase == nullptr) {
|
||||
log_warning("signal", "exception module: unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
char module_path[MAX_PATH] {};
|
||||
const auto module = static_cast<HMODULE>(memory.AllocationBase);
|
||||
const auto module_base = reinterpret_cast<uintptr_t>(memory.AllocationBase);
|
||||
const auto exception_address = reinterpret_cast<uintptr_t>(address);
|
||||
const auto module_length = GetModuleFileNameA(module, module_path, sizeof(module_path));
|
||||
|
||||
if (module_length > 0) {
|
||||
const std::string module_name(module_path, std::min<size_t>(module_length, sizeof(module_path)));
|
||||
log_warning("signal", "exception module: '{}' base={}, rva=0x{:x}",
|
||||
module_name, fmt::ptr(memory.AllocationBase), exception_address - module_base);
|
||||
} else {
|
||||
log_warning("signal", "exception module: base={}, rva=0x{:x}",
|
||||
fmt::ptr(memory.AllocationBase), exception_address - module_base);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_instruction_bytes(const void *address) {
|
||||
MEMORY_BASIC_INFORMATION memory {};
|
||||
if (!query_memory(address, &memory) ||
|
||||
memory.State != MEM_COMMIT ||
|
||||
(memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0) {
|
||||
log_warning("signal", "instruction bytes: unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr size_t max_instruction_bytes = 16;
|
||||
const auto region_end = reinterpret_cast<uintptr_t>(memory.BaseAddress) + memory.RegionSize;
|
||||
const auto instruction_address = reinterpret_cast<uintptr_t>(address);
|
||||
const auto available = region_end > instruction_address ? region_end - instruction_address : 0;
|
||||
const auto byte_count = std::min(max_instruction_bytes, static_cast<size_t>(available));
|
||||
uint8_t bytes[max_instruction_bytes] {};
|
||||
SIZE_T bytes_read = 0;
|
||||
if (byte_count == 0 ||
|
||||
!ReadProcessMemory(GetCurrentProcess(), address, bytes, byte_count, &bytes_read)) {
|
||||
log_warning("signal", "instruction bytes: read failed at {}: 0x{:08x}",
|
||||
fmt::ptr(address), GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string byte_string;
|
||||
byte_string.reserve(bytes_read * 3);
|
||||
|
||||
for (size_t index = 0; index < bytes_read; index++) {
|
||||
fmt::format_to(std::back_inserter(byte_string), "{:02x}{}",
|
||||
bytes[index], index + 1 < bytes_read ? " " : "");
|
||||
}
|
||||
|
||||
log_warning("signal", "instruction bytes: {}", byte_string);
|
||||
}
|
||||
|
||||
static void log_stack_context(const CONTEXT *context) {
|
||||
const auto *tib = reinterpret_cast<const NT_TIB *>(NtCurrentTeb());
|
||||
const auto stack_base = reinterpret_cast<uintptr_t>(tib->StackBase);
|
||||
const auto stack_limit = reinterpret_cast<uintptr_t>(tib->StackLimit);
|
||||
#ifdef _WIN64
|
||||
const auto stack_pointer = static_cast<uintptr_t>(context->Rsp);
|
||||
#else
|
||||
const auto stack_pointer = static_cast<uintptr_t>(context->Esp);
|
||||
#endif
|
||||
const auto stack_used = stack_base > stack_pointer ? stack_base - stack_pointer : 0;
|
||||
const auto stack_remaining = stack_pointer > stack_limit ? stack_pointer - stack_limit : 0;
|
||||
|
||||
log_warning("signal", "stack: base={}, limit={}, pointer={}, used=0x{:x}, remaining=0x{:x}",
|
||||
fmt::ptr(tib->StackBase),
|
||||
fmt::ptr(tib->StackLimit),
|
||||
fmt::ptr(reinterpret_cast<const void *>(stack_pointer)),
|
||||
stack_used,
|
||||
stack_remaining);
|
||||
}
|
||||
|
||||
static void log_exception_context(struct _EXCEPTION_POINTERS *ExceptionInfo, DWORD last_error) {
|
||||
const auto *record = ExceptionInfo->ExceptionRecord;
|
||||
const auto *context = ExceptionInfo->ContextRecord;
|
||||
|
||||
log_warning("signal", "thread id: {}", GetCurrentThreadId());
|
||||
log_warning("signal", "thread last error (GetLastError()) at exception: 0x{:08x}", last_error);
|
||||
log_warning("signal", "exception address: {}", fmt::ptr(record->ExceptionAddress));
|
||||
log_exception_module(record->ExceptionAddress);
|
||||
log_instruction_bytes(record->ExceptionAddress);
|
||||
log_exception_parameters(record);
|
||||
|
||||
if ((record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION ||
|
||||
record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR) &&
|
||||
record->NumberParameters >= 2) {
|
||||
const auto operation = record->ExceptionInformation[0];
|
||||
const auto address = reinterpret_cast<const void *>(record->ExceptionInformation[1]);
|
||||
log_warning("signal", "invalid memory access: {} at {}",
|
||||
access_operation(operation), fmt::ptr(address));
|
||||
log_memory_region("fault memory", address);
|
||||
log_adjacent_memory_regions(address);
|
||||
}
|
||||
|
||||
if (record->ExceptionCode == EXCEPTION_IN_PAGE_ERROR && record->NumberParameters >= 3) {
|
||||
log_warning("signal", "in-page error status: 0x{:08x}",
|
||||
static_cast<DWORD>(record->ExceptionInformation[2]));
|
||||
}
|
||||
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_stack_context(context);
|
||||
|
||||
#ifdef _WIN64
|
||||
log_warning("signal", "registers: rax={:016x} rbx={:016x} rcx={:016x} rdx={:016x}",
|
||||
context->Rax, context->Rbx, context->Rcx, context->Rdx);
|
||||
log_warning("signal", "registers: rsi={:016x} rdi={:016x} rbp={:016x} rsp={:016x}",
|
||||
context->Rsi, context->Rdi, context->Rbp, context->Rsp);
|
||||
log_warning("signal", "registers: r8={:016x} r9={:016x} r10={:016x} r11={:016x}",
|
||||
context->R8, context->R9, context->R10, context->R11);
|
||||
log_warning("signal", "registers: r12={:016x} r13={:016x} r14={:016x} r15={:016x}",
|
||||
context->R12, context->R13, context->R14, context->R15);
|
||||
log_warning("signal", "registers: rip={:016x} eflags={:08x}",
|
||||
context->Rip, context->EFlags);
|
||||
#else
|
||||
log_warning("signal", "registers: eax={:08x} ebx={:08x} ecx={:08x} edx={:08x}",
|
||||
context->Eax, context->Ebx, context->Ecx, context->Edx);
|
||||
log_warning("signal", "registers: esi={:08x} edi={:08x} ebp={:08x} esp={:08x}",
|
||||
context->Esi, context->Edi, context->Ebp, context->Esp);
|
||||
log_warning("signal", "registers: eip={:08x} eflags={:08x}",
|
||||
context->Eip, context->EFlags);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void write_minidump(struct _EXCEPTION_POINTERS *ExceptionInfo) {
|
||||
if (MiniDumpWriteDump_local == nullptr) {
|
||||
log_warning("signal", "minidump creation function not available, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
HANDLE minidump_file = CreateFileA(
|
||||
"minidump.dmp",
|
||||
GENERIC_WRITE,
|
||||
0,
|
||||
nullptr,
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
|
||||
if (minidump_file == INVALID_HANDLE_VALUE) {
|
||||
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
|
||||
GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
|
||||
ExceptionParam.ThreadId = GetCurrentThreadId();
|
||||
ExceptionParam.ExceptionPointers = ExceptionInfo;
|
||||
ExceptionParam.ClientPointers = FALSE;
|
||||
|
||||
constexpr auto extended_type = static_cast<MINIDUMP_TYPE>(
|
||||
MiniDumpWithUnloadedModules |
|
||||
MiniDumpWithIndirectlyReferencedMemory |
|
||||
MiniDumpWithProcessThreadData |
|
||||
MiniDumpWithFullMemoryInfo |
|
||||
MiniDumpWithThreadInfo |
|
||||
MiniDumpIgnoreInaccessibleMemory);
|
||||
|
||||
auto written_type = extended_type;
|
||||
auto minidump_written = MiniDumpWriteDump_local(
|
||||
GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
minidump_file,
|
||||
extended_type,
|
||||
&ExceptionParam,
|
||||
nullptr,
|
||||
nullptr);
|
||||
auto minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
|
||||
|
||||
if (!minidump_written) {
|
||||
log_warning("signal", "failed to write extended minidump: 0x{:08x}; retrying MiniDumpNormal",
|
||||
minidump_error);
|
||||
|
||||
LARGE_INTEGER file_start {};
|
||||
if (SetFilePointerEx(minidump_file, file_start, nullptr, FILE_BEGIN) &&
|
||||
SetEndOfFile(minidump_file)) {
|
||||
written_type = MiniDumpNormal;
|
||||
minidump_written = MiniDumpWriteDump_local(
|
||||
GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
minidump_file,
|
||||
MiniDumpNormal,
|
||||
&ExceptionParam,
|
||||
nullptr,
|
||||
nullptr);
|
||||
minidump_error = minidump_written ? ERROR_SUCCESS : GetLastError();
|
||||
} else {
|
||||
minidump_error = GetLastError();
|
||||
}
|
||||
}
|
||||
|
||||
LARGE_INTEGER minidump_size {};
|
||||
const auto minidump_size_available = minidump_written &&
|
||||
GetFileSizeEx(minidump_file, &minidump_size);
|
||||
CloseHandle(minidump_file);
|
||||
|
||||
if (minidump_written) {
|
||||
if (minidump_size_available) {
|
||||
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x}, size={} bytes)",
|
||||
static_cast<unsigned>(written_type), minidump_size.QuadPart);
|
||||
} else {
|
||||
log_info("signal", "wrote minidump to 'minidump.dmp' (type=0x{:08x})",
|
||||
static_cast<unsigned>(written_type));
|
||||
}
|
||||
} else {
|
||||
log_warning("signal", "failed to write 'minidump.dmp': 0x{:08x}", minidump_error);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_exception_chain(const struct _EXCEPTION_RECORD *record) {
|
||||
constexpr size_t max_exception_chain_depth = 8;
|
||||
auto *record_cause = record->ExceptionRecord;
|
||||
size_t depth = 0;
|
||||
|
||||
while (record_cause != nullptr && depth < max_exception_chain_depth) {
|
||||
struct _EXCEPTION_RECORD cause {};
|
||||
SIZE_T bytes_read = 0;
|
||||
if (!ReadProcessMemory(
|
||||
GetCurrentProcess(),
|
||||
record_cause,
|
||||
&cause,
|
||||
sizeof(cause),
|
||||
&bytes_read) ||
|
||||
bytes_read != sizeof(cause)) {
|
||||
log_warning("signal", "failed to read exception chain record {} at {}: 0x{:08x}",
|
||||
depth, fmt::ptr(record_cause), GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
log_warning("signal", "caused by: {} at {}",
|
||||
exception_code(&cause), fmt::ptr(cause.ExceptionAddress));
|
||||
log_exception_parameters(&cause);
|
||||
record_cause = cause.ExceptionRecord;
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (record_cause != nullptr) {
|
||||
log_warning("signal", "exception chain truncated after {} records", max_exception_chain_depth);
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
|
||||
log_info("signal", "console ctrl handler called: {}", control_code(dwCtrlType));
|
||||
|
||||
@@ -90,15 +474,23 @@ static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
|
||||
}
|
||||
|
||||
static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) {
|
||||
const auto last_error = GetLastError();
|
||||
|
||||
// ignore signal if disabled or no exception info provided
|
||||
if (!launcher::signal::DISABLE && ExceptionInfo != nullptr) {
|
||||
if (!launcher::signal::DISABLE &&
|
||||
ExceptionInfo != nullptr &&
|
||||
ExceptionInfo->ExceptionRecord != nullptr &&
|
||||
InterlockedCompareExchange(&EXCEPTION_HANDLER_ACTIVE, 1, 0) == 0) {
|
||||
|
||||
// get exception record
|
||||
struct _EXCEPTION_RECORD *ExceptionRecord = ExceptionInfo->ExceptionRecord;
|
||||
|
||||
// print signal
|
||||
log_warning("signal", "exception raised: {}", exception_code(ExceptionRecord));
|
||||
log_exception_context(ExceptionInfo, last_error);
|
||||
|
||||
// write the minidump before other diagnostics inspect potentially damaged state
|
||||
write_minidump(ExceptionInfo);
|
||||
|
||||
switch (ExceptionRecord->ExceptionCode) {
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
@@ -128,12 +520,11 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
|
||||
// (e.g., ACIO init hang due to RTSS)
|
||||
deferredlogs::dump_to_logger(true);
|
||||
|
||||
// walk the exception chain
|
||||
struct _EXCEPTION_RECORD *record_cause = ExceptionRecord->ExceptionRecord;
|
||||
while (record_cause != nullptr) {
|
||||
log_warning("signal", "caused by: {}", exception_code(record_cause));
|
||||
record_cause = record_cause->ExceptionRecord;
|
||||
}
|
||||
// dump memory information before inspecting potentially damaged exception and stack data
|
||||
memutils::show_available_memory();
|
||||
|
||||
// walk the exception chain with bounded, checked reads
|
||||
log_exception_chain(ExceptionRecord);
|
||||
|
||||
// print stacktrace
|
||||
StackWalker sw;
|
||||
@@ -142,47 +533,12 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
|
||||
log_warning("signal", "failed to print callstack");
|
||||
}
|
||||
|
||||
if (MiniDumpWriteDump_local != nullptr) {
|
||||
HANDLE minidump_file = CreateFileA(
|
||||
"minidump.dmp",
|
||||
GENERIC_WRITE,
|
||||
0,
|
||||
nullptr,
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
|
||||
if (minidump_file != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
|
||||
ExceptionParam.ThreadId = GetCurrentThreadId();
|
||||
ExceptionParam.ExceptionPointers = ExceptionInfo;
|
||||
ExceptionParam.ClientPointers = FALSE;
|
||||
|
||||
MiniDumpWriteDump_local(
|
||||
GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
minidump_file,
|
||||
MiniDumpNormal,
|
||||
&ExceptionParam,
|
||||
nullptr,
|
||||
nullptr);
|
||||
|
||||
CloseHandle(minidump_file);
|
||||
} else {
|
||||
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
|
||||
GetLastError());
|
||||
}
|
||||
} else {
|
||||
log_warning("signal", "minidump creation function not available, skipping");
|
||||
}
|
||||
|
||||
// dump memory information
|
||||
memutils::show_available_memory();
|
||||
|
||||
// this will stall all UI threads for this process
|
||||
show_popup_for_crash();
|
||||
|
||||
log_fatal("signal", "end");
|
||||
|
||||
InterlockedExchange(&EXCEPTION_HANDLER_ACTIVE, 0);
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
#include "superexit.h"
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include "windows.h"
|
||||
|
||||
#include "cfg/configurator.h"
|
||||
#include "launcher/shutdown.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/logging.h"
|
||||
#include "touch/touch.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "games/io.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace superexit {
|
||||
|
||||
static std::thread *THREAD = nullptr;
|
||||
static bool THREAD_RUNNING = false;
|
||||
|
||||
bool has_focus() {
|
||||
HWND fg_wnd = GetForegroundWindow();
|
||||
if (fg_wnd == NULL) {
|
||||
@@ -29,92 +22,22 @@ namespace superexit {
|
||||
return fg_pid == GetCurrentProcessId();
|
||||
}
|
||||
|
||||
void enable() {
|
||||
|
||||
// check if already running
|
||||
if (THREAD)
|
||||
return;
|
||||
|
||||
// create new thread
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
|
||||
// log
|
||||
log_info("superexit", "enabled");
|
||||
|
||||
// set variable to false to stop
|
||||
while (THREAD_RUNNING) {
|
||||
|
||||
// check rawinput for ALT+F4
|
||||
bool rawinput_exit = false;
|
||||
if (RI_MGR != nullptr) {
|
||||
auto devices = RI_MGR->devices_get();
|
||||
for (auto &device : devices) {
|
||||
switch (device.type) {
|
||||
case rawinput::KEYBOARD: {
|
||||
auto &key_states = device.keyboardInfo->key_states;
|
||||
for (int page_index = 0; page_index < 1024; page_index += 256) {
|
||||
if (key_states[page_index + VK_MENU]
|
||||
&& key_states[page_index + VK_F4]) {
|
||||
rawinput_exit = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool async_key_exit =
|
||||
(GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
|
||||
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
|
||||
|
||||
bool overlay_exit = false;
|
||||
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||
if (buttons &&
|
||||
(!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
|
||||
GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::SuperExit))) {
|
||||
overlay_exit = true;
|
||||
}
|
||||
|
||||
// check for exit
|
||||
if (rawinput_exit || async_key_exit) {
|
||||
if (has_focus()) {
|
||||
log_info("superexit", "detected ALT+F4, exiting...");
|
||||
launcher::shutdown();
|
||||
}
|
||||
}
|
||||
if (overlay_exit) {
|
||||
if (has_focus()) {
|
||||
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
|
||||
launcher::shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// slow down
|
||||
Sleep(100);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
void disable() {
|
||||
if (!THREAD) {
|
||||
void handle_hotkeys(bool alt_f4, bool mapped_exit) {
|
||||
if (!alt_f4 && !mapped_exit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// stop old thread
|
||||
THREAD_RUNNING = false;
|
||||
THREAD->join();
|
||||
|
||||
// delete thread
|
||||
delete THREAD;
|
||||
THREAD = nullptr;
|
||||
|
||||
// log
|
||||
log_info("superexit", "disabled");
|
||||
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||
return;
|
||||
}
|
||||
if (!has_focus()) {
|
||||
return;
|
||||
}
|
||||
if (alt_f4) {
|
||||
log_info("superexit", "detected ALT+F4, exiting...");
|
||||
launcher::shutdown();
|
||||
return;
|
||||
}
|
||||
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
|
||||
launcher::shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
|
||||
namespace superexit {
|
||||
bool has_focus();
|
||||
void enable();
|
||||
void disable();
|
||||
void handle_hotkeys(bool alt_f4, bool mapped_exit);
|
||||
}
|
||||
|
||||
+32
-64
@@ -1,6 +1,7 @@
|
||||
#include "eamuse.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
@@ -10,7 +11,6 @@
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "games/sdvx/sdvx.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/time.h"
|
||||
#include "util/utils.h"
|
||||
#include "overlay/overlay.h"
|
||||
@@ -25,10 +25,8 @@ static double CARD_INSERT_TIME[2] = {0, 0};
|
||||
static double CARD_INSERT_TIMEOUT = 2.0;
|
||||
static char CARD_INSERT_UID[2][8] = {{0}, {0}};
|
||||
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
|
||||
static int COIN_STOCK = 0;
|
||||
static bool COIN_BLOCK = false;
|
||||
static std::thread *COIN_INPUT_THREAD;
|
||||
static bool COIN_INPUT_THREAD_ACTIVE = false;
|
||||
static std::atomic_int COIN_STOCK {0};
|
||||
static std::atomic_bool COIN_BLOCK {false};
|
||||
static uint16_t KEYPAD_STATE[] = {0, 0};
|
||||
static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0};
|
||||
static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0};
|
||||
@@ -292,79 +290,49 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) {
|
||||
}
|
||||
|
||||
bool eamuse_coin_get_block() {
|
||||
return COIN_BLOCK;
|
||||
return COIN_BLOCK.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void eamuse_coin_set_block(bool block) {
|
||||
COIN_BLOCK = block;
|
||||
COIN_BLOCK.store(block, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
int eamuse_coin_get_stock() {
|
||||
return COIN_STOCK;
|
||||
return COIN_STOCK.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void eamuse_coin_set_stock(int amount) {
|
||||
COIN_STOCK = amount;
|
||||
COIN_STOCK.store(amount, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool eamuse_coin_consume(int amount) {
|
||||
if (COIN_STOCK < amount) {
|
||||
return false;
|
||||
} else {
|
||||
COIN_STOCK -= amount;
|
||||
return true;
|
||||
auto stock = COIN_STOCK.load(std::memory_order_relaxed);
|
||||
while (stock >= amount) {
|
||||
if (COIN_STOCK.compare_exchange_weak(
|
||||
stock,
|
||||
stock - amount,
|
||||
std::memory_order_relaxed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int eamuse_coin_consume_stock() {
|
||||
int stock = COIN_STOCK;
|
||||
COIN_STOCK = 0;
|
||||
return stock;
|
||||
return COIN_STOCK.exchange(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
int eamuse_coin_add() {
|
||||
return ++COIN_STOCK;
|
||||
return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
}
|
||||
|
||||
void eamuse_coin_start_thread() {
|
||||
|
||||
// set active
|
||||
COIN_INPUT_THREAD_ACTIVE = true;
|
||||
|
||||
// create thread
|
||||
COIN_INPUT_THREAD = new std::thread([]() {
|
||||
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||
static bool COIN_INPUT_KEY_STATE = false;
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
while (COIN_INPUT_THREAD_ACTIVE) {
|
||||
|
||||
// check input key
|
||||
if (overlay_buttons && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(
|
||||
games::OverlayButtons::InsertCoin))) {
|
||||
if (!COIN_INPUT_KEY_STATE) {
|
||||
if (COIN_BLOCK)
|
||||
log_info("eamuse", "coin inserted while blocked");
|
||||
else {
|
||||
log_info("eamuse", "coin insert");
|
||||
COIN_STOCK++;
|
||||
}
|
||||
}
|
||||
COIN_INPUT_KEY_STATE = true;
|
||||
} else {
|
||||
COIN_INPUT_KEY_STATE = false;
|
||||
}
|
||||
|
||||
// once every two frames
|
||||
timer.sleep(1000 / 30);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void eamuse_coin_stop_thread() {
|
||||
COIN_INPUT_THREAD_ACTIVE = false;
|
||||
COIN_INPUT_THREAD->join();
|
||||
delete COIN_INPUT_THREAD;
|
||||
COIN_INPUT_THREAD = nullptr;
|
||||
void eamuse_coin_insert() {
|
||||
if (COIN_BLOCK.load(std::memory_order_relaxed)) {
|
||||
log_info("eamuse", "coin inserted while blocked");
|
||||
} else {
|
||||
log_info("eamuse", "coin insert");
|
||||
COIN_STOCK.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
void eamuse_pin_macro_start_thread() {
|
||||
@@ -409,7 +377,6 @@ void eamuse_pin_macro_start_thread() {
|
||||
size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()};
|
||||
|
||||
std::optional<uint8_t> active_unit = std::nullopt;
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
while (PIN_MACRO_THREAD_ACTIVE) {
|
||||
// wait for key press or auto-trigger
|
||||
@@ -438,7 +405,7 @@ void eamuse_pin_macro_start_thread() {
|
||||
}
|
||||
|
||||
if (!active_unit.has_value()) {
|
||||
timer.sleep(20);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -454,22 +421,22 @@ void eamuse_pin_macro_start_thread() {
|
||||
eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]);
|
||||
}
|
||||
pin_index[unit]++;
|
||||
timer.sleep(100);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// clear
|
||||
eamuse_set_keypad_overrides(unit, 0);
|
||||
timer.sleep(50);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// end of PIN
|
||||
if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) {
|
||||
active_unit = std::nullopt;
|
||||
timer.sleep(120);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(120));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
timer.sleep(200);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -675,7 +642,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");
|
||||
|
||||
@@ -58,9 +58,7 @@ bool eamuse_coin_consume(int amount);
|
||||
int eamuse_coin_consume_stock();
|
||||
|
||||
int eamuse_coin_add();
|
||||
|
||||
void eamuse_coin_start_thread();
|
||||
void eamuse_coin_stop_thread();
|
||||
void eamuse_coin_insert();
|
||||
|
||||
void eamuse_pin_macro_start_thread();
|
||||
void eamuse_pin_macro_stop_thread();
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#include "hotkeys.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <stop_token>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "games/io.h"
|
||||
#include "launcher/superexit.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace hotkeys {
|
||||
|
||||
namespace {
|
||||
|
||||
// 8 ms targets short screenshot pulses; sleep_for may use a coarser scheduler
|
||||
// interval during early boot or when process timer adjustments are disabled
|
||||
constexpr auto MIN_SAMPLE_INTERVAL = std::chrono::milliseconds(8);
|
||||
|
||||
std::atomic_bool SCREENSHOT_PENDING {false};
|
||||
std::mutex INPUT_MUTEX;
|
||||
bool INPUT_ENABLED = false;
|
||||
bool RAW_INPUT_ENABLED = false;
|
||||
std::jthread WORKER;
|
||||
|
||||
bool read_button(std::vector<Button> *buttons, size_t index) {
|
||||
// getState retains each binding's focus, modifier, inversion, and debounce policy
|
||||
return RI_MGR && buttons && index < buttons->size() &&
|
||||
GameAPI::Buttons::getState(RI_MGR, buttons->at(index));
|
||||
}
|
||||
|
||||
bool read_alt_f4() {
|
||||
// preserve both legacy detection paths whenever raw input is available
|
||||
bool pressed = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
|
||||
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
|
||||
if (!RAW_INPUT_ENABLED || !RI_MGR) {
|
||||
return pressed;
|
||||
}
|
||||
return pressed || RI_MGR->keyboard_combo_pressed(VK_MENU, VK_F4);
|
||||
}
|
||||
|
||||
bool rising_edge(bool current, bool &previous) {
|
||||
const bool edge = current && !previous;
|
||||
previous = current;
|
||||
return edge;
|
||||
}
|
||||
|
||||
void run(std::stop_token stop_token) {
|
||||
bool screenshot_previous = false;
|
||||
bool coin_previous = false;
|
||||
|
||||
while (!stop_token.stop_requested()) {
|
||||
bool coin_edge = false;
|
||||
bool super_exit_current = false;
|
||||
bool alt_f4_current = false;
|
||||
|
||||
{
|
||||
// lifecycle functions hold this mutex until raw-input polling is complete
|
||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||
if (INPUT_ENABLED || RAW_INPUT_ENABLED) {
|
||||
auto *buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||
const bool screenshot_down = INPUT_ENABLED && read_button(
|
||||
buttons, games::OverlayButtons::Screenshot);
|
||||
const bool coin_current = INPUT_ENABLED && read_button(
|
||||
buttons, games::OverlayButtons::InsertCoin);
|
||||
const bool super_exit_down = RAW_INPUT_ENABLED && read_button(
|
||||
buttons, games::OverlayButtons::SuperExit);
|
||||
|
||||
// global_hotkeys_triggered takes OVERLAY_MUTEX, then the overlay's
|
||||
// hotkeys_mutex; its button reads may then take device mutexes. polling
|
||||
// every mapped-input tick is intentional so HotkeyToggle releases cannot
|
||||
// be missed when the render thread stalls.
|
||||
const bool gate_active = overlay::global_hotkeys_triggered();
|
||||
const bool screenshot_current = screenshot_down && gate_active;
|
||||
super_exit_current = super_exit_down && gate_active;
|
||||
|
||||
if (rising_edge(screenshot_current, screenshot_previous)) {
|
||||
SCREENSHOT_PENDING.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
coin_edge = rising_edge(coin_current, coin_previous);
|
||||
} else {
|
||||
screenshot_previous = false;
|
||||
coin_previous = false;
|
||||
}
|
||||
|
||||
alt_f4_current = read_alt_f4();
|
||||
}
|
||||
|
||||
if (coin_edge) {
|
||||
eamuse_coin_insert();
|
||||
}
|
||||
|
||||
// pass held state so returning focus can exit without another key press
|
||||
superexit::handle_hotkeys(alt_f4_current, super_exit_current);
|
||||
|
||||
std::this_thread::sleep_for(MIN_SAMPLE_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (WORKER.joinable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
||||
WORKER = std::jthread(run);
|
||||
log_info("hotkeys", "sampler started");
|
||||
}
|
||||
|
||||
void enable_raw_input() {
|
||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||
RAW_INPUT_ENABLED = true;
|
||||
}
|
||||
|
||||
void enable_input() {
|
||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
||||
INPUT_ENABLED = true;
|
||||
log_info("hotkeys", "configured input enabled");
|
||||
}
|
||||
|
||||
void disable_input() {
|
||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||
INPUT_ENABLED = false;
|
||||
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
||||
log_info("hotkeys", "configured input disabled");
|
||||
}
|
||||
|
||||
void disable_raw_input() {
|
||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||
RAW_INPUT_ENABLED = false;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (!WORKER.joinable()) {
|
||||
return;
|
||||
}
|
||||
WORKER.request_stop();
|
||||
WORKER.join();
|
||||
log_info("hotkeys", "sampler stopped");
|
||||
}
|
||||
|
||||
bool consume_screenshot() {
|
||||
return SCREENSHOT_PENDING.exchange(false, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
namespace hotkeys {
|
||||
|
||||
// ALT+F4 monitoring spans boot and teardown; configured actions are enabled separately
|
||||
void start();
|
||||
void enable_raw_input();
|
||||
void enable_input();
|
||||
void disable_input();
|
||||
void disable_raw_input();
|
||||
void stop();
|
||||
bool consume_screenshot();
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
// enable touch functions - set version to windows 7
|
||||
// mingw otherwise doesn't load touch stuff
|
||||
#define _WIN32_WINNT 0x0601
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "wintouchemu.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#define TOUCH_SIMULATE_FAT_FINGERS 0
|
||||
#define TOUCH_DEBUG_VERBOSE 0
|
||||
|
||||
#if TOUCH_DEBUG_VERBOSE
|
||||
#define log_debug(module, format_str, ...) logger::push( \
|
||||
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
|
||||
#else
|
||||
#define log_debug(module, format_str, ...)
|
||||
#endif
|
||||
|
||||
namespace nativetouchhook {
|
||||
|
||||
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
|
||||
|
||||
static void strip_contact_size(PTOUCHINPUT point) {
|
||||
|
||||
#if TOUCH_SIMULATE_FAT_FINGERS
|
||||
point->dwMask |= 0x004;
|
||||
point->cxContact = 80 * 100;
|
||||
point->cyContact = 60 * 100;
|
||||
#endif
|
||||
|
||||
// most monitors do not set TOUCHEVENTFMASK_CONTACTAREA, but for
|
||||
// monitors that do set it, IIDX can get very confused (SDVX is not
|
||||
// affected)
|
||||
//
|
||||
// while the test menu and the touch "glow" seem to work properly,
|
||||
// interacting with subscreen menu items or entering PIN becomes
|
||||
// very unpredictable
|
||||
//
|
||||
// to fix this, simply remove the contact area width and height
|
||||
//
|
||||
// note: test menu > I/O > touch test gives 5 numbers:
|
||||
// n: x, y, w, h
|
||||
// where
|
||||
// n is the nth touch input since boot
|
||||
// x, y are coordinates (center of finger)
|
||||
// w, h are contact width and height
|
||||
//
|
||||
// when TOUCHEVENTFMASK_CONTACTAREA is not set, w/h will
|
||||
// automatically be seen as 1x1, which works perfectly fine
|
||||
|
||||
log_debug(
|
||||
"touch::native",
|
||||
"[{}, {}] dwMask = 0x{:x}, cxContact = {}, cyContact = {}",
|
||||
point->x / 100,
|
||||
point->y / 100,
|
||||
point->dwMask,
|
||||
point->cxContact,
|
||||
point->cyContact);
|
||||
|
||||
point->dwMask &= ~(0x004ul); // clear TOUCHEVENTFMASK_CONTACTAREA
|
||||
point->cxContact = 0;
|
||||
point->cyContact = 0;
|
||||
}
|
||||
|
||||
static void flip_touch_points(PTOUCHINPUT point) {
|
||||
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x;
|
||||
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y;
|
||||
}
|
||||
|
||||
static BOOL WINAPI GetTouchInputInfoHook(
|
||||
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
|
||||
|
||||
// call the original fist
|
||||
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
|
||||
if (result == 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool flip_values = false;
|
||||
if (avs::game::is_model("KFC") && rawinput::touch::DISPLAY_INITIALIZED) {
|
||||
log_debug(
|
||||
"touch::native", "DISPLAY_ORIENTATION = {}, DISPLAY_SIZE_X = {}, DISPLAY_SIZE_Y = {}",
|
||||
rawinput::touch::DISPLAY_ORIENTATION,
|
||||
rawinput::touch::DISPLAY_SIZE_X,
|
||||
rawinput::touch::DISPLAY_SIZE_Y);
|
||||
if (rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
|
||||
flip_values = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (rawinput::touch::INVERTED) {
|
||||
flip_values = !flip_values;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < cInputs; i++) {
|
||||
PTOUCHINPUT point = &pInputs[i];
|
||||
|
||||
if (avs::game::is_model("LDJ")) {
|
||||
strip_contact_size(point);
|
||||
}
|
||||
|
||||
if (flip_values) {
|
||||
flip_touch_points(point);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void hook(HMODULE module) {
|
||||
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
|
||||
if (GetTouchInputInfo_orig != nullptr) {
|
||||
log_misc("touch::native", "GetTouchInputInfo hooked");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#include <windows.h>
|
||||
|
||||
namespace nativetouchhook {
|
||||
void hook(HMODULE module);
|
||||
}
|
||||
|
||||
@@ -495,6 +495,14 @@ namespace wintouchemu {
|
||||
if (hWnd != nullptr && USE_MOUSE) {
|
||||
bool button_pressed = get_async_primary_mouse();
|
||||
|
||||
// while the subscreen overlay cannot accept the mouse, treat the button as
|
||||
// released; this ends any active touch and prevents new mouse touch contacts
|
||||
if (overlay::OVERLAY &&
|
||||
overlay::OVERLAY->has_subscreen_touch_transform() &&
|
||||
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
|
||||
button_pressed = false;
|
||||
}
|
||||
|
||||
// if there was a touch event in the last 500 ms, don't insert new button presses
|
||||
if (button_pressed && (now - last_touch_event) < 500) {
|
||||
button_pressed = false;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "games/io.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "games/nost/nost.h"
|
||||
#include "games/popn/popn.h"
|
||||
#include "games/rb/touch_debug.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
@@ -51,6 +52,7 @@
|
||||
#include "windows/sdvx_sub.h"
|
||||
#include "windows/keypad.h"
|
||||
#include "windows/log.h"
|
||||
#include "windows/nostalgia_touch_piano.h"
|
||||
#include "windows/obs.h"
|
||||
#include "windows/patch_manager.h"
|
||||
#include "windows/exitprompt.cpp"
|
||||
@@ -156,6 +158,11 @@ void overlay::destroy(HWND hWnd) {
|
||||
}
|
||||
}
|
||||
|
||||
bool overlay::global_hotkeys_triggered() {
|
||||
const std::lock_guard<std::mutex> lock(OVERLAY_MUTEX);
|
||||
return !overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered();
|
||||
}
|
||||
|
||||
overlay::SpiceOverlay::SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device)
|
||||
: renderer(OverlayRenderer::D3D9), hWnd(hWnd), d3d(d3d), device(device) {
|
||||
log_info("overlay", "initializing (D3D9)");
|
||||
@@ -404,6 +411,12 @@ void overlay::SpiceOverlay::init() {
|
||||
window_fps->set_active(true);
|
||||
}
|
||||
|
||||
if (!cfg::CONFIGURATOR_STANDALONE &&
|
||||
avs::game::is_model("PAN") && games::nost::ENABLE_TOUCH_MODE) {
|
||||
window_nostalgia_touch_piano =
|
||||
std::make_unique<overlay::windows::NostalgiaTouchPiano>(this);
|
||||
}
|
||||
|
||||
// owned separately from `windows` so it is not part of the overlay window layer
|
||||
window_main_menu = std::make_unique<overlay::windows::ExitPrompt>(this);
|
||||
|
||||
@@ -556,13 +569,18 @@ void overlay::SpiceOverlay::new_frame() {
|
||||
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& this->window_fps->get_active();
|
||||
|
||||
const bool draw_nostalgia_touch_piano = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& this->window_nostalgia_touch_piano
|
||||
&& this->window_nostalgia_touch_piano->get_active();
|
||||
|
||||
// draw RB touch diagnostics
|
||||
const bool draw_rb_touch_debug = this->renderer != OverlayRenderer::SOFTWARE
|
||||
&& games::rb::touch_debug_overlay_enabled();
|
||||
|
||||
// check if there is nothing to draw
|
||||
this->has_pending_frame = false;
|
||||
if (!this->active && !draw_notifications && !draw_fps_persistent && !draw_rb_touch_debug) {
|
||||
if (!this->active && !draw_notifications && !draw_fps_persistent &&
|
||||
!draw_nostalgia_touch_piano && !draw_rb_touch_debug) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -596,7 +614,13 @@ void overlay::SpiceOverlay::new_frame() {
|
||||
for (auto &window : this->windows) {
|
||||
window->build();
|
||||
}
|
||||
}
|
||||
|
||||
if (draw_nostalgia_touch_piano) {
|
||||
this->window_nostalgia_touch_piano->build();
|
||||
}
|
||||
|
||||
if (this->active) {
|
||||
// draw the main menu on top of the overlay windows
|
||||
this->window_main_menu->build();
|
||||
|
||||
@@ -819,6 +843,10 @@ void overlay::SpiceOverlay::update() {
|
||||
// FPS window
|
||||
this->window_fps->update();
|
||||
|
||||
if (this->window_nostalgia_touch_piano) {
|
||||
this->window_nostalgia_touch_piano->update();
|
||||
}
|
||||
|
||||
// main menu (owned separately from the overlay window layer)
|
||||
this->window_main_menu->update();
|
||||
|
||||
@@ -885,11 +913,25 @@ bool overlay::SpiceOverlay::get_active() {
|
||||
return this->active;
|
||||
}
|
||||
|
||||
bool overlay::SpiceOverlay::is_subscreen_overlay_visible() {
|
||||
return this->get_active() &&
|
||||
this->window_sub != nullptr &&
|
||||
this->window_sub->get_active();
|
||||
}
|
||||
|
||||
bool overlay::SpiceOverlay::accepts_subscreen_mouse_input() {
|
||||
return this->is_subscreen_overlay_visible() &&
|
||||
this->window_sub->is_mouse_hovered();
|
||||
}
|
||||
|
||||
bool overlay::SpiceOverlay::has_focus() {
|
||||
return this->get_active() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow);
|
||||
}
|
||||
|
||||
bool overlay::SpiceOverlay::hotkeys_triggered() {
|
||||
const std::lock_guard<std::mutex> lock(this->hotkeys_mutex);
|
||||
|
||||
// this query also consumes the shared HotkeyToggle edge; the mutex gives all callers one latch
|
||||
// prevent hotkeys in spicecfg
|
||||
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||
return false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user