feat(clients/steam): M4 — desktop SDL clients capture the rich Steam inputs
The Linux + Windows native clients (clients/{linux,windows}/src/gamepad.rs) now
capture and send the Steam Controller / Steam Deck rich inputs, so a real Deck
(off Steam Input) or a Steam Controller on a desktop client drives the host's
virtual hid-steam pad end-to-end:
- Set SDL's HIDAPI Steam hints (SDL_JOYSTICK_HIDAPI_STEAMDECK / _STEAM) before
init so SDL opens Valve devices directly (paddles + both trackpads + gyro as
first-class SDL gamepad inputs).
- Detect the Deck/SC by VID/PID (0x28DE + 0x1205 / 0x1102 / 0x1142) ->
GamepadPref::SteamDeck (there is no SDL gamepad type for it), so the host
builds the virtual Deck with the right identity.
- Map the SDL paddle + Misc1 buttons -> BTN_PADDLE1..4 / BTN_MISC1 (a free win
for Xbox Elite paddles too).
- Route a SECOND touchpad -> RichInput::TouchpadEx (SDL touchpad 0 = left ->
surface 1, 1 = right -> surface 2, signed coords); a single touchpad keeps the
legacy Touchpad. New forward_touch() helper centralizes the choice.
- Track held touchpad contacts per (surface, finger) and lift them on pad
switch/detach so a contact held at that moment can't stick.
- Sensor (gyro/accel) capture was already generic across pad types.
Linux client builds + clippy clean; the Windows client is a near-verbatim
mirror (windows CI compiles it). On a Deck in Game Mode, Steam Input still holds
the device — the user disables Steam Input for the client (the Decky UX, next);
on a desktop client (or a Deck with Steam Input off) the hints just work.
Remaining M4: Decky Disable-Steam-Input UX, Apple/Android parity, and the C-ABI
PunktfunkRichInputEx + send_rich_input2 (Apple/embedder send path). Not pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+114
-27
@@ -58,6 +58,7 @@ impl PadInfo {
|
|||||||
GamepadPref::DualSense => "DualSense",
|
GamepadPref::DualSense => "DualSense",
|
||||||
GamepadPref::DualShock4 => "DualShock 4",
|
GamepadPref::DualShock4 => "DualShock 4",
|
||||||
GamepadPref::XboxOne => "Xbox One",
|
GamepadPref::XboxOne => "Xbox One",
|
||||||
|
GamepadPref::SteamDeck => "Steam Deck",
|
||||||
_ => "",
|
_ => "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,6 +189,13 @@ fn button_bit(b: sdl3::gamepad::Button) -> Option<u32> {
|
|||||||
Button::DPadLeft => wire::BTN_DPAD_LEFT,
|
Button::DPadLeft => wire::BTN_DPAD_LEFT,
|
||||||
Button::DPadRight => wire::BTN_DPAD_RIGHT,
|
Button::DPadRight => wire::BTN_DPAD_RIGHT,
|
||||||
Button::Touchpad => wire::BTN_TOUCHPAD,
|
Button::Touchpad => wire::BTN_TOUCHPAD,
|
||||||
|
// Back grips / paddles (Steam Deck L4/L5/R4/R5, Xbox Elite P1–P4) + the misc/Share button.
|
||||||
|
// PADDLE1/2/3/4 = R4/L4/R5/L5 (see the host `input::gamepad`).
|
||||||
|
Button::RightPaddle1 => wire::BTN_PADDLE1,
|
||||||
|
Button::LeftPaddle1 => wire::BTN_PADDLE2,
|
||||||
|
Button::RightPaddle2 => wire::BTN_PADDLE3,
|
||||||
|
Button::LeftPaddle2 => wire::BTN_PADDLE4,
|
||||||
|
Button::Misc1 => wire::BTN_MISC1,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -259,6 +267,10 @@ struct Worker {
|
|||||||
/// Wire state of the active pad — zeroed on the wire at switch/detach.
|
/// Wire state of the active pad — zeroed on the wire at switch/detach.
|
||||||
last_axis: [i32; 6],
|
last_axis: [i32; 6],
|
||||||
held_buttons: Vec<u32>,
|
held_buttons: Vec<u32>,
|
||||||
|
/// Touchpad contacts the host believes are down, keyed by `(surface, finger)` — lifted on pad
|
||||||
|
/// switch / detach so a contact held at that moment doesn't stick. surface 0 = the legacy single
|
||||||
|
/// touchpad, 1/2 = a Steam left/right pad.
|
||||||
|
held_touches: std::collections::HashSet<(u8, u8)>,
|
||||||
last_accel: [i16; 3],
|
last_accel: [i16; 3],
|
||||||
/// Raises the UI escape signal; the escape chord fires it once per press.
|
/// Raises the UI escape signal; the escape chord fires it once per press.
|
||||||
escape_tx: async_channel::Sender<()>,
|
escape_tx: async_channel::Sender<()>,
|
||||||
@@ -275,13 +287,22 @@ impl Worker {
|
|||||||
|
|
||||||
fn pad_info(&self, id: u32) -> Option<PadInfo> {
|
fn pad_info(&self, id: u32) -> Option<PadInfo> {
|
||||||
let pad = self.opened.get(&id)?;
|
let pad = self.opened.get(&id)?;
|
||||||
|
let mut pref = pref_for_type(
|
||||||
|
self.subsystem
|
||||||
|
.type_for_id(sdl3::sys::joystick::SDL_JoystickID(id)),
|
||||||
|
);
|
||||||
|
// There is no SDL gamepad type for the Steam Deck / Steam Controller, so detect Valve by
|
||||||
|
// VID/PID (Deck 0x1205, SC wired 0x1102, SC dongle 0x1142) — the host then builds the virtual
|
||||||
|
// hid-steam pad with the back grips + dual trackpads and the right glyph identity.
|
||||||
|
if pad.vendor_id() == Some(0x28DE)
|
||||||
|
&& matches!(pad.product_id(), Some(0x1205 | 0x1102 | 0x1142))
|
||||||
|
{
|
||||||
|
pref = GamepadPref::SteamDeck;
|
||||||
|
}
|
||||||
Some(PadInfo {
|
Some(PadInfo {
|
||||||
id,
|
id,
|
||||||
name: pad.name().unwrap_or_else(|| "Controller".into()),
|
name: pad.name().unwrap_or_else(|| "Controller".into()),
|
||||||
pref: pref_for_type(
|
pref,
|
||||||
self.subsystem
|
|
||||||
.type_for_id(sdl3::sys::joystick::SDL_JoystickID(id)),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,9 +318,34 @@ impl Worker {
|
|||||||
}
|
}
|
||||||
*v = i32::MIN;
|
*v = i32::MIN;
|
||||||
}
|
}
|
||||||
|
// Lift any touchpad contact the host still believes is down (surface 0 = legacy pad).
|
||||||
|
for (surface, finger) in self.held_touches.drain() {
|
||||||
|
let rich = if surface == 0 {
|
||||||
|
RichInput::Touchpad {
|
||||||
|
pad: 0,
|
||||||
|
finger,
|
||||||
|
active: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RichInput::TouchpadEx {
|
||||||
|
pad: 0,
|
||||||
|
surface,
|
||||||
|
finger,
|
||||||
|
touch: false,
|
||||||
|
click: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
pressure: 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = c.send_rich_input(rich);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.held_buttons.clear();
|
self.held_buttons.clear();
|
||||||
self.last_axis = [i32::MIN; 6];
|
self.last_axis = [i32::MIN; 6];
|
||||||
|
self.held_touches.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +381,56 @@ impl Worker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forward one touchpad contact on the rich-input plane. A multi-touchpad pad (Steam Deck / Steam
|
||||||
|
/// Controller) sends `TouchpadEx` with the surface (SDL touchpad 0 = left → 1, 1 = right → 2) and
|
||||||
|
/// signed coordinates; a single-touchpad pad (DualSense) keeps the legacy `Touchpad` (unsigned).
|
||||||
|
fn forward_touch(
|
||||||
|
&mut self,
|
||||||
|
which: u32,
|
||||||
|
touchpad: u32,
|
||||||
|
finger: u8,
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
active: bool,
|
||||||
|
) {
|
||||||
|
let Some(c) = self.attached.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let multi = self
|
||||||
|
.opened
|
||||||
|
.get(&which)
|
||||||
|
.map(|p| p.touchpads_count() >= 2)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let (cx, cy) = (x.clamp(0.0, 1.0), y.clamp(0.0, 1.0));
|
||||||
|
let surface = if multi { (touchpad as u8) + 1 } else { 0 };
|
||||||
|
let rich = if multi {
|
||||||
|
RichInput::TouchpadEx {
|
||||||
|
pad: 0,
|
||||||
|
surface,
|
||||||
|
finger,
|
||||||
|
touch: active,
|
||||||
|
click: false,
|
||||||
|
x: (cx * 65535.0 - 32768.0) as i16,
|
||||||
|
y: (cy * 65535.0 - 32768.0) as i16,
|
||||||
|
pressure: 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RichInput::Touchpad {
|
||||||
|
pad: 0,
|
||||||
|
finger,
|
||||||
|
active,
|
||||||
|
x: (cx * 65535.0) as u16,
|
||||||
|
y: (cy * 65535.0) as u16,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = c.send_rich_input(rich);
|
||||||
|
if active {
|
||||||
|
self.held_touches.insert((surface, finger));
|
||||||
|
} else {
|
||||||
|
self.held_touches.remove(&(surface, finger));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
@@ -349,6 +445,12 @@ fn run(
|
|||||||
// own thread.
|
// own thread.
|
||||||
sdl3::hint::set("SDL_NO_SIGNAL_HANDLERS", "1");
|
sdl3::hint::set("SDL_NO_SIGNAL_HANDLERS", "1");
|
||||||
sdl3::hint::set("SDL_JOYSTICK_THREAD", "1");
|
sdl3::hint::set("SDL_JOYSTICK_THREAD", "1");
|
||||||
|
// Let SDL's HIDAPI drivers open Valve Steam Controller / Steam Deck devices directly, so the
|
||||||
|
// paddles, both trackpads, and gyro arrive as first-class SDL gamepad inputs. On a Deck in Game
|
||||||
|
// Mode, Steam Input still holds the device — the user must disable Steam Input for this app (see
|
||||||
|
// the Decky UX); on a desktop client (or a Deck with Steam Input off) the hints just work.
|
||||||
|
sdl3::hint::set("SDL_JOYSTICK_HIDAPI_STEAMDECK", "1");
|
||||||
|
sdl3::hint::set("SDL_JOYSTICK_HIDAPI_STEAM", "1");
|
||||||
let sdl = sdl3::init().map_err(|e| e.to_string())?;
|
let sdl = sdl3::init().map_err(|e| e.to_string())?;
|
||||||
let subsystem = sdl.gamepad().map_err(|e| e.to_string())?;
|
let subsystem = sdl.gamepad().map_err(|e| e.to_string())?;
|
||||||
let mut pump = sdl.event_pump().map_err(|e| e.to_string())?;
|
let mut pump = sdl.event_pump().map_err(|e| e.to_string())?;
|
||||||
@@ -361,6 +463,7 @@ fn run(
|
|||||||
attached: None,
|
attached: None,
|
||||||
last_axis: [i32::MIN; 6],
|
last_axis: [i32::MIN; 6],
|
||||||
held_buttons: Vec::new(),
|
held_buttons: Vec::new(),
|
||||||
|
held_touches: std::collections::HashSet::new(),
|
||||||
last_accel: [0; 3],
|
last_accel: [0; 3],
|
||||||
escape_tx: escape_tx.clone(),
|
escape_tx: escape_tx.clone(),
|
||||||
chord_armed: false,
|
chord_armed: false,
|
||||||
@@ -474,9 +577,11 @@ fn run(
|
|||||||
send(w.attached.as_ref().unwrap(), InputKind::GamepadAxis, id, v);
|
send(w.attached.as_ref().unwrap(), InputKind::GamepadAxis, id, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DualSense touchpad → the rich-input plane, normalized 0..=65535.
|
// Touchpad contacts → the rich-input plane. One pad (DualSense) keeps the legacy
|
||||||
|
// `Touchpad`; two pads (Steam Deck / Steam Controller) send `TouchpadEx` per surface.
|
||||||
Event::ControllerTouchpadDown {
|
Event::ControllerTouchpadDown {
|
||||||
which,
|
which,
|
||||||
|
touchpad,
|
||||||
finger,
|
finger,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@@ -484,41 +589,23 @@ fn run(
|
|||||||
}
|
}
|
||||||
| Event::ControllerTouchpadMotion {
|
| Event::ControllerTouchpadMotion {
|
||||||
which,
|
which,
|
||||||
|
touchpad,
|
||||||
finger,
|
finger,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
..
|
..
|
||||||
} if active == Some(which) && w.attached.is_some() => {
|
} if active == Some(which) && w.attached.is_some() => {
|
||||||
let _ = w
|
w.forward_touch(which, touchpad as u32, finger as u8, x, y, true);
|
||||||
.attached
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.send_rich_input(RichInput::Touchpad {
|
|
||||||
pad: 0,
|
|
||||||
finger: finger as u8,
|
|
||||||
active: true,
|
|
||||||
x: (x.clamp(0.0, 1.0) * 65535.0) as u16,
|
|
||||||
y: (y.clamp(0.0, 1.0) * 65535.0) as u16,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Event::ControllerTouchpadUp {
|
Event::ControllerTouchpadUp {
|
||||||
which,
|
which,
|
||||||
|
touchpad,
|
||||||
finger,
|
finger,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
..
|
..
|
||||||
} if active == Some(which) && w.attached.is_some() => {
|
} if active == Some(which) && w.attached.is_some() => {
|
||||||
let _ = w
|
w.forward_touch(which, touchpad as u32, finger as u8, x, y, false);
|
||||||
.attached
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.send_rich_input(RichInput::Touchpad {
|
|
||||||
pad: 0,
|
|
||||||
finger: finger as u8,
|
|
||||||
active: false,
|
|
||||||
x: (x.clamp(0.0, 1.0) * 65535.0) as u16,
|
|
||||||
y: (y.clamp(0.0, 1.0) * 65535.0) as u16,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Motion: accel events update the cache; each gyro event ships a sample
|
// Motion: accel events update the cache; each gyro event ships a sample
|
||||||
// (the DualSense reports both at ~250 Hz). Scale convention shared with
|
// (the DualSense reports both at ~250 Hz). Scale convention shared with
|
||||||
|
|||||||
+108
-27
@@ -169,6 +169,13 @@ fn button_bit(b: sdl3::gamepad::Button) -> Option<u32> {
|
|||||||
Button::DPadLeft => wire::BTN_DPAD_LEFT,
|
Button::DPadLeft => wire::BTN_DPAD_LEFT,
|
||||||
Button::DPadRight => wire::BTN_DPAD_RIGHT,
|
Button::DPadRight => wire::BTN_DPAD_RIGHT,
|
||||||
Button::Touchpad => wire::BTN_TOUCHPAD,
|
Button::Touchpad => wire::BTN_TOUCHPAD,
|
||||||
|
// Back grips / paddles (Steam Deck L4/L5/R4/R5, Xbox Elite P1–P4) + the misc/Share button.
|
||||||
|
// PADDLE1/2/3/4 = R4/L4/R5/L5 (see the host `input::gamepad`).
|
||||||
|
Button::RightPaddle1 => wire::BTN_PADDLE1,
|
||||||
|
Button::LeftPaddle1 => wire::BTN_PADDLE2,
|
||||||
|
Button::RightPaddle2 => wire::BTN_PADDLE3,
|
||||||
|
Button::LeftPaddle2 => wire::BTN_PADDLE4,
|
||||||
|
Button::Misc1 => wire::BTN_MISC1,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -240,6 +247,9 @@ struct Worker {
|
|||||||
/// Wire state of the active pad — zeroed on the wire at switch/detach.
|
/// Wire state of the active pad — zeroed on the wire at switch/detach.
|
||||||
last_axis: [i32; 6],
|
last_axis: [i32; 6],
|
||||||
held_buttons: Vec<u32>,
|
held_buttons: Vec<u32>,
|
||||||
|
/// Touchpad contacts the host believes are down, keyed by `(surface, finger)` — lifted on pad
|
||||||
|
/// switch / detach. surface 0 = the legacy single touchpad, 1/2 = a Steam left/right pad.
|
||||||
|
held_touches: std::collections::HashSet<(u8, u8)>,
|
||||||
last_accel: [i16; 3],
|
last_accel: [i16; 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,13 +262,21 @@ impl Worker {
|
|||||||
|
|
||||||
fn pad_info(&self, id: u32) -> Option<PadInfo> {
|
fn pad_info(&self, id: u32) -> Option<PadInfo> {
|
||||||
let pad = self.opened.get(&id)?;
|
let pad = self.opened.get(&id)?;
|
||||||
|
let mut pref = pref_for_type(
|
||||||
|
self.subsystem
|
||||||
|
.type_for_id(sdl3::sys::joystick::SDL_JoystickID(id)),
|
||||||
|
);
|
||||||
|
// No SDL type for the Steam Deck / Steam Controller — detect Valve by VID/PID (Deck 0x1205,
|
||||||
|
// SC wired 0x1102, SC dongle 0x1142) so the host builds the virtual hid-steam pad.
|
||||||
|
if pad.vendor_id() == Some(0x28DE)
|
||||||
|
&& matches!(pad.product_id(), Some(0x1205 | 0x1102 | 0x1142))
|
||||||
|
{
|
||||||
|
pref = GamepadPref::SteamDeck;
|
||||||
|
}
|
||||||
Some(PadInfo {
|
Some(PadInfo {
|
||||||
id,
|
id,
|
||||||
name: pad.name().unwrap_or_else(|| "Controller".into()),
|
name: pad.name().unwrap_or_else(|| "Controller".into()),
|
||||||
pref: pref_for_type(
|
pref,
|
||||||
self.subsystem
|
|
||||||
.type_for_id(sdl3::sys::joystick::SDL_JoystickID(id)),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,9 +292,33 @@ impl Worker {
|
|||||||
}
|
}
|
||||||
*v = i32::MIN;
|
*v = i32::MIN;
|
||||||
}
|
}
|
||||||
|
for (surface, finger) in self.held_touches.drain() {
|
||||||
|
let rich = if surface == 0 {
|
||||||
|
RichInput::Touchpad {
|
||||||
|
pad: 0,
|
||||||
|
finger,
|
||||||
|
active: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RichInput::TouchpadEx {
|
||||||
|
pad: 0,
|
||||||
|
surface,
|
||||||
|
finger,
|
||||||
|
touch: false,
|
||||||
|
click: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
pressure: 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = c.send_rich_input(rich);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.held_buttons.clear();
|
self.held_buttons.clear();
|
||||||
self.last_axis = [i32::MIN; 6];
|
self.last_axis = [i32::MIN; 6];
|
||||||
|
self.held_touches.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +334,56 @@ impl Worker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forward one touchpad contact on the rich-input plane. A multi-touchpad pad (Steam Deck / Steam
|
||||||
|
/// Controller) sends `TouchpadEx` with the surface (SDL touchpad 0 = left → 1, 1 = right → 2) and
|
||||||
|
/// signed coordinates; a single-touchpad pad (DualSense) keeps the legacy `Touchpad` (unsigned).
|
||||||
|
fn forward_touch(
|
||||||
|
&mut self,
|
||||||
|
which: u32,
|
||||||
|
touchpad: u32,
|
||||||
|
finger: u8,
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
active: bool,
|
||||||
|
) {
|
||||||
|
let Some(c) = self.attached.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let multi = self
|
||||||
|
.opened
|
||||||
|
.get(&which)
|
||||||
|
.map(|p| p.touchpads_count() >= 2)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let (cx, cy) = (x.clamp(0.0, 1.0), y.clamp(0.0, 1.0));
|
||||||
|
let surface = if multi { (touchpad as u8) + 1 } else { 0 };
|
||||||
|
let rich = if multi {
|
||||||
|
RichInput::TouchpadEx {
|
||||||
|
pad: 0,
|
||||||
|
surface,
|
||||||
|
finger,
|
||||||
|
touch: active,
|
||||||
|
click: false,
|
||||||
|
x: (cx * 65535.0 - 32768.0) as i16,
|
||||||
|
y: (cy * 65535.0 - 32768.0) as i16,
|
||||||
|
pressure: 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RichInput::Touchpad {
|
||||||
|
pad: 0,
|
||||||
|
finger,
|
||||||
|
active,
|
||||||
|
x: (cx * 65535.0) as u16,
|
||||||
|
y: (cy * 65535.0) as u16,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = c.send_rich_input(rich);
|
||||||
|
if active {
|
||||||
|
self.held_touches.insert((surface, finger));
|
||||||
|
} else {
|
||||||
|
self.held_touches.remove(&(surface, finger));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
@@ -305,6 +397,10 @@ fn run(
|
|||||||
// thread.
|
// thread.
|
||||||
sdl3::hint::set("SDL_NO_SIGNAL_HANDLERS", "1");
|
sdl3::hint::set("SDL_NO_SIGNAL_HANDLERS", "1");
|
||||||
sdl3::hint::set("SDL_JOYSTICK_THREAD", "1");
|
sdl3::hint::set("SDL_JOYSTICK_THREAD", "1");
|
||||||
|
// Let SDL's HIDAPI drivers open Valve Steam Controller / Steam Deck devices directly, so the
|
||||||
|
// paddles, both trackpads, and gyro arrive as first-class SDL gamepad inputs.
|
||||||
|
sdl3::hint::set("SDL_JOYSTICK_HIDAPI_STEAMDECK", "1");
|
||||||
|
sdl3::hint::set("SDL_JOYSTICK_HIDAPI_STEAM", "1");
|
||||||
let sdl = sdl3::init().map_err(|e| e.to_string())?;
|
let sdl = sdl3::init().map_err(|e| e.to_string())?;
|
||||||
let subsystem = sdl.gamepad().map_err(|e| e.to_string())?;
|
let subsystem = sdl.gamepad().map_err(|e| e.to_string())?;
|
||||||
let mut pump = sdl.event_pump().map_err(|e| e.to_string())?;
|
let mut pump = sdl.event_pump().map_err(|e| e.to_string())?;
|
||||||
@@ -317,6 +413,7 @@ fn run(
|
|||||||
attached: None,
|
attached: None,
|
||||||
last_axis: [i32::MIN; 6],
|
last_axis: [i32::MIN; 6],
|
||||||
held_buttons: Vec::new(),
|
held_buttons: Vec::new(),
|
||||||
|
held_touches: std::collections::HashSet::new(),
|
||||||
last_accel: [0; 3],
|
last_accel: [0; 3],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -426,9 +523,11 @@ fn run(
|
|||||||
send(w.attached.as_ref().unwrap(), InputKind::GamepadAxis, id, v);
|
send(w.attached.as_ref().unwrap(), InputKind::GamepadAxis, id, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DualSense touchpad → the rich-input plane, normalized 0..=65535.
|
// Touchpad contacts → the rich-input plane. One pad (DualSense) keeps the legacy
|
||||||
|
// `Touchpad`; two pads (Steam Deck / Steam Controller) send `TouchpadEx` per surface.
|
||||||
Event::ControllerTouchpadDown {
|
Event::ControllerTouchpadDown {
|
||||||
which,
|
which,
|
||||||
|
touchpad,
|
||||||
finger,
|
finger,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@@ -436,41 +535,23 @@ fn run(
|
|||||||
}
|
}
|
||||||
| Event::ControllerTouchpadMotion {
|
| Event::ControllerTouchpadMotion {
|
||||||
which,
|
which,
|
||||||
|
touchpad,
|
||||||
finger,
|
finger,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
..
|
..
|
||||||
} if active == Some(which) && w.attached.is_some() => {
|
} if active == Some(which) && w.attached.is_some() => {
|
||||||
let _ = w
|
w.forward_touch(which, touchpad as u32, finger as u8, x, y, true);
|
||||||
.attached
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.send_rich_input(RichInput::Touchpad {
|
|
||||||
pad: 0,
|
|
||||||
finger: finger as u8,
|
|
||||||
active: true,
|
|
||||||
x: (x.clamp(0.0, 1.0) * 65535.0) as u16,
|
|
||||||
y: (y.clamp(0.0, 1.0) * 65535.0) as u16,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Event::ControllerTouchpadUp {
|
Event::ControllerTouchpadUp {
|
||||||
which,
|
which,
|
||||||
|
touchpad,
|
||||||
finger,
|
finger,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
..
|
..
|
||||||
} if active == Some(which) && w.attached.is_some() => {
|
} if active == Some(which) && w.attached.is_some() => {
|
||||||
let _ = w
|
w.forward_touch(which, touchpad as u32, finger as u8, x, y, false);
|
||||||
.attached
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.send_rich_input(RichInput::Touchpad {
|
|
||||||
pad: 0,
|
|
||||||
finger: finger as u8,
|
|
||||||
active: false,
|
|
||||||
x: (x.clamp(0.0, 1.0) * 65535.0) as u16,
|
|
||||||
y: (y.clamp(0.0, 1.0) * 65535.0) as u16,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Motion: accel events update the cache; each gyro event ships a sample (the
|
// Motion: accel events update the cache; each gyro event ships a sample (the
|
||||||
// DualSense reports both at ~250 Hz). Scale convention shared with the other
|
// DualSense reports both at ~250 Hz). Scale convention shared with the other
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
# Rich Steam Controller & Steam Deck support
|
# Rich Steam Controller & Steam Deck support
|
||||||
|
|
||||||
> **Status:** **M0–M3 GREEN — virtual Deck binds + byte-exact + wired backend + the rich wire,
|
> **Status:** **M0–M3 GREEN + M4 desktop-capture done (2026-06-29).** The host side is complete:
|
||||||
> on-box (2026-06-29).** The greenfield virtual `hid-steam` device works end-to-end as a selectable
|
> the virtual `hid-steam` Deck binds, is byte-exact, is a wired backend (`PUNKTFUNK_GAMEPAD=
|
||||||
> host gamepad backend (`PUNKTFUNK_GAMEPAD=steamdeck`), and the protocol now carries the rich Steam
|
> steamdeck`), and the protocol carries the rich Steam inputs. The **Linux + Windows SDL clients now
|
||||||
> inputs (back buttons + second trackpad). Next: M4 (client capture — SDL Steam hints, paddles, 2nd
|
> capture + send** them. Remaining M4: the Decky Disable-Steam-Input UX, Apple/Android parity, and
|
||||||
> touchpad, the Decky Disable-Steam-Input UX, + the C-ABI `PunktfunkRichInputEx`/`send_rich_input2`
|
> the C-ABI `PunktfunkRichInputEx`/`send_rich_input2` (Apple/embedder send path).
|
||||||
> for the Apple/embedder send path). The Steam analogue of the shipped virtual DualSense.
|
>
|
||||||
|
> **M4 (desktop client capture) result:** `clients/{linux,windows}/src/gamepad.rs` (the SDL services)
|
||||||
|
> now: set the SDL HIDAPI Steam hints (`SDL_JOYSTICK_HIDAPI_STEAMDECK`/`_STEAM`) so SDL opens Valve
|
||||||
|
> devices directly; detect the Deck/SC by VID/PID (`0x28DE` + `0x1205`/`0x1102`/`0x1142`) →
|
||||||
|
> `GamepadPref::SteamDeck`; map the SDL paddle + Misc1 buttons → the `BTN_PADDLE1..4`/`BTN_MISC1`
|
||||||
|
> wire bits; and route a **second** touchpad → `RichInput::TouchpadEx` (SDL touchpad 0 = left →
|
||||||
|
> surface 1, 1 = right → surface 2, signed coords) while a single touchpad keeps the legacy
|
||||||
|
> `Touchpad`. Held touchpad contacts are now tracked per `(surface,finger)` and lifted on pad
|
||||||
|
> switch/detach. Sensor (gyro/accel) capture was already generic. Linux client builds + clippy clean;
|
||||||
|
> Windows is a near-verbatim mirror (windows CI compiles it). **Caveat:** on a Deck in Game Mode,
|
||||||
|
> Steam Input still holds the device — the user must disable Steam Input for the client (the Decky UX,
|
||||||
|
> next); on a desktop client (or a Deck with Steam Input off) the hints just work.
|
||||||
>
|
>
|
||||||
> **M3 result (protocol / ABI wire, on-box):** strictly additive + forward-compatible (§5).
|
> **M3 result (protocol / ABI wire, on-box):** strictly additive + forward-compatible (§5).
|
||||||
> Core: back-button bits `BTN_PADDLE1..4` + `BTN_MISC1` (in Moonlight's `buttonFlags2<<16`
|
> Core: back-button bits `BTN_PADDLE1..4` + `BTN_MISC1` (in Moonlight's `buttonFlags2<<16`
|
||||||
|
|||||||
Reference in New Issue
Block a user