committed by
GitHub
6 changed files with 337 additions and 260 deletions
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using Avalonia.X11.Dispatching; |
|||
|
|||
namespace Avalonia.Wayland; |
|||
|
|||
/// <summary>
|
|||
/// GLib (GMainLoop) based UI-thread dispatcher for the Wayland backend, enabled via
|
|||
/// <see cref="WaylandPlatformOptions.UseGLibMainLoop"/>. 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.
|
|||
/// </summary>
|
|||
internal sealed class WaylandGlibDispatcher : GlibDispatcherImplBase |
|||
{ |
|||
public WaylandGlibDispatcher(Action<Exception>? externalExceptionLogger) |
|||
: base(externalExceptionLogger) |
|||
{ |
|||
} |
|||
} |
|||
@ -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; |
|||
|
|||
/// <summary>
|
|||
/// 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).
|
|||
/// </summary>
|
|||
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<Exception>? _externalExceptionLogger; |
|||
private bool _signaled; |
|||
private bool _signaledSourceAdded; |
|||
private readonly object _signalLock = new(); |
|||
private readonly Stack<ManagedLoopFrame> _runLoopStack = new(); |
|||
|
|||
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); |
|||
private uint? _glibTimerSourceTag; |
|||
|
|||
protected GlibDispatcherImplBase(Action<Exception>? 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; |
|||
|
|||
/// <summary>
|
|||
/// 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.
|
|||
/// </summary>
|
|||
protected virtual void Flush() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>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.</summary>
|
|||
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<ExceptionDispatchInfo> 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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue