9af8e9a7d9
apple / swift (push) Successful in 54s
ci / rust (push) Failing after 30s
ci / web (push) Successful in 43s
android / android (push) Successful in 3m21s
ci / docs-site (push) Successful in 53s
deb / build-publish (push) Successful in 2m17s
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 4s
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 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
ci / bench (push) Successful in 4m39s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m29s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m4s
docker / deploy-docs (push) Successful in 6s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
9.5 KiB
Markdown
135 lines
9.5 KiB
Markdown
# Windows virtual DualSense — game detection handoff
|
|
|
|
Goal: get the host's virtual DualSense **detected and usable in games** (Cyberpunk's native PS5 path +
|
|
others) on the Windows host. This doc is the portable handoff (the investigation lives here, not in any
|
|
one agent's memory). Run the experiments **on the Windows host** (`.173`, repo at
|
|
`C:\Users\Public\punktfunk-native`).
|
|
|
|
## Status (2026-06-22)
|
|
|
|
- **Input works.** Client → host → virtual DualSense → games read input. Verified in Steam's controller
|
|
test (buttons/sticks).
|
|
- **The HID is a CORRECT, COMPLETE DualSense.** An SDL3 probe reports our live device as
|
|
`name='DualSense Wireless Controller' vid=0x054C pid=0x0CE6 isGamepad=True gamepadType=PS5`. SDL =
|
|
HIDAPI = what Steam (and many games) build on → that's why Steam works. So the report descriptor,
|
|
feature reports, and identity are right; this is **not** a descriptor/feature-report problem.
|
|
- **Cyberpunk's native DualSense path does NOT detect it at all.** (Steam Input was off — Cyberpunk was
|
|
reading the raw HID.)
|
|
- **Rumble:** host-side is proven working (driver captures the game's `0x02`, `parse_ds_output` extracts
|
|
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)
|
|
|
|
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.)
|
|
|
|
## How to reproduce / iterate (on `.173`)
|
|
|
|
### 1. Spawn a live virtual DualSense to test against
|
|
```
|
|
C:\Users\Public\punktfunk-native\target\debug\punktfunk-host.exe dualsense-windows-test --seconds 60
|
|
```
|
|
Creates `SWD\PUNKTFUNK\PF_PAD_0` (+ its HID child) and holds it, pushing a cycling input. Or just connect
|
|
a client — the real session creates the identical device. (Build with the env `CMAKE_POLICY_VERSION_MINIMUM=3.5`.)
|
|
|
|
### 2. SDL3 detection oracle (already set up: `C:\Users\Public\sdltest\SDL3.dll`)
|
|
Confirms HID-level recognition (HIDAPI). Run while a device from step 1 is live. PowerShell + C# (note:
|
|
PS 5.1's Add-Type is C# 5 — **no** interpolated strings, **no** inline `out` vars, **no**
|
|
`Marshal.PtrToStringUTF8`; SDL3 bools are 1 byte → `[return: MarshalAs(UnmanagedType.I1)]`):
|
|
```powershell
|
|
$cs = @'
|
|
using System; using System.Runtime.InteropServices; using System.Text;
|
|
public static class S {
|
|
const string D = @"C:\Users\Public\sdltest\SDL3.dll";
|
|
[DllImport(D)][return: MarshalAs(UnmanagedType.I1)] public static extern bool SDL_Init(uint f);
|
|
[DllImport(D)] public static extern IntPtr SDL_GetJoysticks(out int c);
|
|
[DllImport(D)] public static extern IntPtr SDL_GetJoystickNameForID(uint id);
|
|
[DllImport(D)] public static extern ushort SDL_GetJoystickVendorForID(uint id);
|
|
[DllImport(D)] public static extern ushort SDL_GetJoystickProductForID(uint id);
|
|
[DllImport(D)][return: MarshalAs(UnmanagedType.I1)] public static extern bool SDL_IsGamepad(uint id);
|
|
[DllImport(D)] public static extern IntPtr SDL_OpenGamepad(uint id);
|
|
[DllImport(D)] public static extern int SDL_GetGamepadType(IntPtr g);
|
|
static string U(IntPtr p){ if(p==IntPtr.Zero)return""; int n=0; while(Marshal.ReadByte(p,n)!=0)n++; byte[] b=new byte[n]; Marshal.Copy(p,b,0,n); return Encoding.UTF8.GetString(b); }
|
|
public static string Run(){ if(!SDL_Init(0x2000))return"init fail"; System.Threading.Thread.Sleep(1500);
|
|
int n=0; IntPtr a=SDL_GetJoysticks(out n); StringBuilder sb=new StringBuilder("joysticks: "+n+"\n");
|
|
for(int i=0;i<n;i++){ uint id=(uint)Marshal.ReadInt32(a,i*4); bool ig=SDL_IsGamepad(id); int t=ig?SDL_GetGamepadType(SDL_OpenGamepad(id)):-1;
|
|
sb.AppendLine(" '"+U(SDL_GetJoystickNameForID(id))+"' vid=0x"+SDL_GetJoystickVendorForID(id).ToString("x4")+" pid=0x"+SDL_GetJoystickProductForID(id).ToString("x4")+" isGamepad="+ig+" type="+t+" (PS5=6)"); }
|
|
return sb.ToString(); }
|
|
}
|
|
'@
|
|
Add-Type -TypeDefinition $cs; [S]::Run()
|
|
```
|
|
Expected today: it lists our device with `type=6` (PS5). That's the baseline "HID is correct".
|
|
|
|
## Next experiments — MUST run ON THE INTERACTIVE DESKTOP, not over SSH
|
|
|
|
WGI / RawInput / GameInput enumeration returns **empty from a headless SSH session** (no window/message
|
|
pump) — only HIDAPI works headless. So these must run in the logged-in desktop session (RDP in, or run
|
|
locally) while a DualSense session is live:
|
|
|
|
1. **Determine which API Cyberpunk uses and whether it sees the SWD device.** Enumerate via, separately:
|
|
- `Windows.Gaming.Input` (`RawGameController.RawGameControllers`, `Gamepad.Gamepads`),
|
|
- RawInput (`GetRawInputDeviceList` → filter HID gamepad usage 01/05),
|
|
- GameInput (`GameInputCreate` → `EnumerateDevices`) — `GameInputRedistService` is installed on `.173`.
|
|
Compare which list our `VID_054C&PID_0CE6` appears in. The one(s) it's *missing from* point at the API
|
|
Cyberpunk uses.
|
|
2. **If WGI/GameInput exclude it:** make the SwDeviceCreate device enumerate more like a real USB device.
|
|
`SwDeviceCreate` takes a `pProperties` (`DEVPROPERTY[]`) array — try setting bus-type / container-id /
|
|
compatible-IDs so the newer APIs accept it. If that's insufficient, the heavyweight option is a
|
|
USB-emulating bus driver (the way ViGEmBus presents a real-looking device) instead of SwDeviceCreate +
|
|
UMDF-HID.
|
|
3. **Rule out an XInput device taking priority** (a leftover ViGEm pad, etc.).
|
|
4. **Correctness (not the detection blocker):** `DS_FEATURE_CALIBRATION` in the driver is **42 bytes**
|
|
but the report descriptor declares feature `0x05` as **41** (1 id + 40 data, `0x95 0x28`). Trim to 41;
|
|
wrong calibration only affects motion, and SDL accepts the device regardless.
|
|
|
|
## On-box layout (`.173`, builds + tools)
|
|
|
|
- **Host repo / build:** `C:\Users\Public\punktfunk-native` → `cargo build -p punktfunk-host`
|
|
(debug for `dualsense-windows-test`; `--release --features nvenc` is what the service runs). The
|
|
build env is persisted Machine-scope (`PUNKTFUNK_NVENC_LIB_DIR`, `LIBCLANG_PATH`,
|
|
`CMAKE_POLICY_VERSION_MINIMUM`) — see `scripts\windows\`. **One-call rebuild+redeploy of the
|
|
service: `scripts\windows\deploy-host.ps1`** (stop → build → restart, `.bak` rollback); web:
|
|
`scripts\windows\build-web.ps1`. bun=`C:\Users\Public\bun`, node=`C:\Users\Public\node-v22.11.0-win-x64`.
|
|
- **Host service:** scheduled task / SCM `PunktfunkHost` runs `…\target\release\punktfunk-host.exe
|
|
service run` → spawns `serve` (currently native-only, `PUNKTFUNK_HOST_CMD=serve` in
|
|
`C:\ProgramData\punktfunk\host.env`). Restart: `sc stop/start PunktfunkHost`. Native port 9777, mgmt
|
|
47990. (NB: Sunshine/Apollo conflicts on the GameStream ports — keep it stopped, or run native-only.)
|
|
- **UMDF driver build project:** `C:\Users\Public\m0\windows-drivers-rs\examples\pf-dualsense`
|
|
(`pf_dualsense.inx` + `src\lib.rs` live here; the canonical copies are in the repo under
|
|
`packaging/windows/dualsense-driver/` — keep them in sync). Rebuild + reinstall recipe (e.g. after the
|
|
calibration fix), all from that dir, env `LIBCLANG_PATH=C:\Program Files\LLVM\bin`,
|
|
`Version_Number=10.0.26100.0`:
|
|
1. `cargo make` → `target\debug\pf_dualsense_package\`
|
|
2. **Clear the FORCE_INTEGRITY PE bit** (wdk-build sets `/INTEGRITYCHECK`, which blocks self-signed
|
|
load): clear bit 0x80 at `PE_header_offset+0x5e` of `pf_dualsense.dll`, then re-sign.
|
|
3. `signtool sign /fd SHA256 /sha1 6A52984E54376C45A1C236B1A2C8A746C5AB6131 pf_dualsense.dll`
|
|
4. `Inf2Cat /driver:<pkg> /os:10_x64` → re-sign the `.cat` with the same thumbprint.
|
|
5. `pnputil /delete-driver <old oemNN.inf> /uninstall /force` then `pnputil /add-driver
|
|
pf_dualsense.inf /install`. (Self-signed cert is already trusted on `.173`; Secure Boot ON, HVCI off.)
|
|
- **SDL oracle:** `C:\Users\Public\sdltest\SDL3.dll`. **Test device:** `punktfunk-host.exe
|
|
dualsense-windows-test --seconds N` creates one `SWD\PUNKTFUNK\PF_PAD_0` and holds it.
|
|
|
|
## Key code
|
|
|
|
| What | File |
|
|
| --- | --- |
|
|
| Host backend (`create_swdevice`, the `Global\pfds-shm-<idx>` section, write_state/service/pump) | `crates/punktfunk-host/src/inject/dualsense_windows.rs` |
|
|
| UMDF driver (HID descriptor, feature reports, `on_output_report`) | `packaging/windows/dualsense-driver/src/lib.rs` |
|
|
| Shared report codec (`serialize_state` input, `parse_ds_output` feedback) | `crates/punktfunk-host/src/inject/dualsense_proto.rs` |
|
|
| Pad seam (`PadBackend`, `pump` → rumble `0xCA` / hidout `0xCD`) | `crates/punktfunk-host/src/punktfunk1.rs` |
|
|
|
|
## Facts proven (don't re-litigate)
|
|
- `SwDeviceCreate` requirements: enumerator must have **no underscore** (`punktfunk`); the completion
|
|
**callback is mandatory** (NULL → E_INVALIDARG). Per-session device works; auto-removed on disconnect.
|
|
- HID descriptor + feature reports are DS5-accurate enough that **SDL identifies it as PS5**.
|
|
- Host-side rumble works end to end; the client (macOS) rendering of `0xCA` is the open rumble bug.
|