touch: restore wintouchemu, fall back to it when native touch hooks fail (#834)

## Link to GitHub Issue or related Pull Request, if one exists
Fixes #833

## Description of change
Last couple PRs - such as #820 #827 #828 - made the native touch hook &
touch injection using `InjectTouchInput` the default path, since it
performs much more reliably with both real touch screens and mouse (or
any other synthetic source).

However, user has reported that WINE lacks `InjectTouchInput` which
means this won't work.

As a fix, revive the old wintouchemu code. Native touch is still the
default, but under following circumstances:

1. if `-touchemuforce` is set, or
2. if any of the required Windows touch APIs are unavailable

then we fail over from native touch to wintouchemu code. 

For Linux, condition #2 would be hit during init, and gracefully switch
over.

Caveat: the poke code for IIDX and Nost will continue to require native
touch, I do not want to maintain two paths for this. This means that
iidx poke will stop working on Linux, unfortunately.

## Testing
Tested on Windows with `-touchemuforce` set. This is mostly reverting
Linux code path back to where we were last release, so this should just
work with wine.
This commit is contained in:
bicarus
2026-07-26 04:29:45 -07:00
committed by GitHub
parent 64600826b7
commit bd2fbfcb67
20 changed files with 435 additions and 56 deletions
+8 -1
View File
@@ -11,6 +11,7 @@
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/audio/mme.h" #include "hooks/audio/mme.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "util/cpuutils.h" #include "util/cpuutils.h"
@@ -653,7 +654,13 @@ namespace games::gitadora {
// monitor/touch hooks (windowed or full screen) // monitor/touch hooks (windowed or full screen)
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) { if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay // enable touch hook for subscreen overlay
nativetouch::hook(avs::game::DLL_INSTANCE); const auto native_touch_ready = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!native_touch_ready) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
}
#if !SPICE_XP #if !SPICE_XP
+20 -1
View File
@@ -22,6 +22,7 @@
#include "touch/touch.h" #include "touch/touch.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/wintouchemu.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "util/fileutils.h" #include "util/fileutils.h"
@@ -40,6 +41,7 @@
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include "ezusb.h" #include "ezusb.h"
#include "io.h" #include "io.h"
#include "poke.h"
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr; static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr; static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
@@ -62,6 +64,8 @@ namespace games::iidx {
bool TDJ_MODE = false; bool TDJ_MODE = false;
bool FORCE_720P = false; bool FORCE_720P = false;
bool DISABLE_ESPEC_IO = false; bool DISABLE_ESPEC_IO = false;
bool NATIVE_TOUCH = true;
bool ENABLE_POKE = false;
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt; std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt; std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt; std::optional<std::string> ASIO_DRIVER = std::nullopt;
@@ -348,7 +352,16 @@ namespace games::iidx {
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook // need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
devicehook_init(avs::core::DLL_INSTANCE); devicehook_init(avs::core::DLL_INSTANCE);
nativetouch::hook(avs::game::DLL_INSTANCE); NATIVE_TOUCH = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!NATIVE_TOUCH) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
"beatmania IIDX",
"main",
avs::game::DLL_INSTANCE);
}
} }
// insert BI2X hooks // insert BI2X hooks
@@ -525,6 +538,12 @@ namespace games::iidx {
} }
} }
void IIDXGame::post_attach() {
if (ENABLE_POKE) {
poke::enable();
}
}
void IIDXGame::detach() { void IIDXGame::detach() {
Game::detach(); Game::detach();
+3
View File
@@ -28,6 +28,8 @@ namespace games::iidx {
extern bool TDJ_MODE; extern bool TDJ_MODE;
extern bool FORCE_720P; extern bool FORCE_720P;
extern bool DISABLE_ESPEC_IO; extern bool DISABLE_ESPEC_IO;
extern bool NATIVE_TOUCH;
extern bool ENABLE_POKE;
extern std::optional<std::string> SOUND_OUTPUT_DEVICE; extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
extern std::optional<std::string> ASIO_DRIVER; extern std::optional<std::string> ASIO_DRIVER;
extern uint8_t DIGITAL_TT_SENS; extern uint8_t DIGITAL_TT_SENS;
@@ -52,6 +54,7 @@ namespace games::iidx {
virtual void attach() override; virtual void attach() override;
virtual void pre_attach() override; virtual void pre_attach() override;
virtual void post_attach() override;
virtual void detach() override; virtual void detach() override;
private: private:
+6
View File
@@ -4,6 +4,7 @@
#include "windows.h" #include "windows.h"
#include "games/io.h" #include "games/io.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
@@ -116,6 +117,11 @@ namespace games::iidx::poke {
if (THREAD) if (THREAD)
return; return;
if (!games::iidx::NATIVE_TOUCH) {
log_warning("poke", "keypad touch emulation requires native touch; disabled");
return;
}
// create new thread // create new thread
THREAD_RUNNING = true; THREAD_RUNNING = true;
THREAD = new std::thread([] { THREAD = new std::thread([] {
+10 -3
View File
@@ -2,6 +2,7 @@
#include "poke.h" #include "poke.h"
#include "touch_mode.h" #include "touch_mode.h"
#include "hooks/setupapihook.h" #include "hooks/setupapihook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "avs/game.h" #include "avs/game.h"
@@ -41,9 +42,15 @@ namespace games::nost {
setupapihook_init(avs::game::DLL_INSTANCE); setupapihook_init(avs::game::DLL_INSTANCE);
setupapihook_add(touch_settings); setupapihook_add(touch_settings);
nativetouch::hook(avs::game::DLL_INSTANCE); const auto native_touch_ready = !wintouchemu::FORCE &&
if (ENABLE_TOUCH_MODE) { nativetouch::hook(avs::game::DLL_INSTANCE);
touch_mode::enable(); if (!native_touch_ready) {
wintouchemu::FORCE = true;
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
} else {
if (ENABLE_TOUCH_MODE) {
touch_mode::enable();
}
} }
} }
+6
View File
@@ -9,6 +9,7 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "touch/native/inject.h" #include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h" #include "util/precise_timer.h"
@@ -59,6 +60,11 @@ namespace games::nost::poke {
return; return;
} }
if (!nativetouch::is_hooked()) {
log_warning("poke", "Nostalgia poke requires native touch; disabled");
return;
}
// create new thread // create new thread
THREAD_RUNNING = true; THREAD_RUNNING = true;
THREAD = new std::thread([] { THREAD = new std::thread([] {
+14 -1
View File
@@ -16,6 +16,7 @@
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "launcher/logger.h" #include "launcher/logger.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/wintouchemu.h"
#include "util/sysutils.h" #include "util/sysutils.h"
#include "io.h" #include "io.h"
#include "util/deferlog.h" #include "util/deferlog.h"
@@ -24,6 +25,7 @@
namespace games::popn { namespace games::popn {
bool SHOW_PIKA_MONITOR_WARNING = false; bool SHOW_PIKA_MONITOR_WARNING = false;
bool NATIVE_TOUCH = true;
#if SPICE64 && !SPICE_XP #if SPICE64 && !SPICE_XP
@@ -713,7 +715,18 @@ 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
nativetouch::hook(avs::game::DLL_INSTANCE); NATIVE_TOUCH = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!NATIVE_TOUCH) {
wintouchemu::FORCE = true;
if (!GRAPHICS_WINDOWED || GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
"",
"Main Screen",
avs::game::DLL_INSTANCE);
}
}
sysutils::hook_EnumDisplayDevicesA(); sysutils::hook_EnumDisplayDevicesA();
+1
View File
@@ -22,6 +22,7 @@ namespace games::popn {
#endif #endif
extern bool SHOW_PIKA_MONITOR_WARNING; extern bool SHOW_PIKA_MONITOR_WARNING;
extern bool NATIVE_TOUCH;
class POPNGame : public games::Game { class POPNGame : public games::Game {
public: public:
+13 -1
View File
@@ -26,6 +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/wintouchemu.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include "camera.h" #include "camera.h"
@@ -453,7 +454,18 @@ namespace games::sdvx {
} }
if (is_valkyrie_model()) { if (is_valkyrie_model()) {
nativetouch::hook(avs::game::DLL_INSTANCE); const auto native_touch_ready = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!native_touch_ready) {
wintouchemu::FORCE = true;
if (!GRAPHICS_WINDOWED) {
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
"SOUND VOLTEX",
"Main Screen",
avs::game::DLL_INSTANCE);
}
}
// insert BI2X hooks // insert BI2X hooks
bi2x_hook_init(); bi2x_hook_init();
+25 -3
View File
@@ -28,6 +28,7 @@
#include "util/logging.h" #include "util/logging.h"
#include "util/fileutils.h" #include "util/fileutils.h"
#include "util/utils.h" #include "util/utils.h"
#include "misc/wintouchemu.h"
#include "touch/native/inject.h" #include "touch/native/inject.h"
#include "util/time.h" #include "util/time.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
@@ -307,6 +308,22 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
} }
} }
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
// drop mouse inputs since only wintouches should be used
switch (uMsg) {
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
return true;
}
}
// window resize // window resize
graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam); graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam);
@@ -786,7 +803,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;
nativetouch::inject::register_and_attach_window(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 { } 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(
@@ -1160,8 +1181,9 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc)); SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
const bool native_touch_overlay = const bool native_touch_overlay =
(games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) || (games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
(games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS); (games::popn::NATIVE_TOUCH &&
games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
if (native_touch_overlay) { if (native_touch_overlay) {
nativetouch::inject::register_and_attach_window(hWnd); nativetouch::inject::register_and_attach_window(hWnd);
} }
+2 -2
View File
@@ -38,10 +38,10 @@
#include "games/gitadora/gitadora.h" #include "games/gitadora/gitadora.h"
#include "games/hpm/hpm.h" #include "games/hpm/hpm.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/iidx/poke.h"
#ifdef SPICE64 #ifdef SPICE64
#include "games/iidx/camera.h" #include "games/iidx/camera.h"
#endif #endif
#include "games/iidx/poke.h"
#include "games/jb/jb.h" #include "games/jb/jb.h"
#include "games/mga/mga.h" #include "games/mga/mga.h"
#include "games/nost/nost.h" #include "games/nost/nost.h"
@@ -1783,7 +1783,7 @@ int main_implementation(int argc, char *argv[]) {
// enable subscreen touch emulation // enable subscreen touch emulation
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) { if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
games::iidx::poke::enable(); games::iidx::ENABLE_POKE = true;
} }
if (options[launcher::Options::NostalgiaPoke].is_active()) { if (options[launcher::Options::NostalgiaPoke].is_active()) {
games::nost::ENABLE_POKE = true; games::nost::ENABLE_POKE = true;
+2 -2
View File
@@ -1791,10 +1791,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Touch Parameters", .category = "Touch Parameters",
}, },
{ {
.title = "Force Touch Emulation", .title = "Force Legacy Touch Emulation",
.name = "touchemuforce", .name = "touchemuforce",
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. " .desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
"This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.", "Do not enable this unless you have trouble with using a mouse to emulate touch input.",
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Touch Parameters", .category = "Touch Parameters",
}, },
+236 -11
View File
@@ -4,19 +4,44 @@
#include "wintouchemu.h" #include "wintouchemu.h"
#include <chrono>
#include <algorithm>
#include <optional>
#include <thread>
#include "cfg/screen_resize.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "games/popn/popn.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/windows/generic_sub.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
#include "rawinput/touch.h"
#include "avs/game.h"
namespace wintouchemu { namespace wintouchemu {
typedef struct {
POINT pos;
bool last_button_pressed;
DWORD touch_event;
} mouse_state_t;
// settings // settings
bool FORCE = false; bool FORCE = false;
bool INJECT_MOUSE_AS_WM_TOUCH = false;
bool LOG_FPS = false;
bool ADD_TOUCH_FLAG_PRIMARY = false; bool ADD_TOUCH_FLAG_PRIMARY = false;
// state
double last_touch_event = 0.0;
static inline bool is_emu_enabled() { static inline bool is_emu_enabled() {
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR; return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
} }
@@ -56,13 +81,16 @@ namespace wintouchemu {
// state // state
BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int); BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int);
bool USE_MOUSE = false;
std::vector<TouchEvent> TOUCH_EVENTS; std::vector<TouchEvent> TOUCH_EVENTS;
std::vector<TouchPoint> TOUCH_POINTS; std::vector<TouchPoint> TOUCH_POINTS;
HMODULE HOOKED_MODULE = nullptr; HMODULE HOOKED_MODULE = nullptr;
std::string WINDOW_TITLE_START = ""; std::string WINDOW_TITLE_START = "";
bool INITIALIZED = false; std::optional<std::string> WINDOW_TITLE_END = std::nullopt;
volatile bool INITIALIZED = false;
mouse_state_t mouse_state;
void hook(const char *window_title, HMODULE module) { void hook(const char *window_title, HMODULE module, int delay_in_s) {
// hooks // hooks
auto system_metrics_hook = detour::iat_try( auto system_metrics_hook = detour::iat_try(
@@ -81,8 +109,31 @@ namespace wintouchemu {
// set module and title // set module and title
HOOKED_MODULE = module; HOOKED_MODULE = module;
WINDOW_TITLE_START = window_title; WINDOW_TITLE_START = window_title;
log_misc("wintouchemu", "initializing");
INITIALIZED = true; if (0 < delay_in_s) {
// some games crash when touch events are injected too early during boot
std::thread t([&]() {
log_misc("wintouchemu", "defer initialization until later (with delay of {}s)", delay_in_s);
std::this_thread::sleep_for(std::chrono::seconds(delay_in_s));
log_misc("wintouchemu", "initializing", delay_in_s);
INITIALIZED = true;
});
t.detach();
} else {
log_misc("wintouchemu", "initializing");
INITIALIZED = true;
}
}
void hook_title_ends(const char *window_title_start, const char *window_title_end, HMODULE module) {
hook(window_title_start, module);
WINDOW_TITLE_END = window_title_end;
}
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;
} }
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) { static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
@@ -94,6 +145,7 @@ namespace wintouchemu {
// set touch inputs // set touch inputs
bool result = false; bool result = false;
bool mouse_used = false;
for (UINT input = 0; input < cInputs; input++) { for (UINT input = 0; input < cInputs; input++) {
auto *touch_input = &pInputs[input]; auto *touch_input = &pInputs[input];
@@ -126,7 +178,13 @@ namespace wintouchemu {
// log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y)); // log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
if (overlay::OVERLAY) { if (GRAPHICS_IIDX_WSUB) {
// touch was received on subscreen window.
RECT clientRect {};
GetClientRect(TDJ_SUBSCREEN_WINDOW, &clientRect);
x = (float) x / clientRect.right * SPICETOUCH_TOUCH_WIDTH + SPICETOUCH_TOUCH_X;
y = (float) y / clientRect.bottom * SPICETOUCH_TOUCH_HEIGHT + SPICETOUCH_TOUCH_Y;
} else if (overlay::OVERLAY) {
// touch was received on global coords // touch was received on global coords
valid = overlay::OVERLAY->transform_touch_point(&x, &y); valid = overlay::OVERLAY->transform_touch_point(&x, &y);
} else { } else {
@@ -172,10 +230,89 @@ namespace wintouchemu {
touch_input->cxContact = 0; touch_input->cxContact = 0;
touch_input->cyContact = 0; touch_input->cyContact = 0;
} else { if (avs::game::is_model("KFC") &&
rawinput::touch::DISPLAY_INITIALIZED &&
rawinput::touch::DISPLAY_ORIENTATION == DMDO_270) {
flip_touch_points(touch_input);
}
} else if (USE_MOUSE && !mouse_used) {
// disable further mouse inputs this call
mouse_used = true;
if (mouse_state.touch_event) {
result = true;
touch_input->x = mouse_state.pos.x;
touch_input->y = mouse_state.pos.y;
if (GRAPHICS_WINDOWED) {
touch_input->x -= SPICETOUCH_TOUCH_X;
touch_input->y -= SPICETOUCH_TOUCH_Y;
}
// log_misc(
// "wintouchemu",
// "mouse state ({}, {}) event={}",
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
auto valid = true;
if (overlay::OVERLAY) {
valid = overlay::OVERLAY->transform_touch_point(
&touch_input->x, &touch_input->y);
}
// touch inputs require 100x precision per pixel
touch_input->x *= 100;
touch_input->y *= 100;
touch_input->hSource = hTouchInput;
touch_input->dwID = 0;
touch_input->dwFlags = 0;
switch (mouse_state.touch_event) {
case TOUCHEVENTF_DOWN:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
}
break;
case TOUCHEVENTF_MOVE:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
}
break;
case TOUCHEVENTF_UP:
// don't check valid so that this touch ID can be released
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_UP;
break;
}
touch_input->dwMask = 0;
touch_input->dwTime = 0;
touch_input->dwExtraInfo = 0;
touch_input->cxContact = 0;
touch_input->cyContact = 0;
// reset it since the event was consumed & propagated as touch
mouse_state.touch_event = 0;
}
} else if (!GRAPHICS_IIDX_WSUB) {
/*
* For some reason, Nostalgia won't show an active touch point unless a move event
* triggers in the same frame. To work around this, we just supply a fake move
* event if we didn't update the same pointer ID in the same call.
*/
// beatstream requires a MOVE for active points in each update
// add one for every active point without a matching input event
// find touch point which has no associated input event // find touch point which has no associated input event
TouchPoint *touch_point = nullptr; TouchPoint *touch_point = nullptr;
for (auto &tp : TOUCH_POINTS) { for (auto &tp : TOUCH_POINTS) {
@@ -243,6 +380,21 @@ namespace wintouchemu {
title = get_window_title(hWnd); title = get_window_title(hWnd);
} }
// if a window title end is set, check to see if it matches
if (WINDOW_TITLE_END.has_value() && !string_ends_with(title.c_str(), WINDOW_TITLE_END.value().c_str())) {
hWnd = nullptr;
title = "";
for (auto &window : find_windows_beginning_with(WINDOW_TITLE_START)) {
auto check_title = get_window_title(window);
if (string_ends_with(check_title.c_str(), WINDOW_TITLE_END.value().c_str())) {
hWnd = std::move(window);
title = std::move(check_title);
break;
}
}
}
// check window // check window
if (hWnd == nullptr) { if (hWnd == nullptr) {
return; return;
@@ -250,13 +402,39 @@ namespace wintouchemu {
// check if windowed // check if windowed
if (GRAPHICS_WINDOWED) { if (GRAPHICS_WINDOWED) {
// create touch window - create overlay if not yet existing at this point if (GRAPHICS_IIDX_WSUB) {
log_info("wintouchemu", "create touch window relative to main game window"); // no handling is needed here
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY); // graphics::MoveWindow_hook will attach hook to windowed subscreen
log_info("wintouchemu", "attach touch hook to windowed subscreen for TDJ");
USE_MOUSE = false;
} else if (avs::game::is_model("LDJ") && !GENERIC_SUB_WINDOW_FULLSIZE) {
// overlay subscreen in IIDX
// use mouse position as ImGui overlay will block the touch window
log_info("wintouchemu", "use mouse cursor API for ldj overlay subscreen");
USE_MOUSE = true;
} else if (games::popn::is_pikapika_model()) {
// same as iidx case above
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
USE_MOUSE = true;
} else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
USE_MOUSE = true;
} else {
// create touch window - create overlay if not yet existing at this point
log_info("wintouchemu", "create touch window relative to main game window");
touch_create_wnd(hWnd, overlay::ENABLED && !overlay::OVERLAY);
USE_MOUSE = false;
}
} else if (INJECT_MOUSE_AS_WM_TOUCH) {
log_info(
"wintouchemu",
"using raw mouse cursor API in full screen and injecting them as WM_TOUCH events");
USE_MOUSE = true;
} else { } else {
log_info("wintouchemu", "activating DirectX hooks"); log_info("wintouchemu", "activating DirectX hooks");
// mouse position based input only // mouse position based input only
touch_attach_dx_hook(); touch_attach_dx_hook();
USE_MOUSE = false;
} }
// hooks // hooks
@@ -267,6 +445,8 @@ namespace wintouchemu {
} }
} }
const auto now = get_performance_milliseconds();
// update touch events // update touch events
if (hWnd != nullptr) { if (hWnd != nullptr) {
@@ -286,11 +466,56 @@ namespace wintouchemu {
// check if new events are available // check if new events are available
if (event_count > 0) { if (event_count > 0) {
last_touch_event = now;
// send fake event to make the game update it's touch inputs // send fake event to make the game update it's touch inputs
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC); auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook); wndProc(hWnd, WM_TOUCH, MAKEWORD(event_count, 0), (LPARAM) GetTouchInputInfoHook);
} }
// update frame logging
if (LOG_FPS) {
static int log_frames = 0;
static uint64_t log_time = 0;
log_frames++;
if (log_time < get_system_seconds()) {
if (log_time > 0) {
log_info("wintouchemu", "polling at {} touch frames per second", log_frames);
}
log_frames = 0;
log_time = get_system_seconds();
}
}
}
// send separate WM_TOUCH event for mouse
// this must be separate from actual touch events because some games will ignore the return
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to
// send empty events when the mouse button is not clicked/released
if (hWnd != nullptr && USE_MOUSE) {
bool button_pressed = get_async_primary_mouse();
// if there was a touch event in the last 500 ms, don't insert new button presses
if (button_pressed && (now - last_touch_event) < 500) {
button_pressed = false;
}
// figure out what kind of touch event to simulate
if (button_pressed && !mouse_state.last_button_pressed) {
mouse_state.touch_event = TOUCHEVENTF_DOWN;
} else if (button_pressed && mouse_state.last_button_pressed) {
mouse_state.touch_event = TOUCHEVENTF_MOVE;
} else if (!button_pressed && mouse_state.last_button_pressed) {
mouse_state.touch_event = TOUCHEVENTF_UP;
}
mouse_state.last_button_pressed = button_pressed;
if (mouse_state.touch_event) {
GetCursorPos(&mouse_state.pos);
// send fake event to make the game update it's touch inputs
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
wndProc(hWnd, WM_TOUCH, MAKEWORD(1, 0), (LPARAM) GetTouchInputInfoHook);
}
} }
} }
} }
+7 -1
View File
@@ -6,8 +6,14 @@ namespace wintouchemu {
// settings // settings
extern bool FORCE; extern bool FORCE;
extern bool INJECT_MOUSE_AS_WM_TOUCH;
extern bool LOG_FPS;
extern bool ADD_TOUCH_FLAG_PRIMARY; extern bool ADD_TOUCH_FLAG_PRIMARY;
void hook(const char *window_title, HMODULE module = nullptr); void hook(const char *window_title, HMODULE module = nullptr, int delay_in_s=0);
void hook_title_ends(
const char *window_title_start,
const char *window_title_ends,
HMODULE module = nullptr);
void update(); void update();
} }
+34 -16
View File
@@ -338,9 +338,7 @@ namespace nativetouch::inject {
// attach mouse injection without replacing the window's existing procedure // attach mouse injection without replacing the window's existing procedure
static void attach_window_impl(HWND window, bool register_touch) { static void attach_window_impl(HWND window, bool register_touch) {
initialize_touch_injection(); if (!initialize_touch_injection()) {
if (!touch_injection_available()) {
if (register_touch) { if (register_touch) {
log_warning( log_warning(
"touch::native", "touch::native",
@@ -422,7 +420,7 @@ namespace nativetouch::inject {
} }
// load and initialize Windows 8 touch injection without a static API dependency // load and initialize Windows 8 touch injection without a static API dependency
void initialize_touch_injection() { bool initialize_touch_injection() {
std::call_once(initialization_once, [] { std::call_once(initialization_once, [] {
initialize_synthetic_touch(); initialize_synthetic_touch();
contact_refresh_message = contact_refresh_message =
@@ -433,17 +431,26 @@ namespace nativetouch::inject {
} }
// load all APIs from user32 without adding static imports // 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"); const auto user32 = libutils::load_library("user32.dll");
InitializeTouchInjection_ptr = libutils::try_proc<decltype(InitializeTouchInjection_ptr)>( InitializeTouchInjection_ptr = libutils::try_proc<decltype(InitializeTouchInjection_ptr)>(
user32, "InitializeTouchInjection"); user32, "InitializeTouchInjection");
if (InitializeTouchInjection_ptr == nullptr) {
log_warning(
"touch::native", "InitializeTouchInjection unavailable; mouse touch injection disabled");
return;
}
InjectTouchInput_ptr = libutils::try_proc<decltype(InjectTouchInput_ptr)>( InjectTouchInput_ptr = libutils::try_proc<decltype(InjectTouchInput_ptr)>(
user32, "InjectTouchInput"); user32, "InjectTouchInput");
if (InjectTouchInput_ptr == nullptr) {
if (InitializeTouchInjection_ptr == nullptr ||
InjectTouchInput_ptr == nullptr) {
clear_touch_injection_functions(); clear_touch_injection_functions();
log_warning( log_warning(
"touch::native", "touch injection API unavailable; mouse touch injection disabled"); "touch::native", "InjectTouchInput unavailable; mouse touch injection disabled");
return; return;
} }
@@ -457,20 +464,31 @@ namespace nativetouch::inject {
log_misc("touch::native", "mouse touch injection initialized"); log_misc("touch::native", "mouse touch injection initialized");
}); });
} return InitializeTouchInjection_ptr != nullptr && InjectTouchInput_ptr != nullptr;
bool touch_injection_available() {
return InjectTouchInput_ptr != nullptr;
} }
// install injection support for touch windows registered by the game module // install injection support for touch windows registered by the game module
void hook(HMODULE module) { bool hook_available(HMODULE module) {
initialize_touch_injection(); 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_orig = detour::iat_try(
"RegisterTouchWindow", RegisterTouchWindowHook, module); "RegisterTouchWindow", RegisterTouchWindowHook, module);
if (RegisterTouchWindow_orig != nullptr) { if (RegisterTouchWindow_orig == nullptr) {
log_misc("touch::native", "RegisterTouchWindow hooked"); log_warning("touch::native", "failed to hook RegisterTouchWindow");
return false;
} }
log_misc("touch::native", "RegisterTouchWindow hooked");
return true;
} }
} }
+2 -1
View File
@@ -7,7 +7,8 @@ struct tagTOUCHINPUT;
namespace nativetouch::inject { namespace nativetouch::inject {
void attach_window(HWND window); void attach_window(HWND window);
void register_and_attach_window(HWND window); void register_and_attach_window(HWND window);
void hook(HMODULE module); bool hook_available(HMODULE module);
bool hook(HMODULE module);
bool inject_synthetic_touch(POINT position, bool down); bool inject_synthetic_touch(POINT position, bool down);
bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down); bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down);
bool transform_touch_input(tagTOUCHINPUT *point); bool transform_touch_input(tagTOUCHINPUT *point);
+1 -2
View File
@@ -10,9 +10,8 @@ namespace nativetouch::inject {
Synthetic, Synthetic,
}; };
void initialize_touch_injection(); bool initialize_touch_injection();
void initialize_synthetic_touch(); void initialize_synthetic_touch();
bool touch_injection_available();
void refresh_contact_lifetime(); void refresh_contact_lifetime();
bool contact_is_active(); bool contact_is_active();
@@ -113,11 +113,12 @@ namespace nativetouch::inject {
} }
static HWND prepare_synthetic_touch() { static HWND prepare_synthetic_touch() {
initialize_touch_injection(); if (!initialize_touch_injection()) {
return nullptr;
}
const auto window = get_injection_window(); const auto window = get_injection_window();
if (!touch_injection_available() || window == nullptr || if (window == nullptr || synthetic_touch_message == 0) {
synthetic_touch_message == 0) {
return nullptr; return nullptr;
} }
return window; return window;
+40 -7
View File
@@ -223,25 +223,58 @@ namespace nativetouch {
} }
} }
void hook(HMODULE module) { static bool hook_prerequisites_available(HMODULE module) {
if (detour::iat_find("GetTouchInputInfo", module) == nullptr) {
log_warning("touch::native", "GetTouchInputInfo unavailable");
return false;
}
if (settings::EMULATE_DIGITIZER &&
detour::iat_find("GetSystemMetrics", module) == nullptr) {
log_warning("touch::native", "GetSystemMetrics unavailable");
return false;
}
return true;
}
bool hook(HMODULE module) {
native_touch_hooked = false;
initialize_game_settings(); initialize_game_settings();
inject::hook(module); // check if the OS supports touch API (Win7+) and injection API (requires Win8+)
if (!hook_prerequisites_available(module)) {
return false;
}
if (!inject::hook_available(module)) {
return false;
}
native_touch_hooked = true; // try hooking injection API first since they require the highest OS level
// (WINE specifically did not implement this in 2026)
if (!inject::hook(module)) {
return false;
}
// GetSystemMetrics
if (settings::EMULATE_DIGITIZER) { if (settings::EMULATE_DIGITIZER) {
GetSystemMetrics_orig = detour::iat_try( GetSystemMetrics_orig = detour::iat_try(
"GetSystemMetrics", GetSystemMetricsHook, module); "GetSystemMetrics", GetSystemMetricsHook, module);
if (GetSystemMetrics_orig != nullptr) { if (GetSystemMetrics_orig == nullptr) {
log_misc("touch::native", "GetSystemMetrics hooked"); log_warning("touch::native", "failed to hook GetSystemMetrics");
return false;
} }
log_misc("touch::native", "GetSystemMetrics hooked");
} }
// GetTouchInputInfo
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_warning("touch::native", "failed to hook GetTouchInputInfo");
return false;
} }
log_misc("touch::native", "GetTouchInputInfo hooked");
native_touch_hooked = true;
return true;
} }
} }
+1 -1
View File
@@ -15,7 +15,7 @@ namespace nativetouch {
using TouchInputFilter = bool (*)(const NativeTouchEvent &event); using TouchInputFilter = bool (*)(const NativeTouchEvent &event);
void hook(HMODULE module); bool hook(HMODULE module);
void refresh_contact_lifetime(); void refresh_contact_lifetime();
void set_input_filter(TouchInputFilter filter); void set_input_filter(TouchInputFilter filter);