refactor(windows-host): §2.5 step 1 — delete the dead/write-only monitor-lifecycle code

Removes the cruft the §2.5 ownership-model rewrite would otherwise carry forward, and corrects a
false invariant the docs described:

  * CURRENT_MON_GEN (sudovda) — the "current monitor generation" global was WRITE-ONLY. It was
    stored on every mgr_acquire (both backends) but its only reader, idd_push's `my_gen`, was set
    and NEVER read. The "session capturer re-checks the monitor gen each frame and bails on a
    reconnect" behaviour the doc describes was never wired — per-frame staleness is the SEPARATE
    ring FrameToken.generation / IDD_GENERATION mechanism (which works and is untouched). So the
    monitor-gen-via-WinCaptureTarget carry the design proposed is unnecessary. Deleted the static,
    its stores in both backends, the pf_vdisplay import, and idd_push's dead `my_gen` field/read.
    (MON_GEN — the lease-generation counter behind the stale-lease no-op — is REAL and kept.)

  * IDD_PERSIST + open_or_reuse + IddReuseHandle (idd_push) — a persistent-capturer reuse path
    from an early prototype, defined but with ZERO callers across the crate. Deleted, plus the now
    -orphaned `use std::sync::Mutex` and the now-dead `set_client_10bit` setter.

