From a6f3cc694d0f9ff0db099cfdb421255a798c2870 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:06:37 -0400 Subject: [PATCH 1/4] feat(Windows): Add touch, pen, and keyboard scan-code support Implements Windows touchscreen and pen tablet backends using the synthetic pointer injection API (Windows 10 1809+). Adds fractional absolute coordinates to mouse events, a US English keyboard layout scan-code table, and scan-code translation options to KeyboardKeyEvent. Introduces PointerTransition and PointerViewport types to the public API, and adds cancel_contact/leave_contact to Touchscreen. --- src/core/backend.cpp | 4 +- src/core/backend.hpp | 6 +- src/core/runtime.cpp | 52 +- src/include/libvirtualhid/runtime.hpp | 27 + src/include/libvirtualhid/types.hpp | 95 +++ src/platform/linux/uhid_backend.cpp | 4 +- src/platform/windows/keylayout.hpp | 286 +++++++ src/platform/windows/windows_backend.cpp | 716 +++++++++++++++++- .../fixtures/windows_backend_test_hooks.hpp | 37 +- tests/fixtures/linux_backend_test_hooks.cpp | 6 +- tests/fixtures/windows_backend_test_hooks.cpp | 178 ++++- tests/unit/test_runtime.cpp | 21 +- tests/unit/test_windows_backend.cpp | 80 +- 13 files changed, 1473 insertions(+), 39 deletions(-) create mode 100644 src/platform/windows/keylayout.hpp diff --git a/src/core/backend.cpp b/src/core/backend.cpp index bc6c5a5..930c88b 100644 --- a/src/core/backend.cpp +++ b/src/core/backend.cpp @@ -76,7 +76,7 @@ namespace lvh::detail { return OperationStatus::success(); } - OperationStatus release_contact(std::int32_t /*contact_id*/) override { + OperationStatus release_contact(std::int32_t /*contact_id*/, PointerTransition /*transition*/) override { return OperationStatus::success(); } @@ -94,7 +94,7 @@ namespace lvh::detail { return OperationStatus::success(); } - OperationStatus release_contact(std::int32_t /*contact_id*/) override { + OperationStatus release_contact(std::int32_t /*contact_id*/, PointerTransition /*transition*/) override { return OperationStatus::success(); } diff --git a/src/core/backend.hpp b/src/core/backend.hpp index 2be5e8b..1896a7f 100644 --- a/src/core/backend.hpp +++ b/src/core/backend.hpp @@ -185,9 +185,10 @@ namespace lvh::detail { * @brief Release a touch contact. * * @param contact_id Consumer-stable contact identifier. + * @param transition Pointer transition used to remove the contact. * @return Submit status. */ - virtual OperationStatus release_contact(std::int32_t contact_id) = 0; + virtual OperationStatus release_contact(std::int32_t contact_id, PointerTransition transition) = 0; /** * @brief Get platform-visible nodes associated with this backend device. @@ -236,9 +237,10 @@ namespace lvh::detail { * @brief Release a trackpad contact. * * @param contact_id Consumer-stable contact identifier. + * @param transition Pointer transition used to remove the contact. * @return Submit status. */ - virtual OperationStatus release_contact(std::int32_t contact_id) = 0; + virtual OperationStatus release_contact(std::int32_t contact_id, PointerTransition transition) = 0; /** * @brief Submit a trackpad button transition. diff --git a/src/core/runtime.cpp b/src/core/runtime.cpp index db9b1d5..4a59f8e 100644 --- a/src/core/runtime.cpp +++ b/src/core/runtime.cpp @@ -5,6 +5,7 @@ // standard includes #include +#include #include #include #include @@ -638,6 +639,19 @@ namespace lvh { return submit({.kind = MouseEventKind::absolute_motion, .x = x, .y = y, .width = width, .height = height}); } + OperationStatus Mouse::move_absolute(float x, float y, std::int32_t width, std::int32_t height) { + return submit({ + .kind = MouseEventKind::absolute_motion, + .x = static_cast(std::lround(x)), + .y = static_cast(std::lround(y)), + .absolute_x = x, + .absolute_y = y, + .has_fractional_absolute_coordinates = true, + .width = width, + .height = height, + }); + } + OperationStatus Mouse::button(MouseButton button, bool pressed) { MouseEvent event; event.kind = MouseEventKind::button; @@ -749,7 +763,41 @@ namespace lvh { device_, "touchscreen is closed", [contact_id](auto &backend) { - return backend.release_contact(contact_id); + return backend.release_contact(contact_id, PointerTransition::release); + }, + [contact_id](auto &device) { + device.last_contact.id = contact_id; + } + ); + } + + OperationStatus Touchscreen::cancel_contact(std::int32_t contact_id) { + if (contact_id < 0) { + return OperationStatus::failure(ErrorCode::invalid_argument, "touch contact id must not be negative"); + } + + return submit_touch_event( + device_, + "touchscreen is closed", + [contact_id](auto &backend) { + return backend.release_contact(contact_id, PointerTransition::cancel); + }, + [contact_id](auto &device) { + device.last_contact.id = contact_id; + } + ); + } + + OperationStatus Touchscreen::leave_contact(std::int32_t contact_id) { + if (contact_id < 0) { + return OperationStatus::failure(ErrorCode::invalid_argument, "touch contact id must not be negative"); + } + + return submit_touch_event( + device_, + "touchscreen is closed", + [contact_id](auto &backend) { + return backend.release_contact(contact_id, PointerTransition::leave); }, [contact_id](auto &device) { device.last_contact.id = contact_id; @@ -846,7 +894,7 @@ namespace lvh { device_, "trackpad is closed", [contact_id](auto &backend) { - return backend.release_contact(contact_id); + return backend.release_contact(contact_id, PointerTransition::release); }, [contact_id](auto &device) { device.last_contact.id = contact_id; diff --git a/src/include/libvirtualhid/runtime.hpp b/src/include/libvirtualhid/runtime.hpp index 1294fa7..caaac54 100644 --- a/src/include/libvirtualhid/runtime.hpp +++ b/src/include/libvirtualhid/runtime.hpp @@ -425,6 +425,17 @@ namespace lvh { */ OperationStatus move_absolute(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height); + /** + * @brief Submit absolute pointer movement with fractional coordinates. + * + * @param x Absolute X coordinate. + * @param y Absolute Y coordinate. + * @param width Width of the absolute coordinate space. + * @param height Height of the absolute coordinate space. + * @return Submit operation status. + */ + OperationStatus move_absolute(float x, float y, std::int32_t width, std::int32_t height); + /** * @brief Submit a mouse button transition. * @@ -554,6 +565,22 @@ namespace lvh { */ OperationStatus release_contact(std::int32_t contact_id); + /** + * @brief Cancel a touch contact. + * + * @param contact_id Consumer-stable contact identifier. + * @return Submit operation status. + */ + OperationStatus cancel_contact(std::int32_t contact_id); + + /** + * @brief Mark a touch contact as leaving range. + * + * @param contact_id Consumer-stable contact identifier. + * @return Submit operation status. + */ + OperationStatus leave_contact(std::int32_t contact_id); + /** * @brief Get the most recently submitted touch contact. * diff --git a/src/include/libvirtualhid/types.hpp b/src/include/libvirtualhid/types.hpp index a90c89e..17ca166 100644 --- a/src/include/libvirtualhid/types.hpp +++ b/src/include/libvirtualhid/types.hpp @@ -719,6 +719,21 @@ namespace lvh { * @brief Whether the key is pressed. */ bool pressed = false; + + /** + * @brief Optional platform scan code to submit instead of translating `key_code`. + */ + std::uint16_t scan_code = 0; + + /** + * @brief Whether `key_code` is normalized to the Windows US English keyboard layout. + */ + bool uses_normalized_key_code = false; + + /** + * @brief Whether the backend should prefer a native scan-code translation when `scan_code` is not provided. + */ + bool prefer_native_scan_code = false; }; /** @@ -731,6 +746,41 @@ namespace lvh { std::string text; }; + /** + * @brief Pixel viewport used by backends that need screen-local pointer coordinates. + */ + struct PointerViewport { + /** + * @brief Horizontal viewport offset in pixels. + */ + std::int32_t offset_x = 0; + + /** + * @brief Vertical viewport offset in pixels. + */ + std::int32_t offset_y = 0; + + /** + * @brief Viewport width in pixels, or `0` to use the platform default. + */ + std::int32_t width = 0; + + /** + * @brief Viewport height in pixels, or `0` to use the platform default. + */ + std::int32_t height = 0; + }; + + /** + * @brief Pointer state transition requested for contact-capable devices. + */ + enum class PointerTransition : std::uint8_t { + update, ///< Pointer is in range, with contact state described by the event. + release, ///< Pointer contact ended normally. + cancel, ///< Pointer contact was canceled. + leave, ///< Pointer left range without an in-contact release. + }; + /** * @brief Mouse buttons accepted by the mouse event model. */ @@ -772,6 +822,21 @@ namespace lvh { */ std::int32_t y = 0; + /** + * @brief Fractional absolute X coordinate used when `has_fractional_absolute_coordinates` is true. + */ + float absolute_x = 0.0F; + + /** + * @brief Fractional absolute Y coordinate used when `has_fractional_absolute_coordinates` is true. + */ + float absolute_y = 0.0F; + + /** + * @brief Whether absolute motion should use `absolute_x` and `absolute_y` instead of integer `x` and `y`. + */ + bool has_fractional_absolute_coordinates = false; + /** * @brief Width of the absolute coordinate space. */ @@ -826,6 +891,26 @@ namespace lvh { * @brief Contact orientation in degrees, typically in the inclusive range `[-90, 90]`. */ std::int32_t orientation = 0; + + /** + * @brief Whether the contact is touching the surface. + */ + bool touching = true; + + /** + * @brief Pixel viewport that receives the contact. + */ + PointerViewport viewport; + + /** + * @brief Contact major-axis size in viewport pixels, or `0.0` when unknown. + */ + float contact_major_axis = 0.0F; + + /** + * @brief Contact minor-axis size in viewport pixels, or `0.0` when unknown. + */ + float contact_minor_axis = 0.0F; }; /** @@ -888,6 +973,16 @@ namespace lvh { * @brief Y-axis tilt in degrees. */ float tilt_y = 0.0F; + + /** + * @brief Pointer transition represented by this tool state. + */ + PointerTransition transition = PointerTransition::update; + + /** + * @brief Pixel viewport that receives the pen tool. + */ + PointerViewport viewport; }; /** diff --git a/src/platform/linux/uhid_backend.cpp b/src/platform/linux/uhid_backend.cpp index ebb0687..0e44dec 100644 --- a/src/platform/linux/uhid_backend.cpp +++ b/src/platform/linux/uhid_backend.cpp @@ -1759,7 +1759,7 @@ namespace lvh::detail { return place_touch_contact(contact, false); } - OperationStatus release_contact(std::int32_t contact_id) override { + OperationStatus release_contact(std::int32_t contact_id, PointerTransition /*transition*/) override { return release_touch_contact(contact_id, false); } @@ -1792,7 +1792,7 @@ namespace lvh::detail { return place_touch_contact(contact, true); } - OperationStatus release_contact(std::int32_t contact_id) override { + OperationStatus release_contact(std::int32_t contact_id, PointerTransition /*transition*/) override { return release_touch_contact(contact_id, true); } diff --git a/src/platform/windows/keylayout.hpp b/src/platform/windows/keylayout.hpp new file mode 100644 index 0000000..279d46a --- /dev/null +++ b/src/platform/windows/keylayout.hpp @@ -0,0 +1,286 @@ +/** + * @file src/platform/windows/keylayout.hpp + * @brief Windows keyboard layout helpers. + */ +#pragma once + +// lib includes +#include + +// standard includes +#include +#include + +namespace lvh::detail { + /** + * @brief Virtual-key to scan-code mapping for the Windows US English keyboard layout. + */ + constexpr std::array windows_us_english_virtual_key_to_scan_code { + 0, /* 0x00 */ + 0, /* 0x01 */ + 0, /* 0x02 */ + 70, /* 0x03 */ + 0, /* 0x04 */ + 0, /* 0x05 */ + 0, /* 0x06 */ + 0, /* 0x07 */ + 14, /* 0x08 */ + 15, /* 0x09 */ + 0, /* 0x0a */ + 0, /* 0x0b */ + 76, /* 0x0c */ + 28, /* 0x0d */ + 0, /* 0x0e */ + 0, /* 0x0f */ + 42, /* 0x10 */ + 29, /* 0x11 */ + 56, /* 0x12 */ + 0, /* 0x13 */ + 58, /* 0x14 */ + 0, /* 0x15 */ + 0, /* 0x16 */ + 0, /* 0x17 */ + 0, /* 0x18 */ + 0, /* 0x19 */ + 0, /* 0x1a */ + 1, /* 0x1b */ + 0, /* 0x1c */ + 0, /* 0x1d */ + 0, /* 0x1e */ + 0, /* 0x1f */ + 57, /* 0x20 */ + 73, /* 0x21 */ + 81, /* 0x22 */ + 79, /* 0x23 */ + 71, /* 0x24 */ + 75, /* 0x25 */ + 72, /* 0x26 */ + 77, /* 0x27 */ + 80, /* 0x28 */ + 0, /* 0x29 */ + 0, /* 0x2a */ + 0, /* 0x2b */ + 84, /* 0x2c */ + 82, /* 0x2d */ + 83, /* 0x2e */ + 99, /* 0x2f */ + 11, /* 0x30 */ + 2, /* 0x31 */ + 3, /* 0x32 */ + 4, /* 0x33 */ + 5, /* 0x34 */ + 6, /* 0x35 */ + 7, /* 0x36 */ + 8, /* 0x37 */ + 9, /* 0x38 */ + 10, /* 0x39 */ + 0, /* 0x3a */ + 0, /* 0x3b */ + 0, /* 0x3c */ + 0, /* 0x3d */ + 0, /* 0x3e */ + 0, /* 0x3f */ + 0, /* 0x40 */ + 30, /* 0x41 */ + 48, /* 0x42 */ + 46, /* 0x43 */ + 32, /* 0x44 */ + 18, /* 0x45 */ + 33, /* 0x46 */ + 34, /* 0x47 */ + 35, /* 0x48 */ + 23, /* 0x49 */ + 36, /* 0x4a */ + 37, /* 0x4b */ + 38, /* 0x4c */ + 50, /* 0x4d */ + 49, /* 0x4e */ + 24, /* 0x4f */ + 25, /* 0x50 */ + 16, /* 0x51 */ + 19, /* 0x52 */ + 31, /* 0x53 */ + 20, /* 0x54 */ + 22, /* 0x55 */ + 47, /* 0x56 */ + 17, /* 0x57 */ + 45, /* 0x58 */ + 21, /* 0x59 */ + 44, /* 0x5a */ + 91, /* 0x5b */ + 92, /* 0x5c */ + 93, /* 0x5d */ + 0, /* 0x5e */ + 95, /* 0x5f */ + 82, /* 0x60 */ + 79, /* 0x61 */ + 80, /* 0x62 */ + 81, /* 0x63 */ + 75, /* 0x64 */ + 76, /* 0x65 */ + 77, /* 0x66 */ + 71, /* 0x67 */ + 72, /* 0x68 */ + 73, /* 0x69 */ + 55, /* 0x6a */ + 78, /* 0x6b */ + 0, /* 0x6c */ + 74, /* 0x6d */ + 83, /* 0x6e */ + 53, /* 0x6f */ + 59, /* 0x70 */ + 60, /* 0x71 */ + 61, /* 0x72 */ + 62, /* 0x73 */ + 63, /* 0x74 */ + 64, /* 0x75 */ + 65, /* 0x76 */ + 66, /* 0x77 */ + 67, /* 0x78 */ + 68, /* 0x79 */ + 87, /* 0x7a */ + 88, /* 0x7b */ + 100, /* 0x7c */ + 101, /* 0x7d */ + 102, /* 0x7e */ + 103, /* 0x7f */ + 104, /* 0x80 */ + 105, /* 0x81 */ + 106, /* 0x82 */ + 107, /* 0x83 */ + 108, /* 0x84 */ + 109, /* 0x85 */ + 110, /* 0x86 */ + 118, /* 0x87 */ + 0, /* 0x88 */ + 0, /* 0x89 */ + 0, /* 0x8a */ + 0, /* 0x8b */ + 0, /* 0x8c */ + 0, /* 0x8d */ + 0, /* 0x8e */ + 0, /* 0x8f */ + 69, /* 0x90 */ + 70, /* 0x91 */ + 0, /* 0x92 */ + 0, /* 0x93 */ + 0, /* 0x94 */ + 0, /* 0x95 */ + 0, /* 0x96 */ + 0, /* 0x97 */ + 0, /* 0x98 */ + 0, /* 0x99 */ + 0, /* 0x9a */ + 0, /* 0x9b */ + 0, /* 0x9c */ + 0, /* 0x9d */ + 0, /* 0x9e */ + 0, /* 0x9f */ + 42, /* 0xa0 */ + 54, /* 0xa1 */ + 29, /* 0xa2 */ + 29, /* 0xa3 */ + 56, /* 0xa4 */ + 56, /* 0xa5 */ + 106, /* 0xa6 */ + 105, /* 0xa7 */ + 103, /* 0xa8 */ + 104, /* 0xa9 */ + 101, /* 0xaa */ + 102, /* 0xab */ + 50, /* 0xac */ + 32, /* 0xad */ + 46, /* 0xae */ + 48, /* 0xaf */ + 25, /* 0xb0 */ + 16, /* 0xb1 */ + 36, /* 0xb2 */ + 34, /* 0xb3 */ + 108, /* 0xb4 */ + 109, /* 0xb5 */ + 107, /* 0xb6 */ + 33, /* 0xb7 */ + 0, /* 0xb8 */ + 0, /* 0xb9 */ + 39, /* 0xba */ + 13, /* 0xbb */ + 51, /* 0xbc */ + 12, /* 0xbd */ + 52, /* 0xbe */ + 53, /* 0xbf */ + 41, /* 0xc0 */ + 115, /* 0xc1 */ + 126, /* 0xc2 */ + 0, /* 0xc3 */ + 0, /* 0xc4 */ + 0, /* 0xc5 */ + 0, /* 0xc6 */ + 0, /* 0xc7 */ + 0, /* 0xc8 */ + 0, /* 0xc9 */ + 0, /* 0xca */ + 0, /* 0xcb */ + 0, /* 0xcc */ + 0, /* 0xcd */ + 0, /* 0xce */ + 0, /* 0xcf */ + 0, /* 0xd0 */ + 0, /* 0xd1 */ + 0, /* 0xd2 */ + 0, /* 0xd3 */ + 0, /* 0xd4 */ + 0, /* 0xd5 */ + 0, /* 0xd6 */ + 0, /* 0xd7 */ + 0, /* 0xd8 */ + 0, /* 0xd9 */ + 0, /* 0xda */ + 26, /* 0xdb */ + 43, /* 0xdc */ + 27, /* 0xdd */ + 40, /* 0xde */ + 0, /* 0xdf */ + 0, /* 0xe0 */ + 0, /* 0xe1 */ + 86, /* 0xe2 */ + 0, /* 0xe3 */ + 0, /* 0xe4 */ + 0, /* 0xe5 */ + 0, /* 0xe6 */ + 0, /* 0xe7 */ + 0, /* 0xe8 */ + 113, /* 0xe9 */ + 92, /* 0xea */ + 123, /* 0xeb */ + 0, /* 0xec */ + 111, /* 0xed */ + 90, /* 0xee */ + 0, /* 0xef */ + 0, /* 0xf0 */ + 91, /* 0xf1 */ + 0, /* 0xf2 */ + 95, /* 0xf3 */ + 0, /* 0xf4 */ + 94, /* 0xf5 */ + 0, /* 0xf6 */ + 0, /* 0xf7 */ + 0, /* 0xf8 */ + 93, /* 0xf9 */ + 0, /* 0xfa */ + 98, /* 0xfb */ + 0, /* 0xfc */ + 0, /* 0xfd */ + 0, /* 0xfe */ + 0, /* 0xff */ + }; + + /** + * @brief Translate a Windows virtual key to the canonical US English scan code. + * + * @param key_code Windows virtual key code. + * @return Canonical scan code, or `0` when no mapping exists. + */ + constexpr std::uint16_t windows_us_english_scan_code(KeyboardKeyCode key_code) { + return windows_us_english_virtual_key_to_scan_code[key_code & 0xFFU]; + } +} // namespace lvh::detail diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index 132e48d..77aa020 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -3,9 +3,25 @@ * @brief Windows UMDF control-channel backend definitions. */ +#ifndef DOXYGEN + #if !defined(WINVER) || WINVER < 0x0A00 + #undef WINVER + #define WINVER 0x0A00 + #endif + #if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0A00 + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0A00 + #endif + #if !defined(NTDDI_VERSION) || NTDDI_VERSION < 0x0A000006 + #undef NTDDI_VERSION + #define NTDDI_VERSION 0x0A000006 + #endif +#endif + // local includes #include "core/backend.hpp" #include "platform/windows/control_protocol.hpp" +#include "platform/windows/keylayout.hpp" #include #include @@ -15,13 +31,16 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -51,6 +70,16 @@ namespace lvh::detail { using UniqueHandle = std::unique_ptr; using SendInputFunction = std::function)>; + using SyncThreadDesktopFunction = std::function; + + /** + * @brief Runtime-loaded Windows synthetic pointer API entry points. + */ + struct SyntheticPointerApi { + std::function create; ///< Device creation entry point. + std::function inject; ///< Pointer injection entry point. + std::function destroy; ///< Device destroy entry point. + }; UINT send_input_with_win32(std::span inputs) { return ::SendInput( @@ -65,6 +94,62 @@ namespace lvh::detail { return function; } + HDESK sync_thread_desktop_with_win32() { + const auto desktop = ::OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL); + if (!desktop) { + return nullptr; + } + + static_cast(::SetThreadDesktop(desktop)); + ::CloseDesktop(desktop); + return desktop; + } + + SyncThreadDesktopFunction &sync_thread_desktop_function() { + static SyncThreadDesktopFunction function = sync_thread_desktop_with_win32; + return function; + } + + thread_local HDESK last_known_input_desktop = nullptr; + + template + Function load_user32_function(HMODULE user32, const char *name) { + const auto address = ::GetProcAddress(user32, name); + Function function {}; + static_assert(sizeof(function) == sizeof(address)); + std::memcpy(&function, &address, sizeof(function)); + return function; + } + + SyntheticPointerApi make_win32_synthetic_pointer_api() { + const auto user32 = ::GetModuleHandleA("user32.dll"); + if (!user32) { + return {}; + } + + const auto create = load_user32_function(user32, "CreateSyntheticPointerDevice"); + const auto inject = load_user32_function(user32, "InjectSyntheticPointerInput"); + const auto destroy = load_user32_function(user32, "DestroySyntheticPointerDevice"); + if (!create || !inject || !destroy) { + return {}; + } + + return { + .create = create, + .inject = inject, + .destroy = destroy, + }; + } + + SyntheticPointerApi &synthetic_pointer_api() { + static SyntheticPointerApi api = make_win32_synthetic_pointer_api(); + return api; + } + + bool synthetic_pointer_available(const SyntheticPointerApi &api) { + return api.create && api.inject && api.destroy; + } + constexpr GUID control_device_interface_guid { 0x3890af65, 0x2da0, @@ -109,14 +194,32 @@ namespace lvh::detail { return OperationStatus::failure(code, message.str()); } - OperationStatus send_input(std::span inputs, std::string_view operation) { + template + OperationStatus submit_with_desktop_retry(Submit submit, std::string_view operation) { using enum ErrorCode; - if (const auto sent = send_input_function()(inputs); sent != static_cast(inputs.size())) { - return windows_failure(backend_failure, operation, ::GetLastError()); + if (submit()) { + return OperationStatus::success(); } - return OperationStatus::success(); + auto error_code = ::GetLastError(); + const auto desktop = sync_thread_desktop_function()(); + if (last_known_input_desktop != desktop) { + last_known_input_desktop = desktop; + if (submit()) { + return OperationStatus::success(); + } + error_code = ::GetLastError(); + } + + return windows_failure(backend_failure, operation, error_code); + } + + OperationStatus send_input(std::span inputs, std::string_view operation) { + return submit_with_desktop_retry([&inputs] { + return send_input_function()(inputs) == static_cast(inputs.size()); + }, + operation); } OperationStatus send_input(const INPUT &input, std::string_view operation) { @@ -124,6 +227,24 @@ namespace lvh::detail { return send_input(std::span {inputs}, operation); } + OperationStatus inject_synthetic_pointer_input( + const SyntheticPointerApi &api, + HSYNTHETICPOINTERDEVICE device, + const POINTER_TYPE_INFO *pointer_info, + UINT32 count, + std::string_view operation + ) { + using enum ErrorCode; + + if (!synthetic_pointer_available(api)) { + return OperationStatus::failure(backend_unavailable, "Windows synthetic pointer APIs are unavailable"); + } + return submit_with_desktop_retry([&api, device, pointer_info, count] { + return api.inject(device, pointer_info, count) != FALSE; + }, + operation); + } + std::vector enumerate_control_device_interface_paths() { std::vector paths; @@ -208,16 +329,71 @@ namespace lvh::detail { return 0; } - LONG scale_absolute_axis(std::int32_t value, std::int32_t dimension) { + LONG scale_absolute_axis(float value, std::int32_t dimension) { if (dimension <= 1) { return 0; } - const auto clamped = std::clamp(value, 0, dimension); - const auto numerator = static_cast(clamped) * std::numeric_limits::max(); - return static_cast(numerator / dimension); + const auto clamped = std::clamp(value, 0.0F, static_cast(dimension)); + const auto scaled = clamped * static_cast(std::numeric_limits::max()) / static_cast(dimension); + return static_cast(std::lround(scaled)); + } + + PointerViewport resolve_pointer_viewport(PointerViewport viewport) { + if (viewport.width > 0 && viewport.height > 0) { + return viewport; + } + + viewport.offset_x = ::GetSystemMetrics(SM_XVIRTUALSCREEN); + viewport.offset_y = ::GetSystemMetrics(SM_YVIRTUALSCREEN); + viewport.width = std::max(1, ::GetSystemMetrics(SM_CXVIRTUALSCREEN)); + viewport.height = std::max(1, ::GetSystemMetrics(SM_CYVIRTUALSCREEN)); + return viewport; } + POINT pointer_location(const PointerViewport &raw_viewport, float x, float y) { + const auto viewport = resolve_pointer_viewport(raw_viewport); + return { + .x = viewport.offset_x + static_cast(std::lround(std::clamp(x, 0.0F, 1.0F) * static_cast(viewport.width))), + .y = viewport.offset_y + static_cast(std::lround(std::clamp(y, 0.0F, 1.0F) * static_cast(viewport.height))), + }; + } + + void update_pointer_location(POINTER_INFO &pointer_info, const PointerViewport &viewport, float x, float y) { + pointer_info.ptPixelLocation = pointer_location(viewport, x, y); + } + + bool extended_key(KeyboardKeyCode key_code) { + switch (key_code) { + case VK_LWIN: + case VK_RWIN: + case VK_RMENU: + case VK_RCONTROL: + case VK_INSERT: + case VK_DELETE: + case VK_HOME: + case VK_END: + case VK_PRIOR: + case VK_NEXT: + case VK_UP: + case VK_DOWN: + case VK_LEFT: + case VK_RIGHT: + case VK_DIVIDE: + case VK_APPS: + return true; + default: + return false; + } + } + + bool can_map_virtual_key_to_scan_code(KeyboardKeyCode key_code) { + return key_code != VK_LWIN && key_code != VK_RWIN && key_code != VK_PAUSE; + } + + constexpr auto synthetic_pointer_repeat_interval = std::chrono::milliseconds {50}; ///< Active pointer refresh interval. + constexpr auto pointer_edge_triggered_flags = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE; ///< One-frame pointer flags. + std::vector resolve_control_device_paths() { constexpr auto environment_name = "LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE"; if (const auto required_size = ::GetEnvironmentVariableA(environment_name, nullptr, 0); required_size > 1U) { @@ -717,8 +893,27 @@ namespace lvh::detail { INPUT input {}; input.type = INPUT_KEYBOARD; input.ki.wVk = event.key_code; + if (event.scan_code != 0U) { + input.ki.wVk = 0; + input.ki.wScan = event.scan_code; + input.ki.dwFlags |= KEYEVENTF_SCANCODE; + } else { + if (event.uses_normalized_key_code) { + input.ki.wScan = windows_us_english_scan_code(event.key_code); + } else if (event.prefer_native_scan_code && can_map_virtual_key_to_scan_code(event.key_code)) { + input.ki.wScan = static_cast(::MapVirtualKeyW(event.key_code, MAPVK_VK_TO_VSC)); + } + + if (input.ki.wScan != 0U) { + input.ki.wVk = 0; + input.ki.dwFlags |= KEYEVENTF_SCANCODE; + } + } + if (extended_key(event.key_code)) { + input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; + } if (!event.pressed) { - input.ki.dwFlags = KEYEVENTF_KEYUP; + input.ki.dwFlags |= KEYEVENTF_KEYUP; } return send_input(input, "submit Windows keyboard input"); @@ -811,8 +1006,14 @@ namespace lvh::detail { break; case absolute_motion: mouse.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK; - mouse.dx = scale_absolute_axis(event.x, event.width); - mouse.dy = scale_absolute_axis(event.y, event.height); + mouse.dx = scale_absolute_axis( + event.has_fractional_absolute_coordinates ? event.absolute_x : static_cast(event.x), + event.width + ); + mouse.dy = scale_absolute_axis( + event.has_fractional_absolute_coordinates ? event.absolute_y : static_cast(event.y), + event.height + ); break; case button: mouse.dwFlags = mouse_button_flags(event.button, event.pressed); @@ -840,6 +1041,489 @@ namespace lvh::detail { bool open_ = true; }; + /** + * @brief Backend touchscreen implemented with Windows synthetic pointer injection. + */ + class WindowsTouchscreen final: public BackendTouchscreen { + public: + WindowsTouchscreen(SyntheticPointerApi api, HSYNTHETICPOINTERDEVICE device): + api_ {std::move(api)}, + device_ {device} { + start_repeat_thread(); + } + + WindowsTouchscreen(const WindowsTouchscreen &) = delete; + WindowsTouchscreen &operator=(const WindowsTouchscreen &) = delete; + WindowsTouchscreen(WindowsTouchscreen &&) noexcept = delete; + WindowsTouchscreen &operator=(WindowsTouchscreen &&) noexcept = delete; + + ~WindowsTouchscreen() override { + static_cast(close()); + } + + static BackendTouchscreenCreationResult create(const SyntheticPointerApi &api, const CreateTouchscreenOptions &options) { + using enum ErrorCode; + + if (options.profile.device_type != DeviceType::touchscreen) { + return {unsupported_device_status("Windows touchscreen backend requires a touchscreen profile"), nullptr}; + } + if (!synthetic_pointer_available(api)) { + return { + OperationStatus::failure(backend_unavailable, "Windows touchscreen backend requires Windows 10 1809 or later"), + nullptr, + }; + } + + const auto device = api.create(PT_TOUCH, max_contacts, POINTER_FEEDBACK_DEFAULT); + if (!device) { + return {windows_failure(backend_failure, "create Windows touchscreen device", ::GetLastError()), nullptr}; + } + + return {OperationStatus::success(), std::make_unique(api, device)}; + } + + OperationStatus place_contact(const TouchContact &contact) override { + using enum ErrorCode; + + if (contact.id < 0) { + return OperationStatus::failure(invalid_argument, "Windows touch contact id must not be negative"); + } + + std::lock_guard lock {mutex_}; + if (!open_) { + return OperationStatus::failure(device_closed, "Windows touchscreen is closed"); + } + + const auto slot = slot_for_contact(contact.id); + if (!slot.has_value()) { + return OperationStatus::failure(invalid_argument, "too many active Windows touch contacts"); + } + + auto &pointer = contacts_[*slot]; + pointer.type = PT_TOUCH; + auto &touch_info = pointer.touchInfo; + auto &pointer_info = touch_info.pointerInfo; + const auto was_touching = contact_touching_[*slot]; + pointer_info.pointerType = PT_TOUCH; + pointer_info.pointerId = static_cast(contact.id); + pointer_info.pointerFlags |= POINTER_FLAG_INRANGE; + if (contact.touching) { + pointer_info.pointerFlags |= POINTER_FLAG_INCONTACT; + pointer_info.pointerFlags |= was_touching ? POINTER_FLAG_UPDATE : POINTER_FLAG_DOWN; + } else { + pointer_info.pointerFlags &= ~POINTER_FLAG_INCONTACT; + pointer_info.pointerFlags |= POINTER_FLAG_UPDATE; + } + update_pointer_location(pointer_info, contact.viewport, contact.x, contact.y); + + touch_info.touchMask = TOUCH_MASK_NONE; + if (contact.touching && contact.pressure > 0.0F) { + touch_info.touchMask |= TOUCH_MASK_PRESSURE; + touch_info.pressure = static_cast(std::lround(std::clamp(contact.pressure, 0.0F, 1.0F) * 1024.0F)); + } else if (contact.touching) { + touch_info.pressure = 512; + } else { + touch_info.pressure = 0; + touch_info.rcContact = {}; + } + if (contact.orientation != 0) { + touch_info.touchMask |= TOUCH_MASK_ORIENTATION; + touch_info.orientation = static_cast((contact.orientation % 360 + 360) % 360); + } else { + touch_info.orientation = 0; + } + if (contact.touching) { + update_contact_area(touch_info, contact); + } + + const auto status = inject_locked("submit Windows touchscreen contact"); + if (status.ok()) { + pointer_info.pointerFlags &= ~pointer_edge_triggered_flags; + new_slot_ = false; + contact_touching_[*slot] = contact.touching; + } + return status; + } + + OperationStatus release_contact(std::int32_t contact_id, PointerTransition transition) override { + using enum ErrorCode; + + std::lock_guard lock {mutex_}; + if (!open_) { + return OperationStatus::failure(device_closed, "Windows touchscreen is closed"); + } + + const auto slot = find_contact_slot(contact_id); + if (!slot.has_value()) { + return OperationStatus::success(); + } + + auto &pointer_info = contacts_[*slot].touchInfo.pointerInfo; + const auto was_touching = contact_touching_[*slot]; + pointer_info.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); + switch (transition) { + case PointerTransition::cancel: + pointer_info.pointerFlags |= POINTER_FLAG_CANCELED; + pointer_info.pointerFlags |= was_touching ? POINTER_FLAG_UP : POINTER_FLAG_UPDATE; + break; + case PointerTransition::leave: + pointer_info.pointerFlags |= POINTER_FLAG_UPDATE; + break; + default: + pointer_info.pointerFlags |= was_touching ? POINTER_FLAG_UP : POINTER_FLAG_UPDATE; + break; + } + const auto status = inject_locked("release Windows touchscreen contact"); + if (!status.ok()) { + return status; + } + + erase_slot(*slot); + return OperationStatus::success(); + } + + OperationStatus close() override { + stop_repeat_thread(); + + std::lock_guard lock {mutex_}; + if (!open_) { + return OperationStatus::success(); + } + + open_ = false; + if (device_) { + api_.destroy(device_); + device_ = nullptr; + } + return OperationStatus::success(); + } + + private: + static constexpr UINT32 max_contacts = 10; ///< Windows synthetic touchscreen contact capacity. + + static OperationStatus unsupported_device_status(std::string message) { + return OperationStatus::failure(ErrorCode::unsupported_profile, std::move(message)); + } + + static void update_contact_area(POINTER_TOUCH_INFO &touch_info, const TouchContact &contact) { + if (contact.contact_major_axis <= 0.0F || contact.contact_minor_axis <= 0.0F) { + touch_info.rcContact = {}; + return; + } + + const auto rotation = static_cast(contact.orientation) * static_cast(std::numbers::pi) / 180.0F; + const auto minor_rotation = rotation + (static_cast(std::numbers::pi) / 2.0F); + const auto width = + std::abs(std::cos(rotation) * contact.contact_major_axis) + + std::abs(std::cos(minor_rotation) * contact.contact_minor_axis); + const auto height = + std::abs(std::sin(rotation) * contact.contact_major_axis) + + std::abs(std::sin(minor_rotation) * contact.contact_minor_axis); + const auto &location = touch_info.pointerInfo.ptPixelLocation; + touch_info.rcContact.left = location.x - static_cast(std::floor(width / 2.0F)); + touch_info.rcContact.right = location.x + static_cast(std::ceil(width / 2.0F)); + touch_info.rcContact.top = location.y - static_cast(std::floor(height / 2.0F)); + touch_info.rcContact.bottom = location.y + static_cast(std::ceil(height / 2.0F)); + touch_info.touchMask |= TOUCH_MASK_CONTACTAREA; + } + + std::optional find_contact_slot(std::int32_t contact_id) const { + for (std::size_t index = 0; index < active_contacts_; ++index) { + if (contact_ids_[index] == contact_id) { + return index; + } + } + + return std::nullopt; + } + + std::optional slot_for_contact(std::int32_t contact_id) { + if (const auto slot = find_contact_slot(contact_id); slot.has_value()) { + new_slot_ = false; + return slot; + } + + if (active_contacts_ >= contacts_.size()) { + return std::nullopt; + } + + const auto slot = active_contacts_++; + contact_ids_[slot] = contact_id; + new_slot_ = true; + return slot; + } + + void erase_slot(std::size_t slot) { + for (std::size_t index = slot; index + 1U < active_contacts_; ++index) { + contacts_[index] = contacts_[index + 1U]; + contact_ids_[index] = contact_ids_[index + 1U]; + contact_touching_[index] = contact_touching_[index + 1U]; + } + contacts_[active_contacts_ - 1U] = {}; + contact_ids_[active_contacts_ - 1U].reset(); + contact_touching_[active_contacts_ - 1U] = false; + --active_contacts_; + } + + OperationStatus inject_locked(std::string_view operation) { + return inject_synthetic_pointer_input( + api_, + device_, + contacts_.data(), + static_cast(active_contacts_), + operation + ); + } + + void start_repeat_thread() { + repeat_thread_ = std::jthread {[this](std::stop_token stop_token) { + while (!stop_token.stop_requested()) { + std::this_thread::sleep_for(synthetic_pointer_repeat_interval); + if (stop_token.stop_requested()) { + break; + } + + std::lock_guard lock {mutex_}; + if (!open_ || active_contacts_ == 0U) { + continue; + } + static_cast(inject_locked("refresh Windows touchscreen contacts")); + } + }}; + } + + void stop_repeat_thread() { + if (repeat_thread_.joinable()) { + repeat_thread_.request_stop(); + repeat_thread_.join(); + } + } + + SyntheticPointerApi api_; + HSYNTHETICPOINTERDEVICE device_ {}; + mutable std::mutex mutex_; + std::array contacts_ {}; + std::array, max_contacts> contact_ids_ {}; + std::array contact_touching_ {}; + std::size_t active_contacts_ = 0; + bool new_slot_ = false; + bool open_ = true; + std::jthread repeat_thread_; + }; + + /** + * @brief Backend pen tablet implemented with Windows synthetic pointer injection. + */ + class WindowsPenTablet final: public BackendPenTablet { + public: + WindowsPenTablet(SyntheticPointerApi api, HSYNTHETICPOINTERDEVICE device): + api_ {std::move(api)}, + device_ {device} { + pointer_.type = PT_PEN; + pointer_.penInfo.pointerInfo.pointerType = PT_PEN; + pointer_.penInfo.pointerInfo.pointerId = 0; + start_repeat_thread(); + } + + WindowsPenTablet(const WindowsPenTablet &) = delete; + WindowsPenTablet &operator=(const WindowsPenTablet &) = delete; + WindowsPenTablet(WindowsPenTablet &&) noexcept = delete; + WindowsPenTablet &operator=(WindowsPenTablet &&) noexcept = delete; + + ~WindowsPenTablet() override { + static_cast(close()); + } + + static BackendPenTabletCreationResult create(const SyntheticPointerApi &api, const CreatePenTabletOptions &options) { + using enum ErrorCode; + + if (options.profile.device_type != DeviceType::pen_tablet) { + return {unsupported_device_status("Windows pen tablet backend requires a pen tablet profile"), nullptr}; + } + if (!synthetic_pointer_available(api)) { + return { + OperationStatus::failure(backend_unavailable, "Windows pen tablet backend requires Windows 10 1809 or later"), + nullptr, + }; + } + + const auto device = api.create(PT_PEN, 1, POINTER_FEEDBACK_DEFAULT); + if (!device) { + return {windows_failure(backend_failure, "create Windows pen tablet device", ::GetLastError()), nullptr}; + } + + return {OperationStatus::success(), std::make_unique(api, device)}; + } + + OperationStatus place_tool(const PenToolState &state) override { + using enum ErrorCode; + + std::lock_guard lock {mutex_}; + if (!open_) { + return OperationStatus::failure(device_closed, "Windows pen tablet is closed"); + } + + auto &pen_info = pointer_.penInfo; + update_tool_flags(pen_info, state.tool); + auto &pointer_info = pen_info.pointerInfo; + pointer_info.pointerType = PT_PEN; + pointer_info.pointerId = 0; + if (state.transition == PointerTransition::update) { + pointer_info.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE; + if (state.pressure >= 0.0F) { + pointer_info.pointerFlags |= POINTER_FLAG_INCONTACT; + if (!contacting_) { + pointer_info.pointerFlags |= POINTER_FLAG_DOWN; + } + } else { + pointer_info.pointerFlags &= ~POINTER_FLAG_INCONTACT; + } + active_ = true; + contacting_ = state.pressure >= 0.0F; + update_pointer_location(pointer_info, state.viewport, state.x, state.y); + } else { + pointer_info.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE); + switch (state.transition) { + case PointerTransition::cancel: + pointer_info.pointerFlags |= POINTER_FLAG_CANCELED; + pointer_info.pointerFlags |= contacting_ ? POINTER_FLAG_UP : POINTER_FLAG_UPDATE; + break; + case PointerTransition::leave: + pointer_info.pointerFlags |= POINTER_FLAG_UPDATE; + break; + default: + pointer_info.pointerFlags |= contacting_ ? POINTER_FLAG_UP : POINTER_FLAG_UPDATE; + break; + } + active_ = false; + contacting_ = false; + } + + pen_info.penMask = PEN_MASK_NONE; + if (state.pressure > 0.0F) { + pen_info.penMask |= PEN_MASK_PRESSURE; + pen_info.pressure = static_cast(std::lround(std::clamp(state.pressure, 0.0F, 1.0F) * 1024.0F)); + } else { + pen_info.pressure = 0; + } + if (state.tilt_x != 0.0F || state.tilt_y != 0.0F) { + pen_info.penMask |= PEN_MASK_TILT_X | PEN_MASK_TILT_Y; + pen_info.tiltX = static_cast(std::lround(std::clamp(state.tilt_x, -90.0F, 90.0F))); + pen_info.tiltY = static_cast(std::lround(std::clamp(state.tilt_y, -90.0F, 90.0F))); + } else { + pen_info.tiltX = 0; + pen_info.tiltY = 0; + } + if (barrel_pressed_) { + pen_info.penFlags |= PEN_FLAG_BARREL; + } else { + pen_info.penFlags &= ~PEN_FLAG_BARREL; + } + + const auto status = inject_locked("submit Windows pen tablet tool"); + if (status.ok()) { + pointer_info.pointerFlags &= ~pointer_edge_triggered_flags; + } + return status; + } + + OperationStatus button(PenButton /*button*/, bool pressed) override { + using enum ErrorCode; + + std::lock_guard lock {mutex_}; + if (!open_) { + return OperationStatus::failure(device_closed, "Windows pen tablet is closed"); + } + + barrel_pressed_ = pressed; + if (barrel_pressed_) { + pointer_.penInfo.penFlags |= PEN_FLAG_BARREL; + } else { + pointer_.penInfo.penFlags &= ~PEN_FLAG_BARREL; + } + if (active_) { + pointer_.penInfo.pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE; + const auto status = inject_locked("submit Windows pen tablet button"); + pointer_.penInfo.pointerInfo.pointerFlags &= ~pointer_edge_triggered_flags; + return status; + } + + return OperationStatus::success(); + } + + OperationStatus close() override { + stop_repeat_thread(); + + std::lock_guard lock {mutex_}; + if (!open_) { + return OperationStatus::success(); + } + + open_ = false; + if (device_) { + api_.destroy(device_); + device_ = nullptr; + } + return OperationStatus::success(); + } + + private: + static OperationStatus unsupported_device_status(std::string message) { + return OperationStatus::failure(ErrorCode::unsupported_profile, std::move(message)); + } + + static void update_tool_flags(POINTER_PEN_INFO &pen_info, PenToolType tool) { + switch (tool) { + case PenToolType::eraser: + pen_info.penFlags |= PEN_FLAG_ERASER; + break; + case PenToolType::unchanged: + break; + default: + pen_info.penFlags &= ~PEN_FLAG_ERASER; + break; + } + } + + OperationStatus inject_locked(std::string_view operation) { + return inject_synthetic_pointer_input(api_, device_, &pointer_, 1, operation); + } + + void start_repeat_thread() { + repeat_thread_ = std::jthread {[this](std::stop_token stop_token) { + while (!stop_token.stop_requested()) { + std::this_thread::sleep_for(synthetic_pointer_repeat_interval); + if (stop_token.stop_requested()) { + break; + } + + std::lock_guard lock {mutex_}; + if (!open_ || !active_) { + continue; + } + static_cast(inject_locked("refresh Windows pen tablet tool")); + } + }}; + } + + void stop_repeat_thread() { + if (repeat_thread_.joinable()) { + repeat_thread_.request_stop(); + repeat_thread_.join(); + } + } + + SyntheticPointerApi api_; + HSYNTHETICPOINTERDEVICE device_ {}; + mutable std::mutex mutex_; + POINTER_TYPE_INFO pointer_ {}; + bool barrel_pressed_ = false; + bool active_ = false; + bool contacting_ = false; + bool open_ = true; + std::jthread repeat_thread_; + }; + class WindowsBackend final: public Backend { public: WindowsBackend(): @@ -856,6 +1540,8 @@ namespace lvh::detail { capabilities_.requires_installed_driver = true; capabilities_.supports_keyboard = true; capabilities_.supports_mouse = true; + capabilities_.supports_touchscreen = synthetic_pointer_available(synthetic_pointer_api()); + capabilities_.supports_pen_tablet = synthetic_pointer_available(synthetic_pointer_api()); if (!command_channel || !event_channel) { return; @@ -933,9 +1619,9 @@ namespace lvh::detail { BackendTouchscreenCreationResult create_touchscreen( DeviceId /*id*/, - const CreateTouchscreenOptions & /*options*/ + const CreateTouchscreenOptions &options ) override { - return {unsupported_device_status("Windows backend currently supports gamepad, keyboard, and mouse devices only"), nullptr}; + return WindowsTouchscreen::create(synthetic_pointer_api(), options); } BackendTrackpadCreationResult create_trackpad( @@ -947,9 +1633,9 @@ namespace lvh::detail { BackendPenTabletCreationResult create_pen_tablet( DeviceId /*id*/, - const CreatePenTabletOptions & /*options*/ + const CreatePenTabletOptions &options ) override { - return {unsupported_device_status("Windows backend currently supports gamepad, keyboard, and mouse devices only"), nullptr}; + return WindowsPenTablet::create(synthetic_pointer_api(), options); } private: diff --git a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp index fe75b62..79a7b95 100644 --- a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp @@ -78,7 +78,9 @@ namespace lvh::detail::test { OperationStatus empty_text_status; OperationStatus invalid_text_status; OperationStatus failure_status; + OperationStatus normalized_status; OperationStatus invalid_profile_status; + WindowsSendInputRecord normalized_input; }; struct WindowsMouseSendInputResult { @@ -97,15 +99,46 @@ namespace lvh::detail::test { }; struct WindowsUnsupportedInputResult { - OperationStatus touchscreen_status; OperationStatus trackpad_status; - OperationStatus pen_tablet_status; + }; + + struct WindowsSyntheticPointerRecord { + std::uint32_t type = 0; + std::uint32_t count = 0; + std::uint32_t pointer_flags = 0; + std::uint32_t pointer_id = 0; + std::int32_t x = 0; + std::int32_t y = 0; + std::uint32_t touch_mask = 0; + std::uint32_t touch_pressure = 0; + std::uint32_t touch_orientation = 0; + std::uint32_t pen_mask = 0; + std::uint32_t pen_flags = 0; + std::int32_t pen_tilt_x = 0; + std::int32_t pen_tilt_y = 0; + }; + + struct WindowsSyntheticPointerResult { + BackendCapabilities capabilities; + OperationStatus touchscreen_create_status; + OperationStatus touchscreen_place_status; + OperationStatus touchscreen_release_status; + OperationStatus touchscreen_invalid_profile_status; + OperationStatus touchscreen_failure_status; + OperationStatus pen_tablet_create_status; + OperationStatus pen_tablet_button_status; + OperationStatus pen_tablet_tool_status; + OperationStatus pen_tablet_invalid_profile_status; + std::size_t created_devices = 0; + std::size_t destroyed_devices = 0; + std::vector injected_pointers; }; struct WindowsBackendSendInputResult { WindowsKeyboardSendInputResult keyboard; WindowsMouseSendInputResult mouse; WindowsUnsupportedInputResult unsupported; + WindowsSyntheticPointerResult synthetic; std::vector sent_inputs; }; diff --git a/tests/fixtures/linux_backend_test_hooks.cpp b/tests/fixtures/linux_backend_test_hooks.cpp index fa1c754..c93e11e 100644 --- a/tests/fixtures/linux_backend_test_hooks.cpp +++ b/tests/fixtures/linux_backend_test_hooks.cpp @@ -1040,7 +1040,7 @@ namespace lvh::detail::test { UinputTouchscreen touchscreen {descriptors[1]}; auto status = touchscreen.place_contact(contact); if (status.ok()) { - status = touchscreen.release_contact(contact.id); + status = touchscreen.release_contact(contact.id, PointerTransition::release); } static_cast(touchscreen.close()); auto records = read_input_events_until_eof(descriptors[0]); @@ -1063,7 +1063,7 @@ namespace lvh::detail::test { status = trackpad.button(false); } if (status.ok()) { - status = trackpad.release_contact(contact.id); + status = trackpad.release_contact(contact.id, PointerTransition::release); } static_cast(trackpad.close()); auto records = read_input_events_until_eof(descriptors[0]); @@ -1109,7 +1109,7 @@ namespace lvh::detail::test { }); } for (auto id = 4; id >= 0 && status.ok(); --id) { - status = trackpad.release_contact(id); + status = trackpad.release_contact(id, PointerTransition::release); } static_cast(trackpad.close()); auto records = read_input_events_until_eof(descriptors[0]); diff --git a/tests/fixtures/windows_backend_test_hooks.cpp b/tests/fixtures/windows_backend_test_hooks.cpp index 7311d94..69d9fed 100644 --- a/tests/fixtures/windows_backend_test_hooks.cpp +++ b/tests/fixtures/windows_backend_test_hooks.cpp @@ -269,9 +269,13 @@ namespace lvh::detail { public: explicit ScopedFakeSendInput(FakeSendInputState &state) { previous_function_ = send_input_function(); + previous_sync_function_ = sync_thread_desktop_function(); send_input_function() = [&state](std::span inputs) { return fake_send_input(state, inputs); }; + sync_thread_desktop_function() = [] { + return static_cast(nullptr); + }; } ScopedFakeSendInput(const ScopedFakeSendInput &) = delete; @@ -281,10 +285,116 @@ namespace lvh::detail { ~ScopedFakeSendInput() { send_input_function() = std::move(previous_function_); + sync_thread_desktop_function() = std::move(previous_sync_function_); } private: SendInputFunction previous_function_ = send_input_with_win32; + SyncThreadDesktopFunction previous_sync_function_ = sync_thread_desktop_with_win32; + }; + + struct FakeSyntheticPointerState { + std::vector injected_pointers; + std::vector created_types; + std::size_t destroyed_devices = 0; + std::optional forced_inject_result; + bool force_create_failure = false; + std::uintptr_t next_device = 1; + }; + + test::WindowsSyntheticPointerRecord make_synthetic_pointer_record(const POINTER_TYPE_INFO &pointer, UINT32 count) { + test::WindowsSyntheticPointerRecord record; + record.type = pointer.type; + record.count = count; + if (pointer.type == PT_TOUCH) { + const auto &touch = pointer.touchInfo; + record.pointer_flags = touch.pointerInfo.pointerFlags; + record.pointer_id = touch.pointerInfo.pointerId; + record.x = touch.pointerInfo.ptPixelLocation.x; + record.y = touch.pointerInfo.ptPixelLocation.y; + record.touch_mask = touch.touchMask; + record.touch_pressure = touch.pressure; + record.touch_orientation = touch.orientation; + } else if (pointer.type == PT_PEN) { + const auto &pen = pointer.penInfo; + record.pointer_flags = pen.pointerInfo.pointerFlags; + record.pointer_id = pen.pointerInfo.pointerId; + record.x = pen.pointerInfo.ptPixelLocation.x; + record.y = pen.pointerInfo.ptPixelLocation.y; + record.pen_mask = pen.penMask; + record.pen_flags = pen.penFlags; + record.pen_tilt_x = pen.tiltX; + record.pen_tilt_y = pen.tiltY; + } + + return record; + } + + HSYNTHETICPOINTERDEVICE fake_create_synthetic_pointer_device( + FakeSyntheticPointerState &state, + POINTER_INPUT_TYPE type, + ULONG /*max_count*/, + POINTER_FEEDBACK_MODE /*mode*/ + ) { + if (state.force_create_failure) { + ::SetLastError(ERROR_ACCESS_DENIED); + return nullptr; + } + + state.created_types.push_back(type); + return reinterpret_cast(state.next_device++); + } + + BOOL fake_inject_synthetic_pointer_input( + FakeSyntheticPointerState &state, + HSYNTHETICPOINTERDEVICE /*device*/, + const POINTER_TYPE_INFO *pointer_info, + UINT32 count + ) { + for (UINT32 index = 0; index < count; ++index) { + state.injected_pointers.push_back(make_synthetic_pointer_record(pointer_info[index], count)); + } + + if (state.forced_inject_result.has_value()) { + ::SetLastError(ERROR_ACCESS_DENIED); + return *state.forced_inject_result; + } + + return TRUE; + } + + void fake_destroy_synthetic_pointer_device(FakeSyntheticPointerState &state, HSYNTHETICPOINTERDEVICE /*device*/) { + ++state.destroyed_devices; + } + + class ScopedFakeSyntheticPointerApi { + public: + explicit ScopedFakeSyntheticPointerApi(FakeSyntheticPointerState &state) { + previous_api_ = synthetic_pointer_api(); + synthetic_pointer_api() = SyntheticPointerApi { + .create = [&state](POINTER_INPUT_TYPE type, ULONG max_count, POINTER_FEEDBACK_MODE mode) { + return fake_create_synthetic_pointer_device(state, type, max_count, mode); + }, + .inject = [&state](HSYNTHETICPOINTERDEVICE device, const POINTER_TYPE_INFO *pointer_info, UINT32 count) { + return fake_inject_synthetic_pointer_input(state, device, pointer_info, count); + }, + .destroy = [&state](HSYNTHETICPOINTERDEVICE device) { + fake_destroy_synthetic_pointer_device(state, device); + }, + }; + } + + ScopedFakeSyntheticPointerApi(const ScopedFakeSyntheticPointerApi &) = delete; + ScopedFakeSyntheticPointerApi &operator=(const ScopedFakeSyntheticPointerApi &) = delete; + ScopedFakeSyntheticPointerApi(ScopedFakeSyntheticPointerApi &&) noexcept = delete; + ScopedFakeSyntheticPointerApi &operator=(ScopedFakeSyntheticPointerApi &&) noexcept = delete; + + ~ScopedFakeSyntheticPointerApi() { + synthetic_pointer_api() = std::move(previous_api_); + } + + private: + SyntheticPointerApi previous_api_; }; } // namespace @@ -479,9 +589,12 @@ namespace lvh::detail { using enum MouseEventKind; WindowsBackendSendInputResult result; - WindowsBackend backend {nullptr, nullptr}; FakeSendInputState fake_send_input; ScopedFakeSendInput scoped_send_input {fake_send_input}; + FakeSyntheticPointerState fake_synthetic_pointer; + ScopedFakeSyntheticPointerApi scoped_synthetic_pointer {fake_synthetic_pointer}; + WindowsBackend backend {nullptr, nullptr}; + result.synthetic.capabilities = backend.capabilities(); CreateKeyboardOptions keyboard_options; keyboard_options.profile = profiles::keyboard(); @@ -496,6 +609,14 @@ namespace lvh::detail { fake_send_input.forced_sent_count = 0U; result.keyboard.failure_status = keyboard.keyboard->submit({.key_code = 0x42, .pressed = true}); fake_send_input.forced_sent_count.reset(); + + const auto prior_input_count = fake_send_input.sent_inputs.size(); + result.keyboard.normalized_status = + keyboard.keyboard->submit({.key_code = 0x41, .pressed = true, .uses_normalized_key_code = true}); + if (fake_send_input.sent_inputs.size() > prior_input_count) { + result.keyboard.normalized_input = fake_send_input.sent_inputs.back(); + fake_send_input.sent_inputs.resize(prior_input_count); + } } else { result.keyboard.down_status = keyboard.status; } @@ -547,7 +668,32 @@ namespace lvh::detail { CreateTouchscreenOptions touchscreen_options; touchscreen_options.profile = profiles::touchscreen(); - result.unsupported.touchscreen_status = backend.create_touchscreen(14, touchscreen_options).status; + if (auto touchscreen = backend.create_touchscreen(14, touchscreen_options); touchscreen) { + result.synthetic.touchscreen_create_status = touchscreen.status; + result.synthetic.touchscreen_place_status = touchscreen.touchscreen->place_contact({ + .id = 7, + .x = 0.25F, + .y = 0.5F, + .pressure = 0.75F, + .orientation = 30, + .viewport = {.offset_x = 100, .offset_y = 200, .width = 400, .height = 300}, + .contact_major_axis = 20.0F, + .contact_minor_axis = 10.0F, + }); + result.synthetic.touchscreen_release_status = + touchscreen.touchscreen->release_contact(7, PointerTransition::release); + + fake_synthetic_pointer.forced_inject_result = FALSE; + result.synthetic.touchscreen_failure_status = touchscreen.touchscreen->place_contact({ + .id = 8, + .x = 0.1F, + .y = 0.2F, + .viewport = {.offset_x = 0, .offset_y = 0, .width = 100, .height = 100}, + }); + fake_synthetic_pointer.forced_inject_result.reset(); + } else { + result.synthetic.touchscreen_create_status = touchscreen.status; + } CreateTrackpadOptions trackpad_options; trackpad_options.profile = profiles::trackpad(); @@ -555,9 +701,35 @@ namespace lvh::detail { CreatePenTabletOptions pen_tablet_options; pen_tablet_options.profile = profiles::pen_tablet(); - result.unsupported.pen_tablet_status = backend.create_pen_tablet(16, pen_tablet_options).status; + if (auto pen_tablet = backend.create_pen_tablet(16, pen_tablet_options); pen_tablet) { + result.synthetic.pen_tablet_create_status = pen_tablet.status; + result.synthetic.pen_tablet_button_status = pen_tablet.pen_tablet->button(PenButton::secondary, true); + result.synthetic.pen_tablet_tool_status = pen_tablet.pen_tablet->place_tool({ + .tool = PenToolType::eraser, + .x = 0.5F, + .y = 0.75F, + .pressure = 0.5F, + .distance = -1.0F, + .tilt_x = 10.0F, + .tilt_y = -20.0F, + .viewport = {.offset_x = 10, .offset_y = 20, .width = 200, .height = 100}, + }); + } else { + result.synthetic.pen_tablet_create_status = pen_tablet.status; + } + + CreateTouchscreenOptions invalid_touchscreen_options; + invalid_touchscreen_options.profile = profiles::pen_tablet(); + result.synthetic.touchscreen_invalid_profile_status = backend.create_touchscreen(17, invalid_touchscreen_options).status; + + CreatePenTabletOptions invalid_pen_tablet_options; + invalid_pen_tablet_options.profile = profiles::touchscreen(); + result.synthetic.pen_tablet_invalid_profile_status = backend.create_pen_tablet(18, invalid_pen_tablet_options).status; result.sent_inputs = std::move(fake_send_input.sent_inputs); + result.synthetic.created_devices = fake_synthetic_pointer.created_types.size(); + result.synthetic.destroyed_devices = fake_synthetic_pointer.destroyed_devices; + result.synthetic.injected_pointers = std::move(fake_synthetic_pointer.injected_pointers); return result; } diff --git a/tests/unit/test_runtime.cpp b/tests/unit/test_runtime.cpp index 7e19466..c715459 100644 --- a/tests/unit/test_runtime.cpp +++ b/tests/unit/test_runtime.cpp @@ -47,9 +47,7 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) { EXPECT_TRUE(runtime->capabilities().requires_installed_driver); EXPECT_TRUE(runtime->capabilities().supports_keyboard); EXPECT_TRUE(runtime->capabilities().supports_mouse); - EXPECT_FALSE(runtime->capabilities().supports_touchscreen); EXPECT_FALSE(runtime->capabilities().supports_trackpad); - EXPECT_FALSE(runtime->capabilities().supports_pen_tablet); auto keyboard = runtime->create_keyboard(); ASSERT_TRUE(keyboard) << keyboard.status.message(); @@ -57,9 +55,24 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) { auto mouse = runtime->create_mouse(); ASSERT_TRUE(mouse) << mouse.status.message(); EXPECT_TRUE(mouse.mouse->close().ok()); - EXPECT_EQ(runtime->create_touchscreen().status.code(), lvh::ErrorCode::unsupported_profile); + + auto touchscreen = runtime->create_touchscreen(); + if (runtime->capabilities().supports_touchscreen) { + ASSERT_TRUE(touchscreen) << touchscreen.status.message(); + EXPECT_TRUE(touchscreen.touchscreen->close().ok()); + } else { + EXPECT_FALSE(touchscreen); + } + EXPECT_EQ(runtime->create_trackpad().status.code(), lvh::ErrorCode::unsupported_profile); - EXPECT_EQ(runtime->create_pen_tablet().status.code(), lvh::ErrorCode::unsupported_profile); + + auto pen_tablet = runtime->create_pen_tablet(); + if (runtime->capabilities().supports_pen_tablet) { + ASSERT_TRUE(pen_tablet) << pen_tablet.status.message(); + EXPECT_TRUE(pen_tablet.pen_tablet->close().ok()); + } else { + EXPECT_FALSE(pen_tablet); + } if (!runtime->capabilities().supports_gamepad) { auto created = runtime->create_gamepad(lvh::profiles::xbox_360()); diff --git a/tests/unit/test_windows_backend.cpp b/tests/unit/test_windows_backend.cpp index e52462d..d1e4318 100644 --- a/tests/unit/test_windows_backend.cpp +++ b/tests/unit/test_windows_backend.cpp @@ -3,10 +3,29 @@ * @brief Unit tests for the Windows backend control-channel integration. */ +#ifndef DOXYGEN + #if !defined(WINVER) || WINVER < 0x0A00 + #undef WINVER + #define WINVER 0x0A00 + #endif + #if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0A00 + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0A00 + #endif + #if !defined(NTDDI_VERSION) || NTDDI_VERSION < 0x0A000006 + #undef NTDDI_VERSION + #define NTDDI_VERSION 0x0A000006 + #endif +#endif + // local includes #include "fixtures/fixtures.hpp" #include "fixtures/windows_backend_test_hooks.hpp" +// standard includes +#include +#include + #ifndef NOMINMAX #define NOMINMAX #endif @@ -25,6 +44,15 @@ namespace { EXPECT_TRUE(status.ok()) << status.message(); } + template + const lvh::detail::test::WindowsSyntheticPointerRecord *find_synthetic_pointer( + const std::vector &records, + Predicate predicate + ) { + const auto position = std::find_if(records.begin(), records.end(), predicate); + return position == records.end() ? nullptr : &*position; + } + } // namespace TEST_F(WindowsBackendTest, FakeChannelExercisesLifecycleSubmitCloseAndOutput) { @@ -112,6 +140,11 @@ TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsu expect_ok(result.keyboard.empty_text_status); EXPECT_EQ(result.keyboard.invalid_text_status.code(), lvh::ErrorCode::invalid_argument); EXPECT_EQ(result.keyboard.failure_status.code(), lvh::ErrorCode::backend_failure); + expect_ok(result.keyboard.normalized_status); + EXPECT_EQ(result.keyboard.normalized_input.type, INPUT_KEYBOARD); + EXPECT_EQ(result.keyboard.normalized_input.virtual_key, 0U); + EXPECT_EQ(result.keyboard.normalized_input.scan_code, 30U); + EXPECT_EQ(result.keyboard.normalized_input.key_flags, KEYEVENTF_SCANCODE); expect_ok(result.mouse.relative_status); expect_ok(result.mouse.absolute_status); @@ -127,9 +160,48 @@ TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsu EXPECT_EQ(result.keyboard.invalid_profile_status.code(), lvh::ErrorCode::unsupported_profile); EXPECT_EQ(result.mouse.invalid_profile_status.code(), lvh::ErrorCode::unsupported_profile); - EXPECT_EQ(result.unsupported.touchscreen_status.code(), lvh::ErrorCode::unsupported_profile); EXPECT_EQ(result.unsupported.trackpad_status.code(), lvh::ErrorCode::unsupported_profile); - EXPECT_EQ(result.unsupported.pen_tablet_status.code(), lvh::ErrorCode::unsupported_profile); + + EXPECT_TRUE(result.synthetic.capabilities.supports_touchscreen); + EXPECT_TRUE(result.synthetic.capabilities.supports_pen_tablet); + expect_ok(result.synthetic.touchscreen_create_status); + expect_ok(result.synthetic.touchscreen_place_status); + expect_ok(result.synthetic.touchscreen_release_status); + EXPECT_EQ(result.synthetic.touchscreen_invalid_profile_status.code(), lvh::ErrorCode::unsupported_profile); + EXPECT_EQ(result.synthetic.touchscreen_failure_status.code(), lvh::ErrorCode::backend_failure); + expect_ok(result.synthetic.pen_tablet_create_status); + expect_ok(result.synthetic.pen_tablet_button_status); + expect_ok(result.synthetic.pen_tablet_tool_status); + EXPECT_EQ(result.synthetic.pen_tablet_invalid_profile_status.code(), lvh::ErrorCode::unsupported_profile); + EXPECT_EQ(result.synthetic.created_devices, 2U); + EXPECT_EQ(result.synthetic.destroyed_devices, 2U); + ASSERT_GE(result.synthetic.injected_pointers.size(), 4U); + const auto first_touch = find_synthetic_pointer(result.synthetic.injected_pointers, [](const auto &record) { + return record.type == PT_TOUCH && record.pointer_id == 7U && (record.pointer_flags & POINTER_FLAG_DOWN) != 0U; + }); + ASSERT_NE(first_touch, nullptr); + EXPECT_EQ(first_touch->x, 200); + EXPECT_EQ(first_touch->y, 350); + EXPECT_EQ(first_touch->touch_pressure, 768U); + + const auto released_touch = find_synthetic_pointer(result.synthetic.injected_pointers, [](const auto &record) { + return record.type == PT_TOUCH && record.pointer_id == 7U && (record.pointer_flags & POINTER_FLAG_UP) != 0U; + }); + ASSERT_NE(released_touch, nullptr); + + const auto failed_touch = find_synthetic_pointer(result.synthetic.injected_pointers, [](const auto &record) { + return record.type == PT_TOUCH && record.pointer_id == 8U; + }); + ASSERT_NE(failed_touch, nullptr); + + const auto pen_tool = find_synthetic_pointer(result.synthetic.injected_pointers, [](const auto &record) { + return record.type == PT_PEN && (record.pointer_flags & POINTER_FLAG_DOWN) != 0U; + }); + ASSERT_NE(pen_tool, nullptr); + EXPECT_EQ(pen_tool->x, 110); + EXPECT_EQ(pen_tool->y, 95); + EXPECT_EQ(pen_tool->pen_flags & PEN_FLAG_ERASER, PEN_FLAG_ERASER); + EXPECT_EQ(pen_tool->pen_flags & PEN_FLAG_BARREL, PEN_FLAG_BARREL); ASSERT_EQ(result.sent_inputs.size(), 18U); EXPECT_EQ(result.sent_inputs[0].type, INPUT_KEYBOARD); @@ -152,8 +224,8 @@ TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsu EXPECT_EQ(result.sent_inputs[7].mouse_x, 11); EXPECT_EQ(result.sent_inputs[7].mouse_y, -12); EXPECT_EQ(result.sent_inputs[8].mouse_flags, MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK); - EXPECT_EQ(result.sent_inputs[8].mouse_x, 32767); - EXPECT_EQ(result.sent_inputs[8].mouse_y, 32767); + EXPECT_EQ(result.sent_inputs[8].mouse_x, 32768); + EXPECT_EQ(result.sent_inputs[8].mouse_y, 32768); EXPECT_EQ(result.sent_inputs[9].mouse_flags, MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK); EXPECT_EQ(result.sent_inputs[9].mouse_x, 0); EXPECT_EQ(result.sent_inputs[9].mouse_y, 0); From 41bc2e6e9fe564306423fbd624cbe1bd93649dfe Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:29:21 -0400 Subject: [PATCH 2/4] Sonar fixes --- src/platform/windows/windows_backend.cpp | 77 +++++++++++++++---- tests/fixtures/windows_backend_test_hooks.cpp | 7 +- tests/unit/test_windows_backend.cpp | 12 ++- 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index 77aa020..ff181f9 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -4,16 +4,22 @@ */ #ifndef DOXYGEN - #if !defined(WINVER) || WINVER < 0x0A00 + #if defined(WINVER) && WINVER < 0x0A00 #undef WINVER + #endif + #ifndef WINVER #define WINVER 0x0A00 #endif - #if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0A00 + #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 #undef _WIN32_WINNT + #endif + #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0A00 #endif - #if !defined(NTDDI_VERSION) || NTDDI_VERSION < 0x0A000006 + #if defined(NTDDI_VERSION) && NTDDI_VERSION < 0x0A000006 #undef NTDDI_VERSION + #endif + #ifndef NTDDI_VERSION #define NTDDI_VERSION 0x0A000006 #endif #endif @@ -72,6 +78,13 @@ namespace lvh::detail { using SendInputFunction = std::function)>; using SyncThreadDesktopFunction = std::function; + /** + * @brief Thread-local desktop identity used for SendInput retry decisions. + */ + struct LastKnownInputDesktop { + HDESK value = nullptr; ///< Last desktop returned by OpenInputDesktop. + }; + /** * @brief Runtime-loaded Windows synthetic pointer API entry points. */ @@ -110,7 +123,10 @@ namespace lvh::detail { return function; } - thread_local HDESK last_known_input_desktop = nullptr; + LastKnownInputDesktop &last_known_input_desktop() { + thread_local LastKnownInputDesktop desktop; + return desktop; + } template Function load_user32_function(HMODULE user32, const char *name) { @@ -203,9 +219,9 @@ namespace lvh::detail { } auto error_code = ::GetLastError(); - const auto desktop = sync_thread_desktop_function()(); - if (last_known_input_desktop != desktop) { - last_known_input_desktop = desktop; + auto &known_desktop = last_known_input_desktop(); + if (const auto desktop = sync_thread_desktop_function()(); known_desktop.value != desktop) { + known_desktop.value = desktop; if (submit()) { return OperationStatus::success(); } @@ -1057,8 +1073,8 @@ namespace lvh::detail { WindowsTouchscreen(WindowsTouchscreen &&) noexcept = delete; WindowsTouchscreen &operator=(WindowsTouchscreen &&) noexcept = delete; - ~WindowsTouchscreen() override { - static_cast(close()); + ~WindowsTouchscreen() noexcept override { + close_noexcept(); } static BackendTouchscreenCreationResult create(const SyntheticPointerApi &api, const CreateTouchscreenOptions &options) { @@ -1173,8 +1189,7 @@ namespace lvh::detail { pointer_info.pointerFlags |= was_touching ? POINTER_FLAG_UP : POINTER_FLAG_UPDATE; break; } - const auto status = inject_locked("release Windows touchscreen contact"); - if (!status.ok()) { + if (const auto status = inject_locked("release Windows touchscreen contact"); !status.ok()) { return status; } @@ -1299,6 +1314,24 @@ namespace lvh::detail { } } + /** + * @brief Close the device from the destructor without allowing exceptions to escape. + */ + void close_noexcept() noexcept { + stop_repeat_thread(); + + std::lock_guard lock {mutex_}; + if (!open_) { + return; + } + + open_ = false; + if (device_) { + api_.destroy(device_); + device_ = nullptr; + } + } + SyntheticPointerApi api_; HSYNTHETICPOINTERDEVICE device_ {}; mutable std::mutex mutex_; @@ -1330,8 +1363,8 @@ namespace lvh::detail { WindowsPenTablet(WindowsPenTablet &&) noexcept = delete; WindowsPenTablet &operator=(WindowsPenTablet &&) noexcept = delete; - ~WindowsPenTablet() override { - static_cast(close()); + ~WindowsPenTablet() noexcept override { + close_noexcept(); } static BackendPenTabletCreationResult create(const SyntheticPointerApi &api, const CreatePenTabletOptions &options) { @@ -1513,6 +1546,24 @@ namespace lvh::detail { } } + /** + * @brief Close the device from the destructor without allowing exceptions to escape. + */ + void close_noexcept() noexcept { + stop_repeat_thread(); + + std::lock_guard lock {mutex_}; + if (!open_) { + return; + } + + open_ = false; + if (device_) { + api_.destroy(device_); + device_ = nullptr; + } + } + SyntheticPointerApi api_; HSYNTHETICPOINTERDEVICE device_ {}; mutable std::mutex mutex_; diff --git a/tests/fixtures/windows_backend_test_hooks.cpp b/tests/fixtures/windows_backend_test_hooks.cpp index 69d9fed..aaa6df9 100644 --- a/tests/fixtures/windows_backend_test_hooks.cpp +++ b/tests/fixtures/windows_backend_test_hooks.cpp @@ -10,6 +10,9 @@ #include "../../src/platform/windows/windows_backend.cpp" #undef create_platform_backend +// standard includes +#include + namespace lvh::detail { namespace { @@ -341,8 +344,10 @@ namespace lvh::detail { return nullptr; } + static_assert(sizeof(HSYNTHETICPOINTERDEVICE) == sizeof(state.next_device)); + state.created_types.push_back(type); - return reinterpret_cast(state.next_device++); + return std::bit_cast(state.next_device++); } BOOL fake_inject_synthetic_pointer_input( diff --git a/tests/unit/test_windows_backend.cpp b/tests/unit/test_windows_backend.cpp index d1e4318..d9d5e53 100644 --- a/tests/unit/test_windows_backend.cpp +++ b/tests/unit/test_windows_backend.cpp @@ -4,16 +4,22 @@ */ #ifndef DOXYGEN - #if !defined(WINVER) || WINVER < 0x0A00 + #if defined(WINVER) && WINVER < 0x0A00 #undef WINVER + #endif + #ifndef WINVER #define WINVER 0x0A00 #endif - #if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0A00 + #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 #undef _WIN32_WINNT + #endif + #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0A00 #endif - #if !defined(NTDDI_VERSION) || NTDDI_VERSION < 0x0A000006 + #if defined(NTDDI_VERSION) && NTDDI_VERSION < 0x0A000006 #undef NTDDI_VERSION + #endif + #ifndef NTDDI_VERSION #define NTDDI_VERSION 0x0A000006 #endif #endif From b1a6f0bb59bd4b595b0cdcb0a98e525cb6356c1b Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:55:08 -0400 Subject: [PATCH 3/4] Sonar duplication --- src/platform/windows/windows_backend.cpp | 275 ++++++++++------------- 1 file changed, 121 insertions(+), 154 deletions(-) diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index ff181f9..0576842 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -166,6 +166,10 @@ namespace lvh::detail { return api.create && api.inject && api.destroy; } + OperationStatus unsupported_profile_status(std::string message) { + return OperationStatus::failure(ErrorCode::unsupported_profile, std::move(message)); + } + constexpr GUID control_device_interface_guid { 0x3890af65, 0x2da0, @@ -1057,15 +1061,106 @@ namespace lvh::detail { bool open_ = true; }; + /** + * @brief Shared lifecycle and refresh support for Windows synthetic pointer devices. + */ + class WindowsSyntheticPointerDevice { + protected: + WindowsSyntheticPointerDevice(SyntheticPointerApi api, HSYNTHETICPOINTERDEVICE device): + api_ {std::move(api)}, + device_ {device} {} + + WindowsSyntheticPointerDevice(const WindowsSyntheticPointerDevice &) = delete; + WindowsSyntheticPointerDevice &operator=(const WindowsSyntheticPointerDevice &) = delete; + WindowsSyntheticPointerDevice(WindowsSyntheticPointerDevice &&) noexcept = delete; + WindowsSyntheticPointerDevice &operator=(WindowsSyntheticPointerDevice &&) noexcept = delete; + + virtual ~WindowsSyntheticPointerDevice() = default; + + /** + * @brief Start periodic synthetic pointer refreshes while input is active. + * + * @param operation Operation description used for failure reporting. + */ + void start_repeat_thread(std::string_view operation) { + repeat_thread_ = std::jthread {[this, operation = std::string {operation}](std::stop_token stop_token) { + while (!stop_token.stop_requested()) { + std::this_thread::sleep_for(synthetic_pointer_repeat_interval); + if (stop_token.stop_requested()) { + break; + } + + std::lock_guard lock {mutex_}; + if (!open_ || !has_repeat_input_locked()) { + continue; + } + static_cast(inject_locked(operation)); + } + }}; + } + + /** + * @brief Close the device and release the synthetic pointer handle. + * + * @return Success status after the device is closed. + */ + OperationStatus close_device() { + stop_repeat_thread(); + + std::lock_guard lock {mutex_}; + close_device_locked(); + return OperationStatus::success(); + } + + /** + * @brief Close the device from the destructor without allowing exceptions to escape. + */ + void close_device_noexcept() noexcept { + stop_repeat_thread(); + + std::lock_guard lock {mutex_}; + close_device_locked(); + } + + SyntheticPointerApi api_; + HSYNTHETICPOINTERDEVICE device_ {}; + mutable std::mutex mutex_; + bool open_ = true; + + private: + virtual bool has_repeat_input_locked() const = 0; + virtual OperationStatus inject_locked(std::string_view operation) = 0; + + void stop_repeat_thread() { + if (repeat_thread_.joinable()) { + repeat_thread_.request_stop(); + repeat_thread_.join(); + } + } + + void close_device_locked() { + if (!open_) { + return; + } + + open_ = false; + if (device_) { + api_.destroy(device_); + device_ = nullptr; + } + } + + std::jthread repeat_thread_; + }; + /** * @brief Backend touchscreen implemented with Windows synthetic pointer injection. */ - class WindowsTouchscreen final: public BackendTouchscreen { + class WindowsTouchscreen final: public WindowsSyntheticPointerDevice, public BackendTouchscreen { public: WindowsTouchscreen(SyntheticPointerApi api, HSYNTHETICPOINTERDEVICE device): - api_ {std::move(api)}, - device_ {device} { - start_repeat_thread(); + WindowsSyntheticPointerDevice {std::move(api), device} { + start_repeat_thread("refresh Windows touchscreen contacts"); } WindowsTouchscreen(const WindowsTouchscreen &) = delete; @@ -1074,14 +1169,14 @@ namespace lvh::detail { WindowsTouchscreen &operator=(WindowsTouchscreen &&) noexcept = delete; ~WindowsTouchscreen() noexcept override { - close_noexcept(); + close_device_noexcept(); } static BackendTouchscreenCreationResult create(const SyntheticPointerApi &api, const CreateTouchscreenOptions &options) { using enum ErrorCode; if (options.profile.device_type != DeviceType::touchscreen) { - return {unsupported_device_status("Windows touchscreen backend requires a touchscreen profile"), nullptr}; + return {unsupported_profile_status("Windows touchscreen backend requires a touchscreen profile"), nullptr}; } if (!synthetic_pointer_available(api)) { return { @@ -1198,28 +1293,12 @@ namespace lvh::detail { } OperationStatus close() override { - stop_repeat_thread(); - - std::lock_guard lock {mutex_}; - if (!open_) { - return OperationStatus::success(); - } - - open_ = false; - if (device_) { - api_.destroy(device_); - device_ = nullptr; - } - return OperationStatus::success(); + return close_device(); } private: static constexpr UINT32 max_contacts = 10; ///< Windows synthetic touchscreen contact capacity. - static OperationStatus unsupported_device_status(std::string message) { - return OperationStatus::failure(ErrorCode::unsupported_profile, std::move(message)); - } - static void update_contact_area(POINTER_TOUCH_INFO &touch_info, const TouchContact &contact) { if (contact.contact_major_axis <= 0.0F || contact.contact_minor_axis <= 0.0F) { touch_info.rcContact = {}; @@ -1280,7 +1359,11 @@ namespace lvh::detail { --active_contacts_; } - OperationStatus inject_locked(std::string_view operation) { + bool has_repeat_input_locked() const override { + return active_contacts_ != 0U; + } + + OperationStatus inject_locked(std::string_view operation) override { return inject_synthetic_pointer_input( api_, device_, @@ -1290,72 +1373,24 @@ namespace lvh::detail { ); } - void start_repeat_thread() { - repeat_thread_ = std::jthread {[this](std::stop_token stop_token) { - while (!stop_token.stop_requested()) { - std::this_thread::sleep_for(synthetic_pointer_repeat_interval); - if (stop_token.stop_requested()) { - break; - } - - std::lock_guard lock {mutex_}; - if (!open_ || active_contacts_ == 0U) { - continue; - } - static_cast(inject_locked("refresh Windows touchscreen contacts")); - } - }}; - } - - void stop_repeat_thread() { - if (repeat_thread_.joinable()) { - repeat_thread_.request_stop(); - repeat_thread_.join(); - } - } - - /** - * @brief Close the device from the destructor without allowing exceptions to escape. - */ - void close_noexcept() noexcept { - stop_repeat_thread(); - - std::lock_guard lock {mutex_}; - if (!open_) { - return; - } - - open_ = false; - if (device_) { - api_.destroy(device_); - device_ = nullptr; - } - } - - SyntheticPointerApi api_; - HSYNTHETICPOINTERDEVICE device_ {}; - mutable std::mutex mutex_; std::array contacts_ {}; std::array, max_contacts> contact_ids_ {}; std::array contact_touching_ {}; std::size_t active_contacts_ = 0; bool new_slot_ = false; - bool open_ = true; - std::jthread repeat_thread_; }; /** * @brief Backend pen tablet implemented with Windows synthetic pointer injection. */ - class WindowsPenTablet final: public BackendPenTablet { + class WindowsPenTablet final: public WindowsSyntheticPointerDevice, public BackendPenTablet { public: WindowsPenTablet(SyntheticPointerApi api, HSYNTHETICPOINTERDEVICE device): - api_ {std::move(api)}, - device_ {device} { + WindowsSyntheticPointerDevice {std::move(api), device} { pointer_.type = PT_PEN; pointer_.penInfo.pointerInfo.pointerType = PT_PEN; pointer_.penInfo.pointerInfo.pointerId = 0; - start_repeat_thread(); + start_repeat_thread("refresh Windows pen tablet tool"); } WindowsPenTablet(const WindowsPenTablet &) = delete; @@ -1364,14 +1399,14 @@ namespace lvh::detail { WindowsPenTablet &operator=(WindowsPenTablet &&) noexcept = delete; ~WindowsPenTablet() noexcept override { - close_noexcept(); + close_device_noexcept(); } static BackendPenTabletCreationResult create(const SyntheticPointerApi &api, const CreatePenTabletOptions &options) { using enum ErrorCode; if (options.profile.device_type != DeviceType::pen_tablet) { - return {unsupported_device_status("Windows pen tablet backend requires a pen tablet profile"), nullptr}; + return {unsupported_profile_status("Windows pen tablet backend requires a pen tablet profile"), nullptr}; } if (!synthetic_pointer_available(api)) { return { @@ -1485,26 +1520,10 @@ namespace lvh::detail { } OperationStatus close() override { - stop_repeat_thread(); - - std::lock_guard lock {mutex_}; - if (!open_) { - return OperationStatus::success(); - } - - open_ = false; - if (device_) { - api_.destroy(device_); - device_ = nullptr; - } - return OperationStatus::success(); + return close_device(); } private: - static OperationStatus unsupported_device_status(std::string message) { - return OperationStatus::failure(ErrorCode::unsupported_profile, std::move(message)); - } - static void update_tool_flags(POINTER_PEN_INFO &pen_info, PenToolType tool) { switch (tool) { case PenToolType::eraser: @@ -1518,61 +1537,18 @@ namespace lvh::detail { } } - OperationStatus inject_locked(std::string_view operation) { - return inject_synthetic_pointer_input(api_, device_, &pointer_, 1, operation); - } - - void start_repeat_thread() { - repeat_thread_ = std::jthread {[this](std::stop_token stop_token) { - while (!stop_token.stop_requested()) { - std::this_thread::sleep_for(synthetic_pointer_repeat_interval); - if (stop_token.stop_requested()) { - break; - } - - std::lock_guard lock {mutex_}; - if (!open_ || !active_) { - continue; - } - static_cast(inject_locked("refresh Windows pen tablet tool")); - } - }}; - } - - void stop_repeat_thread() { - if (repeat_thread_.joinable()) { - repeat_thread_.request_stop(); - repeat_thread_.join(); - } + bool has_repeat_input_locked() const override { + return active_; } - /** - * @brief Close the device from the destructor without allowing exceptions to escape. - */ - void close_noexcept() noexcept { - stop_repeat_thread(); - - std::lock_guard lock {mutex_}; - if (!open_) { - return; - } - - open_ = false; - if (device_) { - api_.destroy(device_); - device_ = nullptr; - } + OperationStatus inject_locked(std::string_view operation) override { + return inject_synthetic_pointer_input(api_, device_, &pointer_, 1, operation); } - SyntheticPointerApi api_; - HSYNTHETICPOINTERDEVICE device_ {}; - mutable std::mutex mutex_; POINTER_TYPE_INFO pointer_ {}; bool barrel_pressed_ = false; bool active_ = false; bool contacting_ = false; - bool open_ = true; - std::jthread repeat_thread_; }; class WindowsBackend final: public Backend { @@ -1639,7 +1615,7 @@ namespace lvh::detail { if (options.profile.gamepad_kind == GamepadProfileKind::xbox_360) { return { - unsupported_device_status( + unsupported_profile_status( "Windows UMDF/VHF backend cannot expose Xbox 360 XUSB gamepads; use an XUSB fallback for this profile" ), nullptr, @@ -1654,7 +1630,7 @@ namespace lvh::detail { const CreateKeyboardOptions &options ) override { if (options.profile.device_type != DeviceType::keyboard) { - return {unsupported_device_status("Windows keyboard backend requires a keyboard profile"), nullptr}; + return {unsupported_profile_status("Windows keyboard backend requires a keyboard profile"), nullptr}; } return {OperationStatus::success(), std::make_unique()}; @@ -1662,7 +1638,7 @@ namespace lvh::detail { BackendMouseCreationResult create_mouse(DeviceId /*id*/, const CreateMouseOptions &options) override { if (options.profile.device_type != DeviceType::mouse) { - return {unsupported_device_status("Windows mouse backend requires a mouse profile"), nullptr}; + return {unsupported_profile_status("Windows mouse backend requires a mouse profile"), nullptr}; } return {OperationStatus::success(), std::make_unique()}; @@ -1679,7 +1655,7 @@ namespace lvh::detail { DeviceId /*id*/, const CreateTrackpadOptions & /*options*/ ) override { - return {unsupported_device_status("Windows backend currently supports gamepad, keyboard, and mouse devices only"), nullptr}; + return {unsupported_profile_status("Windows backend currently supports gamepad, keyboard, and mouse devices only"), nullptr}; } BackendPenTabletCreationResult create_pen_tablet( @@ -1690,15 +1666,6 @@ namespace lvh::detail { } private: - static OperationStatus unsupported_device_status(std::string message) { - using enum ErrorCode; - - return OperationStatus::failure( - unsupported_profile, - std::move(message) - ); - } - BackendCapabilities capabilities_; std::shared_ptr context_; }; From 34637488efd52de0336b211f3349a73481d216fa Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:42:42 -0400 Subject: [PATCH 4/4] Fix Sonar synthetic pointer state access --- src/platform/windows/windows_backend.cpp | 64 ++++++++++++++++++------ 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index 0576842..f388bd8 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -1122,15 +1122,49 @@ namespace lvh::detail { close_device_locked(); } - SyntheticPointerApi api_; - HSYNTHETICPOINTERDEVICE device_ {}; - mutable std::mutex mutex_; - bool open_ = true; + /** + * @brief Lock the synthetic pointer device state. + * + * @return Lock guarding the device state. + */ + [[nodiscard]] std::unique_lock lock_device() const { + return std::unique_lock {mutex_}; + } + + /** + * @brief Check whether the synthetic pointer device is open while the caller holds the lock. + * + * @return true when the device is open, false otherwise. + */ + bool is_open_locked() const { + return open_; + } + + /** + * @brief Inject synthetic pointer input while the caller holds the lock. + * + * @param inputs Synthetic pointer packets to inject. + * @param count Number of packets to inject. + * @param operation Operation description used for failure reporting. + * @return Operation status from the injection call. + */ + OperationStatus inject_synthetic_pointer_input_locked( + const POINTER_TYPE_INFO *inputs, + UINT32 count, + std::string_view operation + ) { + return inject_synthetic_pointer_input(api_, device_, inputs, count, operation); + } private: virtual bool has_repeat_input_locked() const = 0; virtual OperationStatus inject_locked(std::string_view operation) = 0; + SyntheticPointerApi api_; + HSYNTHETICPOINTERDEVICE device_ {}; + mutable std::mutex mutex_; + bool open_ = true; + void stop_repeat_thread() { if (repeat_thread_.joinable()) { repeat_thread_.request_stop(); @@ -1200,8 +1234,8 @@ namespace lvh::detail { return OperationStatus::failure(invalid_argument, "Windows touch contact id must not be negative"); } - std::lock_guard lock {mutex_}; - if (!open_) { + const auto lock = lock_device(); + if (!is_open_locked()) { return OperationStatus::failure(device_closed, "Windows touchscreen is closed"); } @@ -1259,8 +1293,8 @@ namespace lvh::detail { OperationStatus release_contact(std::int32_t contact_id, PointerTransition transition) override { using enum ErrorCode; - std::lock_guard lock {mutex_}; - if (!open_) { + const auto lock = lock_device(); + if (!is_open_locked()) { return OperationStatus::failure(device_closed, "Windows touchscreen is closed"); } @@ -1364,9 +1398,7 @@ namespace lvh::detail { } OperationStatus inject_locked(std::string_view operation) override { - return inject_synthetic_pointer_input( - api_, - device_, + return inject_synthetic_pointer_input_locked( contacts_.data(), static_cast(active_contacts_), operation @@ -1426,8 +1458,8 @@ namespace lvh::detail { OperationStatus place_tool(const PenToolState &state) override { using enum ErrorCode; - std::lock_guard lock {mutex_}; - if (!open_) { + const auto lock = lock_device(); + if (!is_open_locked()) { return OperationStatus::failure(device_closed, "Windows pen tablet is closed"); } @@ -1498,8 +1530,8 @@ namespace lvh::detail { OperationStatus button(PenButton /*button*/, bool pressed) override { using enum ErrorCode; - std::lock_guard lock {mutex_}; - if (!open_) { + const auto lock = lock_device(); + if (!is_open_locked()) { return OperationStatus::failure(device_closed, "Windows pen tablet is closed"); } @@ -1542,7 +1574,7 @@ namespace lvh::detail { } OperationStatus inject_locked(std::string_view operation) override { - return inject_synthetic_pointer_input(api_, device_, &pointer_, 1, operation); + return inject_synthetic_pointer_input_locked(&pointer_, 1, operation); } POINTER_TYPE_INFO pointer_ {};