mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 15:41:23 +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 '../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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowRestore() {
|
||||
// A plain restore (no maximize involved) must clear the minimized flag.
|
||||
stateGlobal.setMinimized(false);
|
||||
super.onWindowRestore();
|
||||
}
|
||||
|
||||
_saveFrame({bool? flush}) async {
|
||||
try {
|
||||
if (tabType == DesktopTabType.main) {
|
||||
|
||||
@@ -6,15 +6,14 @@ 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
|
||||
/// verifies the engine consumed it. The verdict is recorded in
|
||||
/// `texture-render-health` — a failure turns texture rendering off before the
|
||||
/// first session goes black, a pass clears a stale failure (self-healing
|
||||
/// after a driver/OS fix). Mounted once, in the main window.
|
||||
/// 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);
|
||||
|
||||
@@ -31,12 +30,24 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||
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);
|
||||
@@ -49,9 +60,9 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||
}
|
||||
|
||||
void _onTimings(List<FrameTiming> timings) {
|
||||
_lastTimings = DateTime.now();
|
||||
if (_sawTimings) return;
|
||||
_sawTimings = true;
|
||||
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||
_start();
|
||||
}
|
||||
|
||||
@@ -75,7 +86,11 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||
if (bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0) {
|
||||
_finish(true);
|
||||
} 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) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||
if (ok != null) {
|
||||
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
|
||||
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');
|
||||
}
|
||||
} else if (!old.startsWith('failed')) {
|
||||
debugPrint('texture render probe failed, disabling texture rendering');
|
||||
bind.mainSetLocalOption(
|
||||
key: kOptionTextureRenderHealth, value: 'failed-probe');
|
||||
showToast(translate('texture-render-fallback-tip'));
|
||||
if (_wasEffectiveOn) {
|
||||
showToast(translate('texture-render-fallback-tip'));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_textureKey != -1) {
|
||||
@@ -111,6 +131,7 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||
if (_textureKey != -1) {
|
||||
_renderer.closeTexture(_textureKey);
|
||||
_textureKey = -1;
|
||||
@@ -121,8 +142,8 @@ class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_textureId == -1) return const SizedBox.shrink();
|
||||
// Must actually composite for the engine to sample the texture; 1x1 in a
|
||||
// corner is imperceptible.
|
||||
// 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)),
|
||||
|
||||
@@ -52,15 +52,14 @@ class _PixelbufferTexture {
|
||||
});
|
||||
}
|
||||
|
||||
destroy(bool closeSession, FFI ffi) async {
|
||||
destroy(FFI ffi) async {
|
||||
_closed = true;
|
||||
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||
_destroying = true;
|
||||
if (_ptr != 0) {
|
||||
// Compare-and-clear: clears only if Rust still holds this pointer, so
|
||||
// a registration a new window has already made stays intact (#8016).
|
||||
// Returning from this synchronous call also guarantees no push
|
||||
// through the old pointer is still in flight.
|
||||
// Compare-and-clear: only clears if Rust still holds this pointer
|
||||
// (#8016-safe); returning from this synchronous call also means no
|
||||
// push through the old pointer is still in flight.
|
||||
platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr);
|
||||
_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
|
||||
_closed = true;
|
||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||
@@ -229,11 +228,11 @@ class TextureModel {
|
||||
tryRemoveTexture(int idx) {
|
||||
_control.remove(idx);
|
||||
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
||||
_pixelbufferRenderTextures[idx]!.destroy(true, ffi);
|
||||
_pixelbufferRenderTextures[idx]!.destroy(ffi);
|
||||
_pixelbufferRenderTextures.remove(idx);
|
||||
}
|
||||
if (_gpuRenderTextures.containsKey(idx)) {
|
||||
_gpuRenderTextures[idx]!.destroy(true, ffi);
|
||||
_gpuRenderTextures[idx]!.destroy(ffi);
|
||||
_gpuRenderTextures.remove(idx);
|
||||
}
|
||||
}
|
||||
@@ -253,25 +252,25 @@ class TextureModel {
|
||||
}
|
||||
}
|
||||
|
||||
onRemotePageDispose(bool closeSession) async {
|
||||
onRemotePageDispose() async {
|
||||
final ffi = parent.target;
|
||||
if (ffi == null) return;
|
||||
for (final texture in _pixelbufferRenderTextures.values) {
|
||||
await texture.destroy(closeSession, ffi);
|
||||
await texture.destroy(ffi);
|
||||
}
|
||||
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;
|
||||
if (ffi == null) return;
|
||||
for (final texture in _pixelbufferRenderTextures.values) {
|
||||
await texture.destroy(closeSession, ffi);
|
||||
await texture.destroy(ffi);
|
||||
}
|
||||
for (final texture in _gpuRenderTextures.values) {
|
||||
await texture.destroy(closeSession, ffi);
|
||||
await texture.destroy(ffi);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1462,6 +1462,10 @@ class RustdeskImpl {
|
||||
required int ptr,
|
||||
dynamic hint}) {}
|
||||
|
||||
bool mainTextureRenderProbeSupported({dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void mainPushTextureProbeFrame({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,
|
||||
notify_render_type: Option<RenderType>,
|
||||
// Watchdog: frames pushed to a texture the engine never consumes mean
|
||||
// texture rendering is broken on this machine (black view while the
|
||||
// connection works). Armed until the first consumption is observed, so a
|
||||
// later minimized window cannot false-positive.
|
||||
// texture rendering is broken (black view on a live connection). Armed
|
||||
// until a consumption is observed since arming.
|
||||
pushed_count: u64,
|
||||
watchdog_pushed_at_sample: u64,
|
||||
watchdog_consumed_base: Option<u64>,
|
||||
watchdog_since: Option<Instant>,
|
||||
watchdog_last_sample: Option<Instant>,
|
||||
watchdog_stall_windows: u32,
|
||||
watchdog_armed: bool,
|
||||
}
|
||||
|
||||
impl DisplaySessionInfo {
|
||||
fn reset_watchdog(&mut self) {
|
||||
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_stall_windows = 0;
|
||||
self.watchdog_armed = true;
|
||||
}
|
||||
|
||||
// Returns true when texture rendering is deemed broken: >=3 sampled
|
||||
// seconds in which frames kept being pushed but none was ever consumed.
|
||||
// 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.
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
let pushed_in_window = self.pushed_count - self.watchdog_pushed_at_sample;
|
||||
self.watchdog_pushed_at_sample = self.pushed_count;
|
||||
if pushed_in_window >= 10 {
|
||||
self.watchdog_stall_windows += 1;
|
||||
}
|
||||
if self.watchdog_stall_windows >= 3 {
|
||||
if self.pushed_count >= 30
|
||||
&& self
|
||||
.watchdog_since
|
||||
.map(|t| now.duration_since(t) >= Duration::from_secs(3))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.watchdog_armed = false;
|
||||
return true;
|
||||
}
|
||||
@@ -342,9 +348,8 @@ impl DisplaySessionInfo {
|
||||
}
|
||||
|
||||
// Video Texture Renderer in Flutter
|
||||
// Each display entry has its own mutex so the per-frame plugin call only ever
|
||||
// holds that display's lock; a stalled plugin/driver call must not back up
|
||||
// the session-level locks (which would freeze every window's UI thread).
|
||||
// Per-display mutexes: the per-frame plugin call must not hold session-level
|
||||
// locks, or a stalled plugin/driver call freezes every window's UI thread.
|
||||
#[derive(Clone)]
|
||||
struct VideoRenderer {
|
||||
is_support_multi_ui_session: bool,
|
||||
@@ -503,11 +508,9 @@ impl VideoRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the pointer only if it still holds `ptr`. An unconditional clear
|
||||
// could wipe out the registration a new window just made while a tab
|
||||
// moves between windows (#8016); skipping the clear (the old behavior on
|
||||
// move) left Rust pushing into a freed native texture. Waiting on the
|
||||
// display mutex also drains an in-flight push through the old pointer.
|
||||
// Compare-and-clear: an unconditional clear could wipe the registration a
|
||||
// new window just made when a tab moves between windows (#8016); waiting
|
||||
// on the display mutex also drains an in-flight push via the old pointer.
|
||||
fn unregister_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
||||
if ptr == 0 {
|
||||
return;
|
||||
@@ -622,10 +625,11 @@ impl VideoRenderer {
|
||||
rgba.h
|
||||
);
|
||||
}
|
||||
// The stream is the source of truth; if the sizes still
|
||||
// disagree after this many frames the peer info is not
|
||||
// coming, and dropping forever leaves a live session black.
|
||||
if info.size_mismatch_count < 30 {
|
||||
// If sizes still disagree after this many frames the peer info
|
||||
// is not coming and dropping forever leaves a live session
|
||||
// black. Legacy single-ui-session pairs frames loosely
|
||||
// (values().next()), so only adopt where pairing is exact.
|
||||
if !self.is_support_multi_ui_session || info.size_mismatch_count < 30 {
|
||||
return false;
|
||||
}
|
||||
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
|
||||
// 1x1 texture and polls whether the engine consumed it, validating the
|
||||
// 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")))]
|
||||
pub fn push_texture_probe_frame(ptr: usize) {
|
||||
if ptr == 0 {
|
||||
@@ -2059,7 +2076,8 @@ pub fn push_texture_probe_frame(ptr: usize) {
|
||||
}) else {
|
||||
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) };
|
||||
}
|
||||
|
||||
@@ -2079,12 +2097,15 @@ pub fn get_texture_probe_consumed(ptr: usize) -> u64 {
|
||||
unsafe { func(ptr as _) }
|
||||
}
|
||||
|
||||
// Frames are being pushed but the engine never consumes them: texture
|
||||
// rendering does not work in this environment (GPU/driver/engine breakage).
|
||||
// Fall back to software rendering for the live session and record it so the
|
||||
// default flips to opt-in; the startup probe re-validates on later launches.
|
||||
// Frames are being pushed but the engine never consumes them: fall back to
|
||||
// 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) {
|
||||
// One record per breakage; later fires (other displays/sessions) no-op.
|
||||
if crate::ui_interface::texture_render_health_failed() {
|
||||
return;
|
||||
}
|
||||
log::error!(
|
||||
"texture rendering failed for session {}, falling back to software rendering",
|
||||
session_id
|
||||
@@ -2099,12 +2120,30 @@ fn on_texture_render_failed(session_id: SessionID) {
|
||||
.unwrap_or(0)
|
||||
),
|
||||
);
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.push_event(
|
||||
"use_texture_render",
|
||||
&[("v", "N"), ("reason", "fallback")],
|
||||
&[],
|
||||
);
|
||||
// Mirror main_set_local_option: every render session must observe the
|
||||
// new effective value, not only the failing one.
|
||||
for session in sessions::get_sessions() {
|
||||
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.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<()> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
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
|
||||
// but never consumed by the engine). A failed record forces texture render
|
||||
// off on every desktop platform — even an explicit "Y" — because the live
|
||||
// fallback relies on it; toggling the option (or a passing probe) clears it.
|
||||
// Watchdog/probe breakage record: a failure forces texture render off on all
|
||||
// desktop platforms (even an explicit "Y") — the live fallback relies on it;
|
||||
// toggling the option or a passing probe clears it.
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn texture_render_health_failed() -> bool {
|
||||
|
||||
Reference in New Issue
Block a user