A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

733 lines
25 KiB

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
{
/// <summary>
/// A renderer which renders the state of the visual tree to an intermediate scene graph
/// representation which is then rendered on a rendering thread.
/// </summary>
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>? _scene;
private DirtyVisuals? _dirty;
private HashSet<Visual>? _recalculateChildren;
private IRef<IRenderTargetBitmapImpl>? _overlay;
private int _lastSceneId = -1;
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
private IRef<IDrawOperation>? _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;
/// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
/// </summary>
/// <param name="root">The control to render.</param>
/// <param name="renderLoop">The render loop.</param>
/// <param name="sceneBuilder">The scene builder to use. Optional.</param>
/// <param name="dispatcher">The dispatcher to use. Optional.</param>
/// <param name="rendererLock">Lock object used before trying to access render target</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
/// </summary>
/// <param name="root">The control to render.</param>
/// <param name="renderTarget">The render target.</param>
/// <param name="sceneBuilder">The scene builder to use. Optional.</param>
/// <remarks>
/// This constructor is intended to be used for unit testing.
/// </remarks>
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;
}
/// <inheritdoc/>
public bool DrawFps { get; set; }
/// <inheritdoc/>
public bool DrawDirtyRects { get; set; }
/// <summary>
/// Gets or sets a path to which rendered frame should be rendered for debugging.
/// </summary>
public string? DebugFramesPath { get; set; }
/// <summary>
/// Forces the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered
/// </summary>
public bool RenderOnlyOnRenderThread { get; set; }
/// <inheritdoc/>
public event EventHandler<SceneInvalidatedEventArgs>? SceneInvalidated;
/// <summary>
/// Gets the render layers.
/// </summary>
internal RenderLayers Layers { get; }
/// <summary>
/// Gets the current render target.
/// </summary>
internal IRenderTarget? RenderTarget { get; private set; }
/// <inheritdoc/>
public void AddDirty(Visual visual)
{
_dirty?.Add(visual);
}
/// <summary>
/// Disposes of the renderer and detaches from the render loop.
/// </summary>
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;
}
}
/// <inheritdoc/>
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool>? 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<Visual>();
}
/// <inheritdoc/>
public Visual? HitTestFirst(Point p, Visual root, Func<Visual, bool>? 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);
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
public void Resized(Size size)
{
}
/// <inheritdoc/>
public void Start()
{
lock (_startStopLock)
{
if (!_running && _renderLoop != null)
{
_renderLoop.Add(this);
_running = true;
}
}
}
/// <inheritdoc/>
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);
}
}
Scene? TryGetChildScene(IRef<IDrawOperation>? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene;
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty;
}
/// <inheritdoc/>
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>? scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(ref IDrawingContextImpl? context,
bool recursiveCall = false)
{
IRef<Scene>? 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<Visual>();
_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<IRenderTargetBitmapImpl> 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, $"frame-{id}-layer-{index++}.png");
layer.Bitmap.Item.Save(fileName);
}
}
}
}