feat(gamepad): DualSense Edge backend — Linux UHID + Windows UMDF (N1)

The plain-DualSense transport + report codec under the Edge USB identity
(054C:0DF2, verbatim 389-byte real-device descriptor cross-checked against
the raw usbmon capture + hhd's production virtual Edge), so the wire back
grips (BTN_PADDLE1..4: Deck L4/L5/R4/R5, Elite P1-P4) land on the Edge's
NATIVE buttons[2] bits instead of the fold/drop policy: PADDLE1/2 -> the
right/left back buttons, PADDLE3/4 -> the right/left Fn buttons (kernel
BTN_TRIGGER_HAPPY1..4 on >= 7.2; SDL/Steam read hidraw on any kernel).

- proto: Edge descriptor + btn2 bits + edge_paddle_bits(), pinned against
  hid-playstation DS_EDGE_BUTTONS_* and SDL_hidapi_ps5 (tests).
- Linux: DsUhidIdentity parameterizes the UHID create; DsEdgeLinuxProto /
  DualSenseEdgeManager. Headless-validated on .21 (7.1): driver=playstation
  binds 0DF2, all 4 input devices created, probe lightbar/player-LED
  feedback round-trips; dualsense-test grew --edge (cycles all 4 paddles).
- Windows: UMDF driver serves device_type=2 (Edge descriptor/attrs/strings,
  DS feature blobs); WinDsIdentity parameterizes the SwDevice profile +
  devtype stamp; DsEdgeWinProto / DualSenseEdgeWindowsManager; INF gains
  pf_dualsenseedge. Driver change => resign + reinstall before on-glass.
- Router: DualSenseEdge arms in route_handle/apply_rich/pump/heartbeat;
  pick_gamepad folds Edge -> itself on linux||windows; degrade_if_no_uhid
  covers it.
- Client (SDL): 054C:0DF2 declares DualSenseEdge (no distinct SDL type);
  Edge physical pads take the raw DS5 effects path; console-UI glyphs =
  Shapes. Apple/Android pickers follow separately.

Verified: .21 clippy -D warnings + 292/0 host tests + on-box UHID bind
smoke; .133 clippy pending in this push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 10:49:31 +02:00
parent 45bde370e2
commit 1830e095f8
13 changed files with 618 additions and 119 deletions
+68 -12
View File
@@ -1752,7 +1752,8 @@ const INJECTOR_REOPEN_BACKOFF: std::time::Duration = std::time::Duration::from_s
///
/// - Xbox 360 / One — uinput on Linux ([`GamepadManager`](crate::inject::gamepad::GamepadManager),
/// two identities), the XUSB companion driver (classic XInput) on Windows.
/// - DualSense / DualShock 4 — Linux UHID `hid-playstation`, or the Windows UMDF minidriver.
/// - DualSense / DualSense Edge / DualShock 4 — Linux UHID `hid-playstation`, or the Windows UMDF
/// minidriver (device-type 0/2/1).
/// - Steam Deck — Linux UHID `hid-steam`.
///
/// [`resolve_pad_kind`] folds any kind a platform can't build into one it can, so this never
@@ -1771,12 +1772,16 @@ struct Pads {
#[cfg(target_os = "linux")]
dualsense: Option<crate::inject::dualsense::DualSenseManager>,
#[cfg(target_os = "linux")]
dualsense_edge: Option<crate::inject::dualsense::DualSenseEdgeManager>,
#[cfg(target_os = "linux")]
dualshock4: Option<crate::inject::dualshock4::DualShock4Manager>,
#[cfg(target_os = "linux")]
steamdeck: Option<crate::inject::steam_controller::SteamControllerManager>,
#[cfg(target_os = "windows")]
dualsense_win: Option<crate::inject::dualsense_windows::DualSenseWindowsManager>,
#[cfg(target_os = "windows")]
dualsense_edge_win: Option<crate::inject::dualsense_edge_windows::DualSenseEdgeWindowsManager>,
#[cfg(target_os = "windows")]
dualshock4_win: Option<crate::inject::dualshock4_windows::DualShock4WindowsManager>,
}
@@ -1798,12 +1803,16 @@ impl Pads {
#[cfg(target_os = "linux")]
dualsense: None,
#[cfg(target_os = "linux")]
dualsense_edge: None,
#[cfg(target_os = "linux")]
dualshock4: None,
#[cfg(target_os = "linux")]
steamdeck: None,
#[cfg(target_os = "windows")]
dualsense_win: None,
#[cfg(target_os = "windows")]
dualsense_edge_win: None,
#[cfg(target_os = "windows")]
dualshock4_win: None,
}
}
@@ -1855,6 +1864,11 @@ impl Pads {
.get_or_insert_with(crate::inject::dualsense::DualSenseManager::new)
.handle(ev),
#[cfg(target_os = "linux")]
GamepadPref::DualSenseEdge => self
.dualsense_edge
.get_or_insert_with(crate::inject::dualsense::DualSenseEdgeManager::new)
.handle(ev),
#[cfg(target_os = "linux")]
GamepadPref::DualShock4 => self
.dualshock4
.get_or_insert_with(crate::inject::dualshock4::DualShock4Manager::new)
@@ -1879,6 +1893,13 @@ impl Pads {
.get_or_insert_with(crate::inject::dualsense_windows::DualSenseWindowsManager::new)
.handle(ev),
#[cfg(target_os = "windows")]
GamepadPref::DualSenseEdge => self
.dualsense_edge_win
.get_or_insert_with(
crate::inject::dualsense_edge_windows::DualSenseEdgeWindowsManager::new,
)
.handle(ev),
#[cfg(target_os = "windows")]
GamepadPref::DualShock4 => self
.dualshock4_win
.get_or_insert_with(
@@ -1920,6 +1941,12 @@ impl Pads {
}
}
#[cfg(target_os = "linux")]
GamepadPref::DualSenseEdge => {
if let Some(m) = &mut self.dualsense_edge {
m.apply_rich(rich)
}
}
#[cfg(target_os = "linux")]
GamepadPref::DualShock4 => {
if let Some(m) = &mut self.dualshock4 {
m.apply_rich(rich)
@@ -1938,6 +1965,12 @@ impl Pads {
}
}
#[cfg(target_os = "windows")]
GamepadPref::DualSenseEdge => {
if let Some(m) = &mut self.dualsense_edge_win {
m.apply_rich(rich)
}
}
#[cfg(target_os = "windows")]
GamepadPref::DualShock4 => {
if let Some(m) = &mut self.dualshock4_win {
m.apply_rich(rich)
@@ -1967,6 +2000,9 @@ impl Pads {
if let Some(m) = &mut self.dualsense {
m.pump(&mut rumble, &mut hidout);
}
if let Some(m) = &mut self.dualsense_edge {
m.pump(&mut rumble, &mut hidout);
}
if let Some(m) = &mut self.dualshock4 {
m.pump(&mut rumble, &mut hidout);
}
@@ -1979,6 +2015,9 @@ impl Pads {
if let Some(m) = &mut self.dualsense_win {
m.pump(&mut rumble, &mut hidout);
}
if let Some(m) = &mut self.dualsense_edge_win {
m.pump(&mut rumble, &mut hidout);
}
if let Some(m) = &mut self.dualshock4_win {
m.pump(&mut rumble, &mut hidout);
}
@@ -1996,6 +2035,9 @@ impl Pads {
if let Some(m) = &mut self.dualsense {
m.heartbeat(gap);
}
if let Some(m) = &mut self.dualsense_edge {
m.heartbeat(gap);
}
if let Some(m) = &mut self.dualshock4 {
m.heartbeat(gap);
}
@@ -2009,6 +2051,9 @@ impl Pads {
if let Some(m) = &mut self.dualsense_win {
m.heartbeat(gap);
}
if let Some(m) = &mut self.dualsense_edge_win {
m.heartbeat(gap);
}
if let Some(m) = &mut self.dualshock4_win {
m.heartbeat(gap);
}
@@ -2700,11 +2745,10 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
// DualSense touchpad left/right per DsState::apply_rich). Folding to Xbox360 dropped
// all of that silently.
GamepadPref::SteamDeck if windows => GamepadPref::DualSense,
// DualSense Edge: until its backend lands (N1), fold to the plain DualSense wherever
// that exists — it keeps the rich planes (touchpad/motion/lightbar) alive; only the
// back-button bits go through the paddle fold. NOT Xbox360 (that would drop the rich
// planes entirely, the SteamDeck-on-Windows lesson above).
GamepadPref::DualSenseEdge if linux || windows => GamepadPref::DualSense,
// DualSense Edge: Linux UHID hid-playstation / Windows UMDF (device-type 2) — the plain
// DualSense plus native back/Fn buttons, so the wire paddles stop hitting the fold/drop
// policy. Degrades to Xbox360 elsewhere like its siblings.
GamepadPref::DualSenseEdge if linux || windows => GamepadPref::DualSenseEdge,
// Switch Pro: no backend yet (N2) — falls through to Xbox360 below.
_ => GamepadPref::Xbox360,
}
@@ -2718,7 +2762,10 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
fn degrade_if_no_uhid(chosen: GamepadPref) -> GamepadPref {
let needs_uhid = matches!(
chosen,
GamepadPref::DualSense | GamepadPref::DualShock4 | GamepadPref::SteamDeck
GamepadPref::DualSense
| GamepadPref::DualSenseEdge
| GamepadPref::DualShock4
| GamepadPref::SteamDeck
);
if needs_uhid
&& std::fs::OpenOptions::new()
@@ -5286,11 +5333,20 @@ mod tests {
assert_eq!(pick_gamepad(Auto, Some("deck"), false, true), DualSense);
assert_eq!(pick_gamepad(SteamDeck, None, false, false), Xbox360);
// DualSense Edge: folds to the plain DualSense (keeps the rich planes) until the Edge
// backend lands (gamepad-new-types N1); Xbox360 where no DualSense backend exists.
assert_eq!(pick_gamepad(DualSenseEdge, None, true, false), DualSense);
assert_eq!(pick_gamepad(DualSenseEdge, None, false, true), DualSense);
assert_eq!(pick_gamepad(Auto, Some("edge"), true, false), DualSense);
// DualSense Edge: native on Linux (UHID) AND Windows (UMDF device-type 2); Xbox360
// elsewhere.
assert_eq!(
pick_gamepad(DualSenseEdge, None, true, false),
DualSenseEdge
);
assert_eq!(
pick_gamepad(DualSenseEdge, None, false, true),
DualSenseEdge
);
assert_eq!(
pick_gamepad(Auto, Some("edge"), true, false),
DualSenseEdge
);
assert_eq!(pick_gamepad(DualSenseEdge, None, false, false), Xbox360);
// Switch Pro: no backend yet (gamepad-new-types N2) — folds to Xbox360 everywhere.
assert_eq!(pick_gamepad(SwitchPro, None, true, false), Xbox360);