refactor(inject/linux/steam_controller): convert to UhidManager<SteamProto> (3.3)
The most hook-laden conversion: SteamControllerManager becomes a pub type alias of UhidManager<SteamProto>. The Steam-specific pieces map cleanly onto the trait — open() delegates to open_transport (usbip → gadget → UHID fallback, which keeps its own per-transport logging, so no extra success line, matching the old ensure), merge_frame preserves the trackpad coords/touch-bits/clicks + motion across button-only frames (the G2 fix, verbatim), and the gamepad-mode-entry pulse rides the force_heartbeat hook. DeckTransport goes pub (type Pad in a public-trait impl). Also un-fuses a doc-comment glitch where the manager's doc had been merged onto the DeckTransport enum. Verified on .21: clippy --all-targets -D warnings clean; full suite 290 pass / 0 fail. Part of G12/3.3 (§3a.4 commit 8) — all five stateful managers now share one skeleton. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,10 +23,9 @@ use super::steam_proto::{
|
|||||||
btn, parse_steam_output, serial_reply, serialize_deck_state, SteamState, STEAMDECK_PRODUCT,
|
btn, parse_steam_output, serial_reply, serialize_deck_state, SteamState, STEAMDECK_PRODUCT,
|
||||||
STEAMDECK_RDESC, STEAM_REPORT_LEN, STEAM_VENDOR,
|
STEAMDECK_RDESC, STEAM_REPORT_LEN, STEAM_VENDOR,
|
||||||
};
|
};
|
||||||
use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS};
|
use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager};
|
||||||
use crate::inject::pad_gate::PadGate;
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use punktfunk_core::quic::{HidOutput, RichInput};
|
use punktfunk_core::quic::RichInput;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
@@ -235,16 +234,12 @@ impl Drop for SteamDeckPad {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All virtual Steam Deck pads of a session — the Steam analogue of
|
|
||||||
/// [`DualSenseManager`](super::dualsense::DualSenseManager), selected with `PUNKTFUNK_GAMEPAD=steamdeck`.
|
|
||||||
/// Button/stick frames arrive via [`handle`](Self::handle); the right trackpad + motion via
|
|
||||||
/// [`apply_rich`](Self::apply_rich); [`pump`](Self::pump) services the kernel handshake + routes
|
|
||||||
/// rumble back; [`heartbeat`](Self::heartbeat) keeps the pad alive (and drives the mode-entry pulse).
|
|
||||||
/// The transport a manager pad drives. UHID is universal but Steam Input won't promote it (a UHID
|
/// The transport a manager pad drives. UHID is universal but Steam Input won't promote it (a UHID
|
||||||
/// device has no USB interface number, `Interface: -1`); the USB **gadget** (`raw_gadget`, SteamOS)
|
/// device has no USB interface number, `Interface: -1`); the USB **gadget** (`raw_gadget`, SteamOS)
|
||||||
/// and **usbip** (`vhci_hcd`, universal) both present the controller on USB interface 2, which Steam
|
/// and **usbip** (`vhci_hcd`, universal) both present the controller on USB interface 2, which Steam
|
||||||
/// Input *does* promote. Selected per-pad by [`open_transport`].
|
/// Input *does* promote. Selected per-pad by [`open_transport`]. (`pub`: the type appears as
|
||||||
enum DeckTransport {
|
/// `type Pad` in the `PadProto` impl, a public trait.)
|
||||||
|
pub enum DeckTransport {
|
||||||
Uhid(SteamDeckPad),
|
Uhid(SteamDeckPad),
|
||||||
Gadget(crate::inject::steam_gadget::SteamDeckGadget),
|
Gadget(crate::inject::steam_gadget::SteamDeckGadget),
|
||||||
Usbip(crate::inject::steam_usbip::SteamDeckUsbip),
|
Usbip(crate::inject::steam_usbip::SteamDeckUsbip),
|
||||||
@@ -356,59 +351,36 @@ fn open_transport(idx: u8) -> Result<DeckTransport> {
|
|||||||
Ok(DeckTransport::Uhid(p))
|
Ok(DeckTransport::Uhid(p))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SteamControllerManager {
|
/// The Steam-Deck-specific half of the shared stateful manager (see [`PadProto`]): the transport
|
||||||
pads: Vec<Option<DeckTransport>>,
|
/// open (usbip → gadget → UHID fallback via [`open_transport`], which logs its own per-transport
|
||||||
state: Vec<SteamState>,
|
/// outcome), the [`SteamState`] mappers, and the kernel-handshake service pass. Lifecycle (slot
|
||||||
last_rumble: Vec<(u16, u16)>,
|
/// table, unplug sweep, heartbeat, rumble dedup) lives in [`UhidManager`]; the gamepad-mode-entry
|
||||||
last_write: Vec<Instant>,
|
/// pulse rides the [`force_heartbeat`](PadProto::force_heartbeat) hook.
|
||||||
/// Create-retry gate: a transient `/dev/uhid` failure backs off and retries instead of
|
#[derive(Default)]
|
||||||
/// permanently disabling every pad for the session.
|
pub struct SteamProto;
|
||||||
gate: PadGate,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SteamControllerManager {
|
impl PadProto for SteamProto {
|
||||||
fn default() -> SteamControllerManager {
|
type Pad = DeckTransport;
|
||||||
SteamControllerManager::new()
|
type State = SteamState;
|
||||||
}
|
const LABEL: &'static str = "Steam Deck";
|
||||||
}
|
const DEVICE: &'static str = "Steam Deck";
|
||||||
|
const CREATE_HINT: &'static str = "";
|
||||||
|
|
||||||
impl SteamControllerManager {
|
fn open(&mut self, idx: u8) -> Result<DeckTransport> {
|
||||||
pub fn new() -> SteamControllerManager {
|
open_transport(idx)
|
||||||
SteamControllerManager {
|
|
||||||
pads: (0..MAX_PADS).map(|_| None).collect(),
|
|
||||||
state: vec![SteamState::neutral(); MAX_PADS],
|
|
||||||
last_rumble: vec![(0, 0); MAX_PADS],
|
|
||||||
last_write: vec![Instant::now(); MAX_PADS],
|
|
||||||
gate: PadGate::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(&mut self, ev: &GamepadEvent) {
|
fn neutral(&self) -> SteamState {
|
||||||
match ev {
|
SteamState::neutral()
|
||||||
GamepadEvent::Arrival { index, kind, .. } => {
|
|
||||||
tracing::info!(index, kind, "controller arrival (Steam Deck)");
|
|
||||||
self.ensure(*index as usize);
|
|
||||||
}
|
}
|
||||||
GamepadEvent::State(f) => {
|
|
||||||
let idx = f.index as usize;
|
/// Merge buttons/sticks/triggers, preserving the rich-plane fields (trackpad + motion arrive
|
||||||
if idx >= MAX_PADS {
|
/// separately and must survive a button-only frame).
|
||||||
return;
|
fn merge_frame(
|
||||||
}
|
&self,
|
||||||
for (i, slot) in self.pads.iter_mut().enumerate() {
|
prev: &SteamState,
|
||||||
if slot.is_some() && f.active_mask & (1 << i) == 0 {
|
f: &crate::gamestream::gamepad::GamepadFrame,
|
||||||
tracing::info!(index = i, "controller unplugged (Steam Deck)");
|
) -> SteamState {
|
||||||
*slot = None;
|
|
||||||
self.state[i] = SteamState::neutral();
|
|
||||||
self.last_rumble[i] = (0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if f.active_mask & (1 << idx) == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.ensure(idx);
|
|
||||||
// Merge buttons/sticks/triggers, preserving the rich-plane fields (trackpad + motion
|
|
||||||
// arrive separately and must survive a button-only frame).
|
|
||||||
let prev = self.state[idx];
|
|
||||||
let mut s = SteamState::from_gamepad(
|
let mut s = SteamState::from_gamepad(
|
||||||
f.buttons,
|
f.buttons,
|
||||||
f.ls_x,
|
f.ls_x,
|
||||||
@@ -431,86 +403,41 @@ impl SteamControllerManager {
|
|||||||
// wire-button's RPAD_CLICK — the two are OR'd only at serialize.
|
// wire-button's RPAD_CLICK — the two are OR'd only at serialize.
|
||||||
s.lpad_click = prev.lpad_click;
|
s.lpad_click = prev.lpad_click;
|
||||||
s.rpad_click = prev.rpad_click;
|
s.rpad_click = prev.rpad_click;
|
||||||
self.state[idx] = s;
|
s
|
||||||
self.write(idx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_rich(&self, st: &mut SteamState, rich: RichInput) {
|
||||||
|
st.apply_rich(rich);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_state(&self, pad: &mut DeckTransport, st: &SteamState) {
|
||||||
|
pad.write_state(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Answer the kernel handshake and forward rumble on the universal plane. The Steam Deck has
|
||||||
|
/// no rich host→client feedback plane (no lightbar / adaptive triggers), so `hidout` stays
|
||||||
|
/// empty.
|
||||||
|
fn service(&self, pad: &mut DeckTransport, _idx: u8) -> PadFeedback {
|
||||||
|
PadFeedback {
|
||||||
|
rumble: pad.service(),
|
||||||
|
hidout: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a rich client→host event (right trackpad / motion) to an existing pad.
|
/// Force a steady stream while a pad is still pulsing its gamepad-mode entry (so the `b9.6`
|
||||||
pub fn apply_rich(&mut self, rich: RichInput) {
|
/// toggle completes even with no game input).
|
||||||
let idx = match rich {
|
fn force_heartbeat(&self, pad: &DeckTransport) -> bool {
|
||||||
RichInput::Touchpad { pad, .. }
|
pad.in_mode_entry()
|
||||||
| RichInput::Motion { pad, .. }
|
|
||||||
| RichInput::TouchpadEx { pad, .. } => pad as usize,
|
|
||||||
};
|
|
||||||
if idx >= MAX_PADS || self.pads[idx].is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.state[idx].apply_rich(rich);
|
|
||||||
self.write(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write(&mut self, idx: usize) {
|
|
||||||
let st = self.state[idx];
|
|
||||||
if let Some(pad) = self.pads[idx].as_mut() {
|
|
||||||
pad.write_state(&st);
|
|
||||||
}
|
|
||||||
self.last_write[idx] = Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-emit each live pad's current report when silent past `max_gap`, and force a steady stream
|
|
||||||
/// while a pad is still pulsing its gamepad-mode entry (so the `b9.6` toggle completes even with
|
|
||||||
/// no game input).
|
|
||||||
pub fn heartbeat(&mut self, max_gap: Duration) {
|
|
||||||
let now = Instant::now();
|
|
||||||
for i in 0..self.pads.len() {
|
|
||||||
let Some(pad) = self.pads[i].as_ref() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if pad.in_mode_entry() || now.duration_since(self.last_write[i]) >= max_gap {
|
|
||||||
self.write(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure(&mut self, idx: usize) {
|
|
||||||
if idx >= MAX_PADS || self.pads[idx].is_some() || !self.gate.allow(Instant::now()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match open_transport(idx as u8) {
|
|
||||||
Ok(t) => {
|
|
||||||
self.pads[idx] = Some(t);
|
|
||||||
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 — retrying with backoff");
|
|
||||||
self.gate.on_failure(Instant::now());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Service every pad: answer the kernel handshake and forward rumble on the universal plane.
|
|
||||||
/// `rumble` fires `(index, low, high)` only on a level change. The Steam Deck has no rich
|
|
||||||
/// host→client feedback plane (no lightbar / adaptive triggers), so `hidout` goes unused.
|
|
||||||
pub fn pump(&mut self, mut rumble: impl FnMut(u16, u16, u16), _hidout: impl FnMut(HidOutput)) {
|
|
||||||
for i in 0..self.pads.len() {
|
|
||||||
let Some(pad) = self.pads[i].as_mut() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if let Some(r) = pad.service() {
|
|
||||||
if self.last_rumble[i] != r {
|
|
||||||
self.last_rumble[i] = r;
|
|
||||||
rumble(i as u16, r.0, r.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All virtual Steam Deck pads of a session — the Steam analogue of
|
||||||
|
/// [`DualSenseManager`](super::dualsense::DualSenseManager), selected with
|
||||||
|
/// `PUNKTFUNK_GAMEPAD=steamdeck`. Button/stick frames arrive via `handle`; the trackpads + motion
|
||||||
|
/// via `apply_rich`; `pump` services the kernel handshake + routes rumble back; `heartbeat` keeps
|
||||||
|
/// the pad alive (and drives the mode-entry pulse) — all from the shared [`UhidManager`].
|
||||||
|
pub type SteamControllerManager = UhidManager<SteamProto>;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user