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