nost: move nostalgia from wintouchemu to native touch hook (#827)

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

## Description of change
Switch over Nostalgia from wintouchemu to native touch hook.

Nostalgia has some strict timing requirements (touches must be updated
on every acio poll) so this takes a slightly different path to maintain
mouse holds.

Only a handful of consumers of wintouchemu remain:

1. beatstream - but it's off by default, only enabled as errata for
buggy touchscreens, or if the user forces it on (for Show Cursor)
2. gitadora single-window overlay - this is just for mouse so not a big
deal.
3. MFC HG mode - not a priority to fix.

## Testing
Tested full screen and windowed mode with touch / mouse / poke.
This commit is contained in:
bicarus
2026-07-22 00:56:03 -07:00
committed by GitHub
parent e2daefbb6a
commit 09ed8b948c
13 changed files with 270 additions and 128 deletions
+65 -5
View File
@@ -114,11 +114,18 @@ namespace nativetouch::inject {
// normally the active touch window, while dedicated TDJ uses the subscreen window
static std::atomic<HWND> injection_window { nullptr };
// cross-thread game-loop refreshes are coalesced so they cannot flood the window queue
static std::atomic<bool> contact_refresh_pending { false };
static std::once_flag initialization_once;
static int window_subclass_token;
// asks the touch window thread to send an UPDATE when the game loop runs elsewhere
static UINT contact_refresh_message;
// submit one synthetic contact frame to Windows touch injection
static bool inject_touch_frame(POINT position, POINTER_FLAGS pointer_flags) {
static bool inject_touch_frame(
POINT position, POINTER_FLAGS pointer_flags, bool retry_if_not_ready = false) {
POINTER_TOUCH_INFO contact {};
contact.pointerInfo.pointerType = PT_TOUCH;
contact.pointerInfo.pointerId = 0;
@@ -131,13 +138,14 @@ namespace nativetouch::inject {
return true;
}
// if UPDATE fails, drop it
// drop ordinary UPDATE frames when Windows is still processing the prior frame
const auto error = GetLastError();
if (error == ERROR_NOT_READY && (pointer_flags & POINTER_FLAG_UPDATE)) {
if (error == ERROR_NOT_READY &&
(pointer_flags & POINTER_FLAG_UPDATE) && !retry_if_not_ready) {
return true;
}
// if DOWN or UP fails, retry once after a brief delay; if it still fails, log a warning
// retry required frames once after a brief delay
if (error == ERROR_NOT_READY) {
Sleep(INJECTION_RETRY_DELAY_MS);
result = InjectTouchInput_ptr(1, &contact);
@@ -222,6 +230,41 @@ namespace nativetouch::inject {
return true;
}
// contact state belongs to the injection window thread; this helper must run there
static void refresh_contact_lifetime_on_window_thread(HWND window) {
if (!contact_is_owned_by(contact_state.owner, window)) {
return;
}
inject_touch_frame(contact_state.position, CONTACT_UPDATE_FLAGS, true);
}
// PAN calls this from ac_io_update to keep a stationary contact alive. Contact state is
// owned by the touch window thread, so same-thread calls refresh immediately. Calls from
// another thread post one coalesced private message for the window thread to handle.
void refresh_contact_lifetime() {
const auto window = injection_window.load(std::memory_order_acquire);
if (window == nullptr) {
return;
}
// preserve synchronous game-loop timing when it already runs on the window thread
const auto window_thread = GetWindowThreadProcessId(window, nullptr);
if (window_thread == GetCurrentThreadId()) {
refresh_contact_lifetime_on_window_thread(window);
return;
}
// otherwise marshal one outstanding refresh instead of sharing contact state across threads
if (contact_refresh_message == 0 ||
contact_refresh_pending.exchange(true, std::memory_order_acq_rel)) {
return;
}
if (!PostMessageW(window, contact_refresh_message, 0, 0)) {
contact_refresh_pending.store(false, std::memory_order_release);
}
}
void set_contact_timer(
ContactOwner owner, HWND window, UINT_PTR timer_id) {
if (contact_is_owned_by(owner, window)) {
@@ -255,6 +298,13 @@ namespace nativetouch::inject {
HWND window, UINT message, WPARAM w_param, LPARAM l_param,
UINT_PTR subclass_id, DWORD_PTR) {
// consume marshalled lifetime refreshes on the contact-owning window thread
if (contact_refresh_message != 0 && message == contact_refresh_message) {
contact_refresh_pending.store(false, std::memory_order_release);
refresh_contact_lifetime_on_window_thread(window);
return 0;
}
// IIDX handles touch on its main window even when input belongs to the subscreen
// forward the touch message there
if (message == WM_TOUCH &&
@@ -278,6 +328,7 @@ namespace nativetouch::inject {
HWND expected_window = window;
injection_window.compare_exchange_strong(
expected_window, nullptr, std::memory_order_acq_rel);
contact_refresh_pending.store(false, std::memory_order_release);
touch_delivery_window.store(nullptr, std::memory_order_release);
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
}
@@ -313,7 +364,10 @@ namespace nativetouch::inject {
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());
log_warning(
"touch::native",
"failed to attach mouse touch injection to window: {}",
GetLastError());
return;
}
@@ -371,6 +425,12 @@ namespace nativetouch::inject {
void initialize_touch_injection() {
std::call_once(initialization_once, [] {
initialize_synthetic_touch();
contact_refresh_message =
RegisterWindowMessageW(L"spice2x.native_touch.refresh_contact");
if (contact_refresh_message == 0) {
log_warning(
"touch::native", "failed to register contact refresh message: {}", GetLastError());
}
// load all APIs from user32 without adding static imports
const auto user32 = libutils::load_library("user32.dll");
@@ -13,6 +13,7 @@ namespace nativetouch::inject {
void initialize_touch_injection();
void initialize_synthetic_touch();
bool touch_injection_available();
void refresh_contact_lifetime();
bool contact_is_active();
bool contact_is_owned_by(ContactOwner owner, HWND window);
+8 -5
View File
@@ -4,6 +4,7 @@
#include <windows.h>
#include "inject_internal.h"
#include "settings.h"
#include "transform.h"
#include "touch/touch.h"
@@ -64,11 +65,13 @@ namespace nativetouch::inject {
// 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");
if (!settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
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");
}
}
}
@@ -8,6 +8,7 @@
#include "inject.h"
#include "inject_internal.h"
#include "settings.h"
#include "transform.h"
#include "util/logging.h"
@@ -38,10 +39,10 @@ namespace nativetouch::inject {
// synthetic touches preempt the mouse and keep it disabled until release or timeout
static void begin_synthetic_contact(HWND window, POINT position, bool screen_space) {
// dedicated TDJ injects into the physical subscreen, so map Windows'
// returned coordinates back into the game's touch coordinate space
// remember when Windows-returned coordinates must map back into game space
const auto transform_returned_coordinates =
transform::is_tdj_dedicated_subscreen(window);
transform::is_tdj_dedicated_subscreen(window) ||
settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
if (!screen_space && !transform::game_to_screen(window, &position)) {
return;
}
+49 -2
View File
@@ -5,6 +5,8 @@
#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"
@@ -22,6 +24,13 @@
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 bool native_touch_hooked = false;
static bool native_display_initialized = false;
@@ -29,6 +38,28 @@ namespace nativetouch {
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)) {
@@ -102,13 +133,13 @@ namespace nativetouch {
static BOOL WINAPI GetTouchInputInfoHook(
HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) {
// Refresh after exclusive fullscreen establishes the final display mode.
// 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 first
const auto result = GetTouchInputInfo_orig(hTouchInput, cInputs, pInputs, cbSize);
if (result == 0) {
return result;
@@ -162,11 +193,27 @@ namespace nativetouch {
return native_touch_hooked;
}
void refresh_contact_lifetime() {
if (settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
inject::refresh_contact_lifetime();
}
}
void hook(HMODULE module) {
initialize_game_settings();
inject::hook(module);
native_touch_hooked = true;
if (settings::EMULATE_DIGITIZER) {
GetSystemMetrics_orig = detour::iat_try(
"GetSystemMetrics", GetSystemMetricsHook, module);
if (GetSystemMetrics_orig != nullptr) {
log_misc("touch::native", "GetSystemMetrics hooked");
}
}
GetTouchInputInfo_orig = detour::iat_try("GetTouchInputInfo", GetTouchInputInfoHook, module);
if (GetTouchInputInfo_orig != nullptr) {
log_misc("touch::native", "GetTouchInputInfo hooked");
@@ -2,6 +2,7 @@
namespace nativetouch {
void hook(HMODULE module);
void refresh_contact_lifetime();
// true once hook() has installed the native touch stack for the current game;
// such games consume touch through the GetTouchInputInfo hook rather than spicetouch
+7
View File
@@ -0,0 +1,7 @@
#pragma once
namespace nativetouch::settings {
extern bool EMULATE_DIGITIZER;
extern bool REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP;
extern bool SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
}
+35 -1
View File
@@ -2,17 +2,47 @@
#include "hooks/graphics/graphics.h"
#include "overlay/overlay.h"
#include "settings.h"
#include "touch/touch.h"
namespace nativetouch::transform {
static bool game_client_to_screen(HWND window, POINT *position) {
RECT client_rect {};
if (window == nullptr ||
!GetClientRect(window, &client_rect) ||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
!PtInRect(&client_rect, *position)) {
return false;
}
return ClientToScreen(window, position) != FALSE;
}
static bool screen_to_game_client(HWND window, POINT *position) {
RECT client_rect {};
if (window == nullptr ||
!GetClientRect(window, &client_rect) ||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
!ScreenToClient(window, position) ||
!PtInRect(&client_rect, *position)) {
return false;
}
return true;
}
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
// convert game touch coordinates to Windows desktop coordinates
bool game_to_screen(HWND window, POINT *position) {
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
return game_client_to_screen(window, position);
}
if (!is_tdj_dedicated_subscreen(window)) {
return true;
}
@@ -54,6 +84,10 @@ namespace nativetouch::transform {
// convert physical screen coordinates to game touch coordinates for a known target
bool screen_to_game(HWND window, POINT *position) {
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
return screen_to_game_client(window, position);
}
// scale the resized IIDX subscreen client area into the game's touch-display coordinates
if (is_tdj_dedicated_subscreen(window)) {
RECT client_rect {};