Files
punktfunk/design/windows-dualsense-scoping.md
T
enricobuehler 7b99b41ede docs(design): trim shipped plans, consolidate cluster, add index
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>
2026-06-26 16:39:06 +00:00

11 KiB
Raw Permalink Blame History

Windows host — virtual DualSense scoping

Status: SHIPPED — M0 feasibility gate PASSED (2026-06-21), M1M4 landed. Driver: packaging/windows/drivers/pf-dualsense/ (README there); host backend crates/punktfunk-host/src/inject/dualsense_windows.rs + shared contract inject/dualsense_proto.rs. Commits aa159df (Rust UMDF driver + shm channel), 4a73102 (host backend), fde438a/6db3525 (SwDeviceCreate per-session devnode), b0c8233 (pure-user-mode DS4/Xbox 360, ViGEm dropped). This doc is trimmed to design rationale + open items; implementation detail lives in the code and the driver README.

Why UMDF2, and why a real virtual DualSense (the WHY)

Apollo's backlog "DS4 ViGEm target on Windows" is the wrong target for actual DualSense. ViGEmBus emulates only Xbox 360 (XUSB) and DualShock 4 — never a DualSense. Because this is a host-side virtual pad, the DualSense-defining features (adaptive triggers, the fine haptic actuators, DS5 identity) only work end-to-end if the game sees a real DualSense and therefore drives them. A DS4 virtual pad makes the game take its DS4 code path and never emit those commands, so the client's adaptive-trigger rendering is never exercised. ViGEm DS4 structurally cannot deliver adaptive triggers — that ceiling is the whole reason not to copy Apollo here (and Apollo itself does DualSense only on Linux via inputtino; its Windows path is ViGEm XUSB/DS4_REPORT_EX only — zero virtual-HID/DualSense code to vendor).

The right path is the Windows analog of the Linux host's /dev/uhid device: present a real virtual DualSense HID device (Sony VID 054C / PID 0CE6, the inputtino PS5 report descriptor we already ship) so the game/Steam/GameInput bind it as genuine.

Mechanism = a UMDF2 (user-mode) HID minidriver, created/torn-down per session via SwDeviceCreate, as a lower filter under the OS pass-through driver mshidumdf.sys. This is the same driver tier as SudoVDA (UMDF, not kernel), so the existing vendor → sign → Inno-installer machinery applies almost unchanged. Two corrections drove this conclusion over the 2026-06-20 draft:

  • VHF (Virtual HID Framework) supports a HID source driver only in kernel mode — it is not the mechanism for a user-mode virtual pad. The user-mode mechanism is a UMDF2 HID minidriver built from the vhidmini2 sample. So the earlier "KMDF, a higher bar than SudoVDA" framing was wrong: it is the same UMDF tier.
  • UMDF 2.0 is NOT COM-based (COM/IDriverEntry/IWDFDriver are legacy UMDF 1.x). UMDF 2.0 uses the same C-style WDF object model as KMDF — a DriverEntry symbol + C function pointers, no vtable. This is precisely why a Rust FFI implementation is even conceivable.

Everything except the host backend was already platform-agnostic and DualSense-complete (protocol planes 0xCC/0xCA/0xCD, the HidOutput feedback abstraction, pad-type negotiation, clients, the C-ABI). The DualSense HID contract (the 232-byte DUALSENSE_RDESC, serialize_state for input report 0x01, parse_ds_output for output report 0x02, the 0x05/0x09/0x20 feature blobs, USB framing no-CRC) was already pure transport-independent Rust — so the report bytes are identical to Linux and only the device-framing layer is new.

Why Rust ("Option R") despite zero precedent

The user's strong preference was a self-authored Rust driver, accepted as pioneering risk. microsoft/windows-drivers-rs officially targets UMDF and ships a real (but bare-stub) UMDF sample; because UMDF 2.0 is the C function-pointer model, the FFI maps cleanly. The honest gap going in: the whole HID-minidriver layer (WdfFdoInitSetFilter, the manual inverted-call queue, IOCTL_UMDF_HID_* dispatch, HID_XFER_PACKET) was hand-written unsafe FFI with no safe wrappers, and every other shipping virtual-HID controller driver (vhidmini2, HIDMaestro, DsHidMini) is C — so symbol coverage for the UMDF target was unproven. The de-risk plan was a C vhidmini2 shim fallback (keeping all DualSense logic in the Rust host either way), with forking HIDMaestro as the last resort (rejected for real use because HIDMaestro omits adaptive triggers — it cannot prove the one thing that makes a virtual DualSense worth building).

Outcome: Option R confirmed. The M0 spike answered both the build-symbol question and the on-glass gate with a Rust driver — no C shim needed. The DualSense logic stays in Rust where it already lived.

M0 feasibility gate — PASSED (2026-06-21), and the three bugs

The blocking gate (RTX box 192.168.1.173; the dev VM is headless/WARP and cannot validate game-facing HID recognition) asked two questions no prior art settled:

  1. Recognition — is a virtual 054C:0CE6 UMDF2 device accepted as a genuine DualSense by Windows.Gaming.Input / GameInput / Steam? YES — Steam recognized it and drove its DualSense-specific LEDs.
  2. Adaptive-trigger fidelity — does the game's output report 0x02 (the adaptive-trigger block) actually reach the driver's WriteReport/SetOutputReport callback? YES — captured two Steam-Input output reports (validFlag1=0x14 = LIGHTBAR|PLAYER_INDICATOR). Adaptive-trigger bytes ride the same 0x02 path.

