From baa550037a112e8fa6c9b2d263ecadd65e8d0894 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:38:17 -0700 Subject: [PATCH 1/5] 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 --- src/spice2x/CMakeLists.txt | 1 + src/spice2x/README.md | 14 ++++ src/spice2x/api/client.h | 13 ++++ src/spice2x/api/controller.cpp | 9 +++ src/spice2x/api/modules/iidx.cpp | 6 +- src/spice2x/api/modules/sdvx.cpp | 67 +++++++++++++++++++ src/spice2x/api/modules/sdvx.h | 24 +++++++ .../api/resources/python/spiceapi/__init__.py | 1 + .../api/resources/python/spiceapi/sdvx.py | 12 ++++ src/spice2x/games/iidx/bi2x_hook.cpp | 11 +-- src/spice2x/games/sdvx/bi2x_hook.cpp | 39 ++++------- src/spice2x/games/sdvx/sdvx.cpp | 13 ++++ src/spice2x/games/sdvx/sdvx.h | 4 ++ 13 files changed, 182 insertions(+), 32 deletions(-) create mode 100644 src/spice2x/api/client.h create mode 100644 src/spice2x/api/modules/sdvx.cpp create mode 100644 src/spice2x/api/modules/sdvx.h create mode 100644 src/spice2x/api/resources/python/spiceapi/sdvx.py diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 5711010..8b80adb 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -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 diff --git a/src/spice2x/README.md b/src/spice2x/README.md index 63bfbe6..e6eae30 100644 --- a/src/spice2x/README.md +++ b/src/spice2x/README.md @@ -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 diff --git a/src/spice2x/api/client.h b/src/spice2x/api/client.h new file mode 100644 index 0000000..c73fc6d --- /dev/null +++ b/src/spice2x/api/client.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace api { + + extern std::atomic_uint32_t CLIENT_COUNT; + + inline bool has_clients() { + return CLIENT_COUNT.load(std::memory_order_relaxed) > 0; + } +} diff --git a/src/spice2x/api/controller.cpp b/src/spice2x/api/controller.cpp index 19713fa..1fd41d5 100644 --- a/src/spice2x/api/controller.cpp +++ b/src/spice2x/api/controller.cpp @@ -5,6 +5,7 @@ #include +#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() { diff --git a/src/spice2x/api/modules/iidx.cpp b/src/spice2x/api/modules/iidx.cpp index b65423f..cb47df7 100644 --- a/src/spice2x/api/modules/iidx.cpp +++ b/src/spice2x/api/modules/iidx.cpp @@ -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(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()); } diff --git a/src/spice2x/api/modules/sdvx.cpp b/src/spice2x/api/modules/sdvx.cpp new file mode 100644 index 0000000..b4c059c --- /dev/null +++ b/src/spice2x/api/modules/sdvx.cpp @@ -0,0 +1,67 @@ +#include "sdvx.h" + +#include + +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(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()); + } +} diff --git a/src/spice2x/api/modules/sdvx.h b/src/spice2x/api/modules/sdvx.h new file mode 100644 index 0000000..d9519d0 --- /dev/null +++ b/src/spice2x/api/modules/sdvx.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#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> 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); + }; +} diff --git a/src/spice2x/api/resources/python/spiceapi/__init__.py b/src/spice2x/api/resources/python/spiceapi/__init__.py index b7fe465..f0da213 100644 --- a/src/spice2x/api/resources/python/spiceapi/__init__.py +++ b/src/spice2x/api/resources/python/spiceapi/__init__.py @@ -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 * diff --git a/src/spice2x/api/resources/python/spiceapi/sdvx.py b/src/spice2x/api/resources/python/spiceapi/sdvx.py new file mode 100644 index 0000000..57ebdab --- /dev/null +++ b/src/spice2x/api/resources/python/spiceapi/sdvx.py @@ -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() diff --git a/src/spice2x/games/iidx/bi2x_hook.cpp b/src/spice2x/games/iidx/bi2x_hook.cpp index 4c9dc81..8e04a27 100644 --- a/src/spice2x/games/iidx/bi2x_hook.cpp +++ b/src/spice2x/games/iidx/bi2x_hook.cpp @@ -3,6 +3,7 @@ #if SPICE64 #include +#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]; + } } } diff --git a/src/spice2x/games/sdvx/bi2x_hook.cpp b/src/spice2x/games/sdvx/bi2x_hook.cpp index 09cb08f..dc53f81 100644 --- a/src/spice2x/games/sdvx/bi2x_hook.cpp +++ b/src/spice2x/games/sdvx/bi2x_hook.cpp @@ -3,6 +3,7 @@ #if SPICE64 #include +#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) { diff --git a/src/spice2x/games/sdvx/sdvx.cpp b/src/spice2x/games/sdvx/sdvx.cpp index a263e4a..e20f94b 100644 --- a/src/spice2x/games/sdvx/sdvx.cpp +++ b/src/spice2x/games/sdvx/sdvx.cpp @@ -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 && diff --git a/src/spice2x/games/sdvx/sdvx.h b/src/spice2x/games/sdvx/sdvx.h index bc868fe..6e54054 100644 --- a/src/spice2x/games/sdvx/sdvx.h +++ b/src/spice2x/games/sdvx/sdvx.h @@ -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") && From 2386047c2f4a4b62d6d4e0f2743b29795778ab74 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:22:29 -0700 Subject: [PATCH 2/5] 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. --- src/spice2x/api/modules/touch.cpp | 11 ++++++----- src/spice2x/touch/native/transform.cpp | 27 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/spice2x/api/modules/touch.cpp b/src/spice2x/api/modules/touch.cpp index 49be819..eeb7550 100644 --- a/src/spice2x/api/modules/touch.cpp +++ b/src/spice2x/api/modules/touch.cpp @@ -55,9 +55,10 @@ 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; + // landscape API coordinates already match the primary screen orientation; + // portrait coordinates are rotated by apply_touch_errata + native_canvas_w = GRAPHICS_FS_ORIENTATION_SWAP ? 1920 : 1080; + native_canvas_h = GRAPHICS_FS_ORIENTATION_SWAP ? 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 +219,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_FS_ORIENTATION_SWAP) { + // rotate API coordinates into SDVX's portrait touch space x = 1080 - y_raw; y = x_raw; } diff --git a/src/spice2x/touch/native/transform.cpp b/src/spice2x/touch/native/transform.cpp index 267ca1d..8ba5528 100644 --- a/src/spice2x/touch/native/transform.cpp +++ b/src/spice2x/touch/native/transform.cpp @@ -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(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ? + GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT); + const auto landscape_height = static_cast(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) { @@ -144,6 +164,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; From 3da352488ef67c805a642258e2dfbf59779856b6 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:47:38 -0700 Subject: [PATCH 3/5] graphics: refactor screenshots (#860) First commit - pure refactoring, no functional changes Second commit - clean up & minor bug fix. --- src/spice2x/CMakeLists.txt | 1 + .../graphics/backends/d3d9/d3d9_backend.cpp | 329 +--------------- .../backends/d3d9/d3d9_screenshot.cpp | 353 ++++++++++++++++++ .../graphics/backends/d3d9/d3d9_screenshot.h | 9 + 4 files changed, 366 insertions(+), 326 deletions(-) create mode 100644 src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp create mode 100644 src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 8b80adb..9fa0c0e 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -536,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 diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index 94a98c1..17755d1 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -5,12 +5,8 @@ #include #include #include -#include #include -#ifdef __GNUC__ -#include -#endif #include "avs/game.h" #include "cfg/screen_resize.h" @@ -19,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 @@ -64,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 */ @@ -100,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 D3D9_ADAPTER = std::nullopt; DWORD D3D9_BEHAVIOR_DISABLE = 0; @@ -1499,178 +1475,6 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) { } } -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(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(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"); - - // 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( - 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, @@ -1718,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 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_d3d9_poll_screenshot_hotkey(); + graphics_d3d9_process_screenshot_and_capture(device, SUB_SWAP_CHAIN); } void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) { diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp new file mode 100644 index 0000000..26522bf --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp @@ -0,0 +1,353 @@ +#include "d3d9_screenshot.h" + +#include +#include +#include + +#include + +#ifdef __GNUC__ +#include +#endif + +#include "avs/game.h" +#include "games/io.h" +#include "hooks/graphics/graphics.h" +#include "launcher/launcher.h" +#include "misc/clipboard.h" +#include "misc/eamuse.h" +#include "overlay/notifications.h" +#include "overlay/overlay.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; + +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(finished_copy.pBits); + auto pixels = std::unique_ptr(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(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"); + + // 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( + 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"); + } +} + +void graphics_d3d9_poll_screenshot_hotkey() { + 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; + } +} + +void graphics_d3d9_process_screenshot_and_capture( + IDirect3DDevice9 *device, + IDirect3DSwapChain9 *sub_swap_chain) { + // 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.Format, + desc.Width, + 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 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(surface_process); + } + } +} diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h new file mode 100644 index 0000000..efa9096 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +void graphics_d3d9_poll_screenshot_hotkey(); + +void graphics_d3d9_process_screenshot_and_capture( + IDirect3DDevice9 *device, + IDirect3DSwapChain9 *sub_swap_chain); From 13a171f1998788335d71416f88d76dbeadae1b98 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:58:48 -0700 Subject: [PATCH 4/5] 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 --- src/spice2x/api/modules/touch.cpp | 12 +++++++----- src/spice2x/touch/native/transform.cpp | 6 ++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/spice2x/api/modules/touch.cpp b/src/spice2x/api/modules/touch.cpp index eeb7550..3986651 100644 --- a/src/spice2x/api/modules/touch.cpp +++ b/src/spice2x/api/modules/touch.cpp @@ -55,10 +55,12 @@ namespace api::modules { native_canvas_w = 0; native_canvas_h = 0; if (is_sdvx) { - // landscape API coordinates already match the primary screen orientation; - // portrait coordinates are rotated by apply_touch_errata - native_canvas_w = GRAPHICS_FS_ORIENTATION_SWAP ? 1920 : 1080; - native_canvas_h = GRAPHICS_FS_ORIENTATION_SWAP ? 1080 : 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; @@ -219,7 +221,7 @@ 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 && !GRAPHICS_FS_ORIENTATION_SWAP) { + } 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; diff --git a/src/spice2x/touch/native/transform.cpp b/src/spice2x/touch/native/transform.cpp index 8ba5528..3bfc128 100644 --- a/src/spice2x/touch/native/transform.cpp +++ b/src/spice2x/touch/native/transform.cpp @@ -147,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) From ce2f93d234a550e269add7d26432f918a21e7abc Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:11:42 -0700 Subject: [PATCH 5/5] graphics: refactor DX9 screenshot (part 2) (#861) --- .../backends/d3d9/d3d9_screenshot.cpp | 296 +++++++++++------- 1 file changed, 176 insertions(+), 120 deletions(-) diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp index 26522bf..4b53643 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include @@ -38,6 +40,33 @@ typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)( 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; + +struct BackbufferCopy { + D3DSURFACE_DESC desc {}; + SurfacePtr surface; +}; + +} // namespace + static void save_capture( int screen, D3DFORMAT format, @@ -226,128 +255,155 @@ void graphics_d3d9_poll_screenshot_hotkey() { } } +static std::optional 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 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 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) { - // 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.Format, - desc.Width, - 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 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(surface_process); - } + 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)); }