feat(gamepad): pure-user-mode Windows DualShock 4 + Xbox 360 (drop ViGEm) + installer + multi-pad
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
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
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>
This commit is contained in:
@@ -19,17 +19,124 @@ one agent's memory). Run the experiments **on the Windows host** (`.173`, repo a
|
||||
the motors, host forwards `0xCA` — log: `rumble: forwarding to client (0xCA) low=16128 high=16128`).
|
||||
The break is the **client** (macOS) not rendering `0xCA` onto the physical pad. Separate task/agent.
|
||||
|
||||
## Root-cause hypothesis (the thing to confirm/fix)
|
||||
## Root cause — CONFIRMED (2026-06-22, run live on the interactive desktop, console session 3)
|
||||
|
||||
The device is **software-enumerated**: `SWD\PUNKTFUNK\PF_PAD_0` → child `HID\VID_054C&PID_0CE6`. It is NOT
|
||||
a real USB or Bluetooth device. SDL/HIDAPI enumerate any HID by VID/PID (incl. SWD) — so they see it. A
|
||||
game's *native* DualSense path is pickier. Two likely causes:
|
||||
1. **Windows.Gaming.Input (WGI) / GameInput exclude SWD (software) HID devices** that raw-HID
|
||||
enumeration includes. Many modern titles use these.
|
||||
2. **USB-vs-Bluetooth detection by device-path prefix.** Native DS5 code picks the report format (64-byte
|
||||
USB report `0x01` vs 78-byte BT report `0x31`) from the connection type. If it keys off the device
|
||||
path (`USB\…` vs `BTHENUM\…`) rather than the report length, our `SWD\…` path matches neither and it
|
||||
mis-detects. (SDL keys off the *report length* = 64 → USB → works.)
|
||||
The break is the device's **PnP identity / device-interface path**, not the HID descriptor or feature
|
||||
reports. `hidclass` derives the HID child's path token and its `HID\VID_054C&PID_0CE6` hardware-ids from the
|
||||
**parent bus device's hardware-id**. Our parent is the software (SWD) devnode `SWD\PUNKTFUNK\PF_PAD_0` whose
|
||||
hardware-id is `pf_dualsense` (no VID/PID), so hidclass emits only the *VendorID+usage* fallback and **no
|
||||
PID**. Measured on this box (one virtual pad live + one real 8BitDo present):
|
||||
|
||||
HID-child hardware-ids (`DEVPKEY_Device_HardwareIds`, CompatibleIds empty):
|
||||
`HID\pf_dualsense` · `HID\VID_054C&UP:0001_U:0005` · `HID_DEVICE_SYSTEM_GAME` · `HID_DEVICE_UP:0001_U:0005`
|
||||
· `HID_DEVICE` — **note the absent `HID\VID_054C&PID_0CE6`.** `HIDD_ATTRIBUTES` itself is correct (VID 054C
|
||||
/ PID 0CE6), which is why attribute-readers work.
|
||||
|
||||
Device-interface paths (from `HKLM\SYSTEM\CurrentControlSet\Control\DeviceClasses\{4d1e55b2-…}`):
|
||||
|
||||
| Device | HID interface path |
|
||||
| --- | --- |
|
||||
| **Ours (virtual)** | `\\?\HID#punktfunk#1&ca418da&0&0000#{…}` — **no `VID_/PID_` token** |
|
||||
| Real DualShock 4 (USB, registry remnant) | `\\?\HID#VID_054C&PID_05C4&REV_0100#…` |
|
||||
| Real DualSense (BT, registry remnant) | `\\?\HID#{00001124-…}_VID&0002054c_PID&0ce6#…` |
|
||||
|
||||
**Cross-API enumeration (the decisive experiment — impossible over SSH, run live in the console session):**
|
||||
|
||||
| API | Sees our virtual DS5? | Identity reported | Reads from |
|
||||
| --- | --- | --- | --- |
|
||||
| SDL3 / HIDAPI | ✅ | 054C:0CE6, type=PS5 | `HIDD_ATTRIBUTES` → Steam works |
|
||||
| RawInput | ✅ | 054C:0CE6 | `HIDD_ATTRIBUTES` |
|
||||
| WGI `RawGameController` | ✅ | 054C:0CE6 | `HIDD_ATTRIBUTES` |
|
||||
| WGI `Gamepad` | ❌ empty | — | (empty for *all* pads on this box — no Xbox-profile pad; not DS-specific) |
|
||||
| **MS GameInput** | ✅ enumerates it | **vid=0x0000 pid=0x0000** | **PnP path / hardware-ids** |
|
||||
| Cyberpunk native PS5 | ❌ | — | needs the DS5 VID/PID identity |
|
||||
|
||||
The GameInput result is the clincher: it **does** enumerate our pad — descriptor fingerprint matches exactly
|
||||
(15 buttons, 6 axes, 1 hat, usage Game Pad 0x05) — but reports **vid/pid = 0**, while it reads the real
|
||||
8BitDo's `vid=0x3434` correctly. So GameInput (and, by the same logic, a native PS5 path) takes VID/PID from
|
||||
the **PnP device path / hardware-ids, NOT from `HIDD_ATTRIBUTES`**, and ours carry no `VID_054C&PID_0CE6`.
|
||||
Everything that reads attributes directly (SDL / RawInput / WGI-raw) is fine; everything that keys off the
|
||||
device *identity/path* (GameInput, native DualSense detection) sees a generic, unidentified gamepad → no
|
||||
PS5 path.
|
||||
|
||||
**⇒ The fix must put `VID_054C&PID_0CE6` into the device-interface path and the `HID\VID&PID` hardware-ids**
|
||||
(give the device a real-USB-like PnP identity), not merely correct `HIDD_ATTRIBUTES`. See "Fix options".
|
||||
|
||||
**Secondary driver gaps found (not the detection blocker, but fix while here):**
|
||||
- `IOCTL_HID_GET_STRING` (id 4, ioctl `0x000b0013`) returns `STATUS_NOT_IMPLEMENTED` — a game polls it
|
||||
repeatedly (seen live in `pfds-driver.log`). Implement manufacturer / product / serial strings
|
||||
(`"DualSense Wireless Controller"`, a serial). Native PS5 code can read the serial to tell USB from BT.
|
||||
- `DS_FEATURE_CALIBRATION` is **42** bytes but the report descriptor declares feature `0x05` as **41**
|
||||
(`0x95 0x28` = 40 data + 1 id). Trim to 41 (motion-only; SDL accepts it regardless).
|
||||
|
||||
## Fix — implemented & validated at the identity layer (2026-06-22)
|
||||
|
||||
`create_swdevice` (`inject/dualsense_windows.rs`) now sets, via **`SW_DEVICE_CREATE_INFO` struct fields**
|
||||
(NOT `pProperties` — empirically a `DEVPROPERTY` write of these PnP-owned identity keys is ignored; the
|
||||
create-time struct fields are the supported lever, confirmed on `.173`):
|
||||
- **`pszzCompatibleIds`** = `USB\VID_054C&PID_0CE6`, `USB\Class_03&SubClass_00&Prot_00`, `USB\Class_03`
|
||||
(Windows appends `SWD\Generic`). HIDAPI/SDL/libScePad walk HID-child → `CM_Get_Parent` → this parent's
|
||||
CompatibleIds and string-match `"USB"` → **`bus_type` now resolves to USB** (was UNKNOWN).
|
||||
- **`pszzHardwareIds`** = `pf_dualsense` **first** (so the INF still binds our UMDF driver), then
|
||||
`USB\VID_054C&PID_0CE6&REV_0100`, `USB\VID_054C&PID_0CE6`. hidclass then derives the real-DS5 child ids
|
||||
**`HID\VID_054C&PID_0CE6[&REV_0100]`** (previously only `HID\VID_054C&UP:0001_U:0005`).
|
||||
- **`pContainerId`** = a deterministic per-pad GUID `{50464453-0000-0000-0000-00000000000<idx>}` ("PFDS")
|
||||
(avoids the null-sentinel-ContainerId `xinput1_4` slot-skip bug; groups the pad's devnodes).
|
||||
|
||||
**Validated live** (real shipping path, `dualsense-windows-test --index 1` alongside the running service's
|
||||
pad 0): INF still binds (`Service=MsHidUmdf`), parent CompatibleIds/HardwareIds + per-pad ContainerId set,
|
||||
the HID child gains `HID\VID_054C&PID_0CE6`, and the HIDAPI parent-walk reports **bus_type=USB**.
|
||||
SDL / RawInput / WGI `RawGameController` identity stays correct (054C:0CE6).
|
||||
|
||||
**Remaining gap (NOT fixed by the above): GameInput VID/PID still reads 0.** GameInput parses VID/PID from
|
||||
the HID child's **instance path** (`HID\punktfunk\1&…`), which carries no `VID_…&PID_…` token; neither
|
||||
CompatibleIds nor HardwareIds change the instance path. Only a real USB-bus instance path
|
||||
(`HID\VID_054C&PID_0CE6\…`) does — i.e. a **ViGEm-style KMDF USB-emulating bus driver** (the rank-3, last
|
||||
resort). Pursue only if a target title uses GameInput AND the identity fix above doesn't satisfy it; prior
|
||||
art (HIDMaestro) shows pure user-mode pads ARE accepted by WGI/GameInput, so other parity (descriptor /
|
||||
strings / mapping) may matter more than a genuine USB bus.
|
||||
|
||||
## Next steps
|
||||
|
||||
> **Deployed to `.173` (2026-06-22):** the host identity fix is live in the `PunktfunkHost` service (release
|
||||
> rebuilt + restarted) and the driver fixes are installed + signed (`oem74.inf`, `punktfunk-ds-test` cert).
|
||||
> The box is ready for the decisive on-glass test. A rollback copy of the prior driver is at
|
||||
> `C:\Users\Public\giprobe\driver-backup-oem74`.
|
||||
|
||||
1. **Decisive on-glass test (only the user can run):** launch Cyberpunk 2077 with Steam Input OFF against a
|
||||
virtual DS5 carrying the new identity; check the in-game glyphs/prompt switch to DualSense. Cleanest
|
||||
single-pad test (frees the service's pad 0 so only the new-identity pad is present):
|
||||
`sc stop PunktfunkHost` → `target\debug\punktfunk-host.exe dualsense-windows-test --index 0 --seconds 600`
|
||||
(new identity + live cycling Cross/stick), launch the game; then deploy the release + restart with
|
||||
`scripts\windows\deploy-host.ps1`.
|
||||
2. **Driver-side correctness — DONE & installed (2026-06-22).** Rebuilt/resigned/reinstalled per the recipe
|
||||
below; validated live (`hidstrings` probe + `pfds-driver.log`):
|
||||
- `IOCTL_HID_GET_STRING` now implemented (was `STATUS_NOT_IMPLEMENTED`). **Discovery:** Windows polls
|
||||
this device's string slots with low-word ids **`0x0E`/`0x0F`/`0x10`** (lang `0x0409`) cyclically — NOT
|
||||
the `0/1/2` `HID_STRING_ID_*` constants. The handler maps them (+ `0/1/2` as fallbacks):
|
||||
`0x0E`→manufacturer "Sony Interactive Entertainment", `0x0F`→product "DualSense Wireless Controller",
|
||||
`0x10`→serial "35533AD6E774" (the `0x09` pairing-report MAC). Verified: `HidD_GetManufacturer/Product/
|
||||
SerialNumberString` now return those three distinct strings.
|
||||
- `DS_FEATURE_CALIBRATION` trimmed 42 → 41 bytes (1 id + 40 data) to match the descriptor's feature
|
||||
`0x05` (`0x95 0x28`).
|
||||
- The repo source (`packaging/windows/dualsense-driver/src/lib.rs`) and the m0 build copy were diverged
|
||||
by *formatting only*; they are now back in sync (the repo file was copied to m0 before building).
|
||||
3. If a GameInput-only title needs the real VID/PID → the rank-3 KMDF USB-emulating bus driver.
|
||||
|
||||
## On-box experiment tooling (built 2026-06-22, `C:\Users\Public\giprobe\`)
|
||||
- `probe.cpp` (+`build.bat`) — GameInput enumeration/fingerprint via `LoadLibrary("GameInput.dll")` +
|
||||
`GameInputCreate`/`RegisterDeviceCallback` (GDK header). Prints each device's vid/pid/usage/counts —
|
||||
this is what proved GameInput reads our pad as vid=0.
|
||||
- `swexp.cpp` (+`build-swexp.bat`) — standalone `SwDeviceCreate` identity experiment: variations for
|
||||
`pszzCompatibleIds` (struct field) vs `DEVPKEY_Device_CompatibleIds` (pProperties — ignored),
|
||||
`pszzHardwareIds` USB ids, `pContainerId`. Create at a spare instance id, hold, inspect. Built with the
|
||||
VS18 MSVC toolchain via `vcvars64.bat`.
|
||||
- WGI probe: Windows PowerShell **5.1** WinRT projection of `RawGameController`/`Gamepad` (pump the message
|
||||
loop; subscribe `RawGameControllerAdded` to kick enumeration).
|
||||
- Parent-walk bus check: from the HID child, `DEVPKEY_Device_Parent` → that node's
|
||||
`DEVPKEY_Device_CompatibleIds`, match `^USB`/`^BTH` — mirrors HIDAPI's `hid_internal_detect_bus_type()`.
|
||||
- NOTE: the agent shell's PowerShell tool chokes on inline `@'…'@` here-strings feeding `Add-Type` (throws
|
||||
a spurious "Remove-Item on system path '/' is blocked"); write C#/scripts to a file and run them instead.
|
||||
|
||||
## How to reproduce / iterate (on `.173`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user