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:
Generated
+29
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
-1
@@ -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).
|
||||
|
||||
+4
-4
@@ -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,
|
||||
+4
-4
@@ -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,
|
||||
+1
-1
@@ -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;
|
||||
+7
-7
@@ -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<DeckTransport> {
|
||||
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<SteamProto>;
|
||||
/// 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,
|
||||
);
|
||||
+5
-5
@@ -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<TritonTransport> {
|
||||
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)),
|
||||
+4
-4
@@ -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,
|
||||
+1
-1
@@ -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`.
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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,
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
}
|
||||
+3
-3
@@ -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<InputEvent>,
|
||||
}
|
||||
|
||||
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<InputEvent> {
|
||||
pub fn sender(&self) -> std::sync::mpsc::Sender<InputEvent> {
|
||||
self.tx.clone()
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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};
|
||||
+2
-2
@@ -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<DsEdgeWinProto>;
|
||||
+4
-4
@@ -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,
|
||||
+4
-4
@@ -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,
|
||||
+10
-2
@@ -140,7 +140,9 @@ impl 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
|
||||
// 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<String> = 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())
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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).
|
||||
+4
-4
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
@@ -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<Box<dyn InputInjector>> {
|
||||
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;
|
||||
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user