feat(host/windows,drivers): gamepad driver attach/heartbeat health surfaced in logs

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 66b041e4ba
10 changed files with 502 additions and 62 deletions
+9
View File
@@ -36,6 +36,15 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
boundary, and finished captures are saved as on-disk recordings
(`~/.config/punktfunk/captures/*.json`) browsable/exportable from the console's **Performance** page
(recharts). Endpoints `/api/v1/stats/*` (bearer-only). *Implemented; not yet on-glass validated.*
**Web-console log view** (`log_capture.rs`): a `tracing` layer tees DEBUG-and-up (independent of
`RUST_LOG`) into a 4096-entry in-memory ring, served cursor-paged at `GET /api/v1/logs`
(bearer-only) → the console's **Logs** page (follow/pause · level filter · search). The Windows
gamepad drivers now stamp attach/heartbeat marks into their shm sections and the host's
`DriverAttach` watcher turns silence into a one-shot diagnosis WARN (driver-store check + CM
devnode problem code) — failure-mode table: [`design/gamepad-driver-health.md`](design/gamepad-driver-health.md).
The Android client gained Settings → **Connected controllers** (device list + VID/PID + resolved
pad type + live input test) for the client end of the same chain. *Log view + driver health:
Linux-tested; Windows/Android sides CI/device-validation pending.*
- **Native protocol (`punktfunk/1`): full session planes, validated live.** QUIC
control plane (`punktfunk-core` `quic` feature: Hello{mode}/Welcome{full Config}/Start), data
plane = the hardened core `Session` over raw UDP with **GF(2¹⁶) Leopard FEC + AES-GCM**
+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);
};
}
@@ -1174,17 +1174,13 @@ impl Encoder for NvencD3d11Encoder {
// the oldest ready AU. `None` = nothing completed yet — the session loop keeps the frame
// in flight and re-polls next tick, capture never blocks on the WDDM scheduling wait.
if self.async_rt.is_some() {
loop {
let done = match self
.async_rt
.as_mut()
.expect("checked just above")
.done_rx
.try_recv()
{
Ok(d) => d,
Err(_) => break,
};
while let Ok(done) = self
.async_rt
.as_mut()
.expect("checked just above")
.done_rx
.try_recv()
{
self.absorb_done(done)?;
}
return Ok(self
@@ -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;
}
}
+65
View File
@@ -0,0 +1,65 @@
# Windows gamepad-driver health: failure modes and how each is surfaced
Written for the "host doesn't see the client's gamepad" class of bug report (2026-07-02). The
Windows virtual pads have many silent ways to fail: the stack spans the host process, a named
shared-memory section, a PnP software devnode, a UMDF driver in its own WUDFHost.exe, and finally
the game's input API (XInput / HID / SDL). Before this work the host logged only its *own* create
calls — a pad could "exist" with no driver installed and nothing was ever logged. This document
enumerates the failure modes and states, for each, how it is now detected and what the log line
says. All host lines land in stderr / `%ProgramData%\punktfunk\logs\host.log` (service) **and** the
in-memory ring served at `GET /api/v1/logs` → the web console **Logs** page.
## The health signals
The gamepad drivers have no IOCTL plane (`hidclass` gates the device stack), so the only
cross-process channel is the shared section itself. Two fields were carved out of reserved space
(layout-compatible; old drivers simply never write them, `pf-driver-proto` pins the offsets):
| field | XusbShm | PadShm | writer | meaning |
|---|---|---|---|---|
| `driver_proto` | @32 | @144 | driver | `GAMEPAD_PROTO_VERSION` once attached; `0` = no driver on this section |
| `driver_heartbeat` | @36 | @148 | driver | XUSB: +1 per serviced XInput IOCTL (game-visible path). DS/DS4: +1 per ~8 ms timer tick (liveness) |
Host side, every pad owns a `DriverAttach` watcher (`inject/windows/gamepad_raii.rs`), fed from the
existing `service()` poll. State machine, each transition logs exactly once:
- `driver_proto != 0` → INFO `gamepad driver attached to the shared section` (with `late=true` if
it came after the warning); WARN on a proto/host version mismatch.
- 3 s of silence → one diagnosis WARN combining: **driver-store check** (`pnputil /enum-drivers`,
cached once per process, only run on the failure path), **devnode PnP status** (`CM_Locate_DevNodeW`
+ `CM_Get_DevNode_Status` on the instance id captured from the SwDeviceCreate callback, with a
plain-language hint per CM problem code), and the driver's own debug log path.
## Failure modes
| # | failure | cause examples | detection | surfaced as |
|---|---|---|---|---|
| 1 | Driver package not installed | fresh box, installer's `driver install --gamepad` skipped/failed, package pruned | attach timeout → `pnputil /enum-drivers` misses `pf_xusb.inf`/`pf_dualsense.inf` | WARN `driver package NOT in the driver store — run: punktfunk-host.exe driver install --gamepad` |
| 2 | Package present but binding failed | certificate not in Root/TrustedPublisher, Memory Integrity (HVCI) rejects it, stale DriverVer kept the old binary | attach timeout → devnode problem code (28 = drivers not installed, 52 = signature rejected, 31/39 = load failure) | WARN with the CM problem code + hint |
| 3 | Driver bound but crashed / never started | WUDFHost crash, `WdfDeviceCreate`/queue failure inside the driver | attach timeout → devnode status shows `driver_loaded`/`started` flags; the driver's own log (`C:\Users\Public\pf*-driver.log`) has the failing WDF call | WARN referencing both |
| 4 | `SwDeviceCreate` fails outright | not Administrator/SYSTEM, PnP wedged, `_` in enumerator (E_INVALIDARG) | existing error path (unchanged) | WARN `SwDeviceCreate failed; … devnode unavailable`, pad continues on the out-of-band fallback |
| 5 | `SwDeviceCreate` callback never fires | PnP service hung | **was silently mis-read as success** (zero-init `HRESULT(0)` + ignored `WaitForSingleObject` return). Fixed: `result` inits to `E_FAIL`, the wait result is checked | ERROR `enumeration callback never fired (10s) — PnP may be wedged` |
| 6 | Driver attached, then WUDFHost died mid-session | crash, killed | `driver_heartbeat` freezes (DS/DS4: timer-driven, so a freeze is conclusive; XUSB: only advances while a game polls, so absence is *not* an error) | field exists for a future stall check; not auto-warned yet (XUSB semantics make a generic rule false-positive-prone) |
| 7 | Version skew host↔driver | new host + old installed driver (or vice versa) | `driver_proto` ≠ host's `GAMEPAD_PROTO_VERSION`; pre-health drivers read as never-attached | WARN `driver/host protocol mismatch — update the drivers` (mismatch) / the mode-1 diagnosis text notes the pre-health case |
| 8 | Whole backend latched off | first pad creation failed → `broken` latch disables pads for the session | existing behaviour, now with remedy text | ERROR `…controller input disabled until the next client connect (install/repair: punktfunk-host.exe driver install --gamepad)` |
| 9 | Section created but game can't see the pad | XInput slot ordering, HidHide-style HID filters on the game process, RPCS3 pad-handler config, GameInput's instance-path VID/PID parse | **not host-detectable** — outside our process and the driver's stack. XUSB `driver_heartbeat` advancing proves "some XInput client polls us", which brackets the problem to the game's side | diagnosis text points at the driver log; the client-side controller view (Android "Connected controllers") covers the other end of the chain |
## What deliberately did NOT change
- The `broken` latch stays one-way per session (retry loops against a missing driver would spam
PnP); the log line now says so and gives the remedy.
- No mgmt-API health endpoint yet — the log ring is the surfacing channel. If the web console ever
grows a "gamepad health" card, `DriverAttach` is the state to expose.
- The DS/DS4 heartbeat is not yet watched for mid-session stalls (mode 6): worth adding once the
XUSB/DS semantics split is encoded (DS freeze = conclusive, XUSB freeze = normal when no game
polls).
## Validation status
- Linux-side: workspace build/tests/clippy green; `pf-driver-proto` layout asserts pin the new
offsets (compile-time).
- Windows host code + both drivers: compile-checked in CI only (this box cannot cross-build the
native deps); **not yet on-box validated**. On-box test recipe: stop the service, `pnputil
/delete-driver` the gamepad package, connect a client with a pad → expect the mode-1 WARN in the
console Logs page within ~3 s of the pad arriving; reinstall drivers → expect the
`attached (late=true)` INFO on the next session.
@@ -229,11 +229,14 @@ static INPUT_REPORT: std::sync::Mutex<[u8; 64]> = std::sync::Mutex::new(NEUTRAL_
// 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 host channel is a named section the (privileged) host CREATES and the driver
// OPENS. Layout (256 B): magic u32 @0 ("PFDS"), input_seq u32 @4, input_report[64] @8,
// output_seq u32 @72, output_report[64] @76.
// OPENS. Layout (256 B, must match pf_driver_proto::gamepad::PadShm): magic u32 @0 ("PFDS"),
// input_seq u32 @4, input_report[64] @8, output_seq u32 @72, output_report[64] @76,
// device_type u8 @140, driver_proto u32 @144 (we stamp GAMEPAD_PROTO_VERSION = the host's
// driver-attach health signal), driver_heartbeat u32 @148 (we bump per timer tick = liveness).
const FILE_MAP_RW: u32 = 0x0002 | 0x0004; // FILE_MAP_WRITE | FILE_MAP_READ
const SHM_MAGIC: u32 = 0x5046_4453; // "PFDS" little-endian
const SHM_SIZE: usize = 256;
const GAMEPAD_PROTO_VERSION: u32 = 1; // must match pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION
static LOGGED_SHM: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
// kernel32 file-mapping APIs (resolved via std's kernel32 import; UMDF permits file mapping).
@@ -770,6 +773,15 @@ extern "C" fn evt_timer(timer: WDFTIMER) {
*g = buf;
}
}
// Health marks the host watches: driver_proto @144 (attach signal, idempotent) and
// driver_heartbeat @148 (+1 per ~8 ms tick = liveness). Lets the host tell "driver bound
// and alive" apart from "driver package missing/failed to bind".
// SAFETY: view points at a mapped 256-byte section; proto @144, heartbeat @148.
unsafe {
core::ptr::write_unaligned(view.add(144) as *mut u32, GAMEPAD_PROTO_VERSION);
let hb = view.add(148) as *mut u32;
core::ptr::write_unaligned(hb, core::ptr::read_unaligned(hb).wrapping_add(1));
}
});
// SAFETY: timer valid; parent is the manual queue.
let queue =
+27 -2
View File
@@ -70,13 +70,16 @@ const XUSB_VERSION: u16 = 0x0103;
const WdfIoQueueDispatchParallel: i32 = 2;
const WdfUseDefault: i32 = 2; // WDF_TRI_STATE
// ---- shared-memory layout (host ↔ driver), must match the host's xbox_xusb_windows backend ----
// ---- shared-memory layout (host ↔ driver), must match pf_driver_proto::gamepad::XusbShm ----
// magic u32 @0 ("PFXU"); packet u32 @4 (host bumps on state change → dwPacketNumber); the XUSB_REPORT
// payload @8: wButtons u16 @8, bLeftTrigger @10, bRightTrigger @11, sThumbLX i16 @12, LY @14, RX @16,
// RY @18; rumble_seq u32 @24 (driver bumps on SET_STATE); rumble large @28, small @29.
// RY @18; rumble_seq u32 @24 (driver bumps on SET_STATE); rumble large @28, small @29;
// driver_proto u32 @32 (we stamp GAMEPAD_PROTO_VERSION = attach signal for the host's health check);
// driver_heartbeat u32 @36 (we bump per serviced IOCTL = the game-visible polling path moves).
const FILE_MAP_RW: u32 = 0x0002 | 0x0004;
const SHM_MAGIC: u32 = 0x5558_4650; // "PFXU" little-endian
const SHM_SIZE: usize = 64;
const GAMEPAD_PROTO_VERSION: u32 = 1; // must match pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION
unsafe extern "system" {
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
@@ -234,6 +237,9 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
return st;
}
// Tell the host we're alive on the section (its driver-attach health check keys off this).
touch_driver_marks();
log("[pf-xusb] device ready (XUSB interface registered)");
STATUS_SUCCESS
}
@@ -285,6 +291,22 @@ fn read_state() -> (u32, u16, u8, u8, i16, i16, i16, i16) {
out
}
/// Stamp the driver health marks the host watches: `driver_proto` @32 (the attach signal,
/// idempotent) and `driver_heartbeat` @36 (+1). Called at device add and on every serviced IOCTL,
/// so the host can tell "driver bound and alive" apart from "driver package missing/failed to
/// bind" and see the game-visible polling path advance. No-op until the host's section exists
/// (with_shm re-opens per access, so a section created after we started still gets marked).
fn touch_driver_marks() {
with_shm(|v| {
// SAFETY: v points at a mapped SHM_SIZE section with valid magic; proto @32, heartbeat @36.
unsafe {
core::ptr::write_unaligned(v.add(32) as *mut u32, GAMEPAD_PROTO_VERSION);
let hb = v.add(36) as *mut u32;
core::ptr::write_unaligned(hb, core::ptr::read_unaligned(hb).wrapping_add(1));
}
});
}
/// Publish a game's rumble (from SET_STATE) into shared memory for the host to forward.
fn publish_rumble(large: u8, small: u8) {
with_shm(|v| {
@@ -352,6 +374,9 @@ extern "C" fn evt_io_device_control(
input_len: usize,
ioctl: ULONG,
) {
// Health marks first: attach signal + heartbeat (also covers a section the host created after
// this device started — the marks land on the next XInput poll).
touch_driver_marks();
let status: NTSTATUS = match ioctl {
IOCTL_XUSB_GET_INFORMATION => copy_to_output(request, &build_information()),
IOCTL_XUSB_GET_INFORMATION_EX => {