mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 15:41:23 +03:00
- 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>
153 lines
4.9 KiB
Dart
153 lines
4.9 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;
|
|
|
|
@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;
|
|
bind.mainPushTextureProbeFrame(ptr: _ptr);
|
|
if (bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0) {
|
|
_finish(true);
|
|
} else if (_ticks >= 10) {
|
|
// 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);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _finish(bool? ok) {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
|
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')) {
|
|
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)),
|
|
);
|
|
}
|
|
}
|