feat(windows-host): HostConfig foundation + staged Goal-1 roadmap (Goal-1 stage 1)

config.rs: typed HostConfig parsed ONCE from env (idd_push/encoder_pref/no_helper/force_helper), replacing per-call env::var re-reads (PUNKTFUNK_ENCODER was re-read on EVERY windows_resolved_backend() call; PUNKTFUNK_IDD_PUSH is read 8x across the host — the recompute that lets capture + encode disagree on the backend, plan §2.4). Migrated the two highest-churn dispatch reads onto it (encode::windows_resolved_backend, punktfunk1::should_use_helper). Behavior-identical: the env is constant for the process lifetime (the service loads host.env before launch), so a lazily-parsed global == parsed-once-at-startup.

docs/windows-host-goal1-plan.md: the ORDERED, independently-shippable execution plan for Goal-1 (the plan's biggest unstarted goal — a from-scratch layered host architecture). Six behavior-preserving, box-verified stages (HostConfig -> SessionPlan -> SessionContext/SessionFactory -> seam-trait tightenings -> src/windows tree), because the host is live-validated and a monolithic rewrite would strand it broken. Stage 1 done here; stages 3-5 rewire the deployed path and require on-glass re-test.

Verified: Linux + box (--features nvenc) cargo check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 17:02:16 +00:00
parent cd591514ad
commit a3eefc2374
5 changed files with 113 additions and 8 deletions
+45
View File
@@ -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<HostConfig> = OnceLock::new();
CFG.get_or_init(HostConfig::from_env)
}
+2 -4
View File
@@ -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,
+1
View File
@@ -15,6 +15,7 @@
#![allow(dead_code)]
mod audio;
mod config;
mod capture;
mod discovery;
#[cfg(target_os = "linux")]
+4 -4
View File
@@ -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