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/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 { /// 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.Compositor.cs b/src/Avalonia.Base/Media/MediaContext.Compositor.cs index d9d3c915d4..9bdd77960d 100644 --- a/src/Avalonia.Base/Media/MediaContext.Compositor.cs +++ b/src/Avalonia.Base/Media/MediaContext.Compositor.cs @@ -40,28 +40,42 @@ partial class MediaContext _animationsAreWaitingForComposition = false; // Check if we have uncommited changes - if (_scheduleCommitOnLastCompositionBatchCompletion && _pendingCompositionBatches.Count > 0) + if (_scheduleCommitOnLastCompositionBatchCompletion) { - CommitCompositorsWithThrottling(); _scheduleCommitOnLastCompositionBatchCompletion = false; + 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); } + /// + /// 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 5bd4a51102..c261dd80b8 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,17 @@ 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 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(); @@ -32,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 @@ -95,11 +111,13 @@ internal partial class MediaContext : ICompositorScheduler { try { + _isRendering = true; RenderCore(); } finally { _nextRenderOp = null; + _isRendering = false; } } @@ -114,10 +132,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) { @@ -127,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(); } } @@ -160,9 +173,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 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/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/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 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) { 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()); } - - } } 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()); + } } }