diff --git a/crates/pf-client-core/src/gamepad.rs b/crates/pf-client-core/src/gamepad.rs index 986f1ad6..3024dd0a 100644 --- a/crates/pf-client-core/src/gamepad.rs +++ b/crates/pf-client-core/src/gamepad.rs @@ -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"); diff --git a/crates/punktfunk-host/src/inject/proto/dualsense_proto.rs b/crates/punktfunk-host/src/inject/proto/dualsense_proto.rs index 955623c7..62889ae6 100644 --- a/crates/punktfunk-host/src/inject/proto/dualsense_proto.rs +++ b/crates/punktfunk-host/src/inject/proto/dualsense_proto.rs @@ -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));