7b99b41ede
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>
158 lines
11 KiB
Markdown
158 lines
11 KiB
Markdown
# Windows host — virtual DualSense scoping
|
||
|
||
> **Status:** SHIPPED — M0 feasibility gate PASSED (2026-06-21), M1–M4 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
|
||
250–560/yr, 1–7 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.
|