Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/windows-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/platform/windows/driver/libvirtualhid.inf.in
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
120 changes: 104 additions & 16 deletions src/platform/windows/windows_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename CancelOperation, typename FinishOperation>
void cancel_and_drain_overlapped_io(
OVERLAPPED &overlapped,
DWORD *bytes_returned,
CancelOperation &&cancel_operation,
FinishOperation &&finish_operation
) {
static_cast<void>(std::forward<CancelOperation>(cancel_operation)(overlapped));
static_cast<void>(std::forward<FinishOperation>(finish_operation)(overlapped, bytes_returned, TRUE));
}

template<typename StartOperation, typename FinishOperation>
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<StartOperation>(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<FinishOperation>(finish_operation)(overlapped, bytes_returned, TRUE) == FALSE) {
return windows_failure(ErrorCode::backend_failure, operation, ::GetLastError());
}

return OperationStatus::success();
}

template<typename Submit>
OperationStatus submit_with_desktop_retry(Submit submit, std::string_view operation) {
using enum ErrorCode;
Expand Down Expand Up @@ -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) {
Expand All @@ -638,11 +702,11 @@ namespace lvh::detail {
INFINITE
);
if (wait_result == WAIT_OBJECT_0 + 1U) {
static_cast<void>(::CancelIoEx(handle_->value.get(), &overlapped));
cancel_and_drain();
return std::nullopt;
}
if (wait_result != WAIT_OBJECT_0) {
static_cast<void>(::CancelIoEx(handle_->value.get(), &overlapped));
cancel_and_drain();
return std::nullopt;
}
}
Expand Down Expand Up @@ -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<typename Input>
Expand All @@ -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_;
Expand Down
17 changes: 17 additions & 0 deletions tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
80 changes: 80 additions & 0 deletions tests/fixtures/windows_backend_test_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
21 changes: 21 additions & 0 deletions tests/unit/test_windows_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading