feat(host/windows,drivers): gamepad driver attach/heartbeat health surfaced in logs
apple / swift (push) Successful in 1m12s
windows-drivers / probe-and-proto (push) Successful in 14s
windows-drivers / driver-build (push) Successful in 1m15s
apple / screenshots (push) Successful in 5m30s
android / android (push) Successful in 3m35s
ci / web (push) Successful in 51s
ci / rust (push) Successful in 1m44s
ci / docs-site (push) Successful in 58s
deb / build-publish (push) Successful in 4m6s
ci / bench (push) Successful in 4m50s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
decky / build-publish (push) Successful in 13s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 51s
windows-host / package (push) Failing after 2m28s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m40s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m40s
docker / deploy-docs (push) Successful in 5s

The gamepad drivers have no IOCTL plane (hidclass gates the stack), so
until now the host had ZERO visibility into whether a driver ever
bound: a pad could be "created" with no driver installed and nothing
was logged. Two health fields are carved from reserved shm space
(layout-compatible; pf-driver-proto pins the offsets): driver_proto —
stamped by pf-xusb at device add + per serviced XInput IOCTL (movement
= the game-visible path) and by pf-dualsense/DS4 from its ~125Hz timer
— and driver_heartbeat. Host-side, every pad owns a DriverAttach
watcher fed from the existing service() poll: INFO on attach (WARN on
proto mismatch), and after 3s of silence ONE diagnosis WARN combining
a cached pnputil /enum-drivers store check, the devnode's CM problem
code (CM_Locate_DevNodeW/CM_Get_DevNode_Status on the instance id now
captured from the create callback, with plain-language hints: 28 = not
installed, 52 = signature/Memory Integrity, …) and the driver's debug
log path. Also fixes a real bug both SwDeviceCreate wrappers shared:
the 10s WaitForSingleObject result was ignored and the callback
HRESULT zero-initialised, so a PnP timeout read as SUCCESS (now E_FAIL
init + explicit timeout error). Failure-mode table:
design/gamepad-driver-health.md.

