Skip to content

Commit 117301c

Browse files
fix(Windows): use overlapped IO for backend IOCTLs
Switch Windows backend DeviceIoControl command paths to a shared per-thread OVERLAPPED event flow, correctly handling immediate completion, ERROR_IO_PENDING, and completion failures on async handles. Add fixture/unit coverage for event usage and pending waits, and update UMDF packaging/docs to run in a dedicated high-priority host process and document the overlapped command behavior.
1 parent 0c9624b commit 117301c

6 files changed

Lines changed: 232 additions & 17 deletions

File tree

docs/windows-driver.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ backend asks the broker service to create and destroy gamepads through a local
3535
named pipe, while input reports stay on the direct driver path after creation.
3636
This keeps license and active-device checks outside the input hot path.
3737

38+
The UMDF service runs in a dedicated high-priority host process, as recommended
39+
for response-sensitive input drivers. This isolates its VHF input work from
40+
normal-priority UMDF device pools while keeping the driver entirely user-mode.
41+
3842
The broker pipe explicitly grants local authenticated users generic read access
3943
plus the individual data-write and attribute-write rights needed to exchange
4044
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
8892
the handle only for the authorized create IOCTL. The driver associates output
8993
events with that file object, so feedback from a virtual gamepad is delivered
9094
only to the runtime that created it instead of being consumed by another
91-
libvirtualhid client.
95+
libvirtualhid client. Because the shared handle is opened for overlapped I/O,
96+
command IOCTLs also supply a valid `OVERLAPPED` event and explicitly wait for
97+
pending completion instead of mixing synchronous calls with an asynchronous
98+
handle. Each caller thread reuses its event to avoid creating a kernel handle
99+
for every input report.
92100

93101
The driver opens a separate VHF source target for each virtual gamepad and
94102
parents that target to the control-file handle that created it. If the creating

src/platform/windows/driver/libvirtualhid.inf.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ UmdfServiceOrder=libvirtualhid_umdf
6262
[libvirtualhid_umdf_Install]
6363
UmdfLibraryVersion=@LIBVIRTUALHID_UMDF_LIBRARY_VERSION@
6464
UmdfHostProcessSharing=ProcessSharingDisabled
65+
UmdfHostPriority=PriorityHigh
6566
ServiceBinary=%13%\libvirtualhid_umdf.dll
6667

6768
[Strings]

src/platform/windows/windows_backend.cpp

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,58 @@ namespace lvh::detail {
221221
return OperationStatus::failure(code, message.str());
222222
}
223223

