Files
spice2x-r3d/src/spice2x/misc/wintouchemu.cpp
T
bicarusandGitHub 18421ffbfe touch: make native touch the default for iidx/sdvx/popn (#820)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #814

## Description of change
Remove iidx/sdvx/popon `native touch` options.

Remove dead code in wintouchemu that deals with these games.

Native touch is now at feature-parity with wintouchemu. Native touch
options work better with touch screens, while rawinput-based wintouchemu
has compat issues on some touchscreens. For these subscreen games
deprecate the usage of wintouchemu and make them use native touch stack
by default.

### For future consideration
Gitadora - not sure what to do with this one. We only use wintouchemu
for the single window case. For all other configurations, the game
accepts mouse and touch input without any hooks, so perhaps nothing
needs to be done here.

Nostalgia - consider moving away from wintouchemu and adopt native touch
hook here as well.

## Testing
WIP
2026-07-21 02:15:56 -07:00

461 lines
17 KiB
C++

// enable touch functions - set version to windows 7
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601
#include "wintouchemu.h"
#include <chrono>
#include <algorithm>
#include <thread>
#include "games/gitadora/gitadora.h"
#include "hooks/graphics/graphics.h"
#include "overlay/overlay.h"
#include "touch/touch.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.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;
}
static decltype(GetSystemMetrics) *GetSystemMetrics_orig = nullptr;
static decltype(RegisterTouchWindow) *RegisterTouchWindow_orig = nullptr;
static int WINAPI GetSystemMetrics_hook(int nIndex) {
/*
* fake touch screen
* the game requires 0x01 and 0x02 flags to be set
* 0x40 and 0x80 are set for completeness
*/
if (nIndex == 94)
return 0x01 | 0x02 | 0x40 | 0x80;
// call original
if (GetSystemMetrics_orig != nullptr) {
return GetSystemMetrics_orig(nIndex);
}
// return error
return 0;
}
static BOOL WINAPI RegisterTouchWindow_hook(HWND hwnd, ULONG ulFlags) {
// don't register it if the emu is enabled
if (is_emu_enabled()) {
return true;
}
// call default
return RegisterTouchWindow_orig(hwnd, ulFlags);
}
// 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 = "";
volatile bool INITIALIZED = false;
mouse_state_t mouse_state;
void hook(const char *window_title, HMODULE module, int delay_in_s) {
// hooks
auto system_metrics_hook = detour::iat_try(
"GetSystemMetrics", GetSystemMetrics_hook, module);
auto register_touch_window_hook = detour::iat_try(
"RegisterTouchWindow", RegisterTouchWindow_hook, module);
// don't hook twice
if (GetSystemMetrics_orig == nullptr) {
GetSystemMetrics_orig = system_metrics_hook;
}
if (RegisterTouchWindow_orig == nullptr) {
RegisterTouchWindow_orig = register_touch_window_hook;
}
// set module and title
HOOKED_MODULE = module;
WINDOW_TITLE_START = window_title;
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;
}
}
static BOOL WINAPI GetTouchInputInfoHook(HANDLE hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
// check if original should be called
if (hTouchInput != GetTouchInputInfoHook) {
return GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
}
// set touch inputs
bool result = false;
bool mouse_used = false;
for (UINT input = 0; input < cInputs; input++) {
auto *touch_input = &pInputs[input];
// clear touch input
touch_input->x = 0;
touch_input->y = 0;
touch_input->hSource = nullptr;
touch_input->dwID = 0;
touch_input->dwFlags = 0;
touch_input->dwMask = 0;
touch_input->dwTime = 0;
touch_input->dwExtraInfo = 0;
touch_input->cxContact = 0;
touch_input->cyContact = 0;
// get touch event
TouchEvent *touch_event = nullptr;
if (TOUCH_EVENTS.size() > input) {
touch_event = &TOUCH_EVENTS.at(input);
}
// check touch point
if (touch_event) {
// set touch point
result = true;
auto x = touch_event->x;
auto y = touch_event->y;
auto valid = true;
// log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
if (overlay::OVERLAY) {
// touch was received on global coords
valid = overlay::OVERLAY->transform_touch_point(&x, &y);
} else {
valid = false;
}
touch_input->x = x * 100;
touch_input->y = y * 100;
touch_input->hSource = hTouchInput;
touch_input->dwID = touch_event->id;
touch_input->dwFlags = 0;
switch (touch_event->type) {
case TOUCH_DOWN:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_DOWN;
}
break;
case TOUCH_MOVE:
if (valid) {
if (ADD_TOUCH_FLAG_PRIMARY) {
touch_input->dwFlags |= TOUCHEVENTF_PRIMARY;
}
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
}
break;
case TOUCH_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;
} 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 {
/*
* 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.
*/
// find touch point which has no associated input event
TouchPoint *touch_point = nullptr;
for (auto &tp : TOUCH_POINTS) {
bool found = false;
for (UINT i = 0; i < cInputs; i++) {
if (input > 0 && pInputs[i].dwID == tp.id) {
found = true;
break;
}
}
if (!found) {
touch_point = &tp;
break;
}
}
// check if unused touch point was found
if (touch_point) {
// set touch point
result = true;
touch_input->x = touch_point->x * 100;
touch_input->y = touch_point->y * 100;
touch_input->hSource = hTouchInput;
touch_input->dwID = touch_point->id;
touch_input->dwFlags = 0;
touch_input->dwFlags |= TOUCHEVENTF_MOVE;
touch_input->dwMask = 0;
touch_input->dwTime = 0;
touch_input->dwExtraInfo = 0;
touch_input->cxContact = 0;
touch_input->cyContact = 0;
}
}
}
// return success
return result;
}
void update() {
// check if initialized
if (!INITIALIZED) {
return;
}
// no need for hooks if touch is available
if (!is_emu_enabled()) {
return;
}
// get window handle
static HWND hWnd = nullptr;
if (hWnd == nullptr) {
// start with the active foreground window
hWnd = GetForegroundWindow();
auto title = get_window_title(hWnd);
// if the foreground window does not match the window title start, find a window
// that does
if (!string_begins_with(title, WINDOW_TITLE_START)) {
hWnd = FindWindowBeginsWith(WINDOW_TITLE_START);
title = get_window_title(hWnd);
}
// check window
if (hWnd == nullptr) {
return;
}
// check if windowed
if (GRAPHICS_WINDOWED) {
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
auto GetTouchInputInfo_orig_new = detour::iat_try(
"GetTouchInputInfo", GetTouchInputInfoHook, HOOKED_MODULE);
if (GetTouchInputInfo_orig == nullptr) {
GetTouchInputInfo_orig = GetTouchInputInfo_orig_new;
}
}
const auto now = get_performance_milliseconds();
// update touch events
if (hWnd != nullptr) {
// get touch events
TOUCH_EVENTS.clear();
touch_get_events(TOUCH_EVENTS);
// get touch points
TOUCH_POINTS.clear();
touch_get_points(TOUCH_POINTS);
// get event count
auto event_count = TOUCH_EVENTS.size();
// for the fake move events
event_count += MAX(0, (int) (TOUCH_POINTS.size() - TOUCH_EVENTS.size()));
// 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);
}
}
}
}