68b9f108ab
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>
83 lines
4.1 KiB
Swift
83 lines
4.1 KiB
Swift
// The gamepad wire contract shared by capture (GamepadCapture), feedback (GamepadFeedback),
|
||
// and the tests — the pad count, button bits, axis ids, and the touchpad/motion unit conversions.
|
||
|
||
import Foundation
|
||
|
||
/// The gamepad wire contract (mirrors `punktfunk_core::input::gamepad`).
|
||
public enum GamepadWire {
|
||
/// Gamepads addressable on the wire — the pad index rides the low byte of `flags` on every
|
||
/// per-pad event, 0...15 (`punktfunk_core::input::MAX_PADS`).
|
||
public static let maxPads: Int = 16
|
||
|
||
public static let dpadUp: UInt32 = 0x0001
|
||
public static let dpadDown: UInt32 = 0x0002
|
||
public static let dpadLeft: UInt32 = 0x0004
|
||
public static let dpadRight: UInt32 = 0x0008
|
||
public static let start: UInt32 = 0x0010
|
||
public static let back: UInt32 = 0x0020
|
||
public static let leftStickClick: UInt32 = 0x0040
|
||
public static let rightStickClick: UInt32 = 0x0080
|
||
public static let leftShoulder: UInt32 = 0x0100
|
||
public static let rightShoulder: UInt32 = 0x0200
|
||
public static let guide: UInt32 = 0x0400
|
||
public static let a: UInt32 = 0x1000
|
||
public static let b: UInt32 = 0x2000
|
||
public static let x: UInt32 = 0x4000
|
||
public static let y: UInt32 = 0x8000
|
||
/// DualSense touchpad click (Moonlight's extended-button bit position).
|
||
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] = [
|
||
dpadUp, dpadDown, dpadLeft, dpadRight, start, back,
|
||
leftStickClick, rightStickClick, leftShoulder, rightShoulder, guide,
|
||
a, b, x, y, touchpadClick, misc1,
|
||
]
|
||
|
||
public static let axisLSX: UInt32 = 0
|
||
public static let axisLSY: UInt32 = 1
|
||
public static let axisRSX: UInt32 = 2
|
||
public static let axisRSY: UInt32 = 3
|
||
public static let axisLT: UInt32 = 4
|
||
public static let axisRT: UInt32 = 5
|
||
|
||
/// Raw DualSense gyro units per rad/s: hid-playstation's calibration over the host's
|
||
/// fixed blob resolves to 20 LSB per deg/s.
|
||
public static let gyroLSBPerRadS: Float = 20 * 180 / .pi
|
||
/// Raw DualSense accelerometer units per g (same derivation).
|
||
public static let accelLSBPerG: Float = 10_000
|
||
|
||
/// GC touchpad coordinates (±1, +y up) → wire (0...65535, origin top-left, +y down).
|
||
public static func touchpad(x: Float, y: Float) -> (x: UInt16, y: UInt16) {
|
||
let wx = ((x.clamped(to: -1...1) + 1) / 2 * 65535).rounded()
|
||
let wy = ((1 - y.clamped(to: -1...1)) / 2 * 65535).rounded()
|
||
return (UInt16(wx), UInt16(wy))
|
||
}
|
||
|
||
/// Scale + clamp one motion component into the raw signed-16 sensor domain.
|
||
public static func motionRaw(_ value: Float, scale: Float) -> Int16 {
|
||
Int16((value * scale).rounded().clamped(to: Float(Int16.min)...Float(Int16.max)))
|
||
}
|
||
}
|
||
|
||
extension Float {
|
||
fileprivate func clamped(to range: ClosedRange<Float>) -> Float {
|
||
Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
|
||
}
|
||
}
|