feat(windows): add keyboard/mouse support and expose global UMDF control-device path (#34)

This commit is contained in:
Dave Lane authored and GitHub committed 2026-07-01 09:14:12 -04:00
1 parent 0d704b0c6d
commit f06302f841
11 files changed
+629 -34

No files matched your search

+10 -7
View File
@@ -99,13 +99,16 @@ be compiler-neutral: prefer a stable C ABI, named pipe, device interface IOCTL,
or similar control channel over passing C++ STL types across that boundary.
The current Windows backend selects a UMDF control-channel implementation for
`BackendKind::platform_default`. It probes `\\.\LibVirtualHid`, reports
`requires_installed_driver = true`, and only advertises gamepad/output-report
support when the driver package is installed and the control device can be
opened. The client library stays buildable with MSVC and MinGW/UCRT64 because
the backend talks to the driver through fixed-size C protocol structures and
Win32 `DeviceIoControl` calls. The default control device path can be overridden
for diagnostics with `LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE`.
`BackendKind::platform_default`. It always exposes keyboard and mouse through
Win32 `SendInput`, then probes `\\.\LibVirtualHid` for descriptor-driven virtual
gamepads. It reports `requires_installed_driver = true`, and only advertises
gamepad/output-report support when the driver package is installed and the
control device can be opened. Touchscreen, trackpad, and pen tablet support are
not implemented in the Windows backend yet. The client library stays buildable
with MSVC and MinGW/UCRT64 because the gamepad path talks to the driver through
fixed-size C protocol structures and Win32 `DeviceIoControl` calls. The default
control device path can be overridden for diagnostics with
`LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE`.
The UMDF driver uses Windows Virtual HID Framework (VHF) for OS-visible gamepad
devices. Create requests start a VHF child device from the requested descriptor,
+9 -2
View File
@@ -12,8 +12,15 @@ target_sources(${PROJECT_NAME}
if(LIBVIRTUALHID_USES_THREADS)
find_package(Threads REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBEVDEV REQUIRED IMPORTED_TARGET libevdev)
if(LIBEVDEV_CUSTOM_INCLUDE_DIR AND LIBEVDEV_CUSTOM_LIBRARY)
add_library(PkgConfig::LIBEVDEV UNKNOWN IMPORTED)
set_target_properties(PkgConfig::LIBEVDEV PROPERTIES
IMPORTED_LOCATION "${LIBEVDEV_CUSTOM_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${LIBEVDEV_CUSTOM_INCLUDE_DIR}")
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBEVDEV REQUIRED IMPORTED_TARGET libevdev)
endif()
if(LIBVIRTUALHID_ENABLE_XTEST)
find_package(X11 QUIET)
endif()
@@ -23,6 +23,7 @@
namespace lvh::detail::windows {
inline constexpr std::string_view default_control_device_path {LVH_WINDOWS_CONTROL_DEVICE_PATH};
inline constexpr std::string_view global_control_device_path {LVH_WINDOWS_GLOBAL_CONTROL_DEVICE_PATH};
inline std::uint32_t gamepad_flags(const GamepadProfileCapabilities &capabilities) {
std::uint32_t flags = 0;
@@ -53,6 +53,7 @@ EVT_VHF_ASYNC_OPERATION LvhEvtVhfWriteReport;
namespace {
constexpr auto symbolic_link_name = L"\\DosDevices\\LibVirtualHid";
constexpr auto global_symbolic_link_name = L"\\DosDevices\\Global\\LibVirtualHid";
struct DeviceRecord {
std::mutex mutex;
@@ -472,12 +473,15 @@ NTSTATUS LvhEvtDeviceAdd(WDFDRIVER driver, PWDFDEVICE_INIT device_init) {
}
UNICODE_STRING symbolic_link;
RtlInitUnicodeString(&symbolic_link, symbolic_link_name);
RtlInitUnicodeString(&symbolic_link, global_symbolic_link_name);
status = WdfDeviceCreateSymbolicLink(device, &symbolic_link);
if (!NT_SUCCESS(status)) {
return status;
}
RtlInitUnicodeString(&symbolic_link, symbolic_link_name);
static_cast<void>(WdfDeviceCreateSymbolicLink(device, &symbolic_link));
status = initialize_vhf_target(device);
if (!NT_SUCCESS(status)) {
return status;
@@ -10,6 +10,7 @@
inline constexpr uint32_t LVH_WINDOWS_CONTROL_PROTOCOL_VERSION = 1u;
inline constexpr char LVH_WINDOWS_CONTROL_DEVICE_PATH[] = R"(\\.\LibVirtualHid)";
inline constexpr char LVH_WINDOWS_GLOBAL_CONTROL_DEVICE_PATH[] = R"(\\.\Global\LibVirtualHid)";
inline constexpr uint32_t LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE = 1024u;
inline constexpr uint32_t LVH_WINDOWS_MAX_INPUT_REPORT_SIZE = 256u;
@@ -161,6 +162,7 @@ enum {
};
static const char LVH_WINDOWS_CONTROL_DEVICE_PATH[] = "\\\\.\\LibVirtualHid";
static const char LVH_WINDOWS_GLOBAL_CONTROL_DEVICE_PATH[] = "\\\\.\\Global\\LibVirtualHid";
static const uint32_t LVH_WINDOWS_IOCTL_CREATE_GAMEPAD = 0x8000E000u;
static const uint32_t LVH_WINDOWS_IOCTL_DESTROY_DEVICE = 0x8000E004u;
+249 -14
View File
@@ -17,10 +17,13 @@
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <sstream>
#include <string>
#include <string_view>
@@ -44,6 +47,20 @@ namespace lvh::detail {
class WindowsBackendContext;
using UniqueHandle = std::unique_ptr<void, decltype(&::CloseHandle)>;
using SendInputFunction = std::function<UINT(std::span<INPUT>)>;
UINT send_input_with_win32(std::span<INPUT> inputs) {
return ::SendInput(
static_cast<UINT>(inputs.size()),
inputs.data(),
static_cast<int>(sizeof(INPUT))
);
}
SendInputFunction &send_input_function() {
static SendInputFunction function = send_input_with_win32;
return function;
}
UniqueHandle make_unique_handle(HANDLE handle) {
return {handle, &::CloseHandle};
@@ -82,17 +99,80 @@ namespace lvh::detail {
return OperationStatus::failure(code, message.str());
}
std::string resolve_control_device_path() {
OperationStatus send_input(std::span<INPUT> inputs, std::string_view operation) {
using enum ErrorCode;
if (const auto sent = send_input_function()(inputs); sent != static_cast<UINT>(inputs.size())) {
return windows_failure(backend_failure, operation, ::GetLastError());
}
return OperationStatus::success();
}
OperationStatus send_input(const INPUT &input, std::string_view operation) {
std::array inputs {input};
return send_input(std::span<INPUT> {inputs}, operation);
}
DWORD mouse_button_flags(MouseButton button, bool pressed) {
switch (button) {
using enum MouseButton;
case left:
return pressed ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP;
case middle:
return pressed ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP;
case right:
return pressed ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP;
case side:
case extra:
return pressed ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP;
}
return 0;
}
DWORD mouse_button_data(MouseButton button) {
switch (button) {
using enum MouseButton;
case side:
return XBUTTON1;
case extra:
return XBUTTON2;
case left:
case middle:
case right:
return 0;
}
return 0;
}
LONG scale_absolute_axis(std::int32_t value, std::int32_t dimension) {
if (dimension <= 1) {
return 0;
}
const auto clamped = std::clamp(value, 0, dimension);
const auto numerator = static_cast<std::int64_t>(clamped) * std::numeric_limits<std::uint16_t>::max();
return static_cast<LONG>(numerator / dimension);
}
std::vector<std::string> 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) {
std::string path(required_size - 1U, '\0');
const auto copied_size = ::GetEnvironmentVariableA(environment_name, path.data(), required_size);
if (copied_size > 0U && copied_size < required_size) {
return path;
return {path};
}
}
return std::string {windows::default_control_device_path};
return {
std::string {windows::default_control_device_path},
std::string {windows::global_control_device_path},
};
}
OperationStatus protocol_status(std::uint32_t status, std::string_view operation) {
@@ -371,6 +451,16 @@ namespace lvh::detail {
UniqueHandle handle_;
};
std::unique_ptr<WindowsControlChannel> open_control_channel() {
for (const auto &path : resolve_control_device_paths()) {
if (auto channel = Win32WindowsControlChannel::open(path)) {
return channel;
}
}
return nullptr;
}
class WindowsGamepadState {
public:
WindowsGamepadState(
@@ -593,12 +683,147 @@ namespace lvh::detail {
return context_->close_gamepad(state_);
}
class WindowsKeyboard final: public BackendKeyboard {
public:
OperationStatus submit(const KeyboardEvent &event) override {
using enum ErrorCode;
if (!open_) {
return OperationStatus::failure(device_closed, "Windows keyboard is closed");
}
INPUT input {};
input.type = INPUT_KEYBOARD;
input.ki.wVk = event.key_code;
if (!event.pressed) {
input.ki.dwFlags = KEYEVENTF_KEYUP;
}
return send_input(input, "submit Windows keyboard input");
}
OperationStatus type_text(const KeyboardTextEvent &event) override {
using enum ErrorCode;
if (!open_) {
return OperationStatus::failure(device_closed, "Windows keyboard is closed");
}
if (event.text.empty()) {
return OperationStatus::success();
}
const auto required_size = ::MultiByteToWideChar(
CP_UTF8,
MB_ERR_INVALID_CHARS,
event.text.data(),
static_cast<int>(event.text.size()),
nullptr,
0
);
if (required_size <= 0) {
return windows_failure(invalid_argument, "convert UTF-8 text for Windows keyboard input", ::GetLastError());
}
std::vector<WCHAR> wide_text(static_cast<std::size_t>(required_size));
const auto converted_size = ::MultiByteToWideChar(
CP_UTF8,
MB_ERR_INVALID_CHARS,
event.text.data(),
static_cast<int>(event.text.size()),
wide_text.data(),
required_size
);
if (converted_size <= 0) {
return windows_failure(invalid_argument, "convert UTF-8 text for Windows keyboard input", ::GetLastError());
}
std::vector<INPUT> inputs;
inputs.reserve(static_cast<std::size_t>(converted_size) * 2U);
for (const auto character : wide_text) {
INPUT input {};
input.type = INPUT_KEYBOARD;
input.ki.wScan = character;
input.ki.dwFlags = KEYEVENTF_UNICODE;
inputs.push_back(input);
}
for (const auto character : wide_text) {
INPUT input {};
input.type = INPUT_KEYBOARD;
input.ki.wScan = character;
input.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
inputs.push_back(input);
}
return send_input(std::span<INPUT> {inputs}, "submit Windows keyboard text input");
}
OperationStatus close() override {
open_ = false;
return OperationStatus::success();
}
private:
bool open_ = true;
};
class WindowsMouse final: public BackendMouse {
public:
OperationStatus submit(const MouseEvent &event) override {
using enum ErrorCode;
if (!open_) {
return OperationStatus::failure(device_closed, "Windows mouse is closed");
}
INPUT input {};
input.type = INPUT_MOUSE;
auto &mouse = input.mi;
switch (event.kind) {
using enum MouseEventKind;
case relative_motion:
mouse.dwFlags = MOUSEEVENTF_MOVE;
mouse.dx = event.x;
mouse.dy = event.y;
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);
break;
case button:
mouse.dwFlags = mouse_button_flags(event.button, event.pressed);
mouse.mouseData = mouse_button_data(event.button);
break;
case vertical_scroll:
mouse.dwFlags = MOUSEEVENTF_WHEEL;
mouse.mouseData = static_cast<DWORD>(event.high_resolution_scroll);
break;
case horizontal_scroll:
mouse.dwFlags = MOUSEEVENTF_HWHEEL;
mouse.mouseData = static_cast<DWORD>(event.high_resolution_scroll);
break;
}
return send_input(input, "submit Windows mouse input");
}
OperationStatus close() override {
open_ = false;
return OperationStatus::success();
}
private:
bool open_ = true;
};
class WindowsBackend final: public Backend {
public:
WindowsBackend():
WindowsBackend(
Win32WindowsControlChannel::open(resolve_control_device_path()),
Win32WindowsControlChannel::open(resolve_control_device_path())
open_control_channel(),
open_control_channel()
) {}
WindowsBackend(
@@ -607,6 +832,8 @@ namespace lvh::detail {
) {
capabilities_.backend_name = "windows-umdf";
capabilities_.requires_installed_driver = true;
capabilities_.supports_keyboard = true;
capabilities_.supports_mouse = true;
if (!command_channel || !event_channel) {
return;
@@ -656,43 +883,51 @@ namespace lvh::detail {
BackendKeyboardCreationResult create_keyboard(
DeviceId /*id*/,
const CreateKeyboardOptions & /*options*/
const CreateKeyboardOptions &options
) override {
return {unsupported_device_status(), nullptr};
if (options.profile.device_type != DeviceType::keyboard) {
return {unsupported_device_status("Windows keyboard backend requires a keyboard profile"), nullptr};
}
return {OperationStatus::success(), std::make_unique<WindowsKeyboard>()};
}
BackendMouseCreationResult create_mouse(DeviceId /*id*/, const CreateMouseOptions & /*options*/) override {
return {unsupported_device_status(), nullptr};
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 {OperationStatus::success(), std::make_unique<WindowsMouse>()};
}
BackendTouchscreenCreationResult create_touchscreen(
DeviceId /*id*/,
const CreateTouchscreenOptions & /*options*/
) override {
return {unsupported_device_status(), nullptr};
return {unsupported_device_status("Windows backend currently supports gamepad, keyboard, and mouse devices only"), nullptr};
}
BackendTrackpadCreationResult create_trackpad(
DeviceId /*id*/,
const CreateTrackpadOptions & /*options*/
) override {
return {unsupported_device_status(), nullptr};
return {unsupported_device_status("Windows backend currently supports gamepad, keyboard, and mouse devices only"), nullptr};
}
BackendPenTabletCreationResult create_pen_tablet(
DeviceId /*id*/,
const CreatePenTabletOptions & /*options*/
) override {
return {unsupported_device_status(), nullptr};
return {unsupported_device_status("Windows backend currently supports gamepad, keyboard, and mouse devices only"), nullptr};
}
private:
static OperationStatus unsupported_device_status() {
static OperationStatus unsupported_device_status(std::string message) {
using enum ErrorCode;
return OperationStatus::failure(
unsupported_profile,
"Windows MVP backend currently supports gamepad devices only"
std::move(message)
);
}
@@ -37,21 +37,80 @@ namespace lvh::detail::test {
OperationStatus backend_failure_status;
OperationStatus transport_failure_status;
OperationStatus unavailable_status;
OperationStatus oversized_descriptor_status;
OperationStatus oversized_input_report_status;
OperationStatus oversized_output_report_status;
OperationStatus empty_nodes_create_status;
OperationStatus oversized_submit_status;
std::vector<DeviceNode> empty_device_nodes;
};
struct WindowsBackendUtilityResult {
std::string default_device_path;
std::string custom_device_path;
std::vector<std::string> default_device_paths;
std::vector<std::string> custom_device_paths;
OperationStatus formatted_error_status;
OperationStatus fallback_error_status;
OperationStatus keyboard_create_status;
OperationStatus keyboard_close_status;
OperationStatus keyboard_submit_after_close_status;
OperationStatus mouse_create_status;
OperationStatus mouse_close_status;
OperationStatus mouse_submit_after_close_status;
bool timeout_result = true;
};
struct WindowsSendInputRecord {
std::uint32_t type = 0;
std::uint16_t virtual_key = 0;
std::uint16_t scan_code = 0;
std::uint32_t key_flags = 0;
std::int32_t mouse_x = 0;
std::int32_t mouse_y = 0;
std::uint32_t mouse_data = 0;
std::uint32_t mouse_flags = 0;
};
struct WindowsKeyboardSendInputResult {
OperationStatus down_status;
OperationStatus up_status;
OperationStatus text_status;
OperationStatus empty_text_status;
OperationStatus invalid_text_status;
OperationStatus failure_status;
OperationStatus invalid_profile_status;
};
struct WindowsMouseSendInputResult {
OperationStatus relative_status;
OperationStatus absolute_status;
OperationStatus degenerate_absolute_status;
OperationStatus left_button_status;
OperationStatus middle_button_status;
OperationStatus right_button_status;
OperationStatus side_button_status;
OperationStatus extra_button_status;
OperationStatus vertical_scroll_status;
OperationStatus horizontal_scroll_status;
OperationStatus failure_status;
OperationStatus invalid_profile_status;
};
struct WindowsUnsupportedInputResult {
OperationStatus touchscreen_status;
OperationStatus trackpad_status;
OperationStatus pen_tablet_status;
};
struct WindowsBackendSendInputResult {
WindowsKeyboardSendInputResult keyboard;
WindowsMouseSendInputResult mouse;
WindowsUnsupportedInputResult unsupported;
std::vector<WindowsSendInputRecord> sent_inputs;
};
WindowsBackendLifecycleResult windows_backend_fake_channel_lifecycle();
WindowsBackendFailureResult windows_backend_fake_channel_failures();
WindowsBackendUtilityResult windows_backend_fake_channel_utilities();
WindowsBackendSendInputResult windows_backend_send_input_devices();
} // namespace lvh::detail::test
+183 -2
View File
@@ -230,6 +230,63 @@ namespace lvh::detail {
return backend->create_gamepad(1, options).status;
}
struct FakeSendInputState {
std::vector<test::WindowsSendInputRecord> sent_inputs;
std::optional<UINT> forced_sent_count;
};
test::WindowsSendInputRecord make_send_input_record(const INPUT &input) {
test::WindowsSendInputRecord record;
record.type = input.type;
if (input.type == INPUT_KEYBOARD) {
record.virtual_key = input.ki.wVk;
record.scan_code = input.ki.wScan;
record.key_flags = input.ki.dwFlags;
} else if (input.type == INPUT_MOUSE) {
record.mouse_x = input.mi.dx;
record.mouse_y = input.mi.dy;
record.mouse_data = input.mi.mouseData;
record.mouse_flags = input.mi.dwFlags;
}
return record;
}
UINT fake_send_input(FakeSendInputState &state, std::span<INPUT> inputs) {
for (const auto &input : inputs) {
state.sent_inputs.push_back(make_send_input_record(input));
}
if (state.forced_sent_count.has_value()) {
::SetLastError(ERROR_ACCESS_DENIED);
return *state.forced_sent_count;
}
return static_cast<UINT>(inputs.size());
}
class ScopedFakeSendInput {
public:
explicit ScopedFakeSendInput(FakeSendInputState &state) {
previous_function_ = send_input_function();
send_input_function() = [&state](std::span<INPUT> inputs) {
return fake_send_input(state, inputs);
};
}
ScopedFakeSendInput(const ScopedFakeSendInput &) = delete;
ScopedFakeSendInput &operator=(const ScopedFakeSendInput &) = delete;
ScopedFakeSendInput(ScopedFakeSendInput &&) noexcept = delete;
ScopedFakeSendInput &operator=(ScopedFakeSendInput &&) noexcept = delete;
~ScopedFakeSendInput() {
send_input_function() = std::move(previous_function_);
}
private:
SendInputFunction previous_function_ = send_input_with_win32;
};
} // namespace
namespace test {
@@ -304,6 +361,18 @@ namespace lvh::detail {
CreateGamepadOptions options;
options.profile = profiles::xbox_360();
result.unavailable_status = backend.create_gamepad(3, options).status;
auto oversized_descriptor_options = options;
oversized_descriptor_options.profile.report_descriptor.assign(LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE + 1U, 0x01);
result.oversized_descriptor_status = backend.create_gamepad(17, oversized_descriptor_options).status;
auto oversized_input_options = options;
oversized_input_options.profile.input_report_size = LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 1U;
result.oversized_input_report_status = backend.create_gamepad(18, oversized_input_options).status;
auto oversized_output_options = options;
oversized_output_options.profile.output_report_size = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE + 1U;
result.oversized_output_report_status = backend.create_gamepad(19, oversized_output_options).status;
}
{
@@ -326,7 +395,7 @@ namespace lvh::detail {
WindowsBackendUtilityResult windows_backend_fake_channel_utilities() {
WindowsBackendUtilityResult result;
result.default_device_path = resolve_control_device_path();
result.default_device_paths = resolve_control_device_paths();
constexpr auto environment_name = "LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE";
constexpr auto custom_path = R"(\\.\LibVirtualHid-Test)";
@@ -337,7 +406,7 @@ namespace lvh::detail {
static_cast<DWORD>(original_value.size())
);
static_cast<void>(::SetEnvironmentVariableA(environment_name, custom_path));
result.custom_device_path = resolve_control_device_path();
result.custom_device_paths = resolve_control_device_paths();
static_cast<void>(::SetEnvironmentVariableA(
environment_name,
original_size > 0U && original_size < original_value.size() ? original_value.data() : nullptr
@@ -348,6 +417,32 @@ namespace lvh::detail {
result.fallback_error_status =
windows_failure(ErrorCode::backend_failure, "format unknown Windows error", 0xE0000001U);
{
WindowsBackend backend {nullptr, nullptr};
CreateKeyboardOptions keyboard_options;
keyboard_options.profile = profiles::keyboard();
auto keyboard = backend.create_keyboard(8, keyboard_options);
result.keyboard_create_status = keyboard.status;
if (keyboard) {
result.keyboard_close_status = keyboard.keyboard->close();
result.keyboard_submit_after_close_status =
keyboard.keyboard->submit({.key_code = 0x41, .pressed = true});
}
CreateMouseOptions mouse_options;
mouse_options.profile = profiles::mouse();
auto mouse = backend.create_mouse(9, mouse_options);
result.mouse_create_status = mouse.status;
if (mouse) {
result.mouse_close_status = mouse.mouse->close();
MouseEvent mouse_event;
mouse_event.kind = MouseEventKind::relative_motion;
mouse_event.x = 1;
result.mouse_submit_after_close_status = mouse.mouse->submit(mouse_event);
}
}
auto invalid_context = std::make_shared<WindowsBackendContext>(
std::unique_ptr<WindowsControlChannel> {},
std::unique_ptr<WindowsControlChannel> {}
@@ -369,6 +464,92 @@ namespace lvh::detail {
return result;
}
WindowsBackendSendInputResult windows_backend_send_input_devices() {
using enum MouseEventKind;
WindowsBackendSendInputResult result;
WindowsBackend backend {nullptr, nullptr};
FakeSendInputState fake_send_input;
ScopedFakeSendInput scoped_send_input {fake_send_input};
CreateKeyboardOptions keyboard_options;
keyboard_options.profile = profiles::keyboard();
if (auto keyboard = backend.create_keyboard(10, keyboard_options); keyboard) {
result.keyboard.down_status = keyboard.keyboard->submit({.key_code = 0x41, .pressed = true});
result.keyboard.up_status = keyboard.keyboard->submit({.key_code = 0x41, .pressed = false});
result.keyboard.text_status = keyboard.keyboard->type_text({.text = "Az"});
result.keyboard.empty_text_status = keyboard.keyboard->type_text({.text = ""});
result.keyboard.invalid_text_status =
keyboard.keyboard->type_text({.text = std::string(1U, static_cast<char>(0xFF))});
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();
} else {
result.keyboard.down_status = keyboard.status;
}
CreateMouseOptions mouse_options;
mouse_options.profile = profiles::mouse();
if (auto mouse = backend.create_mouse(11, mouse_options); mouse) {
result.mouse.relative_status = mouse.mouse->submit({.kind = relative_motion, .x = 11, .y = -12});
result.mouse.absolute_status =
mouse.mouse->submit({.kind = absolute_motion, .x = 50, .y = 25, .width = 100, .height = 50});
result.mouse.degenerate_absolute_status =
mouse.mouse->submit({.kind = absolute_motion, .x = 99, .y = 99, .width = 1, .height = 0});
MouseEvent button_event;
button_event.kind = button;
button_event.button = MouseButton::left;
button_event.pressed = true;
result.mouse.left_button_status = mouse.mouse->submit(button_event);
button_event.button = MouseButton::middle;
button_event.pressed = false;
result.mouse.middle_button_status = mouse.mouse->submit(button_event);
button_event.button = MouseButton::right;
button_event.pressed = true;
result.mouse.right_button_status = mouse.mouse->submit(button_event);
button_event.button = MouseButton::side;
result.mouse.side_button_status = mouse.mouse->submit(button_event);
button_event.button = MouseButton::extra;
button_event.pressed = false;
result.mouse.extra_button_status = mouse.mouse->submit(button_event);
result.mouse.vertical_scroll_status = mouse.mouse->submit({.kind = vertical_scroll, .high_resolution_scroll = 120});
result.mouse.horizontal_scroll_status =
mouse.mouse->submit({.kind = horizontal_scroll, .high_resolution_scroll = -240});
fake_send_input.forced_sent_count = 0U;
result.mouse.failure_status = mouse.mouse->submit({.kind = relative_motion, .x = 1, .y = 1});
fake_send_input.forced_sent_count.reset();
} else {
result.mouse.relative_status = mouse.status;
}
CreateKeyboardOptions invalid_keyboard_options;
invalid_keyboard_options.profile = profiles::mouse();
result.keyboard.invalid_profile_status = backend.create_keyboard(12, invalid_keyboard_options).status;
CreateMouseOptions invalid_mouse_options;
invalid_mouse_options.profile = profiles::keyboard();
result.mouse.invalid_profile_status = backend.create_mouse(13, invalid_mouse_options).status;
CreateTouchscreenOptions touchscreen_options;
touchscreen_options.profile = profiles::touchscreen();
result.unsupported.touchscreen_status = backend.create_touchscreen(14, touchscreen_options).status;
CreateTrackpadOptions trackpad_options;
trackpad_options.profile = profiles::trackpad();
result.unsupported.trackpad_status = backend.create_trackpad(15, trackpad_options).status;
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;
result.sent_inputs = std::move(fake_send_input.sent_inputs);
return result;
}
} // namespace test
} // namespace lvh::detail
+8 -4
View File
@@ -45,14 +45,18 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) {
#elif defined(_WIN32)
EXPECT_EQ(runtime->capabilities().backend_name, "windows-umdf");
EXPECT_TRUE(runtime->capabilities().requires_installed_driver);
EXPECT_FALSE(runtime->capabilities().supports_keyboard);
EXPECT_FALSE(runtime->capabilities().supports_mouse);
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);
EXPECT_EQ(runtime->create_keyboard().status.code(), lvh::ErrorCode::unsupported_profile);
EXPECT_EQ(runtime->create_mouse().status.code(), lvh::ErrorCode::unsupported_profile);
auto keyboard = runtime->create_keyboard();
ASSERT_TRUE(keyboard) << keyboard.status.message();
EXPECT_TRUE(keyboard.keyboard->close().ok());
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);
EXPECT_EQ(runtime->create_trackpad().status.code(), lvh::ErrorCode::unsupported_profile);
EXPECT_EQ(runtime->create_pen_tablet().status.code(), lvh::ErrorCode::unsupported_profile);
+100 -2
View File
@@ -7,10 +7,24 @@
#include "fixtures/fixtures.hpp"
#include "fixtures/windows_backend_test_hooks.hpp"
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
// platform includes
#include <Windows.h>
namespace {
class WindowsBackendTest: public WindowsTest {};
void expect_ok(const lvh::OperationStatus &status) {
EXPECT_TRUE(status.ok()) << status.message();
}
} // namespace
TEST_F(WindowsBackendTest, FakeChannelExercisesLifecycleSubmitCloseAndOutput) {
@@ -20,6 +34,8 @@ TEST_F(WindowsBackendTest, FakeChannelExercisesLifecycleSubmitCloseAndOutput) {
EXPECT_TRUE(result.capabilities.requires_installed_driver);
EXPECT_TRUE(result.capabilities.supports_virtual_hid);
EXPECT_TRUE(result.capabilities.supports_gamepad);
EXPECT_TRUE(result.capabilities.supports_keyboard);
EXPECT_TRUE(result.capabilities.supports_mouse);
EXPECT_TRUE(result.capabilities.supports_output_reports);
ASSERT_TRUE(result.create_status.ok()) << result.create_status.message();
@@ -53,6 +69,9 @@ TEST_F(WindowsBackendTest, FakeChannelCoversCreateFailureBranches) {
EXPECT_EQ(result.backend_failure_status.code(), lvh::ErrorCode::backend_failure);
EXPECT_EQ(result.transport_failure_status.code(), lvh::ErrorCode::backend_failure);
EXPECT_EQ(result.unavailable_status.code(), lvh::ErrorCode::backend_unavailable);
EXPECT_EQ(result.oversized_descriptor_status.code(), lvh::ErrorCode::invalid_argument);
EXPECT_EQ(result.oversized_input_report_status.code(), lvh::ErrorCode::invalid_argument);
EXPECT_EQ(result.oversized_output_report_status.code(), lvh::ErrorCode::invalid_argument);
ASSERT_TRUE(result.empty_nodes_create_status.ok()) << result.empty_nodes_create_status.message();
EXPECT_TRUE(result.empty_device_nodes.empty());
@@ -62,8 +81,11 @@ TEST_F(WindowsBackendTest, FakeChannelCoversCreateFailureBranches) {
TEST_F(WindowsBackendTest, UtilityHookCoversEnvironmentErrorAndThreadBranches) {
const auto result = lvh::detail::test::windows_backend_fake_channel_utilities();
EXPECT_EQ(result.default_device_path, R"(\\.\LibVirtualHid)");
EXPECT_EQ(result.custom_device_path, R"(\\.\LibVirtualHid-Test)");
ASSERT_EQ(result.default_device_paths.size(), 2U);
EXPECT_EQ(result.default_device_paths[0], R"(\\.\LibVirtualHid)");
EXPECT_EQ(result.default_device_paths[1], R"(\\.\Global\LibVirtualHid)");
ASSERT_EQ(result.custom_device_paths.size(), 1U);
EXPECT_EQ(result.custom_device_paths[0], R"(\\.\LibVirtualHid-Test)");
EXPECT_EQ(result.formatted_error_status.code(), lvh::ErrorCode::backend_failure);
EXPECT_EQ(result.fallback_error_status.code(), lvh::ErrorCode::backend_failure);
EXPECT_NE(result.formatted_error_status.message().find("format known Windows error:"), std::string::npos);
@@ -71,5 +93,81 @@ TEST_F(WindowsBackendTest, UtilityHookCoversEnvironmentErrorAndThreadBranches) {
result.fallback_error_status.message().find("format unknown Windows error: Windows error 3758096385"),
std::string::npos
);
EXPECT_TRUE(result.keyboard_create_status.ok()) << result.keyboard_create_status.message();
EXPECT_TRUE(result.keyboard_close_status.ok()) << result.keyboard_close_status.message();
EXPECT_EQ(result.keyboard_submit_after_close_status.code(), lvh::ErrorCode::device_closed);
EXPECT_TRUE(result.mouse_create_status.ok()) << result.mouse_create_status.message();
EXPECT_TRUE(result.mouse_close_status.ok()) << result.mouse_close_status.message();
EXPECT_EQ(result.mouse_submit_after_close_status.code(), lvh::ErrorCode::device_closed);
EXPECT_FALSE(result.timeout_result);
}
TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsupportedProfiles) {
const auto result = lvh::detail::test::windows_backend_send_input_devices();
expect_ok(result.keyboard.down_status);
expect_ok(result.keyboard.up_status);
expect_ok(result.keyboard.text_status);
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.mouse.relative_status);
expect_ok(result.mouse.absolute_status);
expect_ok(result.mouse.degenerate_absolute_status);
expect_ok(result.mouse.left_button_status);
expect_ok(result.mouse.middle_button_status);
expect_ok(result.mouse.right_button_status);
expect_ok(result.mouse.side_button_status);
expect_ok(result.mouse.extra_button_status);
expect_ok(result.mouse.vertical_scroll_status);
expect_ok(result.mouse.horizontal_scroll_status);
EXPECT_EQ(result.mouse.failure_status.code(), lvh::ErrorCode::backend_failure);
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);
ASSERT_EQ(result.sent_inputs.size(), 18U);
EXPECT_EQ(result.sent_inputs[0].type, INPUT_KEYBOARD);
EXPECT_EQ(result.sent_inputs[0].virtual_key, 0x41U);
EXPECT_EQ(result.sent_inputs[0].key_flags, 0U);
EXPECT_EQ(result.sent_inputs[1].virtual_key, 0x41U);
EXPECT_EQ(result.sent_inputs[1].key_flags, KEYEVENTF_KEYUP);
EXPECT_EQ(result.sent_inputs[2].scan_code, static_cast<std::uint16_t>(L'A'));
EXPECT_EQ(result.sent_inputs[2].key_flags, KEYEVENTF_UNICODE);
EXPECT_EQ(result.sent_inputs[3].scan_code, static_cast<std::uint16_t>(L'z'));
EXPECT_EQ(result.sent_inputs[3].key_flags, KEYEVENTF_UNICODE);
EXPECT_EQ(result.sent_inputs[4].scan_code, static_cast<std::uint16_t>(L'A'));
EXPECT_EQ(result.sent_inputs[4].key_flags, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP);
EXPECT_EQ(result.sent_inputs[5].scan_code, static_cast<std::uint16_t>(L'z'));
EXPECT_EQ(result.sent_inputs[5].key_flags, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP);
EXPECT_EQ(result.sent_inputs[6].virtual_key, 0x42U);
EXPECT_EQ(result.sent_inputs[7].type, INPUT_MOUSE);
EXPECT_EQ(result.sent_inputs[7].mouse_flags, MOUSEEVENTF_MOVE);
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[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);
EXPECT_EQ(result.sent_inputs[10].mouse_flags, MOUSEEVENTF_LEFTDOWN);
EXPECT_EQ(result.sent_inputs[11].mouse_flags, MOUSEEVENTF_MIDDLEUP);
EXPECT_EQ(result.sent_inputs[12].mouse_flags, MOUSEEVENTF_RIGHTDOWN);
EXPECT_EQ(result.sent_inputs[13].mouse_flags, MOUSEEVENTF_XDOWN);
EXPECT_EQ(result.sent_inputs[13].mouse_data, XBUTTON1);
EXPECT_EQ(result.sent_inputs[14].mouse_flags, MOUSEEVENTF_XUP);
EXPECT_EQ(result.sent_inputs[14].mouse_data, XBUTTON2);
EXPECT_EQ(result.sent_inputs[15].mouse_flags, MOUSEEVENTF_WHEEL);
EXPECT_EQ(result.sent_inputs[15].mouse_data, 120U);
EXPECT_EQ(result.sent_inputs[16].mouse_flags, MOUSEEVENTF_HWHEEL);
EXPECT_EQ(result.sent_inputs[16].mouse_data, static_cast<std::uint32_t>(static_cast<DWORD>(-240)));
EXPECT_EQ(result.sent_inputs[17].mouse_flags, MOUSEEVENTF_MOVE);
EXPECT_EQ(result.sent_inputs[17].mouse_x, 1);
EXPECT_EQ(result.sent_inputs[17].mouse_y, 1);
}
+1
View File
@@ -39,6 +39,7 @@ namespace {
TEST(WindowsProtocolTest, ExposesStableProtocolConstants) {
EXPECT_STREQ(lvh::detail::windows::default_control_device_path.data(), R"(\\.\LibVirtualHid)");
EXPECT_STREQ(lvh::detail::windows::global_control_device_path.data(), R"(\\.\Global\LibVirtualHid)");
EXPECT_EQ(LVH_WINDOWS_IOCTL_CREATE_GAMEPAD, 0x8000E000U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_DESTROY_DEVICE, 0x8000E004U);