Browse Source

Merge pull request #11552 from AvaloniaUI/feature/media-context-4

Media system refactoring
pull/11572/head
Max Katz 3 years ago
committed by GitHub
parent
commit
c6788bd33d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/Android/Avalonia.Android/AndroidPlatform.cs
  2. 3
      src/Avalonia.Base/Animation/Easings/IEasing.cs
  3. 4
      src/Avalonia.Base/Layout/LayoutManager.cs
  4. 24
      src/Avalonia.Base/Media/MediaContext.Compositor.cs
  5. 87
      src/Avalonia.Base/Media/MediaContext.cs
  6. 8
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  7. 17
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs
  8. 3
      src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs
  9. 4
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  10. 6
      src/iOS/Avalonia.iOS/Platform.cs
  11. 13
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

6
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -52,11 +52,7 @@ namespace Avalonia.Android
EglPlatformGraphics.TryInitialize(); EglPlatformGraphics.TryInitialize();
} }
Compositor = new Compositor( Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformGraphics>());
} }
} }

3
src/Avalonia.Base/Animation/Easings/IEasing.cs

@ -1,8 +1,11 @@
using Avalonia.Metadata;
namespace Avalonia.Animation.Easings namespace Avalonia.Animation.Easings
{ {
/// <summary> /// <summary>
/// Defines the interface for easing classes. /// Defines the interface for easing classes.
/// </summary> /// </summary>
[NotClientImplementable]
public interface IEasing public interface IEasing
{ {
/// <summary> /// <summary>

4
src/Avalonia.Base/Layout/LayoutManager.cs

@ -28,10 +28,12 @@ namespace Avalonia.Layout
private bool _queued; private bool _queued;
private bool _running; private bool _running;
private int _totalPassCount; private int _totalPassCount;
private Action _invokeOnRender;
public LayoutManager(ILayoutRoot owner) public LayoutManager(ILayoutRoot owner)
{ {
_owner = owner as Layoutable ?? throw new ArgumentNullException(nameof(owner)); _owner = owner as Layoutable ?? throw new ArgumentNullException(nameof(owner));
_invokeOnRender = ExecuteQueuedLayoutPass;
} }
public virtual event EventHandler? LayoutUpdated; public virtual event EventHandler? LayoutUpdated;
@ -345,7 +347,7 @@ namespace Avalonia.Layout
if (!_queued && !_running) if (!_queued && !_running)
{ {
_queued = true; _queued = true;
MediaContext.Instance.QueueLayoutPass(this); MediaContext.Instance.BeginInvokeOnRender(_invokeOnRender);
} }
} }

24
src/Avalonia.Base/Media/MediaContext.Compositor.cs

@ -40,28 +40,42 @@ partial class MediaContext
_animationsAreWaitingForComposition = false; _animationsAreWaitingForComposition = false;
// Check if we have uncommited changes // Check if we have uncommited changes
if (_scheduleCommitOnLastCompositionBatchCompletion && _pendingCompositionBatches.Count > 0) if (_scheduleCommitOnLastCompositionBatchCompletion)
{ {
CommitCompositorsWithThrottling();
_scheduleCommitOnLastCompositionBatchCompletion = false; _scheduleCommitOnLastCompositionBatchCompletion = false;
if (!CommitCompositorsWithThrottling())
ScheduleRenderForAnimationsIfNeeded();
} }
// Check if there are active animations and schedule the next render // Check if there are active animations and schedule the next render
else if(_clock.HasSubscriptions) else
ScheduleRender(false); ScheduleRenderForAnimationsIfNeeded();
} }
}
void ScheduleRenderForAnimationsIfNeeded()
{
if (_clock.HasSubscriptions)
ScheduleRender(false);
} }
/// <summary>
/// Triggers a composition commit if any batches are waiting to be sent,
/// handles throttling
/// </summary>
/// <returns>true if there are pending commits in-flight and there will be a "all-done" callback later</returns>
private bool CommitCompositorsWithThrottling() private bool CommitCompositorsWithThrottling()
{ {
// Check if we are still waiting for previous composition batches // Check if we are still waiting for previous composition batches
if (_pendingCompositionBatches.Count > 0) if (_pendingCompositionBatches.Count > 0)
{ {
_scheduleCommitOnLastCompositionBatchCompletion = true; _scheduleCommitOnLastCompositionBatchCompletion = true;
// Previous commit isn't handled yet
return true; return true;
} }
if (_requestedCommits.Count == 0) if (_requestedCommits.Count == 0)
// Nothing to do, and there are no pending commits
return false; return false;
foreach (var c in _requestedCommits) foreach (var c in _requestedCommits)

87
src/Avalonia.Base/Media/MediaContext.cs

@ -16,6 +16,7 @@ internal partial class MediaContext : ICompositorScheduler
private DispatcherOperation? _nextRenderOp; private DispatcherOperation? _nextRenderOp;
private DispatcherOperation? _inputMarkerOp; private DispatcherOperation? _inputMarkerOp;
private TimeSpan _inputMarkerAddedAt; private TimeSpan _inputMarkerAddedAt;
private bool _isRendering;
private bool _animationsAreWaitingForComposition; private bool _animationsAreWaitingForComposition;
private const double MaxSecondsWithoutInput = 1; private const double MaxSecondsWithoutInput = 1;
private readonly Action _render; private readonly Action _render;
@ -23,7 +24,17 @@ internal partial class MediaContext : ICompositorScheduler
private readonly HashSet<Compositor> _requestedCommits = new(); private readonly HashSet<Compositor> _requestedCommits = new();
private readonly Dictionary<Compositor, Batch> _pendingCompositionBatches = new(); private readonly Dictionary<Compositor, Batch> _pendingCompositionBatches = new();
private record TopLevelInfo(Compositor Compositor, CompositingRenderer Renderer, ILayoutManager LayoutManager); private record TopLevelInfo(Compositor Compositor, CompositingRenderer Renderer, ILayoutManager LayoutManager);
private readonly HashSet<LayoutManager> _queuedLayoutManagers = new();
private List<Action>? _invokeOnRenderCallbacks;
private readonly Stack<List<Action>> _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<object, TopLevelInfo> _topLevels = new(); private Dictionary<object, TopLevelInfo> _topLevels = new();
@ -32,6 +43,11 @@ internal partial class MediaContext : ICompositorScheduler
_render = Render; _render = Render;
_inputMarkerHandler = InputMarkerHandler; _inputMarkerHandler = InputMarkerHandler;
_clock = new(this); _clock = new(this);
_animationsTimer.Tick += (_, _) =>
{
_animationsTimer.Stop();
ScheduleRender(false);
};
} }
public static MediaContext Instance public static MediaContext Instance
@ -95,11 +111,13 @@ internal partial class MediaContext : ICompositorScheduler
{ {
try try
{ {
_isRendering = true;
RenderCore(); RenderCore();
} }
finally finally
{ {
_nextRenderOp = null; _nextRenderOp = null;
_isRendering = false;
} }
} }
@ -114,10 +132,7 @@ internal partial class MediaContext : ICompositorScheduler
for (var c = 0; c < 10; c++) for (var c = 0; c < 10; c++)
{ {
_clock.HasNewSubscriptions = false; _clock.HasNewSubscriptions = false;
//TODO: Integrate LayoutManager's attempt limit here FireInvokeOnRenderCallbacks();
foreach (var layout in _queuedLayoutManagers.ToArray())
layout.ExecuteQueuedLayoutPass();
_queuedLayoutManagers.Clear();
if (_clock.HasNewSubscriptions) if (_clock.HasNewSubscriptions)
{ {
@ -127,14 +142,12 @@ internal partial class MediaContext : ICompositorScheduler
break; 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) if (_requestedCommits.Count > 0 || _clock.HasSubscriptions)
{ {
_animationsAreWaitingForComposition = true; _animationsAreWaitingForComposition = CommitCompositorsWithThrottling();
CommitCompositorsWithThrottling(); if (!_animationsAreWaitingForComposition && _clock.HasSubscriptions)
_animationsTimer.Start();
} }
} }
@ -160,9 +173,55 @@ internal partial class MediaContext : ICompositorScheduler
} }
} }
public void QueueLayoutPass(LayoutManager layoutManager) /// <summary>
/// Calls all _invokeOnRenderCallbacks until no more are added
/// </summary>
private void FireInvokeOnRenderCallbacks()
{ {
_queuedLayoutManagers.Add(layoutManager); int callbackLoopCount = 0;
ScheduleRender(true); 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);
} }
} }

