diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index 2eebf59..29ffe81 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -2141,30 +2141,6 @@ fn session_watcher_loop(tx: std::sync::mpsc::Sender, stop: Arc>> = 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 /// 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 @@ -2240,31 +2216,20 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> { bit_depth, "punktfunk/1 virtual display" ); - // IDD-push reconnect preempt: a fresh connection means the prior client is gone. Hold IDD_SETUP_LOCK - // across the preempt + pipeline build so a reconnect FLOOD can't run concurrent monitor - // create/teardown. Then STOP the prior session (it ends cleanly while its monitor still composites - // frames) and WAIT for it to release its monitor, before building a FRESH one — instead of the - // driver-churning teardown of a monitor under a still-live session. Register THIS session's stop so - // the next reconnect preempts it. + // IDD-push reconnect preempt (the dance now lives in the manager, Goal-1 §2.5): serialize setup so a + // reconnect FLOOD can't run concurrent monitor create/teardown, STOP the prior session + WAIT for it + // to release its monitor (instead of tearing a monitor out from under a still-live session), and + // register THIS session's stop. The returned guard holds the setup lock across the pipeline build; + // dropping it lets the next reconnect begin (and preempt us). #[cfg(target_os = "windows")] - let idd_push_session = plan.capture == crate::session_plan::CaptureBackend::IddPush; - #[cfg(target_os = "windows")] - 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 _idd_setup_guard = (plan.capture == crate::session_plan::CaptureBackend::IddPush) + .then(|| crate::vdisplay::manager::vdm().begin_idd_setup(stop.clone())); 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, 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); + drop(_idd_setup_guard); // 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 diff --git a/crates/punktfunk-host/src/vdisplay/windows/manager.rs b/crates/punktfunk-host/src/vdisplay/windows/manager.rs index f0efb22..077e4e9 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/manager.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/manager.rs @@ -120,6 +120,13 @@ pub(crate) struct VirtualDisplayManager { /// Monotonic lease-generation counter (was the `MON_GEN` global). gen: AtomicU64, state: Mutex, + /// 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>>, } static VDM: OnceLock = OnceLock::new(); @@ -133,6 +140,8 @@ pub(crate) fn init(driver: Box) -> &'static VirtualDisplayMa watchdog_s: AtomicU32::new(3), gen: AtomicU64::new(1), 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, + ) -> 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`). /// 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.