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); } }