224+
UniqueHandle &overlapped_device_io_event() {
225+
thread_local UniqueHandle operation_event {nullptr, &::CloseHandle};
226+
if (!operation_event) {
227+
operation_event = make_unique_handle(::CreateEventA(nullptr, TRUE, FALSE, nullptr));
228+
}
229+
230+
return operation_event;
231+
}
232+
233+
template<typename CancelOperation, typename FinishOperation>
234+
void cancel_and_drain_overlapped_io(
235+
OVERLAPPED &overlapped,
236+
DWORD *bytes_returned,
237+
CancelOperation &&cancel_operation,
238+
FinishOperation &&finish_operation
239+
) {
240+
static_cast<void>(std::forward<CancelOperation>(cancel_operation)(overlapped));
241+
static_cast<void>(std::forward<FinishOperation>(finish_operation)(overlapped, bytes_returned, TRUE));
242+
}
243+
244+
template<typename StartOperation, typename FinishOperation>
245+
OperationStatus run_overlapped_device_io(
246+
std::string_view operation,
247+
DWORD *bytes_returned,
248+
StartOperation &&start_operation,
249+
FinishOperation &&finish_operation
250+
) {
251+
const auto &operation_event = overlapped_device_io_event();
252+
if (!operation_event) {
253+
return windows_failure(ErrorCode::backend_failure, operation, ::GetLastError());
254+
}
255+
if (::ResetEvent(operation_event.get()) == FALSE) {
256+
return windows_failure(ErrorCode::backend_failure, operation, ::GetLastError());
257+
}
258+
259+
OVERLAPPED overlapped {};
260+
overlapped.hEvent = operation_event.get();
261+
if (std::forward<StartOperation>(start_operation)(overlapped, bytes_returned) != FALSE) {
262+
return OperationStatus::success();
263+
}
264+
265+
if (const auto start_error = ::GetLastError(); start_error != ERROR_IO_PENDING) {
266+
return windows_failure(ErrorCode::backend_failure, operation, start_error);
267+
}
268+
269+
if (std::forward<FinishOperation>(finish_operation)(overlapped, bytes_returned, TRUE) == FALSE) {
270+
return windows_failure(ErrorCode::backend_failure, operation, ::GetLastError());
271+
}
272+
273+
return OperationStatus::success();
274+
}
275+
224276
template<typename Submit>
225277
OperationStatus submit_with_desktop_retry(Submit submit, std::string_view operation) {
226278
using enum ErrorCode;
@@ -621,6 +673,18 @@ namespace lvh::detail {
621673
OVERLAPPED overlapped {};
622674
overlapped.hEvent = operation_event.get();
623675
DWORD bytes_returned = 0;
676+
const auto cancel_and_drain = [this, &overlapped, &bytes_returned] {
677+
cancel_and_drain_overlapped_io(
678+
overlapped,
679+
&bytes_returned,
680+
[this](OVERLAPPED &pending) {
681+
return ::CancelIoEx(handle_->value.get(), &pending);
682+
},
683+
[this](OVERLAPPED &pending, DWORD *result_size, BOOL wait) {
684+
return ::GetOverlappedResult(handle_->value.get(), &pending, result_size, wait);
685+
}
686+
);
687+
};
624688

625689
if (const auto started = ::DeviceIoControl(handle_->value.get(), LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT, nullptr, 0, &event, sizeof(event), &bytes_returned, &overlapped); started == FALSE) {
626690
if (const auto error_code = ::GetLastError(); error_code != ERROR_IO_PENDING) {
@@ -638,11 +702,11 @@ namespace lvh::detail {
638702
INFINITE
639703
);
640704
if (wait_result == WAIT_OBJECT_0 + 1U) {
641-
static_cast<void>(::CancelIoEx(handle_->value.get(), &overlapped));
705+
cancel_and_drain();
642706
return std::nullopt;
643707
}
644708
if (wait_result != WAIT_OBJECT_0) {
645-
static_cast<void>(::CancelIoEx(handle_->value.get(), &overlapped));
709+
cancel_and_drain();
646710
return std::nullopt;
647711
}
648712
}
@@ -672,13 +736,25 @@ namespace lvh::detail {
672736
DWORD *bytes_returned,
673737
std::string_view operation
674738
) const {
675-
using enum ErrorCode;
676-
677-
if (::DeviceIoControl(handle_->value.get(), control_code, &input, sizeof(input), &output, sizeof(output), bytes_returned, nullptr) == FALSE) {
678-
return windows_failure(backend_failure, operation, ::GetLastError());
679-
}
680-
681-
return OperationStatus::success();
739+
return run_overlapped_device_io(
740+
operation,
741+
bytes_returned,
742+
[this, control_code, &input, &output](OVERLAPPED &overlapped, DWORD *result_size) {
743+
return ::DeviceIoControl(
744+
handle_->value.get(),
745+
control_code,
746+
&input,
747+
sizeof(input),
748+
&output,
749+
sizeof(output),
750+
result_size,
751+
&overlapped
752+
);
753+
},
754+
[this](OVERLAPPED &overlapped, DWORD *result_size, BOOL wait) {
755+
return ::GetOverlappedResult(handle_->value.get(), &overlapped, result_size, wait);
756+
}
757+
);
682758
}
683759

684760
template<typename Input>
@@ -688,13 +764,25 @@ namespace lvh::detail {
688764
DWORD *bytes_returned,
689765
std::string_view operation
690766
) const {
691-
using enum ErrorCode;
692-
693-
if (::DeviceIoControl(handle_->value.get(), control_code, &input, sizeof(input), nullptr, 0, bytes_returned, nullptr) == FALSE) {
694-
return windows_failure(backend_failure, operation, ::GetLastError());
695-
}
696-
697-
return OperationStatus::success();
767+
return run_overlapped_device_io(
768+
operation,
769+
bytes_returned,
770+
[this, control_code, &input](OVERLAPPED &overlapped, DWORD *result_size) {
771+
return ::DeviceIoControl(
772+
handle_->value.get(),
773+
control_code,
774+
&input,
775+
sizeof(input),
776+
nullptr,
777+
0,
778+
result_size,
779+
&overlapped
780+
);
781+
},
782+
[this](OVERLAPPED &overlapped, DWORD *result_size, BOOL wait) {
783+
return ::GetOverlappedResult(handle_->value.get(), &overlapped, result_size, wait);
784+
}
785+
);
698786
}
699787

700788
std::string path_;

tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ namespace lvh::detail::test {
6565
bool timeout_result = true;
6666
};
6767

68+
struct WindowsOverlappedIoResult {
69+
OperationStatus immediate_status;
70+
OperationStatus pending_status;
71+
OperationStatus start_failure_status;
72+
OperationStatus completion_failure_status;
73+
bool immediate_saw_event = false;
74+
bool immediate_called_completion = false;
75+
bool pending_saw_event = false;
76+
bool pending_waited = false;
77+
bool cancellation_called = false;
78+
bool cancellation_drained_after_cancel = false;
79+
bool cancellation_waited = false;
80+
std::uint32_t immediate_bytes_returned = 0;
81+
std::uint32_t pending_bytes_returned = 0;
82+
};
83+
6884
struct WindowsSendInputRecord {
6985
std::uint32_t type = 0;
7086
std::uint16_t virtual_key = 0;
@@ -151,6 +167,7 @@ namespace lvh::detail::test {
151167
WindowsGenericPidOrderingResult windows_backend_generic_pid_callback_ordering();
152168
WindowsBackendFailureResult windows_backend_fake_channel_failures();
153169
WindowsBackendUtilityResult windows_backend_fake_channel_utilities();
170+
WindowsOverlappedIoResult windows_backend_overlapped_device_io();
154171
WindowsBackendSendInputResult windows_backend_send_input_devices();
155172

156173
} // namespace lvh::detail::test

tests/fixtures/windows_backend_test_hooks.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,86 @@ namespace lvh::detail {
732732
return result;
733733
}
734734

735+
WindowsOverlappedIoResult windows_backend_overlapped_device_io() {
736+
WindowsOverlappedIoResult result;
737+
738+
DWORD bytes_returned = 0;
739+
result.immediate_status = run_overlapped_device_io(
740+
"complete immediate test operation",
741+
&bytes_returned,
742+
[&result](const OVERLAPPED &overlapped, DWORD *result_size) {
743+
result.immediate_saw_event = overlapped.hEvent != nullptr;
744+
*result_size = 7U;
745+
return TRUE;
746+
},
747+
[&result](OVERLAPPED &, DWORD *, BOOL) {
748+
result.immediate_called_completion = true;
749+
return TRUE;
750+
}
751+
);
752+
result.immediate_bytes_returned = bytes_returned;
753+
754+
bytes_returned = 0;
755+
result.pending_status = run_overlapped_device_io(
756+
"complete pending test operation",
757+
&bytes_returned,
758+
[&result](const OVERLAPPED &overlapped, DWORD *) {
759+
result.pending_saw_event = overlapped.hEvent != nullptr;
760+
::SetLastError(ERROR_IO_PENDING);
761+
return FALSE;
762+
},
763+
[&result](OVERLAPPED &, DWORD *result_size, BOOL wait) {
764+
result.pending_waited = wait != FALSE;
765+
*result_size = 11U;
766+
return TRUE;
767+
}
768+
);
769+
result.pending_bytes_returned = bytes_returned;
770+
771+
OVERLAPPED canceled {};
772+
cancel_and_drain_overlapped_io(
773+
canceled,
774+
&bytes_returned,
775+
[&result](OVERLAPPED &) {
776+
result.cancellation_called = true;
777+
return TRUE;
778+
},
779+
[&result](OVERLAPPED &, DWORD *, BOOL wait) {
780+
result.cancellation_drained_after_cancel = result.cancellation_called;
781+
result.cancellation_waited = wait != FALSE;
782+
::SetLastError(ERROR_OPERATION_ABORTED);
783+
return FALSE;
784+
}
785+
);
786+
787+
result.start_failure_status = run_overlapped_device_io(
788+
"fail test operation start",
789+
&bytes_returned,
790+
[](OVERLAPPED &, DWORD *) {
791+
::SetLastError(ERROR_ACCESS_DENIED);
792+
return FALSE;
793+
},
794+
[](OVERLAPPED &, DWORD *, BOOL) {
795+
return TRUE;
796+
}
797+
);
798+
799+
result.completion_failure_status = run_overlapped_device_io(
800+
"fail pending test operation",
801+
&bytes_returned,
802+
[](OVERLAPPED &, DWORD *) {
803+
::SetLastError(ERROR_IO_PENDING);
804+
return FALSE;
805+
},
806+
[](OVERLAPPED &, DWORD *, BOOL) {
807+
::SetLastError(ERROR_OPERATION_ABORTED);
808+
return FALSE;
809+
}
810+
);
811+
812+
return result;
813+
}
814+
735815
WindowsBackendSendInputResult windows_backend_send_input_devices() {
736816
using enum MouseEventKind;
737817

tests/unit/test_windows_backend.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,27 @@ TEST_F(WindowsBackendTest, UtilityHookCoversEnvironmentErrorAndThreadBranches) {
147147
EXPECT_FALSE(result.timeout_result);
148148
}
149149

150+
TEST_F(WindowsBackendTest, OverlappedDeviceIoUsesAnEventAndWaitsForPendingCompletion) {
151+
const auto result = lvh::detail::test::windows_backend_overlapped_device_io();
152+
153+
expect_ok(result.immediate_status);
154+
EXPECT_TRUE(result.immediate_saw_event);
155+
EXPECT_FALSE(result.immediate_called_completion);
156+
EXPECT_EQ(result.immediate_bytes_returned, 7U);
157+
158+
expect_ok(result.pending_status);
159+
EXPECT_TRUE(result.pending_saw_event);
160+
EXPECT_TRUE(result.pending_waited);
161+
EXPECT_EQ(result.pending_bytes_returned, 11U);
162+
163+
EXPECT_TRUE(result.cancellation_called);
164+
EXPECT_TRUE(result.cancellation_drained_after_cancel);
165+
EXPECT_TRUE(result.cancellation_waited);
166+
167+
EXPECT_EQ(result.start_failure_status.code(), lvh::ErrorCode::backend_failure);
168+
EXPECT_EQ(result.completion_failure_status.code(), lvh::ErrorCode::backend_failure);
169+
}
170+
150171
TEST_F(WindowsBackendTest, SendInputDevicesTranslateKeyboardMouseFailuresAndUnsupportedProfiles) {
151172
const auto result = lvh::detail::test::windows_backend_send_input_devices();
152173

0 commit comments

Comments
 (0)