feat(gamepad): add virtual Xbox One/Series + DualShock 4 pad types
Extends virtual-controller support beyond Xbox 360 + DualSense. Goal: a physical Xbox One or PS4 pad on the client gets a near-native matching virtual pad on the host, auto-resolved from the controller type. Protocol/core: - GamepadPref gains XboxOne (wire 3) + DualShock4 (wire 4); to_u8/from_u8/ from_name/as_str + C ABI PUNKTFUNK_GAMEPAD_XBOXONE/_DUALSHOCK4 constants (compile-time guard ties them to the enum). Single-byte wire form is unchanged, so it's forward-compatible (older peers degrade to Auto). Host (Linux): - New UHID DualShock 4 backend (inject/dualshock4.rs) bound by hid-playstation: lightbar, touchpad, motion, rumble — DualSense minus adaptive triggers / player LEDs / mute. Reuses the DualSense pure state + button mapping; only the report byte layout, the real-DS4 HID descriptor, the GET_REPORT handshake (0x12 MAC mandatory; 0x02 calibration; 0xa3 firmware) and the touchpad resolution (1920x942) differ. Touchpad/motion ride the existing 0xCC plane, lightbar the 0xCD Led plane (deduped); rumble the universal 0xCA plane. - Xbox One/Series is the uinput Xbox-360 backend parameterized with the One S USB identity (045e:02ea) for matching glyphs — XInput-identical otherwise. - PadBackend dispatch + resolver handle both; off Linux the UHID pads and One/Series fold into Xbox 360. Windows-host DS4 (ViGEm) deferred. Clients (auto-resolve physical pad -> virtual type, plus manual settings): - Linux/Windows (SDL3): SDL_GAMEPAD_TYPE_PS4 -> DualShock 4, _XBOXONE -> Xbox One; PadInfo carries the resolved pref; DS4 touchpad/motion capture + lightbar already type-agnostic. Linux settings combo + label updated. - Apple (GameController): GCDualShockGamepad/GCXboxGamepad detection, DS4 touchpad capture, settings picker entries. - Android (Kotlin): InputDevice VID/PID auto-detect (matching the other clients) + settings entries. - probe: --gamepad help/aliases. Also hardens the Android JNI boundary: wrap the teardown + poll-thread shims in catch_unwind so a panic degrades to a logged no-op instead of aborting the app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -687,6 +687,16 @@ pub const PUNKTFUNK_GAMEPAD_XBOX360: u32 = 1;
|
||||
/// feedback arrives on the HID-output plane ([`punktfunk_connection_next_hidout`]). Honored
|
||||
/// only where available (Linux hosts); otherwise the host falls back to X-Box 360.
|
||||
pub const PUNKTFUNK_GAMEPAD_DUALSENSE: u32 = 2;
|
||||
/// uinput X-Box One / Series pad — the X-Box 360 backend with the One/Series USB identity, so
|
||||
/// games show One/Series glyphs. XInput-identical to `XBOX360` otherwise (no game-visible gain;
|
||||
/// impulse-trigger rumble is unreachable through a virtual pad). Useful for glyph-matching a
|
||||
/// physical X-Box One/Series controller on the client.
|
||||
pub const PUNKTFUNK_GAMEPAD_XBOXONE: u32 = 3;
|
||||
/// UHID DualShock 4 (kernel `hid-playstation` ≥ 6.2): lightbar, touchpad, motion, rumble — the
|
||||
/// touchpad/motion arrive over the rich-input plane and lightbar over the HID-output plane, like
|
||||
/// DualSense (minus adaptive triggers / player LEDs / mute). Honored only where available (Linux
|
||||
/// hosts); otherwise the host falls back to X-Box 360.
|
||||
pub const PUNKTFUNK_GAMEPAD_DUALSHOCK4: u32 = 4;
|
||||
|
||||
/// Connect to a `punktfunk/1` host and start a session at `width`x`height`@`refresh_hz`.
|
||||
/// Blocks up to `timeout_ms` for the handshake. Returns NULL on failure. Equivalent to
|
||||
@@ -706,6 +716,16 @@ const _: () = {
|
||||
assert!(PUNKTFUNK_VIDEO_CAP_HDR == crate::quic::VIDEO_CAP_HDR);
|
||||
};
|
||||
|
||||
// Keep the ABI gamepad constants in lockstep with the wire enum (compile-time guard against drift).
|
||||
const _: () = {
|
||||
use crate::config::GamepadPref;
|
||||
assert!(PUNKTFUNK_GAMEPAD_AUTO == GamepadPref::Auto.to_u8() as u32);
|
||||
assert!(PUNKTFUNK_GAMEPAD_XBOX360 == GamepadPref::Xbox360.to_u8() as u32);
|
||||
assert!(PUNKTFUNK_GAMEPAD_DUALSENSE == GamepadPref::DualSense.to_u8() as u32);
|
||||
assert!(PUNKTFUNK_GAMEPAD_XBOXONE == GamepadPref::XboxOne.to_u8() as u32);
|
||||
assert!(PUNKTFUNK_GAMEPAD_DUALSHOCK4 == GamepadPref::DualShock4.to_u8() as u32);
|
||||
};
|
||||
|
||||
/// Trust: `pin_sha256` (NULL or 32 bytes) is the expected SHA-256 fingerprint of the host's
|
||||
/// certificate — a mismatching host is rejected. NULL = trust on first use; persist the
|
||||
/// fingerprint written to `observed_sha256_out` (NULL or 32 bytes, filled on success) and
|
||||
|
||||
@@ -135,10 +135,10 @@ impl CompositorPref {
|
||||
/// Sent in [`Hello`](crate::quic::Hello) as a *preference* and echoed back — resolved to the
|
||||
/// backend actually chosen — in [`Welcome`](crate::quic::Welcome). `Auto` (the default) lets the
|
||||
/// host decide (its `PUNKTFUNK_GAMEPAD` env var, else X-Box 360). A concrete preference is
|
||||
/// honored only if that backend is available on the host (DualSense needs Linux UHID); 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`), appended to `Hello`/`Welcome` — older peers
|
||||
/// simply omit/ignore it.
|
||||
/// 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
|
||||
/// byte (`0 = Auto`, `1 = Xbox360`, `2 = DualSense`, `3 = XboxOne`, `4 = DualShock4`), appended to
|
||||
/// `Hello`/`Welcome` — older peers simply omit/ignore it (an unknown byte degrades to `Auto`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum GamepadPref {
|
||||
/// Let the host pick (its `PUNKTFUNK_GAMEPAD` env var, else X-Box 360).
|
||||
@@ -148,15 +148,24 @@ pub enum GamepadPref {
|
||||
Xbox360,
|
||||
/// UHID DualSense (kernel `hid-playstation`) — adaptive triggers, lightbar, touchpad, motion.
|
||||
DualSense,
|
||||
/// uinput X-Box One / Series pad — the X-Box 360 backend with the One/Series USB identity
|
||||
/// (VID/PID/name), so games show One/Series glyphs. XInput-identical otherwise (impulse-trigger
|
||||
/// rumble is unreachable through any virtual pad, so there's no game-visible gain over `Xbox360`).
|
||||
XboxOne,
|
||||
/// UHID DualShock 4 (kernel `hid-playstation`, ≥ 6.2) — lightbar, touchpad, motion, rumble. Like
|
||||
/// `DualSense` minus adaptive triggers / player LEDs / mute. Needs Linux UHID on the host.
|
||||
DualShock4,
|
||||
}
|
||||
|
||||
impl GamepadPref {
|
||||
/// Wire byte. `0 = Auto`, `1 = Xbox360`, `2 = DualSense`.
|
||||
pub fn to_u8(self) -> u8 {
|
||||
/// Wire byte. `0 = Auto`, `1 = Xbox360`, `2 = DualSense`, `3 = XboxOne`, `4 = DualShock4`.
|
||||
pub const fn to_u8(self) -> u8 {
|
||||
match self {
|
||||
GamepadPref::Auto => 0,
|
||||
GamepadPref::Xbox360 => 1,
|
||||
GamepadPref::DualSense => 2,
|
||||
GamepadPref::XboxOne => 3,
|
||||
GamepadPref::DualShock4 => 4,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +175,8 @@ impl GamepadPref {
|
||||
match v {
|
||||
1 => GamepadPref::Xbox360,
|
||||
2 => GamepadPref::DualSense,
|
||||
3 => GamepadPref::XboxOne,
|
||||
4 => GamepadPref::DualShock4,
|
||||
_ => GamepadPref::Auto,
|
||||
}
|
||||
}
|
||||
@@ -177,16 +188,23 @@ impl GamepadPref {
|
||||
"auto" | "default" => GamepadPref::Auto,
|
||||
"xbox" | "xbox360" | "x360" | "uinput" => GamepadPref::Xbox360,
|
||||
"dualsense" | "ds" | "ps5" => GamepadPref::DualSense,
|
||||
"xboxone" | "xbox-one" | "xone" | "xbox1" | "series" | "xboxseries" => {
|
||||
GamepadPref::XboxOne
|
||||
}
|
||||
"dualshock4" | "dualshock" | "ds4" | "ps4" => GamepadPref::DualShock4,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Canonical lowercase identifier (`"auto"`, `"xbox360"`, `"dualsense"`).
|
||||
/// Canonical lowercase identifier (`"auto"`, `"xbox360"`, `"dualsense"`, `"xboxone"`,
|
||||
/// `"dualshock4"`).
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
GamepadPref::Auto => "auto",
|
||||
GamepadPref::Xbox360 => "xbox360",
|
||||
GamepadPref::DualSense => "dualsense",
|
||||
GamepadPref::XboxOne => "xboxone",
|
||||
GamepadPref::DualShock4 => "dualshock4",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1883,13 +1883,25 @@ mod tests {
|
||||
GamepadPref::Auto,
|
||||
GamepadPref::Xbox360,
|
||||
GamepadPref::DualSense,
|
||||
GamepadPref::XboxOne,
|
||||
GamepadPref::DualShock4,
|
||||
] {
|
||||
assert_eq!(GamepadPref::from_u8(p.to_u8()), p);
|
||||
assert_eq!(GamepadPref::from_name(p.as_str()), Some(p));
|
||||
}
|
||||
// Distinct wire bytes (forward-compat with peers that only know 0..=2).
|
||||
assert_eq!(GamepadPref::XboxOne.to_u8(), 3);
|
||||
assert_eq!(GamepadPref::DualShock4.to_u8(), 4);
|
||||
// Aliases + unknowns.
|
||||
assert_eq!(GamepadPref::from_name("PS5"), Some(GamepadPref::DualSense));
|
||||
assert_eq!(GamepadPref::from_name("x360"), Some(GamepadPref::Xbox360));
|
||||
assert_eq!(GamepadPref::from_name("ps4"), Some(GamepadPref::DualShock4));
|
||||
assert_eq!(GamepadPref::from_name("DS4"), Some(GamepadPref::DualShock4));
|
||||
assert_eq!(
|
||||
GamepadPref::from_name("xbox-one"),
|
||||
Some(GamepadPref::XboxOne)
|
||||
);
|
||||
assert_eq!(GamepadPref::from_name("series"), Some(GamepadPref::XboxOne));
|
||||
assert_eq!(GamepadPref::from_name("nope"), None);
|
||||
// Unknown wire byte degrades to Auto (forward-compatible).
|
||||
assert_eq!(GamepadPref::from_u8(200), GamepadPref::Auto);
|
||||
|
||||
Reference in New Issue
Block a user