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
@@ -80,10 +80,10 @@ pub struct Ds4Feedback {
pub rumble: Option<(u16, u16)>,
/// Lightbar RGB, if the report carried it (deduped by the manager).
pub led: Option<(u8, u8, u8)>,
/// Whether a fresh output report was seen this poll (set by the backend's section poll, not by
/// the parser) — the game-activity signal the [`UhidManager`](crate::uhid_manager)
/// abandoned-rumble force-off keys on.
pub fresh: bool,
/// The driver's output-report ring overflowed this poll — pending reports were DISCARDED and
/// feedback state is unknown; the [`UhidManager`](crate::uhid_manager) must resync (silence +
/// re-armed dedups). Set by the backend's section drain, never by the parser.
pub resync: bool,
}
/// Parse a DualShock 4 USB output report (`0x05`) into a [`Ds4Feedback`]. Layout per the kernel
@@ -182,5 +182,22 @@ mod tests {
parse_ds4_output(&motor_only, &mut fb2);
assert!(fb2.rumble.is_some());
assert_eq!(fb2.led, None); // lightbar not asserted → no spurious change
// LED-only write: rumble not asserted → stays `None` (this is what `rumble_drove` keys
// on — an LED stream must not read as rumble activity), and an explicit flagged zero
// parses as `Some((0, 0))`, never as absence.
let mut led_only = [0u8; 32];
led_only[0] = 0x05;
led_only[1] = 0x02; // LED only
led_only[6] = 0x11;
let mut fb3 = Ds4Feedback::default();
parse_ds4_output(&led_only, &mut fb3);
assert!(fb3.rumble.is_none());
let mut stop = [0u8; 32];
stop[0] = 0x05;
stop[1] = 0x01; // MOTOR flag, motors zero
let mut fb4 = Ds4Feedback::default();
parse_ds4_output(&stop, &mut fb4);
assert_eq!(fb4.rumble, Some((0, 0)));
}
}