Explorar el Código

[X11] Platform threading interface and platform sanity checks

Nikita Tsukanov hace 7 años
padre
commit
16ea1f3762

+ 54 - 0
Avalonia.sln

@@ -188,6 +188,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.OpenGL", "src\Avalonia.OpenGL\Avalonia.OpenGL.csproj", "{7CCAEFC4-135D-401D-BDDD-896B9B7D3569}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{212D02D5-C873-469A-8E78-4A6350EC4A1A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{8B5768BB-71F9-4E23-89B5-DDBA6458B856}"
+EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -1689,6 +1693,54 @@ Global
 		{7CCAEFC4-135D-401D-BDDD-896B9B7D3569}.Release|iPhone.Build.0 = Release|Any CPU
 		{7CCAEFC4-135D-401D-BDDD-896B9B7D3569}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{7CCAEFC4-135D-401D-BDDD-896B9B7D3569}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Release|Any CPU.Build.0 = Release|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Release|iPhone.Build.0 = Release|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Release|iPhone.Build.0 = Release|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1742,6 +1794,8 @@ Global
 		{CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2}
 		{4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
 		{E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+		{212D02D5-C873-469A-8E78-4A6350EC4A1A} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
+		{8B5768BB-71F9-4E23-89B5-DDBA6458B856} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 6 - 0
samples/PlatformSanityChecks/App.xaml

@@ -0,0 +1,6 @@
+<Application xmlns="https://github.com/avaloniaui">
+    <Application.Styles>
+        <StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/>
+        <StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/>
+    </Application.Styles>
+</Application>

+ 13 - 0
samples/PlatformSanityChecks/App.xaml.cs

@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Markup.Xaml;
+
+namespace PlatformSanityChecks
+{
+    public class App : Application
+    {
+        public override void Initialize()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 24 - 0
samples/PlatformSanityChecks/PlatformSanityChecks.csproj

@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp2.0</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    
+    <ProjectReference Include="..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.DotNetCoreRuntime\Avalonia.DotNetCoreRuntime.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
+    <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
+  </ItemGroup>
+
+</Project>

+ 140 - 0
samples/PlatformSanityChecks/Program.cs

@@ -0,0 +1,140 @@
+using System;
+using System.Diagnostics;
+using System.Reactive.Disposables;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using Avalonia;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.X11;
+
+namespace PlatformSanityChecks
+{
+    public class Program
+    {
+        static Thread UiThread;
+        
+        static void Main(string[] args)
+        {
+            UiThread = Thread.CurrentThread;
+            AppBuilder.Configure<App>().RuntimePlatformServicesInitializer();
+            var app = new App();
+            
+            new AvaloniaX11Platform().Initialize();
+
+            CheckPlatformThreading();
+
+
+
+        }
+
+        static bool CheckAccess() => UiThread == Thread.CurrentThread;
+
+        static void VerifyAccess()
+        {
+            if (!CheckAccess())
+                Die("Call from invalid thread");
+        }
+        
+        static Exception Die(string error)
+        {
+            Console.Error.WriteLine(error);
+            Console.Error.WriteLine(Environment.StackTrace);
+            Process.GetCurrentProcess().Kill();
+            throw new Exception(error);
+        }
+
+        static IDisposable Enter([CallerMemberName] string caller = null)
+        {
+            Console.WriteLine("Entering " + caller);
+            return Disposable.Create(() => { Console.WriteLine("Leaving " + caller); });
+        }
+
+        static void EnterLoop(Action<CancellationTokenSource> cb, [CallerMemberName] string caller = null)
+        {
+            using (Enter(caller))
+            {
+                var cts = new CancellationTokenSource();
+                cb(cts);
+                Dispatcher.UIThread.MainLoop(cts.Token);
+                if (!cts.IsCancellationRequested)
+                    Die("Unexpected loop exit");
+            }
+        }
+        
+        static void CheckTimerOrdering() => EnterLoop(cts =>
+        {
+            bool firstFired = false, secondFired = false;
+            DispatcherTimer.Run(() =>
+            {
+                Console.WriteLine("Second tick");
+                VerifyAccess();
+                if (!firstFired)
+                    throw Die("Invalid timer ordering");
+                if (secondFired)
+                    throw Die("Invocation of finished timer");
+                secondFired = true;
+                cts.Cancel();
+                return false;
+            }, TimeSpan.FromSeconds(2));
+            DispatcherTimer.Run(() =>
+            {
+                Console.WriteLine("First tick");
+                VerifyAccess();
+                if (secondFired)
+                    throw Die("Invalid timer ordering");
+                if (firstFired)
+                    throw Die("Invocation of finished timer");
+                firstFired = true;
+                return false;
+            }, TimeSpan.FromSeconds(1));
+        });
+
+        static void CheckTimerTicking() => EnterLoop(cts =>
+        {
+            int ticks = 0;
+            var st = Stopwatch.StartNew();
+            DispatcherTimer.Run(() =>
+            {
+                ticks++;
+                Console.WriteLine($"Tick {ticks} at {st.Elapsed}");
+                if (ticks == 5)
+                {
+                    if (st.Elapsed.TotalSeconds < 4.5)
+                        Die("Timer is too fast");
+                    if (st.Elapsed.TotalSeconds > 6)
+                        Die("Timer is too slow");
+                    cts.Cancel();
+                    return false;
+                }
+
+                return true;
+            }, TimeSpan.FromSeconds(1));
+
+
+        });
+
+
+        static void CheckSignaling() => EnterLoop(cts =>
+        {
+            ThreadPool.QueueUserWorkItem(_ =>
+            {
+                Thread.Sleep(100);
+                Dispatcher.UIThread.Post(() =>
+                {
+                    VerifyAccess();
+                    cts.Cancel();
+                });
+            });
+        });
+
+        static void CheckPlatformThreading()
+        {
+            CheckSignaling();
+            CheckTimerOrdering();
+            CheckTimerTicking();
+
+        }
+
+    }
+}

+ 12 - 0
src/Avalonia.X11/Avalonia.X11.csproj

@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
+    </ItemGroup>
+
+</Project>

+ 12 - 0
src/Avalonia.X11/X11Exception.cs

@@ -0,0 +1,12 @@
+using System;
+
+namespace Avalonia.X11
+{
+    public class X11Exception : Exception
+    {
+        public X11Exception(string message) : base(message)
+        {
+            
+        }
+    }
+}

+ 39 - 0
src/Avalonia.X11/X11Platform.cs

@@ -0,0 +1,39 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Platform;
+using Avalonia.X11;
+using static Avalonia.X11.XLib;
+namespace Avalonia.X11
+{
+    public class AvaloniaX11Platform
+    {
+        public void Initialize()
+        {
+            Display = XOpenDisplay(IntPtr.Zero);
+            DeferredDisplay = XOpenDisplay(IntPtr.Zero);
+            if (Display == IntPtr.Zero)
+                throw new Exception("XOpenDisplay failed");
+
+
+            AvaloniaLocator.CurrentMutable.BindToSelf(this)
+                .Bind<IPlatformThreadingInterface>().ToConstant(new X11PlatformThreading(Display));
+
+        }
+
+        public IntPtr DeferredDisplay { get; set; }
+        public IntPtr Display { get; set; }
+    }
+}
+
+namespace Avalonia
+{
+    public static class AvaloniaX11PlatformExtensions
+    {
+        public static T UseX11<T>(this T builder) where T : AppBuilderBase<T>, new()
+        {
+            builder.UseWindowingSubsystem(() => new AvaloniaX11Platform().Initialize());
+            return builder;
+        }
+    }
+
+}

+ 251 - 0
src/Avalonia.X11/X11PlatformThreading.cs

@@ -0,0 +1,251 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using static Avalonia.X11.XLib;
+
+namespace Avalonia.X11
+{
+    public unsafe class X11PlatformThreading : IPlatformThreadingInterface
+    {
+        private readonly IntPtr _display;
+        private Thread _mainThread;
+
+        [StructLayout(LayoutKind.Explicit)]
+        struct epoll_data
+        {
+            [FieldOffset(0)]
+            public IntPtr ptr;
+            [FieldOffset(0)]
+            public int fd;
+            [FieldOffset(0)]
+            public uint u32;
+            [FieldOffset(0)]
+            public ulong u64;
+        }
+
+        private const int EPOLLIN = 1;
+        private const int EPOLL_CTL_ADD = 1;
+        private const int O_NONBLOCK = 2048;
+        
+        [StructLayout(LayoutKind.Sequential)]
+        struct epoll_event
+        {
+            public uint events;
+            public epoll_data data;
+        }
+        
+        [DllImport("libc")]
+        extern static int epoll_create1(int size);
+
+        [DllImport("libc")]
+        extern static int epoll_ctl(int epfd, int op, int fd, ref epoll_event __event);
+
+        [DllImport("libc")]
+        extern static int epoll_wait(int epfd, epoll_event* events, int maxevents, int timeout);
+
+        [DllImport("libc")]
+        extern static int pipe2(int* fds, int flags);
+        [DllImport("libc")]
+        extern static IntPtr write(int fd, void* buf, IntPtr count);
+        
+        [DllImport("libc")]
+        extern static IntPtr read(int fd, void* buf, IntPtr count);
+        
+        enum EventCodes
+        {
+            X11 = 1,
+            Signal =2
+        }
+
+        private int _sigread, _sigwrite;
+        private object _lock = new object();
+        private bool _signaled;
+        private DispatcherPriority _signaledPriority;
+        private int _epoll;
+        private Stopwatch _clock = Stopwatch.StartNew();
+
+        class X11Timer : IDisposable
+        {
+            private readonly X11PlatformThreading _parent;
+
+            public X11Timer(X11PlatformThreading parent, DispatcherPriority prio, TimeSpan interval, Action tick)
+            {
+                _parent = parent;
+                Priority = prio;
+                Tick = tick;
+                Interval = interval;
+                Reschedule();
+            }
+            
+            public DispatcherPriority Priority { get; }
+            public TimeSpan NextTick { get; private set; }
+            public TimeSpan Interval { get; }
+            public Action Tick { get; }
+            public bool Disposed { get; private set; }
+
+            public void Reschedule()
+            {
+                NextTick = _parent._clock.Elapsed + Interval;
+            }
+
+            public void Dispose()
+            {
+                Disposed = true;
+                lock (_parent._lock)
+                    _parent._timers.Remove(this);
+            }
+        }
+
+        List<X11Timer> _timers = new List<X11Timer>();
+        
+        public X11PlatformThreading(IntPtr display)
+        {
+            _display = display;
+            _mainThread = Thread.CurrentThread;
+            var fd = XLib.XConnectionNumber(display);
+            var ev = new epoll_event()
+            {
+                events = EPOLLIN,
+                data = {u32 = (int)EventCodes.X11}
+            };
+            _epoll = epoll_create1(0);
+            if (_epoll == -1)
+                throw new X11Exception("epoll_create1 failed");
+
+            if (epoll_ctl(_epoll, EPOLL_CTL_ADD, fd, ref ev) == -1)
+                throw new X11Exception("Unable to attach X11 connection handle to epoll");
+
+            var fds = stackalloc int[2];
+            pipe2(fds, O_NONBLOCK);
+            _sigread = fds[0];
+            _sigwrite = fds[1];
+            
+            ev = new epoll_event
+            {
+                events = EPOLLIN,
+                data = {u32 = (int)EventCodes.Signal}
+            };
+            if (epoll_ctl(_epoll, EPOLL_CTL_ADD, _sigread, ref ev) == -1)
+                throw new X11Exception("Unable to attach signal pipe to epoll");
+        }
+
+        int TimerComparer(X11Timer t1, X11Timer t2)
+        {
+            return t2.Priority - t1.Priority;
+        }
+        
+        public void RunLoop(CancellationToken cancellationToken)
+        {
+            var readyTimers = new List<X11Timer>();
+            while (!cancellationToken.IsCancellationRequested)
+            {
+                var now = _clock.Elapsed;
+                TimeSpan? nextTick = null;
+                readyTimers.Clear();
+                lock(_timers)
+                    foreach (var t in _timers)
+                    {
+                        if (nextTick == null || t.NextTick < nextTick.Value)
+                            nextTick = t.NextTick;
+                        if (t.NextTick < now)
+                            readyTimers.Add(t);
+                    }
+                
+                readyTimers.Sort(TimerComparer);
+                
+                foreach (var t in readyTimers)
+                {
+                    if (cancellationToken.IsCancellationRequested)
+                        return;
+                    t.Tick();
+                    if(!t.Disposed)
+                    {
+                        t.Reschedule();
+                        if (nextTick == null || t.NextTick < nextTick.Value)
+                            nextTick = t.NextTick;
+                    }
+                }
+
+                if (cancellationToken.IsCancellationRequested)
+                    return;
+                epoll_event ev;
+                var len = epoll_wait(_epoll, &ev, 1,
+                    nextTick == null ? -1 : Math.Max(1, (int)(nextTick.Value - _clock.Elapsed).TotalMilliseconds));
+                if (cancellationToken.IsCancellationRequested)
+                    return;
+                if (len == 0)
+                {
+                    // We handle timer-related stuff at the beginning of the loop
+                    continue;
+                }
+                else
+                {
+                    if (ev.data.u32 == (int)EventCodes.Signal)
+                    {
+                        int buf = 0;
+                        while (read(_sigread, &buf, new IntPtr(4)).ToInt64() > 0)
+                        {
+                        }
+
+                        DispatcherPriority prio;
+                        lock (_lock)
+                        {
+                            _signaled = false;
+                            prio = _signaledPriority;
+                            _signaledPriority = DispatcherPriority.MinValue;
+                        }
+                        Signaled?.Invoke(prio);
+                    }
+                    else
+                    {
+                        while (XPending(_display))
+                        {
+                            if (cancellationToken.IsCancellationRequested)
+                                return;
+                            XNextEvent(_display, out var xev);
+                        }
+                    }
+                }
+            }
+        }
+
+        
+
+        public void Signal(DispatcherPriority priority)
+        {
+            lock (_lock)
+            {
+                if (priority > _signaledPriority)
+                    _signaledPriority = priority;
+                
+                if(_signaled)
+                    return;
+                _signaled = true;
+                int buf = 0;
+                write(_sigwrite, &buf, new IntPtr(1));
+            }
+        }
+
+        public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _mainThread;
+        public event Action<DispatcherPriority?> Signaled;
+        
+        public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
+        {
+            if (_mainThread != Thread.CurrentThread)
+                throw new InvalidOperationException("StartTimer can be only called from UI thread");
+            if (interval <= TimeSpan.Zero)
+                throw new ArgumentException("Interval must be positive", nameof(interval));
+            
+            // We assume that we are on the main thread and outside of epoll_wait, so there is no need for wakeup signal
+            
+            var timer = new X11Timer(this, priority, interval, tick);
+            lock(_timers)
+                _timers.Add(timer);
+            return timer;
+        }
+    }
+}

+ 105 - 0
src/Avalonia.X11/XLib.cs

@@ -0,0 +1,105 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.X11
+{
+    public static class XLib
+    {
+        
+        [DllImport("libX11.so.6")]
+        public static extern IntPtr XInitThreads();
+        
+        [DllImport("libX11.so.6")]
+        public static extern IntPtr XOpenDisplay(IntPtr name);
+        
+        [DllImport("libX11.so.6")]
+        public static extern int XConnectionNumber(IntPtr display);
+        
+        [DllImport("libX11.so.6")]
+        public static extern IntPtr XLockDisplay(IntPtr display);
+        
+        [DllImport("libX11.so.6")]
+        public static extern IntPtr XUnlockDisplay(IntPtr display);
+        
+        [DllImport("libX11.so.6")]
+        public static extern IntPtr XFreeGC(IntPtr display, IntPtr gc);
+        
+        [DllImport("libX11.so.6")]
+        public static extern IntPtr XCreateGC(IntPtr display, IntPtr drawable, ulong valuemask, IntPtr values);
+        
+        [DllImport("libX11.so.6")]
+        public static extern int XInitImage(ref XImage image);
+        
+        [DllImport("libX11.so.6")]
+        public static extern int XDestroyImage(ref XImage image);
+        
+        [DllImport("libX11.so.6")]
+        public static extern IntPtr XSetErrorHandler(XErrorHandler handler);
+
+        [DllImport("libX11.so.6")]
+        public static extern int XSync(IntPtr display, bool discard);
+        
+        [DllImport("libX11.so.6")]
+        public static extern int XNextEvent(IntPtr display, out XEvent ev);
+        
+        [DllImport("libX11.so.6")]
+        public static extern bool XPending(IntPtr display);
+
+        [StructLayout(LayoutKind.Sequential)]
+        public struct XAnyEvent
+        {
+
+            public int Type;
+            public ulong Serial; /* # of last request processed by server */
+            public bool send_event; /* true if this came from a SendEvent request */
+            public IntPtr display; /* Display the event was read from */
+            public IntPtr window; /* window on which event was requested in event mask */
+        }
+
+        [StructLayout(LayoutKind.Explicit)]
+        public unsafe struct XEvent
+        {
+            [FieldOffset(0)] public int Type;
+            [FieldOffset(0)] public XAnyEvent XAny;
+            [FieldOffset(0)] private fixed int pad[40];
+        }
+
+        public delegate int XErrorHandler(IntPtr display, ref XErrorEvent error);
+
+        [DllImport("libX11.so.6")]
+        public static extern int XPutImage(IntPtr display, IntPtr drawable, IntPtr gc, ref XImage image,
+            int srcx, int srcy, int destx, int desty, uint width, uint height);
+
+        [StructLayout(LayoutKind.Sequential)]
+        public unsafe struct XErrorEvent
+        {
+            public int type;
+            public IntPtr* display; /* Display the event was read from */
+            public ulong serial; /* serial number of failed request */
+            public byte error_code; /* error code of failed request */
+            public byte request_code; /* Major op-code of failed request */
+            public byte minor_code; /* Minor op-code of failed request */
+            public IntPtr resourceid; /* resource id */
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        public unsafe struct XImage
+        {
+            public int width, height; /* size of image */
+            public int xoffset; /* number of pixels offset in X direction */
+            public int format; /* XYBitmap, XYPixmap, ZPixmap */
+            public IntPtr data; /* pointer to image data */
+            public int byte_order; /* data byte order, LSBFirst, MSBFirst */
+            public int bitmap_unit; /* quant. of scanline 8, 16, 32 */
+            public int bitmap_bit_order; /* LSBFirst, MSBFirst */
+            public int bitmap_pad; /* 8, 16, 32 either XY or ZPixmap */
+            public int depth; /* depth of image */
+            public int bytes_per_line; /* accelerator to next scanline */
+            public int bits_per_pixel; /* bits per pixel (ZPixmap) */
+            public ulong red_mask; /* bits in z arrangement */
+            public ulong green_mask;
+            public ulong blue_mask;
+            private fixed byte funcs[128];
+        }
+    }
+}