mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-21 11:50:59 +03:00
fix: texture render lifetime protocol, watchdog fallback, startup probe
Root cause of #15848 (and the long-standing macOS #6296 / Linux #3343 class): raw native texture pointers are shared across the platform thread, the engine raster thread and the video thread, with teardown ordered by a 100 ms sleep - or, when moving a tab to a new window, by nothing at all. A lost race frees the texture while it is still in use: the raster thread parks on a destroyed lock (frozen/black view, a never-presented 'transparent' hole, every later session black) and the video thread hangs while holding session locks (app half-dead until restart, still reported as Responding). - unregister textures with compare-and-clear (new session_unregister_* FFI): a late clear can no longer wipe a new window's registration (#8016) and Rust never keeps pushing into a freed texture; the 100 ms sleeps are gone (the plugins now drain in-flight pushes and defer object deletion until the raster thread is done) - guard the async texture create path against destroy racing it (#13596) - per-display locks: the per-frame plugin call no longer holds session-level locks, so a stalled plugin or driver call cannot freeze every window's UI thread - adopt the frame size after 30 consecutive mismatches instead of dropping frames forever (silent black screen on a live connection) - watchdog: frames pushed but never consumed by the engine fall the session back to software rendering live, record texture-render-health, and flip the effective default off; toggling the option clears the record and re-arms validation - Dart raster-stall monitor records a hung raster thread for the next launch (rendering cannot be rescued in-process in that state) - startup probe: render one frame through a 1x1 texture in the main window each launch; failure disables texture rendering before the first session goes black, a pass self-heals a stale failure record Platform defaults are unchanged (macOS off, Win10+ on, Linux on). Requires flutter_texture_rgba_renderer ad4c37e and flutter_gpu_texture_renderer 767bb9f (pinned in pubspec). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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}) {
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
||||||
}
|
}
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
|
_RasterStallMonitor.start();
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
_ffi.canvasModel.activateLocalCursor();
|
_ffi.canvasModel.activateLocalCursor();
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
@@ -1402,3 +1403,45 @@ class CursorPaint extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detects a hung raster thread: the UI keeps scheduling frames but the
|
||||||
|
/// engine never reports completed frame timings. Rendering cannot be rescued
|
||||||
|
/// in-process (software rendering needs the same raster thread), so this only
|
||||||
|
/// records the breakage — the next launch then defaults texture rendering to
|
||||||
|
/// off and the startup probe re-validates the environment.
|
||||||
|
class _RasterStallMonitor {
|
||||||
|
static bool _started = false;
|
||||||
|
static bool _reported = false;
|
||||||
|
static DateTime? _lastTimings;
|
||||||
|
static DateTime? _scheduledSince;
|
||||||
|
|
||||||
|
static void start() {
|
||||||
|
if (_started || isWeb) return;
|
||||||
|
_started = true;
|
||||||
|
SchedulerBinding.instance.addTimingsCallback((_) {
|
||||||
|
_lastTimings = DateTime.now();
|
||||||
|
});
|
||||||
|
Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
|
if (_reported) return;
|
||||||
|
// A minimized window legitimately stops producing frame timings.
|
||||||
|
if (stateGlobal.isMinimized ||
|
||||||
|
!SchedulerBinding.instance.hasScheduledFrame) {
|
||||||
|
_scheduledSince = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final now = DateTime.now();
|
||||||
|
_scheduledSince ??= now;
|
||||||
|
final lastTimings = _lastTimings;
|
||||||
|
final stalled =
|
||||||
|
now.difference(_scheduledSince!) > const Duration(seconds: 10) &&
|
||||||
|
(lastTimings == null || lastTimings.isBefore(_scheduledSince!));
|
||||||
|
if (stalled) {
|
||||||
|
_reported = true;
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionTextureRenderHealth, value: 'failed-raster-stall');
|
||||||
|
debugPrint(
|
||||||
|
'raster thread stall detected, texture rendering disabled for next launch');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
131
flutter/lib/desktop/widgets/texture_render_probe.dart
Normal file
131
flutter/lib/desktop/widgets/texture_render_probe.dart
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../consts.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
|
||||||
|
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
||||||
|
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
||||||
|
|
||||||
|
/// Startup probe: renders one frame through a 1x1 external texture and
|
||||||
|
/// verifies the engine consumed it. The verdict is recorded in
|
||||||
|
/// `texture-render-health` — a failure turns texture rendering off before the
|
||||||
|
/// first session goes black, a pass clears a stale failure (self-healing
|
||||||
|
/// after a driver/OS fix). Mounted once, in the main window.
|
||||||
|
class TextureRenderProbe extends StatefulWidget {
|
||||||
|
const TextureRenderProbe({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TextureRenderProbe> createState() => _TextureRenderProbeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||||
|
static bool _ranThisLaunch = false;
|
||||||
|
final _renderer = TextureRgbaRenderer();
|
||||||
|
int _textureId = -1;
|
||||||
|
int _textureKey = -1;
|
||||||
|
int _ptr = 0;
|
||||||
|
Timer? _timer;
|
||||||
|
int _ticks = 0;
|
||||||
|
bool _sawTimings = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (_ranThisLaunch || isWeb || !isDesktop) return;
|
||||||
|
_ranThisLaunch = true;
|
||||||
|
// Only probe after the window has really rendered a frame: a hidden
|
||||||
|
// window (silent/tray start) must not record a false failure.
|
||||||
|
SchedulerBinding.instance.addTimingsCallback(_onTimings);
|
||||||
|
Future.delayed(const Duration(seconds: 5), () {
|
||||||
|
if (!_sawTimings) {
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
|
_finish(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTimings(List<FrameTiming> timings) {
|
||||||
|
if (_sawTimings) return;
|
||||||
|
_sawTimings = true;
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
|
_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _start() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
_textureKey = bind.getNextTextureKey();
|
||||||
|
final id = await _renderer.createTexture(_textureKey);
|
||||||
|
if (!mounted || id == -1) {
|
||||||
|
_finish(!mounted ? null : false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ptr = await _renderer.getTexturePtr(_textureKey);
|
||||||
|
if (!mounted || _ptr <= 0) {
|
||||||
|
_finish(!mounted ? null : false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _textureId = id);
|
||||||
|
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||||
|
_ticks += 1;
|
||||||
|
bind.mainPushTextureProbeFrame(ptr: _ptr);
|
||||||
|
if (bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0) {
|
||||||
|
_finish(true);
|
||||||
|
} else if (_ticks >= 10) {
|
||||||
|
_finish(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _finish(bool? ok) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
if (ok != null) {
|
||||||
|
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
|
||||||
|
if (ok) {
|
||||||
|
if (old != 'ok') {
|
||||||
|
bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
|
||||||
|
}
|
||||||
|
} else if (!old.startsWith('failed')) {
|
||||||
|
debugPrint('texture render probe failed, disabling texture rendering');
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionTextureRenderHealth, value: 'failed-probe');
|
||||||
|
showToast(translate('texture-render-fallback-tip'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_textureKey != -1) {
|
||||||
|
_renderer.closeTexture(_textureKey);
|
||||||
|
_textureKey = -1;
|
||||||
|
}
|
||||||
|
_ptr = 0;
|
||||||
|
if (mounted && _textureId != -1) {
|
||||||
|
setState(() => _textureId = -1);
|
||||||
|
} else {
|
||||||
|
_textureId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
if (_textureKey != -1) {
|
||||||
|
_renderer.closeTexture(_textureKey);
|
||||||
|
_textureKey = -1;
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_textureId == -1) return const SizedBox.shrink();
|
||||||
|
// Must actually composite for the engine to sample the texture; 1x1 in a
|
||||||
|
// corner is imperceptible.
|
||||||
|
return IgnorePointer(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 1, height: 1, child: Texture(textureId: _textureId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ class _PixelbufferTexture {
|
|||||||
int _display = 0;
|
int _display = 0;
|
||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
|
bool _closed = false;
|
||||||
|
int _ptr = 0;
|
||||||
int? _id;
|
int? _id;
|
||||||
|
|
||||||
final textureRenderer = TextureRgbaRenderer();
|
final textureRenderer = TextureRgbaRenderer();
|
||||||
@@ -27,11 +29,22 @@ class _PixelbufferTexture {
|
|||||||
_textureKey = bind.getNextTextureKey();
|
_textureKey = bind.getNextTextureKey();
|
||||||
_sessionId = sessionId;
|
_sessionId = sessionId;
|
||||||
|
|
||||||
textureRenderer.createTexture(_textureKey).then((id) async {
|
final textureKey = _textureKey;
|
||||||
|
textureRenderer.createTexture(textureKey).then((id) async {
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
|
if (_closed) {
|
||||||
|
// Destroyed while creation was still in flight (rapid
|
||||||
|
// connect/disconnect); nobody else will close this texture.
|
||||||
|
await textureRenderer.closeTexture(textureKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
||||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
final ptr = await textureRenderer.getTexturePtr(textureKey);
|
||||||
|
if (_closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ptr = ptr;
|
||||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
||||||
@@ -39,13 +52,17 @@ class _PixelbufferTexture {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool unregisterTexture, FFI ffi) async {
|
destroy(bool closeSession, FFI ffi) async {
|
||||||
|
_closed = true;
|
||||||
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (unregisterTexture) {
|
if (_ptr != 0) {
|
||||||
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
// Compare-and-clear: clears only if Rust still holds this pointer, so
|
||||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
// a registration a new window has already made stays intact (#8016).
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
// Returning from this synchronous call also guarantees no push
|
||||||
|
// through the old pointer is still in flight.
|
||||||
|
platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr);
|
||||||
|
_ptr = 0;
|
||||||
}
|
}
|
||||||
await textureRenderer.closeTexture(_textureKey);
|
await textureRenderer.closeTexture(_textureKey);
|
||||||
_textureKey = -1;
|
_textureKey = -1;
|
||||||
@@ -61,6 +78,7 @@ class _GpuTexture {
|
|||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
final support = bind.mainHasGpuTextureRender();
|
final support = bind.mainHasGpuTextureRender();
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
|
bool _closed = false;
|
||||||
int _display = 0;
|
int _display = 0;
|
||||||
int? _id;
|
int? _id;
|
||||||
int? _output;
|
int? _output;
|
||||||
@@ -79,9 +97,18 @@ class _GpuTexture {
|
|||||||
gpuTextureRenderer.registerTexture().then((id) async {
|
gpuTextureRenderer.registerTexture().then((id) async {
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
|
if (_closed) {
|
||||||
|
// Destroyed while creation was still in flight (rapid
|
||||||
|
// connect/disconnect); nobody else will unregister this texture.
|
||||||
|
await gpuTextureRenderer.unregisterTexture(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
_textureId = id;
|
_textureId = id;
|
||||||
ffi.textureModel.setGpuTextureId(display: d, id: id);
|
ffi.textureModel.setGpuTextureId(display: d, id: id);
|
||||||
final output = await gpuTextureRenderer.output(id);
|
final output = await gpuTextureRenderer.output(id);
|
||||||
|
if (_closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_output = output;
|
_output = output;
|
||||||
if (output != null) {
|
if (output != null) {
|
||||||
platformFFI.registerGpuTexture(sessionId, d, output);
|
platformFFI.registerGpuTexture(sessionId, d, output);
|
||||||
@@ -95,20 +122,22 @@ class _GpuTexture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool unregisterTexture, FFI ffi) async {
|
destroy(bool closeSession, FFI ffi) async {
|
||||||
// must stop texture render, render unregistered texture cause crash
|
// must stop texture render, render unregistered texture cause crash
|
||||||
|
_closed = true;
|
||||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (unregisterTexture) {
|
final output = _output;
|
||||||
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
if (output != null) {
|
||||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
// Compare-and-clear, see _PixelbufferTexture.destroy.
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
platformFFI.unregisterGpuTexture(_sessionId!, _display, output);
|
||||||
|
_output = null;
|
||||||
}
|
}
|
||||||
await gpuTextureRenderer.unregisterTexture(_textureId);
|
await gpuTextureRenderer.unregisterTexture(_textureId);
|
||||||
_textureId = -1;
|
_textureId = -1;
|
||||||
_destroying = false;
|
_destroying = false;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$_output");
|
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$output");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -1450,6 +1450,24 @@ class RustdeskImpl {
|
|||||||
required int ptr,
|
required int ptr,
|
||||||
dynamic hint}) {}
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
void sessionUnregisterPixelbufferTexture(
|
||||||
|
{required UuidValue sessionId,
|
||||||
|
required int display,
|
||||||
|
required int ptr,
|
||||||
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
void sessionUnregisterGpuTexture(
|
||||||
|
{required UuidValue sessionId,
|
||||||
|
required int display,
|
||||||
|
required int ptr,
|
||||||
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
void mainPushTextureProbeFrame({required int ptr, dynamic hint}) {}
|
||||||
|
|
||||||
|
int mainGetTextureProbeConsumed({required int ptr, dynamic hint}) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> queryOnlines({required List<String> ids, dynamic hint}) {
|
Future<void> queryOnlines({required List<String> ids, dynamic hint}) {
|
||||||
return Future(() =>
|
return Future(() =>
|
||||||
js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)]));
|
js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)]));
|
||||||
|
|||||||
@@ -538,8 +538,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
|
ref: "767bb9fe9dcd2c23e2664114bf33760842e872e7"
|
||||||
resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
|
resolved-ref: "767bb9fe9dcd2c23e2664114bf33760842e872e7"
|
||||||
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -1298,8 +1298,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
ref: "ad4c37e414ee40853c0e9503b2da6486d4c1768d"
|
||||||
resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
resolved-ref: "ad4c37e414ee40853c0e9503b2da6486d4c1768d"
|
||||||
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
|
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.16"
|
version: "0.0.16"
|
||||||
|
|||||||
@@ -88,13 +88,13 @@ dependencies:
|
|||||||
texture_rgba_renderer:
|
texture_rgba_renderer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
|
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
|
||||||
ref: 42797e0f03141dc2b585f76c64a13974508058b4
|
ref: ad4c37e414ee40853c0e9503b2da6486d4c1768d
|
||||||
percent_indicator: ^4.2.2
|
percent_indicator: ^4.2.2
|
||||||
dropdown_button2: ^2.0.0
|
dropdown_button2: ^2.0.0
|
||||||
flutter_gpu_texture_renderer:
|
flutter_gpu_texture_renderer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
||||||
ref: 08a471bb8ceccdd50483c81cdfa8b81b07b14b87
|
ref: 767bb9fe9dcd2c23e2664114bf33760842e872e7
|
||||||
uuid: ^3.0.7
|
uuid: ^3.0.7
|
||||||
auto_size_text_field: ^2.2.1
|
auto_size_text_field: ^2.2.1
|
||||||
flex_color_picker: ^3.3.0
|
flex_color_picker: ^3.3.0
|
||||||
|
|||||||
Submodule libs/hbb_common updated: 69cea8dafe...d19ce39e51
453
src/flutter.rs
453
src/flutter.rs
@@ -24,8 +24,9 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
Arc, RwLock,
|
Arc, Mutex, RwLock,
|
||||||
},
|
},
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed)
|
/// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed)
|
||||||
@@ -269,26 +270,96 @@ pub type FlutterGpuTextureRendererPluginCApiSetTexture =
|
|||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64;
|
pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64;
|
||||||
|
|
||||||
|
pub type FlutterRgbaRendererPluginGetConsumed = unsafe extern "C" fn(texture_rgba: *mut c_void) -> u64;
|
||||||
|
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
pub type FlutterGpuTextureRendererPluginCApiGetConsumed =
|
||||||
|
unsafe extern "C" fn(output: *mut c_void) -> u64;
|
||||||
|
|
||||||
pub(super) type TextureRgbaPtr = usize;
|
pub(super) type TextureRgbaPtr = usize;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
struct DisplaySessionInfo {
|
struct DisplaySessionInfo {
|
||||||
// TextureRgba pointer in flutter native.
|
// TextureRgba pointer in flutter native.
|
||||||
texture_rgba_ptr: TextureRgbaPtr,
|
texture_rgba_ptr: TextureRgbaPtr,
|
||||||
size: (usize, usize),
|
size: (usize, usize),
|
||||||
|
size_mismatch_count: u32,
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
gpu_output_ptr: usize,
|
gpu_output_ptr: usize,
|
||||||
notify_render_type: Option<RenderType>,
|
notify_render_type: Option<RenderType>,
|
||||||
|
// Watchdog: frames pushed to a texture the engine never consumes mean
|
||||||
|
// texture rendering is broken on this machine (black view while the
|
||||||
|
// connection works). Armed until the first consumption is observed, so a
|
||||||
|
// later minimized window cannot false-positive.
|
||||||
|
pushed_count: u64,
|
||||||
|
watchdog_pushed_at_sample: u64,
|
||||||
|
watchdog_last_sample: Option<Instant>,
|
||||||
|
watchdog_stall_windows: u32,
|
||||||
|
watchdog_armed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplaySessionInfo {
|
||||||
|
fn reset_watchdog(&mut self) {
|
||||||
|
self.pushed_count = 0;
|
||||||
|
self.watchdog_pushed_at_sample = 0;
|
||||||
|
self.watchdog_last_sample = None;
|
||||||
|
self.watchdog_stall_windows = 0;
|
||||||
|
self.watchdog_armed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true when texture rendering is deemed broken: >=3 sampled
|
||||||
|
// seconds in which frames kept being pushed but none was ever consumed.
|
||||||
|
fn check_watchdog(&mut self, consumed: u64) -> bool {
|
||||||
|
if consumed > 0 {
|
||||||
|
self.watchdog_armed = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let pushed_in_window = self.pushed_count - self.watchdog_pushed_at_sample;
|
||||||
|
self.watchdog_pushed_at_sample = self.pushed_count;
|
||||||
|
if pushed_in_window >= 10 {
|
||||||
|
self.watchdog_stall_windows += 1;
|
||||||
|
}
|
||||||
|
if self.watchdog_stall_windows >= 3 {
|
||||||
|
self.watchdog_armed = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn watchdog_sample_due(&mut self) -> bool {
|
||||||
|
if !self.watchdog_armed {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let now = Instant::now();
|
||||||
|
match self.watchdog_last_sample {
|
||||||
|
Some(t) if now.duration_since(t) < Duration::from_secs(1) => false,
|
||||||
|
_ => {
|
||||||
|
self.watchdog_last_sample = Some(now);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video Texture Renderer in Flutter
|
// Video Texture Renderer in Flutter
|
||||||
|
// Each display entry has its own mutex so the per-frame plugin call only ever
|
||||||
|
// holds that display's lock; a stalled plugin/driver call must not back up
|
||||||
|
// the session-level locks (which would freeze every window's UI thread).
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct VideoRenderer {
|
struct VideoRenderer {
|
||||||
is_support_multi_ui_session: bool,
|
is_support_multi_ui_session: bool,
|
||||||
map_display_sessions: Arc<RwLock<HashMap<usize, DisplaySessionInfo>>>,
|
map_display_sessions: Arc<RwLock<HashMap<usize, Arc<Mutex<DisplaySessionInfo>>>>>,
|
||||||
|
// Latched by the watchdog; consumed once by the pushing caller to trigger
|
||||||
|
// the software-render fallback for this ui session.
|
||||||
|
texture_render_failed: Arc<AtomicBool>,
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
|
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
get_consumed_func: Option<Symbol<'static, FlutterRgbaRendererPluginGetConsumed>>,
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>,
|
on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>,
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
get_gpu_consumed_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiGetConsumed>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for VideoRenderer {
|
impl Default for VideoRenderer {
|
||||||
@@ -312,6 +383,17 @@ impl Default for VideoRenderer {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Absent in older plugin builds; the watchdog just stays disabled.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
let get_consumed_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN {
|
||||||
|
Ok(lib) => unsafe {
|
||||||
|
lib.symbol::<FlutterRgbaRendererPluginGetConsumed>(
|
||||||
|
"FlutterRgbaRendererPluginGetConsumed",
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
||||||
Ok(lib) => {
|
Ok(lib) => {
|
||||||
@@ -333,14 +415,29 @@ impl Default for VideoRenderer {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
let get_gpu_consumed_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
||||||
|
Ok(lib) => unsafe {
|
||||||
|
lib.symbol::<FlutterGpuTextureRendererPluginCApiGetConsumed>(
|
||||||
|
"FlutterGpuTextureRendererPluginCApiGetConsumed",
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
map_display_sessions: Default::default(),
|
map_display_sessions: Default::default(),
|
||||||
is_support_multi_ui_session: false,
|
is_support_multi_ui_session: false,
|
||||||
|
texture_render_failed: Default::default(),
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
on_rgba_func,
|
on_rgba_func,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
get_consumed_func,
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
on_texture_func,
|
on_texture_func,
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
get_gpu_consumed_func,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,19 +446,18 @@ impl VideoRenderer {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn set_size(&mut self, display: usize, width: usize, height: usize) {
|
fn set_size(&mut self, display: usize, width: usize, height: usize) {
|
||||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info) = sessions_lock.get(&display) {
|
||||||
|
let mut info = info.lock().unwrap();
|
||||||
info.size = (width, height);
|
info.size = (width, height);
|
||||||
|
info.size_mismatch_count = 0;
|
||||||
info.notify_render_type = None;
|
info.notify_render_type = None;
|
||||||
} else {
|
} else {
|
||||||
sessions_lock.insert(
|
sessions_lock.insert(
|
||||||
display,
|
display,
|
||||||
DisplaySessionInfo {
|
Arc::new(Mutex::new(DisplaySessionInfo {
|
||||||
texture_rgba_ptr: usize::default(),
|
|
||||||
size: (width, height),
|
size: (width, height),
|
||||||
#[cfg(feature = "vram")]
|
..Default::default()
|
||||||
gpu_output_ptr: usize::default(),
|
})),
|
||||||
notify_render_type: None,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +465,8 @@ impl VideoRenderer {
|
|||||||
fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
||||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
if ptr == 0 {
|
if ptr == 0 {
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
if info.texture_rgba_ptr != usize::default() {
|
if info.texture_rgba_ptr != usize::default() {
|
||||||
info.texture_rgba_ptr = usize::default();
|
info.texture_rgba_ptr = usize::default();
|
||||||
}
|
}
|
||||||
@@ -377,10 +474,12 @@ impl VideoRenderer {
|
|||||||
if info.gpu_output_ptr != usize::default() {
|
if info.gpu_output_ptr != usize::default() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
drop(info);
|
||||||
|
sessions_lock.remove(&display);
|
||||||
}
|
}
|
||||||
sessions_lock.remove(&display);
|
|
||||||
} else {
|
} else {
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info) = sessions_lock.get(&display) {
|
||||||
|
let mut info = info.lock().unwrap();
|
||||||
if info.texture_rgba_ptr != usize::default()
|
if info.texture_rgba_ptr != usize::default()
|
||||||
&& info.texture_rgba_ptr != ptr as TextureRgbaPtr
|
&& info.texture_rgba_ptr != ptr as TextureRgbaPtr
|
||||||
{
|
{
|
||||||
@@ -392,38 +491,61 @@ impl VideoRenderer {
|
|||||||
}
|
}
|
||||||
info.texture_rgba_ptr = ptr as _;
|
info.texture_rgba_ptr = ptr as _;
|
||||||
info.notify_render_type = None;
|
info.notify_render_type = None;
|
||||||
|
info.reset_watchdog();
|
||||||
} else {
|
} else {
|
||||||
if ptr != 0 {
|
let mut info = DisplaySessionInfo {
|
||||||
sessions_lock.insert(
|
texture_rgba_ptr: ptr as _,
|
||||||
display,
|
..Default::default()
|
||||||
DisplaySessionInfo {
|
};
|
||||||
texture_rgba_ptr: ptr as _,
|
info.reset_watchdog();
|
||||||
size: (0, 0),
|
sessions_lock.insert(display, Arc::new(Mutex::new(info)));
|
||||||
#[cfg(feature = "vram")]
|
|
||||||
gpu_output_ptr: usize::default(),
|
|
||||||
notify_render_type: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear the pointer only if it still holds `ptr`. An unconditional clear
|
||||||
|
// could wipe out the registration a new window just made while a tab
|
||||||
|
// moves between windows (#8016); skipping the clear (the old behavior on
|
||||||
|
// move) left Rust pushing into a freed native texture. Waiting on the
|
||||||
|
// display mutex also drains an in-flight push through the old pointer.
|
||||||
|
fn unregister_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
||||||
|
if ptr == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
|
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
|
if info.texture_rgba_ptr != ptr as TextureRgbaPtr {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info.texture_rgba_ptr = usize::default();
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
if info.gpu_output_ptr != usize::default() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(info);
|
||||||
|
sessions_lock.remove(&display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub fn register_gpu_output(&self, display: usize, ptr: usize) {
|
pub fn register_gpu_output(&self, display: usize, ptr: usize) {
|
||||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
if ptr == 0 {
|
if ptr == 0 {
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
if info.gpu_output_ptr != usize::default() {
|
if info.gpu_output_ptr != usize::default() {
|
||||||
info.gpu_output_ptr = usize::default();
|
info.gpu_output_ptr = usize::default();
|
||||||
}
|
}
|
||||||
if info.texture_rgba_ptr != usize::default() {
|
if info.texture_rgba_ptr != usize::default() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
drop(info);
|
||||||
|
sessions_lock.remove(&display);
|
||||||
}
|
}
|
||||||
sessions_lock.remove(&display);
|
|
||||||
} else {
|
} else {
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info) = sessions_lock.get(&display) {
|
||||||
|
let mut info = info.lock().unwrap();
|
||||||
if info.gpu_output_ptr != usize::default() && info.gpu_output_ptr != ptr {
|
if info.gpu_output_ptr != usize::default() && info.gpu_output_ptr != ptr {
|
||||||
log::error!(
|
log::error!(
|
||||||
"gpu_output_ptr is not null and not equal to ptr, relace {} to {}",
|
"gpu_output_ptr is not null and not equal to ptr, relace {} to {}",
|
||||||
@@ -433,50 +555,90 @@ impl VideoRenderer {
|
|||||||
}
|
}
|
||||||
info.gpu_output_ptr = ptr as _;
|
info.gpu_output_ptr = ptr as _;
|
||||||
info.notify_render_type = None;
|
info.notify_render_type = None;
|
||||||
|
info.reset_watchdog();
|
||||||
} else {
|
} else {
|
||||||
if ptr != usize::default() {
|
let mut info = DisplaySessionInfo {
|
||||||
sessions_lock.insert(
|
gpu_output_ptr: ptr,
|
||||||
display,
|
..Default::default()
|
||||||
DisplaySessionInfo {
|
};
|
||||||
texture_rgba_ptr: usize::default(),
|
info.reset_watchdog();
|
||||||
size: (0, 0),
|
sessions_lock.insert(display, Arc::new(Mutex::new(info)));
|
||||||
gpu_output_ptr: ptr,
|
|
||||||
notify_render_type: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See unregister_pixelbuffer_texture for why this is compare-and-clear.
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
pub fn unregister_gpu_output(&self, display: usize, ptr: usize) {
|
||||||
|
if ptr == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
|
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
|
if info.gpu_output_ptr != ptr {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info.gpu_output_ptr = usize::default();
|
||||||
|
if info.texture_rgba_ptr != usize::default() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(info);
|
||||||
|
sessions_lock.remove(&display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn display_session_info(&self, display: usize) -> Option<Arc<Mutex<DisplaySessionInfo>>> {
|
||||||
|
let read_lock = self.map_display_sessions.read().unwrap();
|
||||||
|
if !self.is_support_multi_ui_session {
|
||||||
|
read_lock.values().next().cloned()
|
||||||
|
} else {
|
||||||
|
read_lock.get(&display).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool {
|
pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool {
|
||||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
let Some(info_arc) = self.display_session_info(display) else {
|
||||||
let opt_info = if !self.is_support_multi_ui_session {
|
|
||||||
write_lock.values_mut().next()
|
|
||||||
} else {
|
|
||||||
write_lock.get_mut(&display)
|
|
||||||
};
|
|
||||||
let Some(info) = opt_info else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
if info.texture_rgba_ptr == usize::default() {
|
if info.texture_rgba_ptr == usize::default() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.size.0 != rgba.w || info.size.1 != rgba.h {
|
if info.size.0 != rgba.w || info.size.1 != rgba.h {
|
||||||
log::error!(
|
|
||||||
"width/height mismatch: ({},{}) != ({},{})",
|
|
||||||
info.size.0,
|
|
||||||
info.size.1,
|
|
||||||
rgba.w,
|
|
||||||
rgba.h
|
|
||||||
);
|
|
||||||
// Peer info's handling is async and may be late than video frame's handling
|
// Peer info's handling is async and may be late than video frame's handling
|
||||||
// Allow peer info not set, but not allow wrong width/height for correct local cursor position
|
// Allow peer info not set, but not allow wrong width/height for correct local cursor position
|
||||||
if info.size != (0, 0) {
|
if info.size != (0, 0) {
|
||||||
return false;
|
info.size_mismatch_count += 1;
|
||||||
|
if info.size_mismatch_count == 1 {
|
||||||
|
log::error!(
|
||||||
|
"width/height mismatch: ({},{}) != ({},{})",
|
||||||
|
info.size.0,
|
||||||
|
info.size.1,
|
||||||
|
rgba.w,
|
||||||
|
rgba.h
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// The stream is the source of truth; if the sizes still
|
||||||
|
// disagree after this many frames the peer info is not
|
||||||
|
// coming, and dropping forever leaves a live session black.
|
||||||
|
if info.size_mismatch_count < 30 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log::warn!(
|
||||||
|
"adopting frame size ({},{}) after {} mismatched frames",
|
||||||
|
rgba.w,
|
||||||
|
rgba.h,
|
||||||
|
info.size_mismatch_count
|
||||||
|
);
|
||||||
|
info.size = (rgba.w, rgba.h);
|
||||||
|
info.size_mismatch_count = 0;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
info.size_mismatch_count = 0;
|
||||||
}
|
}
|
||||||
if let Some(func) = &self.on_rgba_func {
|
if let Some(func) = &self.on_rgba_func {
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -490,6 +652,20 @@ impl VideoRenderer {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
info.pushed_count += 1;
|
||||||
|
if let Some(get_consumed) = &self.get_consumed_func {
|
||||||
|
if info.watchdog_sample_due() {
|
||||||
|
let consumed = unsafe { get_consumed(info.texture_rgba_ptr as _) };
|
||||||
|
if info.check_watchdog(consumed) {
|
||||||
|
log::error!(
|
||||||
|
"texture rendering broken: {} frames pushed to display {}, none consumed",
|
||||||
|
info.pushed_count,
|
||||||
|
display
|
||||||
|
);
|
||||||
|
self.texture_render_failed.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if info.notify_render_type != Some(RenderType::PixelBuffer) {
|
if info.notify_render_type != Some(RenderType::PixelBuffer) {
|
||||||
info.notify_render_type = Some(RenderType::PixelBuffer);
|
info.notify_render_type = Some(RenderType::PixelBuffer);
|
||||||
true
|
true
|
||||||
@@ -500,21 +676,30 @@ impl VideoRenderer {
|
|||||||
|
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub fn on_texture(&self, display: usize, texture: *mut c_void) -> bool {
|
pub fn on_texture(&self, display: usize, texture: *mut c_void) -> bool {
|
||||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
let Some(info_arc) = self.display_session_info(display) else {
|
||||||
let opt_info = if !self.is_support_multi_ui_session {
|
|
||||||
write_lock.values_mut().next()
|
|
||||||
} else {
|
|
||||||
write_lock.get_mut(&display)
|
|
||||||
};
|
|
||||||
let Some(info) = opt_info else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
if info.gpu_output_ptr == usize::default() {
|
if info.gpu_output_ptr == usize::default() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(func) = &self.on_texture_func {
|
if let Some(func) = &self.on_texture_func {
|
||||||
unsafe { func(info.gpu_output_ptr as _, texture) };
|
unsafe { func(info.gpu_output_ptr as _, texture) };
|
||||||
}
|
}
|
||||||
|
info.pushed_count += 1;
|
||||||
|
if let Some(get_consumed) = &self.get_gpu_consumed_func {
|
||||||
|
if info.watchdog_sample_due() {
|
||||||
|
let consumed = unsafe { get_consumed(info.gpu_output_ptr as _) };
|
||||||
|
if info.check_watchdog(consumed) {
|
||||||
|
log::error!(
|
||||||
|
"gpu texture rendering broken: {} frames pushed to display {}, none consumed",
|
||||||
|
info.pushed_count,
|
||||||
|
display
|
||||||
|
);
|
||||||
|
self.texture_render_failed.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if info.notify_render_type != Some(RenderType::Texture) {
|
if info.notify_render_type != Some(RenderType::Texture) {
|
||||||
info.notify_render_type = Some(RenderType::Texture);
|
info.notify_render_type = Some(RenderType::Texture);
|
||||||
true
|
true
|
||||||
@@ -524,11 +709,10 @@ impl VideoRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_all_display_render_type(&self) {
|
pub fn reset_all_display_render_type(&self) {
|
||||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
let read_lock = self.map_display_sessions.read().unwrap();
|
||||||
write_lock
|
for info in read_lock.values() {
|
||||||
.values_mut()
|
info.lock().unwrap().notify_render_type = None;
|
||||||
.map(|v| v.notify_render_type = None)
|
}
|
||||||
.count();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,9 +845,24 @@ impl FlutterHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_use_texture_render(&self) {
|
pub fn update_use_texture_render(&self) {
|
||||||
self.use_texture_render
|
let v = crate::ui_interface::use_texture_render();
|
||||||
.store(crate::ui_interface::use_texture_render(), Ordering::Relaxed);
|
self.use_texture_render.store(v, Ordering::Relaxed);
|
||||||
self.display_rgbas.write().unwrap().clear();
|
self.display_rgbas.write().unwrap().clear();
|
||||||
|
if v {
|
||||||
|
// Texture render was (re-)enabled; validate it afresh so a still
|
||||||
|
// broken environment fails over again instead of staying black.
|
||||||
|
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
||||||
|
for info in session
|
||||||
|
.renderer
|
||||||
|
.map_display_sessions
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.values()
|
||||||
|
{
|
||||||
|
info.lock().unwrap().reset_watchdog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,12 +1086,13 @@ impl InvokeUiSession for FlutterHandler {
|
|||||||
if !self.use_texture_render.load(Ordering::Relaxed) {
|
if !self.use_texture_render.load(Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
for (session_id, session) in self.session_handlers.read().unwrap().iter() {
|
||||||
if session.renderer.on_texture(display, texture) {
|
if session.renderer.on_texture(display, texture) {
|
||||||
if let Some(stream) = &session.event_stream {
|
if let Some(stream) = &session.event_stream {
|
||||||
stream.add(EventToUI::Texture(display, true));
|
stream.add(EventToUI::Texture(display, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Self::check_texture_render_failed(session_id, session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1262,16 +1462,31 @@ impl FlutterHandler {
|
|||||||
display: usize,
|
display: usize,
|
||||||
rgba: &mut scrap::ImageRgb,
|
rgba: &mut scrap::ImageRgb,
|
||||||
) {
|
) {
|
||||||
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
for (session_id, session) in self.session_handlers.read().unwrap().iter() {
|
||||||
if use_texture_render || session.displays.len() > 1 {
|
if use_texture_render || session.displays.len() > 1 {
|
||||||
if session.renderer.on_rgba(display, rgba) {
|
if session.renderer.on_rgba(display, rgba) {
|
||||||
if let Some(stream) = &session.event_stream {
|
if let Some(stream) = &session.event_stream {
|
||||||
stream.add(EventToUI::Texture(display, false));
|
stream.add(EventToUI::Texture(display, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Self::check_texture_render_failed(session_id, session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consume the watchdog latch outside the per-frame hot path work; the
|
||||||
|
// actual fallback (config write, decoder reset) runs on its own thread.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
fn check_texture_render_failed(session_id: &SessionID, session: &SessionHandler) {
|
||||||
|
if session
|
||||||
|
.renderer
|
||||||
|
.texture_render_failed
|
||||||
|
.swap(false, Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
let session_id = session_id.clone();
|
||||||
|
std::thread::spawn(move || on_texture_render_failed(session_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is only used for the default connection session.
|
// This function is only used for the default connection session.
|
||||||
@@ -1795,6 +2010,106 @@ pub fn session_register_gpu_texture(_session_id: SessionID, _display: usize, _ou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn session_unregister_pixelbuffer_texture(session_id: SessionID, display: usize, ptr: usize) {
|
||||||
|
for s in sessions::get_sessions() {
|
||||||
|
if let Some(h) = s
|
||||||
|
.ui_handler
|
||||||
|
.session_handlers
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(&session_id)
|
||||||
|
{
|
||||||
|
h.renderer.unregister_pixelbuffer_texture(display, ptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn session_unregister_gpu_texture(_session_id: SessionID, _display: usize, _output_ptr: usize) {
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
for s in sessions::get_sessions() {
|
||||||
|
if let Some(h) = s
|
||||||
|
.ui_handler
|
||||||
|
.session_handlers
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(&_session_id)
|
||||||
|
{
|
||||||
|
h.renderer.unregister_gpu_output(_display, _output_ptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Startup-probe plumbing: the main window pushes one frame into a throwaway
|
||||||
|
// 1x1 texture and polls whether the engine consumed it, validating the
|
||||||
|
// texture pipeline before any session depends on it.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn push_texture_probe_frame(ptr: usize) {
|
||||||
|
if ptr == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(lib) = &*TEXTURE_RGBA_RENDERER_PLUGIN else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(func) = (unsafe {
|
||||||
|
lib.symbol::<FlutterRgbaRendererPluginOnRgba>("FlutterRgbaRendererPluginOnRgba")
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let frame: [u8; 4] = [255, 255, 255, 255];
|
||||||
|
unsafe { func(ptr as _, frame.as_ptr(), 4, 1, 1, 1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn get_texture_probe_consumed(ptr: usize) -> u64 {
|
||||||
|
if ptr == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let Ok(lib) = &*TEXTURE_RGBA_RENDERER_PLUGIN else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let Ok(func) = (unsafe {
|
||||||
|
lib.symbol::<FlutterRgbaRendererPluginGetConsumed>("FlutterRgbaRendererPluginGetConsumed")
|
||||||
|
}) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
unsafe { func(ptr as _) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frames are being pushed but the engine never consumes them: texture
|
||||||
|
// rendering does not work in this environment (GPU/driver/engine breakage).
|
||||||
|
// Fall back to software rendering for the live session and record it so the
|
||||||
|
// default flips to opt-in; the startup probe re-validates on later launches.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
fn on_texture_render_failed(session_id: SessionID) {
|
||||||
|
log::error!(
|
||||||
|
"texture rendering failed for session {}, falling back to software rendering",
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
LocalConfig::set_option(
|
||||||
|
hbb_common::config::keys::OPTION_TEXTURE_RENDER_HEALTH.to_owned(),
|
||||||
|
format!(
|
||||||
|
"failed-watchdog@{}",
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||||
|
session.push_event(
|
||||||
|
"use_texture_render",
|
||||||
|
&[("v", "N"), ("reason", "fallback")],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
session.use_texture_render_changed();
|
||||||
|
session.ui_handler.update_use_texture_render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(not(feature = "vram"))]
|
#[cfg(not(feature = "vram"))]
|
||||||
pub fn get_adapter_luid() -> Option<i64> {
|
pub fn get_adapter_luid() -> Option<i64> {
|
||||||
|
|||||||
@@ -1227,6 +1227,13 @@ pub fn main_set_local_option(key: String, value: String) {
|
|||||||
let is_render_target =
|
let is_render_target =
|
||||||
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
|
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
|
||||||
if is_texture_render_key {
|
if is_texture_render_key {
|
||||||
|
// An explicit user toggle gives texture rendering a fresh chance; a
|
||||||
|
// stale failure record must not override it (the watchdog re-records
|
||||||
|
// if the environment is still broken).
|
||||||
|
set_local_option(
|
||||||
|
config::keys::OPTION_TEXTURE_RENDER_HEALTH.to_owned(),
|
||||||
|
"".to_owned(),
|
||||||
|
);
|
||||||
let session_event = [("v", &value)];
|
let session_event = [("v", &value)];
|
||||||
for session in sessions::get_sessions() {
|
for session in sessions::get_sessions() {
|
||||||
if !is_render_target(&session) {
|
if !is_render_target(&session) {
|
||||||
@@ -2295,6 +2302,39 @@ pub fn session_register_gpu_texture(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn session_unregister_pixelbuffer_texture(
|
||||||
|
session_id: SessionID,
|
||||||
|
display: usize,
|
||||||
|
ptr: usize,
|
||||||
|
) -> SyncReturn<()> {
|
||||||
|
SyncReturn(super::flutter::session_unregister_pixelbuffer_texture(
|
||||||
|
session_id, display, ptr,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_unregister_gpu_texture(
|
||||||
|
session_id: SessionID,
|
||||||
|
display: usize,
|
||||||
|
ptr: usize,
|
||||||
|
) -> SyncReturn<()> {
|
||||||
|
SyncReturn(super::flutter::session_unregister_gpu_texture(
|
||||||
|
session_id, display, ptr,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_push_texture_probe_frame(ptr: usize) -> SyncReturn<()> {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
super::flutter::push_texture_probe_frame(ptr);
|
||||||
|
SyncReturn(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_get_texture_probe_consumed(ptr: usize) -> SyncReturn<u64> {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
return SyncReturn(super::flutter::get_texture_probe_consumed(ptr));
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
SyncReturn(0)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn query_onlines(ids: Vec<String>) {
|
pub fn query_onlines(ids: Vec<String>) {
|
||||||
let _ = flutter::async_tasks::query_onlines(ids);
|
let _ = flutter::async_tasks::query_onlines(ids);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,16 @@ pub fn get_option<T: AsRef<str>>(key: T) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The watchdog/startup probe records texture-render breakage (frames pushed
|
||||||
|
// but never consumed by the engine). A failed record forces texture render
|
||||||
|
// off on every desktop platform — even an explicit "Y" — because the live
|
||||||
|
// fallback relies on it; toggling the option (or a passing probe) clears it.
|
||||||
|
#[inline]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn texture_render_health_failed() -> bool {
|
||||||
|
LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER_HEALTH).starts_with("failed")
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn use_texture_render() -> bool {
|
pub fn use_texture_render() -> bool {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
@@ -183,28 +193,33 @@ pub fn use_texture_render() -> bool {
|
|||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
return cfg!(feature = "flutter")
|
|
||||||
&& LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
return cfg!(feature = "flutter")
|
|
||||||
&& LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N";
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
{
|
||||||
if !cfg!(feature = "flutter") {
|
if !cfg!(feature = "flutter") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
|
if texture_render_health_failed() {
|
||||||
#[cfg(debug_assertions)]
|
return false;
|
||||||
let default_texture = true;
|
}
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
let default_texture = crate::platform::is_win_10_or_greater();
|
#[cfg(target_os = "macos")]
|
||||||
if default_texture {
|
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
||||||
LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"
|
|
||||||
} else {
|
#[cfg(target_os = "linux")]
|
||||||
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N";
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let default_texture = true;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let default_texture = crate::platform::is_win_10_or_greater();
|
||||||
|
if default_texture {
|
||||||
|
LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"
|
||||||
|
} else {
|
||||||
|
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user