Windows-only; grep confirms no remaining references to any deleted symbol. Box build to follow.
First of the incremental §2.5 steps (user-approved OnceLock VirtualDisplayManager design).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 19:26:17 +00:00
parent 05e87e6ab0
commit 15202011c1
3 changed files with 3 additions and 121 deletions
@@ -15,7 +15,6 @@ use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
use anyhow::{bail, Context, Result};
use pf_vdisplay_proto::frame;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use windows::core::{w, Interface, HSTRING};
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE, LUID};
@@ -152,111 +151,11 @@ pub struct IddPushCapturer {
last_seq: u64,
last_present: Option<(ID3D11Texture2D, PixelFormat)>,
status_logged: bool,
/// The monitor generation this capturer was opened for. When the active monitor gen changes (a
/// reconnect preempted + recreated the monitor), `next_frame` bails immediately so this session
/// releases its NVENC encoder instead of lingering on the dead ring's 20s deadline.
my_gen: u64,
_keepalive: Box<dyn Send>,
}
// COM objects used only from the owning (encode) thread.
unsafe impl Send for IddPushCapturer {}
/// The persistent IDD-push capturer, kept alive for the host lifetime and SHARED across client
/// sessions. The driver's per-session monitor TEARDOWN→RECREATE path is unstable (on session 2 the
/// target-id resolves to 0, `IddCxSwapChainSetDevice` fails `0x80070057`, then an access violation),
/// while the FIRST-session path is solid. So we create the monitor + ring + swap-chain ONCE and hand
/// every later session a thin handle delegating to this one. The persistent capturer holds a monitor
/// lease for the host lifetime, so `VirtualDisplay::create` always JOINs the same live monitor (same
/// target id) and the reuse match always hits — no recreate, no driver crash. Prototype scope:
/// single-client, single-mode (a different mode would need a recreate, the unstable path).
static IDD_PERSIST: Mutex<Option<IddPushCapturer>> = Mutex::new(None);
/// Open the IDD-push capturer, reusing the persistent one across sessions (see [`IDD_PERSIST`]).
pub fn open_or_reuse(
target: WinCaptureTarget,
preferred: Option<(u32, u32, u32)>,
client_10bit: bool,
keepalive: Box<dyn Send>,
) -> Result<Box<dyn Capturer>> {
let (w, h, _) =
preferred.context("IDD push needs the negotiated mode (WxH) to size the ring")?;
let mut slot = IDD_PERSIST.lock().unwrap();
let reuse = matches!(slot.as_ref(), Some(c) if c.target_id == target.target_id && c.width == w && c.height == h);
match slot.as_mut() {
Some(c) if reuse => {
// Reuse: the persistent capturer already owns the monitor + ring + driver attach. Drop the
// new per-session monitor lease (the persistent capturer's lease keeps the monitor live).
// The ring tracks the display, not the client; only the client's 10-bit cap can differ.
drop(keepalive);
c.set_client_10bit(client_10bit);
tracing::info!(
target_id = target.target_id,
client_10bit,
"IDD push: reusing the persistent capturer (no monitor/ring recreate)"
);
}
Some(c) => bail!(
"IDD-push persistent capturer is {}x{} target {}, this session wants {}x{} target {} — a \
mode/target change needs a recreate (the driver's recreate path is unstable); not \
supported in the persistent prototype",
c.width,
c.height,
c.target_id,
w,
h,
target.target_id
),
None => {
tracing::info!(
target_id = target.target_id,
client_10bit,
"IDD push: creating the persistent capturer (first session)"
);
// (dead persistent path) open() now returns the keepalive on failure; this path has no
// fallback, so discard it on error.
*slot = Some(
IddPushCapturer::open(target, preferred, client_10bit, keepalive)
.map_err(|(e, _keepalive)| e)?,
);
}
}
Ok(Box::new(IddReuseHandle))
}
/// Thin per-session handle: every method delegates to the single persistent [`IddPushCapturer`].
/// Dropping it (session end) does NOT tear down the ring/monitor — that's the whole point.
struct IddReuseHandle;
impl Capturer for IddReuseHandle {
fn next_frame(&mut self) -> Result<CapturedFrame> {
IDD_PERSIST
.lock()
.unwrap()
.as_mut()
.context("IDD-push persistent capturer missing")?
.next_frame()
}
fn try_latest(&mut self) -> Result<Option<CapturedFrame>> {
IDD_PERSIST
.lock()
.unwrap()
.as_mut()
.context("IDD-push persistent capturer missing")?
.try_latest()
}
fn set_active(&self, active: bool) {
if let Some(c) = IDD_PERSIST.lock().unwrap().as_ref() {
c.set_active(active);
}
}
fn hdr_meta(&self) -> Option<punktfunk_core::quic::HdrMeta> {
IDD_PERSIST
.lock()
.unwrap()
.as_ref()
.and_then(|c| c.hdr_meta())
}
}
/// Build a permissive (Everyone:GenericAll) `SECURITY_ATTRIBUTES` so the restricted WUDFHost driver
/// can OPEN the host-created objects — the same `D:(A;;GA;;;WD)` SDDL the gamepad shared section uses.
/// The returned `psd` backing must outlive `sa`; both are dropped when the process exits.
@@ -521,7 +420,6 @@ impl IddPushCapturer {
last_seq: 0,
last_present: None,
status_logged: false,
my_gen: crate::vdisplay::sudovda::CURRENT_MON_GEN.load(Ordering::Relaxed),
// Placeholder; `open()` attaches the real keepalive on success, so a FAILED open can hand
// it back to the caller for the DDA fallback (audit §5.1).
_keepalive: Box::new(()),
@@ -662,12 +560,6 @@ impl IddPushCapturer {
}
}
/// Update the client's 10-bit capability (the reuse path). Only affects whether a fresh `open`
/// proactively enables advanced color; the per-frame conversion follows the display, not the client.
fn set_client_10bit(&mut self, client_10bit: bool) {
self.client_10bit = client_10bit;
}
/// Recreate the ring at the format for `new_display_hdr` (the user flipped "Use HDR"). Bumps the
/// generation so the driver re-attaches ([`is_stale`]) to the new-format textures; clears the
/// header's `latest` so we don't consume a stale slot from the old ring; drops the conversion
@@ -38,9 +38,9 @@ use pf_vdisplay_proto::control;
use super::{Mode, VirtualDisplay, VirtualOutput};
// Backend-NEUTRAL CCD/DXGI helpers reused from the SudoVDA backend (a pf-vdisplay monitor's target_id
// is a real OS target id, so these operate identically). The shared MON_GEN/CURRENT_MON_GEN generation
// counter is reused too, so the IDD-push stale-ring bail works regardless of which backend is active.
use super::sudovda::{CURRENT_MON_GEN, MON_GEN};
// is a real OS target id, so these operate identically). The shared MON_GEN lease-generation counter is
// reused too, so a stale preempted lease can't tear down the live monitor regardless of which backend is active.
use super::sudovda::MON_GEN;
use crate::win_adapter::resolve_render_adapter_luid;
use crate::win_display::{
isolate_displays_ccd, resolve_gdi_name, restore_displays_ccd, set_active_mode, SavedConfig,
@@ -554,7 +554,6 @@ fn mgr_acquire(mode: Mode) -> Result<VirtualOutput> {
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
CURRENT_MON_GEN.store(gen, Ordering::Relaxed);
return Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
@@ -581,7 +580,6 @@ fn mgr_acquire(mode: Mode) -> Result<VirtualOutput> {
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
CURRENT_MON_GEN.store(gen, Ordering::Relaxed);
g.state = MgrState::Active { mon, refs: 1 };
Ok(VirtualOutput {
node_id: 0,
@@ -19,12 +19,6 @@ use std::sync::{Arc, Mutex, Once};
// backends keeps the idd_push stale-ring bail working regardless of which backend is active).
pub(crate) static MON_GEN: AtomicU64 = AtomicU64::new(1);
/// The gen of the CURRENTLY-active monitor. A session capturer captures this at open and re-checks it
/// each frame; when it changes (a reconnect preempted + recreated the monitor), the old session bails
/// IMMEDIATELY instead of lingering on the dead ring's 20s frame deadline — which would otherwise hold
/// its NVENC encoder open and exhaust the GPU's encode-session limit under rapid reconnects.
pub(crate) static CURRENT_MON_GEN: AtomicU64 = AtomicU64::new(0);
/// IDD-push mode: a new client connection preempts + recreates the monitor (single-client reconnect),
/// because a REUSED IddCx monitor's swap-chain is dead. Off → monitors are shared across sessions.
fn idd_push_mode() -> bool {
@@ -590,7 +584,6 @@ fn mgr_acquire(mode: Mode) -> Result<VirtualOutput> {
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
CURRENT_MON_GEN.store(gen, Ordering::Relaxed);
return Ok(VirtualOutput {
node_id: 0,
preferred_mode: pm,
@@ -617,7 +610,6 @@ fn mgr_acquire(mode: Mode) -> Result<VirtualOutput> {
let pm = Some((mon.mode.width, mon.mode.height, mon.mode.refresh_hz));
let target = mon.target();
let gen = mon.gen;
CURRENT_MON_GEN.store(gen, Ordering::Relaxed);
g.state = MgrState::Active { mon, refs: 1 };
Ok(VirtualOutput {
node_id: 0,