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:
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod audio;
|
||||
mod config;
|
||||
mod capture;
|
||||
mod discovery;
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user