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();
}
Compositor = new Compositor(
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformGraphics>());
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());
}
}

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

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

4
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);
}
}

24
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);
}
/// <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()
{
// 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)

87
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<Compositor> _requestedCommits = new();
private readonly Dictionary<Compositor, Batch> _pendingCompositionBatches = new();
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();
@ -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)
/// <summary>
/// Calls all _invokeOnRenderCallbacks until no more are added
/// </summary>
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);
}
}

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
/// </summary>
[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,

17
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)

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

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

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

@ -187,9 +187,7 @@ namespace Avalonia.Browser
}
public Compositor Compositor { get; } = new(
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetRequiredService<IPlatformGraphics>());
public Compositor Compositor { get; } = new(AvaloniaLocator.Current.GetRequiredService<IPlatformGraphics>());
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<IKeyboardDevice>().ToConstant(keyboard);
Compositor = new Compositor(
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformGraphics>());
Compositor = new Compositor(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)));
}
[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