refactor(gamestream/host): source gamepad BTN_* from punktfunk_core + pin the wire bits (G13/G15)

`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) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 14:08:00 +02:00
parent 6263108e15
commit 17457cf4ba
3 changed files with 81 additions and 29 deletions
+32 -23
View File
@@ -50,29 +50,38 @@ pub struct GamepadFrame {
pub rs_y: i16, pub rs_y: i16,
} }
// buttonFlags bits (Limelight.h). // GameStream's `buttonFlags | buttonFlags2 << 16` layout (Limelight.h) is bit-identical to
pub const BTN_DPAD_UP: u32 = 0x0001; // punktfunk's native gamepad wire, so source these from the single point of truth in `punktfunk_core`
pub const BTN_DPAD_DOWN: u32 = 0x0002; // instead of re-declaring the values (the two drifted while separately hand-typed: the click bits
pub const BTN_DPAD_LEFT: u32 = 0x0004; // were named `BTN_LS_CLK`/`BTN_RS_CLK` here vs the core `…_CLICK`). `decode` merges the two 16-bit
pub const BTN_DPAD_RIGHT: u32 = 0x0008; // halves into `buttons` raw; these names exist for the uinput injector's button map + hat math. The
pub const BTN_START: u32 = 0x0010; // extended touchpad-click / Share bits (`BTN_TOUCHPAD` / `BTN_MISC1`) ride `buttons` too but are
pub const BTN_BACK: u32 = 0x0020; // consumed straight from `punktfunk_core` by the DualSense/DS4 protos, so they aren't re-named here.
pub const BTN_LS_CLK: u32 = 0x0040; //
pub const BTN_RS_CLK: u32 = 0x0080; // These are `pub const` aliases rather than a `pub use` re-export on purpose: on Windows the sole
pub const BTN_LB: u32 = 0x0100; // consumer (the Linux uinput map) is cfg'd out, and an unused re-export lints as an error there,
pub const BTN_RB: u32 = 0x0200; // whereas an unused `pub const` does not. The values still come only from core, so they can't drift;
pub const BTN_GUIDE: u32 = 0x0400; // the exact wire values are pinned by `punktfunk1.rs::gamepad_wire_bits_are_pinned`.
pub const BTN_A: u32 = 0x1000; use punktfunk_core::input::gamepad as wire;
pub const BTN_B: u32 = 0x2000; pub const BTN_DPAD_UP: u32 = wire::BTN_DPAD_UP;
pub const BTN_X: u32 = 0x4000; pub const BTN_DPAD_DOWN: u32 = wire::BTN_DPAD_DOWN;
pub const BTN_Y: u32 = 0x8000; pub const BTN_DPAD_LEFT: u32 = wire::BTN_DPAD_LEFT;
// Extended buttons in the `buttonFlags2 << 16` namespace (mirror `punktfunk_core::input::gamepad`): pub const BTN_DPAD_RIGHT: u32 = wire::BTN_DPAD_RIGHT;
// the four back-grip paddles. `decode` already merges `buttonFlags2 << 16` into `buttons`, but the pub const BTN_START: u32 = wire::BTN_START;
// injector map dropped these bits — Sunshine/Moonlight paddle clients were silently no-op'd. pub const BTN_BACK: u32 = wire::BTN_BACK;
pub const BTN_PADDLE1: u32 = 0x0001_0000; pub const BTN_LS_CLICK: u32 = wire::BTN_LS_CLICK;
pub const BTN_PADDLE2: u32 = 0x0002_0000; pub const BTN_RS_CLICK: u32 = wire::BTN_RS_CLICK;
pub const BTN_PADDLE3: u32 = 0x0004_0000; pub const BTN_LB: u32 = wire::BTN_LB;
pub const BTN_PADDLE4: u32 = 0x0008_0000; 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, /// 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`]). /// keyboard, keepalives etc. yield `None` (they're handled by [`super::input::decode`]).
@@ -89,8 +89,8 @@ const BUTTON_MAP: [(u32, u16); 15] = [
(gamepad::BTN_BACK, BTN_SELECT), (gamepad::BTN_BACK, BTN_SELECT),
(gamepad::BTN_START, BTN_START), (gamepad::BTN_START, BTN_START),
(gamepad::BTN_GUIDE, BTN_MODE), (gamepad::BTN_GUIDE, BTN_MODE),
(gamepad::BTN_LS_CLK, BTN_THUMBL), (gamepad::BTN_LS_CLICK, BTN_THUMBL),
(gamepad::BTN_RS_CLK, BTN_THUMBR), (gamepad::BTN_RS_CLICK, BTN_THUMBR),
(gamepad::BTN_PADDLE1, BTN_TRIGGER_HAPPY5), (gamepad::BTN_PADDLE1, BTN_TRIGGER_HAPPY5),
(gamepad::BTN_PADDLE2, BTN_TRIGGER_HAPPY6), (gamepad::BTN_PADDLE2, BTN_TRIGGER_HAPPY6),
(gamepad::BTN_PADDLE3, BTN_TRIGGER_HAPPY7), (gamepad::BTN_PADDLE3, BTN_TRIGGER_HAPPY7),
+47 -4
View File
@@ -5335,11 +5335,54 @@ mod tests {
assert!(s.apply(&gp(InputKind::GamepadAxis, AXIS_LT, 9_999, 0))); assert!(s.apply(&gp(InputKind::GamepadAxis, AXIS_LT, 9_999, 0)));
assert_eq!(s.left_trigger, 255); assert_eq!(s.left_trigger, 255);
assert!(!s.apply(&gp(InputKind::GamepadAxis, 42, 1, 0))); assert!(!s.apply(&gp(InputKind::GamepadAxis, 42, 1, 0)));
}
// The punktfunk/1 button bits are the GameStream bits — one wire contract end to end. /// Freeze the gamepad wire contract: every button bit + axis id pinned to its exact value, read
assert_eq!(BTN_A, crate::gamestream::gamepad::BTN_A); /// through the GameStream namespace (`crate::gamestream::gamepad`, which re-exports
assert_eq!(BTN_GUIDE, crate::gamestream::gamepad::BTN_GUIDE); /// `punktfunk_core::input::gamepad` — the punktfunk/1 native wire and the GameStream/Limelight
assert_eq!(BTN_DPAD_UP, crate::gamestream::gamepad::BTN_DPAD_UP); /// 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. /// Pull and byte-verify `count` synthetic frames through the C ABI connection.