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:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -134,8 +134,12 @@ fn run(
|
||||
// IDD-push bypasses WGC.) Acceptable for the experimental IDD-push A/B path; HDR over IDD-push
|
||||
// is wired only for punktfunk/1 (want_hdr = negotiated bit_depth >= 10). TODO: derive want_hdr
|
||||
// from a GameStream HDR flag once StreamConfig carries one.
|
||||
let mut capturer =
|
||||
capture::capture_virtual_output(vout, false).context("capture virtual output")?;
|
||||
let mut capturer = capture::capture_virtual_output(
|
||||
vout,
|
||||
false,
|
||||
crate::session_plan::CaptureBackend::resolve(),
|
||||
)
|
||||
.context("capture virtual output")?;
|
||||
capturer.set_active(true);
|
||||
return stream_body(&mut *capturer, &sock, cfg, running, force_idr, rfi_range);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod audio;
|
||||
mod config;
|
||||
mod capture;
|
||||
mod config;
|
||||
mod discovery;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod dmabuf_fence;
|
||||
@@ -35,6 +35,7 @@ mod punktfunk1;
|
||||
mod pwinit;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod service;
|
||||
mod session_plan;
|
||||
mod session_tuning;
|
||||
mod spike;
|
||||
mod vdisplay;
|
||||
|
||||
@@ -2184,12 +2184,18 @@ fn virtual_stream(
|
||||
// This thread runs the capture+encode loop (single-process: Linux / synthetic / NO_WGC DDA) — or
|
||||
// tail-calls the relay below. Elevate it so a CPU-heavy game can't deschedule our GPU submission.
|
||||
boost_thread_priority(true);
|
||||
// Resolve the per-session capture / topology / encoder decision ONCE (Goal-1 stage 3): the deployed
|
||||
// path now reads this typed `SessionPlan` instead of re-deriving from config at each dispatch site
|
||||
// (the latent "capture and encode disagree on the backend" hazard, plan §2.4). `bit_depth` is the
|
||||
// only per-session input — capture/topology/encoder are otherwise pure functions of `HostConfig`.
|
||||
let plan = crate::session_plan::SessionPlan::resolve(bit_depth);
|
||||
tracing::info!(?plan, "resolved session plan");
|
||||
// Windows two-process secure-desktop path: when the host runs as SYSTEM (required for the secure
|
||||
// desktop + SendInput), WGC can't activate in-process, so we capture the normal desktop via a
|
||||
// helper spawned in the user session and relay its AUs. (Single-process WGC/DDA is used as the
|
||||
// user, and stays the path on Linux.) See docs/windows-secure-desktop.md.
|
||||
#[cfg(target_os = "windows")]
|
||||
if should_use_helper() {
|
||||
if plan.topology == crate::session_plan::SessionTopology::TwoProcessRelay {
|
||||
return virtual_stream_relay(
|
||||
session,
|
||||
mode,
|
||||
@@ -2220,11 +2226,11 @@ fn virtual_stream(
|
||||
// driver-churning teardown of a monitor under a still-live session. Register THIS session's stop so
|
||||
// the next reconnect preempts it.
|
||||
#[cfg(target_os = "windows")]
|
||||
let idd_setup_guard = crate::config::config()
|
||||
.idd_push
|
||||
.then(|| IDD_SETUP_LOCK.lock().unwrap());
|
||||
let idd_push_session = plan.capture == crate::session_plan::CaptureBackend::IddPush;
|
||||
#[cfg(target_os = "windows")]
|
||||
if crate::config::config().idd_push {
|
||||
let idd_setup_guard = idd_push_session.then(|| IDD_SETUP_LOCK.lock().unwrap());
|
||||
#[cfg(target_os = "windows")]
|
||||
if idd_push_session {
|
||||
let prev = IDD_SESSION_STOP.lock().unwrap().replace(stop.clone());
|
||||
if let Some(prev_stop) = prev {
|
||||
prev_stop.store(true, Ordering::SeqCst);
|
||||
@@ -2233,7 +2239,7 @@ fn virtual_stream(
|
||||
}
|
||||
let mut vd = crate::vdisplay::open(compositor)?;
|
||||
let (mut capturer, mut enc, mut frame, mut interval) =
|
||||
build_pipeline_with_retry(&mut vd, mode, bitrate_kbps, bit_depth)?;
|
||||
build_pipeline_with_retry(&mut vd, mode, bitrate_kbps, bit_depth, plan)?;
|
||||
// Setup done — release the IDD-push setup lock so the next reconnect can begin (and preempt us).
|
||||
#[cfg(target_os = "windows")]
|
||||
drop(idd_setup_guard);
|
||||
@@ -2362,6 +2368,7 @@ fn virtual_stream(
|
||||
cur_mode,
|
||||
bitrate_kbps,
|
||||
bit_depth,
|
||||
plan,
|
||||
)?;
|
||||
Ok((new_vd, pipe))
|
||||
})();
|
||||
@@ -2405,7 +2412,7 @@ fn virtual_stream(
|
||||
// Build the new pipeline BEFORE dropping the old one: the host already acked
|
||||
// the switch as accepted, so a rebuild failure must not kill an otherwise
|
||||
// healthy session — keep streaming the current mode and log instead.
|
||||
match build_pipeline(&mut vd, new_mode, bitrate_kbps, bit_depth) {
|
||||
match build_pipeline(&mut vd, new_mode, bitrate_kbps, bit_depth, plan) {
|
||||
Ok(next_pipe) => {
|
||||
(capturer, enc, frame, interval) = next_pipe;
|
||||
cur_mode = new_mode;
|
||||
@@ -2450,7 +2457,7 @@ fn virtual_stream(
|
||||
tracing::warn!(error = %format!("{e:#}"), rebuild = capture_rebuilds,
|
||||
"capture lost — rebuilding pipeline in place");
|
||||
let (new_cap, new_enc, new_frame, new_interval) =
|
||||
build_pipeline_with_retry(&mut vd, cur_mode, bitrate_kbps, bit_depth)
|
||||
build_pipeline_with_retry(&mut vd, cur_mode, bitrate_kbps, bit_depth, plan)
|
||||
.context("rebuild after capture loss")?;
|
||||
capturer = new_cap;
|
||||
enc = new_enc;
|
||||
@@ -2569,29 +2576,6 @@ fn virtual_stream(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Should this host take the two-process (SYSTEM host + user-session WGC helper) path? Yes when it's
|
||||
/// running as SYSTEM — the only account that can capture the secure desktop + drive SendInput on it,
|
||||
/// and the account under which in-process WGC won't activate. `PUNKTFUNK_FORCE_HELPER` forces it on
|
||||
/// (for testing the relay as a normal user); `PUNKTFUNK_NO_HELPER` forces it off. `PUNKTFUNK_NO_WGC`
|
||||
/// also forces it off — that mode runs pure single-process DDA (one capturer for the normal AND secure
|
||||
/// desktop, Apollo-style), which has no WGC helper to relay.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn should_use_helper() -> bool {
|
||||
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
|
||||
// SYSTEM host's session via shared memory and NVENC is headless, so no user-session WGC helper is
|
||||
// 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 cfg.idd_push {
|
||||
return false;
|
||||
}
|
||||
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
|
||||
/// its keepalive = the sole topology/isolation owner), spawns the WGC helper in the user session to
|
||||
/// capture+encode the NORMAL desktop, and relays the helper's AUs onto the QUIC data plane via the
|
||||
@@ -3041,6 +3025,7 @@ fn build_pipeline_with_retry(
|
||||
mode: punktfunk_core::Mode,
|
||||
bitrate_kbps: u32,
|
||||
bit_depth: u8,
|
||||
plan: crate::session_plan::SessionPlan,
|
||||
) -> Result<Pipeline> {
|
||||
// ~10s first-frame wait per attempt. 8 gives a ~90s budget for the SLOW case: a host-managed
|
||||
// gamescope session cold-starting Steam Big Picture (the SteamOS/Bazzite takeover) can take
|
||||
@@ -3050,7 +3035,7 @@ fn build_pipeline_with_retry(
|
||||
const MAX_ATTEMPTS: u32 = 8;
|
||||
let mut backoff = std::time::Duration::from_millis(500);
|
||||
for attempt in 1..=MAX_ATTEMPTS {
|
||||
match build_pipeline(vd, mode, bitrate_kbps, bit_depth) {
|
||||
match build_pipeline(vd, mode, bitrate_kbps, bit_depth, plan) {
|
||||
Ok(pipe) => {
|
||||
if attempt > 1 {
|
||||
tracing::info!(attempt, "pipeline up after retry");
|
||||
@@ -3109,6 +3094,7 @@ fn build_pipeline(
|
||||
mode: punktfunk_core::Mode,
|
||||
bitrate_kbps: u32,
|
||||
bit_depth: u8,
|
||||
plan: crate::session_plan::SessionPlan,
|
||||
) -> Result<Pipeline> {
|
||||
let vout = vd.create(mode).context("create virtual output")?;
|
||||
// The backend reports the refresh it actually achieved in `preferred_mode.2` (KWin may cap a
|
||||
@@ -3131,7 +3117,7 @@ fn build_pipeline(
|
||||
// VIDEO_CAP_10BIT + host opted in via PUNKTFUNK_10BIT) is our HDR path → BT.2020 PQ Rgb10a2;
|
||||
// otherwise the FP16 IDD frames are converted to 8-bit SDR. (Ignored by non-IDD-push backends,
|
||||
// which auto-detect HDR from the monitor state.)
|
||||
let mut capturer = crate::capture::capture_virtual_output(vout, bit_depth >= 10)
|
||||
let mut capturer = crate::capture::capture_virtual_output(vout, plan.hdr, plan.capture)
|
||||
.context("capture virtual output")?;
|
||||
capturer.set_active(true);
|
||||
let frame = capturer.next_frame().context("first frame")?;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
//! `SessionPlan` — the per-session capture / topology / encoder decision, resolved **once** from
|
||||
//! [`HostConfig`](crate::config) (+ the handshake-negotiated bit depth) into a typed, logged value.
|
||||
//!
|
||||
//! **Goal-1 stage 3** (`docs/windows-host-goal1-plan.md`): before this, the Windows session decision was
|
||||
//! re-derived at three call sites — the capture backend inside `capture::capture_virtual_output`, the
|
||||
//! process topology in `punktfunk1::should_use_helper`, and the encode backend in
|
||||
//! `encode::windows_resolved_backend` — each reading [`config`](crate::config) independently, with no
|
||||
//! single owner (the latent "capture and encode disagree on the backend" hazard, plan §2.4). `SessionPlan`
|
||||
//! resolves them together, once, so the deployed path reads one typed artifact.
|
||||
//!
|
||||
//! Stage 3 routes the **capture** and **topology** decisions through the plan (see
|
||||
//! `capture::capture_virtual_output` taking [`CaptureBackend`] in, and `virtual_stream` reading
|
||||
//! [`SessionTopology`]). The **encoder** is resolved by `encode::windows_resolved_backend` (config-backed
|
||||
//! and GPU-vendor cached since stage 2, so already a single source) and *recorded* here as
|
||||
//! [`EncoderBackend`]. Threading `encoder`/`input_format` into the encoder + capturer opens — which
|
||||
//! removes the `capture → encode::windows_resolved_backend()` back-reference recomputed in `dxgi.rs` —
|
||||
//! is **stage 5**.
|
||||
//!
|
||||
//! The type is platform-neutral so it threads through the shared `virtual_stream`/`build_pipeline`
|
||||
//! signatures; on Linux it resolves to the single portal/single-process path (the 3-way dispatch is a
|
||||
//! Windows-only concern).
|
||||
|
||||
/// Where a session's frames come from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CaptureBackend {
|
||||
/// Linux: the xdg ScreenCast portal → PipeWire (the only Linux capture path).
|
||||
Portal,
|
||||
/// Windows: IDD direct-push — frames pulled straight from the pf-vdisplay driver's shared ring
|
||||
/// (in-process, Session 0; no Desktop Duplication, no WGC helper).
|
||||
IddPush,
|
||||
/// Windows: DXGI Desktop Duplication (`PUNKTFUNK_CAPTURE=dda|dxgi` or `PUNKTFUNK_NO_WGC`).
|
||||
Dda,
|
||||
/// Windows: Windows.Graphics.Capture (the composed-desktop default), with a DDA watchdog fallback.
|
||||
Wgc,
|
||||
}
|
||||
|
||||
impl CaptureBackend {
|
||||
/// Resolve the capture backend from [`config`](crate::config). This is the single resolver shared by
|
||||
/// [`SessionPlan::resolve`] and the standalone callers (GameStream / spike), so they can't drift.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn resolve() -> Self {
|
||||
CaptureBackend::Portal
|
||||
}
|
||||
|
||||
/// Windows precedence (identical to the pre-stage-3 `capture_virtual_output` branch order):
|
||||
/// IDD-push wins; else an explicit `dda`/`dxgi` request or `PUNKTFUNK_NO_WGC` selects DDA; else WGC.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn resolve() -> Self {
|
||||
let cfg = crate::config::config();
|
||||
if cfg.idd_push {
|
||||
CaptureBackend::IddPush
|
||||
} else if matches!(cfg.capture_backend.as_str(), "dda" | "dxgi")
|
||||
|| crate::capture::wgc_disabled()
|
||||
{
|
||||
CaptureBackend::Dda
|
||||
} else {
|
||||
CaptureBackend::Wgc
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
pub fn resolve() -> Self {
|
||||
CaptureBackend::Portal
|
||||
}
|
||||
}
|
||||
|
||||
/// How a session is structured across processes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SessionTopology {
|
||||
/// One process captures + encodes (Linux; Windows non-SYSTEM / IDD-push / `NO_WGC`).
|
||||
SingleProcess,
|
||||
/// SYSTEM host + a user-session WGC helper relay (the Windows normal-desktop path under SYSTEM,
|
||||
/// where in-process WGC can't activate). See `virtual_stream_relay`.
|
||||
TwoProcessRelay,
|
||||
}
|
||||
|
||||
/// The resolved encode backend (recorded for logging / stages 4–5; the per-session encoder open still
|
||||
/// resolves via `encode::windows_resolved_backend`, which is config-backed + GPU-vendor cached).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EncoderBackend {
|
||||
/// Linux: NVENC vs VAAPI is auto-detected inside `encode::open_video` (not modeled here).
|
||||
PlatformAuto,
|
||||
Nvenc,
|
||||
Amf,
|
||||
Qsv,
|
||||
Software,
|
||||
}
|
||||
|
||||
/// The per-session decision, resolved once. `Copy` so it threads through the capture/encode chain
|
||||
/// without ceremony (stage 4 folds it, with the rest of the arg soup, into a `SessionContext`).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SessionPlan {
|
||||
pub capture: CaptureBackend,
|
||||
pub topology: SessionTopology,
|
||||
pub encoder: EncoderBackend,
|
||||
/// Handshake-negotiated encode bit depth (8, or 10 = HEVC Main10).
|
||||
pub bit_depth: u8,
|
||||
/// The IDD-push HDR hint (`bit_depth >= 10`) — the want-HDR flag the capturer was passed before.
|
||||
/// Non-IDD-push Windows backends ignore it and auto-detect HDR from the monitor; Linux is 8-bit.
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
impl SessionPlan {
|
||||
/// Resolve the whole plan once from [`config`](crate::config) + the negotiated `bit_depth`.
|
||||
pub fn resolve(bit_depth: u8) -> Self {
|
||||
SessionPlan {
|
||||
capture: CaptureBackend::resolve(),
|
||||
topology: resolve_topology(),
|
||||
encoder: resolve_encoder(),
|
||||
bit_depth,
|
||||
hdr: bit_depth >= 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process topology. On Windows this is the former `punktfunk1::should_use_helper` logic verbatim; on
|
||||
/// every other platform the session is always single-process.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn resolve_topology() -> SessionTopology {
|
||||
let cfg = crate::config::config();
|
||||
// `NO_HELPER`/`NO_WGC` force single-process; IDD-push captures in-process in Session 0 (no helper);
|
||||
// otherwise the helper runs when forced or when we're SYSTEM (in-process WGC can't activate there).
|
||||
let helper = if cfg.no_helper || crate::capture::wgc_disabled() {
|
||||
false
|
||||
} else if cfg.idd_push {
|
||||
false
|
||||
} else {
|
||||
cfg.force_helper || crate::capture::wgc_relay::running_as_system()
|
||||
};
|
||||
if helper {
|
||||
SessionTopology::TwoProcessRelay
|
||||
} else {
|
||||
SessionTopology::SingleProcess
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn resolve_topology() -> SessionTopology {
|
||||
SessionTopology::SingleProcess
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn resolve_encoder() -> EncoderBackend {
|
||||
match crate::encode::windows_resolved_backend() {
|
||||
crate::encode::WindowsBackend::Nvenc => EncoderBackend::Nvenc,
|
||||
crate::encode::WindowsBackend::Amf => EncoderBackend::Amf,
|
||||
crate::encode::WindowsBackend::Qsv => EncoderBackend::Qsv,
|
||||
crate::encode::WindowsBackend::Software => EncoderBackend::Software,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn resolve_encoder() -> EncoderBackend {
|
||||
EncoderBackend::PlatformAuto
|
||||
}
|
||||
@@ -76,7 +76,12 @@ pub fn run(opts: Options) -> Result<()> {
|
||||
refresh_hz: opts.fps,
|
||||
})
|
||||
.context("create virtual output")?;
|
||||
capture::capture_virtual_output(vout, false).context("capture virtual output")?
|
||||
capture::capture_virtual_output(
|
||||
vout,
|
||||
false,
|
||||
crate::session_plan::CaptureBackend::resolve(),
|
||||
)
|
||||
.context("capture virtual output")?
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -44,12 +44,23 @@ classes of `env::var` read are deliberately **kept live** and documented in `con
|
||||
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
|
||||
`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 3 — `SessionPlan` (the single biggest clarity lever, plan §2.4). ✅ IMPLEMENTED (this commit; on-glass pending).**
|
||||
New `src/session_plan.rs`: a `Copy` `SessionPlan { capture, topology, encoder, bit_depth, hdr }` resolved
|
||||
**once** from `HostConfig` (+ the negotiated `bit_depth`) in `virtual_stream`, logged, and threaded through
|
||||
`build_pipeline_with_retry`/`build_pipeline`. The three dispatch points now read it:
|
||||
- **capture** — `capture::capture_virtual_output` takes a `CaptureBackend` IN (was re-deriving from
|
||||
`config().idd_push`/`capture_backend`/`no_wgc`); `CaptureBackend::resolve()` is the one resolver (also
|
||||
used by the GameStream + spike call sites).
|
||||
- **topology** — `virtual_stream` reads `plan.topology` (`should_use_helper` deleted; its logic is
|
||||
`session_plan::resolve_topology`, verbatim). The IDD-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 `dxgi.rs` back-reference) is **stage 5**.
|
||||
|
||||
Every decision is provably equivalent to the pre-stage-3 scattered reads (same `config()` + cached probes),
|
||||
so it is behavior-preserving. Risk: medium-high (rewires the deployed decision). Verify: box build (Windows
|
||||
compile, which also covers stage 2's Windows-only edits) + on-glass re-test (NVENC + IDD-push + a mode
|
||||
switch) — **pending** (RTX box `192.168.1.173`).
|
||||
|
||||
**Stage 4 — `SessionContext` + `SessionFactory`/`Session`.**
|
||||
Bundle the 12–13-arg `#[allow(too_many_arguments)]` signatures into `SessionContext`; `SessionFactory.build()`
|
||||
|
||||
Reference in New Issue
Block a user