Skip to content
Draft
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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ jobs:
echo "::warning::${kernel_modules_package} is unavailable; relying on the runner image kernel modules."
fi
sudo tee /etc/udev/rules.d/99-libvirtualhid-ci.rules >/dev/null <<'EOF'
KERNEL=="hidraw*", ATTRS{name}=="libvirtualhid*", MODE="0666", TAG+="uaccess"
SUBSYSTEMS=="input", ATTRS{name}=="libvirtualhid*", MODE="0666", TAG+="uaccess"
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", MODE="0666", TAG+="uaccess"
SUBSYSTEM=="input", KERNEL=="event*", MODE="0666", TAG+="uaccess"
EOF
sudo udevadm control --reload-rules
for module in uhid uinput; do
Expand All @@ -132,6 +132,9 @@ jobs:
echo "::warning::${message}"
fi
done
if ! sudo modprobe hid_playstation; then
echo "::warning::Unable to load hid_playstation; SDL HIDAPI will exercise the native hidraw output path."
fi
for node in /dev/uhid /dev/uinput; do
if [[ -e "${node}" ]]; then
sudo chmod a+rw "${node}"
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ behind backend implementations.

- Gamepad profiles for generic HID, Xbox 360, Xbox One, Xbox Series,
DualShock 4, DualSense, and Nintendo Switch Pro-style controllers.
- Descriptor-driven Linux gamepads through `uhid`, plus keyboard, mouse,
touchscreen, trackpad, and pen tablet devices through `uinput`.
- Descriptor-driven PlayStation gamepads through Linux `uhid`; Generic, Xbox,
and Switch Pro gamepads plus keyboard, mouse, touchscreen, trackpad, and pen
tablet devices through `uinput`.
- Windows gamepads through a user-mode UMDF2 control driver backed by Virtual
HID Framework, with keyboard and mouse support through normal Win32 APIs.
- Output callbacks for profile-specific feedback such as rumble, LEDs,
Expand Down
114 changes: 103 additions & 11 deletions docs/platform-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@ HID consumers can enumerate, including SDL/HIDAPI, DirectInput,
Windows.Gaming.Input/GameInput, and browser Gamepad API clients. XInput is not a
direct target of the HID backend.

The library and driver must use the same Windows control-protocol version. A
descriptor-capacity change therefore increments the protocol version so a
stale installed driver fails creation explicitly instead of misreading the
request.

The built-in Generic profile remains a platform-neutral Game Pad publicly. At
the Windows transport boundary, VHF presents it as a DirectInput-compatible
Joystick with the complete PID output-report contract required by DirectInput.
Constant Force and Sine effects are normalized back into the same rumble
callback used by the other backends; unsupported PID effect payloads are
accepted without producing misleading feedback. Windows also applies
DirectInput's idle-at-maximum Z/Rz trigger polarity. Start delay, duration, and
loop count are honored; finite effects emit a zero-rumble callback when they
expire, while explicit stop commands take effect immediately.

Xbox One uses the native eight-byte PID payload exposed by the Windows Xbox HID
stack. Xbox Series uses the `0x045E:0x0B13` HID identity, report ID `1`, and the
contiguous Xbox button layout that Steam maps correctly on the raw HID path.
Share is mirrored as button 12 and as the Consumer Record usage for
descriptor-aware consumers. The driver deliberately avoids publishing the
`0x045E:0x0B13&IG_00` XInputHID hardware ID for Xbox Series because that path
lets Steam register the controller, but it hides Share behind XInput's older
button model. The public Xbox Series profile remains `0x045E:0x0B12`; the
Windows transport applies the Share-capable descriptor and identity at device
creation. Rumble writes use report ID `3` followed by the same eight-byte
four-motor payload. The library applies the actuator-enable mask and duration
field, then reports the body motors as normalized low/high-frequency rumble and
the independent trigger motors as trigger-rumble output.

The VHF driver answers the calibration, pairing, and firmware feature reports
used to initialize DualShock 4 and DualSense HIDAPI output. It also answers the
Switch Pro USB and subcommand initialization sequence and accepts the native
`0x30` input layout, so descriptor-aware consumers can initialize those
controllers before sending their native output reports.

