From f6c6e4e5947945f1b460746f510a6c3fedac1d99 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 17 Jul 2026 11:52:02 +0200 Subject: [PATCH] refactor(host/W6.2): extract the input-injection backends into the pf-inject crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit inject.rs + inject/* (the per-OS injectors — wlroots virtual-input, KWin fake_input, libei/reis, gamescope-EI on Linux; SendInput on Windows — plus the virtual-gamepad HID stack: DualSense/DualShock4/Switch Pro/Steam Controller/Deck over uhid/usbip and the Windows UMDF drivers, the proto codecs, the injector service, and the uhid manager) move into crates/pf-inject behind the InputInjector trait (plan §W6). It consumes punktfunk_core::input (the neutral GamepadEvent/InputEvent vocabulary, moved to core in W5) + the pf-driver-proto wire contract, and reaches pf-capture only for the Windows gamepad-channel WUDFHost check + the resident-mouse compose-kick hook. The one inject->vdisplay coupling (the libei gamescope-EI backend needs the EIS relay socket path) is broken via a leaf: gamescope_ei_socket_file moves to pf-paths as the shared contract — the gamescope producer (host vdisplay) keeps its session-env-lock wrapper around it, the libei consumer (pf-inject) reads it directly post-retarget. The host keeps a `mod inject { pub use pf_inject::* }` shim so every crate::inject::* path (the native/gamestream input planes + devtest) is unchanged; the heavy input deps (wayland/reis/xkbcommon/usbip + the KWin fake-input protocol XML) moved with the crate. Verified: Linux clippy -D warnings (pf-inject + host nvenc,vulkan-encode,pyrowave --all-targets) + pf-inject 69/69 + host 230/230 tests; Windows clippy -D warnings (pf-inject --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 29 +++++++++ Cargo.toml | 1 + crates/pf-inject/Cargo.toml | 65 +++++++++++++++++++ .../protocols/fake-input.xml | 0 .../src/inject/hidout_dedup.rs | 2 +- .../src/inject/keymap.rs | 0 .../src/inject/linux/dualsense.rs | 8 +-- .../src/inject/linux/dualshock4.rs | 8 +-- .../src/inject/linux/gamepad.rs | 2 +- .../src/inject/linux/kwin_fake_input.rs | 0 .../src/inject/linux/libei.rs | 0 .../src/inject/linux/steam_controller.rs | 14 ++-- .../src/inject/linux/steam_controller2.rs | 10 +-- .../src/inject/linux/steam_gadget.rs | 0 .../src/inject/linux/steam_usbip.rs | 0 .../src/inject/linux/switch_pro.rs | 8 +-- .../src/inject/linux/triton_usbip.rs | 2 +- .../src/inject/linux/wlr.rs | 0 .../src/inject/pad_gate.rs | 0 .../src/inject/pad_slots.rs | 2 +- .../src/inject/proto/dualsense_proto.rs | 2 +- .../src/inject/proto/dualshock4_proto.rs | 2 +- .../src/inject/proto/steam_proto.rs | 0 .../src/inject/proto/steam_remap.rs | 0 .../src/inject/proto/switch_proto.rs | 0 .../src/inject/proto/triton_proto.rs | 0 .../src/inject/service.rs | 6 +- .../src/inject/uhid_manager.rs | 4 +- .../inject/windows/dualsense_edge_windows.rs | 4 +- .../src/inject/windows/dualsense_windows.rs | 8 +-- .../src/inject/windows/dualshock4_windows.rs | 8 +-- .../src/inject/windows/gamepad_raii.rs | 12 +++- .../src/inject/windows/gamepad_windows.rs | 2 +- .../src/inject/windows/mouse_windows.rs | 2 +- .../src/inject/windows/sendinput.rs | 8 +-- .../src/inject/windows/steam_deck_windows.rs | 2 +- .../src/inject.rs => pf-inject/src/lib.rs} | 16 +++-- crates/pf-paths/src/lib.rs | 14 ++++ crates/punktfunk-host/Cargo.toml | 4 ++ crates/punktfunk-host/src/main.rs | 7 +- crates/punktfunk-host/src/vdisplay.rs | 4 +- .../src/vdisplay/linux/gamescope.rs | 9 ++- crates/punktfunk-host/src/vdisplay/routing.rs | 7 -- 43 files changed, 199 insertions(+), 73 deletions(-) create mode 100644 crates/pf-inject/Cargo.toml rename crates/{punktfunk-host => pf-inject}/protocols/fake-input.xml (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/hidout_dedup.rs (98%) rename crates/{punktfunk-host => pf-inject}/src/inject/keymap.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/dualsense.rs (99%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/dualshock4.rs (98%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/gamepad.rs (99%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/kwin_fake_input.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/libei.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/steam_controller.rs (98%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/steam_controller2.rs (98%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/steam_gadget.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/steam_usbip.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/switch_pro.rs (97%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/triton_usbip.rs (99%) rename crates/{punktfunk-host => pf-inject}/src/inject/linux/wlr.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/pad_gate.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/pad_slots.rs (99%) rename crates/{punktfunk-host => pf-inject}/src/inject/proto/dualsense_proto.rs (99%) rename crates/{punktfunk-host => pf-inject}/src/inject/proto/dualshock4_proto.rs (99%) rename crates/{punktfunk-host => pf-inject}/src/inject/proto/steam_proto.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/proto/steam_remap.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/proto/switch_proto.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/proto/triton_proto.rs (100%) rename crates/{punktfunk-host => pf-inject}/src/inject/service.rs (98%) rename crates/{punktfunk-host => pf-inject}/src/inject/uhid_manager.rs (99%) rename crates/{punktfunk-host => pf-inject}/src/inject/windows/dualsense_edge_windows.rs (95%) rename crates/{punktfunk-host => pf-inject}/src/inject/windows/dualsense_windows.rs (98%) rename crates/{punktfunk-host => pf-inject}/src/inject/windows/dualshock4_windows.rs (97%) rename crates/{punktfunk-host => pf-inject}/src/inject/windows/gamepad_raii.rs (97%) rename crates/{punktfunk-host => pf-inject}/src/inject/windows/gamepad_windows.rs (99%) rename crates/{punktfunk-host => pf-inject}/src/inject/windows/mouse_windows.rs (99%) rename crates/{punktfunk-host => pf-inject}/src/inject/windows/sendinput.rs (98%) rename crates/{punktfunk-host => pf-inject}/src/inject/windows/steam_deck_windows.rs (99%) rename crates/{punktfunk-host/src/inject.rs => pf-inject/src/lib.rs} (95%) diff --git a/Cargo.lock b/Cargo.lock index 3f006047..0b128405 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2880,6 +2880,34 @@ dependencies = [ name = "pf-host-config" version = "0.12.0" +[[package]] +name = "pf-inject" +version = "0.12.0" +dependencies = [ + "anyhow", + "ashpd", + "futures-util", + "libc", + "parking_lot", + "pf-capture", + "pf-driver-proto", + "pf-host-config", + "pf-paths", + "punktfunk-core", + "reis", + "tokio", + "tracing", + "usbip-sim", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)", + "xkbcommon", +] + [[package]] name = "pf-paths" version = "0.12.0" @@ -3229,6 +3257,7 @@ dependencies = [ "pf-frame", "pf-gpu", "pf-host-config", + "pf-inject", "pf-paths", "pf-win-display", "pf-zerocopy", diff --git a/Cargo.toml b/Cargo.toml index f8042282..9793eb2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "crates/pf-win-display", "crates/pf-encode", "crates/pf-capture", + "crates/pf-inject", "crates/pyrowave-sys", "clients/probe", "clients/linux", diff --git a/crates/pf-inject/Cargo.toml b/crates/pf-inject/Cargo.toml new file mode 100644 index 00000000..faf9741d --- /dev/null +++ b/crates/pf-inject/Cargo.toml @@ -0,0 +1,65 @@ +# Input injection (plan §W6): the per-OS injector backends (wlroots virtual-input, KWin fake_input, +# libei/reis, gamescope-EI on Linux; SendInput on Windows) + the virtual-gamepad HID stack (DualSense/ +# DualShock4/Switch Pro/Steam Controller/Deck over uhid/usbip and the Windows UMDF drivers), extracted +# into a subsystem crate. Consumes punktfunk_core::input (the neutral GamepadEvent/InputEvent vocabulary, +# moved to core in W5) and the pf-driver-proto wire contract; NEVER reaches the orchestrator (the one +# gamescope-EI socket path is the shared pf-paths contract, not a vdisplay reach-in). +[package] +name = "pf-inject" +version = "0.12.0" +edition = "2021" +rust-version.workspace = true +license = "MIT OR Apache-2.0" +description = "punktfunk host input injection: per-OS keyboard/mouse injectors + the virtual-gamepad HID backends behind one InputInjector trait." +publish = false + +[dependencies] +punktfunk-core = { path = "../punktfunk-core", features = ["quic"] } +pf-driver-proto = { path = "../pf-driver-proto" } +pf-host-config = { path = "../pf-host-config" } +pf-paths = { path = "../pf-paths" } +# The Windows gamepad-channel bootstrap reuses the IDD-push WUDFHost verification + the resident-mouse +# compose-kick hook (both live in pf-capture). +pf-capture = { path = "../pf-capture" } +anyhow = "1" +tracing = "0.1" + +[target.'cfg(target_os = "linux")'.dependencies] +libc = "0.2" +parking_lot = "0.12" +# The RemoteDesktop portal for the libei injector on KWin/GNOME (headless grant via kde-authorized). +ashpd = { version = "0.13", features = ["remote_desktop"] } +# Input injection into headless Sway via the wlroots virtual-input Wayland protocols. +wayland-client = "0.31" +wayland-protocols-wlr = { version = "0.3", features = ["client"] } +wayland-protocols-misc = { version = "0.3", features = ["client"] } +wayland-protocols = { version = "0.32", features = ["client"] } +# Codegen for KDE's `org_kde_kwin_fake_input` (vendored in `protocols/fake-input.xml`); the generated +# interface tables reference `wayland-backend`. +wayland-scanner = "0.31" +wayland-backend = "0.3" +# libei (EI sender) for the portable input path on KWin/GNOME (RemoteDesktop portal) + gamescope-EI. +reis = { version = "0.6.1", features = ["tokio"] } +futures-util = "0.3" +tokio = { version = "1", features = ["rt", "rt-multi-thread", "net", "time"] } +# Builds/validates the xkb keymap uploaded to the virtual keyboard + tracks modifier state. +xkbcommon = "0.8" +# Vendored + trimmed usbip server core — presents a virtual Steam Deck over USB/IP for Steam Input. +usbip-sim = { path = "../punktfunk-host/vendor/usbip-sim" } + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Security_Authorization", + "Win32_Devices_DeviceAndDriverInstallation", + "Win32_Devices_Enumeration_Pnp", + # SwDeviceCreate's SW_DEVICE_CREATE_INFO references DEVPROPKEY (Properties). + "Win32_Devices_Properties", + "Win32_System_Memory", + "Win32_System_IO", + "Win32_System_StationsAndDesktops", + "Win32_System_Threading", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_WindowsAndMessaging", +] } diff --git a/crates/punktfunk-host/protocols/fake-input.xml b/crates/pf-inject/protocols/fake-input.xml similarity index 100% rename from crates/punktfunk-host/protocols/fake-input.xml rename to crates/pf-inject/protocols/fake-input.xml diff --git a/crates/punktfunk-host/src/inject/hidout_dedup.rs b/crates/pf-inject/src/inject/hidout_dedup.rs similarity index 98% rename from crates/punktfunk-host/src/inject/hidout_dedup.rs rename to crates/pf-inject/src/inject/hidout_dedup.rs index 11dbe080..5e87a80b 100644 --- a/crates/punktfunk-host/src/inject/hidout_dedup.rs +++ b/crates/pf-inject/src/inject/hidout_dedup.rs @@ -1,6 +1,6 @@ //! Per-pad dedup for the rich HID-output feedback plane (0xCD), carved out of `dualsense_proto` //! (plan §W4 — it is device-agnostic, shared by the DualSense/DS4/Deck managers via -//! [`crate::inject::uhid_manager`], not DualSense-specific). A game bundles rumble + lightbar + +//! [`crate::uhid_manager`], not DualSense-specific). A game bundles rumble + lightbar + //! LEDs + adaptive triggers into one output report, so a merely-rumbling pad re-sends unchanged //! rich state every report; this forwards only genuine changes (one-shot pulses always fire). diff --git a/crates/punktfunk-host/src/inject/keymap.rs b/crates/pf-inject/src/inject/keymap.rs similarity index 100% rename from crates/punktfunk-host/src/inject/keymap.rs rename to crates/pf-inject/src/inject/keymap.rs diff --git a/crates/punktfunk-host/src/inject/linux/dualsense.rs b/crates/pf-inject/src/inject/linux/dualsense.rs similarity index 99% rename from crates/punktfunk-host/src/inject/linux/dualsense.rs rename to crates/pf-inject/src/inject/linux/dualsense.rs index dc9c9d4a..63ba5d22 100644 --- a/crates/punktfunk-host/src/inject/linux/dualsense.rs +++ b/crates/pf-inject/src/inject/linux/dualsense.rs @@ -17,7 +17,7 @@ use super::dualsense_proto::{ DS_EDGE_PRODUCT, DS_FEATURE_CALIBRATION, DS_FEATURE_FIRMWARE, DS_INPUT_REPORT_LEN, DS_PRODUCT, DS_TOUCH_H, DS_TOUCH_W, DS_VENDOR, DUALSENSE_EDGE_RDESC, DUALSENSE_RDESC, }; -use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; +use crate::uhid_manager::{PadFeedback, PadProto, UhidManager}; use anyhow::{Context, Result}; use punktfunk_core::quic::RichInput; use std::fs::{File, OpenOptions}; @@ -232,13 +232,13 @@ impl Drop for DualSensePad { pub struct DsLinuxProto { /// Fallback policy for the Steam back grips a client may send (the DualSense has no back-button /// HID slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop. - remap: crate::inject::steam_remap::RemapConfig, + remap: crate::steam_remap::RemapConfig, } impl Default for DsLinuxProto { fn default() -> DsLinuxProto { DsLinuxProto { - remap: crate::inject::steam_remap::RemapConfig::from_env(), + remap: crate::steam_remap::RemapConfig::from_env(), } } } @@ -268,7 +268,7 @@ impl PadProto for DsLinuxProto { 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); + let buttons = crate::steam_remap::fold_paddles(f.buttons, self.remap.paddles); let mut s = DsState::from_gamepad( buttons, f.ls_x, diff --git a/crates/punktfunk-host/src/inject/linux/dualshock4.rs b/crates/pf-inject/src/inject/linux/dualshock4.rs similarity index 98% rename from crates/punktfunk-host/src/inject/linux/dualshock4.rs rename to crates/pf-inject/src/inject/linux/dualshock4.rs index afb822f5..23f4fcf0 100644 --- a/crates/punktfunk-host/src/inject/linux/dualshock4.rs +++ b/crates/pf-inject/src/inject/linux/dualshock4.rs @@ -18,7 +18,7 @@ use super::dualshock4_proto::{ parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_PRODUCT, DS4_TOUCH_H, DS4_TOUCH_W, DS4_VENDOR, }; -use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; +use crate::uhid_manager::{PadFeedback, PadProto, UhidManager}; use anyhow::{Context, Result}; use punktfunk_core::quic::{HidOutput, RichInput}; use std::fs::{File, OpenOptions}; @@ -302,13 +302,13 @@ impl Drop for DualShock4Pad { pub struct Ds4LinuxProto { /// Fallback policy for the Steam back grips a client may send (the DS4 has no back-button HID /// slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop. - remap: crate::inject::steam_remap::RemapConfig, + remap: crate::steam_remap::RemapConfig, } impl Default for Ds4LinuxProto { fn default() -> Ds4LinuxProto { Ds4LinuxProto { - remap: crate::inject::steam_remap::RemapConfig::from_env(), + remap: crate::steam_remap::RemapConfig::from_env(), } } } @@ -338,7 +338,7 @@ impl PadProto for Ds4LinuxProto { 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); + let buttons = crate::steam_remap::fold_paddles(f.buttons, self.remap.paddles); let mut s = DsState::from_gamepad( buttons, f.ls_x, diff --git a/crates/punktfunk-host/src/inject/linux/gamepad.rs b/crates/pf-inject/src/inject/linux/gamepad.rs similarity index 99% rename from crates/punktfunk-host/src/inject/linux/gamepad.rs rename to crates/pf-inject/src/inject/linux/gamepad.rs index a357251c..6ed7dacc 100644 --- a/crates/punktfunk-host/src/inject/linux/gamepad.rs +++ b/crates/pf-inject/src/inject/linux/gamepad.rs @@ -18,7 +18,7 @@ // Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). #![deny(clippy::undocumented_unsafe_blocks)] -use crate::inject::pad_slots::PadSlots; +use crate::pad_slots::PadSlots; use anyhow::{bail, Result}; use punktfunk_core::input::{gamepad, GamepadFrame, MAX_PADS}; use std::collections::HashMap; diff --git a/crates/punktfunk-host/src/inject/linux/kwin_fake_input.rs b/crates/pf-inject/src/inject/linux/kwin_fake_input.rs similarity index 100% rename from crates/punktfunk-host/src/inject/linux/kwin_fake_input.rs rename to crates/pf-inject/src/inject/linux/kwin_fake_input.rs diff --git a/crates/punktfunk-host/src/inject/linux/libei.rs b/crates/pf-inject/src/inject/linux/libei.rs similarity index 100% rename from crates/punktfunk-host/src/inject/linux/libei.rs rename to crates/pf-inject/src/inject/linux/libei.rs diff --git a/crates/punktfunk-host/src/inject/linux/steam_controller.rs b/crates/pf-inject/src/inject/linux/steam_controller.rs similarity index 98% rename from crates/punktfunk-host/src/inject/linux/steam_controller.rs rename to crates/pf-inject/src/inject/linux/steam_controller.rs index d7c7b464..687cb5a8 100644 --- a/crates/punktfunk-host/src/inject/linux/steam_controller.rs +++ b/crates/pf-inject/src/inject/linux/steam_controller.rs @@ -23,7 +23,7 @@ use super::steam_proto::{ btn, parse_steam_output, sc_from_gamepad, serial_reply, serialize_deck_state, serialize_sc_state, SteamModel, SteamState, STEAMDECK_RDESC, STEAM_REPORT_LEN, STEAM_VENDOR, }; -use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; +use crate::uhid_manager::{PadFeedback, PadProto, UhidManager}; use anyhow::{Context, Result}; use punktfunk_core::quic::RichInput; use std::fs::{File, OpenOptions}; @@ -264,8 +264,8 @@ impl Drop for SteamDeckPad { /// `type Pad` in the `PadProto` impl, a public trait.) pub enum DeckTransport { Uhid(SteamDeckPad), - Gadget(crate::inject::steam_gadget::SteamDeckGadget), - Usbip(crate::inject::steam_usbip::SteamDeckUsbip), + Gadget(crate::steam_gadget::SteamDeckGadget), + Usbip(crate::steam_usbip::SteamDeckUsbip), } impl DeckTransport { @@ -332,7 +332,7 @@ fn warn_if_inputplumber() { /// usbip, and one lacking both still gets a working (if non-promoted) UHID pad. fn open_transport(idx: u8) -> Result { warn_if_inputplumber(); - use crate::inject::{steam_gadget, steam_usbip}; + use crate::{steam_gadget, steam_usbip}; // 1. raw_gadget — the validated SteamOS fast-path (default on there). if steam_gadget::gadget_preferred() { steam_gadget::ensure_modules(); @@ -474,13 +474,13 @@ pub type SteamControllerManager = UhidManager; /// registers neither FF rumble nor a sensors evdev for this model (feedback stays empty). pub struct ScProto { /// Fallback policy for the wire paddles beyond the SC's two grips (PADDLE3/4). - remap: crate::inject::steam_remap::RemapConfig, + remap: crate::steam_remap::RemapConfig, } impl Default for ScProto { fn default() -> ScProto { ScProto { - remap: crate::inject::steam_remap::RemapConfig::from_env(), + remap: crate::steam_remap::RemapConfig::from_env(), } } } @@ -515,7 +515,7 @@ impl PadProto for ScProto { ) -> SteamState { use punktfunk_core::input::gamepad as gs; let native = f.buttons & (gs::BTN_PADDLE1 | gs::BTN_PADDLE2); - let folded = crate::inject::steam_remap::fold_paddles( + let folded = crate::steam_remap::fold_paddles( f.buttons & !(gs::BTN_PADDLE1 | gs::BTN_PADDLE2), self.remap.paddles, ); diff --git a/crates/punktfunk-host/src/inject/linux/steam_controller2.rs b/crates/pf-inject/src/inject/linux/steam_controller2.rs similarity index 98% rename from crates/punktfunk-host/src/inject/linux/steam_controller2.rs rename to crates/pf-inject/src/inject/linux/steam_controller2.rs index 2f11dab3..e3757580 100644 --- a/crates/punktfunk-host/src/inject/linux/steam_controller2.rs +++ b/crates/pf-inject/src/inject/linux/steam_controller2.rs @@ -23,7 +23,7 @@ use super::triton_proto::{ triton_serial, triton_unit_id, TritonState, TRITON_RDESC, TRITON_STATE_LEN, TRITON_VENDOR, TRITON_WIRED_PRODUCT, }; -use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; +use crate::uhid_manager::{PadFeedback, PadProto, UhidManager}; use anyhow::{Context, Result}; use punktfunk_core::quic::{HidOutput, RichInput, HID_RAW_FEATURE, HID_RAW_OUTPUT}; use std::fs::{File, OpenOptions}; @@ -236,7 +236,7 @@ impl Drop for TritonPad { /// UHID as the degraded fallback — the same ladder shape as the Deck's [`super::steam_controller`], /// minus the gadget rung (no captured gadget layout for the Triton, and usbip is universal). pub enum TritonTransport { - Usbip(crate::inject::triton_usbip::TritonUsbip), + Usbip(crate::triton_usbip::TritonUsbip), Uhid(TritonPad), } @@ -273,11 +273,11 @@ impl TritonTransport { /// (on-glass 2026-07-15) to ignore the UHID leg, so reaching the fallback means the pad exists as /// hidraw only — flagged loudly, with the vhci_hcd remedy in the log. fn open_transport(idx: u8, puck: bool) -> Result { - if crate::inject::steam_usbip::usbip_preferred() { + if crate::steam_usbip::usbip_preferred() { let opened = if puck { - crate::inject::triton_usbip::TritonUsbip::open_puck(idx) + crate::triton_usbip::TritonUsbip::open_puck(idx) } else { - crate::inject::triton_usbip::TritonUsbip::open(idx) + crate::triton_usbip::TritonUsbip::open(idx) }; match opened { Ok(u) => return Ok(TritonTransport::Usbip(u)), diff --git a/crates/punktfunk-host/src/inject/linux/steam_gadget.rs b/crates/pf-inject/src/inject/linux/steam_gadget.rs similarity index 100% rename from crates/punktfunk-host/src/inject/linux/steam_gadget.rs rename to crates/pf-inject/src/inject/linux/steam_gadget.rs diff --git a/crates/punktfunk-host/src/inject/linux/steam_usbip.rs b/crates/pf-inject/src/inject/linux/steam_usbip.rs similarity index 100% rename from crates/punktfunk-host/src/inject/linux/steam_usbip.rs rename to crates/pf-inject/src/inject/linux/steam_usbip.rs diff --git a/crates/punktfunk-host/src/inject/linux/switch_pro.rs b/crates/pf-inject/src/inject/linux/switch_pro.rs similarity index 97% rename from crates/punktfunk-host/src/inject/linux/switch_pro.rs rename to crates/pf-inject/src/inject/linux/switch_pro.rs index 06990b2d..086d5be8 100644 --- a/crates/punktfunk-host/src/inject/linux/switch_pro.rs +++ b/crates/pf-inject/src/inject/linux/switch_pro.rs @@ -22,7 +22,7 @@ use super::switch_proto::{ serialize_report_0x30, spi_flash_read, switch_mac, SwitchOutput, SwitchState, PROCON_RDESC, SWITCH_PRODUCT, SWITCH_REPORT_LEN, SWITCH_VENDOR, }; -use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; +use crate::uhid_manager::{PadFeedback, PadProto, UhidManager}; use anyhow::{Context, Result}; use punktfunk_core::quic::{HidOutput, RichInput}; use std::fs::{File, OpenOptions}; @@ -241,13 +241,13 @@ impl Drop for SwitchProPad { pub struct SwitchProProto { /// Fallback policy for the Steam back grips a client may send (a Pro Controller has no /// back-button slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop. - remap: crate::inject::steam_remap::RemapConfig, + remap: crate::steam_remap::RemapConfig, } impl Default for SwitchProProto { fn default() -> SwitchProProto { SwitchProProto { - remap: crate::inject::steam_remap::RemapConfig::from_env(), + remap: crate::steam_remap::RemapConfig::from_env(), } } } @@ -279,7 +279,7 @@ impl PadProto for SwitchProProto { prev: &SwitchState, f: &punktfunk_core::input::GamepadFrame, ) -> SwitchState { - let buttons = crate::inject::steam_remap::fold_paddles(f.buttons, self.remap.paddles); + let buttons = crate::steam_remap::fold_paddles(f.buttons, self.remap.paddles); let mut s = SwitchState::from_gamepad( buttons, f.ls_x, diff --git a/crates/punktfunk-host/src/inject/linux/triton_usbip.rs b/crates/pf-inject/src/inject/linux/triton_usbip.rs similarity index 99% rename from crates/punktfunk-host/src/inject/linux/triton_usbip.rs rename to crates/pf-inject/src/inject/linux/triton_usbip.rs index bbfc6dab..382f6644 100644 --- a/crates/punktfunk-host/src/inject/linux/triton_usbip.rs +++ b/crates/pf-inject/src/inject/linux/triton_usbip.rs @@ -40,7 +40,7 @@ const PUCK_MANAGEMENT_RDESC: &[u8] = &[ /// Everything Steam wrote to the device since the last service pass. #[derive(Debug, Default)] -pub(crate) struct TritonUsbFeedback { +pub struct TritonUsbFeedback { /// `(low, high)` from the last `0x80` rumble output report. pub rumble: Option<(u16, u16)>, /// Raw reports to forward, `(kind, bytes)` — kind = `HID_RAW_OUTPUT`/`HID_RAW_FEATURE`. diff --git a/crates/punktfunk-host/src/inject/linux/wlr.rs b/crates/pf-inject/src/inject/linux/wlr.rs similarity index 100% rename from crates/punktfunk-host/src/inject/linux/wlr.rs rename to crates/pf-inject/src/inject/linux/wlr.rs diff --git a/crates/punktfunk-host/src/inject/pad_gate.rs b/crates/pf-inject/src/inject/pad_gate.rs similarity index 100% rename from crates/punktfunk-host/src/inject/pad_gate.rs rename to crates/pf-inject/src/inject/pad_gate.rs diff --git a/crates/punktfunk-host/src/inject/pad_slots.rs b/crates/pf-inject/src/inject/pad_slots.rs similarity index 99% rename from crates/punktfunk-host/src/inject/pad_slots.rs rename to crates/pf-inject/src/inject/pad_slots.rs index c7a24e3b..ad4a0235 100644 --- a/crates/punktfunk-host/src/inject/pad_slots.rs +++ b/crates/pf-inject/src/inject/pad_slots.rs @@ -1,7 +1,7 @@ //! Shared virtual-pad slot table + creation lifecycle, used by every backend manager (Linux //! uinput/uhid, Windows XUSB/UMDF). See [`PadSlots`]. -use crate::inject::pad_gate::PadGate; +use crate::pad_gate::PadGate; use anyhow::Result; use punktfunk_core::input::MAX_PADS; use std::time::Instant; diff --git a/crates/punktfunk-host/src/inject/proto/dualsense_proto.rs b/crates/pf-inject/src/inject/proto/dualsense_proto.rs similarity index 99% rename from crates/punktfunk-host/src/inject/proto/dualsense_proto.rs rename to crates/pf-inject/src/inject/proto/dualsense_proto.rs index d632ecd9..74e5d4ce 100644 --- a/crates/punktfunk-host/src/inject/proto/dualsense_proto.rs +++ b/crates/pf-inject/src/inject/proto/dualsense_proto.rs @@ -470,7 +470,7 @@ pub struct DsFeedback { /// `(low, high)` motor levels (0..=0xFFFF), if a report carried them. pub rumble: Option<(u16, u16)>, /// Whether a fresh output report was seen this poll (set by the backend's section poll, not by - /// the parser) — the game-activity signal the [`UhidManager`](crate::inject::uhid_manager) + /// the parser) — the game-activity signal the [`UhidManager`](crate::uhid_manager) /// abandoned-rumble force-off keys on. pub fresh: bool, } diff --git a/crates/punktfunk-host/src/inject/proto/dualshock4_proto.rs b/crates/pf-inject/src/inject/proto/dualshock4_proto.rs similarity index 99% rename from crates/punktfunk-host/src/inject/proto/dualshock4_proto.rs rename to crates/pf-inject/src/inject/proto/dualshock4_proto.rs index 2f2747f5..9752c386 100644 --- a/crates/punktfunk-host/src/inject/proto/dualshock4_proto.rs +++ b/crates/pf-inject/src/inject/proto/dualshock4_proto.rs @@ -81,7 +81,7 @@ pub struct Ds4Feedback { /// Lightbar RGB, if the report carried it (deduped by the manager). pub led: Option<(u8, u8, u8)>, /// Whether a fresh output report was seen this poll (set by the backend's section poll, not by - /// the parser) — the game-activity signal the [`UhidManager`](crate::inject::uhid_manager) + /// the parser) — the game-activity signal the [`UhidManager`](crate::uhid_manager) /// abandoned-rumble force-off keys on. pub fresh: bool, } diff --git a/crates/punktfunk-host/src/inject/proto/steam_proto.rs b/crates/pf-inject/src/inject/proto/steam_proto.rs similarity index 100% rename from crates/punktfunk-host/src/inject/proto/steam_proto.rs rename to crates/pf-inject/src/inject/proto/steam_proto.rs diff --git a/crates/punktfunk-host/src/inject/proto/steam_remap.rs b/crates/pf-inject/src/inject/proto/steam_remap.rs similarity index 100% rename from crates/punktfunk-host/src/inject/proto/steam_remap.rs rename to crates/pf-inject/src/inject/proto/steam_remap.rs diff --git a/crates/punktfunk-host/src/inject/proto/switch_proto.rs b/crates/pf-inject/src/inject/proto/switch_proto.rs similarity index 100% rename from crates/punktfunk-host/src/inject/proto/switch_proto.rs rename to crates/pf-inject/src/inject/proto/switch_proto.rs diff --git a/crates/punktfunk-host/src/inject/proto/triton_proto.rs b/crates/pf-inject/src/inject/proto/triton_proto.rs similarity index 100% rename from crates/punktfunk-host/src/inject/proto/triton_proto.rs rename to crates/pf-inject/src/inject/proto/triton_proto.rs diff --git a/crates/punktfunk-host/src/inject/service.rs b/crates/pf-inject/src/inject/service.rs similarity index 98% rename from crates/punktfunk-host/src/inject/service.rs rename to crates/pf-inject/src/inject/service.rs index 43e159a2..0b2182f9 100644 --- a/crates/punktfunk-host/src/inject/service.rs +++ b/crates/pf-inject/src/inject/service.rs @@ -12,12 +12,12 @@ use super::*; /// punktfunk/1 plane forward their decoded keyboard/mouse events here instead of injecting inline, so /// a slow inject (a portal stall, a desktop switch) never head-blocks the network thread's /// keepalive/retransmit servicing. -pub(crate) struct InjectorService { +pub struct InjectorService { tx: std::sync::mpsc::Sender, } impl InjectorService { - pub(crate) fn start() -> InjectorService { + pub fn start() -> InjectorService { // Windows: make sure the process-wide resident virtual HID mouse exists (idempotent). // Without a pointing device present, win32k reports no cursor and DWM composites none // into the IDD frame — SendInput injection alone moves an invisible pointer. @@ -36,7 +36,7 @@ impl InjectorService { /// A sender a session/plane forwards its pointer/keyboard events to. Cloned per caller; dropping a /// clone does NOT stop the service (it runs while any sender — incl. the service's own — lives). - pub(crate) fn sender(&self) -> std::sync::mpsc::Sender { + pub fn sender(&self) -> std::sync::mpsc::Sender { self.tx.clone() } } diff --git a/crates/punktfunk-host/src/inject/uhid_manager.rs b/crates/pf-inject/src/inject/uhid_manager.rs similarity index 99% rename from crates/punktfunk-host/src/inject/uhid_manager.rs rename to crates/pf-inject/src/inject/uhid_manager.rs index bc6b43ee..6a25ef24 100644 --- a/crates/punktfunk-host/src/inject/uhid_manager.rs +++ b/crates/pf-inject/src/inject/uhid_manager.rs @@ -6,8 +6,8 @@ //! Windows XUSB) write frames straight through with no state vec / heartbeat / rich plane, so they //! use [`PadSlots`] directly instead. -use crate::inject::hidout_dedup::HidoutDedup; -use crate::inject::pad_slots::PadSlots; +use crate::hidout_dedup::HidoutDedup; +use crate::pad_slots::PadSlots; use anyhow::Result; use punktfunk_core::input::{GamepadEvent, GamepadFrame, MAX_PADS}; use punktfunk_core::quic::{HidOutput, RichInput}; diff --git a/crates/punktfunk-host/src/inject/windows/dualsense_edge_windows.rs b/crates/pf-inject/src/inject/windows/dualsense_edge_windows.rs similarity index 95% rename from crates/punktfunk-host/src/inject/windows/dualsense_edge_windows.rs rename to crates/pf-inject/src/inject/windows/dualsense_edge_windows.rs index c169e58c..0b860bae 100644 --- a/crates/punktfunk-host/src/inject/windows/dualsense_edge_windows.rs +++ b/crates/pf-inject/src/inject/windows/dualsense_edge_windows.rs @@ -10,7 +10,7 @@ use super::dualsense_proto::{edge_paddle_bits, DsState, DS_TOUCH_H, DS_TOUCH_W}; use super::dualsense_windows::{DsWinPad, WinDsIdentity}; -use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; +use crate::uhid_manager::{PadFeedback, PadProto, UhidManager}; use anyhow::Result; use punktfunk_core::quic::RichInput; @@ -85,6 +85,6 @@ impl PadProto for DsEdgeWinProto { } /// All virtual DualSense Edge pads of a session — the Windows analogue of -/// [`DualSenseEdgeManager`](crate::inject::dualsense::DualSenseEdgeManager), with the same method +/// [`DualSenseEdgeManager`](crate::dualsense::DualSenseEdgeManager), with the same method /// surface (via the shared [`UhidManager`]) as the other Windows pad managers. pub type DualSenseEdgeWindowsManager = UhidManager; diff --git a/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs b/crates/pf-inject/src/inject/windows/dualsense_windows.rs similarity index 98% rename from crates/punktfunk-host/src/inject/windows/dualsense_windows.rs rename to crates/pf-inject/src/inject/windows/dualsense_windows.rs index cd84a441..69334500 100644 --- a/crates/punktfunk-host/src/inject/windows/dualsense_windows.rs +++ b/crates/pf-inject/src/inject/windows/dualsense_windows.rs @@ -22,7 +22,7 @@ use super::dualsense_proto::{ DS_TOUCH_W, }; use super::gamepad_raii::{sw_create_cb, PadChannel, SwCreateCtx}; -use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; +use crate::uhid_manager::{PadFeedback, PadProto, UhidManager}; use anyhow::{anyhow, Result}; use punktfunk_core::quic::RichInput; use std::ffi::c_void; @@ -410,13 +410,13 @@ impl DsWinPad { pub struct DsWinProto { /// Fallback policy for the Steam back grips a client may send (the DualSense has no back-button /// HID slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop. Parity with `linux/dualsense.rs`. - remap: crate::inject::steam_remap::RemapConfig, + remap: crate::steam_remap::RemapConfig, } impl Default for DsWinProto { fn default() -> DsWinProto { DsWinProto { - remap: crate::inject::steam_remap::RemapConfig::from_env(), + remap: crate::steam_remap::RemapConfig::from_env(), } } } @@ -447,7 +447,7 @@ impl PadProto for DsWinProto { 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); + let buttons = crate::steam_remap::fold_paddles(f.buttons, self.remap.paddles); let mut s = DsState::from_gamepad( buttons, f.ls_x, diff --git a/crates/punktfunk-host/src/inject/windows/dualshock4_windows.rs b/crates/pf-inject/src/inject/windows/dualshock4_windows.rs similarity index 97% rename from crates/punktfunk-host/src/inject/windows/dualshock4_windows.rs rename to crates/pf-inject/src/inject/windows/dualshock4_windows.rs index cb4f3243..4325b00a 100644 --- a/crates/punktfunk-host/src/inject/windows/dualshock4_windows.rs +++ b/crates/pf-inject/src/inject/windows/dualshock4_windows.rs @@ -16,7 +16,7 @@ use super::dualshock4_proto::{ parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W, }; use super::gamepad_raii::PadChannel; -use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; +use crate::uhid_manager::{PadFeedback, PadProto, UhidManager}; use anyhow::Result; use punktfunk_core::quic::{HidOutput, RichInput}; use std::time::Duration; @@ -154,13 +154,13 @@ impl Ds4WinPad { pub struct Ds4WinProto { /// Fallback policy for the Steam back grips a client may send (the DS4 has no back-button HID /// slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop. Parity with `linux/dualshock4.rs`. - remap: crate::inject::steam_remap::RemapConfig, + remap: crate::steam_remap::RemapConfig, } impl Default for Ds4WinProto { fn default() -> Ds4WinProto { Ds4WinProto { - remap: crate::inject::steam_remap::RemapConfig::from_env(), + remap: crate::steam_remap::RemapConfig::from_env(), } } } @@ -191,7 +191,7 @@ impl PadProto for Ds4WinProto { 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); + let buttons = crate::steam_remap::fold_paddles(f.buttons, self.remap.paddles); let mut s = DsState::from_gamepad( buttons, f.ls_x, diff --git a/crates/punktfunk-host/src/inject/windows/gamepad_raii.rs b/crates/pf-inject/src/inject/windows/gamepad_raii.rs similarity index 97% rename from crates/punktfunk-host/src/inject/windows/gamepad_raii.rs rename to crates/pf-inject/src/inject/windows/gamepad_raii.rs index 1d3b98af..ae7de599 100644 --- a/crates/punktfunk-host/src/inject/windows/gamepad_raii.rs +++ b/crates/pf-inject/src/inject/windows/gamepad_raii.rs @@ -140,7 +140,9 @@ impl Shm { pub(super) fn create_named(name: &HSTRING, size: usize) -> Result { // Build the descriptor ONCE and reuse it across the squat-retry loop — it (and the OS // allocation it owns) lives to the end of this fn, so it outlives every create below. - let sa = sddl_sa(w!("D:(A;;GA;;;SY)(A;;GA;;;LS)"))?; + // `D:P` (protected) — strip any inherited ACEs so only SYSTEM + LocalService are granted, + // matching the intent of the other named objects (security-review 2026-07-17). + let sa = sddl_sa(w!("D:P(A;;GA;;;SY)(A;;GA;;;LS)"))?; for attempt in 0..5 { if attempt > 0 { std::thread::sleep(Duration::from_millis(50)); @@ -628,7 +630,13 @@ impl DriverAttach { fn driver_store_inventory() -> &'static str { static INV: OnceLock = OnceLock::new(); INV.get_or_init(|| { - std::process::Command::new("pnputil") + // Resolve pnputil by full System32 path — the host runs as SYSTEM and must not trust PATH / + // the CreateProcess search (which checks the launching EXE's own dir first), or a planted + // `pnputil.exe` beside the host binary would run elevated (security-review 2026-07-17). + let pnputil = std::env::var("SystemRoot") + .map(|r| format!(r"{r}\System32\pnputil.exe")) + .unwrap_or_else(|_| "pnputil.exe".to_string()); + std::process::Command::new(&pnputil) .arg("/enum-drivers") .output() .map(|o| String::from_utf8_lossy(&o.stdout).to_ascii_lowercase()) diff --git a/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs b/crates/pf-inject/src/inject/windows/gamepad_windows.rs similarity index 99% rename from crates/punktfunk-host/src/inject/windows/gamepad_windows.rs rename to crates/pf-inject/src/inject/windows/gamepad_windows.rs index 61183f00..2b404426 100644 --- a/crates/punktfunk-host/src/inject/windows/gamepad_windows.rs +++ b/crates/pf-inject/src/inject/windows/gamepad_windows.rs @@ -13,7 +13,7 @@ //! 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::inject::pad_slots::PadSlots; +use crate::pad_slots::PadSlots; use anyhow::{anyhow, Result}; use punktfunk_core::input::{GamepadEvent, MAX_PADS}; use std::ffi::c_void; diff --git a/crates/punktfunk-host/src/inject/windows/mouse_windows.rs b/crates/pf-inject/src/inject/windows/mouse_windows.rs similarity index 99% rename from crates/punktfunk-host/src/inject/windows/mouse_windows.rs rename to crates/pf-inject/src/inject/windows/mouse_windows.rs index 4b034110..279f1928 100644 --- a/crates/punktfunk-host/src/inject/windows/mouse_windows.rs +++ b/crates/pf-inject/src/inject/windows/mouse_windows.rs @@ -222,7 +222,7 @@ fn perform_kick(m: &mut VirtualMouse, aim: KickAim) { } /// Make sure the resident virtual mouse exists (idempotent, best-effort). Called whenever an -/// [`InjectorService`](crate::inject::InjectorService) starts — multiple services (native + +/// [`InjectorService`](crate::InjectorService) starts — multiple services (native + /// GameStream) share the ONE process-wide mouse, guarded here. Spawns a keeper thread that owns /// the devnode for the process lifetime and pumps the channel at a slow tick (delivery is eager at /// open; the pump only handles a late WUDFHost + feeds the attach diagnostics). diff --git a/crates/punktfunk-host/src/inject/windows/sendinput.rs b/crates/pf-inject/src/inject/windows/sendinput.rs similarity index 98% rename from crates/punktfunk-host/src/inject/windows/sendinput.rs rename to crates/pf-inject/src/inject/windows/sendinput.rs index 6caaa152..64aad983 100644 --- a/crates/punktfunk-host/src/inject/windows/sendinput.rs +++ b/crates/pf-inject/src/inject/windows/sendinput.rs @@ -5,7 +5,7 @@ //! (`OpenInputDesktop`/`SetThreadDesktop`) when `SendInput` reports a short write (the input //! desktop switched) — no per-event reattach overhead. //! -//! **Keyboard conventions** (see [`crate::inject::KEY_FLAG_SEMANTIC_VK`]): first-party punktfunk +//! **Keyboard conventions** (see [`crate::KEY_FLAG_SEMANTIC_VK`]): first-party punktfunk //! clients send **US-positional** VKs (the physical key's US-layout VK — layout-independent by //! construction, the mirror of the Linux host's `vk_to_evdev`), resolved here through the fixed //! [`positional_vk_to_scan`] table. GameStream/Moonlight clients send **layout-semantic** VKs @@ -249,7 +249,7 @@ impl InputInjector for SendInputInjector { InputKind::KeyDown | InputKind::KeyUp => { let down = event.kind == InputKind::KeyDown; let vk = (event.code & 0xff) as u16; - let semantic = (event.flags & crate::inject::KEY_FLAG_SEMANTIC_VK) != 0; + let semantic = (event.flags & crate::KEY_FLAG_SEMANTIC_VK) != 0; // Positional wire VKs (first-party clients) resolve through the fixed US table — // never through a layout (module docs). The table covers only the layout-VARIANT // typing area; everything else (F-row, nav, numpad, modifiers) is layout-invariant @@ -350,7 +350,7 @@ fn forced_extended(vk: u16) -> bool { /// US-positional VK → set-1 make scancode for the layout-**variant** typing area (letters, the /// digit row, OEM punctuation, the ISO 102nd key). The exact mirror of the Linux host's -/// `crate::inject::vk_to_evdev` — for these keys the evdev code IS the set-1 scancode — and of +/// `crate::vk_to_evdev` — for these keys the evdev code IS the set-1 scancode — and of /// every first-party client's capture table, so the positional round trip is /// identity-by-construction. Layout-invariant keys are deliberately absent (the /// `MapVirtualKeyExW` fallback resolves them identically under any layout, with its proven @@ -436,7 +436,7 @@ mod tests { if let Some(scan) = positional_vk_to_scan(vk) { assert_eq!( Some(scan), - crate::inject::vk_to_evdev(vk as u8), + crate::vk_to_evdev(vk as u8), "vk 0x{vk:02X}: sendinput scancode diverges from vk_to_evdev" ); checked += 1; diff --git a/crates/punktfunk-host/src/inject/windows/steam_deck_windows.rs b/crates/pf-inject/src/inject/windows/steam_deck_windows.rs similarity index 99% rename from crates/punktfunk-host/src/inject/windows/steam_deck_windows.rs rename to crates/pf-inject/src/inject/windows/steam_deck_windows.rs index 5dbd15f5..183f7990 100644 --- a/crates/punktfunk-host/src/inject/windows/steam_deck_windows.rs +++ b/crates/pf-inject/src/inject/windows/steam_deck_windows.rs @@ -25,7 +25,7 @@ use super::gamepad_raii::PadChannel; use super::steam_proto::{ neutral_deck_report, parse_steam_output, serialize_deck_state, SteamState, STEAM_REPORT_LEN, }; -use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; +use crate::uhid_manager::{PadFeedback, PadProto, UhidManager}; use anyhow::Result; use punktfunk_core::quic::RichInput; use std::time::Duration; diff --git a/crates/punktfunk-host/src/inject.rs b/crates/pf-inject/src/lib.rs similarity index 95% rename from crates/punktfunk-host/src/inject.rs rename to crates/pf-inject/src/lib.rs index 8dd332cb..6b96c030 100644 --- a/crates/punktfunk-host/src/inject.rs +++ b/crates/pf-inject/src/lib.rs @@ -8,6 +8,14 @@ //! events into virtual pointer/keyboard requests. Keyboard codes are Linux evdev; we upload an //! xkb keymap (the host's layout via `XKB_DEFAULT_LAYOUT` et al., defaulting to evdev/US) and //! track modifier state so the compositor resolves shifted keysyms correctly. +//! +//! Extracted into a subsystem crate (plan §W6): consumes `punktfunk_core::input` (the neutral +//! event vocabulary) + `pf-driver-proto` (the HID wire contract), never the orchestrator. + +// Scaffold: trait methods + per-OS backends are defined ahead of the target that uses them. +#![allow(dead_code)] +// Every unsafe block in this crate carries a `// SAFETY:` proof; enforce it (unsafe-proof program). +#![deny(clippy::undocumented_unsafe_blocks)] use anyhow::Result; use punktfunk_core::input::{InputEvent, InputKind}; @@ -18,7 +26,7 @@ mod keymap; pub(crate) use keymap::gs_button_to_evdev; pub use keymap::KEY_FLAG_SEMANTIC_VK; // vk_to_evdev is consumed by the Linux injectors (kwin/libei/wlr) and — on Windows — only by the -// SendInput mirror test; keep the shared `crate::inject::vk_to_evdev` re-export unconditionally. +// SendInput mirror test; keep the shared `crate::vk_to_evdev` re-export unconditionally. #[cfg_attr(not(target_os = "linux"), allow(unused_imports))] pub use keymap::vk_to_evdev; @@ -80,7 +88,7 @@ pub fn open(backend: Backend) -> Result> { libei::LibeiInjector::open_with(libei_ei_source())?, )), Backend::GamescopeEi => Ok(Box::new(libei::LibeiInjector::open_with( - libei::EiSource::SocketPathFile(crate::vdisplay::gamescope_ei_socket_file()), + libei::EiSource::SocketPathFile(pf_paths::gamescope_ei_socket_file()), )?)), } } @@ -168,7 +176,7 @@ pub fn default_backend() -> Backend { #[path = "inject/service.rs"] mod service; -pub(crate) use service::InjectorService; +pub use service::InjectorService; /// How the libei backend reaches its EIS server. KWin goes through the `RemoteDesktop` *portal* /// (with a pre-seeded grant), but GNOME's portal `Start()` needs an interactive approval a @@ -193,7 +201,7 @@ fn libei_ei_source() -> libei::EiSource { // Goal-1 stage 6: Linux UHID/uinput/libei/wlr backends under `inject/linux/`, the Windows UMDF/SendInput // backends under `inject/windows/`, and the transport-independent HID codecs under `inject/proto/`; -// `#[path]` keeps every `crate::inject::*` module name flat. +// `#[path]` keeps every `crate::*` module name flat. #[cfg(target_os = "linux")] #[path = "inject/linux/dualsense.rs"] pub mod dualsense; diff --git a/crates/pf-paths/src/lib.rs b/crates/pf-paths/src/lib.rs index c526edf0..9789308f 100644 --- a/crates/pf-paths/src/lib.rs +++ b/crates/pf-paths/src/lib.rs @@ -9,6 +9,20 @@ use std::path::PathBuf; +/// The shared path of the file where the gamescope backend relays the nested session's +/// `LIBEI_SOCKET` (gamescope's EIS server) for the input injector: `$XDG_RUNTIME_DIR/ +/// punktfunk-gamescope-ei` (per-user 0700), or `/tmp/…` when the runtime dir is unset. It is a +/// **contract shared** by the gamescope producer (`pf-vdisplay`, which writes it under the session +/// env lock) and the libei consumer (`pf-inject`, which reads it after the session env is applied) — +/// a leaf so neither subsystem crate has to reach into the other (plan §W6). Linux-only. +#[cfg(target_os = "linux")] +pub fn gamescope_ei_socket_file() -> PathBuf { + match std::env::var_os("XDG_RUNTIME_DIR").filter(|s| !s.is_empty()) { + Some(rt) => PathBuf::from(rt).join("punktfunk-gamescope-ei"), + None => PathBuf::from("/tmp/punktfunk-gamescope-ei"), + } +} + /// The host config dir (host identity, pairing state, mgmt token, library) — created on demand. /// Linux: `$XDG_CONFIG_HOME/punktfunk` or `~/.config/punktfunk`. Windows: `%ProgramData%\punktfunk` /// (machine-wide — the SYSTEM service and the interactive user share ONE dir that survives logout). diff --git a/crates/punktfunk-host/Cargo.toml b/crates/punktfunk-host/Cargo.toml index 46db0254..3964b2f8 100644 --- a/crates/punktfunk-host/Cargo.toml +++ b/crates/punktfunk-host/Cargo.toml @@ -34,6 +34,10 @@ pf-encode = { path = "../pf-encode" } # trait, extracted to a subsystem crate (plan §W6). The facade (src/capture.rs) hands it the # pre-resolved encode facts + the sealed-channel delivery closure so the edge stays one-way. pf-capture = { path = "../pf-capture" } +# Input injection backends (per-OS injectors + the virtual-gamepad HID stack), extracted to a +# subsystem crate (plan §W6). Consumes core::input; the heavy input deps (wayland/reis/xkbcommon/ +# usbip) moved with it. +pf-inject = { path = "../pf-inject" } # M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP). quinn = "0.11" anyhow = "1" diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 9c61e94e..1a88ff08 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -48,7 +48,12 @@ mod gamestream; #[path = "linux/gpuclocks.rs"] mod gpuclocks; mod hooks; -mod inject; +// The input-injection backends live in the `pf-inject` subsystem crate (plan §W6); this shim keeps +// every existing `crate::inject::*` path valid (the native/gamestream input planes + devtest consume +// the trait, factory, and per-device backends through it). +mod inject { + pub(crate) use pf_inject::*; +} #[cfg(target_os = "windows")] #[path = "windows/install.rs"] mod install; diff --git a/crates/punktfunk-host/src/vdisplay.rs b/crates/punktfunk-host/src/vdisplay.rs index 082bbada..c1a66ce3 100644 --- a/crates/punktfunk-host/src/vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay.rs @@ -47,8 +47,8 @@ pub use routing::{ }; #[cfg(target_os = "linux")] pub use routing::{ - cancel_pending_tv_restore, dedicated_game_exited, gamescope_ei_socket_file, - launch_into_gamescope_session, launch_is_nested, + cancel_pending_tv_restore, dedicated_game_exited, launch_into_gamescope_session, + launch_is_nested, }; /// Compositors punktfunk knows how to drive (plan §6). diff --git a/crates/punktfunk-host/src/vdisplay/linux/gamescope.rs b/crates/punktfunk-host/src/vdisplay/linux/gamescope.rs index 0d1e36ff..c5975f7f 100644 --- a/crates/punktfunk-host/src/vdisplay/linux/gamescope.rs +++ b/crates/punktfunk-host/src/vdisplay/linux/gamescope.rs @@ -1337,11 +1337,10 @@ fn stop_session(unit_name: &str) { /// only if `XDG_RUNTIME_DIR` is unset (gamescope itself requires it, so this is rare); the reader /// ([`crate::inject`]) additionally rejects a symlinked relay file as defense-in-depth. pub fn ei_socket_file() -> std::path::PathBuf { - let runtime = crate::vdisplay::with_env_lock(|| std::env::var_os("XDG_RUNTIME_DIR")); - match runtime { - Some(rt) if !rt.is_empty() => std::path::PathBuf::from(rt).join("punktfunk-gamescope-ei"), - _ => std::path::PathBuf::from("/tmp/punktfunk-gamescope-ei"), - } + // The path itself is the shared `pf_paths::gamescope_ei_socket_file` contract (also read by the + // libei injector). Compute it under the session env lock so a concurrent session handshake's + // `apply_session_env` XDG_RUNTIME_DIR retarget can't race this producer-side read. + crate::vdisplay::with_env_lock(pf_paths::gamescope_ei_socket_file) } /// Shape a resolved launch command for a bare-spawn gamescope session. A Steam URI launch diff --git a/crates/punktfunk-host/src/vdisplay/routing.rs b/crates/punktfunk-host/src/vdisplay/routing.rs index b89a0b9c..09d8acd4 100644 --- a/crates/punktfunk-host/src/vdisplay/routing.rs +++ b/crates/punktfunk-host/src/vdisplay/routing.rs @@ -185,13 +185,6 @@ pub fn cancel_pending_tv_restore() { #[cfg(not(target_os = "linux"))] pub fn cancel_pending_tv_restore() {} -/// Path of the file where the gamescope backend relays the nested session's `LIBEI_SOCKET` -/// (gamescope's EIS server) for the input injector. Under `$XDG_RUNTIME_DIR` (per-user 0700). -#[cfg(target_os = "linux")] -pub fn gamescope_ei_socket_file() -> std::path::PathBuf { - gamescope::ei_socket_file() -} - /// Call when a client session ends: if the host-managed gamescope path took over a box's autologin /// gaming session (stopped its single-instance Steam to stream at the client's mode), **schedule** a /// debounced restore so the TV returns to gaming mode — unless a client reconnects within the window