fix: texture render lifetime protocol, watchdog fallback, startup probe

Root cause of #15848 (and the long-standing macOS #6296 / Linux #3343
class): raw native texture pointers are shared across the platform
thread, the engine raster thread and the video thread, with teardown
ordered by a 100 ms sleep - or, when moving a tab to a new window, by
nothing at all. A lost race frees the texture while it is still in use:
the raster thread parks on a destroyed lock (frozen/black view, a
never-presented 'transparent' hole, every later session black) and the
video thread hangs while holding session locks (app half-dead until
restart, still reported as Responding).

- unregister textures with compare-and-clear (new session_unregister_*
  FFI): a late clear can no longer wipe a new window's registration
  (#8016) and Rust never keeps pushing into a freed texture; the 100 ms
  sleeps are gone (the plugins now drain in-flight pushes and defer
  object deletion until the raster thread is done)
- guard the async texture create path against destroy racing it (#13596)
- per-display locks: the per-frame plugin call no longer holds
  session-level locks, so a stalled plugin or driver call cannot freeze
  every window's UI thread
- adopt the frame size after 30 consecutive mismatches instead of
  dropping frames forever (silent black screen on a live connection)
- watchdog: frames pushed but never consumed by the engine fall the
  session back to software rendering live, record texture-render-health,
  and flip the effective default off; toggling the option clears the
  record and re-arms validation
- Dart raster-stall monitor records a hung raster thread for the next
  launch (rendering cannot be rescued in-process in that state)
- startup probe: render one frame through a 1x1 texture in the main
  window each launch; failure disables texture rendering before the
  first session goes black, a pass self-heals a stale failure record

Platform defaults are unchanged (macOS off, Win10+ on, Linux on).
Requires flutter_texture_rgba_renderer ad4c37e and
flutter_gpu_texture_renderer 767bb9f (pinned in pubspec).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rustdesk
2026-08-13 09:54:17 +08:00
parent c4fd7d692d
commit 7c23e1f4b9
66 changed files with 780 additions and 114 deletions

View File

@@ -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";

View File

@@ -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<DesktopHomePage>
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}) {

View File

@@ -156,6 +156,7 @@ class _RemotePageState extends State<RemotePage>
widget.tabController?.state.listen(_onMacOSTabStateChanged);
}
Get.put<FFI>(_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');
}
});
}
}

View File

@@ -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<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;
@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<FrameTiming> 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)),
);
}
}

View File

@@ -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");
}
}
}

View File

@@ -735,6 +735,11 @@ class FfiModel with ChangeNotifier {
_handleUseTextureRender(
Map<String, dynamic> 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,

View File

@@ -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<void> init(String appType) async {

View File

@@ -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<void> init(String appType) async {
Completer completer = Completer();

View File

@@ -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<void> queryOnlines({required List<String> ids, dynamic hint}) {
return Future(() =>
js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)]));

View File

@@ -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"

View File

@@ -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

View File

@@ -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<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.
pushed_count: u64,
watchdog_pushed_at_sample: u64,
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_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<RwLock<HashMap<usize, DisplaySessionInfo>>>,
map_display_sessions: Arc<RwLock<HashMap<usize, Arc<Mutex<DisplaySessionInfo>>>>>,
// Latched by the watchdog; consumed once by the pushing caller to trigger
// the software-render fallback for this ui session.
texture_render_failed: Arc<AtomicBool>,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
get_consumed_func: Option<Symbol<'static, FlutterRgbaRendererPluginGetConsumed>>,
#[cfg(feature = "vram")]
on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>,
#[cfg(feature = "vram")]
get_gpu_consumed_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiGetConsumed>>,
}
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>(
"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>(
"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<Arc<Mutex<DisplaySessionInfo>>> {
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>("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>("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<i64> {

View File

@@ -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<u64> {
#[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<String>) {
let _ = flutter::async_tasks::query_onlines(ids);
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -176,6 +176,16 @@ 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.
#[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";
}
}
}
}