fix(rumble): accept the v2 vibration valid-flag; bound stale buzz to 1.5 s

Two hardenings against runaway vibration (one Deck session buzzed on a
~2 s cadence): the DualSense output-report parse now also treats
valid_flag2 COMPATIBLE_VIBRATION2 (data[39] BIT2) as rumble-carrying —
a writer hardcoding the ≥2.24-firmware convention previously had its
rumble INCLUDING stops silently ignored, and a missed stop re-sends
stale nonzero state forever via the host's 500 ms refresh. And the
client's SDL rumble duration drops 5 s → 1.5 s: long enough that a
couple of lost 500 ms refreshes don't gap genuine rumble, short enough
that stale state dies on its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 12:29:32 +02:00
parent 4516bd48e1
commit dc834ea478
2 changed files with 12 additions and 4 deletions
+6 -3
View File
@@ -1227,8 +1227,11 @@ impl Worker {
/// Drain and render the feedback planes — rumble plus HID output (lightbar /
/// player LEDs / adaptive triggers) — on the active pad; this thread is their single
/// consumer. The host re-sends rumble state periodically, so a generous duration with
/// refresh-on-update is safe — a dropped stop heals within ~500 ms.
/// consumer. The host re-sends rumble state every ~500 ms, so the SDL duration only
/// needs to outlive a couple of refresh periods: long enough that one or two lost
/// refreshes don't gap a genuine long rumble, short enough that a stale nonzero state
/// (a stop lost host-side, a session torn down mid-buzz) dies on its own instead of
/// droning for seconds.
fn render_feedback(&mut self) {
let Some(connector) = self.attached.clone() else {
return;
@@ -1240,7 +1243,7 @@ impl Worker {
// the right HIDAPI mode, etc.) reads exactly like "rumble doesn't work". The
// host logs the send side on 0xCA, so the two together pinpoint host-game vs
// client-render.
if let Err(e) = p.set_rumble(low, high, 5_000) {
if let Err(e) = p.set_rumble(low, high, 1_500) {
tracing::warn!(low, high, error = %e, "rumble: SDL set_rumble failed");
} else {
tracing::debug!(low, high, "rumble: rendered");
@@ -397,7 +397,12 @@ pub fn parse_ds_output(pad: u8, data: &[u8], fb: &mut DsFeedback) {
// Motor rumble: high-frequency (small/right) motor at data[3], low-frequency (big/left) at
// data[4]. Scale 0..255 → 0..0xFFFF, same (low, high) convention as the uinput pad's mixer,
// and route to the universal rumble plane (0xCA).
if flag0 & 0x03 != 0 {
// Writers on firmware ≥ 2.24 signal rumble via COMPATIBLE_VIBRATION2 in valid_flag2
// (data[39] BIT2) instead of flag0 BIT0. Our feature report advertises 0x0154 so the
// kernel and SDL stay on the flag0 convention, but a writer that hardcodes v2 would
// otherwise have its rumble — including stops — silently ignored, and a missed stop
// buzzes for the rest of the session (the 500 ms refresh re-sends stale state forever).
if flag0 & 0x03 != 0 || data[39] & 0x04 != 0 {
let high = (data[3] as u16) << 8;
let low = (data[4] as u16) << 8;
fb.rumble = Some((low, high));