From 30669d67b3ed1bd1e9a8fb6eea345ad2de0787c2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 29 May 2023 18:25:16 +0600 Subject: [PATCH 1/8] IEasing - not-client-implementable --- src/Avalonia.Base/Animation/Easings/IEasing.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Animation/Easings/IEasing.cs b/src/Avalonia.Base/Animation/Easings/IEasing.cs index 9c9c7b3a99..804c7b7912 100644 --- a/src/Avalonia.Base/Animation/Easings/IEasing.cs +++ b/src/Avalonia.Base/Animation/Easings/IEasing.cs @@ -1,8 +1,11 @@ +using Avalonia.Metadata; + namespace Avalonia.Animation.Easings { /// /// Defines the interface for easing classes. /// + [NotClientImplementable] public interface IEasing { /// From 1b2af003c6e3ee02ac2606c01cf7e725840715ec Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 29 May 2023 19:06:02 +0600 Subject: [PATCH 2/8] Ported FireInvokeOnRenderCallbacks --- src/Avalonia.Base/Layout/LayoutManager.cs | 4 +- src/Avalonia.Base/Media/MediaContext.cs | 64 ++++++++++++++++++++--- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 3665fc4475..6d698ae5ce 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -28,10 +28,12 @@ namespace Avalonia.Layout private bool _queued; private bool _running; private int _totalPassCount; + private Action _invokeOnRender; public LayoutManager(ILayoutRoot owner) { _owner = owner as Layoutable ?? throw new ArgumentNullException(nameof(owner)); + _invokeOnRender = ExecuteQueuedLayoutPass; } public virtual event EventHandler? LayoutUpdated; @@ -345,7 +347,7 @@ namespace Avalonia.Layout if (!_queued && !_running) { _queued = true; - MediaContext.Instance.QueueLayoutPass(this); + MediaContext.Instance.BeginInvokeOnRender(_invokeOnRender); } } diff --git a/src/Avalonia.Base/Media/MediaContext.cs b/src/Avalonia.Base/Media/MediaContext.cs index 5bd4a51102..023b2f728c 100644 --- a/src/Avalonia.Base/Media/MediaContext.cs +++ b/src/Avalonia.Base/Media/MediaContext.cs @@ -16,6 +16,7 @@ internal partial class MediaContext : ICompositorScheduler private DispatcherOperation? _nextRenderOp; private DispatcherOperation? _inputMarkerOp; private TimeSpan _inputMarkerAddedAt; + private bool _isRendering; private bool _animationsAreWaitingForComposition; private const double MaxSecondsWithoutInput = 1; private readonly Action _render; @@ -23,7 +24,9 @@ internal partial class MediaContext : ICompositorScheduler private readonly HashSet _requestedCommits = new(); private readonly Dictionary _pendingCompositionBatches = new(); private record TopLevelInfo(Compositor Compositor, CompositingRenderer Renderer, ILayoutManager LayoutManager); - private readonly HashSet _queuedLayoutManagers = new(); + + private List? _invokeOnRenderCallbacks; + private readonly Stack> _invokeOnRenderCallbackListPool = new(); private Dictionary _topLevels = new(); @@ -95,11 +98,13 @@ internal partial class MediaContext : ICompositorScheduler { try { + _isRendering = true; RenderCore(); } finally { _nextRenderOp = null; + _isRendering = false; } } @@ -114,10 +119,7 @@ internal partial class MediaContext : ICompositorScheduler for (var c = 0; c < 10; c++) { _clock.HasNewSubscriptions = false; - //TODO: Integrate LayoutManager's attempt limit here - foreach (var layout in _queuedLayoutManagers.ToArray()) - layout.ExecuteQueuedLayoutPass(); - _queuedLayoutManagers.Clear(); + FireInvokeOnRenderCallbacks(); if (_clock.HasNewSubscriptions) { @@ -160,9 +162,55 @@ internal partial class MediaContext : ICompositorScheduler } } - public void QueueLayoutPass(LayoutManager layoutManager) + /// + /// Calls all _invokeOnRenderCallbacks until no more are added + /// + private void FireInvokeOnRenderCallbacks() { - _queuedLayoutManagers.Add(layoutManager); - ScheduleRender(true); + int callbackLoopCount = 0; + int count = _invokeOnRenderCallbacks?.Count ?? 0; + + // This outer loop is to re-run layout in case the app causes a layout to get enqueued in response + // to a Loaded event. In this case we would like to re-run layout before we allow render. + do + { + while (count > 0) + { + callbackLoopCount++; + if (callbackLoopCount > 153) + throw new InvalidOperationException("Infinite layout loop detected"); + + var callbacks = _invokeOnRenderCallbacks!; + _invokeOnRenderCallbacks = null; + + for (int i = 0; i < count; i++) + callbacks[i].Invoke(); + + callbacks.Clear(); + _invokeOnRenderCallbackListPool.Push(callbacks); + + count = _invokeOnRenderCallbacks?.Count ?? 0; + } + + // TODO: port the rest of the Loaded logic later + // Fire all the pending Loaded events before Render happens + // but after the layout storm has subsided + // FireLoadedPendingCallbacks(); + + count = _invokeOnRenderCallbacks?.Count ?? 0; + } + while (count > 0); + } + + public void BeginInvokeOnRender(Action callback) + { + if (_invokeOnRenderCallbacks == null) + _invokeOnRenderCallbacks = + _invokeOnRenderCallbackListPool.Count > 0 ? _invokeOnRenderCallbackListPool.Pop() : new(); + + _invokeOnRenderCallbacks.Add(callback); + + if (!_isRendering) + ScheduleRender(true); } } \ No newline at end of file From 9debf63c71fc7b17e4c08b914fc977f67f0d5162 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 29 May 2023 20:31:10 +0600 Subject: [PATCH 3/8] Fixed ToggleSwitch animation --- .../Media/MediaContext.Compositor.cs | 13 ++++++++--- src/Avalonia.Base/Media/MediaContext.cs | 23 ++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Media/MediaContext.Compositor.cs b/src/Avalonia.Base/Media/MediaContext.Compositor.cs index d9d3c915d4..6234f36a12 100644 --- a/src/Avalonia.Base/Media/MediaContext.Compositor.cs +++ b/src/Avalonia.Base/Media/MediaContext.Compositor.cs @@ -40,10 +40,10 @@ partial class MediaContext _animationsAreWaitingForComposition = false; // Check if we have uncommited changes - if (_scheduleCommitOnLastCompositionBatchCompletion && _pendingCompositionBatches.Count > 0) + if (_scheduleCommitOnLastCompositionBatchCompletion) { - CommitCompositorsWithThrottling(); _scheduleCommitOnLastCompositionBatchCompletion = false; + CommitCompositorsWithThrottling(); } // Check if there are active animations and schedule the next render else if(_clock.HasSubscriptions) @@ -52,16 +52,23 @@ partial class MediaContext } + /// + /// Triggers a composition commit if any batches are waiting to be sent, + /// handles throttling + /// + /// true if there are pending commits in-flight and there will be a "all-done" callback later private bool CommitCompositorsWithThrottling() { // Check if we are still waiting for previous composition batches if (_pendingCompositionBatches.Count > 0) { _scheduleCommitOnLastCompositionBatchCompletion = true; + // Previous commit isn't handled yet return true; } - + if (_requestedCommits.Count == 0) + // Nothing to do, and there are no pending commits return false; foreach (var c in _requestedCommits) diff --git a/src/Avalonia.Base/Media/MediaContext.cs b/src/Avalonia.Base/Media/MediaContext.cs index 023b2f728c..c261dd80b8 100644 --- a/src/Avalonia.Base/Media/MediaContext.cs +++ b/src/Avalonia.Base/Media/MediaContext.cs @@ -28,6 +28,14 @@ internal partial class MediaContext : ICompositorScheduler private List? _invokeOnRenderCallbacks; private readonly Stack> _invokeOnRenderCallbackListPool = new(); + private DispatcherTimer _animationsTimer = new(DispatcherPriority.Render) + { + // Since this timer is used to drive animations that didn't contribute to the previous frame at all + // We can safely use 16ms interval until we fix our animation system to actually report the next expected + // frame + Interval = TimeSpan.FromMilliseconds(16) + }; + private Dictionary _topLevels = new(); private MediaContext() @@ -35,6 +43,11 @@ internal partial class MediaContext : ICompositorScheduler _render = Render; _inputMarkerHandler = InputMarkerHandler; _clock = new(this); + _animationsTimer.Tick += (_, _) => + { + _animationsTimer.Stop(); + ScheduleRender(false); + }; } public static MediaContext Instance @@ -129,14 +142,12 @@ internal partial class MediaContext : ICompositorScheduler break; } - - // We are currently using compositor commit callbacks to drive animations - // Later we should use WPF's approach that asks the animation system for the next tick time - // and use some timer if the next animation frame is not needed to be sent to the compositor immediately + if (_requestedCommits.Count > 0 || _clock.HasSubscriptions) { - _animationsAreWaitingForComposition = true; - CommitCompositorsWithThrottling(); + _animationsAreWaitingForComposition = CommitCompositorsWithThrottling(); + if (!_animationsAreWaitingForComposition && _clock.HasSubscriptions) + _animationsTimer.Start(); } } From cd7c62be7108af45881998f03a8c6b04298f3ae0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 29 May 2023 23:12:01 +0600 Subject: [PATCH 4/8] Assume that non-composition-aware transforms are immutable --- .../Composition/Drawing/ServerResourceHelperExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs index 63b81a742c..2a6971e8ea 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs @@ -50,7 +50,6 @@ static class ServerResourceHelperExtensions return immutable; if (transform is ICompositionRenderResource resource) resource.GetForCompositor(compositor); - ThrowNotCompatible(transform); - return null; + return new ImmutableTransform(transform.Value); } } \ No newline at end of file From 72c549a205e89291b65a73d0c6ab3ed280688798 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 29 May 2023 23:21:28 +0600 Subject: [PATCH 5/8] Correctly handle Push/Pop without inner items --- .../Drawing/RenderDataDrawingContext.cs | 17 ++++++++++++----- .../Rendering/SceneGraph/DrawOperationTests.cs | 13 +++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs index 971ae1d8aa..3d96dc6bbb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs @@ -79,12 +79,19 @@ internal class RenderDataDrawingContext : DrawingContext if (!(parent.Node is T)) throw new InvalidOperationException("Invalid Pop operation"); - - foreach(var item in _currentItemList!) - parent.Node.Children.Add(item); - _currentItemList.Clear(); - s_listPool.ReturnAndSetNull(ref _currentItemList); + + var removeLastPush = true; + if (_currentItemList != null) + { + removeLastPush = _currentItemList.Count == 0; + foreach (var item in _currentItemList) + parent.Node.Children.Add(item); + _currentItemList.Clear(); + s_listPool.ReturnAndSetNull(ref _currentItemList); + } _currentItemList = parent.Items; + if (removeLastPush) + _currentItemList.RemoveAt(_currentItemList.Count - 1); } void AddResource(object? resource) diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 5c6115f8c8..2be429adf8 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -132,5 +132,18 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph Assert.True(ctx.Context.GetRenderResults()!.HitTest(new Point(25, 25))); } + + [Fact] + public void Empty_Push_Pop_Sequence_Produces_No_Results() + { + var ctx = new TestContext(_services); + using (ctx.Context.PushTransform(Matrix.CreateTranslation(20, 20))) + using (ctx.Context.PushOpacity(1, default)) + { + + } + + Assert.Null(ctx.Context.GetRenderResults()); + } } } From e9ca33d2839cd4129b6bf4b6b1617cb0eafb261b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 30 May 2023 00:25:12 +0600 Subject: [PATCH 6/8] Fixed animations --- src/Avalonia.Base/Media/MediaContext.Compositor.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Media/MediaContext.Compositor.cs b/src/Avalonia.Base/Media/MediaContext.Compositor.cs index 6234f36a12..9bdd77960d 100644 --- a/src/Avalonia.Base/Media/MediaContext.Compositor.cs +++ b/src/Avalonia.Base/Media/MediaContext.Compositor.cs @@ -43,13 +43,20 @@ partial class MediaContext if (_scheduleCommitOnLastCompositionBatchCompletion) { _scheduleCommitOnLastCompositionBatchCompletion = false; - CommitCompositorsWithThrottling(); + if (!CommitCompositorsWithThrottling()) + ScheduleRenderForAnimationsIfNeeded(); + } // Check if there are active animations and schedule the next render - else if(_clock.HasSubscriptions) - ScheduleRender(false); + else + ScheduleRenderForAnimationsIfNeeded(); } + } + void ScheduleRenderForAnimationsIfNeeded() + { + if (_clock.HasSubscriptions) + ScheduleRender(false); } /// From 9628f31342c4670cff1f7ed93ab43bb76968386f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 30 May 2023 00:25:26 +0600 Subject: [PATCH 7/8] fixed wasm --- src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 638c732d5b..f8fa0011f3 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -187,9 +187,7 @@ namespace Avalonia.Browser } - public Compositor Compositor { get; } = new( - AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetRequiredService()); + public Compositor Compositor { get; } = new(AvaloniaLocator.Current.GetRequiredService()); public void Invalidate(Rect rect) { From f3cb243de5dfeef3b73ff979a4bb34a4059470e4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 29 May 2023 17:49:38 -0400 Subject: [PATCH 8/8] Fix android/ios --- src/Android/Avalonia.Android/AndroidPlatform.cs | 6 +----- src/Avalonia.Base/Rendering/Composition/Compositor.cs | 8 +++----- src/iOS/Avalonia.iOS/Platform.cs | 6 +----- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 2e22386ef8..d5d5f211e9 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -52,11 +52,7 @@ namespace Avalonia.Android EglPlatformGraphics.TryInitialize(); } - Compositor = new Compositor( - AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); - - + Compositor = new Compositor(AvaloniaLocator.Current.GetService()); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 0a4b7be15e..dde9dcd6fb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -66,16 +66,14 @@ namespace Avalonia.Rendering.Composition /// Creates a new compositor on a specified render loop that would use a particular GPU /// [PrivateApi] - public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false) + public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false) : this(RenderLoop.LocatorAutoInstance, gpu, useUiThreadForSynchronousCommits) { - } - - internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false) + + internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false) : this(loop, gpu, useUiThreadForSynchronousCommits, MediaContext.Instance, false) { - } internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu, diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 918c0caaa2..de664c93e0 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -47,12 +47,8 @@ namespace Avalonia.iOS .Bind().ToConstant(DispatcherImpl.Instance) .Bind().ToConstant(keyboard); - Compositor = new Compositor( - AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()); } - - } }