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>
This commit is contained in:
2026-06-26 16:39:06 +00:00
parent 9ea2c17419
commit 7b99b41ede
27 changed files with 1322 additions and 3229 deletions
+130 -389
View File
@@ -1,416 +1,157 @@
# 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.
> **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.
## TL;DR
## Why UMDF2, and why a real virtual DualSense (the WHY)
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.
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 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.
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.
> **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.
**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:
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.
- **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.
## Why this is the wrong place to copy Apollo
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.
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:
## Why Rust ("Option R") despite zero precedent
- 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*).
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).
This is unchanged from the 2026-06-20 draft and remains correct.
**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.
## The parity target — and what's *already* done
## M0 feasibility gate — PASSED (2026-06-21), and the three bugs
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`.
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:
Crucially, **everything except the host backend is already platform-agnostic and DualSense-complete**
(verified against live source):
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.
| 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 ~11811272) |
| 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` |
> **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.
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`:
## Milestones
- `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` ~11811272), flipping `pick_gamepad`/`resolve_gamepad`
(~15581606) from `#[cfg(target_os = "linux")]` to `#[cfg(any(target_os = "linux", target_os =
"windows"))]`, plus the `inject.rs` module gating (~424451). `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 250560/yr;
FIPS hardware token/HSM mandatory; 17 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 M1M6** 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 (M0M6)
| # | Milestone | Output | Gate / risk |
| # | Milestone | State | Commit(s) |
|---|---|---|---|
| **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 |
| **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` |
## Decision matrix
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** (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 |
| **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, 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.
Xbox 360 (XInput) covers most Windows games regardless; Xbox One/Series fold into it on Windows.
## Risk register
## Risk register (condensed)
| 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 | lowmed | 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 |
| 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 questions
## Open items
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.
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.