Much of design/ described work that has since shipped. Trim each doc to
its durable rationale + still-open items (the code is the source of truth
for shipped detail; git history holds the full originals).
- Shipped plans -> status stubs: stats-capture, gamestream-host-plan,
apple-stage2-presenter, windows-service.
- Trimmed completed-out / open-kept: implementation-plan, hdr-pipeline,
host-latency, gpu-contention (fixed stale status table), game-library,
linux-setup (fixed m0->spike + stale zero-copy claim),
session-aware-host-followups, windows-client-bootstrap,
windows-dualsense-{scoping,game-detection}, windows-virtual-display,
security-review (per-finding status table; #12 still open),
apollo-comparison (shipped backlog collapsed to one-liners).
- Windows-host cluster consolidated: windows-host.md -> redirect into
windows-host-rewrite.md (whose stale scorecard is corrected -- goal1 is
merged, M4 done); windows-secure-desktop.md archived (now a fallback
behind IDD-push primary).
- Kept evergreen: ci.md, gamescope-multiuser.md, windows-build-and-packaging.md.
- New design/README.md: per-doc status table + consolidated open-items
roll-up so nothing is tracked in only one buried doc.
- Repoint 5 code comments to the archived secure-desktop doc path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.7 KiB
Windows virtual DualSense — game detection handoff
Status: Identity fix SHIPPED (commits
6db3525,aa159df,4a73102) —crates/punktfunk-host/src/inject/windows/dualsense_windows.rs(create_swdevice). This doc is trimmed to the root-cause analysis, the SwDeviceCreate identity rationale, the GameInput fallback design, and the still-open on-glass Cyberpunk verification. The implementation walkthrough, probe tooling, and the (now-fixed) secondary driver gaps are cut — the shipped code is the source of truth.
Goal: get the host's virtual DualSense detected and usable in games (Cyberpunk's native PS5 path +
others) on the Windows host. Run the decisive experiments on the interactive desktop of the Windows host
(.173) — not over SSH.
Where it works / where it doesn't
- Input works. Client → host → virtual DualSense → games read input (verified in Steam's controller test).
- The HID is a CORRECT, COMPLETE DualSense. SDL3 reports the 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. 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). This is the problem the identity fix targets; on-glass confirmation is still open.
Root cause — the PnP identity, not the HID descriptor (CONFIRMED, run live in console session 3)
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 matrix (the decisive experiment — impossible over SSH, run live in the console):
| 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. 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.
The fix — SwDeviceCreate identity (shipped)
create_swdevice sets the identity via SW_DEVICE_CREATE_INFO struct fields (NOT pProperties — a
DEVPROPERTY write of these PnP-owned identity keys is empirically 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 appendsSWD\Generic). HIDAPI/SDL/libScePad walk HID-child →CM_Get_Parent→ this parent's CompatibleIds and string-match"USB"→bus_typenow resolves to USB (was UNKNOWN).pszzHardwareIds=pf_dualsensefirst (so the INF still binds our UMDF driver), thenUSB\VID_054C&PID_0CE6&REV_0100,USB\VID_054C&PID_0CE6. hidclass then derives the real-DS5 child idsHID\VID_054C&PID_0CE6[&REV_0100](previously onlyHID\VID_054C&UP:0001_U:0005).pContainerId= a deterministic per-pad GUID{50464453-0000-0000-0000-00000000000<idx>}("PFDS") — avoids the null-sentinel-ContainerIdxinput1_4slot-skip bug, and 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).
Why this may still not satisfy GameInput / a native PS5 path: 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 (see fallback below). 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.
GameInput fallback design (rank-3, only if needed)
If a target title uses GameInput AND the shipped identity fix above doesn't satisfy it, the last-resort
option is a rank-3 KMDF USB-emulating bus driver (the way ViGEmBus presents a real-looking device)
instead of SwDeviceCreate + UMDF-HID — it produces a genuine HID\VID_054C&PID_0CE6\… instance path, the one
thing GameInput keys off. Pursue this only if required by a target title; it is heavier than the user-mode
path and HIDMaestro suggests user-mode pads can be made acceptable to GameInput without it.
On-glass Cyberpunk verification procedure (open — only the user can run it)
Must run on the interactive desktop (RDP in or run locally) — WGI / RawInput / GameInput enumeration returns empty from a headless SSH session (no window/message pump); only HIDAPI works headless.
- Free the service's pad 0 so only the new-identity pad is present:
sc stop PunktfunkHost - Spawn a single virtual DS5 carrying the new identity (cycles Cross/stick so input is visible):
target\debug\punktfunk-host.exe dualsense-windows-test --index 0 --seconds 600 - Launch Cyberpunk 2077 with Steam Input OFF (so the game reads the raw HID). Check the in-game glyphs/prompt switch to DualSense.
- Restore the service afterward: redeploy the release + restart with
scripts\windows\deploy-host.ps1.
Key code
| What | File |
|---|---|
Host backend (create_swdevice, the Global\pfds-shm-<idx> section, write_state/service/pump) |
crates/punktfunk-host/src/inject/windows/dualsense_windows.rs |
UMDF driver (HID descriptor, feature reports, on_output_report) |
packaging/windows/drivers/pf-dualsense/src/lib.rs |
Shared report codec (serialize_state input, parse_ds_output feedback) |
crates/punktfunk-host/src/inject/proto/dualsense_proto.rs |
Pad seam (PadBackend, pump → rumble 0xCA / hidout 0xCD) |
crates/punktfunk-host/src/punktfunk1.rs |
Open items
- Decisive on-glass Cyberpunk test — pending execution. Launch Cyberpunk 2077 with Steam Input OFF against a virtual DS5 carrying the new identity; verify the in-game glyphs switch to DualSense (procedure above).
- GameInput rank-3 KMDF USB-emulating bus-driver fallback — optional. Only if a GameInput-only title needs the real VID/PID and the shipped SwDeviceCreate identity fix doesn't satisfy it.
Facts proven (don't re-litigate)
SwDeviceCreaterequirements: enumerator must have no underscore (punktfunk); the completion callback is mandatory (NULL → E_INVALIDARG). Per-session device works; auto-removed on disconnect.- The identity keys must be set via the
SW_DEVICE_CREATE_INFOstruct fields, notpProperties— aDEVPROPERTYwrite of the PnP-owned identity keys is ignored. - HID descriptor + feature reports are DS5-accurate enough that SDL identifies it as PS5.
- The
IOCTL_HID_GET_STRINGandDS_FEATURE_CALIBRATION(42 → 41 bytes) driver gaps were fixed + shipped; the driver answersHidD_GetManufacturer/Product/SerialNumberStringwith distinct strings. (Detail inpackaging/windows/drivers/pf-dualsense/src/lib.rs.) - Host-side rumble works end to end (driver captures the game's
0x02,parse_ds_outputextracts the motors, host forwards0xCA); the client (macOS) rendering of0xCAonto the physical pad is a separate open bug, not part of game detection.