浏览代码

Added guards for compositor reentrancy and exposed batch lifetime events (#12968)

* Added guards for compositor reentrancy and exposed batch lifetime events

* Use manual continue with since our tests are using some borked sync context
Nikita Tsukanov 2 年之前
父节点
当前提交
96d2ea4f22

+ 2 - 2
src/Avalonia.Base/Media/MediaContext.Compositor.cs

@@ -16,7 +16,7 @@ partial class MediaContext
     /// Actually sends the current batch to the compositor and does the required housekeeping
     /// This is the only place that should be allowed to call Commit
     /// </summary>
-    private Batch CommitCompositor(Compositor compositor)
+    private CompositionBatch CommitCompositor(Compositor compositor)
     {
         var commit = compositor.Commit();
         _requestedCommits.Remove(compositor);
@@ -29,7 +29,7 @@ partial class MediaContext
     /// <summary>
     /// Handles batch completion, required to re-schedule a render pass if one was skipped due to compositor throttling
     /// </summary>
-    private void CompositionBatchFinished(Compositor compositor, Batch batch)
+    private void CompositionBatchFinished(Compositor compositor, CompositionBatch batch)
     {
         // Check if it was the last commited batch, since sometimes we are forced to send a new
         // one without waiting for the previous one to complete  

+ 1 - 1
src/Avalonia.Base/Media/MediaContext.cs

@@ -22,7 +22,7 @@ internal partial class MediaContext : ICompositorScheduler
     private readonly Action _render;
     private readonly Action _inputMarkerHandler;
     private readonly HashSet<Compositor> _requestedCommits = new();
-    private readonly Dictionary<Compositor, Batch> _pendingCompositionBatches = new();
+    private readonly Dictionary<Compositor, CompositionBatch> _pendingCompositionBatches = new();
     private record  TopLevelInfo(Compositor Compositor, CompositingRenderer Renderer, ILayoutManager LayoutManager);
 
     private List<Action>? _invokeOnRenderCallbacks;

+ 13 - 7
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@@ -8,6 +8,7 @@ using Avalonia.Collections;
 using Avalonia.Collections.Pooled;
 using Avalonia.Media;
 using Avalonia.Rendering.Composition.Drawing;
+using Avalonia.Threading;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Rendering.Composition;
@@ -25,6 +26,7 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
     private readonly Action _update;
 
     private bool _queuedUpdate;
+    private bool _queuedSceneInvalidation;
     private bool _updating;
     private bool _isDisposed;
 
@@ -172,13 +174,17 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
         _recalculateChildren.Clear();
         CompositionTarget.Size = _root.ClientSize;
         CompositionTarget.Scaling = _root.RenderScaling;
-        TriggerSceneInvalidatedOnBatchCompletion(_compositor.RequestCommitAsync());
-    }
-
-    private async void TriggerSceneInvalidatedOnBatchCompletion(Task batchCompletion)
-    {
-        await batchCompletion;
-        SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
+        
+        var commit = _compositor.RequestCommitAsync();
+        if (!_queuedSceneInvalidation)
+        {
+            _queuedSceneInvalidation = true;
+            commit.ContinueWith(_ => Dispatcher.UIThread.Post(() =>
+            {
+                _queuedSceneInvalidation = false;
+                SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
+            }, DispatcherPriority.Input));
+        }
     }
 
     public void TriggerSceneInvalidatedForUnitTests(Rect rect) =>

+ 13 - 7
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@@ -24,7 +24,7 @@ namespace Avalonia.Rendering.Composition
         internal IRenderLoop Loop { get; }
         internal bool UseUiThreadForSynchronousCommits { get; }
         private ServerCompositor _server;
-        private Batch? _nextCommit;
+        private CompositionBatch? _nextCommit;
         private BatchStreamObjectPool<object?> _batchObjectPool;
         private BatchStreamMemoryPool _batchMemoryPool;
         private Queue<ICompositorSerializable> _objectSerializationQueue = new();
@@ -32,7 +32,7 @@ namespace Avalonia.Rendering.Composition
         private Queue<Action> _invokeBeforeCommitWrite = new(), _invokeBeforeCommitRead = new();
         private HashSet<IDisposable> _disposeOnNextBatch = new();
         internal ServerCompositor Server => _server;
-        private Batch? _pendingBatch;
+        private CompositionBatch? _pendingBatch;
         private readonly object _pendingBatchLock = new();
         private List<Action> _pendingServerCompositorJobs = new();
         private DiagnosticTextRenderer? _diagnosticTextRenderer;
@@ -90,8 +90,14 @@ namespace Avalonia.Rendering.Composition
         /// <summary>
         /// Requests pending changes in the composition objects to be serialized and sent to the render thread
         /// </summary>
-        /// <returns>A task that completes when sent changes are applied and rendered on the render thread</returns>
-        public Task RequestCommitAsync()
+        /// <returns>A task that completes when sent changes are applied on the render thread</returns>
+        public Task RequestCommitAsync() => RequestCompositionBatchCommitAsync().Processed;
+
+        /// <summary>
+        /// Requests pending changes in the composition objects to be serialized and sent to the render thread
+        /// </summary>
+        /// <returns>A CompositionBatch object that provides batch lifetime information</returns>
+        public CompositionBatch RequestCompositionBatchCommitAsync()
         {
             Dispatcher.UIThread.VerifyAccess();
             if (_nextCommit == null)
@@ -105,10 +111,10 @@ namespace Avalonia.Rendering.Composition
                     _triggerCommitRequested();
             }
 
-            return _nextCommit.Processed;
+            return _nextCommit;
         }
 
-        internal Batch Commit()
+        internal CompositionBatch Commit()
         {
             try
             {
@@ -122,7 +128,7 @@ namespace Avalonia.Rendering.Composition
             }
         }
         
-        Batch CommitCore()
+        CompositionBatch CommitCore()
         {
             Dispatcher.UIThread.VerifyAccess();
             using var noPump = NonPumpingLockHelper.Use();

+ 37 - 9
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@@ -7,6 +7,7 @@ using Avalonia.Platform;
 using Avalonia.Rendering.Composition.Animations;
 using Avalonia.Rendering.Composition.Expressions;
 using Avalonia.Rendering.Composition.Transport;
+using Avalonia.Threading;
 
 namespace Avalonia.Rendering.Composition.Server
 {
@@ -20,7 +21,7 @@ namespace Avalonia.Rendering.Composition.Server
     {
         private readonly IRenderLoop _renderLoop;
 
-        private readonly Queue<Batch> _batches = new Queue<Batch>();
+        private readonly Queue<CompositionBatch> _batches = new Queue<CompositionBatch>();
         private readonly Queue<Action> _receivedJobQueue = new();
         public long LastBatchId { get; private set; }
         public Stopwatch Clock { get; } = Stopwatch.StartNew();
@@ -32,6 +33,7 @@ namespace Avalonia.Rendering.Composition.Server
         internal BatchStreamMemoryPool BatchMemoryPool;
         private object _lock = new object();
         private Thread? _safeThread;
+        private bool _uiThreadIsInsideRender;
         public PlatformRenderInterfaceContextManager RenderInterface { get; }
         internal static readonly object RenderThreadDisposeStartMarker = new();
         internal static readonly object RenderThreadJobsStartMarker = new();
@@ -47,7 +49,7 @@ namespace Avalonia.Rendering.Composition.Server
             _renderLoop.Add(this);
         }
 
-        public void EnqueueBatch(Batch batch)
+        public void EnqueueBatch(CompositionBatch batch)
         {
             lock (_batches) 
                 _batches.Enqueue(batch);
@@ -55,13 +57,13 @@ namespace Avalonia.Rendering.Composition.Server
 
         internal void UpdateServerTime() => ServerNow = Clock.Elapsed;
 
-        List<Batch> _reusableToNotifyProcessedList = new();
-        List<Batch> _reusableToNotifyRenderedList = new();
+        List<CompositionBatch> _reusableToNotifyProcessedList = new();
+        List<CompositionBatch> _reusableToNotifyRenderedList = new();
         void ApplyPendingBatches()
         {
             while (true)
             {
-                Batch batch;
+                CompositionBatch batch;
                 lock (_batches)
                 {
                     if(_batches.Count == 0)
@@ -154,20 +156,46 @@ namespace Avalonia.Rendering.Composition.Server
         }
 
         public void Render()
+        {
+            if (Dispatcher.UIThread.CheckAccess())
+            {
+                if (_uiThreadIsInsideRender)
+                    throw new InvalidOperationException("Reentrancy is not supported");
+                _uiThreadIsInsideRender = true;
+                try
+                {
+                    using (Dispatcher.UIThread.DisableProcessing()) 
+                        RenderReentrancySafe();
+                }
+                finally
+                {
+                    _uiThreadIsInsideRender = false;
+                }
+            }
+            else
+                RenderReentrancySafe();
+        }
+        
+        private void RenderReentrancySafe()
         {
             lock (_lock)
             {
                 try
                 {
-                    _safeThread = Thread.CurrentThread;
-                    RenderCore();
+                    try
+                    {
+                        _safeThread = Thread.CurrentThread;
+                        RenderCore();
+                    }
+                    finally
+                    {
+                        NotifyBatchesRendered();
+                    }
                 }
                 finally
                 {
-                    NotifyBatchesRendered();
                     _safeThread = null;
                 }
-                
             }
         }
         

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/Server/SimpleServerObject.cs

@@ -17,7 +17,7 @@ class SimpleServerObject
 
     }
 
-    public void DeserializeChanges(BatchStreamReader reader, Batch batch)
+    public void DeserializeChanges(BatchStreamReader reader, CompositionBatch batch)
     {
         DeserializeChangesCore(reader, batch.CommittedAt);
         ValuesInvalidated();

+ 29 - 7
src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs

@@ -9,16 +9,16 @@ namespace Avalonia.Rendering.Composition.Transport
     /// <summary>
     /// Represents a group of serialized changes from the UI thread to be atomically applied at the render thread
     /// </summary>
-    internal class Batch
+    public class CompositionBatch
     {
         private static long _nextSequenceId = 1;
         private static ConcurrentBag<BatchStreamData> _pool = new();
         private readonly TaskCompletionSource<int> _acceptedTcs = new();
         private readonly TaskCompletionSource<int> _renderedTcs = new();
         
-        public long SequenceId { get; }
+        internal long SequenceId { get; }
         
-        public Batch()
+        internal CompositionBatch()
         {
             SequenceId = Interlocked.Increment(ref _nextSequenceId);
             if (!_pool.TryTake(out var lst))
@@ -27,12 +27,34 @@ namespace Avalonia.Rendering.Composition.Transport
         }
 
         
-        public BatchStreamData Changes { get; private set; }
-        public TimeSpan CommittedAt { get; set; }
+        internal BatchStreamData Changes { get; private set; }
+        internal TimeSpan CommittedAt { get; set; }
+        
+        /// <summary>
+        /// Indicates that batch got deserialized on the render thread and will soon be rendered.
+        /// It's generally a good time to start producing the next one
+        /// </summary>
+        /// <remarks>
+        /// To allow timing-sensitive code to receive the notification in time, the TaskCompletionSource
+        /// is configured to invoke continuations  _synchronously_, so your `await` could happen from the render loop
+        /// if it happens to run on the UI thread.
+        /// It's recommended to use Dispatcher.AwaitOnPriority when consuming from the UI thread 
+        /// </remarks>
         public Task Processed => _acceptedTcs.Task;
+        
+        /// <summary>
+        /// Indicates that batch got rendered on the render thread.
+        /// It's generally a good time to start producing the next one
+        /// </summary>
+        /// <remarks>
+        /// To allow timing-sensitive code to receive the notification in time, the TaskCompletionSource
+        /// is configured to invoke continuations  _synchronously_, so your `await` could happen from the render loop
+        /// if it happens to run on the UI thread.
+        /// It's recommended to use Dispatcher.AwaitOnPriority when consuming from the UI thread 
+        /// </remarks>
         public Task Rendered => _renderedTcs.Task;
         
-        public void NotifyProcessed()
+        internal void NotifyProcessed()
         {
             _pool.Add(Changes);
             Changes = null!;
@@ -40,6 +62,6 @@ namespace Avalonia.Rendering.Composition.Transport
             _acceptedTcs.TrySetResult(0);
         }
         
-        public void NotifyRendered() => _renderedTcs.TrySetResult(0);
+        internal void NotifyRendered() => _renderedTcs.TrySetResult(0);
     }
 }

+ 12 - 0
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@@ -619,4 +619,16 @@ public partial class Dispatcher
         _ = action ?? throw new ArgumentNullException(nameof(action));
         InvokeAsyncImpl(new SendOrPostCallbackDispatcherOperation(this, priority, action, arg, true), CancellationToken.None);
     }
+
+    /// <summary>
+    /// Returns a task awaitable that would invoke continuation on specified dispatcher priority
+    /// </summary>
+    public DispatcherPriorityAwaitable AwaitWithPriority(Task task, DispatcherPriority priority) =>
+        new(this, task, priority);
+    
+    /// <summary>
+    /// Returns a task awaitable that would invoke continuation on specified dispatcher priority
+    /// </summary>
+    public DispatcherPriorityAwaitable<T> AwaitWithPriority<T>(Task<T> task, DispatcherPriority priority) =>
+        new(this, task, priority);
 }

+ 40 - 0
src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+
+namespace Avalonia.Threading;
+
+public class DispatcherPriorityAwaitable : INotifyCompletion
+{
+    private readonly Dispatcher _dispatcher;
+    private protected readonly Task Task;
+    private readonly DispatcherPriority _priority;
+
+    internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task task, DispatcherPriority priority)
+    {
+        _dispatcher = dispatcher;
+        Task = task;
+        _priority = priority;
+    }
+    
+    public void OnCompleted(Action continuation) =>
+        Task.ContinueWith(_ => _dispatcher.Post(continuation, _priority));
+
+    public bool IsCompleted => Task.IsCompleted;
+
+    public void GetResult() => Task.GetAwaiter().GetResult();
+
+    public DispatcherPriorityAwaitable GetAwaiter() => this;
+}
+
+public class DispatcherPriorityAwaitable<T> : DispatcherPriorityAwaitable
+{
+    internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority) : base(
+        dispatcher, task, priority)
+    {
+    }
+
+    public new T GetResult() => ((Task<T>)Task).GetAwaiter().GetResult();
+
+    public new DispatcherPriorityAwaitable<T> GetAwaiter() => this;
+}