Files
spice2x.github.io/src/spice2x/touch/native/nativetouchhook.cpp
T
bicarus bd2fbfcb67 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.
2026-07-26 04:29:45 -07:00

281 lines
9.8 KiB
C++

// enable touch functions - set version to windows 7
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601
#include <atomic>
#include "nativetouchhook.h"
#include "avs/game.h"
#include "rawinput/touch.h"
#include "inject.h"
#include "inject_internal.h"
#include "settings.h"
#include "transform.h"
#include "util/detour.h"
#include "util/logging.h"
#define TOUCH_SIMULATE_FAT_FINGERS 0
#define TOUCH_DEBUG_VERBOSE 0
#if TOUCH_DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
namespace nativetouch {
namespace settings {
bool EMULATE_DIGITIZER = false;
bool REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP = false;
bool SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES = false;
}
static decltype(GetSystemMetrics) *GetSystemMetrics_orig = nullptr;
static decltype(GetTouchInputInfo) *GetTouchInputInfo_orig = nullptr;
static std::atomic<TouchInputFilter> touch_input_filter { nullptr };
static bool native_touch_hooked = false;
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 initialize_game_settings() {
const auto is_pan = avs::game::is_model("PAN");
settings::EMULATE_DIGITIZER = is_pan;
// PAN samples and clears touch events in its I/O update loop. Window-timer updates
// made stationary holds flicker while movement updates remained stable, so refresh
// the native contact from ac_io_update instead of using the generic mouse timer.
settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP = is_pan;
// translate native touch between desktop and game-window client coordinates
settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES = is_pan;
}
static int WINAPI GetSystemMetricsHook(int index) {
if (index == SM_DIGITIZER) {
return NID_INTEGRATED_TOUCH | NID_EXTERNAL_TOUCH |
NID_MULTI_INPUT | NID_READY;
}
return GetSystemMetrics_orig(index);
}
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) {
#if TOUCH_SIMULATE_FAT_FINGERS
point->dwMask |= 0x004;
point->cxContact = 80 * 100;
point->cyContact = 60 * 100;
#endif
// most monitors do not set TOUCHEVENTFMASK_CONTACTAREA, but for
// monitors that do set it, IIDX can get very confused (SDVX is not
// affected)
//
// while the test menu and the touch "glow" seem to work properly,
// interacting with subscreen menu items or entering PIN becomes
// very unpredictable
//
// to fix this, simply remove the contact area width and height
//
// note: test menu > I/O > touch test gives 5 numbers:
// n: x, y, w, h
// where
// n is the nth touch input since boot
// x, y are coordinates (center of finger)
// w, h are contact width and height
//
// when TOUCHEVENTFMASK_CONTACTAREA is not set, w/h will
// automatically be seen as 1x1, which works perfectly fine
log_debug(
"touch::native",
"[{}, {}] dwMask = 0x{:x}, cxContact = {}, cyContact = {}",
point->x / 100,
point->y / 100,
point->dwMask,
point->cxContact,
point->cyContact);
point->dwMask &= ~(0x004ul); // clear TOUCHEVENTFMASK_CONTACTAREA
point->cxContact = 0;
point->cyContact = 0;
}
static void flip_touch_points(PTOUCHINPUT point) {
point->x = native_display_size_x * 100 - point->x;
point->y = native_display_size_y * 100 - point->y;
}
static BOOL WINAPI GetTouchInputInfoHook(
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
// refresh after exclusive fullscreen establishes the final display mode
if (!native_display_initialized) {
update_native_display_mode();
native_display_initialized = true;
}
// call the original first
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
if (result == 0) {
return result;
}
bool flip_hardware_touch = false;
if (avs::game::is_model("KFC")) {
log_debug(
"touch::native", "orientation = {}, display size = {}x{}",
native_display_orientation,
native_display_size_x,
native_display_size_y);
if (native_display_orientation == DMDO_270) {
flip_hardware_touch = true;
}
}
for (size_t i = 0; i < cInputs; i++) {
PTOUCHINPUT point = &pInputs[i];
const auto synthetic = inject::transform_touch_input(point);
if (avs::game::is_model("LDJ")) {
strip_contact_size(point);
}
const auto flip_values = !synthetic &&
(rawinput::touch::INVERTED ^ flip_hardware_touch);
if (flip_values) {
flip_touch_points(point);
}
if (!synthetic) {
POINT position { point->x / 100, point->y / 100 };
const auto transform_result =
transform::hardware_to_game(&position);
if (transform_result == transform::Result::Transformed) {
point->x = position.x * 100;
point->y = position.y * 100;
} else if (transform_result == transform::Result::Rejected &&
!(point->dwFlags & TOUCHEVENTF_UP)) {
// suppress rejected contacts, but preserve UP to release an active touch ID
point->dwFlags = 0;
}
}
const auto filter = touch_input_filter.load(std::memory_order_acquire);
if (point->dwFlags != 0 && filter != nullptr) {
const NativeTouchEvent event {
.id = point->dwID,
.x = point->x / 100,
.y = point->y / 100,
.down = (point->dwFlags & TOUCHEVENTF_DOWN) != 0,
.move = (point->dwFlags & TOUCHEVENTF_MOVE) != 0,
.up = (point->dwFlags & TOUCHEVENTF_UP) != 0,
.synthetic = synthetic,
};
if (filter(event)) {
point->dwFlags = 0;
}
}
}
return result;
}
bool is_hooked() {
return native_touch_hooked;
}
void set_input_filter(TouchInputFilter filter) {
touch_input_filter.store(filter, std::memory_order_release);
}
void refresh_contact_lifetime() {
if (settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
inject::refresh_contact_lifetime();
}
}
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();
// 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;
}
// 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) {
GetSystemMetrics_orig = detour::iat_try(
"GetSystemMetrics", GetSystemMetricsHook, module);
if (GetSystemMetrics_orig == nullptr) {
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);
if (GetTouchInputInfo_orig == nullptr) {
log_warning("touch::native", "failed to hook GetTouchInputInfo");
return false;
}
log_misc("touch::native", "GetTouchInputInfo hooked");
native_touch_hooked = true;
return true;
}
}