touch: inject mouse and poke into native touch modes (#815)

## Link to GitHub Issue or related Pull Request, if one exists
Part 1 of many fixes for #814.

## Description of change
For the native touch hook - add code to inject synthetic touches using
Windows API (`InjectTouchInput`).

Note that this is Windows 8+ only, but we can make an assumption that we
are running on Win10+ for these games (TDJ/UFC/High Cheers) since the
cabs assume Win10.

Detect mouse events and allow IIDX poke to call into this to inject
synthetic touches.

Also, update the nativetouchhook to independently calculate window size
and rotation, instead of relying on rawinput layer.

## Testing
Tested to work with native touch option on IIDX, SDVX, POPN, all full
screen / windowed / sub on/off combinations.

Edge cases:

* sdvx main monitor rotated 270 deg
* touch invert option
* windowed mode resize / moved

All seem to work.
This commit is contained in:
bicarus
2026-07-21 00:04:38 -07:00
committed by GitHub
parent 558e3d0cb3
commit 623e1e3998
20 changed files with 954 additions and 143 deletions
+416
View File
@@ -0,0 +1,416 @@
// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602
#include <atomic>
#include <mutex>
#include <windows.h>
#include <commctrl.h>
#include "inject.h"
#include "inject_internal.h"
#include "transform.h"
#include "hooks/graphics/graphics.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace nativetouch::inject {
constexpr DWORD INJECTION_RETRY_DELAY_MS = 1;
constexpr POINTER_FLAGS CONTACT_DOWN_FLAGS =
POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
constexpr POINTER_FLAGS CONTACT_UPDATE_FLAGS =
POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
// Windows 8 APIs are resolved dynamically to preserve older-OS compatibility
static decltype(RegisterTouchWindow) *RegisterTouchWindow_orig = nullptr;
static decltype(InitializeTouchInjection) *InitializeTouchInjection_ptr = nullptr;
static decltype(InjectTouchInput) *InjectTouchInput_ptr = nullptr;
// state for the single synthetic touch contact
struct ContactState {
ContactOwner owner = ContactOwner::None;
HWND input_window = nullptr;
POINT position {};
UINT_PTR timer_id = 0;
bool is_active() const {
return owner != ContactOwner::None;
}
};
// tracks an injector-owned contact without matching unrelated hardware touches
struct SyntheticTouchIdentity {
POINT down_position {};
HANDLE source = nullptr;
DWORD id = 0;
bool identified = false;
bool pending = false;
bool transform_coordinates = false;
std::atomic<HWND> transform_window { nullptr };
void begin(POINT position, HWND window, bool transform_returned_coordinates) {
down_position = position;
source = nullptr;
id = 0;
identified = false;
pending = true;
transform_coordinates = transform_returned_coordinates;
transform_window.store(window, std::memory_order_release);
}
void reset(HWND expected_window) {
source = nullptr;
id = 0;
identified = false;
pending = false;
transform_coordinates = false;
transform_window.compare_exchange_strong(
expected_window, nullptr, std::memory_order_acq_rel);
}
HWND get_transform_window() const {
return transform_window.load(std::memory_order_acquire);
}
bool matches(PTOUCHINPUT point) {
// InjectTouchInput provides no application marker in TOUCHINPUT. Claim the first
// matching DOWN, then use the source and ID assigned by Windows for this contact.
if (!identified) {
// correlate the pending injection's pixel position
constexpr LONG POSITION_TOLERANCE = 200;
const auto delta_x = point->x - down_position.x * 100;
const auto delta_y = point->y - down_position.y * 100;
if (!pending || !(point->dwFlags & TOUCHEVENTF_DOWN) ||
delta_x < -POSITION_TOLERANCE || delta_x > POSITION_TOLERANCE ||
delta_y < -POSITION_TOLERANCE || delta_y > POSITION_TOLERANCE) {
return false;
}
// save the identity that remains stable through this contact's MOVE and UP.
source = point->hSource;
id = point->dwID;
identified = true;
}
// require both the provider and contact identities to match.
return point->hSource == source && point->dwID == id;
}
bool should_transform_coordinates() const {
return transform_coordinates;
}
};
static ContactState contact_state;
static SyntheticTouchIdentity synthetic_touch_identity;
// main game window that receives WM_TOUCH forwarded from the dedicated TDJ subscreen
static std::atomic<HWND> touch_delivery_window { nullptr };
// window whose UI thread owns synthetic contact state and synthetic touch requests;
// normally the active touch window, while dedicated TDJ uses the subscreen window
static std::atomic<HWND> injection_window { nullptr };
static std::once_flag initialization_once;
static int window_subclass_token;
// submit one synthetic contact frame to Windows touch injection
static bool inject_touch_frame(POINT position, POINTER_FLAGS pointer_flags) {
POINTER_TOUCH_INFO contact {};
contact.pointerInfo.pointerType = PT_TOUCH;
contact.pointerInfo.pointerId = 0;
contact.pointerInfo.pointerFlags = pointer_flags;
contact.pointerInfo.ptPixelLocation = position;
contact.touchFlags = TOUCH_FLAG_NONE;
BOOL result = InjectTouchInput_ptr(1, &contact);
if (result) {
return true;
}
// if UPDATE fails, drop it
const auto error = GetLastError();
if (error == ERROR_NOT_READY && (pointer_flags & POINTER_FLAG_UPDATE)) {
return true;
}
// if DOWN or UP fails, retry once after a brief delay; if it still fails, log a warning
if (error == ERROR_NOT_READY) {
Sleep(INJECTION_RETRY_DELAY_MS);
result = InjectTouchInput_ptr(1, &contact);
}
if (result) {
return true;
}
static bool error_logged = false;
if (!error_logged) {
error_logged = true;
log_warning("touch::native", "failed to inject synthetic touch input: {}", GetLastError());
}
return false;
}
// rewrite only the contact created by this injector into game touch coordinates
bool transform_touch_input(PTOUCHINPUT point) {
const auto transform_window = synthetic_touch_identity.get_transform_window();
if (transform_window == nullptr) {
return false;
}
if (!synthetic_touch_identity.matches(point)) {
// this contact is not owned by this injector; leave it unchanged
return false;
}
if (synthetic_touch_identity.should_transform_coordinates()) {
// Windows receives the physical position; the game receives the mapped position
POINT position { point->x / 100, point->y / 100 };
if (transform::screen_to_game(transform_window, &position)) {
point->x = position.x * 100;
point->y = position.y * 100;
}
}
if (point->dwFlags & TOUCHEVENTF_UP) {
synthetic_touch_identity.reset(transform_window);
}
return true;
}
bool contact_is_active() {
return contact_state.is_active();
}
bool contact_is_owned_by(ContactOwner owner, HWND window) {
return contact_state.is_active() &&
contact_state.owner == owner &&
contact_state.input_window == window;
}
bool begin_contact(
ContactOwner owner,
HWND window,
POINT position,
bool transform_returned_coordinates) {
contact_state.owner = owner;
contact_state.input_window = window;
contact_state.position = position;
contact_state.timer_id = 0;
synthetic_touch_identity.begin(position, window, transform_returned_coordinates);
if (inject_touch_frame(position, CONTACT_DOWN_FLAGS)) {
return true;
}
contact_state = {};
synthetic_touch_identity.reset(window);
return false;
}
bool update_contact(ContactOwner owner, HWND window, POINT position) {
if (!contact_is_owned_by(owner, window) ||
!inject_touch_frame(position, CONTACT_UPDATE_FLAGS)) {
return false;
}
contact_state.position = position;
return true;
}
void set_contact_timer(
ContactOwner owner, HWND window, UINT_PTR timer_id) {
if (contact_is_owned_by(owner, window)) {
contact_state.timer_id = timer_id;
}
}
// end whichever producer currently owns the single synthetic contact
bool release_active_contact() {
if (!contact_state.is_active()) {
return true;
}
const auto input_window = contact_state.input_window;
if (input_window != nullptr && contact_state.timer_id != 0) {
KillTimer(input_window, contact_state.timer_id);
}
const auto result = inject_touch_frame(contact_state.position, POINTER_FLAG_UP);
contact_state = {};
// release mouse capture if this contact owns it
if (input_window != nullptr && GetCapture() == input_window) {
ReleaseCapture();
}
return result;
}
// translate primary mouse messages on a touch window into one touch contact
static LRESULT CALLBACK touch_window_subclass_proc(
HWND window, UINT message, WPARAM w_param, LPARAM l_param,
UINT_PTR subclass_id, DWORD_PTR) {
// IIDX handles touch on its main window even when input belongs to the subscreen
// forward the touch message there
if (message == WM_TOUCH &&
transform::is_tdj_dedicated_subscreen(window)) {
const auto delivery_window =
touch_delivery_window.load(std::memory_order_acquire);
if (delivery_window != nullptr) {
return SendMessageW(delivery_window, message, w_param, l_param);
}
}
if (handle_synthetic_message(window, message, w_param, l_param) ||
handle_mouse_message(window, message, w_param)) {
return 0;
}
if (message == WM_NCDESTROY) {
if (contact_state.input_window == window) {
release_active_contact();
}
HWND expected_window = window;
injection_window.compare_exchange_strong(
expected_window, nullptr, std::memory_order_acq_rel);
touch_delivery_window.store(nullptr, std::memory_order_release);
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
}
return DefSubclassProc(window, message, w_param, l_param);
}
// attach mouse injection without replacing the window's existing procedure
static void attach_window_impl(HWND window, bool register_touch) {
initialize_touch_injection();
if (!touch_injection_available()) {
if (register_touch) {
log_warning(
"touch::native",
"touch injection unavailable; touch window was not registered");
}
return;
}
// if requested, register for touch messages (for cases where the game didn't call it)
if (register_touch &&
(RegisterTouchWindow_orig == nullptr ||
!RegisterTouchWindow_orig(window, TWF_WANTPALM))) {
log_warning(
"touch::native", "failed to register mouse touch window: {}", GetLastError());
return;
}
// set window subclass to intercept messages
if (!SetWindowSubclass(
window,
touch_window_subclass_proc,
reinterpret_cast<UINT_PTR>(&window_subclass_token),
0)) {
log_warning("touch::native", "failed to attach mouse touch injection to window: {}", GetLastError());
return;
}
// publish the UI-thread target for synthetic touch requests
if (!GRAPHICS_IIDX_WSUB || window == TDJ_SUBSCREEN_WINDOW) {
injection_window.store(window, std::memory_order_release);
}
log_misc(
"touch::native", "mouse touch injection attached to window {}", fmt::ptr(window));
}
HWND get_injection_window() {
return injection_window.load(std::memory_order_acquire);
}
void attach_window(HWND window) {
// attach window, but don't register for Windows touch messages
attach_window_impl(window, false);
}
void register_and_attach_window(HWND window) {
// attach window and register for Windows touch messages
attach_window_impl(window, true);
}
// preserve native registration and attach mouse injection to the touch window
static BOOL WINAPI RegisterTouchWindowHook(HWND window, ULONG flags) {
// for TDJ in windowed subscreen mode, the main game window is
// target for delivering touch messages, not the subscreen
if (GRAPHICS_IIDX_WSUB && window != TDJ_SUBSCREEN_WINDOW) {
touch_delivery_window.store(window, std::memory_order_release);
return TRUE;
}
// call original
const auto result = RegisterTouchWindow_orig(window, flags);
if (result) {
// attach but don't register for touch messages
// (we're already in the middle of RegisterTouchWindow as a result
// of the game calling it)
attach_window(window);
}
return result;
}
static void clear_touch_injection_functions() {
InitializeTouchInjection_ptr = nullptr;
InjectTouchInput_ptr = nullptr;
}
// load and initialize Windows 8 touch injection without a static API dependency
void initialize_touch_injection() {
std::call_once(initialization_once, [] {
initialize_synthetic_touch();
// load all APIs from user32 without adding static imports
const auto user32 = libutils::load_library("user32.dll");
InitializeTouchInjection_ptr = libutils::try_proc<decltype(InitializeTouchInjection_ptr)>(
user32, "InitializeTouchInjection");
InjectTouchInput_ptr = libutils::try_proc<decltype(InjectTouchInput_ptr)>(
user32, "InjectTouchInput");
if (InitializeTouchInjection_ptr == nullptr ||
InjectTouchInput_ptr == nullptr) {
clear_touch_injection_functions();
log_warning(
"touch::native", "touch injection API unavailable; mouse touch injection disabled");
return;
}
// reserve one synthetic contact and disable Windows' visual touch feedback
if (!InitializeTouchInjection_ptr(1, TOUCH_FEEDBACK_NONE)) {
log_warning(
"touch::native", "failed to initialize mouse touch injection: {}", GetLastError());
clear_touch_injection_functions();
return;
}
log_misc("touch::native", "mouse touch injection initialized");
});
}
bool touch_injection_available() {
return InjectTouchInput_ptr != nullptr;
}
// install injection support for touch windows registered by the game module
void hook(HMODULE module) {
initialize_touch_injection();
RegisterTouchWindow_orig = detour::iat_try(
"RegisterTouchWindow", RegisterTouchWindowHook, module);
if (RegisterTouchWindow_orig != nullptr) {
log_misc("touch::native", "RegisterTouchWindow hooked");
}
}
}