fix: recover DXGI capture after access loss (#16024)

* fix: recover DXGI capture after access loss

* fix: track DXGI frame ownership

* fix: only recover DXGI access loss

* fix: stabilize DXGI recovery after mode switches

* fix: preserve DXGI recovery budget

* refactor: model DXGI frame lifecycle as enum

* docs: clarify DXGI frame cleanup order

* fix: scope DXGI frame grace to access loss recovery

* fix: preserve DXGI recovery across display changes

* chore: log DXGI recovery attempts at debug level
This commit is contained in:
Joss Gray
2026-09-15 22:18:44 -07:00
committed by GitHub
parent 8154c026a4
commit f9c2f16857
2 changed files with 156 additions and 17 deletions

View File

@@ -41,6 +41,13 @@ impl<T> Drop for ComPtr<T> {
}
}
#[derive(Clone, Copy, PartialEq)]
enum FrameState {
Idle,
Acquired,
Mapped,
}
pub struct Capturer {
device: ComPtr<ID3D11Device>,
display: Display,
@@ -58,6 +65,7 @@ pub struct Capturer {
output_texture: bool,
adapter_desc1: DXGI_ADAPTER_DESC1,
rotate: Rotate,
frame_state: FrameState,
}
impl Capturer {
@@ -174,6 +182,7 @@ impl Capturer {
output_texture: false,
adapter_desc1,
rotate,
frame_state: FrameState::Idle,
})
}
@@ -335,6 +344,7 @@ impl Capturer {
let mut info = mem::MaybeUninit::uninit().assume_init();
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
self.frame_state = FrameState::Acquired;
let frame = ComPtr(frame);
if *info.LastPresentTime.QuadPart() == 0 {
@@ -345,9 +355,11 @@ impl Capturer {
let mut rect = mem::MaybeUninit::uninit().assume_init();
if self.fastlane {
wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?;
self.frame_state = FrameState::Mapped;
} else {
self.surface = ComPtr(self.ohgodwhat(frame.0)?);
wrap_hresult((*self.surface.0).Map(&mut rect, DXGI_MAP_READ))?;
self.frame_state = FrameState::Mapped;
}
Ok((rect.pBits, rect.Pitch))
}
@@ -424,7 +436,7 @@ impl Capturer {
}
}
} else {
self.unmap();
self.release_frame()?;
let r = self.load_frame(timeout)?;
let rotate = match self.display.rotation() {
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => kRotate0,
@@ -472,12 +484,13 @@ impl Capturer {
if self.duplication.0.is_null() {
return Err(std::io::ErrorKind::AddrNotAvailable.into());
}
(*self.duplication.0).ReleaseFrame();
self.release_frame()?;
let mut frame = ptr::null_mut();
#[allow(invalid_value)]
let mut info = mem::MaybeUninit::uninit().assume_init();
wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?;
self.frame_state = FrameState::Acquired;
let frame = ComPtr(frame);
if info.AccumulatedFrames == 0 || *info.LastPresentTime.QuadPart() == 0 {
@@ -574,16 +587,42 @@ impl Capturer {
}
}
fn unmap(&self) {
fn release_frame(&mut self) -> io::Result<()> {
if self.duplication.is_null() {
return Ok(());
}
let mut first_error = None;
// Unmap before ReleaseFrame invalidates the desktop surface; use the same
// order for staging surfaces. Cleanup advances Mapped -> Acquired -> Idle,
// while Idle is a no-op. Advance state even on errors to avoid retrying
// cleanup, but still attempt ReleaseFrame if unmapping fails.
unsafe {
(*self.duplication.0).ReleaseFrame();
if self.fastlane {
(*self.duplication.0).UnMapDesktopSurface();
} else {
if !self.surface.is_null() {
(*self.surface.0).Unmap();
if self.frame_state == FrameState::Mapped {
let result = if self.fastlane {
wrap_hresult((*self.duplication.0).UnMapDesktopSurface())
} else if !self.surface.is_null() {
wrap_hresult((*self.surface.0).Unmap())
} else {
Ok(())
};
self.frame_state = FrameState::Acquired;
if let Err(err) = result {
first_error = Some(err);
}
}
if self.frame_state == FrameState::Acquired {
let result = wrap_hresult((*self.duplication.0).ReleaseFrame());
self.frame_state = FrameState::Idle;
if first_error.is_none() {
if let Err(err) = result {
first_error = Some(err);
}
}
}
}
match first_error {
Some(err) => Err(err),
None => Ok(()),
}
}
@@ -599,8 +638,8 @@ impl Capturer {
impl Drop for Capturer {
fn drop(&mut self) {
if !self.duplication.is_null() {
self.unmap();
if let Err(err) = self.release_frame() {
eprintln!("DXGI frame cleanup failed: {err}");
}
}
}

View File

@@ -52,6 +52,8 @@ use scrap::{
CodecFormat, Display, EncodeInput, TraitCapturer, TraitPixelBuffer,
};
#[cfg(windows)]
use std::io::ErrorKind::ConnectionReset;
#[cfg(windows)]
use std::sync::Once;
use std::{
collections::HashSet,
@@ -62,6 +64,59 @@ use std::{
pub const OPTION_REFRESH: &'static str = "refresh";
#[cfg(windows)]
const DXGI_RECOVERY_LIMIT: usize = 3;
#[cfg(windows)]
const DXGI_RECOVERY_WINDOW: Duration = Duration::from_secs(10);
#[cfg(windows)]
const DXGI_RECOVERY_FRAME_GRACE: Duration = Duration::from_secs(2);
#[cfg(windows)]
struct DxgiRecoveryState {
attempts: usize,
window_started: Option<Instant>,
restart_pending: bool,
fallback_pending: bool,
}
#[cfg(windows)]
impl DxgiRecoveryState {
fn new() -> Self {
Self {
attempts: 0,
window_started: None,
restart_pending: false,
fallback_pending: false,
}
}
fn next_attempt(&mut self) -> Option<usize> {
if self
.window_started
.map(|started| started.elapsed() > DXGI_RECOVERY_WINDOW)
.unwrap_or(true)
{
self.attempts = 0;
self.window_started = Some(Instant::now());
}
if self.attempts >= DXGI_RECOVERY_LIMIT {
self.fallback_pending = true;
return None;
}
self.attempts += 1;
self.restart_pending = true;
Some(self.attempts)
}
fn take_restart_pending(&mut self) -> bool {
std::mem::take(&mut self.restart_pending)
}
fn take_fallback_pending(&mut self) -> bool {
std::mem::take(&mut self.fallback_pending)
}
}
type FrameFetchedNotifierSender = UnboundedSender<(i32, Option<Instant>)>;
type FrameFetchedNotifierReceiver = Arc<TokioMutex<UnboundedReceiver<(i32, Option<Instant>)>>>;
@@ -220,6 +275,8 @@ pub struct VideoService {
sp: GenericService,
idx: usize,
source: VideoSource,
#[cfg(windows)]
dxgi_recovery_state: Arc<Mutex<DxgiRecoveryState>>,
}
impl Deref for VideoService {
@@ -253,6 +310,8 @@ pub fn new(source: VideoSource, idx: usize) -> GenericService {
sp: GenericService::new(get_service_name(source, idx), true),
idx,
source,
#[cfg(windows)]
dxgi_recovery_state: Arc::new(Mutex::new(DxgiRecoveryState::new())),
};
GenericService::run(&vs, run);
vs.sp
@@ -565,9 +624,25 @@ fn run(vs: VideoService) -> ResultType<()> {
let last_portable_service_running = false;
let display_idx = vs.idx;
#[cfg(windows)]
let dxgi_recovery_state = vs.dxgi_recovery_state.clone();
let sp = vs.sp;
let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?;
#[cfg(windows)]
// ACCESS_LOST marks the next successful capturer creation as a recovery. This timestamp is
// consumed once and temporarily holds off the normal WouldBlock-to-GDI fallback, giving the
// replacement DXGI capturer time to produce its first frame. Normal startup is unaffected.
let dxgi_recovery_started = dxgi_recovery_state
.lock()
.unwrap()
.take_restart_pending()
.then(Instant::now);
#[cfg(windows)]
if dxgi_recovery_state.lock().unwrap().take_fallback_pending() {
c.set_gdi();
log::info!("dxgi recovery exhausted, fall back to gdi");
}
#[cfg(windows)]
if !scrap::codec::enable_directx_capture() && !c.is_gdi() {
log::info!("disable dxgi with option, fall back to gdi");
c.set_gdi();
@@ -804,13 +879,18 @@ fn run(vs: VideoService) -> ResultType<()> {
match res {
Err(ref e) if e.kind() == WouldBlock => {
#[cfg(windows)]
if try_gdi > 0 && !c.is_gdi() {
if try_gdi > 3 {
c.set_gdi();
try_gdi = 0;
log::info!("No image, fall back to gdi");
if dxgi_recovery_started
.map(|started| started.elapsed() >= DXGI_RECOVERY_FRAME_GRACE)
.unwrap_or(true)
{
if try_gdi > 0 && !c.is_gdi() {
if try_gdi > 3 {
c.set_gdi();
try_gdi = 0;
log::info!("No image, fall back to gdi");
}
try_gdi += 1;
}
try_gdi += 1;
}
#[cfg(target_os = "linux")]
{
@@ -850,6 +930,13 @@ fn run(vs: VideoService) -> ResultType<()> {
}
}
Err(err) => {
#[cfg(windows)]
// The display-change check can restart capture before error handling below.
let recovery_attempt = if !c.is_gdi() && err.kind() == ConnectionReset {
dxgi_recovery_state.lock().unwrap().next_attempt()
} else {
None
};
// This check may be redundant, but it is better to be safe.
// The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough.
if vs.source.is_monitor() {
@@ -858,6 +945,19 @@ fn run(vs: VideoService) -> ResultType<()> {
#[cfg(windows)]
if !c.is_gdi() {
if err.kind() == ConnectionReset {
if let Some(attempt) = recovery_attempt {
log::debug!(
"dxgi access lost, restart capture: attempt {attempt}, error: {err:?}"
);
bail!("SWITCH");
}
log::warn!(
"dxgi access lost after {DXGI_RECOVERY_LIMIT} restarts in {} seconds, fall back to gdi: {err:?}",
DXGI_RECOVERY_WINDOW.as_secs()
);
dxgi_recovery_state.lock().unwrap().take_fallback_pending();
}
c.set_gdi();
log::info!("dxgi error, fall back to gdi: {:?}", err);
continue;