Linux workspace green; Windows host + drivers CI-compile only, on-box
recipe at the bottom of the design doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:33:31 +00:00
parent 8af1a15aa6
commit c21549c136
9 changed files with 495 additions and 51 deletions
+27 -2
View File
@@ -311,6 +311,13 @@ pub mod gamepad {
/// `device_type` = DualShock 4 (`VID_054C&PID_09CC` HID identity).
pub const DEVTYPE_DUALSHOCK4: u8 = 1;
/// The value a gamepad driver writes into its section's `driver_proto` field once it attaches —
/// the host's positive "driver is alive on this section" signal (health check + version audit).
/// The section starts zeroed, so `0` always means "no driver has attached (yet)"; a pre-health
/// driver never writes the field and reads as not-attached, which the host log line calls out
/// (the remedy is the same: reinstall the drivers). Bump on a gamepad-layout change.
pub const GAMEPAD_PROTO_VERSION: u32 = 1;
/// `Global\pfxusb-shm-<index>` — the virtual Xbox 360 (XInput) shared section.
pub fn xusb_shm_name(index: u8) -> String {
alloc::format!("Global\\pfxusb-shm-{index}")
@@ -342,7 +349,14 @@ pub mod gamepad {
pub rumble_seq: u32,
pub rumble_large: u8,
pub rumble_small: u8,
pub _reserved1: [u8; 34],
pub _pad0: [u8; 2],
/// Written by the driver when it binds (device add) and on every serviced IOCTL:
/// [`GAMEPAD_PROTO_VERSION`]. `0` = no driver attached — the host health check keys off it.
pub driver_proto: u32,
/// Bumped by the driver on every serviced XInput IOCTL — proves the game-visible path (it
/// only advances while something polls the slot, so a static value is not an error).
pub driver_heartbeat: u32,
pub _reserved1: [u8; 24],
}
/// Virtual DualSense / DualShock 4 shared section (256 B). The host writes the `0x01`-style HID
@@ -363,7 +377,14 @@ pub mod gamepad {
pub output: [u8; 64],
/// HID identity selector — see [`DEVTYPE_DUALSENSE`] / [`DEVTYPE_DUALSHOCK4`].
pub device_type: u8,
pub _reserved1: [u8; 115],
pub _pad0: [u8; 3],
/// Written by the driver's timer while it has the section mapped: [`GAMEPAD_PROTO_VERSION`].
/// `0` = no driver attached — the host health check keys off it.
pub driver_proto: u32,
/// Bumped by the driver's ~125 Hz timer each tick — a true liveness heartbeat (unlike the
/// XUSB one, this advances whenever the driver is loaded, game or not).
pub driver_heartbeat: u32,
pub _reserved1: [u8; 104],
}
// Offsets are the wire contract the shipped drivers already read by hand — pin every one. A failing
@@ -385,6 +406,8 @@ pub mod gamepad {
assert!(offset_of!(XusbShm, rumble_seq) == 24);
assert!(offset_of!(XusbShm, rumble_large) == 28);
assert!(offset_of!(XusbShm, rumble_small) == 29);
assert!(offset_of!(XusbShm, driver_proto) == 32);
assert!(offset_of!(XusbShm, driver_heartbeat) == 36);
assert!(size_of::<PadShm>() == 256);
assert!(offset_of!(PadShm, magic) == 0);
@@ -392,6 +415,8 @@ pub mod gamepad {
assert!(offset_of!(PadShm, out_seq) == 72);
assert!(offset_of!(PadShm, output) == 76);
assert!(offset_of!(PadShm, device_type) == 140);
assert!(offset_of!(PadShm, driver_proto) == 144);
assert!(offset_of!(PadShm, driver_heartbeat) == 148);
};
}
@@ -29,7 +29,7 @@ use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
use windows::Win32::Devices::Enumeration::Pnp::{
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Foundation::{CloseHandle, E_FAIL, HANDLE, WAIT_OBJECT_0};
use windows::Win32::System::Threading::{CreateEventW, SetEvent, WaitForSingleObject};
/// Shared-section layout — the single source of truth is [`pf_driver_proto::gamepad::PadShm`] (offset
@@ -47,6 +47,8 @@ pub(super) const OFF_OUTPUT: usize =
/// DualSense (the default — the section is zeroed), 1 = DualShock 4.
pub(super) const OFF_DEVTYPE: usize =
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, device_type);
pub(super) const OFF_DRIVER_PROTO: usize =
core::mem::offset_of!(pf_driver_proto::gamepad::PadShm, driver_proto);
pub(super) const DEVTYPE_DUALSHOCK4: u8 = pf_driver_proto::gamepad::DEVTYPE_DUALSHOCK4;
/// A single virtual DualSense: the SwDeviceCreate'd `pf_pad_<index>` software devnode (the driver
@@ -58,16 +60,20 @@ struct DsWinPad {
_sw: Option<super::gamepad_raii::SwDevice>,
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
shm: super::gamepad_raii::Shm,
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
attach: super::gamepad_raii::DriverAttach,
seq: u8,
ts: u32,
last_out_seq: u32,
}
/// Context for the `SwDeviceCreate` completion callback: an event to signal + the HRESULT it reports.
/// Context for the `SwDeviceCreate` completion callback: an event to signal, the HRESULT it reports,
/// and the PnP instance id PnP assigned (captured for devnode health diagnostics).
#[repr(C)]
struct SwCreateCtx {
event: HANDLE,
result: HRESULT,
instance_id: [u16; 128],
}
/// `SwDeviceCreate` fires this once PnP has enumerated the device; stash the result and wake the
@@ -76,18 +82,35 @@ unsafe extern "system" fn sw_create_cb(
_dev: HSWDEVICE,
result: HRESULT,
ctx: *const c_void,
_id: PCWSTR,
id: PCWSTR,
) {
if !ctx.is_null() {
// SAFETY: ctx is the &mut SwCreateCtx the creator passed; it outlives this callback.
// SAFETY: ctx is the &mut SwCreateCtx the creator passed; it outlives this callback (the
// creator blocks on the event). `id` is a NUL-terminated string for the callback's duration.
unsafe {
let c = ctx as *mut SwCreateCtx;
(*c).result = result;
if !id.is_null() {
for i in 0..(*c).instance_id.len() - 1 {
let ch = *id.0.add(i);
(*c).instance_id[i] = ch;
if ch == 0 {
break;
}
}
}
let _ = SetEvent((*c).event);
}
}
}
impl SwCreateCtx {
fn instance_id(&self) -> Option<String> {
let len = self.instance_id.iter().position(|&c| c == 0)?;
(len > 0).then(|| String::from_utf16_lossy(&self.instance_id[..len]))
}
}
/// The PnP identity for a virtual controller devnode — varies by controller type so the same
/// [`create_swdevice`] builds a DualSense (`VID_054C&PID_0CE6`) or a DualShock 4
/// (`VID_054C&PID_09CC`). The fields map onto the `SW_DEVICE_CREATE_INFO` identity discussed below.
@@ -131,7 +154,7 @@ pub(super) struct SwDeviceProfile<'a> {
/// (hence `punktfunk`, not `pf_dualsense`), and the completion callback is mandatory (the docs mark
/// `pCallback` as `[in]`, not optional — a NULL callback is rejected). The caller must be
/// Administrator (the host service runs as LocalSystem).
pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<HSWDEVICE> {
pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<(HSWDEVICE, Option<String>)> {
// Build a double-NUL-terminated UTF-16 multi-sz from a list of ids.
let multi_sz = |ids: &[&str]| -> Vec<u16> {
ids.iter()
@@ -189,9 +212,12 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<HSWDEVICE> {
// SAFETY: a manual-reset, initially-unsignaled, unnamed event.
let event = unsafe { CreateEventW(None, true, false, PCWSTR::null())? };
// `result` starts as E_FAIL, NOT S_OK: if the wait below times out, a zero-initialised HRESULT
// would read as success and mask the failure (found by the 2026-07 driver-health audit).
let mut ctx = SwCreateCtx {
event,
result: HRESULT(0),
result: E_FAIL,
instance_id: [0; 128],
};
// SAFETY: info + the buffers + ctx outlive the call (we wait on the event before returning);
// windows-rs returns the HSWDEVICE (the C out-param) as the Result value.
@@ -216,10 +242,18 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<HSWDEVICE> {
};
// Block until PnP finishes enumerating (the callback signals), then check its result.
// SAFETY: event is valid.
let wait = unsafe { WaitForSingleObject(event, 10_000) };
// SAFETY: event is valid.
unsafe {
WaitForSingleObject(event, 10_000);
let _ = CloseHandle(event);
}
if wait != WAIT_OBJECT_0 {
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };
return Err(anyhow!(
"SwDeviceCreate enumeration callback never fired (10s) — PnP may be wedged"
));
}
if ctx.result.is_err() {
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };
@@ -228,7 +262,7 @@ pub(super) fn create_swdevice(p: &SwDeviceProfile) -> Result<HSWDEVICE> {
ctx.result
));
}
Ok(hsw)
Ok((hsw, ctx.instance_id()))
}
impl DsWinPad {
@@ -236,10 +270,8 @@ impl DsWinPad {
/// `root\pf_dualsense` devnode (the driver loads on it and maps the section). The devnode lives
/// for the pad's lifetime — dropping the pad removes it (`SwDeviceClose`).
fn open(index: u8) -> Result<DsWinPad> {
let shm = super::gamepad_raii::Shm::create(
&HSTRING::from(pf_driver_proto::gamepad::pad_shm_name(index)),
SHM_SIZE,
)?;
let shm_name = pf_driver_proto::gamepad::pad_shm_name(index);
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
let base = shm.base();
// Stamp the neutral input report, then the magic LAST (the driver only accepts the section
// once magic is set). The device-type stays 0 (DualSense — the section is already zeroed).
@@ -256,23 +288,30 @@ impl DsWinPad {
// rare failure we keep the section + data plane and fall back to an out-of-band `pf_dualsense`
// devnode (installer / dev-box devgen).
let inst = format!("pf_pad_{index}");
let hsw = match create_swdevice(&SwDeviceProfile {
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
instance: &inst,
container_index: index,
hwid: "pf_dualsense",
usb_vid_pid: "VID_054C&PID_0CE6",
description: "punktfunk Virtual DualSense",
}) {
Ok(h) => Some(h),
Ok((h, id)) => (Some(h), id),
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; falling back to an out-of-band pf_dualsense devnode");
None
(None, None)
}
};
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
Ok(DsWinPad {
_sw,
shm,
attach: super::gamepad_raii::DriverAttach::new(
"pf_dualsense",
"pf_dualsense.inf",
"C:\\Users\\Public\\pfds-driver.log",
shm_name,
instance_id,
),
seq: 0,
ts: 0,
last_out_seq: 0,
@@ -292,10 +331,17 @@ impl DsWinPad {
}
/// Poll the section's output slot; parse a new `0x02` report (rumble / LEDs / triggers) into a
/// [`DsFeedback`] for pad `pad`. Returns empty feedback if the driver hasn't published anything new.
/// [`DsFeedback`] for pad `pad`. Returns empty feedback if the driver hasn't published anything
/// new. Also feeds the driver-attach health watcher (the driver's ~125 Hz timer stamps
/// `driver_proto` while it has the section mapped).
fn service(&mut self, pad: u8) -> DsFeedback {
let mut fb = DsFeedback::default();
// SAFETY: base points at SHM_SIZE bytes.
let proto = unsafe {
std::ptr::read_unaligned(self.shm.base().add(OFF_DRIVER_PROTO) as *const u32)
};
self.attach.observe(proto);
// SAFETY: base points at SHM_SIZE bytes.
let seq =
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
if seq != self.last_out_seq {
@@ -471,7 +517,7 @@ impl DualSenseWindowsManager {
self.last_write[idx] = Instant::now();
}
Err(e) => {
tracing::error!(error = %format!("{e:#}"), "virtual DualSense creation failed — controller input disabled");
tracing::error!(error = %format!("{e:#}"), "virtual DualSense creation failed — controller input disabled until the next client connect (install/repair: punktfunk-host.exe driver install --gamepad)");
self.broken = true;
}
}
@@ -9,8 +9,8 @@
use super::dualsense_proto::DsState;
use super::dualsense_windows::{
create_swdevice, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE, OFF_INPUT, OFF_OUTPUT,
OFF_OUT_SEQ, SHM_MAGIC, SHM_SIZE,
create_swdevice, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE, OFF_DRIVER_PROTO, OFF_INPUT,
OFF_OUTPUT, OFF_OUT_SEQ, SHM_MAGIC, SHM_SIZE,
};
use super::dualshock4_proto::{
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W,
@@ -28,6 +28,8 @@ struct Ds4WinPad {
_sw: Option<super::gamepad_raii::SwDevice>,
/// The named shared section the driver maps (RAII — unmapped + closed on drop).
shm: super::gamepad_raii::Shm,
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
attach: super::gamepad_raii::DriverAttach,
counter: u8,
ts: u16,
last_out_seq: u32,
@@ -37,10 +39,8 @@ impl Ds4WinPad {
/// Create + map the section, stamp `device_type = DualShock 4` + a neutral report + the magic,
/// then spawn the `pf_ds4_<index>` devnode (the driver loads on it and maps the section).
fn open(index: u8) -> Result<Ds4WinPad> {
let shm = super::gamepad_raii::Shm::create(
&HSTRING::from(pf_driver_proto::gamepad::pad_shm_name(index)),
SHM_SIZE,
)?;
let shm_name = pf_driver_proto::gamepad::pad_shm_name(index);
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
let base = shm.base();
// device-type FIRST (so it's visible the moment magic is), neutral report, magic LAST.
// SAFETY: base points at SHM_SIZE writable bytes; OFF_DEVTYPE/OFF_INPUT are in range.
@@ -54,23 +54,30 @@ impl Ds4WinPad {
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
}
let inst = format!("pf_ds4_{index}");
let hsw = match create_swdevice(&SwDeviceProfile {
let (hsw, instance_id) = match create_swdevice(&SwDeviceProfile {
instance: &inst,
container_index: index,
hwid: "pf_dualshock4",
usb_vid_pid: "VID_054C&PID_09CC",
description: "punktfunk Virtual DualShock 4",
}) {
Ok(h) => Some(h),
Ok((h, id)) => (Some(h), id),
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; DualShock 4 devnode unavailable");
None
(None, None)
}
};
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
Ok(Ds4WinPad {
_sw,
shm,
attach: super::gamepad_raii::DriverAttach::new(
"pf_dualshock4",
"pf_dualsense.inf", // one driver package serves both HID identities
"C:\\Users\\Public\\pfds-driver.log",
shm_name,
instance_id,
),
counter: 0,
ts: 0,
last_out_seq: 0,
@@ -90,10 +97,16 @@ impl Ds4WinPad {
}
/// Poll the section's output slot; parse a new `0x05` report (rumble / lightbar) into a
/// [`Ds4Feedback`]. Returns empty feedback if the driver hasn't published anything new.
/// [`Ds4Feedback`]. Returns empty feedback if the driver hasn't published anything new. Also
/// feeds the driver-attach health watcher (the driver's ~125 Hz timer stamps `driver_proto`).
fn service(&mut self) -> Ds4Feedback {
let mut fb = Ds4Feedback::default();
// SAFETY: base points at SHM_SIZE bytes.
let proto = unsafe {
std::ptr::read_unaligned(self.shm.base().add(OFF_DRIVER_PROTO) as *const u32)
};
self.attach.observe(proto);
// SAFETY: base points at SHM_SIZE bytes.
let seq =
unsafe { std::ptr::read_unaligned(self.shm.base().add(OFF_OUT_SEQ) as *const u32) };
if seq != self.last_out_seq {
@@ -272,7 +285,7 @@ impl DualShock4WindowsManager {
self.last_write[idx] = Instant::now();
}
Err(e) => {
tracing::error!(error = %format!("{e:#}"), "virtual DualShock 4 creation failed — controller input disabled");
tracing::error!(error = %format!("{e:#}"), "virtual DualShock 4 creation failed — controller input disabled until the next client connect (install/repair: punktfunk-host.exe driver install --gamepad)");
self.broken = true;
}
}
@@ -9,7 +9,13 @@
use anyhow::{anyhow, Result};
use std::os::windows::io::{FromRawHandle, OwnedHandle};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use windows::core::{w, HSTRING, PCWSTR};
use windows::Win32::Devices::DeviceAndDriverInstallation::{
CM_Get_DevNode_Status, CM_Locate_DevNodeW, CM_DEVNODE_STATUS_FLAGS, CM_LOCATE_DEVNODE_NORMAL,
CM_PROB, CR_SUCCESS, DN_DRIVER_LOADED, DN_HAS_PROBLEM, DN_STARTED,
};
use windows::Win32::Devices::Enumeration::Pnp::{SwDeviceClose, HSWDEVICE};
use windows::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows::Win32::Security::Authorization::{
@@ -121,3 +127,204 @@ impl Drop for SwDevice {
unsafe { SwDeviceClose(self.0) };
}
}
// ── Driver health surfacing ─────────────────────────────────────────────────────────────────────
//
// The gamepad drivers have no IOCTL plane (hidclass gates the stack), so the only cross-process
// signal is the shared section itself. The drivers stamp `driver_proto` into their section once
// attached (pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION); [`DriverAttach`] watches that field
// from the host's regular pump and turns silence into actionable WARN/ERROR log lines — the piece
// that used to be missing entirely: a pad could be "created" with no driver installed and nothing
// was ever logged until the user gave up ("host doesn't see my controller" bug reports).
/// How long to give PnP to bind the driver + the driver to stamp the section before warning.
const ATTACH_GRACE: Duration = Duration::from_secs(3);
/// Per-pad driver-attach watcher: feed it the section's `driver_proto` on every service tick; it
/// logs the attach (INFO), a version mismatch (WARN), or — after [`ATTACH_GRACE`] of silence — one
/// diagnosis WARN combining the driver-store check and the devnode problem code. States never
/// repeat their log line, so the pump can call this at full rate.
pub(super) struct DriverAttach {
/// Driver label for log lines (`pf_xusb` / `pf_dualsense` / `pf_dualshock4`).
driver: &'static str,
/// The INF the driver store must hold for this driver (`pf_xusb.inf` / `pf_dualsense.inf`).
inf: &'static str,
/// The driver's own debug log, referenced in the diagnosis line.
driver_log: &'static str,
/// Section name, for log lines.
shm_name: String,
/// PnP instance id of the SwDeviceCreate'd devnode (`None` on the out-of-band fallback path).
instance_id: Option<String>,
created: Instant,
state: AttachState,
}
enum AttachState {
Waiting,
/// Diagnosis logged; still watching so a late attach gets its INFO line.
Warned,
Attached,
}
impl DriverAttach {
pub(super) fn new(
driver: &'static str,
inf: &'static str,
driver_log: &'static str,
shm_name: String,
instance_id: Option<String>,
) -> DriverAttach {
DriverAttach {
driver,
inf,
driver_log,
shm_name,
instance_id,
created: Instant::now(),
state: AttachState::Waiting,
}
}
/// `driver_proto` is the section field the driver stamps once attached (0 = not attached).
pub(super) fn observe(&mut self, driver_proto: u32) {
match self.state {
AttachState::Attached => {}
AttachState::Waiting | AttachState::Warned if driver_proto != 0 => {
let late = matches!(self.state, AttachState::Warned);
tracing::info!(
driver = self.driver,
shm = %self.shm_name,
proto = driver_proto,
late,
"gamepad driver attached to the shared section"
);
if driver_proto != pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION {
tracing::warn!(
driver = self.driver,
driver_proto,
host_proto = pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION,
"gamepad driver/host protocol mismatch — update the drivers: punktfunk-host.exe driver install --gamepad"
);
}
self.state = AttachState::Attached;
}
AttachState::Waiting if self.created.elapsed() >= ATTACH_GRACE => {
self.diagnose();
self.state = AttachState::Warned;
}
_ => {}
}
}
/// One-shot WARN with everything the host can find out about WHY the driver isn't attached:
/// driver-store presence, the devnode's PnP status/problem code, and where to look next.
fn diagnose(&self) {
let store = match driver_store_has(self.inf) {
Some(true) => "driver package present in the driver store",
Some(false) => {
"driver package NOT in the driver store — run: punktfunk-host.exe driver install --gamepad"
}
None => "driver store could not be queried (pnputil failed)",
};
let devnode = match &self.instance_id {
Some(id) => devnode_status_line(id),
None => {
"no per-session devnode (SwDeviceCreate failed earlier — see the warning above)"
.to_string()
}
};
tracing::warn!(
driver = self.driver,
shm = %self.shm_name,
grace_secs = ATTACH_GRACE.as_secs(),
store,
devnode = %devnode,
driver_log = self.driver_log,
"gamepad driver has not attached to the shared section — the virtual pad exists but no \
driver is serving it (games will not see it); an old (pre-health) driver also reads as \
not-attached: update with punktfunk-host.exe driver install --gamepad"
);
}
}
/// Driver-store inventory (`pnputil /enum-drivers`), lower-cased, fetched once per process — only
/// consulted on the failure path, so the one-off subprocess cost never hits a healthy session.
fn driver_store_inventory() -> &'static str {
static INV: OnceLock<String> = OnceLock::new();
INV.get_or_init(|| {
std::process::Command::new("pnputil")
.arg("/enum-drivers")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_ascii_lowercase())
.unwrap_or_default()
})
}
/// Whether the driver store holds `inf` (e.g. `pf_xusb.inf`). `None` = pnputil unavailable/failed.
fn driver_store_has(inf: &str) -> Option<bool> {
let inv = driver_store_inventory();
if inv.is_empty() {
return None;
}
Some(inv.contains(&inf.to_ascii_lowercase()))
}
/// Human-readable PnP status of a devnode: driver bound/started or the CM problem code with a hint.
fn devnode_status_line(instance_id: &str) -> String {
let wide: Vec<u16> = instance_id
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let mut devinst = 0u32;
// SAFETY: `wide` is a valid NUL-terminated UTF-16 instance id; `devinst` receives the handle.
let cr = unsafe {
CM_Locate_DevNodeW(
&mut devinst,
PCWSTR(wide.as_ptr()),
CM_LOCATE_DEVNODE_NORMAL,
)
};
if cr != CR_SUCCESS {
return format!(
"devnode {instance_id} not found (CM_Locate_DevNodeW CR={})",
cr.0
);
}
let mut status = CM_DEVNODE_STATUS_FLAGS(0);
let mut problem = CM_PROB(0);
// SAFETY: devinst is the devnode located above; the two out-params receive status + problem.
let cr = unsafe { CM_Get_DevNode_Status(&mut status, &mut problem, devinst, 0) };
if cr != CR_SUCCESS {
return format!("devnode {instance_id}: status query failed (CR={})", cr.0);
}
if status.0 & DN_HAS_PROBLEM.0 != 0 {
return format!(
"devnode {instance_id} has PnP problem code {} ({}) [status 0x{:08x}]",
problem.0,
cm_problem_hint(problem.0),
status.0
);
}
format!(
"devnode {instance_id} status 0x{:08x} (driver_loaded={} started={})",
status.0,
status.0 & DN_DRIVER_LOADED.0 != 0,
status.0 & DN_STARTED.0 != 0,
)
}
/// The CM_PROB_* codes a virtual-pad devnode realistically hits, with the operator-facing cause.
fn cm_problem_hint(problem: u32) -> &'static str {
match problem {
1 => "not configured — no driver bound; install the drivers",
10 => "device failed to start — driver bound but its start failed; check the driver log",
18 => "reinstall required — re-run driver install",
24 => "device not present/working — PnP could not start the virtual devnode",
28 => "drivers not installed — the pf driver package is missing from the store or its certificate is not trusted",
31 => "driver failed to load — binding found the package but loading it failed",
39 => "driver corrupt or missing — reinstall the drivers",
43 => "reported failure after start — check the driver log",
52 => "driver signature rejected — certificate not in Root/TrustedPublisher, or blocked by Memory Integrity",
_ => "see Device Manager for this code",
}
}
@@ -21,7 +21,7 @@ use windows::core::{w, GUID, HRESULT, HSTRING, PCWSTR};
use windows::Win32::Devices::Enumeration::Pnp::{
SwDeviceClose, SwDeviceCreate, HSWDEVICE, SW_DEVICE_CREATE_INFO,
};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Foundation::{CloseHandle, E_FAIL, HANDLE, WAIT_OBJECT_0};
use windows::Win32::System::Threading::{CreateEventW, SetEvent, WaitForSingleObject};
// Shared-section layout — the single source of truth is `pf_driver_proto::gamepad::XusbShm` (offset
@@ -40,12 +40,15 @@ const OFF_RX: usize = core::mem::offset_of!(XusbShm, thumb_rx);
const OFF_RY: usize = core::mem::offset_of!(XusbShm, thumb_ry);
const OFF_RUMBLE_SEQ: usize = core::mem::offset_of!(XusbShm, rumble_seq);
const OFF_RUMBLE: usize = core::mem::offset_of!(XusbShm, rumble_large); // large @28, small @29
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(XusbShm, driver_proto);
/// Context for the `SwDeviceCreate` completion callback: an event to signal + the HRESULT it reports.
/// Context for the `SwDeviceCreate` completion callback: an event to signal, the HRESULT it reports,
/// and the PnP instance id PnP assigned (captured for devnode health diagnostics).
#[repr(C)]
struct SwCreateCtx {
event: HANDLE,
result: HRESULT,
instance_id: [u16; 128],
}
/// `SwDeviceCreate` fires this once PnP has enumerated the device; stash the result + wake the creator.
@@ -53,24 +56,41 @@ unsafe extern "system" fn sw_create_cb(
_dev: HSWDEVICE,
result: HRESULT,
ctx: *const c_void,
_id: PCWSTR,
id: PCWSTR,
) {
if !ctx.is_null() {
// SAFETY: ctx is the &mut SwCreateCtx the creator passed; it outlives this callback.
// SAFETY: ctx is the &mut SwCreateCtx the creator passed; it outlives this callback (the
// creator blocks on the event). `id` is a NUL-terminated string for the callback's duration.
unsafe {
let c = ctx as *mut SwCreateCtx;
(*c).result = result;
if !id.is_null() {
for i in 0..(*c).instance_id.len() - 1 {
let ch = *id.0.add(i);
(*c).instance_id[i] = ch;
if ch == 0 {
break;
}
}
}
let _ = SetEvent((*c).event);
}
}
}
impl SwCreateCtx {
fn instance_id(&self) -> Option<String> {
let len = self.instance_id.iter().position(|&c| c == 0)?;
(len > 0).then(|| String::from_utf16_lossy(&self.instance_id[..len]))
}
}
/// Spawn the `pf_xusb_<index>` companion devnode (hardware id `pf_xusb`, enumerator `punktfunk`). The
/// INF (System class) binds our UMDF driver, which registers the XUSB interface. Unlike the HID pads,
/// no USB compatible-ids are needed — XInput finds the device by the interface GUID, not VID/PID — but
/// we still pass a deterministic non-null `pContainerId` (the null-sentinel trips an `xinput1_4`
/// slot-skip bug). `SwDeviceClose` removes it on drop.
fn create_swdevice(index: u8) -> Result<HSWDEVICE> {
fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
let hwids: Vec<u16> = "pf_xusb".encode_utf16().chain([0u16, 0u16]).collect();
let instid: Vec<u16> = format!("pf_xusb_{index}")
.encode_utf16()
@@ -100,9 +120,12 @@ fn create_swdevice(index: u8) -> Result<HSWDEVICE> {
// SAFETY: a manual-reset, initially-unsignaled, unnamed event.
let event = unsafe { CreateEventW(None, true, false, PCWSTR::null())? };
// `result` starts as E_FAIL, NOT S_OK: if the wait below times out, a zero-initialised HRESULT
// would read as success and mask the failure (found by the 2026-07 driver-health audit).
let mut ctx = SwCreateCtx {
event,
result: HRESULT(0),
result: E_FAIL,
instance_id: [0; 128],
};
// SAFETY: info + buffers + ctx outlive the call (we wait on the event before returning).
let hsw = match unsafe {
@@ -125,10 +148,18 @@ fn create_swdevice(index: u8) -> Result<HSWDEVICE> {
}
};
// SAFETY: event valid; block until PnP finishes enumerating, then check the callback result.
let wait = unsafe { WaitForSingleObject(event, 10_000) };
// SAFETY: event is valid.
unsafe {
WaitForSingleObject(event, 10_000);
let _ = CloseHandle(event);
}
if wait != WAIT_OBJECT_0 {
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };
return Err(anyhow!(
"SwDeviceCreate(pf_xusb) enumeration callback never fired (10s) — PnP may be wedged"
));
}
if ctx.result.is_err() {
// SAFETY: hsw is the handle SwDeviceCreate returned.
unsafe { SwDeviceClose(hsw) };
@@ -137,7 +168,7 @@ fn create_swdevice(index: u8) -> Result<HSWDEVICE> {
ctx.result
));
}
Ok(hsw)
Ok((hsw, ctx.instance_id()))
}
/// A single virtual Xbox 360 pad: the `pf_xusb_<index>` devnode plus the mapped shared section.
@@ -146,6 +177,8 @@ struct XusbWinPad {
_sw: Option<super::gamepad_raii::SwDevice>,
/// Owns `Global\pfxusb-shm-<index>` (the section + its mapped view; drop unmaps + closes).
shm: super::gamepad_raii::Shm,
/// Watches the section's `driver_proto` field and logs attach / never-attached diagnosis.
attach: super::gamepad_raii::DriverAttach,
packet: u32,
last_rumble_seq: u32,
}
@@ -155,10 +188,8 @@ impl XusbWinPad {
fn open(index: u8) -> Result<XusbWinPad> {
// Permissive-DACL named section the WUDFHost (whatever account) can open; `Shm` owns the
// section handle + its mapped view (zero-filled) and unmaps/closes on drop.
let shm = super::gamepad_raii::Shm::create(
&HSTRING::from(pf_driver_proto::gamepad::xusb_shm_name(index)),
SHM_SIZE,
)?;
let shm_name = pf_driver_proto::gamepad::xusb_shm_name(index);
let shm = super::gamepad_raii::Shm::create(&HSTRING::from(shm_name.as_str()), SHM_SIZE)?;
let base = shm.base();
// Zero the section then stamp the magic LAST (the driver only accepts it once magic is set).
// SAFETY: base points at SHM_SIZE writable bytes.
@@ -166,17 +197,24 @@ impl XusbWinPad {
std::ptr::write_bytes(base, 0, SHM_SIZE);
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
}
let hsw = match create_swdevice(index) {
Ok(h) => Some(h),
let (hsw, instance_id) = match create_swdevice(index) {
Ok((h, id)) => (Some(h), id),
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; XUSB devnode unavailable");
None
(None, None)
}
};
let _sw = hsw.map(super::gamepad_raii::SwDevice::new);
Ok(XusbWinPad {
_sw,
shm,
attach: super::gamepad_raii::DriverAttach::new(
"pf_xusb",
"pf_xusb.inf",
"C:\\Users\\Public\\pfxusb-driver.log",
shm_name,
instance_id,
),
packet: 0,
last_rumble_seq: 0,
})
@@ -204,10 +242,14 @@ impl XusbWinPad {
}
/// Poll the section for a game's rumble (the driver bumps `rumble_seq` on each SET_STATE). Returns
/// `(large, small)` motor levels (0..=255) when a new one arrived.
/// `(large, small)` motor levels (0..=255) when a new one arrived. Also feeds the driver-attach
/// health watcher (the driver stamps `driver_proto` at device add + on every serviced IOCTL).
fn service(&mut self) -> Option<(u8, u8)> {
let base = self.shm.base();
// SAFETY: base points at SHM_SIZE bytes.
let proto = unsafe { std::ptr::read_unaligned(base.add(OFF_DRIVER_PROTO) as *const u32) };
self.attach.observe(proto);
// SAFETY: base points at SHM_SIZE bytes.
let seq = unsafe { std::ptr::read_unaligned(base.add(OFF_RUMBLE_SEQ) as *const u32) };
if seq == self.last_rumble_seq {
return None;
@@ -257,7 +299,7 @@ impl GamepadManager {
self.last_rumble[idx] = (0, 0);
}
Err(e) => {
tracing::error!(error = %format!("{e:#}"), "virtual Xbox 360 creation failed — controller input disabled (is the pf_xusb driver installed?)");
tracing::error!(error = %format!("{e:#}"), "virtual Xbox 360 creation failed — controller input disabled until the next client connect (install/repair: punktfunk-host.exe driver install --gamepad)");
self.broken = true;
}
}