DualSenseWindowsManager now SwDeviceCreate's the pf_dualsense devnode per session (SwDeviceClose on drop), matching the Linux UHID pad's lifecycle. It's best-effort: SwDeviceCreate currently hits an unresolved E_INVALIDARG when a completion callback is passed (an underscore in the enumerator name was a second cause, fixed by using "punktfunk"), so on failure the host keeps the section + data plane and falls back to an out-of-band devnode (installer/devgen) — see docs/windows-dualsense-scoping.md. Add a `dualsense-windows-test` host CLI that drives the manager (create devnode + push a frame + hold), used to validate the path. Live on the RTX box: the manager creates the section + pushes report 0x01 and a devnode serves it to a HID read (b1=0xC0, b8=0x28) — the host-side data plane works end to end. cargo check + clippy -D warnings clean on x86_64-pc-windows-msvc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
32 KiB
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
vhidmini2sample (or DMF'sDmf_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 report0x01packer (controller state → bytes).parse_ds_output— the output report0x02parser (game's rumble/LED/trigger block →HidOutput), valid-flag gated.- Feature blobs
0x05calibration,0x09pairing,0x20firmware. 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 callsHidRegisterMinidriveron 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: callWdfFdoInitSetFilter, then create the I/O queue(s). - Descriptor IOCTLs (
GET_DEVICE_DESCRIPTOR/GET_REPORT_DESCRIPTOR/GET_DEVICE_ATTRIBUTES) — trivial:RequestCopyFromBuffera static blob. For punktfunk these blobs are the existingDUALSENSE_RDESC(232 B) + aHID_DEVICE_ATTRIBUTESfilled054C/0CE6. - Output / feature IOCTLs (
WRITE_REPORT/SET_OUTPUT_REPORT/GET_FEATURE/SET_FEATURE) — pull theHID_XFER_PACKET(report id + buffer) and hand the bytes to the host. These carry the game's0x02output report (rumble / lightbar / adaptive-trigger block) — exactly whatparse_ds_outputalready decodes. - Input path (
READ_REPORT, pad → game) — the only non-trivial mechanic, an inverted call: eachREAD_REPORTrequest is pended into a manualWDFQUEUE(WdfIoQueueDispatchManual+WdfRequestForwardToIoQueue) and later popped (WdfIoQueueRetrieveNextRequest), filled, and completed (WdfRequestComplete) whenever the host has a fresh0x01input report.vhidmini2drives this from a periodic timer; punktfunk drives it from each new0x01report arriving over the host channel — structurally identical to the existing Linux/dev/uhidloop.
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.rsDS_*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 thewindowscrate, no WDK needed), enumerating a root device whose hardware IDs match the pre-staged INF. Requires Administrator. The device exists only while theHSWDEVICEhandle (i.e. the host process) is open —SwDeviceCloseremoves 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 (sameRichInput→ report0x01packing viaserialize_state, sameHidOutputparsing viaparse_ds_output), talking to the driver over an IOCTL channel instead of/dev/uhid. - Packaging: vendor + sign the
.dll/.inf/.catand install via the existingpackaging/windowsmachinery (pnputil+ aninstall-*.ps1, bundled in the Innosetup.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 aWDF_DRIVER_CONFIGwithEvtDriverDeviceAdd: Some(...), and callsWdfDriverCreate+WdfDeviceCreateviacall_unsafe_wdf_function_binding!over rawwdk-sysFFI. 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-writtenunsafeFFI with no safe wrappers, againstvhidmini2/GazeHid-scale glue (a few hundred lines). The heavy domain logic is not in the driver — it already exists indualsense.rs. -
The honest blockers: zero precedent — every shipping virtual-HID controller driver (
vhidmini2, HIDMaestro, DsHidMini, EmuController, GazeHid) is C. Microsoft labelswindows-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.catand treats the.dllopaquely, 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 2025windows-drivers-rsbuild 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_VirtualHidMinifrom Rust (large, awkward C surface) — reimplement the modestvhidmini2queue/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:
- 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). - Build the submission:
MakeCabthe.dll+.inf(+.pdb/symbols) into per-driver subfolders (folder names < 40 chars, no special chars, no UNC);SignTool signthe CAB with the EV cert (/fd sha256+ RFC3161 timestamp/tr … /td sha256). - Submit: Partner Center → Submit new hardware, leave test-signing unchecked, request the desired signatures.
- Microsoft re-signs: it appends a Microsoft SHA-2 signature and regenerates + signs a new
.catwith a Microsoft cert (your.catis 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/pnputilinstall 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.ps1keeps thecertutil -addstore Root/TrustedPublisherstep (cloned frominstall-sudovda.ps1). - public build → Microsoft-attestation-re-signed
.cat, andinstall-dualsense.ps1drops thecertutilimport (justpnputil /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:
- Recognition: is a virtual
054C:0CE6UMDF2 device accepted as a genuine DualSense byWindows.Gaming.Input/ GameInput / Steam (and native-DS5 games)? HIDMaestro proves DualSense recognition is possible, but… - Adaptive-trigger fidelity: does the game's output report
0x02(the adaptive-trigger block) actually reach the driver'sWriteReport/SetOutputReportcallback? 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
- 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.
vhidmini2has 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. - 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 fromwdk-sysfor the UMDF target? - Attestation for a Rust-authored
.dll(likely fine, unverified): attestation re-signs the.catand treats the.dllopaquely (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. - 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.
- 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).
- HidHide (carried over): needed at all on a usually-headless host, or only when a physical pad is attached?
- 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. - DsHidMini end-user signing tier (unknown): self-signed vs attestation in its WixSharp MSI — useful as a second public-distribution data point.