Workstream M6 of the haptics sweep — the desktop session's gamepad path. Closes B13, R8, R16.
B13 — the Deck lost its trackpad at every session start
SDL's Valve HIDAPI driver clears the pad's digital mappings during enumeration, which is part of bringing the gamepad subsystem up.
So holding the drivers off from inside GamepadService::pumpedcould never work: receiving a GamepadSubsystem means enumeration has already happened. The hint set there detached a driver that had already done the damage, and lizard mode only returned seconds later when the firmware watchdog restored it.
The presenter now disables them with its other pre-SDL_Init hints. The threaded worker (gamepad.rs::run) always had this right — only the caller-pumped path was wrong, and it could not fix itself from where it sits, hence a separate entry point.
R8 — player LEDs did nothing on any non-DualSense pad
The match arm handled the DualSense raw-effects path and let everything else fall through a bare _, though SDL exposes set_player_index and owns the per-device pattern.
The wire carries a positional bitmask, not an index. The bridge is the popcount: every convention that reaches this wire spells "player N" as N lit LEDs — DualSense 0x04/0x0A/0x15/0x1B/0x1F and Switch/XInput 0x01/0x03/0x07/0x0F alike — so counting works for both where reading a bit position would only ever suit one. No lit LED means no player, not player 0.
The remaining unhandled variants are now named rather than swept up by _, so a new one cannot join them silently.
R16 — a forwarded pad could be left buzzing at session end
detach() only posts Ctl::Detach; the close that flushes the pad, sends the host-side GamepadRemove and explicitly zeroes the motors runs when the pump next drains it. Single mode broke out of the loop immediately after detaching, and Event::Quit never detached at all — so both skipped it entirely.
The teardown now sits where every exit converges rather than on the individual breaks.
The finding as written was incomplete. Several paths leave that loop by ? on a fatal overlay or present error, and GamepadPump had no Drop — so those still left a rumbling pad rumbling. The pump now also silences its slots on Drop. The explicit call stays, because a pad should go quiet before a session join and vkDeviceWaitIdle, not after.
Drop closes the slots directly rather than draining the queue that would have done it. Same physical outcome, and it touches no lock — whereas draining reaches a Mutex::lock().unwrap(), and a panic there during an unwind aborts the process.
Verification
Run on amd64 Linux in pf-lxcheck2 (these crates are cfg(linux/windows); a red cargo check on macOS is pre-existing and unrelated — even wol.rs fails there).
Non-vacuity proven: planting the plausible wrong mapping (trailing_zeros(), the other obvious reading of a bitmask) fails 2 of the 3 new tests. The third passes under both — it pins 0 → None, an invariant common to either reading.
Not verified
No on-glass. B13's payoff is a Steam Deck keeping its trackpad cursor through a session start, and R16's is a pad that was mid-rumble when a session ended — neither is reachable from a unit test. R8's mapping is the part with genuine automated cover.
Workstream **M6** of the haptics sweep — the desktop session's gamepad path. Closes **B13, R8, R16**.
### B13 — the Deck lost its trackpad at every session start
SDL's Valve HIDAPI driver clears the pad's digital mappings during *enumeration*, which is part of bringing the gamepad subsystem up.
So holding the drivers off from inside `GamepadService::pumped` **could never work**: receiving a `GamepadSubsystem` means enumeration has already happened. The hint set there detached a driver that had already done the damage, and lizard mode only returned seconds later when the firmware watchdog restored it.
The presenter now disables them with its other pre-`SDL_Init` hints. The threaded worker (`gamepad.rs::run`) always had this right — only the caller-pumped path was wrong, and it could not fix itself from where it sits, hence a separate entry point.
### R8 — player LEDs did nothing on any non-DualSense pad
The match arm handled the DualSense raw-effects path and let everything else fall through a bare `_`, though SDL exposes `set_player_index` and owns the per-device pattern.
The wire carries a positional **bitmask**, not an index. The bridge is the popcount: every convention that reaches this wire spells "player N" as N lit LEDs — DualSense `0x04/0x0A/0x15/0x1B/0x1F` and Switch/XInput `0x01/0x03/0x07/0x0F` alike — so counting works for both where reading a bit *position* would only ever suit one. No lit LED means no player, not player 0.
The remaining unhandled variants are now named rather than swept up by `_`, so a new one cannot join them silently.
### R16 — a forwarded pad could be left buzzing at session end
`detach()` only posts `Ctl::Detach`; the close that flushes the pad, sends the host-side `GamepadRemove` and explicitly zeroes the motors runs when the pump next drains it. Single mode broke out of the loop immediately after detaching, and `Event::Quit` never detached at all — so both skipped it entirely.
The teardown now sits where every exit converges rather than on the individual `break`s.
**The finding as written was incomplete.** Several paths leave that loop by `?` on a fatal overlay or present error, and `GamepadPump` had no `Drop` — so those still left a rumbling pad rumbling. The pump now also silences its slots on `Drop`. The explicit call stays, because a pad should go quiet *before* a session join and `vkDeviceWaitIdle`, not after.
`Drop` closes the slots directly rather than draining the queue that would have done it. Same physical outcome, and it touches no lock — whereas draining reaches a `Mutex::lock().unwrap()`, and a panic there during an unwind aborts the process.
---
### Verification
Run on amd64 Linux in `pf-lxcheck2` (these crates are `cfg(linux/windows)`; a red `cargo check` on macOS is pre-existing and unrelated — even `wol.rs` fails there).
- `cargo clippy --all-targets --locked -p pf-client-core -p pf-presenter -- -D warnings` — **exit 0**
- `cargo test -p pf-client-core` — **85 passed / 0 failed** (82 baseline + 3 new)
- `cargo fmt --all --check` — clean
- **Non-vacuity proven**: planting the *plausible* wrong mapping (`trailing_zeros()`, the other obvious reading of a bitmask) fails 2 of the 3 new tests. The third passes under both — it pins `0 → None`, an invariant common to either reading.
### Not verified
No on-glass. B13's payoff is a Steam Deck keeping its trackpad cursor through a session start, and R16's is a pad that was mid-rumble when a session ended — neither is reachable from a unit test. R8's mapping is the part with genuine automated cover.
Three faults in the desktop session's gamepad path.
The Steam Deck lost its built-in trackpad-mouse at the start of every session.
SDL's Valve HIDAPI driver clears the pad's digital mappings during
*enumeration*, which is part of bringing the gamepad subsystem up — so holding
the drivers off from inside GamepadService::pumped could never work: receiving
a GamepadSubsystem means the enumeration has already happened. The hint set
there detached a driver that had already done the damage, and lizard mode only
came back seconds later when the firmware watchdog restored it. The presenter
now disables them with its other pre-SDL_Init hints. The threaded worker always
had this right; only the caller-pumped path was wrong, and it could not fix
itself, hence a separate entry point its callers can place correctly.
Player LEDs did nothing at all on any pad that is not a DualSense. The match
arm handled the DualSense raw-effects path and let everything else fall through
a bare `_`, though SDL exposes set_player_index and owns the per-device
pattern. The wire carries a positional bitmask rather than an index, and the
bridge is the popcount: every convention that reaches this wire spells "player
N" as N lit LEDs — the DualSense patterns 0x04/0x0A/0x15/0x1B/0x1F and the
Switch/XInput run 0x01/0x03/0x07/0x0F alike — so counting them works for both,
where reading a bit position would only ever suit one. No lit LED means no
player, not player 0. The remaining unhandled variants are now named rather
than swept up by `_`, so a new one cannot join them silently.
A forwarded pad could be left buzzing when the session ended. detach() only
posts Ctl::Detach; the close that flushes the pad, tells the host to remove it
and explicitly zeroes the motors runs when the pump next drains that message.
Single mode broke out of the loop immediately after detaching and Event::Quit
never detached at all, so both skipped it entirely. The teardown now sits where
every exit converges instead of on the individual breaks. That still leaves the
several paths that leave by `?` on a fatal overlay or present error, so the
pump also silences its slots on Drop — the explicit call stays, because a pad
should go quiet before a long teardown rather than after it. Drop closes the
slots directly rather than draining the queue that would have done it: same
physical outcome, and it touches no lock, where draining reaches an unwrap on a
Mutex that would abort the process if it panicked mid-unwind.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Workstream M6 of the haptics sweep — the desktop session's gamepad path. Closes B13, R8, R16.
B13 — the Deck lost its trackpad at every session start
SDL's Valve HIDAPI driver clears the pad's digital mappings during enumeration, which is part of bringing the gamepad subsystem up.
So holding the drivers off from inside
GamepadService::pumpedcould never work: receiving aGamepadSubsystemmeans enumeration has already happened. The hint set there detached a driver that had already done the damage, and lizard mode only returned seconds later when the firmware watchdog restored it.The presenter now disables them with its other pre-
SDL_Inithints. The threaded worker (gamepad.rs::run) always had this right — only the caller-pumped path was wrong, and it could not fix itself from where it sits, hence a separate entry point.R8 — player LEDs did nothing on any non-DualSense pad
The match arm handled the DualSense raw-effects path and let everything else fall through a bare
_, though SDL exposesset_player_indexand owns the per-device pattern.The wire carries a positional bitmask, not an index. The bridge is the popcount: every convention that reaches this wire spells "player N" as N lit LEDs — DualSense
0x04/0x0A/0x15/0x1B/0x1Fand Switch/XInput0x01/0x03/0x07/0x0Falike — so counting works for both where reading a bit position would only ever suit one. No lit LED means no player, not player 0.The remaining unhandled variants are now named rather than swept up by
_, so a new one cannot join them silently.R16 — a forwarded pad could be left buzzing at session end
detach()only postsCtl::Detach; the close that flushes the pad, sends the host-sideGamepadRemoveand explicitly zeroes the motors runs when the pump next drains it. Single mode broke out of the loop immediately after detaching, andEvent::Quitnever detached at all — so both skipped it entirely.The teardown now sits where every exit converges rather than on the individual
breaks.The finding as written was incomplete. Several paths leave that loop by
?on a fatal overlay or present error, andGamepadPumphad noDrop— so those still left a rumbling pad rumbling. The pump now also silences its slots onDrop. The explicit call stays, because a pad should go quiet before a session join andvkDeviceWaitIdle, not after.Dropcloses the slots directly rather than draining the queue that would have done it. Same physical outcome, and it touches no lock — whereas draining reaches aMutex::lock().unwrap(), and a panic there during an unwind aborts the process.Verification
Run on amd64 Linux in
pf-lxcheck2(these crates arecfg(linux/windows); a redcargo checkon macOS is pre-existing and unrelated — evenwol.rsfails there).cargo clippy --all-targets --locked -p pf-client-core -p pf-presenter -- -D warnings— exit 0cargo test -p pf-client-core— 85 passed / 0 failed (82 baseline + 3 new)cargo fmt --all --check— cleantrailing_zeros(), the other obvious reading of a bitmask) fails 2 of the 3 new tests. The third passes under both — it pins0 → None, an invariant common to either reading.Not verified
No on-glass. B13's payoff is a Steam Deck keeping its trackpad cursor through a session start, and R16's is a pad that was mid-rumble when a session ended — neither is reachable from a unit test. R8's mapping is the part with genuine automated cover.