mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-10 14:31:02 +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 kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||
const String kOptionTextureRender = "use-texture-render";
|
||||
const String kOptionTextureRenderHealth = "texture-render-health";
|
||||
const String kOptionD3DRender = "allow-d3d-render";
|
||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||
const String kOptionOpenInWindows = "allow-open-in-windows";
|
||||
|
||||
@@ -25,6 +25,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:window_size/window_size.dart' as window_size;
|
||||
import '../widgets/button.dart';
|
||||
import '../widgets/texture_render_probe.dart';
|
||||
|
||||
class DesktopHomePage extends StatefulWidget {
|
||||
const DesktopHomePage({Key? key}) : super(key: key);
|
||||
@@ -60,15 +61,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final isIncomingOnly = bind.isIncomingOnly();
|
||||
return _buildBlock(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return Stack(
|
||||
children: [
|
||||
buildLeftPane(context),
|
||||
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
||||
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
||||
_buildBlock(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildLeftPane(context),
|
||||
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
||||
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
||||
],
|
||||
)),
|
||||
const Positioned(left: 0, top: 0, child: TextureRenderProbe()),
|
||||
],
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBlock({required Widget child}) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import '../../common/shared_state.dart';
|
||||
import '../../utils/image.dart';
|
||||
import '../widgets/remote_toolbar.dart';
|
||||
import '../widgets/kb_layout_type_chooser.dart';
|
||||
import '../widgets/raster_stall_monitor.dart';
|
||||
import '../widgets/tabbar_widget.dart';
|
||||
import 'macos_full_screen_focus_recovery.dart';
|
||||
|
||||
@@ -156,6 +157,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
widget.tabController?.state.listen(_onMacOSTabStateChanged);
|
||||
}
|
||||
Get.put<FFI>(_ffi, tag: widget.id);
|
||||
RasterStallMonitor.start();
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
_ffi.canvasModel.activateLocalCursor();
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
@@ -647,7 +649,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
// Clear callback reference to prevent memory leaks and stale references
|
||||
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
||||
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||
_ffi.textureModel.onRemotePageDispose();
|
||||
if (closeSession && !isMacOS) {
|
||||
// ensure we leave this session, this is a double check
|
||||
// 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 '../widgets/remote_toolbar.dart';
|
||||
import '../widgets/kb_layout_type_chooser.dart';
|
||||
import '../widgets/raster_stall_monitor.dart';
|
||||
import '../widgets/tabbar_widget.dart';
|
||||
|
||||
import 'package:flutter_hbb/native/custom_cursor.dart'
|
||||
@@ -102,6 +103,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
super.initState();
|
||||
_ffi = FFI(widget.sessionId);
|
||||
Get.put<FFI>(_ffi, tag: widget.id);
|
||||
RasterStallMonitor.start();
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||
@@ -222,7 +224,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
// https://github.com/flutter/flutter/issues/64935
|
||||
super.dispose();
|
||||
debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
|
||||
_ffi.textureModel.onViewCameraPageDispose(closeSession);
|
||||
_ffi.textureModel.onViewCameraPageDispose();
|
||||
if (closeSession) {
|
||||
// ensure we leave this session, this is a double check
|
||||
_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/view_camera_page.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/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -388,6 +389,7 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
void onWindowMinimize() {
|
||||
stateGlobal.setMinimized(true);
|
||||
stateGlobal.setMaximized(false);
|
||||
_updateSessionsRenderVisible(false);
|
||||
super.onWindowMinimize();
|
||||
}
|
||||
|
||||
@@ -395,6 +397,7 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
void onWindowMaximize() {
|
||||
stateGlobal.setMinimized(false);
|
||||
_setMaximized(true);
|
||||
_updateSessionsRenderVisible(true);
|
||||
super.onWindowMaximize();
|
||||
}
|
||||
|
||||
@@ -402,9 +405,34 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
void onWindowUnmaximize() {
|
||||
stateGlobal.setMinimized(false);
|
||||
_setMaximized(false);
|
||||
_updateSessionsRenderVisible(true);
|
||||
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 {
|
||||
try {
|
||||
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;
|
||||
SessionID? _sessionId;
|
||||
bool _destroying = false;
|
||||
bool _closed = false;
|
||||
int _ptr = 0;
|
||||
int? _id;
|
||||
|
||||
final textureRenderer = TextureRgbaRenderer();
|
||||
@@ -27,11 +29,22 @@ class _PixelbufferTexture {
|
||||
_textureKey = bind.getNextTextureKey();
|
||||
_sessionId = sessionId;
|
||||
|
||||
textureRenderer.createTexture(_textureKey).then((id) async {
|
||||
final textureKey = _textureKey;
|
||||
textureRenderer.createTexture(textureKey).then((id) async {
|
||||
_id = id;
|
||||
if (id != -1) {
|
||||
if (_closed) {
|
||||
// Destroyed while creation was still in flight (rapid
|
||||
// connect/disconnect); nobody else will close this texture.
|
||||
await textureRenderer.closeTexture(textureKey);
|
||||
return;
|
||||
}
|
||||
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
||||
final ptr = await textureRenderer.getTexturePtr(textureKey);
|
||||
if (_closed) {
|
||||
return;
|
||||
}
|
||||
_ptr = ptr;
|
||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||
debugPrint(
|
||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
||||
@@ -39,13 +52,16 @@ class _PixelbufferTexture {
|
||||
});
|
||||
}
|
||||
|
||||
destroy(bool unregisterTexture, FFI ffi) async {
|
||||
destroy(FFI ffi) async {
|
||||
_closed = true;
|
||||
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||
_destroying = true;
|
||||
if (unregisterTexture) {
|
||||
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
if (_ptr != 0) {
|
||||
// Compare-and-clear: only clears if Rust still holds this pointer
|
||||
// (#8016-safe); returning from this synchronous call also means no
|
||||
// push through the old pointer is still in flight.
|
||||
platformFFI.unregisterPixelbufferTexture(_sessionId!, display, _ptr);
|
||||
_ptr = 0;
|
||||
}
|
||||
await textureRenderer.closeTexture(_textureKey);
|
||||
_textureKey = -1;
|
||||
@@ -61,6 +77,7 @@ class _GpuTexture {
|
||||
SessionID? _sessionId;
|
||||
final support = bind.mainHasGpuTextureRender();
|
||||
bool _destroying = false;
|
||||
bool _closed = false;
|
||||
int _display = 0;
|
||||
int? _id;
|
||||
int? _output;
|
||||
@@ -79,9 +96,18 @@ class _GpuTexture {
|
||||
gpuTextureRenderer.registerTexture().then((id) async {
|
||||
_id = id;
|
||||
if (id != null) {
|
||||
if (_closed) {
|
||||
// Destroyed while creation was still in flight (rapid
|
||||
// connect/disconnect); nobody else will unregister this texture.
|
||||
await gpuTextureRenderer.unregisterTexture(id);
|
||||
return;
|
||||
}
|
||||
_textureId = id;
|
||||
ffi.textureModel.setGpuTextureId(display: d, id: id);
|
||||
final output = await gpuTextureRenderer.output(id);
|
||||
if (_closed) {
|
||||
return;
|
||||
}
|
||||
_output = output;
|
||||
if (output != null) {
|
||||
platformFFI.registerGpuTexture(sessionId, d, output);
|
||||
@@ -95,20 +121,22 @@ class _GpuTexture {
|
||||
}
|
||||
}
|
||||
|
||||
destroy(bool unregisterTexture, FFI ffi) async {
|
||||
destroy(FFI ffi) async {
|
||||
// must stop texture render, render unregistered texture cause crash
|
||||
_closed = true;
|
||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||
_destroying = true;
|
||||
if (unregisterTexture) {
|
||||
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
final output = _output;
|
||||
if (output != null) {
|
||||
// Compare-and-clear, see _PixelbufferTexture.destroy.
|
||||
platformFFI.unregisterGpuTexture(_sessionId!, _display, output);
|
||||
_output = null;
|
||||
}
|
||||
await gpuTextureRenderer.unregisterTexture(_textureId);
|
||||
_textureId = -1;
|
||||
_destroying = false;
|
||||
debugPrint(
|
||||
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$_output");
|
||||
"destroy gpu texture: peerId: ${ffi.id} display:$_display, textureId:$_id, output:$output");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,11 +228,11 @@ class TextureModel {
|
||||
tryRemoveTexture(int idx) {
|
||||
_control.remove(idx);
|
||||
if (_pixelbufferRenderTextures.containsKey(idx)) {
|
||||
_pixelbufferRenderTextures[idx]!.destroy(true, ffi);
|
||||
_pixelbufferRenderTextures[idx]!.destroy(ffi);
|
||||
_pixelbufferRenderTextures.remove(idx);
|
||||
}
|
||||
if (_gpuRenderTextures.containsKey(idx)) {
|
||||
_gpuRenderTextures[idx]!.destroy(true, ffi);
|
||||
_gpuRenderTextures[idx]!.destroy(ffi);
|
||||
_gpuRenderTextures.remove(idx);
|
||||
}
|
||||
}
|
||||
@@ -224,25 +252,25 @@ class TextureModel {
|
||||
}
|
||||
}
|
||||
|
||||
onRemotePageDispose(bool closeSession) async {
|
||||
onRemotePageDispose() async {
|
||||
final ffi = parent.target;
|
||||
if (ffi == null) return;
|
||||
for (final texture in _pixelbufferRenderTextures.values) {
|
||||
await texture.destroy(closeSession, ffi);
|
||||
await texture.destroy(ffi);
|
||||
}
|
||||
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;
|
||||
if (ffi == null) return;
|
||||
for (final texture in _pixelbufferRenderTextures.values) {
|
||||
await texture.destroy(closeSession, ffi);
|
||||
await texture.destroy(ffi);
|
||||
}
|
||||
for (final texture in _gpuRenderTextures.values) {
|
||||
await texture.destroy(closeSession, ffi);
|
||||
await texture.destroy(ffi);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -735,6 +735,11 @@ class FfiModel with ChangeNotifier {
|
||||
_handleUseTextureRender(
|
||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
|
||||
if (evt['reason'] == 'fallback') {
|
||||
// The Rust watchdog detected that pushed frames were never rendered
|
||||
// and switched this session to software rendering.
|
||||
showToast(translate('texture-render-fallback-tip'));
|
||||
}
|
||||
waitForFirstImage.value = true;
|
||||
isRefreshing = true;
|
||||
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
|
||||
|
||||
@@ -131,6 +131,12 @@ class PlatformFFI {
|
||||
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||
_ffiBind.sessionRegisterGpuTexture(
|
||||
sessionId: sessionId, display: display, ptr: ptr);
|
||||
void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) =>
|
||||
_ffiBind.sessionUnregisterPixelbufferTexture(
|
||||
sessionId: sessionId, display: display, ptr: ptr);
|
||||
void unregisterGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||
_ffiBind.sessionUnregisterGpuTexture(
|
||||
sessionId: sessionId, display: display, ptr: ptr);
|
||||
|
||||
/// Init the FFI class, loads the native Rust core library.
|
||||
Future<void> init(String appType) async {
|
||||
|
||||
@@ -136,6 +136,12 @@ class PlatformFFI {
|
||||
void registerGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||
_ffiBind.sessionRegisterGpuTexture(
|
||||
sessionId: sessionId, display: display, ptr: ptr);
|
||||
void unregisterPixelbufferTexture(SessionID sessionId, int display, int ptr) =>
|
||||
_ffiBind.sessionUnregisterPixelbufferTexture(
|
||||
sessionId: sessionId, display: display, ptr: ptr);
|
||||
void unregisterGpuTexture(SessionID sessionId, int display, int ptr) =>
|
||||
_ffiBind.sessionUnregisterGpuTexture(
|
||||
sessionId: sessionId, display: display, ptr: ptr);
|
||||
|
||||
Future<void> init(String appType) async {
|
||||
Completer completer = Completer();
|
||||
|
||||
@@ -1450,6 +1450,31 @@ class RustdeskImpl {
|
||||
required int ptr,
|
||||
dynamic hint}) {}
|
||||
|
||||
void sessionUnregisterPixelbufferTexture(
|
||||
{required UuidValue sessionId,
|
||||
required int display,
|
||||
required int ptr,
|
||||
dynamic hint}) {}
|
||||
|
||||
void sessionUnregisterGpuTexture(
|
||||
{required UuidValue sessionId,
|
||||
required int display,
|
||||
required int ptr,
|
||||
dynamic hint}) {}
|
||||
|
||||
void 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}) {
|
||||
return Future(() =>
|
||||
js.context.callMethod('setByName', ['query_onlines', jsonEncode(ids)]));
|
||||
|
||||
@@ -538,8 +538,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
|
||||
resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87"
|
||||
ref: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
|
||||
resolved-ref: "208619e750a5fd904c689a9babd6ccf0f7c1ca88"
|
||||
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
@@ -1298,8 +1298,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
||||
resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4"
|
||||
ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
|
||||
resolved-ref: "883326ddd4fb2af1484bf873b4ea856a0ac440bc"
|
||||
url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer"
|
||||
source: git
|
||||
version: "0.0.16"
|
||||
|
||||
@@ -88,13 +88,13 @@ dependencies:
|
||||
texture_rgba_renderer:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer
|
||||
ref: 42797e0f03141dc2b585f76c64a13974508058b4
|
||||
ref: 883326ddd4fb2af1484bf873b4ea856a0ac440bc
|
||||
percent_indicator: ^4.2.2
|
||||
dropdown_button2: ^2.0.0
|
||||
flutter_gpu_texture_renderer:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
||||
ref: 08a471bb8ceccdd50483c81cdfa8b81b07b14b87
|
||||
ref: 208619e750a5fd904c689a9babd6ccf0f7c1ca88
|
||||
uuid: ^3.0.7
|
||||
auto_size_text_field: ^2.2.1
|
||||
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},
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc, RwLock,
|
||||
atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed)
|
||||
@@ -269,26 +270,119 @@ pub type FlutterGpuTextureRendererPluginCApiSetTexture =
|
||||
#[cfg(feature = "vram")]
|
||||
pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64;
|
||||
|
||||
pub type FlutterRgbaRendererPluginGetConsumed = unsafe extern "C" fn(texture_rgba: *mut c_void) -> u64;
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
pub type FlutterGpuTextureRendererPluginCApiGetConsumed =
|
||||
unsafe extern "C" fn(output: *mut c_void) -> u64;
|
||||
|
||||
pub(super) type TextureRgbaPtr = usize;
|
||||
|
||||
// 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 {
|
||||
// TextureRgba pointer in flutter native.
|
||||
texture_rgba_ptr: TextureRgbaPtr,
|
||||
size: (usize, usize),
|
||||
size_mismatch_count: u32,
|
||||
#[cfg(feature = "vram")]
|
||||
gpu_output_ptr: usize,
|
||||
notify_render_type: Option<RenderType>,
|
||||
// Watchdog: frames pushed to a texture the engine never consumes mean
|
||||
// texture rendering is broken (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
|
||||
// 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)]
|
||||
struct VideoRenderer {
|
||||
is_support_multi_ui_session: bool,
|
||||
map_display_sessions: Arc<RwLock<HashMap<usize, DisplaySessionInfo>>>,
|
||||
map_display_sessions: Arc<RwLock<HashMap<usize, Arc<Mutex<DisplaySessionInfo>>>>>,
|
||||
// Latched by the watchdog (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")))]
|
||||
on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
get_consumed_func: Option<Symbol<'static, FlutterRgbaRendererPluginGetConsumed>>,
|
||||
#[cfg(feature = "vram")]
|
||||
on_texture_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiSetTexture>>,
|
||||
#[cfg(feature = "vram")]
|
||||
get_gpu_consumed_func: Option<Symbol<'static, FlutterGpuTextureRendererPluginCApiGetConsumed>>,
|
||||
}
|
||||
|
||||
impl Default for VideoRenderer {
|
||||
@@ -312,6 +406,17 @@ impl Default for VideoRenderer {
|
||||
None
|
||||
}
|
||||
};
|
||||
// Absent in older plugin builds; the watchdog just stays disabled.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let get_consumed_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN {
|
||||
Ok(lib) => unsafe {
|
||||
lib.symbol::<FlutterRgbaRendererPluginGetConsumed>(
|
||||
"FlutterRgbaRendererPluginGetConsumed",
|
||||
)
|
||||
.ok()
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
#[cfg(feature = "vram")]
|
||||
let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
||||
Ok(lib) => {
|
||||
@@ -333,14 +438,30 @@ impl Default for VideoRenderer {
|
||||
None
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "vram")]
|
||||
let get_gpu_consumed_func = match &*TEXTURE_GPU_RENDERER_PLUGIN {
|
||||
Ok(lib) => unsafe {
|
||||
lib.symbol::<FlutterGpuTextureRendererPluginCApiGetConsumed>(
|
||||
"FlutterGpuTextureRendererPluginCApiGetConsumed",
|
||||
)
|
||||
.ok()
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
Self {
|
||||
map_display_sessions: Default::default(),
|
||||
is_support_multi_ui_session: false,
|
||||
texture_render_failed: Default::default(),
|
||||
render_visible: Arc::new(AtomicBool::new(true)),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
on_rgba_func,
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
get_consumed_func,
|
||||
#[cfg(feature = "vram")]
|
||||
on_texture_func,
|
||||
#[cfg(feature = "vram")]
|
||||
get_gpu_consumed_func,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,19 +470,18 @@ impl VideoRenderer {
|
||||
#[inline]
|
||||
fn set_size(&mut self, display: usize, width: usize, height: usize) {
|
||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
||||
if let Some(info) = sessions_lock.get(&display) {
|
||||
let mut info = info.lock().unwrap();
|
||||
info.size = (width, height);
|
||||
info.size_mismatch_count = 0;
|
||||
info.notify_render_type = None;
|
||||
} else {
|
||||
sessions_lock.insert(
|
||||
display,
|
||||
DisplaySessionInfo {
|
||||
texture_rgba_ptr: usize::default(),
|
||||
Arc::new(Mutex::new(DisplaySessionInfo {
|
||||
size: (width, height),
|
||||
#[cfg(feature = "vram")]
|
||||
gpu_output_ptr: usize::default(),
|
||||
notify_render_type: None,
|
||||
},
|
||||
..Default::default()
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -369,7 +489,8 @@ impl VideoRenderer {
|
||||
fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) {
|
||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||
if ptr == 0 {
|
||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
||||
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||
let mut info = info_arc.lock().unwrap();
|
||||
if info.texture_rgba_ptr != usize::default() {
|
||||
info.texture_rgba_ptr = usize::default();
|
||||
}
|
||||
@@ -377,10 +498,12 @@ impl VideoRenderer {
|
||||
if info.gpu_output_ptr != usize::default() {
|
||||
return;
|
||||
}
|
||||
drop(info);
|
||||
sessions_lock.remove(&display);
|
||||
}
|
||||
sessions_lock.remove(&display);
|
||||
} else {
|
||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
||||
if let Some(info) = sessions_lock.get(&display) {
|
||||
let mut info = info.lock().unwrap();
|
||||
if info.texture_rgba_ptr != usize::default()
|
||||
&& info.texture_rgba_ptr != ptr as TextureRgbaPtr
|
||||
{
|
||||
@@ -392,38 +515,59 @@ impl VideoRenderer {
|
||||
}
|
||||
info.texture_rgba_ptr = ptr as _;
|
||||
info.notify_render_type = None;
|
||||
info.reset_watchdog();
|
||||
} else {
|
||||
if ptr != 0 {
|
||||
sessions_lock.insert(
|
||||
display,
|
||||
DisplaySessionInfo {
|
||||
texture_rgba_ptr: ptr as _,
|
||||
size: (0, 0),
|
||||
#[cfg(feature = "vram")]
|
||||
gpu_output_ptr: usize::default(),
|
||||
notify_render_type: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
let mut info = DisplaySessionInfo {
|
||||
texture_rgba_ptr: ptr as _,
|
||||
..Default::default()
|
||||
};
|
||||
info.reset_watchdog();
|
||||
sessions_lock.insert(display, Arc::new(Mutex::new(info)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")]
|
||||
pub fn register_gpu_output(&self, display: usize, ptr: usize) {
|
||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||
if ptr == 0 {
|
||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
||||
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||
let mut info = info_arc.lock().unwrap();
|
||||
if info.gpu_output_ptr != usize::default() {
|
||||
info.gpu_output_ptr = usize::default();
|
||||
}
|
||||
if info.texture_rgba_ptr != usize::default() {
|
||||
return;
|
||||
}
|
||||
drop(info);
|
||||
sessions_lock.remove(&display);
|
||||
}
|
||||
sessions_lock.remove(&display);
|
||||
} else {
|
||||
if let Some(info) = sessions_lock.get_mut(&display) {
|
||||
if let Some(info) = sessions_lock.get(&display) {
|
||||
let mut info = info.lock().unwrap();
|
||||
if info.gpu_output_ptr != usize::default() && info.gpu_output_ptr != ptr {
|
||||
log::error!(
|
||||
"gpu_output_ptr is not null and not equal to ptr, relace {} to {}",
|
||||
@@ -433,50 +577,91 @@ impl VideoRenderer {
|
||||
}
|
||||
info.gpu_output_ptr = ptr as _;
|
||||
info.notify_render_type = None;
|
||||
info.reset_watchdog();
|
||||
} else {
|
||||
if ptr != usize::default() {
|
||||
sessions_lock.insert(
|
||||
display,
|
||||
DisplaySessionInfo {
|
||||
texture_rgba_ptr: usize::default(),
|
||||
size: (0, 0),
|
||||
gpu_output_ptr: ptr,
|
||||
notify_render_type: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
let mut info = DisplaySessionInfo {
|
||||
gpu_output_ptr: ptr,
|
||||
..Default::default()
|
||||
};
|
||||
info.reset_watchdog();
|
||||
sessions_lock.insert(display, Arc::new(Mutex::new(info)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See unregister_pixelbuffer_texture for why this is compare-and-clear.
|
||||
#[cfg(feature = "vram")]
|
||||
pub fn unregister_gpu_output(&self, display: usize, ptr: usize) {
|
||||
if ptr == 0 {
|
||||
return;
|
||||
}
|
||||
let mut sessions_lock = self.map_display_sessions.write().unwrap();
|
||||
if let Some(info_arc) = sessions_lock.get(&display).cloned() {
|
||||
let mut info = info_arc.lock().unwrap();
|
||||
if info.gpu_output_ptr != ptr {
|
||||
return;
|
||||
}
|
||||
info.gpu_output_ptr = usize::default();
|
||||
if info.texture_rgba_ptr != usize::default() {
|
||||
return;
|
||||
}
|
||||
drop(info);
|
||||
sessions_lock.remove(&display);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn display_session_info(&self, display: usize) -> Option<Arc<Mutex<DisplaySessionInfo>>> {
|
||||
let read_lock = self.map_display_sessions.read().unwrap();
|
||||
if !self.is_support_multi_ui_session {
|
||||
read_lock.values().next().cloned()
|
||||
} else {
|
||||
read_lock.get(&display).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool {
|
||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
||||
let opt_info = if !self.is_support_multi_ui_session {
|
||||
write_lock.values_mut().next()
|
||||
} else {
|
||||
write_lock.get_mut(&display)
|
||||
};
|
||||
let Some(info) = opt_info else {
|
||||
let Some(info_arc) = self.display_session_info(display) else {
|
||||
return false;
|
||||
};
|
||||
let mut info = info_arc.lock().unwrap();
|
||||
if info.texture_rgba_ptr == usize::default() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if info.size.0 != rgba.w || info.size.1 != rgba.h {
|
||||
log::error!(
|
||||
"width/height mismatch: ({},{}) != ({},{})",
|
||||
info.size.0,
|
||||
info.size.1,
|
||||
rgba.w,
|
||||
rgba.h
|
||||
);
|
||||
// Peer info's handling is async and may be late than video frame's handling
|
||||
// Allow peer info not set, but not allow wrong width/height for correct local cursor position
|
||||
if info.size != (0, 0) {
|
||||
return false;
|
||||
info.size_mismatch_count += 1;
|
||||
if info.size_mismatch_count == 1 {
|
||||
log::error!(
|
||||
"width/height mismatch: ({},{}) != ({},{})",
|
||||
info.size.0,
|
||||
info.size.1,
|
||||
rgba.w,
|
||||
rgba.h
|
||||
);
|
||||
}
|
||||
// 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 {
|
||||
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) {
|
||||
info.notify_render_type = Some(RenderType::PixelBuffer);
|
||||
true
|
||||
@@ -500,21 +702,36 @@ impl VideoRenderer {
|
||||
|
||||
#[cfg(feature = "vram")]
|
||||
pub fn on_texture(&self, display: usize, texture: *mut c_void) -> bool {
|
||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
||||
let opt_info = if !self.is_support_multi_ui_session {
|
||||
write_lock.values_mut().next()
|
||||
} else {
|
||||
write_lock.get_mut(&display)
|
||||
};
|
||||
let Some(info) = opt_info else {
|
||||
let Some(info_arc) = self.display_session_info(display) else {
|
||||
return false;
|
||||
};
|
||||
let mut info = info_arc.lock().unwrap();
|
||||
if info.gpu_output_ptr == usize::default() {
|
||||
return false;
|
||||
}
|
||||
if let Some(func) = &self.on_texture_func {
|
||||
unsafe { func(info.gpu_output_ptr as _, texture) };
|
||||
}
|
||||
info.pushed_count += 1;
|
||||
// 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) {
|
||||
info.notify_render_type = Some(RenderType::Texture);
|
||||
true
|
||||
@@ -524,11 +741,10 @@ impl VideoRenderer {
|
||||
}
|
||||
|
||||
pub fn reset_all_display_render_type(&self) {
|
||||
let mut write_lock = self.map_display_sessions.write().unwrap();
|
||||
write_lock
|
||||
.values_mut()
|
||||
.map(|v| v.notify_render_type = None)
|
||||
.count();
|
||||
let read_lock = self.map_display_sessions.read().unwrap();
|
||||
for info in read_lock.values() {
|
||||
info.lock().unwrap().notify_render_type = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,9 +877,24 @@ impl FlutterHandler {
|
||||
}
|
||||
|
||||
pub fn update_use_texture_render(&self) {
|
||||
self.use_texture_render
|
||||
.store(crate::ui_interface::use_texture_render(), Ordering::Relaxed);
|
||||
let v = crate::ui_interface::use_texture_render();
|
||||
self.use_texture_render.store(v, Ordering::Relaxed);
|
||||
self.display_rgbas.write().unwrap().clear();
|
||||
if v {
|
||||
// Texture render was (re-)enabled; validate it afresh so a still
|
||||
// broken environment fails over again instead of staying black.
|
||||
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
||||
for info in session
|
||||
.renderer
|
||||
.map_display_sessions
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
{
|
||||
info.lock().unwrap().reset_watchdog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,12 +1118,13 @@ impl InvokeUiSession for FlutterHandler {
|
||||
if !self.use_texture_render.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
||||
for (session_id, session) in self.session_handlers.read().unwrap().iter() {
|
||||
if session.renderer.on_texture(display, texture) {
|
||||
if let Some(stream) = &session.event_stream {
|
||||
stream.add(EventToUI::Texture(display, true));
|
||||
}
|
||||
}
|
||||
Self::check_texture_render_failed(session_id, session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1262,16 +1494,31 @@ impl FlutterHandler {
|
||||
display: usize,
|
||||
rgba: &mut scrap::ImageRgb,
|
||||
) {
|
||||
for (_, session) in self.session_handlers.read().unwrap().iter() {
|
||||
for (session_id, session) in self.session_handlers.read().unwrap().iter() {
|
||||
if use_texture_render || session.displays.len() > 1 {
|
||||
if session.renderer.on_rgba(display, rgba) {
|
||||
if let Some(stream) = &session.event_stream {
|
||||
stream.add(EventToUI::Texture(display, false));
|
||||
}
|
||||
}
|
||||
Self::check_texture_render_failed(session_id, session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Consume the watchdog latch outside the per-frame hot path work; the
|
||||
// actual fallback (config write, decoder reset) runs on its own thread.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn check_texture_render_failed(session_id: &SessionID, session: &SessionHandler) {
|
||||
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.
|
||||
@@ -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]
|
||||
#[cfg(not(feature = "vram"))]
|
||||
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) {
|
||||
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);
|
||||
set_local_option(key, value.clone());
|
||||
let is_render_target =
|
||||
|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 {
|
||||
// An explicit user toggle gives texture rendering a fresh chance; a
|
||||
// stale failure record must not override it (the watchdog re-records
|
||||
// if the environment is still broken).
|
||||
set_local_option(
|
||||
config::keys::OPTION_TEXTURE_RENDER_HEALTH.to_owned(),
|
||||
"".to_owned(),
|
||||
);
|
||||
let session_event = [("v", &value)];
|
||||
for session in sessions::get_sessions() {
|
||||
if !is_render_target(&session) {
|
||||
@@ -2295,6 +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>) {
|
||||
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"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].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"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].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"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].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"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].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"),
|
||||
("Continue", "继续"),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", "纹理渲染失效,已自动切换为软件渲染。"),
|
||||
].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"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].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"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].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"),
|
||||
("Continue", "Weiter"),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].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"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].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."),
|
||||
("whitelist_cidr_tip", "CIDR notation is supported, e.g. 192.168.1.0/24"),
|
||||
("Your ip is blocked by the peer", "Your IP is blocked by the peer"),
|
||||
("texture-render-fallback-tip", "Texture rendering failed and was disabled. Using software rendering instead."),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "La notacio CIDR estas subtenata, ekzemple 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Se admite la notación CIDR, por ejemplo 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Toetatud on CIDR-tähistus, näiteks 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR notazioa onartzen da, adibidez 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "نماد CIDR پشتیبانی می شود، برای مثال 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR-merkintä on tuettu, esimerkiksi 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "La notation CIDR est prise en charge, par exemple 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "მხარდაჭერილია CIDR ჩანაწერი, მაგალითად 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR નોટેશન સપોર્ટેડ છે, ઉदાહરણ તરીકે 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "יש תמיכה בסימון CIDR, לדוגמה 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR नोटेशन समर्थित है, उदाहरण के लिए 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Podržan je CIDR zapis, primjerice 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "A CIDR jelölés támogatott, például 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Notasi CIDR didukung, misalnya 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "È supportata la notazione CIDR, ad esempio 192.168.1.0/24"),
|
||||
("Continue", "Continua"),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR 表記に対応しています。例: 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR 표기를 지원합니다. 예: 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR жазбасына қолдау көрсетіледі, мысалы 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Palaikomas CIDR žymėjimas, pavyzdžiui 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Tiek atbalstīts CIDR pieraksts, piemēram 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR നൊട്ടേഷൻ പിന്തുണയ്ക്കുന്നു, ഉദാഹരണത്തിന് 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR-notasjon støttes, for eksempel 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR-notatie wordt ondersteund, bijv. 192.168.1.0/24"),
|
||||
("Continue", "Doorgaan"),
|
||||
("Browser didn't open? Use the url below to sign in.", "Is de browser niet geopend? Gebruik onderstaande URL om in te loggen."),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Obsługiwana jest notacja CIDR, na przykład 192.168.1.0/24"),
|
||||
("Continue", "Kontynuuj"),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "A notação CIDR é suportada, por exemplo 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "A notação CIDR é suportada, por exemplo 192.168.1.0/24"),
|
||||
("Continue", "Continuar"),
|
||||
("Browser didn't open? Use the url below to sign in.", "O navegador não foi aberto? Use a URL abaixo para fazer login."),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Notația CIDR este acceptată, de exemplu 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Поддерживается нотация CIDR, например 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Sa notatzione CIDR est suportada, pro esempru 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Je podporovaný zápis CIDR, napríklad 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Podprt je zapis CIDR, na primer 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Mbështetet shënimi CIDR, për shembull 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Podržan je CIDR zapis, na primer 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR-notation stöds, till exempel 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR குறியீடு ஆதரிக்கப்படுகிறது, எடுத்துக்காட்டாக 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", ""),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "รองรับรูปแบบ CIDR เช่น 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "CIDR gösterimi desteklenir, örneğin 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "支援 CIDR 寫法,例如 192.168.1.0/24"),
|
||||
("Continue", "繼續"),
|
||||
("Browser didn't open? Use the url below to sign in.", "瀏覽器未開啟?請使用下方網址登入。"),
|
||||
("texture-render-fallback-tip", "紋理渲染失效,已自動切換為軟體渲染。"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Підтримується нотація CIDR, наприклад 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -775,5 +775,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("whitelist_cidr_tip", "Hỗ trợ ký hiệu CIDR, ví dụ 192.168.1.0/24"),
|
||||
("Continue", ""),
|
||||
("Browser didn't open? Use the url below to sign in.", ""),
|
||||
("texture-render-fallback-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
pub fn use_texture_render() -> bool {
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -183,28 +192,33 @@ pub fn use_texture_render() -> bool {
|
||||
#[cfg(target_os = "ios")]
|
||||
return false;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return cfg!(feature = "flutter")
|
||||
&& LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return cfg!(feature = "flutter")
|
||||
&& LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
if !cfg!(feature = "flutter") {
|
||||
return false;
|
||||
}
|
||||
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
|
||||
#[cfg(debug_assertions)]
|
||||
let default_texture = true;
|
||||
#[cfg(not(debug_assertions))]
|
||||
let default_texture = crate::platform::is_win_10_or_greater();
|
||||
if default_texture {
|
||||
LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"
|
||||
} else {
|
||||
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
||||
if texture_render_health_failed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
|
||||
#[cfg(debug_assertions)]
|
||||
let default_texture = true;
|
||||
#[cfg(not(debug_assertions))]
|
||||
let default_texture = crate::platform::is_win_10_or_greater();
|
||||
if default_texture {
|
||||
LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"
|
||||
} else {
|
||||
return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user