// enable Windows 8 touch injection types; the functions are loaded dynamically #define _WIN32_WINNT 0x0602 #include #include #include #include #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 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 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 injection_window { nullptr }; // cross-thread game-loop refreshes are coalesced so they cannot flood the window queue static std::atomic 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(&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( user32, "InitializeTouchInjection"); if (InitializeTouchInjection_ptr == nullptr) { log_warning( "touch::native", "InitializeTouchInjection unavailable; mouse touch injection disabled"); return; } InjectTouchInput_ptr = libutils::try_proc( 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; } }