diff --git a/crates/punktfunk-host/src/capture/windows/idd_push.rs b/crates/punktfunk-host/src/capture/windows/idd_push.rs index 0f3f6e8..e7625b7 100644 --- a/crates/punktfunk-host/src/capture/windows/idd_push.rs +++ b/crates/punktfunk-host/src/capture/windows/idd_push.rs @@ -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, } // 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> = 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, -) -> Result> { - 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 { - IDD_PERSIST - .lock() - .unwrap() - .as_mut() - .context("IDD-push persistent capturer missing")? - .next_frame() - } - fn try_latest(&mut self) -> Result> { - 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 { - 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 diff --git a/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs b/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs index d5c679c..cd7c87c 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs @@ -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 { 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 { 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, diff --git a/crates/punktfunk-host/src/vdisplay/windows/sudovda.rs b/crates/punktfunk-host/src/vdisplay/windows/sudovda.rs index 8147d87..51c1af8 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/sudovda.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/sudovda.rs @@ -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 { 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 { 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,