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>
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 0–3) 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 underWUDFRd, 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 sectionGlobal\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.
cargo make(envLIBCLANG_PATH,Version_Number=10.0.26100.0) →target\debug\pf_xusb_package\.- Clear the FORCE_INTEGRITY PE bit (bit
0x80ate_lfanew+0x5eofpf_xusb.dll). signtool sign /fd SHA256 /sha1 6A52984E54376C45A1C236B1A2C8A746C5AB6131 pf_xusb.dll.Inf2Cat /driver:<pkg> /os:10_X64→ re-signpf_xusb.catwith the same thumbprint.pnputil /add-driver pf_xusb.inf(no/install; the host SwDeviceCreate'spf_xusbper 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.)