From ae6a085ebcd002f8f4f3a47973dc6217e32261c3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 13 Mar 2026 19:27:47 +0500 Subject: [PATCH] Don't tick with render loop when app is idle (#20873) * Don't tick with render loop when app is idle * Update src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * wip * wip * api diff * fixes * Address review: clear wakeupPending at tick start, guard CarbonEmissionsHack subscriptions - Clear _wakeupPending at start of TimerTick so wakeups already processed by the current tick don't force an unnecessary extra tick - Guard CarbonEmissionsHack against duplicate subscriptions using a private attached property Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix lock-order inversion in Add/Remove vs TimerTick Move Wakeup() and Stop() calls outside the _items lock in Add/Remove to prevent deadlock with TimerTick which acquires _timerLock then _items. Add/Remove are UI-thread-only so the extracted logic remains safe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review: remove unused usings, guard double Stop(), fix SleepLoop extra frame - Remove unused using directives from IRenderLoopTask.cs - Guard TimerTick Stop() with _running check to prevent double Stop() when Remove() already stopped the timer - SleepLoopRenderTimer: use WaitOne(timeout) instead of Thread.Sleep so Stop() can interrupt the sleep, and recheck _stopped before Tick Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix DisplayLinkTimer foreground handler bypassing render loop state Only resume the display link on WillEnterForeground if the timer was calling Start() to avoid setting _stopped=false when the render loop had the timer stopped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix DisplayLinkTimer thread safety, revert global NU5104 suppression - Stop() now only sets _stopped flag; OnLinkTick() self-pauses the CADisplayLink from the timer thread to avoid thread-affinity issues - Revert NU5104 global suppression in SharedVersion.props Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cap _ticksSinceLastCommit to prevent int overflow Stop incrementing once it reaches CommitGraceTicks to prevent wrapping negative and keeping the render loop awake indefinitely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: remove Start/Stop from IRenderTimer, merge into Tick setter Timer start/stop is now controlled entirely by setting the Tick property: non-null starts, null stops. This eliminates the explicit Start()/Stop() methods from IRenderTimer, making the API simpler. DefaultRenderLoop controls the timer purely through Tick assignment under its _timerLock. A new _hasItems flag tracks subscriber presence since Tick is now transient (null when idle). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address review comments on timer thread safety and guards - ChoreographerTimer: add _frameCallbackActive guard to prevent double PostFrameCallback from both Tick setter and SubscribeView - ServerCompositor: cap _ticksSinceLastCommit at int.MaxValue - SleepLoopRenderTimer: make _tick volatile, remove _stopped recheck (guard moved to DefaultRenderLoop) - DefaultRenderLoop: add _running check at tick start to drop late ticks - ThreadProxyRenderTimer: add lock for internal state manipulation - DisplayLinkTimer: add lock for all internal state manipulation - Re-add NU5104 suppression to SharedVersion.props Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make _hasItems volatile for cross-thread visibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: guard against redundant starts in DefaultRenderTimer, make _tick volatile across all timers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove CarbonEmissionsHack, revert iOS/Android timers to always-ticking - Delete CarbonEmissionsHack class and its XAML reference - Revert DisplayLinkTimer (iOS) to original always-ticking implementation - Revert ChoreographerTimer (Android) to original always-ticking implementation - Add TODO comments for future start/stop on RenderLoop request Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix DirectCompositionConnection WaitOne not respecting process exit cancellation Use WaitHandle.WaitAny with both _wakeEvent and cts.Token.WaitHandle so the loop can exit when ProcessExit fires while the timer is stopped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * timers * XML docs * Cache delegate --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/Avalonia.Headless.XUnit.nupkg.xml | 4 +- api/Avalonia.Headless.nupkg.xml | 4 +- api/Avalonia.Win32.Interoperability.nupkg.xml | 4 +- api/Avalonia.nupkg.xml | 148 +++++++++++++++++- build/SharedVersion.props | 2 +- .../Pages/ClipboardPage.xaml.cs | 5 +- .../Avalonia.Android/AndroidPlatform.cs | 4 +- src/Android/Avalonia.Android/AvaloniaView.cs | 2 +- .../Avalonia.Android/ChoreographerTimer.cs | 60 +++---- .../Rendering/Composition/Compositor.cs | 2 +- .../Server/ServerCompositionTarget.cs | 14 ++ .../Composition/Server/ServerCompositor.cs | 44 ++++-- .../Server/ServerCompositorAnimations.cs | 2 + .../Transport/BatchStreamArrayPool.cs | 75 ++++++--- .../Rendering/DefaultRenderTimer.cs | 44 ++---- src/Avalonia.Base/Rendering/IRenderLoop.cs | 16 +- .../Rendering/IRenderLoopTask.cs | 5 +- src/Avalonia.Base/Rendering/IRenderTimer.cs | 13 +- src/Avalonia.Base/Rendering/RenderLoop.cs | 140 ++++++++++++----- .../Rendering/SleepLoopRenderTimer.cs | 61 ++++---- .../Rendering/ThreadProxyRenderTimer.cs | 65 +++++--- .../Remote/PreviewerWindowingPlatform.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 9 +- src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- .../AvaloniaNativeRenderTimer.cs | 48 +++--- src/Avalonia.X11/X11Platform.cs | 2 +- .../Rendering/BrowserRenderTimer.cs | 13 +- .../Rendering/BrowserSharedRenderLoop.cs | 2 +- .../AvaloniaHeadlessPlatform.cs | 6 +- .../LinuxFramebufferPlatform.cs | 2 +- .../DirectCompositionConnection.cs | 30 +++- .../Avalonia.Win32/DirectX/DxgiConnection.cs | 31 +++- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- .../Composition/WinUiCompositorConnection.cs | 30 +++- src/iOS/Avalonia.iOS/DisplayLinkTimer.cs | 12 +- src/iOS/Avalonia.iOS/Platform.cs | 2 +- .../Composition/CompositionAnimationTests.cs | 2 +- .../Compositor/CompositionTargetUpdate.cs | 6 +- .../Composition/DirectFbCompositionTests.cs | 2 +- .../Avalonia.RenderTests/ManualRenderTimer.cs | 2 +- .../Avalonia.RenderTests/TestRenderHelper.cs | 2 +- .../CompositorTestServices.cs | 7 +- tests/Avalonia.UnitTests/RendererMocks.cs | 2 +- 43 files changed, 632 insertions(+), 298 deletions(-) diff --git a/api/Avalonia.Headless.XUnit.nupkg.xml b/api/Avalonia.Headless.XUnit.nupkg.xml index c87cf909fe..15a56561b9 100644 --- a/api/Avalonia.Headless.XUnit.nupkg.xml +++ b/api/Avalonia.Headless.XUnit.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -73,4 +73,4 @@ baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll - + \ No newline at end of file diff --git a/api/Avalonia.Headless.nupkg.xml b/api/Avalonia.Headless.nupkg.xml index 229047057a..435df92d13 100644 --- a/api/Avalonia.Headless.nupkg.xml +++ b/api/Avalonia.Headless.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -37,4 +37,4 @@ baseline/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll current/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll - + \ No newline at end of file diff --git a/api/Avalonia.Win32.Interoperability.nupkg.xml b/api/Avalonia.Win32.Interoperability.nupkg.xml index 3672bb9b99..33fc2ac062 100644 --- a/api/Avalonia.Win32.Interoperability.nupkg.xml +++ b/api/Avalonia.Win32.Interoperability.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -37,4 +37,4 @@ baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll - + \ No newline at end of file diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index b4c8bba386..5fe2e48f60 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -1597,6 +1597,42 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Start + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Stop + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect) @@ -1609,6 +1645,30 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) @@ -3031,6 +3091,42 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Start + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.DefaultRenderTimer.Stop + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.IRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Rendering.SceneInvalidatedEventArgs.#ctor(Avalonia.Rendering.IRenderRoot,Avalonia.Rect) @@ -3043,6 +3139,30 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.SleepLoopRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.add_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Rendering.ThreadProxyRenderTimer.remove_Tick(System.Action{System.TimeSpan}) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Utilities.AvaloniaResourcesIndexReaderWriter.WriteResources(System.IO.Stream,System.Collections.Generic.List{System.ValueTuple{System.String,System.Int32,System.Func{System.IO.Stream}}}) @@ -3979,6 +4099,18 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Start + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Stop + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 P:Avalonia.Input.IInputRoot.FocusRoot @@ -4291,6 +4423,18 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Start + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + + + CP0006 + M:Avalonia.Rendering.IRenderTimer.Stop + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 P:Avalonia.Input.IInputRoot.FocusRoot @@ -5077,4 +5221,4 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - + \ No newline at end of file diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 37d14a5647..b8c0dd4d43 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -8,7 +8,7 @@ https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link https://github.com/AvaloniaUI/Avalonia/ true - $(NoWarn);CS1591 + $(NoWarn);CS1591;NU5104 MIT Icon.png Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly. diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs index 2b87ceb7b1..4cdde6b824 100644 --- a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs @@ -34,7 +34,10 @@ namespace ControlCatalog.Pages { InitializeComponent(); _clipboardLastDataObjectChecker = - new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject); + new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject) + { + IsEnabled = false + }; using var asset = AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/image1.jpg")); _defaultImage = new Bitmap(asset); diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 7a3059cb65..460aaec5ca 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -76,19 +76,21 @@ namespace Avalonia.Android public static AndroidPlatformOptions? Options { get; private set; } internal static Compositor? Compositor { get; private set; } + internal static ChoreographerTimer? Timer { get; private set; } public static void Initialize() { Options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl()); + Timer = new ChoreographerTimer(); AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToConstant(new ChoreographerTimer()) + .Bind().ToConstant(RenderLoop.FromTimer(Timer)) .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })) .Bind().ToConstant(new AndroidActivatableLifetime()); diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 86b96772ce..d8df486bb3 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -100,7 +100,7 @@ namespace Avalonia.Android return; if (isVisible && _timerSubscription == null) { - if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer) + if (AndroidPlatform.Timer is { } timer) { _timerSubscription = timer.SubscribeView(this); } diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index adca9c72ce..9bc8e78a52 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -18,10 +18,9 @@ namespace Avalonia.Android private readonly AutoResetEvent _event = new(false); private readonly GCHandle _timerHandle; private readonly HashSet _views = new(); - private Action? _tick; + private bool _pendingCallback; private long _lastTime; - private int _count; public ChoreographerTimer() { @@ -40,28 +39,13 @@ namespace Avalonia.Android public bool RunsInBackground => true; - public event Action Tick + public Action? Tick { - add + get => _tick; + set { - lock (_lock) - { - _tick += value; - _count++; - - if (_count == 1) - { - PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); - } - } - } - remove - { - lock (_lock) - { - _tick -= value; - _count--; - } + _tick = value; + PostFrameCallbackIfNeeded(); } } @@ -70,20 +54,14 @@ namespace Avalonia.Android lock (_lock) { _views.Add(view); - - if (_views.Count == 1) - { - PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); - } + PostFrameCallbackIfNeeded(); } return Disposable.Create( () => { - lock (_lock) - { + lock (_lock) _views.Remove(view); - } } ); } @@ -109,14 +87,28 @@ namespace Avalonia.Android } } + private void PostFrameCallbackIfNeeded() + { + lock (_lock) + { + if(_pendingCallback) + return; + + if (_tick == null || _views.Count == 0) + return; + + _pendingCallback = true; + + PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); + } + } + private void DoFrameCallback(long frameTimeNanos, IntPtr data) { lock (_lock) { - if (_count > 0 && _views.Count > 0) - { - PostFrameCallback(_choreographer.Task.Result, data); - } + _pendingCallback = false; + PostFrameCallbackIfNeeded(); _lastTime = frameTimeNanos; _event.Set(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 2398468456..2acda6c57d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -51,7 +51,7 @@ namespace Avalonia.Rendering.Composition /// [PrivateApi] public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false) - : this(RenderLoop.LocatorAutoInstance, gpu, useUiThreadForSynchronousCommits) + : this(AvaloniaLocator.Current.GetRequiredService(), gpu, useUiThreadForSynchronousCommits) { } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index f8382547b9..81a3c09b35 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -39,6 +39,11 @@ namespace Avalonia.Rendering.Composition.Server public ICompositionTargetDebugEvents? DebugEvents { get; set; } public int RenderedVisuals { get; set; } public int VisitedVisuals { get; set; } + + /// + /// Returns true if the target is enabled and has pending work but its render target was not ready. + /// + internal bool IsWaitingForReadyRenderTarget { get; private set; } public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : base(compositor) @@ -125,6 +130,8 @@ namespace Avalonia.Rendering.Composition.Server public void Render() { + IsWaitingForReadyRenderTarget = false; + if (_disposed) return; @@ -143,11 +150,15 @@ namespace Avalonia.Rendering.Composition.Server try { if (_renderTarget == null && !_compositor.IsReadyToCreateRenderTarget(_surfaces())) + { + IsWaitingForReadyRenderTarget = IsEnabled; return; + } _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); } catch (RenderTargetNotReadyException) { + IsWaitingForReadyRenderTarget = IsEnabled; return; } catch (RenderTargetCorruptedException) @@ -164,7 +175,10 @@ namespace Avalonia.Rendering.Composition.Server return; if (!_renderTarget.IsReady) + { + IsWaitingForReadyRenderTarget = IsEnabled; return; + } var needLayer = _overlays.RequireLayer // Check if we don't need overlays // Check if render target can be rendered to directly and preserves the previous frame diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 76e649407f..b8cc5afca2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -44,6 +44,9 @@ namespace Avalonia.Rendering.Composition.Server public CompositionOptions Options { get; } public ServerCompositorAnimations Animations { get; } public ReadbackIndices Readback { get; } = new(); + + private int _ticksSinceLastCommit; + private const int CommitGraceTicks = 10; public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, CompositionOptions options, @@ -64,6 +67,7 @@ namespace Avalonia.Rendering.Composition.Server { lock (_batches) _batches.Enqueue(batch); + _renderLoop.Wakeup(); } internal void UpdateServerTime() => ServerNow = Clock.Elapsed; @@ -72,6 +76,7 @@ namespace Avalonia.Rendering.Composition.Server readonly List _reusableToNotifyRenderedList = new(); void ApplyPendingBatches() { + bool hadBatches = false; while (true) { CompositionBatch batch; @@ -119,7 +124,13 @@ namespace Avalonia.Rendering.Composition.Server _reusableToNotifyProcessedList.Add(batch); LastBatchId = batch.SequenceId; + hadBatches = true; } + + if (hadBatches) + _ticksSinceLastCommit = 0; + else if (_ticksSinceLastCommit < int.MaxValue) + _ticksSinceLastCommit++; } void ReadServerJobs(BatchStreamReader reader, Queue queue, object endMarker) @@ -171,8 +182,10 @@ namespace Avalonia.Rendering.Composition.Server _reusableToNotifyRenderedList.Clear(); } - public void Render() => Render(true); - public void Render(bool catchExceptions) + bool IRenderLoopTask.Render() => ExecuteRender(true); + public void Render(bool catchExceptions) => ExecuteRender(catchExceptions); + + private bool ExecuteRender(bool catchExceptions) { if (Dispatcher.UIThread.CheckAccess()) { @@ -182,7 +195,7 @@ namespace Avalonia.Rendering.Composition.Server try { using (Dispatcher.UIThread.DisableProcessing()) - RenderReentrancySafe(catchExceptions); + return RenderReentrancySafe(catchExceptions); } finally { @@ -190,10 +203,10 @@ namespace Avalonia.Rendering.Composition.Server } } else - RenderReentrancySafe(catchExceptions); + return RenderReentrancySafe(catchExceptions); } - private void RenderReentrancySafe(bool catchExceptions) + private bool RenderReentrancySafe(bool catchExceptions) { lock (_lock) { @@ -202,7 +215,7 @@ namespace Avalonia.Rendering.Composition.Server try { _safeThread = Thread.CurrentThread; - RenderCore(catchExceptions); + return RenderCore(catchExceptions); } finally { @@ -235,17 +248,16 @@ namespace Avalonia.Rendering.Composition.Server return Stopwatch.GetElapsedTime(compositorGlobalPassesStarted); } - private void RenderCore(bool catchExceptions) + private bool RenderCore(bool catchExceptions) { - UpdateServerTime(); var compositorGlobalPassesElapsed = ExecuteGlobalPasses(); try { - if(!RenderInterface.IsReady) - return; + if (!RenderInterface.IsReady) + return true; RenderInterface.EnsureValidBackendContext(); ExecuteServerJobs(_receivedJobQueue); @@ -263,6 +275,18 @@ namespace Avalonia.Rendering.Composition.Server { Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e); } + + // Request a tick if we have active animations or if there are recent batches + if (Animations.NeedNextTick || _ticksSinceLastCommit < CommitGraceTicks) + return true; + + // Request a tick if we had unready targets in the last tick, to check if they are ready next time + foreach (var target in _activeTargets) + if (target.IsWaitingForReadyRenderTarget) + return true; + + // Otherwise there is no need to waste CPU cycles, tell the timer to pause + return false; } public void AddCompositionTarget(ServerCompositionTarget target) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs index 1f2c7dedb8..0e59cd8f03 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs @@ -30,6 +30,8 @@ internal class ServerCompositorAnimations _dirtyAnimatedObjects.Clear(); } + public bool NeedNextTick => _clockItems.Count > 0; + public void AddDirtyAnimatedObject(ServerObjectAnimations obj) { if (_dirtyAnimatedObjects.Add(obj)) diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 7e1c9e711f..d9003659a1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Threading; @@ -13,52 +14,75 @@ namespace Avalonia.Rendering.Composition.Transport; /// internal abstract class BatchStreamPoolBase : IDisposable { + private readonly Action>? _startTimer; readonly Stack _pool = new(); bool _disposed; int _usage; readonly int[] _usageStatistics = new int[10]; int _usageStatisticsSlot; - readonly bool _reclaimImmediately; + private readonly WeakReference> _updateRef; + private readonly Dispatcher? _reclaimOnDispatcher; + private bool _timerIsRunning; + private ulong _currentUpdateTick, _lastActivityTick; public int CurrentUsage => _usage; public int CurrentPool => _pool.Count; public BatchStreamPoolBase(bool needsFinalize, bool reclaimImmediately, Action>? startTimer = null) { + _startTimer = startTimer; if(!needsFinalize) - GC.SuppressFinalize(needsFinalize); + GC.SuppressFinalize(this); - var updateRef = new WeakReference>(this); - if ( - reclaimImmediately - || Dispatcher.FromThread(Thread.CurrentThread) == null) - _reclaimImmediately = true; - else - StartUpdateTimer(startTimer, updateRef); + _updateRef = new WeakReference>(this); + _reclaimOnDispatcher = !reclaimImmediately ? Dispatcher.FromThread(Thread.CurrentThread) : null; + EnsureUpdateTimer(); } + - static void StartUpdateTimer(Action>? startTimer, WeakReference> updateRef) + void EnsureUpdateTimer() { - Func timerProc = () => + if (_timerIsRunning || !NeedsTimer) + return; + + var timerProc = GetTimerProc(_updateRef); + + if (_startTimer != null) + _startTimer(timerProc); + else { - if (updateRef.TryGetTarget(out var target)) + if (_reclaimOnDispatcher != null) { - target.UpdateStatistics(); - return true; + if (_reclaimOnDispatcher.CheckAccess()) + DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); + else + _reclaimOnDispatcher.Post( + () => DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)), + DispatcherPriority.Normal); } + } + + _timerIsRunning = true; + // Explicit capture + static Func GetTimerProc(WeakReference> updateRef) => () => + { + if (updateRef.TryGetTarget(out var target)) + return target.UpdateTimerTick(); return false; }; - if (startTimer != null) - startTimer(timerProc); - else - DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); } - private void UpdateStatistics() + [MemberNotNullWhen(true, nameof(_reclaimOnDispatcher))] + private bool NeedsTimer => _reclaimOnDispatcher != null && + _currentUpdateTick - _lastActivityTick < (uint)_usageStatistics.Length * 2 + 1; + private bool ReclaimImmediately => _reclaimOnDispatcher == null; + + private bool UpdateTimerTick() { lock (_pool) { + _currentUpdateTick++; var maximumUsage = _usageStatistics.Max(); var recentlyUsedPooledSlots = maximumUsage - _usage; var keepSlots = Math.Max(recentlyUsedPooledSlots, 10); @@ -67,9 +91,17 @@ internal abstract class BatchStreamPoolBase : IDisposable _usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; _usageStatistics[_usageStatisticsSlot] = 0; + + return _timerIsRunning = NeedsTimer; } } + private void OnActivity() + { + _lastActivityTick = _currentUpdateTick; + EnsureUpdateTimer(); + } + protected abstract T CreateItem(); protected virtual void ClearItem(T item) @@ -90,6 +122,8 @@ internal abstract class BatchStreamPoolBase : IDisposable if (_usageStatistics[_usageStatisticsSlot] < _usage) _usageStatistics[_usageStatisticsSlot] = _usage; + OnActivity(); + if (_pool.Count != 0) return _pool.Pop(); } @@ -103,9 +137,10 @@ internal abstract class BatchStreamPoolBase : IDisposable lock (_pool) { _usage--; - if (!_disposed && !_reclaimImmediately) + if (!_disposed && !ReclaimImmediately) { _pool.Push(item); + OnActivity(); return; } } diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs index 102cc30e87..cc24086305 100644 --- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs @@ -15,8 +15,7 @@ namespace Avalonia.Rendering [PrivateApi] public class DefaultRenderTimer : IRenderTimer { - private int _subscriberCount; - private Action? _tick; + private volatile Action? _tick; private IDisposable? _subscription; /// @@ -36,40 +35,28 @@ namespace Avalonia.Rendering public int FramesPerSecond { get; } /// - public event Action Tick + public Action? Tick { - add + get => _tick; + set { - _tick += value; - - if (_subscriberCount++ == 0) + if (value != null) { - Start(); + _tick = value; + _subscription ??= StartCore(InternalTick); } - } - - remove - { - if (--_subscriberCount == 0) + else { - Stop(); + _subscription?.Dispose(); + _subscription = null; + _tick = null; } - - _tick -= value; } } /// public virtual bool RunsInBackground => true; - /// - /// Starts the timer. - /// - protected void Start() - { - _subscription = StartCore(InternalTick); - } - /// /// Provides the implementation of starting the timer. /// @@ -85,15 +72,6 @@ namespace Avalonia.Rendering return new Timer(_ => tick(TimeSpan.FromMilliseconds(Environment.TickCount)), null, interval, interval); } - /// - /// Stops the timer. - /// - protected void Stop() - { - _subscription?.Dispose(); - _subscription = null; - } - private void InternalTick(TimeSpan tickCount) { _tick?.Invoke(tickCount); diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs index bf2c221b03..e887832ebc 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoop.cs @@ -9,8 +9,8 @@ namespace Avalonia.Rendering /// The render loop is responsible for advancing the animation timer and updating the scene /// graph for visible windows. /// - [NotClientImplementable] - internal interface IRenderLoop + [PrivateApi] + public interface IRenderLoop { /// /// Adds an update task. @@ -20,17 +20,23 @@ namespace Avalonia.Rendering /// Registered update tasks will be polled on each tick of the render loop after the /// animation timer has been pulsed. /// - void Add(IRenderLoopTask i); + internal void Add(IRenderLoopTask i); /// /// Removes an update task. /// /// The update task. - void Remove(IRenderLoopTask i); + internal void Remove(IRenderLoopTask i); /// /// Indicates if the rendering is done on a non-UI thread. /// - bool RunsInBackground { get; } + internal bool RunsInBackground { get; } + + /// + /// Wakes up the render loop to schedule the next tick. + /// Thread-safe: can be called from any thread. + /// + internal void Wakeup(); } } diff --git a/src/Avalonia.Base/Rendering/IRenderLoopTask.cs b/src/Avalonia.Base/Rendering/IRenderLoopTask.cs index f63855e651..67416cc155 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoopTask.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoopTask.cs @@ -1,10 +1,7 @@ -using System; -using System.Threading.Tasks; - namespace Avalonia.Rendering { internal interface IRenderLoopTask { - void Render(); + bool Render(); } } diff --git a/src/Avalonia.Base/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs index 396e84d492..772dcf7656 100644 --- a/src/Avalonia.Base/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/IRenderTimer.cs @@ -10,16 +10,19 @@ namespace Avalonia.Rendering public interface IRenderTimer { /// - /// Raised when the render timer ticks to signal a new frame should be drawn. + /// Gets or sets the callback to be invoked when the timer ticks. + /// This property can be set from any thread, but it's guaranteed that it's not set concurrently + /// (i. e. render loop always does it under a lock). + /// Setting the value to null suggests the timer to stop ticking, however + /// timer is allowed to produce ticks on the previously set value as long as it stops doing so /// /// - /// This event can be raised on any thread; it is the responsibility of the subscriber to - /// switch execution to the right thread. + /// The callback can be invoked on any thread /// - event Action Tick; + Action? Tick { get; set; } /// - /// Indicates if the timer ticks on a non-UI thread + /// Indicates if the timer ticks on a non-UI thread. /// bool RunsInBackground { get; } } diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index 846cce7a23..9af9c54443 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -2,58 +2,52 @@ using System.Collections.Generic; using System.Threading; using Avalonia.Logging; +using Avalonia.Metadata; using Avalonia.Threading; namespace Avalonia.Rendering { /// - /// The application render loop. + /// Provides factory methods for creating instances. + /// + [PrivateApi] + public static class RenderLoop + { + /// + /// Creates an from an . + /// + public static IRenderLoop FromTimer(IRenderTimer timer) => new DefaultRenderLoop(timer); + } + + /// + /// Default implementation of the application render loop. /// /// /// The render loop is responsible for advancing the animation timer and updating the scene - /// graph for visible windows. + /// graph for visible windows. It owns the sleep/wake state machine: setting + /// to a non-null callback to start the timer and to null to + /// stop it, under a lock so that timer implementations never see concurrent changes. /// - internal class RenderLoop : IRenderLoop + internal class DefaultRenderLoop : IRenderLoop { private readonly List _items = new List(); private readonly List _itemsCopy = new List(); - private IRenderTimer? _timer; + private Action _tick; + private readonly IRenderTimer _timer; + private readonly object _timerLock = new(); private int _inTick; - - public static IRenderLoop LocatorAutoInstance - { - get - { - var loop = AvaloniaLocator.Current.GetService(); - if (loop == null) - { - var timer = AvaloniaLocator.Current.GetRequiredService(); - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(loop = new RenderLoop(timer)); - } - - return loop; - } - } + private volatile bool _hasItems; + private bool _running; + private bool _wakeupPending; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The render timer. - public RenderLoop(IRenderTimer timer) + public DefaultRenderLoop(IRenderTimer timer) { _timer = timer; - } - - /// - /// Gets the render timer. - /// - protected IRenderTimer Timer - { - get - { - return _timer ??= AvaloniaLocator.Current.GetRequiredService(); - } + _tick = TimerTick; } /// @@ -62,14 +56,17 @@ namespace Avalonia.Rendering _ = i ?? throw new ArgumentNullException(nameof(i)); Dispatcher.UIThread.VerifyAccess(); + bool shouldStart; lock (_items) { _items.Add(i); + shouldStart = _items.Count == 1; + } - if (_items.Count == 1) - { - Timer.Tick += TimerTick; - } + if (shouldStart) + { + _hasItems = true; + Wakeup(); } } @@ -78,19 +75,48 @@ namespace Avalonia.Rendering { _ = i ?? throw new ArgumentNullException(nameof(i)); Dispatcher.UIThread.VerifyAccess(); + + bool shouldStop; lock (_items) { _items.Remove(i); + shouldStop = _items.Count == 0; + } - if (_items.Count == 0) + if (shouldStop) + { + _hasItems = false; + lock (_timerLock) { - Timer.Tick -= TimerTick; + if (_running) + { + _running = false; + _wakeupPending = false; + _timer.Tick = null; + } } } } /// - public bool RunsInBackground => Timer.RunsInBackground; + public bool RunsInBackground => _timer.RunsInBackground; + + /// + public void Wakeup() + { + lock (_timerLock) + { + if (_hasItems && !_running) + { + _running = true; + _timer.Tick = _tick; + } + else + { + _wakeupPending = true; + } + } + } private void TimerTick(TimeSpan time) { @@ -98,21 +124,49 @@ namespace Avalonia.Rendering { try { - + // Consume any pending wakeup — this tick will process its work. + // Only wakeups arriving during task execution will keep the timer running. + // Also drop late ticks that arrive after the timer was stopped. + lock (_timerLock) + { + if (!_running) + return; + _wakeupPending = false; + } + lock (_items) { _itemsCopy.Clear(); _itemsCopy.AddRange(_items); } - + var wantsNextTick = false; for (int i = 0; i < _itemsCopy.Count; i++) { - _itemsCopy[i].Render(); + wantsNextTick |= _itemsCopy[i].Render(); } _itemsCopy.Clear(); + if (!wantsNextTick) + { + lock (_timerLock) + { + if (!_running) + { + // Already stopped by Remove() + } + else if (_wakeupPending) + { + _wakeupPending = false; + } + else + { + _running = false; + _timer.Tick = null; + } + } + } } catch (Exception ex) { diff --git a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs index 3ad4ea94d0..570dc4cb30 100644 --- a/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs @@ -8,10 +8,10 @@ namespace Avalonia.Rendering [PrivateApi] public class SleepLoopRenderTimer : IRenderTimer { - private Action? _tick; - private int _count; - private readonly object _lock = new object(); - private bool _running; + private volatile Action? _tick; + private volatile bool _stopped = true; + private bool _threadStarted; + private readonly AutoResetEvent _wakeEvent = new(false); private readonly Stopwatch _st = Stopwatch.StartNew(); private readonly TimeSpan _timeBetweenTicks; @@ -19,28 +19,30 @@ namespace Avalonia.Rendering { _timeBetweenTicks = TimeSpan.FromSeconds(1d / fps); } - - public event Action Tick + + public Action? Tick { - add + get => _tick; + set { - lock (_lock) + if (value != null) { - _tick += value; - _count++; - if (_running) - return; - _running = true; - new Thread(LoopProc) { IsBackground = true }.Start(); + _tick = value; + _stopped = false; + if (!_threadStarted) + { + _threadStarted = true; + new Thread(LoopProc) { IsBackground = true }.Start(); + } + else + { + _wakeEvent.Set(); + } } - - } - remove - { - lock (_lock) + else { - _tick -= value; - _count--; + _stopped = true; + _tick = null; } } } @@ -52,24 +54,17 @@ namespace Avalonia.Rendering var lastTick = _st.Elapsed; while (true) { + if (_stopped) + _wakeEvent.WaitOne(); + var now = _st.Elapsed; var timeTillNextTick = lastTick + _timeBetweenTicks - now; - if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick); + if (timeTillNextTick.TotalMilliseconds > 1) + _wakeEvent.WaitOne(timeTillNextTick); lastTick = now = _st.Elapsed; - lock (_lock) - { - if (_count == 0) - { - _running = false; - return; - } - } _tick?.Invoke(now); - } } - - } } diff --git a/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs b/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs index 0f3387cd1a..d15d3a052e 100644 --- a/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/ThreadProxyRenderTimer.cs @@ -12,8 +12,9 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer private readonly Stopwatch _stopwatch; private readonly Thread _timerThread; private readonly AutoResetEvent _autoResetEvent; - private Action? _tick; - private int _subscriberCount; + private readonly object _lock = new(); + private volatile Action? _tick; + private volatile bool _active; private bool _registered; public ThreadProxyRenderTimer(IRenderTimer inner, int maxStackSize = 1 * 1024 * 1024) @@ -24,33 +25,54 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer _timerThread = new Thread(RenderTimerThreadFunc, maxStackSize) { Name = "RenderTimerLoop", IsBackground = true }; } - public event Action Tick + public Action? Tick { - add + get => _tick; + set { - _tick += value; - - if (!_registered) + lock (_lock) { - _registered = true; - _timerThread.Start(); + if (value != null) + { + _tick = value; + _active = true; + EnsureStarted(); + _inner.Tick = InnerTick; + } + else + { + // Don't set _inner.Tick = null here — may be on the wrong thread. + // InnerTick will detect _active=false and clear _inner.Tick on the correct thread. + _active = false; + _tick = null; + } } + } + } - if (_subscriberCount++ == 0) - { - _inner.Tick += InnerTick; - } + public bool RunsInBackground => true; + + private void EnsureStarted() + { + if (!_registered) + { + _registered = true; + _stopwatch.Start(); + _timerThread.Start(); } + } - remove + private void InnerTick(TimeSpan obj) + { + lock (_lock) { - if (--_subscriberCount == 0) + if (!_active) { - _inner.Tick -= InnerTick; + _inner.Tick = null; + return; } - - _tick -= value; } + _autoResetEvent.Set(); } private void RenderTimerThreadFunc() @@ -60,11 +82,4 @@ public sealed class ThreadProxyRenderTimer : IRenderTimer _tick?.Invoke(_stopwatch.Elapsed); } } - - private void InnerTick(TimeSpan obj) - { - _autoResetEvent.Set(); - } - - public bool RunsInBackground => true; } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 43eddb010d..bc94ff4388 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -55,7 +55,7 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToSingleton() .Bind().ToConstant(Keyboard) .Bind().ToSingleton() - .Bind().ToConstant(new UiThreadRenderTimer(60)) + .Bind().ToConstant(RenderLoop.FromTimer(new UiThreadRenderTimer(60))) .Bind().ToConstant(instance) .Bind().ToSingleton() .Bind().ToSingleton(); diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index d13d442e09..d9c8e333cb 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -69,16 +69,11 @@ namespace Avalonia.DesignerSupport.Remote private sealed class DummyRenderTimer : IRenderTimer { - public event Action Tick - { - add { } - remove { } - } - + public Action? Tick { get; set; } public bool RunsInBackground => false; } - public Compositor Compositor { get; } = new(new RenderLoop(new DummyRenderTimer()), null); + public Compositor Compositor { get; } = new(RenderLoop.FromTimer(new DummyRenderTimer()), null); public void Dispose() { diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 34f0ee766e..825eb254be 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -122,7 +122,7 @@ namespace Avalonia.Native .Bind().ToConstant(this) .Bind().ToConstant(clipboardImpl) .Bind().ToConstant(clipboard) - .Bind().ToConstant(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer()))) + .Bind().ToConstant(RenderLoop.FromTimer(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer())))) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) .Bind().ToConstant(applicationPlatform) diff --git a/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs index 625de5d6bc..d484b82ac7 100644 --- a/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs +++ b/src/Avalonia.Native/AvaloniaNativeRenderTimer.cs @@ -9,9 +9,8 @@ internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTim { private readonly IAvnPlatformRenderTimer _platformRenderTimer; private readonly Stopwatch _stopwatch; - private Action? _tick; - private int _subscriberCount; - private bool registered; + private volatile Action? _tick; + private bool _registered; public AvaloniaNativeRenderTimer(IAvnPlatformRenderTimer platformRenderTimer) { @@ -19,42 +18,41 @@ internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTim _stopwatch = Stopwatch.StartNew(); } - public event Action Tick + public Action? Tick { - add + get => _tick; + set { - _tick += value; - - if (!registered) + if (value != null) { - registered = true; - var registrationResult = _platformRenderTimer.RegisterTick(this); - if (registrationResult != 0) - { - throw new InvalidOperationException( - $"Avalonia.Native was not able to start the RenderTimer. Native error code is: {registrationResult}"); - } + _tick = value; + EnsureRegistered(); + _platformRenderTimer.Start(); } - - if (_subscriberCount++ == 0) + else { - _platformRenderTimer.Start(); + _platformRenderTimer.Stop(); + _tick = null; } } + } - remove + public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool(); + + private void EnsureRegistered() + { + if (!_registered) { - if (--_subscriberCount == 0) + _registered = true; + var registrationResult = _platformRenderTimer.RegisterTick(this); + if (registrationResult != 0) { - _platformRenderTimer.Stop(); + throw new InvalidOperationException( + $"Avalonia.Native was not able to start the RenderTimer. Native error code is: {registrationResult}"); } - - _tick -= value; } } - public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool(); - public void Run() { _tick?.Invoke(_stopwatch.Elapsed); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index bff986d2d1..566b0d907a 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -87,7 +87,7 @@ namespace Avalonia.X11 : new X11PlatformThreading(this); Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(timer) + .Bind().ToConstant(RenderLoop.FromTimer(timer)) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super")) .Bind().ToFunc(() => KeyboardDevice) diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs index fea94d0248..de9a167954 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs +++ b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs @@ -18,19 +18,16 @@ internal class BrowserRenderTimer : IRenderTimer public bool RunsInBackground { get; } - public event Action? Tick + public Action? Tick { - add + set { if (!BrowserWindowingPlatform.IsThreadingEnabled) StartOnThisThread(); - _tick += value; - } - remove - { - _tick -= value; + _tick = value; } + get => _tick; } public void StartOnThisThread() @@ -50,4 +47,4 @@ internal class BrowserRenderTimer : IRenderTimer tick.Invoke(TimeSpan.FromMilliseconds(timestamp)); } } -} +} \ No newline at end of file diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs index 8d454ff582..1d9d1248b8 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs +++ b/src/Browser/Avalonia.Browser/Rendering/BrowserSharedRenderLoop.cs @@ -9,5 +9,5 @@ internal static class BrowserSharedRenderLoop { private static BrowserRenderTimer? s_browserUiRenderTimer; public static BrowserRenderTimer RenderTimer => s_browserUiRenderTimer ??= new BrowserRenderTimer(false); - public static Lazy RenderLoop = new(() => new RenderLoop(RenderTimer), true); + public static Lazy RenderLoop = new(() => Avalonia.Rendering.RenderLoop.FromTimer(RenderTimer), true); } diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b56e686d4b..8e44942d32 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -15,6 +15,7 @@ namespace Avalonia.Headless public static class AvaloniaHeadlessPlatform { internal static Compositor? Compositor { get; private set; } + private static RenderTimer? s_renderTimer; private class RenderTimer : DefaultRenderTimer { @@ -85,7 +86,7 @@ namespace Avalonia.Headless .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(new KeyboardDevice()) - .Bind().ToConstant(new RenderTimer(60)) + .Bind().ToConstant(Rendering.RenderLoop.FromTimer(s_renderTimer = new RenderTimer(60))) .Bind().ToConstant(new HeadlessWindowingPlatform(opts.FrameBufferFormat)) .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })); @@ -99,9 +100,8 @@ namespace Avalonia.Headless /// Count of frames to be ticked on the timer. public static void ForceRenderTimerTick(int count = 1) { - var timer = AvaloniaLocator.Current.GetService() as RenderTimer; for (var c = 0; c < count; c++) - timer?.ForceTick(); + s_renderTimer?.ForceTick(); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index ee8b85919e..3239957d73 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -64,7 +64,7 @@ namespace Avalonia.LinuxFramebuffer Dispatcher.InitializeUIThreadDispatcher(new EpollDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(timer) + .Bind().ToConstant(RenderLoop.FromTimer(timer)) .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() diff --git a/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs b/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs index 6c73fbeb9e..55d49ca30c 100644 --- a/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs +++ b/src/Windows/Avalonia.Win32/DComposition/DirectCompositionConnection.cs @@ -21,15 +21,36 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor { private static readonly Guid IID_IDCompositionDesktopDevice = Guid.Parse("5f4633fe-1e08-4cb8-8c75-ce24333f5602"); - public event Action? Tick; + private volatile Action? _tick; public bool RunsInBackground => true; private readonly DirectCompositionShared _shared; + private readonly AutoResetEvent _wakeEvent = new(false); + private volatile bool _stopped = true; public DirectCompositionConnection(DirectCompositionShared shared) { _shared = shared; } + + public Action? Tick + { + get => _tick; + set + { + if (value != null) + { + _tick = value; + _stopped = false; + _wakeEvent.Set(); + } + else + { + _stopped = true; + _tick = null; + } + } + } private static bool TryCreateAndRegisterCore() { @@ -52,7 +73,7 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor } AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connect)); tcs.SetResult(true); } catch (Exception e) @@ -81,8 +102,11 @@ internal class DirectCompositionConnection : IRenderTimer, IWindowsSurfaceFactor { try { + if (_stopped) + WaitHandle.WaitAny([_wakeEvent, cts.Token.WaitHandle]); + device.WaitForCommitCompletion(); - Tick?.Invoke(_stopwatch.Elapsed); + _tick?.Invoke(_stopwatch.Elapsed); } catch (Exception ex) { diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs index 678b15e0d7..9ee2f25c86 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Platform.Surfaces; @@ -25,8 +26,10 @@ namespace Avalonia.Win32.DirectX public bool RunsInBackground => true; - public event Action? Tick; + private volatile Action? _tick; private readonly object _syncLock; + private readonly AutoResetEvent _wakeEvent = new(false); + private volatile bool _stopped = true; private IDXGIOutput? _output; @@ -37,6 +40,25 @@ namespace Avalonia.Win32.DirectX { _syncLock = syncLock; } + + public Action? Tick + { + get => _tick; + set + { + if (value != null) + { + _tick = value; + _stopped = false; + _wakeEvent.Set(); + } + else + { + _stopped = true; + _tick = null; + } + } + } public static bool TryCreateAndRegister() { @@ -70,6 +92,9 @@ namespace Avalonia.Win32.DirectX { try { + if (_stopped) + _wakeEvent.WaitOne(); + lock (_syncLock) { if (_output is not null) @@ -94,7 +119,7 @@ namespace Avalonia.Win32.DirectX // but theoretically someone could have a weirder setup out there DwmFlush(); } - Tick?.Invoke(_stopwatch.Elapsed); + _tick?.Invoke(_stopwatch.Elapsed); } } catch (Exception ex) @@ -199,7 +224,7 @@ namespace Avalonia.Win32.DirectX var connection = new DxgiConnection(pumpLock); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connection)); tcs.SetResult(true); connection.RunLoop(); } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index f158d539ff..7903a62d8f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -98,7 +98,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(WindowsKeyboardDevice.Instance) .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToConstant(renderTimer) + .Bind().ToConstant(RenderLoop.FromTimer(renderTimer)) .Bind().ToConstant(s_instance) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control) { diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs index fa7ff2e7a3..d4cdfcbe42 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs @@ -17,8 +17,29 @@ namespace Avalonia.Win32.WinRT.Composition; internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFactory { private readonly WinUiCompositionShared _shared; - public event Action? Tick; + private readonly AutoResetEvent _wakeEvent = new(false); + private volatile bool _stopped = true; + private volatile Action? _tick; public bool RunsInBackground => true; + + public Action? Tick + { + get => _tick; + set + { + if (value != null) + { + _tick = value; + _stopped = false; + _wakeEvent.Set(); + } + else + { + _stopped = true; + _tick = null; + } + } + } public WinUiCompositorConnection() { @@ -58,7 +79,7 @@ internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFa }); connect = new WinUiCompositorConnection(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(RenderLoop.FromTimer(connect)); tcs.SetResult(true); } @@ -102,8 +123,11 @@ internal class WinUiCompositorConnection : IRenderTimer, Win32.IWindowsSurfaceFa { _currentCommit?.Dispose(); _currentCommit = null; - _parent.Tick?.Invoke(_st.Elapsed); + _parent._tick?.Invoke(_st.Elapsed); + // Always schedule a commit so the current frame's work reaches DWM. ScheduleNextCommit(); + if (_parent._stopped) + _parent._wakeEvent.WaitOne(); } private void ScheduleNextCommit() diff --git a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs index 676554811e..e8a313afa8 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkTimer.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using System.Threading; -using System.Threading.Tasks; using Avalonia.Rendering; using CoreAnimation; using Foundation; @@ -11,7 +10,7 @@ namespace Avalonia.iOS { class DisplayLinkTimer : IRenderTimer { - public event Action? Tick; + private volatile Action? _tick; private Stopwatch _st = Stopwatch.StartNew(); public DisplayLinkTimer() @@ -31,9 +30,16 @@ namespace Avalonia.iOS public bool RunsInBackground => true; + // TODO: start/stop on RenderLoop request + public Action? Tick + { + get => _tick; + set => _tick = value; + } + private void OnLinkTick() { - Tick?.Invoke(_st.Elapsed); + _tick?.Invoke(_st.Elapsed); } } } diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 29633a8609..79926a3836 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -93,7 +93,7 @@ namespace Avalonia.iOS { Key.PageUp , "⇞" }, { Key.Right , "→" }, { Key.Space , "␣" }, { Key.Tab , "⇥" }, { Key.Up , "↑" } }, ctrl: "⌃", meta: "⌘", shift: "⇧", alt: "⌥")) - .Bind().ToConstant(Timer) + .Bind().ToConstant(RenderLoop.FromTimer(Timer)) .Bind().ToConstant(keyboard); if (appDelegate is not null) diff --git a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs index cf6d7a8aee..21ac9c1ae1 100644 --- a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs @@ -88,7 +88,7 @@ public class CompositionAnimationTests : ScopedTestBase { using var scope = AvaloniaLocator.EnterScope(); var compositor = - new Compositor(new RenderLoop(new CompositorTestServices.ManualRenderTimer()), null); + new Compositor(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null); var target = compositor.CreateSolidColorVisual(); var ani = new ScalarKeyFrameAnimation(compositor); foreach (var frame in data.Frames) diff --git a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs index 7451e3c843..06df626857 100644 --- a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs +++ b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs @@ -22,7 +22,7 @@ public class CompositionTargetUpdateOnly : IDisposable class Timer : IRenderTimer { - event Action IRenderTimer.Tick { add { } remove { } } + public Action Tick { get; set; } = null!; public bool RunsInBackground => false; } @@ -52,7 +52,7 @@ public class CompositionTargetUpdateOnly : IDisposable { _includeRender = includeRender; _app = UnitTestApplication.Start(TestServices.StyledWindow); - _compositor = new Compositor(new RenderLoop(new Timer()), null, true, new ManualScheduler(), true, + _compositor = new Compositor(RenderLoop.FromTimer(new Timer()), null, true, new ManualScheduler(), true, Dispatcher.UIThread, null); _target = _compositor.CreateCompositionTarget(() => [new NullFramebuffer()]); _target.PixelSize = new PixelSize(1000, 1000); @@ -99,7 +99,7 @@ public class CompositionTargetUpdateOnly : IDisposable { _target.Root.Offset = new Vector3D(_target.Root.Offset.X == 0 ? 1 : 0, 0, 0); _compositor.Commit(); - _compositor.Server.Render(); + _compositor.Server.Render(false); if (!_includeRender) _target.Server.Update(); diff --git a/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs b/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs index 588785cf69..5c44809a85 100644 --- a/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs +++ b/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs @@ -47,7 +47,7 @@ public class DirectFbCompositionTests : TestBase void Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised(bool advertised) { var timer = new ManualRenderTimer(); - var compositor = new Compositor(new RenderLoop(timer), null, true, + var compositor = new Compositor(RenderLoop.FromTimer(timer), null, true, new DispatcherCompositorScheduler(), true, Dispatcher.UIThread, new CompositionOptions { UseRegionDirtyRectClipping = true diff --git a/tests/Avalonia.RenderTests/ManualRenderTimer.cs b/tests/Avalonia.RenderTests/ManualRenderTimer.cs index 6247691453..1b6e7539a0 100644 --- a/tests/Avalonia.RenderTests/ManualRenderTimer.cs +++ b/tests/Avalonia.RenderTests/ManualRenderTimer.cs @@ -5,7 +5,7 @@ namespace Avalonia.Skia.RenderTests { public class ManualRenderTimer : IRenderTimer { - public event Action? Tick; + public Action? Tick { get; set; } public bool RunsInBackground => false; public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero); } diff --git a/tests/Avalonia.RenderTests/TestRenderHelper.cs b/tests/Avalonia.RenderTests/TestRenderHelper.cs index 68cf05e9b9..87132881f7 100644 --- a/tests/Avalonia.RenderTests/TestRenderHelper.cs +++ b/tests/Avalonia.RenderTests/TestRenderHelper.cs @@ -63,7 +63,7 @@ static class TestRenderHelper { var timer = new ManualRenderTimer(); - var compositor = new Compositor(new RenderLoop(timer), null, true, + var compositor = new Compositor(RenderLoop.FromTimer(timer), null, true, new DispatcherCompositorScheduler(), true, Dispatcher.UIThread); using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat)) diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index e90e1cff0e..cb2a84049c 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -42,9 +42,10 @@ public class CompositorTestServices : IDisposable _app = UnitTestApplication.Start(services); try { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(Timer); + var renderLoop = RenderLoop.FromTimer(Timer); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(renderLoop); - Compositor = new Compositor(new RenderLoop(Timer), null, + Compositor = new Compositor(renderLoop, null, true, new DispatcherCompositorScheduler(), true, Dispatcher.UIThread); var impl = new TopLevelImpl(Compositor, size ?? new Size(1000, 1000)); TopLevel = new EmbeddableControlRoot(impl) @@ -136,7 +137,7 @@ public class CompositorTestServices : IDisposable public class ManualRenderTimer : IRenderTimer { - public event Action? Tick; + public Action? Tick { get; set; } public bool RunsInBackground => false; public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero); } diff --git a/tests/Avalonia.UnitTests/RendererMocks.cs b/tests/Avalonia.UnitTests/RendererMocks.cs index 32d171e147..9b172fe342 100644 --- a/tests/Avalonia.UnitTests/RendererMocks.cs +++ b/tests/Avalonia.UnitTests/RendererMocks.cs @@ -17,7 +17,7 @@ namespace Avalonia.UnitTests } public static Compositor CreateDummyCompositor() => - new(new RenderLoop(new CompositorTestServices.ManualRenderTimer()), null, false, + new(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null, false, new CompositionCommitScheduler(), true, Dispatcher.UIThread); class CompositionCommitScheduler : ICompositorScheduler