Merge branch 'main' into nac-spice22x

Merge official spice2x 'main', up to b53447b
Notes: dc82c98  is where the nixac fork starts.

- Upstream's OBJECT-library + forbidden-static-DLL-import build refactor
  merged with nixAC's blob/dsdmo resources
- SPICE_XP / runtime large-address-aware detection builds have been
  integrated from upstream
- README.md grabs from upstream; .gitignore keeps nixAC's fork-specific
  entries
This commit is contained in:
2026-08-07 00:21:58 +02:00
359 changed files with 71032 additions and 27946 deletions
+213
View File
@@ -0,0 +1,213 @@
#include "gdi_overlay.h"
#include <cstddef>
#include "util/logging.h"
namespace {
// the back buffer matches the window DC; the software buffer has a fixed 32-bit layout
enum class BufferType {
TargetCompatible,
Bgra32,
};
struct GdiBuffer {
HDC dc = nullptr;
HBITMAP bitmap = nullptr;
// bitmap originally selected into the memory DC, restored before cleanup
HGDIOBJ old_bitmap = nullptr;
int width = 0;
int height = 0;
};
// back buffer holds the complete frame; overlay buffer holds ImGui software pixels
GdiBuffer BACK_BUFFER;
GdiBuffer OVERLAY_BUFFER;
void release_buffer(GdiBuffer &buffer) {
// destroy the DC before the bitmap so cleanup is safe even if restoration fails
if (buffer.dc != nullptr) {
if (buffer.old_bitmap != nullptr && buffer.old_bitmap != HGDI_ERROR) {
SelectObject(buffer.dc, buffer.old_bitmap);
}
DeleteDC(buffer.dc);
}
if (buffer.bitmap != nullptr) {
DeleteObject(buffer.bitmap);
}
buffer = {};
}
// ensures the buffer has a memory DC with a bitmap of the requested size and type
// selected into it. a matching allocation is reused; otherwise the old resources are
// released and recreated. returns false if the dimensions or any GDI operation fail.
bool ensure_buffer(
GdiBuffer &buffer,
HDC target_dc,
int width,
int height,
BufferType type,
const char *name) {
if (width <= 0 || height <= 0) {
return false;
}
if (buffer.dc != nullptr && buffer.bitmap != nullptr &&
buffer.width == width && buffer.height == height) {
return true;
}
// keep allocations across frames and recreate only after a size change
release_buffer(buffer);
buffer.dc = CreateCompatibleDC(target_dc);
if (buffer.dc == nullptr) {
log_warning("touch", "failed to create {} DC: {}", name, GetLastError());
return false;
}
// compatible bitmaps are fast presentation targets; BGRA bitmaps accept raw pixels
if (type == BufferType::TargetCompatible) {
buffer.bitmap = CreateCompatibleBitmap(target_dc, width, height);
} else {
buffer.bitmap = CreateBitmap(width, height, 1, sizeof(uint32_t) * 8, nullptr);
}
if (buffer.bitmap == nullptr) {
log_warning("touch", "failed to create {} bitmap: {}", name, GetLastError());
release_buffer(buffer);
return false;
}
buffer.old_bitmap = SelectObject(buffer.dc, buffer.bitmap);
if (buffer.old_bitmap == nullptr || buffer.old_bitmap == HGDI_ERROR) {
log_warning("touch", "failed to select {} bitmap: {}", name, GetLastError());
release_buffer(buffer);
return false;
}
buffer.width = width;
buffer.height = height;
return true;
}
bool update_overlay_buffer(
HDC target_dc,
const uint32_t *pixels,
bool pixels_dirty,
int width,
int height) {
if (pixels == nullptr) {
return false;
}
bool needs_update = pixels_dirty || OVERLAY_BUFFER.bitmap == nullptr ||
OVERLAY_BUFFER.width != width || OVERLAY_BUFFER.height != height;
if (!ensure_buffer(
OVERLAY_BUFFER,
target_dc,
width,
height,
BufferType::Bgra32,
"software overlay")) {
return false;
}
if (!needs_update) {
return true;
}
// SetDIBits requires the destination bitmap not to be selected into a DC
HGDIOBJ overlay_bitmap =
SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.old_bitmap);
if (overlay_bitmap == nullptr || overlay_bitmap == HGDI_ERROR) {
log_warning("touch", "failed to deselect software overlay bitmap: {}", GetLastError());
release_buffer(OVERLAY_BUFFER);
return false;
}
BITMAPINFO bitmap_info {};
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmap_info.bmiHeader.biWidth = width;
bitmap_info.bmiHeader.biHeight = -height;
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = sizeof(uint32_t) * 8;
bitmap_info.bmiHeader.biCompression = BI_RGB;
int copied_lines = SetDIBits(
target_dc,
OVERLAY_BUFFER.bitmap,
0,
height,
pixels,
&bitmap_info,
DIB_RGB_COLORS);
HGDIOBJ old_bitmap = SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.bitmap);
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
log_warning("touch", "failed to reselect software overlay bitmap: {}", GetLastError());
release_buffer(OVERLAY_BUFFER);
return false;
}
OVERLAY_BUFFER.old_bitmap = old_bitmap;
if (copied_lines != height) {
log_warning("touch", "failed to update software overlay bitmap: {} of {} lines copied",
copied_lines, height);
release_buffer(OVERLAY_BUFFER);
return false;
}
return true;
}
}
HDC touch_gdi_overlay_begin_frame(
HDC target_dc,
HBRUSH background_brush,
int width,
int height,
const uint32_t *overlay_pixels,
bool overlay_pixels_dirty,
int overlay_width,
int overlay_height) {
if (!ensure_buffer(
BACK_BUFFER,
target_dc,
width,
height,
BufferType::TargetCompatible,
"overlay back buffer")) {
return nullptr;
}
HDC draw_dc = BACK_BUFFER.dc;
SetBkMode(draw_dc, TRANSPARENT);
// start each frame from the transparent color-key background
RECT buffer_rect {0, 0, width, height};
FillRect(draw_dc, &buffer_rect, background_brush);
if (update_overlay_buffer(
target_dc,
overlay_pixels,
overlay_pixels_dirty,
overlay_width,
overlay_height) &&
!BitBlt(draw_dc, 0, 0, overlay_width, overlay_height,
OVERLAY_BUFFER.dc, 0, 0, SRCCOPY)) {
log_warning("touch", "failed to draw software overlay bitmap: {}", GetLastError());
}
return draw_dc;
}
void touch_gdi_overlay_present(HDC target_dc) {
// one full-window blit exposes the completed frame without an intermediate erase
if (!BitBlt(target_dc, 0, 0, BACK_BUFFER.width, BACK_BUFFER.height,
BACK_BUFFER.dc, 0, 0, SRCCOPY)) {
log_warning("touch", "failed to present overlay back buffer: {}", GetLastError());
}
}
void touch_gdi_overlay_release() {
release_buffer(BACK_BUFFER);
release_buffer(OVERLAY_BUFFER);
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#include <windows.h>
// prepares a complete offscreen frame and returns its drawing DC; returns null on failure
HDC touch_gdi_overlay_begin_frame(
HDC target_dc,
HBRUSH background_brush,
int width,
int height,
const uint32_t *overlay_pixels,
bool overlay_pixels_dirty,
int overlay_width,
int overlay_height);
// presents the frame prepared by the most recent successful begin call
void touch_gdi_overlay_present(HDC target_dc);
// releases all cached GDI resources
void touch_gdi_overlay_release();
+493
View File
@@ -0,0 +1,493 @@
// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602
#include <atomic>
#include <mutex>
#include <windows.h>
#include <commctrl.h>
#include "inject.h"
#include "inject_internal.h"
#include "transform.h"
#include "hooks/graphics/graphics.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace nativetouch::inject {
constexpr DWORD INJECTION_RETRY_DELAY_MS = 1;
constexpr POINTER_FLAGS CONTACT_DOWN_FLAGS =
POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
constexpr POINTER_FLAGS CONTACT_UPDATE_FLAGS =
POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
// Windows 8 APIs are resolved dynamically to preserve older-OS compatibility
static decltype(RegisterTouchWindow) *RegisterTouchWindow_orig = nullptr;
static decltype(InitializeTouchInjection) *InitializeTouchInjection_ptr = nullptr;
static decltype(InjectTouchInput) *InjectTouchInput_ptr = nullptr;
// state for the single synthetic touch contact
struct ContactState {
ContactOwner owner = ContactOwner::None;
HWND input_window = nullptr;
POINT position {};
UINT_PTR timer_id = 0;
bool is_active() const {
return owner != ContactOwner::None;
}
};
// tracks an injector-owned contact without matching unrelated hardware touches
struct SyntheticTouchIdentity {
POINT down_position {};
HANDLE source = nullptr;
DWORD id = 0;
bool identified = false;
bool pending = false;
bool transform_coordinates = false;
std::atomic<HWND> transform_window { nullptr };
void begin(POINT position, HWND window, bool transform_returned_coordinates) {
down_position = position;
source = nullptr;
id = 0;
identified = false;
pending = true;
transform_coordinates = transform_returned_coordinates;
transform_window.store(window, std::memory_order_release);
}
void reset(HWND expected_window) {
source = nullptr;
id = 0;
identified = false;
pending = false;
transform_coordinates = false;
transform_window.compare_exchange_strong(
expected_window, nullptr, std::memory_order_acq_rel);
}
HWND get_transform_window() const {
return transform_window.load(std::memory_order_acquire);
}
bool matches(PTOUCHINPUT point) {
// InjectTouchInput provides no application marker in TOUCHINPUT. Claim the first
// matching DOWN, then use the source and ID assigned by Windows for this contact.
if (!identified) {
// correlate the pending injection's pixel position
constexpr LONG POSITION_TOLERANCE = 200;
const auto delta_x = point->x - down_position.x * 100;
const auto delta_y = point->y - down_position.y * 100;
if (!pending || !(point->dwFlags & TOUCHEVENTF_DOWN) ||
delta_x < -POSITION_TOLERANCE || delta_x > POSITION_TOLERANCE ||
delta_y < -POSITION_TOLERANCE || delta_y > POSITION_TOLERANCE) {
return false;
}
// save the identity that remains stable through this contact's MOVE and UP.
source = point->hSource;
id = point->dwID;
identified = true;
}
// require both the provider and contact identities to match.
return point->hSource == source && point->dwID == id;
}
bool should_transform_coordinates() const {
return transform_coordinates;
}
};
static ContactState contact_state;
static SyntheticTouchIdentity synthetic_touch_identity;
// main game window that receives WM_TOUCH forwarded from the dedicated TDJ subscreen
static std::atomic<HWND> touch_delivery_window { nullptr };
// window whose UI thread owns synthetic contact state and synthetic touch requests;
// 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, bool retry_if_not_ready = false) {
POINTER_TOUCH_INFO contact {};
contact.pointerInfo.pointerType = PT_TOUCH;
contact.pointerInfo.pointerId = 0;
contact.pointerInfo.pointerFlags = pointer_flags;
contact.pointerInfo.ptPixelLocation = position;
contact.touchFlags = TOUCH_FLAG_NONE;
BOOL result = InjectTouchInput_ptr(1, &contact);
if (result) {
return true;
}
// 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) && !retry_if_not_ready) {
return true;
}
// retry required frames once after a brief delay
if (error == ERROR_NOT_READY) {
Sleep(INJECTION_RETRY_DELAY_MS);
result = InjectTouchInput_ptr(1, &contact);
}
if (result) {
return true;
}
static bool error_logged = false;
if (!error_logged) {
error_logged = true;
log_warning("touch::native", "failed to inject synthetic touch input: {}", GetLastError());
}
return false;
}
// rewrite only the contact created by this injector into game touch coordinates
bool transform_touch_input(PTOUCHINPUT point) {
const auto transform_window = synthetic_touch_identity.get_transform_window();
if (transform_window == nullptr) {
return false;
}
if (!synthetic_touch_identity.matches(point)) {
// this contact is not owned by this injector; leave it unchanged
return false;
}
if (synthetic_touch_identity.should_transform_coordinates()) {
// Windows receives the physical position; the game receives the mapped position
POINT position { point->x / 100, point->y / 100 };
if (transform::screen_to_game(transform_window, &position)) {
point->x = position.x * 100;
point->y = position.y * 100;
}
}
if (point->dwFlags & TOUCHEVENTF_UP) {
synthetic_touch_identity.reset(transform_window);
}
return true;
}
bool contact_is_active() {
return contact_state.is_active();
}
bool contact_is_owned_by(ContactOwner owner, HWND window) {
return contact_state.is_active() &&
contact_state.owner == owner &&
contact_state.input_window == window;
}
bool begin_contact(
ContactOwner owner,
HWND window,
POINT position,
bool transform_returned_coordinates) {
contact_state.owner = owner;
contact_state.input_window = window;
contact_state.position = position;
contact_state.timer_id = 0;
synthetic_touch_identity.begin(position, window, transform_returned_coordinates);
if (inject_touch_frame(position, CONTACT_DOWN_FLAGS)) {
return true;
}
contact_state = {};
synthetic_touch_identity.reset(window);
return false;
}
bool update_contact(ContactOwner owner, HWND window, POINT position) {
if (!contact_is_owned_by(owner, window) ||
!inject_touch_frame(position, CONTACT_UPDATE_FLAGS)) {
return false;
}
contact_state.position = position;
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)) {
contact_state.timer_id = timer_id;
}
}
// end whichever producer currently owns the single synthetic contact
bool release_active_contact() {
if (!contact_state.is_active()) {
return true;
}
const auto input_window = contact_state.input_window;
if (input_window != nullptr && contact_state.timer_id != 0) {
KillTimer(input_window, contact_state.timer_id);
}
const auto result = inject_touch_frame(contact_state.position, POINTER_FLAG_UP);
contact_state = {};
// release mouse capture if this contact owns it
if (input_window != nullptr && GetCapture() == input_window) {
ReleaseCapture();
}
return result;
}
// translate primary mouse messages on a touch window into one touch contact
static LRESULT CALLBACK touch_window_subclass_proc(
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 &&
transform::is_tdj_dedicated_subscreen(window)) {
const auto delivery_window =
touch_delivery_window.load(std::memory_order_acquire);
if (delivery_window != nullptr) {
return SendMessageW(delivery_window, message, w_param, l_param);
}
}
if (handle_synthetic_message(window, message, w_param, l_param) ||
handle_mouse_message(window, message, w_param)) {
return 0;
}
if (message == WM_NCDESTROY) {
if (contact_state.input_window == window) {
release_active_contact();
}
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);
}
return DefSubclassProc(window, message, w_param, l_param);
}
// attach mouse injection without replacing the window's existing procedure
static void attach_window_impl(HWND window, bool register_touch) {
if (!initialize_touch_injection()) {
if (register_touch) {
log_warning(
"touch::native",
"touch injection unavailable; touch window was not registered");
}
return;
}
// if requested, register for touch messages (for cases where the game didn't call it)
if (register_touch &&
(RegisterTouchWindow_orig == nullptr ||
!RegisterTouchWindow_orig(window, TWF_WANTPALM))) {
log_warning(
"touch::native", "failed to register mouse touch window: {}", GetLastError());
return;
}
// set window subclass to intercept messages
if (!SetWindowSubclass(
window,
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());
return;
}
// publish the UI-thread target for synthetic touch requests
if (!GRAPHICS_IIDX_WSUB || window == TDJ_SUBSCREEN_WINDOW) {
injection_window.store(window, std::memory_order_release);
}
log_misc(
"touch::native", "mouse touch injection attached to window {}", fmt::ptr(window));
}
HWND get_injection_window() {
return injection_window.load(std::memory_order_acquire);
}
void attach_window(HWND window) {
// attach window, but don't register for Windows touch messages
attach_window_impl(window, false);
}
void register_and_attach_window(HWND window) {
// attach window and register for Windows touch messages
attach_window_impl(window, true);
}
// preserve native registration and attach mouse injection to the touch window
static BOOL WINAPI RegisterTouchWindowHook(HWND window, ULONG flags) {
// TDJ handles touches on the main window, including contacts that the
// dedicated subscreen forwards there
if (GRAPHICS_IIDX_WSUB && window != TDJ_SUBSCREEN_WINDOW) {
touch_delivery_window.store(window, std::memory_order_release);
}
// call original
const auto result = RegisterTouchWindow_orig(window, flags);
if (result) {
// attach but don't register for touch messages
// (we're already in the middle of RegisterTouchWindow as a result
// of the game calling it)
attach_window(window);
}
return result;
}
static void clear_touch_injection_functions() {
InitializeTouchInjection_ptr = nullptr;
InjectTouchInput_ptr = nullptr;
}
// load and initialize Windows 8 touch injection without a static API dependency
bool 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
//
// 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");
InitializeTouchInjection_ptr = libutils::try_proc<decltype(InitializeTouchInjection_ptr)>(
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)>(
user32, "InjectTouchInput");
if (InjectTouchInput_ptr == nullptr) {
clear_touch_injection_functions();
log_warning(
"touch::native", "InjectTouchInput unavailable; mouse touch injection disabled");
return;
}
// reserve one synthetic contact and disable Windows' visual touch feedback
if (!InitializeTouchInjection_ptr(1, TOUCH_FEEDBACK_NONE)) {
log_warning(
"touch::native", "failed to initialize mouse touch injection: {}", GetLastError());
clear_touch_injection_functions();
return;
}
log_misc("touch::native", "mouse touch injection initialized");
});
return InitializeTouchInjection_ptr != nullptr && InjectTouchInput_ptr != nullptr;
}
// install injection support for touch windows registered by the game module
bool hook_available(HMODULE module) {
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", RegisterTouchWindowHook, module);
if (RegisterTouchWindow_orig == nullptr) {
log_warning("touch::native", "failed to hook RegisterTouchWindow");
return false;
}
log_misc("touch::native", "RegisterTouchWindow hooked");
return true;
}
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <windows.h>
struct tagTOUCHINPUT;
namespace nativetouch::inject {
void attach_window(HWND window);
void register_and_attach_window(HWND window);
bool hook_available(HMODULE module);
bool hook(HMODULE module);
bool inject_synthetic_touch(POINT position, bool down);
bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down);
bool transform_touch_input(tagTOUCHINPUT *point);
}
@@ -0,0 +1,31 @@
#pragma once
#include <windows.h>
namespace nativetouch::inject {
enum class ContactOwner {
None,
Mouse,
Synthetic,
};
bool initialize_touch_injection();
void initialize_synthetic_touch();
void refresh_contact_lifetime();
bool contact_is_active();
bool contact_is_owned_by(ContactOwner owner, HWND window);
bool begin_contact(
ContactOwner owner,
HWND window,
POINT position,
bool transform_returned_coordinates);
bool update_contact(ContactOwner owner, HWND window, POINT position);
void set_contact_timer(ContactOwner owner, HWND window, UINT_PTR timer_id);
bool release_active_contact();
HWND get_injection_window();
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param);
bool handle_synthetic_message(HWND window, UINT message, WPARAM w_param, LPARAM l_param);
}
+146
View File
@@ -0,0 +1,146 @@
// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602
#include <windows.h>
#include "inject_internal.h"
#include "settings.h"
#include "transform.h"
#include "touch/touch.h"
#include "util/logging.h"
namespace nativetouch::inject {
constexpr UINT CONTACT_TIMER_INTERVAL_MS = 16;
static int mouse_contact_timer_token;
struct PrimaryMouseButton {
UINT down_message;
UINT double_click_message;
UINT up_message;
WPARAM state_mask;
};
// honor the user's swapped-button setting when choosing the primary button
static PrimaryMouseButton get_primary_mouse_button() {
if (GetSystemMetrics(SM_SWAPBUTTON)) {
return { WM_RBUTTONDOWN, WM_RBUTTONDBLCLK, WM_RBUTTONUP, MK_RBUTTON };
}
return { WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, WM_LBUTTONUP, MK_LBUTTON };
}
// use the current physical cursor but reject points outside the subscreen
static bool get_mouse_injection_position(HWND window, POINT *position) {
// queued WM_MOUSEMOVE coordinates can lag behind the cursor; injecting them makes
// Windows move its primary pointer back to stale positions during a drag.
if (!GetCursorPos(position)) {
return false;
}
POINT transformed = *position;
return transform::mouse_to_game(window, &transformed);
}
// release the active injected contact and its window capture
static void end_mouse_contact(HWND window) {
if (contact_is_owned_by(ContactOwner::Mouse, window)) {
release_active_contact();
}
}
// begin a contact at the physical cursor position and capture future mouse input
static void begin_mouse_contact(HWND window) {
if (contact_is_active()) {
return;
}
POINT position;
if (!get_mouse_injection_position(window, &position) ||
!begin_contact(ContactOwner::Mouse, window, position, true)) {
return;
}
// keep receiving drag messages after the cursor leaves the client area
SetCapture(window);
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");
}
}
}
// update the contact while the primary button remains held
static void move_mouse_contact(
HWND window, WPARAM w_param, WPARAM primary_button_state) {
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
return;
}
POINT position;
if (!get_mouse_injection_position(window, &position)) {
end_mouse_contact(window);
return;
}
if ((w_param & primary_button_state) == 0) {
end_mouse_contact(window);
return;
}
update_contact(ContactOwner::Mouse, window, position);
}
// emit stationary update frames so Windows keeps the contact alive
static void refresh_mouse_contact(HWND window) {
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
return;
}
POINT position {};
if (!GetCursorPos(&position)) {
return;
}
POINT transformed = position;
if (!transform::mouse_to_game(window, &transformed)) {
end_mouse_contact(window);
return;
}
update_contact(ContactOwner::Mouse, window, position);
}
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param) {
if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST &&
is_mouse_message_from_touchscreen()) {
return true;
}
if (message == WM_TIMER &&
w_param == reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token)) {
refresh_mouse_contact(window);
return true;
}
const auto primary_button = get_primary_mouse_button();
if (message == primary_button.down_message ||
message == primary_button.double_click_message) {
begin_mouse_contact(window);
} else if (message == WM_MOUSEMOVE) {
move_mouse_contact(window, w_param, primary_button.state_mask);
} else if (message == primary_button.up_message) {
end_mouse_contact(window);
} else if (message == WM_CANCELMODE || message == WM_KILLFOCUS ||
message == WM_CAPTURECHANGED) {
end_mouse_contact(window);
}
return false;
}
}
@@ -0,0 +1,175 @@
// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602
#include <mutex>
#include <windows.h>
#include <windowsx.h>
#include "inject.h"
#include "inject_internal.h"
#include "settings.h"
#include "transform.h"
#include "util/logging.h"
namespace nativetouch::inject {
constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100;
enum class SyntheticTouchMessage : WPARAM {
Up, // used by callers releasing a contact
DownGameSpace, // coordinates are relative to the game's logical touch surface
DownScreenSpace, // coordinates are absolute pixels in Windows desktop coordinates
};
static std::once_flag synthetic_initialization_once;
static int synthetic_contact_timer_token;
static UINT synthetic_touch_message;
void initialize_synthetic_touch() {
std::call_once(synthetic_initialization_once, [] {
synthetic_touch_message = RegisterWindowMessageW(L"spice2x.native_touch.inject");
if (synthetic_touch_message == 0) {
log_warning(
"touch::native", "failed to register synthetic touch message: {}", GetLastError());
}
});
}
// 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) {
// remember when Windows-returned coordinates must map back into game space
const auto transform_returned_coordinates =
transform::is_tdj_dedicated_subscreen(window) ||
settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
if (!screen_space && !transform::game_to_screen(window, &position)) {
return;
}
const auto timer_id = reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token);
// when this producer already owns the contact, move it instead of releasing and
// re-pressing so continuous input (such as the API surface) drags smoothly
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
if (update_contact(ContactOwner::Synthetic, window, position)) {
// refresh the safety timeout while updates keep arriving
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
}
return;
}
}
if (!release_active_contact()) {
return;
}
if (!begin_contact(
ContactOwner::Synthetic,
window,
position,
transform_returned_coordinates)) {
return;
}
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
} else {
log_warning("touch::native", "failed to start synthetic touch timeout timer");
}
}
static void end_synthetic_contact(HWND window) {
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
release_active_contact();
}
}
bool handle_synthetic_message(
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
if (synthetic_touch_message != 0 && message == synthetic_touch_message) {
POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) };
switch (static_cast<SyntheticTouchMessage>(w_param)) {
case SyntheticTouchMessage::DownGameSpace:
begin_synthetic_contact(window, position, false);
break;
case SyntheticTouchMessage::DownScreenSpace:
begin_synthetic_contact(window, position, true);
break;
default:
end_synthetic_contact(window);
break;
}
return true;
}
if (message == WM_TIMER &&
w_param == reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token)) {
end_synthetic_contact(window);
return true;
}
return false;
}
static HWND prepare_synthetic_touch() {
if (!initialize_touch_injection()) {
return nullptr;
}
const auto window = get_injection_window();
if (window == nullptr || synthetic_touch_message == 0) {
return nullptr;
}
return window;
}
static bool post_synthetic_touch(
HWND window, POINT position, SyntheticTouchMessage message) {
return PostMessageW(
window,
synthetic_touch_message,
static_cast<WPARAM>(message),
MAKELPARAM(position.x, position.y)) != FALSE;
}
// inject a point expressed in the game's synthetic touch coordinate space
bool inject_synthetic_touch(POINT position, bool down) {
const auto window = prepare_synthetic_touch();
if (window == nullptr) {
return false;
}
const auto message = down
? SyntheticTouchMessage::DownGameSpace
: SyntheticTouchMessage::Up;
return post_synthetic_touch(window, position, message);
}
// map a logical canvas point onto the live injection window before injecting it
bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down) {
const auto window = prepare_synthetic_touch();
if (window == nullptr) {
return false;
}
if (!down) {
return post_synthetic_touch(window, position, SyntheticTouchMessage::Up);
}
RECT client_rect {};
if (canvas.cx <= 0 || canvas.cy <= 0 ||
!GetClientRect(window, &client_rect) ||
client_rect.right <= 0 || client_rect.bottom <= 0) {
return false;
}
position.x = MulDiv(position.x, client_rect.right, canvas.cx);
position.y = MulDiv(position.y, client_rect.bottom, canvas.cy);
if (!ClientToScreen(window, &position)) {
return false;
}
return post_synthetic_touch(window, position, SyntheticTouchMessage::DownScreenSpace);
}
}
@@ -0,0 +1,281 @@
// 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;
}
}
@@ -0,0 +1,26 @@
#pragma once
#include <windows.h>
namespace nativetouch {
struct NativeTouchEvent {
DWORD id;
LONG x;
LONG y;
bool down;
bool move;
bool up;
bool synthetic;
};
using TouchInputFilter = bool (*)(const NativeTouchEvent &event);
bool hook(HMODULE module);
void refresh_contact_lifetime();
void set_input_filter(TouchInputFilter filter);
// true once hook() has installed the native touch stack for the current game;
// such games consume touch through the GetTouchInputInfo hook rather than spicetouch
bool is_hooked();
}
+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;
}
+160
View File
@@ -0,0 +1,160 @@
#include "transform.h"
#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 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;
}
RECT client_rect {};
if (!GetClientRect(window, &client_rect) ||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
return false;
}
position->x = MulDiv(
position->x - SPICETOUCH_TOUCH_X,
client_rect.right,
SPICETOUCH_TOUCH_WIDTH);
position->y = MulDiv(
position->y - SPICETOUCH_TOUCH_Y,
client_rect.bottom,
SPICETOUCH_TOUCH_HEIGHT);
return ClientToScreen(window, position) != FALSE;
}
static bool has_active_overlay_transform() {
return overlay::OVERLAY != nullptr &&
overlay::OVERLAY->get_active() &&
overlay::OVERLAY->has_subscreen_touch_transform();
}
static bool transform_overlay_touch_position(POINT *position) {
// convert physical screen coordinates to the window-relative coordinates the overlay expects
if (GRAPHICS_WINDOWED) {
position->x -= SPICETOUCH_TOUCH_X;
position->y -= SPICETOUCH_TOUCH_Y;
}
// ask the overlay to do the game-specific translation
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
}
// 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 {};
if (!GetClientRect(window, &client_rect) ||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
return false;
}
if (!ScreenToClient(window, position)) {
return false;
}
if (!PtInRect(&client_rect, *position)) {
return false;
}
position->x = SPICETOUCH_TOUCH_X +
MulDiv(position->x, SPICETOUCH_TOUCH_WIDTH, client_rect.right);
position->y = SPICETOUCH_TOUCH_Y +
MulDiv(position->y, SPICETOUCH_TOUCH_HEIGHT, client_rect.bottom);
return true;
}
// check if subscreen overlay is active and can transform the touch point;
// if not, the touch point is valid as-is
if (!has_active_overlay_transform()) {
return true;
}
// ask the overlay to transform the touch point into game coordinates
return transform_overlay_touch_position(position);
}
bool mouse_to_game(HWND window, POINT *position) {
// exception: iidx tdj dedicated subscreen window is allowed
if (is_tdj_dedicated_subscreen(window)) {
return screen_to_game(window, position);
}
// if this game has a subscreen overlay that can transform touch input
// but the window is hidden or not under the cursor, reject mouse-as-touch
// (e.g., iidx/sdvx are rejected here, but nostalgia is allowed)
if (overlay::OVERLAY != nullptr &&
overlay::OVERLAY->has_subscreen_touch_transform() &&
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
return false;
}
return screen_to_game(window, position);
}
// route hardware screen coordinates through dedicated or overlay mapping and report the result
Result hardware_to_game(POINT *position) {
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
const auto active_overlay = has_active_overlay_transform();
// no dedicated subscreen or active overlay mapping; pass the point through unchanged
if (!dedicated_subscreen && !active_overlay) {
return Result::Unchanged;
}
// route through the dedicated subscreen when active, otherwise through the overlay
const auto valid = screen_to_game(
dedicated_subscreen ? TDJ_SUBSCREEN_WINDOW : nullptr,
position);
// reject out-of-bounds points and any coordinate conversion failure
return valid ? Result::Transformed : Result::Rejected;
}
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <windows.h>
namespace nativetouch::transform {
enum class Result {
Unchanged,
Transformed,
Rejected,
};
bool is_tdj_dedicated_subscreen(HWND window);
bool game_to_screen(HWND window, POINT *position);
bool screen_to_game(HWND window, POINT *position);
bool mouse_to_game(HWND window, POINT *position);
Result hardware_to_game(POINT *position);
}
+158 -108
View File
@@ -11,6 +11,7 @@
#include "avs/game.h"
#include "external/imgui/imgui.h"
#include "games/jb/jb.h"
#include "hooks/graphics/graphics.h"
#include "misc/eamuse.h"
#include "overlay/overlay.h"
@@ -19,9 +20,12 @@
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
#include "gdi_overlay.h"
#include "handler.h"
#include "touch_gestures.h"
#include "win7.h"
#include "win8.h"
@@ -35,6 +39,16 @@ static const int TOUCH_EVENT_BUFFER_SIZE = 1024 * 4;
static const int TOUCH_EVENT_BUFFER_THRESHOLD1 = 1024 * 2;
static const int TOUCH_EVENT_BUFFER_THRESHOLD2 = 1024 * 3;
// timer id for the overlay repaint tick
static const UINT_PTR SPICETOUCH_OVERLAY_TIMER_ID = 1;
// overlay repaint interval; the WinXP-compat build stays at 30 FPS
#if !SPICE_XP
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 60;
#else
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 30;
#endif // !SPICE_XP
// in mainline spicetools, this was false (show by default)
// in spice2x, this is true (hide by default)
bool SPICETOUCH_CARD_DISABLE = true;
@@ -215,22 +229,45 @@ void update_card_button() {
}
}
static void release_all_mouse_touch_points() {
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
for (size_t x = 0; x < TOUCH_POINTS.size();) {
TouchPoint *tp = &TOUCH_POINTS[x];
if (tp->id == 0u) {
TouchEvent te {
.id = tp->id,
.x = tp->x,
.y = tp->y,
.type = TOUCH_UP,
.mouse = tp->mouse,
};
add_touch_event(&te);
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
} else {
x++;
}
}
}
static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
// check if touch was registered
if (!SPICETOUCH_REGISTERED_TOUCH) {
SPICETOUCH_REGISTERED_TOUCH = true;
// check if touch is available
// register the handler when a touch screen is present
if (is_touch_available("SpiceTouchWndProc")) {
// notify the handler of our window
TOUCH_HANDLER->window_register(hWnd);
}
// enable card unless the feature is disabled
if (!SPICETOUCH_CARD_DISABLE) {
SPICETOUCH_CARD_ENABLED = true;
}
// enable the card button whenever the option is set, even without a touch
// screen (mouse clicks are handled as touch input and can trigger it)
if (!SPICETOUCH_CARD_DISABLE) {
SPICETOUCH_CARD_ENABLED = true;
}
}
@@ -243,6 +280,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
const auto is_windowed_sub =
(GRAPHICS_IIDX_WSUB && hWnd == TDJ_SUBSCREEN_WINDOW) ||
(hWnd == SDVX_SUBSCREEN_WINDOW) ||
(hWnd == POPN_SUBSCREEN_WINDOW) ||
(hWnd == GFDM_SUBSCREEN_WINDOW);
if (msg == WM_CLOSE) {
@@ -280,7 +318,10 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
break;
}
case WM_TIMER: {
InvalidateRect(hWnd, NULL, TRUE);
// request a repaint; the frame is composed into an offscreen buffer, so no
// background erase is needed (bErase = FALSE avoids a transparent flash)
InvalidateRect(hWnd, NULL, FALSE);
break;
}
case WM_PAINT: {
@@ -312,52 +353,52 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
}
// draw overlay
// render the software overlay before BeginPaint so only GDI composition
// and the final blit happen while the window is being painted
int overlay_width = 0, overlay_height = 0;
uint32_t *overlay_pixels = nullptr;
bool overlay_pixels_dirty = false;
if (overlay_enabled) {
// update and render
overlay::OVERLAY->update();
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
// get pixel data
int width, height;
uint32_t *pixel_data = overlay::OVERLAY.get()->sw_get_pixel_data(&width, &height);
if (width > 0 && height > 0) {
overlay_pixels = overlay::OVERLAY.get()->sw_get_pixel_data(
&overlay_width, &overlay_height);
overlay_pixels_dirty = overlay::OVERLAY->sw_pixels_dirty;
}
bool overlay_active = overlay_enabled && overlay::OVERLAY->get_active();
// create bitmap
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
// compose the whole frame into an offscreen back buffer and present it with a
// single blit; the window never shows a half-erased (transparent) surface
// mid-paint, which is what caused the occasional flicker at higher frame rates
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
// prepare paint
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
HDC hdcMem = CreateCompatibleDC(hdc);
SetBkMode(hdc, TRANSPARENT);
RECT bufferRect {};
GetClientRect(hWnd, &bufferRect);
int buffer_width = bufferRect.right - bufferRect.left;
int buffer_height = bufferRect.bottom - bufferRect.top;
/*
* draw bitmap
* - this currently sets the background to black because of SRCCOPY
* - SRCPAINT will blend but colors are wrong
* - once this is figured out we could also try hooking WM_PAINT and
* draw directly to the game window
*/
SelectObject(hdcMem, bitmap);
BitBlt(hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY);
// clean up
DeleteObject(bitmap);
DeleteDC(hdcMem);
EndPaint(hWnd, &paint);
}
HBRUSH color_key_brush =
(HBRUSH) GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND);
HDC draw_dc = touch_gdi_overlay_begin_frame(
hdc,
color_key_brush,
buffer_width,
buffer_height,
overlay_pixels,
overlay_pixels_dirty,
overlay_width,
overlay_height);
if (draw_dc == nullptr) {
EndPaint(hWnd, &paint);
return 0;
}
// draw card input
if (SPICETOUCH_CARD_ENABLED && (SPICETOUCH_FONT != nullptr)) {
// prepare paint
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
SetBkMode(hdc, TRANSPARENT);
// draw the insert-card button below the jubeat debug overlay; it is
// hidden while the overlay is active
if (SPICETOUCH_CARD_ENABLED && SPICETOUCH_FONT != nullptr && !overlay_active) {
// create brushes
HBRUSH brushBorder = CreateSolidBrush(RGB(0, 196, 0));
@@ -387,7 +428,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
SPICETOUCH_CARD_RECT = boxRect;
// draw borders
FillRect(hdc, &boxRect, brushBorder);
FillRect(draw_dc, &boxRect, brushBorder);
// modify box rect
boxRect.left += 1;
@@ -396,7 +437,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
boxRect.bottom -= 1;
// fill box
FillRect(hdc, &boxRect, brushFill);
FillRect(draw_dc, &boxRect, brushFill);
// modify box rect
if (should_rotate) {
@@ -408,21 +449,27 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
}
// draw text
SelectObject(hdc, SPICETOUCH_FONT);
SetTextColor(hdc, RGB(0, 196, 0));
DrawText(hdc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
SelectObject(draw_dc, SPICETOUCH_FONT);
SetTextColor(draw_dc, RGB(0, 196, 0));
DrawText(draw_dc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
// delete objects
DeleteObject(brushFill);
DeleteObject(brushBorder);
// end paint
EndPaint(hWnd, &paint);
return 0;
}
// call default window procedure
return DefWindowProc(hWnd, msg, wParam, lParam);
#if !SPICE_XP
// draw the jubeat debug overlay on top (hidden while the overlay is active)
if (overlay_enabled && !overlay_active && games::jb::touch_debug_overlay_enabled()) {
games::jb::touch_draw_debug_overlay(draw_dc);
}
#endif // !SPICE_XP
// present the composed frame in a single blit
touch_gdi_overlay_present(hdc);
EndPaint(hWnd, &paint);
return 0;
}
case WM_CREATE: {
@@ -452,6 +499,11 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
return 0;
}
case WM_DESTROY: {
touch_gdi_overlay_release();
if (SPICETOUCH_FONT != nullptr) {
DeleteObject(SPICETOUCH_FONT);
SPICETOUCH_FONT = nullptr;
}
PostQuitMessage(0);
return 0;
}
@@ -489,41 +541,29 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
// parse mouse messages
switch (msg) {
case WM_LBUTTONDOWN: {
// check if mouse is enabled
if (SPICETOUCH_ENABLE_MOUSE) {
if (is_mouse_message_from_touchscreen()) {
return 0;
}
// subscribe to mouse messages even when the cursor leaves the window
SetCapture(hWnd);
// release all old events before inserting a new one
release_all_mouse_touch_points();
// lock touch points
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
// remove all points with ID 0
for (size_t x = 0; x < TOUCH_POINTS.size(); x++) {
TouchPoint *tp = &TOUCH_POINTS[x];
if (tp->id == 0u) {
// generate touch up event
TouchEvent te {
.id = tp->id,
.x = tp->x,
.y = tp->y,
.type = TOUCH_UP,
.mouse = tp->mouse,
};
add_touch_event(&te);
// erase touch point
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
}
}
// create touch point
TouchPoint tp {
.id = 0,
.x = GET_X_LPARAM(lParam),
.y = GET_Y_LPARAM(lParam),
.mouse = true,
.down_ms = get_performance_milliseconds(),
};
TOUCH_POINTS.push_back(tp);
@@ -544,9 +584,11 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
break;
}
case WM_MOUSEMOVE: {
// check if mouse is enabled
if (SPICETOUCH_ENABLE_MOUSE) {
if (is_mouse_message_from_touchscreen()) {
return 0;
}
// lock touch points
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
@@ -581,40 +623,31 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
break;
}
case WM_LBUTTONUP: {
// check if mouse is enabled
if (SPICETOUCH_ENABLE_MOUSE) {
if (is_mouse_message_from_touchscreen()) {
return 0;
}
// lock touch points
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
// remove all points with ID 0
for (size_t x = 0; x < TOUCH_POINTS.size(); x++) {
TouchPoint *tp = &TOUCH_POINTS[x];
if (tp->id == 0u) {
// generate touch up event
TouchEvent te {
.id = tp->id,
.x = tp->x,
.y = tp->y,
.type = TOUCH_UP,
.mouse = tp->mouse,
};
add_touch_event(&te);
// remove touch point
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
}
release_all_mouse_touch_points();
if (GetCapture() == hWnd) {
ReleaseCapture();
}
}
break;
}
default:
case WM_CAPTURECHANGED:
case WM_CANCELMODE: {
// to deal with window losing the capture after SetCapture
release_all_mouse_touch_points();
if (msg == WM_CANCELMODE && GetCapture() == hWnd) {
ReleaseCapture();
}
break;
}
default:
// call original function
if (SPICETOUCH_CALL_OLD_PROC && SPICETOUCH_OLD_PROC != nullptr) {
return SPICETOUCH_OLD_PROC(hWnd, msg, wParam, lParam);
@@ -662,6 +695,8 @@ void touch_attach_dx_hook() {
// initialize touch handler
touch_initialize();
log_info("touch", "touch_attach_dx_hook: attaching SpiceTouchWndProc...");
// add dx hook
graphics_add_wnd_proc(SpiceTouchWndProc);
@@ -680,6 +715,8 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
// initialize touch handler
touch_initialize();
log_info("touch", "touch_create_wnd: creating SPICETOUCH_TOUCH_THREAD...");
// create thread
SPICETOUCH_TOUCH_THREAD = new std::thread([hWnd, overlay]() {
@@ -740,6 +777,9 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
ShowWindow(touch_window, SW_SHOWNOACTIVATE);
UpdateWindow(touch_window);
// disable the OS touch contact visualization for our own touch window
disable_touch_gestures(touch_window);
// register
touch_register_window(touch_window);
@@ -752,8 +792,8 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
// create instance
overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window));
// draw overlay in 30 FPS
SetTimer(touch_window, 1, 1000 / 30, NULL);
// draw overlay repaint timer (30 FPS on WinXP, 60 FPS otherwise)
SetTimer(touch_window, SPICETOUCH_OVERLAY_TIMER_ID, SPICETOUCH_OVERLAY_TIMER_MS, NULL);
}
}
@@ -837,6 +877,9 @@ void touch_write_points(std::vector<TouchPoint> *touch_points) {
// create new touch point when not found
if (!found) {
// stamp the landing time so debounce can measure the contact's age
tp.down_ms = get_performance_milliseconds();
// add touch point
TOUCH_POINTS.push_back(tp);
@@ -901,7 +944,7 @@ void touch_get_points(std::vector<TouchPoint> &touch_points, bool overlay) {
if (!overlay &&
overlay::OVERLAY &&
overlay::OVERLAY->get_active() &&
!overlay::OVERLAY->can_transform_touch_input() &&
!overlay::OVERLAY->has_subscreen_touch_transform() &&
ImGui::GetIO().WantCaptureMouse) {
return;
@@ -928,7 +971,7 @@ void touch_get_events(std::vector<TouchEvent> &touch_events, bool overlay) {
if (!overlay &&
overlay::OVERLAY &&
overlay::OVERLAY->get_active() &&
!overlay::OVERLAY->can_transform_touch_input() &&
!overlay::OVERLAY->has_subscreen_touch_transform() &&
ImGui::GetIO().WantCaptureMouse) {
TOUCH_EVENTS.reset();
@@ -955,4 +998,11 @@ void update_spicetouch_window_dimensions(HWND hWnd) {
SPICETOUCH_TOUCH_Y = topleft.y;
SPICETOUCH_TOUCH_WIDTH = bottomright.x - topleft.x;
SPICETOUCH_TOUCH_HEIGHT = bottomright.y - topleft.y;
}
}
// https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
bool is_mouse_message_from_touchscreen() {
constexpr ULONG_PTR MI_WP_SIGNATURE = 0xFF515700;
constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00;
return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE;
}
+2
View File
@@ -7,6 +7,7 @@ struct TouchPoint {
DWORD id;
LONG x, y;
bool mouse;
double down_ms = 0.0; // performance-counter milliseconds when the contact first landed
};
enum TouchEventType {
TOUCH_DOWN,
@@ -28,6 +29,7 @@ extern int SPICETOUCH_TOUCH_WIDTH;
extern int SPICETOUCH_TOUCH_HEIGHT;
bool is_touch_available(LPCSTR caller);
bool is_mouse_message_from_touchscreen();
void touch_attach_wnd(HWND hWnd);
void touch_attach_dx_hook();
+168
View File
@@ -0,0 +1,168 @@
// set version to Windows 8 to enable Windows 8 touch functions
#define _WIN32_WINNT 0x0602
#include <propsys.h>
#include <mutex>
#include "touch_gestures.h"
#include "util/libutils.h"
#include "util/logging.h"
// tablet/pen service flags (MicrosoftTabletPenServiceProperty atom)
// these are not present in the mingw headers
#ifndef TABLET_DISABLE_PRESSANDHOLD
#define TABLET_DISABLE_PRESSANDHOLD 0x00000001
#define TABLET_DISABLE_PENTAPFEEDBACK 0x00000008
#define TABLET_DISABLE_PENBARRELFEEDBACK 0x00000010
#define TABLET_DISABLE_TOUCHUIFORCEON 0x00000100
#define TABLET_DISABLE_TOUCHUIFORCEOFF 0x00000200
#define TABLET_DISABLE_TOUCHSWITCH 0x00008000
#define TABLET_DISABLE_FLICKS 0x00010000
#define TABLET_DISABLE_SMOOTHSCROLLING 0x00080000
#define TABLET_DISABLE_FLICKFALLBACKKEYS 0x00100000
#endif
static const char TABLET_ATOM_NAME[] = "MicrosoftTabletPenServiceProperty";
// PKEY_EdgeGesture_DisableTouchWhenFullscreen format GUID
// (not defined in the mingw headers); defined inline to avoid an initguid.h
// symbol clash with touch/win8.cpp which declares the same name
// {32CE38B2-2C9A-41B1-9BC5-B3784394AA44}
static const GUID EDGEGESTURE_DISABLE_FMT =
{ 0x32CE38B2, 0x2C9A, 0x41B1, { 0x9B, 0xC5, 0xB3, 0x78, 0x43, 0x94, 0xAA, 0x44 } };
static HINSTANCE USER32_INSTANCE = nullptr;
typedef BOOL (WINAPI *SetWindowFeedbackSetting_t)(HWND, FEEDBACK_TYPE, DWORD, UINT32, const VOID *);
static SetWindowFeedbackSetting_t pSetWindowFeedbackSetting = nullptr;
static HINSTANCE SHELL32_INSTANCE = nullptr;
typedef HRESULT (WINAPI *SHGetPropertyStoreForWindow_t)(HWND, REFIID, void **);
static SHGetPropertyStoreForWindow_t pSHGetPropertyStoreForWindow = nullptr;
static std::once_flag INIT_FLAG;
// resolve the libraries and entry points once; disable_touch_gestures may be
// called concurrently from the CreateWindowEx hooks and the touch thread
static void init_procs() {
std::call_once(INIT_FLAG, []() {
USER32_INSTANCE = libutils::load_library("user32.dll");
if (USER32_INSTANCE != nullptr) {
pSetWindowFeedbackSetting = libutils::try_proc<SetWindowFeedbackSetting_t>(
USER32_INSTANCE, "SetWindowFeedbackSetting");
}
SHELL32_INSTANCE = libutils::try_library("shell32.dll");
if (SHELL32_INSTANCE != nullptr) {
pSHGetPropertyStoreForWindow = libutils::try_proc<SHGetPropertyStoreForWindow_t>(
SHELL32_INSTANCE, "SHGetPropertyStoreForWindow");
}
});
}
static void disable_feedback_visuals(HWND hwnd) {
if (pSetWindowFeedbackSetting == nullptr) {
return;
}
BOOL enabled = FALSE;
pSetWindowFeedbackSetting(
hwnd,
FEEDBACK_TOUCH_CONTACTVISUALIZATION,
0, sizeof(enabled), &enabled);
pSetWindowFeedbackSetting(
hwnd,
FEEDBACK_TOUCH_TAP,
0, sizeof(enabled), &enabled);
pSetWindowFeedbackSetting(
hwnd,
FEEDBACK_TOUCH_DOUBLETAP,
0, sizeof(enabled), &enabled);
pSetWindowFeedbackSetting(
hwnd,
FEEDBACK_TOUCH_PRESSANDHOLD,
0, sizeof(enabled), &enabled);
pSetWindowFeedbackSetting(
hwnd,
FEEDBACK_TOUCH_RIGHTTAP,
0, sizeof(enabled), &enabled);
pSetWindowFeedbackSetting(
hwnd,
FEEDBACK_GESTURE_PRESSANDTAP,
0, sizeof(enabled), &enabled);
}
static void disable_gesture_behaviors(HWND hwnd) {
// the tablet/pen service reads this window property to decide which
// touch/pen gestures to suppress for the window. this covers:
// - press-and-hold (touch right-click / long-press ring)
// - pen tap/barrel feedback
// - flicks (edge/directional flick navigation gestures)
// - the touch keyboard invocation UI
// - smooth scrolling / flick fallback keys
DWORD tablet_flags = TABLET_DISABLE_PRESSANDHOLD |
TABLET_DISABLE_PENTAPFEEDBACK |
TABLET_DISABLE_PENBARRELFEEDBACK |
TABLET_DISABLE_FLICKS |
TABLET_DISABLE_TOUCHUIFORCEOFF |
TABLET_DISABLE_TOUCHSWITCH |
TABLET_DISABLE_SMOOTHSCROLLING |
TABLET_DISABLE_FLICKFALLBACKKEYS;
ATOM atom_id = GlobalAddAtomA(TABLET_ATOM_NAME);
if (atom_id > 0) {
SetPropA(hwnd, TABLET_ATOM_NAME, (HANDLE) ((ULONG_PTR) tablet_flags));
}
}
static void disable_edge_gestures(HWND hwnd) {
// suppress the touch edge swipes (charms/back/app bar) for the window while
// it is fullscreen. this is normally done by the touch handler's
// window_register(), but that is not called for every handler (e.g. the
// rawinput handler is a no-op), so apply it here as well.
if (pSHGetPropertyStoreForWindow == nullptr) {
return;
}
IPropertyStore *ps = nullptr;
HRESULT hr = pSHGetPropertyStoreForWindow(hwnd, IID_IPropertyStore, (void **) &ps);
if (SUCCEEDED(hr) && ps != nullptr) {
PROPERTYKEY key = { EDGEGESTURE_DISABLE_FMT, 2 };
PROPVARIANT var {};
var.vt = VT_BOOL;
var.boolVal = VARIANT_TRUE;
hr = ps->SetValue(key, var);
ps->Release();
if (FAILED(hr)) {
log_warning("touch_gestures",
"failed to disable edge gestures on HWND={}", fmt::ptr(hwnd));
}
}
}
void disable_touch_gestures(HWND hwnd) {
init_procs();
if (USER32_INSTANCE == nullptr) {
return;
}
log_misc("touch_gestures",
"disable visual feedback and gestures for touch events for HWND={}", fmt::ptr(hwnd));
// disable the visual feedback (contact circles, tap/double-tap stars, etc.)
disable_feedback_visuals(hwnd);
// disable the actual gesture behaviors (press-and-hold, flicks, etc.)
disable_gesture_behaviors(hwnd);
// disable the fullscreen touch edge swipes
disable_edge_gestures(hwnd);
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <windows.h>
// disable the OS touch UX for a window: visual feedback (contact circles,
// tap/press-and-hold indicators) and gesture behaviors (press-and-hold
// right-click, flicks, etc.)
void disable_touch_gestures(HWND hwnd);
+2
View File
@@ -11,6 +11,7 @@
#include "util/libutils.h"
#include "util/logging.h"
#include "util/time.h"
#include "rawinput/touch.h"
// mingw issue #2205 workaround
@@ -177,6 +178,7 @@ void Win7Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg
.x = point.x,
.y = point.y,
.mouse = false,
.down_ms = get_performance_milliseconds(),
};
TOUCH_POINTS.push_back(tp);
+9 -1
View File
@@ -12,6 +12,7 @@
#include "util/libutils.h"
#include "util/logging.h"
#include "util/time.h"
#include "rawinput/touch.h"
// mingw does not seem to have this either
@@ -114,10 +115,16 @@ bool Win8Handler::is_available() {
bool Win8Handler::window_register(HWND hWnd) {
// atom settings
// keep this in sync with touch/touch_gestures.cpp so registering a touch
// window does not downgrade the flag set applied there
DWORD dwHwndTabletProperty = TABLET_DISABLE_PRESSANDHOLD |
TABLET_DISABLE_PENTAPFEEDBACK |
TABLET_DISABLE_PENBARRELFEEDBACK |
TABLET_DISABLE_FLICKS;
TABLET_DISABLE_FLICKS |
TABLET_DISABLE_TOUCHUIFORCEOFF |
TABLET_DISABLE_TOUCHSWITCH |
TABLET_DISABLE_SMOOTHSCROLLING |
TABLET_DISABLE_FLICKFALLBACKKEYS;
// get atom ID
ATOM atomID = GlobalAddAtom(ATOM_NAME);
@@ -214,6 +221,7 @@ void Win8Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg
.x = point.x,
.y = point.y,
.mouse = false,
.down_ms = get_performance_milliseconds(),
};
TOUCH_POINTS.push_back(tp);