Files
punktfunk/packaging/windows/drivers/pf-gamepad/src/lib.rs
T
enricobuehler dfebb9dfbb chore(safety): hoist the unsafe lints into the workspace tables (WP2c hoist)
undocumented_unsafe_blocks joins unsafe_op_in_unsafe_fn in
[workspace.lints], and the ~100 scattered per-file #![deny(...)] attributes
(85 files) are deleted — a new crate, or a new module in an old one, is now
covered on creation rather than on remembering. The per-file form is how
pf-vkhdr-layer, wdk-probe and half of pf-clipboard stayed uncovered.

There are THREE workspaces, so the claim is made three times: the main
Cargo.toml, packaging/windows/drivers (workspace table + [lints]
workspace = true in all seven members), and packaging/windows/pf-vkhdr-layer
(its [lints] table, previous commit). pf-update now opts into workspace
lints; the two vendored member snapshots (cros-codecs, usbip-sim) stay out
deliberately and now both say so.

Newly-covered fallout was two link-sanity tests (pyrowave-sys, libvpl-sys)
— proofs written. Stale prose that claimed the workspace held
unsafe_op_in_unsafe_fn at "warn" (it has been deny) or pointed at the
deleted attributes is corrected.

nvenc_core.rs is carved OUT of the unsafe_op_in_unsafe_fn fence: its
exemption rationale ("raw entry-table calls almost line for line") was
false — the file makes zero FFI calls. Its unsafe surface is C-union writes
whose soundness hangs on which codec arm is active, and its own 4:4:4 note
records the shipped bug (hevcConfig bytes stamped onto an AV1 config) that
per-operation blocks make visible. It now runs the strictest discipline in
the crate: clippy::multiple_unsafe_ops_per_block at deny, one union access
per block, each naming its codec guard.

Verified here: cargo fmt clean in all three workspaces; native clippy
-D warnings clean for everything that compiles on macOS (the three
pre-existing mac-native failures — pf-client-core wol.rs, pf-encode
dead-code/closure-call, probe mic_burst — reproduce on the clean tree).
Linux/Windows legs ride the .25/.133 gate.
2026-08-11 23:26:28 +02:00

