diff --git a/crates/hypercolor-windows-capture/Cargo.toml b/crates/hypercolor-windows-capture/Cargo.toml index 21fa19562..08ec19011 100644 --- a/crates/hypercolor-windows-capture/Cargo.toml +++ b/crates/hypercolor-windows-capture/Cargo.toml @@ -40,6 +40,7 @@ windows = { version = "0.62", default-features = false, features = [ "Win32_Graphics_Gdi", "Win32_Security", "Win32_System_LibraryLoader", + "Win32_System_Threading", ] } [[bench]] diff --git a/crates/hypercolor-windows-capture/src/duplication/gpu_surface.rs b/crates/hypercolor-windows-capture/src/duplication/gpu_surface.rs index 0bbf6660e..f14ab3248 100644 --- a/crates/hypercolor-windows-capture/src/duplication/gpu_surface.rs +++ b/crates/hypercolor-windows-capture/src/duplication/gpu_surface.rs @@ -2609,6 +2609,45 @@ pub(super) mod fixture { publication.shared.texture_handle.borrowed().as_raw() } + pub(crate) fn wait_for_surface_progress(plan: &PreparedGpuSurfacePlan) -> CaptureResult<()> { + use windows::Win32::Foundation::WAIT_OBJECT_0; + use windows::Win32::System::Threading::{CreateEventW, WaitForSingleObject}; + + for slot in plan.routes.iter().flat_map(|route| &route.slots) { + let value = match slot.publication.state.load(Ordering::Acquire) { + USE_UNCLAIMED => slot.publication.synchronization.producer_ready_value, + USE_RELEASE_QUEUED => slot.publication.synchronization.consumer_release_value, + _ => continue, + }; + // SAFETY: default security and no name create a private auto-reset event. + let event = OwnedHandle::new( + unsafe { CreateEventW(None, false, false, None) } + .map_err(|error| CaptureError::windows("create fixture fence event", error))?, + )?; + // SAFETY: the plan owns the fence and event remains live through the wait. + unsafe { + slot.publication + .shared + .fence + .SetEventOnCompletion(value, event.0) + } + .map_err(|error| CaptureError::windows("arm fixture fence event", error))?; + // SAFETY: event is a valid owned handle. The bound diagnoses a stalled + // native queue; it is not a publication cadence or polling budget. + let result = unsafe { WaitForSingleObject(event.0, 10_000) }; + if result != WAIT_OBJECT_0 { + return Err(CaptureError::windows( + "wait for fixture GPU progress", + format!( + "fence {value}, wait {result:?}, slots {:?}", + slot_diagnostics(plan) + ), + )); + } + } + Ok(()) + } + pub(crate) fn slot_diagnostics( plan: &PreparedGpuSurfacePlan, ) -> Vec<(usize, u8, u64, Option)> { diff --git a/crates/hypercolor-windows-capture/src/duplication/tests.rs b/crates/hypercolor-windows-capture/src/duplication/tests.rs index 47091569c..e8afe3977 100644 --- a/crates/hypercolor-windows-capture/src/duplication/tests.rs +++ b/crates/hypercolor-windows-capture/src/duplication/tests.rs @@ -894,7 +894,10 @@ fn abandoned_exact_gpu_surfaces_reclaim_under_sustained_pressure() { fixture.outcomes.clear(); for sequence in 42..74 { - let publication = (0..64) + // A busy slot crosses at most two GPU boundaries: producer readiness, + // then the abandoned release. Observe those fences before retrying so + // concurrent WARP scheduling cannot consume an arbitrary polling budget. + let publication = (0..3) .find_map(|_| { let outcomes = super::gpu_surface::fixture::republish(&mut fixture, sequence) .expect("abandoned slots remain safely reclaimable"); @@ -903,7 +906,8 @@ fn abandoned_exact_gpu_surfaces_reclaim_under_sustained_pressure() { crate::GpuSurfacePublishOutcome::Busy(_) => None, }); if publication.is_none() { - std::thread::sleep(std::time::Duration::from_millis(1)); + super::gpu_surface::fixture::wait_for_surface_progress(&fixture.plan) + .expect("queued native work completes before retrying publication"); } publication })