// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.VisualTree;
using System.Collections.Generic;
using System.IO;
using Avalonia.Media.Immutable;
using System.Threading;
using System.Linq;
using Avalonia.Utilities;
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, IVisualBrushRenderer
{
private readonly IDispatcher _dispatcher;
private readonly IRenderLoop _renderLoop;
private readonly IVisual _root;
private readonly ISceneBuilder _sceneBuilder;
private bool _running;
private volatile IRef _scene;
private DirtyVisuals _dirty;
private IRef _overlay;
private bool _updateQueued;
private object _rendering = new object();
private int _lastSceneId = -1;
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
private IRef _currentDraw;
///
/// 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.
public DeferredRenderer(
IRenderRoot root,
IRenderLoop renderLoop,
ISceneBuilder sceneBuilder = null,
IDispatcher dispatcher = null)
{
Contract.Requires(root != null);
_dispatcher = dispatcher ?? Dispatcher.UIThread;
_root = root;
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers();
_renderLoop = renderLoop;
}
///
/// 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(
IVisual root,
IRenderTarget renderTarget,
ISceneBuilder sceneBuilder = null)
{
Contract.Requires(root != null);
Contract.Requires(renderTarget != null);
_root = root;
RenderTarget = renderTarget;
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers();
}
///
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; }
///
/// Gets the render layers.
///
internal RenderLayers Layers { get; }
///
/// Gets the current render target.
///
internal IRenderTarget RenderTarget { get; private set; }
///
public void AddDirty(IVisual visual)
{
_dirty?.Add(visual);
}
///
/// Disposes of the renderer and detaches from the render loop.
///
public void Dispose()
{
var scene = Interlocked.Exchange(ref _scene, null);
scene?.Dispose();
Stop();
Layers.Clear();
RenderTarget?.Dispose();
}
///
public IEnumerable HitTest(Point p, IVisual root, Func filter)
{
if (_renderLoop == null && (_dirty == null || _dirty.Count > 0))
{
// When unit testing the renderLoop may be null, so update the scene manually.
UpdateScene();
}
return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty();
}
///
public void Paint(Rect rect)
{
}
///
public void Resized(Size size)
{
}
///
public void Start()
{
if (!_running && _renderLoop != null)
{
_renderLoop.Tick += OnRenderLoopTick;
_running = true;
}
}
///
public void Stop()
{
if (_running && _renderLoop != null)
{
_renderLoop.Tick -= OnRenderLoopTick;
_running = false;
}
}
///
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
return (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
}
///
void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
var childScene = (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual];
if (childScene != null)
{
Render(context, (VisualNode)childScene.Root, null, new Rect(childScene.Size));
}
}
internal void UnitTestUpdateScene() => UpdateScene();
internal void UnitTestRender() => Render(_scene.Item);
private void Render(Scene scene)
{
bool renderOverlay = DrawDirtyRects || DrawFps;
bool composite = false;
if (RenderTarget == null)
{
RenderTarget = ((IRenderRoot)_root).CreateRenderTarget();
}
if (renderOverlay)
{
_dirtyRectsDisplay.Tick();
}
try
{
if (scene != null && scene.Size != Size.Empty)
{
IDrawingContextImpl context = null;
if (scene.Generation != _lastSceneId)
{
context = RenderTarget.CreateDrawingContext(this);
Layers.Update(scene, context);
RenderToLayers(scene);
if (DebugFramesPath != null)
{
SaveDebugFrames(scene.Generation);
}
_lastSceneId = scene.Generation;
composite = true;
}
if (renderOverlay)
{
context = context ?? RenderTarget.CreateDrawingContext(this);
RenderOverlay(scene, context);
RenderComposite(scene, context);
}
else if (composite)
{
context = context ?? RenderTarget.CreateDrawingContext(this);
RenderComposite(scene, context);
}
context?.Dispose();
}
}
catch (RenderTargetCorruptedException ex)
{
Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
RenderTarget?.Dispose();
RenderTarget = null;
}
}
private void Render(IDrawingContextImpl context, VisualNode node, IVisual 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);
foreach (var operation in node.DrawOperations)
{
_currentDraw = operation;
operation.Item.Render(context);
_currentDraw = null;
}
foreach (var child in node.Children)
{
Render(context, (VisualNode)child, layer, clipBounds);
}
node.EndRender(context, isLayerRoot);
}
}
}
private void RenderToLayers(Scene scene)
{
if (scene.Layers.HasDirty)
{
foreach (var layer in scene.Layers)
{
var renderTarget = Layers[layer.LayerRoot].Bitmap;
var node = (VisualNode)scene.FindNode(layer.LayerRoot);
if (node != null)
{
using (var context = renderTarget.Item.CreateDrawingContext(this))
{
foreach (var rect in layer.Dirty)
{
context.Transform = Matrix.Identity;
context.PushClip(rect);
context.Clear(Colors.Transparent);
Render(context, node, layer.LayerRoot, rect);
context.PopClip();
if (DrawDirtyRects)
{
_dirtyRectsDisplay.Add(rect);
}
}
}
}
}
}
}
private void RenderOverlay(Scene scene, IDrawingContextImpl 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.FillRectangle(brush, r.Rect);
}
}
private void RenderComposite(Scene scene, IDrawingContextImpl context)
{
var clientRect = new Rect(scene.Size);
foreach (var layer in scene.Layers)
{
var bitmap = Layers[layer.LayerRoot].Bitmap;
var sourceRect = new Rect(0, 0, bitmap.Item.PixelWidth, bitmap.Item.PixelHeight);
if (layer.GeometryClip != null)
{
context.PushGeometryClip(layer.GeometryClip);
}
if (layer.OpacityMask == null)
{
context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect);
}
else
{
context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect);
}
if (layer.GeometryClip != null)
{
context.PopGeometryClip();
}
}
if (_overlay != null)
{
var sourceRect = new Rect(0, 0, _overlay.Item.PixelWidth, _overlay.Item.PixelHeight);
context.DrawImage(_overlay, 0.5, sourceRect, clientRect);
}
if (DrawFps)
{
RenderFps(context, clientRect, scene.Layers.Count);
}
}
private void UpdateScene()
{
Dispatcher.UIThread.VerifyAccess();
try
{
if (_root.IsVisible)
{
var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root));
var scene = sceneRef.Item;
if (_dirty == null)
{
_dirty = new DirtyVisuals();
_sceneBuilder.UpdateAll(scene);
}
else if (_dirty.Count > 0)
{
foreach (var visual in _dirty)
{
_sceneBuilder.Update(scene, visual);
}
}
var oldScene = Interlocked.Exchange(ref _scene, sceneRef);
oldScene?.Dispose();
_dirty.Clear();
(_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
}
else
{
var oldScene = Interlocked.Exchange(ref _scene, null);
oldScene?.Dispose();
}
}
finally
{
_updateQueued = false;
}
}
private void OnRenderLoopTick(object sender, EventArgs e)
{
if (Monitor.TryEnter(_rendering))
{
try
{
if (!_updateQueued && (_dirty == null || _dirty.Count > 0))
{
_updateQueued = true;
_dispatcher.Post(UpdateScene, DispatcherPriority.Render);
}
using (var scene = _scene?.Clone())
{
Render(scene?.Item);
}
}
catch { }
finally
{
Monitor.Exit(_rendering);
}
}
}
private IRef GetOverlay(
IDrawingContextImpl parentContext,
Size size,
double scaling)
{
var pixelSize = size * scaling;
if (_overlay == null ||
_overlay.Item.PixelWidth != pixelSize.Width ||
_overlay.Item.PixelHeight != 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, $"frame-{id}-layer-{index++}.png");
layer.Bitmap.Item.Save(fileName);
}
}
}
}