diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml
index 3b847adcbb..2c2e1c4075 100644
--- a/samples/ControlCatalog/App.xaml
+++ b/samples/ControlCatalog/App.xaml
@@ -60,7 +60,7 @@
-
+
diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs
index 5d83ced011..089f5cb660 100644
--- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs
+++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs
@@ -4,11 +4,8 @@ using Avalonia.Platform;
namespace Avalonia.Threading;
-interface IDispatcherImpl
+public interface IDispatcherImpl
{
-
- //IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick);
-
bool CurrentThreadIsLoopThread { get; }
// Asynchronously triggers Signaled callback
@@ -19,7 +16,7 @@ interface IDispatcherImpl
}
-interface IDispatcherImplWithPendingInput : IDispatcherImpl
+public interface IDispatcherImplWithPendingInput : IDispatcherImpl
{
// Checks if dispatcher implementation can
bool CanQueryPendingInput { get; }
@@ -27,7 +24,7 @@ interface IDispatcherImplWithPendingInput : IDispatcherImpl
bool HasPendingInput { get; }
}
-interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput
+public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput
{
// Runs the event loop
void RunLoop(CancellationToken token);
diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
deleted file mode 100644
index e1f6db9c60..0000000000
--- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using System.Threading;
-using Avalonia.Metadata;
-using Avalonia.Platform;
-using Avalonia.Threading;
-
-namespace Avalonia.Controls.Platform
-{
- [Unstable]
- public class InternalPlatformThreadingInterface : IPlatformThreadingInterface
- {
- public InternalPlatformThreadingInterface()
- {
- TlsCurrentThreadIsLoopThread = true;
- }
-
- private readonly AutoResetEvent _signaled = new AutoResetEvent(false);
-
-
- public void RunLoop(CancellationToken cancellationToken)
- {
- var handles = new[] { _signaled, cancellationToken.WaitHandle };
-
- while (!cancellationToken.IsCancellationRequested)
- {
- Signaled?.Invoke(null);
- WaitHandle.WaitAny(handles);
- }
- }
-
-
- class TimerImpl : IDisposable
- {
- private readonly DispatcherPriority _priority;
- private readonly TimeSpan _interval;
- private readonly Action _tick;
- private Timer? _timer;
- private GCHandle _handle;
-
- public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick)
- {
- _priority = priority;
- _interval = interval;
- _tick = tick;
- _timer = new Timer(OnTimer, null, interval, Timeout.InfiniteTimeSpan);
- _handle = GCHandle.Alloc(_timer);
- }
-
- private void OnTimer(object? state)
- {
- if (_timer == null)
- return;
- Dispatcher.UIThread.Post(() =>
- {
-
- if (_timer == null)
- return;
- _tick();
- _timer?.Change(_interval, Timeout.InfiniteTimeSpan);
- });
- }
-
-
- public void Dispose()
- {
- _handle.Free();
- _timer?.Dispose();
- _timer = null;
- }
- }
-
- public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
- {
- return new TimerImpl(priority, interval, tick);
- }
-
- public void Signal(DispatcherPriority prio)
- {
- _signaled.Set();
- }
-
- [ThreadStatic] private static bool TlsCurrentThreadIsLoopThread;
-
- public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread;
- public event Action? Signaled;
-#pragma warning disable CS0067
- public event Action? Tick;
-#pragma warning restore CS0067
-
- }
-}
diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs
new file mode 100644
index 0000000000..bc7595ef4e
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using Avalonia.Metadata;
+using Avalonia.Threading;
+
+namespace Avalonia.Controls.Platform;
+
+[Unstable]
+public class ManagedDispatcherImpl : IControlledDispatcherImpl
+{
+ private readonly IManagedDispatcherInputProvider? _inputProvider;
+ private readonly AutoResetEvent _wakeup = new(false);
+ private bool _signaled;
+ private readonly object _lock = new();
+ private readonly Stopwatch _clock = Stopwatch.StartNew();
+ private TimeSpan? _nextTimer;
+ private readonly Thread _loopThread = Thread.CurrentThread;
+
+ public interface IManagedDispatcherInputProvider
+ {
+ bool HasInput { get; }
+ void DispatchNextInputEvent();
+ }
+
+ public ManagedDispatcherImpl(IManagedDispatcherInputProvider? inputProvider)
+ {
+ _inputProvider = inputProvider;
+ }
+
+ public bool CurrentThreadIsLoopThread => _loopThread == Thread.CurrentThread;
+ public void Signal()
+ {
+ lock (_lock)
+ {
+ _signaled = true;
+ _wakeup.Set();
+ }
+ }
+
+ public event Action? Signaled;
+ public event Action? Timer;
+ public void UpdateTimer(int? dueTimeInTicks)
+ {
+ lock (_lock)
+ {
+ _nextTimer = dueTimeInTicks == null
+ ? null
+ : _clock.Elapsed + TimeSpan.FromMilliseconds(dueTimeInTicks.Value);
+ if (!CurrentThreadIsLoopThread)
+ _wakeup.Set();
+ }
+ }
+
+ public bool CanQueryPendingInput => _inputProvider != null;
+ public bool HasPendingInput => _inputProvider?.HasInput ?? false;
+
+ public void RunLoop(CancellationToken token)
+ {
+ while (!token.IsCancellationRequested)
+ {
+ bool signaled;
+ lock (_lock)
+ {
+ signaled = _signaled;
+ _signaled = false;
+ }
+
+ if (signaled)
+ {
+ Signaled?.Invoke();
+ continue;
+ }
+
+ bool fireTimer = false;
+ lock (_lock)
+ {
+ if (_nextTimer < _clock.Elapsed)
+ {
+ fireTimer = true;
+ _nextTimer = null;
+ }
+ }
+
+ if (fireTimer)
+ {
+ Timer?.Invoke();
+ continue;
+ }
+
+ if (_inputProvider?.HasInput == true)
+ {
+ _inputProvider.DispatchNextInputEvent();
+ continue;
+ }
+
+ if (_nextTimer != null)
+ {
+ var waitFor = _clock.Elapsed - _nextTimer.Value;
+ if (waitFor.TotalMilliseconds < 1)
+ continue;
+ _wakeup.WaitOne(waitFor);
+ }
+ else
+ _wakeup.WaitOne();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
index cd14b2d69a..808153cc4a 100644
--- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
+++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
@@ -6,6 +6,7 @@ using Avalonia.Input.Platform;
using Avalonia.Platform;
using Avalonia.Remote.Protocol;
using Avalonia.Rendering;
+using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
@@ -46,13 +47,12 @@ namespace Avalonia.DesignerSupport.Remote
{
s_transport = transport;
var instance = new PreviewerWindowingPlatform();
- var threading = new InternalPlatformThreadingInterface();
AvaloniaLocator.CurrentMutable
.Bind().ToSingleton()
.Bind().ToSingleton()
.Bind().ToConstant(Keyboard)
.Bind().ToSingleton()
- .Bind().ToConstant(threading)
+ .Bind().ToConstant(new ManagedDispatcherImpl(null))
.Bind().ToConstant(new RenderLoop())
.Bind().ToConstant(new DefaultRenderTimer(60))
.Bind().ToConstant(instance)
diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
index b0b1d731d2..319b0da7bf 100644
--- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
+++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
@@ -60,7 +60,7 @@ namespace Avalonia.Headless
internal static void Initialize(AvaloniaHeadlessPlatformOptions opts)
{
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(new HeadlessPlatformThreadingInterface())
+ .Bind().ToConstant(new ManagedDispatcherImpl(null))
.Bind().ToSingleton()
.Bind().ToSingleton()
.Bind().ToSingleton()
diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs
deleted file mode 100644
index 046e4645e3..0000000000
--- a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System;
-using Avalonia.Reactive;
-using System.Threading;
-using Avalonia.Platform;
-using Avalonia.Threading;
-
-namespace Avalonia.Headless
-{
- class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface
- {
- public HeadlessPlatformThreadingInterface()
- {
- _thread = Thread.CurrentThread;
- }
-
- private AutoResetEvent _event = new AutoResetEvent(false);
- private Thread _thread;
- private object _lock = new object();
- private DispatcherPriority? _signaledPriority;
-
- public void RunLoop(CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- DispatcherPriority? signaled = null;
- lock (_lock)
- {
- signaled = _signaledPriority;
- _signaledPriority = null;
- }
- if(signaled.HasValue)
- Signaled?.Invoke(signaled);
- WaitHandle.WaitAny(new[] {cancellationToken.WaitHandle, _event}, TimeSpan.FromMilliseconds(20));
- }
- }
-
- public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
- {
- if (interval.TotalMilliseconds < 10)
- interval = TimeSpan.FromMilliseconds(10);
-
- var stopped = false;
- Timer timer = null;
- timer = new Timer(_ =>
- {
- if (stopped)
- return;
-
- Dispatcher.UIThread.Post(() =>
- {
- try
- {
- tick();
- }
- finally
- {
- if (!stopped)
- timer.Change(interval, Timeout.InfiniteTimeSpan);
- }
- });
- },
- null, interval, Timeout.InfiniteTimeSpan);
-
- return Disposable.Create(() =>
- {
- stopped = true;
- timer.Dispose();
- });
- }
-
- public void Signal(DispatcherPriority priority)
- {
- lock (_lock)
- {
- if (_signaledPriority == null || _signaledPriority.Value > priority)
- {
- _signaledPriority = priority;
- }
- _event.Set();
- }
- }
-
- public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread;
- public event Action Signaled;
- }
-}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
index 9dc7f08064..af4a70f128 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
@@ -8,13 +8,15 @@ using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
+ using Avalonia.Threading;
-namespace Avalonia.LinuxFramebuffer
+ namespace Avalonia.LinuxFramebuffer
{
class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider
{
private readonly IOutputBackend _outputBackend;
private readonly IInputBackend _inputBackend;
+ private readonly RawEventGrouper _inputQueue;
public IInputRoot InputRoot { get; private set; }
@@ -22,9 +24,12 @@ namespace Avalonia.LinuxFramebuffer
{
_outputBackend = outputBackend;
_inputBackend = inputBackend;
+ _inputQueue = new RawEventGrouper(groupedInput => Input?.Invoke(groupedInput),
+ LinuxFramebufferPlatform.EventGrouperDispatchQueue);
Surfaces = new object[] { _outputBackend };
- _inputBackend.Initialize(this, e => Input?.Invoke(e));
+ _inputBackend.Initialize(this, e =>
+ Dispatcher.UIThread.Post(() => _inputQueue.HandleEvent(e), DispatcherPriority.Send ));
}
public IRenderer CreateRenderer(IRenderRoot root)
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
index 686050e7c2..61c94a48bd 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
@@ -14,12 +14,10 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
private int _epoll;
private Action _onInput;
private IInputRoot _inputRoot;
- private RawEventGroupingThreadingHelper _inputQueue;
public EvDevBackend(EvDevDeviceDescription[] devices)
{
_deviceDescriptions = devices;
- _inputQueue = new RawEventGroupingThreadingHelper(e => _onInput?.Invoke(e));
}
unsafe void InputThread()
@@ -45,12 +43,9 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
}
}
- private void OnRawEvent(RawInputEventArgs obj)
- {
- _inputQueue.OnEvent(obj);
- }
-
-
+ private void OnRawEvent(RawInputEventArgs obj) => _onInput?.Invoke(obj);
+
+
public void Initialize(IScreenInfoProvider info, Action onInput)
{
_onInput = onInput;
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
index bff9ddc55c..300cbc2689 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
@@ -13,14 +13,12 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
private IInputRoot _inputRoot;
private TouchDevice _touch = new TouchDevice();
private const string LibInput = nameof(Avalonia.LinuxFramebuffer) + "/" + nameof(Avalonia.LinuxFramebuffer.Input) + "/" + nameof(LibInput);
- private readonly RawEventGroupingThreadingHelper _inputQueue;
private Action _onInput;
private Dictionary _pointers = new Dictionary();
public LibInputBackend()
{
var ctx = libinput_path_create_context();
- _inputQueue = new(e => _onInput?.Invoke(e));
new Thread(() => InputThread(ctx)).Start();
}
@@ -58,7 +56,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
}
}
- private void ScheduleInput(RawInputEventArgs ev) => _inputQueue.OnEvent(ev);
+ private void ScheduleInput(RawInputEventArgs ev) => _onInput.Invoke(ev);
private void HandleTouch(IntPtr ev, LibInputEventType type)
{
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
index c3e90f5fd7..bc178c8ecb 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
@@ -16,6 +16,8 @@ using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
+using Avalonia.Threading;
+
#nullable enable
namespace Avalonia.LinuxFramebuffer
@@ -23,9 +25,7 @@ namespace Avalonia.LinuxFramebuffer
class LinuxFramebufferPlatform
{
IOutputBackend _fb;
- private static readonly Stopwatch St = Stopwatch.StartNew();
- internal static uint Timestamp => (uint)St.ElapsedTicks;
- public static InternalPlatformThreadingInterface? Threading;
+ public static ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue = new();
internal static Compositor Compositor { get; private set; } = null!;
@@ -34,18 +34,16 @@ namespace Avalonia.LinuxFramebuffer
{
_fb = backend;
}
-
-
+
void Initialize()
{
- Threading = new InternalPlatformThreadingInterface();
if (_fb is IGlOutputBackend gl)
AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformGraphics);
var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions();
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(Threading)
+ .Bind().ToConstant(new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)))
.Bind().ToConstant(new DefaultRenderTimer(opts.Fps))
.Bind().ToConstant(new RenderLoop())
.Bind().ToTransient()
diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs
index 66b24e9722..cdcd5af142 100644
--- a/src/Shared/RawEventGrouping.cs
+++ b/src/Shared/RawEventGrouping.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections.Pooled;
+using Avalonia.Controls.Platform;
using Avalonia.Input.Raw;
using Avalonia.Threading;
@@ -34,6 +35,19 @@ class ManualRawEventGrouperDispatchQueue : IRawEventGrouperDispatchQueue
}
}
+internal class ManualRawEventGrouperDispatchQueueDispatcherInputProvider : ManagedDispatcherImpl.IManagedDispatcherInputProvider
+{
+ private readonly ManualRawEventGrouperDispatchQueue _queue;
+
+ public ManualRawEventGrouperDispatchQueueDispatcherInputProvider(ManualRawEventGrouperDispatchQueue queue)
+ {
+ _queue = queue;
+ }
+
+ public bool HasInput => _queue.HasJobs;
+ public void DispatchNextInputEvent() => _queue.DispatchNext();
+}
+
internal class AutomaticRawEventGrouperDispatchQueue : IRawEventGrouperDispatchQueue
{
private readonly Queue<(RawInputEventArgs args, Action handler)> _inputQueue = new();
diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs
index 81474d5efb..fd258dfd96 100644
--- a/tests/Avalonia.RenderTests/TestBase.cs
+++ b/tests/Avalonia.RenderTests/TestBase.cs
@@ -17,6 +17,7 @@ using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
+using Avalonia.Utilities;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
#if AVALONIA_SKIA
@@ -122,7 +123,8 @@ namespace Avalonia.Direct2D1.RenderTests
// Free pools
for (var c = 0; c < 11; c++)
- TestThreadingInterface.RunTimers();
+ foreach (var dp in Dispatcher.SnapshotTimersForUnitTests())
+ dp.ForceFire();
writableBitmap.Save(compositedPath);
}
}