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:
2026-07-13 12:33:49 +02:00
parent 43e52437c0
commit 5109a4c80a
9 changed files with 193 additions and 39 deletions
@@ -23,6 +23,7 @@ use super::dualsense_proto::{
};
use super::gamepad_raii::PadChannel;
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
use crate::inject::pad_gate::PadGate;
use anyhow::{anyhow, Result};
use punktfunk_core::quic::{HidOutput, RichInput};
use std::ffi::c_void;
@@ -385,7 +386,9 @@ pub struct DualSenseWindowsManager {
state: Vec<DsState>,
last_rumble: Vec<(u16, u16)>,
last_write: Vec<Instant>,
broken: bool,
/// Create-retry gate: a transient UMDF-channel failure backs off and retries instead of
/// permanently disabling every pad for the session.
gate: PadGate,
}
impl Default for DualSenseWindowsManager {
@@ -401,7 +404,7 @@ impl DualSenseWindowsManager {
state: vec![DsState::neutral(); MAX_PADS],
last_rumble: vec![(0, 0); MAX_PADS],
last_write: vec![Instant::now(); MAX_PADS],
broken: false,
gate: PadGate::new(),
}
}
@@ -486,7 +489,7 @@ impl DualSenseWindowsManager {
}
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 DsWinPad::open(idx as u8) {
@@ -499,10 +502,11 @@ impl DualSenseWindowsManager {
self.state[idx] = DsState::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 DualSense creation failed — controller input disabled until the next client connect (install/repair: punktfunk-host.exe driver install --gamepad)");
self.broken = true;
tracing::error!(error = %format!("{e:#}"), "virtual DualSense creation failed — retrying with backoff (install/repair: punktfunk-host.exe driver install --gamepad)");
self.gate.on_failure(Instant::now());
}
}
}