a01f8a2f58
Root cause of the ACCESS_LOST (0x887A0026) churn + context-change freeze, found live: the box is a HYBRID system (RTX 4090 + AMD Radeon iGPU + SudoVDA). DXGI does hybrid GPU-preference resolution and REPARENTS the SudoVDA output between adapters (SET_RENDER_ADAPTER is ignored — the IDD lands on the iGPU 0x23664 while we duplicate on the 4090 0x15768), which constantly invalidates Desktop Duplication. Apollo runs fine on this same box because it hooks this away. Port Apollo's hook: replace win32u.dll!NtGdiDdDDIGetCachedHybridQueryValue to always report D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, so DXGI skips preference resolution and never reparents the output → DDA stays on one adapter. Installed once before the first DXGI factory/enumeration (DuplCapturer::open). We fully replace the function (never call the original) so a 12-byte absolute-jmp prologue patch suffices — no detour crate / C length-disassembler dependency, just VirtualProtect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
175 lines
9.3 KiB
TOML
175 lines
9.3 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"] }
|
|
# 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"] }
|
|
axum = "0.8"
|
|
mdns-sd = "0.20"
|
|
tokio = { version = "1", features = ["full"] }
|
|
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"
|
|
rcgen = { version = "0.13", default-features = false, features = ["aws_lc_rs", "pem"] }
|
|
x509-parser = "0.16"
|
|
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
|
rustls = "0.23"
|
|
rustls-pemfile = "2"
|
|
# 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"] }
|
|
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"
|
|
|
|
# Opus stereo encode for the host->client audio plane. The `opus` crate vendors libopus via
|
|
# `audiopus_sys` (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"] }
|
|
ffmpeg-next = "8"
|
|
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"] }
|
|
# 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"
|
|
# Builds/validates the xkb keymap uploaded to the virtual keyboard + tracks modifier state.
|
|
xkbcommon = "0.8"
|
|
# The safe `opus` crate is stereo-only; surround (5.1/7.1) needs the libopus *multistream*
|
|
# encoder (`opus_multistream_encoder_*`). `audiopus_sys` is the sys layer `opus` already
|
|
# vendors (same libopus link), so this adds bindings, not a second copy of the library.
|
|
audiopus_sys = "0.2"
|
|
# 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"
|
|
|
|
[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",
|
|
"Win32_Devices_DeviceAndDriverInstallation",
|
|
"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/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",
|
|
] }
|
|
# Software H.264 encoder (GPU-less path + NVENC fallback). The default `source` feature statically
|
|
# compiles OpenH264 (BSD-2) — no system lib, builds on MSVC; nasm on PATH adds the SIMD fast path.
|
|
openh264 = "0.9"
|
|
# WASAPI loopback audio capture (default render endpoint -> 48 kHz stereo f32 for the Opus path).
|
|
wasapi = "0.23"
|
|
# Virtual Xbox 360 gamepad via ViGEmBus (the uinput-xpad analogue) — driver installed separately.
|
|
# `unstable_xtarget_notification` exposes the rumble/LED back-channel (the game's force-feedback →
|
|
# `request_notification`), the analogue of the Linux uinput EV_FF read path.
|
|
vigem-client = { version = "0.1", features = ["unstable_xtarget_notification"] }
|
|
# NVENC hardware encoder (NVENC SDK, D3D11 input). The SDK pins `cudarc` with
|
|
# `cuda-version-from-build-system` (a build-time CUDA-toolkit probe); its `ci-check` feature switches
|
|
# cudarc to `dynamic-loading` (loads nvcuda.dll at runtime — nothing needed at build), which is how
|
|
# the crate builds on docs.rs/CI. We enable it so the GPU-less VM/CI compiles; the DirectX NVENC path
|
|
# never calls CUDA at runtime, so the pinned CUDA bindings version is irrelevant.
|
|
nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional = true }
|
|
|
|
[features]
|
|
# NVENC hardware encode (Windows). OFF by default: it pulls the NVENC SDK, and the host then needs
|
|
# the NVENC entry points (NvEncodeAPICreateInstance / NvEncodeAPIGetMaxSupportedVersion) at link
|
|
# time — i.e. `nvencodeapi.lib` from the NVIDIA Video Codec SDK (or an import lib generated from
|
|
# nvEncodeAPI64.dll) on the linker path. Build the GPU host with `--features nvenc`.
|
|
nvenc = ["dep:nvidia-video-codec-sdk"]
|