mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-03 00:38:14 -07:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adf4cccd4a | |||
| bf8e194685 | |||
| a2e508208c | |||
| f5888609a8 | |||
| c6cd72c528 | |||
| 82e0c053d0 | |||
| 3b29227dbc | |||
| ce2f93d234 | |||
| 13a171f199 | |||
| 3da352488e | |||
| 2386047c2f | |||
| baa550037a | |||
| b53447bed5 | |||
| 4959a58de3 | |||
| 9141ff453b |
@@ -332,6 +332,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
api/modules/control.cpp
|
api/modules/control.cpp
|
||||||
api/modules/touch.cpp
|
api/modules/touch.cpp
|
||||||
api/modules/iidx.cpp
|
api/modules/iidx.cpp
|
||||||
|
api/modules/sdvx.cpp
|
||||||
api/serial.cpp
|
api/serial.cpp
|
||||||
api/modules/drs.cpp
|
api/modules/drs.cpp
|
||||||
api/modules/lcd.cpp
|
api/modules/lcd.cpp
|
||||||
@@ -535,6 +536,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/graphics/nvapi_hook.cpp
|
hooks/graphics/nvapi_hook.cpp
|
||||||
hooks/graphics/nvenc_hook.cpp
|
hooks/graphics/nvenc_hook.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||||
|
hooks/graphics/backends/d3d9/d3d9_screenshot.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_device.cpp
|
hooks/graphics/backends/d3d9/d3d9_device.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_gfdm.cpp
|
hooks/graphics/backends/d3d9/d3d9_gfdm.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
|
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
|
||||||
@@ -576,6 +578,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
misc/device.cpp
|
misc/device.cpp
|
||||||
misc/eamuse.cpp
|
misc/eamuse.cpp
|
||||||
misc/extdev.cpp
|
misc/extdev.cpp
|
||||||
|
misc/hotkeys.cpp
|
||||||
misc/sciunit.cpp
|
misc/sciunit.cpp
|
||||||
misc/sde.cpp
|
misc/sde.cpp
|
||||||
misc/wintouchemu.cpp
|
misc/wintouchemu.cpp
|
||||||
|
|||||||
@@ -267,6 +267,20 @@ which also means that your hex edits are applicable directly.
|
|||||||
- `Side Panel Right Inner`
|
- `Side Panel Right Inner`
|
||||||
- `Side Panel Right`
|
- `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
|
#### LCD
|
||||||
- info()
|
- info()
|
||||||
- returns information about the serial LCD controller some games use
|
- returns information about the serial LCD controller some games use
|
||||||
|
|||||||
@@ -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 <utility>
|
||||||
|
|
||||||
|
#include "client.h"
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "util/crypt.h"
|
#include "util/crypt.h"
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
#include "modules/lcd.h"
|
#include "modules/lcd.h"
|
||||||
#include "modules/lights.h"
|
#include "modules/lights.h"
|
||||||
#include "modules/memory.h"
|
#include "modules/memory.h"
|
||||||
|
#include "modules/sdvx.h"
|
||||||
#include "modules/touch.h"
|
#include "modules/touch.h"
|
||||||
#include "modules/resize.h"
|
#include "modules/resize.h"
|
||||||
#include "request.h"
|
#include "request.h"
|
||||||
@@ -36,6 +38,8 @@
|
|||||||
using namespace rapidjson;
|
using namespace rapidjson;
|
||||||
using namespace api;
|
using namespace api;
|
||||||
|
|
||||||
|
std::atomic_uint32_t api::CLIENT_COUNT = 0;
|
||||||
|
|
||||||
Controller::Controller(unsigned short port, std::string password, bool pretty)
|
Controller::Controller(unsigned short port, std::string password, bool pretty)
|
||||||
: port(port), password(std::move(password)), pretty(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::LCD());
|
||||||
state->modules.push_back(new modules::Lights());
|
state->modules.push_back(new modules::Lights());
|
||||||
state->modules.push_back(new modules::Memory());
|
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::Touch());
|
||||||
state->modules.push_back(new modules::Resize());
|
state->modules.push_back(new modules::Resize());
|
||||||
|
|
||||||
|
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::free_state(api::ClientState *state) {
|
void Controller::free_state(api::ClientState *state) {
|
||||||
@@ -424,6 +431,8 @@ void Controller::free_state(api::ClientState *state) {
|
|||||||
|
|
||||||
// free cipher
|
// free cipher
|
||||||
delete state->cipher;
|
delete state->cipher;
|
||||||
|
|
||||||
|
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::free_socket() {
|
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) {
|
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
|
||||||
// Create an array for the light state
|
// Create an array for the light state
|
||||||
Value light_state(kArrayType);
|
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) {
|
for (const auto [r, g, b] : mapping.data) {
|
||||||
light_state.PushBack(r, res.doc()->GetAllocator());
|
light_state.PushBack(r, res.doc()->GetAllocator());
|
||||||
light_state.PushBack(g, res.doc()->GetAllocator());
|
light_state.PushBack(g, res.doc()->GetAllocator());
|
||||||
light_state.PushBack(b, 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());
|
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
|
||||||
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "touch/native/inject.h"
|
#include "touch/native/inject.h"
|
||||||
#include "touch/native/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
|
#include "touch/native/transform.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
|
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
@@ -40,6 +42,35 @@ namespace api::modules {
|
|||||||
return nativetouch::inject::inject_synthetic_touch(position, 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") {
|
Touch::Touch() : Module("touch") {
|
||||||
is_sdvx = avs::game::is_model("KFC");
|
is_sdvx = avs::game::is_model("KFC");
|
||||||
|
|
||||||
@@ -55,9 +86,12 @@ namespace api::modules {
|
|||||||
native_canvas_w = 0;
|
native_canvas_w = 0;
|
||||||
native_canvas_h = 0;
|
native_canvas_h = 0;
|
||||||
if (is_sdvx) {
|
if (is_sdvx) {
|
||||||
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata
|
// windowed API coordinates land on the sub screen window as-is; fullscreen
|
||||||
native_canvas_w = 1080;
|
// coordinates are rotated into the game's touch space by apply_touch_errata
|
||||||
native_canvas_h = 1920;
|
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")) {
|
} else if (avs::game::is_model("LDJ")) {
|
||||||
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
||||||
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
||||||
@@ -66,6 +100,10 @@ namespace api::modules {
|
|||||||
// pop'n music API touch surface
|
// pop'n music API touch surface
|
||||||
native_canvas_w = 1280;
|
native_canvas_w = 1280;
|
||||||
native_canvas_h = 800;
|
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["read"] = std::bind(&Touch::read, this, _1, _2);
|
||||||
@@ -209,19 +247,14 @@ namespace api::modules {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Touch::apply_touch_errata(int &x, int &y) {
|
void Touch::apply_touch_errata(int &x, int &y) {
|
||||||
int x_raw = x;
|
|
||||||
int y_raw = y;
|
|
||||||
|
|
||||||
if (is_tdj_fhd) {
|
if (is_tdj_fhd) {
|
||||||
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p)
|
// 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
|
// 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
|
// the target of the touch events so just assume it's the sub screen
|
||||||
x = x_raw * 1920 / 1280;
|
x = x * 1920 / 1280;
|
||||||
y = y_raw * 1080 / 720;
|
y = y * 1080 / 720;
|
||||||
} else if (is_sdvx) {
|
} else if (is_sdvx) {
|
||||||
// for exceed gear, they are both 1080p screens, but need to apply transformation
|
sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
|
||||||
x = 1080 - y_raw;
|
|
||||||
y = x_raw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from .coin import *
|
|||||||
from .control import *
|
from .control import *
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .iidx import *
|
from .iidx import *
|
||||||
|
from .sdvx import *
|
||||||
from .info import *
|
from .info import *
|
||||||
from .keypads import *
|
from .keypads import *
|
||||||
from .lights 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()
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
<dpiAware>true</dpiAware>
|
<dpiAware>true</dpiAware>
|
||||||
|
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">Legacy</activeCodePage>
|
||||||
</asmv3:windowsSettings>
|
</asmv3:windowsSettings>
|
||||||
</asmv3:application>
|
</asmv3:application>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace games::gitadora {
|
|||||||
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
||||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||||
bool ALLOW_REALTEK_AUDIO = false;
|
bool ALLOW_REALTEK_AUDIO = false;
|
||||||
|
bool NATIVE_TOUCH = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Prevent GitaDora from creating folders on F drive
|
* Prevent GitaDora from creating folders on F drive
|
||||||
@@ -809,17 +810,16 @@ namespace games::gitadora {
|
|||||||
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single-window mode needs touch injection for its overlay. Two-head fullscreen
|
// touch injection drives mouse-as-touch and API touch for the subscreen,
|
||||||
// mode uses the real SMALL output, so it only needs the display-topology shim.
|
// no matter whether it is drawn by the overlay (single-window mode) or by
|
||||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
// the dedicated SMALL window
|
||||||
// enable touch hook for subscreen overlay
|
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||||
const auto native_touch_ready = !wintouchemu::FORCE &&
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
if (!native_touch_ready) {
|
// the legacy fallback can only feed the subscreen overlay
|
||||||
wintouchemu::FORCE = true;
|
wintouchemu::FORCE = true;
|
||||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !SPICE_XP
|
#if !SPICE_XP
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ namespace games::gitadora {
|
|||||||
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
||||||
extern std::optional<std::string> ASIO_DRIVER;
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
extern bool ALLOW_REALTEK_AUDIO;
|
extern bool ALLOW_REALTEK_AUDIO;
|
||||||
|
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 {
|
class GitaDoraGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#if SPICE64
|
#if SPICE64
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include "api/client.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.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_g], rgb.g);
|
||||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
||||||
|
|
||||||
for (unsigned int i = 0; i < data_size; ++i) {
|
if (api::has_clients()) {
|
||||||
map.data[i].r = data[i * 3];
|
for (size_t i = 0; i < data_size; ++i) {
|
||||||
map.data[i].g = data[i * 3 + 1];
|
map.data[i].r = data[i * 3];
|
||||||
map.data[i].b = data[i * 3 + 2];
|
map.data[i].g = data[i * 3 + 1];
|
||||||
|
map.data[i].b = data[i * 3 + 2];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ namespace games::iidx {
|
|||||||
|
|
||||||
// settings
|
// settings
|
||||||
bool FLIP_CAMS = false;
|
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 = false;
|
||||||
bool TDJ_CAMERA_PREFER_16_9 = true;
|
bool TDJ_CAMERA_PREFER_16_9 = true;
|
||||||
bool TDJ_MODE = false;
|
bool TDJ_MODE = false;
|
||||||
@@ -424,10 +424,7 @@ namespace games::iidx {
|
|||||||
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
||||||
|
|
||||||
// check if cam hook should be enabled
|
// check if cam hook should be enabled
|
||||||
if (!DISABLE_CAMS.has_value()) {
|
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::legacy) {
|
||||||
log_fatal("iidx", "assertion failure - DISABLE_CAMS not set during attach");
|
|
||||||
}
|
|
||||||
if (!DISABLE_CAMS.value()) {
|
|
||||||
init_legacy_camera_hook(FLIP_CAMS);
|
init_legacy_camera_hook(FLIP_CAMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,19 +451,10 @@ namespace games::iidx {
|
|||||||
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
|
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for cab camera access for the second time (first time was in launcher.cpp)
|
// auto with iidx module means turn off camera (non-cab use)
|
||||||
// this time, we are inside -iidx module hook, which means the user is likely NOT on a cab
|
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::automatic) {
|
||||||
// therefore, start with cams OFF by default, and allow user to forcibly override to ON
|
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
|
||||||
if (!games::iidx::DISABLE_CAMS.has_value()) {
|
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// windowed subscreen, enabled by default, unless turned off by user
|
// windowed subscreen, enabled by default, unless turned off by user
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
|
|
||||||
namespace games::iidx {
|
namespace games::iidx {
|
||||||
|
|
||||||
|
enum class cab_camera_access_mode {
|
||||||
|
automatic,
|
||||||
|
off,
|
||||||
|
on,
|
||||||
|
legacy,
|
||||||
|
};
|
||||||
|
|
||||||
enum class iidx_aio_emulation_state {
|
enum class iidx_aio_emulation_state {
|
||||||
unknown,
|
unknown,
|
||||||
bi2a_com2,
|
bi2a_com2,
|
||||||
@@ -20,7 +27,7 @@ namespace games::iidx {
|
|||||||
// settings
|
// settings
|
||||||
|
|
||||||
extern bool FLIP_CAMS;
|
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;
|
||||||
extern bool TDJ_CAMERA_PREFER_16_9;
|
extern bool TDJ_CAMERA_PREFER_16_9;
|
||||||
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
|
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#if SPICE64
|
#if SPICE64
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include "api/client.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -349,43 +350,29 @@ namespace games::sdvx {
|
|||||||
* 9 - v unit - 258 bytes - 86 colors
|
* 9 - v unit - 258 bytes - 86 colors
|
||||||
*
|
*
|
||||||
* data is stored in RGB order, 3 bytes per color
|
* 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
|
// check index bounds
|
||||||
if (tapeledutils::is_enabled() && index < std::size(mapping)) {
|
if (tapeledutils::is_enabled() && index < std::size(TAPELED_MAPPING)) {
|
||||||
auto &map = mapping[index];
|
auto &map = TAPELED_MAPPING[index];
|
||||||
|
const auto data_size = map.data.size();
|
||||||
|
|
||||||
// pick a color to use
|
// 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
|
// program the lights into API
|
||||||
auto &lights = get_lights();
|
auto &lights = get_lights();
|
||||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
|
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_g], rgb.g);
|
||||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
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) {
|
if (This != custom_node) {
|
||||||
|
|||||||
@@ -61,6 +61,19 @@ namespace games::sdvx {
|
|||||||
static HKEY real_asio_reg_handle = nullptr;
|
static HKEY real_asio_reg_handle = nullptr;
|
||||||
static HKEY real_asio_device_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) {
|
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
||||||
if (lpSubKey != nullptr &&
|
if (lpSubKey != nullptr &&
|
||||||
phkResult != nullptr &&
|
phkResult != nullptr &&
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "games/game.h"
|
#include "games/game.h"
|
||||||
|
#include "util/tapeled.h"
|
||||||
|
|
||||||
namespace games::sdvx {
|
namespace games::sdvx {
|
||||||
|
|
||||||
@@ -25,6 +26,9 @@ namespace games::sdvx {
|
|||||||
// states
|
// states
|
||||||
extern bool SHOW_VM_MONITOR_WARNING;
|
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() {
|
static inline bool is_valkyrie_model() {
|
||||||
return (
|
return (
|
||||||
avs::game::is_model("KFC") &&
|
avs::game::is_model("KFC") &&
|
||||||
|
|||||||
@@ -23,10 +23,7 @@
|
|||||||
#include "external/imgui/backends/imgui_impl_dx11.h"
|
#include "external/imgui/backends/imgui_impl_dx11.h"
|
||||||
#include "overlay/imgui/impl_spice.h"
|
#include "overlay/imgui/impl_spice.h"
|
||||||
|
|
||||||
#include "games/io.h"
|
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "launcher/launcher.h"
|
|
||||||
#include "misc/eamuse.h"
|
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -108,8 +105,62 @@ Present1_t Present1_orig = nullptr;
|
|||||||
bool g_swapchain_hooked = false;
|
bool g_swapchain_hooked = false;
|
||||||
bool g_swapchain1_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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +169,6 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
|||||||
return;
|
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
|
// theme the native title bar; first present is the only reliable point for
|
||||||
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
|
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
|
||||||
set_window_dark_titlebar(desc.OutputWindow);
|
set_window_dark_titlebar(desc.OutputWindow);
|
||||||
@@ -149,43 +194,42 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
|||||||
device->Release();
|
device->Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour).
|
// screenshots have to keep working with the overlay disabled, so they are not gated on it
|
||||||
void poll_screenshot_hotkey() {
|
void pump_frame(IDXGISwapChain *swapchain) {
|
||||||
static bool s_down = false;
|
const bool has_overlay =
|
||||||
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
||||||
const bool pressed = buttons
|
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
|
||||||
&& (!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)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
poll_screenshot_hotkey();
|
graphics_poll_screenshot_hotkey();
|
||||||
|
|
||||||
// size imgui to the backbuffer (not window client). dxgi may upscale
|
// before the overlay render so the screenshot excludes it
|
||||||
// a small backbuffer into a larger client rect; without this override
|
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||||
// imgui would draw past the RTV and the mouse mapping would be off.
|
d3d11_hooks::try_screenshot(swapchain);
|
||||||
DXGI_SWAP_CHAIN_DESC desc {};
|
|
||||||
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
|
|
||||||
ImGui_ImplSpice_SetDisplaySizeOverride(
|
|
||||||
(float) desc.BufferDesc.Width,
|
|
||||||
(float) desc.BufferDesc.Height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay::OVERLAY->update();
|
if (has_overlay) {
|
||||||
overlay::OVERLAY->new_frame();
|
|
||||||
overlay::OVERLAY->render();
|
|
||||||
|
|
||||||
// after overlay render so toasts/menus end up in the saved image.
|
// size imgui to the backbuffer (not window client). dxgi may upscale
|
||||||
d3d11_hooks::try_screenshot(swapchain);
|
// 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(
|
HRESULT STDMETHODCALLTYPE Present_hook(
|
||||||
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
|
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
|
||||||
{
|
{
|
||||||
try_create_overlay(swapchain);
|
// a test present doesn't display anything; don't pick a window or take a screenshot off it
|
||||||
pump_overlay(swapchain);
|
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||||
|
try_create_overlay(swapchain);
|
||||||
|
pump_frame(swapchain);
|
||||||
|
}
|
||||||
return Present_orig(swapchain, SyncInterval, Flags);
|
return Present_orig(swapchain, SyncInterval, Flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,8 +250,10 @@ HRESULT STDMETHODCALLTYPE Present1_hook(
|
|||||||
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
|
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
|
||||||
const DXGI_PRESENT_PARAMETERS *pParams)
|
const DXGI_PRESENT_PARAMETERS *pParams)
|
||||||
{
|
{
|
||||||
try_create_overlay(swapchain);
|
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||||
pump_overlay(swapchain);
|
try_create_overlay(swapchain);
|
||||||
|
pump_frame(swapchain);
|
||||||
|
}
|
||||||
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
#include "d3d9_backend.h"
|
#include "d3d9_backend.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <external/robin_hood.h>
|
|
||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
#ifdef __GNUC__
|
|
||||||
#include <d3dx9tex.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "cfg/screen_resize.h"
|
#include "cfg/screen_resize.h"
|
||||||
@@ -18,28 +15,23 @@
|
|||||||
#include "games/popn/popn.h"
|
#include "games/popn/popn.h"
|
||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
#include "games/mfc/mfc.h"
|
#include "games/mfc/mfc.h"
|
||||||
#include "games/io.h"
|
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "launcher/options.h"
|
#include "launcher/options.h"
|
||||||
#include "launcher/signal.h"
|
#include "launcher/signal.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "misc/clipboard.h"
|
|
||||||
#include "misc/eamuse.h"
|
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "overlay/notifications.h"
|
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
#include "util/fileutils.h"
|
|
||||||
#include "util/flags_helper.h"
|
#include "util/flags_helper.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "util/memutils.h"
|
#include "util/memutils.h"
|
||||||
#include "util/threadpool.h"
|
|
||||||
|
|
||||||
#include "d3d9_device.h"
|
#include "d3d9_device.h"
|
||||||
|
#include "d3d9_screenshot.h"
|
||||||
|
|
||||||
#ifdef min
|
#ifdef min
|
||||||
#undef min
|
#undef min
|
||||||
@@ -63,19 +55,6 @@
|
|||||||
return __ret; \
|
return __ret; \
|
||||||
} while (0)
|
} 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
|
* 9 on 12
|
||||||
*/
|
*/
|
||||||
@@ -99,8 +78,6 @@ typedef IDirect3D9* (WINAPI *Direct3DCreate9On12_t)(
|
|||||||
static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr;
|
static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr;
|
||||||
static char D3D9_DIRECT3D_CREATE9_CONTENTS[16];
|
static char D3D9_DIRECT3D_CREATE9_CONTENTS[16];
|
||||||
|
|
||||||
static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false;
|
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
std::optional<UINT> D3D9_ADAPTER = std::nullopt;
|
std::optional<UINT> D3D9_ADAPTER = std::nullopt;
|
||||||
DWORD D3D9_BEHAVIOR_DISABLE = 0;
|
DWORD D3D9_BEHAVIOR_DISABLE = 0;
|
||||||
@@ -113,6 +90,12 @@ static Direct3DCreate9On12Ex_t Direct3DCreate9On12Ex_orig = nullptr;
|
|||||||
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
|
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
|
||||||
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr;
|
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr;
|
||||||
|
|
||||||
|
// 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(
|
static void graphics_d3d9_ldj_init_sub_screen(
|
||||||
IDirect3DDevice9Ex *device,
|
IDirect3DDevice9Ex *device,
|
||||||
D3DPRESENT_PARAMETERS *present_params,
|
D3DPRESENT_PARAMETERS *present_params,
|
||||||
@@ -1445,6 +1428,12 @@ IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen() {
|
|||||||
return surface;
|
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) {
|
static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
|
||||||
// iidx/sdvx
|
// iidx/sdvx
|
||||||
int swapchain = 1;
|
int swapchain = 1;
|
||||||
@@ -1472,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
|
// early versions of popn HC needs this as well, but not on by default as it can cause
|
||||||
// graphical glitches on some GPUs
|
// 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);
|
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(
|
void graphics_d3d9_on_present(
|
||||||
HWND hFocusWindow,
|
HWND hFocusWindow,
|
||||||
IDirect3DDevice9 *device,
|
IDirect3DDevice9 *device,
|
||||||
IDirect3DDevice9 *wrapped_device) {
|
WrappedIDirect3DDevice9 *wrapped_device) {
|
||||||
|
|
||||||
// image resize / orientation swap. run here (the present path) rather than from `EndScene`,
|
// 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
|
// which may fire several times per frame on multi-pass / render-to-texture games. this is the
|
||||||
@@ -1664,6 +1489,13 @@ void graphics_d3d9_on_present(
|
|||||||
SurfaceHook(device);
|
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
|
// 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
|
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
|
||||||
// `dx9osd.dll` for pfreepanic.
|
// `dx9osd.dll` for pfreepanic.
|
||||||
@@ -1683,6 +1515,15 @@ void graphics_d3d9_on_present(
|
|||||||
device->EndScene();
|
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
|
// for IIDX TDJ / SDVX UFC, handle subscreen
|
||||||
const bool is_vm = games::sdvx::is_valkyrie_model();
|
const bool is_vm = games::sdvx::is_valkyrie_model();
|
||||||
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
|
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
|
||||||
@@ -1696,136 +1537,6 @@ void graphics_d3d9_on_present(
|
|||||||
if (is_mfc) {
|
if (is_mfc) {
|
||||||
wintouchemu::update();
|
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) {
|
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#include "d3d9_gfdm.h"
|
#include "d3d9_gfdm.h"
|
||||||
|
|
||||||
|
struct WrappedIDirect3DDevice9;
|
||||||
|
|
||||||
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
|
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
|
||||||
static const GUID IID_WrappedIDirect3D9 = {
|
static const GUID IID_WrappedIDirect3D9 = {
|
||||||
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
|
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
|
||||||
@@ -13,7 +15,9 @@ void graphics_d3d9_init();
|
|||||||
void graphics_d3d9_on_present(
|
void graphics_d3d9_on_present(
|
||||||
HWND hFocusWindow,
|
HWND hFocusWindow,
|
||||||
IDirect3DDevice9 *device,
|
IDirect3DDevice9 *device,
|
||||||
IDirect3DDevice9 *wrapped_device);
|
WrappedIDirect3DDevice9 *wrapped_device);
|
||||||
|
|
||||||
|
void graphics_d3d9_notify_subscreen_present();
|
||||||
|
|
||||||
IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen();
|
IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen();
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,11 @@
|
|||||||
#include "shaders/vertex_shader.h"
|
#include "shaders/vertex_shader.h"
|
||||||
#endif
|
#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, ...) \
|
#define CHECK_RESULT_FMT(x, fmt, ...) \
|
||||||
HRESULT __ret = (x); \
|
HRESULT __ret = (x); \
|
||||||
if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \
|
if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \
|
||||||
@@ -134,6 +139,10 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
|
|||||||
this->main_swapchain->Release();
|
this->main_swapchain->Release();
|
||||||
this->main_swapchain = nullptr;
|
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) {
|
for (auto &sc : this->sub_swapchain) {
|
||||||
if (sc) {
|
if (sc) {
|
||||||
sc->Release();
|
sc->Release();
|
||||||
@@ -332,20 +341,25 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
|
|||||||
int index = 0;
|
int index = 0;
|
||||||
bool create_swap_chain = false;
|
bool create_swap_chain = false;
|
||||||
bool create_fake_swap_chain = false;
|
bool create_fake_swap_chain = false;
|
||||||
|
bool arena_slot = false;
|
||||||
if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||||
create_swap_chain = true;
|
create_swap_chain = true;
|
||||||
|
|
||||||
} else if (games::gitadora::is_arena_model() &&
|
} else if (games::gitadora::is_arena_model() &&
|
||||||
(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) {
|
if (pPresentationParameters->BackBufferWidth == 800) {
|
||||||
// SMALL (subscreen)
|
// SMALL (subscreen)
|
||||||
create_swap_chain = true;
|
create_swap_chain = true;
|
||||||
|
arena_slot = true;
|
||||||
index = 0;
|
index = 0;
|
||||||
|
|
||||||
} else if (pPresentationParameters->BackBufferWidth == 1080) {
|
} else if (pPresentationParameters->BackBufferWidth == 1080) {
|
||||||
// LEFT/RIGHT
|
// LEFT/RIGHT
|
||||||
create_swap_chain = true;
|
create_swap_chain = true;
|
||||||
|
arena_slot = true;
|
||||||
index = 1;
|
index = 1;
|
||||||
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
|
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
|
||||||
index = 2;
|
index = 2;
|
||||||
@@ -357,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 (create_fake_swap_chain) {
|
||||||
if (!fake_sub_swapchain[index]) {
|
if (!fake_sub_swapchain[index]) {
|
||||||
log_info(
|
log_info(
|
||||||
@@ -487,6 +506,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
fake_sub_swapchain[0]->AddRef();
|
fake_sub_swapchain[0]->AddRef();
|
||||||
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
|
*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);
|
graphics_screens_register(iSwapChain);
|
||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
@@ -517,6 +554,64 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
|
|||||||
return n;
|
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(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
|
||||||
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <initguid.h>
|
#include <initguid.h>
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
@@ -234,6 +235,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
|
virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
|
||||||
#pragma endregion
|
#pragma endregion
|
||||||
|
|
||||||
|
// 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_two_head_exclusive() const;
|
||||||
bool is_gfdm_logical_small_swapchain(UINT swapchain) const;
|
bool is_gfdm_logical_small_swapchain(UINT swapchain) const;
|
||||||
bool is_gfdm_logical_side_swapchain(UINT swapchain) const;
|
bool is_gfdm_logical_side_swapchain(UINT swapchain) const;
|
||||||
@@ -262,6 +267,7 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
std::atomic_ulong refs = 1;
|
std::atomic_ulong refs = 1;
|
||||||
|
|
||||||
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
||||||
|
WrappedIDirect3DSwapChain9 *implicit_sub_swapchain = nullptr;
|
||||||
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
||||||
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
|
|
||||||
|
#include "games/gitadora/gitadora.h"
|
||||||
|
|
||||||
inline constexpr UINT GFDM_SIDE_WIDTH = 1080;
|
inline constexpr UINT GFDM_SIDE_WIDTH = 1080;
|
||||||
inline constexpr UINT GFDM_SIDE_HEIGHT = 1920;
|
inline constexpr UINT GFDM_SIDE_HEIGHT = 1920;
|
||||||
inline constexpr UINT GFDM_SMALL_WIDTH = 800;
|
inline constexpr UINT GFDM_SMALL_WIDTH = games::gitadora::ARENA_SUBSCREEN_WIDTH;
|
||||||
inline constexpr UINT GFDM_SMALL_HEIGHT = 1280;
|
inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
|
||||||
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
|
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
|
||||||
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
|
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -96,6 +96,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
|
|||||||
pDirtyRegion,
|
pDirtyRegion,
|
||||||
dwFlags);
|
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
|
// 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
|
// 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
|
// through another supported mode and restore it once, then retry the interrupted MAIN
|
||||||
|
|||||||
@@ -17,11 +17,13 @@
|
|||||||
#include "games/ddr/ddr.h"
|
#include "games/ddr/ddr.h"
|
||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
|
#include "games/io.h"
|
||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
#include "games/popn/popn.h"
|
#include "games/popn/popn.h"
|
||||||
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
|
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
|
||||||
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
|
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
|
#include "misc/hotkeys.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "touch/touch_gestures.h"
|
#include "touch/touch_gestures.h"
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "touch/native/inject.h"
|
#include "touch/native/inject.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
|
|
||||||
@@ -116,6 +119,8 @@ uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT = 0;
|
|||||||
// settings
|
// settings
|
||||||
std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146";
|
std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146";
|
||||||
std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots";
|
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(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr;
|
||||||
static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr;
|
static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr;
|
||||||
@@ -212,6 +217,18 @@ static void gitadora_remember_window(HWND hWnd, const std::string &window_name)
|
|||||||
} else if (window_name == "SMALL") {
|
} else if (window_name == "SMALL") {
|
||||||
GFDM_SUBSCREEN_WINDOW = hWnd;
|
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(
|
bool graphics_gitadora_prepare_two_head_device_window(
|
||||||
@@ -612,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_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");
|
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()
|
const std::string gfdm_window_name = games::gitadora::is_arena_model()
|
||||||
? gitadora_canonical_window_name(effective_window_name)
|
? gitadora_canonical_window_name(effective_window_name)
|
||||||
@@ -716,6 +735,20 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW);
|
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
|
// only hook touch window if multiple windows are allowed
|
||||||
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
|
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
|
||||||
gitadora_remember_window(result, gfdm_window_name);
|
gitadora_remember_window(result, gfdm_window_name);
|
||||||
@@ -727,6 +760,11 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
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) {
|
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
gitadora_force_window_style(result);
|
gitadora_force_window_style(result);
|
||||||
@@ -1100,6 +1138,15 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
|||||||
return true;
|
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
|
// call original
|
||||||
return ShowWindow_orig(hWnd, nCmdShow);
|
return ShowWindow_orig(hWnd, nCmdShow);
|
||||||
}
|
}
|
||||||
@@ -1316,7 +1363,9 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
|
|||||||
const bool native_touch_overlay =
|
const bool native_touch_overlay =
|
||||||
(games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
|
(games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
|
||||||
(games::popn::NATIVE_TOUCH &&
|
(games::popn::NATIVE_TOUCH &&
|
||||||
games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
|
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) {
|
if (native_touch_overlay) {
|
||||||
nativetouch::inject::register_and_attach_window(hWnd);
|
nativetouch::inject::register_and_attach_window(hWnd);
|
||||||
}
|
}
|
||||||
@@ -1374,6 +1423,12 @@ void graphics_screens_get(std::vector<int> &screens) {
|
|||||||
screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end());
|
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() {
|
void graphics_screenshot_trigger() {
|
||||||
GRAPHICS_SCREENSHOT_TRIGGER = true;
|
GRAPHICS_SCREENSHOT_TRIGGER = true;
|
||||||
}
|
}
|
||||||
@@ -1522,7 +1577,7 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string graphics_screenshot_genpath() {
|
std::string graphics_screenshot_genpath(const std::vector<int> &screens) {
|
||||||
|
|
||||||
// verify dir path
|
// verify dir path
|
||||||
if (GRAPHICS_SCREENSHOT_DIR.empty()) {
|
if (GRAPHICS_SCREENSHOT_DIR.empty()) {
|
||||||
@@ -1547,11 +1602,21 @@ std::string graphics_screenshot_genpath() {
|
|||||||
auto tm_now = *std::gmtime(&t_now);
|
auto tm_now = *std::gmtime(&t_now);
|
||||||
auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d"));
|
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;
|
size_t id = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id);
|
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;
|
return filepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ extern bool FAKE_SUBSCREEN_ADAPTER;
|
|||||||
// settings
|
// settings
|
||||||
extern std::string GRAPHICS_DEVICEID;
|
extern std::string GRAPHICS_DEVICEID;
|
||||||
extern std::string GRAPHICS_SCREENSHOT_DIR;
|
extern std::string GRAPHICS_SCREENSHOT_DIR;
|
||||||
|
extern bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY;
|
||||||
|
extern bool GRAPHICS_SCREENSHOT_SUBSCREENS;
|
||||||
|
|
||||||
// Direct3D 9 settings
|
// Direct3D 9 settings
|
||||||
extern std::optional<UINT> D3D9_ADAPTER;
|
extern std::optional<UINT> D3D9_ADAPTER;
|
||||||
@@ -119,6 +121,7 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
|
|||||||
|
|
||||||
void graphics_init();
|
void graphics_init();
|
||||||
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
|
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
|
||||||
|
bool graphics_gitadora_has_dedicated_subscreen();
|
||||||
// The native GITADORA two-head D3D9 group uses the game's named SMALL
|
// The native GITADORA two-head D3D9 group uses the game's named SMALL
|
||||||
// device window for the native physical SMALL head. The game requests
|
// device window for the native physical SMALL head. The game requests
|
||||||
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
|
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
|
||||||
@@ -134,6 +137,7 @@ void graphics_hook_subscreen_window(HWND hWnd);
|
|||||||
void graphics_screens_register(int screen);
|
void graphics_screens_register(int screen);
|
||||||
void graphics_screens_unregister(int screen);
|
void graphics_screens_unregister(int screen);
|
||||||
void graphics_screens_get(std::vector<int> &screens);
|
void graphics_screens_get(std::vector<int> &screens);
|
||||||
|
void graphics_poll_screenshot_hotkey();
|
||||||
void graphics_screenshot_trigger();
|
void graphics_screenshot_trigger();
|
||||||
bool graphics_screenshot_consume();
|
bool graphics_screenshot_consume();
|
||||||
void graphics_capture_trigger(int screen);
|
void graphics_capture_trigger(int screen);
|
||||||
@@ -144,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,
|
bool rgb = true, int quality = 80, bool downsample = true, int divide = 0,
|
||||||
uint64_t *timestamp = nullptr,
|
uint64_t *timestamp = nullptr,
|
||||||
int *width = nullptr, int *height = 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
|
// graphics_windowed.cpp
|
||||||
void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
|
void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
|
#include "games/popn/popn.h"
|
||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
|
#include "util/deferlog.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -24,12 +26,13 @@ constexpr UINT CODEPAGE_SHIFT_JIS = 932;
|
|||||||
static decltype(GetACP) *GetACP_orig = nullptr;
|
static decltype(GetACP) *GetACP_orig = nullptr;
|
||||||
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
|
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
|
||||||
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
|
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
|
||||||
|
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
|
||||||
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
|
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
|
||||||
|
|
||||||
#ifdef SPICE64
|
#ifdef SPICE64
|
||||||
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
|
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
|
||||||
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_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(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
|
||||||
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
|
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
|
||||||
#endif
|
#endif
|
||||||
@@ -182,9 +185,32 @@ static BOOL WINAPI IsDBCSLeadByte_hook (
|
|||||||
BYTE TestChar
|
BYTE TestChar
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
if (IsDBCSLeadByteEx_orig) {
|
||||||
|
return IsDBCSLeadByteEx_orig(CODEPAGE_SHIFT_JIS, TestChar);
|
||||||
|
}
|
||||||
|
|
||||||
return IsDBCSLeadByteEx(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
|
static
|
||||||
int
|
int
|
||||||
WINAPI
|
WINAPI
|
||||||
@@ -220,6 +246,8 @@ WideCharToMultiByte_hook(
|
|||||||
lpUsedDefaultChar);
|
lpUsedDefaultChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
int
|
int
|
||||||
WINAPI
|
WINAPI
|
||||||
GetLocaleInfoA_hook(
|
GetLocaleInfoA_hook(
|
||||||
@@ -250,6 +278,18 @@ GetLocaleInfoA_hook(
|
|||||||
void hooks::lang::early_init() {
|
void hooks::lang::early_init() {
|
||||||
log_info("hooks::lang", "early initialization");
|
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
|
// hooking these two functions fixes the jubeat mojibake
|
||||||
detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig);
|
detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig);
|
||||||
detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);
|
detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);
|
||||||
@@ -307,7 +347,25 @@ void hooks::lang::early_init() {
|
|||||||
&IsDBCSLeadByte_orig);
|
&IsDBCSLeadByte_orig);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (games::gitadora::is_arena_model() || avs::game::is_model("T44")) {
|
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");
|
log_info("hooks::lang", "hooking WideCharToMultiByte");
|
||||||
detour::trampoline_try(
|
detour::trampoline_try(
|
||||||
"kernel32.dll",
|
"kernel32.dll",
|
||||||
@@ -315,7 +373,6 @@ void hooks::lang::early_init() {
|
|||||||
WideCharToMultiByte_hook,
|
WideCharToMultiByte_hook,
|
||||||
&WideCharToMultiByte_orig);
|
&WideCharToMultiByte_orig);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <assert.h>
|
|
||||||
#include <shlwapi.h>
|
#include <shlwapi.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <cfg/configurator.h>
|
#include <cfg/configurator.h>
|
||||||
@@ -88,13 +87,13 @@
|
|||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "launcher/logger.h"
|
#include "launcher/logger.h"
|
||||||
#include "launcher/signal.h"
|
#include "launcher/signal.h"
|
||||||
#include "launcher/superexit.h"
|
|
||||||
#include "launcher/richpresence.h"
|
#include "launcher/richpresence.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "launcher/options.h"
|
#include "launcher/options.h"
|
||||||
#include "misc/bt5api.h"
|
#include "misc/bt5api.h"
|
||||||
#include "misc/device.h"
|
#include "misc/device.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
#include "misc/hotkeys.h"
|
||||||
#include "misc/extdev.h"
|
#include "misc/extdev.h"
|
||||||
#include "misc/ami2000.h"
|
#include "misc/ami2000.h"
|
||||||
#include "misc/sciunit.h"
|
#include "misc/sciunit.h"
|
||||||
@@ -524,22 +523,25 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
games::iidx::FLIP_CAMS = true;
|
games::iidx::FLIP_CAMS = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IIDX CONNECT_CAMERA logic here for cases where user is running without -iidx module
|
// Resolve the IIDX camera policy here so it also applies without the -iidx module.
|
||||||
// for now, we assume that user may be running on a cab
|
const auto &cab_camera_access = options[launcher::Options::IIDXCabCamAccess];
|
||||||
// (games::iidx::DISABLE_CAMS starts out as false unless user overrides)
|
if (cab_camera_access.is_active()) {
|
||||||
// we will check again in IIDX module with a different default
|
const auto value = cab_camera_access.value_text();
|
||||||
assert(!games::iidx::DISABLE_CAMS.has_value());
|
if (value == "off") {
|
||||||
if (options[launcher::Options::IIDXDisableCameras].value_bool()) {
|
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
|
||||||
games::iidx::DISABLE_CAMS = true;
|
} 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") {
|
if (options[launcher::Options::IIDXDisableCameras].value_bool()) {
|
||||||
games::iidx::DISABLE_CAMS = true;
|
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::IIDXCamHook].value_bool()) {
|
if (options[launcher::Options::IIDXCamHook].value_bool()) {
|
||||||
games::iidx::TDJ_CAMERA = true;
|
games::iidx::TDJ_CAMERA = true;
|
||||||
// Disable legacy behaviour to avoid conflict
|
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
|
||||||
games::iidx::DISABLE_CAMS = true;
|
|
||||||
}
|
}
|
||||||
// CONNECT_CAMERA env var will be set once logging is enabled
|
// CONNECT_CAMERA env var will be set once logging is enabled
|
||||||
|
|
||||||
@@ -1101,6 +1103,12 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::ScreenshotFolder].is_active()) {
|
if (options[launcher::Options::ScreenshotFolder].is_active()) {
|
||||||
GRAPHICS_SCREENSHOT_DIR = options[launcher::Options::ScreenshotFolder].value_text();
|
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()) {
|
if (options[launcher::Options::DisableColoredOutput].value_bool()) {
|
||||||
logger::COLOR = false;
|
logger::COLOR = false;
|
||||||
}
|
}
|
||||||
@@ -1717,14 +1725,13 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
GRAPHICS_FS_ORIENTATION_SWAP = true;
|
GRAPHICS_FS_ORIENTATION_SWAP = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply an explicit off outside of the -iidx module so it also works on cabinets.
|
||||||
// for cab usage - set environment variables (outside of -iidx module)
|
if (games::iidx::CAB_CAMERA_ACCESS == games::iidx::cab_camera_access_mode::off &&
|
||||||
if (games::iidx::DISABLE_CAMS.has_value() &&
|
|
||||||
games::iidx::DISABLE_CAMS.value() &&
|
|
||||||
!cfg::CONFIGURATOR_STANDALONE) {
|
!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
log_misc("launcher::iidx", "CONNECT_CAMERA env var set to 0");
|
log_misc("launcher::iidx", "CONNECT_CAMERA env var set to 0");
|
||||||
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (games::iidx::SOUND_OUTPUT_DEVICE.has_value() &&
|
if (games::iidx::SOUND_OUTPUT_DEVICE.has_value() &&
|
||||||
games::iidx::SOUND_OUTPUT_DEVICE.value() != "auto" &&
|
games::iidx::SOUND_OUTPUT_DEVICE.value() != "auto" &&
|
||||||
!cfg::CONFIGURATOR_STANDALONE) {
|
!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
@@ -1776,9 +1783,8 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
nvapi::initialize();
|
nvapi::initialize();
|
||||||
// add application profile to nvcp
|
// add application profile to nvcp
|
||||||
nvapi::set_profile_settings();
|
nvapi::set_profile_settings();
|
||||||
// enable super exit
|
// keep ALT+F4 available during lengthy non-standalone boot
|
||||||
superexit::enable();
|
hotkeys::start();
|
||||||
|
|
||||||
// enable subscreen touch emulation
|
// enable subscreen touch emulation
|
||||||
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
|
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
|
||||||
games::iidx::ENABLE_POKE = true;
|
games::iidx::ENABLE_POKE = true;
|
||||||
@@ -2451,6 +2457,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
|
|
||||||
// initialize raw input
|
// initialize raw input
|
||||||
RI_MGR = std::make_unique<rawinput::RawInputManager>();
|
RI_MGR = std::make_unique<rawinput::RawInputManager>();
|
||||||
|
hotkeys::enable_raw_input();
|
||||||
for (const auto &device : sextet_devices) {
|
for (const auto &device : sextet_devices) {
|
||||||
RI_MGR->sextet_register(device);
|
RI_MGR->sextet_register(device);
|
||||||
}
|
}
|
||||||
@@ -2473,6 +2480,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
log_misc("rawinput", "Analog mappings:");
|
log_misc("rawinput", "Analog mappings:");
|
||||||
dump_analog_bindings();
|
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)
|
// 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")) {
|
if (show_cursor_if_no_touch && !is_touch_available("launcher::main_implementation")) {
|
||||||
GRAPHICS_SHOW_CURSOR = true;
|
GRAPHICS_SHOW_CURSOR = true;
|
||||||
@@ -2702,9 +2712,6 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
|
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// start coin input thread
|
|
||||||
eamuse_coin_start_thread();
|
|
||||||
|
|
||||||
// pin macro
|
// pin macro
|
||||||
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
|
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
|
||||||
eamuse_pin_macro_start_thread();
|
eamuse_pin_macro_start_thread();
|
||||||
@@ -2754,6 +2761,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
log_info("launcher", "calling game entry");
|
log_info("launcher", "calling game entry");
|
||||||
avs::game::entry_main();
|
avs::game::entry_main();
|
||||||
|
|
||||||
|
// stop screenshot and coin polling; mapped SuperExit and ALT+F4 remain active
|
||||||
|
hotkeys::disable_input();
|
||||||
|
|
||||||
// clear presence
|
// clear presence
|
||||||
richpresence::shutdown();
|
richpresence::shutdown();
|
||||||
|
|
||||||
@@ -2791,9 +2801,6 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
// free api controller
|
// free api controller
|
||||||
API_CONTROLLER.reset();
|
API_CONTROLLER.reset();
|
||||||
|
|
||||||
// stop coin input thread
|
|
||||||
eamuse_coin_stop_thread();
|
|
||||||
|
|
||||||
eamuse_pin_macro_stop_thread();
|
eamuse_pin_macro_stop_thread();
|
||||||
|
|
||||||
// BT5API
|
// BT5API
|
||||||
@@ -2804,6 +2811,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
sdk::fini_sdk_modules();
|
sdk::fini_sdk_modules();
|
||||||
|
|
||||||
// stop raw input
|
// stop raw input
|
||||||
|
hotkeys::disable_raw_input();
|
||||||
RI_MGR.reset();
|
RI_MGR.reset();
|
||||||
|
|
||||||
// debug hook
|
// debug hook
|
||||||
@@ -2836,8 +2844,8 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
// dispose crypt
|
// dispose crypt
|
||||||
crypt::dispose();
|
crypt::dispose();
|
||||||
|
|
||||||
// disable super exit
|
// end early/late ALT+F4 monitoring at the same teardown point as legacy SuperExit
|
||||||
superexit::disable();
|
hotkeys::stop();
|
||||||
|
|
||||||
// disable poke
|
// disable poke
|
||||||
games::iidx::poke::disable();
|
games::iidx::poke::disable();
|
||||||
|
|||||||
@@ -640,17 +640,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// IIDXCabCamAccess
|
// IIDXCabCamAccess
|
||||||
.title = "IIDX Use Official AC Cams",
|
.title = "IIDX Official AC Camera Access",
|
||||||
.name = "iidxcabcams",
|
.name = "iidxcabcams",
|
||||||
.desc = "For IIDX25+, allow direct access to cameras from real arcade cabinets. "
|
.desc = "Controls how the game accesses USB cameras for IIDX 25+.\n\n"
|
||||||
"Only turn this on if you have OFFICIAL arcade cameras connected to the correct USB ports. Default: auto.",
|
"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,
|
.type = OptionType::Enum,
|
||||||
.game_name = "Beatmania IIDX",
|
.game_name = "Beatmania IIDX",
|
||||||
.category = "Cab Peripherals",
|
.category = "Cab Peripherals",
|
||||||
.elements = {
|
.elements = {
|
||||||
{"auto", ""},
|
{"auto", ""},
|
||||||
|
{"on", ""},
|
||||||
|
{"legacy", ""},
|
||||||
{"off", ""},
|
{"off", ""},
|
||||||
{"on", ""}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -977,7 +981,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.display_name = "sdvxsubredraw",
|
.display_name = "sdvxsubredraw",
|
||||||
.aliases= "sdvxsubredraw",
|
.aliases= "sdvxsubredraw",
|
||||||
.desc = "Check if submonitor in fullscreen mode doesn't update every frame; "
|
.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,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Sound Voltex",
|
.game_name = "Sound Voltex",
|
||||||
.category = "Advanced Game Options",
|
.category = "Advanced Game Options",
|
||||||
@@ -1171,7 +1175,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
|
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
|
||||||
.name = "popnsubredraw",
|
.name = "popnsubredraw",
|
||||||
.desc = "Check if submonitor in fullscreen mode appears stuck; "
|
.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,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Pop'n Music",
|
.game_name = "Pop'n Music",
|
||||||
.category = "Advanced Game Options",
|
.category = "Advanced Game Options",
|
||||||
@@ -3393,6 +3397,22 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "OBS Control",
|
.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) {
|
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
||||||
|
|||||||
@@ -320,7 +320,9 @@ namespace launcher {
|
|||||||
OBSWebSocketHost,
|
OBSWebSocketHost,
|
||||||
OBSWebSocketPort,
|
OBSWebSocketPort,
|
||||||
OBSWebSocketPassword,
|
OBSWebSocketPassword,
|
||||||
OBSWebSocketDebug
|
OBSWebSocketDebug,
|
||||||
|
ScreenshotIncludeOverlay,
|
||||||
|
ScreenshotSubscreens
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class OptionsCategory {
|
enum class OptionsCategory {
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
#include "superexit.h"
|
#include "superexit.h"
|
||||||
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
#include "windows.h"
|
#include "windows.h"
|
||||||
|
|
||||||
|
#include "cfg/configurator.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "rawinput/rawinput.h"
|
|
||||||
#include "util/logging.h"
|
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "misc/eamuse.h"
|
#include "util/logging.h"
|
||||||
#include "games/io.h"
|
|
||||||
#include "overlay/overlay.h"
|
|
||||||
|
|
||||||
namespace superexit {
|
namespace superexit {
|
||||||
|
|
||||||
static std::thread *THREAD = nullptr;
|
|
||||||
static bool THREAD_RUNNING = false;
|
|
||||||
|
|
||||||
bool has_focus() {
|
bool has_focus() {
|
||||||
HWND fg_wnd = GetForegroundWindow();
|
HWND fg_wnd = GetForegroundWindow();
|
||||||
if (fg_wnd == NULL) {
|
if (fg_wnd == NULL) {
|
||||||
@@ -29,92 +22,22 @@ namespace superexit {
|
|||||||
return fg_pid == GetCurrentProcessId();
|
return fg_pid == GetCurrentProcessId();
|
||||||
}
|
}
|
||||||
|
|
||||||
void enable() {
|
void handle_hotkeys(bool alt_f4, bool mapped_exit) {
|
||||||
|
if (!alt_f4 && !mapped_exit) {
|
||||||
// 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) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||||
// stop old thread
|
return;
|
||||||
THREAD_RUNNING = false;
|
}
|
||||||
THREAD->join();
|
if (!has_focus()) {
|
||||||
|
return;
|
||||||
// delete thread
|
}
|
||||||
delete THREAD;
|
if (alt_f4) {
|
||||||
THREAD = nullptr;
|
log_info("superexit", "detected ALT+F4, exiting...");
|
||||||
|
launcher::shutdown();
|
||||||
// log
|
return;
|
||||||
log_info("superexit", "disabled");
|
}
|
||||||
|
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
|
||||||
|
launcher::shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,5 @@
|
|||||||
|
|
||||||
namespace superexit {
|
namespace superexit {
|
||||||
bool has_focus();
|
bool has_focus();
|
||||||
void enable();
|
void handle_hotkeys(bool alt_f4, bool mapped_exit);
|
||||||
void disable();
|
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-55
@@ -25,10 +25,8 @@ static double CARD_INSERT_TIME[2] = {0, 0};
|
|||||||
static double CARD_INSERT_TIMEOUT = 2.0;
|
static double CARD_INSERT_TIMEOUT = 2.0;
|
||||||
static char CARD_INSERT_UID[2][8] = {{0}, {0}};
|
static char CARD_INSERT_UID[2][8] = {{0}, {0}};
|
||||||
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
|
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
|
||||||
static int COIN_STOCK = 0;
|
static std::atomic_int COIN_STOCK {0};
|
||||||
static bool COIN_BLOCK = false;
|
static std::atomic_bool COIN_BLOCK {false};
|
||||||
static std::thread *COIN_INPUT_THREAD;
|
|
||||||
static bool COIN_INPUT_THREAD_ACTIVE = false;
|
|
||||||
static uint16_t KEYPAD_STATE[] = {0, 0};
|
static uint16_t KEYPAD_STATE[] = {0, 0};
|
||||||
static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0};
|
static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0};
|
||||||
static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0};
|
static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0};
|
||||||
@@ -292,78 +290,49 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool eamuse_coin_get_block() {
|
bool eamuse_coin_get_block() {
|
||||||
return COIN_BLOCK;
|
return COIN_BLOCK.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_coin_set_block(bool block) {
|
void eamuse_coin_set_block(bool block) {
|
||||||
COIN_BLOCK = block;
|
COIN_BLOCK.store(block, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
int eamuse_coin_get_stock() {
|
int eamuse_coin_get_stock() {
|
||||||
return COIN_STOCK;
|
return COIN_STOCK.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_coin_set_stock(int amount) {
|
void eamuse_coin_set_stock(int amount) {
|
||||||
COIN_STOCK = amount;
|
COIN_STOCK.store(amount, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool eamuse_coin_consume(int amount) {
|
bool eamuse_coin_consume(int amount) {
|
||||||
if (COIN_STOCK < amount) {
|
auto stock = COIN_STOCK.load(std::memory_order_relaxed);
|
||||||
return false;
|
while (stock >= amount) {
|
||||||
} else {
|
if (COIN_STOCK.compare_exchange_weak(
|
||||||
COIN_STOCK -= amount;
|
stock,
|
||||||
return true;
|
stock - amount,
|
||||||
|
std::memory_order_relaxed)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int eamuse_coin_consume_stock() {
|
int eamuse_coin_consume_stock() {
|
||||||
int stock = COIN_STOCK;
|
return COIN_STOCK.exchange(0, std::memory_order_relaxed);
|
||||||
COIN_STOCK = 0;
|
|
||||||
return stock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int eamuse_coin_add() {
|
int eamuse_coin_add() {
|
||||||
return ++COIN_STOCK;
|
return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_coin_start_thread() {
|
void eamuse_coin_insert() {
|
||||||
|
if (COIN_BLOCK.load(std::memory_order_relaxed)) {
|
||||||
// set active
|
log_info("eamuse", "coin inserted while blocked");
|
||||||
COIN_INPUT_THREAD_ACTIVE = true;
|
} else {
|
||||||
|
log_info("eamuse", "coin insert");
|
||||||
// create thread
|
COIN_STOCK.fetch_add(1, std::memory_order_relaxed);
|
||||||
COIN_INPUT_THREAD = new std::thread([]() {
|
}
|
||||||
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
|
|
||||||
static bool COIN_INPUT_KEY_STATE = false;
|
|
||||||
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
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(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_pin_macro_start_thread() {
|
void eamuse_pin_macro_start_thread() {
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ bool eamuse_coin_consume(int amount);
|
|||||||
int eamuse_coin_consume_stock();
|
int eamuse_coin_consume_stock();
|
||||||
|
|
||||||
int eamuse_coin_add();
|
int eamuse_coin_add();
|
||||||
|
void eamuse_coin_insert();
|
||||||
void eamuse_coin_start_thread();
|
|
||||||
void eamuse_coin_stop_thread();
|
|
||||||
|
|
||||||
void eamuse_pin_macro_start_thread();
|
void eamuse_pin_macro_start_thread();
|
||||||
void eamuse_pin_macro_stop_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();
|
||||||
|
}
|
||||||
@@ -158,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)
|
overlay::SpiceOverlay::SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device)
|
||||||
: renderer(OverlayRenderer::D3D9), hWnd(hWnd), d3d(d3d), device(device) {
|
: renderer(OverlayRenderer::D3D9), hWnd(hWnd), d3d(d3d), device(device) {
|
||||||
log_info("overlay", "initializing (D3D9)");
|
log_info("overlay", "initializing (D3D9)");
|
||||||
@@ -924,6 +929,9 @@ bool overlay::SpiceOverlay::has_focus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool overlay::SpiceOverlay::hotkeys_triggered() {
|
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
|
// prevent hotkeys in spicecfg
|
||||||
if (cfg::CONFIGURATOR_STANDALONE) {
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ namespace overlay {
|
|||||||
bool fps_down = false;
|
bool fps_down = false;
|
||||||
bool hotkey_toggle = false;
|
bool hotkey_toggle = false;
|
||||||
bool hotkey_toggle_last = false;
|
bool hotkey_toggle_last = false;
|
||||||
|
std::mutex hotkeys_mutex;
|
||||||
|
|
||||||
// true between new_frame()'s ImGui::NewFrame() and render()'s ImGui::Render(),
|
// true between new_frame()'s ImGui::NewFrame() and render()'s ImGui::Render(),
|
||||||
// so render() never runs without a matching NewFrame.
|
// so render() never runs without a matching NewFrame.
|
||||||
@@ -235,4 +236,5 @@ namespace overlay {
|
|||||||
#endif
|
#endif
|
||||||
void create_software(HWND hWnd);
|
void create_software(HWND hWnd);
|
||||||
void destroy(HWND hWnd = nullptr);
|
void destroy(HWND hWnd = nullptr);
|
||||||
|
bool global_hotkeys_triggered();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ namespace overlay::windows {
|
|||||||
const ImVec4 YELLOW(1.f, 1.f, 0.f, 1.f);
|
const ImVec4 YELLOW(1.f, 1.f, 0.f, 1.f);
|
||||||
const ImVec4 WHITE(1.f, 1.f, 1.f, 1.f);
|
const ImVec4 WHITE(1.f, 1.f, 1.f, 1.f);
|
||||||
|
|
||||||
|
// a subscreen is an already-composited backbuffer, so its alpha bits are not
|
||||||
|
// meaningful for display. Use AddImage's diffuse alpha (opaque by default)
|
||||||
|
// instead of the texture alpha while preserving ImGui's normal blend state.
|
||||||
|
static void ignore_texture_alpha(const ImDrawList *, const ImDrawCmd *command) {
|
||||||
|
auto device = static_cast<IDirect3DDevice9 *>(command->UserCallbackData);
|
||||||
|
device->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
|
||||||
|
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2);
|
||||||
|
}
|
||||||
|
|
||||||
GenericSubScreen::GenericSubScreen(SpiceOverlay *overlay) : Window(overlay), device(overlay->get_device()) {
|
GenericSubScreen::GenericSubScreen(SpiceOverlay *overlay) : Window(overlay), device(overlay->get_device()) {
|
||||||
this->remove_window_padding = true;
|
this->remove_window_padding = true;
|
||||||
// ImGuiWindowFlags_NoBackground is needed as the background is drawn on top of the subscreen image
|
// ImGuiWindowFlags_NoBackground is needed as the background is drawn on top of the subscreen image
|
||||||
@@ -239,10 +248,13 @@ namespace overlay::windows {
|
|||||||
auto draw_list = this->draws_window
|
auto draw_list = this->draws_window
|
||||||
? ImGui::GetWindowDrawList()
|
? ImGui::GetWindowDrawList()
|
||||||
: ImGui::GetBackgroundDrawList();
|
: ImGui::GetBackgroundDrawList();
|
||||||
|
draw_list->AddCallback(ignore_texture_alpha, this->device);
|
||||||
draw_list->AddImage(
|
draw_list->AddImage(
|
||||||
reinterpret_cast<ImTextureID>(this->texture),
|
reinterpret_cast<ImTextureID>(this->texture),
|
||||||
overlay_content_top_left,
|
overlay_content_top_left,
|
||||||
bottom_right);
|
bottom_right);
|
||||||
|
// following ImGui draws should resume normal texture-alpha modulation
|
||||||
|
draw_list->AddCallback(ImGui::GetPlatformIO().DrawCallback_ResetRenderState);
|
||||||
|
|
||||||
if (this->draws_window) {
|
if (this->draws_window) {
|
||||||
// draw an invisible button so that it swallows mouse input
|
// draw an invisible button so that it swallows mouse input
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
|
#include <iterator>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -2543,6 +2544,30 @@ rawinput::Device *rawinput::RawInputManager::devices_get(const std::string &name
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool rawinput::RawInputManager::keyboard_combo_pressed(uint16_t first, uint16_t second) {
|
||||||
|
if (first >= 256 || second >= 256) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::recursive_mutex> devices_lock(this->devices_mutex);
|
||||||
|
for (auto &device : this->devices) {
|
||||||
|
if (device.type != rawinput::KEYBOARD ||
|
||||||
|
device.keyboardInfo == nullptr ||
|
||||||
|
device.mutex == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> device_lock(*device.mutex);
|
||||||
|
const auto &states = device.keyboardInfo->key_states;
|
||||||
|
for (size_t page = 0; page < std::size(states); page += 256) {
|
||||||
|
if (states[page + first] && states[page + second]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void rawinput::RawInputManager::add_callback_add(void *data, std::function<void (void *, Device *)> callback) {
|
void rawinput::RawInputManager::add_callback_add(void *data, std::function<void (void *, Device *)> callback) {
|
||||||
this->callback_add.push_back(DeviceCallback {
|
this->callback_add.push_back(DeviceCallback {
|
||||||
.data = data,
|
.data = data,
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ namespace rawinput {
|
|||||||
|
|
||||||
void __stdcall devices_print();
|
void __stdcall devices_print();
|
||||||
Device *devices_get(const std::string &name, bool updated = false);
|
Device *devices_get(const std::string &name, bool updated = false);
|
||||||
|
bool keyboard_combo_pressed(uint16_t first, uint16_t second);
|
||||||
|
|
||||||
inline std::list<Device> &devices_get() {
|
inline std::list<Device> &devices_get() {
|
||||||
return devices;
|
return devices;
|
||||||
|
|||||||
@@ -123,6 +123,30 @@ namespace nativetouch::inject {
|
|||||||
// asks the touch window thread to send an UPDATE when the game loop runs elsewhere
|
// asks the touch window thread to send an UPDATE when the game loop runs elsewhere
|
||||||
static UINT contact_refresh_message;
|
static UINT contact_refresh_message;
|
||||||
|
|
||||||
|
// the window that shows the touch surface; contacts can be registered from any thread,
|
||||||
|
// so this is published by the graphics window hooks rather than derived from the globals
|
||||||
|
static std::atomic<HWND> preferred_injection_window { nullptr };
|
||||||
|
|
||||||
|
void set_preferred_injection_window(HWND window) {
|
||||||
|
preferred_injection_window.store(window, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
// a game that publishes its touch surface needs injection only on that window
|
||||||
|
static bool is_published_touch_window(HWND window) {
|
||||||
|
const auto preferred = preferred_injection_window.load(std::memory_order_acquire);
|
||||||
|
return preferred == nullptr || window == preferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
// games can register several windows for touch; keep synthetic contacts on the one
|
||||||
|
// that actually shows the touch surface
|
||||||
|
static bool is_preferred_injection_window(HWND window) {
|
||||||
|
if (GRAPHICS_IIDX_WSUB) {
|
||||||
|
return window == TDJ_SUBSCREEN_WINDOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_published_touch_window(window);
|
||||||
|
}
|
||||||
|
|
||||||
// submit one synthetic contact frame to Windows touch injection
|
// submit one synthetic contact frame to Windows touch injection
|
||||||
static bool inject_touch_frame(
|
static bool inject_touch_frame(
|
||||||
POINT position, POINTER_FLAGS pointer_flags, bool retry_if_not_ready = false) {
|
POINT position, POINTER_FLAGS pointer_flags, bool retry_if_not_ready = false) {
|
||||||
@@ -325,11 +349,22 @@ namespace nativetouch::inject {
|
|||||||
if (contact_state.input_window == window) {
|
if (contact_state.input_window == window) {
|
||||||
release_active_contact();
|
release_active_contact();
|
||||||
}
|
}
|
||||||
|
|
||||||
HWND expected_window = window;
|
HWND expected_window = window;
|
||||||
injection_window.compare_exchange_strong(
|
injection_window.compare_exchange_strong(
|
||||||
expected_window, nullptr, std::memory_order_acq_rel);
|
expected_window, nullptr, std::memory_order_acq_rel);
|
||||||
|
|
||||||
|
HWND expected_preferred_window = window;
|
||||||
|
preferred_injection_window.compare_exchange_strong(
|
||||||
|
expected_preferred_window, nullptr, std::memory_order_acq_rel);
|
||||||
|
|
||||||
|
if (GRAPHICS_IIDX_WSUB) {
|
||||||
|
HWND expected_delivery_window = window;
|
||||||
|
touch_delivery_window.compare_exchange_strong(
|
||||||
|
expected_delivery_window, nullptr, std::memory_order_acq_rel);
|
||||||
|
}
|
||||||
|
|
||||||
contact_refresh_pending.store(false, std::memory_order_release);
|
contact_refresh_pending.store(false, std::memory_order_release);
|
||||||
touch_delivery_window.store(nullptr, std::memory_order_release);
|
|
||||||
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
|
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +405,7 @@ namespace nativetouch::inject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// publish the UI-thread target for synthetic touch requests
|
// publish the UI-thread target for synthetic touch requests
|
||||||
if (!GRAPHICS_IIDX_WSUB || window == TDJ_SUBSCREEN_WINDOW) {
|
if (is_preferred_injection_window(window)) {
|
||||||
injection_window.store(window, std::memory_order_release);
|
injection_window.store(window, std::memory_order_release);
|
||||||
}
|
}
|
||||||
log_misc(
|
log_misc(
|
||||||
@@ -403,7 +438,7 @@ namespace nativetouch::inject {
|
|||||||
// call original
|
// call original
|
||||||
const auto result = RegisterTouchWindow_orig(window, flags);
|
const auto result = RegisterTouchWindow_orig(window, flags);
|
||||||
|
|
||||||
if (result) {
|
if (result && is_published_touch_window(window)) {
|
||||||
// attach but don't register for touch messages
|
// attach but don't register for touch messages
|
||||||
// (we're already in the middle of RegisterTouchWindow as a result
|
// (we're already in the middle of RegisterTouchWindow as a result
|
||||||
// of the game calling it)
|
// of the game calling it)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ struct tagTOUCHINPUT;
|
|||||||
namespace nativetouch::inject {
|
namespace nativetouch::inject {
|
||||||
void attach_window(HWND window);
|
void attach_window(HWND window);
|
||||||
void register_and_attach_window(HWND window);
|
void register_and_attach_window(HWND window);
|
||||||
|
void set_preferred_injection_window(HWND window);
|
||||||
bool hook_available(HMODULE module);
|
bool hook_available(HMODULE module);
|
||||||
bool hook(HMODULE module);
|
bool hook(HMODULE module);
|
||||||
bool inject_synthetic_touch(POINT position, bool down);
|
bool inject_synthetic_touch(POINT position, bool down);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "transform.h"
|
#include "transform.h"
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
@@ -37,6 +38,11 @@ namespace nativetouch::transform {
|
|||||||
window == TDJ_SUBSCREEN_WINDOW;
|
window == TDJ_SUBSCREEN_WINDOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mouse-as-touch only applies while the cursor is over the target window
|
||||||
|
static bool is_cursor_over_window(HWND window, POINT position) {
|
||||||
|
return screen_to_game_client(window, &position);
|
||||||
|
}
|
||||||
|
|
||||||
// convert game touch coordinates to Windows desktop coordinates
|
// convert game touch coordinates to Windows desktop coordinates
|
||||||
bool game_to_screen(HWND window, POINT *position) {
|
bool game_to_screen(HWND window, POINT *position) {
|
||||||
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
||||||
@@ -65,7 +71,13 @@ namespace nativetouch::transform {
|
|||||||
return ClientToScreen(window, position) != FALSE;
|
return ClientToScreen(window, position) != FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool has_active_overlay_transform() {
|
static bool overlay_owns_touch_input() {
|
||||||
|
// the arena SMALL window is the touch surface whenever it exists, so the
|
||||||
|
// subscreen overlay must not claim touch input in those window modes
|
||||||
|
if (graphics_gitadora_has_dedicated_subscreen()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return overlay::OVERLAY != nullptr &&
|
return overlay::OVERLAY != nullptr &&
|
||||||
overlay::OVERLAY->get_active() &&
|
overlay::OVERLAY->get_active() &&
|
||||||
overlay::OVERLAY->has_subscreen_touch_transform();
|
overlay::OVERLAY->has_subscreen_touch_transform();
|
||||||
@@ -82,6 +94,30 @@ namespace nativetouch::transform {
|
|||||||
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
|
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SDVX still expects portrait coordinates when its image is rendered in landscape:
|
||||||
|
// (x, y) -> (width * (1 - y / height), height * x / width).
|
||||||
|
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height) {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto input_x = position->x;
|
||||||
|
position->x = width - MulDiv(position->y, width, height);
|
||||||
|
position->y = MulDiv(input_x, height, width);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the digitizer is mapped to the zero-based primary display, so the contact is already
|
||||||
|
// in the effective landscape resolution the rotation is based on
|
||||||
|
static bool transform_sdvx_landscape_touch_position(POINT *position) {
|
||||||
|
const auto landscape_width = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||||
|
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT);
|
||||||
|
const auto landscape_height = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||||
|
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH);
|
||||||
|
|
||||||
|
return sdvx_landscape_rotate(position, landscape_width, landscape_height);
|
||||||
|
}
|
||||||
|
|
||||||
// convert physical screen coordinates to game touch coordinates for a known target
|
// convert physical screen coordinates to game touch coordinates for a known target
|
||||||
bool screen_to_game(HWND window, POINT *position) {
|
bool screen_to_game(HWND window, POINT *position) {
|
||||||
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
||||||
@@ -112,7 +148,7 @@ namespace nativetouch::transform {
|
|||||||
|
|
||||||
// check if subscreen overlay is active and can transform the touch point;
|
// check if subscreen overlay is active and can transform the touch point;
|
||||||
// if not, the touch point is valid as-is
|
// if not, the touch point is valid as-is
|
||||||
if (!has_active_overlay_transform()) {
|
if (!overlay_owns_touch_input()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +163,18 @@ namespace nativetouch::transform {
|
|||||||
return screen_to_game(window, position);
|
return screen_to_game(window, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exception: sdvx windowed subscreen does not use the subscreen overlay transform
|
||||||
|
if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) {
|
||||||
|
return is_cursor_over_window(window, *position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// exception: the arena SMALL window is the touch panel, so accept the mouse there
|
||||||
|
// (and only there) with the coordinates a real contact on it would produce
|
||||||
|
if (graphics_gitadora_has_dedicated_subscreen()) {
|
||||||
|
return window == GFDM_SUBSCREEN_WINDOW &&
|
||||||
|
is_cursor_over_window(window, *position);
|
||||||
|
}
|
||||||
|
|
||||||
// if this game has a subscreen overlay that can transform touch input
|
// if this game has a subscreen overlay that can transform touch input
|
||||||
// but the window is hidden or not under the cursor, reject mouse-as-touch
|
// but the window is hidden or not under the cursor, reject mouse-as-touch
|
||||||
// (e.g., iidx/sdvx are rejected here, but nostalgia is allowed)
|
// (e.g., iidx/sdvx are rejected here, but nostalgia is allowed)
|
||||||
@@ -142,7 +190,14 @@ namespace nativetouch::transform {
|
|||||||
// route hardware screen coordinates through dedicated or overlay mapping and report the result
|
// route hardware screen coordinates through dedicated or overlay mapping and report the result
|
||||||
Result hardware_to_game(POINT *position) {
|
Result hardware_to_game(POINT *position) {
|
||||||
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
|
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
|
||||||
const auto active_overlay = has_active_overlay_transform();
|
const auto active_overlay = overlay_owns_touch_input();
|
||||||
|
|
||||||
|
// special case for SDVX landscape mode
|
||||||
|
if (!dedicated_subscreen && !active_overlay &&
|
||||||
|
GRAPHICS_FS_ORIENTATION_SWAP && avs::game::is_model("KFC")) {
|
||||||
|
return transform_sdvx_landscape_touch_position(position) ?
|
||||||
|
Result::Transformed : Result::Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
// no dedicated subscreen or active overlay mapping; pass the point through unchanged
|
// no dedicated subscreen or active overlay mapping; pass the point through unchanged
|
||||||
if (!dedicated_subscreen && !active_overlay) {
|
if (!dedicated_subscreen && !active_overlay) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ namespace nativetouch::transform {
|
|||||||
};
|
};
|
||||||
|
|
||||||
bool is_tdj_dedicated_subscreen(HWND window);
|
bool is_tdj_dedicated_subscreen(HWND window);
|
||||||
|
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height);
|
||||||
bool game_to_screen(HWND window, POINT *position);
|
bool game_to_screen(HWND window, POINT *position);
|
||||||
bool screen_to_game(HWND window, POINT *position);
|
bool screen_to_game(HWND window, POINT *position);
|
||||||
bool mouse_to_game(HWND window, POINT *position);
|
bool mouse_to_game(HWND window, POINT *position);
|
||||||
|
|||||||
Reference in New Issue
Block a user