mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-13 07:51:01 +03:00
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:
@@ -21,6 +21,7 @@ import '../../common/shared_state.dart';
|
|||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/remote_toolbar.dart';
|
import '../widgets/remote_toolbar.dart';
|
||||||
import '../widgets/kb_layout_type_chooser.dart';
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
|
import '../widgets/raster_stall_monitor.dart';
|
||||||
import '../widgets/tabbar_widget.dart';
|
import '../widgets/tabbar_widget.dart';
|
||||||
import 'macos_full_screen_focus_recovery.dart';
|
import 'macos_full_screen_focus_recovery.dart';
|
||||||
|
|
||||||
@@ -156,7 +157,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
||||||
}
|
}
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
_RasterStallMonitor.start();
|
RasterStallMonitor.start();
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
_ffi.canvasModel.activateLocalCursor();
|
_ffi.canvasModel.activateLocalCursor();
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
@@ -648,7 +649,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// Clear callback reference to prevent memory leaks and stale references
|
// Clear callback reference to prevent memory leaks and stale references
|
||||||
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
||||||
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
||||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
_ffi.textureModel.onRemotePageDispose();
|
||||||
if (closeSession && !isMacOS) {
|
if (closeSession && !isMacOS) {
|
||||||
// ensure we leave this session, this is a double check
|
// ensure we leave this session, this is a double check
|
||||||
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
|
// 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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import '../../common/shared_state.dart';
|
|||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/remote_toolbar.dart';
|
import '../widgets/remote_toolbar.dart';
|
||||||
import '../widgets/kb_layout_type_chooser.dart';
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
|
import '../widgets/raster_stall_monitor.dart';
|
||||||
import '../widgets/tabbar_widget.dart';
|
import '../widgets/tabbar_widget.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/native/custom_cursor.dart'
|
import 'package:flutter_hbb/native/custom_cursor.dart'
|
||||||
@@ -102,6 +103,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_ffi = FFI(widget.sessionId);
|
_ffi = FFI(widget.sessionId);
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
|
RasterStallMonitor.start();
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
@@ -222,7 +224,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
||||||
_ffi.textureModel.onViewCameraPageDispose(closeSession);
|
_ffi.textureModel.onViewCameraPageDispose();
|
||||||
if (closeSession) {
|
if (closeSession) {
|
||||||
// ensure we leave this session, this is a double check
|
// ensure we leave this session, this is a double check
|
||||||
_ffi.inputModel.enterOrLeave(false);
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
|
|||||||
52
flutter/lib/desktop/widgets/raster_stall_monitor.dart
Normal file
52
flutter/lib/desktop/widgets/raster_stall_monitor.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../consts.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/state_model.dart';
|
||||||
|
|
||||||
|
/// Records a hung raster thread (frames continuously scheduled but no frame
|
||||||
|
/// timings delivered for 30s) in `texture-render-health`; a hang cannot be
|
||||||
|
/// rescued in-process, so the next launch defaults texture rendering off.
|
||||||
|
class RasterStallMonitor {
|
||||||
|
static bool _started = false;
|
||||||
|
static bool _reported = false;
|
||||||
|
static DateTime? _lastTimings;
|
||||||
|
static DateTime _lastQuiet = DateTime.now();
|
||||||
|
|
||||||
|
static void start() {
|
||||||
|
if (_started || isWeb) return;
|
||||||
|
_started = true;
|
||||||
|
SchedulerBinding.instance.addTimingsCallback((_) {
|
||||||
|
_lastTimings = DateTime.now();
|
||||||
|
});
|
||||||
|
Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
|
if (_reported) return;
|
||||||
|
final now = DateTime.now();
|
||||||
|
final lifecycle = SchedulerBinding.instance.lifecycleState;
|
||||||
|
// Minimized/inactive (incl. screen lock) or idle (nothing scheduled):
|
||||||
|
// no timings is legitimate, keep moving the quiet anchor forward.
|
||||||
|
if (stateGlobal.isMinimized ||
|
||||||
|
(lifecycle != null && lifecycle != AppLifecycleState.resumed) ||
|
||||||
|
!SchedulerBinding.instance.hasScheduledFrame) {
|
||||||
|
_lastQuiet = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ref = _lastQuiet;
|
||||||
|
final lastTimings = _lastTimings;
|
||||||
|
if (lastTimings != null && lastTimings.isAfter(ref)) {
|
||||||
|
ref = lastTimings;
|
||||||
|
}
|
||||||
|
if (now.difference(ref) > const Duration(seconds: 30)) {
|
||||||
|
_reported = true;
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionTextureRenderHealth, value: 'failed-raster-stall');
|
||||||
|
debugPrint(
|
||||||
|
'raster thread stall detected, texture rendering disabled for next launch');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -405,6 +405,13 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
super.onWindowUnmaximize();
|
super.onWindowUnmaximize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowRestore() {
|
||||||
|
// A plain restore (no maximize involved) must clear the minimized flag.
|
||||||
|
stateGlobal.setMinimized(false);
|
||||||
|
super.onWindowRestore();
|
||||||
|
}
|
||||||
|
|
||||||
_saveFrame({bool? flush}) async {
|
_saveFrame({bool? flush}) async {
|
||||||
try {
|
try {
|
||||||
if (tabType == DesktopTabType.main) {
|
if (tabType == DesktopTabType.main) {
|
||||||
|
|||||||
@@ -6,15 +6,14 @@ import 'package:flutter/scheduler.dart';
|
|||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/state_model.dart';
|
||||||
|
|
||||||
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
||||||
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
||||||
|
|
||||||
/// Startup probe: renders one frame through a 1x1 external texture and
|
/// Startup probe: renders one frame through a 1x1 external texture and
|
||||||
/// verifies the engine consumed it. The verdict is recorded in
|
/// records in `texture-render-health` whether the engine consumed it, so a
|
||||||
/// `texture-render-health` — a failure turns texture rendering off before the
|
/// broken environment is detected before the first session goes black.
|
||||||
/// first session goes black, a pass clears a stale failure (self-healing
|
|
||||||
/// after a driver/OS fix). Mounted once, in the main window.
|
|
||||||
class TextureRenderProbe extends StatefulWidget {
|
class TextureRenderProbe extends StatefulWidget {
|
||||||
const TextureRenderProbe({Key? key}) : super(key: key);
|
const TextureRenderProbe({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -31,12 +30,24 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
|||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
int _ticks = 0;
|
int _ticks = 0;
|
||||||
bool _sawTimings = false;
|
bool _sawTimings = false;
|
||||||
|
bool _wasEffectiveOn = false;
|
||||||
|
DateTime? _lastTimings;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (_ranThisLaunch || isWeb || !isDesktop) return;
|
if (_ranThisLaunch || isWeb || !isDesktop) return;
|
||||||
_ranThisLaunch = true;
|
_ranThisLaunch = true;
|
||||||
|
if (bind.isIncomingOnly()) return;
|
||||||
|
// An old plugin without the consumed counter cannot be judged, and a
|
||||||
|
// recorded raster-stall means compositing a texture may hang this window.
|
||||||
|
if (!bind.mainTextureRenderProbeSupported()) return;
|
||||||
|
if (bind
|
||||||
|
.mainGetLocalOption(key: kOptionTextureRenderHealth)
|
||||||
|
.startsWith('failed-raster-stall')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_wasEffectiveOn = bind.mainGetUseTextureRender();
|
||||||
// Only probe after the window has really rendered a frame: a hidden
|
// Only probe after the window has really rendered a frame: a hidden
|
||||||
// window (silent/tray start) must not record a false failure.
|
// window (silent/tray start) must not record a false failure.
|
||||||
SchedulerBinding.instance.addTimingsCallback(_onTimings);
|
SchedulerBinding.instance.addTimingsCallback(_onTimings);
|
||||||
@@ -49,9 +60,9 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onTimings(List<FrameTiming> timings) {
|
void _onTimings(List<FrameTiming> timings) {
|
||||||
|
_lastTimings = DateTime.now();
|
||||||
if (_sawTimings) return;
|
if (_sawTimings) return;
|
||||||
_sawTimings = true;
|
_sawTimings = true;
|
||||||
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
|
||||||
_start();
|
_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +86,11 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
|||||||
if (bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0) {
|
if (bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0) {
|
||||||
_finish(true);
|
_finish(true);
|
||||||
} else if (_ticks >= 10) {
|
} else if (_ticks >= 10) {
|
||||||
_finish(false);
|
// Only a window that is visibly compositing can prove a failure.
|
||||||
|
final timingsFresh = _lastTimings != null &&
|
||||||
|
DateTime.now().difference(_lastTimings!) <
|
||||||
|
const Duration(milliseconds: 1500);
|
||||||
|
_finish(!stateGlobal.isMinimized && timingsFresh ? false : null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -83,17 +98,22 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
|||||||
void _finish(bool? ok) {
|
void _finish(bool? ok) {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
if (ok != null) {
|
if (ok != null) {
|
||||||
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
|
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
if (old != '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')) {
|
||||||
bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
|
bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
|
||||||
}
|
}
|
||||||
} else if (!old.startsWith('failed')) {
|
} else if (!old.startsWith('failed')) {
|
||||||
debugPrint('texture render probe failed, disabling texture rendering');
|
debugPrint('texture render probe failed, disabling texture rendering');
|
||||||
bind.mainSetLocalOption(
|
bind.mainSetLocalOption(
|
||||||
key: kOptionTextureRenderHealth, value: 'failed-probe');
|
key: kOptionTextureRenderHealth, value: 'failed-probe');
|
||||||
showToast(translate('texture-render-fallback-tip'));
|
if (_wasEffectiveOn) {
|
||||||
|
showToast(translate('texture-render-fallback-tip'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_textureKey != -1) {
|
if (_textureKey != -1) {
|
||||||
@@ -111,6 +131,7 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
if (_textureKey != -1) {
|
if (_textureKey != -1) {
|
||||||
_renderer.closeTexture(_textureKey);
|
_renderer.closeTexture(_textureKey);
|
||||||
_textureKey = -1;
|
_textureKey = -1;
|
||||||
@@ -121,8 +142,8 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_textureId == -1) return const SizedBox.shrink();
|
if (_textureId == -1) return const SizedBox.shrink();
|
||||||
// Must actually composite for the engine to sample the texture; 1x1 in a
|
// Must actually composite for the engine to sample the texture; the
|
||||||
// corner is imperceptible.
|
// pushed pixel is fully transparent.
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 1, height: 1, child: Texture(textureId: _textureId)),
|
width: 1, height: 1, child: Texture(textureId: _textureId)),
|
||||||
|
|||||||
@@ -52,15 +52,14 @@ class _PixelbufferTexture {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool closeSession, FFI ffi) async {
|
destroy(FFI ffi) async {
|
||||||
_closed = true;
|
_closed = true;
|
||||||
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (_ptr != 0) {
|
if (_ptr != 0) {
|
||||||
// Compare-and-clear: clears only if Rust still holds this pointer, so
|
// Compare-and-clear: only clears if Rust still holds this pointer
|
||||||
// a registration a new window has already made stays intact (#8016).
|
// (#8016-safe); returning from this synchronous call also means no
|
||||||
// Returning from this synchronous call also guarantees no push
|
// push through the old pointer is still in flight.
|
||||||
// through the old pointer is still in flight.
|
|
||||||
platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr);
|
platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr);
|
||||||
_ptr = 0;
|
_ptr = 0;
|
||||||
}
|
}
|
||||||
@@ -122,7 +121,7 @@ class _GpuTexture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool closeSession, FFI ffi) async {
|
destroy(FFI ffi) async {
|
||||||
// must stop texture render, render unregistered texture cause crash
|
// must stop texture render, render unregistered texture cause crash
|
||||||
_closed = true;
|
_closed = true;
|
||||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||||
@@ -229,11 +228,11 @@ class TextureModel {
|
|||||||
tryRemoveTexture(int idx) {
|
tryRemoveTexture(int idx) {
|
||||||
_control.remove(idx);
|
_control.remove(idx);
|
||||||
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
||||||
_pixelbufferRenderTextures[idx]!.destroy(true, ffi);
|
_pixelbufferRenderTextures[idx]!.destroy(ffi);
|
||||||
_pixelbufferRenderTextures.remove(idx);
|
_pixelbufferRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
if (_gpuRenderTextures.containsKey(idx)) {
|
if (_gpuRenderTextures.containsKey(idx)) {
|
||||||
_gpuRenderTextures[idx]!.destroy(true, ffi);
|
_gpuRenderTextures[idx]!.destroy(ffi);
|
||||||
_gpuRenderTextures.remove(idx);
|
_gpuRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,25 +252,25 @@ class TextureModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemotePageDispose(bool closeSession) async {
|
onRemotePageDispose() async {
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
for (final texture in _pixelbufferRenderTextures.values) {
|
for (final texture in _pixelbufferRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
for (final texture in _gpuRenderTextures.values) {
|
for (final texture in _gpuRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewCameraPageDispose(bool closeSession) async {
|
onViewCameraPageDispose() async {
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
for (final texture in _pixelbufferRenderTextures.values) {
|
for (final texture in _pixelbufferRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
for (final texture in _gpuRenderTextures.values) {
|
for (final texture in _gpuRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1462,6 +1462,10 @@ class RustdeskImpl {
|
|||||||
required int ptr,
|
required int ptr,
|
||||||
dynamic hint}) {}
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
bool mainTextureRenderProbeSupported({dynamic hint}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void mainPushTextureProbeFrame({required int ptr, dynamic hint}) {}
|
void mainPushTextureProbeFrame({required int ptr, dynamic hint}) {}
|
||||||
|
|
||||||
int mainGetTextureProbeConsumed({required int ptr, dynamic hint}) {
|
int mainGetTextureProbeConsumed({required int ptr, dynamic hint}) {
|
||||||
|
|||||||
117
src/flutter.rs
117
src/flutter.rs
@@ -288,38 +288,44 @@ struct DisplaySessionInfo {
|
|||||||
gpu_output_ptr: usize,
|
gpu_output_ptr: usize,
|
||||||
notify_render_type: Option<RenderType>,
|
notify_render_type: Option<RenderType>,
|
||||||
// Watchdog: frames pushed to a texture the engine never consumes mean
|
// Watchdog: frames pushed to a texture the engine never consumes mean
|
||||||
// texture rendering is broken on this machine (black view while the
|
// texture rendering is broken (black view on a live connection). Armed
|
||||||
// connection works). Armed until the first consumption is observed, so a
|
// until a consumption is observed since arming.
|
||||||
// later minimized window cannot false-positive.
|
|
||||||
pushed_count: u64,
|
pushed_count: u64,
|
||||||
watchdog_pushed_at_sample: u64,
|
watchdog_consumed_base: Option<u64>,
|
||||||
|
watchdog_since: Option<Instant>,
|
||||||
watchdog_last_sample: Option<Instant>,
|
watchdog_last_sample: Option<Instant>,
|
||||||
watchdog_stall_windows: u32,
|
|
||||||
watchdog_armed: bool,
|
watchdog_armed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplaySessionInfo {
|
impl DisplaySessionInfo {
|
||||||
fn reset_watchdog(&mut self) {
|
fn reset_watchdog(&mut self) {
|
||||||
self.pushed_count = 0;
|
self.pushed_count = 0;
|
||||||
self.watchdog_pushed_at_sample = 0;
|
self.watchdog_consumed_base = None;
|
||||||
|
self.watchdog_since = None;
|
||||||
self.watchdog_last_sample = None;
|
self.watchdog_last_sample = None;
|
||||||
self.watchdog_stall_windows = 0;
|
|
||||||
self.watchdog_armed = true;
|
self.watchdog_armed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true when texture rendering is deemed broken: >=3 sampled
|
// The plugin counter is cumulative and never resets, so compare against a
|
||||||
// seconds in which frames kept being pushed but none was ever consumed.
|
// snapshot taken when arming; damage-driven streams can be sparse, so
|
||||||
|
// judge on cumulative pushes plus elapsed time, not per-second rate.
|
||||||
fn check_watchdog(&mut self, consumed: u64) -> bool {
|
fn check_watchdog(&mut self, consumed: u64) -> bool {
|
||||||
if consumed > 0 {
|
let now = Instant::now();
|
||||||
|
let Some(base) = self.watchdog_consumed_base else {
|
||||||
|
self.watchdog_consumed_base = Some(consumed);
|
||||||
|
self.watchdog_since = Some(now);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if consumed > base {
|
||||||
self.watchdog_armed = false;
|
self.watchdog_armed = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let pushed_in_window = self.pushed_count - self.watchdog_pushed_at_sample;
|
if self.pushed_count >= 30
|
||||||
self.watchdog_pushed_at_sample = self.pushed_count;
|
&& self
|
||||||
if pushed_in_window >= 10 {
|
.watchdog_since
|
||||||
self.watchdog_stall_windows += 1;
|
.map(|t| now.duration_since(t) >= Duration::from_secs(3))
|
||||||
}
|
.unwrap_or(false)
|
||||||
if self.watchdog_stall_windows >= 3 {
|
{
|
||||||
self.watchdog_armed = false;
|
self.watchdog_armed = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -342,9 +348,8 @@ impl DisplaySessionInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Video Texture Renderer in Flutter
|
// Video Texture Renderer in Flutter
|
||||||
// Each display entry has its own mutex so the per-frame plugin call only ever
|
// Per-display mutexes: the per-frame plugin call must not hold session-level
|
||||||
// holds that display's lock; a stalled plugin/driver call must not back up
|
// locks, or a stalled plugin/driver call freezes every window's UI thread.
|
||||||
// the session-level locks (which would freeze every window's UI thread).
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct VideoRenderer {
|
struct VideoRenderer {
|
||||||
is_support_multi_ui_session: bool,
|
is_support_multi_ui_session: bool,
|
||||||
@@ -503,11 +508,9 @@ impl VideoRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the pointer only if it still holds `ptr`. An unconditional clear
|
// Compare-and-clear: an unconditional clear could wipe the registration a
|
||||||
// could wipe out the registration a new window just made while a tab
|
// new window just made when a tab moves between windows (#8016); waiting
|
||||||
// moves between windows (#8016); skipping the clear (the old behavior on
|
// on the display mutex also drains an in-flight push via the old pointer.
|
||||||
// move) left Rust pushing into a freed native texture. Waiting on the
|
|
||||||
// display mutex also drains an in-flight push through the old pointer.
|
|
||||||
fn unregister_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
fn unregister_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
||||||
if ptr == 0 {
|
if ptr == 0 {
|
||||||
return;
|
return;
|
||||||
@@ -622,10 +625,11 @@ impl VideoRenderer {
|
|||||||
rgba.h
|
rgba.h
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// The stream is the source of truth; if the sizes still
|
// If sizes still disagree after this many frames the peer info
|
||||||
// disagree after this many frames the peer info is not
|
// is not coming and dropping forever leaves a live session
|
||||||
// coming, and dropping forever leaves a live session black.
|
// black. Legacy single-ui-session pairs frames loosely
|
||||||
if info.size_mismatch_count < 30 {
|
// (values().next()), so only adopt where pairing is exact.
|
||||||
|
if !self.is_support_multi_ui_session || info.size_mismatch_count < 30 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
log::warn!(
|
log::warn!(
|
||||||
@@ -2046,6 +2050,19 @@ pub fn session_unregister_gpu_texture(_session_id: SessionID, _display: usize, _
|
|||||||
// Startup-probe plumbing: the main window pushes one frame into a throwaway
|
// Startup-probe plumbing: the main window pushes one frame into a throwaway
|
||||||
// 1x1 texture and polls whether the engine consumed it, validating the
|
// 1x1 texture and polls whether the engine consumed it, validating the
|
||||||
// texture pipeline before any session depends on it.
|
// texture pipeline before any session depends on it.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn texture_render_probe_supported() -> bool {
|
||||||
|
match &*TEXTURE_RGBA_RENDERER_PLUGIN {
|
||||||
|
Ok(lib) => unsafe {
|
||||||
|
lib.symbol::<FlutterRgbaRendererPluginGetConsumed>(
|
||||||
|
"FlutterRgbaRendererPluginGetConsumed",
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
},
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn push_texture_probe_frame(ptr: usize) {
|
pub fn push_texture_probe_frame(ptr: usize) {
|
||||||
if ptr == 0 {
|
if ptr == 0 {
|
||||||
@@ -2059,7 +2076,8 @@ pub fn push_texture_probe_frame(ptr: usize) {
|
|||||||
}) else {
|
}) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let frame: [u8; 4] = [255, 255, 255, 255];
|
// Fully transparent so the 1x1 probe pixel is invisible on any theme.
|
||||||
|
let frame: [u8; 4] = [0, 0, 0, 0];
|
||||||
unsafe { func(ptr as _, frame.as_ptr(), 4, 1, 1, 1) };
|
unsafe { func(ptr as _, frame.as_ptr(), 4, 1, 1, 1) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2079,12 +2097,15 @@ pub fn get_texture_probe_consumed(ptr: usize) -> u64 {
|
|||||||
unsafe { func(ptr as _) }
|
unsafe { func(ptr as _) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frames are being pushed but the engine never consumes them: texture
|
// Frames are being pushed but the engine never consumes them: fall back to
|
||||||
// rendering does not work in this environment (GPU/driver/engine breakage).
|
// software rendering and record the breakage (health flips the default off;
|
||||||
// Fall back to software rendering for the live session and record it so the
|
// a passing startup probe or an explicit option toggle clears it).
|
||||||
// default flips to opt-in; the startup probe re-validates on later launches.
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[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) {
|
||||||
|
// One record per breakage; later fires (other displays/sessions) no-op.
|
||||||
|
if crate::ui_interface::texture_render_health_failed() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
log::error!(
|
log::error!(
|
||||||
"texture rendering failed for session {}, falling back to software rendering",
|
"texture rendering failed for session {}, falling back to software rendering",
|
||||||
session_id
|
session_id
|
||||||
@@ -2099,12 +2120,30 @@ fn on_texture_render_failed(session_id: SessionID) {
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
// Mirror main_set_local_option: every render session must observe the
|
||||||
session.push_event(
|
// new effective value, not only the failing one.
|
||||||
"use_texture_render",
|
for session in sessions::get_sessions() {
|
||||||
&[("v", "N"), ("reason", "fallback")],
|
if !(session.is_default() || session.is_view_camera()) {
|
||||||
&[],
|
continue;
|
||||||
);
|
}
|
||||||
|
// The soft path cannot rescue multi-display windows; don't claim it.
|
||||||
|
let fallback_rescues = session
|
||||||
|
.ui_handler
|
||||||
|
.session_handlers
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(&session_id)
|
||||||
|
.map(|h| h.displays.len() <= 1)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if fallback_rescues {
|
||||||
|
session.push_event(
|
||||||
|
"use_texture_render",
|
||||||
|
&[("v", "N"), ("reason", "fallback")],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
session.push_event("use_texture_render", &[("v", "N")], &[]);
|
||||||
|
}
|
||||||
session.use_texture_render_changed();
|
session.use_texture_render_changed();
|
||||||
session.ui_handler.update_use_texture_render();
|
session.ui_handler.update_use_texture_render();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2322,6 +2322,13 @@ pub fn session_unregister_gpu_texture(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
SyncReturn(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main_push_texture_probe_frame(ptr: usize) -> SyncReturn<()> {
|
pub fn main_push_texture_probe_frame(ptr: usize) -> SyncReturn<()> {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
super::flutter::push_texture_probe_frame(ptr);
|
super::flutter::push_texture_probe_frame(ptr);
|
||||||
|
|||||||
@@ -176,10 +176,9 @@ pub fn get_option<T: AsRef<str>>(key: T) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The watchdog/startup probe records texture-render breakage (frames pushed
|
// Watchdog/probe breakage record: a failure forces texture render off on all
|
||||||
// but never consumed by the engine). A failed record forces texture render
|
// desktop platforms (even an explicit "Y") — the live fallback relies on it;
|
||||||
// off on every desktop platform — even an explicit "Y" — because the live
|
// toggling the option or a passing probe clears it.
|
||||||
// fallback relies on it; toggling the option (or a passing probe) clears it.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn texture_render_health_failed() -> bool {
|
pub fn texture_render_health_failed() -> bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user