MediaContext.cs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Threading;
  4. using Avalonia.Layout;
  5. using Avalonia.Rendering;
  6. using Avalonia.Rendering.Composition;
  7. using Avalonia.Rendering.Composition.Transport;
  8. using Avalonia.Threading;
  9. namespace Avalonia.Media;
  10. internal partial class MediaContext : ICompositorScheduler
  11. {
  12. private DispatcherOperation? _nextRenderOp;
  13. private DispatcherOperation? _inputMarkerOp;
  14. private TimeSpan _inputMarkerAddedAt;
  15. private bool _isRendering;
  16. private bool _animationsAreWaitingForComposition;
  17. private readonly double MaxSecondsWithoutInput;
  18. private readonly Action _render;
  19. private readonly Action _inputMarkerHandler;
  20. private readonly HashSet<Compositor> _requestedCommits = new();
  21. private readonly Dictionary<Compositor, CompositionBatch> _pendingCompositionBatches = new();
  22. private readonly Dispatcher _dispatcher;
  23. private record TopLevelInfo(Compositor Compositor, CompositingRenderer Renderer, ILayoutManager LayoutManager);
  24. private List<Action>? _invokeOnRenderCallbacks;
  25. private readonly Stack<List<Action>> _invokeOnRenderCallbackListPool = new();
  26. private readonly DispatcherTimer _animationsTimer = new(DispatcherPriority.Render)
  27. {
  28. // Since this timer is used to drive animations that didn't contribute to the previous frame at all
  29. // We can safely use 16ms interval until we fix our animation system to actually report the next expected
  30. // frame
  31. Interval = TimeSpan.FromMilliseconds(16)
  32. };
  33. private readonly Dictionary<object, TopLevelInfo> _topLevels = new();
  34. private MediaContext(Dispatcher dispatcher, TimeSpan inputStarvationTimeout)
  35. {
  36. _render = Render;
  37. _inputMarkerHandler = InputMarkerHandler;
  38. _clock = new(this);
  39. _dispatcher = dispatcher;
  40. MaxSecondsWithoutInput = inputStarvationTimeout.TotalSeconds;
  41. _animationsTimer.Tick += (_, _) =>
  42. {
  43. _animationsTimer.Stop();
  44. ScheduleRender(false);
  45. };
  46. }
  47. public static MediaContext Instance
  48. {
  49. get
  50. {
  51. // Technically it's supposed to be a thread-static singleton, but we don't have multiple threads
  52. // and need to do a full reset for unit tests
  53. var context = AvaloniaLocator.Current.GetService<MediaContext>();
  54. if (context == null)
  55. {
  56. var opts = AvaloniaLocator.Current.GetService<DispatcherOptions>() ?? new();
  57. context = new MediaContext(Dispatcher.UIThread, opts.InputStarvationTimeout);
  58. AvaloniaLocator.CurrentMutable.Bind<MediaContext>().ToConstant(context);
  59. }
  60. return context;
  61. }
  62. }
  63. /// <summary>
  64. /// Schedules the next render operation, handles render throttling for input processing
  65. /// </summary>
  66. private void ScheduleRender(bool now)
  67. {
  68. // Already scheduled, nothing to do
  69. if (_nextRenderOp != null)
  70. {
  71. if (now)
  72. _nextRenderOp.Priority = DispatcherPriority.Render;
  73. return;
  74. }
  75. // Sometimes our animation, layout and render passes might be taking more than a frame to complete
  76. // which can cause a "freeze"-like state when UI is being updated, but input is never being processed
  77. // So here we inject an operation with Input priority to check if Input wasn't being processed
  78. // for a long time. If that's the case the next rendering operation will be scheduled to happen after all pending input
  79. var priority = DispatcherPriority.Render;
  80. if (_inputMarkerOp == null)
  81. {
  82. _inputMarkerOp = _dispatcher.InvokeAsync(_inputMarkerHandler, DispatcherPriority.Input);
  83. _inputMarkerAddedAt = _time.Elapsed;
  84. }
  85. else if (!now && (_time.Elapsed - _inputMarkerAddedAt).TotalSeconds > MaxSecondsWithoutInput)
  86. {
  87. priority = DispatcherPriority.Input;
  88. }
  89. var renderOp = new DispatcherOperation(_dispatcher, priority, _render, throwOnUiThread: true);
  90. _nextRenderOp = renderOp;
  91. _dispatcher.InvokeAsyncImpl(renderOp, CancellationToken.None);
  92. }
  93. /// <summary>
  94. /// This handles the _inputMarkerOp message. We're using
  95. /// _inputMarkerOp to determine if input priority dispatcher ops
  96. /// have been processes.
  97. /// </summary>
  98. private void InputMarkerHandler()
  99. {
  100. //set the marker to null so we know that input priority has been processed
  101. _inputMarkerOp = null;
  102. }
  103. private void Render()
  104. {
  105. try
  106. {
  107. _isRendering = true;
  108. RenderCore();
  109. }
  110. finally
  111. {
  112. _nextRenderOp = null;
  113. _isRendering = false;
  114. }
  115. }
  116. private void RenderCore()
  117. {
  118. var now = _time.Elapsed;
  119. if (!_animationsAreWaitingForComposition)
  120. _clock.Pulse(now);
  121. // Since new animations could be started during the layout and can affect layout/render
  122. // We are doing several iterations when it happens
  123. for (var c = 0; c < 10; c++)
  124. {
  125. FireInvokeOnRenderCallbacks();
  126. if (_clock.HasNewSubscriptions)
  127. {
  128. _clock.PulseNewSubscriptions();
  129. continue;
  130. }
  131. break;
  132. }
  133. if (_requestedCommits.Count > 0 || _clock.HasSubscriptions)
  134. {
  135. _animationsAreWaitingForComposition = CommitCompositorsWithThrottling();
  136. if (!_animationsAreWaitingForComposition && _clock.HasSubscriptions)
  137. _animationsTimer.Start();
  138. }
  139. }
  140. // Used for unit tests
  141. public bool IsTopLevelActive(object key) => _topLevels.ContainsKey(key);
  142. public void AddTopLevel(object key, ILayoutManager layoutManager, IRenderer renderer)
  143. {
  144. if(_topLevels.ContainsKey(key))
  145. return;
  146. var render = (CompositingRenderer)renderer;
  147. _topLevels.Add(key, new TopLevelInfo(render.Compositor, render, layoutManager));
  148. render.Start();
  149. ScheduleRender(true);
  150. }
  151. public void RemoveTopLevel(object key)
  152. {
  153. if (_topLevels.TryGetValue(key, out var info))
  154. {
  155. _topLevels.Remove(key);
  156. info.Renderer.Stop();
  157. }
  158. }
  159. /// <summary>
  160. /// Calls all _invokeOnRenderCallbacks until no more are added
  161. /// </summary>
  162. private void FireInvokeOnRenderCallbacks()
  163. {
  164. int callbackLoopCount = 0;
  165. int count = _invokeOnRenderCallbacks?.Count ?? 0;
  166. // This outer loop is to re-run layout in case the app causes a layout to get enqueued in response
  167. // to a Loaded event. In this case we would like to re-run layout before we allow render.
  168. do
  169. {
  170. while (count > 0)
  171. {
  172. callbackLoopCount++;
  173. if (callbackLoopCount > 153)
  174. throw new InvalidOperationException("Infinite layout loop detected");
  175. var callbacks = _invokeOnRenderCallbacks!;
  176. _invokeOnRenderCallbacks = null;
  177. for (int i = 0; i < count; i++)
  178. callbacks[i].Invoke();
  179. callbacks.Clear();
  180. _invokeOnRenderCallbackListPool.Push(callbacks);
  181. count = _invokeOnRenderCallbacks?.Count ?? 0;
  182. }
  183. // TODO: port the rest of the Loaded logic later
  184. // Fire all the pending Loaded events before Render happens
  185. // but after the layout storm has subsided
  186. // FireLoadedPendingCallbacks();
  187. count = _invokeOnRenderCallbacks?.Count ?? 0;
  188. }
  189. while (count > 0);
  190. }
  191. /// <summary>
  192. /// Executes the <paramref name="callback">callback</paramref> in the next iteration of the current UI-thread
  193. /// render loop / layout pass that.
  194. /// </summary>
  195. /// <param name="callback"></param>
  196. public void BeginInvokeOnRender(Action callback)
  197. {
  198. if (_invokeOnRenderCallbacks == null)
  199. _invokeOnRenderCallbacks =
  200. _invokeOnRenderCallbackListPool.Count > 0 ? _invokeOnRenderCallbackListPool.Pop() : new();
  201. _invokeOnRenderCallbacks.Add(callback);
  202. if (!_isRendering)
  203. ScheduleRender(true);
  204. }
  205. }