From e919fa6a2ea39488d4d09f9f9c278de93ab2f141 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 22 Jun 2026 10:34:58 +0000 Subject: [PATCH] docs(windows): DualSense in-game detection handoff The virtual DualSense is a correct, complete DS5 at the HID level (SDL3 reports PS5) and input works, but a game's native DualSense path (Cyberpunk) doesn't detect the software-enumerated (SWD) device that SDL/HIDAPI accept. Captures the diagnosis, the on-box layout + tools (SDL oracle, dualsense-windows-test, driver rebuild recipe), and the on-glass next experiments (WGI/RawInput/GameInput enumeration) so the work continues from any machine without agent memory. Co-Authored-By: Claude Opus 4.8 --- docs/windows-dualsense-game-detection.md | 132 +++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/windows-dualsense-game-detection.md diff --git a/docs/windows-dualsense-game-detection.md b/docs/windows-dualsense-game-detection.md new file mode 100644 index 0000000..a5d70ab --- /dev/null +++ b/docs/windows-dualsense-game-detection.md @@ -0,0 +1,132 @@ +# 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 /os:10_x64` → re-sign the `.cat` with the same thumbprint. + 5. `pnputil /delete-driver /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-` 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.