using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Rendering { /// /// A renderer which renders the state of the visual tree to an intermediate scene graph /// representation which is then rendered on a rendering thread. /// public class DeferredRenderer : RendererBase, IRenderer, IRenderLoopTask, IVisualBrushRenderer { private readonly IDispatcher? _dispatcher; private readonly IRenderLoop? _renderLoop; private readonly Visual _root; private readonly ISceneBuilder _sceneBuilder; private bool _running; private bool _disposed; private volatile IRef? _scene; private DirtyVisuals? _dirty; private HashSet? _recalculateChildren; private IRef? _overlay; private int _lastSceneId = -1; private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); private IRef? _currentDraw; private readonly IDeferredRendererLock _lock; private readonly object _sceneLock = new object(); private readonly object _startStopLock = new object(); private readonly object _renderLoopIsRenderingLock = new object(); private readonly Action _updateSceneIfNeededDelegate; /// /// Initializes a new instance of the class. /// /// The control to render. /// The render loop. /// The scene builder to use. Optional. /// The dispatcher to use. Optional. /// Lock object used before trying to access render target public DeferredRenderer( IRenderRoot root, IRenderLoop renderLoop, ISceneBuilder? sceneBuilder = null, IDispatcher? dispatcher = null, IDeferredRendererLock? rendererLock = null) : base(true) { _dispatcher = dispatcher ?? Dispatcher.UIThread; _root = root as Visual ?? throw new ArgumentNullException(nameof(root)); _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); _renderLoop = renderLoop; _lock = rendererLock ?? new ManagedDeferredRendererLock(); _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; } /// /// Initializes a new instance of the class. /// /// The control to render. /// The render target. /// The scene builder to use. Optional. /// /// This constructor is intended to be used for unit testing. /// public DeferredRenderer( Visual root, IRenderTarget renderTarget, ISceneBuilder? sceneBuilder = null) : base(true) { _root = root ?? throw new ArgumentNullException(nameof(root)); RenderTarget = renderTarget ?? throw new ArgumentNullException(nameof(renderTarget)); _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); _lock = new ManagedDeferredRendererLock(); _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; } /// public bool DrawFps { get; set; } /// public bool DrawDirtyRects { get; set; } /// /// Gets or sets a path to which rendered frame should be rendered for debugging. /// public string? DebugFramesPath { get; set; } /// /// Forces the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered /// public bool RenderOnlyOnRenderThread { get; set; } /// public event EventHandler? SceneInvalidated; /// /// Gets the render layers. /// internal RenderLayers Layers { get; } /// /// Gets the current render target. /// internal IRenderTarget? RenderTarget { get; private set; } /// public void AddDirty(Visual visual) { _dirty?.Add(visual); } /// /// Disposes of the renderer and detaches from the render loop. /// public void Dispose() { lock (_sceneLock) { if (_disposed) return; _disposed = true; var scene = _scene; _scene = null; scene?.Dispose(); } Stop(); // Wait for any in-progress rendering to complete lock(_renderLoopIsRenderingLock){} DisposeRenderTarget(); } public void RecalculateChildren(Visual visual) => _recalculateChildren?.Add(visual); void DisposeRenderTarget() { using (var l = _lock.TryLock()) { if(l == null) { // We are still trying to render on the render thread, try again a bit later DispatcherTimer.RunOnce(DisposeRenderTarget, TimeSpan.FromMilliseconds(50), DispatcherPriority.Background); return; } Layers.Clear(); RenderTarget?.Dispose(); RenderTarget = null; } } /// public IEnumerable HitTest(Point p, Visual root, Func? filter) { EnsureCanHitTest(); //It's safe to access _scene here without a lock since //it's only changed from UI thread which we are currently on return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty(); } /// public Visual? HitTestFirst(Point p, Visual root, Func? filter) { EnsureCanHitTest(); //It's safe to access _scene here without a lock since //it's only changed from UI thread which we are currently on return _scene?.Item.HitTestFirst(p, root, filter); } /// public void Paint(Rect rect) { if (RenderOnlyOnRenderThread) { // Renderer is stopped and doesn't tick on the render thread // This indicates a bug somewhere in our code // (currently happens when a window gets minimized with Show desktop on Windows 10) if(!_running) return; while (true) { Scene? scene; bool? updated; lock (_sceneLock) { updated = UpdateScene(); scene = _scene?.Item; } // Renderer is in invalid state, skip drawing if(updated == null) return; // Wait for the scene to be rendered or disposed scene?.Rendered.Wait(); // That was an up-to-date scene, we can return immediately if (updated == true) return; } } else { var t = (IRenderLoopTask)this; if (t.NeedsUpdate) UpdateScene(); if (_scene?.Item != null) Render(true); } } /// public void Resized(Size size) { } /// public void Start() { lock (_startStopLock) { if (!_running && _renderLoop != null) { _renderLoop.Add(this); _running = true; } } } /// public void Stop() { lock (_startStopLock) { if (_running && _renderLoop != null) { _renderLoop.Remove(this); _running = false; } } } bool NeedsUpdate => _dirty == null || _dirty.Count > 0; bool IRenderLoopTask.NeedsUpdate => NeedsUpdate; void IRenderLoopTask.Update(TimeSpan time) => UpdateScene(); void IRenderLoopTask.Render() { lock (_renderLoopIsRenderingLock) { lock(_startStopLock) if(!_running) return; Render(false); } } static Scene? TryGetChildScene(IRef? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene; /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { var childScene = TryGetChildScene(_currentDraw); if (childScene != null) { Render(context, (VisualNode)childScene.Root, null, new Rect(childScene.Size)); } } internal void UnitTestUpdateScene() => UpdateScene(); internal void UnitTestRender() => Render(false); internal Scene? UnitTestScene() => _scene?.Item; private void EnsureCanHitTest() { if (_renderLoop == null && (_dirty == null || _dirty.Count > 0)) { // When unit testing the renderLoop may be null, so update the scene manually. UpdateScene(); } } internal void Render(bool forceComposite) { using (var l = _lock.TryLock()) { if (l == null) return; IDrawingContextImpl? context = null; try { try { var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context); if (updated) FpsTick(); using (scene) { if (scene?.Item != null) { try { var overlay = DrawDirtyRects || DrawFps; if (DrawDirtyRects) _dirtyRectsDisplay.Tick(); if (overlay) RenderOverlay(scene.Item, ref context); if (updated || forceComposite || overlay) RenderComposite(scene.Item, ref context); } finally { scene.Item.MarkAsRendered(); } } } } finally { context?.Dispose(); } } catch (RenderTargetCorruptedException ex) { Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex); RenderTarget?.Dispose(); RenderTarget = null; } } } private (IRef? scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(ref IDrawingContextImpl? context, bool recursiveCall = false) { IRef? sceneRef; lock (_sceneLock) sceneRef = _scene?.Clone(); if (sceneRef == null) return (null, false); using (sceneRef) { var scene = sceneRef.Item; if (scene.Generation != _lastSceneId) { EnsureDrawingContext(ref context); Layers.Update(scene, context); RenderToLayers(scene); if (DebugFramesPath != null) { SaveDebugFrames(scene.Generation); } lock (_sceneLock) _lastSceneId = scene.Generation; var isUiThread = Dispatcher.UIThread.CheckAccess(); // We have consumed the previously available scene, but there might be some dirty // rects since the last update. *If* we are on UI thread, we can force immediate scene // rebuild before rendering anything on-screen // We are calling the same method recursively here if (!recursiveCall && isUiThread && NeedsUpdate) { UpdateScene(); var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true); return (rs, true); } // We are rendering a new scene version, so it's highly likely // that there is already a pending update for animations // So we are scheduling an update call so UI thread could prepare a scene before // the next render timer tick if (!recursiveCall && !isUiThread) Dispatcher.UIThread.Post(_updateSceneIfNeededDelegate, DispatcherPriority.Render); // Indicate that we have updated the layers return (sceneRef.Clone(), true); } // Just return scene, layers weren't updated return (sceneRef.Clone(), false); } } private void Render(IDrawingContextImpl context, VisualNode node, Visual? layer, Rect clipBounds) { if (layer == null || node.LayerRoot == layer) { clipBounds = node.ClipBounds.Intersect(clipBounds); if (!clipBounds.IsEmpty && node.Opacity > 0) { var isLayerRoot = node.Visual == layer; node.BeginRender(context, isLayerRoot); var drawOperations = node.DrawOperations; var drawOperationsCount = drawOperations.Count; for (int i = 0; i < drawOperationsCount; i++) { var operation = drawOperations[i]; _currentDraw = operation; operation.Item.Render(context); _currentDraw = null; } var children = node.Children; var childrenCount = children.Count; for (int i = 0; i < childrenCount; i++) { var child = children[i]; Render(context, (VisualNode)child, layer, clipBounds); } node.EndRender(context, isLayerRoot); } } } private void RenderToLayers(Scene scene) { foreach (var layer in scene.Layers) { var renderLayer = Layers[layer.LayerRoot]; if (layer.Dirty.IsEmpty && !renderLayer.IsEmpty) continue; var renderTarget = renderLayer.Bitmap; var node = (VisualNode?)scene.FindNode(layer.LayerRoot); if (node != null) { using (var context = renderTarget.Item.CreateDrawingContext(this)) { if (renderLayer.IsEmpty) { // Render entire layer root node context.Clear(Colors.Transparent); context.Transform = Matrix.Identity; context.PushClip(node.ClipBounds); Render(context, node, layer.LayerRoot, node.ClipBounds); context.PopClip(); if (DrawDirtyRects) { _dirtyRectsDisplay.Add(node.ClipBounds); } renderLayer.IsEmpty = false; } else { var scale = scene.Scaling; foreach (var rect in layer.Dirty) { var snappedRect = SnapToDevicePixels(rect, scale); context.Transform = Matrix.Identity; context.PushClip(snappedRect); context.Clear(Colors.Transparent); Render(context, node, layer.LayerRoot, snappedRect); context.PopClip(); if (DrawDirtyRects) { _dirtyRectsDisplay.Add(snappedRect); } } } } } } } private static Rect SnapToDevicePixels(Rect rect, double scale) { return new Rect( new Point( Math.Floor(rect.X * scale) / scale, Math.Floor(rect.Y * scale) / scale), new Point( Math.Ceiling(rect.Right * scale) / scale, Math.Ceiling(rect.Bottom * scale) / scale)); } private void RenderOverlay(Scene scene, ref IDrawingContextImpl? parentContent) { EnsureDrawingContext(ref parentContent); if (DrawDirtyRects) { var overlay = GetOverlay(parentContent, scene.Size, scene.Scaling); using (var context = overlay.Item.CreateDrawingContext(this)) { context.Clear(Colors.Transparent); RenderDirtyRects(context); } } else { _overlay?.Dispose(); _overlay = null; } } private void RenderDirtyRects(IDrawingContextImpl context) { foreach (var r in _dirtyRectsDisplay) { var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity); context.DrawRectangle(brush,null, r.Rect); } } private void RenderComposite(Scene scene, ref IDrawingContextImpl? context) { EnsureDrawingContext(ref context); context.Clear(Colors.Transparent); var clientRect = new Rect(scene.Size); var firstLayer = true; foreach (var layer in scene.Layers) { var bitmap = Layers[layer.LayerRoot].Bitmap; var sourceRect = new Rect(0, 0, bitmap.Item.PixelSize.Width, bitmap.Item.PixelSize.Height); if (layer.GeometryClip != null) { context.PushGeometryClip(layer.GeometryClip); } if (layer.OpacityMask == null) { if (firstLayer && bitmap.Item.CanBlit) bitmap.Item.Blit(context); else context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); } else { context.DrawBitmap(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); } if (layer.GeometryClip != null) { context.PopGeometryClip(); } firstLayer = false; } if (_overlay != null) { var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height); context.DrawBitmap(_overlay, 0.5, sourceRect, clientRect); } if (DrawFps) { using (var c = new DrawingContext(context, false)) { RenderFps(c, clientRect, scene.Layers.Count); } } } private void EnsureDrawingContext([NotNull] ref IDrawingContextImpl? context) { if (context != null) { return; } if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) { RenderTarget!.Dispose(); RenderTarget = null; } if (RenderTarget == null) { RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); } context = RenderTarget.CreateDrawingContext(this); } private void UpdateSceneIfNeeded() { if(NeedsUpdate) UpdateScene(); } private bool? UpdateScene() { Dispatcher.UIThread.VerifyAccess(); using var noPump = NonPumpingLockHelper.Use(); lock (_sceneLock) { if (_disposed) return null; if (_scene?.Item.Generation > _lastSceneId) return false; } if (_root.IsVisible) { var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); var scene = sceneRef.Item; if (_dirty == null) { _dirty = new DirtyVisuals(); _recalculateChildren = new HashSet(); _sceneBuilder.UpdateAll(scene); } else { foreach (var visual in _recalculateChildren!) { var node = scene.FindNode(visual); ((VisualNode?)node)?.SortChildren(scene); } _recalculateChildren.Clear(); foreach (var visual in _dirty) { _sceneBuilder.Update(scene, visual); } } lock (_sceneLock) { var oldScene = _scene; _scene = sceneRef; oldScene?.Dispose(); } _dirty.Clear(); if (SceneInvalidated != null) { var rect = new Rect(); foreach (var layer in scene.Layers) { foreach (var dirty in layer.Dirty) { rect = rect.Union(dirty); } } SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); } return true; } else { lock (_sceneLock) { var oldScene = _scene; _scene = null; oldScene?.Dispose(); } return null; } } private IRef GetOverlay( IDrawingContextImpl parentContext, Size size, double scaling) { var pixelSize = size * scaling; if (_overlay == null || _overlay.Item.PixelSize.Width != pixelSize.Width || _overlay.Item.PixelSize.Height != pixelSize.Height) { _overlay?.Dispose(); _overlay = RefCountable.Create(parentContext.CreateLayer(size)); } return _overlay; } private void SaveDebugFrames(int id) { var index = 0; foreach (var layer in Layers) { var fileName = Path.Combine(DebugFramesPath ?? string.Empty, FormattableString.Invariant($"frame-{id}-layer-{index++}.png")); layer.Bitmap.Item.Save(fileName); } } } }