Skip to content

Commit 6e854f9

Browse files
feat:(Windows): add HID keyboard support
Add a licensed driver-backed keyboard path on Windows, including protocol/version updates, HID report framing, broker validation, and Win32 fallback behavior when the licensed path is unavailable. Update tests and documentation to cover the new keyboard device type and control-protocol semantics.
1 parent 27526d0 commit 6e854f9

19 files changed

Lines changed: 1112 additions & 69 deletions

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<div align="center">
2020
<h2>🎮 Windows Virtual HID Driver License</h2>
2121
<p>
22-
<strong>A license is required to create virtual gamepads or Raw Input-visible mice with the Windows driver.</strong><br>
22+
<strong>A license is required to create virtual gamepads, keyboards, or Raw Input-visible mice with the Windows driver.</strong><br>
2323
This requirement is Windows-only; non-Windows backends do not currently require a license.<br>
2424
Yearly and lifetime options are available.
2525
</p>
@@ -48,9 +48,9 @@ behind backend implementations.
4848
- Descriptor-driven PlayStation gamepads through Linux `uhid`; Generic, Xbox,
4949
and Switch Pro gamepads plus keyboard, mouse, touchscreen, trackpad, and pen
5050
tablet devices through `uinput`.
51-
- Windows gamepads and Raw Input-visible mice through a user-mode UMDF2 control
52-
driver backed by Virtual HID Framework, with keyboard and fallback mouse
53-
support through normal Win32 APIs.
51+
- Windows gamepads, keyboards, and Raw Input-visible mice through a user-mode
52+
UMDF2 control driver backed by Virtual HID Framework, with Win32 keyboard and
53+
mouse fallbacks when the licensed driver path is unavailable.
5454
- Output callbacks for profile-specific feedback such as rumble, LEDs,
5555
adaptive triggers, and raw HID output reports when available.
5656
- An optional `virtualhid_control` native UI tool for creating, removing,
@@ -107,7 +107,8 @@ devices from the OS, or ship a Windows kernel-mode driver.
107107
108108
Linux and Windows provide virtual-device backends. Linux uses standard
109109
user-space kernel interfaces. Windows remains user-mode: the C++ library talks
110-
to a UMDF2 control driver, and the driver publishes HID gamepads through VHF.
110+
to a UMDF2 control driver, and the driver publishes HID gamepads, keyboards,
111+
and mice through VHF.
111112
macOS currently provides a limited CoreGraphics synthetic-input backend for
112113
keyboard and mouse only. It is not a virtual-HID backend and does not yet
113114
support gamepads; native macOS virtual-HID gamepad support is planned.

