fix(host/windows): force off a game-abandoned rumble on the UMDF virtual pads

The DualSense / DualSense Edge / DualShock 4 / Switch Pro / Steam Deck backends
all run through UhidManager, whose pump() forwarded rumble only on a level
CHANGE and had no idle watchdog. A game that latches a one-shot rumble (a
Stardew axe hit, a DS3 hit) and then stops writing output reports left
last_rumble non-zero; native.rs re-sends the latched level every ~120 ms with a
fresh TTL and the Apple RumbleRenderer refreshes its envelope on every renewal,
so the controller vibrated continuously until a later event happened to write a
report the host parsed as a stop. The XUSB path already guards against this
(RUMBLE_IDLE_TIMEOUT force-off, 19e9828e); that guard was never ported here, so
every UMDF pad regressed for game-abandoned rumble once clients began
negotiating first-class virtual DualSense/DS4/etc. on Windows.

Port the guard into UhidManager::pump, keyed on game ACTIVITY (a fresh output
report, even at an unchanged level) so a rumble the game keeps asserting is
never cut — only an abandoned residual. The activity signal rides a new
PadFeedback.game_drove: Option<bool>; the Windows backends set it from a fresh
out_seq (via a `fresh` flag on DsFeedback/Ds4Feedback; the Deck uses is_some()).
Linux backends leave it None (untracked → always-active → the force-off never
fires there), so their behaviour is unchanged. +2 deterministic unit tests.

Verified: cargo check -p punktfunk-host --tests green on both Windows (.173) and
Linux (home-worker-5); the 10 inject::uhid_manager tests pass on Linux.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 17:45:29 +02:00
parent 1a7e3a6e4f
commit b45323c0be
11 changed files with 135 additions and 2 deletions
@@ -303,6 +303,9 @@ impl PadProto for DsLinuxProto {
PadFeedback {
rumble: fb.rumble,
hidout: fb.hidout,
// Linux hid-playstation reliably surfaces the game's rumble stop, so this backend does
// not need the abandoned-rumble force-off — stays untracked (see `PadFeedback`).
game_drove: None,
}
}
}
@@ -389,6 +392,9 @@ impl PadProto for DsEdgeLinuxProto {
PadFeedback {
rumble: fb.rumble,
hidout: fb.hidout,
// Linux hid-playstation reliably surfaces the game's rumble stop, so this backend does
// not need the abandoned-rumble force-off — stays untracked (see `PadFeedback`).
game_drove: None,
}
}
}