refactor(inject/host): extract the shared PadGate create-retry policy + fix the permanent broken latch (G3/G12)
All seven virtual-pad managers (Linux uinput/uhid: gamepad, dualsense, dualshock4, steam_controller; Windows XUSB/UMDF: gamepad, dualsense, dualshock4) carried an identical copy-pasted `broken: bool` latch that was set on the FIRST pad-creation error and never cleared — so a single transient failure (a startup race on /dev/uinput, a momentary EBUSY, the Windows companion driver not yet ready) permanently disabled EVERY controller for the rest of the session, even after the cause cleared. Extract that latch into one shared, unit-tested `PadGate` (inject/pad_gate.rs) with the fix baked in: capped exponential backoff (1s doubling to 30s) instead of a permanent kill. After a failure, creation is blocked only until the backoff elapses — so the manager no longer re-attempts (and re-logs) on every one of the 60–240 input frames/sec — then a single retry is allowed; a success resets the backoff. A genuinely broken setup therefore self-heals within one backoff window of the fix (udev reload / driver install / next client connect) with no host restart. The gate is manager-wide, matching the old flag's semantics (these failures are systemic, not per-slot). This folds G3 (broken latch) into G12 (dedup the manager skeleton): the latch now lives in one place across all seven backends. Verified on the Linux host build (.21): cargo clippy -D warnings clean, full punktfunk-host suite 277 passed / 0 failed, 4 new PadGate tests green. Windows managers verified separately on the x64 box. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ use super::steam_proto::{
|
||||
STEAMDECK_RDESC, STEAM_REPORT_LEN, STEAM_VENDOR,
|
||||
};
|
||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
||||
use crate::inject::pad_gate::PadGate;
|
||||
use anyhow::{Context, Result};
|
||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
||||
use std::fs::{File, OpenOptions};
|
||||
@@ -353,7 +354,9 @@ pub struct SteamControllerManager {
|
||||
state: Vec<SteamState>,
|
||||
last_rumble: Vec<(u16, u16)>,
|
||||
last_write: Vec<Instant>,
|
||||
broken: bool,
|
||||
/// Create-retry gate: a transient `/dev/uhid` failure backs off and retries instead of
|
||||
/// permanently disabling every pad for the session.
|
||||
gate: PadGate,
|
||||
}
|
||||
|
||||
impl Default for SteamControllerManager {
|
||||
@@ -369,7 +372,7 @@ impl SteamControllerManager {
|
||||
state: vec![SteamState::neutral(); MAX_PADS],
|
||||
last_rumble: vec![(0, 0); MAX_PADS],
|
||||
last_write: vec![Instant::now(); MAX_PADS],
|
||||
broken: false,
|
||||
gate: PadGate::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +468,7 @@ impl SteamControllerManager {
|
||||
}
|
||||
|
||||
fn ensure(&mut self, idx: usize) {
|
||||
if idx >= MAX_PADS || self.pads[idx].is_some() || self.broken {
|
||||
if idx >= MAX_PADS || self.pads[idx].is_some() || !self.gate.allow(Instant::now()) {
|
||||
return;
|
||||
}
|
||||
match open_transport(idx as u8) {
|
||||
@@ -474,10 +477,11 @@ impl SteamControllerManager {
|
||||
self.state[idx] = SteamState::neutral();
|
||||
self.last_rumble[idx] = (0, 0);
|
||||
self.last_write[idx] = Instant::now();
|
||||
self.gate.on_success();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %format!("{e:#}"), "virtual Steam Deck creation failed — controller input disabled");
|
||||
self.broken = true;
|
||||
tracing::error!(error = %format!("{e:#}"), "virtual Steam Deck creation failed — retrying with backoff");
|
||||
self.gate.on_failure(Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user