# 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-`; `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.