Files
punktfunk/packaging/windows/xusb-driver/README.md
T
enricobuehler b0c82333d2
audit / cargo-audit (push) Successful in 17s
apple / swift (push) Successful in 57s
android / android (push) Successful in 4m36s
ci / web (push) Successful in 34s
ci / docs-site (push) Successful in 52s
release / apple (push) Successful in 7m31s
ci / rust (push) Successful in 8m37s
ci / bench (push) Successful in 4m39s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
deb / build-publish (push) Successful in 2m35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m18s
flatpak / build-publish (push) Successful in 4m0s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m31s
docker / deploy-docs (push) Successful in 19s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m22s
windows-host / package (push) Successful in 2m56s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m13s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m15s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 59s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m3s
feat(gamepad): pure-user-mode Windows DualShock 4 + Xbox 360 (drop ViGEm) + installer + multi-pad
Windows virtual gamepads now have zero external dependencies - ViGEmBus is removed.

- DualShock 4: Windows UMDF backend (inject/dualshock4_windows.rs + dualshock4_proto.rs),
  reusing the DualSense SwDeviceCreate game-detection identity fix. The one UMDF driver serves
  the DS5 or DS4 identity/descriptor/features/strings per a device_type byte the host stamps into
  shared memory. Driver also gains IOCTL_HID_GET_STRING and a 41-byte calibration feature.
- Xbox 360: a new UMDF2 XUSB companion driver (packaging/windows/xusb-driver/) that registers
  GUID_DEVINTERFACE_XUSB and answers the buffered XInput IOCTLs from a shared section, so classic
  XInputGetState/SetState work with no kernel bus driver. inject/gamepad_windows.rs is rewritten
  to drive it and the vigem-client dependency is removed. Xbox One folds to the 360 XInput path.
- Installer: vendor + pnputil-install the three UMDF drivers (packaging/windows/gamepad-drivers/
  + install-gamepad-drivers.ps1, wired into pack-host-installer.ps1 + punktfunk-host.iss).
- Multi-pad: the host stamps each pad index into the device Location (pszDeviceLocation); the
  driver reads it via WdfDeviceAllocAndQueryProperty to map its own *-shm-<index>, with
  UmdfHostProcessSharing=ProcessSharingDisabled giving each pad its own host (per-pad statics).

Validated live on the Windows host: Cyberpunk native DualSense detection, DS4 identity + descriptor,
XInputGetState + rumble round-trip, two pads -> two distinct XInput slots, and a full installer build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:35:03 +02: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.)