diff --git a/crates/punktfunk-core/src/input.rs b/crates/punktfunk-core/src/input.rs index a38b61d7..791c2161 100644 --- a/crates/punktfunk-core/src/input.rs +++ b/crates/punktfunk-core/src/input.rs @@ -320,6 +320,42 @@ impl InputEvent { } } +/// One decoded GameStream (Moonlight-plane) controller event. Shared vocabulary: the host's +/// GameStream/Moonlight decode path produces these, and the platform-neutral input injectors +/// (`pf-inject`) consume them — so the type lives in `core::input`, below both, rather than in +/// either plane. The `buttons` bitmask uses the same [`gamepad`] `BTN_*` layout as the native +/// [`GamepadSnapshot`] (GameStream's `buttonFlags | buttonFlags2 << 16` is bit-identical). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GamepadEvent { + /// Full state of one controller + the set of attached controllers. + State(GamepadFrame), + /// Sunshine arrival metadata (precedes the first State for that pad). + Arrival { + index: u8, + /// 0 unknown, 1 xbox, 2 ps, 3 nintendo. + kind: u8, + /// LI_CCAP_* bits (0x02 = rumble). + capabilities: u16, + }, +} + +/// Snapshot of one controller's inputs (Moonlight conventions: sticks −32768..32767 with +Y +/// up, triggers 0..255, buttons = `buttonFlags | buttonFlags2 << 16`). The decoded-frame twin of +/// [`GamepadSnapshot`] on the GameStream/Moonlight plane; see [`GamepadEvent`] for why it lives here. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct GamepadFrame { + pub index: i16, + /// Bit n set = controller n attached; a clear bit for an allocated pad means unplug. + pub active_mask: u16, + pub buttons: u32, + pub left_trigger: u8, + pub right_trigger: u8, + pub ls_x: i16, + pub ls_y: i16, + pub rs_x: i16, + pub rs_y: i16, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/punktfunk-host/src/devtest.rs b/crates/punktfunk-host/src/devtest.rs index 1bb0148b..149e3edc 100644 --- a/crates/punktfunk-host/src/devtest.rs +++ b/crates/punktfunk-host/src/devtest.rs @@ -220,7 +220,7 @@ pub fn deck_windows_spike(args: &[String]) -> Result<()> { /// removes the devnode. #[cfg(target_os = "windows")] pub fn dualsense_windows_test(args: &[String]) -> Result<()> { - use crate::gamestream::gamepad::{GamepadEvent, GamepadFrame}; + use punktfunk_core::input::{GamepadEvent, GamepadFrame}; use std::time::{Duration, Instant}; let secs: u64 = args .iter() diff --git a/crates/punktfunk-host/src/gamestream/gamepad.rs b/crates/punktfunk-host/src/gamestream/gamepad.rs index 540ee5e1..67446303 100644 --- a/crates/punktfunk-host/src/gamestream/gamepad.rs +++ b/crates/punktfunk-host/src/gamestream/gamepad.rs @@ -16,39 +16,10 @@ const MAGIC_MULTI_CONTROLLER: u32 = 0x0C; /// Sunshine extension: controller arrival metadata (type/capabilities). const MAGIC_CONTROLLER_ARRIVAL: u32 = 0x5500_0004; -/// Most controllers a session tracks (Sunshine's MAX_GAMEPADS). -pub const MAX_PADS: usize = 16; - -/// One decoded controller event. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum GamepadEvent { - /// Full state of one controller + the set of attached controllers. - State(GamepadFrame), - /// Sunshine arrival metadata (precedes the first State for that pad). - Arrival { - index: u8, - /// 0 unknown, 1 xbox, 2 ps, 3 nintendo. - kind: u8, - /// LI_CCAP_* bits (0x02 = rumble). - capabilities: u16, - }, -} - -/// Snapshot of one controller's inputs (Moonlight conventions: sticks −32768..32767 with +Y -/// up, triggers 0..255, buttons = `buttonFlags | buttonFlags2 << 16`). -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct GamepadFrame { - pub index: i16, - /// Bit n set = controller n attached; a clear bit for an allocated pad means unplug. - pub active_mask: u16, - pub buttons: u32, - pub left_trigger: u8, - pub right_trigger: u8, - pub ls_x: i16, - pub ls_y: i16, - pub rs_x: i16, - pub rs_y: i16, -} +// The decoded controller types ([`GamepadEvent`]/[`GamepadFrame`]) and the pad count +// ([`punktfunk_core::input::MAX_PADS`]) are shared vocabulary between this Moonlight decode path and +// the platform-neutral injectors, so they live in `core::input` (below both) rather than here. +use punktfunk_core::input::{GamepadEvent, GamepadFrame}; // 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` diff --git a/crates/punktfunk-host/src/inject.rs b/crates/punktfunk-host/src/inject.rs index 1e668f24..3fdc7638 100644 --- a/crates/punktfunk-host/src/inject.rs +++ b/crates/punktfunk-host/src/inject.rs @@ -321,7 +321,7 @@ pub mod gamepad { pub fn new() -> Self { GamepadManager } - pub fn handle(&mut self, _ev: &crate::gamestream::gamepad::GamepadEvent) {} + pub fn handle(&mut self, _ev: &punktfunk_core::input::GamepadEvent) {} pub fn pump_rumble(&mut self, _send: impl FnMut(u16, u16, u16)) {} } } diff --git a/crates/punktfunk-host/src/inject/linux/dualsense.rs b/crates/punktfunk-host/src/inject/linux/dualsense.rs index de22f0e3..dc9c9d4a 100644 --- a/crates/punktfunk-host/src/inject/linux/dualsense.rs +++ b/crates/punktfunk-host/src/inject/linux/dualsense.rs @@ -265,7 +265,7 @@ impl PadProto for DsLinuxProto { /// Merge buttons/sticks/triggers from the frame, preserving touch + motion + pad clicks (those /// come on the rich-input plane and must survive a button-only frame). - fn merge_frame(&self, prev: &DsState, f: &crate::gamestream::gamepad::GamepadFrame) -> DsState { + fn merge_frame(&self, prev: &DsState, f: &punktfunk_core::input::GamepadFrame) -> DsState { // Steam back grips have no DualSense slot — fold them onto standard buttons per the // configured policy (default drop) so they aren't silently lost. let buttons = crate::inject::steam_remap::fold_paddles(f.buttons, self.remap.paddles); @@ -355,7 +355,7 @@ impl PadProto for DsEdgeLinuxProto { /// Merge buttons/sticks/triggers from the frame, preserving the rich-plane fields — like the /// plain DualSense, EXCEPT the wire paddles are not folded away: they land on the Edge's own /// `buttons[2]` bits (rebuilt from every button frame, so no extra persistence). - fn merge_frame(&self, prev: &DsState, f: &crate::gamestream::gamepad::GamepadFrame) -> DsState { + fn merge_frame(&self, prev: &DsState, f: &punktfunk_core::input::GamepadFrame) -> DsState { let mut s = DsState::from_gamepad( f.buttons, f.ls_x, diff --git a/crates/punktfunk-host/src/inject/linux/dualshock4.rs b/crates/punktfunk-host/src/inject/linux/dualshock4.rs index adf31c8b..afb822f5 100644 --- a/crates/punktfunk-host/src/inject/linux/dualshock4.rs +++ b/crates/punktfunk-host/src/inject/linux/dualshock4.rs @@ -335,7 +335,7 @@ impl PadProto for Ds4LinuxProto { /// Merge buttons/sticks/triggers from the frame, preserving touch + motion + pad clicks (those /// arrive on the rich-input plane and must survive a button-only frame). - fn merge_frame(&self, prev: &DsState, f: &crate::gamestream::gamepad::GamepadFrame) -> DsState { + fn merge_frame(&self, prev: &DsState, f: &punktfunk_core::input::GamepadFrame) -> DsState { // Steam back grips have no DS4 slot — fold them onto standard buttons per the configured // policy (default drop) so they aren't silently lost. let buttons = crate::inject::steam_remap::fold_paddles(f.buttons, self.remap.paddles); diff --git a/crates/punktfunk-host/src/inject/linux/gamepad.rs b/crates/punktfunk-host/src/inject/linux/gamepad.rs index 78017782..d714adc7 100644 --- a/crates/punktfunk-host/src/inject/linux/gamepad.rs +++ b/crates/punktfunk-host/src/inject/linux/gamepad.rs @@ -2,7 +2,7 @@ //! 360 pad", `045e:028e`) so SDL/Steam/Proton match their built-in mapping with zero //! configuration — exactly what Sunshine emulates. One [`VirtualPad`] per attached client //! controller, managed by [`GamepadManager`] from decoded -//! [`GamepadFrame`](crate::gamestream::gamepad::GamepadFrame)s. +//! [`GamepadFrame`](punktfunk_core::input::GamepadFrame)s. //! //! Rumble flows the *other* way on the same fd: games upload force-feedback effects //! (`EV_UINPUT`/`UI_FF_UPLOAD` → `UI_BEGIN/END_FF_UPLOAD` ioctls) and trigger them with @@ -18,9 +18,10 @@ // Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). #![deny(clippy::undocumented_unsafe_blocks)] -use crate::gamestream::gamepad::{self, GamepadFrame, MAX_PADS}; +use crate::gamestream::gamepad; use crate::inject::pad_slots::PadSlots; use anyhow::{bail, Result}; +use punktfunk_core::input::{GamepadFrame, MAX_PADS}; use std::collections::HashMap; use std::os::fd::{AsRawFd, OwnedFd}; use std::time::Instant; @@ -582,8 +583,8 @@ impl GamepadManager { } /// Handle one decoded controller event (create/destroy by mask, then apply state). - pub fn handle(&mut self, ev: &crate::gamestream::gamepad::GamepadEvent) { - use crate::gamestream::gamepad::GamepadEvent; + pub fn handle(&mut self, ev: &punktfunk_core::input::GamepadEvent) { + use punktfunk_core::input::GamepadEvent; match ev { GamepadEvent::Arrival { index, kind, .. } => { tracing::info!(index, kind, "controller arrival ({})", self.slots.label()); diff --git a/crates/punktfunk-host/src/inject/linux/steam_controller.rs b/crates/punktfunk-host/src/inject/linux/steam_controller.rs index 0c7ac4ad..d7c7b464 100644 --- a/crates/punktfunk-host/src/inject/linux/steam_controller.rs +++ b/crates/punktfunk-host/src/inject/linux/steam_controller.rs @@ -402,7 +402,7 @@ impl PadProto for SteamProto { fn merge_frame( &self, prev: &SteamState, - f: &crate::gamestream::gamepad::GamepadFrame, + f: &punktfunk_core::input::GamepadFrame, ) -> SteamState { let mut s = SteamState::from_gamepad( f.buttons, @@ -511,7 +511,7 @@ impl PadProto for ScProto { fn merge_frame( &self, prev: &SteamState, - f: &crate::gamestream::gamepad::GamepadFrame, + f: &punktfunk_core::input::GamepadFrame, ) -> SteamState { use punktfunk_core::input::gamepad as gs; let native = f.buttons & (gs::BTN_PADDLE1 | gs::BTN_PADDLE2); diff --git a/crates/punktfunk-host/src/inject/linux/steam_controller2.rs b/crates/punktfunk-host/src/inject/linux/steam_controller2.rs index cbdd983f..2f11dab3 100644 --- a/crates/punktfunk-host/src/inject/linux/steam_controller2.rs +++ b/crates/punktfunk-host/src/inject/linux/steam_controller2.rs @@ -329,7 +329,7 @@ impl PadProto for TritonProto { fn merge_frame( &self, prev: &TritonState, - f: &crate::gamestream::gamepad::GamepadFrame, + f: &punktfunk_core::input::GamepadFrame, ) -> TritonState { let mut s = TritonState::from_gamepad( f.buttons, diff --git a/crates/punktfunk-host/src/inject/linux/switch_pro.rs b/crates/punktfunk-host/src/inject/linux/switch_pro.rs index ae333ae1..06990b2d 100644 --- a/crates/punktfunk-host/src/inject/linux/switch_pro.rs +++ b/crates/punktfunk-host/src/inject/linux/switch_pro.rs @@ -277,7 +277,7 @@ impl PadProto for SwitchProProto { fn merge_frame( &self, prev: &SwitchState, - f: &crate::gamestream::gamepad::GamepadFrame, + f: &punktfunk_core::input::GamepadFrame, ) -> SwitchState { let buttons = crate::inject::steam_remap::fold_paddles(f.buttons, self.remap.paddles); let mut s = SwitchState::from_gamepad( diff --git a/crates/punktfunk-host/src/inject/pad_slots.rs b/crates/punktfunk-host/src/inject/pad_slots.rs index 4f2427d8..c7a24e3b 100644 --- a/crates/punktfunk-host/src/inject/pad_slots.rs +++ b/crates/punktfunk-host/src/inject/pad_slots.rs @@ -1,9 +1,9 @@ //! Shared virtual-pad slot table + creation lifecycle, used by every backend manager (Linux //! uinput/uhid, Windows XUSB/UMDF). See [`PadSlots`]. -use crate::gamestream::gamepad::MAX_PADS; use crate::inject::pad_gate::PadGate; use anyhow::Result; +use punktfunk_core::input::MAX_PADS; use std::time::Instant; // The unplug sweep walks a u16 `active_mask` (the wire type); every slot must have a bit. diff --git a/crates/punktfunk-host/src/inject/uhid_manager.rs b/crates/punktfunk-host/src/inject/uhid_manager.rs index 20a68a98..bc6b43ee 100644 --- a/crates/punktfunk-host/src/inject/uhid_manager.rs +++ b/crates/punktfunk-host/src/inject/uhid_manager.rs @@ -6,10 +6,10 @@ //! Windows XUSB) write frames straight through with no state vec / heartbeat / rich plane, so they //! use [`PadSlots`] directly instead. -use crate::gamestream::gamepad::{GamepadEvent, GamepadFrame, MAX_PADS}; use crate::inject::hidout_dedup::HidoutDedup; use crate::inject::pad_slots::PadSlots; use anyhow::Result; +use punktfunk_core::input::{GamepadEvent, GamepadFrame, MAX_PADS}; use punktfunk_core::quic::{HidOutput, RichInput}; use std::time::{Duration, Instant}; diff --git a/crates/punktfunk-host/src/inject/windows/dualsense_edge_windows.rs b/crates/punktfunk-host/src/inject/windows/dualsense_edge_windows.rs index b206b550..c169e58c 100644 --- a/crates/punktfunk-host/src/inject/windows/dualsense_edge_windows.rs +++ b/crates/punktfunk-host/src/inject/windows/dualsense_edge_windows.rs @@ -44,7 +44,7 @@ impl PadProto for DsEdgeWinProto { /// Merge buttons/sticks/triggers from the frame, preserving the rich-plane fields — like the /// plain DualSense, EXCEPT the wire paddles land on the Edge's own `buttons[2]` bits /// (rebuilt from every button frame, so no extra persistence). - fn merge_frame(&self, prev: &DsState, f: &crate::gamestream::gamepad::GamepadFrame) -> DsState { + fn merge_frame(&self, prev: &DsState, f: &punktfunk_core::input::GamepadFrame) -> DsState { let mut s = DsState::from_gamepad( f.buttons, f.ls_x, diff --git a/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs b/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs index 3ed0f33b..e13aa4ce 100644 --- a/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs +++ b/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs @@ -438,7 +438,7 @@ impl PadProto for DsWinProto { /// Merge buttons/sticks/triggers from the frame, preserving touch + motion + pad clicks (rich- /// plane fields that must survive a button-only frame) — exactly as `linux/dualsense.rs` does. - fn merge_frame(&self, prev: &DsState, f: &crate::gamestream::gamepad::GamepadFrame) -> DsState { + fn merge_frame(&self, prev: &DsState, f: &punktfunk_core::input::GamepadFrame) -> DsState { // Steam back grips have no DualSense slot — fold them onto standard buttons per the // configured policy (default drop) so they aren't silently lost. let buttons = crate::inject::steam_remap::fold_paddles(f.buttons, self.remap.paddles); diff --git a/crates/punktfunk-host/src/inject/windows/dualshock4_windows.rs b/crates/punktfunk-host/src/inject/windows/dualshock4_windows.rs index 29252838..bf34b5ab 100644 --- a/crates/punktfunk-host/src/inject/windows/dualshock4_windows.rs +++ b/crates/punktfunk-host/src/inject/windows/dualshock4_windows.rs @@ -187,7 +187,7 @@ impl PadProto for Ds4WinProto { /// Merge buttons/sticks/triggers from the frame, preserving touch + motion + pad clicks (rich- /// plane fields that must survive a button-only frame) — exactly as `linux/dualshock4.rs` does. - fn merge_frame(&self, prev: &DsState, f: &crate::gamestream::gamepad::GamepadFrame) -> DsState { + fn merge_frame(&self, prev: &DsState, f: &punktfunk_core::input::GamepadFrame) -> DsState { // Steam back grips have no DS4 slot — fold them onto standard buttons per the configured // policy (default drop) so they aren't silently lost. let buttons = crate::inject::steam_remap::fold_paddles(f.buttons, self.remap.paddles); diff --git a/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs b/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs index 53a99481..61183f00 100644 --- a/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs +++ b/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs @@ -13,9 +13,9 @@ //! level changes to the client (the universal 0xCA plane), mirroring the Linux `EV_FF` read path. use super::gamepad_raii::{sw_create_cb, PadChannel, SwCreateCtx}; -use crate::gamestream::gamepad::{GamepadEvent, MAX_PADS}; use crate::inject::pad_slots::PadSlots; use anyhow::{anyhow, Result}; +use punktfunk_core::input::{GamepadEvent, MAX_PADS}; use std::ffi::c_void; use std::sync::atomic::{fence, AtomicU32, Ordering}; use std::time::{Duration, Instant}; diff --git a/crates/punktfunk-host/src/inject/windows/steam_deck_windows.rs b/crates/punktfunk-host/src/inject/windows/steam_deck_windows.rs index 134f611b..6b888df2 100644 --- a/crates/punktfunk-host/src/inject/windows/steam_deck_windows.rs +++ b/crates/punktfunk-host/src/inject/windows/steam_deck_windows.rs @@ -179,7 +179,7 @@ impl PadProto for DeckWinProto { fn merge_frame( &self, prev: &SteamState, - f: &crate::gamestream::gamepad::GamepadFrame, + f: &punktfunk_core::input::GamepadFrame, ) -> SteamState { use super::steam_proto::btn; let mut s = SteamState::from_gamepad( diff --git a/crates/punktfunk-host/src/native/input.rs b/crates/punktfunk-host/src/native/input.rs index c3d1f38f..8b90e463 100644 --- a/crates/punktfunk-host/src/native/input.rs +++ b/crates/punktfunk-host/src/native/input.rs @@ -56,8 +56,8 @@ impl PadState { self.rs_y = s.rs_y; } - fn frame(&self, index: usize, active_mask: u16) -> crate::gamestream::gamepad::GamepadFrame { - crate::gamestream::gamepad::GamepadFrame { + fn frame(&self, index: usize, active_mask: u16) -> punktfunk_core::input::GamepadFrame { + punktfunk_core::input::GamepadFrame { index: index as i16, active_mask, buttons: self.buttons, @@ -192,8 +192,8 @@ impl Pads { self.kinds[idx] = resolved; } - fn handle(&mut self, ev: &crate::gamestream::gamepad::GamepadEvent) { - use crate::gamestream::gamepad::GamepadEvent; + fn handle(&mut self, ev: &punktfunk_core::input::GamepadEvent) { + use punktfunk_core::input::GamepadEvent; // Present = a create/update frame (the pad's mask bit is set); a cleared bit is the // removal frame emitted by the native detach path (`GamepadRemove`). let (idx, present) = match ev { @@ -212,7 +212,7 @@ impl Pads { } /// Dispatch a decoded event to the manager for `kind`, creating it lazily. - fn route_handle(&mut self, kind: GamepadPref, ev: &crate::gamestream::gamepad::GamepadEvent) { + fn route_handle(&mut self, kind: GamepadPref, ev: &punktfunk_core::input::GamepadEvent) { match kind { #[cfg(target_os = "linux")] GamepadPref::DualSense => self @@ -682,7 +682,7 @@ pub(super) fn input_thread( if idx < MAX_WIRE_PADS && pad_state[idx].apply(&ev) { pad_mask |= 1 << idx; let frame = pad_state[idx].frame(idx, pad_mask); - pads.handle(&crate::gamestream::gamepad::GamepadEvent::State(frame)); + pads.handle(&punktfunk_core::input::GamepadEvent::State(frame)); } } InputKind::GamepadState => { @@ -704,9 +704,7 @@ pub(super) fn input_thread( if first || pad_state[idx] != before { pad_mask |= 1 << idx; let frame = pad_state[idx].frame(idx, pad_mask); - pads.handle(&crate::gamestream::gamepad::GamepadEvent::State( - frame, - )); + pads.handle(&punktfunk_core::input::GamepadEvent::State(frame)); } } } @@ -729,7 +727,7 @@ pub(super) fn input_thread( pad_mask &= !(1 << idx); pad_state[idx] = PadState::default(); let frame = pad_state[idx].frame(idx, pad_mask); - pads.handle(&crate::gamestream::gamepad::GamepadEvent::State(frame)); + pads.handle(&punktfunk_core::input::GamepadEvent::State(frame)); tracing::info!(pad = idx, "gamepad unplugged (native detach)"); } // Fresh feedback bookkeeping so a later re-plug on this index inherits no