feat(gamepad): controller discovery + client-negotiated pad type + rich DualSense end to end
The Apple client grows full gamepad support and punktfunk/1 learns to negotiate the virtual pad type: - Protocol: Hello carries a GamepadPref byte (offset 21, the same trailing-byte back-compat pattern as the compositor; echoed resolved in Welcome at 54). Host precedence: explicit client choice > PUNKTFUNK_GAMEPAD env > Xbox 360, DualSense (UHID) only where available. ABI: punktfunk_connect_ex2 + punktfunk_connection_gamepad (connect_ex delegates; ABI_VERSION stays 2 — the trailing byte IS the compat mechanism). punktfunk-client-rs gets --gamepad. - Swift client: GamepadManager (app-lifetime discovery + selection — Settings lists every controller with capabilities/battery/"In use"; exactly ONE pad forwards as pad 0, auto = most recently connected, or pinned), GamepadCapture (snapshot-diff button/axis events, DualSense touchpad + ~250 Hz motion on the rich-input plane, held state released on switch/deactivate/stop), GamepadFeedback (rumble → CoreHaptics per-handle engines; lightbar → GCDeviceLight; player LEDs → playerIndex; adaptive-trigger blocks → the table-driven DualSenseTriggerEffect parser → GCDualSenseAdaptiveTrigger, exact for the 10-zone positional modes). The pad type auto-resolves from the physical controller at connect time, user-overridable in Settings. - Host DualSense fixes surfaced by adversarial review against hid-playstation / SDL / Nielk1 ground truth: input-report sensor/touch offsets were off by one (the kernel read garbage motion + phantom touches), the L2/R2 trigger blocks were swapped (the report is right-trigger-first), feedback now gates on the report's valid-flags (a plain rumble write no longer blanks lightbar/ triggers), and the touchpad rescale clamps to the advertised ABS_MT extents. - Tests: Hello/Welcome trailing-byte back-compat, pick_gamepad precedence, byte-exact input-report layout, valid-flag gating, per-mode trigger-parser table (incl. packed 3-bit zones), wire conversions, and a scripted loopback feedback burst (PUNKTFUNK_TEST_FEEDBACK=1) asserted through the xcframework on the rumble + HID-output planes. Validated: cargo test/clippy/fmt green on macOS + Linux (61 host tests), swift build/test green, test-loopback.sh green, tvOS/iOS targets compile. DualSense motion sign/scale is derived from the calibration blob, not yet live-verified (constants isolated in GamepadWire). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -621,6 +621,19 @@ pub const PUNKTFUNK_COMPOSITOR_MUTTER: u32 = 3;
|
||||
/// gamescope (spawned nested).
|
||||
pub const PUNKTFUNK_COMPOSITOR_GAMESCOPE: u32 = 4;
|
||||
|
||||
/// Gamepad-backend preference for [`punktfunk_connect_ex2`] (`gamepad` arg): which virtual pad
|
||||
/// the host creates for this session's controllers. Precedence host-side: an explicit client
|
||||
/// choice > the host's `PUNKTFUNK_GAMEPAD` env var > X-Box 360. `AUTO` (or any unrecognized
|
||||
/// value) = host decides. The resolved choice is echoed over the protocol (`Welcome`) and
|
||||
/// readable via [`punktfunk_connection_gamepad`].
|
||||
pub const PUNKTFUNK_GAMEPAD_AUTO: u32 = 0;
|
||||
/// uinput X-Box 360 pad (the universal default — every game speaks XInput).
|
||||
pub const PUNKTFUNK_GAMEPAD_XBOX360: u32 = 1;
|
||||
/// UHID DualSense (kernel `hid-playstation`): adaptive triggers, lightbar, touchpad, motion —
|
||||
/// 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;
|
||||
|
||||
/// 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
|
||||
/// [`punktfunk_connect_ex`] with `compositor = PUNKTFUNK_COMPOSITOR_AUTO`.
|
||||
@@ -674,6 +687,7 @@ pub unsafe extern "C" fn punktfunk_connect(
|
||||
/// the `PUNKTFUNK_COMPOSITOR_*` values). `PUNKTFUNK_COMPOSITOR_AUTO` (or any unrecognized value)
|
||||
/// lets the host decide; a concrete value is honored only if available, else the host falls back
|
||||
/// to auto-detect. The resolved choice is logged host-side and returned over the protocol.
|
||||
/// Equivalent to [`punktfunk_connect_ex2`] with `gamepad = PUNKTFUNK_GAMEPAD_AUTO`.
|
||||
///
|
||||
/// # Safety
|
||||
/// Same as [`punktfunk_connect`].
|
||||
@@ -691,6 +705,49 @@ pub unsafe extern "C" fn punktfunk_connect_ex(
|
||||
client_cert_pem: *const std::os::raw::c_char,
|
||||
client_key_pem: *const std::os::raw::c_char,
|
||||
timeout_ms: u32,
|
||||
) -> *mut PunktfunkConnection {
|
||||
unsafe {
|
||||
punktfunk_connect_ex2(
|
||||
host,
|
||||
port,
|
||||
width,
|
||||
height,
|
||||
refresh_hz,
|
||||
compositor,
|
||||
PUNKTFUNK_GAMEPAD_AUTO,
|
||||
pin_sha256,
|
||||
observed_sha256_out,
|
||||
client_cert_pem,
|
||||
client_key_pem,
|
||||
timeout_ms,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`punktfunk_connect_ex`], but additionally requests which virtual `gamepad` backend the
|
||||
/// host creates for this session's pads (one of the `PUNKTFUNK_GAMEPAD_*` values).
|
||||
/// `PUNKTFUNK_GAMEPAD_AUTO` (or any unrecognized value) lets the host decide (its
|
||||
/// `PUNKTFUNK_GAMEPAD` env var, else X-Box 360); a concrete value is honored only if that
|
||||
/// backend is available on the host. The resolved choice is readable via
|
||||
/// [`punktfunk_connection_gamepad`] — only a DualSense session emits HID-output feedback.
|
||||
///
|
||||
/// # Safety
|
||||
/// Same as [`punktfunk_connect`].
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn punktfunk_connect_ex2(
|
||||
host: *const std::os::raw::c_char,
|
||||
port: u16,
|
||||
width: u32,
|
||||
height: u32,
|
||||
refresh_hz: u32,
|
||||
compositor: u32,
|
||||
gamepad: u32,
|
||||
pin_sha256: *const u8,
|
||||
observed_sha256_out: *mut u8,
|
||||
client_cert_pem: *const std::os::raw::c_char,
|
||||
client_key_pem: *const std::os::raw::c_char,
|
||||
timeout_ms: u32,
|
||||
) -> *mut PunktfunkConnection {
|
||||
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
if host.is_null() {
|
||||
@@ -705,7 +762,14 @@ pub unsafe extern "C" fn punktfunk_connect_ex(
|
||||
height,
|
||||
refresh_hz,
|
||||
};
|
||||
let pref = crate::config::CompositorPref::from_u8(compositor as u8);
|
||||
// "Any unrecognized value = Auto" must hold for the FULL u32 domain — `as u8`
|
||||
// would wrap 0x101 into a concrete choice before from_u8's fallback could apply.
|
||||
let pref = u8::try_from(compositor)
|
||||
.map(crate::config::CompositorPref::from_u8)
|
||||
.unwrap_or_default();
|
||||
let gamepad = u8::try_from(gamepad)
|
||||
.map(crate::config::GamepadPref::from_u8)
|
||||
.unwrap_or_default();
|
||||
let pin = if pin_sha256.is_null() {
|
||||
None
|
||||
} else {
|
||||
@@ -725,6 +789,7 @@ pub unsafe extern "C" fn punktfunk_connect_ex(
|
||||
port,
|
||||
mode,
|
||||
pref,
|
||||
gamepad,
|
||||
pin,
|
||||
identity,
|
||||
std::time::Duration::from_millis(timeout_ms as u64),
|
||||
@@ -1153,6 +1218,33 @@ pub unsafe extern "C" fn punktfunk_connection_mode(
|
||||
})
|
||||
}
|
||||
|
||||
/// The virtual gamepad backend the host actually resolved for this session (one of the
|
||||
/// `PUNKTFUNK_GAMEPAD_*` values; the `Welcome`'s echo of the [`punktfunk_connect_ex2`]
|
||||
/// preference). `PUNKTFUNK_GAMEPAD_AUTO` = an older host that didn't say — assume X-Box 360,
|
||||
/// no HID-output feedback. Safe any time after connect.
|
||||
///
|
||||
/// # Safety
|
||||
/// `c` is a valid connection handle; `gamepad` is writable (NULL is skipped).
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn punktfunk_connection_gamepad(
|
||||
c: *const PunktfunkConnection,
|
||||
gamepad: *mut u32,
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let c = match unsafe { c.as_ref() } {
|
||||
Some(c) => c,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
unsafe {
|
||||
if !gamepad.is_null() {
|
||||
*gamepad = c.inner.resolved_gamepad.to_u8() as u32;
|
||||
}
|
||||
}
|
||||
PunktfunkStatus::Ok
|
||||
})
|
||||
}
|
||||
|
||||
/// Ask the host to switch the live session to `width`x`height`@`refresh_hz` without
|
||||
/// reconnecting (window resized, refresh changed). Non-blocking enqueue: on acceptance the
|
||||
/// stream continues at the new mode — the first new-mode access unit is an IDR with
|
||||
|
||||
Reference in New Issue
Block a user