refactor(host/W6.2): extract the input-injection backends into the pf-inject crate

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 11:52:02 +02:00
parent 0992548de7
commit f6c6e4e594
43 changed files with 199 additions and 73 deletions
Generated
+29
View File
@@ -2880,6 +2880,34 @@ dependencies = [
name = "pf-host-config" name = "pf-host-config"
version = "0.12.0" 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]] [[package]]
name = "pf-paths" name = "pf-paths"
version = "0.12.0" version = "0.12.0"
@@ -3229,6 +3257,7 @@ dependencies = [
"pf-frame", "pf-frame",
"pf-gpu", "pf-gpu",
"pf-host-config", "pf-host-config",
"pf-inject",
"pf-paths", "pf-paths",
"pf-win-display", "pf-win-display",
"pf-zerocopy", "pf-zerocopy",
+1
View File
@@ -18,6 +18,7 @@ members = [
"crates/pf-win-display", "crates/pf-win-display",
"crates/pf-encode", "crates/pf-encode",
"crates/pf-capture", "crates/pf-capture",
"crates/pf-inject",
"crates/pyrowave-sys", "crates/pyrowave-sys",
"clients/probe", "clients/probe",
"clients/linux", "clients/linux",
+65
View File
@@ -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",
] }
@@ -1,6 +1,6 @@
//! Per-pad dedup for the rich HID-output feedback plane (0xCD), carved out of `dualsense_proto` //! 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 //! (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 //! 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). //! rich state every report; this forwards only genuine changes (one-shot pulses always fire).
@@ -17,7 +17,7 @@ use super::dualsense_proto::{
DS_EDGE_PRODUCT, DS_FEATURE_CALIBRATION, DS_FEATURE_FIRMWARE, DS_INPUT_REPORT_LEN, DS_PRODUCT, 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, 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 anyhow::{Context, Result};
use punktfunk_core::quic::RichInput; use punktfunk_core::quic::RichInput;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
@@ -232,13 +232,13 @@ impl Drop for DualSensePad {
pub struct DsLinuxProto { pub struct DsLinuxProto {
/// Fallback policy for the Steam back grips a client may send (the DualSense has no back-button /// 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. /// HID slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop.
remap: crate::inject::steam_remap::RemapConfig, remap: crate::steam_remap::RemapConfig,
} }
impl Default for DsLinuxProto { impl Default for DsLinuxProto {
fn default() -> DsLinuxProto { fn default() -> DsLinuxProto {
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 { 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 // Steam back grips have no DualSense slot — fold them onto standard buttons per the
// configured policy (default drop) so they aren't silently lost. // 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( let mut s = DsState::from_gamepad(
buttons, buttons,
f.ls_x, f.ls_x,
@@ -18,7 +18,7 @@ use super::dualshock4_proto::{
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_PRODUCT, DS4_TOUCH_H, parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_PRODUCT, DS4_TOUCH_H,
DS4_TOUCH_W, DS4_VENDOR, 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 anyhow::{Context, Result};
use punktfunk_core::quic::{HidOutput, RichInput}; use punktfunk_core::quic::{HidOutput, RichInput};
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
@@ -302,13 +302,13 @@ impl Drop for DualShock4Pad {
pub struct Ds4LinuxProto { pub struct Ds4LinuxProto {
/// Fallback policy for the Steam back grips a client may send (the DS4 has no back-button HID /// 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. /// slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop.
remap: crate::inject::steam_remap::RemapConfig, remap: crate::steam_remap::RemapConfig,
} }
impl Default for Ds4LinuxProto { impl Default for Ds4LinuxProto {
fn default() -> Ds4LinuxProto { fn default() -> Ds4LinuxProto {
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 { 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 // Steam back grips have no DS4 slot — fold them onto standard buttons per the configured
// policy (default drop) so they aren't silently lost. // 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( let mut s = DsState::from_gamepad(
buttons, buttons,
f.ls_x, f.ls_x,
@@ -18,7 +18,7 @@
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). // Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
#![deny(clippy::undocumented_unsafe_blocks)] #![deny(clippy::undocumented_unsafe_blocks)]
use crate::inject::pad_slots::PadSlots; use crate::pad_slots::PadSlots;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use punktfunk_core::input::{gamepad, GamepadFrame, MAX_PADS}; use punktfunk_core::input::{gamepad, GamepadFrame, MAX_PADS};
use std::collections::HashMap; use std::collections::HashMap;
@@ -23,7 +23,7 @@ use super::steam_proto::{
btn, parse_steam_output, sc_from_gamepad, serial_reply, serialize_deck_state, btn, parse_steam_output, sc_from_gamepad, serial_reply, serialize_deck_state,
serialize_sc_state, SteamModel, SteamState, STEAMDECK_RDESC, STEAM_REPORT_LEN, STEAM_VENDOR, 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 anyhow::{Context, Result};
use punktfunk_core::quic::RichInput; use punktfunk_core::quic::RichInput;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
@@ -264,8 +264,8 @@ impl Drop for SteamDeckPad {
/// `type Pad` in the `PadProto` impl, a public trait.) /// `type Pad` in the `PadProto` impl, a public trait.)
pub enum DeckTransport { pub enum DeckTransport {
Uhid(SteamDeckPad), Uhid(SteamDeckPad),
Gadget(crate::inject::steam_gadget::SteamDeckGadget), Gadget(crate::steam_gadget::SteamDeckGadget),
Usbip(crate::inject::steam_usbip::SteamDeckUsbip), Usbip(crate::steam_usbip::SteamDeckUsbip),
} }
impl DeckTransport { impl DeckTransport {
@@ -332,7 +332,7 @@ fn warn_if_inputplumber() {
/// usbip, and one lacking both still gets a working (if non-promoted) UHID pad. /// usbip, and one lacking both still gets a working (if non-promoted) UHID pad.
fn open_transport(idx: u8) -> Result<DeckTransport> { fn open_transport(idx: u8) -> Result<DeckTransport> {
warn_if_inputplumber(); 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). // 1. raw_gadget — the validated SteamOS fast-path (default on there).
if steam_gadget::gadget_preferred() { if steam_gadget::gadget_preferred() {
steam_gadget::ensure_modules(); steam_gadget::ensure_modules();
@@ -474,13 +474,13 @@ pub type SteamControllerManager = UhidManager<SteamProto>;
/// registers neither FF rumble nor a sensors evdev for this model (feedback stays empty). /// registers neither FF rumble nor a sensors evdev for this model (feedback stays empty).
pub struct ScProto { pub struct ScProto {
/// Fallback policy for the wire paddles beyond the SC's two grips (PADDLE3/4). /// 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 { impl Default for ScProto {
fn default() -> ScProto { fn default() -> ScProto {
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 { ) -> SteamState {
use punktfunk_core::input::gamepad as gs; use punktfunk_core::input::gamepad as gs;
let native = f.buttons & (gs::BTN_PADDLE1 | gs::BTN_PADDLE2); 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), f.buttons & !(gs::BTN_PADDLE1 | gs::BTN_PADDLE2),
self.remap.paddles, self.remap.paddles,
); );
@@ -23,7 +23,7 @@ use super::triton_proto::{
triton_serial, triton_unit_id, TritonState, TRITON_RDESC, TRITON_STATE_LEN, TRITON_VENDOR, triton_serial, triton_unit_id, TritonState, TRITON_RDESC, TRITON_STATE_LEN, TRITON_VENDOR,
TRITON_WIRED_PRODUCT, TRITON_WIRED_PRODUCT,
}; };
use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; use crate::uhid_manager::{PadFeedback, PadProto, UhidManager};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use punktfunk_core::quic::{HidOutput, RichInput, HID_RAW_FEATURE, HID_RAW_OUTPUT}; use punktfunk_core::quic::{HidOutput, RichInput, HID_RAW_FEATURE, HID_RAW_OUTPUT};
use std::fs::{File, OpenOptions}; 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`], /// 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). /// minus the gadget rung (no captured gadget layout for the Triton, and usbip is universal).
pub enum TritonTransport { pub enum TritonTransport {
Usbip(crate::inject::triton_usbip::TritonUsbip), Usbip(crate::triton_usbip::TritonUsbip),
Uhid(TritonPad), 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 /// (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. /// hidraw only — flagged loudly, with the vhci_hcd remedy in the log.
fn open_transport(idx: u8, puck: bool) -> Result<TritonTransport> { fn open_transport(idx: u8, puck: bool) -> Result<TritonTransport> {
if crate::inject::steam_usbip::usbip_preferred() { if crate::steam_usbip::usbip_preferred() {
let opened = if puck { let opened = if puck {
crate::inject::triton_usbip::TritonUsbip::open_puck(idx) crate::triton_usbip::TritonUsbip::open_puck(idx)
} else { } else {
crate::inject::triton_usbip::TritonUsbip::open(idx) crate::triton_usbip::TritonUsbip::open(idx)
}; };
match opened { match opened {
Ok(u) => return Ok(TritonTransport::Usbip(u)), Ok(u) => return Ok(TritonTransport::Usbip(u)),
@@ -22,7 +22,7 @@ use super::switch_proto::{
serialize_report_0x30, spi_flash_read, switch_mac, SwitchOutput, SwitchState, PROCON_RDESC, serialize_report_0x30, spi_flash_read, switch_mac, SwitchOutput, SwitchState, PROCON_RDESC,
SWITCH_PRODUCT, SWITCH_REPORT_LEN, SWITCH_VENDOR, 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 anyhow::{Context, Result};
use punktfunk_core::quic::{HidOutput, RichInput}; use punktfunk_core::quic::{HidOutput, RichInput};
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
@@ -241,13 +241,13 @@ impl Drop for SwitchProPad {
pub struct SwitchProProto { pub struct SwitchProProto {
/// Fallback policy for the Steam back grips a client may send (a Pro Controller has no /// 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. /// back-button slot). `PUNKTFUNK_STEAM_REMAP=paddles=…`; default drop.
remap: crate::inject::steam_remap::RemapConfig, remap: crate::steam_remap::RemapConfig,
} }
impl Default for SwitchProProto { impl Default for SwitchProProto {
fn default() -> SwitchProProto { fn default() -> SwitchProProto {
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, prev: &SwitchState,
f: &punktfunk_core::input::GamepadFrame, f: &punktfunk_core::input::GamepadFrame,
) -> SwitchState { ) -> 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( let mut s = SwitchState::from_gamepad(
buttons, buttons,
f.ls_x, f.ls_x,
@@ -40,7 +40,7 @@ const PUCK_MANAGEMENT_RDESC: &[u8] = &[
/// Everything Steam wrote to the device since the last service pass. /// Everything Steam wrote to the device since the last service pass.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(crate) struct TritonUsbFeedback { pub struct TritonUsbFeedback {
/// `(low, high)` from the last `0x80` rumble output report. /// `(low, high)` from the last `0x80` rumble output report.
pub rumble: Option<(u16, u16)>, pub rumble: Option<(u16, u16)>,
/// Raw reports to forward, `(kind, bytes)` — kind = `HID_RAW_OUTPUT`/`HID_RAW_FEATURE`. /// Raw reports to forward, `(kind, bytes)` — kind = `HID_RAW_OUTPUT`/`HID_RAW_FEATURE`.
@@ -1,7 +1,7 @@
//! Shared virtual-pad slot table + creation lifecycle, used by every backend manager (Linux //! Shared virtual-pad slot table + creation lifecycle, used by every backend manager (Linux
//! uinput/uhid, Windows XUSB/UMDF). See [`PadSlots`]. //! uinput/uhid, Windows XUSB/UMDF). See [`PadSlots`].
use crate::inject::pad_gate::PadGate; use crate::pad_gate::PadGate;
use anyhow::Result; use anyhow::Result;
use punktfunk_core::input::MAX_PADS; use punktfunk_core::input::MAX_PADS;
use std::time::Instant; use std::time::Instant;
@@ -470,7 +470,7 @@ pub struct DsFeedback {
/// `(low, high)` motor levels (0..=0xFFFF), if a report carried them. /// `(low, high)` motor levels (0..=0xFFFF), if a report carried them.
pub rumble: Option<(u16, u16)>, pub rumble: Option<(u16, u16)>,
/// Whether a fresh output report was seen this poll (set by the backend's section poll, not by /// 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. /// abandoned-rumble force-off keys on.
pub fresh: bool, pub fresh: bool,
} }
@@ -81,7 +81,7 @@ pub struct Ds4Feedback {
/// Lightbar RGB, if the report carried it (deduped by the manager). /// Lightbar RGB, if the report carried it (deduped by the manager).
pub led: Option<(u8, u8, u8)>, pub led: Option<(u8, u8, u8)>,
/// Whether a fresh output report was seen this poll (set by the backend's section poll, not by /// 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. /// abandoned-rumble force-off keys on.
pub fresh: bool, pub fresh: bool,
} }
@@ -12,12 +12,12 @@ use super::*;
/// punktfunk/1 plane forward their decoded keyboard/mouse events here instead of injecting inline, so /// 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 /// a slow inject (a portal stall, a desktop switch) never head-blocks the network thread's
/// keepalive/retransmit servicing. /// keepalive/retransmit servicing.
pub(crate) struct InjectorService { pub struct InjectorService {
tx: std::sync::mpsc::Sender<InputEvent>, tx: std::sync::mpsc::Sender<InputEvent>,
} }
impl InjectorService { impl InjectorService {
pub(crate) fn start() -> InjectorService { pub fn start() -> InjectorService {
// Windows: make sure the process-wide resident virtual HID mouse exists (idempotent). // 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 // Without a pointing device present, win32k reports no cursor and DWM composites none
// into the IDD frame — SendInput injection alone moves an invisible pointer. // 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 /// 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). /// 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<InputEvent> { pub fn sender(&self) -> std::sync::mpsc::Sender<InputEvent> {
self.tx.clone() self.tx.clone()
} }
} }
@@ -6,8 +6,8 @@
//! Windows XUSB) write frames straight through with no state vec / heartbeat / rich plane, so they //! Windows XUSB) write frames straight through with no state vec / heartbeat / rich plane, so they
//! use [`PadSlots`] directly instead. //! use [`PadSlots`] directly instead.
use crate::inject::hidout_dedup::HidoutDedup; use crate::hidout_dedup::HidoutDedup;
use crate::inject::pad_slots::PadSlots; use crate::pad_slots::PadSlots;
use anyhow::Result; use anyhow::Result;
use punktfunk_core::input::{GamepadEvent, GamepadFrame, MAX_PADS}; use punktfunk_core::input::{GamepadEvent, GamepadFrame, MAX_PADS};
use punktfunk_core::quic::{HidOutput, RichInput}; use punktfunk_core::quic::{HidOutput, RichInput};
@@ -10,7 +10,7 @@
use super::dualsense_proto::{edge_paddle_bits, DsState, DS_TOUCH_H, DS_TOUCH_W}; use super::dualsense_proto::{edge_paddle_bits, DsState, DS_TOUCH_H, DS_TOUCH_W};
use super::dualsense_windows::{DsWinPad, WinDsIdentity}; 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 anyhow::Result;
use punktfunk_core::quic::RichInput; 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 /// 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. /// surface (via the shared [`UhidManager`]) as the other Windows pad managers.
pub type DualSenseEdgeWindowsManager = UhidManager<DsEdgeWinProto>; pub type DualSenseEdgeWindowsManager = UhidManager<DsEdgeWinProto>;
@@ -22,7 +22,7 @@ use super::dualsense_proto::{
DS_TOUCH_W, DS_TOUCH_W,
}; };
use super::gamepad_raii::{sw_create_cb, PadChannel, SwCreateCtx}; 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 anyhow::{anyhow, Result};
use punktfunk_core::quic::RichInput; use punktfunk_core::quic::RichInput;
use std::ffi::c_void; use std::ffi::c_void;
@@ -410,13 +410,13 @@ impl DsWinPad {
pub struct DsWinProto { pub struct DsWinProto {
/// Fallback policy for the Steam back grips a client may send (the DualSense has no back-button /// 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`. /// 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 { impl Default for DsWinProto {
fn default() -> DsWinProto { fn default() -> DsWinProto {
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 { 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 // Steam back grips have no DualSense slot — fold them onto standard buttons per the
// configured policy (default drop) so they aren't silently lost. // 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( let mut s = DsState::from_gamepad(
buttons, buttons,
f.ls_x, f.ls_x,
@@ -16,7 +16,7 @@ use super::dualshock4_proto::{
parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W, parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W,
}; };
use super::gamepad_raii::PadChannel; use super::gamepad_raii::PadChannel;
use crate::inject::uhid_manager::{PadFeedback, PadProto, UhidManager}; use crate::uhid_manager::{PadFeedback, PadProto, UhidManager};
use anyhow::Result; use anyhow::Result;
use punktfunk_core::quic::{HidOutput, RichInput}; use punktfunk_core::quic::{HidOutput, RichInput};
use std::time::Duration; use std::time::Duration;
@@ -154,13 +154,13 @@ impl Ds4WinPad {
pub struct Ds4WinProto { pub struct Ds4WinProto {
/// Fallback policy for the Steam back grips a client may send (the DS4 has no back-button HID /// 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`. /// 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 { impl Default for Ds4WinProto {
fn default() -> Ds4WinProto { fn default() -> Ds4WinProto {
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 { 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 // Steam back grips have no DS4 slot — fold them onto standard buttons per the configured
// policy (default drop) so they aren't silently lost. // 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( let mut s = DsState::from_gamepad(
buttons, buttons,
f.ls_x, f.ls_x,
@@ -140,7 +140,9 @@ impl Shm {
pub(super) fn create_named(name: &HSTRING, size: usize) -> Result<Shm> { pub(super) fn create_named(name: &HSTRING, size: usize) -> Result<Shm> {
// Build the descriptor ONCE and reuse it across the squat-retry loop — it (and the OS // 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. // 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 { for attempt in 0..5 {
if attempt > 0 { if attempt > 0 {
std::thread::sleep(Duration::from_millis(50)); std::thread::sleep(Duration::from_millis(50));
@@ -628,7 +630,13 @@ impl DriverAttach {
fn driver_store_inventory() -> &'static str { fn driver_store_inventory() -> &'static str {
static INV: OnceLock<String> = OnceLock::new(); static INV: OnceLock<String> = OnceLock::new();
INV.get_or_init(|| { 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") .arg("/enum-drivers")
.output() .output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_ascii_lowercase()) .map(|o| String::from_utf8_lossy(&o.stdout).to_ascii_lowercase())
@@ -13,7 +13,7 @@
//! level changes to the client (the universal 0xCA plane), mirroring the Linux `EV_FF` read path. //! 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 super::gamepad_raii::{sw_create_cb, PadChannel, SwCreateCtx};
use crate::inject::pad_slots::PadSlots; use crate::pad_slots::PadSlots;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use punktfunk_core::input::{GamepadEvent, MAX_PADS}; use punktfunk_core::input::{GamepadEvent, MAX_PADS};
use std::ffi::c_void; use std::ffi::c_void;
@@ -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 /// 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 /// 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 /// 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). /// open; the pump only handles a late WUDFHost + feeds the attach diagnostics).
@@ -5,7 +5,7 @@
//! (`OpenInputDesktop`/`SetThreadDesktop`) when `SendInput` reports a short write (the input //! (`OpenInputDesktop`/`SetThreadDesktop`) when `SendInput` reports a short write (the input
//! desktop switched) — no per-event reattach overhead. //! 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 //! 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 //! 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 //! [`positional_vk_to_scan`] table. GameStream/Moonlight clients send **layout-semantic** VKs
@@ -249,7 +249,7 @@ impl InputInjector for SendInputInjector {
InputKind::KeyDown | InputKind::KeyUp => { InputKind::KeyDown | InputKind::KeyUp => {
let down = event.kind == InputKind::KeyDown; let down = event.kind == InputKind::KeyDown;
let vk = (event.code & 0xff) as u16; 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 — // 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 // 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 // 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 /// 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 /// 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 /// every first-party client's capture table, so the positional round trip is
/// identity-by-construction. Layout-invariant keys are deliberately absent (the /// identity-by-construction. Layout-invariant keys are deliberately absent (the
/// `MapVirtualKeyExW` fallback resolves them identically under any layout, with its proven /// `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) { if let Some(scan) = positional_vk_to_scan(vk) {
assert_eq!( assert_eq!(
Some(scan), 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" "vk 0x{vk:02X}: sendinput scancode diverges from vk_to_evdev"
); );
checked += 1; checked += 1;
@@ -25,7 +25,7 @@ use super::gamepad_raii::PadChannel;
use super::steam_proto::{ use super::steam_proto::{
neutral_deck_report, parse_steam_output, serialize_deck_state, SteamState, STEAM_REPORT_LEN, 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 anyhow::Result;
use punktfunk_core::quic::RichInput; use punktfunk_core::quic::RichInput;
use std::time::Duration; use std::time::Duration;
@@ -8,6 +8,14 @@
//! events into virtual pointer/keyboard requests. Keyboard codes are Linux evdev; we upload an //! 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 //! 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. //! 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 anyhow::Result;
use punktfunk_core::input::{InputEvent, InputKind}; use punktfunk_core::input::{InputEvent, InputKind};
@@ -18,7 +26,7 @@ mod keymap;
pub(crate) use keymap::gs_button_to_evdev; pub(crate) use keymap::gs_button_to_evdev;
pub use keymap::KEY_FLAG_SEMANTIC_VK; 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 // 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))] #[cfg_attr(not(target_os = "linux"), allow(unused_imports))]
pub use keymap::vk_to_evdev; pub use keymap::vk_to_evdev;
@@ -80,7 +88,7 @@ pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
libei::LibeiInjector::open_with(libei_ei_source())?, libei::LibeiInjector::open_with(libei_ei_source())?,
)), )),
Backend::GamescopeEi => Ok(Box::new(libei::LibeiInjector::open_with( 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"] #[path = "inject/service.rs"]
mod service; 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* /// 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 /// (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 // 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/`; // 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")] #[cfg(target_os = "linux")]
#[path = "inject/linux/dualsense.rs"] #[path = "inject/linux/dualsense.rs"]
pub mod dualsense; pub mod dualsense;
+14
View File
@@ -9,6 +9,20 @@
use std::path::PathBuf; 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. /// 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` /// 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). /// (machine-wide — the SYSTEM service and the interactive user share ONE dir that survives logout).
+4
View File
@@ -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 # 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. # pre-resolved encode facts + the sealed-channel delivery closure so the edge stays one-way.
pf-capture = { path = "../pf-capture" } 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). # M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP).
quinn = "0.11" quinn = "0.11"
anyhow = "1" anyhow = "1"
+6 -1
View File
@@ -48,7 +48,12 @@ mod gamestream;
#[path = "linux/gpuclocks.rs"] #[path = "linux/gpuclocks.rs"]
mod gpuclocks; mod gpuclocks;
mod hooks; 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")] #[cfg(target_os = "windows")]
#[path = "windows/install.rs"] #[path = "windows/install.rs"]
mod install; mod install;
+2 -2
View File
@@ -47,8 +47,8 @@ pub use routing::{
}; };
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub use routing::{ pub use routing::{
cancel_pending_tv_restore, dedicated_game_exited, gamescope_ei_socket_file, cancel_pending_tv_restore, dedicated_game_exited, launch_into_gamescope_session,
launch_into_gamescope_session, launch_is_nested, launch_is_nested,
}; };
/// Compositors punktfunk knows how to drive (plan §6). /// Compositors punktfunk knows how to drive (plan §6).
@@ -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 /// 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. /// ([`crate::inject`]) additionally rejects a symlinked relay file as defense-in-depth.
pub fn ei_socket_file() -> std::path::PathBuf { pub fn ei_socket_file() -> std::path::PathBuf {
let runtime = crate::vdisplay::with_env_lock(|| std::env::var_os("XDG_RUNTIME_DIR")); // The path itself is the shared `pf_paths::gamescope_ei_socket_file` contract (also read by the
match runtime { // libei injector). Compute it under the session env lock so a concurrent session handshake's
Some(rt) if !rt.is_empty() => std::path::PathBuf::from(rt).join("punktfunk-gamescope-ei"), // `apply_session_env` XDG_RUNTIME_DIR retarget can't race this producer-side read.
_ => std::path::PathBuf::from("/tmp/punktfunk-gamescope-ei"), 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 /// Shape a resolved launch command for a bare-spawn gamescope session. A Steam URI launch
@@ -185,13 +185,6 @@ pub fn cancel_pending_tv_restore() {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
pub fn cancel_pending_tv_restore() {} 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 /// 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 /// 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 /// debounced restore so the TV returns to gaming mode — unless a client reconnects within the window