refactor(windows-host): §2.5 step 3 — isolate the IDD-push preempt into the manager

The last two virtual-display globals lived in punktfunk1: IDD_SETUP_LOCK (serialize IDD-push
setup against a reconnect flood) + IDD_SESSION_STOP (the prior session's stop flag, signalled +
waited-on so a reconnect preempts the stale session cleanly). Both move onto VirtualDisplayManager
as fields, behind one `vdm().begin_idd_setup(stop)` method that locks the setup gate, registers
this session's stop while signalling the prior one, waits for the monitor to release, and hands
back the setup guard the session holds across the pipeline build. punktfunk1 no longer reaches
into vdisplay internals for the preempt — it just calls the manager and holds the guard.

Behaviour-identical (same lock/signal/wait order, same guard lifetime). Completes §2.5's
"delete the smeared globals": CURRENT_MON_GEN/MON_GEN/MGR x2/IDD_PERSIST/IDD_SETUP_LOCK/
IDD_SESSION_STOP are all gone, replaced by the one OnceLock VirtualDisplayManager with a typed
OwnedHandle device. Box build to follow; on-glass reconnect-leak test pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 19:58:02 +00:00
parent d9b8b88a42
commit fe61597d92
2 changed files with 36 additions and 43 deletions
+8 -43
View File
@@ -2141,30 +2141,6 @@ fn session_watcher_loop(tx: std::sync::mpsc::Sender<SessionSwitch>, stop: Arc<At
} }
} }
/// Real capture→encode→punktfunk/1: a native virtual output at the client's mode, NVENC AUs
/// stamped with the capture wall clock (the client derives per-frame pipeline latency).
///
/// `reconfig` delivers accepted mid-stream mode switches: the capture/encode pipeline is
/// rebuilt at the new mode (capturer drop tears down the PipeWire stream and, via its
/// keepalive, the virtual output) while the data-plane `session` continues untouched —
/// the rebuilt encoder opens with an IDR + in-band parameter sets. `probe_rx`/`probe_result_tx`
/// carry speed-test bursts (see [`service_probes`]).
/// The stop flag of the current in-process IDD-push session, so a NEW connection can PREEMPT it.
/// A fresh connection means the prior client is gone (a reconnect) and a reused IddCx monitor's
/// swap-chain is dead — so we stop the prior session (it releases its monitor cleanly while frames
/// still flow), then build a fresh one, instead of joining a dying session or tearing its monitor out
/// from under it (which churns the driver's ADD/REMOVE path and wedges it under rapid reconnects).
#[cfg(target_os = "windows")]
static IDD_SESSION_STOP: std::sync::Mutex<Option<Arc<AtomicBool>>> = std::sync::Mutex::new(None);
/// Serializes IDD-push session SETUP (preempt + monitor create + first frame). Held across setup,
/// released before the encode loop — so a reconnect FLOOD can never run concurrent monitor
/// create/teardown (the churn that fails the ADD IOCTL and wedges the driver). Each session finishes
/// setup before the next acquires this and preempts it, by which point the preempted session is in its
/// encode loop and releases its monitor promptly.
#[cfg(target_os = "windows")]
static IDD_SETUP_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// All per-session inputs for [`virtual_stream`] / [`virtual_stream_relay`], bundled so the session entry /// All per-session inputs for [`virtual_stream`] / [`virtual_stream_relay`], bundled so the session entry
/// is one moved value instead of a 13-positional-argument `#[allow(too_many_arguments)]` signature /// is one moved value instead of a 13-positional-argument `#[allow(too_many_arguments)]` signature
/// (Goal-1 stage 4, plan §2.4). Everything is **owned** — the receivers move in (`virtual_stream` is their /// (Goal-1 stage 4, plan §2.4). Everything is **owned** — the receivers move in (`virtual_stream` is their
@@ -2240,31 +2216,20 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
bit_depth, bit_depth,
"punktfunk/1 virtual display" "punktfunk/1 virtual display"
); );
// IDD-push reconnect preempt: a fresh connection means the prior client is gone. Hold IDD_SETUP_LOCK // IDD-push reconnect preempt (the dance now lives in the manager, Goal-1 §2.5): serialize setup so a
// across the preempt + pipeline build so a reconnect FLOOD can't run concurrent monitor // reconnect FLOOD can't run concurrent monitor create/teardown, STOP the prior session + WAIT for it
// create/teardown. Then STOP the prior session (it ends cleanly while its monitor still composites // to release its monitor (instead of tearing a monitor out from under a still-live session), and
// frames) and WAIT for it to release its monitor, before building a FRESH one — instead of the // register THIS session's stop. The returned guard holds the setup lock across the pipeline build;
// driver-churning teardown of a monitor under a still-live session. Register THIS session's stop so // dropping it lets the next reconnect begin (and preempt us).
// the next reconnect preempts it.
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let idd_push_session = plan.capture == crate::session_plan::CaptureBackend::IddPush; let _idd_setup_guard = (plan.capture == crate::session_plan::CaptureBackend::IddPush)
#[cfg(target_os = "windows")] .then(|| crate::vdisplay::manager::vdm().begin_idd_setup(stop.clone()));
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);
crate::vdisplay::manager::vdm()
.wait_for_monitor_released(std::time::Duration::from_secs(3));
}
}
let mut vd = crate::vdisplay::open(compositor)?; let mut vd = crate::vdisplay::open(compositor)?;
let (mut capturer, mut enc, mut frame, mut interval) = let (mut capturer, mut enc, mut frame, mut interval) =
build_pipeline_with_retry(&mut vd, mode, bitrate_kbps, bit_depth, plan)?; 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). // Setup done — release the IDD-push setup lock so the next reconnect can begin (and preempt us).
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
drop(idd_setup_guard); drop(_idd_setup_guard);
// Windows single-process DDA path (PUNKTFUNK_NO_WGC=1): the SudoVDA virtual display, isolated as the // Windows single-process DDA path (PUNKTFUNK_NO_WGC=1): the SudoVDA virtual display, isolated as the
// SOLE active output, goes into fullscreen independent-flip (one plane on one display) which Desktop // SOLE active output, goes into fullscreen independent-flip (one plane on one display) which Desktop
@@ -120,6 +120,13 @@ pub(crate) struct VirtualDisplayManager {
/// Monotonic lease-generation counter (was the `MON_GEN` global). /// Monotonic lease-generation counter (was the `MON_GEN` global).
gen: AtomicU64, gen: AtomicU64,
state: Mutex<MgrState>, state: Mutex<MgrState>,
/// Serializes IDD-push session SETUP (preempt + monitor create) so a reconnect flood can't run
/// concurrent monitor create/teardown — held by the session across the pipeline build (was the
/// `IDD_SETUP_LOCK` global in `punktfunk1`).
setup_lock: Mutex<()>,
/// The current IDD-push session's stop flag; a new connection signals the prior one to release its
/// monitor before the fresh one is created (was the `IDD_SESSION_STOP` global in `punktfunk1`).
idd_session_stop: Mutex<Option<Arc<AtomicBool>>>,
} }
static VDM: OnceLock<VirtualDisplayManager> = OnceLock::new(); static VDM: OnceLock<VirtualDisplayManager> = OnceLock::new();
@@ -133,6 +140,8 @@ pub(crate) fn init(driver: Box<dyn VdisplayDriver>) -> &'static VirtualDisplayMa
watchdog_s: AtomicU32::new(3), watchdog_s: AtomicU32::new(3),
gen: AtomicU64::new(1), gen: AtomicU64::new(1),
state: Mutex::new(MgrState::Idle), state: Mutex::new(MgrState::Idle),
setup_lock: Mutex::new(()),
idd_session_stop: Mutex::new(None),
}) })
} }
@@ -390,6 +399,25 @@ impl VirtualDisplayManager {
}; };
} }
/// Begin an IDD-push session setup (Goal-1 §2.5 — was the `IDD_SETUP_LOCK` / `IDD_SESSION_STOP` /
/// `wait_for_monitor_released` dance smeared across `punktfunk1`). Serializes via the setup lock,
/// registers THIS session's stop flag while signalling the PRIOR IDD-push session to stop, and waits
/// for it to release its monitor — so a reconnect (whose reused IddCx swap-chain is dead) preempts the
/// stale session cleanly before a fresh monitor is created. Returns the setup guard; the caller holds
/// it across the pipeline build, then drops it so the next reconnect can begin (and preempt this one).
pub(crate) fn begin_idd_setup(
&'static self,
stop: Arc<AtomicBool>,
) -> std::sync::MutexGuard<'static, ()> {
let guard = self.setup_lock.lock().unwrap();
let prev = self.idd_session_stop.lock().unwrap().replace(stop);
if let Some(prev_stop) = prev {
prev_stop.store(true, Ordering::SeqCst);
self.wait_for_monitor_released(Duration::from_secs(3));
}
guard
}
/// Wait (up to `timeout`) for the active monitor to be RELEASED (the MGR is no longer `Active`). /// Wait (up to `timeout`) for the active monitor to be RELEASED (the MGR is no longer `Active`).
/// Used by the IDD-push reconnect preempt: after signalling the old session to stop, wait here so it /// Used by the IDD-push reconnect preempt: after signalling the old session to stop, wait here so it
/// tears its monitor down cleanly before we acquire a fresh one. /// tears its monitor down cleanly before we acquire a fresh one.