fix: address texture watchdog/probe review findings

- watchdog: compare the plugin's cumulative consumed counter against a
  snapshot taken when arming (re-arm was a no-op before), and judge on
  cumulative pushes + elapsed time so sparse damage-driven streams are
  still detected
- failure handling is idempotent (one record per breakage) and updates
  every render session like main_set_local_option does, not only the
  failing one; the fallback toast is not claimed for multi-display
  windows the soft path cannot rescue
- probe: skip when the consumed API is missing (old plugin) or a
  raster-stall is recorded (compositing could hang the main window);
  a fail verdict requires fresh frame timings and a non-minimized
  window; a pass never clears failed-raster-stall; toast only when
  texture rendering was effectively on; probe pixel is transparent
- raster-stall monitor: judge on frame-timing staleness (a mid-episode
  hang was undetectable before), gate on lifecycle/minimized/idle with
  a moving quiet anchor, threshold 30s, shared with the camera page;
  clear stateGlobal minimized flag on plain window restore
- adopt mismatched frame sizes only in multi-ui-session mode (legacy
  mode pairs frames loosely and could ping-pong between displays)
- drop the now-dead closeSession parameter from texture destroy();
  trim comments to repo style

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rustdesk
2026-08-13 11:22:13 +08:00
parent c5adac828b
commit 7da2bbe6ac
10 changed files with 201 additions and 111 deletions

View File

@@ -21,6 +21,7 @@ import '../../common/shared_state.dart';
import '../../utils/image.dart';
import '../widgets/remote_toolbar.dart';
import '../widgets/kb_layout_type_chooser.dart';
import '../widgets/raster_stall_monitor.dart';
import '../widgets/tabbar_widget.dart';
import 'macos_full_screen_focus_recovery.dart';
@@ -156,7 +157,7 @@ class _RemotePageState extends State<RemotePage>
widget.tabController?.state.listen(_onMacOSTabStateChanged);
}
Get.put<FFI>(_ffi, tag: widget.id);
_RasterStallMonitor.start();
RasterStallMonitor.start();
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
_ffi.canvasModel.activateLocalCursor();
showKBLayoutTypeChooserIfNeeded(
@@ -648,7 +649,7 @@ class _RemotePageState extends State<RemotePage>
// Clear callback reference to prevent memory leaks and stale references
_ffi.inputModel.onRelativeMouseModeDisabled = null;
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
_ffi.textureModel.onRemotePageDispose(closeSession);
_ffi.textureModel.onRemotePageDispose();
if (closeSession && !isMacOS) {
// ensure we leave this session, this is a double check
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
@@ -1404,44 +1405,3 @@ class CursorPaint extends StatelessWidget {
}
}
/// Detects a hung raster thread: the UI keeps scheduling frames but the
/// engine never reports completed frame timings. Rendering cannot be rescued
/// in-process (software rendering needs the same raster thread), so this only
/// records the breakage — the next launch then defaults texture rendering to
/// off and the startup probe re-validates the environment.
class _RasterStallMonitor {
static bool _started = false;
static bool _reported = false;
static DateTime? _lastTimings;
static DateTime? _scheduledSince;
static void start() {
if (_started || isWeb) return;
_started = true;
SchedulerBinding.instance.addTimingsCallback((_) {
_lastTimings = DateTime.now();
});
Timer.periodic(const Duration(seconds: 2), (_) {
if (_reported) return;
// A minimized window legitimately stops producing frame timings.
if (stateGlobal.isMinimized ||
!SchedulerBinding.instance.hasScheduledFrame) {
_scheduledSince = null;
return;
}
final now = DateTime.now();
_scheduledSince ??= now;
final lastTimings = _lastTimings;
final stalled =
now.difference(_scheduledSince!) > const Duration(seconds: 10) &&
(lastTimings == null || lastTimings.isBefore(_scheduledSince!));
if (stalled) {
_reported = true;
bind.mainSetLocalOption(
key: kOptionTextureRenderHealth, value: 'failed-raster-stall');
debugPrint(
'raster thread stall detected, texture rendering disabled for next launch');
}
});
}
}

View File

@@ -19,6 +19,7 @@ import '../../common/shared_state.dart';
import '../../utils/image.dart';
import '../widgets/remote_toolbar.dart';
import '../widgets/kb_layout_type_chooser.dart';
import '../widgets/raster_stall_monitor.dart';
import '../widgets/tabbar_widget.dart';
import 'package:flutter_hbb/native/custom_cursor.dart'
@@ -102,6 +103,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
super.initState();
_ffi = FFI(widget.sessionId);
Get.put<FFI>(_ffi, tag: widget.id);
RasterStallMonitor.start();
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
@@ -222,7 +224,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
// https://github.com/flutter/flutter/issues/64935
super.dispose();
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onViewCameraPageDispose(closeSession);
_ffi.textureModel.onViewCameraPageDispose();
if (closeSession) {
// ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);