diff --git a/docs/windows-driver.md b/docs/windows-driver.md index 63f17de..70e0dd9 100644 --- a/docs/windows-driver.md +++ b/docs/windows-driver.md @@ -35,6 +35,10 @@ backend asks the broker service to create and destroy gamepads through a local named pipe, while input reports stay on the direct driver path after creation. This keeps license and active-device checks outside the input hot path. +The UMDF service runs in a dedicated high-priority host process, as recommended +for response-sensitive input drivers. This isolates its VHF input work from +normal-priority UMDF device pools while keeping the driver entirely user-mode. + The broker pipe explicitly grants local authenticated users generic read access plus the individual data-write and attribute-write rights needed to exchange request and response messages in message mode. It does not grant clients the @@ -88,7 +92,11 @@ output read. Broker protocol version 2 preserves that association by duplicating the handle only for the authorized create IOCTL. The driver associates output events with that file object, so feedback from a virtual gamepad is delivered only to the runtime that created it instead of being consumed by another -libvirtualhid client. +libvirtualhid client. Because the shared handle is opened for overlapped I/O, +command IOCTLs also supply a valid `OVERLAPPED` event and explicitly wait for +pending completion instead of mixing synchronous calls with an asynchronous +handle. Each caller thread reuses its event to avoid creating a kernel handle +for every input report. The driver opens a separate VHF source target for each virtual gamepad and parents that target to the control-file handle that created it. If the creating diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in index 005e1ae..4817dbf 100644 --- a/src/platform/windows/driver/libvirtualhid.inf.in +++ b/src/platform/windows/driver/libvirtualhid.inf.in @@ -62,6 +62,7 @@ UmdfServiceOrder=libvirtualhid_umdf [libvirtualhid_umdf_Install] UmdfLibraryVersion=@LIBVIRTUALHID_UMDF_LIBRARY_VERSION@ UmdfHostProcessSharing=ProcessSharingDisabled +UmdfHostPriority=PriorityHigh ServiceBinary=%13%\libvirtualhid_umdf.dll [Strings] diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index ba45163..d129fa4 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -221,6 +221,58 @@ namespace lvh::detail { return OperationStatus::failure(code, message.str()); } + UniqueHandle &overlapped_device_io_event() { + thread_local UniqueHandle operation_event {nullptr, &::CloseHandle}; + if (!operation_event) { + operation_event = make_unique_handle(::CreateEventA(nullptr, TRUE, FALSE, nullptr)); + } + + return operation_event; + } + + template + void cancel_and_drain_overlapped_io( + OVERLAPPED &overlapped, + DWORD *bytes_returned, + CancelOperation &&cancel_operation, + FinishOperation &&finish_operation + ) { + static_cast(std::forward(cancel_operation)(overlapped)); + static_cast(std::forward(finish_operation)(overlapped, bytes_returned, TRUE)); + } + + template + OperationStatus run_overlapped_device_io( + std::string_view operation, + DWORD *bytes_returned, + StartOperation &&start_operation, + FinishOperation &&finish_operation + ) { + const auto &operation_event = overlapped_device_io_event(); + if (!operation_event) { + return windows_failure(ErrorCode::backend_failure, operation, ::GetLastError()); + } + if (::ResetEvent(operation_event.get()) == FALSE) { + return windows_failure(ErrorCode::backend_failure, operation, ::GetLastError()); + } + + OVERLAPPED overlapped {}; + overlapped.hEvent = operation_event.get(); + if (std::forward(start_operation)(overlapped, bytes_returned) != FALSE) { + return OperationStatus::success(); + } + + if (const auto start_error = ::GetLastError(); start_error != ERROR_IO_PENDING) { + return windows_failure(ErrorCode::backend_failure, operation, start_error); + } + + if (std::forward(finish_operation)(overlapped, bytes_returned, TRUE) == FALSE) { + return windows_failure(ErrorCode::backend_failure, operation, ::GetLastError()); + } + + return OperationStatus::success(); + } + template OperationStatus submit_with_desktop_retry(Submit submit, std::string_view operation) { using enum ErrorCode; @@ -621,6 +673,18 @@ namespace lvh::detail { OVERLAPPED overlapped {}; overlapped.hEvent = operation_event.get(); DWORD bytes_returned = 0; + const auto cancel_and_drain = [this, &overlapped, &bytes_returned] { + cancel_and_drain_overlapped_io( + overlapped, + &bytes_returned, + [this](OVERLAPPED &pending) { + return ::CancelIoEx(handle_->value.get(), &pending); + }, + [this](OVERLAPPED &pending, DWORD *result_size, BOOL wait) { + return ::GetOverlappedResult(handle_->value.get(), &pending, result_size, wait); + } + ); + }; if (const auto started = ::DeviceIoControl(handle_->value.get(), LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT, nullptr, 0, &event, sizeof(event), &bytes_returned, &overlapped); started == FALSE) { if (const auto error_code = ::GetLastError(); error_code != ERROR_IO_PENDING) { @@ -638,11 +702,11 @@ namespace lvh::detail { INFINITE ); if (wait_result == WAIT_OBJECT_0 + 1U) { - static_cast(::CancelIoEx(handle_->value.get(), &overlapped)); + cancel_and_drain(); return std::nullopt; } if (wait_result != WAIT_OBJECT_0) { - static_cast(::CancelIoEx(handle_->value.get(), &overlapped)); + cancel_and_drain(); return std::nullopt; } } @@ -672,13 +736,25 @@ namespace lvh::detail { DWORD *bytes_returned, std::string_view operation ) const { - using enum ErrorCode; - - if (::DeviceIoControl(handle_->value.get(), control_code, &input, sizeof(input), &output, sizeof(output), bytes_returned, nullptr) == FALSE) { - return windows_failure(backend_failure, operation, ::GetLastError()); - } - - return OperationStatus::success(); + return run_overlapped_device_io( + operation, + bytes_returned, + [this, control_code, &input, &output](OVERLAPPED &overlapped, DWORD *result_size) { + return ::DeviceIoControl( + handle_->value.get(), + control_code, + &input, + sizeof(input), + &output, + sizeof(output), + result_size, + &overlapped + ); + }, + [this](OVERLAPPED &overlapped, DWORD *result_size, BOOL wait) { + return ::GetOverlappedResult(handle_->value.get(), &overlapped, result_size, wait); + } + ); } template @@ -688,13 +764,25 @@ namespace lvh::detail { DWORD *bytes_returned, std::string_view operation ) const { - using enum ErrorCode; - - if (::DeviceIoControl(handle_->value.get(), control_code, &input, sizeof(input), nullptr, 0, bytes_returned, nullptr) == FALSE) { - return windows_failure(backend_failure, operation, ::GetLastError()); - } - - return OperationStatus::success(); + return run_overlapped_device_io( + operation, + bytes_returned, + [this, control_code, &input](OVERLAPPED &overlapped, DWORD *result_size) { + return ::DeviceIoControl( + handle_->value.get(), + control_code, + &input, + sizeof(input), + nullptr, + 0, + result_size, + &overlapped + ); + }, + [this](OVERLAPPED &overlapped, DWORD *result_size, BOOL wait) { + return ::GetOverlappedResult(handle_->value.get(), &overlapped, result_size, wait); + } + ); } std::string path_; diff --git a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp index 3dcb58f..4967caa 100644 --- a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp @@ -65,6 +65,22 @@ namespace lvh::detail::test { bool timeout_result = true; }; + struct WindowsOverlappedIoResult { + OperationStatus immediate_status; + OperationStatus pending_status; + OperationStatus start_failure_status; + OperationStatus completion_failure_status; + bool immediate_saw_event = false; + bool immediate_called_completion = false; + bool pending_saw_event = false; + bool pending_waited = false; + bool cancellation_called = false; + bool cancellation_drained_after_cancel = false; + bool cancellation_waited = false; + std::uint32_t immediate_bytes_returned = 0; + std::uint32_t pending_bytes_returned = 0; + }; + struct WindowsSendInputRecord { std::uint32_t type = 0; std::uint16_t virtual_key = 0; @@ -151,6 +167,7 @@ namespace lvh::detail::test { WindowsGenericPidOrderingResult windows_backend_generic_pid_callback_ordering(); WindowsBackendFailureResult windows_backend_fake_channel_failures(); WindowsBackendUtilityResult windows_backend_fake_channel_utilities(); + WindowsOverlappedIoResult windows_backend_overlapped_device_io(); WindowsBackendSendInputResult windows_backend_send_input_devices(); } // namespace lvh::detail::test diff --git a/tests/fixtures/windows_backend_test_hooks.cpp b/tests/fixtures/windows_backend_test_hooks.cpp index 55f44f3..e7381f6 100644 --- a/tests/fixtures/windows_backend_test_hooks.cpp +++ b/tests/fixtures/windows_backend_test_hooks.cpp @@ -732,6 +732,86 @@ namespace lvh::detail { return result; } + WindowsOverlappedIoResult windows_backend_overlapped_device_io() { + WindowsOverlappedIoResult result; + + DWORD bytes_returned = 0; + result.immediate_status = run_overlapped_device_io( + "complete immediate test operation", + &bytes_returned, + [&result](const OVERLAPPED &overlapped, DWORD *result_size) { + result.immediate_saw_event = overlapped.hEvent != nullptr; + *result_size = 7U; + return TRUE; + }, + [&result](OVERLAPPED &, DWORD *, BOOL) { + result.immediate_called_completion = true; + return TRUE; + } + ); + result.immediate_bytes_returned = bytes_returned; + + bytes_returned = 0; + result.pending_status = run_overlapped_device_io( + "complete pending test operation", + &bytes_returned, + [&result](const OVERLAPPED &overlapped, DWORD *) { + result.pending_saw_event = overlapped.hEvent != nullptr; + ::SetLastError(ERROR_IO_PENDING); + return FALSE; + }, + [&result](OVERLAPPED &, DWORD *result_size, BOOL wait) { + result.pending_waited = wait != FALSE; + *result_size = 11U; + return TRUE; + } + ); + result.pending_bytes_returned = bytes_returned; + + OVERLAPPED canceled {}; + cancel_and_drain_overlapped_io( + canceled, + &bytes_returned, + [&result](OVERLAPPED &) { + result.cancellation_called = true; + return TRUE; + }, + [&result](OVERLAPPED &, DWORD *, BOOL wait) { + result.cancellation_drained_after_cancel = result.cancellation_called; + result.cancellation_waited = wait != FALSE; + ::SetLastError(ERROR_OPERATION_ABORTED); + return FALSE; + } + ); + + result.start_failure_status = run_overlapped_device_io( + "fail test operation start", + &bytes_returned, + [](OVERLAPPED &, DWORD *) { + ::SetLastError(ERROR_ACCESS_DENIED); + return FALSE; + }, + [](OVERLAPPED &, DWORD *, BOOL) { + return TRUE; + } + ); + + result.completion_failure_status = run_overlapped_device_io( + "fail pending test operation", + &bytes_returned, + [](OVERLAPPED &, DWORD *) { + ::SetLastError(ERROR_IO_PENDING); + return FALSE; + }, + [](OVERLAPPED &, DWORD *, BOOL) { + ::SetLastError(ERROR_OPERATION_ABORTED); + return FALSE; + } + ); + + return result; + } + WindowsBackendSendInputResult windows_backend_send_input_devices() { using enum MouseEventKind; diff --git a/tests/unit/test_windows_backend.cpp b/tests/unit/test_windows_backend.cpp index fd1f916..993a710 100644 --- a/tests/unit/test_windows_backend.cpp +++ b/tests/unit/test_windows_backend.cpp @@ -147,6 +147,27 @@ TEST_F(WindowsBackendTest, UtilityHookCoversEnvironmentErrorAndThreadBranches) { EXPECT_FALSE(result.timeout_result); } +TEST_F(WindowsBackendTest, OverlappedDeviceIoUsesAnEventAndWaitsForPendingCompletion) { + const auto result = lvh::detail::test::windows_backend_overlapped_device_io(); + + expect_ok(result.immediate_status); + EXPECT_TRUE(result.immediate_saw_event); + EXPECT_FALSE(result.immediate_called_completion); + EXPECT_EQ(result.immediate_bytes_returned, 7U); + + expect_ok(result.pending_status); + EXPECT_TRUE(result.pending_saw_event); + EXPECT_TRUE(result.pending_waited); + EXPECT_EQ(result.pending_bytes_returned, 11U); + + EXPECT_TRUE(result.cancellation_called); + EXPECT_TRUE(result.cancellation_drained_after_cancel); + EXPECT_TRUE(result.cancellation_waited); + + EXPECT_EQ(result.start_failure_status.code(), lvh::ErrorCode::backend_failure); + EXPECT_EQ(result.completion_failure_status.code(), lvh::ErrorCode::backend_failure); +} + TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsupportedProfiles) { const auto result = lvh::detail::test::windows_backend_send_input_devices();