Merge branch 'main' into nac-spice22x
Merge official spice2x 'main', up tob53447bNotes:dc82c98is 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:
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user