Files
punktfunk/docs/windows-dualsense-scoping.md
T
enricobuehler aa159df33f feat(windows): Rust UMDF virtual DualSense driver + shared-memory host channel
A self-authored UMDF2 HID minidriver (packaging/windows/dualsense-driver) that
presents a virtual Sony DualSense (VID 054C/PID 0CE6) on Windows — adaptive
triggers / lightbar / rumble that ViGEm structurally cannot deliver.

Validated live on an RTX box (Win11 25H2, Secure Boot ON): the self-signed driver
loads, Steam recognizes it as a genuine DualSense, and a game's 0x02 output report
reaches the driver. The host<->driver channel is a named shared-memory section
(Global\pfds-shm-<idx>) the host creates and the driver maps from its timer: input
report 0x01 host->driver, output report 0x02 driver->host — input and output proven
both directions live. This bypasses hidclass, which gates both a custom device
interface and custom IOCTLs on the HID node, and UMDF has no control device.

Built in Rust on microsoft/windows-drivers-rs. The load wall was the PE
FORCE_INTEGRITY bit that wdk-build sets via /INTEGRITYCHECK (forces a CI-trusted
page-hash signature a self-signed cert cannot satisfy) — cleared post-build. See
packaging/windows/dualsense-driver/README.md for the build/sign/install recipe.

Deferred: SwDeviceCreate per-session device lifecycle; removing the inert in-driver
IOCTL-channel code; full on-glass session test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:36:39 +00:00

30 KiB
Raw Blame History

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 ~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

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 ~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 SudoVDAbelow 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::SwDeviceCreateResult<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 openSwDeviceClose 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.

Milestone plan (M0M6)

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

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.