docs/maintainer/store-review-validation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Paste this into the Partner Center certification notes field:
1515
```text
1616
This package installs the libvirtualhid Windows user-mode UMDF/VHF virtual HID driver and local broker service. Applications consume it through the libvirtualhid client API, and the MSI includes a native diagnostic UI for local validation.
1717
18-
Every virtual gamepad or driver-backed Raw Input mouse creation requires an active license. A currently granted review license key with an available device activation is supplied separately in the Partner Center certification credentials or notes. The key is not embedded in the package or this document.
18+
Every virtual gamepad, driver-backed keyboard, or driver-backed Raw Input mouse creation requires an active license. A currently granted review license key with an available device activation is supplied separately in the Partner Center certification credentials or notes. The key is not embedded in the package or this document.
1919
2020
Launch the validation tool below.
2121
@@ -34,7 +34,7 @@ Start-Process "$installRoot\tools\windows\virtualhid_control.exe"
3434
In the libvirtualhid control window, paste the supplied review key into the License key field and click Activate license. Confirm the status changes to Licensed. Then leave the default Xbox Series profile selected and click Create. Use the button and axis controls in the UI to submit input to the virtual controller. Next, change Device type to Mouse and click Create. Use Tab or the arrow keys to highlight the mouse controls and Space or Enter to activate relative movement, momentary button, and wheel input without using the physical mouse.
3535
3636
Expected result:
37-
- The backend status reports windows-umdf with gamepad and mouse support available
37+
- The backend status reports windows-umdf with gamepad, keyboard, and mouse support available
3838
- The libvirtualhid_broker service is running
3939
- License validation succeeds and the license status reports Licensed
4040
- A virtual HID gamepad is created and appears in the device list

docs/platform-support.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,29 @@ Use capability queries for behavior such as:
2525
## Windows
2626

2727
The Windows backend keeps the normal C++ library buildable with MSVC and
28-
MinGW/UCRT64. Gamepad creation and Raw Input-visible relative mouse input use a
29-
user-mode UMDF2 control driver and Windows Virtual HID Framework. Keyboard,
30-
absolute mouse input, and the mouse fallback use Win32 APIs.
28+
MinGW/UCRT64. Gamepad creation, Raw Input-visible keyboard input, and Raw
29+
Input-visible relative mouse input use a user-mode UMDF2 control driver and
30+
Windows Virtual HID Framework. Keyboard text input, absolute mouse input, and
31+
the keyboard and mouse fallbacks use Win32 APIs.
3132

3233
The C++ library communicates with the driver through fixed-size protocol
3334
structures and `DeviceIoControl`, not C++ STL types. This keeps the public API
3435
compiler-neutral and preserves the boundary between the MinGW/MSVC client
3536
library and the WDK/MSVC driver package.
3637

37-
When the driver is installed, the backend publishes HID gamepads that standard
38-
HID consumers can enumerate, including SDL/HIDAPI, DirectInput,
39-
Windows.Gaming.Input/GameInput, and browser Gamepad API clients. XInput is not a
40-
direct target of the HID backend.
38+
When the driver is installed and licensed, the backend publishes HID gamepads,
39+
keyboards, and mice that standard HID and Raw Input consumers can enumerate.
40+
Gamepad consumers include SDL/HIDAPI, DirectInput,
41+
Windows.Gaming.Input/GameInput, and browser Gamepad API clients. XInput is not
42+
a direct target of the HID backend.
43+
44+
Driver-backed keyboard key transitions use a standard keyboard-page HID report
45+
with modifier state and sixteen simultaneous non-modifier usages. Unicode text
46+
requests and keyboard-page keys that the descriptor cannot represent retain
47+
the Win32 injection path. If driver or license creation is unavailable,
48+
keyboard creation falls back to the existing Win32 implementation. Unexpected
49+
protocol and driver failures remain visible to the caller instead of silently
50+
changing the input path.
4151

4252
The library and driver must use the same Windows control-protocol version. A
4353
descriptor-capacity change therefore increments the protocol version so a

docs/usage.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ unless they explicitly enable additional options.
7777
- `LIBVIRTUALHID_BUILD_WINDOWS_DRIVER`: build the Windows UMDF2 driver package
7878
with the Microsoft WDK/MSVC toolchain.
7979
- `LIBVIRTUALHID_BUILD_WINDOWS_BROKER`: build the Windows broker service used by
80-
the driver package for gamepad creation, active-device limits, and license
81-
state.
80+
the driver package for licensed virtual HID device creation, active-device
81+
limits, and license state.
8282
- `LIBVIRTUALHID_ENABLE_PACKAGING`: enable CPack package metadata.
8383
- `LIBVIRTUALHID_WARNINGS_AS_ERRORS`: treat project warnings as errors.
8484

@@ -107,16 +107,16 @@ rumble, and raw report events delivered through the normal callback path. Button
107107
controls are momentary by default, so they behave like physical gamepad buttons;
108108
on Windows, the UI also displays broker license status and can activate,
109109
refresh, or deactivate a machine license without elevation. Windows UMDF
110-
gamepad creation requires a current machine authorization, but does not perform
111-
an online request per controller. The broker validates in the background at
112-
startup and once per day. If Polar cannot be reached, it retries every 60
113-
seconds. Existing gamepads are retained for one hour, but a new gamepad can be
114-
created only when no licensed gamepad is active. When the outage reaches one
115-
hour, the broker removes excess licensed gamepads and retains at most one. A
110+
virtual HID device creation requires a current machine authorization, but does
111+
not perform an online request per device. The broker validates in the
112+
background at startup and once per day. If Polar cannot be reached, it retries
113+
every 60 seconds. Existing devices are retained for one hour, but a new device
114+
can be created only when no licensed device is active. When the outage reaches one
115+
hour, the broker removes excess licensed devices and retains at most one. A
116116
yearly subscription must validate successfully within 25 hours of its previous
117-
validation, so the remaining gamepad is removed when that deadline passes. A
118-
lifetime license can retain the one-gamepad fallback until validation succeeds.
119-
If the broker service restarts, it removes gamepads left by the previous broker
117+
validation, so the remaining device is removed when that deadline passes. A
118+
lifetime license can retain the one-device fallback until validation succeeds.
119+
If the broker service restarts, it removes devices left by the previous broker
120120
instance before accepting new creation requests. Failed removals are retried.
121121
Both supported plans rely on Polar's entitlement status rather than a locally
122122
enforced calendar expiration. A granted yearly license follows its subscription
@@ -125,10 +125,10 @@ response does not include the subscription renewal date; the linked Polar accoun
125125
portal remains authoritative instead of the broker estimating a date. Polar
126126
server time, Windows uptime, and a per-boot marker track subscription validation
127127
age without relying on the user-adjustable Windows date. After Windows restarts,
128-
a yearly subscription must reconnect to Polar before gamepad creation; a lifetime
129-
license can use the one-gamepad outage fallback. A confirmed missing, revoked,
128+
a yearly subscription must reconnect to Polar before device creation; a lifetime
129+
license can use the one-device outage fallback. A confirmed missing, revoked,
130130
disabled, or mismatched entitlement invalidates the license and removes all
131-
licensed gamepads.
131+
licensed virtual HID devices.
132132
Purchase and account-management buttons use the compiled URLs in
133133
`src/platform/windows/shared/lvh_windows_broker_config.hpp`.
134134
Enable `Lock buttons` to click-to-toggle behavior for held inputs.

docs/windows-driver.md

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Windows Driver Package
22

3-
Windows virtual HID gamepad and Raw Input mouse support uses a user-mode UMDF2
4-
control driver backed by Virtual HID Framework. The driver package is separate
5-
from the normal C++ library build: the library remains consumable from MSVC and
6-
MinGW/UCRT64, while the driver package is built with the Microsoft SDK/WDK
7-
toolchain.
3+
Windows virtual HID gamepad, keyboard, and Raw Input mouse support uses a
4+
user-mode UMDF2 control driver backed by Virtual HID Framework. The driver
5+
package is separate from the normal C++ library build: the library remains
6+
consumable from MSVC and MinGW/UCRT64, while the driver package is built with
7+
the Microsoft SDK/WDK toolchain.
88

99
Windows 11 version 21H2 and later is the supported driver target. The INF also
1010
provides a best-effort compatibility path for Windows 10 version 2004 and later
@@ -19,19 +19,19 @@ so Store listing copy should describe the installed driver component.
1919
### Short Description
2020

2121
```text
22-
User-mode virtual HID driver package that enables compatible apps to create virtual gamepads and Raw Input mice on Windows.
22+
User-mode virtual HID driver package that enables compatible apps to create virtual gamepads, keyboards, and Raw Input mice on Windows.
2323
```
2424

2525
### Description
2626

2727
```text
2828
Virtual HID Driver installs the user-mode driver component used by compatible
29-
applications to create virtual HID gamepads and mice on Windows.
29+
applications to create virtual HID gamepads, keyboards, and mice on Windows.
3030
3131
The package includes a local diagnostic UI for creating and testing virtual
3232
gamepads and mice. Compatible applications can also request virtual HID gamepads
33-
or mice, and Windows applications that understand standard HID devices can
34-
discover them.
33+
keyboards, or mice, and Windows applications that understand standard HID
34+
devices can discover them.
3535
```
3636

3737
## Architecture
@@ -86,8 +86,9 @@ axis, trigger, motion, battery, and touch-position states with the newest report
8686
Button, D-pad, trigger-threshold, report-ID, and touch-contact lifecycle changes
8787
remain ordered in a bounded transition queue. This keeps continuously moving
8888
controls close to the latest submitted state while preserving ordinary button
89-
press and release transitions. Relative mouse motion and wheel values are
90-
accumulated by button state and emitted in descriptor-sized chunks, so VHF
89+
press and release transitions. Keyboard reports also remain ordered so short
90+
key transitions are not coalesced away. Relative mouse motion and wheel values
91+
are accumulated by button state and emitted in descriptor-sized chunks, so VHF
9192
backpressure does not turn relative movement into a replaceable absolute state.
9293
Profile initialization replies are prioritized over pending controller states
9394
so the Switch Pro handshake remains responsive.
@@ -103,10 +104,10 @@ they are not a separate runtime bypass for creating or destroying virtual
103104
devices.
104105

105106
The library and installed driver must use the same control-protocol version.
106-
Control protocol version 3 adds an explicit device type to the common create
107-
request while retaining the 2048-byte report-descriptor capacity needed by the
108-
complete DirectInput PID descriptor. A version mismatch is rejected rather than
109-
interpreting a differently sized request.
107+
Control protocol version 4 adds the canonical keyboard device type and report
108+
contract to the common create request. It retains the explicit device type and
109+
2048-byte report-descriptor capacity introduced by version 3. A version mismatch
110+
is rejected rather than interpreting a request with different semantics.
110111

111112
Each backend runtime uses one control-file handle for commands and its pending
112113
output read. Broker protocol version 4 carries the generalized device create
@@ -131,8 +132,8 @@ recreate their devices after the broker service restarts.
131132

132133
The backend reports `requires_installed_driver = true` and only advertises
133134
gamepad/output-report support when the broker is reachable and the control
134-
device can be opened. Keyboard injection and the mouse `SendInput` fallback do
135-
not require the driver package; a Raw Input-visible mouse does require the
135+
device can be opened. The keyboard and mouse `SendInput` fallbacks do not
136+
require the driver package; a Raw Input-visible keyboard or mouse requires the
136137
driver and the same broker license as a gamepad.
137138

138139
## Build
@@ -331,9 +332,9 @@ opens the shared persistent Polar Checkout Link. Account management opens the
331332
where customers can manage their five allowed machine activations.
332333

333334
Normal Windows UMDF virtual HID device creation requires a current machine
334-
authorization, but device creation itself does not contact Polar. A mouse does
335-
not consume another Polar machine activation; it is another active device under
336-
the existing machine license. The broker validates the
335+
authorization, but device creation itself does not contact Polar. A keyboard or
336+
mouse does not consume another Polar machine activation; each is another active
337+
device under the existing machine license. The broker validates the
337338
saved activation immediately after service startup and then once per day in the
338339
background. If validation cannot complete because of a temporary network or
339340
provider failure, the broker retries every 60 seconds. Devices that already
@@ -388,6 +389,18 @@ WinHTTP resolve, connect, send, and receive operations have explicit timeouts of
388389

389390
## Profile Compatibility
390391

392+
For a keyboard, the Windows backend preserves the requested bus type, VID, PID,
393+
version, name, manufacturer, and stable ID. The Windows transport owns the HID
394+
framing: it uses a report-ID-free standard keyboard descriptor with eight
395+
modifier bits, sixteen simultaneous keyboard-page usages, and a one-byte LED
396+
output report. Normal key transitions use the licensed VHF device so Raw Input
397+
clients enumerate a physical-style HID keyboard instead of receiving only
398+
`SendInput` injection. Unicode text input and keys outside the descriptor's
399+
keyboard-page range continue through `SendInput`. If the driver, broker, or
400+
license is unavailable, keyboard creation retains the existing `SendInput`
401+
fallback; malformed requests and unexpected driver failures are returned to the
402+
caller.
403+
391404
For a mouse, the Windows backend preserves the requested bus type, VID, PID,
392405
version, name, manufacturer, and stable ID. The Windows transport owns the HID
393406
framing: it uses a report-ID-free seven-byte descriptor with five buttons,

src/platform/windows/broker/broker_request_validation.hpp

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <utility>
1818

1919
// local includes
20+
#include "keyboard_protocol.hpp"
2021
#include "lvh_windows_broker_protocol.h"
2122
#include "mouse_protocol.hpp"
2223

@@ -79,14 +80,23 @@ namespace lvh::windows::broker_validation {
7980
inline bool valid_device_request(const LvhWindowsCreateDeviceRequest &request) {
8081
const auto &sizes = request.report_sizes;
8182
const auto known_device = request.device_type == LVH_WINDOWS_DEVICE_GAMEPAD ||
83+
request.device_type == LVH_WINDOWS_DEVICE_KEYBOARD ||
8284
request.device_type == LVH_WINDOWS_DEVICE_MOUSE;
8385
const auto known_bus = request.bus_type == LVH_WINDOWS_BUS_UNKNOWN ||
8486
request.bus_type == LVH_WINDOWS_BUS_USB ||
8587
request.bus_type == LVH_WINDOWS_BUS_BLUETOOTH;
8688
const auto known_profile = request.gamepad_kind <= LVH_WINDOWS_GAMEPAD_DUALSHOCK4;
87-
const auto valid_mouse_descriptor =
89+
const auto valid_driver_descriptor =
8890
request.device_type == LVH_WINDOWS_DEVICE_GAMEPAD ||
89-
(sizes.report_descriptor_size == lvh::detail::windows::mouse_report_descriptor.size() &&
91+
(request.device_type == LVH_WINDOWS_DEVICE_KEYBOARD &&
92+
sizes.report_descriptor_size == lvh::detail::windows::keyboard_report_descriptor.size() &&
93+
std::equal(
94+
lvh::detail::windows::keyboard_report_descriptor.begin(),
95+
lvh::detail::windows::keyboard_report_descriptor.end(),
96+
request.report_descriptor.begin()
97+
)) ||
98+
(request.device_type == LVH_WINDOWS_DEVICE_MOUSE &&
99+
sizes.report_descriptor_size == lvh::detail::windows::mouse_report_descriptor.size() &&
90100
std::equal(
91101
lvh::detail::windows::mouse_report_descriptor.begin(),
92102
lvh::detail::windows::mouse_report_descriptor.end(),
@@ -96,8 +106,12 @@ namespace lvh::windows::broker_validation {
96106
(request.gamepad_kind == LVH_WINDOWS_GAMEPAD_GENERIC &&
97107
request.flags == 0U &&
98108
request.hardware_ids.report_id == 0U &&
99-
sizes.input_report_size == LVH_WINDOWS_MOUSE_INPUT_REPORT_SIZE &&
100-
sizes.output_report_size == 0U);
109+
((request.device_type == LVH_WINDOWS_DEVICE_KEYBOARD &&
110+
sizes.input_report_size == LVH_WINDOWS_KEYBOARD_INPUT_REPORT_SIZE &&
111+
sizes.output_report_size == LVH_WINDOWS_KEYBOARD_OUTPUT_REPORT_SIZE) ||
112+
(request.device_type == LVH_WINDOWS_DEVICE_MOUSE &&
113+
sizes.input_report_size == LVH_WINDOWS_MOUSE_INPUT_REPORT_SIZE &&
114+
sizes.output_report_size == 0U)));
101115

102116
return request.version == LVH_WINDOWS_CONTROL_PROTOCOL_VERSION &&
103117
request.size == sizeof(request) &&
@@ -106,7 +120,7 @@ namespace lvh::windows::broker_validation {
106120
known_bus &&
107121
known_profile &&
108122
valid_device_fields &&
109-
valid_mouse_descriptor &&
123+
valid_driver_descriptor &&
110124
(request.flags & ~known_gamepad_flags) == 0U &&
111125
all_zero(request.hardware_ids.reserved0) &&
112126
sizes.input_report_size > 0U &&

0 commit comments

Comments
 (0)