[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" } # 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=) 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"