diff --git a/src/Avalonia.Wayland/Avalonia.Wayland.csproj b/src/Avalonia.Wayland/Avalonia.Wayland.csproj
index 2c9e941cac..4b4ebfece8 100644
--- a/src/Avalonia.Wayland/Avalonia.Wayland.csproj
+++ b/src/Avalonia.Wayland/Avalonia.Wayland.csproj
@@ -17,6 +17,8 @@
+
+
diff --git a/src/Avalonia.Wayland/WaylandGlibDispatcher.cs b/src/Avalonia.Wayland/WaylandGlibDispatcher.cs
new file mode 100644
index 0000000000..a1d513d5c6
--- /dev/null
+++ b/src/Avalonia.Wayland/WaylandGlibDispatcher.cs
@@ -0,0 +1,19 @@
+using System;
+using Avalonia.X11.Dispatching;
+
+namespace Avalonia.Wayland;
+
+///
+/// GLib (GMainLoop) based UI-thread dispatcher for the Wayland backend, enabled via
+/// . It lets Avalonia share a GLib main loop with GLib/GTK
+/// based libraries on the UI thread. Unlike X11 it attaches no platform event source of its own: the Wayland
+/// connection is owned and pumped by the worker thread, which posts input/events back via the dispatcher, so the
+/// base class' signaling/timer/background machinery is all the UI thread needs.
+///
+internal sealed class WaylandGlibDispatcher : GlibDispatcherImplBase
+{
+ public WaylandGlibDispatcher(Action? externalExceptionLogger)
+ : base(externalExceptionLogger)
+ {
+ }
+}
diff --git a/src/Avalonia.Wayland/WaylandPlatform.cs b/src/Avalonia.Wayland/WaylandPlatform.cs
index dc42c17b6a..01e608ef92 100644
--- a/src/Avalonia.Wayland/WaylandPlatform.cs
+++ b/src/Avalonia.Wayland/WaylandPlatform.cs
@@ -28,7 +28,10 @@ class WaylandPlatform
// that force an X11 fallback when the compositor lacks them.
var inputDispatchQueue = new AutomaticRawEventGrouperDispatchQueue();
- Dispatcher.InitializeUIThreadDispatcher(new ManagedDispatcherImpl(null));
+ IDispatcherImpl dispatcherImpl = options.UseGLibMainLoop
+ ? new WaylandGlibDispatcher(options.ExternalGLibMainLoopExceptionLogger)
+ : new ManagedDispatcherImpl(null);
+ Dispatcher.InitializeUIThreadDispatcher(dispatcherImpl);
var worker = new WaylandWorker(inputDispatchQueue);
diff --git a/src/Avalonia.Wayland/WaylandPlatformOptions.cs b/src/Avalonia.Wayland/WaylandPlatformOptions.cs
index 313b79e9e8..3c50426239 100644
--- a/src/Avalonia.Wayland/WaylandPlatformOptions.cs
+++ b/src/Avalonia.Wayland/WaylandPlatformOptions.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.OpenGL;
@@ -72,4 +73,21 @@ public class WaylandPlatformOptions
/// decides based on compositor and driver capabilities.
///
public bool? UseDmabufSwapchain { get; set; }
+
+ ///
+ /// If this option is set to true, a GMainLoop and GSource based dispatcher implementation will be used for the
+ /// UI thread instead of the default managed one.
+ /// Use this if you need to use GLib-based libraries on the main thread.
+ ///
+ public bool UseGLibMainLoop { get; set; }
+
+ ///
+ /// If Avalonia is in control of a run loop, we propagate exceptions by stopping the run loop frame
+ /// and rethrowing an exception. However, if there is no Avalonia-controlled run loop frame,
+ /// there is no way to report such exceptions, since allowing those to escape native->managed call boundary
+ /// will likely brick GLib machinery since it's not aware of managed Exceptions.
+ /// This property allows to inspect such exceptions before they will be ignored.
+ /// Only used when is enabled.
+ ///
+ public Action? ExternalGLibMainLoopExceptionLogger { get; set; }
}
\ No newline at end of file
diff --git a/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs
index f3a73940fe..c39170c9ae 100644
--- a/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs
+++ b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs
@@ -1,87 +1,15 @@
using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Runtime.ExceptionServices;
using System.Threading;
-using Avalonia.Logging;
-using Avalonia.Threading;
using static Avalonia.X11.Interop.Glib;
namespace Avalonia.X11.Dispatching;
-internal class GlibDispatcherImpl :
- IDispatcherImplWithExplicitBackgroundProcessing,
- IControlledDispatcherImpl,
- IX11PlatformDispatcher
+internal class GlibDispatcherImpl : GlibDispatcherImplBase, IX11PlatformDispatcher
{
- /*
- GLib priorities and Avalonia priorities are a bit different. Avalonia follows the WPF model when there
- are "background" and "foreground" priority groups. Foreground jobs are executed before any user input processing,
- background jobs are executed strictly after user input processing.
-
- GLib has numeric priorities that are used in the following way:
- -100 G_PRIORITY_HIGH - "high" priority sources, not really used by GLib/GTK
- 0 G_PRIORITY_DEFAULT - polling X11 events (GTK) and default value for g_timeout_add
- 100 G_PRIORITY_HIGH_IDLE without a clear definition, used as an anchor value of sorts
- 110 Resize/layout operations (GTK)
- 120 Render operations (GTK)
- 200 G_PRIORITY_DEFAULT_IDLE - "idle" priority sources
-
- So, unlike Avalonia, GTK puts way higher priority on input processing, then does resize/layout/render
-
- So, to map our model to GLib we do the following:
- - foreground jobs (including grouped user events) are executed with (-1) priority (_before_ any normal GLib jobs)
- - X11 socket is polled with G_PRIORITY_DEFAULT, all X11 events are read until socket is empty,
- we also group input events at that stage (this matches our epoll-based dispatcher)
- - background jobs are executed with G_PRIORITY_DEFAULT_IDLE, so they would have lower priority than GTK
- foreground jobs
-
- Unfortunately we can't detect if there are pending _non-idle_ GLib jobs using g_main_context_pending, since
- - g_main_context_pending doesn't accept max_priority argument
- - even if it did, that would still involve a syscall to the kernel to poll for fds anyway
-
- So we just report that we don't support pending input query and let the dispatcher to
- call RequestBackgroundProcessing every time, which results in g_idle_add call for every background job.
- Background jobs are expected to be relatively expensive to execute since on Windows
- MsgWaitForMultipleObjectsEx results isn't really free too.
-
- For signaling (aka waking up dispatcher for processing _high_ priority jobs we are using
- g_idle_add_full with (-1) priority. While the naming suggests that it would enqueue an idle job,
- it actually adds an always-triggered source that would be called before other sources with lower priority.
-
- For timers we are using a simple g_timeout_add_full and discard the previous one when dispatcher requests
- an update
-
- Since GLib dispatches event sources in batches, we force-check for "signaled" flag to run high-prio jobs
- whenever we get control back from GLib. We can still occasionally get GTK code to run before high-prio
- Avalonia-jobs, but that should be fine since the point is to keep Avalonia-based jobs ordered properly
- and to not have our low-priority jobs to prevent GLib-based code from running its own "foreground" jobs
-
- Another implementation note here is that GLib (just as any other C library) is NOT aware of C# exceptions,
- so we are NOT allowed to have exceptions to escape native->managed call boundary. So we have exception handlers
- that try to propagate those to the nearest run loop frame that was initiated by Avalonia.
-
- If there is no such frame, we have no choice but to log/swallow those
- */
-
private readonly AvaloniaX11Platform _platform;
-
- // Note that we can't use g_main_context_is_owner outside a run loop, since context doesn't really have an
- // inherent owner when run loop is not running and the context isn't explicitly "locked", so we just assume that
- // the app author is initializing Avalonia on the intended UI thread and won't migrate the default run loop
- // to a different thread
- private readonly Thread _mainThread = Thread.CurrentThread;
-
private readonly X11EventDispatcher _x11Events;
- private bool _signaled;
- private bool _signaledSourceAdded;
- private readonly object _signalLock = new();
- private readonly Stack _runLoopStack = new();
-
- private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
- private uint? _glibTimerSourceTag;
public GlibDispatcherImpl(AvaloniaX11Platform platform)
+ : base(platform.Options.ExternalGLibMainLoopExceptionLogger)
{
_platform = platform;
_x11Events = new X11EventDispatcher(platform);
@@ -92,96 +20,14 @@ internal class GlibDispatcherImpl :
g_source_set_can_recurse(unixFdSource, 1);
}
- public bool CurrentThreadIsLoopThread => _mainThread == Thread.CurrentThread;
-
- public event Action? Signaled;
- public void Signal()
- {
- lock (_signalLock)
- {
- if(_signaled)
- return;
- _signaled = true;
- if(_signaledSourceAdded)
- return;
- _signaledSourceAdded = true;
- }
- g_idle_add_full(G_PRIORITY_DEFAULT - 1, SignalSourceCallback);
- }
+ protected override void Flush() => _x11Events.Flush();
- private void CheckSignaled()
- {
- lock (_signalLock)
- {
- if (!_signaled)
- return;
- _signaled = false;
- }
+ public override bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending;
- try
- {
- Signaled?.Invoke();
- }
- catch (Exception e)
- {
- HandleException(e);
- }
- _x11Events.Flush();
- }
-
- private bool SignalSourceCallback()
- {
- lock (_signalLock)
- {
- _signaledSourceAdded = false;
- }
- CheckSignaled();
- return false;
- }
-
- public event Action? Timer;
- public long Now => _stopwatch.ElapsedMilliseconds;
-
- public void UpdateTimer(long? dueTimeInMs)
- {
- if (_glibTimerSourceTag.HasValue)
- {
- g_source_remove(_glibTimerSourceTag.Value);
- _glibTimerSourceTag = null;
- }
-
- if (dueTimeInMs == null)
- return;
-
- var interval = (uint)Math.Max(0, (int)Math.Min(int.MaxValue, dueTimeInMs.Value - Now));
- _glibTimerSourceTag = g_timeout_add_once(interval, TimerCallback);
- }
-
- private void TimerCallback()
- {
- try
- {
- Timer?.Invoke();
- }
- catch (Exception e)
- {
- HandleException(e);
- }
- _x11Events.Flush();
- }
-
- public event Action? ReadyForBackgroundProcessing;
-
- public void RequestBackgroundProcessing() =>
- g_idle_add_once(() => ReadyForBackgroundProcessing?.Invoke());
-
- public bool CanQueryPendingInput => false;
- public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending;
-
private bool X11SourceCallback(int i, GIOCondition gioCondition)
{
CheckSignaled();
- var token = _runLoopStack.Count > 0 ? _runLoopStack.Peek().Cancelled : CancellationToken.None;
+ var token = CurrentLoopCancellation;
try
{
// Completely drain X11 socket while we are at it
@@ -209,106 +55,6 @@ internal class GlibDispatcherImpl :
return true;
}
-
- public void RunLoop(CancellationToken token)
- {
- if(token.IsCancellationRequested)
- return;
-
- using var loop = new ManagedLoopFrame(token);
- _runLoopStack.Push(loop);
- loop.Run();
- _runLoopStack.Pop();
-
- // Propagate any managed exceptions that we've captured from this frame
- if(loop.Exceptions.Count == 1)
- loop.Exceptions[0].Throw();
- else if (loop.Exceptions.Count > 1)
- throw new AggregateException(loop.Exceptions.Select(x => x.SourceException));
- }
-
- void HandleException(Exception e)
- {
- if (_runLoopStack.Count > 0)
- {
- var frame = _runLoopStack.Peek();
- frame.Exceptions.Add(ExceptionDispatchInfo.Capture(e));
- frame.Stop();
- }
- else
- {
- var externalLogger = _platform.Options.ExternalGLibMainLoopExceptionLogger;
- if (externalLogger != null)
- externalLogger.Invoke(e);
- else
- Logger.TryGet(LogEventLevel.Error, LogArea.Control)
- ?.Log("Dispatcher", "Unhandled exception: {exception}", e);
- }
- }
-
- private class ManagedLoopFrame : IDisposable
- {
- private readonly CancellationToken _externalToken;
- private CancellationTokenSource? _internalTokenSource;
- public CancellationToken Cancelled { get; private set; }
-
- private readonly IntPtr _loop = g_main_loop_new(IntPtr.Zero, 1);
- public List Exceptions { get; } = new();
- private readonly object _destroyLock = new();
- private bool _disposed;
-
- public ManagedLoopFrame(CancellationToken token)
- {
- _externalToken = token;
- }
-
- public void Stop()
- {
- try
- {
- _internalTokenSource?.Cancel();
- }
- catch
- {
- // Ignore
- }
- }
-
- public void Run()
- {
- if (_externalToken.IsCancellationRequested)
- return;
- using (_internalTokenSource = new())
- using (var composite =
- CancellationTokenSource.CreateLinkedTokenSource(_externalToken, _internalTokenSource.Token))
- {
- Cancelled = composite.Token;
- using (Cancelled.Register(() =>
- {
- lock (_destroyLock)
- {
- if (_disposed)
- return;
- g_main_loop_quit(_loop);
- }
- }))
- {
- g_main_loop_run(_loop);
- }
- }
- }
-
- public void Dispose()
- {
- lock (_destroyLock)
- {
- if(_disposed)
- return;
- _disposed = true;
- g_main_loop_unref(_loop);
- }
- }
- }
public X11EventDispatcher EventDispatcher => _x11Events;
}
diff --git a/src/Avalonia.X11/Dispatching/GlibDispatcherImplBase.cs b/src/Avalonia.X11/Dispatching/GlibDispatcherImplBase.cs
new file mode 100644
index 0000000000..241bbc67f9
--- /dev/null
+++ b/src/Avalonia.X11/Dispatching/GlibDispatcherImplBase.cs
@@ -0,0 +1,289 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using Avalonia.Logging;
+using Avalonia.Threading;
+using static Avalonia.X11.Interop.Glib;
+namespace Avalonia.X11.Dispatching;
+
+///
+/// Platform-agnostic GLib (GMainLoop/GSource) based dispatcher implementation. It maps Avalonia's dispatcher model
+/// onto GLib so Avalonia code can share a GLib main loop with GLib/GTK based libraries on the UI thread.
+/// It owns no platform event source by itself; backends derive from it to attach their own (e.g. the X11 socket).
+///
+internal abstract class GlibDispatcherImplBase :
+ IDispatcherImplWithExplicitBackgroundProcessing,
+ IControlledDispatcherImpl
+{
+ /*
+ GLib priorities and Avalonia priorities are a bit different. Avalonia follows the WPF model when there
+ are "background" and "foreground" priority groups. Foreground jobs are executed before any user input processing,
+ background jobs are executed strictly after user input processing.
+
+ GLib has numeric priorities that are used in the following way:
+ -100 G_PRIORITY_HIGH - "high" priority sources, not really used by GLib/GTK
+ 0 G_PRIORITY_DEFAULT - polling X11 events (GTK) and default value for g_timeout_add
+ 100 G_PRIORITY_HIGH_IDLE without a clear definition, used as an anchor value of sorts
+ 110 Resize/layout operations (GTK)
+ 120 Render operations (GTK)
+ 200 G_PRIORITY_DEFAULT_IDLE - "idle" priority sources
+
+ So, unlike Avalonia, GTK puts way higher priority on input processing, then does resize/layout/render
+
+ So, to map our model to GLib we do the following:
+ - foreground jobs (including grouped user events) are executed with (-1) priority (_before_ any normal GLib jobs)
+ - the platform event source is polled with G_PRIORITY_DEFAULT, all events are read until the socket is empty,
+ we also group input events at that stage (this matches our epoll-based dispatcher)
+ - background jobs are executed with G_PRIORITY_DEFAULT_IDLE, so they would have lower priority than GTK
+ foreground jobs
+
+ Unfortunately we can't detect if there are pending _non-idle_ GLib jobs using g_main_context_pending, since
+ - g_main_context_pending doesn't accept max_priority argument
+ - even if it did, that would still involve a syscall to the kernel to poll for fds anyway
+
+ So we just report that we don't support pending input query and let the dispatcher to
+ call RequestBackgroundProcessing every time, which results in g_idle_add call for every background job.
+ Background jobs are expected to be relatively expensive to execute since on Windows
+ MsgWaitForMultipleObjectsEx results isn't really free too.
+
+ For signaling (aka waking up dispatcher for processing _high_ priority jobs we are using
+ g_idle_add_full with (-1) priority. While the naming suggests that it would enqueue an idle job,
+ it actually adds an always-triggered source that would be called before other sources with lower priority.
+
+ For timers we are using a simple g_timeout_add_full and discard the previous one when dispatcher requests
+ an update
+
+ Since GLib dispatches event sources in batches, we force-check for "signaled" flag to run high-prio jobs
+ whenever we get control back from GLib. We can still occasionally get GTK code to run before high-prio
+ Avalonia-jobs, but that should be fine since the point is to keep Avalonia-based jobs ordered properly
+ and to not have our low-priority jobs to prevent GLib-based code from running its own "foreground" jobs
+
+ Another implementation note here is that GLib (just as any other C library) is NOT aware of C# exceptions,
+ so we are NOT allowed to have exceptions to escape native->managed call boundary. So we have exception handlers
+ that try to propagate those to the nearest run loop frame that was initiated by Avalonia.
+
+ If there is no such frame, we have no choice but to log/swallow those
+ */
+
+ // Note that we can't use g_main_context_is_owner outside a run loop, since context doesn't really have an
+ // inherent owner when run loop is not running and the context isn't explicitly "locked", so we just assume that
+ // the app author is initializing Avalonia on the intended UI thread and won't migrate the default run loop
+ // to a different thread
+ private readonly Thread _mainThread = Thread.CurrentThread;
+
+ private readonly Action? _externalExceptionLogger;
+ private bool _signaled;
+ private bool _signaledSourceAdded;
+ private readonly object _signalLock = new();
+ private readonly Stack _runLoopStack = new();
+
+ private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
+ private uint? _glibTimerSourceTag;
+
+ protected GlibDispatcherImplBase(Action? externalExceptionLogger)
+ {
+ _externalExceptionLogger = externalExceptionLogger;
+ }
+
+ public bool CurrentThreadIsLoopThread => _mainThread == Thread.CurrentThread;
+
+ public event Action? Signaled;
+ public void Signal()
+ {
+ lock (_signalLock)
+ {
+ if(_signaled)
+ return;
+ _signaled = true;
+ if(_signaledSourceAdded)
+ return;
+ _signaledSourceAdded = true;
+ }
+ g_idle_add_full(G_PRIORITY_DEFAULT - 1, SignalSourceCallback);
+ }
+
+ protected void CheckSignaled()
+ {
+ lock (_signalLock)
+ {
+ if (!_signaled)
+ return;
+ _signaled = false;
+ }
+
+ try
+ {
+ Signaled?.Invoke();
+ }
+ catch (Exception e)
+ {
+ HandleException(e);
+ }
+ Flush();
+ }
+
+ private bool SignalSourceCallback()
+ {
+ lock (_signalLock)
+ {
+ _signaledSourceAdded = false;
+ }
+ CheckSignaled();
+ return false;
+ }
+
+ public event Action? Timer;
+ public long Now => _stopwatch.ElapsedMilliseconds;
+
+ public void UpdateTimer(long? dueTimeInMs)
+ {
+ if (_glibTimerSourceTag.HasValue)
+ {
+ g_source_remove(_glibTimerSourceTag.Value);
+ _glibTimerSourceTag = null;
+ }
+
+ if (dueTimeInMs == null)
+ return;
+
+ var interval = (uint)Math.Max(0, (int)Math.Min(int.MaxValue, dueTimeInMs.Value - Now));
+ _glibTimerSourceTag = g_timeout_add_once(interval, TimerCallback);
+ }
+
+ private void TimerCallback()
+ {
+ try
+ {
+ Timer?.Invoke();
+ }
+ catch (Exception e)
+ {
+ HandleException(e);
+ }
+ Flush();
+ }
+
+ public event Action? ReadyForBackgroundProcessing;
+
+ public void RequestBackgroundProcessing() =>
+ g_idle_add_once(() => ReadyForBackgroundProcessing?.Invoke());
+
+ public virtual bool CanQueryPendingInput => false;
+ public virtual bool HasPendingInput => false;
+
+ ///
+ /// Flush any pending output to the platform event source after Avalonia jobs ran. Called whenever control
+ /// returns to GLib from a signaled/timer callback. The base implementation does nothing; backends that own a
+ /// socket (e.g. X11) override it to flush their connection.
+ ///
+ protected virtual void Flush()
+ {
+ }
+
+ /// The cancellation token of the innermost Avalonia-controlled run loop frame, or None when no frame
+ /// is running. Backends use it to stop draining their event source once a loop frame asked to quit.
+ protected CancellationToken CurrentLoopCancellation =>
+ _runLoopStack.Count > 0 ? _runLoopStack.Peek().Cancelled : CancellationToken.None;
+
+ public void RunLoop(CancellationToken token)
+ {
+ if(token.IsCancellationRequested)
+ return;
+
+ using var loop = new ManagedLoopFrame(token);
+ _runLoopStack.Push(loop);
+ loop.Run();
+ _runLoopStack.Pop();
+
+ // Propagate any managed exceptions that we've captured from this frame
+ if(loop.Exceptions.Count == 1)
+ loop.Exceptions[0].Throw();
+ else if (loop.Exceptions.Count > 1)
+ throw new AggregateException(loop.Exceptions.Select(x => x.SourceException));
+ }
+
+ protected void HandleException(Exception e)
+ {
+ if (_runLoopStack.Count > 0)
+ {
+ var frame = _runLoopStack.Peek();
+ frame.Exceptions.Add(ExceptionDispatchInfo.Capture(e));
+ frame.Stop();
+ }
+ else
+ {
+ if (_externalExceptionLogger != null)
+ _externalExceptionLogger.Invoke(e);
+ else
+ Logger.TryGet(LogEventLevel.Error, LogArea.Control)
+ ?.Log("Dispatcher", "Unhandled exception: {exception}", e);
+ }
+ }
+
+ private class ManagedLoopFrame : IDisposable
+ {
+ private readonly CancellationToken _externalToken;
+ private CancellationTokenSource? _internalTokenSource;
+ public CancellationToken Cancelled { get; private set; }
+
+ private readonly IntPtr _loop = g_main_loop_new(IntPtr.Zero, 1);
+ public List Exceptions { get; } = new();
+ private readonly object _destroyLock = new();
+ private bool _disposed;
+
+ public ManagedLoopFrame(CancellationToken token)
+ {
+ _externalToken = token;
+ }
+
+ public void Stop()
+ {
+ try
+ {
+ _internalTokenSource?.Cancel();
+ }
+ catch
+ {
+ // Ignore
+ }
+ }
+
+ public void Run()
+ {
+ if (_externalToken.IsCancellationRequested)
+ return;
+ using (_internalTokenSource = new())
+ using (var composite =
+ CancellationTokenSource.CreateLinkedTokenSource(_externalToken, _internalTokenSource.Token))
+ {
+ Cancelled = composite.Token;
+ using (Cancelled.Register(() =>
+ {
+ lock (_destroyLock)
+ {
+ if (_disposed)
+ return;
+ g_main_loop_quit(_loop);
+ }
+ }))
+ {
+ g_main_loop_run(_loop);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_destroyLock)
+ {
+ if(_disposed)
+ return;
+ _disposed = true;
+ g_main_loop_unref(_loop);
+ }
+ }
+ }
+}