Compare commits

...

6 Commits

Author SHA1 Message Date
rustdesk
24a16d9a30 fix: address codex review findings on probe/watchdog fallback
- a probe or raster-stall failure record now also downgrades sessions
  that are already running: main_set_local_option broadcasts the
  fallback for failed-* health writes, which also closes the hole where
  the watchdog's idempotence guard no-oped after the probe had already
  written the record
- probe success requires a frame timing newer than the first push:
  'consumed' advances inside the plugin callback before the GL/Metal
  upload, so a raster thread hanging in the driver no longer counts as
  a pass (and cannot clear a previous failure)
- watchdog failures are tagged with the failing backend
  (failed-watchdog-rgba/gpu); the rgba-only probe clears only the rgba
  class, so a working pixel-buffer path can no longer re-enable a
  broken D3D shared-handle path every launch
- the watchdog pauses while the session's window is hidden (new
  session_set_render_visible FFI wired to the window minimize/restore
  events): a display registered in a minimized window no longer records
  a false global failure; observation now counts pushes within the
  window rather than since registration
- probe failure verdicts additionally require a resumed lifecycle,
  matching the raster-stall monitor
- linux plugin: deferred-unref grace lengthened to 10s (no raster-side
  completion barrier exists; documented as heuristic), ref bumped

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:30:15 +08:00
rustdesk
8fc82d04ac keep lockfile at repo resolution, only bump the two plugin refs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:48:28 +08:00
rustdesk
c6c001a15e bump texture plugins with adversarial-review fixes
flutter_texture_rgba_renderer 7932bf9: linux double-free/terminate-UAF
fixes, exported GetConsumed (was invisible to dlsym under hidden
visibility - the watchdog was silently disabled on Linux), C++14
shared_timed_mutex; macos autoreleasepool + failed-registration guard.
flutter_gpu_texture_renderer 208619e: rendering_ no longer sticks true
before the first populate; honest GetConsumed semantics (descriptor
fetches; EGL bind failure still advances it) - noted at the gpu
watchdog call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:47:54 +08:00
rustdesk
7da2bbe6ac fix: address texture watchdog/probe review findings
- watchdog: compare the plugin's cumulative consumed counter against a
  snapshot taken when arming (re-arm was a no-op before), and judge on
  cumulative pushes + elapsed time so sparse damage-driven streams are
  still detected
- failure handling is idempotent (one record per breakage) and updates
  every render session like main_set_local_option does, not only the
  failing one; the fallback toast is not claimed for multi-display
  windows the soft path cannot rescue
- probe: skip when the consumed API is missing (old plugin) or a
  raster-stall is recorded (compositing could hang the main window);
  a fail verdict requires fresh frame timings and a non-minimized
  window; a pass never clears failed-raster-stall; toast only when
  texture rendering was effectively on; probe pixel is transparent
- raster-stall monitor: judge on frame-timing staleness (a mid-episode
  hang was undetectable before), gate on lifecycle/minimized/idle with
  a moving quiet anchor, threshold 30s, shared with the camera page;
  clear stateGlobal minimized flag on plain window restore
- adopt mismatched frame sizes only in multi-ui-session mode (legacy
  mode pairs frames loosely and could ping-pong between displays)
- drop the now-dead closeSession parameter from texture destroy();
  trim comments to repo style

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:22:13 +08:00
rustdesk
c5adac828b bump hbb_common to merged main (texture-render-health key)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:58:31 +08:00
rustdesk
7c23e1f4b9 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>
2026-08-13 09:54:17 +08:00
69 changed files with 1000 additions and 125 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

