Files
punktfunk/crates/punktfunk-host/Cargo.toml
T
enricobuehler 27a5d8daac refactor(host/W6.2): extract virtual-display orchestration into the pf-vdisplay crate
vdisplay.rs + vdisplay/* (the per-compositor Linux backends — KWin zkde-screencast,
wlroots swaymsg, Mutter RemoteDesktop, Hyprland — and the Windows IddCx/pf-vdisplay
driver backend, behind one VirtualDisplay trait; the mode-conflict admission
registry, the display policy/identity/custom-preset state, and the session-env /
gamescope routing) move into crates/pf-vdisplay (plan §W6). The DDC/CI panel-power
control (used only here) and the KWin zkde protocol XML move with it. This
completes the host-crate decomposition: capture, encode, inject, and vdisplay are
now four subsystem crates over the shared leaves, and punktfunk-host is the
orchestrator (serve/supervisor + native + gamestream + mgmt).

Coupling breaks (all down-only, cargo-tree acyclic):
- capture::dxgi identity -> pf_frame::dxgi; win_display/monitor_devnode/
  console_session_mismatch -> pf-win-display leaf; can_open_another_session ->
  pf-encode (the NVENC session-budget admission gate — acyclic peer edge).
- The registry's DisplayCreated/DisplayReleased emits into the host SSE event bus
  invert to a leaf hook: pf-vdisplay emits a neutral DisplayEvent to a
  host-registered DISPLAY_EVENT_SINK, so it never reaches the orchestrator's
  events module.
- The IddCx driver module is renamed pf_vdisplay -> driver (its old name collided
  with the crate name through the host's `mod vdisplay` shim glob).

The host keeps `mod vdisplay { pub use pf_vdisplay::* }` so every crate::vdisplay::*
path (serve/mgmt/native/the capture FrameChannelSender seam) is unchanged; the
heavy deps (wayland/ashpd/tokio + the zkde protocol) moved with the crate.
Co-authored: a fail-closed IOCTL-reply-length security fix (reject short/zeroed
pf-vdisplay driver replies before trusting protocol_version/target_id/wudf_pid/luid,
security-review 2026-07-17) rides this commit in the moved driver module.

Verified: Linux clippy -D warnings (pf-vdisplay + host nvenc,vulkan-encode,pyrowave
--all-targets) + pf-vdisplay 63/63 + host 167/167 tests; Windows clippy -D warnings
(pf-vdisplay --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:14:08 +02:00

292 lines
17 KiB
TOML

[package]
name = "punktfunk-host"
description = "punktfunk Linux streaming host: virtual display, capture, encode, input injection"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[dependencies]
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
# Config-dir + owner-private file helpers (moved out of the gamestream junk drawer, plan §W6).
pf-paths = { path = "../pf-paths" }
# Process-wide host config global, extracted to a leaf crate (plan §W6).
pf-host-config = { path = "../pf-host-config" }
# GPU vendor/adapter detection + selection, extracted to a leaf crate (plan §W6).
pf-gpu = { path = "../pf-gpu" }
# Linux GPU zero-copy plumbing (CUDA/EGL/Vulkan dmabuf import + the isolated worker), extracted
# to a leaf crate (plan §W6). Compiles empty on non-Linux, so it lives in the main deps.
pf-zerocopy = { path = "../pf-zerocopy" }
# Shared frame/format vocabulary (CapturedFrame/PixelFormat/…), HDR metadata, thread QoS, and the
# Windows DXGI capture identity — the leaf both capture and encode speak (plan §W6).
pf-frame = { path = "../pf-frame" }
# Windows display-topology helpers (CCD/GDI mode-set, PnP monitor devnodes, display-change watch),
# extracted to a leaf crate (plan §W6). Empty on non-Windows, so it lives in the main deps.
pf-win-display = { path = "../pf-win-display" }
# Video encode backends (NVENC/VAAPI/AMF/QSV/Vulkan-Video/PyroWave/openh264) behind one Encoder
# trait, extracted to a subsystem crate (plan §W6). The host's nvenc/amf-qsv/vulkan-encode/pyrowave
# features forward here (see [features]); the heavy encoder deps (ffmpeg-next, the NVENC SDK,
# openh264, pyrowave-sys) moved with it.
pf-encode = { path = "../pf-encode" }
# Frame capture backends (Linux PipeWire portal + Windows IDD direct-push) behind one Capturer
# 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" }
# Virtual-display orchestration (per-compositor Linux backends + the Windows IddCx driver backend),
# extracted to a subsystem crate (plan §W6). The DDC panel control + KWin zkde protocol moved with it.
pf-vdisplay = { path = "../pf-vdisplay" }
# M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP).
quinn = "0.11"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# The log ring (log_capture.rs) normalizes `log`-crate events off the bridge's "log" shim target
# back to the real module path, so the console's target column and the ring's noise gate see
# `mdns_sd::…` instead of "log".
tracing-log = "0.2"
axum = "0.8"
mdns-sd = "0.20"
# Wake-on-LAN: report the host's wake-capable NIC MAC(s) to clients via the mDNS `mac` TXT record.
# `mac_address` reads a NIC's hardware address; `if-addrs` maps the routed IP to its interface name.
mac_address = "1"
if-addrs = "0.13"
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12"
rsa = "0.9"
sha2 = { version = "0.10", features = ["oid"] }
aes = "0.8"
aes-gcm = "0.10"
cbc = { version = "0.1", features = ["alloc"] }
rand = "0.8"
hex = "0.4"
# Cover-art delivery in the game library: encode Lutris's local JPEGs into `data:` URLs and decode
# the Epic launcher's base64 `catcache.bin`. Cross-platform (Linux Lutris art + Windows Epic art).
base64 = "0.22"
# Blocking HTTP for the library cover-art warmer (no-auth GOG api.gog.com + Xbox displaycatalog),
# run on a background thread off the hot path. `ureq` is small + sync (no tokio here) and bundles
# webpki roots (no system cert dependency). Cross-platform so the fetch/parse code is compiled +
# checked everywhere even though only the Windows GOG/Xbox providers need it today.
ureq = "2"
rcgen = { version = "0.13", default-features = false, features = ["aws_lc_rs", "pem"] }
x509-parser = "0.16"
# Only used for the plain-HTTP nvhttp listener (`bind().serve()`); HTTPS/mTLS is hand-rolled over
# tokio-rustls (axum-server can't surface the peer cert), so we do NOT enable `tls-rustls` — that
# feature is what pulled the unmaintained `rustls-pemfile` (security-review dep hygiene).
axum-server = "0.8"
rustls = "0.23"
# Manual HTTPS+mTLS serve loop for the mgmt API (axum-server can't surface the peer cert): a
# tokio-rustls handshake exposes the client cert, then hyper serves the axum Router with the
# verified fingerprint injected as a request extension. Versions match the workspace lock.
tokio-rustls = "0.26"
hyper = { version = "1", features = ["server", "http1", "http2"] }
hyper-util = { version = "0.1", features = ["server", "server-auto", "tokio", "service"] }
tower = { version = "0.5", features = ["util"] }
# Stream combinators for the mgmt API's SSE event feed (`GET /api/v1/events`) — already in the
# tree transitively (axum/hyper) and as a Linux target dep; control plane only.
futures-util = "0.3"
# Webhook signing (X-Punktfunk-Signature: sha256=<hex HMAC>) for operator hooks; pairs with
# the existing sha2. Already in the lockfile transitively.
hmac = "0.12"
rusty_enet = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Management/control-plane REST API + OpenAPI (control pane, M2). `axum_extras` wires
# utoipa into axum 0.8 extractors; utoipa-axum collects `#[utoipa::path]` routes into the
# spec; utoipa-scalar serves the interactive docs. Codegen-friendly: the spec is emitted
# verbatim by the `openapi` subcommand. Control plane only — never the per-frame path.
utoipa = { version = "5", features = ["axum_extras"] }
utoipa-axum = "0.2"
utoipa-scalar = { version = "0.3", features = ["axum"] }
[dev-dependencies]
# Drive the management API router in-process (no socket) in the handler tests.
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1"
# Disposable directory fixtures for the Steam local-librarycache scan tests (library.rs).
tempfile = "3"
# Emit `log`-crate records through the tracing-log bridge in the log_capture tests.
log = "0.4"
# Opus encode for the host->client audio plane — stereo (`opus::Encoder`) AND 5.1/7.1 surround
# (`opus::MSEncoder`, the safe multistream API the crate exposes; no `audiopus_sys` needed). The
# crate vendors libopus (cmake-built from source — no system lib, no vcpkg), so it builds on Windows
# MSVC too (needs CMake + NASM, both on the box). Both platforms that have an audio-capture backend.
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
opus = "0.3"
[target.'cfg(target_os = "linux")'.dependencies]
# `screencast` gates the ScreenCast portal module; `remote_desktop` adds the RemoteDesktop
# portal we use for libei input on KWin/GNOME; `tokio` is the default runtime.
# `open_pipe_wire_remote` is unconditional, so ashpd's own `pipewire` feature is not
# needed — we drive PipeWire with the `pipewire` crate below.
ashpd = { version = "0.13", features = ["screencast", "remote_desktop"] }
libc = "0.2"
# Must match the pipewire crate ashpd 0.13 links (libspa/pipewire-sys `links` key is
# unique per build), i.e. 0.9 — NOT the 0.10 the setup doc mentions.
pipewire = "0.9"
# ashpd 0.13 uses the tokio runtime; a current-thread runtime drives the one-time
# portal handshake (control plane — never the per-frame path).
tokio = { version = "1", features = ["rt", "rt-multi-thread", "net", "time"] }
# Input injection into headless Sway via the wlroots virtual-input Wayland protocols
# (uinput won't reach a compositor running with WLR_LIBINPUT_NO_DEVICES=1).
wayland-client = "0.31"
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
wayland-protocols-misc = { version = "0.3", features = ["client"] }
# `xdg-output` (zxdg_output_v1): the per-output *logical* geometry (post-scale size + global
# position), used by the KWin fake_input backend to map absolute coordinates under display scaling.
wayland-protocols = { version = "0.32", features = ["client"] }
# Codegen for KDE's `zkde_screencast_unstable_v1` (vendored in `protocols/`): create a KWin
# virtual output sized to the client's resolution and get its PipeWire node (KRdp's path).
# `wayland-backend` is referenced by the generated interface tables.
wayland-scanner = "0.31"
wayland-backend = "0.3"
# Parse `pw-dump` JSON to find gamescope's PipeWire node (gamescope backend).
serde_json = "1"
# Read the Lutris library DB (`pga.db`) for the Lutris store provider. `bundled` vendors + compiles
# SQLite (cc, already needed for ffmpeg/opus) so there's no system libsqlite3 runtime dependency —
# clean for the deb/rpm/flatpak packaging. Opened read-only/immutable (Lutris may hold it open).
rusqlite = { version = "0.40", features = ["bundled"] }
# Builds/validates the xkb keymap uploaded to the virtual keyboard + tracks modifier state.
xkbcommon = "0.8"
# libei (EI sender) for the portable input path on KWin/GNOME (RemoteDesktop portal).
# The `tokio` feature wires reis's event stream into tokio's reactor.
reis = { version = "0.6.1", features = ["tokio"] }
# `StreamExt::next` on reis's tokio event stream in the libei worker loop.
futures-util = "0.3"
# Zero-copy capture (plan §9): EGL imports the PipeWire dmabuf, CUDA maps it, NVENC encodes
# it with no CPU roundtrip. `khronos-egl` (dynamic = load the NVIDIA libEGL at runtime) gives
# eglCreateImage + the dma_buf import; the CUDA driver API (EGL interop) and libgbm are linked
# via hand-rolled FFI in `src/zerocopy/` (no Rust crate exposes the EGL-interop driver calls).
khronos-egl = { version = "6", features = ["dynamic"] }
# Vulkan bridge for LINEAR dmabufs (gamescope): import via VK_EXT_external_memory_dma_buf,
# GPU-copy into an exportable allocation, export OPAQUE_FD → cuImportExternalMemory (the
# officially-supported CUDA pairing; raw dmabuf fds are rejected by the desktop driver).
ash = "0.38"
# `libcuda.so.1` is dlopen'd at runtime (NOT link-time) so one Linux binary runs on NVIDIA
# (zero-copy via CUDA) AND on AMD/Intel (VAAPI, no NVIDIA driver present) — see `zerocopy::cuda`.
libloading = "0.8"
# Vendored + trimmed `usbip` server core (no libusb) — presents a virtual Steam Deck over USB/IP
# so the local `vhci_hcd` attaches it: the shippable, Secure-Boot-clean, Steam-Input-promotable
# virtual-Deck transport on non-SteamOS hosts (`inject/linux/steam_usbip.rs`). See the crate's NOTICE.
usbip-sim = { path = "vendor/usbip-sim" }
[target.'cfg(target_os = "windows")'.dependencies]
# Windows host backends. `windows` covers the Win32/CCD APIs the SudoVDA virtual-display backend
# drives (SetupAPI device enumeration, DeviceIoControl IOCTLs, QueryDisplayConfig name resolution);
# capture/encode/input/audio backends extend the feature set as they land.
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_Security",
# ConvertStringSecurityDescriptorToSecurityDescriptorW — the SDDL on the virtual-DualSense
# shared-memory section (inject/dualsense_windows.rs) so the UMDF host can open it.
"Win32_Security_Authorization",
"Win32_Devices_DeviceAndDriverInstallation",
# SwDeviceCreate/SwDeviceClose — the per-session virtual-DualSense devnode
# (inject/dualsense_windows.rs).
"Win32_Devices_Enumeration_Pnp",
"Win32_Devices_Display",
"Win32_Storage_FileSystem",
"Win32_System_IO",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
"Win32_System_StationsAndDesktops",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D_Fxc",
"Win32_Graphics_Gdi",
# Windows.Graphics.Capture (WGC) backend — composed-desktop capture (overlay/MPO-correct HDR).
"Foundation",
"Graphics",
"Graphics_Capture",
"Graphics_DirectX",
"Graphics_DirectX_Direct3D11",
"Win32_System_WinRT_Direct3D11",
"Win32_System_WinRT_Graphics_Capture",
# WGC runs under SYSTEM via interactive-user impersonation (WGC won't activate as SYSTEM).
"Win32_System_RemoteDesktop",
# Two-process secure-desktop design: the SYSTEM host spawns the WGC helper in the interactive
# user session (CreateProcessAsUserW) with stdout/stdin redirected to anonymous pipes.
"Win32_System_Threading",
"Win32_System_Pipes",
"Win32_System_Environment",
# Force-composed-flip overlay: a topmost layered window on the Winlogon desktop disqualifies the
# secure desktop's fullscreen independent-flip so Desktop Duplication can capture it.
"Win32_System_LibraryLoader",
# VirtualProtect — for the inline patch of the win32u GPU-preference shim (Apollo's MinHook port:
# the hybrid-GPU output-reparenting hook that keeps Desktop Duplication stable on a 4090+iGPU box).
# See capture/windows/dxgi.rs `install_gpu_pref_hook`. No trampoline (we fully replace the fn) → no detour
# crate / no C length-disassembler dep; a 12-byte absolute-jmp prologue patch suffices.
"Win32_System_Memory",
# Per-monitor-v2 DPI awareness — IDXGIOutput5::DuplicateOutput1 (the modern capture path Apollo
# uses; FP16/format-list, robust to overlay/format churn) requires the process to be DPI-aware.
"Win32_UI_HiDpi",
# Windows service supervisor (src/service.rs): a kill-on-close job object so a service crash never
# orphans the SYSTEM host it launched into the interactive session.
"Win32_System_JobObjects",
# CoCreateInstance(PolicyConfigClient) — set the default audio playback/recording endpoints via the
# undocumented IPolicyConfig (audio/windows/audio_control.rs) so mic + desktop audio auto-wire.
"Win32_System_Com",
# SetUnhandledExceptionFilter + EXCEPTION_POINTERS — the last-resort native-crash logger
# (src/windows/crash.rs); Kernel gates the CONTEXT type EXCEPTION_POINTERS embeds.
"Win32_System_Diagnostics_Debug",
"Win32_System_Kernel",
# CreateToolhelp32Snapshot/Process32*W — the conflicting-streaming-host process scan
# (src/detect/windows.rs): is Sunshine/Apollo/... running alongside us?
"Win32_System_Diagnostics_ToolHelp",
] }
# The SCM plumbing for the `service` subcommand (define_windows_service! / dispatcher / control
# handler / ServiceManager install). Wraps the Win32 service API; the supervision loop itself uses
# the `windows` crate above.
windows-service = "0.7"
# Read the GOG.com install registry (HKLM\SOFTWARE\WOW6432Node\GOG.com\Games) for the GOG store
# provider — ergonomic + correct-by-construction vs. hand-rolled Reg* FFI for subkey enumeration.
winreg = "0.56"
# Parse each Xbox/Game-Pass game's MicrosoftGame.config (GDK manifest XML) for the Xbox store
# provider — a small read-only DOM is all we need (Identity/Executable/ShellVisuals/StoreId).
roxmltree = "0.21"
# WASAPI loopback audio capture (default render endpoint -> 48 kHz stereo f32 for the Opus path).
wasapi = "0.23"
# Shared host<->driver wire contract for the pf-vdisplay IddCx virtual-display backend
# (vdisplay/pf_vdisplay.rs): the control-plane IOCTL codes + `#[repr(C)] Pod` request/reply structs,
# defined ONCE so host<->driver ABI drift is a compile error. `bytemuck` serializes those structs
# to/from the DeviceIoControl byte buffers.
pf-driver-proto = { path = "../pf-driver-proto" }
bytemuck = { version = "1.19", features = ["derive"] }
# The encode feature flags now FORWARD to the pf-encode subsystem crate (the heavy encoder deps —
# ffmpeg-next, the NVENC SDK, openh264, pyrowave-sys — moved there, plan §W6). Selecting a feature
# on the host turns on the matching backend inside pf-encode.
[features]
# PyroWave ships in every default build (the codec stays strictly opt-in per session — a client
# must explicitly prefer CODEC_PYROWAVE; nothing changes for normal HEVC/AV1 sessions).
default = ["pyrowave"]
# NVENC hardware encode (Linux CUDA + Windows D3D11). OFF by default; entry points resolved at
# RUNTIME from the driver DLL/so, so the same binary starts fine on AMD/Intel boxes. Build the GPU
# host with `--features nvenc`.
nvenc = ["pf-encode/nvenc"]
# AMD/Intel hardware encode on Windows (AMF/QSV via ffmpeg-next). OFF by default: needs a `FFMPEG_DIR`
# (BtbN lgpl-shared with `*_amf`/`*_qsv`) at build and bundles the FFmpeg DLLs at runtime. Build the
# all-vendor GPU host with `--features nvenc,amf-qsv`.
amf-qsv = ["pf-encode/amf-qsv"]
# Raw Vulkan Video HEVC/AV1 encode on Linux (AMD/Intel) — real reference-frame-invalidation loss
# recovery via explicit DPB reference slots (design/linux-vulkan-video-encode.md). OFF by default;
# reuses pf-encode's `ash` bindings (no new dep). Runtime-gated further by PUNKTFUNK_VULKAN_ENCODE.
vulkan-encode = ["pf-encode/vulkan-encode"]
# PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md). Builds
# the vendored codec from source in pf-encode. ON by default (see `default`); sessions reach it only
# through explicit client opt-in.
pyrowave = ["pf-encode/pyrowave"]
# Build-time icon/version-info embedding (build.rs; Windows dev/CI hosts only — Linux packaging
# builds of this crate never execute the winresource block).
[target.'cfg(windows)'.build-dependencies]
winresource = "0.1"