Compare commits

...

10 Commits

Author SHA1 Message Date
bicarus 82e0c053d0 overlay: create a dedicated thread for polling hotkeys (#864)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Create a new thread that polls for the following hotkeys:

* super exit (both alt+f4 and bound key)
* coin insert
* screenshot

The goal is to make the capture of these more reliable, because before
this PR it wasn't.

## Testing
2026-08-12 20:04:59 -07:00
James Liu 3b29227dbc gitadora: fix XG2 gibberish text showing (#863)
> [!NOTE]
> Before submitting code changes... 
> * Please do note that this is a GPL v3.0 open source project.
> * Please read the
[CONTRIBUTING](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md)
guide.
> * Maintainers reserve the right to reject or modify your submission
without reason.
> * No new compiler warnings must be introduced. Check the CI build
results.
> 
> Feel free to remove this section after you have read it.

## Link to GitHub Issue or related Pull Request, if one exists

## Description of change
Fix gibberish in dynamic Japanese text in GuitarFreaks/DrumMania XG2
(K32/K33) when Windows uses a non-Japanese system code page.

XG2 converts UTF-8 property strings through WideCharToMultiByte(CP_ACP)
before rendering. On systems where the active code page is not
Shift-JIS, these strings are converted using the system code page and
become corrupted or turn into question marks.

Spice2x already hooks WideCharToMultiByte and redirects these
conversions to CP932 for newer 64-bit Gitadora models. This change makes
the existing hook available to 32-bit builds and enables it specifically
for XG2 models K32 and K33.

The existing 64-bit Gitadora Arena and T44 conditions remain unchanged.

## Testing
Before
<img width="1280" height="745" alt="image"
src="https://github.com/user-attachments/assets/34aa2c93-89e8-4c4f-b267-6cbc236f19de"
/>
<img width="2054" height="1188" alt="image"
src="https://github.com/user-attachments/assets/d88b5770-5545-4402-b9e7-3126fe048fb9"
/>

After
<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/d43b4e47-b6af-4b97-8898-53849c9a0ca4"
/>
<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/884301e2-1edd-4ad2-92ac-ece656853fe7"
/>

- Tested GuitarFreaks XG2 on a non-Japanese (English US) Windows.
- Verified that Community Log preset comments render correctly in
Japanese.
- Verified that Cooperation Challenge descriptions, rewards, and
progress text render correctly.
- Confirmed that the previous mojibake and question marks no longer
appear.
- GitHub Actions build completed successfully for both architectures.
2026-08-12 00:08:05 -07:00
bicarus ce2f93d234 graphics: refactor DX9 screenshot (part 2) (#861) 2026-08-11 01:11:42 -07:00
bicarus 13a171f199 sdvx: fix api touch in windowed mode (#862)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #858 

## Description of change
Fix orientation being wrong in windowed mode when API touch is used

Fix mouse no longer working when API client is in use.

This PR only affects windowed mode SDVX.

## Testing
2026-08-10 18:58:48 -07:00
bicarus 3da352488e graphics: refactor screenshots (#860)
First commit - pure refactoring, no functional changes

Second commit - clean up & minor bug fix.
2026-08-09 20:47:38 -07:00
bicarus 2386047c2f sdvx: fix touchscreen and API touch in landscape mode (#859)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #858 

## Description of change
Landscape mode needs another adjustment applied to touch coordinates.

This fixex both native touch and wintouchemu paths.

## Testing
Tested landscape, also with full screen resolution changes.
2026-08-08 16:22:29 -07:00
bicarus baa550037a api: sdvx tape led (#857)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #852

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

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

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

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

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

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

New truth table for `-iidxcabcams` :

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

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

## Description of change

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

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

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

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

## Testing
2026-08-04 09:04:29 -07:00
41 changed files with 1051 additions and 594 deletions
+3
View File
@@ -332,6 +332,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
api/modules/control.cpp
api/modules/touch.cpp
api/modules/iidx.cpp
api/modules/sdvx.cpp
api/serial.cpp
api/modules/drs.cpp
api/modules/lcd.cpp
@@ -535,6 +536,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/graphics/nvapi_hook.cpp
hooks/graphics/nvenc_hook.cpp
hooks/graphics/backends/d3d9/d3d9_backend.cpp
hooks/graphics/backends/d3d9/d3d9_screenshot.cpp
hooks/graphics/backends/d3d9/d3d9_device.cpp
hooks/graphics/backends/d3d9/d3d9_gfdm.cpp
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
@@ -576,6 +578,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
misc/device.cpp
misc/eamuse.cpp
misc/extdev.cpp
misc/hotkeys.cpp
misc/sciunit.cpp
misc/sde.cpp
misc/wintouchemu.cpp
+14
View File
@@ -267,6 +267,20 @@ which also means that your hex edits are applicable directly.
- `Side Panel Right Inner`
- `Side Panel Right`
#### SDVX
- tapeled_get(name: str, ...)
- returns a list containing a dict of the current tape LED states. The dict keys are:
- `Title`
- `Upper Left Speaker`
- `Upper Right Speaker`
- `Left Wing`
- `Right Wing`
- `Control Panel`
- `Lower Left Speaker`
- `Lower Right Speaker`
- `Woofer`
- `V Unit`
#### LCD
- info()
- returns information about the serial LCD controller some games use
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <atomic>
#include <cstdint>
namespace api {
extern std::atomic_uint32_t CLIENT_COUNT;
inline bool has_clients() {
return CLIENT_COUNT.load(std::memory_order_relaxed) > 0;
}
}
+9
View File
@@ -5,6 +5,7 @@
#include <utility>
#include "client.h"
#include "cfg/configurator.h"
#include "external/rapidjson/document.h"
#include "util/crypt.h"
@@ -28,6 +29,7 @@
#include "modules/lcd.h"
#include "modules/lights.h"
#include "modules/memory.h"
#include "modules/sdvx.h"
#include "modules/touch.h"
#include "modules/resize.h"
#include "request.h"
@@ -36,6 +38,8 @@
using namespace rapidjson;
using namespace api;
std::atomic_uint32_t api::CLIENT_COUNT = 0;
Controller::Controller(unsigned short port, std::string password, bool pretty)
: port(port), password(std::move(password)), pretty(pretty)
{
@@ -411,8 +415,11 @@ void Controller::init_state(api::ClientState *state) {
state->modules.push_back(new modules::LCD());
state->modules.push_back(new modules::Lights());
state->modules.push_back(new modules::Memory());
state->modules.push_back(new modules::SDVX());
state->modules.push_back(new modules::Touch());
state->modules.push_back(new modules::Resize());
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
}
void Controller::free_state(api::ClientState *state) {
@@ -424,6 +431,8 @@ void Controller::free_state(api::ClientState *state) {
// free cipher
delete state->cipher;
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
}
void Controller::free_socket() {
+4 -2
View File
@@ -106,14 +106,16 @@ namespace api::modules {
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
// Create an array for the light state
Value light_state(kArrayType);
light_state.Reserve(mapping.data.capacity() * 3, res.doc()->GetAllocator());
light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator());
}
// Can't use StringRef here, turns some strings partially into null bytes for some reason
// can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
}
+67
View File
@@ -0,0 +1,67 @@
#include "sdvx.h"
#include <functional>
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
SDVX::SDVX() : Module("sdvx") {
functions["tapeled_get"] = std::bind(&SDVX::tapeled_get, this, _1, _2);
for (auto &light : games::sdvx::TAPELED_MAPPING) {
lights_by_names.emplace(light.lightName, light);
}
}
/**
* tapeled_get()
* tapeled_get(name: str, ...)
*/
void SDVX::tapeled_get(Request &req, Response &res) {
Value response_object(kObjectType);
// all tape leds
if (req.params.Size() == 0) {
// iterate through each device and dump its lights data into the response
for (const auto &mapping : games::sdvx::TAPELED_MAPPING) {
copy_tapeled_data(res, response_object, mapping);
}
} else {
// specified light names
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsString()) {
error_type(res, "name", "string");
return;
}
const auto name = param.GetString();
if (const auto &it = lights_by_names.find(name); it != lights_by_names.end()) {
copy_tapeled_data(res, response_object, it->second.get());
}
}
}
res.add_data(response_object);
}
void SDVX::copy_tapeled_data(Response &res, Value &response_object,
const tapeledutils::tape_led &mapping)
{
Value light_state(kArrayType);
light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator());
}
// can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <functional>
#include <string>
#include "api/module.h"
#include "api/request.h"
#include "external/robin_hood.h"
#include "games/sdvx/sdvx.h"
namespace api::modules {
class SDVX : public Module {
public:
SDVX();
private:
robin_hood::unordered_map<std::string, std::reference_wrapper<tapeledutils::tape_led>> lights_by_names;
void tapeled_get(Request &req, Response &res);
void copy_tapeled_data(Response &res, rapidjson::Value &response_object,
const tapeledutils::tape_led &mapping);
};
}
+8 -5
View File
@@ -55,9 +55,12 @@ namespace api::modules {
native_canvas_w = 0;
native_canvas_h = 0;
if (is_sdvx) {
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata
native_canvas_w = 1080;
native_canvas_h = 1920;
// windowed and landscape API coordinates already match the primary screen orientation;
// fullscreen portrait coordinates are rotated by apply_touch_errata
const bool landscape_coordinates =
GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP;
native_canvas_w = landscape_coordinates ? 1920 : 1080;
native_canvas_h = landscape_coordinates ? 1080 : 1920;
} else if (avs::game::is_model("LDJ")) {
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
@@ -218,8 +221,8 @@ namespace api::modules {
// the target of the touch events so just assume it's the sub screen
x = x_raw * 1920 / 1280;
y = y_raw * 1080 / 720;
} else if (is_sdvx) {
// for exceed gear, they are both 1080p screens, but need to apply transformation
} else if (is_sdvx && !GRAPHICS_WINDOWED && !GRAPHICS_FS_ORIENTATION_SWAP) {
// rotate API coordinates into SDVX's portrait touch space
x = 1080 - y_raw;
y = x_raw;
}
@@ -7,6 +7,7 @@ from .coin import *
from .control import *
from .exceptions import *
from .iidx import *
from .sdvx import *
from .info import *
from .keypads import *
from .lights import *
@@ -0,0 +1,12 @@
from .connection import Connection
from .request import Request
def sdvx_tapeled_get(con: Connection, *light_names):
req = Request("sdvx", "tapeled_get")
for light_name in light_names:
req.add_param(light_name)
res = con.request(req)
return res.get_data()
+1
View File
@@ -6,6 +6,7 @@
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<dpiAware>true</dpiAware>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">Legacy</activeCodePage>
</asmv3:windowsSettings>
</asmv3:application>
<dependency>
+7 -4
View File
@@ -3,6 +3,7 @@
#if SPICE64
#include <cstdint>
#include "api/client.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -443,10 +444,12 @@ namespace games::iidx {
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
for (unsigned int i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
if (api::has_clients()) {
for (size_t i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
}
}
+6 -18
View File
@@ -58,7 +58,7 @@ namespace games::iidx {
// settings
bool FLIP_CAMS = false;
std::optional<bool> DISABLE_CAMS;
cab_camera_access_mode CAB_CAMERA_ACCESS = cab_camera_access_mode::automatic;
bool TDJ_CAMERA = false;
bool TDJ_CAMERA_PREFER_16_9 = true;
bool TDJ_MODE = false;
@@ -424,10 +424,7 @@ namespace games::iidx {
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
// check if cam hook should be enabled
if (!DISABLE_CAMS.has_value()) {
log_fatal("iidx", "assertion failure - DISABLE_CAMS not set during attach");
}
if (!DISABLE_CAMS.value()) {
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::legacy) {
init_legacy_camera_hook(FLIP_CAMS);
}
@@ -454,19 +451,10 @@ namespace games::iidx {
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
}
// check for cab camera access for the second time (first time was in launcher.cpp)
// this time, we are inside -iidx module hook, which means the user is likely NOT on a cab
// therefore, start with cams OFF by default, and allow user to forcibly override to ON
if (!games::iidx::DISABLE_CAMS.has_value()) {
games::iidx::DISABLE_CAMS = true;
if (options->at(launcher::Options::IIDXCabCamAccess).is_active() &&
options->at(launcher::Options::IIDXCabCamAccess).value_text() == "on") {
games::iidx::DISABLE_CAMS = false;
}
if (games::iidx::DISABLE_CAMS.value()) {
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
// auto with iidx module means turn off camera (non-cab use)
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::automatic) {
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
// windowed subscreen, enabled by default, unless turned off by user
+8 -1
View File
@@ -11,6 +11,13 @@
namespace games::iidx {
enum class cab_camera_access_mode {
automatic,
off,
on,
legacy,
};
enum class iidx_aio_emulation_state {
unknown,
bi2a_com2,
@@ -20,7 +27,7 @@ namespace games::iidx {
// settings
extern bool FLIP_CAMS;
extern std::optional<bool> DISABLE_CAMS;
extern cab_camera_access_mode CAB_CAMERA_ACCESS;
extern bool TDJ_CAMERA;
extern bool TDJ_CAMERA_PREFER_16_9;
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
+13 -26
View File
@@ -3,6 +3,7 @@
#if SPICE64
#include <cstdint>
#include "api/client.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -349,43 +350,29 @@ namespace games::sdvx {
* 9 - v unit - 258 bytes - 86 colors
*
* data is stored in RGB order, 3 bytes per color
*
* TODO: expose this data via API
*/
// data mapping
static struct TapeLedMapping {
size_t data_size;
int index_r, index_g, index_b;
TapeLedMapping(size_t data_size, int index_r, int index_g, int index_b)
: data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {}
} mapping[] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B },
};
// check index bounds
if (tapeledutils::is_enabled() && index < std::size(mapping)) {
auto &map = mapping[index];
if (tapeledutils::is_enabled() && index < std::size(TAPELED_MAPPING)) {
auto &map = TAPELED_MAPPING[index];
const auto data_size = map.data.size();
// pick a color to use
const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size);
const auto rgb = tapeledutils::pick_color_from_led_tape(data, data_size);
// program the lights into API
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
if (api::has_clients()) {
for (size_t i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
}
}
if (This != custom_node) {
+13
View File
@@ -61,6 +61,19 @@ namespace games::sdvx {
static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr;
tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B, "Title" },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B, "Upper Left Speaker" },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B, "Upper Right Speaker" },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B, "Left Wing" },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B, "Right Wing" },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B, "Control Panel" },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B, "Lower Left Speaker" },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B, "Lower Right Speaker" },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B, "Woofer" },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B, "V Unit" },
};
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (lpSubKey != nullptr &&
phkResult != nullptr &&
+4
View File
@@ -6,6 +6,7 @@
#include "avs/game.h"
#include "games/game.h"
#include "util/tapeled.h"
namespace games::sdvx {
@@ -25,6 +26,9 @@ namespace games::sdvx {
// states
extern bool SHOW_VM_MONITOR_WARNING;
constexpr int SDVX_TAPELED_TOTAL = 10;
extern tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL];
static inline bool is_valkyrie_model() {
return (
avs::game::is_model("KFC") &&
@@ -23,10 +23,7 @@
#include "external/imgui/backends/imgui_impl_dx11.h"
#include "overlay/imgui/impl_spice.h"
#include "games/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h"
#include "util/utils.h"
// --------------------------------------------------------------------------
@@ -149,26 +146,12 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
device->Release();
}
// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour).
void poll_screenshot_hotkey() {
static bool s_down = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game());
const bool pressed = buttons
&& (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered())
&& GameAPI::Buttons::getState(RI_MGR,
buttons->at(games::OverlayButtons::Screenshot));
if (pressed && !s_down) {
graphics_screenshot_trigger();
}
s_down = pressed;
}
void pump_overlay(IDXGISwapChain *swapchain) {
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) {
return;
}
poll_screenshot_hotkey();
graphics_poll_screenshot_hotkey();
// size imgui to the backbuffer (not window client). dxgi may upscale
// a small backbuffer into a larger client rect; without this override
@@ -1,15 +1,12 @@
#include "d3d9_backend.h"
#include <atomic>
#include <cassert>
#include <memory>
#include <thread>
#include <vector>
#include <external/robin_hood.h>
#include <d3d9.h>
#ifdef __GNUC__
#include <d3dx9tex.h>
#endif
#include "avs/game.h"
#include "cfg/screen_resize.h"
@@ -18,28 +15,23 @@
#include "games/popn/popn.h"
#include "games/sdvx/sdvx.h"
#include "games/mfc/mfc.h"
#include "games/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "launcher/options.h"
#include "launcher/signal.h"
#include "launcher/shutdown.h"
#include "misc/clipboard.h"
#include "misc/eamuse.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "overlay/notifications.h"
#include "util/detour.h"
#include "util/deferlog.h"
#include "util/fileutils.h"
#include "util/flags_helper.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/utils.h"
#include "util/memutils.h"
#include "util/threadpool.h"
#include "d3d9_device.h"
#include "d3d9_screenshot.h"
#ifdef min
#undef min
@@ -63,19 +55,6 @@
return __ret; \
} while (0)
#ifdef __GNUC__
typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t;
#else
#define D3DXIFF_PNG ((DWORD) 3)
typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)(
LPCSTR pDestFile,
DWORD DestFormat,
LPDIRECT3DSURFACE9 pSrcSurface,
CONST PALETTEENTRY *pSrcPalette,
CONST RECT *pSrcRect);
#endif
/*
* 9 on 12
*/
@@ -99,8 +78,6 @@ typedef IDirect3D9* (WINAPI *Direct3DCreate9On12_t)(
static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr;
static char D3D9_DIRECT3D_CREATE9_CONTENTS[16];
static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false;
// settings
std::optional<UINT> D3D9_ADAPTER = std::nullopt;
DWORD D3D9_BEHAVIOR_DISABLE = 0;
@@ -113,6 +90,12 @@ static Direct3DCreate9On12Ex_t Direct3DCreate9On12Ex_orig = nullptr;
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
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(
IDirect3DDevice9Ex *device,
D3DPRESENT_PARAMETERS *present_params,
@@ -1445,6 +1428,12 @@ IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen() {
return surface;
}
void graphics_d3d9_notify_subscreen_present() {
if (SUBSCREEN_FORCE_REDRAW && !SUBSCREEN_FORCE_REDRAW_IN_PROGRESS) {
SUBSCREEN_PRESENTED_SINCE_LAST_MAIN.store(true, std::memory_order_relaxed);
}
}
static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
// iidx/sdvx
int swapchain = 1;
@@ -1472,184 +1461,20 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
//
// early versions of popn HC needs this as well, but not on by default as it can cause
// graphical glitches on some GPUs
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) {
//
// treat forced redraw as a fallback so it does not duplicate a successful game present.
const bool force_redraw = SUBSCREEN_FORCE_REDRAW &&
!SUBSCREEN_PRESENTED_SINCE_LAST_MAIN.exchange(false, std::memory_order_relaxed);
if (GRAPHICS_WINDOWED || force_redraw) {
SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = true;
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = false;
}
}
}
static void save_capture(
int screen,
D3DFORMAT format,
UINT width,
UINT height,
IDirect3DSurface9 *surface) {
HRESULT hr;
// lock surface to be able to access the data
D3DLOCKED_RECT finished_copy {};
hr = surface->LockRect(&finished_copy, nullptr, 0);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
graphics_capture_skip(screen);
return;
}
// copy pixel data
size_t pitch = finished_copy.Pitch;
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
auto pixels = new uint8_t[width * height * 3];
for (size_t row = 0; row < height; row++) {
size_t offset_pixels = 0;
size_t offset_row = row * width * 3;
switch (format) {
case D3DFMT_R8G8B8: {
for (size_t offset = 0; offset < pitch; offset += 3) {
auto cell = data + row * pitch + offset;
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = cell[0];
pixel[1] = cell[1];
pixel[2] = cell[2];
offset_pixels += 3;
}
break;
}
case D3DFMT_X8R8G8B8:
case D3DFMT_A8R8G8B8: {
for (size_t offset = 0; offset < pitch; offset += 4) {
auto cell = data + row * pitch + offset;
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = cell[2];
pixel[1] = cell[1];
pixel[2] = cell[0];
offset_pixels += 3;
}
break;
}
case D3DFMT_X8B8G8R8:
case D3DFMT_A8B8G8R8: {
for (size_t offset = 0; offset < pitch; offset += 4) {
auto cell = data + row * pitch + offset;
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = cell[0];
pixel[1] = cell[1];
pixel[2] = cell[2];
offset_pixels += 3;
}
break;
}
default: {
for (size_t offset = 0; offset < width; offset++) {
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = 0;
pixel[1] = 0;
pixel[2] = 0;
offset_pixels += 3;
}
}
}
}
// unlock surface
hr = surface->UnlockRect();
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
graphics_capture_skip(screen);
return;
}
// enqueue
graphics_capture_enqueue(screen, pixels, width, height);
}
static void save_screenshot(const std::string &file_path, UINT height, IDirect3DSurface9 *surface) {
HRESULT hr;
D3DLOCKED_RECT finished_copy {};
hr = surface->LockRect(&finished_copy, nullptr, 0);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
return;
}
// set alpha channel to 255
{
auto pitch = finished_copy.Pitch;
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
for (size_t i = 0; i < height; i++) {
for (int j = 3; j < pitch; j += 4) {
data[i * pitch + j] = 255;
}
}
}
hr = surface->UnlockRect();
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
return;
}
// lazy load function
static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr;
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>("D3DXSaveSurfaceToFileA");
// check if function was not found, likely because d3dx9 is not loaded
if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) {
ATTEMPTED_D3DX9_LOAD_LIBRARY = true;
for (size_t i = 43; i >= 24; i--) {
auto lib_name = fmt::format("d3dx9_{}.dll", i);
auto d3dx9 = libutils::try_library(lib_name);
// Check if library was not found
if (d3dx9 == nullptr) {
continue;
}
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>(
d3dx9, "D3DXSaveSurfaceToFileA");
// Check if function was not found
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
FreeLibrary(d3dx9);
d3dx9 = nullptr;
continue;
}
log_info("graphics::d3d9", "found surface save function in '{}'", lib_name);
break;
}
}
}
if (D3DXSaveSurfaceToFileA_ptr != nullptr) {
// save to file
log_info("graphics::d3d9", "saving screenshot to {}", file_path);
auto hr = D3DXSaveSurfaceToFileA_ptr(file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "Failed to save screenshot");
overlay::notifications::add(
overlay::notifications::Severity::Error,
"Screenshot failed to save");
return;
}
// save to clipboard
clipboard::copy_image(file_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
} else {
log_warning("graphics::d3d9", "Direct3D save helper function not available");
}
}
void graphics_d3d9_on_present(
HWND hFocusWindow,
IDirect3DDevice9 *device,
@@ -1697,135 +1522,8 @@ void graphics_d3d9_on_present(
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);
}
}
graphics_poll_screenshot_hotkey();
graphics_d3d9_process_screenshot_and_capture(device, SUB_SWAP_CHAIN);
}
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
@@ -15,6 +15,8 @@ void graphics_d3d9_on_present(
IDirect3DDevice9 *device,
IDirect3DDevice9 *wrapped_device);
void graphics_d3d9_notify_subscreen_present();
IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen();
struct WrappedIDirect3D9 : IDirect3D9Ex {
@@ -134,6 +134,10 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
this->main_swapchain->Release();
this->main_swapchain = nullptr;
}
if (this->implicit_sub_swapchain) {
this->implicit_sub_swapchain->Release();
this->implicit_sub_swapchain = nullptr;
}
for (auto &sc : this->sub_swapchain) {
if (sc) {
sc->Release();
@@ -487,6 +491,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
fake_sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
graphics_screens_register(iSwapChain);
return D3D_OK;
} else if (SUBSCREEN_FORCE_REDRAW) {
// store implicit sub swap chain
if (!implicit_sub_swapchain) {
IDirect3DSwapChain9 *real_swapchain = nullptr;
HRESULT ret = pReal->GetSwapChain(iSwapChain, &real_swapchain);
if (FAILED(ret)) {
return ret;
}
implicit_sub_swapchain = new WrappedIDirect3DSwapChain9(this, real_swapchain);
implicit_sub_swapchain->should_run_hooks = false;
}
implicit_sub_swapchain->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(implicit_sub_swapchain);
graphics_screens_register(iSwapChain);
return D3D_OK;
}
@@ -262,6 +262,7 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::atomic_ulong refs = 1;
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *implicit_sub_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
@@ -0,0 +1,390 @@
#include "d3d9_screenshot.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#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"
#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;
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 {
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 void 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;
}
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;
}
}
// 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) {
// 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");
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");
}
}
static std::optional<BackbufferCopy> acquire_backbuffer_copy(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *sub_swap_chain, int screen) {
HRESULT hr = S_OK;
// TODO: verify screen is a valid swapchain
IDirect3DSurface9 *buffer = nullptr;
if (sub_swap_chain != nullptr && screen & 1) {
hr = sub_swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
} else {
hr = device->GetBackBuffer(screen, 0, D3DBACKBUFFER_TYPE_MONO, &buffer);
}
if (FAILED(hr) || buffer == nullptr) {
log_warning("graphics::d3d9",
"failed to get back buffer, hr={}",
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 {
.desc = desc,
.surface = SurfacePtr(temp_surface),
};
}
static void dispatch_surface_save(
const ImageRequest &request,
BackbufferCopy copy) {
auto surface_process = [request, copy = std::move(copy)]() {
switch (request.kind) {
case ImageRequestKind::Capture:
save_capture(
request.screen,
copy.desc.Format,
copy.desc.Width,
copy.desc.Height,
copy.surface.get());
break;
case ImageRequestKind::Screenshot: {
auto file_path = graphics_screenshot_genpath();
if (!file_path.empty()) {
save_screenshot(
file_path,
copy.desc.Format,
copy.desc.Width,
copy.desc.Height,
copy.surface.get());
}
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 std::optional<ImageRequest> consume_image_request() {
if (graphics_screenshot_consume()) {
return ImageRequest {
.kind = ImageRequestKind::Screenshot,
.screen = 0,
};
}
int capture_screen = 0;
if (graphics_capture_consume(&capture_screen)) {
return ImageRequest {
.kind = ImageRequestKind::Capture,
.screen = capture_screen,
};
}
return std::nullopt;
}
void graphics_d3d9_process_screenshot_and_capture(
IDirect3DDevice9 *device,
IDirect3DSwapChain9 *sub_swap_chain) {
const auto request = consume_image_request();
if (!request.has_value()) {
return;
}
auto copy = acquire_backbuffer_copy(
device,
sub_swap_chain,
request->screen);
if (!copy.has_value()) {
if (request->kind == ImageRequestKind::Capture) {
graphics_capture_skip(request->screen);
}
return;
}
dispatch_surface_save(*request, std::move(*copy));
}
@@ -0,0 +1,7 @@
#pragma once
#include <d3d9.h>
void graphics_d3d9_process_screenshot_and_capture(
IDirect3DDevice9 *device,
IDirect3DSwapChain9 *sub_swap_chain);
@@ -96,6 +96,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
pDirtyRegion,
dwFlags);
// a successful game subscreen present suppresses the next forced fallback present.
if (SUCCEEDED(result) &&
(this == pDev->implicit_sub_swapchain || this == pDev->sub_swapchain[0])) {
graphics_d3d9_notify_subscreen_present();
}
// Some drivers report S_PRESENT_MODE_CHANGED after Windows moves the portrait SMALL
// head into the requested exclusive mode, leaving both group heads black. Toggle SMALL
// through another supported mode and restore it once, then retry the interrupted MAIN
+8
View File
@@ -17,11 +17,13 @@
#include "games/ddr/ddr.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "games/io.h"
#include "games/sdvx/sdvx.h"
#include "games/popn/popn.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
#include "launcher/shutdown.h"
#include "misc/hotkeys.h"
#include "overlay/overlay.h"
#include "touch/touch.h"
#include "touch/touch_gestures.h"
@@ -1374,6 +1376,12 @@ void graphics_screens_get(std::vector<int> &screens) {
screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end());
}
void graphics_poll_screenshot_hotkey() {
if (hotkeys::consume_screenshot()) {
graphics_screenshot_trigger();
}
}
void graphics_screenshot_trigger() {
GRAPHICS_SCREENSHOT_TRIGGER = true;
}
+1
View File
@@ -134,6 +134,7 @@ void graphics_hook_subscreen_window(HWND hWnd);
void graphics_screens_register(int screen);
void graphics_screens_unregister(int screen);
void graphics_screens_get(std::vector<int> &screens);
void graphics_poll_screenshot_hotkey();
void graphics_screenshot_trigger();
bool graphics_screenshot_consume();
void graphics_capture_trigger(int screen);
+60 -3
View File
@@ -13,7 +13,9 @@
#include "avs/game.h"
#include "games/iidx/iidx.h"
#include "games/gitadora/gitadora.h"
#include "games/popn/popn.h"
#include "games/sdvx/sdvx.h"
#include "util/deferlog.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -24,12 +26,13 @@ constexpr UINT CODEPAGE_SHIFT_JIS = 932;
static decltype(GetACP) *GetACP_orig = nullptr;
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
#ifdef SPICE64
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_orig = nullptr;
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
#endif
@@ -182,9 +185,32 @@ static BOOL WINAPI IsDBCSLeadByte_hook (
BYTE TestChar
)
{
if (IsDBCSLeadByteEx_orig) {
return IsDBCSLeadByteEx_orig(CODEPAGE_SHIFT_JIS, TestChar);
}
return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar);
}
static BOOL WINAPI IsDBCSLeadByteEx_hook(
UINT CodePage,
BYTE TestChar)
{
switch (CodePage) {
case CP_ACP:
case CP_THREAD_ACP:
CodePage = CODEPAGE_SHIFT_JIS;
break;
default:
break;
}
return IsDBCSLeadByteEx_orig(CodePage, TestChar);
}
#endif
static
int
WINAPI
@@ -220,6 +246,8 @@ WideCharToMultiByte_hook(
lpUsedDefaultChar);
}
#ifdef SPICE64
int
WINAPI
GetLocaleInfoA_hook(
@@ -250,6 +278,18 @@ GetLocaleInfoA_hook(
void hooks::lang::early_init() {
log_info("hooks::lang", "early initialization");
const auto native_code_page = GetACP();
if (native_code_page == CP_UTF8) {
log_warning(
"hooks::lang",
"Windows is using UTF-8 as the system code page; "
"some games may render text incorrectly or behave unexpectedly");
deferredlogs::defer_error_messages({
"Windows is using UTF-8 as the system code page",
" some games may render text incorrectly or behave unexpectedly"});
}
// hooking these two functions fixes the jubeat mojibake
detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig);
detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);
@@ -307,7 +347,25 @@ void hooks::lang::early_init() {
&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");
detour::trampoline_try(
"kernel32.dll",
@@ -315,7 +373,6 @@ void hooks::lang::early_init() {
WideCharToMultiByte_hook,
&WideCharToMultiByte_orig);
}
#endif
}
+31 -29
View File
@@ -5,7 +5,6 @@
#include <cmath>
#include <cstdlib>
#include <assert.h>
#include <shlwapi.h>
#include <windows.h>
#include <cfg/configurator.h>
@@ -88,13 +87,13 @@
#include "launcher/launcher.h"
#include "launcher/logger.h"
#include "launcher/signal.h"
#include "launcher/superexit.h"
#include "launcher/richpresence.h"
#include "launcher/shutdown.h"
#include "launcher/options.h"
#include "misc/bt5api.h"
#include "misc/device.h"
#include "misc/eamuse.h"
#include "misc/hotkeys.h"
#include "misc/extdev.h"
#include "misc/ami2000.h"
#include "misc/sciunit.h"
@@ -524,22 +523,25 @@ int main_implementation(int argc, char *argv[]) {
games::iidx::FLIP_CAMS = true;
}
// IIDX CONNECT_CAMERA logic here for cases where user is running without -iidx module
// for now, we assume that user may be running on a cab
// (games::iidx::DISABLE_CAMS starts out as false unless user overrides)
// we will check again in IIDX module with a different default
assert(!games::iidx::DISABLE_CAMS.has_value());
if (options[launcher::Options::IIDXDisableCameras].value_bool()) {
games::iidx::DISABLE_CAMS = true;
// Resolve the IIDX camera policy here so it also applies without the -iidx module.
const auto &cab_camera_access = options[launcher::Options::IIDXCabCamAccess];
if (cab_camera_access.is_active()) {
const auto value = cab_camera_access.value_text();
if (value == "off") {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
} else if (value == "on") {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::on;
} else if (value == "legacy") {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::legacy;
}
}
if (options[launcher::Options::IIDXCabCamAccess].is_active() &&
options[launcher::Options::IIDXCabCamAccess].value_text() == "off") {
games::iidx::DISABLE_CAMS = true;
if (options[launcher::Options::IIDXDisableCameras].value_bool()) {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
}
if (options[launcher::Options::IIDXCamHook].value_bool()) {
games::iidx::TDJ_CAMERA = true;
// Disable legacy behaviour to avoid conflict
games::iidx::DISABLE_CAMS = true;
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
}
// CONNECT_CAMERA env var will be set once logging is enabled
@@ -1717,14 +1719,13 @@ int main_implementation(int argc, char *argv[]) {
GRAPHICS_FS_ORIENTATION_SWAP = true;
}
// for cab usage - set environment variables (outside of -iidx module)
if (games::iidx::DISABLE_CAMS.has_value() &&
games::iidx::DISABLE_CAMS.value() &&
// apply an explicit off outside of the -iidx module so it also works on cabinets.
if (games::iidx::CAB_CAMERA_ACCESS == games::iidx::cab_camera_access_mode::off &&
!cfg::CONFIGURATOR_STANDALONE) {
log_misc("launcher::iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
if (games::iidx::SOUND_OUTPUT_DEVICE.has_value() &&
games::iidx::SOUND_OUTPUT_DEVICE.value() != "auto" &&
!cfg::CONFIGURATOR_STANDALONE) {
@@ -1776,9 +1777,8 @@ int main_implementation(int argc, char *argv[]) {
nvapi::initialize();
// add application profile to nvcp
nvapi::set_profile_settings();
// enable super exit
superexit::enable();
// keep ALT+F4 available during lengthy non-standalone boot
hotkeys::start();
// enable subscreen touch emulation
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
games::iidx::ENABLE_POKE = true;
@@ -2451,6 +2451,7 @@ int main_implementation(int argc, char *argv[]) {
// initialize raw input
RI_MGR = std::make_unique<rawinput::RawInputManager>();
hotkeys::enable_raw_input();
for (const auto &device : sextet_devices) {
RI_MGR->sextet_register(device);
}
@@ -2473,6 +2474,9 @@ int main_implementation(int argc, char *argv[]) {
log_misc("rawinput", "Analog mappings:");
dump_analog_bindings();
// mappings are ready; begin screenshot and coin polling during late startup
hotkeys::enable_input();
// for certain games, show cursor if no touch is available (must be called after RI_MGR is available)
if (show_cursor_if_no_touch && !is_touch_available("launcher::main_implementation")) {
GRAPHICS_SHOW_CURSOR = true;
@@ -2702,9 +2706,6 @@ int main_implementation(int argc, char *argv[]) {
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
}
// start coin input thread
eamuse_coin_start_thread();
// pin macro
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
eamuse_pin_macro_start_thread();
@@ -2754,6 +2755,9 @@ int main_implementation(int argc, char *argv[]) {
log_info("launcher", "calling game entry");
avs::game::entry_main();
// stop screenshot and coin polling; mapped SuperExit and ALT+F4 remain active
hotkeys::disable_input();
// clear presence
richpresence::shutdown();
@@ -2791,9 +2795,6 @@ int main_implementation(int argc, char *argv[]) {
// free api controller
API_CONTROLLER.reset();
// stop coin input thread
eamuse_coin_stop_thread();
eamuse_pin_macro_stop_thread();
// BT5API
@@ -2804,6 +2805,7 @@ int main_implementation(int argc, char *argv[]) {
sdk::fini_sdk_modules();
// stop raw input
hotkeys::disable_raw_input();
RI_MGR.reset();
// debug hook
@@ -2836,8 +2838,8 @@ int main_implementation(int argc, char *argv[]) {
// dispose crypt
crypt::dispose();
// disable super exit
superexit::disable();
// end early/late ALT+F4 monitoring at the same teardown point as legacy SuperExit
hotkeys::stop();
// disable poke
games::iidx::poke::disable();
+10 -6
View File
@@ -640,17 +640,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// IIDXCabCamAccess
.title = "IIDX Use Official AC Cams",
.title = "IIDX Official AC Camera Access",
.name = "iidxcabcams",
.desc = "For IIDX25+, allow direct access to cameras from real arcade cabinets. "
"Only turn this on if you have OFFICIAL arcade cameras connected to the correct USB ports. Default: auto.",
.desc = "Controls how the game accesses USB cameras for IIDX 25+.\n\n"
"auto (default): use [off] when the IIDX module is enabled; otherwise, use [on].\n\n"
"on: game discovers and directly accesses cameras; requires official cameras on correct USB ports.\n\n"
"legacy: for IIDX 25/26 only; allow camera access with emulated discovery.\n\n"
"off: prevent game from accessing USB cameras.",
.type = OptionType::Enum,
.game_name = "Beatmania IIDX",
.category = "Cab Peripherals",
.elements = {
{"auto", ""},
{"on", ""},
{"legacy", ""},
{"off", ""},
{"on", ""}
},
},
{
@@ -977,7 +981,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.display_name = "sdvxsubredraw",
.aliases= "sdvxsubredraw",
.desc = "Check if submonitor in fullscreen mode doesn't update every frame; "
"this option forces subscreen to redraw every frame.",
"this option presents the subscreen when the game does not.",
.type = OptionType::Bool,
.game_name = "Sound Voltex",
.category = "Advanced Game Options",
@@ -1171,7 +1175,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
.name = "popnsubredraw",
.desc = "Check if submonitor in fullscreen mode appears stuck; "
"this option forces subscreen to redraw every frame.",
"this option presents the subscreen when the game does not.",
.type = OptionType::Bool,
.game_name = "Pop'n Music",
.category = "Advanced Game Options",
+18 -95
View File
@@ -1,21 +1,14 @@
#include "superexit.h"
#include <thread>
#include "windows.h"
#include "cfg/configurator.h"
#include "launcher/shutdown.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "touch/touch.h"
#include "misc/eamuse.h"
#include "games/io.h"
#include "overlay/overlay.h"
#include "util/logging.h"
namespace superexit {
static std::thread *THREAD = nullptr;
static bool THREAD_RUNNING = false;
bool has_focus() {
HWND fg_wnd = GetForegroundWindow();
if (fg_wnd == NULL) {
@@ -29,92 +22,22 @@ namespace superexit {
return fg_pid == GetCurrentProcessId();
}
void enable() {
// check if already running
if (THREAD)
return;
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
// log
log_info("superexit", "enabled");
// set variable to false to stop
while (THREAD_RUNNING) {
// check rawinput for ALT+F4
bool rawinput_exit = false;
if (RI_MGR != nullptr) {
auto devices = RI_MGR->devices_get();
for (auto &device : devices) {
switch (device.type) {
case rawinput::KEYBOARD: {
auto &key_states = device.keyboardInfo->key_states;
for (int page_index = 0; page_index < 1024; page_index += 256) {
if (key_states[page_index + VK_MENU]
&& key_states[page_index + VK_F4]) {
rawinput_exit = true;
}
}
break;
}
default:
break;
}
}
}
bool async_key_exit =
(GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
bool overlay_exit = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game());
if (buttons &&
(!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::SuperExit))) {
overlay_exit = true;
}
// check for exit
if (rawinput_exit || async_key_exit) {
if (has_focus()) {
log_info("superexit", "detected ALT+F4, exiting...");
launcher::shutdown();
}
}
if (overlay_exit) {
if (has_focus()) {
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
launcher::shutdown();
}
}
// slow down
Sleep(100);
}
return nullptr;
});
}
void disable() {
if (!THREAD) {
void handle_hotkeys(bool alt_f4, bool mapped_exit) {
if (!alt_f4 && !mapped_exit) {
return;
}
// stop old thread
THREAD_RUNNING = false;
THREAD->join();
// delete thread
delete THREAD;
THREAD = nullptr;
// log
log_info("superexit", "disabled");
if (cfg::CONFIGURATOR_STANDALONE) {
return;
}
if (!has_focus()) {
return;
}
if (alt_f4) {
log_info("superexit", "detected ALT+F4, exiting...");
launcher::shutdown();
return;
}
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
launcher::shutdown();
}
}
+1 -2
View File
@@ -2,6 +2,5 @@
namespace superexit {
bool has_focus();
void enable();
void disable();
void handle_hotkeys(bool alt_f4, bool mapped_exit);
}
+24 -55
View File
@@ -25,10 +25,8 @@ static double CARD_INSERT_TIME[2] = {0, 0};
static double CARD_INSERT_TIMEOUT = 2.0;
static char CARD_INSERT_UID[2][8] = {{0}, {0}};
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
static int COIN_STOCK = 0;
static bool COIN_BLOCK = false;
static std::thread *COIN_INPUT_THREAD;
static bool COIN_INPUT_THREAD_ACTIVE = false;
static std::atomic_int COIN_STOCK {0};
static std::atomic_bool COIN_BLOCK {false};
static uint16_t KEYPAD_STATE[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0};
@@ -292,78 +290,49 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) {
}
bool eamuse_coin_get_block() {
return COIN_BLOCK;
return COIN_BLOCK.load(std::memory_order_relaxed);
}
void eamuse_coin_set_block(bool block) {
COIN_BLOCK = block;
COIN_BLOCK.store(block, std::memory_order_relaxed);
}
int eamuse_coin_get_stock() {
return COIN_STOCK;
return COIN_STOCK.load(std::memory_order_relaxed);
}
void eamuse_coin_set_stock(int amount) {
COIN_STOCK = amount;
COIN_STOCK.store(amount, std::memory_order_relaxed);
}
bool eamuse_coin_consume(int amount) {
if (COIN_STOCK < amount) {
return false;
} else {
COIN_STOCK -= amount;
return true;
auto stock = COIN_STOCK.load(std::memory_order_relaxed);
while (stock >= amount) {
if (COIN_STOCK.compare_exchange_weak(
stock,
stock - amount,
std::memory_order_relaxed)) {
return true;
}
}
return false;
}
int eamuse_coin_consume_stock() {
int stock = COIN_STOCK;
COIN_STOCK = 0;
return stock;
return COIN_STOCK.exchange(0, std::memory_order_relaxed);
}
int eamuse_coin_add() {
return ++COIN_STOCK;
return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1;
}
void eamuse_coin_start_thread() {
// set active
COIN_INPUT_THREAD_ACTIVE = true;
// create thread
COIN_INPUT_THREAD = new std::thread([]() {
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
static bool COIN_INPUT_KEY_STATE = false;
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_coin_insert() {
if (COIN_BLOCK.load(std::memory_order_relaxed)) {
log_info("eamuse", "coin inserted while blocked");
} else {
log_info("eamuse", "coin insert");
COIN_STOCK.fetch_add(1, std::memory_order_relaxed);
}
}
void eamuse_pin_macro_start_thread() {
+1 -3
View File
@@ -58,9 +58,7 @@ bool eamuse_coin_consume(int amount);
int eamuse_coin_consume_stock();
int eamuse_coin_add();
void eamuse_coin_start_thread();
void eamuse_coin_stop_thread();
void eamuse_coin_insert();
void eamuse_pin_macro_start_thread();
void eamuse_pin_macro_stop_thread();
+154
View File
@@ -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);
}
}
+13
View File
@@ -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();
}
+8
View File
@@ -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)
: renderer(OverlayRenderer::D3D9), hWnd(hWnd), d3d(d3d), device(device) {
log_info("overlay", "initializing (D3D9)");
@@ -924,6 +929,9 @@ bool overlay::SpiceOverlay::has_focus() {
}
bool overlay::SpiceOverlay::hotkeys_triggered() {
const std::lock_guard<std::mutex> lock(this->hotkeys_mutex);
// this query also consumes the shared HotkeyToggle edge; the mutex gives all callers one latch
// prevent hotkeys in spicecfg
if (cfg::CONFIGURATOR_STANDALONE) {
return false;
+2
View File
@@ -212,6 +212,7 @@ namespace overlay {
bool fps_down = false;
bool hotkey_toggle = false;
bool hotkey_toggle_last = false;
std::mutex hotkeys_mutex;
// true between new_frame()'s ImGui::NewFrame() and render()'s ImGui::Render(),
// so render() never runs without a matching NewFrame.
@@ -235,4 +236,5 @@ namespace overlay {
#endif
void create_software(HWND hWnd);
void destroy(HWND hWnd = nullptr);
bool global_hotkeys_triggered();
}
+25
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <chrono>
#include <cstdarg>
#include <iterator>
#include <map>
#include <thread>
#include <utility>
@@ -2543,6 +2544,30 @@ rawinput::Device *rawinput::RawInputManager::devices_get(const std::string &name
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) {
this->callback_add.push_back(DeviceCallback {
.data = data,
+1
View File
@@ -171,6 +171,7 @@ namespace rawinput {
void __stdcall devices_print();
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() {
return devices;
+33
View File
@@ -1,5 +1,6 @@
#include "transform.h"
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "overlay/overlay.h"
#include "settings.h"
@@ -82,6 +83,25 @@ namespace nativetouch::transform {
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
}
// the digitizer is mapped to the zero-based primary display, while SDVX
// still expects portrait coordinates when its image is rendered in landscape:
// (x, y) -> (width * (1 - y / height), height * x / width).
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);
if (landscape_width <= 0 || landscape_height <= 0) {
return false;
}
const auto input_x = position->x;
position->x = landscape_width -
MulDiv(position->y, landscape_width, landscape_height);
position->y = MulDiv(input_x, landscape_height, landscape_width);
return true;
}
// convert physical screen coordinates to game touch coordinates for a known target
bool screen_to_game(HWND window, POINT *position) {
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
@@ -127,6 +147,12 @@ namespace nativetouch::transform {
return screen_to_game(window, position);
}
// exception: sdvx windowed subscreen does not use the subscreen overlay transform
if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) {
POINT client_position = *position;
return screen_to_game_client(window, &client_position);
}
// if this game has a subscreen overlay that can transform touch input
// but the window is hidden or not under the cursor, reject mouse-as-touch
// (e.g., iidx/sdvx are rejected here, but nostalgia is allowed)
@@ -144,6 +170,13 @@ namespace nativetouch::transform {
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
const auto active_overlay = has_active_overlay_transform();
// 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
if (!dedicated_subscreen && !active_overlay) {
return Result::Unchanged;