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
+5 -1
View File
@@ -572,7 +572,6 @@ set(SOURCE_FILES ${SOURCE_FILES}
misc/device.cpp misc/device.cpp
misc/eamuse.cpp misc/eamuse.cpp
misc/extdev.cpp misc/extdev.cpp
misc/nativetouchhook.cpp
misc/sciunit.cpp misc/sciunit.cpp
misc/sde.cpp misc/sde.cpp
misc/wintouchemu.cpp misc/wintouchemu.cpp
@@ -657,6 +656,11 @@ set(SOURCE_FILES ${SOURCE_FILES}
# touch # touch
touch/gdi_overlay.cpp 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.cpp
touch/touch_gestures.cpp touch/touch_gestures.cpp
touch/win7.cpp touch/win7.cpp
+2 -2
View File
@@ -20,7 +20,7 @@
#include "hooks/sleephook.h" #include "hooks/sleephook.h"
#include "launcher/options.h" #include "launcher/options.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "misc/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/detour.h" #include "util/detour.h"
@@ -351,7 +351,7 @@ namespace games::iidx {
devicehook_init(avs::core::DLL_INSTANCE); devicehook_init(avs::core::DLL_INSTANCE);
if (NATIVE_TOUCH) { if (NATIVE_TOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
} else { } else {
wintouchemu::FORCE = true; wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
+27 -101
View File
@@ -11,22 +11,11 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/windows/generic_sub.h" #include "overlay/windows/generic_sub.h"
#include "rawinput/rawinput.h" #include "touch/native/inject.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.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 { namespace games::iidx::poke {
static std::thread *THREAD = nullptr; static std::thread *THREAD = nullptr;
@@ -116,73 +105,15 @@ namespace games::iidx::poke {
{"1/D", 50}, {"1/D", 50},
}; };
#if POKE_NATIVE_TOUCH static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
void initialize_native_touch_library() { if (touch_points.empty()) {
if (USER32_INSTANCE == nullptr) {
USER32_INSTANCE = libutils::load_library("user32.dll");
}
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
USER32_INSTANCE, "InitializeTouchInjection");
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
USER32_INSTANCE, "InjectTouchInput");
}
void emulate_native_touch(TouchPoint tp, bool is_down) {
if (pInjectTouchInput == nullptr) {
return; return;
} }
POINTER_TOUCH_INFO contact; const auto &touch = touch_points.front();
BOOL bRet = TRUE; nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
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());
}
}
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
int i = 0;
for (auto &touch : *touch_points) {
emulate_native_touch(touch, true);
}
}
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
for (auto &touch : *touch_points) {
emulate_native_touch(touch, false);
}
touch_points->clear();
}
#endif
void clear_touch_points(std::vector<TouchPoint> *touch_points) { void clear_touch_points(std::vector<TouchPoint> *touch_points) {
std::vector<DWORD> touch_ids; std::vector<DWORD> touch_ids;
for (auto &touch : *touch_points) { for (auto &touch : *touch_points) {
@@ -209,36 +140,19 @@ namespace games::iidx::poke {
std::vector<TouchPoint> touch_points; std::vector<TouchPoint> touch_points;
std::vector<uint16_t> last_keypad_state = {0, 0}; std::vector<uint16_t> last_keypad_state = {0, 0};
const bool use_native = games::iidx::NATIVE_TOUCH;
#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
// set variable to false to stop // set variable to false to stop
while (THREAD_RUNNING) { while (THREAD_RUNNING) {
// clean up touch from last frame // clean up touch from last frame
if (touch_points.size() > 0) { if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) { if (use_native) {
clear_native_touch_points(&touch_points); inject_native_touch_points(touch_points, false);
touch_points.clear();
} else { } else {
clear_touch_points(&touch_points); clear_touch_points(&touch_points);
} }
#else
clear_touch_points(&touch_points);
#endif
} }
for (int unit = 0; unit < 2; unit++) { for (int unit = 0; unit < 2; unit++) {
@@ -264,8 +178,16 @@ namespace games::iidx::poke {
float x = x_iter->second / 1920.0; float x = x_iter->second / 1920.0;
float y = y_iter->second / 1080.0; float y = y_iter->second / 1080.0;
if (use_native) { if (use_native) {
x *= rawinput::TOUCHSCREEN_RANGE_X; if (GRAPHICS_WINDOWED) {
y *= rawinput::TOUCHSCREEN_RANGE_Y; 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) { } else if (GRAPHICS_IIDX_WSUB) {
// Scale to windowed subscreen // Scale to windowed subscreen
x *= GRAPHICS_WSUB_WIDTH; x *= GRAPHICS_WSUB_WIDTH;
@@ -309,21 +231,25 @@ namespace games::iidx::poke {
} // for all units } // for all units
if (touch_points.size() > 0) { if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) { if (use_native) {
emulate_native_touch_points(&touch_points); inject_native_touch_points(touch_points, true);
} else { } else {
touch_write_points(&touch_points); touch_write_points(&touch_points);
} }
#else
touch_write_points(&touch_points);
#endif
} }
// slow down // slow down
timer.sleep(50); 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; return nullptr;
}); });
} }
+3 -5
View File
@@ -19,7 +19,7 @@
#include "util/sysutils.h" #include "util/sysutils.h"
#include "io.h" #include "io.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "misc/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
namespace games::popn { namespace games::popn {
@@ -715,15 +715,13 @@ namespace games::popn {
// 00000100 0000000B 00000001 (button 9) // 00000100 0000000B 00000001 (button 9)
// set third column to 0 and it will work with BIO2 // set third column to 0 and it will work with BIO2
if (!GRAPHICS_WINDOWED) {
if (NATIVE_TOUCH) { if (NATIVE_TOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
} else { } else if (!GRAPHICS_WINDOWED) {
wintouchemu::FORCE = true; wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE); wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE);
} }
}
sysutils::hook_EnumDisplayDevicesA(); sysutils::hook_EnumDisplayDevicesA();
+2 -2
View File
@@ -26,7 +26,7 @@
#include "util/libutils.h" #include "util/libutils.h"
#include "util/sysutils.h" #include "util/sysutils.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include "camera.h" #include "camera.h"
@@ -456,7 +456,7 @@ namespace games::sdvx {
if (is_valkyrie_model()) { if (is_valkyrie_model()) {
if (NATIVETOUCH) { if (NATIVETOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) { } else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
// hook touch window // hook touch window
// in windowed mode, game can accept mouse input on the second screen // in windowed mode, game can accept mouse input on the second screen
+12
View File
@@ -29,6 +29,7 @@
#include "util/fileutils.h" #include "util/fileutils.h"
#include "util/utils.h" #include "util/utils.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "touch/native/inject.h"
#include "util/time.h" #include "util/time.h"
#include "rawinput/rawinput.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; nWidth = rect.right - rect.left;
nHeight = rect.bottom - rect.top; nHeight = rect.bottom - rect.top;
if (games::iidx::NATIVE_TOUCH) {
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
} else {
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW); touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
}
} else { } else {
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead // Existing behaviour: suppress subscreen window and prompt user to use overlay instead
log_info( log_info(
@@ -1163,6 +1168,13 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC)); WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc)); SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(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 // NOLEGACY causes WM_CHAR to be not received
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch) // 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"}))) { if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
-2
View File
@@ -15,8 +15,6 @@ namespace overlay::windows {
this->disabled_message = "Close this overlay and use the second window. (try ALT+TAB)"; this->disabled_message = "Close this overlay and use the second window. (try ALT+TAB)";
} else if (games::iidx::IIDX_TDJ_MONITOR_WARNING) { } else if (games::iidx::IIDX_TDJ_MONITOR_WARNING) {
this->disabled_message = "TDJ mode subscreen overlay is not compatible with -dxmainadapter option; use -mainmonitor instead"; 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; float size = 0.5f;
-2
View File
@@ -15,8 +15,6 @@ namespace overlay::windows {
this->disabled_message = "Game did not launch as Pikapika Pop-kun (invalid <spec>)!"; this->disabled_message = "Game did not launch as Pikapika Pop-kun (invalid <spec>)!";
} else if (games::popn::SHOW_PIKA_MONITOR_WARNING) { } else if (games::popn::SHOW_PIKA_MONITOR_WARNING) {
this->disabled_message = "Subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead"; 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) { } else if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
this->disabled_message = "Subscren overlay was not enabled in spicecfg. Use the second window (ALT+TAB)."; this->disabled_message = "Subscren overlay was not enabled in spicecfg. Use the second window (ALT+TAB).";
} }
-2
View File
@@ -14,8 +14,6 @@ namespace overlay::windows {
this->disabled_message = "Valkyrie Model mode is not enabled!"; this->disabled_message = "Valkyrie Model mode is not enabled!";
} else if (games::sdvx::SHOW_VM_MONITOR_WARNING) { } else if (games::sdvx::SHOW_VM_MONITOR_WARNING) {
this->disabled_message = "VM mode subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead"; 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) { } else if (GRAPHICS_WINDOWED) {
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) { if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
this->disabled_message = "Subscreen has been disabled by the user (-sdvxnosub)."; this->disabled_message = "Subscreen has been disabled by the user (-sdvxnosub).";
+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");
}
}
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <windows.h>
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);
}
@@ -0,0 +1,31 @@
#pragma once
#include <windows.h>
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);
}
+143
View File
@@ -0,0 +1,143 @@
// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602
#include <windows.h>
#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<UINT_PTR>(&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<UINT_PTR>(&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;
}
}
@@ -0,0 +1,104 @@
// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602
#include <mutex>
#include <windows.h>
#include <windowsx.h>
#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<UINT_PTR>(&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<UINT_PTR>(&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<WPARAM>(down),
MAKELPARAM(position.x, position.y)) != FALSE;
}
}
@@ -3,9 +3,9 @@
#define _WIN32_WINNT 0x0601 #define _WIN32_WINNT 0x0601
#include "avs/game.h" #include "avs/game.h"
#include "wintouchemu.h"
#include "rawinput/touch.h" #include "rawinput/touch.h"
#include "hooks/graphics/graphics.h" #include "inject.h"
#include "transform.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
@@ -20,9 +20,36 @@
#define log_debug(module, format_str, ...) #define log_debug(module, format_str, ...)
#endif #endif
namespace nativetouchhook { namespace nativetouch {
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr; 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) { static void strip_contact_size(PTOUCHINPUT point) {
@@ -67,51 +94,72 @@ namespace nativetouchhook {
} }
static void flip_touch_points(PTOUCHINPUT point) { static void flip_touch_points(PTOUCHINPUT point) {
point->x = rawinput::touch::DISPLAY_SIZE_X * 100 - point->x; point->x = native_display_size_x * 100 - point->x;
point->y = rawinput::touch::DISPLAY_SIZE_Y * 100 - point->y; point->y = native_display_size_y * 100 - point->y;
} }
static BOOL WINAPI GetTouchInputInfoHook( static BOOL WINAPI GetTouchInputInfoHook(
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) { 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 // call the original fist
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize); const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
if (result == 0) { if (result == 0) {
return result; return result;
} }
bool flip_values = false; bool flip_hardware_touch = false;
if (avs::game::is_model("KFC") && rawinput::touch::DISPLAY_INITIALIZED) { if (avs::game::is_model("KFC")) {
log_debug( log_debug(
"touch::native", "DISPLAY_ORIENTATION = {}, DISPLAY_SIZE_X = {}, DISPLAY_SIZE_Y = {}", "touch::native", "orientation = {}, display size = {}x{}",
rawinput::touch::DISPLAY_ORIENTATION, native_display_orientation,
rawinput::touch::DISPLAY_SIZE_X, native_display_size_x,
rawinput::touch::DISPLAY_SIZE_Y); native_display_size_y);
if (rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) { if (native_display_orientation == DMDO_270) {
flip_values = true; flip_hardware_touch = true;
} }
} }
if (rawinput::touch::INVERTED) {
flip_values = !flip_values;
}
for (size_t i = 0; i < cInputs; i++) { for (size_t i = 0; i < cInputs; i++) {
PTOUCHINPUT point = &pInputs[i]; PTOUCHINPUT point = &pInputs[i];
const auto synthetic = inject::transform_touch_input(point);
if (avs::game::is_model("LDJ")) { if (avs::game::is_model("LDJ")) {
strip_contact_size(point); strip_contact_size(point);
} }
const auto flip_values = !synthetic &&
(rawinput::touch::INVERTED ^ flip_hardware_touch);
if (flip_values) { if (flip_values) {
flip_touch_points(point); 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; return result;
} }
void hook(HMODULE module) { void hook(HMODULE module) {
inject::hook(module);
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");
@@ -1,6 +1,6 @@
#include <windows.h> #include <windows.h>
namespace nativetouchhook { namespace nativetouch {
void hook(HMODULE module); void hook(HMODULE module);
} }
+107
View File
@@ -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;
}
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <windows.h>
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);
}
+1 -3
View File
@@ -81,8 +81,6 @@ static const char *LOG_MODULE_NAME = "touch";
static TouchHandler *TOUCH_HANDLER = nullptr; static TouchHandler *TOUCH_HANDLER = nullptr;
static bool is_mouse_message_from_touchscreen();
TouchHandler::TouchHandler(std::string name) { TouchHandler::TouchHandler(std::string name) {
log_info("touch", "Using touch handler: {}", 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 // 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 MI_WP_SIGNATURE = 0xFF515700;
constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00; constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00;
return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE; return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE;
+1
View File
@@ -29,6 +29,7 @@ extern int SPICETOUCH_TOUCH_WIDTH;
extern int SPICETOUCH_TOUCH_HEIGHT; extern int SPICETOUCH_TOUCH_HEIGHT;
bool is_touch_available(LPCSTR caller); bool is_touch_available(LPCSTR caller);
bool is_mouse_message_from_touchscreen();
void touch_attach_wnd(HWND hWnd); void touch_attach_wnd(HWND hWnd);
void touch_attach_dx_hook(); void touch_attach_dx_hook();