-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented GLib-based dispatcher (#17281)
* Implemented GLib-based dispatcher This should allow running Avalonia on the same thread as another UI toolkit that supports running on top of GRunLoop (e. g. GTK) * Force-drain the X11 event queue, since g_runloop_quit won't exit the loop otherwise #Conflicts: # src/Avalonia.X11/X11Platform.cs
- Loading branch information
Showing
12 changed files
with
654 additions
and
187 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
#nullable enable | ||
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 | ||
{ | ||
/* | ||
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<ManagedLoopFrame> _runLoopStack = new(); | ||
|
||
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); | ||
private uint? _glibTimerSourceTag; | ||
|
||
public GlibDispatcherImpl(AvaloniaX11Platform platform) | ||
{ | ||
_platform = platform; | ||
_x11Events = new X11EventDispatcher(platform); | ||
var unixFdId = g_unix_fd_add_full(G_PRIORITY_DEFAULT, _x11Events.Fd, GIOCondition.G_IO_IN, | ||
X11SourceCallback); | ||
// We can trigger a nested event loop when handling X11 events, so we need to mark the source as recursive | ||
var unixFdSource = g_main_context_find_source_by_id(IntPtr.Zero, unixFdId); | ||
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); | ||
} | ||
|
||
private void CheckSignaled() | ||
{ | ||
lock (_signalLock) | ||
{ | ||
if (!_signaled) | ||
return; | ||
_signaled = false; | ||
} | ||
|
||
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; | ||
try | ||
{ | ||
// Completely drain X11 socket while we are at it | ||
while (_x11Events.IsPending) | ||
{ | ||
// If we don't actually drain our X11 socket, GLib _will_ call us again even if | ||
// we request the run loop to quit | ||
_x11Events.DispatchX11Events(CancellationToken.None); | ||
if (!token.IsCancellationRequested) | ||
{ | ||
while (_platform.EventGrouperDispatchQueue.HasJobs) | ||
{ | ||
CheckSignaled(); | ||
_platform.EventGrouperDispatchQueue.DispatchNext(); | ||
} | ||
|
||
_x11Events.Flush(); | ||
} | ||
} | ||
} | ||
catch (Exception e) | ||
{ | ||
HandleException(e); | ||
} | ||
|
||
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.ExterinalGLibMainLoopExceptionLogger; | ||
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<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); | ||
} | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading; | ||
using static Avalonia.X11.XLib; | ||
namespace Avalonia.X11; | ||
|
||
internal class X11EventDispatcher | ||
{ | ||
private readonly AvaloniaX11Platform _platform; | ||
private readonly IntPtr _display; | ||
|
||
public delegate void EventHandler(ref XEvent xev); | ||
public int Fd { get; } | ||
private readonly Dictionary<IntPtr, EventHandler> _eventHandlers; | ||
|
||
public X11EventDispatcher(AvaloniaX11Platform platform) | ||
{ | ||
_platform = platform; | ||
_display = platform.Display; | ||
_eventHandlers = platform.Windows; | ||
Fd = XLib.XConnectionNumber(_display); | ||
} | ||
|
||
public bool IsPending => XPending(_display) != 0; | ||
|
||
public unsafe void DispatchX11Events(CancellationToken cancellationToken) | ||
{ | ||
while (IsPending) | ||
{ | ||
if (cancellationToken.IsCancellationRequested) | ||
return; | ||
|
||
XNextEvent(_display, out var xev); | ||
if(XFilterEvent(ref xev, IntPtr.Zero)) | ||
continue; | ||
|
||
if (xev.type == XEventName.GenericEvent) | ||
XGetEventData(_display, &xev.GenericEventCookie); | ||
try | ||
{ | ||
if (xev.type == XEventName.GenericEvent) | ||
{ | ||
if (_platform.XI2 != null && _platform.Info.XInputOpcode == | ||
xev.GenericEventCookie.extension) | ||
{ | ||
_platform.XI2.OnEvent((XIEvent*)xev.GenericEventCookie.data); | ||
} | ||
} | ||
else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler)) | ||
handler(ref xev); | ||
} | ||
finally | ||
{ | ||
if (xev.type == XEventName.GenericEvent && xev.GenericEventCookie.data != null) | ||
XFreeEventData(_display, &xev.GenericEventCookie); | ||
} | ||
} | ||
Flush(); | ||
} | ||
|
||
public void Flush() => XFlush(_display); | ||
} |
Oops, something went wrong.