Files
punktfunk/packaging/windows/drivers/pf-xusb/README.md
T
enricobuehler 92e68024f1 fix(windows-installer): build the gamepad drivers from source in CI too
Fold the pf-dualsense (DualSense / DualShock 4) and pf-xusb (Xbox 360 / XInput)
UMDF drivers into the in-tree drivers workspace (their source had stale
../../crates/wdk-* path-deps from before the wdk vendoring reorg and could no
longer build at all) and build them from source per release, exactly like
pf-vdisplay - same anti-stale reasoning. One `cargo build --release` now builds
all three drivers against the vendored wdk-sys (incl. the bindgen 0.72 pin), and
build-gamepad-drivers.ps1 signs pf_dualsense + pf_xusb (clear FORCE_INTEGRITY ->
sign dll -> stampinf -> Inf2Cat -> sign cat) with one shared cert + .cer,
matching the layout install-gamepad-drivers.ps1 expects. pack-host-installer.ps1
builds + stages them instead of the retired checked-in binaries.

Validated on the runner: the whole workspace (pf-vdisplay + pf-dualsense +
pf-xusb) builds with CARGO_TARGET_DIR=C:\t set, and build-gamepad-drivers.ps1
produces signed pf_dualsense.{dll,inf,cat} + pf_xusb.{dll,inf,cat} + the .cer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:08:40 +00:00

