mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-08 13:31:03 +03:00
fix: address codex review findings on probe/watchdog fallback
- a probe or raster-stall failure record now also downgrades sessions that are already running: main_set_local_option broadcasts the fallback for failed-* health writes, which also closes the hole where the watchdog's idempotence guard no-oped after the probe had already written the record - probe success requires a frame timing newer than the first push: 'consumed' advances inside the plugin callback before the GL/Metal upload, so a raster thread hanging in the driver no longer counts as a pass (and cannot clear a previous failure) - watchdog failures are tagged with the failing backend (failed-watchdog-rgba/gpu); the rgba-only probe clears only the rgba class, so a working pixel-buffer path can no longer re-enable a broken D3D shared-handle path every launch - the watchdog pauses while the session's window is hidden (new session_set_render_visible FFI wired to the window minimize/restore events): a display registered in a minimized window no longer records a false global failure; observation now counts pushes within the window rather than since registration - probe failure verdicts additionally require a resumed lifecycle, matching the raster-stall monitor - linux plugin: deferred-unref grace lengthened to 10s (no raster-side completion barrier exists; documented as heuristic), ref bumped Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -388,6 +389,7 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
void onWindowMinimize() {
|
||||
stateGlobal.setMinimized(true);
|
||||
stateGlobal.setMaximized(false);
|
||||
_updateSessionsRenderVisible(false);
|
||||
super.onWindowMinimize();
|
||||
}
|
||||
|
||||
@@ -395,6 +397,7 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
void onWindowMaximize() {
|
||||
stateGlobal.setMinimized(false);
|
||||
_setMaximized(true);
|
||||
_updateSessionsRenderVisible(true);
|
||||
super.onWindowMaximize();
|
||||
}
|
||||
|
||||
@@ -402,6 +405,7 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
void onWindowUnmaximize() {
|
||||
stateGlobal.setMinimized(false);
|
||||
_setMaximized(false);
|
||||
_updateSessionsRenderVisible(true);
|
||||
super.onWindowUnmaximize();
|
||||
}
|
||||
|
||||
@@ -409,9 +413,26 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
void onWindowRestore() {
|
||||
// A plain restore (no maximize involved) must clear the minimized flag.
|
||||
stateGlobal.setMinimized(false);
|
||||
_updateSessionsRenderVisible(true);
|
||||
super.onWindowRestore();
|
||||
}
|
||||
|
||||
// A hidden window composites nothing; pause the Rust-side texture watchdog
|
||||
// for its sessions so it cannot record a false failure.
|
||||
void _updateSessionsRenderVisible(bool visible) {
|
||||
if (tabType != DesktopTabType.remoteScreen &&
|
||||
tabType != DesktopTabType.viewCamera) {
|
||||
return;
|
||||
}
|
||||
for (final tab in controller.state.value.tabs) {
|
||||
try {
|
||||
final ffi = Get.find<FFI>(tag: tab.key);
|
||||
bind.sessionSetRenderVisible(
|
||||
sessionId: ffi.sessionId, visible: visible);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
_saveFrame({bool? flush}) async {
|
||||
try {
|
||||
if (tabType == DesktopTabType.main) {
|
||||
|
||||
@@ -32,6 +32,7 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||
bool _sawTimings = false;
|
||||
bool _wasEffectiveOn = false;
|
||||
DateTime? _lastTimings;
|
||||
DateTime? _firstPush;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -82,15 +83,31 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||
setState(() => _textureId = id);
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||
_ticks += 1;
|
||||
_firstPush ??= DateTime.now();
|
||||
bind.mainPushTextureProbeFrame(ptr: _ptr);
|
||||
if (bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0) {
|
||||
final consumed = bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0;
|
||||
// "Consumed" advances inside the plugin callback, before the GL/Metal
|
||||
// upload; only a frame timing after the push proves a completed frame.
|
||||
final frameCompleted = consumed &&
|
||||
_lastTimings != null &&
|
||||
_lastTimings!.isAfter(_firstPush!);
|
||||
if (frameCompleted) {
|
||||
_finish(true);
|
||||
} else if (_ticks >= 10) {
|
||||
if (consumed) {
|
||||
_finish(null);
|
||||
return;
|
||||
}
|
||||
// Only a window that is visibly compositing can prove a failure.
|
||||
final lifecycle = SchedulerBinding.instance.lifecycleState;
|
||||
final active =
|
||||
lifecycle == null || lifecycle == AppLifecycleState.resumed;
|
||||
final timingsFresh = _lastTimings != null &&
|
||||
DateTime.now().difference(_lastTimings!) <
|
||||
const Duration(milliseconds: 1500);
|
||||
_finish(!stateGlobal.isMinimized && timingsFresh ? false : null);
|
||||
_finish(!stateGlobal.isMinimized && active && timingsFresh
|
||||
? false
|
||||
: null);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -102,9 +119,12 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||
if (ok != null) {
|
||||
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
|
||||
if (ok) {
|
||||
// A 1x1 probe pass disproves the black-texture class, not a
|
||||
// raster-stall under load; that record only clears via the toggle.
|
||||
if (old != 'ok' && !old.startsWith('failed-raster-stall')) {
|
||||
// This rgba probe disproves only the rgba black-texture class: gpu
|
||||
// failures and raster stalls clear via the option toggle alone.
|
||||
final clearable = old.isEmpty ||
|
||||
old.startsWith('failed-probe') ||
|
||||
old.startsWith('failed-watchdog-rgba');
|
||||
if (clearable) {
|
||||
bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
|
||||
}
|
||||
} else if (!old.startsWith('failed')) {
|
||||
|
||||
@@ -1462,6 +1462,9 @@ class RustdeskImpl {
|
||||
required int ptr,
|
||||
dynamic hint}) {}
|
||||
|
||||
void sessionSetRenderVisible(
|
||||
{required UuidValue sessionId, required bool visible, dynamic hint}) {}
|
||||
|
||||
bool mainTextureRenderProbeSupported({dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1298,8 +1298,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "7932bf9c7f64b34b3d136ff07acd7e30dd279c34"
|
||||
resolved-ref: "7932bf9c7f64b34b3d136ff07acd7e30dd279c34"
|
||||
ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
|
||||
resolved-ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
|
||||
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
|
||||
source: git
|
||||
version: "0.0.16"
|
||||
|
||||
@@ -88,7 +88,7 @@ dependencies:
|
||||
texture_rgba_renderer:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
|
||||
ref: 7932bf9c7f64b34b3d136ff07acd7e30dd279c34
|
||||
ref: 883326ddd4fb2af1484bf873b4ea856a0ac440bc
|
||||
percent_indicator: ^4.2.2
|
||||
dropdown_button2: ^2.0.0
|
||||
flutter_gpu_texture_renderer:
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::{
|
||||
os::raw::{c_char, c_int, c_void},
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
@@ -278,6 +278,12 @@ pub type FlutterGpuTextureRendererPluginCApiGetConsumed =
|
||||
|
||||
pub(super) type TextureRgbaPtr = usize;
|
||||
|
||||
// Which texture backend the watchdog saw fail; the health record carries it
|
||||
// so the rgba-only startup probe never clears a gpu-path failure.
|
||||
pub(super) const WATCHDOG_FAILED_RGBA: u8 = 1;
|
||||
#[cfg(feature = "vram")]
|
||||
pub(super) const WATCHDOG_FAILED_GPU: u8 = 2;
|
||||
|
||||
#[derive(Default)]
|
||||
struct DisplaySessionInfo {
|
||||
// TextureRgba pointer in flutter native.
|
||||
@@ -292,6 +298,7 @@ struct DisplaySessionInfo {
|
||||
// until a consumption is observed since arming.
|
||||
pushed_count: u64,
|
||||
watchdog_consumed_base: Option<u64>,
|
||||
watchdog_pushed_base: u64,
|
||||
watchdog_since: Option<Instant>,
|
||||
watchdog_last_sample: Option<Instant>,
|
||||
watchdog_armed: bool,
|
||||
@@ -301,18 +308,27 @@ impl DisplaySessionInfo {
|
||||
fn reset_watchdog(&mut self) {
|
||||
self.pushed_count = 0;
|
||||
self.watchdog_consumed_base = None;
|
||||
self.watchdog_pushed_base = 0;
|
||||
self.watchdog_since = None;
|
||||
self.watchdog_last_sample = None;
|
||||
self.watchdog_armed = true;
|
||||
}
|
||||
|
||||
// Restart the observation window without disarming; used while the window
|
||||
// is hidden, where the engine legitimately composites nothing.
|
||||
fn pause_watchdog(&mut self) {
|
||||
self.watchdog_consumed_base = None;
|
||||
self.watchdog_since = None;
|
||||
}
|
||||
|
||||
// The plugin counter is cumulative and never resets, so compare against a
|
||||
// snapshot taken when arming; damage-driven streams can be sparse, so
|
||||
// judge on cumulative pushes plus elapsed time, not per-second rate.
|
||||
// judge on pushes within the observation window plus elapsed time.
|
||||
fn check_watchdog(&mut self, consumed: u64) -> bool {
|
||||
let now = Instant::now();
|
||||
let Some(base) = self.watchdog_consumed_base else {
|
||||
self.watchdog_consumed_base = Some(consumed);
|
||||
self.watchdog_pushed_base = self.pushed_count;
|
||||
self.watchdog_since = Some(now);
|
||||
return false;
|
||||
};
|
||||
@@ -320,7 +336,7 @@ impl DisplaySessionInfo {
|
||||
self.watchdog_armed = false;
|
||||
return false;
|
||||
}
|
||||
if self.pushed_count >= 30
|
||||
if self.pushed_count - self.watchdog_pushed_base >= 30
|
||||
&& self
|
||||
.watchdog_since
|
||||
.map(|t| now.duration_since(t) >= Duration::from_secs(3))
|
||||
@@ -354,9 +370,11 @@ impl DisplaySessionInfo {
|
||||
struct VideoRenderer {
|
||||
is_support_multi_ui_session: bool,
|
||||
map_display_sessions: Arc<RwLock<HashMap<usize, Arc<Mutex<DisplaySessionInfo>>>>>,
|
||||
// Latched by the watchdog; consumed once by the pushing caller to trigger
|
||||
// the software-render fallback for this ui session.
|
||||
texture_render_failed: Arc<AtomicBool>,
|
||||
// Latched by the watchdog (WATCHDOG_FAILED_*); consumed once by the
|
||||
// pushing caller to trigger the software-render fallback for this session.
|
||||
texture_render_failed: Arc<AtomicU8>,
|
||||
// Hidden windows legitimately composite nothing; the watchdog pauses.
|
||||
render_visible: Arc<AtomicBool>,
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -435,6 +453,7 @@ impl Default for VideoRenderer {
|
||||
map_display_sessions: Default::default(),
|
||||
is_support_multi_ui_session: false,
|
||||
texture_render_failed: Default::default(),
|
||||
render_visible: Arc::new(AtomicBool::new(true)),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
on_rgba_func,
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -658,7 +677,9 @@ impl VideoRenderer {
|
||||
}
|
||||
info.pushed_count += 1;
|
||||
if let Some(get_consumed) = &self.get_consumed_func {
|
||||
if info.watchdog_sample_due() {
|
||||
if !self.render_visible.load(Ordering::Relaxed) {
|
||||
info.pause_watchdog();
|
||||
} else if info.watchdog_sample_due() {
|
||||
let consumed = unsafe { get_consumed(info.texture_rgba_ptr as _) };
|
||||
if info.check_watchdog(consumed) {
|
||||
log::error!(
|
||||
@@ -666,7 +687,8 @@ impl VideoRenderer {
|
||||
info.pushed_count,
|
||||
display
|
||||
);
|
||||
self.texture_render_failed.store(true, Ordering::SeqCst);
|
||||
self.texture_render_failed
|
||||
.store(WATCHDOG_FAILED_RGBA, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -695,7 +717,9 @@ impl VideoRenderer {
|
||||
// advances it), so this only detects never-composited outputs; the
|
||||
// rgba path and the startup probe cover bind-failure black screens.
|
||||
if let Some(get_consumed) = &self.get_gpu_consumed_func {
|
||||
if info.watchdog_sample_due() {
|
||||
if !self.render_visible.load(Ordering::Relaxed) {
|
||||
info.pause_watchdog();
|
||||
} else if info.watchdog_sample_due() {
|
||||
let consumed = unsafe { get_consumed(info.gpu_output_ptr as _) };
|
||||
if info.check_watchdog(consumed) {
|
||||
log::error!(
|
||||
@@ -703,7 +727,8 @@ impl VideoRenderer {
|
||||
info.pushed_count,
|
||||
display
|
||||
);
|
||||
self.texture_render_failed.store(true, Ordering::SeqCst);
|
||||
self.texture_render_failed
|
||||
.store(WATCHDOG_FAILED_GPU, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1485,13 +1510,13 @@ impl FlutterHandler {
|
||||
// actual fallback (config write, decoder reset) runs on its own thread.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn check_texture_render_failed(session_id: &SessionID, session: &SessionHandler) {
|
||||
if session
|
||||
let kind = session
|
||||
.renderer
|
||||
.texture_render_failed
|
||||
.swap(false, Ordering::SeqCst)
|
||||
{
|
||||
.swap(0, Ordering::SeqCst);
|
||||
if kind != 0 {
|
||||
let session_id = session_id.clone();
|
||||
std::thread::spawn(move || on_texture_render_failed(session_id));
|
||||
std::thread::spawn(move || on_texture_render_failed(session_id, kind));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2033,6 +2058,26 @@ pub fn session_unregister_pixelbuffer_texture(session_id: SessionID, display: us
|
||||
}
|
||||
}
|
||||
|
||||
// Hidden windows legitimately composite nothing; pausing the watchdog there
|
||||
// keeps a minimized/background window from recording a false failure.
|
||||
#[inline]
|
||||
pub fn session_set_render_visible(session_id: SessionID, visible: bool) {
|
||||
for s in sessions::get_sessions() {
|
||||
if let Some(h) = s
|
||||
.ui_handler
|
||||
.session_handlers
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&session_id)
|
||||
{
|
||||
h.renderer
|
||||
.render_visible
|
||||
.store(visible, Ordering::Relaxed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn session_unregister_gpu_texture(_session_id: SessionID, _display: usize, _output_ptr: usize) {
|
||||
#[cfg(feature = "vram")]
|
||||
@@ -2104,8 +2149,9 @@ pub fn get_texture_probe_consumed(ptr: usize) -> u64 {
|
||||
// software rendering and record the breakage (health flips the default off;
|
||||
// a passing startup probe or an explicit option toggle clears it).
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn on_texture_render_failed(session_id: SessionID) {
|
||||
fn on_texture_render_failed(session_id: SessionID, kind: u8) {
|
||||
// One record per breakage; later fires (other displays/sessions) no-op.
|
||||
// Sessions already running were downgraded when the record was written.
|
||||
if crate::ui_interface::texture_render_health_failed() {
|
||||
return;
|
||||
}
|
||||
@@ -2113,10 +2159,16 @@ fn on_texture_render_failed(session_id: SessionID) {
|
||||
"texture rendering failed for session {}, falling back to software rendering",
|
||||
session_id
|
||||
);
|
||||
let backend = if kind == WATCHDOG_FAILED_RGBA {
|
||||
"rgba"
|
||||
} else {
|
||||
"gpu"
|
||||
};
|
||||
LocalConfig::set_option(
|
||||
hbb_common::config::keys::OPTION_TEXTURE_RENDER_HEALTH.to_owned(),
|
||||
format!(
|
||||
"failed-watchdog@{}",
|
||||
"failed-watchdog-{}@{}",
|
||||
backend,
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
|
||||
@@ -1222,10 +1222,24 @@ pub fn main_set_env(key: String, value: Option<String>) -> SyncReturn<()> {
|
||||
|
||||
pub fn main_set_local_option(key: String, value: String) {
|
||||
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
|
||||
let is_texture_render_health_key = key.eq(config::keys::OPTION_TEXTURE_RENDER_HEALTH);
|
||||
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
|
||||
set_local_option(key, value.clone());
|
||||
let is_render_target =
|
||||
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
|
||||
if is_texture_render_health_key && value.starts_with("failed") {
|
||||
// Probe/raster-stall failures must also downgrade sessions that are
|
||||
// already running (they snapshotted the old effective value, and the
|
||||
// watchdog's own fallback no-ops once a record exists).
|
||||
for session in sessions::get_sessions() {
|
||||
if !is_render_target(&session) {
|
||||
continue;
|
||||
}
|
||||
session.push_event("use_texture_render", &[("v", "N")], &[]);
|
||||
session.use_texture_render_changed();
|
||||
session.ui_handler.update_use_texture_render();
|
||||
}
|
||||
}
|
||||
if is_texture_render_key {
|
||||
// An explicit user toggle gives texture rendering a fresh chance; a
|
||||
// stale failure record must not override it (the watchdog re-records
|
||||
@@ -2322,6 +2336,12 @@ pub fn session_unregister_gpu_texture(
|
||||
))
|
||||
}
|
||||
|
||||
pub fn session_set_render_visible(session_id: SessionID, visible: bool) -> SyncReturn<()> {
|
||||
SyncReturn(super::flutter::session_set_render_visible(
|
||||
session_id, visible,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn main_texture_render_probe_supported() -> SyncReturn<bool> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
return SyncReturn(super::flutter::texture_render_probe_supported());
|
||||
|
||||
Reference in New Issue
Block a user