mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 14:50:41 -07:00
bd2fbfcb67
## Link to GitHub Issue or related Pull Request, if one exists Fixes #833 ## Description of change Last couple PRs - such as #820 #827 #828 - made the native touch hook & touch injection using `InjectTouchInput` the default path, since it performs much more reliably with both real touch screens and mouse (or any other synthetic source). However, user has reported that WINE lacks `InjectTouchInput` which means this won't work. As a fix, revive the old wintouchemu code. Native touch is still the default, but under following circumstances: 1. if `-touchemuforce` is set, or 2. if any of the required Windows touch APIs are unavailable then we fail over from native touch to wintouchemu code. For Linux, condition #2 would be hit during init, and gracefully switch over. Caveat: the poke code for IIDX and Nost will continue to require native touch, I do not want to maintain two paths for this. This means that iidx poke will stop working on Linux, unfortunately. ## Testing Tested on Windows with `-touchemuforce` set. This is mostly reverting Linux code path back to where we were last release, so this should just work with wine.
495 lines
18 KiB
C++
495 lines
18 KiB
C++
// 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 };
|
|
|
|
// cross-thread game-loop refreshes are coalesced so they cannot flood the window queue
|
|
static std::atomic<bool> contact_refresh_pending { false };
|
|
|
|
static std::once_flag initialization_once;
|
|
static int window_subclass_token;
|
|
|
|
// asks the touch window thread to send an UPDATE when the game loop runs elsewhere
|
|
static UINT contact_refresh_message;
|
|
|
|
// submit one synthetic contact frame to Windows touch injection
|
|
static bool inject_touch_frame(
|
|
POINT position, POINTER_FLAGS pointer_flags, bool retry_if_not_ready = false) {
|
|
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;
|
|
}
|
|
|
|
// drop ordinary UPDATE frames when Windows is still processing the prior frame
|
|
const auto error = GetLastError();
|
|
if (error == ERROR_NOT_READY &&
|
|
(pointer_flags & POINTER_FLAG_UPDATE) && !retry_if_not_ready) {
|
|
return true;
|
|
}
|
|
|
|
// retry required frames once after a brief delay
|
|
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;
|
|
}
|
|
|
|
// contact state belongs to the injection window thread; this helper must run there
|
|
static void refresh_contact_lifetime_on_window_thread(HWND window) {
|
|
if (!contact_is_owned_by(contact_state.owner, window)) {
|
|
return;
|
|
}
|
|
|
|
inject_touch_frame(contact_state.position, CONTACT_UPDATE_FLAGS, true);
|
|
}
|
|
|
|
// PAN calls this from ac_io_update to keep a stationary contact alive. Contact state is
|
|
// owned by the touch window thread, so same-thread calls refresh immediately. Calls from
|
|
// another thread post one coalesced private message for the window thread to handle.
|
|
void refresh_contact_lifetime() {
|
|
const auto window = injection_window.load(std::memory_order_acquire);
|
|
if (window == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// preserve synchronous game-loop timing when it already runs on the window thread
|
|
const auto window_thread = GetWindowThreadProcessId(window, nullptr);
|
|
if (window_thread == GetCurrentThreadId()) {
|
|
refresh_contact_lifetime_on_window_thread(window);
|
|
return;
|
|
}
|
|
|
|
// otherwise marshal one outstanding refresh instead of sharing contact state across threads
|
|
if (contact_refresh_message == 0 ||
|
|
contact_refresh_pending.exchange(true, std::memory_order_acq_rel)) {
|
|
return;
|
|
}
|
|
if (!PostMessageW(window, contact_refresh_message, 0, 0)) {
|
|
contact_refresh_pending.store(false, std::memory_order_release);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
|
|
// consume marshalled lifetime refreshes on the contact-owning window thread
|
|
if (contact_refresh_message != 0 && message == contact_refresh_message) {
|
|
contact_refresh_pending.store(false, std::memory_order_release);
|
|
refresh_contact_lifetime_on_window_thread(window);
|
|
return 0;
|
|
}
|
|
|
|
// 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);
|
|
contact_refresh_pending.store(false, std::memory_order_release);
|
|
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) {
|
|
if (!initialize_touch_injection()) {
|
|
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
|
|
bool initialize_touch_injection() {
|
|
std::call_once(initialization_once, [] {
|
|
initialize_synthetic_touch();
|
|
contact_refresh_message =
|
|
RegisterWindowMessageW(L"spice2x.native_touch.refresh_contact");
|
|
if (contact_refresh_message == 0) {
|
|
log_warning(
|
|
"touch::native", "failed to register contact refresh message: {}", GetLastError());
|
|
}
|
|
|
|
// load all APIs from user32 without adding static imports
|
|
//
|
|
// note that these are expected to be present in Windows 8 and above;
|
|
// however, on WINE, touch implementation remains in Windows 7 era
|
|
// and therefore the hooks below will fail, hence the fallback to
|
|
// legacy wintouchemu code
|
|
const auto user32 = libutils::load_library("user32.dll");
|
|
InitializeTouchInjection_ptr = libutils::try_proc<decltype(InitializeTouchInjection_ptr)>(
|
|
user32, "InitializeTouchInjection");
|
|
if (InitializeTouchInjection_ptr == nullptr) {
|
|
log_warning(
|
|
"touch::native", "InitializeTouchInjection unavailable; mouse touch injection disabled");
|
|
return;
|
|
}
|
|
|
|
InjectTouchInput_ptr = libutils::try_proc<decltype(InjectTouchInput_ptr)>(
|
|
user32, "InjectTouchInput");
|
|
if (InjectTouchInput_ptr == nullptr) {
|
|
clear_touch_injection_functions();
|
|
log_warning(
|
|
"touch::native", "InjectTouchInput 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");
|
|
});
|
|
return InitializeTouchInjection_ptr != nullptr && InjectTouchInput_ptr != nullptr;
|
|
}
|
|
|
|
// install injection support for touch windows registered by the game module
|
|
bool hook_available(HMODULE module) {
|
|
if (detour::iat_find("RegisterTouchWindow", module) == nullptr) {
|
|
log_warning("touch::native", "RegisterTouchWindow unavailable");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool hook(HMODULE module) {
|
|
if (!initialize_touch_injection()) {
|
|
return false;
|
|
}
|
|
|
|
RegisterTouchWindow_orig = detour::iat_try(
|
|
"RegisterTouchWindow", RegisterTouchWindowHook, module);
|
|
if (RegisterTouchWindow_orig == nullptr) {
|
|
log_warning("touch::native", "failed to hook RegisterTouchWindow");
|
|
return false;
|
|
}
|
|
|
|
log_misc("touch::native", "RegisterTouchWindow hooked");
|
|
return true;
|
|
}
|
|
}
|