mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-20 19:31:00 +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:
@@ -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