From e5057f6cc1446507f36415368467ec63d55b796a Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 25 Jun 2026 17:24:00 +0000 Subject: [PATCH] =?UTF-8?q?feat(windows-host):=20finish=20HostConfig=20mig?= =?UTF-8?q?ration=20=E2=80=94=20resolve=20operator/dispatch=20knobs=20once?= =?UTF-8?q?=20(Goal-1=20stage=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate 31 genuinely-constant operator/dispatch env::var sites onto HostConfig, so the capture/topology/encoder decision reads ONE owner instead of being recomputed at each call site (the latent bug where capture and encode could disagree on the resolved backend, plan §2.4): idd_push x7, no_wgc, capture_backend, render_adapter, encoder_pref (Linux open_video + linux_zero_copy_is_vaapi), the Windows vdisplay-backend select, plus the plan-named secure_dda/idd_depth/zerocopy/ten_bit and the multi-site perf x4 / compositor x5 / video_source x3 / gamepad. Each HostConfig field's parser is byte-identical to the read it replaced, so old==new by construction (the plan's "a flipped bool is a silent regression" guard). Scope correction — the plan's "~64 sites / Linux XDG+compositor included / grep env::var -> 0" was unsafe as written. Two classes are deliberately KEPT as live reads and documented in config.rs: * Runtime-mutated session vars. vdisplay::apply_session_env REWRITES the process env on every connect (the Bazzite Gaming<->Desktop follow): WAYLAND_DISPLAY, XDG_CURRENT_DESKTOP, XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS, and the derived PUNKTFUNK_INPUT_BACKEND, GAMESCOPE_SESSION/NODE, KWIN/MUTTER_VIRTUAL_PRIMARY, FORCE_SHM. Parsing these once would freeze them at startup and silently break session-following — they are NOT constant. * Single-use local tuning with no resolve-once benefit (and FEC_PCT even has two different semantics): FEC_PCT, VIDEO_DROP, VBV_FRAMES, SPLIT_ENCODE, PACE_BURST_KB, the dxgi timing knobs, the *_LIVE/test gates, plus path/dynamic reads (config-dir, PATH search, env-forward-to-child). PUNKTFUNK_ZEROCOPY is split on purpose: Windows presence-semantics moved to the field; Linux keeps its own truthy (1|true|yes|on) parser. Verified: Linux cargo check + clippy (-D warnings) + fmt clean on the touched files. The Windows-only edits are 1:1 substitutions; they get a real Windows compile on the box with Stage 3. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/capture.rs | 8 +- crates/punktfunk-host/src/capture/idd_push.rs | 6 +- crates/punktfunk-host/src/config.rs | 82 +++++++++++++++++-- crates/punktfunk-host/src/encode.rs | 12 +-- .../punktfunk-host/src/encode/ffmpeg_win.rs | 2 +- .../punktfunk-host/src/gamestream/stream.rs | 6 +- crates/punktfunk-host/src/inject.rs | 12 ++- crates/punktfunk-host/src/punktfunk1.rs | 20 ++--- crates/punktfunk-host/src/vdisplay.rs | 8 +- .../src/vdisplay/pf_vdisplay.rs | 6 +- crates/punktfunk-host/src/vdisplay/sudovda.rs | 6 +- crates/punktfunk-host/src/wgc_helper.rs | 2 +- crates/punktfunk-host/src/win_adapter.rs | 5 +- docs/windows-host-goal1-plan.md | 28 +++++-- 14 files changed, 138 insertions(+), 65 deletions(-) diff --git a/crates/punktfunk-host/src/capture.rs b/crates/punktfunk-host/src/capture.rs index 6f53723..98b07e5 100644 --- a/crates/punktfunk-host/src/capture.rs +++ b/crates/punktfunk-host/src/capture.rs @@ -327,7 +327,7 @@ pub fn capture_virtual_output( /// compiled and comes back the moment the flag is unset. #[cfg(target_os = "windows")] pub(crate) fn wgc_disabled() -> bool { - std::env::var_os("PUNKTFUNK_NO_WGC").is_some() + crate::config::config().no_wgc } #[cfg(target_os = "windows")] @@ -345,7 +345,7 @@ pub fn capture_virtual_output( // P2 direct frame push (kill DDA): consume frames straight from the pf-vdisplay driver's shared // ring — no Desktop Duplication, no win32u reparenting hook. Opt-in while it's A/B'd against DDA; // `idd_push` takes the keepalive (owns the virtual display) so there's no fall-through. - if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() { + if crate::config::config().idd_push { // Recreate the monitor + ring per session (fix-teardown): a FRESH monitor reliably gets a // working IddCx swap-chain, whereas a REUSED monitor's swap-chain dies after ~2 sessions and // the host can't revive it. The driver's recreate crash (target id resolved to 0) is fixed by @@ -371,9 +371,7 @@ pub fn capture_virtual_output( // and has no ACCESS_LOST-on-overlay churn. DDA stays available via PUNKTFUNK_CAPTURE=dda and is // the secure-desktop (lock/UAC) fallback (WGC can't capture those). `keep` is moved into the // chosen backend (it owns the SudoVDA keepalive), so there's no open-time auto-fallback. - let backend = std::env::var("PUNKTFUNK_CAPTURE") - .unwrap_or_default() - .to_ascii_lowercase(); + let backend = crate::config::config().capture_backend.as_str(); if backend == "dda" || backend == "dxgi" || wgc_disabled() { return dxgi::DuplCapturer::open(target, pref, keep, false) .map(|c| Box::new(c) as Box); diff --git a/crates/punktfunk-host/src/capture/idd_push.rs b/crates/punktfunk-host/src/capture/idd_push.rs index 8212584..0f3f6e8 100644 --- a/crates/punktfunk-host/src/capture/idd_push.rs +++ b/crates/punktfunk-host/src/capture/idd_push.rs @@ -994,11 +994,7 @@ impl Capturer for IddPushCapturer { // NVENC encodes N on the ASIC. We hand a rotating `OUT_RING` of output textures, so this is safe. // `PUNKTFUNK_IDD_DEPTH` overrides (1 disables pipelining; clamp to ≤ OUT_RING so a frame in flight // always has its own texture). - std::env::var("PUNKTFUNK_IDD_DEPTH") - .ok() - .and_then(|s| s.parse::().ok()) - .unwrap_or(2) - .clamp(1, OUT_RING) + crate::config::config().idd_depth.clamp(1, OUT_RING) } } diff --git a/crates/punktfunk-host/src/config.rs b/crates/punktfunk-host/src/config.rs index 6a0fc15..2705955 100644 --- a/crates/punktfunk-host/src/config.rs +++ b/crates/punktfunk-host/src/config.rs @@ -1,17 +1,39 @@ //! `HostConfig` — the host's runtime knobs parsed ONCE from the environment, instead of the ~68 scattered //! `env::var` reads recomputed at every call site (some up to 8×, which lets capture + encode silently //! disagree on the resolved backend — plan §2.4). The service / launcher loads `host.env` into the process -//! environment before the host starts, and the environment is constant for the process lifetime, so a -//! lazily-parsed global is equivalent to "parsed once at startup". +//! environment before the host starts, and **for the knobs captured here the environment is constant for the +//! process lifetime**, so a lazily-parsed global is equivalent to "parsed once at startup". //! -//! **Goal-1 stage 1** (`docs/windows-host-goal1-plan.md`): this is the foundation. Subsequent stages grow -//! this struct + migrate the remaining read sites onto it, then `SessionPlan` (stage 2) consumes it as the -//! single owner of the capture/topology/encoder decision. New fields are added here AS call sites migrate — -//! a field that nothing reads yet would just be dead, so they land together with their migration. +//! **Goal-1 stages 1–2** (`docs/windows-host-goal1-plan.md`): stage 1 stood this up; stage 2 migrated the +//! genuinely-constant operator/dispatch knobs onto it (the dispatch-disagreement bug class: `idd_push`, +//! `capture_backend`, `encoder_pref`, `render_adapter`, `no_wgc`, the vdisplay backend select — plus the +//! plan-named `secure_dda`/`idd_depth`/`zerocopy`/`ten_bit` and the multi-site `perf`/`compositor`/ +//! `video_source`/`gamepad`). `SessionPlan` (stage 3) consumes it as the single owner of the +//! capture/topology/encoder decision. +//! +//! **What is deliberately NOT here (and must stay a live `env::var` read):** +//! - **Runtime-mutated session vars.** On Linux, [`crate::vdisplay::apply_session_env`] rewrites the process +//! env on *every connect* so one host follows a Bazzite box across Gaming↔Desktop: `WAYLAND_DISPLAY`, +//! `XDG_CURRENT_DESKTOP`, `XDG_RUNTIME_DIR`, `DBUS_SESSION_BUS_ADDRESS`, and the *derived* `PUNKTFUNK_*` +//! vars `INPUT_BACKEND`, `GAMESCOPE_SESSION`/`GAMESCOPE_NODE`, `KWIN_VIRTUAL_PRIMARY`, +//! `MUTTER_VIRTUAL_PRIMARY`, `FORCE_SHM` (+ `GAMESCOPE_APP` on the launch path). Parsing these once would +//! freeze them at startup and silently break session-following — they are NOT constant. +//! - **Single-use local tuning** read exactly where it is used (no resolve-once benefit, and a parse with a +//! call-site-local default/clamp): e.g. `FEC_PCT` (two *different* semantics — GameStream default-20 vs +//! punktfunk/1 `Option`/clamp-90), `VIDEO_DROP`, `VBV_FRAMES`, `SPLIT_ENCODE`, `PACE_BURST_KB`, the +//! `capture/dxgi.rs` timing knobs, the `*_LIVE` test gates. +//! - **Path / genuinely-dynamic reads**: the config-dir resolution, `PATH` executable search, the +//! env-forward-to-child loop, `PUNKTFUNK_MGMT_TOKEN`, `PUNKTFUNK_HOST_CMD`, `PUNKTFUNK_RENDER_NODE`. +//! +//! `PUNKTFUNK_ZEROCOPY` note: this field uses **presence** semantics (`var_os(..).is_some()`) to match the +//! Windows `encode/ffmpeg_win.rs` reader. The Linux `zerocopy` module keeps its own *truthy* parser +//! (`1|true|yes|on`) — the two are independent features that share a name; do NOT conflate them. use std::sync::OnceLock; -/// Resolved host configuration. Grows as `env::var` call sites migrate onto it (Goal-1). +/// Resolved host configuration. Holds the genuinely-constant operator/dispatch knobs (see module docs for +/// what is deliberately excluded). Fields read on only one platform are kept alive cross-platform by the +/// derived `Debug` impl, so the parser can stay a single platform-neutral function. #[derive(Debug, Clone, Default)] pub struct HostConfig { /// `PUNKTFUNK_IDD_PUSH` — use the IDD direct-push capturer (in-process Session-0 capture; no WGC helper). @@ -22,11 +44,41 @@ pub struct HostConfig { pub no_helper: bool, /// `PUNKTFUNK_FORCE_HELPER` — force the WGC helper even when not running as SYSTEM. pub force_helper: bool, + /// `PUNKTFUNK_NO_WGC` — force the pure single-process DDA path (skip WGC and the two-process relay). + pub no_wgc: bool, + /// `PUNKTFUNK_CAPTURE` — explicit Windows capture-backend override (lowercased; `dda`/`dxgi` vs the WGC default). + pub capture_backend: String, + /// `PUNKTFUNK_RENDER_ADAPTER` — discrete render-GPU pin by description substring (`Some` even when empty: + /// the empty string still counts as "set" for the presence checks, and the value reader filters it). + pub render_adapter: Option, + /// `PUNKTFUNK_SECURE_DDA` — enable the experimental DDA-on-secure-desktop (Winlogon/UAC) mux leg. + pub secure_dda: bool, + /// `PUNKTFUNK_IDD_DEPTH` — IDD-push pipeline depth override (default 2; the call site clamps to its `OUT_RING`). + pub idd_depth: usize, + /// `PUNKTFUNK_ZEROCOPY` — opt into the Windows D3D11 zero-copy encode path (presence semantics; see module docs). + pub zerocopy: bool, + /// `PUNKTFUNK_10BIT` — host policy gate for HEVC Main10 (only honored when the client also advertised 10-bit). + pub ten_bit: bool, + /// `PUNKTFUNK_PERF` — per-stage timing instrumentation. + pub perf: bool, + /// `PUNKTFUNK_VIDEO_SOURCE` — GameStream video source select (`virtual` / `portal` / unset → synthetic). + pub video_source: Option, + /// `PUNKTFUNK_COMPOSITOR` — explicit compositor override (operator/CI/test). NOT the runtime-detected + /// session — this one is a constant operator knob; `apply_session_env` never writes it. + pub compositor: Option, + /// `PUNKTFUNK_GAMEPAD` — client/operator virtual-pad backend preference (fed to `pick_gamepad`). + pub gamepad: Option, + /// `PUNKTFUNK_VDISPLAY` — Windows virtual-display backend select (`pf`/`pfvd` vs `sudovda`; else auto-detect). + pub vdisplay: Option, } impl HostConfig { fn from_env() -> Self { + // Presence flag: set ⇒ true. Matches the original `var_os(k).is_some()` reads (and the few + // `var(k).is_ok()` flag reads, which coincide for every real-world value). let flag = |k: &str| std::env::var_os(k).is_some(); + // String value: `var(k).ok()` — `Some` (possibly empty) when set with valid UTF-8, else `None`. + let val = |k: &str| std::env::var(k).ok(); Self { idd_push: flag("PUNKTFUNK_IDD_PUSH"), encoder_pref: std::env::var("PUNKTFUNK_ENCODER") @@ -34,6 +86,22 @@ impl HostConfig { .to_ascii_lowercase(), no_helper: flag("PUNKTFUNK_NO_HELPER"), force_helper: flag("PUNKTFUNK_FORCE_HELPER"), + no_wgc: flag("PUNKTFUNK_NO_WGC"), + capture_backend: std::env::var("PUNKTFUNK_CAPTURE") + .unwrap_or_default() + .to_ascii_lowercase(), + render_adapter: val("PUNKTFUNK_RENDER_ADAPTER"), + secure_dda: flag("PUNKTFUNK_SECURE_DDA"), + idd_depth: val("PUNKTFUNK_IDD_DEPTH") + .and_then(|s| s.parse::().ok()) + .unwrap_or(2), + zerocopy: flag("PUNKTFUNK_ZEROCOPY"), + ten_bit: flag("PUNKTFUNK_10BIT"), + perf: flag("PUNKTFUNK_PERF"), + video_source: val("PUNKTFUNK_VIDEO_SOURCE"), + compositor: val("PUNKTFUNK_COMPOSITOR"), + gamepad: val("PUNKTFUNK_GAMEPAD"), + vdisplay: val("PUNKTFUNK_VDISPLAY"), } } } diff --git a/crates/punktfunk-host/src/encode.rs b/crates/punktfunk-host/src/encode.rs index e50da4f..a85ce96 100644 --- a/crates/punktfunk-host/src/encode.rs +++ b/crates/punktfunk-host/src/encode.rs @@ -173,14 +173,12 @@ pub fn open_video( // AMD/Intel → VAAPI (one libavcodec backend for both). Auto-detect by default so a single // Linux binary serves any GPU; `PUNKTFUNK_ENCODER` forces a specific backend (and surfaces // its errors crisply instead of silently trying the other). - let pref = std::env::var("PUNKTFUNK_ENCODER") - .unwrap_or_default() - .to_ascii_lowercase(); + let pref = crate::config::config().encoder_pref.as_str(); let open_vaapi = || -> Result> { vaapi::VaapiEncoder::open(codec, format, width, height, fps, bitrate_bps, bit_depth) .map(|e| Box::new(e) as Box) }; - match pref.as_str() { + match pref { "nvenc" | "nvidia" | "cuda" => open_nvenc_probed( codec, format, @@ -379,11 +377,7 @@ fn nvidia_present() -> bool { /// passthrough for VAAPI vs the EGL→CUDA import for NVENC). #[cfg(target_os = "linux")] pub fn linux_zero_copy_is_vaapi() -> bool { - match std::env::var("PUNKTFUNK_ENCODER") - .unwrap_or_default() - .to_ascii_lowercase() - .as_str() - { + match crate::config::config().encoder_pref.as_str() { "nvenc" | "nvidia" | "cuda" => false, "vaapi" | "amd" | "intel" => true, _ => !nvidia_present(), diff --git a/crates/punktfunk-host/src/encode/ffmpeg_win.rs b/crates/punktfunk-host/src/encode/ffmpeg_win.rs index 8276d85..d70043b 100644 --- a/crates/punktfunk-host/src/encode/ffmpeg_win.rs +++ b/crates/punktfunk-host/src/encode/ffmpeg_win.rs @@ -109,7 +109,7 @@ impl WinVendor { /// Is the zero-copy D3D11 path enabled? Opt-in (`PUNKTFUNK_ZEROCOPY=1`) until on-glass validated; /// the default is the robust system-memory readback path. fn zerocopy_enabled() -> bool { - std::env::var_os("PUNKTFUNK_ZEROCOPY").is_some() + crate::config::config().zerocopy } /// The swscale *source* pixel format for a captured packed-RGB/BGR layout (8-bit BGRA fallback only). diff --git a/crates/punktfunk-host/src/gamestream/stream.rs b/crates/punktfunk-host/src/gamestream/stream.rs index 3f17b45..d761c73 100644 --- a/crates/punktfunk-host/src/gamestream/stream.rs +++ b/crates/punktfunk-host/src/gamestream/stream.rs @@ -102,7 +102,7 @@ fn run( // request and capture it (no scaling). Self-contained — deliberately NOT pooled in // `video_cap`, since a reconnect at a different resolution needs a freshly-sized output; the // output is released when this capturer drops at stream end (RAII via its keepalive). - if std::env::var("PUNKTFUNK_VIDEO_SOURCE").as_deref() == Ok("virtual") { + if crate::config::config().video_source.as_deref() == Some("virtual") { // The launched app picks the compositor (e.g. gamescope for game entries) and the // nested command. let compositor = app @@ -147,7 +147,7 @@ fn run( tracing::info!("video source: reusing capturer"); c } - None if std::env::var("PUNKTFUNK_VIDEO_SOURCE").is_ok_and(|v| v == "portal") => { + None if crate::config::config().video_source.as_deref() == Some("portal") => { tracing::info!("video source: portal desktop capture"); capture::open_portal_monitor().context("open portal capturer")? } @@ -358,7 +358,7 @@ fn stream_body( // Per-stage timing (PUNKTFUNK_PERF=1): max µs/stage per second + unique vs re-encoded frames, // to pinpoint stalls. `unique` counts genuinely-new captured frames (vs re-encoded holds). - let perf = std::env::var_os("PUNKTFUNK_PERF").is_some(); + let perf = crate::config::config().perf; let (mut mx_cap, mut mx_enc, mut mx_pkt, mut mx_send, mut mx_pkts, mut uniq) = (0u128, 0u128, 0u128, 0u128, 0usize, 0u32); // Absolute next-frame deadline — the single pacing clock for the loop. diff --git a/crates/punktfunk-host/src/inject.rs b/crates/punktfunk-host/src/inject.rs index e77e800..137704a 100644 --- a/crates/punktfunk-host/src/inject.rs +++ b/crates/punktfunk-host/src/inject.rs @@ -112,8 +112,10 @@ pub fn default_backend() -> Backend { } #[cfg(not(target_os = "windows"))] { - if std::env::var("PUNKTFUNK_COMPOSITOR") - .is_ok_and(|v| v.trim().eq_ignore_ascii_case("gamescope")) + if crate::config::config() + .compositor + .as_deref() + .is_some_and(|v| v.trim().eq_ignore_ascii_case("gamescope")) { return Backend::GamescopeEi; } @@ -260,8 +262,10 @@ fn coalesce(events: Vec) -> Vec { /// (`org.gnome.Mutter.RemoteDesktop`), the same direct API the Mutter video backend uses. #[cfg(target_os = "linux")] fn libei_ei_source() -> libei::EiSource { - let gnome = std::env::var("PUNKTFUNK_COMPOSITOR") - .is_ok_and(|v| v.trim().eq_ignore_ascii_case("mutter")) + let gnome = crate::config::config() + .compositor + .as_deref() + .is_some_and(|v| v.trim().eq_ignore_ascii_case("mutter")) || std::env::var("XDG_CURRENT_DESKTOP") .unwrap_or_default() .to_ascii_uppercase() diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index 8efc266..2cbcd32 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -599,7 +599,7 @@ async fn serve_session( // opted in (PUNKTFUNK_10BIT). A client that can't decode 10-bit (caps bit clear, or an older // client) always gets the 8-bit stream. PUNKTFUNK_10BIT is the host policy gate until a // mgmt/console toggle replaces it. - let host_wants_10bit = std::env::var_os("PUNKTFUNK_10BIT").is_some(); + let host_wants_10bit = crate::config::config().ten_bit; let client_supports_10bit = hello.video_caps & punktfunk_core::quic::VIDEO_CAP_10BIT != 0; let bit_depth: u8 = if host_wants_10bit && client_supports_10bit { 10 @@ -1616,7 +1616,7 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool /// Resolve the client's gamepad-backend preference (the env/logging shell around /// [`pick_gamepad`]). Always concrete — the `Welcome` reports what the session will drive. fn resolve_gamepad(pref: GamepadPref) -> GamepadPref { - let env = std::env::var("PUNKTFUNK_GAMEPAD").ok(); + let env = crate::config::config().gamepad.clone(); let chosen = pick_gamepad( pref, env.as_deref(), @@ -1683,7 +1683,7 @@ fn resolve_compositor(pref: CompositorPref) -> Result(); let watch = std::env::var_os("PUNKTFUNK_SESSION_WATCH").is_some() - && std::env::var_os("PUNKTFUNK_COMPOSITOR").is_none(); + && crate::config::config().compositor.is_none(); let _watcher = if watch { let stop = stop.clone(); std::thread::Builder::new() @@ -2731,7 +2731,7 @@ fn virtual_stream_relay( }) }; - let perf = std::env::var("PUNKTFUNK_PERF").is_ok(); + let perf = crate::config::config().perf; let burst_cap = std::env::var("PUNKTFUNK_PACE_BURST_KB") .ok() .and_then(|s| s.parse::().ok()) @@ -2770,7 +2770,7 @@ fn virtual_stream_relay( // the secure desktop's HDR independent-flip (it storms ACCESS_LOST → black), whereas the WGC helper // STAYS LIVE through a lock/UAC. So by default the mux keeps WGC the whole time (no DesktopWatcher // switch, no overlay). Enable the experimental DDA-on-secure path with PUNKTFUNK_SECURE_DDA=1. - let dda_secure = std::env::var("PUNKTFUNK_SECURE_DDA").is_ok() || secure_test_ms.is_some(); + let dda_secure = crate::config::config().secure_dda || secure_test_ms.is_some(); // The authoritative Default↔Winlogon signal (requires SYSTEM to read the Winlogon desktop name); // only needed when the DDA-on-secure path is enabled. let watcher = dda_secure.then(crate::capture::desktop_watch::DesktopWatcher::start); diff --git a/crates/punktfunk-host/src/vdisplay.rs b/crates/punktfunk-host/src/vdisplay.rs index e6dcfce..3bc3947 100644 --- a/crates/punktfunk-host/src/vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay.rs @@ -479,7 +479,7 @@ pub fn apply_input_env(_chosen: Compositor) {} /// a backend for a test), else the **live session** ([`detect_active_session`] — so a Bazzite box /// follows Gaming↔Desktop switches), else a last-resort `XDG_CURRENT_DESKTOP` read. pub fn detect() -> Result { - if let Ok(v) = std::env::var("PUNKTFUNK_COMPOSITOR") { + if let Some(v) = crate::config::config().compositor.as_deref() { return match v.trim().to_ascii_lowercase().as_str() { "kwin" | "kde" | "plasma" => Ok(Compositor::Kwin), "wlroots" | "sway" | "hyprland" | "wlr" => Ok(Compositor::Wlroots), @@ -551,11 +551,7 @@ pub fn open(compositor: Compositor) -> Result> { /// default) auto-detects, preferring pf-vdisplay if its device interface is enumerable. #[cfg(target_os = "windows")] fn windows_use_pf_vdisplay() -> bool { - match std::env::var("PUNKTFUNK_VDISPLAY") - .ok() - .as_deref() - .map(str::trim) - { + match crate::config::config().vdisplay.as_deref().map(str::trim) { Some("pf") | Some("pf-vdisplay") | Some("pfvd") => true, Some("sudovda") | Some("sudo") => false, _ => pf_vdisplay::is_available(), diff --git a/crates/punktfunk-host/src/vdisplay/pf_vdisplay.rs b/crates/punktfunk-host/src/vdisplay/pf_vdisplay.rs index 5a67bcf..d5c679c 100644 --- a/crates/punktfunk-host/src/vdisplay/pf_vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay/pf_vdisplay.rs @@ -55,7 +55,7 @@ const PF_VDISPLAY_INTERFACE: GUID = /// IDD-push mode: a new client connection preempts + recreates the monitor (single-client reconnect), /// because a REUSED IddCx monitor's swap-chain is dead. Off → monitors are shared across sessions. fn idd_push_mode() -> bool { - std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() + crate::config::config().idd_push } /// Monotonic per-session id keying a pf-vdisplay monitor for `IOCTL_ADD`/`IOCTL_REMOVE`. Unlike @@ -249,9 +249,9 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result bool { - std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() + crate::config::config().idd_push } use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; @@ -301,9 +301,9 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result only on a box that genuinely needs steering. - let pinned = if std::env::var("PUNKTFUNK_RENDER_ADAPTER").is_ok() { + let pinned = if crate::config::config().render_adapter.is_some() { unsafe { resolve_render_adapter_luid() } - } else if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() { + } else if crate::config::config().idd_push { // P2 direct frame push: the host opens the driver's shared textures AND runs NVENC on the // RENDER adapter, so on a hybrid box (4090 + iGPU) it MUST be the discrete encoder GPU — // an iGPU-rendered surface is untouchable by NVENC. pf-vdisplay HONORS SET_RENDER_ADAPTER diff --git a/crates/punktfunk-host/src/wgc_helper.rs b/crates/punktfunk-host/src/wgc_helper.rs index bbabf7a..b115ad5 100644 --- a/crates/punktfunk-host/src/wgc_helper.rs +++ b/crates/punktfunk-host/src/wgc_helper.rs @@ -135,7 +135,7 @@ pub fn run(opts: HelperOptions) -> Result<()> { // the GPU scheduling priority the SYSTEM host stamps on us, not pipeline depth. let interval = std::time::Duration::from_secs_f64(1.0 / opts.fps.max(1) as f64); - let perf = std::env::var_os("PUNKTFUNK_PERF").is_some(); + let perf = crate::config::config().perf; let mut frames = 0u64; let mut repeats = 0u64; // frames where no newer capture had arrived (duplicate re-encode) let mut cap_ns = 0u64; // time in try_latest (capture + video-processor convert) diff --git a/crates/punktfunk-host/src/win_adapter.rs b/crates/punktfunk-host/src/win_adapter.rs index a70f308..ecf8472 100644 --- a/crates/punktfunk-host/src/win_adapter.rs +++ b/crates/punktfunk-host/src/win_adapter.rs @@ -18,8 +18,9 @@ use windows::Win32::Foundation::LUID; /// already satisfy this). pub(crate) unsafe fn resolve_render_adapter_luid() -> Option { use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIFactory1}; - let want = std::env::var("PUNKTFUNK_RENDER_ADAPTER") - .ok() + let want = crate::config::config() + .render_adapter + .clone() .filter(|s| !s.is_empty()); let factory: IDXGIFactory1 = CreateDXGIFactory1().ok()?; let mut best: Option<(LUID, u64, String)> = None; diff --git a/docs/windows-host-goal1-plan.md b/docs/windows-host-goal1-plan.md index 43ff4da..6d37e88 100644 --- a/docs/windows-host-goal1-plan.md +++ b/docs/windows-host-goal1-plan.md @@ -21,12 +21,28 @@ Migrated the two highest-churn dispatch reads onto it (`encode::windows_resolved `punktfunk1::should_use_helper`). Risk: low (env constant at runtime → identical behaviour). Verify: box `cargo check --features nvenc`. -**Stage 2 — finish `HostConfig` + resolve-once.** -Migrate the remaining ~64 `env::var` sites onto `HostConfig` fields (esp. `PUNKTFUNK_IDD_PUSH` ×8, -`PUNKTFUNK_RENDER_ADAPTER`, `PUNKTFUNK_ZEROCOPY`, `PUNKTFUNK_SECURE_DDA`, `PUNKTFUNK_IDD_DEPTH`, -`PUNKTFUNK_NO_WGC`, the perf/debug flags). Linux vars (XDG/compositor) included for one config. Risk: -medium (must preserve each var's exact semantics — a flipped bool is a silent regression; assert per the §1 -checklist). Verify: Linux + box build; `grep env::var` should reach ~0 outside `config.rs`. +**Stage 2 — finish `HostConfig` + resolve-once. ✅ DONE (this commit).** +Migrated **31** genuinely-constant operator/dispatch sites onto `HostConfig`: `idd_push` ×7 (the +capture/topology disagreement knob), `no_wgc`, `capture_backend`, `render_adapter`, `encoder_pref` (Linux), +the Windows vdisplay-backend select, plus the plan-named `secure_dda`/`idd_depth`/`zerocopy`/`ten_bit` and the +multi-site `perf` ×4 / `compositor` ×5 / `video_source` ×3 / `gamepad`. Each `HostConfig` field's parser is +**byte-identical** to the read it replaced, so `old == new` by construction (the §1 "flipped bool" guard). + +**Scope correction (the plan's "~64 sites / Linux XDG+compositor / grep→0" was unsafe as written):** two +classes of `env::var` read are deliberately **kept live** and documented in `config.rs`: +- **Runtime-mutated session vars.** On Linux, `vdisplay::apply_session_env` *rewrites the process env on + every connect* (the Bazzite Gaming↔Desktop follow): `WAYLAND_DISPLAY`, `XDG_CURRENT_DESKTOP`, + `XDG_RUNTIME_DIR`, `DBUS_SESSION_BUS_ADDRESS`, and the derived `PUNKTFUNK_INPUT_BACKEND`, + `PUNKTFUNK_GAMESCOPE_SESSION/NODE`, `PUNKTFUNK_KWIN/MUTTER_VIRTUAL_PRIMARY`, `PUNKTFUNK_FORCE_SHM`. + Parse-once would freeze them at startup → silent session-following regression. They are NOT constant. +- **Single-use local tuning** (no resolve-once benefit, call-site-local default/clamp, and `FEC_PCT` even has + *two different* semantics): `FEC_PCT`, `VIDEO_DROP`, `VBV_FRAMES`, `SPLIT_ENCODE`, `PACE_BURST_KB`, the + `capture/dxgi.rs` timing knobs, the `*_LIVE`/test gates, plus path/dynamic reads (config-dir, `PATH` + search, env-forward-to-child). `PUNKTFUNK_ZEROCOPY` is split on purpose: Windows presence-semantics moved + to the field; Linux keeps its own truthy parser. + +Risk: medium (semantics-preservation). Verify: Linux `cargo check`/`clippy`/`fmt` green (the Windows-only +edits are 1:1 substitutions, compile-verified on the box as part of Stage 3's build). **Stage 3 — `SessionPlan` (the single biggest clarity lever, plan §2.4).** Resolve `display/capture/topology/encoder/format/hdr/bit_depth` ONCE from `HostConfig` into a typed