Files
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
..

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.)