feat(gamepad/apple): send Share/Create as BTN_MISC1 + pin wire bits to the C ABI
G5: buttonMask mapped the dedicated share/create/capture element onto BTN_BACK, the same bit as View (buttonOptions). On an Xbox-Series pad those are two distinct physical buttons, so Share was indistinguishable from View on the host and never delivered the capture bit the host already decodes (DualSense mute / Steam quick-access). Route it to BTN_MISC1 instead, matching the Rust client's `Button::Misc1 => wire::BTN_MISC1`. Adds `misc1` to GamepadWire and allButtons so a held capture button is released on flush like the others. (On-glass verify owed on a real Xbox-Series pad; a clone pad that exposes one button as both buttonOptions and Share now emits back+misc1 for it — harmless on a plain xpad session and rare otherwise.) G22 (partial): define paddle1..4 for wire completeness, but leave them out of buttonMask/allButtons until the GameController paddleButton1..4 ↔ BTN_PADDLE physical correspondence is confirmed on a real Elite pad. G15: replace the 3-bit spot-check with an exhaustive assertion of every GamepadWire button/axis constant against the generated C ABI header (punktfunk_core.h), so any Swift-side drift from punktfunk_core::input::gamepad fails CI. swift build + full PunktfunkKit suite green (124 passed, 5 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -305,11 +305,15 @@ public final class GamepadCapture {
|
|||||||
if g.dpad.right.isPressed { b |= GamepadWire.dpadRight }
|
if g.dpad.right.isPressed { b |= GamepadWire.dpadRight }
|
||||||
if g.buttonMenu.isPressed { b |= GamepadWire.start }
|
if g.buttonMenu.isPressed { b |= GamepadWire.start }
|
||||||
if g.buttonOptions?.isPressed == true { b |= GamepadWire.back }
|
if g.buttonOptions?.isPressed == true { b |= GamepadWire.back }
|
||||||
// The share/create/capture element (Xbox Series share, a clone pad's screenshot button —
|
// The dedicated share/create/capture element (Xbox-Series Share, DualSense Create, a clone
|
||||||
// e.g. the GameSir G8's, below its d-pad) folds into back/select too. On pads that expose
|
// pad's screenshot button — e.g. the GameSir G8's, below its d-pad) → the wire's capture
|
||||||
// the create button BOTH as buttonOptions and as the share element this OR is harmless —
|
// bit, matching the Rust client's `Button::Misc1 => wire::BTN_MISC1`. On an Xbox-Series pad
|
||||||
// same wire bit.
|
// this is a button physically DISTINCT from View (buttonOptions, above), so it must not
|
||||||
if g.buttons[GCInputButtonShare]?.isPressed == true { b |= GamepadWire.back }
|
// collapse onto back — the host reads MISC1 as its own control (DualSense mute / Steam
|
||||||
|
// quick-access). Caveat: a pad that surfaces ONE physical button as both buttonOptions and
|
||||||
|
// this share element now emits back+misc1 for it — harmless on a plain xpad session (no
|
||||||
|
// misc button) and rare otherwise. NOTE: on-glass verify on a real Xbox-Series pad.
|
||||||
|
if g.buttons[GCInputButtonShare]?.isPressed == true { b |= GamepadWire.misc1 }
|
||||||
if g.leftThumbstickButton?.isPressed == true { b |= GamepadWire.leftStickClick }
|
if g.leftThumbstickButton?.isPressed == true { b |= GamepadWire.leftStickClick }
|
||||||
if g.rightThumbstickButton?.isPressed == true { b |= GamepadWire.rightStickClick }
|
if g.rightThumbstickButton?.isPressed == true { b |= GamepadWire.rightStickClick }
|
||||||
if g.leftShoulder.isPressed { b |= GamepadWire.leftShoulder }
|
if g.leftShoulder.isPressed { b |= GamepadWire.leftShoulder }
|
||||||
|
|||||||
@@ -26,11 +26,27 @@ public enum GamepadWire {
|
|||||||
public static let y: UInt32 = 0x8000
|
public static let y: UInt32 = 0x8000
|
||||||
/// DualSense touchpad click (Moonlight's extended-button bit position).
|
/// DualSense touchpad click (Moonlight's extended-button bit position).
|
||||||
public static let touchpadClick: UInt32 = 0x10_0000
|
public static let touchpadClick: UInt32 = 0x10_0000
|
||||||
|
/// Misc / capture button — Xbox-Series Share, DualSense Create, Steam-Deck quick-access
|
||||||
|
/// (Moonlight's extended-button namespace; `input::gamepad::BTN_MISC1`). The host routes it to
|
||||||
|
/// the DualSense mute / Steam quick-access menu; a plain virtual xpad has no such button.
|
||||||
|
public static let misc1: UInt32 = 0x0020_0000
|
||||||
|
/// Back-grip paddles (Xbox Elite P1–P4 / DualSense Edge / Steam-Deck L4-L5-R4-R5), in
|
||||||
|
/// Moonlight's extended-button namespace (`input::gamepad::BTN_PADDLE1..4`, R4/L4/R5/L5).
|
||||||
|
/// Defined for wire completeness and pinned by the tests; `GamepadCapture.buttonMask` does not
|
||||||
|
/// read them yet — the GameController `paddleButton1..4` ↔ BTN_PADDLE physical correspondence
|
||||||
|
/// needs confirming on a real Elite pad first (see the gamepad-review-cleanup plan, G22), so
|
||||||
|
/// they are intentionally absent from `allButtons` until that forwarding lands.
|
||||||
|
public static let paddle1: UInt32 = 0x0001_0000
|
||||||
|
public static let paddle2: UInt32 = 0x0002_0000
|
||||||
|
public static let paddle3: UInt32 = 0x0004_0000
|
||||||
|
public static let paddle4: UInt32 = 0x0008_0000
|
||||||
|
|
||||||
|
/// Every button `buttonMask`/`sendGuide` can set — walked by `sync`'s transition diff and by
|
||||||
|
/// `flush` on release. Paddles are excluded until their capture lands (see above).
|
||||||
public static let allButtons: [UInt32] = [
|
public static let allButtons: [UInt32] = [
|
||||||
dpadUp, dpadDown, dpadLeft, dpadRight, start, back,
|
dpadUp, dpadDown, dpadLeft, dpadRight, start, back,
|
||||||
leftStickClick, rightStickClick, leftShoulder, rightShoulder, guide,
|
leftStickClick, rightStickClick, leftShoulder, rightShoulder, guide,
|
||||||
a, b, x, y, touchpadClick,
|
a, b, x, y, touchpadClick, misc1,
|
||||||
]
|
]
|
||||||
|
|
||||||
public static let axisLSX: UInt32 = 0
|
public static let axisLSX: UInt32 = 0
|
||||||
|
|||||||
@@ -27,11 +27,16 @@ final class GamepadWireTests: XCTestCase {
|
|||||||
XCTAssertEqual(GamepadWire.x, 0x4000)
|
XCTAssertEqual(GamepadWire.x, 0x4000)
|
||||||
XCTAssertEqual(GamepadWire.y, 0x8000)
|
XCTAssertEqual(GamepadWire.y, 0x8000)
|
||||||
XCTAssertEqual(GamepadWire.touchpadClick, 0x10_0000)
|
XCTAssertEqual(GamepadWire.touchpadClick, 0x10_0000)
|
||||||
|
XCTAssertEqual(GamepadWire.misc1, 0x0020_0000)
|
||||||
// Every button is enumerated exactly once (releaseAll walks this list).
|
// Every button is enumerated exactly once (releaseAll walks this list).
|
||||||
let combined: UInt32 = GamepadWire.allButtons.reduce(0) { $0 | $1 }
|
let combined: UInt32 = GamepadWire.allButtons.reduce(0) { $0 | $1 }
|
||||||
XCTAssertEqual(combined, 0x0010_F7FF)
|
XCTAssertEqual(combined, 0x0030_F7FF)
|
||||||
XCTAssertEqual(GamepadWire.allButtons.count, 16)
|
XCTAssertEqual(GamepadWire.allButtons.count, 17)
|
||||||
XCTAssertEqual(GamepadWire.allButtons.count, Set(GamepadWire.allButtons).count)
|
XCTAssertEqual(GamepadWire.allButtons.count, Set(GamepadWire.allButtons).count)
|
||||||
|
// Paddles are defined but not yet forwarded, so they stay out of allButtons for now.
|
||||||
|
for paddle in [GamepadWire.paddle1, GamepadWire.paddle2, GamepadWire.paddle3, GamepadWire.paddle4] {
|
||||||
|
XCTAssertFalse(GamepadWire.allButtons.contains(paddle))
|
||||||
|
}
|
||||||
// Axis ids.
|
// Axis ids.
|
||||||
XCTAssertEqual(GamepadWire.axisLSX, 0)
|
XCTAssertEqual(GamepadWire.axisLSX, 0)
|
||||||
XCTAssertEqual(GamepadWire.axisLSY, 1)
|
XCTAssertEqual(GamepadWire.axisLSY, 1)
|
||||||
@@ -41,6 +46,42 @@ final class GamepadWireTests: XCTestCase {
|
|||||||
XCTAssertEqual(GamepadWire.axisRT, 5)
|
XCTAssertEqual(GamepadWire.axisRT, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testButtonBitsMatchTheCABIVerbatim() {
|
||||||
|
// Assert EVERY wire constant against the generated C ABI header (punktfunk_core.h, the same
|
||||||
|
// source `punktfunk_core::input::gamepad` emits), so a Swift-side edit that drifts from the
|
||||||
|
// Rust contract fails CI — not just the handful spot-checked above. (Cross-cutting review
|
||||||
|
// finding G15: the button values were re-declared per client with only a 3-of-19 check.)
|
||||||
|
XCTAssertEqual(GamepadWire.dpadUp, UInt32(PUNKTFUNK_BTN_DPAD_UP))
|
||||||
|
XCTAssertEqual(GamepadWire.dpadDown, UInt32(PUNKTFUNK_BTN_DPAD_DOWN))
|
||||||
|
XCTAssertEqual(GamepadWire.dpadLeft, UInt32(PUNKTFUNK_BTN_DPAD_LEFT))
|
||||||
|
XCTAssertEqual(GamepadWire.dpadRight, UInt32(PUNKTFUNK_BTN_DPAD_RIGHT))
|
||||||
|
XCTAssertEqual(GamepadWire.start, UInt32(PUNKTFUNK_BTN_START))
|
||||||
|
XCTAssertEqual(GamepadWire.back, UInt32(PUNKTFUNK_BTN_BACK))
|
||||||
|
XCTAssertEqual(GamepadWire.leftStickClick, UInt32(PUNKTFUNK_BTN_LS_CLICK))
|
||||||
|
XCTAssertEqual(GamepadWire.rightStickClick, UInt32(PUNKTFUNK_BTN_RS_CLICK))
|
||||||
|
XCTAssertEqual(GamepadWire.leftShoulder, UInt32(PUNKTFUNK_BTN_LB))
|
||||||
|
XCTAssertEqual(GamepadWire.rightShoulder, UInt32(PUNKTFUNK_BTN_RB))
|
||||||
|
XCTAssertEqual(GamepadWire.guide, UInt32(PUNKTFUNK_BTN_GUIDE))
|
||||||
|
XCTAssertEqual(GamepadWire.a, UInt32(PUNKTFUNK_BTN_A))
|
||||||
|
XCTAssertEqual(GamepadWire.b, UInt32(PUNKTFUNK_BTN_B))
|
||||||
|
XCTAssertEqual(GamepadWire.x, UInt32(PUNKTFUNK_BTN_X))
|
||||||
|
XCTAssertEqual(GamepadWire.y, UInt32(PUNKTFUNK_BTN_Y))
|
||||||
|
XCTAssertEqual(GamepadWire.touchpadClick, UInt32(PUNKTFUNK_BTN_TOUCHPAD))
|
||||||
|
XCTAssertEqual(GamepadWire.misc1, UInt32(PUNKTFUNK_GAMEPAD_BTN_MISC1))
|
||||||
|
XCTAssertEqual(GamepadWire.paddle1, UInt32(PUNKTFUNK_GAMEPAD_BTN_PADDLE1))
|
||||||
|
XCTAssertEqual(GamepadWire.paddle2, UInt32(PUNKTFUNK_GAMEPAD_BTN_PADDLE2))
|
||||||
|
XCTAssertEqual(GamepadWire.paddle3, UInt32(PUNKTFUNK_GAMEPAD_BTN_PADDLE3))
|
||||||
|
XCTAssertEqual(GamepadWire.paddle4, UInt32(PUNKTFUNK_GAMEPAD_BTN_PADDLE4))
|
||||||
|
// Axis ids and pad count share the same header.
|
||||||
|
XCTAssertEqual(GamepadWire.axisLSX, UInt32(PUNKTFUNK_AXIS_LS_X))
|
||||||
|
XCTAssertEqual(GamepadWire.axisLSY, UInt32(PUNKTFUNK_AXIS_LS_Y))
|
||||||
|
XCTAssertEqual(GamepadWire.axisRSX, UInt32(PUNKTFUNK_AXIS_RS_X))
|
||||||
|
XCTAssertEqual(GamepadWire.axisRSY, UInt32(PUNKTFUNK_AXIS_RS_Y))
|
||||||
|
XCTAssertEqual(GamepadWire.axisLT, UInt32(PUNKTFUNK_AXIS_LT))
|
||||||
|
XCTAssertEqual(GamepadWire.axisRT, UInt32(PUNKTFUNK_AXIS_RT))
|
||||||
|
XCTAssertEqual(GamepadWire.maxPads, Int(MAX_PADS))
|
||||||
|
}
|
||||||
|
|
||||||
func testPadIndexRidesFlagsOnEveryPerPadEvent() {
|
func testPadIndexRidesFlagsOnEveryPerPadEvent() {
|
||||||
// The wire pad index is the low byte of `flags` (punktfunk_core::input) on button + axis.
|
// The wire pad index is the low byte of `flags` (punktfunk_core::input) on button + axis.
|
||||||
let btn = PunktfunkInputEvent.gamepadButton(GamepadWire.a, down: true, pad: 3)
|
let btn = PunktfunkInputEvent.gamepadButton(GamepadWire.a, down: true, pad: 3)
|
||||||
|
|||||||
Reference in New Issue
Block a user