Browse Source
* 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.csrelease/11.2.0-rc2
committed by
Max Katz
12 changed files with 654 additions and 187 deletions
@ -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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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); |
|||
} |
|||
@ -0,0 +1,192 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Platform.Interop; |
|||
using static Avalonia.X11.Interop.Glib; |
|||
namespace Avalonia.X11.Interop; |
|||
|
|||
internal static unsafe class Glib |
|||
{ |
|||
private const string GlibName = "libglib-2.0.so.0"; |
|||
private const string GObjectName = "libgobject-2.0.so.0"; |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern void g_slist_free(GSList* data); |
|||
|
|||
[DllImport(GObjectName)] |
|||
private static extern void g_object_ref(IntPtr instance); |
|||
|
|||
[DllImport(GObjectName)] |
|||
private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal, |
|||
IntPtr handler, IntPtr userData, int flags); |
|||
|
|||
[DllImport(GObjectName)] |
|||
private static extern void g_object_unref(IntPtr instance); |
|||
|
|||
[DllImport(GObjectName)] |
|||
private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId); |
|||
|
|||
public const int G_PRIORITY_HIGH = -100; |
|||
public const int G_PRIORITY_DEFAULT = 0; |
|||
public const int G_PRIORITY_HIGH_IDLE = 100; |
|||
public const int G_PRIORITY_DEFAULT_IDLE = 200; |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern IntPtr g_main_loop_new(IntPtr context, int is_running); |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern void g_main_loop_quit(IntPtr loop); |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern void g_main_loop_run(IntPtr loop); |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern void g_main_loop_unref(IntPtr loop); |
|||
|
|||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] |
|||
public delegate void GOnceSourceFunc(IntPtr userData); |
|||
|
|||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] |
|||
public delegate int GSourceFunc(IntPtr userData); |
|||
|
|||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] |
|||
public delegate void GDestroyNotify(IntPtr userData); |
|||
|
|||
[DllImport(GlibName)] |
|||
private static extern int g_idle_add_once(GOnceSourceFunc cb, IntPtr userData); |
|||
|
|||
private static readonly GOnceSourceFunc s_onceSourceCb = (userData) => |
|||
{ |
|||
var h = GCHandle.FromIntPtr(userData); |
|||
var cb = (Action)h.Target!; |
|||
|
|||
h.Free(); |
|||
cb(); |
|||
}; |
|||
|
|||
public static void g_idle_add_once(Action cb) => |
|||
g_idle_add_once(s_onceSourceCb, GCHandle.ToIntPtr(GCHandle.Alloc(cb))); |
|||
|
|||
[DllImport(GlibName)] |
|||
private static extern uint g_timeout_add_once(uint interval, GOnceSourceFunc cb, IntPtr userData); |
|||
|
|||
public static uint g_timeout_add_once(uint interval, Action cb) => |
|||
g_timeout_add_once(interval, s_onceSourceCb, GCHandle.ToIntPtr(GCHandle.Alloc(cb))); |
|||
|
|||
private static readonly GDestroyNotify s_gcHandleDestroyNotify = handle => GCHandle.FromIntPtr(handle).Free(); |
|||
|
|||
private static readonly GSourceFunc s_sourceFuncDispatchCallback = |
|||
handle => ((Func<bool>)GCHandle.FromIntPtr(handle).Target)() ? 1 : 0; |
|||
|
|||
[DllImport(GlibName)] |
|||
private static extern uint g_idle_add_full (int priority, GSourceFunc function, IntPtr data, GDestroyNotify notify); |
|||
|
|||
public static uint g_idle_add_full(int priority, Func<bool> callback) |
|||
=> g_idle_add_full(priority, s_sourceFuncDispatchCallback, GCHandle.ToIntPtr(GCHandle.Alloc(callback)), |
|||
s_gcHandleDestroyNotify); |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern int g_source_get_can_recurse (IntPtr source); |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern void g_source_set_can_recurse (IntPtr source, int can_recurse); |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern IntPtr g_main_context_find_source_by_id (IntPtr context, uint source_id); |
|||
|
|||
[Flags] |
|||
public enum GIOCondition |
|||
{ |
|||
G_IO_IN = 1, |
|||
G_IO_OUT = 4, |
|||
G_IO_PRI = 2, |
|||
G_IO_ERR = 8, |
|||
G_IO_HUP = 16, |
|||
G_IO_NVAL = 32 |
|||
} |
|||
|
|||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] |
|||
public delegate int GUnixFDSourceFunc(int fd, GIOCondition condition, IntPtr user_data); |
|||
|
|||
private static readonly GUnixFDSourceFunc s_unixFdSourceCallback = (fd, cond, handle) => |
|||
((Func<int, GIOCondition, bool>)GCHandle.FromIntPtr(handle).Target)(fd, cond) ? 1 : 0; |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern uint g_unix_fd_add_full (int priority, |
|||
int fd, |
|||
GIOCondition condition, |
|||
GUnixFDSourceFunc function, |
|||
IntPtr user_data, |
|||
GDestroyNotify notify); |
|||
|
|||
public static uint g_unix_fd_add_full(int priority, int fd, GIOCondition condition, |
|||
Func<int, GIOCondition, bool> cb) => |
|||
g_unix_fd_add_full(priority, fd, condition, s_unixFdSourceCallback, GCHandle.ToIntPtr(GCHandle.Alloc(cb)), |
|||
s_gcHandleDestroyNotify); |
|||
|
|||
[DllImport(GlibName)] |
|||
public static extern int g_source_remove (uint tag); |
|||
|
|||
private class ConnectedSignal : IDisposable |
|||
{ |
|||
private readonly IntPtr _instance; |
|||
private GCHandle _handle; |
|||
private readonly ulong _id; |
|||
|
|||
public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id) |
|||
{ |
|||
_instance = instance; |
|||
g_object_ref(instance); |
|||
_handle = handle; |
|||
_id = id; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_handle.IsAllocated) |
|||
{ |
|||
g_signal_handler_disconnect(_instance, _id); |
|||
g_object_unref(_instance); |
|||
_handle.Free(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static IDisposable ConnectSignal<T>(IntPtr obj, string name, T handler) |
|||
{ |
|||
var handle = GCHandle.Alloc(handler); |
|||
var ptr = Marshal.GetFunctionPointerForDelegate(handler); |
|||
using (var utf = new Utf8Buffer(name)) |
|||
{ |
|||
var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); |
|||
if (id == 0) |
|||
throw new ArgumentException("Unable to connect to signal " + name); |
|||
return new ConnectedSignal(obj, handle, id); |
|||
} |
|||
} |
|||
|
|||
public static Task<T> RunOnGlibThread<T>(Func<T> action) |
|||
{ |
|||
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously); |
|||
g_timeout_add_once(0, () => |
|||
{ |
|||
try |
|||
{ |
|||
tcs.SetResult(action()); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
tcs.TrySetException(e); |
|||
} |
|||
}); |
|||
return tcs.Task; |
|||
} |
|||
} |
|||
|
|||
[StructLayout(LayoutKind.Sequential)] |
|||
internal unsafe struct GSList |
|||
{ |
|||
public readonly IntPtr Data; |
|||
public readonly GSList* Next; |
|||
} |
|||
Loading…
Reference in new issue