From 17457cf4ba66abf7417b5438e6499a4f02d0a8e5 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 13 Jul 2026 14:08:00 +0200 Subject: [PATCH] refactor(gamestream/host): source gamepad BTN_* from punktfunk_core + pin the wire bits (G13/G15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gamestream/gamepad.rs` hand-declared its own copy of the GameStream buttonFlags/buttonFlags2 layout, which had drifted from the single source of truth in `punktfunk_core::input::gamepad`: the click bits were named `BTN_LS_CLK`/`BTN_RS_CLK` (vs core's `…_CLICK`). The two layouts are bit-identical — GameStream/Limelight and the punktfunk/1 native wire are one contract — so define the gamestream names as `pub const` aliases of the core constants. Values now come solely from core (can't drift); kept as `pub const` (not a `pub use` re-export) because on Windows the only consumer — the Linux uinput button map — is cfg'd out, where an unused re-export lints as an error but an unused pub const does not. Rename the two injector call-sites (`inject/linux/gamepad.rs`) to the canonical `BTN_LS_CLICK`/`BTN_RS_CLICK`. G15 host half: replace the 3-bit gamestream-vs-core spot-check with an exhaustive golden-value test (`gamepad_wire_bits_are_pinned`) that freezes every button bit + axis id to its exact wire value, so renumbering a bit in core — which would silently break every shipped client — fails a test first. The host counterpart to the client-side C-ABI cross-checks. Verified on Linux .21: clippy -D warnings clean, pin test + gamepad suite green. (Windows verified together with the rest of Phase 3.) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../punktfunk-host/src/gamestream/gamepad.rs | 55 +++++++++++-------- .../src/inject/linux/gamepad.rs | 4 +- crates/punktfunk-host/src/punktfunk1.rs | 51 +++++++++++++++-- 3 files changed, 81 insertions(+), 29 deletions(-) diff --git a/crates/punktfunk-host/src/gamestream/gamepad.rs b/crates/punktfunk-host/src/gamestream/gamepad.rs index 4479479b..535c2c3e 100644 --- a/crates/punktfunk-host/src/gamestream/gamepad.rs +++ b/crates/punktfunk-host/src/gamestream/gamepad.rs @@ -50,29 +50,38 @@ pub struct GamepadFrame { pub rs_y: i16, } -// buttonFlags bits (Limelight.h). -pub const BTN_DPAD_UP: u32 = 0x0001; -pub const BTN_DPAD_DOWN: u32 = 0x0002; -pub const BTN_DPAD_LEFT: u32 = 0x0004; -pub const BTN_DPAD_RIGHT: u32 = 0x0008; -pub const BTN_START: u32 = 0x0010; -pub const BTN_BACK: u32 = 0x0020; -pub const BTN_LS_CLK: u32 = 0x0040; -pub const BTN_RS_CLK: u32 = 0x0080; -pub const BTN_LB: u32 = 0x0100; -pub const BTN_RB: u32 = 0x0200; -pub const BTN_GUIDE: u32 = 0x0400; -pub const BTN_A: u32 = 0x1000; -pub const BTN_B: u32 = 0x2000; -pub const BTN_X: u32 = 0x4000; -pub const BTN_Y: u32 = 0x8000; -// Extended buttons in the `buttonFlags2 << 16` namespace (mirror `punktfunk_core::input::gamepad`): -// the four back-grip paddles. `decode` already merges `buttonFlags2 << 16` into `buttons`, but the -// injector map dropped these bits — Sunshine/Moonlight paddle clients were silently no-op'd. -pub const BTN_PADDLE1: u32 = 0x0001_0000; -pub const BTN_PADDLE2: u32 = 0x0002_0000; -pub const BTN_PADDLE3: u32 = 0x0004_0000; -pub const BTN_PADDLE4: u32 = 0x0008_0000; +// GameStream's `buttonFlags | buttonFlags2 << 16` layout (Limelight.h) is bit-identical to +// punktfunk's native gamepad wire, so source these from the single point of truth in `punktfunk_core` +// instead of re-declaring the values (the two drifted while separately hand-typed: the click bits +// were named `BTN_LS_CLK`/`BTN_RS_CLK` here vs the core `…_CLICK`). `decode` merges the two 16-bit +// halves into `buttons` raw; these names exist for the uinput injector's button map + hat math. The +// extended touchpad-click / Share bits (`BTN_TOUCHPAD` / `BTN_MISC1`) ride `buttons` too but are +// consumed straight from `punktfunk_core` by the DualSense/DS4 protos, so they aren't re-named here. +// +// These are `pub const` aliases rather than a `pub use` re-export on purpose: on Windows the sole +// consumer (the Linux uinput map) is cfg'd out, and an unused re-export lints as an error there, +// whereas an unused `pub const` does not. The values still come only from core, so they can't drift; +// the exact wire values are pinned by `punktfunk1.rs::gamepad_wire_bits_are_pinned`. +use punktfunk_core::input::gamepad as wire; +pub const BTN_DPAD_UP: u32 = wire::BTN_DPAD_UP; +pub const BTN_DPAD_DOWN: u32 = wire::BTN_DPAD_DOWN; +pub const BTN_DPAD_LEFT: u32 = wire::BTN_DPAD_LEFT; +pub const BTN_DPAD_RIGHT: u32 = wire::BTN_DPAD_RIGHT; +pub const BTN_START: u32 = wire::BTN_START; +pub const BTN_BACK: u32 = wire::BTN_BACK; +pub const BTN_LS_CLICK: u32 = wire::BTN_LS_CLICK; +pub const BTN_RS_CLICK: u32 = wire::BTN_RS_CLICK; +pub const BTN_LB: u32 = wire::BTN_LB; +pub const BTN_RB: u32 = wire::BTN_RB; +pub const BTN_GUIDE: u32 = wire::BTN_GUIDE; +pub const BTN_A: u32 = wire::BTN_A; +pub const BTN_B: u32 = wire::BTN_B; +pub const BTN_X: u32 = wire::BTN_X; +pub const BTN_Y: u32 = wire::BTN_Y; +pub const BTN_PADDLE1: u32 = wire::BTN_PADDLE1; +pub const BTN_PADDLE2: u32 = wire::BTN_PADDLE2; +pub const BTN_PADDLE3: u32 = wire::BTN_PADDLE3; +pub const BTN_PADDLE4: u32 = wire::BTN_PADDLE4; /// Decode one decrypted control plaintext into a controller event, if it is one. Mouse, /// keyboard, keepalives etc. yield `None` (they're handled by [`super::input::decode`]). diff --git a/crates/punktfunk-host/src/inject/linux/gamepad.rs b/crates/punktfunk-host/src/inject/linux/gamepad.rs index 491cd1a9..2e85ce93 100644 --- a/crates/punktfunk-host/src/inject/linux/gamepad.rs +++ b/crates/punktfunk-host/src/inject/linux/gamepad.rs @@ -89,8 +89,8 @@ const BUTTON_MAP: [(u32, u16); 15] = [ (gamepad::BTN_BACK, BTN_SELECT), (gamepad::BTN_START, BTN_START), (gamepad::BTN_GUIDE, BTN_MODE), - (gamepad::BTN_LS_CLK, BTN_THUMBL), - (gamepad::BTN_RS_CLK, BTN_THUMBR), + (gamepad::BTN_LS_CLICK, BTN_THUMBL), + (gamepad::BTN_RS_CLICK, BTN_THUMBR), (gamepad::BTN_PADDLE1, BTN_TRIGGER_HAPPY5), (gamepad::BTN_PADDLE2, BTN_TRIGGER_HAPPY6), (gamepad::BTN_PADDLE3, BTN_TRIGGER_HAPPY7), diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index 04a8e48b..38147ed2 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -5335,11 +5335,54 @@ mod tests { assert!(s.apply(&gp(InputKind::GamepadAxis, AXIS_LT, 9_999, 0))); assert_eq!(s.left_trigger, 255); assert!(!s.apply(&gp(InputKind::GamepadAxis, 42, 1, 0))); + } - // The punktfunk/1 button bits are the GameStream bits — one wire contract end to end. - assert_eq!(BTN_A, crate::gamestream::gamepad::BTN_A); - assert_eq!(BTN_GUIDE, crate::gamestream::gamepad::BTN_GUIDE); - assert_eq!(BTN_DPAD_UP, crate::gamestream::gamepad::BTN_DPAD_UP); + /// Freeze the gamepad wire contract: every button bit + axis id pinned to its exact value, read + /// through the GameStream namespace (`crate::gamestream::gamepad`, which re-exports + /// `punktfunk_core::input::gamepad` — the punktfunk/1 native wire and the GameStream/Limelight + /// wire are one and the same). Renumbering a bit in core, or dropping one from that re-export, + /// silently breaks every already-shipped client, so it must fail here first. This is the host + /// counterpart to the client-side C-ABI cross-checks in the Apple/Android gamepad tests. + #[test] + fn gamepad_wire_bits_are_pinned() { + use crate::gamestream::gamepad as gm; + use punktfunk_core::input::gamepad as pf; + // buttonFlags — low 16 bits, named via the GameStream re-export the injectors use. + assert_eq!(gm::BTN_DPAD_UP, 0x0000_0001); + assert_eq!(gm::BTN_DPAD_DOWN, 0x0000_0002); + assert_eq!(gm::BTN_DPAD_LEFT, 0x0000_0004); + assert_eq!(gm::BTN_DPAD_RIGHT, 0x0000_0008); + assert_eq!(gm::BTN_START, 0x0000_0010); + assert_eq!(gm::BTN_BACK, 0x0000_0020); + assert_eq!(gm::BTN_LS_CLICK, 0x0000_0040); + assert_eq!(gm::BTN_RS_CLICK, 0x0000_0080); + assert_eq!(gm::BTN_LB, 0x0000_0100); + assert_eq!(gm::BTN_RB, 0x0000_0200); + assert_eq!(gm::BTN_GUIDE, 0x0000_0400); + assert_eq!(gm::BTN_A, 0x0000_1000); + assert_eq!(gm::BTN_B, 0x0000_2000); + assert_eq!(gm::BTN_X, 0x0000_4000); + assert_eq!(gm::BTN_Y, 0x0000_8000); + // buttonFlags2 — high 16 bits: back-grip paddles (re-exported), plus the touchpad-click / + // Share bits the DualSense/DS4 protos consume straight from core. + assert_eq!(gm::BTN_PADDLE1, 0x0001_0000); + assert_eq!(gm::BTN_PADDLE2, 0x0002_0000); + assert_eq!(gm::BTN_PADDLE3, 0x0004_0000); + assert_eq!(gm::BTN_PADDLE4, 0x0008_0000); + assert_eq!(pf::BTN_TOUCHPAD, 0x0010_0000); + assert_eq!(pf::BTN_MISC1, 0x0020_0000); + // Axis ids — dense, 0-based. + assert_eq!( + [ + pf::AXIS_LS_X, + pf::AXIS_LS_Y, + pf::AXIS_RS_X, + pf::AXIS_RS_Y, + pf::AXIS_LT, + pf::AXIS_RT, + ], + [0, 1, 2, 3, 4, 5] + ); } /// Pull and byte-verify `count` synthetic frames through the C ABI connection.