# Windows host — virtual DualSense scoping **Status:** **M0 feasibility gate PASSED (2026-06-21)** — a self-authored **Rust** UMDF virtual DualSense loads self-signed under Secure Boot, is recognized as a genuine DualSense by Steam, and receives `0x02` output reports at its write callback. Driver source: `packaging/windows/dualsense-driver/`. (Earlier in this doc's history the gate looked blocked by Secure Boot / driver code-integrity — that was wrong; the real blocker was the PE FORCE_INTEGRITY bit that `wdk-build` sets via `/INTEGRITYCHECK`, cleared post-build.) Web-research pass complete; the mechanism conclusion is **reversed** from the 2026-06-20 draft. This doc **supersedes the 2026-06-20 VHF scoping** — VHF was the wrong answer (it is kernel-only and cannot host a user-mode HID source), and the correct mechanism is a **UMDF2 user-mode HID minidriver**, the same driver tier punktfunk already vendors/signs/installs for SudoVDA. Two product decisions are now fixed and drive this plan: **(1)** the driver is for **public end-user distribution** (so: EV cert + Microsoft attestation signing, not just the fleet self-signed recipe), and **(2)** the strong preference is a **self-authored Rust driver**, with a thin C/C++ shim as the realistic fallback and forking HIDMaestro as the last resort. ## TL;DR Apollo's backlog item #23/#89 ("DS4 ViGEm target on Windows") is the **wrong target** if the goal is *actual DualSense*. ViGEmBus emulates only **Xbox 360 (XUSB)** and **DualShock 4 (DS4)** — 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 means the game takes its DS4 code path and never emits those commands, so the client's adaptive-trigger rendering is never exercised. ViGEm DS4 structurally **cannot** deliver adaptive triggers. The right path is the Windows analog of what the Linux host already does over `/dev/uhid`: present a **real virtual DualSense HID device** (Sony VID `054C` / PID `0CE6`, the inputtino PS5 report descriptor punktfunk already ships). On Windows that is a **UMDF2 (user-mode) HID minidriver** — created/torn-down per session from the host via `SwDeviceCreate`, sitting as a lower filter under the OS pass-through driver `mshidumdf.sys`. It is the **same driver tier as SudoVDA** (UMDF, not kernel), so the existing vendor → sign → Inno-installer machinery applies almost unchanged. > **Supersedes the 2026-06-20 VHF scoping.** That draft concluded "a kernel-mode virtual-HID device > via the Virtual HID Framework (VHF) — a SudoVDA-class driver effort." The decisive correction: > **VHF supports a HID *source* driver only in kernel mode** (Microsoft "Virtual HID Framework > (VHF)"). A user-mode (UMDF) HID source is **not** a VHF use case — it is a UMDF2 HID minidriver > built from the `vhidmini2` sample (or DMF's `Dmf_VirtualHidMini`). The earlier "KMDF is a higher > bar than SudoVDA's UMDF/IddCx" framing is therefore wrong: the correct mechanism is **the same > UMDF tier as SudoVDA**, not above it. Everything except the host backend is already platform-agnostic and DualSense-complete (verified against live code), so this is a well-bounded host-side addition. **The whole effort is gated by an on-glass feasibility spike (M0)** that no prior art settles: whether a virtual `054C:0CE6` device is accepted as a genuine DualSense by `Windows.Gaming.Input` / GameInput / Steam **and** whether the game's output report `0x02` (the adaptive-trigger block) actually reaches the driver's write callback. ## Why this is the wrong place to copy Apollo Apollo (and all of Sunshine's lineage) **does DualSense only on Linux** (`inputtino`, `DualSenseWired`). Its Windows input path (`src/platform/windows/input.cpp`) is ViGEm `XUSB_REPORT` + `DS4_REPORT_EX` only — `MPS2_TO_DS4_ACCEL` motion conversion, inverse-ViGEmBus gyro calibration, DS4 touchpad packing. There is **zero** virtual-HID / DualSense code on Apollo's Windows side. So: - Copying Apollo on Windows gets us a **DS4**, with the adaptive-trigger ceiling baked in. - There is **no in-ecosystem upstream** (Sunshine/Apollo/Wolf) that already solved a virtual *DualSense* on Windows to vendor from. The closest prior art is in the **virtual-HID-controller** space, not the streaming-host space: HIDMaestro and Nefarius DsHidMini (see *Mechanism*). This is unchanged from the 2026-06-20 draft and remains correct. ## The parity target — and what's *already* done The Linux host (`crates/punktfunk-host/src/inject/dualsense.rs`) creates a **UHID** device presenting the genuine DualSense descriptor, so the kernel `hid-playstation` driver binds it and games see a real DualSense — gamepad + motion + touchpad + lightbar/player-LEDs + adaptive triggers. It writes HID **input** report `0x01` (controller state) and reads HID **output** report `0x02` (the game's rumble/LED/trigger feedback), which it forwards to the client as `punktfunk_core::quic::HidOutput`. Crucially, **everything except the host backend is already platform-agnostic and DualSense-complete** (verified against live source): | Layer | State | Where | |---|---|---| | Protocol planes (rich input `0xCC`, rumble `0xCA`, HID-output `0xCD`) | ✅ done | `punktfunk_core::quic` | | Feedback abstraction (`HidOutput::{Led,PlayerLeds,Trigger,…}`) | ✅ done | `punktfunk_core::quic` | | Pad-type negotiation (client pref > env > default), `GamepadPref::DualSense` | ✅ done | `punktfunk1.rs` `resolve_gamepad` (~1577) | | Backend dispatch (`enum PadBackend`) | ✅ done; `DualSense`/`DualShock4` arms are `#[cfg(target_os="linux")]` | `punktfunk1.rs` (PadBackend ~1181–1272) | | Clients (capture + adaptive-trigger/lightbar/haptic/touchpad/motion rendering) | ✅ done, all platforms | `clients/*` | | C-ABI (`next_hidout` / `send_rich_input`) | ✅ done | `abi.rs` | | **Host virtual-DualSense backend** | **Linux only (UHID)** | `inject/dualsense.rs` | So a Windows DualSense backend needs **no protocol, client, or C-ABI change**. The whole DualSense **HID contract already exists as pure, transport-independent Rust + const data**, kernel-verified byte-for-byte against `hid-playstation.c` / inputtino / SDL, in `inject/dualsense.rs`: - `DUALSENSE_RDESC` — the 232-byte USB report descriptor. - `serialize_state` — the input report `0x01` packer (controller state → bytes). - `parse_ds_output` — the output report `0x02` parser (game's rumble/LED/trigger block → `HidOutput`), valid-flag gated. - Feature blobs `0x05` calibration, `0x09` pairing, `0x20` firmware. **USB framing (no CRC).** **No hardware capture is needed** — the bytes are already correct and proven. The *only* Linux coupling is the `/dev/uhid` event framing (`UHID_CREATE2`/`INPUT2`/`OUTPUT`/`GET_REPORT`) in `DualSensePad::open`/`write_state`/`service`. A Windows backend swaps that framing for the `SwDeviceCreate` + IOCTL channel to the UMDF driver; **the report bytes are identical.** > **One in-repo bug to fix in passing:** `DS_FEATURE_CALIBRATION` (`0x05`) is currently **42 bytes**; > the spec is **41**. Trim it for strict Windows consumers as part of M1 (`42 → 41`). `dualshock4.rs` (committed `3e6c9f6`) is a worked **second** example of the multi-pad-type `PadBackend` pattern, reusing the DualSense state — a template for how the Windows arm slots in. The host integration seam is small and already mapped: ~1 enum arm + 5 match arms in the `PadBackend` block (`punktfunk1.rs` ~1181–1272), flipping `pick_gamepad`/`resolve_gamepad` (~1558–1606) from `#[cfg(target_os = "linux")]` to `#[cfg(any(target_os = "linux", target_os = "windows"))]`, plus the `inject.rs` module gating (~424–451). `gamepad_windows.rs` is today ViGEm-Xbox360-only (138 LOC); the new `inject/dualsense_windows.rs` sits beside it, and ViGEm stays for Xbox 360 / Xbox One. ## The Windows mechanism — UMDF2 HID minidriver (not VHF) Windows has **no userspace HID-device creation** (unlike Linux UHID), so a real virtual DualSense needs a driver component. The decisive correction over the prior 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. (Microsoft, "Virtual HID Framework (VHF)".) - The user-mode mechanism is a **UMDF2 HID minidriver**: a small lower-filter driver under the OS-supplied pass-through driver **`mshidumdf.sys`** (which calls `HidRegisterMinidriver` on the minidriver's behalf). This is the **same UMDF tier as SudoVDA** — *below* kernel work, not above it. A second prior-research correction that matters for the language choice: **UMDF 2.0 is NOT COM-based.** COM / `IDriverEntry` / `IWDFDriver` belong to legacy **UMDF 1.x**. UMDF 2.0 uses the same **C-style WDF object model as KMDF** — a `DriverEntry` symbol plus C function pointers (`EvtDriverDeviceAdd`, `EvtIoDeviceControl`) stored in config structs. There is no vtable to implement. (Microsoft, "Porting a Driver from UMDF 1 to UMDF 2", "Getting Started with UMDF v2".) This is precisely why a Rust FFI implementation is even conceivable (see *Driver language*). ### What the driver actually does (small, well-bounded) A UMDF2 HID minidriver holds **no device logic** — it shuttles bytes. Its entire job is one `EvtIoDeviceControl` callback branching on ~10 HID IOCTLs (Microsoft, "Creating WDF HID Minidrivers"; reference source `vhidmini2`): - In `EvtDriverDeviceAdd`: call `WdfFdoInitSetFilter`, then create the I/O queue(s). - **Descriptor IOCTLs** (`GET_DEVICE_DESCRIPTOR` / `GET_REPORT_DESCRIPTOR` / `GET_DEVICE_ATTRIBUTES`) — trivial: `RequestCopyFromBuffer` a static blob. For punktfunk these blobs are the **existing `DUALSENSE_RDESC` (232 B)** + a `HID_DEVICE_ATTRIBUTES` filled `054C`/`0CE6`. - **Output / feature IOCTLs** (`WRITE_REPORT` / `SET_OUTPUT_REPORT` / `GET_FEATURE` / `SET_FEATURE`) — pull the `HID_XFER_PACKET` (report id + buffer) and hand the bytes to the host. These carry the game's `0x02` output report (rumble / lightbar / **adaptive-trigger** block) — exactly what `parse_ds_output` already decodes. - **Input path** (`READ_REPORT`, pad → game) — the only non-trivial mechanic, an **inverted call**: each `READ_REPORT` request is pended into a manual `WDFQUEUE` (`WdfIoQueueDispatchManual` + `WdfRequestForwardToIoQueue`) and later popped (`WdfIoQueueRetrieveNextRequest`), filled, and completed (`WdfRequestComplete`) whenever the host has a fresh `0x01` input report. `vhidmini2` drives this from a periodic timer; punktfunk drives it from each new `0x01` report arriving over the host channel — **structurally identical to the existing Linux `/dev/uhid` loop.** Because UMDF can't marshal embedded pointers, `mshidumdf.sys` converts `IOCTL_HID_*` into `IOCTL_UMDF_HID_*` (e.g. `IOCTL_UMDF_HID_GET_INPUT_REPORT`, `IOCTL_UMDF_HID_SET_FEATURE`), passing `reportBuffer` / `reportId` as separate buffers — the driver branches on those. ### Integration sketch ``` host process (Rust) <-- SwDeviceCreate + IOCTL channel --> UMDF2 HID minidriver <-- HID --> game / Steam / GameInput PadState -------------- input report 0x01 -------------> inverted READ_REPORT queue HidOutput <----- output report 0x02 (WriteReport cb) ----- EvtIoDeviceControl ``` - **Descriptor reuse:** the exact inputtino PS5 descriptor + feature-report replies we already ship for Linux (`dualsense.rs` `DS_*` constants) — same bytes, same VID/PID, so Windows + games recognize it as a DualSense. - **Host-side device creation:** `windows::Win32::Devices::Enumeration::Pnp::SwDeviceCreate` → `Result` (pure Win32, in the `windows` crate, **no WDK needed**), enumerating a root device whose hardware IDs match the pre-staged INF. Requires Administrator. **The device exists only while the `HSWDEVICE` handle (i.e. the host process) is open** — `SwDeviceClose` removes it — so the pad is created/destroyed with the session, exactly like the Linux UHID fd. The INF is pre-staged once (`pnputil /add-driver`). - **Userspace bridge:** a `DualSenseManager`-shaped struct mirroring the Linux one (same `RichInput` → report `0x01` packing via `serialize_state`, same `HidOutput` parsing via `parse_ds_output`), talking to the driver over an IOCTL channel instead of `/dev/uhid`. - **Packaging:** vendor + sign the `.dll`/`.inf`/`.cat` and install via the existing `packaging/windows` machinery (`pnputil` + an `install-*.ps1`, bundled in the Inno `setup.exe`). The precedent — SudoVDA, a UMDF/IddCx driver — is already in the repo. ## Driver language — recommendation The user strongly prefers a **self-authored Rust driver**. Verified verdict: **a Rust UMDF2 HID minidriver is technically viable but unproven and pioneering** — it does not clear the bar for a *low-risk* M2. Honest ranking of the three options: ### Option R — fully self-authored Rust driver (preferred; viable, but pioneering) - **What's real today:** `microsoft/windows-drivers-rs` (`wdk`, `wdk-sys`, `wdk-build`, `wdk-macros`) officially targets WDM + KMDF + **UMDF** (tested UMDF 2.33). It ships a *real* Rust UMDF sample, `examples/sample-umdf-driver/src/lib.rs`, that `#[unsafe(export_name = "DriverEntry")]`, builds a `WDF_DRIVER_CONFIG` with `EvtDriverDeviceAdd: Some(...)`, and calls `WdfDriverCreate` + `WdfDeviceCreate` via `call_unsafe_wdf_function_binding!` over raw `wdk-sys` FFI. Because UMDF 2.0 is the C function-pointer model (no COM vtable), the FFI maps cleanly. - **The gap:** that sample is a **bare stub** — no I/O queue, no IOCTL dispatch, no HID. The entire HID-minidriver layer (`WdfFdoInitSetFilter`, the manual inverted-call queue, `IOCTL_UMDF_HID_*` dispatch, `HID_XFER_PACKET`, `METHOD_NEITHER`) would be **hand-written `unsafe` FFI with no safe wrappers**, against `vhidmini2`/GazeHid-scale glue (a few hundred lines). The heavy domain logic is *not* in the driver — it already exists in `dualsense.rs`. - **The honest blockers:** **zero precedent** — every shipping virtual-HID controller driver (`vhidmini2`, HIDMaestro, DsHidMini, EmuController, GazeHid) is **C**. Microsoft labels `windows-drivers-rs` "not yet recommended for production use" (Sept 2025) and has **not settled the WHCP/attestation submission path for Rust drivers** — directly relevant given the public-distribution requirement (though attestation re-signs the `.cat` and treats the `.dll` opaquely, so signing *should* be language-agnostic — unverified). Whether all needed WDF symbols (`WdfIoQueueCreate`, `WdfFdoInitSetFilter`, `WdfRequestRetrieveOutputMemory`, manual-queue APIs, `WDF_IO_QUEUE_CONFIG_INIT`) are generated/usable for the UMDF target is **unverified against the bindings — this is exactly what the M0 build spike must answer.** Note the Dec 2025 `windows-drivers-rs` build break (Discussion #591) is a transient LLVM-22-tip bindgen issue, fixed by pinning LLVM 21.1.2 — not a fundamental defect. Do **not** C-FFI-bind DMF's `Dmf_VirtualHidMini` from Rust (large, awkward C surface) — reimplement the modest `vhidmini2` queue/IOCTL glue directly. ### Option C — thin C/C++ UMDF2 shim + all logic in the Rust host (realistic fallback / lowest-risk M2) Clone `vhidmini2` (`WdfFdoInitSetFilter` + `EvtIoDeviceControl` + manual inverted-call queue, a few hundred LOC of generic byte-shuttling); keep **all** DualSense logic in the existing Rust host (`dualsense.rs` descriptors/packers/parsers fed over the IOCTL channel); the `SwDeviceCreate` host bridge stays pure Rust in the `windows` crate (no WDK). This **mirrors HIDMaestro's split** (generic C/C++ UMDF2 HID minidriver under `mshidumdf.sys`, all profile/DualSense logic in the user-mode service) **and punktfunk's own Linux design.** It is the user's pre-ranked middle option and the fastest way to reach the M0 on-glass gate. ### Option H — fork/reuse HIDMaestro (last resort) HIDMaestro is a proven, pure-UMDF2 virtual controller (self-signed, no EV/test-signing/reboot) recognized by DirectInput/XInput/SDL3/WGI/GameInput/RawInput + Steam, with a **DualSense profile** (byte-exact VID/PID + descriptor). Use only if even the C shim stalls **and** adaptive-trigger fidelity is not required — **HIDMaestro omits adaptive triggers from its DS5 feature list**, so it cannot prove the very thing that makes a virtual DualSense worth building. Its driver is C; its service is C#. ### Recommendation **Lead with Option R for the long-term codebase, but de-risk the on-glass gate with Option C in M2.** Concretely: run the **M0 spike in two halves** — (a) a `windows-drivers-rs` UMDF *build* spike to confirm the WDF queue/IOCTL symbols are usable from Rust at all, and (b) the on-glass recognition gate using whichever driver is fastest to stand up (the C `vhidmini2` shim is the safe bet). If (a) passes **and** the on-glass gate passes, author the M2 driver in **Rust** (it would be the first Rust UMDF HID driver, accepted as pioneering risk per the user's explicit preference). If (a) is shaky, ship M2 as the **C shim** and migrate the driver to Rust later, once `windows-drivers-rs` ships safe WDF/HID abstractions. Either way the DualSense *logic* stays in Rust where it already lives. Forking HIDMaestro is the fallback-of-fallbacks and is acceptable only if adaptive triggers are dropped from scope. ## Signing Two recipes coexist in the Inno installer, selected by the bundled payload — the same pattern already proven for SudoVDA. ### Fleet / self-signed (dev + internal boxes) The in-repo precedent is `packaging/windows/install-sudovda.ps1`: import the bundled `.cer` into the machine **Root** *and* **TrustedPublisher** stores (`certutil -addstore -f`), then `pnputil /add-driver /install`. This installs silently **only** because the publisher is pre-trusted on that machine. Microsoft is explicit that this auto-import-into-Root practice "should never be followed for any driver package distributed outside your organization" — so it is the **fleet** path, never the public one. ### Public end-user distribution — EV cert + Microsoft attestation For arms-length public users, the correct tier is **Microsoft attestation signing** via Partner Center (verified: "Attestation signing supports Windows Desktop kernel mode **and user mode** drivers"; processable types include `.cab`/`.dll`). Pipeline: 1. **Prerequisites:** a registered **Windows Hardware Developer Program** (Partner Center) account (free to register; sign in with an Entra ID global-admin work account; accept the agreements, provide org/D-U-N-S info, respond to the legal-contact verification email) and an **EV code-signing certificate** (mandatory to register *and* to sign the submission CAB; ~USD 250–560/yr; FIPS hardware token/HSM mandatory; 1–7 business-day identity vetting). Windows ADK (`MakeCab`). 2. **Build the submission:** `MakeCab` the `.dll` + `.inf` (+ `.pdb`/symbols) into per-driver subfolders (folder names < 40 chars, no special chars, no UNC); `SignTool sign` the CAB with the EV cert (`/fd sha256` + RFC3161 timestamp `/tr … /td sha256`). 3. **Submit:** Partner Center → *Submit new hardware*, **leave test-signing unchecked**, request the desired signatures. 4. **Microsoft re-signs:** it appends a Microsoft SHA-2 signature and **regenerates + signs a new `.cat` with a Microsoft cert** (your `.cat` is replaced). Because the catalog signer is then Microsoft (already trusted), **PnP installs silently — no publisher prompt, no test-signing, no reboot, and no shipping our cert into users' Root store.** Validation: `devcon`/`pnputil` install must not show "Windows can't verify the publisher of this driver software." **Important nuance — is attestation even *required* for UMDF?** UMDF is user-mode, so it is **exempt from kernel-mode code-integrity *load* enforcement** — the driver `.dll` will *load* without a Microsoft signature. But **PnP *installation* still requires a signed catalog whose publisher is trusted.** A driver signed only with a plain publicly-trusted (OV/EV) Authenticode cert that is *not* already in TrustedPublisher will **install, but with the blocking "Windows Security / would you like to install this device software?" prompt** (setupapi warning `0x800b0109`, error `0xe0000242` "publisher … not yet established as trusted"). So a bare Authenticode signature is **not** sufficient for a prompt-free public install — **attestation is the minimal correct public path.** The April 2026 kernel-trust change (removing trust for legacy cross-signed *kernel* drivers) **does not affect** attestation/WHQL or user-mode UMDF drivers. What attestation does **not** do: attestation-signed drivers are **not** distributed via Windows Update — irrelevant here, since punktfunk bundles the driver in its Inno installer exactly like SudoVDA. (Azure Trusted Signing is **not** an option for the driver `.cat` at all — it signs only user-mode PE / `/INTEGRITYCHECK` / SmartScreen, and cannot substitute for the EV cert in Partner Center; it could only improve SmartScreen reputation on the installer `.exe`.) Note attestation does **not** require HLK/WHQL testing. The heavier fallback, only if attestation's "testing scenarios" positioning ever hardens into a block, is full **WHQL/HLK** submission (also yields a Microsoft-signed catalog, plus Windows Update eligibility). ### Coexistence in the Inno installer `packaging/windows/punktfunk-host.iss` already gates the SudoVDA driver payload behind `#ifdef WithDriver` + the `installdriver` task + a `[Run]` call to `install-sudovda.ps1`. Add an analogous gated payload + `install-dualsense.ps1` for the virtual DualSense driver, switching the bundled `.cat` per build: - **fleet build** → self-signed `.cat` + `install-dualsense.ps1` keeps the `certutil -addstore Root/TrustedPublisher` step (cloned from `install-sudovda.ps1`). - **public build** → Microsoft-attestation-re-signed `.cat`, and `install-dualsense.ps1` **drops** the `certutil` import (just `pnputil /add-driver /install`). Operationally, the EV key lives on a non-exportable FIPS token, so the **CAB signing + Partner Center submission is a manual offline step**, not a CI secret (cloud-HSM/Azure Key Vault EV options exist but need per-CA confirmation). The Microsoft-resigned `.cat` is then committed as the vendored public payload, the way SudoVDA's signed driver is vendored in `packaging/windows/sudovda/`. ## Feasibility gate (BLOCKING — M0, on-glass only) No prior art settles the two questions that decide whether this whole effort is worth building. **This gate blocks M1–M6** and can only be answered on the **RTX box (`192.168.1.173`)** — the dev VM is headless/WARP and cannot validate game-facing HID recognition: 1. **Recognition:** is a virtual `054C:0CE6` UMDF2 device accepted as a *genuine DualSense* by `Windows.Gaming.Input` / GameInput / Steam (and native-DS5 games)? HIDMaestro proves DualSense *recognition* is possible, but… 2. **Adaptive-trigger fidelity:** does the game's output report `0x02` (the adaptive-trigger block) actually reach the driver's `WriteReport`/`SetOutputReport` callback? **HIDMaestro omits adaptive triggers**, so no prior art proves this — it must be **measured on glass**. If (2) fails, the realistic product is a DualSense *identity* without adaptive triggers — at which point the value over ViGEm DS4 collapses and the project should likely **defer** rather than ship. **M0 RESULT (2026-06-21): GATE PASSED.** Both answered YES on the RTX box with a self-authored **Rust** UMDF minidriver (`packaging/windows/dualsense-driver/`). (1) **Recognition:** Steam recognized the virtual `054C:0CE6` device as a genuine DualSense and drove its DualSense-specific LEDs. (2) **`0x02` reaches the write callback:** captured two Steam-Input output reports (`validFlag1=0x14` = LIGHTBAR|PLAYER_INDICATOR). Adaptive-trigger-specific bytes ride the same `0x02` path (Cyberpunk confirmation is gravy, not a gate). Three bugs had to be fixed to get there — the load wall was the PE **FORCE_INTEGRITY** bit (`wdk-build`'s `/INTEGRITYCHECK`; clear bit `0x80` at PE+0x5e + re-sign), then `WdfTimerCreate` exec-level, then a parallel queue's zeroed `NumberOfPresentedRequests`. **Option R (Rust) confirmed for M2; no C shim needed.** ## Milestone plan (M0–M6) | # | Milestone | Output | Gate / risk | |---|---|---|---| | **M0 ✅ DONE** | **Feasibility spike — PASSED (2026-06-21)** | (a) Rust `windows-drivers-rs` UMDF build spike — symbols usable, driver authored in Rust; (b) on-glass on the RTX box: self-signed Rust `054C:0CE6` UMDF minidriver loads under Secure Boot, Steam recognizes it as a DualSense, `0x02` output reaches the write callback. Source: `packaging/windows/dualsense-driver/` | **PASSED.** Option R (Rust) chosen for M2. Load needed clearing the PE FORCE_INTEGRITY bit | | **M1** | Linux codec refactor | Extract the transport-independent contract from `dualsense.rs` into `inject/dualsense_proto.rs` (`DUALSENSE_RDESC`, `serialize_state`, `parse_ds_output`, feature blobs); **fix `DS_FEATURE_CALIBRATION` 42 → 41**; Linux backend keeps passing | Pure refactor; keep Linux loopback green | | **M2** | UMDF2 driver | The HID minidriver + INF + signed `.cat` (test-signed for dev). **Language per M0(a):** Rust if the build spike is solid, else the `vhidmini2`-derived C shim. INF carries the required UMDF directives (`UmdfKernelModeClientPolicy=AllowKernelModeClients`, `UmdfMethodNeitherAction=Copy`, `UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects`, `UmdfFsContextUsePolicy=CanUseFsContext2`), root-enumerated `HIDClass`, filter under `mshidumdf.sys` | Pioneering if Rust; manual inverted-call queue is the hard part | | **M3** | Rust host bridge | `inject/dualsense_windows.rs`: `SwDeviceCreate` per-session device (hold `HSWDEVICE` for the session) + the inverted-call IOCTL channel, feeding `0x01` and surfacing `0x02` as `HidOutput` — reusing `dualsense_proto.rs` | Channel design (single control device + inverted-call IOCTL vs shared-memory) | | **M4** | Un-gate the seam + negotiation | New `PadBackend::DualSense` Windows arm; relax the `#[cfg(target_os="linux")]` guards on DualSense/DualShock4 in `pick_gamepad`/`resolve_gamepad` to `any(linux, windows)`; wire `GamepadPref::DualSense` resolution | Small; `dualshock4.rs` is the template | | **M5** | On-glass E2E | Client → host → virtual DualSense → game, with adaptive triggers / lightbar / touchpad / motion / rumble round-tripping; latency check | RTX box; the real proof | | **M6** | Packaging / installer | Vendor + sign the driver; `install-dualsense.ps1` (fleet vs public variant); gate the payload in `punktfunk-host.iss`; complete the **EV cert + attestation** submission for the public build | EV-cert procurement + Partner Center turnaround are lead-time items — start early | ## Decision matrix | Option | Adaptive triggers / DS5 identity | Effort | When it's right | |---|---|---|---| | **A. UMDF2 virtual DualSense** (parity) | ✅ full (pending the M0 gate) | medium — **UMDF, same tier as SudoVDA** (was mis-scoped as "kernel/large" in the 2026-06-20 draft) | the goal — matches the Linux host | | **B. ViGEm DS4** (interim) | ❌ never (DS4 ceiling) | small | quick PS-pad on Windows w/ touchpad/motion/lightbar/rumble, no adaptive triggers | | **C. Hybrid** | A for DS5 clients, B/Xbox 360 fallback | A + small | belt-and-suspenders once A exists | | **D. Defer** | — | — | if the M0 gate fails (esp. output `0x02` fidelity), or a higher-ROI item wins the slot | Xbox 360 (XInput, via ViGEm) is already implemented and covers most Windows games regardless; Xbox One/Series fold into it on Windows. Windows-host DualShock 4 (ViGEm) remains separately deferred. ## Risk register | Risk | Likelihood | Impact | Mitigation | |---|---|---|---| | Output report `0x02` (adaptive triggers) never reaches the driver write callback | medium | **fatal** to the value prop | M0(b) measures it directly; if it fails → Option D | | `054C:0CE6` UMDF2 device not accepted as a real DualSense by WGI/GameInput/Steam | low–med | fatal | M0(b); HIDMaestro suggests recognition works, but confirm | | Rust UMDF driver pioneering risk (first of its kind; no safe WDF/HID wrappers; symbol coverage unproven) | medium | schedule | M0(a) build spike; **Option C (C shim) as the de-risked M2 fallback** | | EV cert + Partner Center attestation lead time / friction | medium | schedule | Start procurement at M0; lean on the SudoVDA UMDF submission precedent | | EV key non-exportable → can't sign in CI | high | low | Accept a manual offline sign+submit step; vendor the Microsoft-resigned `.cat` | | `SwDeviceCreate` device lifetime tied to the host process handle | known | low | Hold `HSWDEVICE` for the session lifetime (matches Linux UHID fd semantics) | | `windows-drivers-rs` transient toolchain breaks (e.g. LLVM-22 bindgen, Disc. #591) | low | low | Pin LLVM 21.1.2; not a fundamental defect | | `DS_FEATURE_CALIBRATION` 42-byte blob rejected by strict Windows consumers | low | low | Trim to 41 bytes in M1 | ## Open questions 1. **Driver channel design** (unknown): punktfunk's own driver↔host protocol — simplest is a private control device with an inverted-call IOCTL for input + IOCTLs for output/feature, vs HIDMaestro's shared-memory section. `vhidmini2` has *no* service channel (it self-generates via a timer), so this must be designed fresh (or read out of HIDMaestro/DsHidMini source). **Resolve in M3.** 2. **Rust UMDF symbol coverage** (unknown — the M0(a) gate): are all needed WDF symbols (`WdfIoQueueCreate`, `WdfFdoInitSetFilter`, `WdfRequestRetrieveOutputMemory`, manual-queue APIs, `WDF_IO_QUEUE_CONFIG_INIT`) generated/usable from `wdk-sys` for the UMDF target? 3. **Attestation for a Rust-authored `.dll`** (likely fine, unverified): attestation re-signs the `.cat` and treats the `.dll` opaquely (allowed type), so language *should* be irrelevant to signing — but Microsoft has not explicitly settled the WHCP path for Rust drivers. Confirm via a Partner Center dry-run. 4. **Single multi-driver CAB** (unknown, operationally useful): can one Partner Center submission carry *both* the existing SudoVDA driver and the new DualSense driver? Multi-driver CABs are supported in general; unverified for this account. 5. **EV cert + Partner Center mechanics** (unknown): exact cost/turnaround; whether a cloud-HSM EV option lets CI sign, or whether it must be a manual offline step (most likely the latter). 6. **HidHide** (carried over): needed at all on a usually-headless host, or only when a physical pad is attached? 7. **Min-OS / UMDFVERSION target** (unknown): which `UmdfLibraryVersion` / WDK to target for the widest Win10/11 install base, consistent with punktfunk's existing host support matrix. 8. **DsHidMini end-user signing tier** (unknown): self-signed vs attestation in its WixSharp MSI — useful as a second public-distribution data point.