80 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pf-xusb — virtual Xbox 360 XUSB companion (UMDF2, classic XInput)
A **pure-user-mode** UMDF2 driver that makes a virtual Xbox 360 controller visible to classic
**`XInputGetState`** with **no kernel bus driver** (no ViGEmBus) — the HIDMaestro approach. It is the
Windows counterpart to ViGEm's X360 target, owned in-tree.
## Why this is not the HID driver
XInput does **not** use HID. `xinput1_4.dll` enumerates the **XUSB device-interface GUID**
`{EC87F1E3-C13B-4100-B5F7-8B84D54260CB}` (`SetupDiEnumDeviceInterfaces`), opens the Nth present
instance (= player slot 03) with `CreateFile`, and polls it with buffered IOCTLs. So this driver:
- is **not** a HID minidriver (no `MsHidUmdf`) — it's a plain UMDF2 function driver under `WUDFRd`,
**System** setup class;
- registers the XUSB interface with `WdfDeviceCreateDeviceInterface(device, &XUSB_GUID, NULL)`;
- answers the XUSB IOCTLs (all `METHOD_BUFFERED`, delivered to user mode by the reflector) from
controller state the host publishes into a shared section `Global\pfxusb-shm-0`; a game's rumble
(`SET_STATE`) is published back for the host to forward to the client.
The WAIT_* IOCTLs return `STATUS_INVALID_DEVICE_REQUEST`, which makes `xinput1_4` fall back to
synchronous `GET_STATE` polling — so no manual queue / timer is needed for classic XInput. (WGI/
GameInput admission additionally needs a `xinputhid` `UpperFilters` registry tripwire + the async
`WAIT_FOR_INPUT` pump — not implemented; classic XInput does not need it.)
## Verified wire formats (source: HIDMaestro `driver/companion.c`, nefarius/XInputHooker `XUSB.h`, ViGEm)
| IOCTL | Code | Reply |
| --- | --- | --- |
| `GET_INFORMATION` | `0x80006000` | 12 B: `[0]`=ver `0x0103`, `[2]`=count `0x01`, `[8]`=VID `045E`, `[10]`=PID `028E` — marks the slot **connected** |
| `GET_CAPABILITIES` | `0x8000E004` | 24 B (or 36 B V2 if `outLen>=36`): Type `0x03`/SubType `0x01`, motor max `0xFFFF` (advertise rumble) |
| `GET_STATE` | `0x8000E00C` | **29 B**: `[0]`ver `[2]`count `[5]`u32 packet# `[0x0B]`u16 wButtons `[0x0D]`LT `[0x0E]`RT `[0x0F..0x16]`4×i16 sticks |
| `SET_STATE` | `0x8000A010` | input 5 B `{00, led, large, small, subcmd}`: `subcmd 0x02`=rumble (large `[2]`, small `[3]`), `0x01`=player-LED |
| `GET_LED_STATE` | `0x8000E008` | `{0,0,0x06}` |
| `GET_BATTERY_INFORMATION` | `0x8000E018` | `{0,0x01,0x03,0}` |
| `WAIT_GUIDE_BUTTON` / `WAIT_FOR_INPUT` | `0x8000E014` / `0x8000E3AC` | `STATUS_INVALID_DEVICE_REQUEST` → GET_STATE fallback |
`wButtons` is the `XINPUT_GAMEPAD_*` bitmap (DPAD_UP `0x0001` … A `0x1000` B `0x2000` X `0x4000`
Y `0x8000`). `dwPacketNumber` (GET_STATE `[5]`) must increment whenever the payload changes.
## Shared-memory layout `Global\pfxusb-shm-0` (64 B) — host writes state, driver writes rumble
`magic u32 @0` (`"PFXU"` `0x55584650`) · `packet u32 @4` (host bumps → dwPacketNumber) · `wButtons u16
@8` · `LT @10` · `RT @11` · `LX/LY/RX/RY i16 @12/@14/@16/@18` · `rumble_seq u32 @24` (driver bumps) ·
`large @28` · `small @29`.
## Validated live on `.173` (2026-06-22)
`XInputGetState(0)` returns **CONNECTED** with the pushed buttons/sticks and an incrementing
`dwPacketNumber`; `XInputSetState(0xC000, 0x4000)` reaches the driver as `00 00 c0 40 02` → host sees
`large=192 small=64`. Test tools: `C:\Users\Public\giprobe\xusbtest.exe` (creates the `pf_xusb`
devnode + cycling state via shm) and `xinputtest.exe` (`XInputGetState`/`SetState` harness).
## Build / sign / install (same recipe as the DualSense driver)
Built from `C:\Users\Public\m0\windows-drivers-rs\examples\pf-xusb` (the `../../crates` paths resolve
there); these repo files are the canonical copies — keep them in sync.
1. `cargo make` (env `LIBCLANG_PATH`, `Version_Number=10.0.26100.0`) → `target\debug\pf_xusb_package\`.
2. Clear the FORCE_INTEGRITY PE bit (bit `0x80` at `e_lfanew+0x5e` of `pf_xusb.dll`).
3. `signtool sign /fd SHA256 /sha1 6A52984E54376C45A1C236B1A2C8A746C5AB6131 pf_xusb.dll`.
4. `Inf2Cat /driver:<pkg> /os:10_X64` → re-sign `pf_xusb.cat` with the same thumbprint.
5. `pnputil /add-driver pf_xusb.inf` (no `/install`; the host SwDeviceCreate's `pf_xusb` per session).
## Host integration (done)
`crates/punktfunk-host/src/inject/gamepad_windows.rs` is the Windows `GamepadManager` (used by
`PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, maps `pfxusb-shm-<index>`, writes
the XInput state from the client's gamepad frame (already XInput-convention) and forwards rumble. There
is **no ViGEmBus dependency** anymore. The driver is vendored + pnputil-installed by the Inno Setup
installer (`packaging/windows/gamepad-drivers/` + `install-gamepad-drivers.ps1`).
## Multi-pad
The host stamps each pad's index into the device Location (`pszDeviceLocation`); the driver reads it
via `WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)` in EvtDeviceAdd and maps its own
`pfxusb-shm-<index>`. `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) gives each pad its own
WUDFHost, so the per-pad `SHM_INDEX` static doesn't collide. Validated live: two pads → two distinct
XInput slots. (XInput assigns the player slot 0-3 by interface-enumeration order, independent of this
index — which only routes shared memory.)