@@ -21,6 +21,7 @@ import '../../common/shared_state.dart';
import '../../utils/image.dart'; import '../../utils/image.dart';
import '../widgets/remote_toolbar.dart'; import '../widgets/remote_toolbar.dart';
import '../widgets/kb_layout_type_chooser.dart'; import '../widgets/kb_layout_type_chooser.dart';
import '../widgets/raster_stall_monitor.dart';
import '../widgets/tabbar_widget.dart'; import '../widgets/tabbar_widget.dart';
import 'macos_full_screen_focus_recovery.dart'; import 'macos_full_screen_focus_recovery.dart';
@@ -156,6 +157,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(
@@ -647,7 +649,7 @@ class _RemotePageState extends State<RemotePage>
// Clear callback reference to prevent memory leaks and stale references // Clear callback reference to prevent memory leaks and stale references
_ffi.inputModel.onRelativeMouseModeDisabled = null; _ffi.inputModel.onRelativeMouseModeDisabled = null;
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...). // Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
_ffi.textureModel.onRemotePageDispose(closeSession); _ffi.textureModel.onRemotePageDispose();
if (closeSession && !isMacOS) { if (closeSession && !isMacOS) {
// ensure we leave this session, this is a double check // ensure we leave this session, this is a double check
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS. // enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
@@ -1402,3 +1404,4 @@ class CursorPaint extends StatelessWidget {
); );
} }
} }

View File

