fix(host/inject,drivers): rumble root fixes A-C — lossless report ring + rumble-keyed idle watchdogs

B: PadFeedback.game_drove -> rumble_drove, keyed on vibration-asserting reports — an
LED/adaptive-trigger stream can no longer feed the abandoned-rumble force-off while a
coalesced stop never re-asserts (the confirmed unbounded stuck-ON path). C: Linux parity —
every UHID backend now arms the shared watchdog (Steam Input drives these pads over hidraw
with Windows abandonment semantics) and the uinput mixer force-stops abandoned
infinite-replay FF effects (FfState, unit-tested). Shared PUNKTFUNK_RUMBLE_IDLE_MS hatch
(0 = off; non-zero floored above SDL's ~2 s rumble resend).

A: PadShm v2.1 — a 1024 B tail extension carrying an 8-slot lossless output-report ring,
feature-negotiated via zeroed reserved fields (out_ring_ver; deliberately NO
GAMEPAD_PROTO_VERSION bump — mixed generations degrade to the legacy latest-report slot
instead of failing closed). The pf-dualsense driver dual-writes both planes
(publish_output); the host's shared OutputDrain drains oldest->newest with a torn-read
recheck and an overflow->resync path (PadFeedback.resync force-stops + re-arms dedups).
pf-umdf-util grows a min_data_size map fallback. Ds*Feedback.fresh removed (dead).

design/rumble-root-fix.md par. A-C. Verified: pf-inject tests+clippy Linux+Windows (53/53
on winbox incl. the stop-coalesce repro); drivers ws check+clippy on the CI runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 14:07:32 +02:00
parent 570ff504ad
commit 9e6fc6e071
20 changed files with 877 additions and 212 deletions
+60 -6
View File
@@ -690,10 +690,46 @@ pub mod gamepad {
pub _reserved1: [u8; 20],
}
/// Virtual DualSense / DualShock 4 shared section (256 B). The host writes the `0x01`-style HID
/// input report into `input`; the driver feeds it to game `READ_REPORT`s and publishes a game's
/// `0x02` output (rumble / lightbar / player-LEDs / adaptive triggers) into `output`, bumping
/// `out_seq`. `device_type` selects the HID identity ([`DEVTYPE_DUALSENSE`] / [`DEVTYPE_DUALSHOCK4`]).
/// The legacy (pre-ring) [`PadShm`] size. Old binaries on either side were built against a
/// 256-byte layout; every field they know sits below this offset, and the ring extension keeps
/// bytes `0..256` byte-identical. Pagefile-backed sections are page-granular, so a view of
/// either generation's size maps against either generation's section — but a driver must
/// still be able to fall back to mapping this size if the full-size map is ever refused
/// (see `pf_umdf_util::ChannelConfig::min_data_size`).
pub const PAD_SHM_LEGACY_SIZE: usize = 256;
/// Output-report ring depth. 8 slots at the host's ~4 ms poll tolerates a sustained 2 kHz
/// writer — double any real HID output rate.
pub const OUT_RING_LEN: u32 = 8;
pub const OUT_RING_LEN_USIZE: usize = OUT_RING_LEN as usize;
/// One slot of the lossless output-report ring: the report bytes as the game wrote them
/// (report id first), with the exact length — unlike the legacy latest-report slot, whose
/// fixed 64-byte copy can carry a stale tail from a previous longer report.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
pub struct OutSlot {
/// Valid bytes in `data` (0..=64). `0` = never written.
pub len: u32,
pub data: [u8; 64],
}
/// Virtual DualSense / DualShock 4 shared section (1024 B; bytes `0..256` are the v2 legacy
/// layout verbatim — [`PAD_SHM_LEGACY_SIZE`]). The host writes the `0x01`-style HID input
/// report into `input`; the driver feeds it to game `READ_REPORT`s and publishes a game's
/// `0x02` output (rumble / lightbar / player-LEDs / adaptive triggers) twice: into the legacy
/// latest-report `output` slot (bumping `out_seq` — every host generation reads this), and,
/// when the host stamped `out_ring_ver`, into the lossless `out_ring` (bumping `ring_head`).
/// The ring exists because the single slot COALESCES: a rumble-stop report overwritten by an
/// LED/trigger report inside one host poll window was gone forever — the confirmed stuck-rumble
/// path (`design/rumble-root-fix.md` §A). `device_type` selects the HID identity
/// ([`DEVTYPE_DUALSENSE`] / [`DEVTYPE_DUALSHOCK4`]).
///
/// Version posture: this is a TAIL extension negotiated by zeroed-reserved capability fields,
/// deliberately NOT a [`GAMEPAD_PROTO_VERSION`] bump — the bootstrap fails CLOSED on a version
/// mismatch (no pad at all), which is the wrong failure mode for a feedback-quality fix. An
/// old driver never reads the new fields; an old host never stamps `out_ring_ver`, so a new
/// driver stays on the legacy slot against it.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
pub struct PadShm {
@@ -718,7 +754,20 @@ pub mod gamepad {
/// The pad index this section serves (host-stamped before the magic) — see
/// [`XusbShm::pad_index`]. Carved from v1 reserved space (v2).
pub pad_index: u32,
pub _reserved1: [u8; 100],
/// Host-stamped `1` at section creation ⇔ "this section carries the `out_ring` region and
/// the host drains it". The section starts zeroed and an old host never writes it, so `0`
/// tells a new driver to stay legacy-only. Carved from v2 reserved space (v2.1).
pub out_ring_ver: u32,
/// Driver-bumped (AFTER writing `out_ring[ring_head % OUT_RING_LEN]`) count of reports
/// ever published to the ring — the host's drain cursor compares against its own tail and
/// detects overflow by `head - tail > OUT_RING_LEN`. Same publish-then-bump store order as
/// `out_seq` (the host's Acquire load orders the reads). Carved from v2 reserved space
/// (v2.1).
pub ring_head: u32,
pub _reserved1: [u8; 92],
/// The lossless output-report ring (v2.1) — see the struct docs and [`OutSlot`].
pub out_ring: [OutSlot; OUT_RING_LEN_USIZE],
pub _reserved2: [u8; 224],
}
// Offsets are the wire contract the shipped drivers already read by hand — pin every one. A failing
@@ -744,7 +793,7 @@ pub mod gamepad {
assert!(offset_of!(XusbShm, driver_heartbeat) == 36);
assert!(offset_of!(XusbShm, pad_index) == 40);
assert!(size_of::<PadShm>() == 256);
assert!(size_of::<PadShm>() == 1024);
assert!(offset_of!(PadShm, magic) == 0);
assert!(offset_of!(PadShm, input) == 8);
assert!(offset_of!(PadShm, out_seq) == 72);
@@ -753,6 +802,11 @@ pub mod gamepad {
assert!(offset_of!(PadShm, driver_proto) == 144);
assert!(offset_of!(PadShm, driver_heartbeat) == 148);
assert!(offset_of!(PadShm, pad_index) == 152);
// v2.1 ring extension — everything below PAD_SHM_LEGACY_SIZE is the v2 layout verbatim.
assert!(offset_of!(PadShm, out_ring_ver) == 156);
assert!(offset_of!(PadShm, ring_head) == 160);
assert!(offset_of!(PadShm, out_ring) == PAD_SHM_LEGACY_SIZE);
assert!(size_of::<OutSlot>() == 68);
assert!(size_of::<PadBootstrap>() == 32);
assert!(offset_of!(PadBootstrap, magic) == 0);