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
+236 -11
View File
@@ -4,18 +4,43 @@
#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 "overlay/overlay.h"
#include "overlay/windows/generic_sub.h"
#include "touch/touch.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
#include "rawinput/touch.h"
#include "avs/game.h"
namespace wintouchemu {
typedef struct {
POINT pos;
bool last_button_pressed;
DWORD touch_event;
} mouse_state_t;
// settings
bool FORCE = false;
bool INJECT_MOUSE_AS_WM_TOUCH = false;
bool LOG_FPS = false;
bool ADD_TOUCH_FLAG_PRIMARY = false;
// state
double last_touch_event = 0.0;
static inline bool is_emu_enabled() {
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
@@ -56,13 +81,16 @@ namespace wintouchemu {
// state
BOOL (WINAPI *GetTouchInputInfo_orig)(HANDLE, UINT, PTOUCHINPUT, int);
bool USE_MOUSE = false;
std::vector<TouchEvent> TOUCH_EVENTS;
std::vector<TouchPoint> TOUCH_POINTS;
HMODULE HOOKED_MODULE = nullptr;
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
auto system_metrics_hook = detour::iat_try(
@@ -81,8 +109,31 @@ namespace wintouchemu {
// set module and title
HOOKED_MODULE = module;
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) {
@@ -94,6 +145,7 @@ namespace wintouchemu {
// set touch inputs
bool result = false;
bool mouse_used = false;
for (UINT input = 0; input < cInputs; input++) {
auto *touch_input = &pInputs[input];
@@ -126,7 +178,13 @@ namespace wintouchemu {
// 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
valid = overlay::OVERLAY->transform_touch_point(&x, &y);
} else {
@@ -172,10 +230,89 @@ namespace wintouchemu {
touch_input->cxContact = 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
TouchPoint *touch_point = nullptr;
for (auto &tp : TOUCH_POINTS) {
@@ -243,6 +380,21 @@ namespace wintouchemu {
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
if (hWnd == nullptr) {
return;
@@ -250,13 +402,39 @@ namespace wintouchemu {
// check if windowed
if (GRAPHICS_WINDOWED) {
// 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);
if (GRAPHICS_IIDX_WSUB) {
// no handling is needed here
// 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 {
log_info("wintouchemu", "activating DirectX hooks");
// mouse position based input only
touch_attach_dx_hook();
USE_MOUSE = false;
}
// hooks
@@ -267,6 +445,8 @@ namespace wintouchemu {
}
}
const auto now = get_performance_milliseconds();
// update touch events
if (hWnd != nullptr) {
@@ -286,11 +466,56 @@ namespace wintouchemu {
// check if new events are available
if (event_count > 0) {
last_touch_event = now;
// send fake event to make the game update it's touch inputs
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
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);
}
}
}
}