1522 lines
86 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// punktfunk virtual DualSense / DualShock 4 / DualSense Edge — UMDF2 HID minidriver.
//
// A Rust port of the WDK `vhidmini2` UMDF2 sample, reconfigured to present a Sony DualSense
// (VID 054C / PID 0CE6), DualShock 4 (device_type=1), DualSense Edge (device_type=2), Steam Deck
// (device_type=3), Xbox Wireless Controller (device_type=4, VID 045E / PID 0B13), Xbox One S
// (device_type=5, 045E / 02FD) or Xbox Elite Wireless Controller Series 2 (device_type=6,
// 045E / 0B22) using the
// report descriptors + feature blobs punktfunk already ships in `inject/`. Games see a genuine
// HID PS controller; the host streams input in / reads output (rumble/lightbar/triggers) back.
//
// No WDF object contexts: this is a singleton virtual device, so per-device state lives in statics.
// The host channel is the **sealed pad channel** (design/gamepad-channel-sealing.md, proto v2): the
// whole handshake + all shared-memory access lives in `pf_umdf_util` (the audited unsafe layer), so
// this crate's channel/HID/IOCTL logic is 100% SAFE Rust. The only `unsafe` here is the unavoidable
// WDF setup FFI in DriverEntry/EvtDeviceAdd/the timer, each with a `// SAFETY:` proof.
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
// Every remaining `unsafe {}` (all WDF setup FFI) must carry a `// SAFETY:` proof.
use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
use pf_driver_proto::gamepad::PadShm;
use pf_umdf_util::channel::{ChannelClient, ChannelConfig};
use pf_umdf_util::wdf::{self, Request};
use wdk_sys::{
NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES,
WDF_TIMER_CONFIG, WDFDEVICE, WDFDRIVER, WDFQUEUE, WDFQUEUE__, WDFREQUEST, WDFTIMER,
call_unsafe_wdf_function_binding, windows::OutputDebugStringA,
};
// ---- NTSTATUS values ----
const STATUS_SUCCESS: NTSTATUS = 0;
const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC000_0002u32 as NTSTATUS;
const STATUS_INVALID_PARAMETER: NTSTATUS = 0xC000_000Du32 as NTSTATUS;
use pf_umdf_util::nt_success;
// ---- HID minidriver IOCTLs: CTL_CODE(FILE_DEVICE_KEYBOARD=0x0b, id, METHOD_NEITHER=3, ANY) ----
const fn hid_ctl(id: u32) -> u32 {
(0x0000_000b << 16) | (id << 2) | 3
}
const IOCTL_HID_GET_DEVICE_DESCRIPTOR: u32 = hid_ctl(0);
const IOCTL_HID_GET_REPORT_DESCRIPTOR: u32 = hid_ctl(1);
const IOCTL_HID_READ_REPORT: u32 = hid_ctl(2);
const IOCTL_HID_WRITE_REPORT: u32 = hid_ctl(3);
const IOCTL_HID_GET_DEVICE_ATTRIBUTES: u32 = hid_ctl(9);
const IOCTL_HID_GET_STRING: u32 = hid_ctl(4);
const IOCTL_UMDF_HID_SET_FEATURE: u32 = hid_ctl(20);
const IOCTL_UMDF_HID_GET_FEATURE: u32 = hid_ctl(21);
const IOCTL_UMDF_HID_SET_OUTPUT_REPORT: u32 = hid_ctl(22);
const IOCTL_UMDF_HID_GET_INPUT_REPORT: u32 = hid_ctl(23);
// ---- WDF enum values ----
const WdfIoQueueDispatchParallel: i32 = 2;
const WdfIoQueueDispatchManual: i32 = 3;
const WdfUseDefault: i32 = 2; // WDF_TRI_STATE
const WdfExecutionLevelInheritFromParent: i32 = 1; // WDF_EXECUTION_LEVEL
const WdfSynchronizationScopeInheritFromParent: i32 = 1; // WDF_SYNCHRONIZATION_SCOPE
// ---- DualSense identity ----
const DS_VID: u16 = 0x054C;
const DS_PID: u16 = 0x0CE6;
const DS_VER: u16 = 0x0100;
/// DualShock 4 v2 product id — served (same VID/version) when the host stamps device_type=1.
const DS4_PID: u16 = 0x09CC;
/// DualSense Edge product id — served (same VID/version) when the host stamps device_type=2.
const DS_EDGE_PID: u16 = 0x0DF2;
/// The Steam Deck controller identity (Valve 28DE:1205), served when the host stamps
/// device_type=3. Started as the N4 spike (gamepad-new-types §6) answering "does Steam Input on
/// Windows promote a software-devnode HID Deck?"; it is now a shipping identity — every Steam Deck
/// CLIENT streaming to a Windows host declares it, and `steam_deck_windows` builds the pad.
const DECK_VID: u16 = 0x28DE;
const DECK_PID: u16 = 0x1205;
// ---- Xbox identities (device_type = 4 Wireless / 5 One S / 6 Elite Series 2) ----
//
// WHY THIS EXISTS (field 2026-08-09, `punktfunk-field-windows-pad-dead-0260`): the OTHER Windows
// Xbox backend — `pf-xusb` — registers ONLY `GUID_DEVINTERFACE_XUSB` and has no HID collection at
// all, so it is invisible to Steam's hidapi enumeration, to DirectInput, to `joy.cpl`, and to
// WGI/GameInput. Only classic `XInputGetState` via xinput1_4's interface walk ever sees it. A
// reporter spent two weeks on a dead controller for exactly that reason, and switching the client
// to DualSense — a REAL HID pad through this driver — fixed it instantly. This identity gives the
// Xbox pad the same HID footing the PlayStation ones have always had.
//
// ⚠️⚠️ **The VID/PID is a BLUETOOTH Xbox controller on purpose.** The wired ids the rest of the
// tree uses (`045E:028E` X-Box 360, `045E:02EA` Xbox One S USB) are vendor-class XUSB/GIP devices —
// they expose NO HID interface on real hardware, so a HID child claiming one is a device that has
// never existed and inbox promotion has nothing to match. The Xbox pads that genuinely ARE HID are
// the Bluetooth ones, which Windows binds through HIDCLASS.
const XBOX_VID: u16 = 0x045E;
/// Xbox Wireless Controller (Series X|S), Bluetooth — `device_type = 4`, the default Xbox identity.
/// Chosen over the Xbox One S BT id `0x02FD` because the host's OS floor is Windows 11 22H2, where
/// this is the current-generation identity (so glyphs read "Xbox Series") and SDL's mapping
/// database covers it.
///
/// ⭐ It is also the PID Microsoft's own `xinputhid.inf` allow-lists **twice** (once as a
/// `BTHLEDevice` stage-1 id, once as a plain `HID\…&IG_00` stage-2 id) — measured off `.173`,
/// 2026-08-09. That is not what promotes OUR pad (a software devnode matches no allow-list entry;
/// `pfGamepadXbox`'s `AddReg` writes what the matching sections would have written), but it is why
/// this stays the default of the three.
const XBOX_PID: u16 = 0x0B13;
/// Xbox One S controller over Bluetooth — `device_type = 5`.
///
/// ⚠️ **`02FD` appears in `xinputhid.inf` only as a `BTHENUM` (classic-BT bus) id — it has NO
/// stage-2 `HID\…&IG_00` model line.** That killed it as a "try another PID" lever for the
/// promotion work (handoff §4 B1). It does not block it as an IDENTITY, because our promotion
/// comes from the INF's own `AddReg` rather than from matching Microsoft's list — but if a future
/// Windows servicing update makes promotion depend on the allow-list again, this identity is the
/// one that loses it first. Worth re-measuring on glass before recommending it to anyone.
const XBOX_PID_ONE_S: u16 = 0x02FD;
/// Xbox Elite Wireless Controller Series 2 — `device_type = 6`. This is the pad
/// `tools/hid-descriptor-dump` captured on `.173` (`BTHLE\DEV_686CE647F191`, `REV_0521`), so it is
/// the one identity here whose real hardware we have measured directly.
const XBOX_PID_ELITE2: u16 = 0x0B22;
/// bcdDevice for every Xbox identity.
///
/// Deliberately ONE value rather than per-identity: the real Elite reports `REV_0521` (measured on
/// `.173`) but `create_swdevice` synthesizes the devnode's USB ids with a hardcoded `&REV_0100`
/// regardless, and SDL folds the version into its joystick GUID — so a version that disagrees with
/// the devnode buys nothing and risks missing a stock mapping. Revisit only with a measurement
/// that shows a consumer keying on it.
const XBOX_VER: u16 = 0x0407;
// Sony DualSense USB HID report descriptor (273 bytes), verbatim from inputtino (== inject/dualsense.rs).
// NOTE: inject/dualsense.rs comments this as "232 bytes" — that comment is wrong; it is 273.
#[rustfmt::skip]
static DUALSENSE_RDESC: [u8; 273] = [
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35,
0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0x06,
0x00, 0xFF, 0x09, 0x20, 0x95, 0x01, 0x81, 0x02, 0x05, 0x01, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07,
0x35, 0x00, 0x46, 0x3B, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00, 0x05,
0x09, 0x19, 0x01, 0x29, 0x0F, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0F, 0x81, 0x02, 0x06,
0x00, 0xFF, 0x09, 0x21, 0x95, 0x0D, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x22, 0x15, 0x00, 0x26,
0xFF, 0x00, 0x75, 0x08, 0x95, 0x34, 0x81, 0x02, 0x85, 0x02, 0x09, 0x23, 0x95, 0x2F, 0x91, 0x02,
0x85, 0x05, 0x09, 0x33, 0x95, 0x28, 0xB1, 0x02, 0x85, 0x08, 0x09, 0x34, 0x95, 0x2F, 0xB1, 0x02,
0x85, 0x09, 0x09, 0x24, 0x95, 0x13, 0xB1, 0x02, 0x85, 0x0A, 0x09, 0x25, 0x95, 0x1A, 0xB1, 0x02,
0x85, 0x20, 0x09, 0x26, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x21, 0x09, 0x27, 0x95, 0x04, 0xB1, 0x02,
0x85, 0x22, 0x09, 0x40, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x80, 0x09, 0x28, 0x95, 0x3F, 0xB1, 0x02,
0x85, 0x81, 0x09, 0x29, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x82, 0x09, 0x2A, 0x95, 0x09, 0xB1, 0x02,
0x85, 0x83, 0x09, 0x2B, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x84, 0x09, 0x2C, 0x95, 0x3F, 0xB1, 0x02,
0x85, 0x85, 0x09, 0x2D, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xA0, 0x09, 0x2E, 0x95, 0x01, 0xB1, 0x02,
0x85, 0xE0, 0x09, 0x2F, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF0, 0x09, 0x30, 0x95, 0x3F, 0xB1, 0x02,
0x85, 0xF1, 0x09, 0x31, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF2, 0x09, 0x32, 0x95, 0x0F, 0xB1, 0x02,
0x85, 0xF4, 0x09, 0x35, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF5, 0x09, 0x36, 0x95, 0x03, 0xB1, 0x02,
0xC0,
];
// Feature reports hid-playstation / Steam read during init (each array's first byte is the report id).
#[rustfmt::skip]
static DS_FEATURE_CALIBRATION: [u8; 41] = [ // 0x05 motion calibration: 1 id + 40 data (descriptor declares feature 0x05 as 0x95 0x28 = 40)
0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x27, 0xF0, 0xD8, 0x10, 0x27, 0xF0, 0xD8, 0x10,
0x27, 0xF0, 0xD8, 0xF4, 0x01, 0xF4, 0x01, 0x10, 0x27, 0xF0, 0xD8, 0x10, 0x27, 0xF0, 0xD8, 0x10,
0x27, 0xF0, 0xD8, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00,
];
#[rustfmt::skip]
static DS_FEATURE_PAIRING: [u8; 20] = [ // 0x09 pairing info (MAC at 1..7)
0x09, 0x74, 0xE7, 0xD6, 0x3A, 0x53, 0x35, 0x08, 0x25, 0x00, 0x1E, 0x00, 0xEE, 0x74, 0xD0, 0xBC,
0x00, 0x00, 0x00, 0x00,
];
#[rustfmt::skip]
static DS_FEATURE_FIRMWARE: [u8; 64] = [ // 0x20 firmware info; bytes 44..46 = update version,
// kept ABOVE Sony's real releases (0x0630 as of 2026-08) — an older value makes PlayStation
// Accessories and libScePad titles demand a firmware update the virtual pad cannot take.
// Mirrors inject/proto/dualsense_proto.rs DS_FEATURE_FIRMWARE; keep the two in sync.
0x20, 0x4A, 0x75, 0x6E, 0x20, 0x31, 0x39, 0x20, 0x32, 0x30, 0x32, 0x33, 0x31, 0x34, 0x3A, 0x34,
0x37, 0x3A, 0x33, 0x34, 0x03, 0x00, 0x44, 0x00, 0x08, 0x02, 0x00, 0x01, 0x36, 0x00, 0x00, 0x01,
0xC1, 0xC8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0x09, 0x00, 0x00,
0x14, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
// ---- DualShock 4 v2 assets (served when the host stamps device_type=1) ----
// Sony DualShock 4 v2 USB HID report descriptor (507 bytes), verbatim from inject/dualshock4.rs.
#[rustfmt::skip]
static DS4_RDESC: [u8; 507] = [
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31,
0x09, 0x32, 0x09, 0x35, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95,
0x04, 0x81, 0x02, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46,
0x3B, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00,
0x05, 0x09, 0x19, 0x01, 0x29, 0x0E, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
0x95, 0x0E, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x20, 0x75, 0x06, 0x95,
0x01, 0x15, 0x00, 0x25, 0x7F, 0x81, 0x02, 0x05, 0x01, 0x09, 0x33, 0x09,
0x34, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02,
0x06, 0x00, 0xFF, 0x09, 0x21, 0x95, 0x36, 0x81, 0x02, 0x85, 0x05, 0x09,
0x22, 0x95, 0x1F, 0x91, 0x02, 0x85, 0x04, 0x09, 0x23, 0x95, 0x24, 0xB1,
0x02, 0x85, 0x02, 0x09, 0x24, 0x95, 0x24, 0xB1, 0x02, 0x85, 0x08, 0x09,
0x25, 0x95, 0x03, 0xB1, 0x02, 0x85, 0x10, 0x09, 0x26, 0x95, 0x04, 0xB1,
0x02, 0x85, 0x11, 0x09, 0x27, 0x95, 0x02, 0xB1, 0x02, 0x85, 0x12, 0x06,
0x02, 0xFF, 0x09, 0x21, 0x95, 0x0F, 0xB1, 0x02, 0x85, 0x13, 0x09, 0x22,
0x95, 0x16, 0xB1, 0x02, 0x85, 0x14, 0x06, 0x05, 0xFF, 0x09, 0x20, 0x95,
0x10, 0xB1, 0x02, 0x85, 0x15, 0x09, 0x21, 0x95, 0x2C, 0xB1, 0x02, 0x06,
0x80, 0xFF, 0x85, 0x80, 0x09, 0x20, 0x95, 0x06, 0xB1, 0x02, 0x85, 0x81,
0x09, 0x21, 0x95, 0x06, 0xB1, 0x02, 0x85, 0x82, 0x09, 0x22, 0x95, 0x05,
0xB1, 0x02, 0x85, 0x83, 0x09, 0x23, 0x95, 0x01, 0xB1, 0x02, 0x85, 0x84,
0x09, 0x24, 0x95, 0x04, 0xB1, 0x02, 0x85, 0x85, 0x09, 0x25, 0x95, 0x06,
0xB1, 0x02, 0x85, 0x86, 0x09, 0x26, 0x95, 0x06, 0xB1, 0x02, 0x85, 0x87,
0x09, 0x27, 0x95, 0x23, 0xB1, 0x02, 0x85, 0x88, 0x09, 0x28, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0x89, 0x09, 0x29, 0x95, 0x02, 0xB1, 0x02, 0x85, 0x90,
0x09, 0x30, 0x95, 0x05, 0xB1, 0x02, 0x85, 0x91, 0x09, 0x31, 0x95, 0x03,
0xB1, 0x02, 0x85, 0x92, 0x09, 0x32, 0x95, 0x03, 0xB1, 0x02, 0x85, 0x93,
0x09, 0x33, 0x95, 0x0C, 0xB1, 0x02, 0x85, 0x94, 0x09, 0x34, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0xA0, 0x09, 0x40, 0x95, 0x06, 0xB1, 0x02, 0x85, 0xA1,
0x09, 0x41, 0x95, 0x01, 0xB1, 0x02, 0x85, 0xA2, 0x09, 0x42, 0x95, 0x01,
0xB1, 0x02, 0x85, 0xA3, 0x09, 0x43, 0x95, 0x30, 0xB1, 0x02, 0x85, 0xA4,
0x09, 0x44, 0x95, 0x0D, 0xB1, 0x02, 0x85, 0xF0, 0x09, 0x47, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0xF1, 0x09, 0x48, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF2,
0x09, 0x49, 0x95, 0x0F, 0xB1, 0x02, 0x85, 0xA7, 0x09, 0x4A, 0x95, 0x01,
0xB1, 0x02, 0x85, 0xA8, 0x09, 0x4B, 0x95, 0x01, 0xB1, 0x02, 0x85, 0xA9,
0x09, 0x4C, 0x95, 0x08, 0xB1, 0x02, 0x85, 0xAA, 0x09, 0x4E, 0x95, 0x01,
0xB1, 0x02, 0x85, 0xAB, 0x09, 0x4F, 0x95, 0x39, 0xB1, 0x02, 0x85, 0xAC,
0x09, 0x50, 0x95, 0x39, 0xB1, 0x02, 0x85, 0xAD, 0x09, 0x51, 0x95, 0x0B,
0xB1, 0x02, 0x85, 0xAE, 0x09, 0x52, 0x95, 0x01, 0xB1, 0x02, 0x85, 0xAF,
0x09, 0x53, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xB0, 0x09, 0x54, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0xE0, 0x09, 0x57, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xB3,
0x09, 0x55, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xB4, 0x09, 0x55, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0xB5, 0x09, 0x56, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xD0,
0x09, 0x58, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xD4, 0x09, 0x59, 0x95, 0x3F,
0xB1, 0x02, 0xC0,
];
// DS4 feature reports games read during init (each array's first byte is the report id).
#[rustfmt::skip]
static DS4_FEATURE_PAIRING: [u8; 16] = [ // 0x12 pairing info (MAC at bytes 1..7)
0x12, 0x01, 0x00, 0xEF, 0xBE, 0xAD, 0xDE, 0x08, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
// 0x02 IMU calibration. A consumer (SDL's `SDL_hidapi_ps4`, or `hid-playstation` when this pad
// is read on Linux) DERIVES its motion scale from these words rather than assuming one: gyro
// resolution = (|pitch_plus| + |pitch_minus|) / (speed_plus + speed_minus) LSB per °/s, accel
// resolution = (acc_plus - acc_minus) / 2 LSB per g. So this blob is where the wire contract
// (20 LSB/°·s, 10000 LSB/g) is declared on the DS4 device type, and it must state exactly what
// the wire delivers — the pre-2026-08 values (±16 / speed 32 / ±8192) declared 0.5 LSB/°·s and
// 8192 LSB/g, i.e. every DS4 session read gyro 40× too fast and accel 1.22× hot.
// Mirrors inject/proto/dualshock4_proto.rs DS4_FEATURE_CALIBRATION; this WDK workspace can't
// depend on pf-inject, so pf-inject's `motion_contract` test parses THIS file and re-derives the
// units from it. Keep the two in sync.
#[rustfmt::skip]
static DS4_FEATURE_CALIBRATION: [u8; 37] = [
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x27, 0xF0, 0xD8, 0x10, 0x27, 0xF0, 0xD8, 0x10,
0x27, 0xF0, 0xD8, 0xF4, 0x01, 0xF4, 0x01, 0x10, 0x27, 0xF0, 0xD8, 0x10, 0x27, 0xF0, 0xD8, 0x10,
0x27, 0xF0, 0xD8, 0x00, 0x00,
];
#[rustfmt::skip]
static DS4_FEATURE_FIRMWARE: [u8; 49] = [ // 0xa3 firmware/build info
0xA3, 0x41, 0x75, 0x67, 0x20, 0x20, 0x33, 0x20, 0x32, 0x30, 0x31, 0x33, 0x00, 0x00, 0x00, 0x00,
0x00, 0x30, 0x37, 0x3A, 0x30, 0x31, 0x3A, 0x31, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
];
// ---- DualSense Edge assets (served when the host stamps device_type=2) ----
// Sony DualSense Edge USB HID report descriptor (389 bytes), verbatim from
// inject/proto/dualsense_proto.rs (a real-device capture; see the provenance note there). Input
// report 0x01 is bit-identical to the plain DualSense — the Edge's Fn/back buttons ride reserved
// bits of buttons[2]; output report 0x02 grows to 63 bytes and 19 profile feature reports are added.
#[rustfmt::skip]
static DS_EDGE_RDESC: [u8; 389] = [
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35,
0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0x06,
0x00, 0xFF, 0x09, 0x20, 0x95, 0x01, 0x81, 0x02, 0x05, 0x01, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07,
0x35, 0x00, 0x46, 0x3B, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00, 0x05,
0x09, 0x19, 0x01, 0x29, 0x0F, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0F, 0x81, 0x02, 0x06,
0x00, 0xFF, 0x09, 0x21, 0x95, 0x0D, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x22, 0x15, 0x00, 0x26,
0xFF, 0x00, 0x75, 0x08, 0x95, 0x34, 0x81, 0x02, 0x85, 0x02, 0x09, 0x23, 0x95, 0x3F, 0x91, 0x02,
0x85, 0x05, 0x09, 0x33, 0x95, 0x28, 0xB1, 0x02, 0x85, 0x08, 0x09, 0x34, 0x95, 0x2F, 0xB1, 0x02,
0x85, 0x09, 0x09, 0x24, 0x95, 0x13, 0xB1, 0x02, 0x85, 0x0A, 0x09, 0x25, 0x95, 0x1A, 0xB1, 0x02,
0x85, 0x20, 0x09, 0x26, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x21, 0x09, 0x27, 0x95, 0x04, 0xB1, 0x02,
0x85, 0x22, 0x09, 0x40, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x80, 0x09, 0x28, 0x95, 0x3F, 0xB1, 0x02,
0x85, 0x81, 0x09, 0x29, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x82, 0x09, 0x2A, 0x95, 0x09, 0xB1, 0x02,
0x85, 0x83, 0x09, 0x2B, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x84, 0x09, 0x2C, 0x95, 0x3F, 0xB1, 0x02,
0x85, 0x85, 0x09, 0x2D, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xA0, 0x09, 0x2E, 0x95, 0x01, 0xB1, 0x02,
0x85, 0xE0, 0x09, 0x2F, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF0, 0x09, 0x30, 0x95, 0x3F, 0xB1, 0x02,
0x85, 0xF1, 0x09, 0x31, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF2, 0x09, 0x32, 0x95, 0x34, 0xB1, 0x02,
0x85, 0xF4, 0x09, 0x35, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF5, 0x09, 0x36, 0x95, 0x03, 0xB1, 0x02,
0x85, 0x60, 0x09, 0x41, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x61, 0x09, 0x42, 0xB1, 0x02, 0x85, 0x62,
0x09, 0x43, 0xB1, 0x02, 0x85, 0x63, 0x09, 0x44, 0xB1, 0x02, 0x85, 0x64, 0x09, 0x45, 0xB1, 0x02,
0x85, 0x65, 0x09, 0x46, 0xB1, 0x02, 0x85, 0x68, 0x09, 0x47, 0xB1, 0x02, 0x85, 0x70, 0x09, 0x48,
0xB1, 0x02, 0x85, 0x71, 0x09, 0x49, 0xB1, 0x02, 0x85, 0x72, 0x09, 0x4A, 0xB1, 0x02, 0x85, 0x73,
0x09, 0x4B, 0xB1, 0x02, 0x85, 0x74, 0x09, 0x4C, 0xB1, 0x02, 0x85, 0x75, 0x09, 0x4D, 0xB1, 0x02,
0x85, 0x76, 0x09, 0x4E, 0xB1, 0x02, 0x85, 0x77, 0x09, 0x4F, 0xB1, 0x02, 0x85, 0x78, 0x09, 0x50,
0xB1, 0x02, 0x85, 0x79, 0x09, 0x51, 0xB1, 0x02, 0x85, 0x7A, 0x09, 0x52, 0xB1, 0x02, 0x85, 0x7B,
0x09, 0x53, 0xB1, 0x02, 0xC0,
];
// ---- N4-spike Steam Deck assets (served when the host stamps device_type=3) ----
// The Deck's captured CONTROLLER-interface report descriptor (38 bytes, interface 2 of a real
// 28DE:1205 — verbatim from inject/proto/steam_proto.rs RDESC_DECK_CTRL): one vendor-defined
// (page 0xFFFF) collection with a 64-byte input + 64-byte feature report.
#[rustfmt::skip]
static DECK_RDESC: [u8; 38] = [
0x06, 0xff, 0xff, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x02, 0x09, 0x03, 0x15, 0x00, 0x26, 0xff, 0x00,
0x75, 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x06, 0x09, 0x07, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75,
0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0,
];
// ---- Xbox assets (served when the host stamps device_type = 4, 5 or 6) ----
//
// ⭐⭐ **ONE DESCRIPTOR SERVES ALL THREE XBOX IDENTITIES, DELIBERATELY.** Xbox Wireless (4),
// Xbox One S (5) and Xbox Elite Series 2 (6) differ ONLY in VID/PID, product string and INF model
// line — in HID terms they are the same pad: same two 16-bit stick pairs, same trigger pair, same
// hat, same 15 buttons, same rumble output report. A report descriptor is the report SHAPE, not
// the identity; the identity is what SDL/Steam/Windows key their stock mappings off, and that
// travels in `hid_attrs`.
//
// This is load-bearing, not laziness. The ⚠️ block below is the record of what ONE hand-written
// descriptor has already cost: three separate bugs (no Feature report ⇒ the sealed channel never
// opened and the pad served neutral forever; no OUTPUT item ⇒ no rumble of any kind and dead
// host-side code; a layout that provably disagrees with the captured hardware). Two more
// hand-written descriptors would multiply that debt by three for no measured gain, and each would
// need its own capture, its own `wReportLength`, its own `xbox_proto` layout tests and its own
// on-glass verification. When a Linux-hidraw capture settles the real layout (handoff §3.3), it
// lands here ONCE and all three identities get it.
//
// A standards-clean Game Pad collection matching the Bluetooth Xbox layout: two 16-bit stick pairs,
// two 10-bit triggers on the Simulation page, a null-state hat, and 15 buttons. Report `0x01`,
// [`XBOX_INPUT_REPORT_LEN`] bytes on the wire including the id. `inject/proto/xbox_proto.rs` packs
// the matching bytes host-side; `xbox_proto`'s tests pin the two together.
//
// ⚠️⚠️⚠️ **PROVENANCE: this descriptor is CONSTRUCTED, not captured — unlike every sibling here
// (`DUALSENSE_RDESC` verbatim from inputtino, `DS4_RDESC` verbatim from `inject/dualshock4.rs`,
// `DECK_RDESC` captured off a real `28DE:1205`). It has never been compared against a real pad.**
// That matters more than usual: we claim a REAL Microsoft VID/PID, and SDL / Steam / Windows keep
// built-in mappings keyed off that VID/PID. If a consumer applies its stock `045E:0B13` mapping to a
// report laid out differently from the real device, every control silently lands on the wrong
// action — the same class of bug this whole change exists to kill.
//
// ⭐ **2026-08-09 — THE CAPTURE NOW EXISTS AND THIS BLOB DISAGREES WITH IT.** A real Xbox Elite
// Series 2 (`045E:0B22`, Bluetooth LE) was captured on `.173` with `tools/hid-descriptor-dump`; the
// dump, its provenance and the DualSense control that validates the tool are in
// `tools/hid-descriptor-dump/captures/`. Re-take it any time with `--vid 045E --pid 0B22`, and
// decode THIS array through the same decoder — no hardware needed — with:
//
// hid-descriptor-dump --rust-source packaging/windows/drivers/pf-gamepad/src/lib.rs \
// --symbol XBOX_RDESC
//
// Four differences, and the ORDER one is the dangerous one:
// * the real pad's game-controller report is **UNNUMBERED** (15 bytes of fields, no report id);
// this one declares Report ID 1;
// * it carries **ONE combined 16-bit `Z`** trigger axis at byte 8, not two Simulation-page axes;
// * it declares **16 buttons at byte 10, BEFORE the hat** — this one puts 15 buttons AFTER it;
// * neither has an OUTPUT collection, so the rumble gap is real on both.
//
// 🛑 **Do NOT simply paste the capture over this array.** Two blockers, recorded in
// `design/xbox-pad-windows-handoff.md` §3.3: (1) it is unverified whether Windows' view equals the
// pad's NATIVE report map — `xinputhid` filters that pad and the captured shape is the legacy
// DirectInput view, so cross-check on Linux hidraw first; (2) **the real descriptor has no Feature
// report, and we cannot ship without one** — `0x85` is the sealed channel's proof transport, and
// report ids are all-or-nothing, so declaring it forces a numbered input report the real pad does
// not have. Matching the hardware byte for byte and keeping the sealed channel as it stands are
// mutually exclusive; that needs a decision, not a paste. Whatever lands, re-run `xbox_proto`'s
// layout tests — they pin these offsets on the host side.
//
// ⚠️ The trailing vendor-defined Feature report `0x85` is NOT cosmetic and must not be trimmed as
// "unused": it is the CHANNEL PROOF transport (`ProofTransport::HidFeatureReport`). The captured
// PlayStation descriptors already declared `0x85`, which is why the proof "costs no descriptor
// change" there — but this descriptor is constructed, so it has to declare the report itself. Built
// without it the pad enumerates perfectly and then delivers NOTHING: hidclass rejects the host's
// `HidD_GetFeature` before the driver sees it, the host refuses to hand over the DATA section
// (measured on .173 2026-08-09 — WGI `RawGameController` saw `045E:0B13` with every axis pinned at
// 0.5000 and a timestamp frozen for 12 consecutive samples), and the pad serves only its neutral
// report forever. `0x3F` payload bytes so `FeatureReportByteLength` lands on 64, the buffer size
// `channel_proof::query` asks with; the proof itself needs 17.
#[rustfmt::skip]
static XBOX_RDESC: [u8; 223] = [
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x05, // Usage (Game Pad)
0xA1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x09, 0x30, // Usage (X) — left stick X
0x09, 0x31, // Usage (Y) — left stick Y
0x15, 0x00, // Logical Minimum (0)
0x27, 0xFF, 0xFF, 0x00, 0x00, // Logical Maximum (65535)
0x95, 0x02, // Report Count (2)
0x75, 0x10, // Report Size (16)
0x81, 0x02, // Input (Data,Var,Abs)
0xC0, // End Collection
// 🛑 THE RIGHT STICK IS `Z`/`Rz`, NOT `Rx`/`Ry`. This declared `Rx`/`Ry` until 2026-08-09 and
// the right stick was DEAD: measured on `.173`, with every axis sweeping on its own phase,
// `LX`/`LY`/`LT`/`RT` all reached XInput and `RX [0..0] RY [-1..-1]` never moved. Left and right
// were declared identically here apart from these two usage bytes, so the usages are the whole
// difference — `xinputhid`, which translates this collection into XUSB, maps `Z`/`Rz` to the
// right stick and does not treat `Rx`/`Ry` as one. `DUALSENSE_RDESC` above (a real capture) uses
// `Z`/`Rz` for its right stick too; the PS pads put the TRIGGERS on `Rx`/`Ry`, which is probably
// where the original mistake came from.
// ⚠️ This survived every bench measurement because the devtest only ever swept LS-X — the axis
// that worked — so `RX [0..0]` read as "nothing is driving it". It was found on glass. The
// devtest now sweeps all six axes on distinct phases so the harness can tell those two apart.
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x09, 0x32, // Usage (Z) — right stick X
0x09, 0x35, // Usage (Rz) — right stick Y
0x15, 0x00, // Logical Minimum (0)
0x27, 0xFF, 0xFF, 0x00, 0x00, // Logical Maximum (65535)
0x95, 0x02, // Report Count (2)
0x75, 0x10, // Report Size (16)
0x81, 0x02, // Input (Data,Var,Abs)
0xC0, // End Collection
0x05, 0x02, // Usage Page (Simulation Controls)
0x09, 0xC5, // Usage (Brake) — left trigger
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x03, // Logical Maximum (1023)
0x95, 0x01, // Report Count (1)
0x75, 0x10, // Report Size (16)
0x81, 0x02, // Input (Data,Var,Abs)
0x09, 0xC4, // Usage (Accelerator) — right trigger
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x03, // Logical Maximum (1023)
0x95, 0x01, // Report Count (1)
0x75, 0x10, // Report Size (16)
0x81, 0x02, // Input (Data,Var,Abs)
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x39, // Usage (Hat switch)
0x15, 0x01, // Logical Minimum (1)
0x25, 0x08, // Logical Maximum (8)
0x35, 0x00, // Physical Minimum (0)
0x46, 0x3B, 0x01, // Physical Maximum (315)
0x65, 0x14, // Unit (Eng Rot: Degrees)
0x75, 0x04, // Report Size (4)
0x95, 0x01, // Report Count (1)
0x81, 0x42, // Input (Data,Var,Abs,Null State)
0x65, 0x00, // Unit (None)
0x75, 0x04, // Report Size (4)
0x95, 0x01, // Report Count (1)
0x81, 0x03, // Input (Cnst,Var,Abs) — pad the hat byte
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (Button 1)
0x29, 0x0F, // Usage Maximum (Button 15)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x0F, // Report Count (15)
0x81, 0x02, // Input (Data,Var,Abs)
0x75, 0x01, // Report Size (1)
0x95, 0x01, // Report Count (1)
0x81, 0x03, // Input (Cnst,Var,Abs) — pad to a byte boundary
// ---- Rumble OUTPUT report `0x03` (Physical Interface Device page) ----
//
// Without this the pad can receive NOTHING. hidclass routes an output report only if the
// descriptor declares one, so with no `0x91` item `on_output_report` never fires,
// `publish_output` never writes the ring, and `parse_xbox_output`
// (`inject/windows/xbox_windows.rs`) is unreachable code — the whole host-side rumble plane is
// already built and was simply never fed. That is why the HID Xbox pad had no rumble at all,
// not merely no trigger rumble.
//
// ⚠️ PROVENANCE — HAND-WRITTEN, and it could not be otherwise. Every other output collection in
// this file is a capture, and §3 of `design/xbox-pad-windows-handoff.md` insists on captures.
// But the Elite capture taken for that work reports `OUTPUT items: 0` (Windows exposes no
// literal report-descriptor bytes; hidapi reconstructs from `HidD_GetPreparsedData`, and that
// reconstruction carries no output collection for this pad). So there was nothing to copy.
// This block is the documented Xbox One S / Elite Bluetooth rumble report — PID-page
// `Set Effect Report`, id `0x03`, 8 payload bytes — chosen because it is exactly the layout
// `parse_xbox_output` and `design/trigger-rumble-plane.md` §2.1 already specify:
// [0x03][enable][left_trigger][right_trigger][left][right][duration][delay][loop]
// with magnitudes 0..100 (hence `Logical Maximum (100)`, not 255).
// **Replace it with a Linux hidraw capture when one can be taken** — that is the only route to
// byte-exact truth here, and the enable-bit assignments for the two TRIGGER actuators remain
// unverified (see trigger-rumble-plane.md WP0).
//
// Declared AFTER the final Input item and re-stating every global it uses, so it cannot
// retroactively alter the 16-byte input layout `xbox_proto`'s tests pin.
0x05, 0x0F, // Usage Page (Physical Interface Device)
0x09, 0x21, // Usage (Set Effect Report)
0x85, 0x03, // Report ID (3)
0xA1, 0x02, // Collection (Logical)
0x09, 0x97, // Usage (DC Enable Actuators)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x04, // Report Size (4)
0x95, 0x01, // Report Count (1)
0x91, 0x02, // Output (Data,Var,Abs) — the enable mask, low nibble
0x15, 0x00, // Logical Minimum (0)
0x25, 0x00, // Logical Maximum (0)
0x75, 0x04, // Report Size (4)
0x95, 0x01, // Report Count (1)
0x91, 0x03, // Output (Cnst,Var,Abs) — pad the enable byte
0x09, 0x70, // Usage (Magnitude)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x64, // Logical Maximum (100) — percent, NOT 255
0x75, 0x08, // Report Size (8)
0x95, 0x04, // Report Count (4) — LT, RT, left handle, right handle
0x91, 0x02, // Output (Data,Var,Abs)
0x09, 0x50, // Usage (Duration)
0x66, 0x01, 0x10, // Unit (SI Linear: seconds)
0x55, 0x0E, // Unit Exponent (-2) — centiseconds
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x91, 0x02, // Output (Data,Var,Abs)
0x09, 0xA7, // Usage (Start Delay) — same unit and range as Duration
0x91, 0x02, // Output (Data,Var,Abs)
0x65, 0x00, // Unit (None)
0x55, 0x00, // Unit Exponent (0)
0x09, 0x7C, // Usage (Loop Count)
0x91, 0x02, // Output (Data,Var,Abs)
0xC0, // End Collection
// The channel-proof feature report — see the ⚠️ above. Declared last so it cannot disturb the
// INPUT layout `xbox_proto` packs against: every global item here (Report Size/Count, Logical
// Min/Max) is re-stated after the final Input item, so nothing above is retroactively changed.
0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
0x85, 0x85, // Report ID (0x85)
0x09, 0x2D, // Usage (0x2D) — the id the PS descriptors use for it
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63) — 1 id + 63 = 64 = FeatureReportByteLength
0xB1, 0x02, // Feature (Data,Var,Abs)
0xC0, // End Collection
];
/// Bytes the Xbox input report occupies on the wire, report id included — 1 id + 8 sticks +
/// 4 triggers + 1 hat + 2 buttons. hidclass sizes its READ_REPORT buffer from the descriptor, and
/// [`Request::copy_to_output`] REFUSES a source longer than that buffer (it does not truncate), so
/// the completion path must serve exactly this many bytes. See [`input_report_len`].
const XBOX_INPUT_REPORT_LEN: usize = 16;
// HID descriptor (9 bytes, packed): len, type=0x21, bcdHID=0x0100, country=0, numDesc=1, then
// {reportType=0x22, wReportLength}. DualSense = 273 (0x0111); DualShock 4 = 507 (0x01FB);
// DualSense Edge = 389 (0x0185).
static HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0x11, 0x01];
static DS4_HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0xFB, 0x01];
static EDGE_HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0x85, 0x01];
static DECK_HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0x26, 0x00]; // 38 bytes
// Serves device_type 4, 5 AND 6 — one descriptor, three identities (see the XBOX_RDESC header).
static XBOX_HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0xDF, 0x00]; // 223 bytes
// Each `wReportLength` above is a SECOND copy of a length that already exists as its descriptor's
// array size, and the two are edited in different places. Getting them out of step does not fail
// loudly — hidclass asks for `wReportLength` bytes and then parses whatever it got, so the pad
// either enumerates with a truncated descriptor or fails to enumerate at all, with nothing naming
// the cause. Assert the pairing at compile time instead; adding an item to a descriptor now cannot
// build until its length is updated too.
const fn declared_len(hid_desc: &[u8; 9]) -> usize {
(hid_desc[7] as usize) | ((hid_desc[8] as usize) << 8)
}
const _: () = assert!(declared_len(&HID_DESC) == DUALSENSE_RDESC.len());
const _: () = assert!(declared_len(&DS4_HID_DESC) == DS4_RDESC.len());
const _: () = assert!(declared_len(&EDGE_HID_DESC) == DS_EDGE_RDESC.len());
const _: () = assert!(declared_len(&DECK_HID_DESC) == DECK_RDESC.len());
const _: () = assert!(declared_len(&XBOX_HID_DESC) == XBOX_RDESC.len());
// HID_DEVICE_ATTRIBUTES (32 bytes): Size(u32)=32, VendorID, ProductID, VersionNumber, Reserved[11].
// `devtype` selects the identity: PS family (same Sony VID/version), the N4-spike Deck, or one of
// the three Xbox pads (same Microsoft VID/version — only the PID differs, which is the entire
// difference between them; they share a report descriptor).
//
// ⚠️ THIS is where an Xbox identity is actually decided. Everything else in the Xbox path —
// descriptor, HID descriptor, report length, neutral report — is shared, so a new Xbox model is a
// PID here, a product string in `on_get_string`, an INF model line and nothing else.
fn hid_attrs(devtype: u8) -> [u8; 32] {
let (vid, pid, ver) = match devtype {
1 => (DS_VID, DS4_PID, DS_VER),
2 => (DS_VID, DS_EDGE_PID, DS_VER),
3 => (DECK_VID, DECK_PID, DS_VER),
4 => (XBOX_VID, XBOX_PID, XBOX_VER),
5 => (XBOX_VID, XBOX_PID_ONE_S, XBOX_VER),
6 => (XBOX_VID, XBOX_PID_ELITE2, XBOX_VER),
_ => (DS_VID, DS_PID, DS_VER),
};
let mut a = [0u8; 32];
a[0..4].copy_from_slice(&32u32.to_le_bytes());
a[4..6].copy_from_slice(&vid.to_le_bytes());
a[6..8].copy_from_slice(&pid.to_le_bytes());
a[8..10].copy_from_slice(&ver.to_le_bytes());
a
}
/// Bytes to hand a pended `IOCTL_HID_READ_REPORT`, per identity.
///
/// The PlayStation/Deck identities all declare 64-byte input reports, which is why the report slot
/// and [`INPUT_REPORT`] are 64 bytes wide and the completion path could hand the whole buffer over
/// unconditionally. The Xbox identity declares a [`XBOX_INPUT_REPORT_LEN`]-byte report, and
/// [`Request::copy_to_output`] returns `STATUS_INVALID_BUFFER_SIZE` when the source is LONGER than
/// the caller's buffer rather than truncating — so handing hidclass 64 bytes for a 16-byte report
/// fails every single read and the pad looks dead.
///
/// Returns 64 for every pre-existing identity, so this is provably a no-op for them. All three
/// Xbox identities share one descriptor, hence one report length.
fn input_report_len(devtype: u8) -> usize {
match devtype {
4..=6 => XBOX_INPUT_REPORT_LEN,
_ => 64,
}
}
// Neutral DualSense input report 0x01 (64 bytes): sticks centered (0x80), triggers 0, dpad neutral (8).
const NEUTRAL_REPORT: [u8; 64] = {
let mut r = [0u8; 64];
r[0] = 0x01; // report id
r[1] = 0x80; // LX
r[2] = 0x80; // LY
r[3] = 0x80; // RX
r[4] = 0x80; // RY
// r[5]=L2, r[6]=R2 = 0; r[7] = seq counter = 0
r[8] = 0x08; // buttons[0]: low nibble = dpad hat (8 = neutral), high nibble = face buttons (0)
r
};
// Neutral DualShock 4 input report 0x01: sticks centered (0x80); the dpad hat is in byte 5 (low
// nibble), so a neutral hat (8) lands there instead of byte 8.
const DS4_NEUTRAL_REPORT: [u8; 64] = {
let mut r = [0u8; 64];
r[0] = 0x01; // report id
r[1] = 0x80; // LX
r[2] = 0x80; // LY
r[3] = 0x80; // RX
r[4] = 0x80; // RY
r[5] = 0x08; // buttons[0]: low nibble = dpad hat (8 = neutral), high nibble = face buttons (0)
r
};
// Neutral Steam Deck input frame (unnumbered): header [0x01, 0x00, ID_CONTROLLER_DECK_STATE=0x09,
// payload-len 0x3C], everything released.
const DECK_NEUTRAL_REPORT: [u8; 64] = {
let mut r = [0u8; 64];
r[0] = 0x01;
r[2] = 0x09;
r[3] = 0x3C;
r
};
// Neutral Xbox input report 0x01: both sticks centred (0x8000 on a 0..65535 axis), triggers 0,
// hat 0 (the descriptor's NULL state — the logical range starts at 1), no buttons held. Only the
// first [`XBOX_INPUT_REPORT_LEN`] bytes are ever served; the rest of the 64-byte slot stays zero so
// the shared [`INPUT_REPORT`] type is unchanged.
const XBOX_NEUTRAL_REPORT: [u8; 64] = {
let mut r = [0u8; 64];
r[0] = 0x01; // report id
r[2] = 0x80; // LX = 0x8000 (little-endian)
r[3] = 0xFF; // LY = 0x7FFF — the Y axes are INVERTED (+y is up on the wire, down in HID),
r[4] = 0x7F; // and mirroring an even-sized range centres one unit low. See `xbox_proto`.
r[6] = 0x80; // RX = 0x8000
r[7] = 0xFF; // RY = 0x7FFF
r[8] = 0x7F;
r
};
fn neutral_report(devtype: u8) -> [u8; 64] {
match devtype {
1 => DS4_NEUTRAL_REPORT,
3 => DECK_NEUTRAL_REPORT,
// Wireless / One S / Elite Series 2 — one report shape, three identities.
4..=6 => XBOX_NEUTRAL_REPORT,
_ => NEUTRAL_REPORT, // DualSense and Edge share the report 0x01 shape
}
}
static MANUAL_QUEUE: AtomicPtr<WDFQUEUE__> = AtomicPtr::new(core::ptr::null_mut());
/// The latest input report the host pushed (report `0x01`) via shared memory; the timer delivers it
/// to pended game READ_REPORTs. Defaults to neutral until the host connects.
static INPUT_REPORT: std::sync::Mutex<[u8; 64]> = std::sync::Mutex::new(NEUTRAL_REPORT);
// ---- the sealed pad channel: layouts + offsets from pf_driver_proto (drift = compile error) ----
// UMDF runs in WUDFHost.exe (user-mode) and hidclass blocks a control channel on the device stack
// (custom interface CreateFile → err 31; custom IOCTL on the HID handle → err 1) and UMDF has no
// control device. So the DATA section (`PadShm` — input report @8, output seq @72, output
// report @76, device_type @140, health marks @144/@148, pad_index @152, output-report ring
// @156..) is UNNAMED and reached only
// through a handle the SYSTEM host duplicated into this WUDFHost, bootstrapped over the named mailbox
// `Global\pfds-boot-<index>`. The handshake + all shared-memory access live in `pf_umdf_util`.
const SHM_MAGIC: u32 = pf_driver_proto::gamepad::PAD_MAGIC; // "PFDS"
const SHM_SIZE: usize = core::mem::size_of::<PadShm>();
const GAMEPAD_PROTO_VERSION: u32 = pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION;
// PadShm field offsets (the driver reads input + device_type, writes output + health marks).
const OFF_INPUT: usize = core::mem::offset_of!(PadShm, input);
const OFF_OUT_SEQ: usize = core::mem::offset_of!(PadShm, out_seq);
const OFF_OUTPUT: usize = core::mem::offset_of!(PadShm, output);
const OFF_DEVICE_TYPE: usize = core::mem::offset_of!(PadShm, device_type);
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(PadShm, driver_proto);
const OFF_DRIVER_HEARTBEAT: usize = core::mem::offset_of!(PadShm, driver_heartbeat);
const OFF_PAD_INDEX: usize = core::mem::offset_of!(PadShm, pad_index);
// v2.1/v2.2 output-report ring (see PadShm docs in pf_driver_proto).
const OFF_OUT_RING_VER: usize = core::mem::offset_of!(PadShm, out_ring_ver);
const OFF_RING_HEAD: usize = core::mem::offset_of!(PadShm, ring_head);
const OFF_OUT_RING_LEN: usize = core::mem::offset_of!(PadShm, out_ring_len);
const OFF_OUT_RING: usize = core::mem::offset_of!(PadShm, out_ring);
const OFF_INPUT_GEN: usize = core::mem::offset_of!(PadShm, input_gen);
/// How many timer ticks separate two runs of the channel/health housekeeping. The tick itself is
/// [`TIMER_PERIOD_MS`]; the pump, the `driver_proto` stamp and the heartbeat keep their historical
/// ~8 ms cadence so nothing that watches them changes rate — only the input path got faster.
const PUMP_EVERY_N_TICKS: u32 = 4;
/// Timer period. Was 8 ms, which — with one pended READ_REPORT completed per tick — capped what a
/// game could observe at ~125 Hz and added up to 8 ms of latency, while clients stream motion at
/// ~250 Hz. 2 ms is about a real DualShock 4's Bluetooth cadence and leaves headroom above the
/// client rate; the extra ticks only do the cheap half (read the input slot, complete one pended
/// read), see [`PUMP_EVERY_N_TICKS`].
const TIMER_PERIOD_MS: u32 = 2;
/// Read the host's input report out of the section under the v2.3 seqlock, so a report caught
/// mid-copy is retried instead of handed to a game.
///
/// The host takes `input_gen` odd before writing the 64 bytes and even after, so an odd sample or
/// a changed one means the read straddled a write. One retry: the host publishes in microseconds
/// and this runs on a 2 ms timer, so a second collision is not a thing that happens, and if it did,
/// re-serving the previous whole report beats serving a torn one.
///
/// Against a pre-v2.3 host the field is never written, so it reads 0 — constant and even — and
/// this accepts on the first pass, exactly as the driver behaved before the seqlock existed.
/// `false` means "no whole report available"; the caller keeps what it had.
fn read_input_report(view: &pf_umdf_util::section::MappedView, buf: &mut [u8; 64]) -> bool {
for _ in 0..2 {
let before = view.load_u32(OFF_INPUT_GEN, Ordering::Acquire);
if !before.is_multiple_of(2) {
continue; // a write is in flight right now
}
view.read_bytes(OFF_INPUT, buf);
// Acquire: the body reads above must not sink below this sample of the generation.
if view.load_u32(OFF_INPUT_GEN, Ordering::Acquire) == before {
return true;
}
}
false
}
const OUT_SLOT_SIZE: usize = core::mem::size_of::<pf_driver_proto::gamepad::OutSlot>();
const OUT_RING_LEN: u32 = pf_driver_proto::gamepad::OUT_RING_LEN;
const OUT_RING_LEN_V22: u32 = pf_driver_proto::gamepad::OUT_RING_LEN_V22;
/// The output-ring length this side's slot math uses against the attached section — the driver's
/// half of the v2.2 negotiation (PadShm docs): the host's `out_ring_ver` stamp declares what it
/// can drain, the mapped length proves the slots exist in OUR view, and the shorter understanding
/// wins. `0` = no ring (pre-v2.1 host, or a fallback-size map too small to hold one) — legacy
/// latest-slot only. Constant per attachment (both inputs are fixed once the view exists).
fn ring_len(view: &pf_umdf_util::section::MappedView) -> u32 {
if view.read_u32(OFF_OUT_RING_VER) == 0
|| view.mapped_len() < pf_driver_proto::gamepad::PAD_SHM_V21_SIZE
{
return 0;
}
if view.read_u32(OFF_OUT_RING_VER) >= 2
&& view.mapped_len() >= pf_driver_proto::gamepad::PAD_SHM_SIZE
{
OUT_RING_LEN_V22
} else {
OUT_RING_LEN
}
}
/// Publish one game output report to the host: the legacy latest-report slot + `out_seq` bump
/// (every host generation reads this), and — when the host stamped `out_ring_ver` (it created the
/// ring region) and our view maps it — the lossless report ring: slot bytes first, then the
/// [`ring_len`] echo (the v2.2 length negotiation — a pre-v2.2 host never reads it), then the
/// `ring_head` bump with `Release` so the host's Acquire load can never observe the bump without
/// the slot bytes and the length that indexed them. The ring is what stops a rumble-STOP report
/// from being coalesced away by a following LED/trigger report inside one host poll window (the
/// confirmed stuck-rumble path).
fn publish_output(view: &pf_umdf_util::section::MappedView, bytes: &[u8]) {
// Serialized: the whole publish is a read-modify-write (read the cursor, write the slot it
// names, then advance it) and the framework dispatches output callbacks in PARALLEL, so two
// can be inside this at once. Unsynchronized, both read the same `ring_head`, both write the
// SAME slot — tearing one report's bytes across the other's — and both store head+1, so the
// cursor advances once for two reports and the host sees a single torn entry.
//
// An atomic `fetch_add` on the head does not fix it. That hands each writer a distinct slot,
// but it advances the cursor BEFORE the slot bytes exist, so the host can read a slot that is
// still being filled — trading a torn slot for a torn slot the host is invited to read. Making
// the head-advance mean "the slot below is complete" is exactly what the lock buys.
//
// Poison-tolerant on purpose. Poison is sticky, so the repo's usual `if let Ok(g) = lock()`
// would skip the publish for the REST OF THE PROCESS after a single panic elsewhere — silently
// ending game output. Recovering the guard is safe here: the protected state is bytes in a
// shared section, not an invariant a panic could have broken.
let _publish = RING_PUBLISH
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
view.write_bytes(OFF_OUTPUT, bytes);
let seq = view.read_u32(OFF_OUT_SEQ).wrapping_add(1);
// Release, not a plain write: the host loads `out_seq` with Acquire specifically to order its
// copy of the report bytes after it (`dualsense_windows.rs`, "Acquire pairs with the driver's
// publish-then-bump store order"). An Acquire load pairs with a Release store and nothing
// else, so as a plain write this promised the host an ordering it never actually established —
// on a weakly-ordered core (ARM64) the fresh seq could arrive ahead of the bytes it announces.
view.store_u32(OFF_OUT_SEQ, seq, Ordering::Release);
let len = ring_len(view);
if len != 0 {
let head = view.read_u32(OFF_RING_HEAD);
let slot = OFF_OUT_RING + (head % len) as usize * OUT_SLOT_SIZE;
let n = bytes.len().min(64);
view.write_u32(slot, n as u32);
view.write_bytes(slot + 4, &bytes[..n]);
view.write_u32(OFF_OUT_RING_LEN, len);
view.store_u32(OFF_RING_HEAD, head.wrapping_add(1), Ordering::Release);
}
}
/// Serializes [`publish_output`] against itself — see the note there for why an atomic cursor is
/// not enough. Uncontended in the common case: one output report at a time is the norm, and the
/// critical section is a few dozen bytes of memcpy into an already-mapped view.
static RING_PUBLISH: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// The sealed-channel client (per-pad: `ProcessSharingDisabled` gives each pad its own WUDFHost, so
/// this static is per-pad). The handshake/adoption/validation state machine lives in `pf_umdf_util`.
static CHANNEL: ChannelClient = ChannelClient::new();
/// The last observed `device_type` (0 = DualSense, 1 = DualShock 4, 2 = DualSense Edge,
/// 3 = Steam Deck, 4 = Xbox Wireless, 5 = Xbox One S, 6 = Xbox Elite Series 2) — the
/// neutral-report shape when the channel detaches, and the fallback identity while unattached.
static LAST_DEVTYPE: AtomicU32 = AtomicU32::new(0);
/// The identity resolved from the devnode's PnP hardware ids at `EvtDeviceAdd` ([`devtype_from_hwids`]);
/// `u32::MAX` = not resolved. See [`device_type`] for why this exists.
static PNP_DEVTYPE: AtomicU32 = AtomicU32::new(u32::MAX);
/// Timer ticks since load — picks the [`PUMP_EVERY_N_TICKS`] ticks that also do the channel
/// handshake and health marks. Wrapping is fine: only its residue matters.
static TICK: AtomicU32 = AtomicU32::new(0);
/// Map a devnode's hardware-id list (lowercase, `;`-separated — see
/// [`wdf::query_hardware_ids`](pf_umdf_util::wdf::query_hardware_ids)) to the `device_type` the host
/// stamps into the section. The host picks one `pf_*` id per identity and lists it FIRST (it is the
/// INF binding contract, pinned by `dualsense_windows::drain_tests::hwid_matches_inf`), so the two
/// can never disagree.
///
/// Order matters: `pf_dualsense` is a prefix of `pf_dualsenseedge`, so the Edge is tested first.
/// (No Xbox token is a prefix of another — `pf_xboxwireless` / `pf_xboxones` / `pf_xboxelite`
/// diverge at the 8th character — but `hwid_devtype_table_matches_the_driver` re-checks that for
/// every pair rather than trusting this note.)
fn devtype_from_hwids(ids: &str) -> Option<u8> {
for (token, devtype) in [
("pf_xboxwireless", 4u8),
("pf_xboxones", 5),
("pf_xboxelite", 6),
("pf_steamdeck", 3),
("pf_dualsenseedge", 2),
("pf_dualshock4", 1),
("pf_dualsense", 0),
] {
if ids.contains(token) {
return Some(devtype);
}
}
None
}
/// This pad's channel config (magic/size/pad_index offset + our logger).
fn channel_cfg() -> ChannelConfig {
ChannelConfig {
tag: "pf-gamepad",
boot_name_prefix: "Global\\pfds-boot-",
data_magic: SHM_MAGIC,
data_size: SHM_SIZE,
// The v2.1 layout grew by tail extension (the output-report ring); against an old host's
// 256-byte section the full-size map still succeeds (sections are page-granular), but if
// it is ever refused, mapping the legacy size keeps the pad alive with the ring disabled.
min_data_size: pf_driver_proto::gamepad::PAD_SHM_LEGACY_SIZE,
pad_index_off: OFF_PAD_INDEX,
log,
}
}
/// The wire pad index the host stamped into the sealed section (0 while the channel hasn't
/// attached yet). Keys every per-pad identity surface: the Deck unit id + serial, the PS
/// identities' pairing MAC (feature 0x09/0x12) and USB serial string — SDL/Steam dedup
/// controllers by serial, so two virtual pads must never share one (identical serials make a
/// second pad read as the FIRST one re-appearing over another transport, and it is merged).
fn pad_index() -> u8 {
(CHANNEL
.data()
.map(|v| v.read_u32(OFF_PAD_INDEX))
.unwrap_or(0)
& 0xFF) as u8
}
/// Whether the world-writable bring-up file log is enabled (resolved once). OPT-IN — debug builds,
/// or the `PFGAMEPAD_DEBUG_LOG` (system-wide) env var — the same treatment pf-vdisplay got in audit
/// §4.4: a RELEASE driver never writes the Public file (info-leak/DoS surface), and the per-report
/// OUTPUT hex dumps stop being a sustained disk-write path during gameplay. DebugView can't see the
/// UMDF host across session 0, so the file stays the bring-up diagnostic when enabled.
fn file_log_enabled() -> bool {
use std::sync::OnceLock;
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| cfg!(debug_assertions) || std::env::var_os("PFGAMEPAD_DEBUG_LOG").is_some())
}
/// Process-lifetime append handle to the bring-up log, opened ONCE and shared via a `Mutex`
/// (pf-vdisplay's pattern) — no per-line open/close.
fn file_appender() -> Option<&'static std::sync::Mutex<std::fs::File>> {
use std::sync::OnceLock;
static APPENDER: OnceLock<Option<std::sync::Mutex<std::fs::File>>> = OnceLock::new();
APPENDER
.get_or_init(|| {
if !file_log_enabled() {
return None;
}
// WUDFHost's own (LocalService) temp dir — NOT world-writable/readable `C:\Users\Public`,
// where the OUTPUT/feature-report hex dumps could leak per-pad identity/serial material to
// any local reader (security-review 2026-07-17). Opt-in/debug only.
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(std::env::temp_dir().join("pf_gamepad-driver.log"))
.ok()
.map(std::sync::Mutex::new)
})
.as_ref()
}
fn log(s: &str) {
// Gated as a whole on [`file_log_enabled`] (the pf-xusb/pf-mouse treatment): `OutputDebugStringA`
// used to fire unconditionally — a syscall + CString alloc per logged event in a RELEASE driver,
// on per-IOCTL paths (the OUTPUT hex dumps during rumble, the cyclic GET_STRING polls). Debug
// builds and the env-var opt-in keep the full debug-string + file tee.
if !file_log_enabled() {
return;
}
if let Ok(c) = std::ffi::CString::new(s) {
// SAFETY: c is a valid null-terminated string for the duration of the call.
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
}
use std::io::Write;
if let Some(m) = file_appender()
&& let Ok(mut f) = m.lock()
{
let _ = writeln!(f, "{s}");
}
}
// The `file_log_enabled()` pre-check skips the `format!` alloc too when logging is off.
macro_rules! dbglog { ($($a:tt)*) => { if file_log_enabled() { log(&format!($($a)*)) } } }
#[unsafe(export_name = "DriverEntry")]
pub unsafe extern "system" fn driver_entry(
driver: PDRIVER_OBJECT,
registry_path: PCUNICODE_STRING,
) -> NTSTATUS {
log("[pf-gamepad] DriverEntry");
// SAFETY: zeroed WDF_DRIVER_CONFIG is a valid all-null config; we then set Size + the callback.
let mut config: WDF_DRIVER_CONFIG = unsafe { core::mem::zeroed() };
config.Size = core::mem::size_of::<WDF_DRIVER_CONFIG>() as ULONG;
config.EvtDriverDeviceAdd = Some(evt_device_add);
// SAFETY: all pointers valid; driver/registry_path provided by the loader.
unsafe {
call_unsafe_wdf_function_binding!(
WdfDriverCreate,
driver,
registry_path,
WDF_NO_OBJECT_ATTRIBUTES,
&mut config,
WDF_NO_HANDLE.cast::<WDFDRIVER>()
)
}
}
extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INIT) -> NTSTATUS {
log("[pf-gamepad] EvtDeviceAdd");
// Mark as a filter (HID minidriver sits below mshidumdf.sys).
// SAFETY: device_init is provided by the framework and non-null.
unsafe { call_unsafe_wdf_function_binding!(WdfFdoInitSetFilter, device_init) };
let mut device: WDFDEVICE = core::ptr::null_mut();
// SAFETY: device_init valid; attributes allowed null; device receives the handle.
let st = unsafe {
call_unsafe_wdf_function_binding!(
WdfDeviceCreate,
&mut device_init,
WDF_NO_OBJECT_ATTRIBUTES,
&mut device
)
};
if !nt_success(st) {
dbglog!("[pf-gamepad] WdfDeviceCreate failed 0x{:08x}", st as u32);
return st;
}
// SAFETY: `device` is the live device just created — the exact contract this fn requires.
let shm_idx = unsafe { wdf::query_location_index(device) };
CHANNEL.set_index(shm_idx);
dbglog!("[pf-gamepad] shm index = {shm_idx}");
// Settle WHICH controller we are before hidclass asks (see `device_type`): the PnP hardware ids
// are the only identity available this early, and every descriptor/attribute answer depends on it.
// SAFETY: `device` is the live device just created — the exact contract this fn requires.
let hwids = unsafe { wdf::query_hardware_ids(device) };
match devtype_from_hwids(&hwids) {
Some(t) => {
PNP_DEVTYPE.store(t as u32, Ordering::Relaxed);
LAST_DEVTYPE.store(t as u32, Ordering::Relaxed);
dbglog!("[pf-gamepad] identity from PnP hardware ids: device_type={t} ({hwids})");
}
// No pf_* id: an unexpected devnode (or a property query that failed). Keep the historical
// behaviour — wait for the channel, then fall back to DualSense.
None => dbglog!(
"[pf-gamepad] no pf_* hardware id in ({hwids}) — identity deferred to the channel"
),
}
// Default parallel queue handling all IOCTLs.
// SAFETY: zeroed config then fields set; Size matches the struct.
let mut qcfg: WDF_IO_QUEUE_CONFIG = unsafe { core::mem::zeroed() };
qcfg.Size = core::mem::size_of::<WDF_IO_QUEUE_CONFIG>() as ULONG;
qcfg.DispatchType = WdfIoQueueDispatchParallel;
qcfg.PowerManaged = WdfUseDefault;
qcfg.DefaultQueue = 1;
qcfg.EvtIoDeviceControl = Some(evt_io_device_control);
// WDF_IO_QUEUE_CONFIG_INIT sets this to (ULONG)-1 (unlimited); mem::zeroed left it 0,
// which on a parallel queue means present ZERO requests → EvtIoDeviceControl never fires.
qcfg.Settings.Parallel.NumberOfPresentedRequests = u32::MAX;
let mut default_queue: WDFQUEUE = core::ptr::null_mut();
// SAFETY: device + config valid; attributes null; queue receives the handle.
let st = unsafe {
call_unsafe_wdf_function_binding!(
WdfIoQueueCreate,
device,
&mut qcfg,
WDF_NO_OBJECT_ATTRIBUTES,
&mut default_queue
)
};
if !nt_success(st) {
dbglog!(
"[pf-gamepad] default WdfIoQueueCreate failed 0x{:08x}",
st as u32
);
return st;
}
// Manual queue: pended READ_REPORT requests are completed by the timer.
// SAFETY: zeroed config then fields set.
let mut mcfg: WDF_IO_QUEUE_CONFIG = unsafe { core::mem::zeroed() };
mcfg.Size = core::mem::size_of::<WDF_IO_QUEUE_CONFIG>() as ULONG;
mcfg.DispatchType = WdfIoQueueDispatchManual;
mcfg.PowerManaged = WdfUseDefault;
let mut manual_queue: WDFQUEUE = core::ptr::null_mut();
// SAFETY: device + config valid; attributes null; queue receives the handle.
let st = unsafe {
call_unsafe_wdf_function_binding!(
WdfIoQueueCreate,
device,
&mut mcfg,
WDF_NO_OBJECT_ATTRIBUTES,
&mut manual_queue
)
};
if !nt_success(st) {
dbglog!(
"[pf-gamepad] manual WdfIoQueueCreate failed 0x{:08x}",
st as u32
);
return st;
}
MANUAL_QUEUE.store(manual_queue, Ordering::SeqCst);
// Periodic timer (parent = manual queue) completes pended reads with the neutral report.
// SAFETY: zeroed config then fields set.
let mut tcfg: WDF_TIMER_CONFIG = unsafe { core::mem::zeroed() };
tcfg.Size = core::mem::size_of::<WDF_TIMER_CONFIG>() as ULONG;
tcfg.EvtTimerFunc = Some(evt_timer);
tcfg.Period = TIMER_PERIOD_MS;
tcfg.AutomaticSerialization = 1; // TRUE — UMDF requires a serialized timer (vhidmini2 pattern)
// SAFETY: a zeroed WDF_OBJECT_ATTRIBUTES is a valid all-null attributes struct; we set Size + the
// fields we use below.
let mut tattr: WDF_OBJECT_ATTRIBUTES = unsafe { core::mem::zeroed() };
tattr.Size = core::mem::size_of::<WDF_OBJECT_ATTRIBUTES>() as ULONG;
tattr.ParentObject = manual_queue.cast();
// mem::zeroed leaves these at 0 (Invalid) → set them like WDF_OBJECT_ATTRIBUTES_INIT
// (matches the working vhidmini2 UMDF timer setup; avoids 0xc0200209 / 0xc00000bb).
tattr.ExecutionLevel = WdfExecutionLevelInheritFromParent;
tattr.SynchronizationScope = WdfSynchronizationScopeInheritFromParent;
let mut timer: WDFTIMER = core::ptr::null_mut();
// SAFETY: config + attributes valid; timer receives the handle.
let st = unsafe {
call_unsafe_wdf_function_binding!(WdfTimerCreate, &mut tcfg, &mut tattr, &mut timer)
};
if !nt_success(st) {
dbglog!("[pf-gamepad] WdfTimerCreate failed 0x{:08x}", st as u32);
return st;
}
let due = -(TIMER_PERIOD_MS as i64) * 10_000;
// SAFETY: timer valid; the due time is TIMER_PERIOD_MS in 100 ns units, negative = relative.
let _started = unsafe { call_unsafe_wdf_function_binding!(WdfTimerStart, timer, due) };
log("[pf-gamepad] device ready");
STATUS_SUCCESS
}
extern "C" fn evt_io_device_control(
_queue: WDFQUEUE,
request: WDFREQUEST,
_output_len: usize,
_input_len: usize,
ioctl: ULONG,
) {
// SAFETY: `request` is the live request for THIS EvtIoDeviceControl invocation — exactly the
// contract `Request::new` requires. Everything after is safe (the token owns completion).
let request = unsafe { Request::new(request) };
// Skip the 8ms READ_REPORT cadence so the log stays readable during a game test;
// the 0x02 OUTPUT report (the gate) and the descriptor handshake still log.
if ioctl != IOCTL_HID_READ_REPORT {
dbglog!("[pf-gamepad] ioctl 0x{ioctl:08x} out={_output_len} in={_input_len}");
}
// READ_REPORT forwards to the manual queue (the timer completes it) — this CONSUMES the request
// token, so it's handled apart from the status-and-complete paths below.
if ioctl == IOCTL_HID_READ_REPORT {
let mq: WDFQUEUE = MANUAL_QUEUE.load(Ordering::SeqCst);
// SAFETY: `mq` is the manual queue created in EvtDeviceAdd (a live WDFQUEUE of this device).
match unsafe { request.forward_to_queue(mq) } {
Ok(()) => {} // framework owns it now (completed by the timer)
Err((req, st)) => req.complete(st), // forward failed → complete with the error
}
return;
}
let status: NTSTATUS = match ioctl {
IOCTL_HID_GET_DEVICE_DESCRIPTOR => request.copy_to_output(match device_type() {
1 => &DS4_HID_DESC,
2 => &EDGE_HID_DESC,
3 => &DECK_HID_DESC,
4..=6 => &XBOX_HID_DESC,
_ => &HID_DESC,
}),
IOCTL_HID_GET_DEVICE_ATTRIBUTES => request.copy_to_output(&hid_attrs(device_type())),
// The three Xbox identities share ONE report descriptor on purpose — see the XBOX_RDESC
// header. Only `hid_attrs` (VID/PID) and `on_get_string` (product string) tell them apart.
IOCTL_HID_GET_REPORT_DESCRIPTOR => request.copy_to_output(match device_type() {
1 => &DS4_RDESC[..],
2 => &DS_EDGE_RDESC[..],
3 => &DECK_RDESC[..],
4..=6 => &XBOX_RDESC[..],
_ => &DUALSENSE_RDESC[..],
}),
IOCTL_HID_WRITE_REPORT | IOCTL_UMDF_HID_SET_OUTPUT_REPORT => {
on_output_report(&request, ioctl)
}
IOCTL_UMDF_HID_SET_FEATURE => on_set_feature(&request),
IOCTL_UMDF_HID_GET_FEATURE => on_get_feature(&request),
// Sliced to the identity's declared report length for the same reason the timer's
// completion is (see `input_report_len`): a source longer than the caller's buffer is
// refused outright, not truncated.
IOCTL_UMDF_HID_GET_INPUT_REPORT => {
let dt = device_type();
request.copy_to_output(&neutral_report(dt)[..input_report_len(dt)])
}
IOCTL_HID_GET_STRING => on_get_string(&request),
// The channel proof (see `pf_umdf_util::hid`): the host asks THIS devnode which process
// serves it, and duplicates the DATA section into the answer — so it never has to trust the
// LocalService-writable bootstrap mailbox to name its target.
_ => STATUS_NOT_IMPLEMENTED,
};
dbglog!(
"[pf-gamepad] ioctl 0x{ioctl:08x} -> 0x{:08x}",
status as u32
);
request.complete(status);
}
// The 0x02 gate: a game writing an output report (rumble / lightbar / ADAPTIVE TRIGGERS). Per the
// UMDF marshalling convention the report data is the *input* buffer and the report id is carried in
// the *output* buffer length. We log it, then publish it to the DATA section for the host.
fn on_output_report(request: &Request, ioctl: ULONG) -> NTSTATUS {
let (bytes, inlen) = match request.input_bytes(64) {
Ok(v) => v,
Err(st) => return st,
};
let report_id = request.output_buffer_len() as u32; // report id, UMDF convention
let mut hex = String::new();
for b in bytes.iter().take(48) {
hex.push_str(&format!("{b:02x} "));
}
let kind = if ioctl == IOCTL_HID_WRITE_REPORT {
"WRITE_REPORT"
} else {
"SET_OUTPUT_REPORT"
};
dbglog!("[pf-gamepad] *** OUTPUT {kind} reportId={report_id} len={inlen} data: {hex}");
// Publish the game's 0x02 output report to the sealed DATA section for the host (rumble /
// lightbar / player-LEDs / adaptive triggers): legacy slot + seq, plus the v2.1 ring.
if !bytes.is_empty()
&& let Some(view) = CHANNEL.data()
{
publish_output(view, &bytes);
}
request.set_information(inlen as u64);
STATUS_SUCCESS
}
/// Deck identity: the last SET_FEATURE payload (the Steam command byte + args, minus the
/// report-id prefix). Steam's Deck contract is command-in-SET_FEATURE → answer-in-GET_FEATURE
/// on the one unnumbered feature report; the PS identities ignore this (their SET_FEATUREs are
/// fire-and-forget) — acking them is all they need.
static LAST_SET_FEATURE: std::sync::Mutex<[u8; 64]> = std::sync::Mutex::new([0; 64]);
// SET_FEATURE: ack (the PS identities' contract), latch the payload for the Deck's GET_FEATURE
// answer, and — the Deck feedback path — publish Steam's rumble/haptic commands to the host.
// Per the UMDF marshalling convention the report data is the input buffer.
fn on_set_feature(request: &Request) -> NTSTATUS {
if let Ok((bytes, _)) = request.input_bytes(64) {
// The wire carries [report-id 0, cmd, …] for the unnumbered Steam report; store the
// command-first view. (PS set-features carry their own report id first — harmless.)
let src: &[u8] = if bytes.first() == Some(&0x00) && bytes.len() > 1 {
&bytes[1..]
} else {
&bytes
};
if let Ok(mut g) = LAST_SET_FEATURE.lock() {
g.fill(0);
let n = src.len().min(64);
g[..n].copy_from_slice(&src[..n]);
}
// Deck feedback: Steam drives rumble (0xEB) and trackpad haptic pulses (0x8F) via
// SET_FEATURE on the unnumbered report — the PS identities get theirs as OUTPUT
// reports instead. Publish them to the host through the same output slot + seq the
// output path uses, re-prefixed with the report-id 0 byte so the host's
// `parse_steam_output` sees the exact wire shape the Linux UHID path delivers.
if device_type() == 3
&& matches!(src.first(), Some(&0xEB) | Some(&0x8F))
&& let Some(view) = CHANNEL.data()
{
let mut out = [0u8; 64];
let n = src.len().min(63);
out[1..1 + n].copy_from_slice(&src[..n]);
publish_output(view, &out);
}
}
dbglog!("[pf-gamepad] SET_FEATURE (acked, latched for GET)");
STATUS_SUCCESS
}
/// Deck identity: build the GET_FEATURE reply from the latched SET_FEATURE command — the
/// 0x83 GET_ATTRIBUTES 9-attribute blob (unit id keyed per pad) or the 0xAE unit serial, both
/// captured from a physical Deck (see inject/proto/steam_proto.rs feature_reply, the source of
/// truth this mirrors). Anything else echoes the latched command.
fn deck_feature_reply() -> [u8; 64] {
let last = LAST_SET_FEATURE.lock().map(|g| *g).unwrap_or([0u8; 64]);
// Per-pad unit id "PF" + the pad index the host stamped into the section — matches
// steam_proto::deck_unit_id / deck_serial, so two virtual Decks never collide in Steam's eyes.
let unit_id: u32 = 0x5046_0000 | pad_index() as u32;
// Steam validates the unit serial's PREFIX before accepting it: a "PF"-leading serial is
// REJECTED ("Invalid or missing unit serial number …") and Steam then substitutes a hash and
// MANGLES the displayed name ("Steam Deck Controllerggg"). An 'F'-leading serial passes, so we
// keep our PunktFunk marker one slot in ("FVPF") — still distinct enough for the Linux side's
// physical-Deck self-detection while satisfying Steam's format check. (This, not the build-time
// attributes below, is what un-mangles the name — verified by A/B on .173.)
let unit_serial = format!("FVPF{unit_id:08X}");
let unit_serial = unit_serial.as_bytes();
let mut r = [0u8; 64];
// The CHANNEL PROOF, Deck flavour: the Deck's ONE feature report is unnumbered and Steam drives
// it as command→response, so the proof rides that same contract instead of a new report id (no
// descriptor change). Two command bytes, so a Steam command we haven't catalogued cannot collide.
if last.starts_with(&pf_driver_proto::gamepad::DECK_PROOF_CMD) {
let proof =
pf_driver_proto::gamepad::ChannelProof::new(CHANNEL.index(), std::process::id());
r[..2].copy_from_slice(&pf_driver_proto::gamepad::DECK_PROOF_CMD);
r[2..18].copy_from_slice(&proof.to_bytes());
return r;
}
match last[0] {
0x83 => {
// GET_ATTRIBUTES_VALUES: [0x83, 0x2d, then 9x (attr-id, value u32-LE)].
r[0] = 0x83;
r[1] = 0x2D;
// Attribute semantics per SDL's controller_constants.h: 0x04 = FIRMWARE_BUILD_TIME
// and 0x0A = BOOTLOADER_BUILD_TIME are unix timestamps that must look like real build
// dates (the old unit-id-derived junk here was cosmetic; the name mangling was the
// serial prefix). Uniqueness rides the serial.
let attrs: [(u8, u32); 9] = [
(0x01, 0x1205), // ATTRIB_PRODUCT_ID
(0x02, 0), // ATTRIB_CAPABILITIES
(0x0A, 0x6408_9000), // ATTRIB_BOOTLOADER_BUILD_TIME (2023-03-08)
(0x04, 0x66A8_C000), // ATTRIB_FIRMWARE_BUILD_TIME (2024-07-30)
(0x09, 0x2E), // ATTRIB_BOARD_REVISION (captured)
(0x0B, 0x0FA0), // ATTRIB_CONNECTION_INTERVAL_IN_US (4 ms)
(0x0D, 0),
(0x0C, 0),
(0x0E, 0),
];
let mut o = 2;
for (id, val) in attrs {
r[o] = id;
r[o + 1..o + 5].copy_from_slice(&val.to_le_bytes());
o += 5;
}
}
0xAE => {
// GET_STRING_ATTRIBUTE: [0xAE, len, attr, ascii…]. Steam requests two strings: attr
// 0x00 = ATTRIB_STR_BOARD_SERIAL (the PCB serial) and 0x01 = ATTRIB_STR_UNIT_SERIAL.
// Echo the exact attr requested (last[2]) — the unit serial is the one that matters:
// getting its format right (FVPF…, see above) is what un-mangles the displayed name.
// Steam ALSO validates the PCB serial against a Valve-internal format we don't have a
// real capture of; it logs "Deck Controller PCB Serial# invalid" for ANY value we send
// (including an empty one — verified on .173), but that line is BENIGN: unlike a bad
// unit serial, it does not mangle the name, change the handle, or block promotion. So we
// serve the unit serial for both attrs and accept the log.
r[0] = 0xAE;
r[1] = unit_serial.len() as u8;
r[2] = last[2];
r[3..3 + unit_serial.len()].copy_from_slice(unit_serial);
}
_ => r.copy_from_slice(&last),
}
r
}
// GET_FEATURE: report id from the input buffer; reply with the matching DualSense/DualShock 4 blob
// (the Deck identity instead answers the latched Steam command — its one feature report is
// unnumbered).
fn on_get_feature(request: &Request) -> NTSTATUS {
if device_type() == 3 {
return request.copy_to_output(&deck_feature_reply());
}
let (bytes, _) = match request.input_bytes(1) {
Ok(v) => v,
Err(st) => return st,
};
let Some(&report_id) = bytes.first() else {
return STATUS_INVALID_PARAMETER;
};
// The CHANNEL PROOF (security-review 2026-07-28): tell the host which process serves this
// devnode, so it never has to trust the LocalService-writable bootstrap mailbox to name its
// duplication target. `0x85` is already declared as a Feature report in all three captured
// descriptors and was previously answered with STATUS_INVALID_PARAMETER, so this costs NO
// report-descriptor change — the identity Steam and SDL fingerprint is untouched. Derived only
// from our own devnode Location + pid: nothing a caller supplies feeds into it.
if report_id == pf_driver_proto::gamepad::HID_FEATURE_REPORT_CHANNEL_PROOF {
let len = request.output_buffer_len();
return match pf_driver_proto::gamepad::ChannelProof::new(
CHANNEL.index(),
std::process::id(),
)
.to_feature_report(report_id, len)
{
Some(rep) => request.copy_to_output(&rep),
None => STATUS_INVALID_PARAMETER, // caller's buffer can't hold id + proof
};
}
// DualSense + Edge use feature ids 0x05/0x09/0x20 (same blobs — SDL forces enhanced-rumble
// for the Edge PID regardless of the firmware version at 0x20[44..46]); DualShock 4 uses
// 0x02/0x12/0xa3.
// The pairing replies are per-pad: the MAC (bytes 1..7, LSB first) low octet carries the pad
// index (see `pad_index` — SDL/Steam dedup controllers by this serial), agreeing with the
// GET_STRING serial in `on_get_string`. The Edge lands on its GET_STRING base (0x75 = DS
// base + 1) so its feature MAC and USB serial string agree too.
let devtype = device_type();
let mut ds_pairing = DS_FEATURE_PAIRING;
ds_pairing[1] = ds_pairing[1]
.wrapping_add(u8::from(devtype == 2))
.wrapping_add(pad_index());
let mut ds4_pairing = DS4_FEATURE_PAIRING;
ds4_pairing[1] = ds4_pairing[1].wrapping_add(pad_index());
let blob: &[u8] = match (devtype, report_id) {
(0 | 2, 0x05) => &DS_FEATURE_CALIBRATION,
(0 | 2, 0x09) => &ds_pairing,
(0 | 2, 0x20) => &DS_FEATURE_FIRMWARE,
(1, 0x02) => &DS4_FEATURE_CALIBRATION,
(1, 0x12) => &ds4_pairing,
(1, 0xA3) => &DS4_FEATURE_FIRMWARE,
(_, other) => {
dbglog!("[pf-gamepad] GET_FEATURE unknown report id 0x{other:02x}");
return STATUS_INVALID_PARAMETER;
}
};
request.copy_to_output(blob)
}
// IOCTL_HID_GET_STRING: the input is a ULONG whose low word is the string id and whose high word is
// the language id. Reply with the requested device string as a NUL-terminated UTF-16 buffer. Native
// PS5 / Steam code reads these (HidD_GetProductString / HidD_GetSerialNumberString — the serial is one
// way they tell USB from BT). Observed live: Windows polls ids 0x0E/0x0F/0x10 (lang 0x0409)
// cyclically — the manufacturer/product/serial slots — NOT the 0/1/2 HID_STRING_ID_* constants; both.
fn on_get_string(request: &Request) -> NTSTATUS {
let (bytes, _) = match request.input_bytes(4) {
Ok(v) => v,
Err(st) => return st,
};
let id_val: u32 = if bytes.len() >= 4 {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
} else {
0
};
let string_id = id_val & 0xFFFF;
let devtype = device_type();
dbglog!("[pf-gamepad] GET_STRING id=0x{string_id:04x} (raw 0x{id_val:08x}) devtype={devtype}");
let s: String = match string_id {
0 | 0x000e => match devtype {
1 => "Sony Computer Entertainment".into(),
3 => "Valve Software".into(),
4..=6 => "Microsoft".into(),
_ => "Sony Interactive Entertainment".into(),
},
// Per-pad serials (see `pad_index`): SDL reads this via HidD_GetSerialNumberString and
// Steam dedups controllers by it. The PS strings are the pairing MAC MSB-first, so the
// low octet — the LAST two hex chars — carries the pad index, agreeing with the patched
// feature 0x09/0x12 replies in `on_get_feature`. The Deck serial must agree with
// deck_feature_reply's 0xAE answer (Steam reads both).
2 | 0x0010 => match devtype {
1 => format!("DEADBEEF00{:02X}", 0x01u8.wrapping_add(pad_index())),
2 => format!("35533AD6E7{:02X}", 0x75u8.wrapping_add(pad_index())),
3 => format!("FVPF{:08X}", 0x5046_0000u32 | pad_index() as u32),
// Xbox pads report a Bluetooth MAC-shaped serial; the low octet carries the pad index
// so Steam dedups multiple forwarded pads, exactly like the PS identities above. Each
// Xbox identity gets its OWN base octet (0x10 / 0x30 / 0x50) rather than sharing one:
// a mixed session can present a Wireless pad and an Elite at once, and two identities
// whose serials differ only by pad index are one off-by-one away from colliding — the
// failure being Steam silently treating two live pads as one device.
4 => format!("F4B0FC2A6C{:02X}", 0x10u8.wrapping_add(pad_index())),
5 => format!("F4B0FC2A6C{:02X}", 0x30u8.wrapping_add(pad_index())),
6 => format!("F4B0FC2A6C{:02X}", 0x50u8.wrapping_add(pad_index())),
_ => format!("35533AD6E7{:02X}", 0x74u8.wrapping_add(pad_index())),
},
_ => match devtype {
1 => "Wireless Controller".into(),
2 => "DualSense Edge Wireless Controller".into(),
3 => "Steam Deck Controller".into(),
// ⚠️ 4 and 5 share a product string ON PURPOSE — a real Xbox Wireless Controller
// (Series X|S, `0B13`) and a real Xbox One S pad (`02FD`) BOTH report exactly
// "Xbox Wireless Controller" over Bluetooth. The PID is what tells them apart, and
// that is what SDL/Steam/Windows key their stock mappings off. Do not "fix" this by
// inventing a distinguishing string; it would make the One S identity a device that
// has never existed. (The INF's Device Manager descriptions DO differ — that string
// is ours, not the pad's.)
4 | 5 => "Xbox Wireless Controller".into(),
6 => "Xbox Elite Wireless Controller Series 2".into(),
_ => "DualSense Wireless Controller".into(),
},
};
let mut wide: Vec<u8> = Vec::with_capacity(s.len() * 2 + 2);
for u in s.encode_utf16() {
wide.extend_from_slice(&u.to_le_bytes());
}
wide.extend_from_slice(&[0, 0]); // NUL terminator (UTF-16)
request.copy_to_output(&wide)
}
/// The device-type selector: 0 = DualSense, 1 = DualShock 4, 2 = DualSense Edge, 3 = Steam Deck,
/// 4 = Xbox Wireless Controller, 5 = Xbox One S, 6 = Xbox Elite Wireless Controller Series 2.
/// Read fresh on each enumeration query — cheap.
///
/// ⚠️ **The sealed section cannot answer the enumeration queries.** hidclass asks for
/// `GET_DEVICE_DESCRIPTOR` / `GET_REPORT_DESCRIPTOR` / `GET_DEVICE_ATTRIBUTES` while it STARTS the
/// device; the host can only deliver the DATA section over the HID device interface
/// (`ProofTransport::HidFeatureReport`), which does not exist until those very queries are answered.
/// So the channel is *structurally* unavailable here, not merely racing — the 1 s wait below always
/// timed out, and every non-DualSense identity silently enumerated with the DualSense VID/PID **and
/// the DualSense report descriptor**. For the Deck that meant Windows parsed the 64-byte
/// `ID_CONTROLLER_DECK_STATE` frame as DualSense report `0x01`: `LX = report[1] = 0x00` (stick hard
/// left), `LY = report[2] = 0x09` (hard up) and a d-pad hat of 0 (UP held) — the "stuck stick/button"
/// a Steam Deck client saw on a Windows host.
///
/// [`PNP_DEVTYPE`] closes it: the devnode's hardware ids carry the identity and are readable at
/// `EvtDeviceAdd`, before anything is asked. The section stays authoritative once attached (same
/// host wrote both). NO in-dispatch wait remains: the old 1 s bounded pump loop could only run
/// for a devnode whose ids matched nothing, where its own premise above guarantees the timeout —
/// it burned a second of a WUDFHost dispatch thread mid-enumeration and then fell back to
/// [`LAST_DEVTYPE`] anyway (which the 8 ms timer keeps fresh whenever the section is attached).
fn device_type() -> u8 {
if let Some(view) = CHANNEL.data() {
let t = view.read_u8(OFF_DEVICE_TYPE);
LAST_DEVTYPE.store(t as u32, Ordering::Relaxed);
return t;
}
let pnp = PNP_DEVTYPE.load(Ordering::Relaxed);
if pnp != u32::MAX {
return pnp as u8;
}
LAST_DEVTYPE.load(Ordering::Relaxed) as u8
}
extern "C" fn evt_timer(timer: WDFTIMER) {
// Two cadences on one timer. EVERY tick ([`TIMER_PERIOD_MS`]) does the cheap input half —
// read the section's report slot, complete one pended READ_REPORT — because that pair is what
// bounds the rate a game can observe, and at the old 8 ms it halved a 250 Hz motion stream.
// The channel handshake and the health marks stay on their historical ~8 ms
// ([`PUMP_EVERY_N_TICKS`]): they cost more, nothing about them wants to be faster, and the
// heartbeat's documented "+1 per ~8 ms tick" is what the host reads as liveness.
let tick = TICK.fetch_add(1, Ordering::Relaxed);
let housekeeping = tick.is_multiple_of(PUMP_EVERY_N_TICKS);
let view = if housekeeping {
// Publish our pid / adopt a delivery / detect host-gone.
CHANNEL.pump(&channel_cfg())
} else {
CHANNEL.data()
};
match view {
Some(view) => {
let mut buf = [0u8; 64];
// A torn read is dropped rather than served: `read_input_report` returns false only
// when it caught the host mid-publish, and the previous whole report stays in place.
if read_input_report(view, &mut buf)
&& buf[0] == 0x01
&& let Ok(mut g) = INPUT_REPORT.lock()
{
*g = buf;
}
if housekeeping {
// Keep the fallback identity fresh: `device_type()`'s last resort (channel
// detached, no PnP match) reads LAST_DEVTYPE, and this tick is the one place that
// always sees the attached section.
LAST_DEVTYPE.store(view.read_u8(OFF_DEVICE_TYPE) as u32, Ordering::Relaxed);
// Health marks the host watches: driver_proto (attach signal, idempotent) and
// driver_heartbeat (+1 per ~8 ms = liveness). Lets the host tell "driver bound and
// alive" apart from "driver package missing/failed to bind".
view.write_u32(OFF_DRIVER_PROTO, GAMEPAD_PROTO_VERSION);
let hb = view.read_u32(OFF_DRIVER_HEARTBEAT).wrapping_add(1);
view.write_u32(OFF_DRIVER_HEARTBEAT, hb);
}
}
None => {
// Host gone (mailbox name vanished) or channel not attached yet: feed games the neutral
// report instead of a frozen last state (matters for the persistent out-of-band devnode,
// which outlives host sessions).
if let Ok(mut g) = INPUT_REPORT.lock() {
*g = neutral_report(LAST_DEVTYPE.load(Ordering::Relaxed) as u8);
}
}
}
// Complete the next pended READ_REPORT with the current input report (safe queue/request API).
// SAFETY: the timer's parent object is the manual queue (set in EvtDeviceAdd); the framework
// guarantees a live handle here.
let queue =
unsafe { call_unsafe_wdf_function_binding!(WdfTimerGetParentObject, timer) } as WDFQUEUE;
// SAFETY: `queue` is that live manual queue — the exact contract `retrieve_next_request` needs.
if let Some(request) = unsafe { wdf::retrieve_next_request(queue) } {
let report = INPUT_REPORT.lock().map(|g| *g).unwrap_or(NEUTRAL_REPORT);
// Serve exactly what this identity's descriptor declares — `copy_to_output` REFUSES a
// source longer than hidclass's buffer instead of truncating, so a 64-byte hand-over for
// the Xbox pad's 16-byte report would fail every read and the pad would look dead.
let st = request.copy_to_output(&report[..input_report_len(device_type())]);
request.complete(st);
}
}