Files
punktfunk/packaging/windows/drivers/pf-gamepad
enricobuehler ce5047f3ad fix(host/pads): the Windows driver stops halving motion and stops serving torn reports
G6 + G15 of the gyro program.

G6 — the UMDF gamepad driver's input path. Its timer ran at 8 ms and completed
one pended READ_REPORT per tick, so a game could observe at most ~125 Hz while
clients stream motion at ~250 Hz: every other sample was overwritten in the slot
before anything read it, and the ones that survived carried up to 8 ms of extra
latency. For gyro, a dropped sample is not a dropped frame — it is rotation that
never reaches the game.

The timer now ticks at 2 ms (about a real DualShock 4's Bluetooth cadence). Only
the cheap half runs on every tick: read the input slot, complete one pended
read. The channel handshake and the health marks stay on their historical ~8 ms,
because they cost more, nothing wants them faster, and `driver_heartbeat`'s
documented "+1 per ~8 ms tick" is what the host reads as liveness.

The same slot is a single unqueued buffer that both sides touch without a lock,
so a driver read landing mid-copy handed the game a report that was half the
previous frame and half the next. For a button that is a one-tick glitch; for
motion it is a spike in angular velocity, which an integrator turns into aim
movement. `PadShm` gains an `input_gen` seqlock (v2.3, carved from reserved
space inside the v2 legacy region): the host takes it odd, fences, writes the 64
bytes, and stores it even; the driver samples it either side of its read and
retries once. The old code's own comment called this out as a known residual —
it is now closed rather than documented.

Version posture matches the ring's, with one simplification: no capability stamp
is needed, because an old host never writes the field and a constant 0 is
indistinguishable from "no write in flight", so a new driver against an old host
behaves exactly as it does today, and an old driver ignores the field entirely.

The Steam Deck write path had neither the seqlock nor even the trailing Release
its DualSense sibling carried; all three Windows backends now publish through
one `publish_input`.

G15 — motion-cadence observability. The host already computed the measurement a
"gyro feels floaty" report needs (client inter-arrival percentiles), but kept
ONE global accumulator, so two motion-capable pads in a session interleaved into
each other's gaps and produced a number describing neither. It also sat at
`debug` behind a `tracing::enabled!` check, so a field log arrived with nothing
in it and the only way to get the measurement was to ask for a re-run.

Now per-pad and always on, summarized at `info` when the session ends — the
moment a field report is being written. It costs one subtraction and one array
increment per sample: percentiles come from a fixed log2 histogram instead of a
growing sorted Vec, so there is no allocation, no per-window sort, and no way
for a client streaming as fast as the link allows to make the instrument
expensive. Percentiles are reported as bucket upper bounds (`_le`), which is a
factor-of-two answer to a question whose answers are orders of magnitude apart.
Gaps of 500 ms or more are counted as stalls rather than folded into the
percentiles — an interruption is not a cadence, and averaging it in would report
a healthy feed as a terrible one.

Gates. Windows CI runner .133, the drivers workspace on the real WDK: cargo
build, clippy -D warnings (which enforces the unsafe-audit lints), and fmt —
all green, against a source whose SHA-256 matches this commit's. Linux CI image:
fmt, build, clippy --all-targets -D warnings over pf-inject / punktfunk-core /
punktfunk-probe / pf-client-core / pf-driver-proto / punktfunk-host, and the
test suites including the 5 new motion-cadence tests — all green.

Not measured on glass. G6's stated gate is a sensor-rate reading (SDL
testcontroller or Steam's calibration screen) that matches the client's send
rate; that is still owed, and a driver change only a compile has seen deserves
it before anyone trusts the number.
2026-08-07 13:44:54 +02:00
..

pf-gamepad — the virtual-gamepad UMDF2 HID minidriver

Renamed from pf-dualsense (2026-07-28). One driver has always served four identities — DualSense, DualShock 4, DualSense Edge and Steam Deck — so the old name read as if the other three lived somewhere else. Only the PACKAGE identity moved (crate, INF, CAT, DLL, UMDF service); the four hardware ids (pf_dualsense, pf_dualshock4, pf_dualsenseedge, pf_steamdeck) are deliberately unchanged — they bind every devnode the host creates and every installed system. driver install --gamepad retires the pre-rename store package so the two can't both claim them.

A self-authored Rust UMDF2 HID minidriver that presents a virtual Sony DualSense (VID 054C / PID 0CE6) to Windows, so games drive adaptive triggers / lightbar / rumble — capabilities ViGEm structurally cannot deliver. It's how the punktfunk Windows host gives a client's DualSense a near-native feel with no external gamepad dependencies (no ViGEmBus).

Shipping: the driver is one member of the in-tree driver workspace (packaging/windows/drivers/), built from source in CI, and bundled + pnputil-installed by the Windows host installer. The host feeds it over a shared memory channel from crates/punktfunk-host/src/inject/windows/dualsense_windows.rs. The same UMDF driver also serves the DualShock 4 identity per a device_type byte the host stamps.

This README captures the driver-authoring lore — the bugs and the signing recipe that make a self-signed UMDF HID driver actually load. The authoritative build/sign/package flow (CI + Inno Setup) lives in the Windows host packaging README.

Build workspace

This crate builds as a member of the packaging/windows/drivers/ workspace, which uses the published crates.io wdk/wdk-sys/wdk-build (0.4/0.5) — not the old dev-box windows-drivers-rs path-deps. It's a separate cargo workspace from the main tree because driver crates are cdylibs built with the WDK toolchain on Windows only; it path-deps the shared ABI crate crates/pf-driver-proto.

Build / sign / install recipe (the one that actually loads)

Prereqs on the Windows box: WDK 26100, LLVM (the current default; bindgen 0.72 builds on clang 22), Rust MSVC. Built as a member of the packaging/windows/drivers/ workspace (plain cargo build, no cargo-make). A self-signed CodeSigning cert in CurrentUser\My + LocalMachine\Root + TrustedPublisher.

Every build needs:

$env:LIBCLANG_PATH = 'C:\Program Files\LLVM\bin'
$env:Version_Number = '10.0.26100.0'   # else wdk-build picks 10.0.28000.0 (no km/crt) and bindgen fails

The shipping flow is build-gamepad-drivers.ps1 (one level up): workspace cargo build --release plus the sign steps below, staged for the installer. The original manual dev-box recipe, kept as lore (paths reflect that era's cargo-make layout):

cargo make                              # -> target\debug\pf_gamepad_package\ (.inf/.cat/.dll)

# *** CRITICAL: clear the PE FORCE_INTEGRITY bit ***
# windows-drivers-rs links the DLL with /INTEGRITYCHECK, which forces a CI-trusted page-hash
# signature a self-signed cert cannot satisfy (CodeIntegrity 3004 "hash not found" /
# 3089 VerificationError 7). SudoVDA.dll (third-party VDD prior art, not used by punktfunk) has
# this bit OFF. Clear bit 0x80 at PE-header offset +0x5e:
$f = 'target\debug\pf_gamepad_package\pf_gamepad.dll'
$b = [IO.File]::ReadAllBytes($f); $pe = [BitConverter]::ToInt32($b,0x3c); $off = $pe + 0x5e
$dc = [BitConverter]::ToUInt16($b,$off); $bb = [BitConverter]::GetBytes([uint16]($dc -band 0xFF7F))
$b[$off]=$bb[0]; $b[$off+1]=$bb[1]; [IO.File]::WriteAllBytes($f,$b)

signtool sign /fd SHA256 /sha1 <cert-thumbprint> $f
Remove-Item target\debug\pf_gamepad_package\pf_gamepad.cat
Inf2Cat /driver:target\debug\pf_gamepad_package /os:10_x64
signtool sign /fd SHA256 /sha1 <cert-thumbprint> target\debug\pf_gamepad_package\pf_gamepad.cat

pnputil /add-driver target\debug\pf_gamepad_package\pf_gamepad.inf /install
devgen /add /hardwareid "root\pf_dualsense"     # creates the (transient, SWD) device node

devgen (under Windows Kits\10\Tools\<ver>\x64\) is only for manual testing — the shipping install is punktfunk-host.exe driver install --gamepad, and the host SwDeviceCreate's the device per session (no persistent devnode). SWD devgen devices clear on reboot. TODO: drop the post-build PE patch by stopping wdk-build emitting /INTEGRITYCHECK.

The three bugs that made it work (porting a WDK C sample to Rust)

WDF_*_CONFIG_INIT / WDF_OBJECT_ATTRIBUTES_INIT macros set non-zero defaults — mem::zeroed() silently breaks them:

  1. FORCE_INTEGRITY (above) — the load wall.
  2. Timer ExecutionLevel — zeroed = Invalid → WdfTimerCreate 0xC0200209. Set ExecutionLevel/SynchronizationScope = InheritFromParent + AutomaticSerialization = TRUE (the working vhidmini2 shape).
  3. Queue Settings.Parallel.NumberOfPresentedRequests — zeroed = 0 → a parallel queue presents zero requests → EvtIoDeviceControl never fires → no HID handshake → ~5 s timeout → CM_PROB_FAILED_START. Set to u32::MAX.

Notes

  • Multi-pad works via UmdfHostProcessSharing=ProcessSharingDisabled — each pad gets its own WUDFHost (so the per-instance statics don't collide), and the driver reads its pad index from the device Location (WdfDeviceAllocAndQueryProperty) to poll its own *-boot-<index> bootstrap mailbox (the DATA section itself is unnamed — the sealed pad channel, punktfunk-planning: gamepad-channel-sealing.md — and its pad_index is validated against this index on attach).
  • Port of the WDK vhidmini2 UMDF2 sample; the DualSense identity + 273-byte descriptor + feature blobs 0x05/0x09/0x20 come from crates/punktfunk-host/src/inject/proto/dualsense_proto.rs.