See [Windows driver package](windows-driver.md) for build, install, validation,
and signing details.

Expand All @@ -46,15 +81,64 @@ and signing details.
The Linux backend uses standard user-space kernel interfaces:

- `uhid` for descriptor-driven HID gamepads.
- `uinput` for keyboard, mouse, touchscreen, trackpad, and pen tablet devices.
- `uinput` for Generic, Xbox 360, Xbox One, Xbox Series, and Switch Pro
gamepads, plus keyboard, mouse, touchscreen, trackpad, and pen tablet
devices.
- `libevdev` internally for uinput device construction.
- X11/XTest only as a keyboard and mouse fallback when `uinput` cannot be used
and an X11 session is available.

Gamepad support prefers `uhid` because descriptors, raw HID identity, feature
reports, and output reports matter for controller compatibility. Keyboard and
pointer devices prefer `uinput` because those devices map naturally to Linux
input devices.
Gamepad support normally prefers `uhid` because descriptors, raw HID identity,
feature reports, and output reports matter for controller compatibility.
Generic, Xbox-family, and Switch Pro profiles instead use `uinput` so SDL,
Steam, browser Gamepad API implementations, and other evdev consumers receive
canonical Linux gamepad events. Face buttons, shoulders, menu buttons, stick
clicks, and Guide use their native evdev codes; sticks use absolute axes. Every
uinput gamepad exposes its directional pad through `ABS_HAT0X` and `ABS_HAT0Y`.
Generic and Xbox triggers remain independent analog `ABS_Z` and `ABS_RZ` axes.
Switch Pro uses the Nintendo face-button
positions, button events for ZL/ZR, and `BTN_Z` for Capture. Profiles with rumble
support normalize rumble, constant, periodic, and ramp uinput force-feedback
effects back into the public callback. A zero-length effect remains active until
its explicit stop event, matching the infinite-effect contract used by SDL and
Steam. The Linux backend lets a new uinput device settle before reading those
effects so an early poll error cannot disable feedback for the device lifetime.
PlayStation rumble is read from native UHID interrupt-channel output reports.

The Generic profile keeps its public `0x1209:0x0001` identity, USB bus, and
Generic device name at the Linux transport boundary. Its uinput device exposes
both `ABS_HAT0X`/`ABS_HAT0Y` hat axes and semantic `BTN_DPAD_*` events so raw
evdev consumers and higher-level gamepad mappers can observe D-pad movement
without re-identifying the device as Xbox. It uses a compact Generic button
layout rather than the sparse Xbox button slots.

Xbox 360 retains its `0x045E:0x028E` identity, while its Linux uinput device uses
the Bluetooth bus so consumers select the sparse button mapping.
Xbox One and Xbox Series retain their public USB identities, but their Linux
uinput devices use the corresponding Bluetooth product identities (`0x0B20`
and `0x0B13`, respectively), whose standard consumer mappings match the events
that uinput exposes. Those three Xbox profiles preserve the 15-slot
Linux gamepad button sequence: unused `BTN_C`, `BTN_Z`, `BTN_TL2`, and `BTN_TR2`
slots are advertised but never pressed, keeping face buttons, shoulders, menu
buttons, Guide, L3, and R3 at their expected indices. D-pad directions are
reported through the hat axes and exposed as logical buttons by standard
gamepad consumers.

DualShock 4 and DualSense remain on `uhid` so their descriptors, motion,
touchpad, battery, feature reports, and profile-specific output reports stay
available. The backend accepts PlayStation output through both UHID interrupt
and control channels. Numbered control-channel output is normalized before
parsing, whether the kernel includes the report number in the payload or
provides it separately on the UHID event.

Switch Pro keeps its Nintendo identity on the Linux uinput path. This follows
the evdev layout used by Linux-native virtual-controller implementations and
allows standard `FF_RUMBLE` effects without emulating the physical controller's
proprietary initialization handshake.

