Skip to content

Commit 0dbf5c2

Browse files
kekekeksmaxkatz6
authored andcommitted
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
1 parent 7da26e2 commit 0dbf5c2

File tree

12 files changed

+654
-187
lines changed

12 files changed

+654
-187
lines changed

samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<ItemGroup>
1111
<Compile Include="..\..\src\Avalonia.X11\NativeDialogs\Gtk.cs" Link="NativeControls\Gtk\Gtk.cs" />
12+
<Compile Include="..\..\src\Avalonia.X11\Interop\Glib.cs" Link="NativeControls\Gtk\Glib.cs" />
1213
<Compile Include="..\..\src\Avalonia.Base\Platform\Interop\Utf8Buffer.cs" Link="NativeControls\Utf8Buffer.cs" />
1314
</ItemGroup>
1415

samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using Avalonia.X11.Interop;
66
using Avalonia.X11.NativeDialogs;
77
using static Avalonia.X11.NativeDialogs.Gtk;
8-
using static Avalonia.X11.NativeDialogs.Glib;
8+
using static Avalonia.X11.Interop.Glib;
99

1010
namespace ControlCatalog.NetCore;
1111

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Diagnostics;
5+
using System.Linq;
6+
using System.Runtime.ExceptionServices;
7+
using System.Threading;
8+
using Avalonia.Logging;
9+
using Avalonia.Threading;
10+
using static Avalonia.X11.Interop.Glib;
11+
namespace Avalonia.X11.Dispatching;
12+
13+
internal class GlibDispatcherImpl :
14+
IDispatcherImplWithExplicitBackgroundProcessing,
15+
IControlledDispatcherImpl
16+
{
17+
/*
18+
GLib priorities and Avalonia priorities are a bit different. Avalonia follows the WPF model when there
19+
are "background" and "foreground" priority groups. Foreground jobs are executed before any user input processing,
20+
background jobs are executed strictly after user input processing.
21+
22+
GLib has numeric priorities that are used in the following way:
23+
-100 G_PRIORITY_HIGH - "high" priority sources, not really used by GLib/GTK
24+
0 G_PRIORITY_DEFAULT - polling X11 events (GTK) and default value for g_timeout_add
25+
100 G_PRIORITY_HIGH_IDLE without a clear definition, used as an anchor value of sorts
26+
110 Resize/layout operations (GTK)
27+
120 Render operations (GTK)
28+
200 G_PRIORITY_DEFAULT_IDLE - "idle" priority sources
29+
30+
So, unlike Avalonia, GTK puts way higher priority on input processing, then does resize/layout/render
31+
32+
So, to map our model to GLib we do the following:
33+
- foreground jobs (including grouped user events) are executed with (-1) priority (_before_ any normal GLib jobs)
34+
- X11 socket is polled with G_PRIORITY_DEFAULT, all X11 events are read until socket is empty,
35+
we also group input events at that stage (this matches our epoll-based dispatcher)
36+
- background jobs are executed with G_PRIORITY_DEFAULT_IDLE, so they would have lower priority than GTK
37+
foreground jobs
38+
39+
Unfortunately we can't detect if there are pending _non-idle_ GLib jobs using g_main_context_pending, since
40+
- g_main_context_pending doesn't accept max_priority argument
41+
- even if it did, that would still involve a syscall to the kernel to poll for fds anyway
42+
43+
So we just report that we don't support pending input query and let the dispatcher to
44+
call RequestBackgroundProcessing every time, which results in g_idle_add call for every background job.
45+
Background jobs are expected to be relatively expensive to execute since on Windows
46+
MsgWaitForMultipleObjectsEx results isn't really free too.
47+
48+
For signaling (aka waking up dispatcher for processing _high_ priority jobs we are using
49+
g_idle_add_full with (-1) priority. While the naming suggests that it would enqueue an idle job,
50+
it actually adds an always-triggered source that would be called before other sources with lower priority.
51+
52+
For timers we are using a simple g_timeout_add_full and discard the previous one when dispatcher requests
53+
an update
54+
55+
Since GLib dispatches event sources in batches, we force-check for "signaled" flag to run high-prio jobs
56+
whenever we get control back from GLib. We can still occasionally get GTK code to run before high-prio
57+
Avalonia-jobs, but that should be fine since the point is to keep Avalonia-based jobs ordered properly
58+
and to not have our low-priority jobs to prevent GLib-based code from running its own "foreground" jobs
59+
60+
Another implementation note here is that GLib (just as any other C library) is NOT aware of C# exceptions,
61+
so we are NOT allowed to have exceptions to escape native->managed call boundary. So we have exception handlers
62+
that try to propagate those to the nearest run loop frame that was initiated by Avalonia.
63+
64+
If there is no such frame, we have no choice but to log/swallow those
65+
*/
66+
67+
private readonly AvaloniaX11Platform _platform;
68+
69+
// Note that we can't use g_main_context_is_owner outside a run loop, since context doesn't really have an
70+
// inherent owner when run loop is not running and the context isn't explicitly "locked", so we just assume that
71+
// the app author is initializing Avalonia on the intended UI thread and won't migrate the default run loop
72+
// to a different thread
73+
private readonly Thread _mainThread = Thread.CurrentThread;
74+
75+
private readonly X11EventDispatcher _x11Events;
76+
private bool _signaled;
77+
private bool _signaledSourceAdded;
78+
private readonly object _signalLock = new();
79+
private readonly Stack<ManagedLoopFrame> _runLoopStack = new();
80+
81+
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
82+
private uint? _glibTimerSourceTag;
83+
84+
public GlibDispatcherImpl(AvaloniaX11Platform platform)
85+
{
86+
_platform = platform;
87+
_x11Events = new X11EventDispatcher(platform);
88+
var unixFdId = g_unix_fd_add_full(G_PRIORITY_DEFAULT, _x11Events.Fd, GIOCondition.G_IO_IN,
89+
X11SourceCallback);
90+
// We can trigger a nested event loop when handling X11 events, so we need to mark the source as recursive
91+
var unixFdSource = g_main_context_find_source_by_id(IntPtr.Zero, unixFdId);
92+
g_source_set_can_recurse(unixFdSource, 1);
93+
}
94+
95+
public bool CurrentThreadIsLoopThread => _mainThread == Thread.CurrentThread;
96+
97+
public event Action? Signaled;
98+
public void Signal()
99+
{
100+
lock (_signalLock)
101+
{
102+
if(_signaled)
103+
return;
104+
_signaled = true;
105+
if(_signaledSourceAdded)
106+
return;
107+
_signaledSourceAdded = true;
108+
}
109+
g_idle_add_full(G_PRIORITY_DEFAULT - 1, SignalSourceCallback);
110+
}
111+
112+
private void CheckSignaled()
113+
{
114+
lock (_signalLock)
115+
{
116+
if (!_signaled)
117+
return;
118+
_signaled = false;
119+
}
120+
121+
try
122+
{
123+
Signaled?.Invoke();
124+
}
125+
catch (Exception e)
126+
{
127+
HandleException(e);
128+
}
129+
_x11Events.Flush();
130+
}
131+
132+
private bool SignalSourceCallback()
133+
{
134+
lock (_signalLock)
135+
{
136+
_signaledSourceAdded = false;
137+
}
138+
CheckSignaled();
139+
return false;
140+
}
141+
142+
public event Action? Timer;
143+
public long Now => _stopwatch.ElapsedMilliseconds;
144+
145+
public void UpdateTimer(long? dueTimeInMs)
146+
{
147+
if (_glibTimerSourceTag.HasValue)
148+
{
149+
g_source_remove(_glibTimerSourceTag.Value);
150+
_glibTimerSourceTag = null;
151+
}
152+
153+
if (dueTimeInMs == null)
154+
return;
155+
156+
var interval = (uint)Math.Max(0, (int)Math.Min(int.MaxValue, dueTimeInMs.Value - Now));
157+
_glibTimerSourceTag = g_timeout_add_once(interval, TimerCallback);
158+
}
159+
160+
private void TimerCallback()
161+
{
162+
try
163+
{
164+
Timer?.Invoke();
165+
}
166+
catch (Exception e)
167+
{
168+
HandleException(e);
169+
}
170+
_x11Events.Flush();
171+
}
172+
173+
public event Action? ReadyForBackgroundProcessing;
174+
175+
public void RequestBackgroundProcessing() =>
176+
g_idle_add_once(() => ReadyForBackgroundProcessing?.Invoke());
177+
178+
public bool CanQueryPendingInput => false;
179+
public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending;
180+
181+
private bool X11SourceCallback(int i, GIOCondition gioCondition)
182+
{
183+
CheckSignaled();
184+
var token = _runLoopStack.Count > 0 ? _runLoopStack.Peek().Cancelled : CancellationToken.None;
185+
try
186+
{
187+
// Completely drain X11 socket while we are at it
188+
while (_x11Events.IsPending)
189+
{
190+
// If we don't actually drain our X11 socket, GLib _will_ call us again even if
191+
// we request the run loop to quit
192+
_x11Events.DispatchX11Events(CancellationToken.None);
193+
if (!token.IsCancellationRequested)
194+
{
195+
while (_platform.EventGrouperDispatchQueue.HasJobs)
196+
{
197+
CheckSignaled();
198+
_platform.EventGrouperDispatchQueue.DispatchNext();
199+
}
200+
201+
_x11Events.Flush();
202+
}
203+
}
204+
}
205+
catch (Exception e)
206+
{
207+
HandleException(e);
208+
}
209+
210+
return true;
211+
}
212+
213+
public void RunLoop(CancellationToken token)
214+
{
215+
if(token.IsCancellationRequested)
216+
return;
217+
218+
using var loop = new ManagedLoopFrame(token);
219+
_runLoopStack.Push(loop);
220+
loop.Run();
221+
_runLoopStack.Pop();
222+
223+
// Propagate any managed exceptions that we've captured from this frame
224+
if(loop.Exceptions.Count == 1)
225+
loop.Exceptions[0].Throw();
226+
else if (loop.Exceptions.Count > 1)
227+
throw new AggregateException(loop.Exceptions.Select(x => x.SourceException));
228+
}
229+
230+
void HandleException(Exception e)
231+
{
232+
if (_runLoopStack.Count > 0)
233+
{
234+
var frame = _runLoopStack.Peek();
235+
frame.Exceptions.Add(ExceptionDispatchInfo.Capture(e));
236+
frame.Stop();
237+
}
238+
else
239+
{
240+
var externalLogger = _platform.Options.ExterinalGLibMainLoopExceptionLogger;
241+
if (externalLogger != null)
242+
externalLogger.Invoke(e);
243+
else
244+
Logger.TryGet(LogEventLevel.Error, LogArea.Control)
245+
?.Log("Dispatcher", "Unhandled exception: {exception}", e);
246+
}
247+
}
248+
249+
private class ManagedLoopFrame : IDisposable
250+
{
251+
private readonly CancellationToken _externalToken;
252+
private CancellationTokenSource? _internalTokenSource;
253+
public CancellationToken Cancelled { get; private set; }
254+
255+
private readonly IntPtr _loop = g_main_loop_new(IntPtr.Zero, 1);
256+
public List<ExceptionDispatchInfo> Exceptions { get; } = new();
257+
private readonly object _destroyLock = new();
258+
private bool _disposed;
259+
260+
public ManagedLoopFrame(CancellationToken token)
261+
{
262+
_externalToken = token;
263+
}
264+
265+
public void Stop()
266+
{
267+
try
268+
{
269+
_internalTokenSource?.Cancel();
270+
}
271+
catch
272+
{
273+
// Ignore
274+
}
275+
}
276+
277+
public void Run()
278+
{
279+
if (_externalToken.IsCancellationRequested)
280+
return;
281+
using (_internalTokenSource = new())
282+
using (var composite =
283+
CancellationTokenSource.CreateLinkedTokenSource(_externalToken, _internalTokenSource.Token))
284+
{
285+
Cancelled = composite.Token;
286+
using (Cancelled.Register(() =>
287+
{
288+
lock (_destroyLock)
289+
{
290+
if (_disposed)
291+
return;
292+
g_main_loop_quit(_loop);
293+
}
294+
}))
295+
{
296+
g_main_loop_run(_loop);
297+
}
298+
}
299+
}
300+
301+
public void Dispose()
302+
{
303+
lock (_destroyLock)
304+
{
305+
if(_disposed)
306+
return;
307+
_disposed = true;
308+
g_main_loop_unref(_loop);
309+
}
310+
}
311+
}
312+
313+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using static Avalonia.X11.XLib;
5+
namespace Avalonia.X11;
6+
7+
internal class X11EventDispatcher
8+
{
9+
private readonly AvaloniaX11Platform _platform;
10+
private readonly IntPtr _display;
11+
12+
public delegate void EventHandler(ref XEvent xev);
13+
public int Fd { get; }
14+
private readonly Dictionary<IntPtr, EventHandler> _eventHandlers;
15+
16+
public X11EventDispatcher(AvaloniaX11Platform platform)
17+
{
18+
_platform = platform;
19+
_display = platform.Display;
20+
_eventHandlers = platform.Windows;
21+
Fd = XLib.XConnectionNumber(_display);
22+
}
23+
24+
public bool IsPending => XPending(_display) != 0;
25+
26+
public unsafe void DispatchX11Events(CancellationToken cancellationToken)
27+
{
28+
while (IsPending)
29+
{
30+
if (cancellationToken.IsCancellationRequested)
31+
return;
32+
33+
XNextEvent(_display, out var xev);
34+
if(XFilterEvent(ref xev, IntPtr.Zero))
35+
continue;
36+
37+
if (xev.type == XEventName.GenericEvent)
38+
XGetEventData(_display, &xev.GenericEventCookie);
39+
try
40+
{
41+
if (xev.type == XEventName.GenericEvent)
42+
{
43+
if (_platform.XI2 != null && _platform.Info.XInputOpcode ==
44+
xev.GenericEventCookie.extension)
45+
{
46+
_platform.XI2.OnEvent((XIEvent*)xev.GenericEventCookie.data);
47+
}
48+
}
49+
else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler))
50+
handler(ref xev);
51+
}
52+
finally
53+
{
54+
if (xev.type == XEventName.GenericEvent && xev.GenericEventCookie.data != null)
55+
XFreeEventData(_display, &xev.GenericEventCookie);
56+
}
57+
}
58+
Flush();
59+
}
60+
61+
public void Flush() => XFlush(_display);
62+
}

0 commit comments

Comments
 (0)