@@ -19,6 +19,7 @@ import '../../common/shared_state.dart';
import '../../utils/image.dart'; import '../../utils/image.dart';
import '../widgets/remote_toolbar.dart'; import '../widgets/remote_toolbar.dart';
import '../widgets/kb_layout_type_chooser.dart'; import '../widgets/kb_layout_type_chooser.dart';
import '../widgets/raster_stall_monitor.dart';
import '../widgets/tabbar_widget.dart'; import '../widgets/tabbar_widget.dart';
import 'package:flutter_hbb/native/custom_cursor.dart' import 'package:flutter_hbb/native/custom_cursor.dart'
@@ -102,6 +103,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
super.initState(); super.initState();
_ffi = FFI(widget.sessionId); _ffi = FFI(widget.sessionId);
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) {
showKBLayoutTypeChooserIfNeeded( showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager); _ffi.ffiModel.pi.platform, _ffi.dialogManager);
@@ -222,7 +224,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
// https://github.com/flutter/flutter/issues/64935 // https://github.com/flutter/flutter/issues/64935
super.dispose(); super.dispose();
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}"); debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onViewCameraPageDispose(closeSession); _ffi.textureModel.onViewCameraPageDispose();
if (closeSession) { if (closeSession) {
// ensure we leave this session, this is a double check // ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false); _ffi.inputModel.enterOrLeave(false);

View File

@@ -0,0 +1,52 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import '../../common.dart';
import '../../consts.dart';
import '../../models/platform_model.dart';
import '../../models/state_model.dart';
/// Records a hung raster thread (frames continuously scheduled but no frame
/// timings delivered for 30s) in `texture-render-health`; a hang cannot be
/// rescued in-process, so the next launch defaults texture rendering off.
class RasterStallMonitor {
static bool _started = false;
static bool _reported = false;
static DateTime? _lastTimings;
static DateTime _lastQuiet = DateTime.now();
static void start() {
if (_started || isWeb) return;
_started = true;
SchedulerBinding.instance.addTimingsCallback((_) {
_lastTimings = DateTime.now();
});
Timer.periodic(const Duration(seconds: 2), (_) {
if (_reported) return;
final now = DateTime.now();
final lifecycle = SchedulerBinding.instance.lifecycleState;
// Minimized/inactive (incl. screen lock) or idle (nothing scheduled):
// no timings is legitimate, keep moving the quiet anchor forward.
if (stateGlobal.isMinimized ||
(lifecycle != null && lifecycle != AppLifecycleState.resumed) ||
!SchedulerBinding.instance.hasScheduledFrame) {
_lastQuiet = now;
return;
}
var ref = _lastQuiet;
final lastTimings = _lastTimings;
if (lastTimings != null && lastTimings.isAfter(ref)) {
ref = lastTimings;
}
if (now.difference(ref) > const Duration(seconds: 30)) {
_reported = true;
bind.mainSetLocalOption(
key: kOptionTextureRenderHealth, value: 'failed-raster-stall');
debugPrint(
'raster thread stall detected, texture rendering disabled for next launch');
}
});
}
}

View File

@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart'; import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -388,6 +389,7 @@ class _DesktopTabState extends State<DesktopTab>
void onWindowMinimize() { void onWindowMinimize() {
stateGlobal.setMinimized(true); stateGlobal.setMinimized(true);
stateGlobal.setMaximized(false); stateGlobal.setMaximized(false);
_updateSessionsRenderVisible(false);
super.onWindowMinimize(); super.onWindowMinimize();
} }
@@ -395,6 +397,7 @@ class _DesktopTabState extends State<DesktopTab>
void onWindowMaximize() { void onWindowMaximize() {
stateGlobal.setMinimized(false); stateGlobal.setMinimized(false);
_setMaximized(true); _setMaximized(true);
_updateSessionsRenderVisible(true);
super.onWindowMaximize(); super.onWindowMaximize();
} }
@@ -402,9 +405,34 @@ class _DesktopTabState extends State<DesktopTab>
void onWindowUnmaximize() { void onWindowUnmaximize() {
stateGlobal.setMinimized(false); stateGlobal.setMinimized(false);
_setMaximized(false); _setMaximized(false);
_updateSessionsRenderVisible(true);
super.onWindowUnmaximize(); super.onWindowUnmaximize();
} }
@override
void onWindowRestore() {
// A plain restore (no maximize involved) must clear the minimized flag.
stateGlobal.setMinimized(false);
_updateSessionsRenderVisible(true);
super.onWindowRestore();
}
// A hidden window composites nothing; pause the Rust-side texture watchdog
// for its sessions so it cannot record a false failure.
void _updateSessionsRenderVisible(bool visible) {
if (tabType != DesktopTabType.remoteScreen &&
tabType != DesktopTabType.viewCamera) {
return;
}
for (final tab in controller.state.value.tabs) {
try {
final ffi = Get.find<FFI>(tag: tab.key);
bind.sessionSetRenderVisible(
sessionId: ffi.sessionId, visible: visible);
} catch (_) {}
}
}
_saveFrame({bool? flush}) async { _saveFrame({bool? flush}) async {
try { try {
if (tabType == DesktopTabType.main) { if (tabType == DesktopTabType.main) {

View File

@@ -0,0 +1,172 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import '../../common.dart';
import '../../consts.dart';
import '../../models/platform_model.dart';
import '../../models/state_model.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
/// Startup probe: renders one frame through a 1x1 external texture and
/// records in `texture-render-health` whether the engine consumed it, so a
/// broken environment is detected before the first session goes black.
class TextureRenderProbe extends StatefulWidget {
const TextureRenderProbe({Key? key}) : super(key: key);
@override
State<TextureRenderProbe> createState() => _TextureRenderProbeState();
}
class _TextureRenderProbeState extends State<TextureRenderProbe> {
static bool _ranThisLaunch = false;
final _renderer = TextureRgbaRenderer();
int _textureId = -1;
int _textureKey = -1;
int _ptr = 0;
Timer? _timer;
int _ticks = 0;
bool _sawTimings = false;
bool _wasEffectiveOn = false;
DateTime? _lastTimings;
DateTime? _firstPush;
@override
void initState() {
super.initState();
if (_ranThisLaunch || isWeb || !isDesktop) return;
_ranThisLaunch = true;
if (bind.isIncomingOnly()) return;
// An old plugin without the consumed counter cannot be judged, and a
// recorded raster-stall means compositing a texture may hang this window.
if (!bind.mainTextureRenderProbeSupported()) return;
if (bind
.mainGetLocalOption(key: kOptionTextureRenderHealth)
.startsWith('failed-raster-stall')) {
return;
}
_wasEffectiveOn = bind.mainGetUseTextureRender();
// Only probe after the window has really rendered a frame: a hidden
// window (silent/tray start) must not record a false failure.
SchedulerBinding.instance.addTimingsCallback(_onTimings);
Future.delayed(const Duration(seconds: 5), () {
if (!_sawTimings) {
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
_finish(null);
}
});
}
void _onTimings(List<FrameTiming> timings) {
_lastTimings = DateTime.now();
if (_sawTimings) return;
_sawTimings = true;
_start();
}
void _start() async {
if (!mounted) return;
_textureKey = bind.getNextTextureKey();
final id = await _renderer.createTexture(_textureKey);
if (!mounted || id == -1) {
_finish(!mounted ? null : false);
return;
}
_ptr = await _renderer.getTexturePtr(_textureKey);
if (!mounted || _ptr <= 0) {
_finish(!mounted ? null : false);
return;
}
setState(() => _textureId = id);
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
_ticks += 1;
_firstPush ??= DateTime.now();
bind.mainPushTextureProbeFrame(ptr: _ptr);
final consumed = bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0;
// "Consumed" advances inside the plugin callback, before the GL/Metal
// upload; only a frame timing after the push proves a completed frame.
final frameCompleted = consumed &&
_lastTimings != null &&
_lastTimings!.isAfter(_firstPush!);
if (frameCompleted) {
_finish(true);
} else if (_ticks >= 10) {
if (consumed) {
_finish(null);
return;
}
// Only a window that is visibly compositing can prove a failure.
final lifecycle = SchedulerBinding.instance.lifecycleState;
final active =
lifecycle == null || lifecycle == AppLifecycleState.resumed;
final timingsFresh = _lastTimings != null &&
DateTime.now().difference(_lastTimings!) <
const Duration(milliseconds: 1500);
_finish(!stateGlobal.isMinimized && active && timingsFresh
? false
: null);
}
});
}
void _finish(bool? ok) {
_timer?.cancel();
_timer = null;
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
if (ok != null) {
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
if (ok) {
// This rgba probe disproves only the rgba black-texture class: gpu
// failures and raster stalls clear via the option toggle alone.
final clearable = old.isEmpty ||
old.startsWith('failed-probe') ||
old.startsWith('failed-watchdog-rgba');
if (clearable) {
bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
}
} else if (!old.startsWith('failed')) {
debugPrint('texture render probe failed, disabling texture rendering');
bind.mainSetLocalOption(
key: kOptionTextureRenderHealth, value: 'failed-probe');
if (_wasEffectiveOn) {
showToast(translate('texture-render-fallback-tip'));
}
}
}
if (_textureKey != -1) {
_renderer.closeTexture(_textureKey);
_textureKey = -1;
}
_ptr = 0;
if (mounted && _textureId != -1) {
setState(() => _textureId = -1);
} else {
_textureId = -1;
}
}
@override
void dispose() {
_timer?.cancel();
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
if (_textureKey != -1) {
_renderer.closeTexture(_textureKey);
_textureKey = -1;
}
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_textureId == -1) return const SizedBox.shrink();
// Must actually composite for the engine to sample the texture; the
// pushed pixel is fully transparent.
return IgnorePointer(
child: SizedBox(
width: 1, height: 1, child: Texture(textureId: _textureId)),
);
}
}

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,16 @@ class _PixelbufferTexture {
}); });
} }
destroy(bool unregisterTexture, FFI ffi) async { destroy(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: only clears if Rust still holds this pointer
// sleep for a while to avoid the texture is used after it's unregistered. // (#8016-safe); returning from this synchronous call also means no
await Future.delayed(Duration(milliseconds: 100)); // 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 +77,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 +96,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 +121,22 @@ class _GpuTexture {
} }
} }
destroy(bool unregisterTexture, FFI ffi) async { destroy(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");
} }
} }
} }
@@ -200,11 +228,11 @@ class TextureModel {
tryRemoveTexture(int idx) { tryRemoveTexture(int idx) {
_control.remove(idx); _control.remove(idx);
if (_pixelbufferRenderTextures.containsKey(idx)) { if (_pixelbufferRenderTextures.containsKey(idx)) {
_pixelbufferRenderTextures[idx]!.destroy(true, ffi); _pixelbufferRenderTextures[idx]!.destroy(ffi);
_pixelbufferRenderTextures.remove(idx); _pixelbufferRenderTextures.remove(idx);
} }
if (_gpuRenderTextures.containsKey(idx)) { if (_gpuRenderTextures.containsKey(idx)) {
_gpuRenderTextures[idx]!.destroy(true, ffi); _gpuRenderTextures[idx]!.destroy(ffi);
_gpuRenderTextures.remove(idx); _gpuRenderTextures.remove(idx);
} }
} }
@@ -224,25 +252,25 @@ class TextureModel {
} }
} }
onRemotePageDispose(bool closeSession) async { onRemotePageDispose() async {
final ffi = parent.target; final ffi = parent.target;
if (ffi == null) return; if (ffi == null) return;
for (final texture in _pixelbufferRenderTextures.values) { for (final texture in _pixelbufferRenderTextures.values) {
await texture.destroy(closeSession, ffi); await texture.destroy(ffi);
} }
for (final texture in _gpuRenderTextures.values) { for (final texture in _gpuRenderTextures.values) {
await texture.destroy(closeSession, ffi); await texture.destroy(ffi);
} }
} }
onViewCameraPageDispose(bool closeSession) async { onViewCameraPageDispose() async {
final ffi = parent.target; final ffi = parent.target;
if (ffi == null) return; if (ffi == null) return;
for (final texture in _pixelbufferRenderTextures.values) { for (final texture in _pixelbufferRenderTextures.values) {
await texture.destroy(closeSession, ffi); await texture.destroy(ffi);
} }
for (final texture in _gpuRenderTextures.values) { for (final texture in _gpuRenderTextures.values) {
await texture.destroy(closeSession, ffi); await texture.destroy(ffi);
} }
} }

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,31 @@ 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 sessionSetRenderVisible(
{required UuidValue sessionId, required bool visible, dynamic hint}) {}
bool mainTextureRenderProbeSupported({dynamic hint}) {
return false;
}
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: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87" resolved-ref: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
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: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4" resolved-ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
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: 883326ddd4fb2af1484bf873b4ea856a0ac440bc
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: 208619e750a5fd904c689a9babd6ccf0f7c1ca88
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

