diff --git a/crates/punktfunk-host/src/config.rs b/crates/punktfunk-host/src/config.rs new file mode 100644 index 0000000..6a0fc15 --- /dev/null +++ b/crates/punktfunk-host/src/config.rs @@ -0,0 +1,45 @@ +//! `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". +//! +//! **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. + +use std::sync::OnceLock; + +/// Resolved host configuration. Grows as `env::var` call sites migrate onto it (Goal-1). +#[derive(Debug, Clone, Default)] +pub struct HostConfig { + /// `PUNKTFUNK_IDD_PUSH` — use the IDD direct-push capturer (in-process Session-0 capture; no WGC helper). + pub idd_push: bool, + /// `PUNKTFUNK_ENCODER` — explicit encoder-backend override (lowercased; empty = auto-detect by GPU vendor). + pub encoder_pref: String, + /// `PUNKTFUNK_NO_HELPER` — never spawn the user-session WGC helper. + pub no_helper: bool, + /// `PUNKTFUNK_FORCE_HELPER` — force the WGC helper even when not running as SYSTEM. + pub force_helper: bool, +} + +impl HostConfig { + fn from_env() -> Self { + let flag = |k: &str| std::env::var_os(k).is_some(); + Self { + idd_push: flag("PUNKTFUNK_IDD_PUSH"), + encoder_pref: std::env::var("PUNKTFUNK_ENCODER") + .unwrap_or_default() + .to_ascii_lowercase(), + no_helper: flag("PUNKTFUNK_NO_HELPER"), + force_helper: flag("PUNKTFUNK_FORCE_HELPER"), + } + } +} + +/// The process-wide host configuration, parsed once on first access. +pub fn config() -> &'static HostConfig { + static CFG: OnceLock = OnceLock::new(); + CFG.get_or_init(HostConfig::from_env) +} diff --git a/crates/punktfunk-host/src/encode.rs b/crates/punktfunk-host/src/encode.rs index e3e7d9b..e50da4f 100644 --- a/crates/punktfunk-host/src/encode.rs +++ b/crates/punktfunk-host/src/encode.rs @@ -450,10 +450,8 @@ enum GpuVendor { /// vendor). Shared by [`open_video`] and the GameStream codec advertisement so both agree. #[cfg(target_os = "windows")] pub(crate) fn windows_resolved_backend() -> WindowsBackend { - let pref = std::env::var("PUNKTFUNK_ENCODER") - .unwrap_or_default() - .to_ascii_lowercase(); - match pref.as_str() { + // Resolved ONCE in HostConfig (Goal-1) — was re-read from PUNKTFUNK_ENCODER on every call. + match crate::config::config().encoder_pref.as_str() { "nvenc" | "hw" | "nvidia" | "cuda" => WindowsBackend::Nvenc, "amf" | "amd" => WindowsBackend::Amf, "qsv" | "intel" => WindowsBackend::Qsv, diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 70bcfdc..9caf20b 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -15,6 +15,7 @@ #![allow(dead_code)] mod audio; +mod config; mod capture; mod discovery; #[cfg(target_os = "linux")] diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index 54023fe..8efc266 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -2577,7 +2577,8 @@ fn virtual_stream( /// desktop, Apollo-style), which has no WGC helper to relay. #[cfg(target_os = "windows")] fn should_use_helper() -> bool { - if std::env::var_os("PUNKTFUNK_NO_HELPER").is_some() || crate::capture::wgc_disabled() { + let cfg = crate::config::config(); + if cfg.no_helper || crate::capture::wgc_disabled() { return false; } // IDD direct-push captures IN-PROCESS in Session 0: the pf-vdisplay driver delivers frames to the @@ -2585,11 +2586,10 @@ fn should_use_helper() -> bool { // needed for VIDEO (and a Session-1 helper couldn't open the Session-0 shared textures anyway). // NOTE: input injection (SendInput) from Session 0 can't reach the user's Session-1 desktop yet — // a known follow-up; this path validates the video transport. See docs/windows-virtual-display-rust-port.md. - if std::env::var_os("PUNKTFUNK_IDD_PUSH").is_some() { + if cfg.idd_push { return false; } - std::env::var_os("PUNKTFUNK_FORCE_HELPER").is_some() - || crate::capture::wgc_relay::running_as_system() + cfg.force_helper || crate::capture::wgc_relay::running_as_system() } /// Windows two-process video stream: the SYSTEM host creates the SudoVDA virtual output (and holds diff --git a/docs/windows-host-goal1-plan.md b/docs/windows-host-goal1-plan.md new file mode 100644 index 0000000..43ff4da --- /dev/null +++ b/docs/windows-host-goal1-plan.md @@ -0,0 +1,61 @@ +# Goal-1 (clean, layered host architecture) — staged execution plan + +The design is in [`windows-host-rewrite.md`](windows-host-rewrite.md) §2.2–2.4. This file is the **ordered, +independently-shippable execution plan**, because the host is **live-validated** (GameStream + punktfunk/1, +NVENC + IDD-push on-glass) and Goal-1 rewires its session/config/dispatch flow — so every stage must +**preserve behavior**, compile + box-verify on its own, and be committed before the next starts. The plan's +own §14 makes the §1 preservation checklist a mandatory per-module assert contract; honour it. + +## Why staged (not one big rewrite) + +`main` is at parity and shipping. A monolithic rewrite would put the validated host in a broken +intermediate state for a long window and make a regression impossible to bisect. Each stage below is a +behaviour-preserving transform with its own verification, so a regression is caught at the stage that +introduced it. + +## Stages (ordered; each = goal · files · risk · verify) + +**Stage 1 — `HostConfig` foundation. ✅ DONE (this commit).** +`config.rs`: typed `HostConfig` parsed ONCE from env (`idd_push`/`encoder_pref`/`no_helper`/`force_helper`). +Migrated the two highest-churn dispatch reads onto it (`encode::windows_resolved_backend`, +`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 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 +`SessionPlan`; replace the 3-place dispatch (`capture_virtual_output`, `should_use_helper`/`virtual_stream`, +`windows_resolved_backend`) with reads off the plan. Fixes the capture/encode backend-disagreement bug +class. Risk: medium-high (rewires the deployed decision). Verify: box build + on-glass re-test (NVENC + +IDD-push + a mode switch). + +**Stage 4 — `SessionContext` + `SessionFactory`/`Session`.** +Bundle the 12–13-arg `#[allow(too_many_arguments)]` signatures into `SessionContext`; `SessionFactory.build()` +owns the RAII chain `vdm.lease(mode) → open_capturer(vout, fmt) → open_encoder(plan) → spawn pipeline`, with +`Session::drop` the ONLY teardown path. Risk: high (teardown ordering — the §1 RAII asserts are mandatory). +Verify: box build + on-glass (connect/disconnect/reconnect, no leaked monitors/threads). + +**Stage 5 — seam-trait tightenings (plan §2.3).** +`Capturer::open_capturer(vout, want: OutputFormat)` takes the format IN (kills the +`capture → encode::windows_resolved_backend()` back-reference recomputed in `dxgi.rs`); HDR/release become +`VirtualLease` methods (session glue names no concrete backend, contains no `unsafe`); optional encoder +features move to `EncoderCaps`. Risk: medium. Verify: box build + on-glass. + +**Stage 6 — `src/windows/` tree (cfg-sprawl confinement, plan §2.2).** +Move the Windows backends under `src/windows/` + `capture/windows/`, `encode/windows/`, `inject/windows/`, +`audio/windows/`, `vdisplay/windows.rs` behind one `#[cfg(windows)] mod windows;` seam. Pure file move + +mod/use-path updates — behaviour-identical. Risk: low-but-huge (dozens of files; compile-verify catches all). +Do LAST so the earlier semantic stages don't fight path churn. Verify: Linux + box build. + +## Guardrails (mandatory, plan §14) + +- Each stage is its own commit; box-verify before moving on. +- Stages 3–5 touch the deployed path → **on-glass re-test** (NVENC + IDD-push, a mode switch, a + connect/disconnect cycle) before the next stage. +- Preserve every `PUNKTFUNK_*` var's exact semantics; when in doubt, assert old==new at the call site.