Three M0 bugs — reference for any future UMDF-in-Rust work:

  1. PE FORCE_INTEGRITY blocks self-signed load. wdk-build's /INTEGRITYCHECK sets the PE FORCE_INTEGRITY bit, which demands a Microsoft-trusted signature to load. Fix: clear bit 0x80 at offset PE+0x5e post-build and re-sign. This was the load wall (earlier "Secure Boot blocks self-signed UMDF" conclusions were wrong).
  2. Timer ExecutionLevel must be InheritFromParent, not zeroed. A mem::zeroed WDF_TIMER_CONFIG gives ExecutionLevel 0, which the framework rejects.
  3. Queue NumberOfPresentedRequests must be u32::MAX, not 0. A zeroed parallel-queue config caps in-flight requests at 0 → EvtIoDeviceControl never fires.

Milestones

# Milestone State Commit(s)
M0 Feasibility spike (Rust UMDF build + on-glass recognition + 0x02 callback) SHIPPED driver aa159df
M1 Extract transport-independent contract into inject/dualsense_proto.rs (DUALSENSE_RDESC, serialize_state, parse_ds_output, feature blobs; calibration trimmed 42→41) SHIPPED 4a73102
M2 UMDF2 HID minidriver + INF + signed .cat (authored in Rust) SHIPPED aa159df
M3 Rust host bridge inject/dualsense_windows.rs (DualSenseWindowsManager over Global\pfds-shm-<idx>; SwDeviceCreate per-session devnode) SHIPPED 4a73102, fde438a, 6db3525
M4 Un-gate the PadBackend::DualSense seam + GamepadPref::DualSense resolution on Windows; ViGEm dropped (pure user-mode DS4/Xbox 360 too) SHIPPED b0c8233

A SwDeviceCreate gotcha surfaced during M3 and is worth keeping: two E_INVALIDARG causes were found — (1) an underscore in the enumerator name (pf_dualsense → must be punktfunk), and (2) passing the completion callback was rejected; the INF lists both root\pf_dualsense (devgen) and pf_dualsense (SwDevice) and the host falls back to an out-of-band devnode when per-session create fails.

Decision matrix (condensed)

Option Adaptive triggers / DS5 identity Effort When it's right
A. UMDF2 virtual DualSense (shipped) full medium — UMDF, same tier as SudoVDA the goal — matches the Linux host
B. ViGEm DS4 never (DS4 ceiling) small quick PS-pad, no adaptive triggers — rejected, ViGEm removed
C. Hybrid A for DS5, Xbox 360 fallback A + small belt-and-suspenders (Xbox 360/XInput still covers most games)
D. Defer would have applied only if the M0 0x02 gate had failed

Xbox 360 (XInput) covers most Windows games regardless; Xbox One/Series fold into it on Windows.

Risk register (condensed)

Risk Status
Output 0x02 never reaches the driver write callback (fatal to value prop) resolved — M0 measured it directly, YES
054C:0CE6 not accepted as a real DualSense resolved — Steam recognizes it
Rust UMDF pioneering risk (no safe WDF/HID wrappers; symbol coverage) resolved — Rust driver shipped, no C shim
SwDeviceCreate device lifetime tied to host process handle accepted — hold HSWDEVICE for the session (matches Linux UHID fd semantics)
windows-drivers-rs transient toolchain breaks (LLVM-22 bindgen, Disc. #591) low — pin LLVM 21.1.2
EV cert + Partner Center attestation lead time / friction open (see below)

Open items

  1. Public-distribution signing — EV cert + Microsoft attestation. The fleet/self-signed recipe (bundled .cer → machine Root + TrustedPublisher via certutil -addstore -f, then pnputil /add-driver /install, cloned from install-sudovda.ps1) works for dev/internal boxes only — Microsoft is explicit it "should never be followed for any driver package distributed outside your organization." For arms-length public users the minimal correct path is Microsoft attestation signing via Partner Center (it re-signs the .cat with a Microsoft cert → silent PnP install, no publisher prompt, no Root-store import). A bare Authenticode/OV/EV signature is not sufficient: it installs but with the blocking "would you like to install this device software?" prompt (setupapi 0x800b0109 / 0xe0000242). Attestation needs a registered Windows Hardware Developer Program (Partner Center) account and an EV code-signing cert (FIPS hardware token, ~USD 250560/yr, 17 day vetting) to register and to sign the submission CAB. UMDF is exempt from kernel-mode load enforcement so the .dll loads unsigned, but installation still needs a trusted catalog. The EV key is non-exportable → CAB signing + submission is a manual offline step, not a CI secret; vendor the Microsoft-resigned .cat like SudoVDA's. (Azure Trusted Signing cannot substitute — it signs only user-mode PE//INTEGRITYCHECK/SmartScreen, not the driver .cat.) Blocks public release; dev/fleet self-signed works today.

  2. GameInput API detection reads VID/PID as 0x0000. The GameInput path does not pick up the 054C:0CE6 identity (reads 0x0000); may require the KMDF USB-emulating bus driver rather than the root-enumerated UMDF HID device. Tracked in design/windows-dualsense-game-detection.md.

  3. HidHide integration — unclear value on a usually-headless host; only relevant when a physical pad is also attached. Decide whether to bundle/integrate at all.

  4. Minimum-OS / UMDFVERSION targeting decision — which UmdfLibraryVersion / WDK to target for the widest Win10/11 install base, consistent with punktfunk's existing host support matrix.

  5. Single multi-driver CAB — can one Partner Center submission carry both SudoVDA and the DualSense driver? Multi-driver CABs are supported in general; unverified for this account.

  6. DsHidMini end-user signing tier — self-signed vs attestation in its WixSharp MSI, useful as a second public-distribution data point.