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 kOptionImageQuality = "image_quality";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionTextureRender = "use-texture-render"; const String kOptionTextureRender = "use-texture-render";
const String kOptionTextureRenderHealth = "texture-render-health";
const String kOptionD3DRender = "allow-d3d-render"; const String kOptionD3DRender = "allow-d3d-render";
const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInTabs = "allow-open-in-tabs";
const String kOptionOpenInWindows = "allow-open-in-windows"; 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_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size; import 'package:window_size/window_size.dart' as window_size;
import '../widgets/button.dart'; import '../widgets/button.dart';
import '../widgets/texture_render_probe.dart';
class DesktopHomePage extends StatefulWidget { class DesktopHomePage extends StatefulWidget {
const DesktopHomePage({Key? key}) : super(key: key); const DesktopHomePage({Key? key}) : super(key: key);
@@ -60,15 +61,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final isIncomingOnly = bind.isIncomingOnly(); final isIncomingOnly = bind.isIncomingOnly();
return _buildBlock( return Stack(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildLeftPane(context), _buildBlock(
if (!isIncomingOnly) const VerticalDivider(width: 1), child: Row(
if (!isIncomingOnly) Expanded(child: buildRightPane(context)), 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}) { Widget _buildBlock({required Widget child}) {

View File

@@ -156,6 +156,7 @@ class _RemotePageState extends State<RemotePage>
widget.tabController?.state.listen(_onMacOSTabStateChanged); widget.tabController?.state.listen(_onMacOSTabStateChanged);
} }
Get.put<FFI>(_ffi, tag: widget.id); Get.put<FFI>(_ffi, tag: widget.id);
_RasterStallMonitor.start();
_ffi.imageModel.addCallbackOnFirstImage((String peerId) { _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
_ffi.canvasModel.activateLocalCursor(); _ffi.canvasModel.activateLocalCursor();
showKBLayoutTypeChooserIfNeeded( 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; int _display = 0;
SessionID? _sessionId; SessionID? _sessionId;
bool _destroying = false; bool _destroying = false;
bool _closed = false;
int _ptr = 0;
int? _id; int? _id;
final textureRenderer = TextureRgbaRenderer(); final textureRenderer = TextureRgbaRenderer();
@@ -27,11 +29,22 @@ class _PixelbufferTexture {
_textureKey = bind.getNextTextureKey(); _textureKey = bind.getNextTextureKey();
_sessionId = sessionId; _sessionId = sessionId;
textureRenderer.createTexture(_textureKey).then((id) async { final textureKey = _textureKey;
textureRenderer.createTexture(textureKey).then((id) async {
_id = id; _id = id;
if (id != -1) { 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); 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); platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
debugPrint( debugPrint(
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr"); "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) { if (!_destroying && _textureKey != -1 && _sessionId != null) {
_destroying = true; _destroying = true;
if (unregisterTexture) { if (_ptr != 0) {
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0); // Compare-and-clear: clears only if Rust still holds this pointer, so
// sleep for a while to avoid the texture is used after it's unregistered. // a registration a new window has already made stays intact (#8016).
await Future.delayed(Duration(milliseconds: 100)); // 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); await textureRenderer.closeTexture(_textureKey);
_textureKey = -1; _textureKey = -1;
@@ -61,6 +78,7 @@ class _GpuTexture {
SessionID? _sessionId; SessionID? _sessionId;
final support = bind.mainHasGpuTextureRender(); final support = bind.mainHasGpuTextureRender();
bool _destroying = false; bool _destroying = false;
bool _closed = false;
int _display = 0; int _display = 0;
int? _id; int? _id;
int? _output; int? _output;
@@ -79,9 +97,18 @@ class _GpuTexture {
gpuTextureRenderer.registerTexture().then((id) async { gpuTextureRenderer.registerTexture().then((id) async {
_id = id; _id = id;
if (id != null) { 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; _textureId = id;
ffi.textureModel.setGpuTextureId(display: d, id: id); ffi.textureModel.setGpuTextureId(display: d, id: id);
final output = await gpuTextureRenderer.output(id); final output = await gpuTextureRenderer.output(id);
if (_closed) {
return;
}
_output = output; _output = output;
if (output != null) { if (output != null) {
platformFFI.registerGpuTexture(sessionId, d, output); 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 // must stop texture render, render unregistered texture cause crash
_closed = true;
if (!_destroying && support && _sessionId != null && _textureId != -1) { if (!_destroying && support && _sessionId != null && _textureId != -1) {
_destroying = true; _destroying = true;
if (unregisterTexture) { final output = _output;
platformFFI.registerGpuTexture(_sessionId!, _display, 0); if (output != null) {
// sleep for a while to avoid the texture is used after it's unregistered. // Compare-and-clear, see _PixelbufferTexture.destroy.
await Future.delayed(Duration(milliseconds: 100)); platformFFI.unregisterGpuTexture(_sessionId!, _display, output);
_output = null;
} }
await gpuTextureRenderer.unregisterTexture(_textureId); await gpuTextureRenderer.unregisterTexture(_textureId);
_textureId = -1; _textureId = -1;
_destroying = false; _destroying = false;
debugPrint( 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( _handleUseTextureRender(
Map<String, dynamic> evt, SessionID sessionId, String peerId) { Map<String, dynamic> evt, SessionID sessionId, String peerId) {
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y'); 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; waitForFirstImage.value = true;
isRefreshing = true; isRefreshing = true;
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId, showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,

View File

@@ -131,6 +131,12 @@ class PlatformFFI {
void registerGpuTexture(SessionID sessionId, int display, int ptr) => void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionRegisterGpuTexture( _ffiBind.sessionRegisterGpuTexture(
sessionId: sessionId, display: display, ptr: ptr); 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. /// Init the FFI class, loads the native Rust core library.
Future<void> init(String appType) async { Future<void> init(String appType) async {

View File

@@ -136,6 +136,12 @@ class PlatformFFI {
void registerGpuTexture(SessionID sessionId, int display, int ptr) => void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
_ffiBind.sessionRegisterGpuTexture( _ffiBind.sessionRegisterGpuTexture(
sessionId: sessionId, display: display, ptr: ptr); 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 { Future<void> init(String appType) async {
Completer completer = Completer(); Completer completer = Completer();

View File

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

View File

@@ -538,8 +538,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87" ref: "767bb9fe9dcd2c23e2664114bf33760842e872e7"
resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87" resolved-ref: "767bb9fe9dcd2c23e2664114bf33760842e872e7"
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
source: git source: git
version: "0.0.1" version: "0.0.1"
@@ -1298,8 +1298,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "42797e0f03141dc2b585f76c64a13974508058b4" ref: "ad4c37e414ee40853c0e9503b2da6486d4c1768d"
resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4" resolved-ref: "ad4c37e414ee40853c0e9503b2da6486d4c1768d"
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer" url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
source: git source: git
version: "0.0.16" version: "0.0.16"

View File

@@ -88,13 +88,13 @@ dependencies:
texture_rgba_renderer: texture_rgba_renderer:
git: git:
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
ref: 42797e0f03141dc2b585f76c64a13974508058b4 ref: ad4c37e414ee40853c0e9503b2da6486d4c1768d
percent_indicator: ^4.2.2 percent_indicator: ^4.2.2
dropdown_button2: ^2.0.0 dropdown_button2: ^2.0.0
flutter_gpu_texture_renderer: flutter_gpu_texture_renderer:
git: git:
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
ref: 08a471bb8ceccdd50483c81cdfa8b81b07b14b87 ref: 767bb9fe9dcd2c23e2664114bf33760842e872e7
uuid: ^3.0.7 uuid: ^3.0.7
auto_size_text_field: ^2.2.1 auto_size_text_field: ^2.2.1
flex_color_picker: ^3.3.0 flex_color_picker: ^3.3.0

View File

@@ -24,8 +24,9 @@ use std::{
str::FromStr, str::FromStr,
sync::{ sync::{
atomic::{AtomicBool, AtomicUsize, Ordering}, 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) /// 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")] #[cfg(feature = "vram")]
pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64; 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; pub(super) type TextureRgbaPtr = usize;
#[derive(Default)]
struct DisplaySessionInfo { struct DisplaySessionInfo {
// TextureRgba pointer in flutter native. // TextureRgba pointer in flutter native.
texture_rgba_ptr: TextureRgbaPtr, texture_rgba_ptr: TextureRgbaPtr,
size: (usize, usize), size: (usize, usize),
size_mismatch_count: u32,
#[cfg(feature = "vram")] #[cfg(feature = "vram")]
gpu_output_ptr: usize, gpu_output_ptr: usize,
notify_render_type: Option<RenderType>, 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 // 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)] #[derive(Clone)]
struct VideoRenderer { struct VideoRenderer {
is_support_multi_ui_session: bool, 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")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>, 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")] #[cfg(feature = "vram")]
on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>, on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>,
#[cfg(feature = "vram")]
get_gpu_consumed_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiGetConsumed>>,
} }
impl Default for VideoRenderer { impl Default for VideoRenderer {
@@ -312,6 +383,17 @@ impl Default for VideoRenderer {
None 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")] #[cfg(feature = "vram")]
let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN { let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
Ok(lib) => { Ok(lib) => {
@@ -333,14 +415,29 @@ impl Default for VideoRenderer {
None 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 { Self {
map_display_sessions: Default::default(), map_display_sessions: Default::default(),
is_support_multi_ui_session: false, is_support_multi_ui_session: false,
texture_render_failed: Default::default(),
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
on_rgba_func, on_rgba_func,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
get_consumed_func,
#[cfg(feature = "vram")] #[cfg(feature = "vram")]
on_texture_func, on_texture_func,
#[cfg(feature = "vram")]
get_gpu_consumed_func,
} }
} }
} }
@@ -349,19 +446,18 @@ impl VideoRenderer {
#[inline] #[inline]
fn set_size(&mut self, display: usize, width: usize, height: usize) { fn set_size(&mut self, display: usize, width: usize, height: usize) {
let mut sessions_lock = self.map_display_sessions.write().unwrap(); 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 = (width, height);
info.size_mismatch_count = 0;
info.notify_render_type = None; info.notify_render_type = None;
} else { } else {
sessions_lock.insert( sessions_lock.insert(
display, display,
DisplaySessionInfo { Arc::new(Mutex::new(DisplaySessionInfo {
texture_rgba_ptr: usize::default(),
size: (width, height), size: (width, height),
#[cfg(feature = "vram")] ..Default::default()
gpu_output_ptr: usize::default(), })),
notify_render_type: None,
},
); );
} }
} }
@@ -369,7 +465,8 @@ impl VideoRenderer {
fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) { fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) {
let mut sessions_lock = self.map_display_sessions.write().unwrap(); let mut sessions_lock = self.map_display_sessions.write().unwrap();
if ptr == 0 { 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() { if info.texture_rgba_ptr != usize::default() {
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() { if info.gpu_output_ptr != usize::default() {
return; return;
} }
drop(info);
sessions_lock.remove(&display);
} }
sessions_lock.remove(&display);
} else { } 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() if info.texture_rgba_ptr != usize::default()
&& info.texture_rgba_ptr != ptr as TextureRgbaPtr && info.texture_rgba_ptr != ptr as TextureRgbaPtr
{ {
@@ -392,38 +491,61 @@ impl VideoRenderer {
} }
info.texture_rgba_ptr = ptr as _; info.texture_rgba_ptr = ptr as _;
info.notify_render_type = None; info.notify_render_type = None;
info.reset_watchdog();
} else { } else {
if ptr != 0 { let mut info = DisplaySessionInfo {
sessions_lock.insert( texture_rgba_ptr: ptr as _,
display, ..Default::default()
DisplaySessionInfo { };
texture_rgba_ptr: ptr as _, info.reset_watchdog();
size: (0, 0), sessions_lock.insert(display, Arc::new(Mutex::new(info)));
#[cfg(feature = "vram")]
gpu_output_ptr: usize::default(),
notify_render_type: None,
},
);
}
} }
} }
} }
// 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")] #[cfg(feature = "vram")]
pub fn register_gpu_output(&self, display: usize, ptr: usize) { pub fn register_gpu_output(&self, display: usize, ptr: usize) {
let mut sessions_lock = self.map_display_sessions.write().unwrap(); let mut sessions_lock = self.map_display_sessions.write().unwrap();
if ptr == 0 { 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() { if info.gpu_output_ptr != usize::default() {
info.gpu_output_ptr = usize::default(); info.gpu_output_ptr = usize::default();
} }
if info.texture_rgba_ptr != usize::default() { if info.texture_rgba_ptr != usize::default() {
return; return;
} }
drop(info);
sessions_lock.remove(&display);
} }
sessions_lock.remove(&display);
} else { } 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 { if info.gpu_output_ptr != usize::default() && info.gpu_output_ptr != ptr {
log::error!( log::error!(
"gpu_output_ptr is not null and not equal to ptr, relace {} to {}", "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.gpu_output_ptr = ptr as _;
info.notify_render_type = None; info.notify_render_type = None;
info.reset_watchdog();
} else { } else {
if ptr != usize::default() { let mut info = DisplaySessionInfo {
sessions_lock.insert( gpu_output_ptr: ptr,
display, ..Default::default()
DisplaySessionInfo { };
texture_rgba_ptr: usize::default(), info.reset_watchdog();
size: (0, 0), sessions_lock.insert(display, Arc::new(Mutex::new(info)));
gpu_output_ptr: ptr,
notify_render_type: None,
},
);
}
} }
} }
} }
// 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")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool { pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool {
let mut write_lock = self.map_display_sessions.write().unwrap(); let Some(info_arc) = self.display_session_info(display) else {
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 {
return false; return false;
}; };
let mut info = info_arc.lock().unwrap();
if info.texture_rgba_ptr == usize::default() { if info.texture_rgba_ptr == usize::default() {
return false; return false;
} }
if info.size.0 != rgba.w || info.size.1 != rgba.h { 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 // 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 // Allow peer info not set, but not allow wrong width/height for correct local cursor position
if info.size != (0, 0) { 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 { if let Some(func) = &self.on_rgba_func {
unsafe { 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) { if info.notify_render_type != Some(RenderType::PixelBuffer) {
info.notify_render_type = Some(RenderType::PixelBuffer); info.notify_render_type = Some(RenderType::PixelBuffer);
true true
@@ -500,21 +676,30 @@ impl VideoRenderer {
#[cfg(feature = "vram")] #[cfg(feature = "vram")]
pub fn on_texture(&self, display: usize, texture: *mut c_void) -> bool { pub fn on_texture(&self, display: usize, texture: *mut c_void) -> bool {
let mut write_lock = self.map_display_sessions.write().unwrap(); let Some(info_arc) = self.display_session_info(display) else {
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 {
return false; return false;
}; };
let mut info = info_arc.lock().unwrap();
if info.gpu_output_ptr == usize::default() { if info.gpu_output_ptr == usize::default() {
return false; return false;
} }
if let Some(func) = &self.on_texture_func { if let Some(func) = &self.on_texture_func {
unsafe { func(info.gpu_output_ptr as _, texture) }; 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) { if info.notify_render_type != Some(RenderType::Texture) {
info.notify_render_type = Some(RenderType::Texture); info.notify_render_type = Some(RenderType::Texture);
true true
@@ -524,11 +709,10 @@ impl VideoRenderer {
} }
pub fn reset_all_display_render_type(&self) { pub fn reset_all_display_render_type(&self) {
let mut write_lock = self.map_display_sessions.write().unwrap(); let read_lock = self.map_display_sessions.read().unwrap();
write_lock for info in read_lock.values() {
.values_mut() info.lock().unwrap().notify_render_type = None;
.map(|v| v.notify_render_type = None) }
.count();
} }
} }
@@ -661,9 +845,24 @@ impl FlutterHandler {
} }
pub fn update_use_texture_render(&self) { pub fn update_use_texture_render(&self) {
self.use_texture_render let v = crate::ui_interface::use_texture_render();
.store(crate::ui_interface::use_texture_render(), Ordering::Relaxed); self.use_texture_render.store(v, Ordering::Relaxed);
self.display_rgbas.write().unwrap().clear(); 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) { if !self.use_texture_render.load(Ordering::Relaxed) {
return; 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 session.renderer.on_texture(display, texture) {
if let Some(stream) = &session.event_stream { if let Some(stream) = &session.event_stream {
stream.add(EventToUI::Texture(display, true)); stream.add(EventToUI::Texture(display, true));
} }
} }
Self::check_texture_render_failed(session_id, session);
} }
} }
@@ -1262,16 +1462,31 @@ impl FlutterHandler {
display: usize, display: usize,
rgba: &mut scrap::ImageRgb, 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 use_texture_render || session.displays.len() > 1 {
if session.renderer.on_rgba(display, rgba) { if session.renderer.on_rgba(display, rgba) {
if let Some(stream) = &session.event_stream { if let Some(stream) = &session.event_stream {
stream.add(EventToUI::Texture(display, false)); 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. // 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] #[inline]
#[cfg(not(feature = "vram"))] #[cfg(not(feature = "vram"))]
pub fn get_adapter_luid() -> Option<i64> { 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 = let is_render_target =
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera(); |session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
if is_texture_render_key { 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)]; let session_event = [("v", &value)];
for session in sessions::get_sessions() { for session in sessions::get_sessions() {
if !is_render_target(&session) { 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>) { pub fn query_onlines(ids: Vec<String>) {
let _ = flutter::async_tasks::query_onlines(ids); 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"), ("whitelist_cidr_tip", "يتم دعم صيغة CIDR، مثال: 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Падтрымліваецца натацыя CIDR, напрыклад: 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Поддържа се CIDR нотация, например: 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "S'admet la notació CIDR, per exemple 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "支持 CIDR 写法,例如 192.168.1.0/24"),
("Continue", "继续"), ("Continue", "继续"),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", "纹理渲染失效,已自动切换为软件渲染。"),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Je podporován zápis CIDR, například 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR-notation understøttes, f.eks. 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Die CIDR-Notation wird unterstützt, z. B. 192.168.1.0/24"),
("Continue", "Weiter"), ("Continue", "Weiter"),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Υποστηρίζεται η σημειογραφία CIDR, π.χ. 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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."), ("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"), ("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"), ("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(); ].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"), ("whitelist_cidr_tip", "La notacio CIDR estas subtenata, ekzemple 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Se admite la notación CIDR, por ejemplo 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Toetatud on CIDR-tähistus, näiteks 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR notazioa onartzen da, adibidez 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "نماد CIDR پشتیبانی می شود، برای مثال 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR-merkintä on tuettu, esimerkiksi 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "La notation CIDR est prise en charge, par exemple 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "მხარდაჭერილია CIDR ჩანაწერი, მაგალითად 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR નોટેશન સપોર્ટેડ છે, ઉदાહરણ તરીકે 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "יש תמיכה בסימון CIDR, לדוגמה 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR नोटेशन समर्थित है, उदाहरण के लिए 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Podržan je CIDR zapis, primjerice 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "A CIDR jelölés támogatott, például 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Notasi CIDR didukung, misalnya 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "È supportata la notazione CIDR, ad esempio 192.168.1.0/24"),
("Continue", "Continua"), ("Continue", "Continua"),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR 表記に対応しています。例: 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR 표기를 지원합니다. 예: 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR жазбасына қолдау көрсетіледі, мысалы 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Palaikomas CIDR žymėjimas, pavyzdžiui 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Tiek atbalstīts CIDR pieraksts, piemēram 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR നൊട്ടേഷൻ പിന്തുണയ്ക്കുന്നു, ഉദാഹരണത്തിന് 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR-notasjon støttes, for eksempel 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR-notatie wordt ondersteund, bijv. 192.168.1.0/24"),
("Continue", "Doorgaan"), ("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."), ("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(); ].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"), ("whitelist_cidr_tip", "Obsługiwana jest notacja CIDR, na przykład 192.168.1.0/24"),
("Continue", "Kontynuuj"), ("Continue", "Kontynuuj"),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "A notação CIDR é suportada, por exemplo 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "A notação CIDR é suportada, por exemplo 192.168.1.0/24"),
("Continue", "Continuar"), ("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."), ("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(); ].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"), ("whitelist_cidr_tip", "Notația CIDR este acceptată, de exemplu 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Поддерживается нотация CIDR, например 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Sa notatzione CIDR est suportada, pro esempru 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Je podporovaný zápis CIDR, napríklad 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Podprt je zapis CIDR, na primer 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Mbështetet shënimi CIDR, për shembull 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Podržan je CIDR zapis, na primer 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR-notation stöds, till exempel 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR குறியீடு ஆதரிக்கப்படுகிறது, எடுத்துக்காட்டாக 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("whitelist_cidr_tip", ""), ("whitelist_cidr_tip", ""),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "รองรับรูปแบบ CIDR เช่น 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "CIDR gösterimi desteklenir, örneğin 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "支援 CIDR 寫法,例如 192.168.1.0/24"),
("Continue", "繼續"), ("Continue", "繼續"),
("Browser didn't open? Use the url below to sign in.", "瀏覽器未開啟?請使用下方網址登入。"), ("Browser didn't open? Use the url below to sign in.", "瀏覽器未開啟?請使用下方網址登入。"),
("texture-render-fallback-tip", "紋理渲染失效,已自動切換為軟體渲染。"),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Підтримується нотація CIDR, наприклад 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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"), ("whitelist_cidr_tip", "Hỗ trợ ký hiệu CIDR, ví dụ 192.168.1.0/24"),
("Continue", ""), ("Continue", ""),
("Browser didn't open? Use the url below to sign in.", ""), ("Browser didn't open? Use the url below to sign in.", ""),
("texture-render-fallback-tip", ""),
].iter().cloned().collect(); ].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] #[inline]
pub fn use_texture_render() -> bool { pub fn use_texture_render() -> bool {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
@@ -183,28 +193,33 @@ pub fn use_texture_render() -> bool {
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
return false; return false;
#[cfg(target_os = "macos")] #[cfg(not(any(target_os = "android", target_os = "ios")))]
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")]
{ {
if !cfg!(feature = "flutter") { if !cfg!(feature = "flutter") {
return false; return false;
} }
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 if texture_render_health_failed() {
#[cfg(debug_assertions)] return false;
let default_texture = true; }
#[cfg(not(debug_assertions))]
let default_texture = crate::platform::is_win_10_or_greater(); #[cfg(target_os = "macos")]
if default_texture { return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"
} else { #[cfg(target_os = "linux")]
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; 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";
}
} }
} }
} }