From 5ec011f0297b027d5c463b61ec80fca32db55116 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 20 Sep 2022 15:33:52 +0300 Subject: [PATCH] Moved commit/frame throttling logic to Compositor --- .../Composition/CompositingRenderer.cs | 31 ++-- .../Composition/CompositionTarget.cs | 2 +- .../Composition/Compositor.Factories.cs | 32 +++++ .../Rendering/Composition/Compositor.cs | 134 ++++++++---------- .../Rendering/Composition/Transport/Batch.cs | 10 +- .../Threading/DispatcherPriority.cs | 11 +- 6 files changed, 117 insertions(+), 103 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index e47c8e7e31..98a6a3600e 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Collections.Pooled; using Avalonia.Media; @@ -27,8 +28,7 @@ public class CompositingRenderer : IRendererWithCompositor private HashSet _dirty = new(); private HashSet _recalculateChildren = new(); private bool _queuedUpdate; - private Action _update; - private Action _invalidateScene; + private Action _update; private bool _updating; internal CompositionTarget CompositionTarget; @@ -47,7 +47,6 @@ public class CompositingRenderer : IRendererWithCompositor CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget); CompositionTarget.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); _update = Update; - _invalidateScene = InvalidateScene; } /// @@ -72,7 +71,7 @@ public class CompositingRenderer : IRendererWithCompositor if(_queuedUpdate) return; _queuedUpdate = true; - _compositor.InvokeWhenReadyForNextCommit(_update); + _compositor.InvokeBeforeNextCommit(_update); } /// @@ -151,12 +150,6 @@ public class CompositingRenderer : IRendererWithCompositor if (compositionChildren.Count == visualChildren.Count) { bool mismatch = false; - if (v.HasNonUniformZIndexChildren) - { - - - } - if (sortedChildren != null) for (var c = 0; c < visualChildren.Count; c++) { @@ -202,9 +195,6 @@ public class CompositingRenderer : IRendererWithCompositor } } - private void InvalidateScene() => - SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); - private void UpdateCore() { _queuedUpdate = false; @@ -252,10 +242,15 @@ public class CompositingRenderer : IRendererWithCompositor _recalculateChildren.Clear(); CompositionTarget.Size = _root.ClientSize; CompositionTarget.Scaling = _root.RenderScaling; - Compositor.InvokeOnNextCommit(_invalidateScene); } - private void Update() + private async void TriggerSceneInvalidatedOnBatchCompletion(Task batchCompletion) + { + await batchCompletion; + SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); + } + + private void Update(Task batchCompletion) { if(_updating) return; @@ -276,10 +271,10 @@ public class CompositingRenderer : IRendererWithCompositor public void Paint(Rect rect) { - Update(); + QueueUpdate(); CompositionTarget.RequestRedraw(); if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) - Compositor.RequestCommitAsync().Wait(); + Compositor.Commit().Wait(); else CompositionTarget.ImmediateUIThreadRender(); } @@ -299,7 +294,7 @@ public class CompositingRenderer : IRendererWithCompositor // Wait for the composition batch to be applied and rendered to guarantee that // render target is not used anymore and can be safely disposed if (Compositor.Loop.RunsInBackground) - _compositor.RequestCommitAsync().Wait(); + _compositor.Commit().Wait(); } /// diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index eb499604e0..d8a608651b 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -129,7 +129,7 @@ namespace Avalonia.Rendering.Composition /// internal void ImmediateUIThreadRender() { - Compositor.RequestCommitAsync(); + Compositor.Commit(); Compositor.Server.Render(); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs new file mode 100644 index 0000000000..1a13d23acd --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -0,0 +1,32 @@ +using System; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public partial class Compositor +{ + /// + /// Creates a new CompositionTarget + /// + /// A factory method to create IRenderTarget to be called from the render thread + /// + public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) + { + return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); + } + + public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); + + public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this); + + public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this) + { + Expression = expression + }; + + public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); + + public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); +} \ 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 10360f7874..21c7dd6bf9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -10,6 +10,7 @@ using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; using Avalonia.Threading; +using Avalonia.Utilities; // Special license applies License.md @@ -24,16 +25,18 @@ namespace Avalonia.Rendering.Composition { internal IRenderLoop Loop { get; } private ServerCompositor _server; - private bool _implicitBatchCommitQueued; - private Action _implicitBatchCommit; + private TaskCompletionSource? _nextCommit; + private Action _commit; private BatchStreamObjectPool _batchObjectPool = new(); private BatchStreamMemoryPool _batchMemoryPool = new(); private List _objectsForSerialization = new(); + private Queue> _invokeBeforeCommit = new(); internal ServerCompositor Server => _server; + private Task? _pendingBatch; + private readonly object _pendingBatchLock = new(); + internal IEasing DefaultEasing { get; } - private List? _invokeOnNextCommit; - private readonly Stack> _invokeListPool = new(); - private Task? _lastBatchCompleted; + /// /// Creates a new compositor on a specified render loop that would use a particular GPU @@ -44,21 +47,15 @@ namespace Avalonia.Rendering.Composition { Loop = loop; _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); - _implicitBatchCommit = ImplicitBatchCommit; + _commit = () => + { + Console.WriteLine("Dispatcher:Commit"); + Commit(); + }; DefaultEasing = new CubicBezierEasing(new Point(0.25f, 0.1f), new Point(0.25f, 1f)); } - /// - /// Creates a new CompositionTarget - /// - /// A factory method to create IRenderTarget to be called from the render thread - /// - public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) - { - return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); - } - /// /// Requests pending changes in the composition objects to be serialized and sent to the render thread /// @@ -66,7 +63,35 @@ namespace Avalonia.Rendering.Composition public Task RequestCommitAsync() { Dispatcher.UIThread.VerifyAccess(); - var batch = new Batch(); + if (_nextCommit == null) + { + _nextCommit = new TaskCompletionSource(); + var pending = _pendingBatch; + if (pending != null) + { + pending.ContinueWith(_ => + { + Dispatcher.UIThread.Post(_commit, DispatcherPriority.Composition); + }); + } + else + Dispatcher.UIThread.Post(_commit, DispatcherPriority.Composition); + } + + return _nextCommit.Task; + } + + internal Task Commit() + { + Dispatcher.UIThread.VerifyAccess(); + using var noPump = NonPumpingLockHelper.Use(); + + _nextCommit ??= new TaskCompletionSource(); + + while (_invokeBeforeCommit.Count > 0) + _invokeBeforeCommit.Dequeue()(_nextCommit.Task); + + var batch = new Batch(_nextCommit); using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) { @@ -84,71 +109,36 @@ namespace Avalonia.Rendering.Composition batch.CommitedAt = Server.Clock.Elapsed; _server.EnqueueBatch(batch); - if (_invokeOnNextCommit != null) - ScheduleCommitCallbacks(batch.Completed); - return _lastBatchCompleted = batch.Completed; - } - - async void ScheduleCommitCallbacks(Task task) - { - var list = _invokeOnNextCommit; - _invokeOnNextCommit = null; - await task; - foreach (var i in list!) - i(); - list.Clear(); - _invokeListPool.Push(list); - } - - public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); - - public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this); - - public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this) - { - Expression = expression - }; - - public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); - - public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); - - private void QueueImplicitBatchCommit() - { - if(_implicitBatchCommitQueued) - return; - _implicitBatchCommitQueued = true; - Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch); - } - - private void ImplicitBatchCommit() - { - _implicitBatchCommitQueued = false; - RequestCommitAsync(); + lock (_pendingBatchLock) + { + _pendingBatch = _nextCommit.Task; + _pendingBatch.ContinueWith(t => + { + lock (_pendingBatchLock) + { + if (_pendingBatch == t) + _pendingBatch = null; + } + }, TaskContinuationOptions.ExecuteSynchronously); + _nextCommit = null; + + return _pendingBatch; + } } internal void RegisterForSerialization(CompositionObject compositionObject) { Dispatcher.UIThread.VerifyAccess(); _objectsForSerialization.Add(compositionObject); - QueueImplicitBatchCommit(); - } - - internal void InvokeOnNextCommit(Action action) - { - _invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new(); - _invokeOnNextCommit.Add(action); + RequestCommitAsync(); } - public void InvokeWhenReadyForNextCommit(Action action) + internal void InvokeBeforeNextCommit(Action action) { - if (_lastBatchCompleted == null || _lastBatchCompleted.IsCompleted) - Dispatcher.UIThread.Post(action, DispatcherPriority.Composition); - else - _lastBatchCompleted.ContinueWith( - static (_, state) => Dispatcher.UIThread.Post((Action)state!, DispatcherPriority.Composition), - action); + Dispatcher.UIThread.VerifyAccess(); + _invokeBeforeCommit.Enqueue(action); + RequestCommitAsync(); } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs index 0cf1650ccf..d3e3664f84 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -15,16 +15,19 @@ namespace Avalonia.Rendering.Composition.Transport { private static long _nextSequenceId = 1; private static ConcurrentBag _pool = new(); + private readonly TaskCompletionSource? _tcs; public long SequenceId { get; } - public Batch() + public Batch(TaskCompletionSource? tcs) { + _tcs = tcs; SequenceId = Interlocked.Increment(ref _nextSequenceId); if (!_pool.TryTake(out var lst)) lst = new BatchStreamData(); Changes = lst; } - private TaskCompletionSource _tcs = new TaskCompletionSource(); + + public BatchStreamData Changes { get; private set; } public TimeSpan CommitedAt { get; set; } @@ -33,9 +36,8 @@ namespace Avalonia.Rendering.Composition.Transport _pool.Add(Changes); Changes = null!; - _tcs.TrySetResult(0); + _tcs?.TrySetResult(0); } - public Task Completed => _tcs.Task; } } diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index b4bf603f74..f041590fc0 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -61,21 +61,16 @@ namespace Avalonia.Threading /// The job will be processed with the same priority as render. /// public static readonly DispatcherPriority Render = new(5); - - /// - /// The job will be processed with the same priority as composition batch commit. - /// - public static readonly DispatcherPriority CompositionBatch = new(6); /// /// The job will be processed with the same priority as composition updates. /// - public static readonly DispatcherPriority Composition = new(7); + public static readonly DispatcherPriority Composition = new(6); /// /// The job will be processed with the same priority as render. /// - public static readonly DispatcherPriority Layout = new(8); + public static readonly DispatcherPriority Layout = new(7); /// /// The job will be processed with the same priority as data binding. @@ -85,7 +80,7 @@ namespace Avalonia.Threading /// /// The job will be processed before other asynchronous operations. /// - public static readonly DispatcherPriority Send = new(9); + public static readonly DispatcherPriority Send = new(8); /// /// Maximum possible priority