From 4836cd94abafe479254140f62f302281b528d3ea Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:57:11 -0700 Subject: [PATCH] touch: inject touches from spiceapi to native touch hooks (#824) ## Link to GitHub Issue or related Pull Request, if one exists Fixes #814 ## Description of change Touches via the API wasn't being delivered to native touch hook; implement this, which closes the last gap we have vs. wintouchemu. While we're here, implement popn subscreen support for companion as well. ## Testing Tested for iidx/sdvx/popn --- src/spice2x/api/modules/touch.cpp | 86 ++++++++++++++++- src/spice2x/api/modules/touch.h | 14 +++ src/spice2x/touch/native/inject.h | 1 + src/spice2x/touch/native/inject_synthetic.cpp | 93 ++++++++++++++++--- src/spice2x/touch/native/nativetouchhook.cpp | 7 ++ src/spice2x/touch/native/nativetouchhook.h | 4 + 6 files changed, 191 insertions(+), 14 deletions(-) diff --git a/src/spice2x/api/modules/touch.cpp b/src/spice2x/api/modules/touch.cpp index ca7eba2..49be819 100644 --- a/src/spice2x/api/modules/touch.cpp +++ b/src/spice2x/api/modules/touch.cpp @@ -1,5 +1,7 @@ #include "touch.h" +#include +#include #include #include "external/rapidjson/document.h" @@ -8,6 +10,8 @@ #include "misc/eamuse.h" #include "launcher/launcher.h" #include "touch/touch.h" +#include "touch/native/inject.h" +#include "touch/native/nativetouchhook.h" #include "util/utils.h" #include "games/iidx/iidx.h" @@ -17,6 +21,25 @@ using namespace rapidjson; namespace api::modules { + static bool inject_native_touch( + const std::vector &touch_points, int canvas_w, int canvas_h) { + if (touch_points.empty()) { + // no active touches remain; release the synthetic contact + return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false); + } + + // only a single synthetic contact is supported, so just use the first touch point + const auto &touch = touch_points.front(); + POINT position { touch.x, touch.y }; + if (canvas_w > 0 && canvas_h > 0) { + return nativetouch::inject::inject_synthetic_touch_from_canvas( + position, + { canvas_w, canvas_h }, + true); + } + return nativetouch::inject::inject_synthetic_touch(position, true); + } + Touch::Touch() : Module("touch") { is_sdvx = avs::game::is_model("KFC"); @@ -26,6 +49,25 @@ namespace api::modules { is_tdj_fhd = false; } + use_native = nativetouch::is_hooked(); + + // resolution the API/companion sends native touch coordinates in (after errata) + native_canvas_w = 0; + native_canvas_h = 0; + if (is_sdvx) { + // exceed gear subscreen, portrait after the rotation applied in apply_touch_errata + native_canvas_w = 1080; + native_canvas_h = 1920; + } else if (avs::game::is_model("LDJ")) { + // TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata + native_canvas_w = is_tdj_fhd ? 1920 : 1280; + native_canvas_h = is_tdj_fhd ? 1080 : 720; + } else if (avs::game::is_model("M39")) { + // pop'n music API touch surface + native_canvas_w = 1280; + native_canvas_h = 800; + } + functions["read"] = std::bind(&Touch::read, this, _1, _2); functions["write"] = std::bind(&Touch::write, this, _1, _2); functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2); @@ -99,7 +141,11 @@ namespace api::modules { } // apply touch points - touch_write_points(&touch_points); + if (use_native) { + write_native(touch_points); + } else { + touch_write_points(&touch_points); + } } /** @@ -123,7 +169,43 @@ namespace api::modules { } // remove all IDs - touch_remove_points(&touch_point_ids); + if (use_native) { + write_reset_native(touch_point_ids); + } else { + touch_remove_points(&touch_point_ids); + } + } + + void Touch::write_native(const std::vector &touch_points) { + if (touch_points.empty()) { + return; + } + + std::lock_guard lock(native_touch_mutex); + const auto touch_id = touch_points.front().id; + if (native_touch_id && *native_touch_id != touch_id) { + if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) { + return; + } + native_touch_id.reset(); + } + if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) { + native_touch_id = touch_id; + } + } + + void Touch::write_reset_native(const std::vector &touch_point_ids) { + std::lock_guard lock(native_touch_mutex); + if (!native_touch_id || + std::find( + touch_point_ids.begin(), + touch_point_ids.end(), + *native_touch_id) == touch_point_ids.end()) { + return; + } + if (inject_native_touch({}, native_canvas_w, native_canvas_h)) { + native_touch_id.reset(); + } } void Touch::apply_touch_errata(int &x, int &y) { diff --git a/src/spice2x/api/modules/touch.h b/src/spice2x/api/modules/touch.h index dfed3ff..62fb217 100644 --- a/src/spice2x/api/modules/touch.h +++ b/src/spice2x/api/modules/touch.h @@ -1,8 +1,12 @@ #pragma once +#include +#include +#include #include #include "api/module.h" #include "api/request.h" +#include "touch/touch.h" namespace api::modules { @@ -13,11 +17,21 @@ namespace api::modules { private: bool is_sdvx; bool is_tdj_fhd; + bool use_native; + std::mutex native_touch_mutex; + std::optional native_touch_id; + + // resolution the API/companion sends native touch coordinates in (after errata); + // used to normalize before mapping into the native injection window + int native_canvas_w; + int native_canvas_h; // function definitions void read(Request &req, Response &res); void write(Request &req, Response &res); void write_reset(Request &req, Response &res); + void write_native(const std::vector &touch_points); + void write_reset_native(const std::vector &touch_point_ids); void apply_touch_errata(int &x, int &y); }; diff --git a/src/spice2x/touch/native/inject.h b/src/spice2x/touch/native/inject.h index 2331caa..cd45a66 100644 --- a/src/spice2x/touch/native/inject.h +++ b/src/spice2x/touch/native/inject.h @@ -9,5 +9,6 @@ namespace nativetouch::inject { void register_and_attach_window(HWND window); void hook(HMODULE module); bool inject_synthetic_touch(POINT position, bool down); + bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down); bool transform_touch_input(tagTOUCHINPUT *point); } diff --git a/src/spice2x/touch/native/inject_synthetic.cpp b/src/spice2x/touch/native/inject_synthetic.cpp index ca29cb4..447edd3 100644 --- a/src/spice2x/touch/native/inject_synthetic.cpp +++ b/src/spice2x/touch/native/inject_synthetic.cpp @@ -16,6 +16,12 @@ namespace nativetouch::inject { constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100; + enum class SyntheticTouchMessage : WPARAM { + Up, // used by callers releasing a contact + DownGameSpace, // coordinates are relative to the game's logical touch surface + DownScreenSpace, // coordinates are absolute pixels in Windows desktop coordinates + }; + static std::once_flag synthetic_initialization_once; static int synthetic_contact_timer_token; static UINT synthetic_touch_message; @@ -31,14 +37,29 @@ namespace nativetouch::inject { } // synthetic touches preempt the mouse and keep it disabled until release or timeout - static void begin_synthetic_contact(HWND window, POINT position) { + static void begin_synthetic_contact(HWND window, POINT position, bool screen_space) { // dedicated TDJ injects into the physical subscreen, so map Windows' // returned coordinates back into the game's touch coordinate space const auto transform_returned_coordinates = transform::is_tdj_dedicated_subscreen(window); - if (!transform::game_to_screen(window, &position)) { + if (!screen_space && !transform::game_to_screen(window, &position)) { return; } + + const auto timer_id = reinterpret_cast(&synthetic_contact_timer_token); + + // when this producer already owns the contact, move it instead of releasing and + // re-pressing so continuous input (such as the API surface) drags smoothly + if (contact_is_owned_by(ContactOwner::Synthetic, window)) { + if (update_contact(ContactOwner::Synthetic, window, position)) { + // refresh the safety timeout while updates keep arriving + if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) { + set_contact_timer(ContactOwner::Synthetic, window, timer_id); + } + return; + } + } + if (!release_active_contact()) { return; } @@ -50,7 +71,6 @@ namespace nativetouch::inject { return; } - const auto timer_id = reinterpret_cast(&synthetic_contact_timer_token); if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) { set_contact_timer(ContactOwner::Synthetic, window, timer_id); } else { @@ -68,10 +88,16 @@ namespace nativetouch::inject { HWND window, UINT message, WPARAM w_param, LPARAM l_param) { if (synthetic_touch_message != 0 && message == synthetic_touch_message) { POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) }; - if (w_param) { - begin_synthetic_contact(window, position); - } else { - end_synthetic_contact(window); + switch (static_cast(w_param)) { + case SyntheticTouchMessage::DownGameSpace: + begin_synthetic_contact(window, position, false); + break; + case SyntheticTouchMessage::DownScreenSpace: + begin_synthetic_contact(window, position, true); + break; + default: + end_synthetic_contact(window); + break; } return true; } @@ -85,20 +111,63 @@ namespace nativetouch::inject { return false; } - // inject synthetic touches; only one contact is supported at a time for simplicity - bool inject_synthetic_touch(POINT position, bool down) { + static HWND prepare_synthetic_touch() { initialize_touch_injection(); const auto window = get_injection_window(); if (!touch_injection_available() || window == nullptr || synthetic_touch_message == 0) { - return false; + return nullptr; } + return window; + } + static bool post_synthetic_touch( + HWND window, POINT position, SyntheticTouchMessage message) { return PostMessageW( window, synthetic_touch_message, - static_cast(down), + static_cast(message), MAKELPARAM(position.x, position.y)) != FALSE; } -} \ No newline at end of file + + // inject a point expressed in the game's synthetic touch coordinate space + bool inject_synthetic_touch(POINT position, bool down) { + const auto window = prepare_synthetic_touch(); + if (window == nullptr) { + return false; + } + + const auto message = down + ? SyntheticTouchMessage::DownGameSpace + : SyntheticTouchMessage::Up; + return post_synthetic_touch(window, position, message); + } + + // map a logical canvas point onto the live injection window before injecting it + bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down) { + const auto window = prepare_synthetic_touch(); + if (window == nullptr) { + return false; + } + + if (!down) { + return post_synthetic_touch(window, position, SyntheticTouchMessage::Up); + } + + RECT client_rect {}; + if (canvas.cx <= 0 || canvas.cy <= 0 || + !GetClientRect(window, &client_rect) || + client_rect.right <= 0 || client_rect.bottom <= 0) { + return false; + } + + position.x = MulDiv(position.x, client_rect.right, canvas.cx); + position.y = MulDiv(position.y, client_rect.bottom, canvas.cy); + if (!ClientToScreen(window, &position)) { + return false; + } + + return post_synthetic_touch(window, position, SyntheticTouchMessage::DownScreenSpace); + } +} diff --git a/src/spice2x/touch/native/nativetouchhook.cpp b/src/spice2x/touch/native/nativetouchhook.cpp index 1be38f7..ef0a43f 100644 --- a/src/spice2x/touch/native/nativetouchhook.cpp +++ b/src/spice2x/touch/native/nativetouchhook.cpp @@ -23,6 +23,7 @@ namespace nativetouch { static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr; + static bool native_touch_hooked = false; static bool native_display_initialized = false; static DWORD native_display_orientation = DMDO_DEFAULT; static long native_display_size_x = 1920L; @@ -157,9 +158,15 @@ namespace nativetouch { return result; } + bool is_hooked() { + return native_touch_hooked; + } + void hook(HMODULE module) { inject::hook(module); + native_touch_hooked = true; + GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module); if (GetTouchInputInfo_orig != nullptr) { log_misc("touch::native", "GetTouchInputInfo hooked"); diff --git a/src/spice2x/touch/native/nativetouchhook.h b/src/spice2x/touch/native/nativetouchhook.h index c26f6f2..fad821f 100644 --- a/src/spice2x/touch/native/nativetouchhook.h +++ b/src/spice2x/touch/native/nativetouchhook.h @@ -2,5 +2,9 @@ namespace nativetouch { void hook(HMODULE module); + + // true once hook() has installed the native touch stack for the current game; + // such games consume touch through the GetTouchInputInfo hook rather than spicetouch + bool is_hooked(); } \ No newline at end of file