8
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 /// Creates a new compositor on a specified render loop that would use a particular GPU
/// </summary> /// </summary>
[PrivateApi] [PrivateApi]
public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false) public Compositor(IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false)
: this(RenderLoop.LocatorAutoInstance, gpu, useUiThreadForSynchronousCommits) : 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) : this(loop, gpu, useUiThreadForSynchronousCommits, MediaContext.Instance, false)
{ {
} }
internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu, internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu,

17
src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs

@ -79,12 +79,19 @@ internal class RenderDataDrawingContext : DrawingContext
if (!(parent.Node is T)) if (!(parent.Node is T))
throw new InvalidOperationException("Invalid Pop operation"); throw new InvalidOperationException("Invalid Pop operation");
foreach(var item in _currentItemList!) var removeLastPush = true;
parent.Node.Children.Add(item); if (_currentItemList != null)
_currentItemList.Clear(); {
s_listPool.ReturnAndSetNull(ref _currentItemList); removeLastPush = _currentItemList.Count == 0;
foreach (var item in _currentItemList)
parent.Node.Children.Add(item);
_currentItemList.Clear();
s_listPool.ReturnAndSetNull(ref _currentItemList);
}
_currentItemList = parent.Items; _currentItemList = parent.Items;
if (removeLastPush)
_currentItemList.RemoveAt(_currentItemList.Count - 1);
} }
void AddResource(object? resource) void AddResource(object? resource)

3
src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs

@ -50,7 +50,6 @@ static class ServerResourceHelperExtensions
return immutable; return immutable;
if (transform is ICompositionRenderResource<ITransform> resource) if (transform is ICompositionRenderResource<ITransform> resource)
resource.GetForCompositor(compositor); resource.GetForCompositor(compositor);
ThrowNotCompatible(transform); return new ImmutableTransform(transform.Value);
return null;
} }
} }

4
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@ -187,9 +187,7 @@ namespace Avalonia.Browser
} }
public Compositor Compositor { get; } = new( public Compositor Compositor { get; } = new(AvaloniaLocator.Current.GetRequiredService<IPlatformGraphics>());
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetRequiredService<IPlatformGraphics>());
public void Invalidate(Rect rect) public void Invalidate(Rect rect)
{ {

6
src/iOS/Avalonia.iOS/Platform.cs

@ -47,12 +47,8 @@ namespace Avalonia.iOS
.Bind<IDispatcherImpl>().ToConstant(DispatcherImpl.Instance) .Bind<IDispatcherImpl>().ToConstant(DispatcherImpl.Instance)
.Bind<IKeyboardDevice>().ToConstant(keyboard); .Bind<IKeyboardDevice>().ToConstant(keyboard);
Compositor = new Compositor( Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformGraphics>());
} }
} }
} }

13
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))); 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());
}
} }
} }

Loading…
Cancel
Save