Files
punktfunk/docs/windows-dualsense-game-detection.md
T
enricobuehler e919fa6a2e
apple / swift (push) Successful in 57s
android / android (push) Failing after 43s
ci / rust (push) Failing after 30s
ci / web (push) Successful in 33s
ci / docs-site (push) Successful in 52s
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 5s
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 5s
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 4s
ci / bench (push) Successful in 4m39s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m22s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m22s
docker / deploy-docs (push) Successful in 17s
windows-host / package (push) Successful in 2m51s
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 <noreply@anthropic.com>
2026-06-22 10:34:58 +00:00

9.3 KiB

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)]):

$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 (GameInputCreateEnumerateDevices) — 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-nativecargo build -p punktfunk-host (debug for dualsense-windows-test; --release --features nvenc is what the service runs). Env: set CMAKE_POLICY_VERSION_MINIMUM=3.5 (audiopus_sys). bun=C:\Users\Public\bun, a standalone 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 maketarget\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.