On descriptor-driven backends, native Switch Pro output reports `0x01` and
`0x10` are decoded into the normalized low- and high-frequency rumble callback.
The original native report remains available in `GamepadOutput::raw_report`.

The optional `virtualhid_control` diagnostic UI uses SDL3 and Dear ImGui through
the repository CPM lockfile. It is intended to stay on the same UI framework for
Expand All @@ -67,8 +151,9 @@ rather than an unconditional default.

### Permissions

Linux deployment is usually a permissions problem, not a kernel-module problem.
Install udev rules such as `/etc/udev/rules.d/60-libvirtualhid.rules`:
Linux deployment requires both device-node permissions and the kernel modules
for the selected virtual-controller path. Install udev rules such as
`/etc/udev/rules.d/60-libvirtualhid.rules`:

```udev
# Allows libvirtualhid consumers to access /dev/uinput
Expand All @@ -87,18 +172,25 @@ KERNEL=="hidraw*", ATTRS{name}=="Your App Controller*", GROUP="input", MODE="066
SUBSYSTEMS=="input", ATTRS{name}=="Your App Controller*", GROUP="input", MODE="0660", TAG+="uaccess"
```

For `uhid` gamepad support, install a modules-load entry such as
`/etc/modules-load.d/60-libvirtualhid.conf`:
For gamepad support, install a modules-load entry such as
`/etc/modules-load.d/60-libvirtualhid.conf`. `hid_playstation` enables the
kernel force-feedback path used by virtual DualShock 4 and DualSense
controllers; descriptor-aware HIDAPI clients can also write their native
output reports through `hidraw`:

```text
uhid
uinput
hid_playstation
```

After installing the rules, load `uhid`, reload udev, and trigger the device
nodes:
After installing the rules, load the modules, reload udev, and trigger the
device nodes:

