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
This commit is contained in:
bicarus
2026-07-21 16:57:11 -07:00
committed by GitHub
parent 9d41ca2515
commit 4836cd94ab
6 changed files with 191 additions and 14 deletions
+82
View File
@@ -1,5 +1,7 @@
#include "touch.h" #include "touch.h"
#include <algorithm>
#include <cstdint>
#include <functional> #include <functional>
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
@@ -8,6 +10,8 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/utils.h" #include "util/utils.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
@@ -17,6 +21,25 @@ using namespace rapidjson;
namespace api::modules { namespace api::modules {
static bool inject_native_touch(
const std::vector<TouchPoint> &touch_points, int canvas_w, int canvas_h) {
if (touch_points.empty()) {
// no active touches remain; release the synthetic contact
return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false);
}
// only a single synthetic contact is supported, so just use the first touch point
const auto &touch = touch_points.front();
POINT position { touch.x, touch.y };
if (canvas_w > 0 && canvas_h > 0) {
return nativetouch::inject::inject_synthetic_touch_from_canvas(
position,
{ canvas_w, canvas_h },
true);
}
return nativetouch::inject::inject_synthetic_touch(position, true);
}
Touch::Touch() : Module("touch") { Touch::Touch() : Module("touch") {
is_sdvx = avs::game::is_model("KFC"); is_sdvx = avs::game::is_model("KFC");
@@ -26,6 +49,25 @@ namespace api::modules {
is_tdj_fhd = false; 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["read"] = std::bind(&Touch::read, this, _1, _2);
functions["write"] = std::bind(&Touch::write, this, _1, _2); functions["write"] = std::bind(&Touch::write, this, _1, _2);
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2); functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
@@ -99,8 +141,12 @@ namespace api::modules {
} }
// apply touch points // apply touch points
if (use_native) {
write_native(touch_points);
} else {
touch_write_points(&touch_points); touch_write_points(&touch_points);
} }
}
/** /**
* write_reset(id: uint, ...) * write_reset(id: uint, ...)
@@ -123,8 +169,44 @@ namespace api::modules {
} }
// remove all IDs // remove all IDs
if (use_native) {
write_reset_native(touch_point_ids);
} else {
touch_remove_points(&touch_point_ids); touch_remove_points(&touch_point_ids);
} }
}
void Touch::write_native(const std::vector<TouchPoint> &touch_points) {
if (touch_points.empty()) {
return;
}
std::lock_guard<std::mutex> lock(native_touch_mutex);
const auto touch_id = touch_points.front().id;
if (native_touch_id && *native_touch_id != touch_id) {
if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) {
return;
}
native_touch_id.reset();
}
if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) {
native_touch_id = touch_id;
}
}
void Touch::write_reset_native(const std::vector<DWORD> &touch_point_ids) {
std::lock_guard<std::mutex> lock(native_touch_mutex);
if (!native_touch_id ||
std::find(
touch_point_ids.begin(),
touch_point_ids.end(),
*native_touch_id) == touch_point_ids.end()) {
return;
}
if (inject_native_touch({}, native_canvas_w, native_canvas_h)) {
native_touch_id.reset();
}
}
void Touch::apply_touch_errata(int &x, int &y) { void Touch::apply_touch_errata(int &x, int &y) {
int x_raw = x; int x_raw = x;
+14
View File
@@ -1,8 +1,12 @@
#pragma once #pragma once
#include <cstdint>
#include <mutex>
#include <optional>
#include <vector> #include <vector>
#include "api/module.h" #include "api/module.h"
#include "api/request.h" #include "api/request.h"
#include "touch/touch.h"
namespace api::modules { namespace api::modules {
@@ -13,11 +17,21 @@ namespace api::modules {
private: private:
bool is_sdvx; bool is_sdvx;
bool is_tdj_fhd; bool is_tdj_fhd;
bool use_native;
std::mutex native_touch_mutex;
std::optional<uint32_t> native_touch_id;
// resolution the API/companion sends native touch coordinates in (after errata);
// used to normalize before mapping into the native injection window
int native_canvas_w;
int native_canvas_h;
// function definitions // function definitions
void read(Request &req, Response &res); void read(Request &req, Response &res);
void write(Request &req, Response &res); void write(Request &req, Response &res);
void write_reset(Request &req, Response &res); void write_reset(Request &req, Response &res);
void write_native(const std::vector<TouchPoint> &touch_points);
void write_reset_native(const std::vector<DWORD> &touch_point_ids);
void apply_touch_errata(int &x, int &y); void apply_touch_errata(int &x, int &y);
}; };
+1
View File
@@ -9,5 +9,6 @@ namespace nativetouch::inject {
void register_and_attach_window(HWND window); void register_and_attach_window(HWND window);
void hook(HMODULE module); void hook(HMODULE module);
bool inject_synthetic_touch(POINT position, bool down); 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); bool transform_touch_input(tagTOUCHINPUT *point);
} }
+79 -10
View File
@@ -16,6 +16,12 @@ namespace nativetouch::inject {
constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100; 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 std::once_flag synthetic_initialization_once;
static int synthetic_contact_timer_token; static int synthetic_contact_timer_token;
static UINT synthetic_touch_message; 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 // 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' // dedicated TDJ injects into the physical subscreen, so map Windows'
// returned coordinates back into the game's touch coordinate space // returned coordinates back into the game's touch coordinate space
const auto transform_returned_coordinates = const auto transform_returned_coordinates =
transform::is_tdj_dedicated_subscreen(window); transform::is_tdj_dedicated_subscreen(window);
if (!transform::game_to_screen(window, &position)) { if (!screen_space && !transform::game_to_screen(window, &position)) {
return; return;
} }
const auto timer_id = reinterpret_cast<UINT_PTR>(&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()) { if (!release_active_contact()) {
return; return;
} }
@@ -50,7 +71,6 @@ namespace nativetouch::inject {
return; return;
} }
const auto timer_id = reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token);
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) { if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
set_contact_timer(ContactOwner::Synthetic, window, timer_id); set_contact_timer(ContactOwner::Synthetic, window, timer_id);
} else { } else {
@@ -68,10 +88,16 @@ namespace nativetouch::inject {
HWND window, UINT message, WPARAM w_param, LPARAM l_param) { HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
if (synthetic_touch_message != 0 && message == synthetic_touch_message) { if (synthetic_touch_message != 0 && message == synthetic_touch_message) {
POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) }; POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) };
if (w_param) { switch (static_cast<SyntheticTouchMessage>(w_param)) {
begin_synthetic_contact(window, position); case SyntheticTouchMessage::DownGameSpace:
} else { begin_synthetic_contact(window, position, false);
break;
case SyntheticTouchMessage::DownScreenSpace:
begin_synthetic_contact(window, position, true);
break;
default:
end_synthetic_contact(window); end_synthetic_contact(window);
break;
} }
return true; return true;
} }
@@ -85,20 +111,63 @@ namespace nativetouch::inject {
return false; return false;
} }
// inject synthetic touches; only one contact is supported at a time for simplicity static HWND prepare_synthetic_touch() {
bool inject_synthetic_touch(POINT position, bool down) {
initialize_touch_injection(); initialize_touch_injection();
const auto window = get_injection_window(); const auto window = get_injection_window();
if (!touch_injection_available() || window == nullptr || if (!touch_injection_available() || window == nullptr ||
synthetic_touch_message == 0) { synthetic_touch_message == 0) {
return false; return nullptr;
}
return window;
} }
static bool post_synthetic_touch(
HWND window, POINT position, SyntheticTouchMessage message) {
return PostMessageW( return PostMessageW(
window, window,
synthetic_touch_message, synthetic_touch_message,
static_cast<WPARAM>(down), static_cast<WPARAM>(message),
MAKELPARAM(position.x, position.y)) != FALSE; MAKELPARAM(position.x, position.y)) != FALSE;
} }
// 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);
}
} }
@@ -23,6 +23,7 @@
namespace nativetouch { namespace nativetouch {
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr; static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
static bool native_touch_hooked = false;
static bool native_display_initialized = false; static bool native_display_initialized = false;
static DWORD native_display_orientation = DMDO_DEFAULT; static DWORD native_display_orientation = DMDO_DEFAULT;
static long native_display_size_x = 1920L; static long native_display_size_x = 1920L;
@@ -157,9 +158,15 @@ namespace nativetouch {
return result; return result;
} }
bool is_hooked() {
return native_touch_hooked;
}
void hook(HMODULE module) { void hook(HMODULE module) {
inject::hook(module); inject::hook(module);
native_touch_hooked = true;
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module); GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
if (GetTouchInputInfo_orig != nullptr) { if (GetTouchInputInfo_orig != nullptr) {
log_misc("touch::native", "GetTouchInputInfo hooked"); log_misc("touch::native", "GetTouchInputInfo hooked");
@@ -2,5 +2,9 @@
namespace nativetouch { namespace nativetouch {
void hook(HMODULE module); 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();
} }