mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 15:41:23 +03:00
- 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>
173 lines
5.6 KiB
Dart
173 lines
5.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
|
|
import '../../common.dart';
|
|
import '../../consts.dart';
|
|
import '../../models/platform_model.dart';
|
|
import '../../models/state_model.dart';
|
|
|
|
import 'package:texture_rgba_renderer/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
|
|
/// records in `texture-render-health` whether the engine consumed it, so a
|
|
/// broken environment is detected before the first session goes black.
|
|
class TextureRenderProbe extends StatefulWidget {
|
|
const TextureRenderProbe({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<TextureRenderProbe> createState() => _TextureRenderProbeState();
|
|
}
|
|
|
|
class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
|
static bool _ranThisLaunch = false;
|
|
final _renderer = TextureRgbaRenderer();
|
|
int _textureId = -1;
|
|
int _textureKey = -1;
|
|
int _ptr = 0;
|
|
Timer? _timer;
|
|
int _ticks = 0;
|
|
bool _sawTimings = false;
|
|
bool _wasEffectiveOn = false;
|
|
DateTime? _lastTimings;
|
|
DateTime? _firstPush;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (_ranThisLaunch || isWeb || !isDesktop) return;
|
|
_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
|
|
// window (silent/tray start) must not record a false failure.
|
|
SchedulerBinding.instance.addTimingsCallback(_onTimings);
|
|
Future.delayed(const Duration(seconds: 5), () {
|
|
if (!_sawTimings) {
|
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
|
_finish(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _onTimings(List<FrameTiming> timings) {
|
|
_lastTimings = DateTime.now();
|
|
if (_sawTimings) return;
|
|
_sawTimings = true;
|
|
_start();
|
|
}
|
|
|
|
void _start() async {
|
|
if (!mounted) return;
|
|
_textureKey = bind.getNextTextureKey();
|
|
final id = await _renderer.createTexture(_textureKey);
|
|
if (!mounted || id == -1) {
|
|
_finish(!mounted ? null : false);
|
|
return;
|
|
}
|
|
_ptr = await _renderer.getTexturePtr(_textureKey);
|
|
if (!mounted || _ptr <= 0) {
|
|
_finish(!mounted ? null : false);
|
|
return;
|
|
}
|
|
setState(() => _textureId = id);
|
|
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
|
_ticks += 1;
|
|
_firstPush ??= DateTime.now();
|
|
bind.mainPushTextureProbeFrame(ptr: _ptr);
|
|
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 && active && timingsFresh
|
|
? false
|
|
: null);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _finish(bool? ok) {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
|
if (ok != null) {
|
|
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
|
|
if (ok) {
|
|
// 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')) {
|
|
debugPrint('texture render probe failed, disabling texture rendering');
|
|
bind.mainSetLocalOption(
|
|
key: kOptionTextureRenderHealth, value: 'failed-probe');
|
|
if (_wasEffectiveOn) {
|
|
showToast(translate('texture-render-fallback-tip'));
|
|
}
|
|
}
|
|
}
|
|
if (_textureKey != -1) {
|
|
_renderer.closeTexture(_textureKey);
|
|
_textureKey = -1;
|
|
}
|
|
_ptr = 0;
|
|
if (mounted && _textureId != -1) {
|
|
setState(() => _textureId = -1);
|
|
} else {
|
|
_textureId = -1;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
|
if (_textureKey != -1) {
|
|
_renderer.closeTexture(_textureKey);
|
|
_textureKey = -1;
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_textureId == -1) return const SizedBox.shrink();
|
|
// Must actually composite for the engine to sample the texture; the
|
|
// pushed pixel is fully transparent.
|
|
return IgnorePointer(
|
|
child: SizedBox(
|
|
width: 1, height: 1, child: Texture(textureId: _textureId)),
|
|
);
|
|
}
|
|
}
|