@@ -23,9 +23,10 @@ use std::{
os::raw::{c_char, c_int, c_void}, os::raw::{c_char, c_int, c_void},
str::FromStr, str::FromStr,
sync::{ sync::{
atomic::{AtomicBool, AtomicUsize, Ordering}, atomic::{AtomicBool, AtomicU8, 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,119 @@ 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;
// Which texture backend the watchdog saw fail; the health record carries it
// so the rgba-only startup probe never clears a gpu-path failure.
pub(super) const WATCHDOG_FAILED_RGBA: u8 = 1;
#[cfg(feature = "vram")]
pub(super) const WATCHDOG_FAILED_GPU: u8 = 2;
#[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 (black view on a live connection). Armed
// until a consumption is observed since arming.
pushed_count: u64,
watchdog_consumed_base: Option<u64>,
watchdog_pushed_base: u64,
watchdog_since: Option<Instant>,
watchdog_last_sample: Option<Instant>,
watchdog_armed: bool,
}
impl DisplaySessionInfo {
fn reset_watchdog(&mut self) {
self.pushed_count = 0;
self.watchdog_consumed_base = None;
self.watchdog_pushed_base = 0;
self.watchdog_since = None;
self.watchdog_last_sample = None;
self.watchdog_armed = true;
}
// Restart the observation window without disarming; used while the window
// is hidden, where the engine legitimately composites nothing.
fn pause_watchdog(&mut self) {
self.watchdog_consumed_base = None;
self.watchdog_since = None;
}
// The plugin counter is cumulative and never resets, so compare against a
// snapshot taken when arming; damage-driven streams can be sparse, so
// judge on pushes within the observation window plus elapsed time.
fn check_watchdog(&mut self, consumed: u64) -> bool {
let now = Instant::now();
let Some(base) = self.watchdog_consumed_base else {
self.watchdog_consumed_base = Some(consumed);
self.watchdog_pushed_base = self.pushed_count;
self.watchdog_since = Some(now);
return false;
};
if consumed > base {
self.watchdog_armed = false;
return false;
}
if self.pushed_count - self.watchdog_pushed_base >= 30
&& self
.watchdog_since
.map(|t| now.duration_since(t) >= Duration::from_secs(3))
.unwrap_or(false)
{
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
// Per-display mutexes: the per-frame plugin call must not hold session-level
// locks, or a stalled plugin/driver call freezes every window's UI thread.
#[derive(Clone)] #[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 (WATCHDOG_FAILED_*); consumed once by the
// pushing caller to trigger the software-render fallback for this session.
texture_render_failed: Arc<AtomicU8>,
// Hidden windows legitimately composite nothing; the watchdog pauses.
render_visible: 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 +406,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 +438,30 @@ 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(),
render_visible: Arc::new(AtomicBool::new(true)),
#[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 +470,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 +489,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 +498,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 +515,59 @@ 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,
},
);
}
} }
} }
} }
// Compare-and-clear: an unconditional clear could wipe the registration a
// new window just made when a tab moves between windows (#8016); waiting
// on the display mutex also drains an in-flight push via the old pointer.
fn unregister_pixelbuffer_texture(&self, display: usize, ptr: usize) {
if ptr == 0 {
return;
}
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 +577,91 @@ 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
);
}
// If sizes still disagree after this many frames the peer info
// is not coming and dropping forever leaves a live session
// black. Legacy single-ui-session pairs frames loosely
// (values().next()), so only adopt where pairing is exact.
if !self.is_support_multi_ui_session || info.size_mismatch_count < 30 {
return false;
}
log::warn!(
"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 +675,23 @@ impl VideoRenderer {
) )
}; };
} }
info.pushed_count += 1;
if let Some(get_consumed) = &self.get_consumed_func {
if !self.render_visible.load(Ordering::Relaxed) {
info.pause_watchdog();
} else 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(WATCHDOG_FAILED_RGBA, 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 +702,36 @@ 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;
// Gpu "consumed" counts descriptor fetches (an EGL bind failure still
// advances it), so this only detects never-composited outputs; the
// rgba path and the startup probe cover bind-failure black screens.
if let Some(get_consumed) = &self.get_gpu_consumed_func {
if !self.render_visible.load(Ordering::Relaxed) {
info.pause_watchdog();
} else 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(WATCHDOG_FAILED_GPU, 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 +741,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 +877,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 +1118,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 +1494,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) {
let kind = session
.renderer
.texture_render_failed
.swap(0, Ordering::SeqCst);
if kind != 0 {
let session_id = session_id.clone();
std::thread::spawn(move || on_texture_render_failed(session_id, kind));
}
}
} }
// This function is only used for the default connection session. // This function is only used for the default connection session.
@@ -1795,6 +2042,168 @@ 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;
}
}
}
// Hidden windows legitimately composite nothing; pausing the watchdog there
// keeps a minimized/background window from recording a false failure.
#[inline]
pub fn session_set_render_visible(session_id: SessionID, visible: bool) {
for s in sessions::get_sessions() {
if let Some(h) = s
.ui_handler
.session_handlers
.read()
.unwrap()
.get(&session_id)
{
h.renderer
.render_visible
.store(visible, Ordering::Relaxed);
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 texture_render_probe_supported() -> bool {
match &*TEXTURE_RGBA_RENDERER_PLUGIN {
Ok(lib) => unsafe {
lib.symbol::<FlutterRgbaRendererPluginGetConsumed>(
"FlutterRgbaRendererPluginGetConsumed",
)
.is_ok()
},
Err(_) => false,
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn push_texture_probe_frame(ptr: usize) {
if ptr == 0 {
return;
}
let Ok(lib) = &*TEXTURE_RGBA_RENDERER_PLUGIN else {
return;
};
let Ok(func) = (unsafe {
lib.symbol::<FlutterRgbaRendererPluginOnRgba>("FlutterRgbaRendererPluginOnRgba")
}) else {
return;
};
// Fully transparent so the 1x1 probe pixel is invisible on any theme.
let frame: [u8; 4] = [0, 0, 0, 0];
unsafe { func(ptr as _, frame.as_ptr(), 4, 1, 1, 1) };
}
#[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: fall back to
// software rendering and record the breakage (health flips the default off;
// a passing startup probe or an explicit option toggle clears it).
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn on_texture_render_failed(session_id: SessionID, kind: u8) {
// One record per breakage; later fires (other displays/sessions) no-op.
// Sessions already running were downgraded when the record was written.
if crate::ui_interface::texture_render_health_failed() {
return;
}
log::error!(
"texture rendering failed for session {}, falling back to software rendering",
session_id
);
let backend = if kind == WATCHDOG_FAILED_RGBA {
"rgba"
} else {
"gpu"
};
LocalConfig::set_option(
hbb_common::config::keys::OPTION_TEXTURE_RENDER_HEALTH.to_owned(),
format!(
"failed-watchdog-{}@{}",
backend,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
),
);
// Mirror main_set_local_option: every render session must observe the
// new effective value, not only the failing one.
for session in sessions::get_sessions() {
if !(session.is_default() || session.is_view_camera()) {
continue;
}
// The soft path cannot rescue multi-display windows; don't claim it.
let fallback_rescues = session
.ui_handler
.session_handlers
.read()
.unwrap()
.get(&session_id)
.map(|h| h.displays.len() <= 1)
.unwrap_or(false);
if fallback_rescues {
session.push_event(
"use_texture_render",
&[("v", "N"), ("reason", "fallback")],
&[],
);
} else {
session.push_event("use_texture_render", &[("v", "N")], &[]);
}
session.use_texture_render_changed();
session.ui_handler.update_use_texture_render();
}
}
#[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

@@ -1222,11 +1222,32 @@ pub fn main_set_env(key: String, value: Option<String>) -> SyncReturn<()> {
pub fn main_set_local_option(key: String, value: String) { pub fn main_set_local_option(key: String, value: String) {
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER); let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
let is_texture_render_health_key = key.eq(config::keys::OPTION_TEXTURE_RENDER_HEALTH);
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER); let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
set_local_option(key, value.clone()); set_local_option(key, value.clone());
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_health_key && value.starts_with("failed") {
// Probe/raster-stall failures must also downgrade sessions that are
// already running (they snapshotted the old effective value, and the
// watchdog's own fallback no-ops once a record exists).
for session in sessions::get_sessions() {
if !is_render_target(&session) {
continue;
}
session.push_event("use_texture_render", &[("v", "N")], &[]);
session.use_texture_render_changed();
session.ui_handler.update_use_texture_render();
}
}
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 +2316,52 @@ 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 session_set_render_visible(session_id: SessionID, visible: bool) -> SyncReturn<()> {
SyncReturn(super::flutter::session_set_render_visible(
session_id, visible,
))
}
pub fn main_texture_render_probe_supported() -> SyncReturn<bool> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return SyncReturn(super::flutter::texture_render_probe_supported());
#[cfg(any(target_os = "android", target_os = "ios"))]
SyncReturn(false)
}
pub fn main_push_texture_probe_frame(ptr: usize) -> SyncReturn<()> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
super::flutter::push_texture_probe_frame(ptr);
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,15 @@ pub fn get_option<T: AsRef<str>>(key: T) -> String {
} }
} }
// Watchdog/probe breakage record: a failure forces texture render off on all
// desktop platforms (even an explicit "Y") — the live fallback relies on it;
// toggling the option or a passing probe clears it.
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn texture_render_health_failed() -> bool {
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 +192,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";
}
} }
} }
} }