diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 6c22057f9..b9d613862 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -88,6 +88,7 @@ const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness"; const String kOptionImageQuality = "image_quality"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionTextureRender = "use-texture-render"; +const String kOptionTextureRenderHealth = "texture-render-health"; const String kOptionD3DRender = "allow-d3d-render"; const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInWindows = "allow-open-in-windows"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 42ec10032..c5beaac86 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -25,6 +25,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; import 'package:window_size/window_size.dart' as window_size; import '../widgets/button.dart'; +import '../widgets/texture_render_probe.dart'; class DesktopHomePage extends StatefulWidget { const DesktopHomePage({Key? key}) : super(key: key); @@ -60,15 +61,20 @@ class _DesktopHomePageState extends State Widget build(BuildContext context) { super.build(context); final isIncomingOnly = bind.isIncomingOnly(); - return _buildBlock( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + return Stack( children: [ - buildLeftPane(context), - if (!isIncomingOnly) const VerticalDivider(width: 1), - if (!isIncomingOnly) Expanded(child: buildRightPane(context)), + _buildBlock( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildLeftPane(context), + if (!isIncomingOnly) const VerticalDivider(width: 1), + if (!isIncomingOnly) Expanded(child: buildRightPane(context)), + ], + )), + const Positioned(left: 0, top: 0, child: TextureRenderProbe()), ], - )); + ); } Widget _buildBlock({required Widget child}) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index a9185d6a3..bed8b76e2 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -156,6 +156,7 @@ class _RemotePageState extends State widget.tabController?.state.listen(_onMacOSTabStateChanged); } Get.put(_ffi, tag: widget.id); + _RasterStallMonitor.start(); _ffi.imageModel.addCallbackOnFirstImage((String peerId) { _ffi.canvasModel.activateLocalCursor(); showKBLayoutTypeChooserIfNeeded( @@ -1402,3 +1403,45 @@ 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'); + } + }); + } +} diff --git a/flutter/lib/desktop/widgets/texture_render_probe.dart b/flutter/lib/desktop/widgets/texture_render_probe.dart new file mode 100644 index 000000000..ba98d25e3 --- /dev/null +++ b/flutter/lib/desktop/widgets/texture_render_probe.dart @@ -0,0 +1,131 @@ +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 '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. +class TextureRenderProbe extends StatefulWidget { + const TextureRenderProbe({Key? key}) : super(key: key); + + @override + State createState() => _TextureRenderProbeState(); +} + +class _TextureRenderProbeState extends State { + static bool _ranThisLaunch = false; + final _renderer = TextureRgbaRenderer(); + int _textureId = -1; + int _textureKey = -1; + int _ptr = 0; + Timer? _timer; + int _ticks = 0; + bool _sawTimings = false; + + @override + void initState() { + super.initState(); + if (_ranThisLaunch || isWeb || !isDesktop) return; + _ranThisLaunch = true; + // 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 timings) { + if (_sawTimings) return; + _sawTimings = true; + SchedulerBinding.instance.removeTimingsCallback(_onTimings); + _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) { + _finish(false); + } + }); + } + + void _finish(bool? ok) { + _timer?.cancel(); + _timer = null; + if (ok != null) { + final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth); + if (ok) { + if (old != 'ok') { + 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 (_textureKey != -1) { + _renderer.closeTexture(_textureKey); + _textureKey = -1; + } + _ptr = 0; + if (mounted && _textureId != -1) { + setState(() => _textureId = -1); + } else { + _textureId = -1; + } + } + + @override + void dispose() { + _timer?.cancel(); + 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; 1x1 in a + // corner is imperceptible. + return IgnorePointer( + child: SizedBox( + width: 1, height: 1, child: Texture(textureId: _textureId)), + ); + } +} diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index a96049134..289403e5e 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -16,6 +16,8 @@ class _PixelbufferTexture { int _display = 0; SessionID? _sessionId; bool _destroying = false; + bool _closed = false; + int _ptr = 0; int? _id; final textureRenderer = TextureRgbaRenderer(); @@ -27,11 +29,22 @@ class _PixelbufferTexture { _textureKey = bind.getNextTextureKey(); _sessionId = sessionId; - textureRenderer.createTexture(_textureKey).then((id) async { + final textureKey = _textureKey; + textureRenderer.createTexture(textureKey).then((id) async { _id = id; if (id != -1) { + if (_closed) { + // Destroyed while creation was still in flight (rapid + // connect/disconnect); nobody else will close this texture. + await textureRenderer.closeTexture(textureKey); + return; + } ffi.textureModel.setRgbaTextureId(display: d, id: id); - final ptr = await textureRenderer.getTexturePtr(_textureKey); + final ptr = await textureRenderer.getTexturePtr(textureKey); + if (_closed) { + return; + } + _ptr = ptr; platformFFI.registerPixelbufferTexture(sessionId, display, ptr); debugPrint( "create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr"); @@ -39,13 +52,17 @@ class _PixelbufferTexture { }); } - destroy(bool unregisterTexture, FFI ffi) async { + destroy(bool closeSession, FFI ffi) async { + _closed = true; if (!_destroying && _textureKey != -1 && _sessionId != null) { _destroying = true; - if (unregisterTexture) { - platformFFI.registerPixelbufferTexture(_sessionId!, display, 0); - // sleep for a while to avoid the texture is used after it's unregistered. - await Future.delayed(Duration(milliseconds: 100)); + 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. + platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr); + _ptr = 0; } await textureRenderer.closeTexture(_textureKey); _textureKey = -1; @@ -61,6 +78,7 @@ class _GpuTexture { SessionID? _sessionId; final support = bind.mainHasGpuTextureRender(); bool _destroying = false; + bool _closed = false; int _display = 0; int? _id; int? _output; @@ -79,9 +97,18 @@ class _GpuTexture { gpuTextureRenderer.registerTexture().then((id) async { _id = id; if (id != null) { + if (_closed) { + // Destroyed while creation was still in flight (rapid + // connect/disconnect); nobody else will unregister this texture. + await gpuTextureRenderer.unregisterTexture(id); + return; + } _textureId = id; ffi.textureModel.setGpuTextureId(display: d, id: id); final output = await gpuTextureRenderer.output(id); + if (_closed) { + return; + } _output = output; if (output != null) { platformFFI.registerGpuTexture(sessionId, d, output); @@ -95,20 +122,22 @@ class _GpuTexture { } } - destroy(bool unregisterTexture, FFI ffi) async { + destroy(bool closeSession, FFI ffi) async { // must stop texture render, render unregistered texture cause crash + _closed = true; if (!_destroying && support && _sessionId != null && _textureId != -1) { _destroying = true; - if (unregisterTexture) { - platformFFI.registerGpuTexture(_sessionId!, _display, 0); - // sleep for a while to avoid the texture is used after it's unregistered. - await Future.delayed(Duration(milliseconds: 100)); + final output = _output; + if (output != null) { + // Compare-and-clear, see _PixelbufferTexture.destroy. + platformFFI.unregisterGpuTexture(_sessionId!, _display, output); + _output = null; } await gpuTextureRenderer.unregisterTexture(_textureId); _textureId = -1; _destroying = false; debugPrint( - "destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$_output"); + "destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$output"); } } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 4a6088bd3..a1c6c5558 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -735,6 +735,11 @@ class FfiModel with ChangeNotifier { _handleUseTextureRender( Map evt, SessionID sessionId, String peerId) { parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y'); + if (evt['reason'] == 'fallback') { + // The Rust watchdog detected that pushed frames were never rendered + // and switched this session to software rendering. + showToast(translate('texture-render-fallback-tip')); + } waitForFirstImage.value = true; isRefreshing = true; showConnectedWaitingForImage(parent.target!.dialogManager, sessionId, diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 8c3c5cf71..06cc0b33c 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -131,6 +131,12 @@ class PlatformFFI { void registerGpuTexture(SessionID sessionId, int display, int ptr) => _ffiBind.sessionRegisterGpuTexture( sessionId: sessionId, display: display, ptr: ptr); + void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) => + _ffiBind.sessionUnregisterPixelbufferTexture( + sessionId: sessionId, display: display, ptr: ptr); + void unregisterGpuTexture(SessionID sessionId, int display, int ptr) => + _ffiBind.sessionUnregisterGpuTexture( + sessionId: sessionId, display: display, ptr: ptr); /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index b65825e51..6787eb18d 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -136,6 +136,12 @@ class PlatformFFI { void registerGpuTexture(SessionID sessionId, int display, int ptr) => _ffiBind.sessionRegisterGpuTexture( sessionId: sessionId, display: display, ptr: ptr); + void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) => + _ffiBind.sessionUnregisterPixelbufferTexture( + sessionId: sessionId, display: display, ptr: ptr); + void unregisterGpuTexture(SessionID sessionId, int display, int ptr) => + _ffiBind.sessionUnregisterGpuTexture( + sessionId: sessionId, display: display, ptr: ptr); Future init(String appType) async { Completer completer = Completer(); diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index ac48dfb0f..f56319324 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1450,6 +1450,24 @@ class RustdeskImpl { required int ptr, dynamic hint}) {} + void sessionUnregisterPixelbufferTexture( + {required UuidValue sessionId, + required int display, + required int ptr, + dynamic hint}) {} + + void sessionUnregisterGpuTexture( + {required UuidValue sessionId, + required int display, + required int ptr, + dynamic hint}) {} + + void mainPushTextureProbeFrame({required int ptr, dynamic hint}) {} + + int mainGetTextureProbeConsumed({required int ptr, dynamic hint}) { + return 0; + } + Future queryOnlines({required List ids, dynamic hint}) { return Future(() => js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)])); diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index cba9ba5ea..36d946437 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -538,8 +538,8 @@ packages: dependency: "direct main" description: path: "." - ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87" - resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87" + ref: "767bb9fe9dcd2c23e2664114bf33760842e872e7" + resolved-ref: "767bb9fe9dcd2c23e2664114bf33760842e872e7" url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" source: git version: "0.0.1" @@ -1298,8 +1298,8 @@ packages: dependency: "direct main" description: path: "." - ref: "42797e0f03141dc2b585f76c64a13974508058b4" - resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4" + ref: "ad4c37e414ee40853c0e9503b2da6486d4c1768d" + resolved-ref: "ad4c37e414ee40853c0e9503b2da6486d4c1768d" url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer" source: git version: "0.0.16" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index b9f8e1ccb..fe7ee53c0 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -88,13 +88,13 @@ dependencies: texture_rgba_renderer: git: url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer - ref: 42797e0f03141dc2b585f76c64a13974508058b4 + ref: ad4c37e414ee40853c0e9503b2da6486d4c1768d percent_indicator: ^4.2.2 dropdown_button2: ^2.0.0 flutter_gpu_texture_renderer: git: url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer - ref: 08a471bb8ceccdd50483c81cdfa8b81b07b14b87 + ref: 767bb9fe9dcd2c23e2664114bf33760842e872e7 uuid: ^3.0.7 auto_size_text_field: ^2.2.1 flex_color_picker: ^3.3.0 diff --git a/libs/hbb_common b/libs/hbb_common index 69cea8daf..d19ce39e5 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 69cea8dafee147848ae88702029f4bf7df7224c3 +Subproject commit d19ce39e511bb2073764be600d4d229118551f8a diff --git a/src/flutter.rs b/src/flutter.rs index f6e3d3edd..67723951d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -24,8 +24,9 @@ use std::{ str::FromStr, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, RwLock, + Arc, Mutex, RwLock, }, + time::{Duration, Instant}, }; /// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed) @@ -269,26 +270,96 @@ pub type FlutterGpuTextureRendererPluginCApiSetTexture = #[cfg(feature = "vram")] pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64; +pub type FlutterRgbaRendererPluginGetConsumed = unsafe extern "C" fn(texture_rgba: *mut c_void) -> u64; + +#[cfg(feature = "vram")] +pub type FlutterGpuTextureRendererPluginCApiGetConsumed = + unsafe extern "C" fn(output: *mut c_void) -> u64; + pub(super) type TextureRgbaPtr = usize; +#[derive(Default)] struct DisplaySessionInfo { // TextureRgba pointer in flutter native. texture_rgba_ptr: TextureRgbaPtr, size: (usize, usize), + size_mismatch_count: u32, #[cfg(feature = "vram")] gpu_output_ptr: usize, notify_render_type: Option, + // 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. + pushed_count: u64, + watchdog_pushed_at_sample: u64, + watchdog_last_sample: Option, + 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_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. + fn check_watchdog(&mut self, consumed: u64) -> bool { + if consumed > 0 { + 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 { + self.watchdog_armed = false; + return true; + } + false + } + + fn watchdog_sample_due(&mut self) -> bool { + if !self.watchdog_armed { + return false; + } + let now = Instant::now(); + match self.watchdog_last_sample { + Some(t) if now.duration_since(t) < Duration::from_secs(1) => false, + _ => { + self.watchdog_last_sample = Some(now); + true + } + } + } } // 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). #[derive(Clone)] struct VideoRenderer { is_support_multi_ui_session: bool, - map_display_sessions: Arc>>, + map_display_sessions: Arc>>>>, + // Latched by the watchdog; consumed once by the pushing caller to trigger + // the software-render fallback for this ui session. + texture_render_failed: Arc, #[cfg(not(any(target_os = "android", target_os = "ios")))] on_rgba_func: Option>, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + get_consumed_func: Option>, #[cfg(feature = "vram")] on_texture_func: Option>, + #[cfg(feature = "vram")] + get_gpu_consumed_func: Option>, } impl Default for VideoRenderer { @@ -312,6 +383,17 @@ impl Default for VideoRenderer { None } }; + // Absent in older plugin builds; the watchdog just stays disabled. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let get_consumed_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN { + Ok(lib) => unsafe { + lib.symbol::( + "FlutterRgbaRendererPluginGetConsumed", + ) + .ok() + }, + Err(_) => None, + }; #[cfg(feature = "vram")] let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN { Ok(lib) => { @@ -333,14 +415,29 @@ impl Default for VideoRenderer { None } }; + #[cfg(feature = "vram")] + let get_gpu_consumed_func = match &*TEXTURE_GPU_RENDERER_PLUGIN { + Ok(lib) => unsafe { + lib.symbol::( + "FlutterGpuTextureRendererPluginCApiGetConsumed", + ) + .ok() + }, + Err(_) => None, + }; Self { map_display_sessions: Default::default(), is_support_multi_ui_session: false, + texture_render_failed: Default::default(), #[cfg(not(any(target_os = "android", target_os = "ios")))] on_rgba_func, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + get_consumed_func, #[cfg(feature = "vram")] on_texture_func, + #[cfg(feature = "vram")] + get_gpu_consumed_func, } } } @@ -349,19 +446,18 @@ impl VideoRenderer { #[inline] fn set_size(&mut self, display: usize, width: usize, height: usize) { let mut sessions_lock = self.map_display_sessions.write().unwrap(); - if let Some(info) = sessions_lock.get_mut(&display) { + if let Some(info) = sessions_lock.get(&display) { + let mut info = info.lock().unwrap(); info.size = (width, height); + info.size_mismatch_count = 0; info.notify_render_type = None; } else { sessions_lock.insert( display, - DisplaySessionInfo { - texture_rgba_ptr: usize::default(), + Arc::new(Mutex::new(DisplaySessionInfo { size: (width, height), - #[cfg(feature = "vram")] - gpu_output_ptr: usize::default(), - notify_render_type: None, - }, + ..Default::default() + })), ); } } @@ -369,7 +465,8 @@ impl VideoRenderer { fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) { let mut sessions_lock = self.map_display_sessions.write().unwrap(); if ptr == 0 { - if let Some(info) = sessions_lock.get_mut(&display) { + if let Some(info_arc) = sessions_lock.get(&display).cloned() { + let mut info = info_arc.lock().unwrap(); if info.texture_rgba_ptr != usize::default() { info.texture_rgba_ptr = usize::default(); } @@ -377,10 +474,12 @@ impl VideoRenderer { if info.gpu_output_ptr != usize::default() { return; } + drop(info); + sessions_lock.remove(&display); } - sessions_lock.remove(&display); } else { - if let Some(info) = sessions_lock.get_mut(&display) { + if let Some(info) = sessions_lock.get(&display) { + let mut info = info.lock().unwrap(); if info.texture_rgba_ptr != usize::default() && info.texture_rgba_ptr != ptr as TextureRgbaPtr { @@ -392,38 +491,61 @@ impl VideoRenderer { } info.texture_rgba_ptr = ptr as _; info.notify_render_type = None; + info.reset_watchdog(); } else { - if ptr != 0 { - sessions_lock.insert( - display, - DisplaySessionInfo { - texture_rgba_ptr: ptr as _, - size: (0, 0), - #[cfg(feature = "vram")] - gpu_output_ptr: usize::default(), - notify_render_type: None, - }, - ); - } + let mut info = DisplaySessionInfo { + texture_rgba_ptr: ptr as _, + ..Default::default() + }; + info.reset_watchdog(); + sessions_lock.insert(display, Arc::new(Mutex::new(info))); } } } + // 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. + fn unregister_pixelbuffer_texture(&self, display: usize, ptr: usize) { + if ptr == 0 { + return; + } + let mut sessions_lock = self.map_display_sessions.write().unwrap(); + if let Some(info_arc) = sessions_lock.get(&display).cloned() { + let mut info = info_arc.lock().unwrap(); + if info.texture_rgba_ptr != ptr as TextureRgbaPtr { + return; + } + info.texture_rgba_ptr = usize::default(); + #[cfg(feature = "vram")] + if info.gpu_output_ptr != usize::default() { + return; + } + drop(info); + sessions_lock.remove(&display); + } + } + #[cfg(feature = "vram")] pub fn register_gpu_output(&self, display: usize, ptr: usize) { let mut sessions_lock = self.map_display_sessions.write().unwrap(); if ptr == 0 { - if let Some(info) = sessions_lock.get_mut(&display) { + if let Some(info_arc) = sessions_lock.get(&display).cloned() { + let mut info = info_arc.lock().unwrap(); if info.gpu_output_ptr != usize::default() { info.gpu_output_ptr = usize::default(); } if info.texture_rgba_ptr != usize::default() { return; } + drop(info); + sessions_lock.remove(&display); } - sessions_lock.remove(&display); } else { - if let Some(info) = sessions_lock.get_mut(&display) { + if let Some(info) = sessions_lock.get(&display) { + let mut info = info.lock().unwrap(); if info.gpu_output_ptr != usize::default() && info.gpu_output_ptr != ptr { log::error!( "gpu_output_ptr is not null and not equal to ptr, relace {} to {}", @@ -433,50 +555,90 @@ impl VideoRenderer { } info.gpu_output_ptr = ptr as _; info.notify_render_type = None; + info.reset_watchdog(); } else { - if ptr != usize::default() { - sessions_lock.insert( - display, - DisplaySessionInfo { - texture_rgba_ptr: usize::default(), - size: (0, 0), - gpu_output_ptr: ptr, - notify_render_type: None, - }, - ); - } + let mut info = DisplaySessionInfo { + gpu_output_ptr: ptr, + ..Default::default() + }; + info.reset_watchdog(); + sessions_lock.insert(display, Arc::new(Mutex::new(info))); } } } + // See unregister_pixelbuffer_texture for why this is compare-and-clear. + #[cfg(feature = "vram")] + pub fn unregister_gpu_output(&self, display: usize, ptr: usize) { + if ptr == 0 { + return; + } + let mut sessions_lock = self.map_display_sessions.write().unwrap(); + if let Some(info_arc) = sessions_lock.get(&display).cloned() { + let mut info = info_arc.lock().unwrap(); + if info.gpu_output_ptr != ptr { + return; + } + info.gpu_output_ptr = usize::default(); + if info.texture_rgba_ptr != usize::default() { + return; + } + drop(info); + sessions_lock.remove(&display); + } + } + + #[inline] + fn display_session_info(&self, display: usize) -> Option>> { + let read_lock = self.map_display_sessions.read().unwrap(); + if !self.is_support_multi_ui_session { + read_lock.values().next().cloned() + } else { + read_lock.get(&display).cloned() + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool { - let mut write_lock = self.map_display_sessions.write().unwrap(); - let opt_info = if !self.is_support_multi_ui_session { - write_lock.values_mut().next() - } else { - write_lock.get_mut(&display) - }; - let Some(info) = opt_info else { + let Some(info_arc) = self.display_session_info(display) else { return false; }; + let mut info = info_arc.lock().unwrap(); if info.texture_rgba_ptr == usize::default() { return false; } if info.size.0 != rgba.w || info.size.1 != rgba.h { - log::error!( - "width/height mismatch: ({},{}) != ({},{})", - info.size.0, - info.size.1, - rgba.w, - rgba.h - ); // Peer info's handling is async and may be late than video frame's handling // Allow peer info not set, but not allow wrong width/height for correct local cursor position if info.size != (0, 0) { - return false; + info.size_mismatch_count += 1; + if info.size_mismatch_count == 1 { + log::error!( + "width/height mismatch: ({},{}) != ({},{})", + info.size.0, + info.size.1, + rgba.w, + 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 { + return false; + } + log::warn!( + "adopting frame size ({},{}) after {} mismatched frames", + rgba.w, + rgba.h, + info.size_mismatch_count + ); + info.size = (rgba.w, rgba.h); + info.size_mismatch_count = 0; } + } else { + info.size_mismatch_count = 0; } if let Some(func) = &self.on_rgba_func { unsafe { @@ -490,6 +652,20 @@ impl VideoRenderer { ) }; } + info.pushed_count += 1; + if let Some(get_consumed) = &self.get_consumed_func { + if info.watchdog_sample_due() { + let consumed = unsafe { get_consumed(info.texture_rgba_ptr as _) }; + if info.check_watchdog(consumed) { + log::error!( + "texture rendering broken: {} frames pushed to display {}, none consumed", + info.pushed_count, + display + ); + self.texture_render_failed.store(true, Ordering::SeqCst); + } + } + } if info.notify_render_type != Some(RenderType::PixelBuffer) { info.notify_render_type = Some(RenderType::PixelBuffer); true @@ -500,21 +676,30 @@ impl VideoRenderer { #[cfg(feature = "vram")] pub fn on_texture(&self, display: usize, texture: *mut c_void) -> bool { - let mut write_lock = self.map_display_sessions.write().unwrap(); - let opt_info = if !self.is_support_multi_ui_session { - write_lock.values_mut().next() - } else { - write_lock.get_mut(&display) - }; - let Some(info) = opt_info else { + let Some(info_arc) = self.display_session_info(display) else { return false; }; + let mut info = info_arc.lock().unwrap(); if info.gpu_output_ptr == usize::default() { return false; } if let Some(func) = &self.on_texture_func { unsafe { func(info.gpu_output_ptr as _, texture) }; } + info.pushed_count += 1; + if let Some(get_consumed) = &self.get_gpu_consumed_func { + if info.watchdog_sample_due() { + let consumed = unsafe { get_consumed(info.gpu_output_ptr as _) }; + if info.check_watchdog(consumed) { + log::error!( + "gpu texture rendering broken: {} frames pushed to display {}, none consumed", + info.pushed_count, + display + ); + self.texture_render_failed.store(true, Ordering::SeqCst); + } + } + } if info.notify_render_type != Some(RenderType::Texture) { info.notify_render_type = Some(RenderType::Texture); true @@ -524,11 +709,10 @@ impl VideoRenderer { } pub fn reset_all_display_render_type(&self) { - let mut write_lock = self.map_display_sessions.write().unwrap(); - write_lock - .values_mut() - .map(|v| v.notify_render_type = None) - .count(); + let read_lock = self.map_display_sessions.read().unwrap(); + for info in read_lock.values() { + info.lock().unwrap().notify_render_type = None; + } } } @@ -661,9 +845,24 @@ impl FlutterHandler { } pub fn update_use_texture_render(&self) { - self.use_texture_render - .store(crate::ui_interface::use_texture_render(), Ordering::Relaxed); + let v = crate::ui_interface::use_texture_render(); + self.use_texture_render.store(v, Ordering::Relaxed); self.display_rgbas.write().unwrap().clear(); + if v { + // Texture render was (re-)enabled; validate it afresh so a still + // broken environment fails over again instead of staying black. + for (_, session) in self.session_handlers.read().unwrap().iter() { + for info in session + .renderer + .map_display_sessions + .read() + .unwrap() + .values() + { + info.lock().unwrap().reset_watchdog(); + } + } + } } } @@ -887,12 +1086,13 @@ impl InvokeUiSession for FlutterHandler { if !self.use_texture_render.load(Ordering::Relaxed) { return; } - for (_, session) in self.session_handlers.read().unwrap().iter() { + for (session_id, session) in self.session_handlers.read().unwrap().iter() { if session.renderer.on_texture(display, texture) { if let Some(stream) = &session.event_stream { stream.add(EventToUI::Texture(display, true)); } } + Self::check_texture_render_failed(session_id, session); } } @@ -1262,16 +1462,31 @@ impl FlutterHandler { display: usize, rgba: &mut scrap::ImageRgb, ) { - for (_, session) in self.session_handlers.read().unwrap().iter() { + for (session_id, session) in self.session_handlers.read().unwrap().iter() { if use_texture_render || session.displays.len() > 1 { if session.renderer.on_rgba(display, rgba) { if let Some(stream) = &session.event_stream { stream.add(EventToUI::Texture(display, false)); } } + Self::check_texture_render_failed(session_id, session); } } } + + // Consume the watchdog latch outside the per-frame hot path work; the + // actual fallback (config write, decoder reset) runs on its own thread. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn check_texture_render_failed(session_id: &SessionID, session: &SessionHandler) { + if session + .renderer + .texture_render_failed + .swap(false, Ordering::SeqCst) + { + let session_id = session_id.clone(); + std::thread::spawn(move || on_texture_render_failed(session_id)); + } + } } // This function is only used for the default connection session. @@ -1795,6 +2010,106 @@ pub fn session_register_gpu_texture(_session_id: SessionID, _display: usize, _ou } } +#[inline] +pub fn session_unregister_pixelbuffer_texture(session_id: SessionID, display: usize, ptr: usize) { + for s in sessions::get_sessions() { + if let Some(h) = s + .ui_handler + .session_handlers + .read() + .unwrap() + .get(&session_id) + { + h.renderer.unregister_pixelbuffer_texture(display, ptr); + break; + } + } +} + +#[inline] +pub fn session_unregister_gpu_texture(_session_id: SessionID, _display: usize, _output_ptr: usize) { + #[cfg(feature = "vram")] + for s in sessions::get_sessions() { + if let Some(h) = s + .ui_handler + .session_handlers + .read() + .unwrap() + .get(&_session_id) + { + h.renderer.unregister_gpu_output(_display, _output_ptr); + break; + } + } +} + +// 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 push_texture_probe_frame(ptr: usize) { + if ptr == 0 { + return; + } + let Ok(lib) = &*TEXTURE_RGBA_RENDERER_PLUGIN else { + return; + }; + let Ok(func) = (unsafe { + lib.symbol::("FlutterRgbaRendererPluginOnRgba") + }) else { + return; + }; + let frame: [u8; 4] = [255, 255, 255, 255]; + unsafe { func(ptr as _, frame.as_ptr(), 4, 1, 1, 1) }; +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_texture_probe_consumed(ptr: usize) -> u64 { + if ptr == 0 { + return 0; + } + let Ok(lib) = &*TEXTURE_RGBA_RENDERER_PLUGIN else { + return 0; + }; + let Ok(func) = (unsafe { + lib.symbol::("FlutterRgbaRendererPluginGetConsumed") + }) else { + return 0; + }; + 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. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn on_texture_render_failed(session_id: SessionID) { + log::error!( + "texture rendering failed for session {}, falling back to software rendering", + session_id + ); + LocalConfig::set_option( + hbb_common::config::keys::OPTION_TEXTURE_RENDER_HEALTH.to_owned(), + format!( + "failed-watchdog@{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .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")], + &[], + ); + session.use_texture_render_changed(); + session.ui_handler.update_use_texture_render(); + } +} + #[inline] #[cfg(not(feature = "vram"))] pub fn get_adapter_luid() -> Option { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9b73c4cd4..47a999efb 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1227,6 +1227,13 @@ pub fn main_set_local_option(key: String, value: String) { let is_render_target = |session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera(); if is_texture_render_key { + // An explicit user toggle gives texture rendering a fresh chance; a + // stale failure record must not override it (the watchdog re-records + // if the environment is still broken). + set_local_option( + config::keys::OPTION_TEXTURE_RENDER_HEALTH.to_owned(), + "".to_owned(), + ); let session_event = [("v", &value)]; for session in sessions::get_sessions() { if !is_render_target(&session) { @@ -2295,6 +2302,39 @@ pub fn session_register_gpu_texture( )) } +pub fn session_unregister_pixelbuffer_texture( + session_id: SessionID, + display: usize, + ptr: usize, +) -> SyncReturn<()> { + SyncReturn(super::flutter::session_unregister_pixelbuffer_texture( + session_id, display, ptr, + )) +} + +pub fn session_unregister_gpu_texture( + session_id: SessionID, + display: usize, + ptr: usize, +) -> SyncReturn<()> { + SyncReturn(super::flutter::session_unregister_gpu_texture( + session_id, display, ptr, + )) +} + +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); + SyncReturn(()) +} + +pub fn main_get_texture_probe_consumed(ptr: usize) -> SyncReturn { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return SyncReturn(super::flutter::get_texture_probe_consumed(ptr)); + #[cfg(any(target_os = "android", target_os = "ios"))] + SyncReturn(0) +} + pub fn query_onlines(ids: Vec) { let _ = flutter::async_tasks::query_onlines(ids); } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 2189648d9..5a0a3cd3b 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "يتم دعم صيغة CIDR، مثال: 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index ac302f3af..1b1ef50b2 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Падтрымліваецца натацыя CIDR, напрыклад: 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index c339270c0..012496c8e 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Поддържа се CIDR нотация, например: 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d3b0ae7e0..6fe025383 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "S'admet la notació CIDR, per exemple 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7423cceb3..20e92eef6 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "支持 CIDR 写法,例如 192.168.1.0/24"), ("Continue", "继续"), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", "纹理渲染失效,已自动切换为软件渲染。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index abd4e60aa..3300e9393 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Je podporován zápis CIDR, například 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 0ecab9098..265498633 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR-notation understøttes, f.eks. 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index d71dfa6ce..e8eff4802 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Die CIDR-Notation wird unterstützt, z. B. 192.168.1.0/24"), ("Continue", "Weiter"), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index deca79aa6..9be4e6bb5 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Υποστηρίζεται η σημειογραφία CIDR, π.χ. 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index fcd68a300..151eecc70 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -285,5 +285,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_whitelist_caveat_tip", "The ID is reported by the connecting client. This whitelist reduces exposure and does not replace the password or 2FA."), ("whitelist_cidr_tip", "CIDR notation is supported, e.g. 192.168.1.0/24"), ("Your ip is blocked by the peer", "Your IP is blocked by the peer"), + ("texture-render-fallback-tip", "Texture rendering failed and was disabled. Using software rendering instead."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index e6cc0cae5..d7f584b07 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "La notacio CIDR estas subtenata, ekzemple 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 2e7ace9cf..27f1801fd 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Se admite la notación CIDR, por ejemplo 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 238c84c88..0fac0c983 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Toetatud on CIDR-tähistus, näiteks 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 3fd38eb55..e33195601 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR notazioa onartzen da, adibidez 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 1e4039be7..e56efa356 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "نماد CIDR پشتیبانی می شود، برای مثال 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 2a21ba049..e789238c2 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR-merkintä on tuettu, esimerkiksi 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 8359587a2..d29bec615 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "La notation CIDR est prise en charge, par exemple 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 97c3e9171..689f100bd 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "მხარდაჭერილია CIDR ჩანაწერი, მაგალითად 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs index c9c2c9177..6e9cba34f 100644 --- a/src/lang/gu.rs +++ b/src/lang/gu.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR નોટેશન સપોર્ટેડ છે, ઉदાહરણ તરીકે 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 3ea0d7626..20804fe5a 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "יש תמיכה בסימון CIDR, לדוגמה 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hi.rs b/src/lang/hi.rs index e3851a0d8..dc2d5fa53 100644 --- a/src/lang/hi.rs +++ b/src/lang/hi.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR नोटेशन समर्थित है, उदाहरण के लिए 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index ee894b0e7..d29210884 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Podržan je CIDR zapis, primjerice 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 14a85f1f7..cf32a38fc 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "A CIDR jelölés támogatott, például 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 7ba387e48..7526dc5a3 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Notasi CIDR didukung, misalnya 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 1297972df..bce001477 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "È supportata la notazione CIDR, ad esempio 192.168.1.0/24"), ("Continue", "Continua"), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index ba6e6cb09..b4d913f22 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR 表記に対応しています。例: 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index f60af542b..39046e299 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR 표기를 지원합니다. 예: 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index fc59efde3..0e0358507 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR жазбасына қолдау көрсетіледі, мысалы 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 3589a2fb3..08568ebdc 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Palaikomas CIDR žymėjimas, pavyzdžiui 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index d4101d6db..89cb3121f 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Tiek atbalstīts CIDR pieraksts, piemēram 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ml.rs b/src/lang/ml.rs index d93760b50..fa7ca2356 100644 --- a/src/lang/ml.rs +++ b/src/lang/ml.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR നൊട്ടേഷൻ പിന്തുണയ്ക്കുന്നു, ഉദാഹരണത്തിന് 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 3cc71a96b..4b6aa994f 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR-notasjon støttes, for eksempel 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 61a5306c9..010b61263 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR-notatie wordt ondersteund, bijv. 192.168.1.0/24"), ("Continue", "Doorgaan"), ("Browser didn't open? Use the url below to sign in.", "Is de browser niet geopend? Gebruik onderstaande URL om in te loggen."), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index df5c53439..8ad55d9ad 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Obsługiwana jest notacja CIDR, na przykład 192.168.1.0/24"), ("Continue", "Kontynuuj"), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 79420e73b..3f7257b07 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "A notação CIDR é suportada, por exemplo 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 61bf5cf48..db797dee5 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "A notação CIDR é suportada, por exemplo 192.168.1.0/24"), ("Continue", "Continuar"), ("Browser didn't open? Use the url below to sign in.", "O navegador não foi aberto? Use a URL abaixo para fazer login."), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 4499df1bd..2fa8138e9 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Notația CIDR este acceptată, de exemplu 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 459549f97..6f9354b04 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Поддерживается нотация CIDR, например 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 1ccfcf7dc..156619c44 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Sa notatzione CIDR est suportada, pro esempru 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 3d4993115..92ee62b94 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Je podporovaný zápis CIDR, napríklad 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 10fc5d909..61e81fb38 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Podprt je zapis CIDR, na primer 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 91f5d4c7a..e5c9500c9 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Mbështetet shënimi CIDR, për shembull 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index b79eccf5b..4c5fef779 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Podržan je CIDR zapis, na primer 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 79dd316cd..cecc5a4ec 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR-notation stöds, till exempel 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 376af972e..44efb9114 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR குறியீடு ஆதரிக்கப்படுகிறது, எடுத்துக்காட்டாக 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index f16cf1ebc..2d81dc97c 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", ""), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index bd87cf5a7..2e00ae759 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "รองรับรูปแบบ CIDR เช่น 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 2925ce792..884824931 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "CIDR gösterimi desteklenir, örneğin 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 438cb8091..cbaf83483 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "支援 CIDR 寫法,例如 192.168.1.0/24"), ("Continue", "繼續"), ("Browser didn't open? Use the url below to sign in.", "瀏覽器未開啟?請使用下方網址登入。"), + ("texture-render-fallback-tip", "紋理渲染失效,已自動切換為軟體渲染。"), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 7e55426d1..125fbbe42 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Підтримується нотація CIDR, наприклад 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index af358831e..65a38a531 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_cidr_tip", "Hỗ trợ ký hiệu CIDR, ví dụ 192.168.1.0/24"), ("Continue", ""), ("Browser didn't open? Use the url below to sign in.", ""), + ("texture-render-fallback-tip", ""), ].iter().cloned().collect(); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 94fde4392..edb5ee62f 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -176,6 +176,16 @@ pub fn get_option>(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. +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn texture_render_health_failed() -> bool { + LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER_HEALTH).starts_with("failed") +} + #[inline] pub fn use_texture_render() -> bool { #[cfg(target_os = "android")] @@ -183,28 +193,33 @@ pub fn use_texture_render() -> bool { #[cfg(target_os = "ios")] return false; - #[cfg(target_os = "macos")] - return cfg!(feature = "flutter") - && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; - - #[cfg(target_os = "linux")] - return cfg!(feature = "flutter") - && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"; - - #[cfg(target_os = "windows")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { if !cfg!(feature = "flutter") { return false; } - // https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 - #[cfg(debug_assertions)] - let default_texture = true; - #[cfg(not(debug_assertions))] - let default_texture = crate::platform::is_win_10_or_greater(); - if default_texture { - LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N" - } else { - return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; + if texture_render_health_failed() { + return false; + } + + #[cfg(target_os = "macos")] + return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; + + #[cfg(target_os = "linux")] + return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"; + + #[cfg(target_os = "windows")] + { + // https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 + #[cfg(debug_assertions)] + let default_texture = true; + #[cfg(not(debug_assertions))] + let default_texture = crate::platform::is_win_10_or_greater(); + if default_texture { + LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N" + } else { + return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; + } } } }