feat(gamepad): GamepadPref wire bytes for DualSense Edge (7) + Switch Pro (8)

Phase 0 of gamepad-new-types: the two new kinds exist on the wire (enum,
to_u8/from_u8/from_name/as_str, C-ABI constants + header), and pick_gamepad
folds them to the closest EXISTING backend until their own backends land —
DualSenseEdge -> DualSense (keeps the rich planes; only the paddles go
through the fold policy), SwitchPro -> Xbox360. Wire round-trip pinned
0..=8 + unknown->Auto; fold table extended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 10:20:24 +02:00
parent 57d89217fb
commit 45bde370e2
5 changed files with 84 additions and 7 deletions
+8
View File
@@ -889,6 +889,12 @@ pub const PUNKTFUNK_GAMEPAD_STEAMCONTROLLER: u32 = 5;
/// four back grips, a right trackpad, and the IMU; re-grabbed by Steam Input with native glyphs when /// four back grips, a right trackpad, and the IMU; re-grabbed by Steam Input with native glyphs when
/// Steam runs on the host. Honored only where available (Linux hosts); else folds to X-Box 360. /// Steam runs on the host. Honored only where available (Linux hosts); else folds to X-Box 360.
pub const PUNKTFUNK_GAMEPAD_STEAMDECK: u32 = 6; pub const PUNKTFUNK_GAMEPAD_STEAMDECK: u32 = 6;
/// DualSense Edge (Sony `054C:0DF2`): the DualSense plus two back buttons + two Fn buttons, so a
/// client's back paddles land on native slots. Folds to `DUALSENSE` until its backend lands.
pub const PUNKTFUNK_GAMEPAD_DUALSENSEEDGE: u32 = 7;
/// Nintendo Switch Pro Controller (Nintendo `057E:2009`, kernel `hid-nintendo`): Nintendo glyphs +
/// positional layout, gyro/accel, HD rumble. Folds to `XBOX360` until its backend lands.
pub const PUNKTFUNK_GAMEPAD_SWITCHPRO: u32 = 8;
/// Extended `InputEvent` gamepad button bits for embedders building raw events: the four back grips /// Extended `InputEvent` gamepad button bits for embedders building raw events: the four back grips
/// (Steam L4/L5/R4/R5 ≙ Xbox-Elite P1P4) + the misc/capture button, in Moonlight's /// (Steam L4/L5/R4/R5 ≙ Xbox-Elite P1P4) + the misc/capture button, in Moonlight's
@@ -945,6 +951,8 @@ const _: () = {
assert!(PUNKTFUNK_GAMEPAD_DUALSHOCK4 == GamepadPref::DualShock4.to_u8() as u32); assert!(PUNKTFUNK_GAMEPAD_DUALSHOCK4 == GamepadPref::DualShock4.to_u8() as u32);
assert!(PUNKTFUNK_GAMEPAD_STEAMCONTROLLER == GamepadPref::SteamController.to_u8() as u32); assert!(PUNKTFUNK_GAMEPAD_STEAMCONTROLLER == GamepadPref::SteamController.to_u8() as u32);
assert!(PUNKTFUNK_GAMEPAD_STEAMDECK == GamepadPref::SteamDeck.to_u8() as u32); assert!(PUNKTFUNK_GAMEPAD_STEAMDECK == GamepadPref::SteamDeck.to_u8() as u32);
assert!(PUNKTFUNK_GAMEPAD_DUALSENSEEDGE == GamepadPref::DualSenseEdge.to_u8() as u32);
assert!(PUNKTFUNK_GAMEPAD_SWITCHPRO == GamepadPref::SwitchPro.to_u8() as u32);
// Extended button bits mirror the wire `input::gamepad` constants. // Extended button bits mirror the wire `input::gamepad` constants.
assert!(PUNKTFUNK_GAMEPAD_BTN_PADDLE1 == g::BTN_PADDLE1); assert!(PUNKTFUNK_GAMEPAD_BTN_PADDLE1 == g::BTN_PADDLE1);
assert!(PUNKTFUNK_GAMEPAD_BTN_PADDLE2 == g::BTN_PADDLE2); assert!(PUNKTFUNK_GAMEPAD_BTN_PADDLE2 == g::BTN_PADDLE2);
+21 -4
View File
@@ -138,8 +138,8 @@ impl CompositorPref {
/// honored only if that backend is available on the host (DualSense / DualShock 4 need Linux UHID); /// honored only if that backend is available on the host (DualSense / DualShock 4 need Linux UHID);
/// otherwise the host falls back and reports the real choice in `Welcome`. The wire form is a single /// otherwise the host falls back and reports the real choice in `Welcome`. The wire form is a single
/// byte (`0 = Auto`, `1 = Xbox360`, `2 = DualSense`, `3 = XboxOne`, `4 = DualShock4`, /// byte (`0 = Auto`, `1 = Xbox360`, `2 = DualSense`, `3 = XboxOne`, `4 = DualShock4`,
/// `5 = SteamController`, `6 = SteamDeck`), appended to `Hello`/`Welcome` — older peers simply /// `5 = SteamController`, `6 = SteamDeck`, `7 = DualSenseEdge`, `8 = SwitchPro`), appended to
/// omit/ignore it (an unknown byte degrades to `Auto`). /// `Hello`/`Welcome` — older peers simply omit/ignore it (an unknown byte degrades to `Auto`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum GamepadPref { pub enum GamepadPref {
/// Let the host pick (its `PUNKTFUNK_GAMEPAD` env var, else X-Box 360). /// Let the host pick (its `PUNKTFUNK_GAMEPAD` env var, else X-Box 360).
@@ -164,11 +164,18 @@ pub enum GamepadPref {
/// the four back grips (L4/L5/R4/R5), a right trackpad, and the IMU; re-grabbed by Steam Input /// the four back grips (L4/L5/R4/R5), a right trackpad, and the IMU; re-grabbed by Steam Input
/// with native glyphs when Steam runs on the host. Needs Linux UHID. /// with native glyphs when Steam runs on the host. Needs Linux UHID.
SteamDeck, SteamDeck,
/// DualSense Edge (Sony `054C:0DF2`, kernel `hid-playstation` ≥ 6.3 / Windows UMDF) — the
/// DualSense plus two back buttons + two Fn buttons, so a client's back paddles (Deck grips,
/// Elite P1P4) land on a native slot instead of the fold/drop policy.
DualSenseEdge,
/// Nintendo Switch Pro Controller (Nintendo `057E:2009`, kernel `hid-nintendo` ≥ 5.16) —
/// correct Nintendo glyphs + positional layout, gyro/accel, HD rumble back. Needs Linux UHID.
SwitchPro,
} }
impl GamepadPref { impl GamepadPref {
/// Wire byte. `0 = Auto`, `1 = Xbox360`, `2 = DualSense`, `3 = XboxOne`, `4 = DualShock4`, /// Wire byte. `0 = Auto`, `1 = Xbox360`, `2 = DualSense`, `3 = XboxOne`, `4 = DualShock4`,
/// `5 = SteamController`, `6 = SteamDeck`. /// `5 = SteamController`, `6 = SteamDeck`, `7 = DualSenseEdge`, `8 = SwitchPro`.
pub const fn to_u8(self) -> u8 { pub const fn to_u8(self) -> u8 {
match self { match self {
GamepadPref::Auto => 0, GamepadPref::Auto => 0,
@@ -178,6 +185,8 @@ impl GamepadPref {
GamepadPref::DualShock4 => 4, GamepadPref::DualShock4 => 4,
GamepadPref::SteamController => 5, GamepadPref::SteamController => 5,
GamepadPref::SteamDeck => 6, GamepadPref::SteamDeck => 6,
GamepadPref::DualSenseEdge => 7,
GamepadPref::SwitchPro => 8,
} }
} }
@@ -191,6 +200,8 @@ impl GamepadPref {
4 => GamepadPref::DualShock4, 4 => GamepadPref::DualShock4,
5 => GamepadPref::SteamController, 5 => GamepadPref::SteamController,
6 => GamepadPref::SteamDeck, 6 => GamepadPref::SteamDeck,
7 => GamepadPref::DualSenseEdge,
8 => GamepadPref::SwitchPro,
_ => GamepadPref::Auto, _ => GamepadPref::Auto,
} }
} }
@@ -208,12 +219,16 @@ impl GamepadPref {
"dualshock4" | "dualshock" | "ds4" | "ps4" => GamepadPref::DualShock4, "dualshock4" | "dualshock" | "ds4" | "ps4" => GamepadPref::DualShock4,
"steamdeck" | "steam-deck" | "deck" => GamepadPref::SteamDeck, "steamdeck" | "steam-deck" | "deck" => GamepadPref::SteamDeck,
"steamcontroller" | "steam-controller" | "steamcon" => GamepadPref::SteamController, "steamcontroller" | "steam-controller" | "steamcon" => GamepadPref::SteamController,
"dualsenseedge" | "dualsense-edge" | "edge" | "dsedge" => GamepadPref::DualSenseEdge,
"switchpro" | "switch-pro" | "switch" | "procontroller" | "pro-controller" => {
GamepadPref::SwitchPro
}
_ => return None, _ => return None,
}) })
} }
/// Canonical lowercase identifier (`"auto"`, `"xbox360"`, `"dualsense"`, `"xboxone"`, /// Canonical lowercase identifier (`"auto"`, `"xbox360"`, `"dualsense"`, `"xboxone"`,
/// `"dualshock4"`, `"steamcontroller"`, `"steamdeck"`). /// `"dualshock4"`, `"steamcontroller"`, `"steamdeck"`, `"dualsenseedge"`, `"switchpro"`).
pub fn as_str(self) -> &'static str { pub fn as_str(self) -> &'static str {
match self { match self {
GamepadPref::Auto => "auto", GamepadPref::Auto => "auto",
@@ -223,6 +238,8 @@ impl GamepadPref {
GamepadPref::DualShock4 => "dualshock4", GamepadPref::DualShock4 => "dualshock4",
GamepadPref::SteamController => "steamcontroller", GamepadPref::SteamController => "steamcontroller",
GamepadPref::SteamDeck => "steamdeck", GamepadPref::SteamDeck => "steamdeck",
GamepadPref::DualSenseEdge => "dualsenseedge",
GamepadPref::SwitchPro => "switchpro",
} }
} }
} }
+30 -3
View File
@@ -275,18 +275,45 @@ fn gamepad_pref_wire_and_names() {
GamepadPref::DualSense, GamepadPref::DualSense,
GamepadPref::XboxOne, GamepadPref::XboxOne,
GamepadPref::DualShock4, GamepadPref::DualShock4,
GamepadPref::SteamController,
GamepadPref::SteamDeck,
GamepadPref::DualSenseEdge,
GamepadPref::SwitchPro,
] { ] {
assert_eq!(GamepadPref::from_u8(p.to_u8()), p); assert_eq!(GamepadPref::from_u8(p.to_u8()), p);
assert_eq!(GamepadPref::from_name(p.as_str()), Some(p)); assert_eq!(GamepadPref::from_name(p.as_str()), Some(p));
} }
// Distinct wire bytes (forward-compat with peers that only know 0..=2). // Every wire byte 0..=8 is assigned, distinct, and pinned (forward-compat with peers
assert_eq!(GamepadPref::XboxOne.to_u8(), 3); // that only know a prefix of the range).
assert_eq!(GamepadPref::DualShock4.to_u8(), 4); for (v, p) in [
(0, GamepadPref::Auto),
(1, GamepadPref::Xbox360),
(2, GamepadPref::DualSense),
(3, GamepadPref::XboxOne),
(4, GamepadPref::DualShock4),
(5, GamepadPref::SteamController),
(6, GamepadPref::SteamDeck),
(7, GamepadPref::DualSenseEdge),
(8, GamepadPref::SwitchPro),
] {
assert_eq!(p.to_u8(), v);
assert_eq!(GamepadPref::from_u8(v), p);
}
// The next unassigned byte degrades to Auto today; assigning it later must update this.
assert_eq!(GamepadPref::from_u8(9), GamepadPref::Auto);
// Aliases + unknowns. // Aliases + unknowns.
assert_eq!(GamepadPref::from_name("PS5"), Some(GamepadPref::DualSense)); assert_eq!(GamepadPref::from_name("PS5"), Some(GamepadPref::DualSense));
assert_eq!(GamepadPref::from_name("x360"), Some(GamepadPref::Xbox360)); assert_eq!(GamepadPref::from_name("x360"), Some(GamepadPref::Xbox360));
assert_eq!(GamepadPref::from_name("ps4"), Some(GamepadPref::DualShock4)); assert_eq!(GamepadPref::from_name("ps4"), Some(GamepadPref::DualShock4));
assert_eq!(GamepadPref::from_name("DS4"), Some(GamepadPref::DualShock4)); assert_eq!(GamepadPref::from_name("DS4"), Some(GamepadPref::DualShock4));
assert_eq!(
GamepadPref::from_name("edge"),
Some(GamepadPref::DualSenseEdge)
);
assert_eq!(
GamepadPref::from_name("Switch-Pro"),
Some(GamepadPref::SwitchPro)
);
assert_eq!( assert_eq!(
GamepadPref::from_name("xbox-one"), GamepadPref::from_name("xbox-one"),
Some(GamepadPref::XboxOne) Some(GamepadPref::XboxOne)
+17
View File
@@ -2700,6 +2700,12 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
// DualSense touchpad left/right per DsState::apply_rich). Folding to Xbox360 dropped // DualSense touchpad left/right per DsState::apply_rich). Folding to Xbox360 dropped
// all of that silently. // all of that silently.
GamepadPref::SteamDeck if windows => GamepadPref::DualSense, 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,
// Switch Pro: no backend yet (N2) — falls through to Xbox360 below.
_ => GamepadPref::Xbox360, _ => GamepadPref::Xbox360,
} }
} }
@@ -5279,6 +5285,17 @@ mod tests {
assert_eq!(pick_gamepad(SteamDeck, None, false, true), DualSense); assert_eq!(pick_gamepad(SteamDeck, None, false, true), DualSense);
assert_eq!(pick_gamepad(Auto, Some("deck"), false, true), DualSense); assert_eq!(pick_gamepad(Auto, Some("deck"), false, true), DualSense);
assert_eq!(pick_gamepad(SteamDeck, None, false, false), Xbox360); 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);
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);
assert_eq!(pick_gamepad(Auto, Some("switchpro"), true, false), Xbox360);
assert_eq!(pick_gamepad(SwitchPro, None, false, true), Xbox360);
} }
#[test] #[test]
+8
View File
@@ -121,6 +121,14 @@
// Steam runs on the host. Honored only where available (Linux hosts); else folds to X-Box 360. // Steam runs on the host. Honored only where available (Linux hosts); else folds to X-Box 360.
#define PUNKTFUNK_GAMEPAD_STEAMDECK 6 #define PUNKTFUNK_GAMEPAD_STEAMDECK 6
// DualSense Edge (Sony `054C:0DF2`): the DualSense plus two back buttons + two Fn buttons, so a
// client's back paddles land on native slots. Folds to `DUALSENSE` until its backend lands.
#define PUNKTFUNK_GAMEPAD_DUALSENSEEDGE 7
// Nintendo Switch Pro Controller (Nintendo `057E:2009`, kernel `hid-nintendo`): Nintendo glyphs +
// positional layout, gyro/accel, HD rumble. Folds to `XBOX360` until its backend lands.
#define PUNKTFUNK_GAMEPAD_SWITCHPRO 8
// Extended `InputEvent` gamepad button bits for embedders building raw events: the four back grips // Extended `InputEvent` gamepad button bits for embedders building raw events: the four back grips
// (Steam L4/L5/R4/R5 ≙ Xbox-Elite P1P4) + the misc/capture button, in Moonlight's // (Steam L4/L5/R4/R5 ≙ Xbox-Elite P1P4) + the misc/capture button, in Moonlight's
// `buttonFlags2 << 16` namespace. Mirror `input::gamepad::BTN_PADDLE1..4` / `BTN_MISC1`. // `buttonFlags2 << 16` namespace. Mirror `input::gamepad::BTN_PADDLE1..4` / `BTN_MISC1`.