```bash
sudo modprobe uhid
sudo modprobe uinput
sudo modprobe hid_playstation
sudo udevadm control --reload-rules
sudo udevadm trigger --property-match=DEVNAME=/dev/uinput
sudo udevadm trigger --property-match=DEVNAME=/dev/uhid
Expand Down
7 changes: 4 additions & 3 deletions docs/store-review-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ In the libvirtualhid control window, leave the default Xbox Series profile selec
Expected result:
- The backend status reports windows-umdf with gamepad support available
- A virtual HID gamepad is created and appears in the device list
- A virtual HID gamepad child device starts, matching either
HID\VID_045E&PID_0B12&IG_00 or HID\VID_045E&PID_02FF&IG_00
- Button and axis values in the UI can be pressed or moved without errors
- A virtual HID gamepad child device starts with the Xbox Series HID ID
HID\VID_045E&PID_0B13
- Button, axis, and Share values in the UI can be pressed or moved without
errors

Optional browser validation:
$installRoot = Join-Path $env:ProgramFiles "libvirtualhid"
Expand Down
3 changes: 2 additions & 1 deletion docs/streaming-host-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ The core API and adapter shape cover the major streaming-host requirements:
- Rich controller metadata.
- Gamepad output callbacks.
- Keyboard and mouse input paths.
- Linux `uhid` gamepads and `uinput` keyboard/pointer devices.
- Linux PlayStation gamepads through `uhid`, Generic/Xbox/Switch Pro gamepads
through `uinput`, and `uinput` keyboard/pointer devices.
- Linux DualSense and DualShock 4 USB/Bluetooth report handling.
- Linux touchscreen, trackpad, and pen tablet device types.
- Windows UMDF/VHF gamepad creation through an installed driver package.
Expand Down
32 changes: 23 additions & 9 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,17 +164,31 @@ touch, motion, battery, feedback, and lifecycle updates onto the platform-neutra

## Built-In Profiles

Built-in gamepad profiles include:

- Generic HID gamepad.
- Xbox 360.
- Xbox One.
- Xbox Series.
- DualShock 4 USB and Bluetooth.
- DualSense USB and Bluetooth.
- Nintendo Switch Pro.
Built-in gamepad profiles and their platform-neutral default device names are:

| Profile | Default device name |
| --- | --- |
| Generic HID gamepad | `(libvirtualhid) Generic Controller` |
| Xbox 360 | `(libvirtualhid) X-Box 360 Controller` |
| Xbox One | `(libvirtualhid) X-Box One Controller` |
| Xbox Series | `(libvirtualhid) X-Box Series Controller` |
| DualShock 4 USB and Bluetooth | `(libvirtualhid) PS4 Controller` |
| DualSense USB and Bluetooth | `(libvirtualhid) PS5 Controller` |
| Nintendo Switch Pro | `(libvirtualhid) Nintendo Pro Controller` |

Consumers may replace `DeviceProfile::name` before creating a gamepad, for
example to prepend an application name while preserving the default controller
identity across platform backends.

The platform-neutral Generic HID descriptor reports the D-pad as buttons 13
through 16 in the input report. Linux may still route that profile through
`uinput`, where the backend exposes those same logical directions through the
standard `ABS_HAT0X` and `ABS_HAT0Y` axes plus `BTN_DPAD_*` button events.

Profiles advertise support for features such as rumble, trigger rumble, RGB
LEDs, adaptive triggers, motion sensors, touchpads, battery state, profile
specific buttons, and raw output reports. Consumers should query profile and
backend capabilities before warning users about unsupported client features.
The `misc1` button represents Share/Capture/Mic Mute-style controls and is
available on the generic, Xbox Series, DualSense, and Switch Pro profiles; Xbox
360 and Xbox One do not advertise that extra button.
46 changes: 35 additions & 11 deletions docs/windows-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ device from the requested descriptor, VID/PID, version, and report layout. Input
reports are submitted through VHF, and HID output writes are normalized back to
the C++ output callback path.

The library and installed driver must use the same control-protocol version.
Protocol version 2 expands the report-descriptor capacity to 2048 bytes for the
complete DirectInput PID descriptor; a version mismatch is rejected rather
than interpreting a differently sized request.

Each backend runtime uses one control-file handle for commands and its pending
output read. The driver associates output events with that handle, so feedback
from a virtual gamepad is delivered only to the runtime that created it instead
of being consumed by another libvirtualhid client.

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
process exits or crashes, Windows cleans up gamepads that were not explicitly
Expand Down Expand Up @@ -153,17 +163,31 @@ The Windows backend publishes HID gamepads through VHF. DirectInput, SDL/HIDAPI,
Windows.Gaming.Input/GameInput, and browser Gamepad API clients should see
standard HID devices after the driver is installed.

The built-in Xbox One and Xbox Series profiles use XboxGIP-shaped HID
descriptors. The Xbox Series profile keeps the public physical USB identity
`VID_045E&PID_0B12`; the driver also publishes a compatible driver-matching
hardware ID for Windows binding. The Xbox 360 profile is rejected by the
UMDF/VHF backend because a real Xbox 360 controller is an XUSB device rather
than a VHF HID gamepad.

DualShock 4, DualSense, Switch Pro, and generic HID profiles are descriptor
driven. Consumers that display raw HID strings may still show the Windows VHF
product label because VHF does not provide a product/manufacturer string
callback.
The built-in Xbox One profile uses its XboxGIP-shaped HID descriptor. The public
Xbox Series profile remains `VID_045E&PID_0B12`, but the Windows transport
presents it as the `VID_045E&PID_0B13` HID identity with report ID `1`. The VHF
path keeps the contiguous Xbox button layout that Steam maps correctly on raw
HID and mirrors Series Share as both button 12 and the Consumer Record usage.
The driver deliberately avoids the `VID_045E&PID_0B13&IG_00` XInputHID hardware
ID for Xbox Series because that binding registers in Steam through XInput but
does not expose the Share button. Series rumble writes use output report ID `3`
followed by the same eight-byte four-motor Xbox payload normalized by the public
callback. The Xbox 360 profile is rejected by the UMDF/VHF backend because a
real Xbox 360 controller is an XUSB device rather than a VHF HID gamepad.

DualShock 4 and DualSense answer the calibration, pairing, and firmware feature
requests used by their Windows HIDAPI initialization paths. Switch Pro answers
the native USB and subcommand handshake and submits native `0x30` input reports.
The built-in Generic profile is presented to Windows as a DirectInput PID
Joystick with the complete output-report set required for DirectInput
enumeration. Constant Force and Sine output is normalized to the portable
gamepad rumble callback; other declared effect payloads are ignored safely. The
backend honors PID start delay, duration, and loop count, and automatically
stops finite effects. These changes remain private to the Windows transport and
do not alter the public platform-neutral profile API.

Consumers that display raw HID strings may still show the Windows VHF product
label because VHF does not provide a product/manufacturer string callback.

## Signing

Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/test-browser-gamepad.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function Get-ExpectedGamepadIdPattern {
"generic" { return "(1209.*0001|vid[_ -]?1209.*pid[_ -]?0001|generic)" }
"x360" { return "(045e.*028e|vid[_ -]?045e.*pid[_ -]?028e|x-?box.*360)" }
"xone" { return "(045e.*02ea|vid[_ -]?045e.*pid[_ -]?02ea|xbox one|x-box one)" }
"xseries" { return "(045e.*0b12|045e.*02ff|vid[_ -]?045e.*pid[_ -]?0b12|vid[_ -]?045e.*pid[_ -]?02ff|xbox wireless|xbox series)" }
"xseries" { return "(045e.*0b12|045e.*0b13|vid[_ -]?045e.*pid[_ -]?0b12|vid[_ -]?045e.*pid[_ -]?0b13|xbox wireless|xbox series)" }
"ds4" { return "(054c.*05c4|vid[_ -]?054c.*pid[_ -]?05c4|dualshock|wireless controller)" }
"ds5" { return "(054c.*0ce6|vid[_ -]?054c.*pid[_ -]?0ce6|dualsense|wireless controller)" }
"switch" { return "(057e.*2009|vid[_ -]?057e.*pid[_ -]?2009|switch|pro controller)" }
Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/test-installed-driver.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ function Get-ExpectedGamepadHardwareId {
"generic" { return @("HID\VID_1209&PID_0001") }
"x360" { return @("HID\VID_045E&PID_028E&IG_00") }
"xone" { return @("HID\VID_045E&PID_02EA&IG_00") }
"xseries" { return @("HID\VID_045E&PID_0B12&IG_00", "HID\VID_045E&PID_02FF&IG_00") }
"xseries" { return @("HID\VID_045E&PID_0B13&IG_00") }
"ds4" { return @("HID\VID_054C&PID_05C4") }
"ds5" { return @("HID\VID_054C&PID_0CE6") }
"switch" { return @("HID\VID_057E&PID_2009") }
Expand Down
5 changes: 4 additions & 1 deletion src/core/backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ namespace lvh::detail {
*/
class FakeGamepad final: public BackendGamepad {
public:
OperationStatus submit(const std::vector<std::uint8_t> & /*report*/) override {
OperationStatus submit(
const GamepadState & /*state*/,
const std::vector<std::uint8_t> & /*report*/
) override {
return OperationStatus::success();
}

Expand Down
8 changes: 6 additions & 2 deletions src/core/backend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,16 @@ namespace lvh::detail {
virtual ~BackendGamepad() = default;

/**
* @brief Submit a packed input report to the backend.
* @brief Submit normalized state and its packed input report to the backend.
*
* HID backends consume @p report, while native platform input backends may
* consume @p state.
*
* @param state Normalized gamepad state.
* @param report Packed HID input report.
* @return Submit status.
*/
virtual OperationStatus submit(const std::vector<std::uint8_t> &report) = 0;
virtual OperationStatus submit(const GamepadState &state, const std::vector<std::uint8_t> &report) = 0;

/**
* @brief Get platform-visible nodes associated with this backend device.
Expand Down
4 changes: 2 additions & 2 deletions src/core/gamepad_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ namespace lvh {
using enum GamepadProfileKind;

case generic:
case xbox_360:
case xbox_one:
case xbox_series:
case dualsense:
case switch_pro:
return true;
case xbox_360:
case xbox_one:
case dualshock4:
return false;
}
Expand Down
Loading
Loading