feat(windows-host): SessionPlan — resolve capture/topology/encoder once per session (Goal-1 stage 3)

New src/session_plan.rs: a Copy `SessionPlan { capture, topology, encoder, bit_depth, hdr }`
resolved ONCE from HostConfig (+ the negotiated bit_depth) at the top of `virtual_stream`,
logged, and threaded through build_pipeline_with_retry/build_pipeline. The three scattered
Windows dispatch points now read this one typed artifact instead of re-deriving from config
(plan §2.4, the "capture and encode disagree on the backend" hazard):

  * capture: capture::capture_virtual_output takes a CaptureBackend IN (was re-reading
    config().idd_push / capture_backend / no_wgc internally). CaptureBackend::resolve() is the
    single resolver, shared with the GameStream + spike call sites.
  * topology: virtual_stream reads plan.topology; should_use_helper is deleted (its body is
    session_plan::resolve_topology, verbatim). The IDD-push reconnect-preempt guard reads
    plan.capture too.
  * encoder: recorded as EncoderBackend from encode::windows_resolved_backend (config-backed +
    GPU-vendor cached since stage 2 -> already a single source). Threading encoder/input_format
    into the encoder+capturer opens (which removes the capture->windows_resolved_backend()
    back-reference recomputed in dxgi.rs) is stage 5.

Behavior-preserving by construction: each resolved decision is provably equivalent to the
pre-stage-3 reads (same config() + the same cached running_as_system()/GPU-vendor probes), so
old==new. SessionPlan is platform-neutral so it threads the shared virtual_stream/build_pipeline
signatures; on Linux it resolves to the single portal/single-process path.

Also fixes a pre-existing mod-ordering fmt drift in main.rs (mod config; / mod capture;).

Verified: Linux cargo check + clippy (-D warnings) + fmt clean on the touched files. Box build
(Windows compile) + on-glass (NVENC + IDD-push + mode switch) pending on the RTX box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 17:47:48 +00:00
parent e5057f6cc1
commit 0a63154293
7 changed files with 218 additions and 50 deletions
+13 -7
View File
@@ -315,8 +315,10 @@ pub fn open_portal_monitor() -> Result<Box<dyn Capturer>> {
pub fn capture_virtual_output(
vout: crate::vdisplay::VirtualOutput,
_want_hdr: bool,
_capture: crate::session_plan::CaptureBackend,
) -> Result<Box<dyn Capturer>> {
// The Linux host stays 8-bit (HDR is blocked upstream), so `want_hdr` is unused here.
// The Linux host stays 8-bit (HDR is blocked upstream), so `want_hdr` is unused here; the capture
// backend is always the portal (the `CaptureBackend` arg is a Windows-only dispatch — ignored here).
linux::PortalCapturer::from_virtual_output(vout).map(|c| Box::new(c) as Box<dyn Capturer>)
}
@@ -334,7 +336,9 @@ pub(crate) fn wgc_disabled() -> bool {
pub fn capture_virtual_output(
vout: crate::vdisplay::VirtualOutput,
want_hdr: bool,
capture: crate::session_plan::CaptureBackend,
) -> Result<Box<dyn Capturer>> {
use crate::session_plan::CaptureBackend;
let target = vout.win_capture.clone().ok_or_else(|| {
anyhow::anyhow!(
"SudoVDA target not yet an active display (needs a WDDM GPU to activate it)"
@@ -343,9 +347,10 @@ pub fn capture_virtual_output(
let pref = vout.preferred_mode;
let keep = vout.keepalive;
// 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 crate::config::config().idd_push {
// ring — no Desktop Duplication, no win32u reparenting hook. Resolved once in the `SessionPlan`
// (was re-derived from `config().idd_push` here); `IddPush` takes the keepalive (owns the virtual
// display) so there's no fall-through.
if capture == CaptureBackend::IddPush {
// 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
@@ -370,9 +375,9 @@ pub fn capture_virtual_output(
// overlay/independent-flip planes DXGI Desktop Duplication misses (the frozen-HDR-animation bug),
// 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 = crate::config::config().capture_backend.as_str();
if backend == "dda" || backend == "dxgi" || wgc_disabled() {
// chosen backend (it owns the SudoVDA keepalive), so there's no open-time auto-fallback. The
// backend choice (`dda`/`dxgi`/`PUNKTFUNK_NO_WGC` → DDA, else WGC) is now resolved once in the plan.
if capture == CaptureBackend::Dda {
return dxgi::DuplCapturer::open(target, pref, keep, false)
.map(|c| Box::new(c) as Box<dyn Capturer>);
}
@@ -418,6 +423,7 @@ pub fn capture_virtual_output(
pub fn capture_virtual_output(
_vout: crate::vdisplay::VirtualOutput,
_want_hdr: bool,
_capture: crate::session_plan::CaptureBackend,
) -> Result<Box<dyn Capturer>> {
anyhow::bail!("virtual-output capture requires Linux or Windows")
}