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

158 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](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.