diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 4f890a6..af19197 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -572,7 +572,6 @@ set(SOURCE_FILES ${SOURCE_FILES} misc/device.cpp misc/eamuse.cpp misc/extdev.cpp - misc/nativetouchhook.cpp misc/sciunit.cpp misc/sde.cpp misc/wintouchemu.cpp @@ -657,6 +656,11 @@ set(SOURCE_FILES ${SOURCE_FILES} # touch touch/gdi_overlay.cpp + touch/native/inject.cpp + touch/native/inject_mouse.cpp + touch/native/inject_synthetic.cpp + touch/native/nativetouchhook.cpp + touch/native/transform.cpp touch/touch.cpp touch/touch_gestures.cpp touch/win7.cpp diff --git a/src/spice2x/games/iidx/iidx.cpp b/src/spice2x/games/iidx/iidx.cpp index ce74cec..4aea811 100644 --- a/src/spice2x/games/iidx/iidx.cpp +++ b/src/spice2x/games/iidx/iidx.cpp @@ -20,7 +20,7 @@ #include "hooks/sleephook.h" #include "launcher/options.h" #include "touch/touch.h" -#include "misc/nativetouchhook.h" +#include "touch/native/nativetouchhook.h" #include "misc/wintouchemu.h" #include "misc/eamuse.h" #include "util/detour.h" @@ -351,7 +351,7 @@ namespace games::iidx { devicehook_init(avs::core::DLL_INSTANCE); if (NATIVE_TOUCH) { - nativetouchhook::hook(avs::game::DLL_INSTANCE); + nativetouch::hook(avs::game::DLL_INSTANCE); } else { wintouchemu::FORCE = true; wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; diff --git a/src/spice2x/games/iidx/poke.cpp b/src/spice2x/games/iidx/poke.cpp index 97a133c..f8f756e 100644 --- a/src/spice2x/games/iidx/poke.cpp +++ b/src/spice2x/games/iidx/poke.cpp @@ -11,22 +11,11 @@ #include "misc/eamuse.h" #include "overlay/overlay.h" #include "overlay/windows/generic_sub.h" -#include "rawinput/rawinput.h" +#include "touch/native/inject.h" #include "touch/touch.h" -#include "util/libutils.h" #include "util/logging.h" #include "util/precise_timer.h" -#define POKE_NATIVE_TOUCH 0 - -#if POKE_NATIVE_TOUCH -static HINSTANCE USER32_INSTANCE = nullptr; -typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD); -typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*); -static InitializeTouchInjection_t pInitializeTouchInjection = nullptr; -static InjectTouchInput_t pInjectTouchInput = nullptr; -#endif - namespace games::iidx::poke { static std::thread *THREAD = nullptr; @@ -116,72 +105,14 @@ namespace games::iidx::poke { {"1/D", 50}, }; -#if POKE_NATIVE_TOUCH - void initialize_native_touch_library() { - if (USER32_INSTANCE == nullptr) { - USER32_INSTANCE = libutils::load_library("user32.dll"); - } - - pInitializeTouchInjection = libutils::try_proc( - USER32_INSTANCE, "InitializeTouchInjection"); - pInjectTouchInput = libutils::try_proc( - USER32_INSTANCE, "InjectTouchInput"); - } - - void emulate_native_touch(TouchPoint tp, bool is_down) { - if (pInjectTouchInput == nullptr) { + static void inject_native_touch_points(const std::vector &touch_points, bool down) { + if (touch_points.empty()) { return; } - POINTER_TOUCH_INFO contact; - BOOL bRet = TRUE; - const int contact_offset = 2; - - memset(&contact, 0, sizeof(POINTER_TOUCH_INFO)); - - contact.pointerInfo.pointerType = PT_TOUCH; - contact.pointerInfo.pointerId = 0; - contact.pointerInfo.ptPixelLocation.x = tp.x; - contact.pointerInfo.ptPixelLocation.y = tp.y; - if (is_down) { - contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; - } else { - contact.pointerInfo.pointerFlags = POINTER_FLAG_UP; - } - - contact.pointerInfo.dwTime = 0; - contact.pointerInfo.PerformanceCount = 0; - - contact.touchFlags = TOUCH_FLAG_NONE; - contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE; - contact.orientation = 0; - contact.pressure = 1024; - - contact.rcContact.top = tp.y - contact_offset; - contact.rcContact.bottom = tp.y + contact_offset; - contact.rcContact.left = tp.x - contact_offset; - contact.rcContact.right = tp.x + contact_offset; - - bRet = InjectTouchInput(1, &contact); - if (!bRet) { - log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError()); - } + const auto &touch = touch_points.front(); + nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down); } - - void emulate_native_touch_points(std::vector *touch_points) { - int i = 0; - for (auto &touch : *touch_points) { - emulate_native_touch(touch, true); - } - } - - void clear_native_touch_points(std::vector *touch_points) { - for (auto &touch : *touch_points) { - emulate_native_touch(touch, false); - } - touch_points->clear(); - } -#endif void clear_touch_points(std::vector *touch_points) { std::vector touch_ids; @@ -209,36 +140,19 @@ namespace games::iidx::poke { std::vector touch_points; std::vector last_keypad_state = {0, 0}; - -#if POKE_NATIVE_TOUCH - bool use_native = games::iidx::NATIVE_TOUCH; - - if (use_native) { - initialize_native_touch_library(); - - if (pInitializeTouchInjection != nullptr) { - pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE); - } - } - -#else - bool use_native = false; -#endif + const bool use_native = games::iidx::NATIVE_TOUCH; // set variable to false to stop while (THREAD_RUNNING) { // clean up touch from last frame if (touch_points.size() > 0) { -#if POKE_NATIVE_TOUCH if (use_native) { - clear_native_touch_points(&touch_points); + inject_native_touch_points(touch_points, false); + touch_points.clear(); } else { clear_touch_points(&touch_points); } -#else - clear_touch_points(&touch_points); -#endif } for (int unit = 0; unit < 2; unit++) { @@ -264,8 +178,16 @@ namespace games::iidx::poke { float x = x_iter->second / 1920.0; float y = y_iter->second / 1080.0; if (use_native) { - x *= rawinput::TOUCHSCREEN_RANGE_X; - y *= rawinput::TOUCHSCREEN_RANGE_Y; + if (GRAPHICS_WINDOWED) { + if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) { + continue; + } + x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH; + y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT; + } else { + x = x_iter->second; + y = y_iter->second; + } } else if (GRAPHICS_IIDX_WSUB) { // Scale to windowed subscreen x *= GRAPHICS_WSUB_WIDTH; @@ -309,21 +231,25 @@ namespace games::iidx::poke { } // for all units if (touch_points.size() > 0) { -#if POKE_NATIVE_TOUCH if (use_native) { - emulate_native_touch_points(&touch_points); + inject_native_touch_points(touch_points, true); } else { touch_write_points(&touch_points); } -#else - touch_write_points(&touch_points); -#endif } // slow down timer.sleep(50); } + if (!touch_points.empty()) { + if (use_native) { + inject_native_touch_points(touch_points, false); + } else { + clear_touch_points(&touch_points); + } + } + return nullptr; }); } diff --git a/src/spice2x/games/popn/popn.cpp b/src/spice2x/games/popn/popn.cpp index 5695872..e9e051c 100644 --- a/src/spice2x/games/popn/popn.cpp +++ b/src/spice2x/games/popn/popn.cpp @@ -19,7 +19,7 @@ #include "util/sysutils.h" #include "io.h" #include "util/deferlog.h" -#include "misc/nativetouchhook.h" +#include "touch/native/nativetouchhook.h" #include "misc/wintouchemu.h" namespace games::popn { @@ -715,14 +715,12 @@ namespace games::popn { // 00000100 0000000B 00000001 (button 9) // set third column to 0 and it will work with BIO2 - if (!GRAPHICS_WINDOWED) { - if (NATIVE_TOUCH) { - nativetouchhook::hook(avs::game::DLL_INSTANCE); - } else { - wintouchemu::FORCE = true; - wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; - wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE); - } + if (NATIVE_TOUCH) { + nativetouch::hook(avs::game::DLL_INSTANCE); + } else if (!GRAPHICS_WINDOWED) { + wintouchemu::FORCE = true; + wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; + wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE); } sysutils::hook_EnumDisplayDevicesA(); diff --git a/src/spice2x/games/sdvx/sdvx.cpp b/src/spice2x/games/sdvx/sdvx.cpp index 650e9a0..df6909c 100644 --- a/src/spice2x/games/sdvx/sdvx.cpp +++ b/src/spice2x/games/sdvx/sdvx.cpp @@ -26,7 +26,7 @@ #include "util/libutils.h" #include "util/sysutils.h" #include "misc/eamuse.h" -#include "misc/nativetouchhook.h" +#include "touch/native/nativetouchhook.h" #include "misc/wintouchemu.h" #include "bi2x_hook.h" #include "camera.h" @@ -456,7 +456,7 @@ namespace games::sdvx { if (is_valkyrie_model()) { if (NATIVETOUCH) { - nativetouchhook::hook(avs::game::DLL_INSTANCE); + nativetouch::hook(avs::game::DLL_INSTANCE); } else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) { // hook touch window // in windowed mode, game can accept mouse input on the second screen diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 5363a45..723ae6d 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -29,6 +29,7 @@ #include "util/fileutils.h" #include "util/utils.h" #include "misc/wintouchemu.h" +#include "touch/native/inject.h" #include "util/time.h" #include "rawinput/rawinput.h" @@ -797,7 +798,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei nWidth = rect.right - rect.left; nHeight = rect.bottom - rect.top; - touch_attach_wnd(TDJ_SUBSCREEN_WINDOW); + if (games::iidx::NATIVE_TOUCH) { + nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW); + } else { + touch_attach_wnd(TDJ_SUBSCREEN_WINDOW); + } } else { // Existing behaviour: suppress subscreen window and prompt user to use overlay instead log_info( @@ -1163,6 +1168,13 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet WNDPROC_ORIG = reinterpret_cast(GetWindowLongPtrA(hWnd, GWLP_WNDPROC)); SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast(WindowProc)); + const bool native_touch_overlay = + (games::iidx::NATIVE_TOUCH && !GRAPHICS_IIDX_WSUB) || + (games::popn::NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS); + if (native_touch_overlay) { + nativetouch::inject::register_and_attach_window(hWnd); + } + // NOLEGACY causes WM_CHAR to be not received // reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch) if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) { diff --git a/src/spice2x/overlay/windows/iidx_sub.cpp b/src/spice2x/overlay/windows/iidx_sub.cpp index 22036e8..53c99e5 100644 --- a/src/spice2x/overlay/windows/iidx_sub.cpp +++ b/src/spice2x/overlay/windows/iidx_sub.cpp @@ -15,8 +15,6 @@ namespace overlay::windows { this->disabled_message = "Close this overlay and use the second window. (try ALT+TAB)"; } else if (games::iidx::IIDX_TDJ_MONITOR_WARNING) { this->disabled_message = "TDJ mode subscreen overlay is not compatible with -dxmainadapter option; use -mainmonitor instead"; - } else if (games::iidx::NATIVE_TOUCH) { - this->disabled_message = "Overlay disabled by user (-iidxnativetouch option is on, used for real touch screens)"; } float size = 0.5f; diff --git a/src/spice2x/overlay/windows/popn_sub.cpp b/src/spice2x/overlay/windows/popn_sub.cpp index fb431da..5a88975 100644 --- a/src/spice2x/overlay/windows/popn_sub.cpp +++ b/src/spice2x/overlay/windows/popn_sub.cpp @@ -15,8 +15,6 @@ namespace overlay::windows { this->disabled_message = "Game did not launch as Pikapika Pop-kun (invalid )!"; } else if (games::popn::SHOW_PIKA_MONITOR_WARNING) { this->disabled_message = "Subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead"; - } else if (games::popn::NATIVE_TOUCH) { - this->disabled_message = "Overlay disabled by user (-popnnativetouch option is on, used for real touch screens)"; } else if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { this->disabled_message = "Subscren overlay was not enabled in spicecfg. Use the second window (ALT+TAB)."; } diff --git a/src/spice2x/overlay/windows/sdvx_sub.cpp b/src/spice2x/overlay/windows/sdvx_sub.cpp index b8fe93b..a7081e1 100644 --- a/src/spice2x/overlay/windows/sdvx_sub.cpp +++ b/src/spice2x/overlay/windows/sdvx_sub.cpp @@ -14,8 +14,6 @@ namespace overlay::windows { this->disabled_message = "Valkyrie Model mode is not enabled!"; } else if (games::sdvx::SHOW_VM_MONITOR_WARNING) { this->disabled_message = "VM mode subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead"; - } else if (games::sdvx::NATIVETOUCH) { - this->disabled_message = "Overlay disabled by user (-sdvxnativetouch option is on, used for real touch screens)"; } else if (GRAPHICS_WINDOWED) { if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) { this->disabled_message = "Subscreen has been disabled by the user (-sdvxnosub)."; diff --git a/src/spice2x/touch/native/inject.cpp b/src/spice2x/touch/native/inject.cpp new file mode 100644 index 0000000..3342889 --- /dev/null +++ b/src/spice2x/touch/native/inject.cpp @@ -0,0 +1,416 @@ +// 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 }; + + 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(&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( + user32, "InitializeTouchInjection"); + InjectTouchInput_ptr = libutils::try_proc( + 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"); + } + } +} diff --git a/src/spice2x/touch/native/inject.h b/src/spice2x/touch/native/inject.h new file mode 100644 index 0000000..2331caa --- /dev/null +++ b/src/spice2x/touch/native/inject.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +struct tagTOUCHINPUT; + +namespace nativetouch::inject { + void attach_window(HWND window); + void register_and_attach_window(HWND window); + void hook(HMODULE module); + bool inject_synthetic_touch(POINT position, bool down); + bool transform_touch_input(tagTOUCHINPUT *point); +} diff --git a/src/spice2x/touch/native/inject_internal.h b/src/spice2x/touch/native/inject_internal.h new file mode 100644 index 0000000..b519ec3 --- /dev/null +++ b/src/spice2x/touch/native/inject_internal.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace nativetouch::inject { + + enum class ContactOwner { + None, + Mouse, + Synthetic, + }; + + void initialize_touch_injection(); + void initialize_synthetic_touch(); + bool touch_injection_available(); + + bool contact_is_active(); + bool contact_is_owned_by(ContactOwner owner, HWND window); + bool begin_contact( + ContactOwner owner, + HWND window, + POINT position, + bool transform_returned_coordinates); + bool update_contact(ContactOwner owner, HWND window, POINT position); + void set_contact_timer(ContactOwner owner, HWND window, UINT_PTR timer_id); + bool release_active_contact(); + + HWND get_injection_window(); + bool handle_mouse_message(HWND window, UINT message, WPARAM w_param); + bool handle_synthetic_message(HWND window, UINT message, WPARAM w_param, LPARAM l_param); +} diff --git a/src/spice2x/touch/native/inject_mouse.cpp b/src/spice2x/touch/native/inject_mouse.cpp new file mode 100644 index 0000000..6958f6c --- /dev/null +++ b/src/spice2x/touch/native/inject_mouse.cpp @@ -0,0 +1,143 @@ +// enable Windows 8 touch injection types; the functions are loaded dynamically +#define _WIN32_WINNT 0x0602 + +#include + +#include "inject_internal.h" +#include "transform.h" + +#include "touch/touch.h" +#include "util/logging.h" + +namespace nativetouch::inject { + + constexpr UINT CONTACT_TIMER_INTERVAL_MS = 16; + + static int mouse_contact_timer_token; + + struct PrimaryMouseButton { + UINT down_message; + UINT double_click_message; + UINT up_message; + WPARAM state_mask; + }; + + // honor the user's swapped-button setting when choosing the primary button + static PrimaryMouseButton get_primary_mouse_button() { + if (GetSystemMetrics(SM_SWAPBUTTON)) { + return { WM_RBUTTONDOWN, WM_RBUTTONDBLCLK, WM_RBUTTONUP, MK_RBUTTON }; + } + return { WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, WM_LBUTTONUP, MK_LBUTTON }; + } + + // use the current physical cursor but reject points outside the subscreen + static bool get_mouse_injection_position(HWND window, POINT *position) { + + // queued WM_MOUSEMOVE coordinates can lag behind the cursor; injecting them makes + // Windows move its primary pointer back to stale positions during a drag. + if (!GetCursorPos(position)) { + return false; + } + + POINT transformed = *position; + return transform::screen_to_game(window, &transformed); + } + + // release the active injected contact and its window capture + static void end_mouse_contact(HWND window) { + if (contact_is_owned_by(ContactOwner::Mouse, window)) { + release_active_contact(); + } + } + + // begin a contact at the physical cursor position and capture future mouse input + static void begin_mouse_contact(HWND window) { + if (contact_is_active()) { + return; + } + + POINT position; + if (!get_mouse_injection_position(window, &position) || + !begin_contact(ContactOwner::Mouse, window, position, true)) { + return; + } + + // keep receiving drag messages after the cursor leaves the client area + SetCapture(window); + const auto timer_id = reinterpret_cast(&mouse_contact_timer_token); + if (SetTimer(window, timer_id, CONTACT_TIMER_INTERVAL_MS, nullptr)) { + set_contact_timer(ContactOwner::Mouse, window, timer_id); + } else { + log_warning("touch::native", "failed to start mouse touch injection timer"); + } + } + + // update the contact while the primary button remains held + static void move_mouse_contact( + HWND window, WPARAM w_param, WPARAM primary_button_state) { + if (!contact_is_owned_by(ContactOwner::Mouse, window)) { + return; + } + + POINT position; + if (!get_mouse_injection_position(window, &position)) { + end_mouse_contact(window); + return; + } + + if ((w_param & primary_button_state) == 0) { + end_mouse_contact(window); + return; + } + + update_contact(ContactOwner::Mouse, window, position); + } + + // emit stationary update frames so Windows keeps the contact alive + static void refresh_mouse_contact(HWND window) { + if (!contact_is_owned_by(ContactOwner::Mouse, window)) { + return; + } + + POINT position {}; + if (!GetCursorPos(&position)) { + return; + } + + POINT transformed = position; + if (!transform::screen_to_game(window, &transformed)) { + end_mouse_contact(window); + return; + } + + update_contact(ContactOwner::Mouse, window, position); + } + + bool handle_mouse_message(HWND window, UINT message, WPARAM w_param) { + if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST && + is_mouse_message_from_touchscreen()) { + return true; + } + + if (message == WM_TIMER && + w_param == reinterpret_cast(&mouse_contact_timer_token)) { + refresh_mouse_contact(window); + return true; + } + + const auto primary_button = get_primary_mouse_button(); + if (message == primary_button.down_message || + message == primary_button.double_click_message) { + begin_mouse_contact(window); + } else if (message == WM_MOUSEMOVE) { + move_mouse_contact(window, w_param, primary_button.state_mask); + } else if (message == primary_button.up_message) { + end_mouse_contact(window); + } else if (message == WM_CANCELMODE || message == WM_KILLFOCUS || + message == WM_CAPTURECHANGED) { + end_mouse_contact(window); + } + + return false; + } +} diff --git a/src/spice2x/touch/native/inject_synthetic.cpp b/src/spice2x/touch/native/inject_synthetic.cpp new file mode 100644 index 0000000..ca29cb4 --- /dev/null +++ b/src/spice2x/touch/native/inject_synthetic.cpp @@ -0,0 +1,104 @@ +// enable Windows 8 touch injection types; the functions are loaded dynamically +#define _WIN32_WINNT 0x0602 + +#include + +#include +#include + +#include "inject.h" +#include "inject_internal.h" +#include "transform.h" + +#include "util/logging.h" + +namespace nativetouch::inject { + + constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100; + + static std::once_flag synthetic_initialization_once; + static int synthetic_contact_timer_token; + static UINT synthetic_touch_message; + + void initialize_synthetic_touch() { + std::call_once(synthetic_initialization_once, [] { + synthetic_touch_message = RegisterWindowMessageW(L"spice2x.native_touch.inject"); + if (synthetic_touch_message == 0) { + log_warning( + "touch::native", "failed to register synthetic touch message: {}", GetLastError()); + } + }); + } + + // synthetic touches preempt the mouse and keep it disabled until release or timeout + static void begin_synthetic_contact(HWND window, POINT position) { + // 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)) { + return; + } + if (!release_active_contact()) { + return; + } + if (!begin_contact( + ContactOwner::Synthetic, + window, + position, + transform_returned_coordinates)) { + 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 { + log_warning("touch::native", "failed to start synthetic touch timeout timer"); + } + } + + static void end_synthetic_contact(HWND window) { + if (contact_is_owned_by(ContactOwner::Synthetic, window)) { + release_active_contact(); + } + } + + bool handle_synthetic_message( + 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); + } + return true; + } + + if (message == WM_TIMER && + w_param == reinterpret_cast(&synthetic_contact_timer_token)) { + end_synthetic_contact(window); + return true; + } + + return false; + } + + // inject synthetic touches; only one contact is supported at a time for simplicity + bool inject_synthetic_touch(POINT position, bool down) { + initialize_touch_injection(); + + const auto window = get_injection_window(); + if (!touch_injection_available() || window == nullptr || + synthetic_touch_message == 0) { + return false; + } + + return PostMessageW( + window, + synthetic_touch_message, + static_cast(down), + MAKELPARAM(position.x, position.y)) != FALSE; + } +} \ No newline at end of file diff --git a/src/spice2x/misc/nativetouchhook.cpp b/src/spice2x/touch/native/nativetouchhook.cpp similarity index 52% rename from src/spice2x/misc/nativetouchhook.cpp rename to src/spice2x/touch/native/nativetouchhook.cpp index ac9d256..1be38f7 100644 --- a/src/spice2x/misc/nativetouchhook.cpp +++ b/src/spice2x/touch/native/nativetouchhook.cpp @@ -3,9 +3,9 @@ #define _WIN32_WINNT 0x0601 #include "avs/game.h" -#include "wintouchemu.h" #include "rawinput/touch.h" -#include "hooks/graphics/graphics.h" +#include "inject.h" +#include "transform.h" #include "util/detour.h" #include "util/logging.h" @@ -20,9 +20,36 @@ #define log_debug(module, format_str, ...) #endif -namespace nativetouchhook { +namespace nativetouch { static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr; + static bool native_display_initialized = false; + static DWORD native_display_orientation = DMDO_DEFAULT; + static long native_display_size_x = 1920L; + static long native_display_size_y = 1080L; + + static void update_native_display_mode() { + RECT display_rect{}; + if (GetWindowRect(GetDesktopWindow(), &display_rect)) { + native_display_size_x = display_rect.right - display_rect.left; + native_display_size_y = display_rect.bottom - display_rect.top; + } + + DEVMODE display_mode{}; + display_mode.dmSize = sizeof(display_mode); + if (EnumDisplaySettingsEx(nullptr, ENUM_CURRENT_SETTINGS, &display_mode, EDS_RAWMODE) && + (display_mode.dmFields & DM_DISPLAYORIENTATION)) { + native_display_orientation = display_mode.dmDisplayOrientation; + } else { + log_info("touch::native", "failed to determine monitor orientation"); + } + + log_info( + "touch::native", "primary display mode: {}x{}, orientation {}", + native_display_size_x, + native_display_size_y, + native_display_orientation); + } static void strip_contact_size(PTOUCHINPUT point) { @@ -67,51 +94,72 @@ namespace nativetouchhook { } static void flip_touch_points(PTOUCHINPUT point) { - point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x; - point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y; + point->x = native_display_size_x * 100 - point->x; + point->y = native_display_size_y * 100 - point->y; } static BOOL WINAPI GetTouchInputInfoHook( HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) { + // Refresh after exclusive fullscreen establishes the final display mode. + if (!native_display_initialized) { + update_native_display_mode(); + native_display_initialized = true; + } + // call the original fist const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize); if (result == 0) { return result; } - bool flip_values = false; - if (avs::game::is_model("KFC") && rawinput::touch::DISPLAY_INITIALIZED) { + bool flip_hardware_touch = false; + if (avs::game::is_model("KFC")) { log_debug( - "touch::native", "DISPLAY_ORIENTATION = {}, DISPLAY_SIZE_X = {}, DISPLAY_SIZE_Y = {}", - rawinput::touch::DISPLAY_ORIENTATION, - rawinput::touch::DISPLAY_SIZE_X, - rawinput::touch::DISPLAY_SIZE_Y); - if (rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) { - flip_values = true; + "touch::native", "orientation = {}, display size = {}x{}", + native_display_orientation, + native_display_size_x, + native_display_size_y); + if (native_display_orientation == DMDO_270) { + flip_hardware_touch = true; } } - if (rawinput::touch::INVERTED) { - flip_values = !flip_values; - } - for (size_t i = 0; i < cInputs; i++) { PTOUCHINPUT point = &pInputs[i]; + const auto synthetic = inject::transform_touch_input(point); + if (avs::game::is_model("LDJ")) { strip_contact_size(point); } + const auto flip_values = !synthetic && + (rawinput::touch::INVERTED ^ flip_hardware_touch); if (flip_values) { flip_touch_points(point); } + if (!synthetic) { + POINT position { point->x / 100, point->y / 100 }; + const auto transform_result = + transform::hardware_to_game(&position); + if (transform_result == transform::Result::Transformed) { + point->x = position.x * 100; + point->y = position.y * 100; + } else if (transform_result == transform::Result::Rejected && + !(point->dwFlags & TOUCHEVENTF_UP)) { + // suppress rejected contacts, but preserve UP to release an active touch ID + point->dwFlags = 0; + } + } } return result; } void hook(HMODULE module) { + inject::hook(module); + GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module); if (GetTouchInputInfo_orig != nullptr) { log_misc("touch::native", "GetTouchInputInfo hooked"); diff --git a/src/spice2x/misc/nativetouchhook.h b/src/spice2x/touch/native/nativetouchhook.h similarity index 66% rename from src/spice2x/misc/nativetouchhook.h rename to src/spice2x/touch/native/nativetouchhook.h index c050f22..c26f6f2 100644 --- a/src/spice2x/misc/nativetouchhook.h +++ b/src/spice2x/touch/native/nativetouchhook.h @@ -1,6 +1,6 @@ #include -namespace nativetouchhook { +namespace nativetouch { void hook(HMODULE module); } \ No newline at end of file diff --git a/src/spice2x/touch/native/transform.cpp b/src/spice2x/touch/native/transform.cpp new file mode 100644 index 0000000..ce2eb38 --- /dev/null +++ b/src/spice2x/touch/native/transform.cpp @@ -0,0 +1,107 @@ +#include "transform.h" + +#include "hooks/graphics/graphics.h" +#include "overlay/overlay.h" +#include "touch/touch.h" + +namespace nativetouch::transform { + + bool is_tdj_dedicated_subscreen(HWND window) { + return window != nullptr && GRAPHICS_WINDOWED && GRAPHICS_IIDX_WSUB && + window == TDJ_SUBSCREEN_WINDOW; + } + + // convert game touch coordinates to physical screen coordinates for dedicated subscreen injection + bool game_to_screen(HWND window, POINT *position) { + if (!is_tdj_dedicated_subscreen(window)) { + return true; + } + + RECT client_rect {}; + if (!GetClientRect(window, &client_rect) || + client_rect.right <= 0 || client_rect.bottom <= 0 || + SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) { + return false; + } + + position->x = MulDiv( + position->x - SPICETOUCH_TOUCH_X, + client_rect.right, + SPICETOUCH_TOUCH_WIDTH); + position->y = MulDiv( + position->y - SPICETOUCH_TOUCH_Y, + client_rect.bottom, + SPICETOUCH_TOUCH_HEIGHT); + return ClientToScreen(window, position) != FALSE; + } + + static bool has_active_overlay_transform() { + return overlay::OVERLAY != nullptr && + overlay::OVERLAY->get_active() && + overlay::OVERLAY->can_transform_touch_input(); + } + + static bool transform_overlay_touch_position(POINT *position) { + // convert physical screen coordinates to the window-relative coordinates the overlay expects + if (GRAPHICS_WINDOWED) { + position->x -= SPICETOUCH_TOUCH_X; + position->y -= SPICETOUCH_TOUCH_Y; + } + + // ask the overlay to do the game-specific translation + return overlay::OVERLAY->transform_touch_point(&position->x, &position->y); + } + + // convert physical screen coordinates to game touch coordinates for a known target + bool screen_to_game(HWND window, POINT *position) { + // scale the resized IIDX subscreen client area into the game's touch-display coordinates + if (is_tdj_dedicated_subscreen(window)) { + RECT client_rect {}; + if (!GetClientRect(window, &client_rect) || + client_rect.right <= 0 || client_rect.bottom <= 0 || + SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) { + return false; + } + + if (!ScreenToClient(window, position)) { + return false; + } + if (!PtInRect(&client_rect, *position)) { + return false; + } + position->x = SPICETOUCH_TOUCH_X + + MulDiv(position->x, SPICETOUCH_TOUCH_WIDTH, client_rect.right); + position->y = SPICETOUCH_TOUCH_Y + + MulDiv(position->y, SPICETOUCH_TOUCH_HEIGHT, client_rect.bottom); + return true; + } + + // check if subscreen overlay is active and can transform the touch point; + // if not, the touch point is valid as-is + if (!has_active_overlay_transform()) { + return true; + } + + // ask the overlay to transform the touch point into game coordinates + return transform_overlay_touch_position(position); + } + + // route hardware screen coordinates through dedicated or overlay mapping and report the result + Result hardware_to_game(POINT *position) { + const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW); + const auto active_overlay = has_active_overlay_transform(); + + // no dedicated subscreen or active overlay mapping; pass the point through unchanged + if (!dedicated_subscreen && !active_overlay) { + return Result::Unchanged; + } + + // route through the dedicated subscreen when active, otherwise through the overlay + const auto valid = screen_to_game( + dedicated_subscreen ? TDJ_SUBSCREEN_WINDOW : nullptr, + position); + + // reject out-of-bounds points and any coordinate conversion failure + return valid ? Result::Transformed : Result::Rejected; + } +} diff --git a/src/spice2x/touch/native/transform.h b/src/spice2x/touch/native/transform.h new file mode 100644 index 0000000..6bf5d8d --- /dev/null +++ b/src/spice2x/touch/native/transform.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace nativetouch::transform { + enum class Result { + Unchanged, + Transformed, + Rejected, + }; + + bool is_tdj_dedicated_subscreen(HWND window); + bool game_to_screen(HWND window, POINT *position); + bool screen_to_game(HWND window, POINT *position); + Result hardware_to_game(POINT *position); +} diff --git a/src/spice2x/touch/touch.cpp b/src/spice2x/touch/touch.cpp index 4fd9bdf..0ade823 100644 --- a/src/spice2x/touch/touch.cpp +++ b/src/spice2x/touch/touch.cpp @@ -81,8 +81,6 @@ static const char *LOG_MODULE_NAME = "touch"; static TouchHandler *TOUCH_HANDLER = nullptr; -static bool is_mouse_message_from_touchscreen(); - TouchHandler::TouchHandler(std::string name) { log_info("touch", "Using touch handler: {}", name); } @@ -1003,7 +1001,7 @@ void update_spicetouch_window_dimensions(HWND hWnd) { } // https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages -static bool is_mouse_message_from_touchscreen() { +bool is_mouse_message_from_touchscreen() { constexpr ULONG_PTR MI_WP_SIGNATURE = 0xFF515700; constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00; return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE; diff --git a/src/spice2x/touch/touch.h b/src/spice2x/touch/touch.h index c1abb20..920e6cd 100644 --- a/src/spice2x/touch/touch.h +++ b/src/spice2x/touch/touch.h @@ -29,6 +29,7 @@ extern int SPICETOUCH_TOUCH_WIDTH; extern int SPICETOUCH_TOUCH_HEIGHT; bool is_touch_available(LPCSTR caller); +bool is_mouse_message_from_touchscreen(); void touch_attach_wnd(HWND hWnd); void touch_attach_dx_hook();