mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-18 10:21:03 +03:00
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:
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user