# 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<HSWDEVICE>` (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.**

**Host integration status (2026-06-21): M1/M3/M4 landed; data plane runtime-proven.** The Linux
DualSense logic is shared via `inject/dualsense_proto.rs`; the Windows backend
`inject/dualsense_windows.rs` (`DualSenseWindowsManager`) drives the driver over the
`Global\pfds-shm-<idx>` section, and the `PadBackend`/`pick_gamepad` seam now resolves DualSense on
Windows. Live-verified on the RTX box: the manager creates the section + pushes report `0x01` and a
devnode serves it to a HID read (manager data plane works). **Open item — `SwDeviceCreate`
per-session devnode:** two `E_INVALIDARG` causes found — (1) an underscore in the enumerator name
(`pf_dualsense` → use `punktfunk`), (2) passing the completion callback is still rejected (cause
unresolved; needs a known-good C reference). So per-session auto-creation is **best-effort/non-fatal**:
the host falls back to an out-of-band `pf_dualsense` devnode (the INF lists both `root\pf_dualsense`
for devgen and `pf_dualsense` for SwDevice; the installer would create it, as SudoVDA does). Remaining:
fix the SwDeviceCreate callback E_INVALIDARG, then the M5 on-glass game test.

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