mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-10 22:41:05 +03:00
Compare commits
6 Commits
hdr-tonema
...
fix-textur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24a16d9a30 | ||
|
|
8fc82d04ac | ||
|
|
c6c001a15e | ||
|
|
7da2bbe6ac | ||
|
|
c5adac828b | ||
|
|
7c23e1f4b9 |
@@ -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}) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import '../../common/shared_state.dart';
|
|||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/remote_toolbar.dart';
|
import '../widgets/remote_toolbar.dart';
|
||||||
import '../widgets/kb_layout_type_chooser.dart';
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
|
import '../widgets/raster_stall_monitor.dart';
|
||||||
import '../widgets/tabbar_widget.dart';
|
import '../widgets/tabbar_widget.dart';
|
||||||
import 'macos_full_screen_focus_recovery.dart';
|
import 'macos_full_screen_focus_recovery.dart';
|
||||||
|
|
||||||
@@ -156,6 +157,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
||||||
}
|
}
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
|
RasterStallMonitor.start();
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
_ffi.canvasModel.activateLocalCursor();
|
_ffi.canvasModel.activateLocalCursor();
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
@@ -647,7 +649,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// Clear callback reference to prevent memory leaks and stale references
|
// Clear callback reference to prevent memory leaks and stale references
|
||||||
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
||||||
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
||||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
_ffi.textureModel.onRemotePageDispose();
|
||||||
if (closeSession && !isMacOS) {
|
if (closeSession && !isMacOS) {
|
||||||
// ensure we leave this session, this is a double check
|
// ensure we leave this session, this is a double check
|
||||||
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
|
// enterOrLeave() is already called previously in _releaseMacOSRemoteInput() for macOS.
|
||||||
@@ -1402,3 +1404,4 @@ class CursorPaint extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import '../../common/shared_state.dart';
|
|||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
import '../widgets/remote_toolbar.dart';
|
import '../widgets/remote_toolbar.dart';
|
||||||
import '../widgets/kb_layout_type_chooser.dart';
|
import '../widgets/kb_layout_type_chooser.dart';
|
||||||
|
import '../widgets/raster_stall_monitor.dart';
|
||||||
import '../widgets/tabbar_widget.dart';
|
import '../widgets/tabbar_widget.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/native/custom_cursor.dart'
|
import 'package:flutter_hbb/native/custom_cursor.dart'
|
||||||
@@ -102,6 +103,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_ffi = FFI(widget.sessionId);
|
_ffi = FFI(widget.sessionId);
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
|
RasterStallMonitor.start();
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
@@ -222,7 +224,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
||||||
_ffi.textureModel.onViewCameraPageDispose(closeSession);
|
_ffi.textureModel.onViewCameraPageDispose();
|
||||||
if (closeSession) {
|
if (closeSession) {
|
||||||
// ensure we leave this session, this is a double check
|
// ensure we leave this session, this is a double check
|
||||||
_ffi.inputModel.enterOrLeave(false);
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
|
|||||||
52
flutter/lib/desktop/widgets/raster_stall_monitor.dart
Normal file
52
flutter/lib/desktop/widgets/raster_stall_monitor.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../consts.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/state_model.dart';
|
||||||
|
|
||||||
|
/// Records a hung raster thread (frames continuously scheduled but no frame
|
||||||
|
/// timings delivered for 30s) in `texture-render-health`; a hang cannot be
|
||||||
|
/// rescued in-process, so the next launch defaults texture rendering off.
|
||||||
|
class RasterStallMonitor {
|
||||||
|
static bool _started = false;
|
||||||
|
static bool _reported = false;
|
||||||
|
static DateTime? _lastTimings;
|
||||||
|
static DateTime _lastQuiet = DateTime.now();
|
||||||
|
|
||||||
|
static void start() {
|
||||||
|
if (_started || isWeb) return;
|
||||||
|
_started = true;
|
||||||
|
SchedulerBinding.instance.addTimingsCallback((_) {
|
||||||
|
_lastTimings = DateTime.now();
|
||||||
|
});
|
||||||
|
Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
|
if (_reported) return;
|
||||||
|
final now = DateTime.now();
|
||||||
|
final lifecycle = SchedulerBinding.instance.lifecycleState;
|
||||||
|
// Minimized/inactive (incl. screen lock) or idle (nothing scheduled):
|
||||||
|
// no timings is legitimate, keep moving the quiet anchor forward.
|
||||||
|
if (stateGlobal.isMinimized ||
|
||||||
|
(lifecycle != null && lifecycle != AppLifecycleState.resumed) ||
|
||||||
|
!SchedulerBinding.instance.hasScheduledFrame) {
|
||||||
|
_lastQuiet = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ref = _lastQuiet;
|
||||||
|
final lastTimings = _lastTimings;
|
||||||
|
if (lastTimings != null && lastTimings.isAfter(ref)) {
|
||||||
|
ref = lastTimings;
|
||||||
|
}
|
||||||
|
if (now.difference(ref) > const Duration(seconds: 30)) {
|
||||||
|
_reported = true;
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionTextureRenderHealth, value: 'failed-raster-stall');
|
||||||
|
debugPrint(
|
||||||
|
'raster thread stall detected, texture rendering disabled for next launch');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
|
|||||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
|
import 'package:flutter_hbb/desktop/pages/view_camera_page.dart';
|
||||||
import 'package:flutter_hbb/main.dart';
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -388,6 +389,7 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
void onWindowMinimize() {
|
void onWindowMinimize() {
|
||||||
stateGlobal.setMinimized(true);
|
stateGlobal.setMinimized(true);
|
||||||
stateGlobal.setMaximized(false);
|
stateGlobal.setMaximized(false);
|
||||||
|
_updateSessionsRenderVisible(false);
|
||||||
super.onWindowMinimize();
|
super.onWindowMinimize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,6 +397,7 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
void onWindowMaximize() {
|
void onWindowMaximize() {
|
||||||
stateGlobal.setMinimized(false);
|
stateGlobal.setMinimized(false);
|
||||||
_setMaximized(true);
|
_setMaximized(true);
|
||||||
|
_updateSessionsRenderVisible(true);
|
||||||
super.onWindowMaximize();
|
super.onWindowMaximize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,9 +405,34 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
void onWindowUnmaximize() {
|
void onWindowUnmaximize() {
|
||||||
stateGlobal.setMinimized(false);
|
stateGlobal.setMinimized(false);
|
||||||
_setMaximized(false);
|
_setMaximized(false);
|
||||||
|
_updateSessionsRenderVisible(true);
|
||||||
super.onWindowUnmaximize();
|
super.onWindowUnmaximize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowRestore() {
|
||||||
|
// A plain restore (no maximize involved) must clear the minimized flag.
|
||||||
|
stateGlobal.setMinimized(false);
|
||||||
|
_updateSessionsRenderVisible(true);
|
||||||
|
super.onWindowRestore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hidden window composites nothing; pause the Rust-side texture watchdog
|
||||||
|
// for its sessions so it cannot record a false failure.
|
||||||
|
void _updateSessionsRenderVisible(bool visible) {
|
||||||
|
if (tabType != DesktopTabType.remoteScreen &&
|
||||||
|
tabType != DesktopTabType.viewCamera) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (final tab in controller.state.value.tabs) {
|
||||||
|
try {
|
||||||
|
final ffi = Get.find<FFI>(tag: tab.key);
|
||||||
|
bind.sessionSetRenderVisible(
|
||||||
|
sessionId: ffi.sessionId, visible: visible);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_saveFrame({bool? flush}) async {
|
_saveFrame({bool? flush}) async {
|
||||||
try {
|
try {
|
||||||
if (tabType == DesktopTabType.main) {
|
if (tabType == DesktopTabType.main) {
|
||||||
|
|||||||
172
flutter/lib/desktop/widgets/texture_render_probe.dart
Normal file
172
flutter/lib/desktop/widgets/texture_render_probe.dart
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../consts.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/state_model.dart';
|
||||||
|
|
||||||
|
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
||||||
|
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
||||||
|
|
||||||
|
/// Startup probe: renders one frame through a 1x1 external texture and
|
||||||
|
/// records in `texture-render-health` whether the engine consumed it, so a
|
||||||
|
/// broken environment is detected before the first session goes black.
|
||||||
|
class TextureRenderProbe extends StatefulWidget {
|
||||||
|
const TextureRenderProbe({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TextureRenderProbe> createState() => _TextureRenderProbeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TextureRenderProbeState extends State<TextureRenderProbe> {
|
||||||
|
static bool _ranThisLaunch = false;
|
||||||
|
final _renderer = TextureRgbaRenderer();
|
||||||
|
int _textureId = -1;
|
||||||
|
int _textureKey = -1;
|
||||||
|
int _ptr = 0;
|
||||||
|
Timer? _timer;
|
||||||
|
int _ticks = 0;
|
||||||
|
bool _sawTimings = false;
|
||||||
|
bool _wasEffectiveOn = false;
|
||||||
|
DateTime? _lastTimings;
|
||||||
|
DateTime? _firstPush;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (_ranThisLaunch || isWeb || !isDesktop) return;
|
||||||
|
_ranThisLaunch = true;
|
||||||
|
if (bind.isIncomingOnly()) return;
|
||||||
|
// An old plugin without the consumed counter cannot be judged, and a
|
||||||
|
// recorded raster-stall means compositing a texture may hang this window.
|
||||||
|
if (!bind.mainTextureRenderProbeSupported()) return;
|
||||||
|
if (bind
|
||||||
|
.mainGetLocalOption(key: kOptionTextureRenderHealth)
|
||||||
|
.startsWith('failed-raster-stall')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_wasEffectiveOn = bind.mainGetUseTextureRender();
|
||||||
|
// Only probe after the window has really rendered a frame: a hidden
|
||||||
|
// window (silent/tray start) must not record a false failure.
|
||||||
|
SchedulerBinding.instance.addTimingsCallback(_onTimings);
|
||||||
|
Future.delayed(const Duration(seconds: 5), () {
|
||||||
|
if (!_sawTimings) {
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
|
_finish(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTimings(List<FrameTiming> timings) {
|
||||||
|
_lastTimings = DateTime.now();
|
||||||
|
if (_sawTimings) return;
|
||||||
|
_sawTimings = true;
|
||||||
|
_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _start() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
_textureKey = bind.getNextTextureKey();
|
||||||
|
final id = await _renderer.createTexture(_textureKey);
|
||||||
|
if (!mounted || id == -1) {
|
||||||
|
_finish(!mounted ? null : false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ptr = await _renderer.getTexturePtr(_textureKey);
|
||||||
|
if (!mounted || _ptr <= 0) {
|
||||||
|
_finish(!mounted ? null : false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _textureId = id);
|
||||||
|
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||||
|
_ticks += 1;
|
||||||
|
_firstPush ??= DateTime.now();
|
||||||
|
bind.mainPushTextureProbeFrame(ptr: _ptr);
|
||||||
|
final consumed = bind.mainGetTextureProbeConsumed(ptr: _ptr) > 0;
|
||||||
|
// "Consumed" advances inside the plugin callback, before the GL/Metal
|
||||||
|
// upload; only a frame timing after the push proves a completed frame.
|
||||||
|
final frameCompleted = consumed &&
|
||||||
|
_lastTimings != null &&
|
||||||
|
_lastTimings!.isAfter(_firstPush!);
|
||||||
|
if (frameCompleted) {
|
||||||
|
_finish(true);
|
||||||
|
} else if (_ticks >= 10) {
|
||||||
|
if (consumed) {
|
||||||
|
_finish(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only a window that is visibly compositing can prove a failure.
|
||||||
|
final lifecycle = SchedulerBinding.instance.lifecycleState;
|
||||||
|
final active =
|
||||||
|
lifecycle == null || lifecycle == AppLifecycleState.resumed;
|
||||||
|
final timingsFresh = _lastTimings != null &&
|
||||||
|
DateTime.now().difference(_lastTimings!) <
|
||||||
|
const Duration(milliseconds: 1500);
|
||||||
|
_finish(!stateGlobal.isMinimized && active && timingsFresh
|
||||||
|
? false
|
||||||
|
: null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _finish(bool? ok) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
|
if (ok != null) {
|
||||||
|
final old = bind.mainGetLocalOption(key: kOptionTextureRenderHealth);
|
||||||
|
if (ok) {
|
||||||
|
// This rgba probe disproves only the rgba black-texture class: gpu
|
||||||
|
// failures and raster stalls clear via the option toggle alone.
|
||||||
|
final clearable = old.isEmpty ||
|
||||||
|
old.startsWith('failed-probe') ||
|
||||||
|
old.startsWith('failed-watchdog-rgba');
|
||||||
|
if (clearable) {
|
||||||
|
bind.mainSetLocalOption(key: kOptionTextureRenderHealth, value: 'ok');
|
||||||
|
}
|
||||||
|
} else if (!old.startsWith('failed')) {
|
||||||
|
debugPrint('texture render probe failed, disabling texture rendering');
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionTextureRenderHealth, value: 'failed-probe');
|
||||||
|
if (_wasEffectiveOn) {
|
||||||
|
showToast(translate('texture-render-fallback-tip'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_textureKey != -1) {
|
||||||
|
_renderer.closeTexture(_textureKey);
|
||||||
|
_textureKey = -1;
|
||||||
|
}
|
||||||
|
_ptr = 0;
|
||||||
|
if (mounted && _textureId != -1) {
|
||||||
|
setState(() => _textureId = -1);
|
||||||
|
} else {
|
||||||
|
_textureId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
SchedulerBinding.instance.removeTimingsCallback(_onTimings);
|
||||||
|
if (_textureKey != -1) {
|
||||||
|
_renderer.closeTexture(_textureKey);
|
||||||
|
_textureKey = -1;
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_textureId == -1) return const SizedBox.shrink();
|
||||||
|
// Must actually composite for the engine to sample the texture; the
|
||||||
|
// pushed pixel is fully transparent.
|
||||||
|
return IgnorePointer(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 1, height: 1, child: Texture(textureId: _textureId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ class _PixelbufferTexture {
|
|||||||
int _display = 0;
|
int _display = 0;
|
||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
|
bool _closed = false;
|
||||||
|
int _ptr = 0;
|
||||||
int? _id;
|
int? _id;
|
||||||
|
|
||||||
final textureRenderer = TextureRgbaRenderer();
|
final textureRenderer = TextureRgbaRenderer();
|
||||||
@@ -27,11 +29,22 @@ class _PixelbufferTexture {
|
|||||||
_textureKey = bind.getNextTextureKey();
|
_textureKey = bind.getNextTextureKey();
|
||||||
_sessionId = sessionId;
|
_sessionId = sessionId;
|
||||||
|
|
||||||
textureRenderer.createTexture(_textureKey).then((id) async {
|
final textureKey = _textureKey;
|
||||||
|
textureRenderer.createTexture(textureKey).then((id) async {
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
|
if (_closed) {
|
||||||
|
// Destroyed while creation was still in flight (rapid
|
||||||
|
// connect/disconnect); nobody else will close this texture.
|
||||||
|
await textureRenderer.closeTexture(textureKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
||||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
final ptr = await textureRenderer.getTexturePtr(textureKey);
|
||||||
|
if (_closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ptr = ptr;
|
||||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
||||||
@@ -39,13 +52,16 @@ class _PixelbufferTexture {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool unregisterTexture, FFI ffi) async {
|
destroy(FFI ffi) async {
|
||||||
|
_closed = true;
|
||||||
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (unregisterTexture) {
|
if (_ptr != 0) {
|
||||||
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
// Compare-and-clear: only clears if Rust still holds this pointer
|
||||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
// (#8016-safe); returning from this synchronous call also means no
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
// push through the old pointer is still in flight.
|
||||||
|
platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr);
|
||||||
|
_ptr = 0;
|
||||||
}
|
}
|
||||||
await textureRenderer.closeTexture(_textureKey);
|
await textureRenderer.closeTexture(_textureKey);
|
||||||
_textureKey = -1;
|
_textureKey = -1;
|
||||||
@@ -61,6 +77,7 @@ class _GpuTexture {
|
|||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
final support = bind.mainHasGpuTextureRender();
|
final support = bind.mainHasGpuTextureRender();
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
|
bool _closed = false;
|
||||||
int _display = 0;
|
int _display = 0;
|
||||||
int? _id;
|
int? _id;
|
||||||
int? _output;
|
int? _output;
|
||||||
@@ -79,9 +96,18 @@ class _GpuTexture {
|
|||||||
gpuTextureRenderer.registerTexture().then((id) async {
|
gpuTextureRenderer.registerTexture().then((id) async {
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
|
if (_closed) {
|
||||||
|
// Destroyed while creation was still in flight (rapid
|
||||||
|
// connect/disconnect); nobody else will unregister this texture.
|
||||||
|
await gpuTextureRenderer.unregisterTexture(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
_textureId = id;
|
_textureId = id;
|
||||||
ffi.textureModel.setGpuTextureId(display: d, id: id);
|
ffi.textureModel.setGpuTextureId(display: d, id: id);
|
||||||
final output = await gpuTextureRenderer.output(id);
|
final output = await gpuTextureRenderer.output(id);
|
||||||
|
if (_closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_output = output;
|
_output = output;
|
||||||
if (output != null) {
|
if (output != null) {
|
||||||
platformFFI.registerGpuTexture(sessionId, d, output);
|
platformFFI.registerGpuTexture(sessionId, d, output);
|
||||||
@@ -95,20 +121,22 @@ class _GpuTexture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool unregisterTexture, FFI ffi) async {
|
destroy(FFI ffi) async {
|
||||||
// must stop texture render, render unregistered texture cause crash
|
// must stop texture render, render unregistered texture cause crash
|
||||||
|
_closed = true;
|
||||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (unregisterTexture) {
|
final output = _output;
|
||||||
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
if (output != null) {
|
||||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
// Compare-and-clear, see _PixelbufferTexture.destroy.
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
platformFFI.unregisterGpuTexture(_sessionId!, _display, output);
|
||||||
|
_output = null;
|
||||||
}
|
}
|
||||||
await gpuTextureRenderer.unregisterTexture(_textureId);
|
await gpuTextureRenderer.unregisterTexture(_textureId);
|
||||||
_textureId = -1;
|
_textureId = -1;
|
||||||
_destroying = false;
|
_destroying = false;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$_output");
|
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$output");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,11 +228,11 @@ class TextureModel {
|
|||||||
tryRemoveTexture(int idx) {
|
tryRemoveTexture(int idx) {
|
||||||
_control.remove(idx);
|
_control.remove(idx);
|
||||||
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
||||||
_pixelbufferRenderTextures[idx]!.destroy(true, ffi);
|
_pixelbufferRenderTextures[idx]!.destroy(ffi);
|
||||||
_pixelbufferRenderTextures.remove(idx);
|
_pixelbufferRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
if (_gpuRenderTextures.containsKey(idx)) {
|
if (_gpuRenderTextures.containsKey(idx)) {
|
||||||
_gpuRenderTextures[idx]!.destroy(true, ffi);
|
_gpuRenderTextures[idx]!.destroy(ffi);
|
||||||
_gpuRenderTextures.remove(idx);
|
_gpuRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,25 +252,25 @@ class TextureModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemotePageDispose(bool closeSession) async {
|
onRemotePageDispose() async {
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
for (final texture in _pixelbufferRenderTextures.values) {
|
for (final texture in _pixelbufferRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
for (final texture in _gpuRenderTextures.values) {
|
for (final texture in _gpuRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewCameraPageDispose(bool closeSession) async {
|
onViewCameraPageDispose() async {
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
for (final texture in _pixelbufferRenderTextures.values) {
|
for (final texture in _pixelbufferRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
for (final texture in _gpuRenderTextures.values) {
|
for (final texture in _gpuRenderTextures.values) {
|
||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(ffi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,31 @@ class RustdeskImpl {
|
|||||||
required int ptr,
|
required int ptr,
|
||||||
dynamic hint}) {}
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
void sessionUnregisterPixelbufferTexture(
|
||||||
|
{required UuidValue sessionId,
|
||||||
|
required int display,
|
||||||
|
required int ptr,
|
||||||
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
void sessionUnregisterGpuTexture(
|
||||||
|
{required UuidValue sessionId,
|
||||||
|
required int display,
|
||||||
|
required int ptr,
|
||||||
|
dynamic hint}) {}
|
||||||
|
|
||||||
|
void sessionSetRenderVisible(
|
||||||
|
{required UuidValue sessionId, required bool visible, dynamic hint}) {}
|
||||||
|
|
||||||
|
bool mainTextureRenderProbeSupported({dynamic hint}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainPushTextureProbeFrame({required int ptr, dynamic hint}) {}
|
||||||
|
|
||||||
|
int mainGetTextureProbeConsumed({required int ptr, dynamic hint}) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> queryOnlines({required List<String> ids, dynamic hint}) {
|
Future<void> queryOnlines({required List<String> ids, dynamic hint}) {
|
||||||
return Future(() =>
|
return Future(() =>
|
||||||
js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)]));
|
js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)]));
|
||||||
|
|||||||
@@ -538,8 +538,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
|
ref: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
|
||||||
resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
|
resolved-ref: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
|
||||||
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -1298,8 +1298,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
|
||||||
resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
resolved-ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
|
||||||
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
|
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.16"
|
version: "0.0.16"
|
||||||
|
|||||||
@@ -88,13 +88,13 @@ dependencies:
|
|||||||
texture_rgba_renderer:
|
texture_rgba_renderer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
|
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
|
||||||
ref: 42797e0f03141dc2b585f76c64a13974508058b4
|
ref: 883326ddd4fb2af1484bf873b4ea856a0ac440bc
|
||||||
percent_indicator: ^4.2.2
|
percent_indicator: ^4.2.2
|
||||||
dropdown_button2: ^2.0.0
|
dropdown_button2: ^2.0.0
|
||||||
flutter_gpu_texture_renderer:
|
flutter_gpu_texture_renderer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
||||||
ref: 08a471bb8ceccdd50483c81cdfa8b81b07b14b87
|
ref: 208619e750a5fd904c689a9babd6ccf0f7c1ca88
|
||||||
uuid: ^3.0.7
|
uuid: ^3.0.7
|
||||||
auto_size_text_field: ^2.2.1
|
auto_size_text_field: ^2.2.1
|
||||||
flex_color_picker: ^3.3.0
|
flex_color_picker: ^3.3.0
|
||||||
|
|||||||
Submodule libs/hbb_common updated: 69cea8dafe...3ed938544f
549
src/flutter.rs
549
src/flutter.rs
@@ -23,9 +23,10 @@ use std::{
|
|||||||
os::raw::{c_char, c_int, c_void},
|
os::raw::{c_char, c_int, c_void},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
|
||||||
Arc, RwLock,
|
Arc, Mutex, RwLock,
|
||||||
},
|
},
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed)
|
/// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed)
|
||||||
@@ -269,26 +270,119 @@ pub type FlutterGpuTextureRendererPluginCApiSetTexture =
|
|||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64;
|
pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64;
|
||||||
|
|
||||||
|
pub type FlutterRgbaRendererPluginGetConsumed = unsafe extern "C" fn(texture_rgba: *mut c_void) -> u64;
|
||||||
|
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
pub type FlutterGpuTextureRendererPluginCApiGetConsumed =
|
||||||
|
unsafe extern "C" fn(output: *mut c_void) -> u64;
|
||||||
|
|
||||||
pub(super) type TextureRgbaPtr = usize;
|
pub(super) type TextureRgbaPtr = usize;
|
||||||
|
|
||||||
|
// Which texture backend the watchdog saw fail; the health record carries it
|
||||||
|
// so the rgba-only startup probe never clears a gpu-path failure.
|
||||||
|
pub(super) const WATCHDOG_FAILED_RGBA: u8 = 1;
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
pub(super) const WATCHDOG_FAILED_GPU: u8 = 2;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
struct DisplaySessionInfo {
|
struct DisplaySessionInfo {
|
||||||
// TextureRgba pointer in flutter native.
|
// TextureRgba pointer in flutter native.
|
||||||
texture_rgba_ptr: TextureRgbaPtr,
|
texture_rgba_ptr: TextureRgbaPtr,
|
||||||
size: (usize, usize),
|
size: (usize, usize),
|
||||||
|
size_mismatch_count: u32,
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
gpu_output_ptr: usize,
|
gpu_output_ptr: usize,
|
||||||
notify_render_type: Option<RenderType>,
|
notify_render_type: Option<RenderType>,
|
||||||
|
// Watchdog: frames pushed to a texture the engine never consumes mean
|
||||||
|
// texture rendering is broken (black view on a live connection). Armed
|
||||||
|
// until a consumption is observed since arming.
|
||||||
|
pushed_count: u64,
|
||||||
|
watchdog_consumed_base: Option<u64>,
|
||||||
|
watchdog_pushed_base: u64,
|
||||||
|
watchdog_since: Option<Instant>,
|
||||||
|
watchdog_last_sample: Option<Instant>,
|
||||||
|
watchdog_armed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplaySessionInfo {
|
||||||
|
fn reset_watchdog(&mut self) {
|
||||||
|
self.pushed_count = 0;
|
||||||
|
self.watchdog_consumed_base = None;
|
||||||
|
self.watchdog_pushed_base = 0;
|
||||||
|
self.watchdog_since = None;
|
||||||
|
self.watchdog_last_sample = None;
|
||||||
|
self.watchdog_armed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the observation window without disarming; used while the window
|
||||||
|
// is hidden, where the engine legitimately composites nothing.
|
||||||
|
fn pause_watchdog(&mut self) {
|
||||||
|
self.watchdog_consumed_base = None;
|
||||||
|
self.watchdog_since = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The plugin counter is cumulative and never resets, so compare against a
|
||||||
|
// snapshot taken when arming; damage-driven streams can be sparse, so
|
||||||
|
// judge on pushes within the observation window plus elapsed time.
|
||||||
|
fn check_watchdog(&mut self, consumed: u64) -> bool {
|
||||||
|
let now = Instant::now();
|
||||||
|
let Some(base) = self.watchdog_consumed_base else {
|
||||||
|
self.watchdog_consumed_base = Some(consumed);
|
||||||
|
self.watchdog_pushed_base = self.pushed_count;
|
||||||
|
self.watchdog_since = Some(now);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if consumed > base {
|
||||||
|
self.watchdog_armed = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.pushed_count - self.watchdog_pushed_base >= 30
|
||||||
|
&& self
|
||||||
|
.watchdog_since
|
||||||
|
.map(|t| now.duration_since(t) >= Duration::from_secs(3))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
self.watchdog_armed = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn watchdog_sample_due(&mut self) -> bool {
|
||||||
|
if !self.watchdog_armed {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let now = Instant::now();
|
||||||
|
match self.watchdog_last_sample {
|
||||||
|
Some(t) if now.duration_since(t) < Duration::from_secs(1) => false,
|
||||||
|
_ => {
|
||||||
|
self.watchdog_last_sample = Some(now);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video Texture Renderer in Flutter
|
// Video Texture Renderer in Flutter
|
||||||
|
// Per-display mutexes: the per-frame plugin call must not hold session-level
|
||||||
|
// locks, or a stalled plugin/driver call freezes every window's UI thread.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct VideoRenderer {
|
struct VideoRenderer {
|
||||||
is_support_multi_ui_session: bool,
|
is_support_multi_ui_session: bool,
|
||||||
map_display_sessions: Arc<RwLock<HashMap<usize, DisplaySessionInfo>>>,
|
map_display_sessions: Arc<RwLock<HashMap<usize, Arc<Mutex<DisplaySessionInfo>>>>>,
|
||||||
|
// Latched by the watchdog (WATCHDOG_FAILED_*); consumed once by the
|
||||||
|
// pushing caller to trigger the software-render fallback for this session.
|
||||||
|
texture_render_failed: Arc<AtomicU8>,
|
||||||
|
// Hidden windows legitimately composite nothing; the watchdog pauses.
|
||||||
|
render_visible: Arc<AtomicBool>,
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
|
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
get_consumed_func: Option<Symbol<'static, FlutterRgbaRendererPluginGetConsumed>>,
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>,
|
on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>,
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
get_gpu_consumed_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiGetConsumed>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for VideoRenderer {
|
impl Default for VideoRenderer {
|
||||||
@@ -312,6 +406,17 @@ impl Default for VideoRenderer {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Absent in older plugin builds; the watchdog just stays disabled.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
let get_consumed_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN {
|
||||||
|
Ok(lib) => unsafe {
|
||||||
|
lib.symbol::<FlutterRgbaRendererPluginGetConsumed>(
|
||||||
|
"FlutterRgbaRendererPluginGetConsumed",
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
||||||
Ok(lib) => {
|
Ok(lib) => {
|
||||||
@@ -333,14 +438,30 @@ impl Default for VideoRenderer {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
let get_gpu_consumed_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
||||||
|
Ok(lib) => unsafe {
|
||||||
|
lib.symbol::<FlutterGpuTextureRendererPluginCApiGetConsumed>(
|
||||||
|
"FlutterGpuTextureRendererPluginCApiGetConsumed",
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
map_display_sessions: Default::default(),
|
map_display_sessions: Default::default(),
|
||||||
is_support_multi_ui_session: false,
|
is_support_multi_ui_session: false,
|
||||||
|
texture_render_failed: Default::default(),
|
||||||
|
render_visible: Arc::new(AtomicBool::new(true)),
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
on_rgba_func,
|
on_rgba_func,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
get_consumed_func,
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
on_texture_func,
|
on_texture_func,
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
get_gpu_consumed_func,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,19 +470,18 @@ impl VideoRenderer {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn set_size(&mut self, display: usize, width: usize, height: usize) {
|
fn set_size(&mut self, display: usize, width: usize, height: usize) {
|
||||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info) = sessions_lock.get(&display) {
|
||||||
|
let mut info = info.lock().unwrap();
|
||||||
info.size = (width, height);
|
info.size = (width, height);
|
||||||
|
info.size_mismatch_count = 0;
|
||||||
info.notify_render_type = None;
|
info.notify_render_type = None;
|
||||||
} else {
|
} else {
|
||||||
sessions_lock.insert(
|
sessions_lock.insert(
|
||||||
display,
|
display,
|
||||||
DisplaySessionInfo {
|
Arc::new(Mutex::new(DisplaySessionInfo {
|
||||||
texture_rgba_ptr: usize::default(),
|
|
||||||
size: (width, height),
|
size: (width, height),
|
||||||
#[cfg(feature = "vram")]
|
..Default::default()
|
||||||
gpu_output_ptr: usize::default(),
|
})),
|
||||||
notify_render_type: None,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +489,8 @@ impl VideoRenderer {
|
|||||||
fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
||||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
if ptr == 0 {
|
if ptr == 0 {
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
if info.texture_rgba_ptr != usize::default() {
|
if info.texture_rgba_ptr != usize::default() {
|
||||||
info.texture_rgba_ptr = usize::default();
|
info.texture_rgba_ptr = usize::default();
|
||||||
}
|
}
|
||||||
@@ -377,10 +498,12 @@ impl VideoRenderer {
|
|||||||
if info.gpu_output_ptr != usize::default() {
|
if info.gpu_output_ptr != usize::default() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
drop(info);
|
||||||
|
sessions_lock.remove(&display);
|
||||||
}
|
}
|
||||||
sessions_lock.remove(&display);
|
|
||||||
} else {
|
} else {
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info) = sessions_lock.get(&display) {
|
||||||
|
let mut info = info.lock().unwrap();
|
||||||
if info.texture_rgba_ptr != usize::default()
|
if info.texture_rgba_ptr != usize::default()
|
||||||
&& info.texture_rgba_ptr != ptr as TextureRgbaPtr
|
&& info.texture_rgba_ptr != ptr as TextureRgbaPtr
|
||||||
{
|
{
|
||||||
@@ -392,38 +515,59 @@ impl VideoRenderer {
|
|||||||
}
|
}
|
||||||
info.texture_rgba_ptr = ptr as _;
|
info.texture_rgba_ptr = ptr as _;
|
||||||
info.notify_render_type = None;
|
info.notify_render_type = None;
|
||||||
|
info.reset_watchdog();
|
||||||
} else {
|
} else {
|
||||||
if ptr != 0 {
|
let mut info = DisplaySessionInfo {
|
||||||
sessions_lock.insert(
|
texture_rgba_ptr: ptr as _,
|
||||||
display,
|
..Default::default()
|
||||||
DisplaySessionInfo {
|
};
|
||||||
texture_rgba_ptr: ptr as _,
|
info.reset_watchdog();
|
||||||
size: (0, 0),
|
sessions_lock.insert(display, Arc::new(Mutex::new(info)));
|
||||||
#[cfg(feature = "vram")]
|
|
||||||
gpu_output_ptr: usize::default(),
|
|
||||||
notify_render_type: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare-and-clear: an unconditional clear could wipe the registration a
|
||||||
|
// new window just made when a tab moves between windows (#8016); waiting
|
||||||
|
// on the display mutex also drains an in-flight push via the old pointer.
|
||||||
|
fn unregister_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
||||||
|
if ptr == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
|
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
|
if info.texture_rgba_ptr != ptr as TextureRgbaPtr {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info.texture_rgba_ptr = usize::default();
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
if info.gpu_output_ptr != usize::default() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(info);
|
||||||
|
sessions_lock.remove(&display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub fn register_gpu_output(&self, display: usize, ptr: usize) {
|
pub fn register_gpu_output(&self, display: usize, ptr: usize) {
|
||||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
if ptr == 0 {
|
if ptr == 0 {
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
if info.gpu_output_ptr != usize::default() {
|
if info.gpu_output_ptr != usize::default() {
|
||||||
info.gpu_output_ptr = usize::default();
|
info.gpu_output_ptr = usize::default();
|
||||||
}
|
}
|
||||||
if info.texture_rgba_ptr != usize::default() {
|
if info.texture_rgba_ptr != usize::default() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
drop(info);
|
||||||
|
sessions_lock.remove(&display);
|
||||||
}
|
}
|
||||||
sessions_lock.remove(&display);
|
|
||||||
} else {
|
} else {
|
||||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
if let Some(info) = sessions_lock.get(&display) {
|
||||||
|
let mut info = info.lock().unwrap();
|
||||||
if info.gpu_output_ptr != usize::default() && info.gpu_output_ptr != ptr {
|
if info.gpu_output_ptr != usize::default() && info.gpu_output_ptr != ptr {
|
||||||
log::error!(
|
log::error!(
|
||||||
"gpu_output_ptr is not null and not equal to ptr, relace {} to {}",
|
"gpu_output_ptr is not null and not equal to ptr, relace {} to {}",
|
||||||
@@ -433,50 +577,91 @@ impl VideoRenderer {
|
|||||||
}
|
}
|
||||||
info.gpu_output_ptr = ptr as _;
|
info.gpu_output_ptr = ptr as _;
|
||||||
info.notify_render_type = None;
|
info.notify_render_type = None;
|
||||||
|
info.reset_watchdog();
|
||||||
} else {
|
} else {
|
||||||
if ptr != usize::default() {
|
let mut info = DisplaySessionInfo {
|
||||||
sessions_lock.insert(
|
gpu_output_ptr: ptr,
|
||||||
display,
|
..Default::default()
|
||||||
DisplaySessionInfo {
|
};
|
||||||
texture_rgba_ptr: usize::default(),
|
info.reset_watchdog();
|
||||||
size: (0, 0),
|
sessions_lock.insert(display, Arc::new(Mutex::new(info)));
|
||||||
gpu_output_ptr: ptr,
|
|
||||||
notify_render_type: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See unregister_pixelbuffer_texture for why this is compare-and-clear.
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
pub fn unregister_gpu_output(&self, display: usize, ptr: usize) {
|
||||||
|
if ptr == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||||
|
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
|
if info.gpu_output_ptr != ptr {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info.gpu_output_ptr = usize::default();
|
||||||
|
if info.texture_rgba_ptr != usize::default() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(info);
|
||||||
|
sessions_lock.remove(&display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn display_session_info(&self, display: usize) -> Option<Arc<Mutex<DisplaySessionInfo>>> {
|
||||||
|
let read_lock = self.map_display_sessions.read().unwrap();
|
||||||
|
if !self.is_support_multi_ui_session {
|
||||||
|
read_lock.values().next().cloned()
|
||||||
|
} else {
|
||||||
|
read_lock.get(&display).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool {
|
pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool {
|
||||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
let Some(info_arc) = self.display_session_info(display) else {
|
||||||
let opt_info = if !self.is_support_multi_ui_session {
|
|
||||||
write_lock.values_mut().next()
|
|
||||||
} else {
|
|
||||||
write_lock.get_mut(&display)
|
|
||||||
};
|
|
||||||
let Some(info) = opt_info else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
if info.texture_rgba_ptr == usize::default() {
|
if info.texture_rgba_ptr == usize::default() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.size.0 != rgba.w || info.size.1 != rgba.h {
|
if info.size.0 != rgba.w || info.size.1 != rgba.h {
|
||||||
log::error!(
|
|
||||||
"width/height mismatch: ({},{}) != ({},{})",
|
|
||||||
info.size.0,
|
|
||||||
info.size.1,
|
|
||||||
rgba.w,
|
|
||||||
rgba.h
|
|
||||||
);
|
|
||||||
// Peer info's handling is async and may be late than video frame's handling
|
// Peer info's handling is async and may be late than video frame's handling
|
||||||
// Allow peer info not set, but not allow wrong width/height for correct local cursor position
|
// Allow peer info not set, but not allow wrong width/height for correct local cursor position
|
||||||
if info.size != (0, 0) {
|
if info.size != (0, 0) {
|
||||||
return false;
|
info.size_mismatch_count += 1;
|
||||||
|
if info.size_mismatch_count == 1 {
|
||||||
|
log::error!(
|
||||||
|
"width/height mismatch: ({},{}) != ({},{})",
|
||||||
|
info.size.0,
|
||||||
|
info.size.1,
|
||||||
|
rgba.w,
|
||||||
|
rgba.h
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// If sizes still disagree after this many frames the peer info
|
||||||
|
// is not coming and dropping forever leaves a live session
|
||||||
|
// black. Legacy single-ui-session pairs frames loosely
|
||||||
|
// (values().next()), so only adopt where pairing is exact.
|
||||||
|
if !self.is_support_multi_ui_session || info.size_mismatch_count < 30 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log::warn!(
|
||||||
|
"adopting frame size ({},{}) after {} mismatched frames",
|
||||||
|
rgba.w,
|
||||||
|
rgba.h,
|
||||||
|
info.size_mismatch_count
|
||||||
|
);
|
||||||
|
info.size = (rgba.w, rgba.h);
|
||||||
|
info.size_mismatch_count = 0;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
info.size_mismatch_count = 0;
|
||||||
}
|
}
|
||||||
if let Some(func) = &self.on_rgba_func {
|
if let Some(func) = &self.on_rgba_func {
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -490,6 +675,23 @@ impl VideoRenderer {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
info.pushed_count += 1;
|
||||||
|
if let Some(get_consumed) = &self.get_consumed_func {
|
||||||
|
if !self.render_visible.load(Ordering::Relaxed) {
|
||||||
|
info.pause_watchdog();
|
||||||
|
} else if info.watchdog_sample_due() {
|
||||||
|
let consumed = unsafe { get_consumed(info.texture_rgba_ptr as _) };
|
||||||
|
if info.check_watchdog(consumed) {
|
||||||
|
log::error!(
|
||||||
|
"texture rendering broken: {} frames pushed to display {}, none consumed",
|
||||||
|
info.pushed_count,
|
||||||
|
display
|
||||||
|
);
|
||||||
|
self.texture_render_failed
|
||||||
|
.store(WATCHDOG_FAILED_RGBA, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if info.notify_render_type != Some(RenderType::PixelBuffer) {
|
if info.notify_render_type != Some(RenderType::PixelBuffer) {
|
||||||
info.notify_render_type = Some(RenderType::PixelBuffer);
|
info.notify_render_type = Some(RenderType::PixelBuffer);
|
||||||
true
|
true
|
||||||
@@ -500,21 +702,36 @@ impl VideoRenderer {
|
|||||||
|
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub fn on_texture(&self, display: usize, texture: *mut c_void) -> bool {
|
pub fn on_texture(&self, display: usize, texture: *mut c_void) -> bool {
|
||||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
let Some(info_arc) = self.display_session_info(display) else {
|
||||||
let opt_info = if !self.is_support_multi_ui_session {
|
|
||||||
write_lock.values_mut().next()
|
|
||||||
} else {
|
|
||||||
write_lock.get_mut(&display)
|
|
||||||
};
|
|
||||||
let Some(info) = opt_info else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
let mut info = info_arc.lock().unwrap();
|
||||||
if info.gpu_output_ptr == usize::default() {
|
if info.gpu_output_ptr == usize::default() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(func) = &self.on_texture_func {
|
if let Some(func) = &self.on_texture_func {
|
||||||
unsafe { func(info.gpu_output_ptr as _, texture) };
|
unsafe { func(info.gpu_output_ptr as _, texture) };
|
||||||
}
|
}
|
||||||
|
info.pushed_count += 1;
|
||||||
|
// Gpu "consumed" counts descriptor fetches (an EGL bind failure still
|
||||||
|
// advances it), so this only detects never-composited outputs; the
|
||||||
|
// rgba path and the startup probe cover bind-failure black screens.
|
||||||
|
if let Some(get_consumed) = &self.get_gpu_consumed_func {
|
||||||
|
if !self.render_visible.load(Ordering::Relaxed) {
|
||||||
|
info.pause_watchdog();
|
||||||
|
} else if info.watchdog_sample_due() {
|
||||||
|
let consumed = unsafe { get_consumed(info.gpu_output_ptr as _) };
|
||||||
|
if info.check_watchdog(consumed) {
|
||||||
|
log::error!(
|
||||||
|
"gpu texture rendering broken: {} frames pushed to display {}, none consumed",
|
||||||
|
info.pushed_count,
|
||||||
|
display
|
||||||
|
);
|
||||||
|
self.texture_render_failed
|
||||||
|
.store(WATCHDOG_FAILED_GPU, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if info.notify_render_type != Some(RenderType::Texture) {
|
if info.notify_render_type != Some(RenderType::Texture) {
|
||||||
info.notify_render_type = Some(RenderType::Texture);
|
info.notify_render_type = Some(RenderType::Texture);
|
||||||
true
|
true
|
||||||
@@ -524,11 +741,10 @@ impl VideoRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_all_display_render_type(&self) {
|
pub fn reset_all_display_render_type(&self) {
|
||||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
let read_lock = self.map_display_sessions.read().unwrap();
|
||||||
write_lock
|
for info in read_lock.values() {
|
||||||
.values_mut()
|
info.lock().unwrap().notify_render_type = None;
|
||||||
.map(|v| v.notify_render_type = None)
|
}
|
||||||
.count();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,9 +877,24 @@ impl FlutterHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_use_texture_render(&self) {
|
pub fn update_use_texture_render(&self) {
|
||||||
self.use_texture_render
|
let v = crate::ui_interface::use_texture_render();
|
||||||
.store(crate::ui_interface::use_texture_render(), Ordering::Relaxed);
|
self.use_texture_render.store(v, Ordering::Relaxed);
|
||||||
self.display_rgbas.write().unwrap().clear();
|
self.display_rgbas.write().unwrap().clear();
|
||||||
|
if v {
|
||||||
|
// Texture render was (re-)enabled; validate it afresh so a still
|
||||||
|
// broken environment fails over again instead of staying black.
|
||||||
|
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
||||||
|
for info in session
|
||||||
|
.renderer
|
||||||
|
.map_display_sessions
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.values()
|
||||||
|
{
|
||||||
|
info.lock().unwrap().reset_watchdog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,12 +1118,13 @@ impl InvokeUiSession for FlutterHandler {
|
|||||||
if !self.use_texture_render.load(Ordering::Relaxed) {
|
if !self.use_texture_render.load(Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
for (session_id, session) in self.session_handlers.read().unwrap().iter() {
|
||||||
if session.renderer.on_texture(display, texture) {
|
if session.renderer.on_texture(display, texture) {
|
||||||
if let Some(stream) = &session.event_stream {
|
if let Some(stream) = &session.event_stream {
|
||||||
stream.add(EventToUI::Texture(display, true));
|
stream.add(EventToUI::Texture(display, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Self::check_texture_render_failed(session_id, session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1262,16 +1494,31 @@ impl FlutterHandler {
|
|||||||
display: usize,
|
display: usize,
|
||||||
rgba: &mut scrap::ImageRgb,
|
rgba: &mut scrap::ImageRgb,
|
||||||
) {
|
) {
|
||||||
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
for (session_id, session) in self.session_handlers.read().unwrap().iter() {
|
||||||
if use_texture_render || session.displays.len() > 1 {
|
if use_texture_render || session.displays.len() > 1 {
|
||||||
if session.renderer.on_rgba(display, rgba) {
|
if session.renderer.on_rgba(display, rgba) {
|
||||||
if let Some(stream) = &session.event_stream {
|
if let Some(stream) = &session.event_stream {
|
||||||
stream.add(EventToUI::Texture(display, false));
|
stream.add(EventToUI::Texture(display, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Self::check_texture_render_failed(session_id, session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consume the watchdog latch outside the per-frame hot path work; the
|
||||||
|
// actual fallback (config write, decoder reset) runs on its own thread.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
fn check_texture_render_failed(session_id: &SessionID, session: &SessionHandler) {
|
||||||
|
let kind = session
|
||||||
|
.renderer
|
||||||
|
.texture_render_failed
|
||||||
|
.swap(0, Ordering::SeqCst);
|
||||||
|
if kind != 0 {
|
||||||
|
let session_id = session_id.clone();
|
||||||
|
std::thread::spawn(move || on_texture_render_failed(session_id, kind));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is only used for the default connection session.
|
// This function is only used for the default connection session.
|
||||||
@@ -1795,6 +2042,168 @@ pub fn session_register_gpu_texture(_session_id: SessionID, _display: usize, _ou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn session_unregister_pixelbuffer_texture(session_id: SessionID, display: usize, ptr: usize) {
|
||||||
|
for s in sessions::get_sessions() {
|
||||||
|
if let Some(h) = s
|
||||||
|
.ui_handler
|
||||||
|
.session_handlers
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(&session_id)
|
||||||
|
{
|
||||||
|
h.renderer.unregister_pixelbuffer_texture(display, ptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hidden windows legitimately composite nothing; pausing the watchdog there
|
||||||
|
// keeps a minimized/background window from recording a false failure.
|
||||||
|
#[inline]
|
||||||
|
pub fn session_set_render_visible(session_id: SessionID, visible: bool) {
|
||||||
|
for s in sessions::get_sessions() {
|
||||||
|
if let Some(h) = s
|
||||||
|
.ui_handler
|
||||||
|
.session_handlers
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(&session_id)
|
||||||
|
{
|
||||||
|
h.renderer
|
||||||
|
.render_visible
|
||||||
|
.store(visible, Ordering::Relaxed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn session_unregister_gpu_texture(_session_id: SessionID, _display: usize, _output_ptr: usize) {
|
||||||
|
#[cfg(feature = "vram")]
|
||||||
|
for s in sessions::get_sessions() {
|
||||||
|
if let Some(h) = s
|
||||||
|
.ui_handler
|
||||||
|
.session_handlers
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(&_session_id)
|
||||||
|
{
|
||||||
|
h.renderer.unregister_gpu_output(_display, _output_ptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Startup-probe plumbing: the main window pushes one frame into a throwaway
|
||||||
|
// 1x1 texture and polls whether the engine consumed it, validating the
|
||||||
|
// texture pipeline before any session depends on it.
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn texture_render_probe_supported() -> bool {
|
||||||
|
match &*TEXTURE_RGBA_RENDERER_PLUGIN {
|
||||||
|
Ok(lib) => unsafe {
|
||||||
|
lib.symbol::<FlutterRgbaRendererPluginGetConsumed>(
|
||||||
|
"FlutterRgbaRendererPluginGetConsumed",
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
},
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn push_texture_probe_frame(ptr: usize) {
|
||||||
|
if ptr == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(lib) = &*TEXTURE_RGBA_RENDERER_PLUGIN else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(func) = (unsafe {
|
||||||
|
lib.symbol::<FlutterRgbaRendererPluginOnRgba>("FlutterRgbaRendererPluginOnRgba")
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Fully transparent so the 1x1 probe pixel is invisible on any theme.
|
||||||
|
let frame: [u8; 4] = [0, 0, 0, 0];
|
||||||
|
unsafe { func(ptr as _, frame.as_ptr(), 4, 1, 1, 1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn get_texture_probe_consumed(ptr: usize) -> u64 {
|
||||||
|
if ptr == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let Ok(lib) = &*TEXTURE_RGBA_RENDERER_PLUGIN else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let Ok(func) = (unsafe {
|
||||||
|
lib.symbol::<FlutterRgbaRendererPluginGetConsumed>("FlutterRgbaRendererPluginGetConsumed")
|
||||||
|
}) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
unsafe { func(ptr as _) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frames are being pushed but the engine never consumes them: fall back to
|
||||||
|
// software rendering and record the breakage (health flips the default off;
|
||||||
|
// a passing startup probe or an explicit option toggle clears it).
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
fn on_texture_render_failed(session_id: SessionID, kind: u8) {
|
||||||
|
// One record per breakage; later fires (other displays/sessions) no-op.
|
||||||
|
// Sessions already running were downgraded when the record was written.
|
||||||
|
if crate::ui_interface::texture_render_health_failed() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log::error!(
|
||||||
|
"texture rendering failed for session {}, falling back to software rendering",
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
let backend = if kind == WATCHDOG_FAILED_RGBA {
|
||||||
|
"rgba"
|
||||||
|
} else {
|
||||||
|
"gpu"
|
||||||
|
};
|
||||||
|
LocalConfig::set_option(
|
||||||
|
hbb_common::config::keys::OPTION_TEXTURE_RENDER_HEALTH.to_owned(),
|
||||||
|
format!(
|
||||||
|
"failed-watchdog-{}@{}",
|
||||||
|
backend,
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Mirror main_set_local_option: every render session must observe the
|
||||||
|
// new effective value, not only the failing one.
|
||||||
|
for session in sessions::get_sessions() {
|
||||||
|
if !(session.is_default() || session.is_view_camera()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The soft path cannot rescue multi-display windows; don't claim it.
|
||||||
|
let fallback_rescues = session
|
||||||
|
.ui_handler
|
||||||
|
.session_handlers
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(&session_id)
|
||||||
|
.map(|h| h.displays.len() <= 1)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if fallback_rescues {
|
||||||
|
session.push_event(
|
||||||
|
"use_texture_render",
|
||||||
|
&[("v", "N"), ("reason", "fallback")],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
session.push_event("use_texture_render", &[("v", "N")], &[]);
|
||||||
|
}
|
||||||
|
session.use_texture_render_changed();
|
||||||
|
session.ui_handler.update_use_texture_render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(not(feature = "vram"))]
|
#[cfg(not(feature = "vram"))]
|
||||||
pub fn get_adapter_luid() -> Option<i64> {
|
pub fn get_adapter_luid() -> Option<i64> {
|
||||||
|
|||||||
@@ -1222,11 +1222,32 @@ pub fn main_set_env(key: String, value: Option<String>) -> SyncReturn<()> {
|
|||||||
|
|
||||||
pub fn main_set_local_option(key: String, value: String) {
|
pub fn main_set_local_option(key: String, value: String) {
|
||||||
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
|
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
|
||||||
|
let is_texture_render_health_key = key.eq(config::keys::OPTION_TEXTURE_RENDER_HEALTH);
|
||||||
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
|
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
|
||||||
set_local_option(key, value.clone());
|
set_local_option(key, value.clone());
|
||||||
let is_render_target =
|
let is_render_target =
|
||||||
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
|
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
|
||||||
|
if is_texture_render_health_key && value.starts_with("failed") {
|
||||||
|
// Probe/raster-stall failures must also downgrade sessions that are
|
||||||
|
// already running (they snapshotted the old effective value, and the
|
||||||
|
// watchdog's own fallback no-ops once a record exists).
|
||||||
|
for session in sessions::get_sessions() {
|
||||||
|
if !is_render_target(&session) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
session.push_event("use_texture_render", &[("v", "N")], &[]);
|
||||||
|
session.use_texture_render_changed();
|
||||||
|
session.ui_handler.update_use_texture_render();
|
||||||
|
}
|
||||||
|
}
|
||||||
if is_texture_render_key {
|
if is_texture_render_key {
|
||||||
|
// An explicit user toggle gives texture rendering a fresh chance; a
|
||||||
|
// stale failure record must not override it (the watchdog re-records
|
||||||
|
// if the environment is still broken).
|
||||||
|
set_local_option(
|
||||||
|
config::keys::OPTION_TEXTURE_RENDER_HEALTH.to_owned(),
|
||||||
|
"".to_owned(),
|
||||||
|
);
|
||||||
let session_event = [("v", &value)];
|
let session_event = [("v", &value)];
|
||||||
for session in sessions::get_sessions() {
|
for session in sessions::get_sessions() {
|
||||||
if !is_render_target(&session) {
|
if !is_render_target(&session) {
|
||||||
@@ -2295,6 +2316,52 @@ pub fn session_register_gpu_texture(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn session_unregister_pixelbuffer_texture(
|
||||||
|
session_id: SessionID,
|
||||||
|
display: usize,
|
||||||
|
ptr: usize,
|
||||||
|
) -> SyncReturn<()> {
|
||||||
|
SyncReturn(super::flutter::session_unregister_pixelbuffer_texture(
|
||||||
|
session_id, display, ptr,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_unregister_gpu_texture(
|
||||||
|
session_id: SessionID,
|
||||||
|
display: usize,
|
||||||
|
ptr: usize,
|
||||||
|
) -> SyncReturn<()> {
|
||||||
|
SyncReturn(super::flutter::session_unregister_gpu_texture(
|
||||||
|
session_id, display, ptr,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_set_render_visible(session_id: SessionID, visible: bool) -> SyncReturn<()> {
|
||||||
|
SyncReturn(super::flutter::session_set_render_visible(
|
||||||
|
session_id, visible,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_texture_render_probe_supported() -> SyncReturn<bool> {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
return SyncReturn(super::flutter::texture_render_probe_supported());
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
SyncReturn(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_push_texture_probe_frame(ptr: usize) -> SyncReturn<()> {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
super::flutter::push_texture_probe_frame(ptr);
|
||||||
|
SyncReturn(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_get_texture_probe_consumed(ptr: usize) -> SyncReturn<u64> {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
return SyncReturn(super::flutter::get_texture_probe_consumed(ptr));
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
SyncReturn(0)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn query_onlines(ids: Vec<String>) {
|
pub fn query_onlines(ids: Vec<String>) {
|
||||||
let _ = flutter::async_tasks::query_onlines(ids);
|
let _ = flutter::async_tasks::query_onlines(ids);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,15 @@ pub fn get_option<T: AsRef<str>>(key: T) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watchdog/probe breakage record: a failure forces texture render off on all
|
||||||
|
// desktop platforms (even an explicit "Y") — the live fallback relies on it;
|
||||||
|
// toggling the option or a passing probe clears it.
|
||||||
|
#[inline]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn texture_render_health_failed() -> bool {
|
||||||
|
LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER_HEALTH).starts_with("failed")
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn use_texture_render() -> bool {
|
pub fn use_texture_render() -> bool {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
@@ -183,28 +192,33 @@ pub fn use_texture_render() -> bool {
|
|||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
return cfg!(feature = "flutter")
|
|
||||||
&& LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
return cfg!(feature = "flutter")
|
|
||||||
&& LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N";
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
{
|
||||||
if !cfg!(feature = "flutter") {
|
if !cfg!(feature = "flutter") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
|
if texture_render_health_failed() {
|
||||||
#[cfg(debug_assertions)]
|
return false;
|
||||||
let default_texture = true;
|
}
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
let default_texture = crate::platform::is_win_10_or_greater();
|
#[cfg(target_os = "macos")]
|
||||||
if default_texture {
|
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
||||||
LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"
|
|
||||||
} else {
|
#[cfg(target_os = "linux")]
|
||||||
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N";
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let default_texture = true;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let default_texture = crate::platform::is_win_10_or_greater();
|
||||||
|
if default_texture {
|
||||||
|
LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"
|
||||||
|
} else {
|
||||||
|
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user