committed by
GitHub
208 changed files with 2665 additions and 8607 deletions
@ -1,5 +1,6 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup Condition="'$(TargetFramework)' != 'net6'"> |
|||
<!-- '!NET6_0_OR_GREATER' equivalent --> |
|||
<ItemGroup Condition="!('$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '6.0')))"> |
|||
<PackageReference Include="System.Memory" Version="4.5.3" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -0,0 +1,51 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using System.Threading; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Media.Imaging; |
|||
|
|||
internal class BitmapMemory : IDisposable |
|||
{ |
|||
private readonly int _memorySize; |
|||
|
|||
public BitmapMemory(PixelFormat format, PixelSize size) |
|||
{ |
|||
Format = format; |
|||
Size = size; |
|||
RowBytes = (size.Width * format.BitsPerPixel + 7) / 8; |
|||
_memorySize = RowBytes * size.Height; |
|||
Address = Marshal.AllocHGlobal(_memorySize); |
|||
GC.AddMemoryPressure(_memorySize); |
|||
} |
|||
|
|||
private void ReleaseUnmanagedResources() |
|||
{ |
|||
if (Address != IntPtr.Zero) |
|||
{ |
|||
GC.RemoveMemoryPressure(_memorySize); |
|||
Marshal.FreeHGlobal(Address); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
ReleaseUnmanagedResources(); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
~BitmapMemory() |
|||
{ |
|||
ReleaseUnmanagedResources(); |
|||
} |
|||
|
|||
public IntPtr Address { get; private set; } |
|||
public PixelSize Size { get; } |
|||
public int RowBytes { get; } |
|||
public PixelFormat Format { get; } |
|||
|
|||
|
|||
|
|||
public void CopyToRgba(IntPtr buffer, int rowBytes) => |
|||
PixelFormatReader.Transcode(buffer, Address, Size, RowBytes, rowBytes, Format); |
|||
} |
|||
@ -0,0 +1,280 @@ |
|||
using System; |
|||
using Avalonia.Platform; |
|||
namespace Avalonia.Media.Imaging; |
|||
|
|||
internal struct Rgba8888Pixel |
|||
{ |
|||
public byte R; |
|||
public byte G; |
|||
public byte B; |
|||
public byte A; |
|||
} |
|||
|
|||
static unsafe class PixelFormatReader |
|||
{ |
|||
public interface IPixelFormatReader |
|||
{ |
|||
Rgba8888Pixel ReadNext(); |
|||
void Reset(IntPtr address); |
|||
} |
|||
|
|||
private static readonly Rgba8888Pixel s_white = new Rgba8888Pixel |
|||
{ |
|||
A = 255, |
|||
B = 255, |
|||
G = 255, |
|||
R = 255 |
|||
}; |
|||
|
|||
private static readonly Rgba8888Pixel s_black = new Rgba8888Pixel |
|||
{ |
|||
A = 255, |
|||
B = 0, |
|||
G = 0, |
|||
R = 0 |
|||
}; |
|||
|
|||
public unsafe struct BlackWhitePixelReader : IPixelFormatReader |
|||
{ |
|||
private int _bit; |
|||
private byte* _address; |
|||
|
|||
public void Reset(IntPtr address) |
|||
{ |
|||
_address = (byte*)address; |
|||
_bit = 0; |
|||
} |
|||
|
|||
public Rgba8888Pixel ReadNext() |
|||
{ |
|||
var shift = 7 - _bit; |
|||
var value = (*_address >> shift) & 1; |
|||
_bit++; |
|||
if (_bit == 8) |
|||
{ |
|||
_address++; |
|||
_bit = 0; |
|||
} |
|||
return value == 1 ? s_white : s_black; |
|||
} |
|||
} |
|||
|
|||
public unsafe struct Gray2PixelReader : IPixelFormatReader |
|||
{ |
|||
private int _bit; |
|||
private byte* _address; |
|||
|
|||
public void Reset(IntPtr address) |
|||
{ |
|||
_address = (byte*)address; |
|||
_bit = 0; |
|||
} |
|||
|
|||
private static Rgba8888Pixel[] Palette = new[] |
|||
{ |
|||
s_black, |
|||
new Rgba8888Pixel |
|||
{ |
|||
A = 255, B = 0x55, G = 0x55, R = 0x55 |
|||
}, |
|||
new Rgba8888Pixel |
|||
{ |
|||
A = 255, B = 0xAA, G = 0xAA, R = 0xAA |
|||
}, |
|||
s_white |
|||
}; |
|||
|
|||
public Rgba8888Pixel ReadNext() |
|||
{ |
|||
var shift = 6 - _bit; |
|||
var value = (byte)((*_address >> shift)); |
|||
value = (byte)((value & 3)); |
|||
_bit += 2; |
|||
if (_bit == 8) |
|||
{ |
|||
_address++; |
|||
_bit = 0; |
|||
} |
|||
|
|||
return Palette[value]; |
|||
} |
|||
} |
|||
|
|||
public unsafe struct Gray4PixelReader : IPixelFormatReader |
|||
{ |
|||
private int _bit; |
|||
private byte* _address; |
|||
|
|||
public void Reset(IntPtr address) |
|||
{ |
|||
_address = (byte*)address; |
|||
_bit = 0; |
|||
} |
|||
|
|||
public Rgba8888Pixel ReadNext() |
|||
{ |
|||
var shift = 4 - _bit; |
|||
var value = (byte)((*_address >> shift)); |
|||
value = (byte)((value & 0xF)); |
|||
value = (byte)(value | (value << 4)); |
|||
_bit += 4; |
|||
if (_bit == 8) |
|||
{ |
|||
_address++; |
|||
_bit = 0; |
|||
} |
|||
|
|||
return new Rgba8888Pixel |
|||
{ |
|||
A = 255, |
|||
B = value, |
|||
G = value, |
|||
R = value |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public unsafe struct Gray8PixelReader : IPixelFormatReader |
|||
{ |
|||
private byte* _address; |
|||
public void Reset(IntPtr address) |
|||
{ |
|||
_address = (byte*)address; |
|||
} |
|||
|
|||
public Rgba8888Pixel ReadNext() |
|||
{ |
|||
var value = *_address; |
|||
_address++; |
|||
|
|||
return new Rgba8888Pixel |
|||
{ |
|||
A = 255, |
|||
B = value, |
|||
G = value, |
|||
R = value |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public unsafe struct Gray16PixelReader : IPixelFormatReader |
|||
{ |
|||
private ushort* _address; |
|||
public Rgba8888Pixel ReadNext() |
|||
{ |
|||
var value16 = *_address; |
|||
_address++; |
|||
var value8 = (byte)(value16 >> 8); |
|||
return new Rgba8888Pixel |
|||
{ |
|||
A = 255, |
|||
B = value8, |
|||
G = value8, |
|||
R = value8 |
|||
}; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (ushort*)address; |
|||
} |
|||
|
|||
public unsafe struct Gray32FloatPixelReader : IPixelFormatReader |
|||
{ |
|||
private byte* _address; |
|||
public Rgba8888Pixel ReadNext() |
|||
{ |
|||
var f = *(float*)_address; |
|||
var srgb = Math.Pow(f, 1 / 2.2); |
|||
var value = (byte)(srgb * 255); |
|||
|
|||
_address += 4; |
|||
return new Rgba8888Pixel |
|||
{ |
|||
A = 255, |
|||
B = value, |
|||
G = value, |
|||
R = value |
|||
}; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (byte*)address; |
|||
} |
|||
|
|||
struct Rgba64 |
|||
{ |
|||
#pragma warning disable CS0649
|
|||
public ushort R; |
|||
public ushort G; |
|||
public ushort B; |
|||
public ushort A; |
|||
#pragma warning restore CS0649
|
|||
} |
|||
|
|||
public unsafe struct Rgba64PixelFormatReader : IPixelFormatReader |
|||
{ |
|||
private Rgba64* _address; |
|||
public Rgba8888Pixel ReadNext() |
|||
{ |
|||
var value = *_address; |
|||
|
|||
_address++; |
|||
return new Rgba8888Pixel |
|||
{ |
|||
A = (byte)(value.A >> 8), |
|||
B = (byte)(value.B >> 8), |
|||
G = (byte)(value.G >> 8), |
|||
R = (byte)(value.R >> 8), |
|||
}; |
|||
} |
|||
|
|||
public void Reset(IntPtr address) => _address = (Rgba64*)address; |
|||
} |
|||
|
|||
public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst, |
|||
PixelFormat format) |
|||
{ |
|||
if (format == PixelFormats.BlackWhite) |
|||
Transcode<BlackWhitePixelReader>(dst, src, size, strideSrc, strideDst); |
|||
else if (format == PixelFormats.Gray2) |
|||
Transcode<Gray2PixelReader>(dst, src, size, strideSrc, strideDst); |
|||
else if (format == PixelFormats.Gray4) |
|||
Transcode<Gray4PixelReader>(dst, src, size, strideSrc, strideDst); |
|||
else if (format == PixelFormats.Gray8) |
|||
Transcode<Gray8PixelReader>(dst, src, size, strideSrc, strideDst); |
|||
else if (format == PixelFormats.Gray16) |
|||
Transcode<Gray16PixelReader>(dst, src, size, strideSrc, strideDst); |
|||
else if (format == PixelFormats.Gray32Float) |
|||
Transcode<Gray32FloatPixelReader>(dst, src, size, strideSrc, strideDst); |
|||
else if (format == PixelFormats.Rgba64) |
|||
Transcode<Rgba64PixelFormatReader>(dst, src, size, strideSrc, strideDst); |
|||
else |
|||
throw new NotSupportedException($"Pixel format {format} is not supported"); |
|||
} |
|||
|
|||
public static bool SupportsFormat(PixelFormat format) |
|||
{ |
|||
return format == PixelFormats.BlackWhite |
|||
|| format == PixelFormats.Gray2 |
|||
|| format == PixelFormats.Gray4 |
|||
|| format == PixelFormats.Gray8 |
|||
|| format == PixelFormats.Gray16 |
|||
|| format == PixelFormats.Gray32Float |
|||
|| format == PixelFormats.Rgba64; |
|||
} |
|||
|
|||
public static void Transcode<TReader>(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader |
|||
{ |
|||
var w = size.Width; |
|||
var h = size.Height; |
|||
TReader reader = default; |
|||
for (var y = 0; y < h; y++) |
|||
{ |
|||
reader.Reset(src + strideSrc * y); |
|||
var dstRow = (Rgba8888Pixel*)(dst + strideDst * y); |
|||
for (var x = 0; x < w; x++) |
|||
{ |
|||
*dstRow = reader.ReadNext(); |
|||
dstRow++; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Metadata; |
|||
|
|||
/// <summary>
|
|||
/// Instructs the compiler to resolve the compiled bindings data type for the item-specific properties of collection-like controls.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property,
|
|||
/// allowing the template to inherit the data type from the Items collection binding.
|
|||
/// </remarks>
|
|||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] |
|||
public sealed class InheritDataTypeFromItemsAttribute : Attribute |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="InheritDataTypeFromItemsAttribute"/> class.
|
|||
/// </summary>
|
|||
/// <param name="ancestorItemsProperty">The name of the property whose item type should be used on the target property.</param>
|
|||
public InheritDataTypeFromItemsAttribute(string ancestorItemsProperty) |
|||
{ |
|||
AncestorItemsProperty = ancestorItemsProperty; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The name of the property whose item type should be used on the target property.
|
|||
/// </summary>
|
|||
public string AncestorItemsProperty { get; } |
|||
|
|||
/// <summary>
|
|||
/// The ancestor type to be used in a lookup for the <see cref="AncestorProperty"/>.
|
|||
/// If null, the declaring type of the target property is used.
|
|||
/// </summary>
|
|||
public Type? AncestorType { get; set; } |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Avalonia.Platform; |
|||
|
|||
public interface IReadableBitmapImpl |
|||
{ |
|||
PixelFormat? Format { get; } |
|||
ILockedFramebuffer Lock(); |
|||
} |
|||
@ -1,9 +1,74 @@ |
|||
namespace Avalonia.Platform |
|||
using System; |
|||
|
|||
namespace Avalonia.Platform |
|||
{ |
|||
public enum PixelFormat |
|||
internal enum PixelFormatEnum |
|||
{ |
|||
Rgb565, |
|||
Rgba8888, |
|||
Bgra8888 |
|||
Bgra8888, |
|||
BlackWhite, |
|||
Gray2, |
|||
Gray4, |
|||
Gray8, |
|||
Gray16, |
|||
Gray32Float, |
|||
Rgba64 |
|||
} |
|||
|
|||
public record struct PixelFormat |
|||
{ |
|||
internal PixelFormatEnum FormatEnum; |
|||
|
|||
public int BitsPerPixel |
|||
{ |
|||
get |
|||
{ |
|||
if (FormatEnum == PixelFormatEnum.BlackWhite) |
|||
return 1; |
|||
else if (FormatEnum == PixelFormatEnum.Gray2) |
|||
return 2; |
|||
else if (FormatEnum == PixelFormatEnum.Gray4) |
|||
return 4; |
|||
else if (FormatEnum == PixelFormatEnum.Gray8) |
|||
return 8; |
|||
else if (FormatEnum == PixelFormatEnum.Rgb565 |
|||
|| FormatEnum == PixelFormatEnum.Gray16) |
|||
return 16; |
|||
else if (FormatEnum == PixelFormatEnum.Rgba64) |
|||
return 64; |
|||
|
|||
return 32; |
|||
} |
|||
} |
|||
|
|||
internal bool HasAlpha => FormatEnum == PixelFormatEnum.Rgba8888 |
|||
|| FormatEnum == PixelFormatEnum.Bgra8888 |
|||
|| FormatEnum == PixelFormatEnum.Rgba64; |
|||
|
|||
internal PixelFormat(PixelFormatEnum format) |
|||
{ |
|||
FormatEnum = format; |
|||
} |
|||
|
|||
public static PixelFormat Rgb565 => PixelFormats.Rgb565; |
|||
public static PixelFormat Rgba8888 => PixelFormats.Rgba8888; |
|||
public static PixelFormat Bgra8888 => PixelFormats.Bgra8888; |
|||
|
|||
public override string ToString() => FormatEnum.ToString(); |
|||
} |
|||
|
|||
public static class PixelFormats |
|||
{ |
|||
public static PixelFormat Rgb565 { get; } = new PixelFormat(PixelFormatEnum.Rgb565); |
|||
public static PixelFormat Rgba8888 { get; } = new PixelFormat(PixelFormatEnum.Rgba8888); |
|||
public static PixelFormat Rgba64 { get; } = new PixelFormat(PixelFormatEnum.Rgba64); |
|||
public static PixelFormat Bgra8888 { get; } = new PixelFormat(PixelFormatEnum.Bgra8888); |
|||
public static PixelFormat BlackWhite { get; } = new PixelFormat(PixelFormatEnum.BlackWhite); |
|||
public static PixelFormat Gray2 { get; } = new PixelFormat(PixelFormatEnum.Gray2); |
|||
public static PixelFormat Gray4 { get; } = new PixelFormat(PixelFormatEnum.Gray4); |
|||
public static PixelFormat Gray8 { get; } = new PixelFormat(PixelFormatEnum.Gray8); |
|||
public static PixelFormat Gray16 { get; } = new PixelFormat(PixelFormatEnum.Gray16); |
|||
public static PixelFormat Gray32Float { get; } = new PixelFormat(PixelFormatEnum.Gray32Float); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,78 @@ |
|||
using System; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server |
|||
{ |
|||
/// <summary>
|
|||
/// A class used to render diagnostic strings (only!), with caching of ASCII glyph runs.
|
|||
/// </summary>
|
|||
internal sealed class DiagnosticTextRenderer |
|||
{ |
|||
private const char FirstChar = (char)32; |
|||
private const char LastChar = (char)126; |
|||
|
|||
private readonly GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; |
|||
|
|||
public double GetMaxHeight() |
|||
{ |
|||
var maxHeight = 0.0; |
|||
|
|||
for (var c = FirstChar; c <= LastChar; c++) |
|||
{ |
|||
var height = _runs[c - FirstChar].Size.Height; |
|||
if (height > maxHeight) |
|||
{ |
|||
maxHeight = height; |
|||
} |
|||
} |
|||
|
|||
return maxHeight; |
|||
} |
|||
|
|||
public DiagnosticTextRenderer(IGlyphTypeface typeface, double fontRenderingEmSize) |
|||
{ |
|||
var chars = new char[LastChar - FirstChar + 1]; |
|||
for (var c = FirstChar; c <= LastChar; c++) |
|||
{ |
|||
var index = c - FirstChar; |
|||
chars[index] = c; |
|||
var glyph = typeface.GetGlyph(c); |
|||
_runs[index] = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph }); |
|||
} |
|||
} |
|||
|
|||
public Size MeasureAsciiText(ReadOnlySpan<char> text) |
|||
{ |
|||
var width = 0.0; |
|||
var height = 0.0; |
|||
|
|||
foreach (var c in text) |
|||
{ |
|||
var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; |
|||
var run = _runs[effectiveChar - FirstChar]; |
|||
width += run.Size.Width; |
|||
height = Math.Max(height, run.Size.Height); |
|||
} |
|||
|
|||
return new Size(width, height); |
|||
} |
|||
|
|||
public void DrawAsciiText(IDrawingContextImpl context, ReadOnlySpan<char> text, IBrush foreground) |
|||
{ |
|||
var offset = 0.0; |
|||
var originalTransform = context.Transform; |
|||
|
|||
foreach (var c in text) |
|||
{ |
|||
var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; |
|||
var run = _runs[effectiveChar - FirstChar]; |
|||
context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); |
|||
context.DrawGlyphRun(foreground, run.PlatformImpl); |
|||
offset += run.Size.Width; |
|||
} |
|||
|
|||
context.Transform = originalTransform; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,176 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Globalization; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Immutable; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server; |
|||
|
|||
/// <summary>
|
|||
/// Represents a simple time graph for diagnostics purpose, used to show layout and render times.
|
|||
/// </summary>
|
|||
internal sealed class FrameTimeGraph |
|||
{ |
|||
private const double HeaderPadding = 2.0; |
|||
|
|||
private readonly IPlatformRenderInterface _renderInterface; |
|||
private readonly ImmutableSolidColorBrush _borderBrush; |
|||
private readonly ImmutablePen _graphPen; |
|||
private readonly double[] _frameValues; |
|||
private readonly Size _size; |
|||
private readonly Size _headerSize; |
|||
private readonly Size _graphSize; |
|||
private readonly double _defaultMaxY; |
|||
private readonly string _title; |
|||
private readonly DiagnosticTextRenderer _textRenderer; |
|||
|
|||
private int _startFrameIndex; |
|||
private int _frameCount; |
|||
|
|||
public Size Size |
|||
=> _size; |
|||
|
|||
public FrameTimeGraph(int maxFrames, Size size, double defaultMaxY, string title, |
|||
DiagnosticTextRenderer textRenderer) |
|||
{ |
|||
Debug.Assert(maxFrames >= 1); |
|||
Debug.Assert(size.Width > 0.0); |
|||
Debug.Assert(size.Height > 0.0); |
|||
|
|||
_renderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); |
|||
_borderBrush = new ImmutableSolidColorBrush(0x80808080); |
|||
_graphPen = new ImmutablePen(Brushes.Blue); |
|||
_frameValues = new double[maxFrames]; |
|||
_size = size; |
|||
_headerSize = new Size(size.Width, textRenderer.GetMaxHeight() + HeaderPadding * 2.0); |
|||
_graphSize = new Size(size.Width, size.Height - _headerSize.Height); |
|||
_defaultMaxY = defaultMaxY; |
|||
_title = title; |
|||
_textRenderer = textRenderer; |
|||
} |
|||
|
|||
public void AddFrameValue(double value) |
|||
{ |
|||
if (_frameCount < _frameValues.Length) |
|||
{ |
|||
_frameValues[_startFrameIndex + _frameCount] = value; |
|||
++_frameCount; |
|||
} |
|||
else |
|||
{ |
|||
// overwrite oldest value
|
|||
_frameValues[_startFrameIndex] = value; |
|||
if (++_startFrameIndex == _frameValues.Length) |
|||
{ |
|||
_startFrameIndex = 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Reset() |
|||
{ |
|||
_startFrameIndex = 0; |
|||
_frameCount = 0; |
|||
} |
|||
|
|||
public void Render(IDrawingContextImpl context) |
|||
{ |
|||
var originalTransform = context.Transform; |
|||
context.PushClip(new Rect(_size)); |
|||
|
|||
context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_size))); |
|||
context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_headerSize))); |
|||
|
|||
context.Transform = originalTransform * Matrix.CreateTranslation(HeaderPadding, HeaderPadding); |
|||
_textRenderer.DrawAsciiText(context, _title.AsSpan(), Brushes.Black); |
|||
|
|||
if (_frameCount > 0) |
|||
{ |
|||
var (min, avg, max) = GetYValues(); |
|||
|
|||
DrawLabelledValue(context, "Min", min, originalTransform, _headerSize.Width * 0.19); |
|||
DrawLabelledValue(context, "Avg", avg, originalTransform, _headerSize.Width * 0.46); |
|||
DrawLabelledValue(context, "Max", max, originalTransform, _headerSize.Width * 0.73); |
|||
|
|||
context.Transform = originalTransform * Matrix.CreateTranslation(0.0, _headerSize.Height); |
|||
context.DrawGeometry(null, _graphPen, BuildGraphGeometry(Math.Max(max, _defaultMaxY))); |
|||
} |
|||
|
|||
context.Transform = originalTransform; |
|||
context.PopClip(); |
|||
} |
|||
|
|||
private void DrawLabelledValue(IDrawingContextImpl context, string label, double value, in Matrix originalTransform, |
|||
double left) |
|||
{ |
|||
context.Transform = originalTransform * Matrix.CreateTranslation(left + HeaderPadding, HeaderPadding); |
|||
|
|||
var brush = value <= _defaultMaxY ? Brushes.Black : Brushes.Red; |
|||
|
|||
#if NET6_0_OR_GREATER
|
|||
Span<char> buffer = stackalloc char[24]; |
|||
buffer.TryWrite(CultureInfo.InvariantCulture, $"{label}: {value,5:F2}ms", out var charsWritten); |
|||
_textRenderer.DrawAsciiText(context, buffer.Slice(0, charsWritten), brush); |
|||
#else
|
|||
var text = FormattableString.Invariant($"{label}: {value,5:F2}ms"); |
|||
_textRenderer.DrawAsciiText(context, text.AsSpan(), brush); |
|||
#endif
|
|||
} |
|||
|
|||
private IStreamGeometryImpl BuildGraphGeometry(double maxY) |
|||
{ |
|||
Debug.Assert(_frameCount > 0); |
|||
|
|||
var graphGeometry = _renderInterface.CreateStreamGeometry(); |
|||
using var geometryContext = graphGeometry.Open(); |
|||
|
|||
var xRatio = _graphSize.Width / _frameValues.Length; |
|||
var yRatio = _graphSize.Height / maxY; |
|||
|
|||
geometryContext.BeginFigure(new Point(0.0, _graphSize.Height - GetFrameValue(0) * yRatio), false); |
|||
|
|||
for (var i = 1; i < _frameCount; ++i) |
|||
{ |
|||
var x = Math.Round(i * xRatio); |
|||
var y = _graphSize.Height - GetFrameValue(i) * yRatio; |
|||
geometryContext.LineTo(new Point(x, y)); |
|||
} |
|||
|
|||
geometryContext.EndFigure(false); |
|||
return graphGeometry; |
|||
} |
|||
|
|||
private (double Min, double Average, double Max) GetYValues() |
|||
{ |
|||
Debug.Assert(_frameCount > 0); |
|||
|
|||
var min = double.MaxValue; |
|||
var max = double.MinValue; |
|||
var total = 0.0; |
|||
|
|||
for (var i = 0; i < _frameCount; ++i) |
|||
{ |
|||
var y = GetFrameValue(i); |
|||
|
|||
total += y; |
|||
|
|||
if (y < min) |
|||
{ |
|||
min = y; |
|||
} |
|||
|
|||
if (y > max) |
|||
{ |
|||
max = y; |
|||
} |
|||
} |
|||
|
|||
return (min, total / _frameCount, max); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private double GetFrameValue(int frameOffset) |
|||
=> _frameValues[(_startFrameIndex + frameOffset) % _frameValues.Length]; |
|||
} |
|||
@ -1,780 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
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 Func<IRenderTarget>? _renderTargetFactory; |
|||
private readonly PlatformRenderInterfaceContextManager? _renderInterface; |
|||
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; |
|||
private List<Action>? _pendingRenderThreadJobs; |
|||
|
|||
/// <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="renderTargetFactory">The target render factory.</param>
|
|||
/// <param name="renderInterface">The Platform Render Context.</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, |
|||
Func<IRenderTarget> renderTargetFactory, |
|||
PlatformRenderInterfaceContextManager? renderInterface = null, |
|||
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; |
|||
_renderTargetFactory = renderTargetFactory; |
|||
_renderInterface = renderInterface; |
|||
_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; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType) |
|||
{ |
|||
if (_renderInterface == null) |
|||
return new((object?)null); |
|||
|
|||
var tcs = new TaskCompletionSource<object?>(); |
|||
_pendingRenderThreadJobs ??= new(); |
|||
_pendingRenderThreadJobs.Add(() => |
|||
{ |
|||
try |
|||
{ |
|||
using (_renderInterface.EnsureCurrent()) |
|||
{ |
|||
tcs.TrySetResult(_renderInterface.Value.TryGetFeature(featureType)); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
tcs.TrySetException(e); |
|||
} |
|||
}); |
|||
return new ValueTask<object?>(tcs.Task); |
|||
} |
|||
|
|||
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<IDrawOperation>? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene; |
|||
|
|||
/// <inheritdoc/>
|
|||
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) |
|||
{ |
|||
return TryGetChildScene(_currentDraw)?.Size ?? default; |
|||
} |
|||
|
|||
/// <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 |
|||
{ |
|||
try |
|||
{ |
|||
if(scene.Item.RenderThreadJobs!=null) |
|||
foreach (var job in scene.Item.RenderThreadJobs) |
|||
job(); |
|||
} |
|||
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.IsDefault && 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?.IsCorrupted == true) |
|||
{ |
|||
RenderTarget!.Dispose(); |
|||
RenderTarget = null; |
|||
} |
|||
|
|||
if (RenderTarget == null) |
|||
{ |
|||
RenderTarget = _renderTargetFactory!(); |
|||
} |
|||
|
|||
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) |
|||
{ |
|||
RenderThreadJobs = _pendingRenderThreadJobs |
|||
}); |
|||
_pendingRenderThreadJobs = null; |
|||
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, FormattableString.Invariant($"frame-{id}-layer-{index++}.png")); |
|||
layer.Bitmap.Item.Save(fileName); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Allows customization of hit-testing
|
|||
/// </summary>
|
|||
public interface ICustomHitTest |
|||
{ |
|||
/// <param name="point">The point to hit test in global coordinate space.</param>
|
|||
bool HitTest(Point point); |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
/// <summary>
|
|||
/// An interface to allow non-templated controls to customize their hit-testing
|
|||
/// when using a renderer with a simple hit-testing algorithm without a scene graph,
|
|||
/// such as <see cref="ImmediateRenderer" />
|
|||
/// </summary>
|
|||
public interface ICustomSimpleHitTest |
|||
{ |
|||
/// <param name="point">The point to hit test in global coordinate space.</param>
|
|||
bool HitTest(Point point); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Allows customization of hit-testing for all renderers.
|
|||
/// </summary>
|
|||
public interface ICustomHitTest : ICustomSimpleHitTest |
|||
{ |
|||
} |
|||
|
|||
public static class CustomSimpleHitTestExtensions |
|||
{ |
|||
public static bool HitTestCustom(this Visual visual, Point point) |
|||
=> (visual as ICustomSimpleHitTest)?.HitTest(point) ?? visual.TransformedBounds?.Contains(point) == true; |
|||
|
|||
public static bool HitTestCustom(this IEnumerable<Visual> children, Point point) |
|||
=> children.Any(ctrl => ctrl.HitTestCustom(point)); |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the interface for a renderer factory.
|
|||
/// </summary>
|
|||
public interface IRendererFactory |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a renderer.
|
|||
/// </summary>
|
|||
/// <param name="root">The root visual.</param>
|
|||
/// <param name="renderLoop">The render loop.</param>
|
|||
IRenderer Create(IRenderRoot root, IRenderLoop renderLoop); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a single layout pass timing.
|
|||
/// </summary>
|
|||
/// <param name="PassCounter">The number of the layout pass.</param>
|
|||
/// <param name="Elapsed">The elapsed time during the layout pass.</param>
|
|||
internal readonly record struct LayoutPassTiming(int PassCounter, TimeSpan Elapsed); |
|||
} |
|||
@ -1,48 +0,0 @@ |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
public class RenderLayer |
|||
{ |
|||
public RenderLayer( |
|||
IDrawingContextImpl drawingContext, |
|||
Size size, |
|||
double scaling, |
|||
Visual layerRoot) |
|||
{ |
|||
Bitmap = RefCountable.Create(drawingContext.CreateLayer(size)); |
|||
Size = size; |
|||
Scaling = scaling; |
|||
LayerRoot = layerRoot; |
|||
IsEmpty = true; |
|||
} |
|||
|
|||
public IRef<IDrawingContextLayerImpl> Bitmap { get; private set; } |
|||
public bool IsEmpty { get; set; } |
|||
public double Scaling { get; private set; } |
|||
public Size Size { get; private set; } |
|||
public Visual LayerRoot { get; } |
|||
|
|||
public void RecreateBitmap(IDrawingContextImpl drawingContext, Size size, double scaling) |
|||
{ |
|||
if (Size != size || Scaling != scaling) |
|||
{ |
|||
var resized = RefCountable.Create(drawingContext.CreateLayer(size)); |
|||
|
|||
using (var context = resized.Item.CreateDrawingContext(null)) |
|||
{ |
|||
Bitmap.Dispose(); |
|||
context.Clear(default); |
|||
|
|||
Bitmap = resized; |
|||
Scaling = scaling; |
|||
Size = size; |
|||
IsEmpty = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,69 +0,0 @@ |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
public class RenderLayers : IEnumerable<RenderLayer> |
|||
{ |
|||
private readonly List<RenderLayer> _inner = new List<RenderLayer>(); |
|||
private readonly Dictionary<Visual, RenderLayer> _index = new Dictionary<Visual, RenderLayer>(); |
|||
|
|||
public int Count => _inner.Count; |
|||
public RenderLayer this[Visual layerRoot] => _index[layerRoot]; |
|||
|
|||
public void Update(Scene scene, IDrawingContextImpl context) |
|||
{ |
|||
for (var i = scene.Layers.Count - 1; i >= 0; --i) |
|||
{ |
|||
var src = scene.Layers[i]; |
|||
|
|||
if (!_index.TryGetValue(src.LayerRoot, out var layer)) |
|||
{ |
|||
layer = new RenderLayer(context, scene.Size, scene.Scaling, src.LayerRoot); |
|||
_inner.Add(layer); |
|||
_index.Add(src.LayerRoot, layer); |
|||
} |
|||
else |
|||
{ |
|||
layer.RecreateBitmap(context, scene.Size, scene.Scaling); |
|||
} |
|||
} |
|||
|
|||
for (var i = 0; i < _inner.Count;) |
|||
{ |
|||
var layer = _inner[i]; |
|||
|
|||
if (!scene.Layers.Exists(layer.LayerRoot)) |
|||
{ |
|||
layer.Bitmap.Dispose(); |
|||
_inner.RemoveAt(i); |
|||
_index.Remove(layer.LayerRoot); |
|||
} |
|||
else |
|||
i++; |
|||
} |
|||
} |
|||
|
|||
public void Clear() |
|||
{ |
|||
foreach (var layer in _index.Values) |
|||
{ |
|||
layer.Bitmap.Dispose(); |
|||
} |
|||
|
|||
_index.Clear(); |
|||
_inner.Clear(); |
|||
} |
|||
|
|||
public bool TryGetValue(Visual layerRoot, [NotNullWhen(true)] out RenderLayer? value) |
|||
{ |
|||
return _index.TryGetValue(layerRoot, out value); |
|||
} |
|||
|
|||
public IEnumerator<RenderLayer> GetEnumerator() => _inner.GetEnumerator(); |
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Rendering; |
|||
|
|||
/// <summary>
|
|||
/// Represents the various types of overlays that can be drawn by a renderer.
|
|||
/// </summary>
|
|||
[Flags] |
|||
public enum RendererDebugOverlays |
|||
{ |
|||
/// <summary>
|
|||
/// Do not draw any overlay.
|
|||
/// </summary>
|
|||
None = 0, |
|||
|
|||
/// <summary>
|
|||
/// Draw a FPS counter.
|
|||
/// </summary>
|
|||
Fps = 1 << 0, |
|||
|
|||
/// <summary>
|
|||
/// Draw invalidated rectangles each frame.
|
|||
/// </summary>
|
|||
DirtyRects = 1 << 1, |
|||
|
|||
/// <summary>
|
|||
/// Draw a graph of past layout times.
|
|||
/// </summary>
|
|||
LayoutTimeGraph = 1 << 2, |
|||
|
|||
/// <summary>
|
|||
/// Draw a graph of past render times.
|
|||
/// </summary>
|
|||
RenderTimeGraph = 1 << 3 |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
using System.ComponentModel; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
/// <summary>
|
|||
/// Manages configurable diagnostics that can be displayed by a renderer.
|
|||
/// </summary>
|
|||
public class RendererDiagnostics : INotifyPropertyChanged |
|||
{ |
|||
private RendererDebugOverlays _debugOverlays; |
|||
private LayoutPassTiming _lastLayoutPassTiming; |
|||
private PropertyChangedEventArgs? _debugOverlaysChangedEventArgs; |
|||
private PropertyChangedEventArgs? _lastLayoutPassTimingChangedEventArgs; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets which debug overlays are displayed by the renderer.
|
|||
/// </summary>
|
|||
public RendererDebugOverlays DebugOverlays |
|||
{ |
|||
get => _debugOverlays; |
|||
set |
|||
{ |
|||
if (_debugOverlays != value) |
|||
{ |
|||
_debugOverlays = value; |
|||
OnPropertyChanged(_debugOverlaysChangedEventArgs ??= new(nameof(DebugOverlays))); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the last layout pass timing that the renderer may display.
|
|||
/// </summary>
|
|||
internal LayoutPassTiming LastLayoutPassTiming |
|||
{ |
|||
get => _lastLayoutPassTiming; |
|||
set |
|||
{ |
|||
if (!_lastLayoutPassTiming.Equals(value)) |
|||
{ |
|||
_lastLayoutPassTiming = value; |
|||
OnPropertyChanged(_lastLayoutPassTimingChangedEventArgs ??= new(nameof(LastLayoutPassTiming))); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public event PropertyChangedEventHandler? PropertyChanged; |
|||
|
|||
/// <summary>
|
|||
/// Called when a property changes on the object.
|
|||
/// </summary>
|
|||
/// <param name="args">The property change details.</param>
|
|||
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args) |
|||
=> PropertyChanged?.Invoke(this, args); |
|||
} |
|||
} |
|||
@ -1,482 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.SceneGraph |
|||
{ |
|||
/// <summary>
|
|||
/// A drawing context which builds a scene graph.
|
|||
/// </summary>
|
|||
internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport |
|||
{ |
|||
private readonly ISceneBuilder _sceneBuilder; |
|||
private VisualNode? _node; |
|||
private int _childIndex; |
|||
private int _drawOperationindex; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DeferredDrawingContextImpl"/> class.
|
|||
/// </summary>
|
|||
/// <param name="sceneBuilder">
|
|||
/// A scene builder used for constructing child scenes for visual brushes.
|
|||
/// </param>
|
|||
/// <param name="layers">The scene layers.</param>
|
|||
public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers) |
|||
{ |
|||
_sceneBuilder = sceneBuilder; |
|||
Layers = layers; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Matrix Transform { get; set; } = Matrix.Identity; |
|||
|
|||
/// <summary>
|
|||
/// Gets the layers in the scene being built.
|
|||
/// </summary>
|
|||
public SceneLayers Layers { get; } |
|||
|
|||
/// <summary>
|
|||
/// Informs the drawing context of the visual node that is about to be rendered.
|
|||
/// </summary>
|
|||
/// <param name="node">The visual node.</param>
|
|||
/// <returns>
|
|||
/// An object which when disposed will commit the changes to visual node.
|
|||
/// </returns>
|
|||
public UpdateState BeginUpdate(VisualNode node) |
|||
{ |
|||
_ = node ?? throw new ArgumentNullException(nameof(node)); |
|||
|
|||
if (_node != null) |
|||
{ |
|||
if (_childIndex < _node.Children.Count) |
|||
{ |
|||
_node.ReplaceChild(_childIndex, node); |
|||
} |
|||
else |
|||
{ |
|||
_node.AddChild(node); |
|||
} |
|||
|
|||
++_childIndex; |
|||
} |
|||
|
|||
var state = new UpdateState(this, _node, _childIndex, _drawOperationindex); |
|||
_node = node; |
|||
_childIndex = _drawOperationindex = 0; |
|||
return state; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Clear(Color color) |
|||
{ |
|||
// Cannot clear a deferred scene.
|
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
// Nothing to do here since we allocate no unmanaged resources.
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes any remaining drawing operations from the visual node.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Drawing operations are updated in place, overwriting existing drawing operations if
|
|||
/// they are different. Once drawing has completed for the current visual node, it is
|
|||
/// possible that there are stale drawing operations at the end of the list. This method
|
|||
/// trims these stale drawing operations.
|
|||
/// </remarks>
|
|||
public void TrimChildren() |
|||
{ |
|||
_node!.TrimChildren(_childIndex); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) |
|||
{ |
|||
var next = NextDrawAs<GeometryNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) |
|||
{ |
|||
Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) |
|||
{ |
|||
var next = NextDrawAs<ImageNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) |
|||
{ |
|||
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) |
|||
{ |
|||
// This method is currently only used to composite layers so shouldn't be called here.
|
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawLine(IPen pen, Point p1, Point p2) |
|||
{ |
|||
var next = NextDrawAs<LineNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) |
|||
{ |
|||
Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, |
|||
BoxShadows boxShadows = default) |
|||
{ |
|||
var next = NextDrawAs<RectangleNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) |
|||
{ |
|||
Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) |
|||
{ |
|||
var next = NextDrawAs<ExperimentalAcrylicNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, material, rect)) |
|||
{ |
|||
Add(new ExperimentalAcrylicNode(Transform, material, rect)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) |
|||
{ |
|||
var next = NextDrawAs<EllipseNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) |
|||
{ |
|||
Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
public void Custom(ICustomDrawOperation custom) |
|||
{ |
|||
var next = NextDrawAs<CustomDrawOperation>(); |
|||
if (next == null || !next.Item.Equals(Transform, custom)) |
|||
Add(new CustomDrawOperation(custom, Transform)); |
|||
else |
|||
++_drawOperationindex; |
|||
} |
|||
|
|||
public object? GetFeature(Type t) => null; |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawGlyphRun(IBrush foreground, IRef<IGlyphRunImpl> glyphRun) |
|||
{ |
|||
var next = NextDrawAs<GlyphRunNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) |
|||
{ |
|||
Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
public IDrawingContextLayerImpl CreateLayer(Size size) |
|||
{ |
|||
throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopClip() |
|||
{ |
|||
var next = NextDrawAs<ClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null)) |
|||
{ |
|||
Add(new ClipNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopGeometryClip() |
|||
{ |
|||
var next = NextDrawAs<GeometryClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null)) |
|||
{ |
|||
Add(new GeometryClipNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopBitmapBlendMode() |
|||
{ |
|||
var next = NextDrawAs<BitmapBlendModeNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null)) |
|||
{ |
|||
Add(new BitmapBlendModeNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopOpacity() |
|||
{ |
|||
var next = NextDrawAs<OpacityNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null)) |
|||
{ |
|||
Add(new OpacityNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopOpacityMask() |
|||
{ |
|||
var next = NextDrawAs<OpacityMaskNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null, null)) |
|||
{ |
|||
Add(new OpacityMaskNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushClip(Rect clip) |
|||
{ |
|||
var next = NextDrawAs<ClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, clip)) |
|||
{ |
|||
Add(new ClipNode(Transform, clip)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void PushClip(RoundedRect clip) |
|||
{ |
|||
var next = NextDrawAs<ClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, clip)) |
|||
{ |
|||
Add(new ClipNode(Transform, clip)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushGeometryClip(IGeometryImpl? clip) |
|||
{ |
|||
if (clip is null) |
|||
return; |
|||
|
|||
var next = NextDrawAs<GeometryClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, clip)) |
|||
{ |
|||
Add(new GeometryClipNode(Transform, clip)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushOpacity(double opacity) |
|||
{ |
|||
var next = NextDrawAs<OpacityNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(opacity)) |
|||
{ |
|||
Add(new OpacityNode(opacity)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushOpacityMask(IBrush mask, Rect bounds) |
|||
{ |
|||
var next = NextDrawAs<OpacityMaskNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(mask, bounds)) |
|||
{ |
|||
Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) |
|||
{ |
|||
var next = NextDrawAs<BitmapBlendModeNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(blendingMode)) |
|||
{ |
|||
Add(new BitmapBlendModeNode(blendingMode)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationindex; |
|||
} |
|||
} |
|||
|
|||
public readonly struct UpdateState : IDisposable |
|||
{ |
|||
public UpdateState( |
|||
DeferredDrawingContextImpl owner, |
|||
VisualNode? node, |
|||
int childIndex, |
|||
int drawOperationIndex) |
|||
{ |
|||
Owner = owner; |
|||
Node = node; |
|||
ChildIndex = childIndex; |
|||
DrawOperationIndex = drawOperationIndex; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Owner._node!.TrimDrawOperations(Owner._drawOperationindex); |
|||
|
|||
var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot!).Dirty; |
|||
|
|||
var drawOperations = Owner._node.DrawOperations; |
|||
var drawOperationsCount = drawOperations.Count; |
|||
|
|||
for (var i = 0; i < drawOperationsCount; i++) |
|||
{ |
|||
dirty.Add(drawOperations[i].Item.Bounds); |
|||
} |
|||
|
|||
Owner._node = Node; |
|||
Owner._childIndex = ChildIndex; |
|||
Owner._drawOperationindex = DrawOperationIndex; |
|||
} |
|||
|
|||
public DeferredDrawingContextImpl Owner { get; } |
|||
public VisualNode? Node { get; } |
|||
public int ChildIndex { get; } |
|||
public int DrawOperationIndex { get; } |
|||
} |
|||
|
|||
private void Add<T>(T node) where T : class, IDrawOperation |
|||
{ |
|||
using (var refCounted = RefCountable.Create(node)) |
|||
{ |
|||
Add(refCounted); |
|||
} |
|||
} |
|||
|
|||
private void Add(IRef<IDrawOperation> node) |
|||
{ |
|||
if (_drawOperationindex < _node!.DrawOperations.Count) |
|||
{ |
|||
_node.ReplaceDrawOperation(_drawOperationindex, node); |
|||
} |
|||
else |
|||
{ |
|||
_node.AddDrawOperation(node); |
|||
} |
|||
|
|||
++_drawOperationindex; |
|||
} |
|||
|
|||
private IRef<T>? NextDrawAs<T>() where T : class, IDrawOperation |
|||
{ |
|||
return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef<T> : null; |
|||
} |
|||
|
|||
private IDisposable? CreateChildScene(IBrush? brush) |
|||
{ |
|||
var visualBrush = brush as VisualBrush; |
|||
|
|||
if (visualBrush != null) |
|||
{ |
|||
var visual = visualBrush.Visual; |
|||
|
|||
if (visual != null) |
|||
{ |
|||
(visual as IVisualBrushInitialize)?.EnsureInitialized(); |
|||
var scene = new Scene(visual); |
|||
_sceneBuilder.UpdateAll(scene); |
|||
return scene; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.SceneGraph |
|||
{ |
|||
/// <summary>
|
|||
/// Builds a scene graph from a visual tree.
|
|||
/// </summary>
|
|||
public interface ISceneBuilder |
|||
{ |
|||
/// <summary>
|
|||
/// Builds the initial scene graph for a visual tree.
|
|||
/// </summary>
|
|||
/// <param name="scene">The scene to build.</param>
|
|||
void UpdateAll(Scene scene); |
|||
|
|||
/// <summary>
|
|||
/// Updates the visual (and potentially its children) in a scene.
|
|||
/// </summary>
|
|||
/// <param name="scene">The scene.</param>
|
|||
/// <param name="visual">The visual to update.</param>
|
|||
/// <returns>True if changes were made, otherwise false.</returns>
|
|||
bool Update(Scene scene, Visual visual); |
|||
} |
|||
} |
|||
@ -1,105 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.SceneGraph |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a node in the low-level scene graph representing a <see cref="Visual"/>.
|
|||
/// </summary>
|
|||
public interface IVisualNode : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the visual to which the node relates.
|
|||
/// </summary>
|
|||
Visual Visual { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the parent scene graph node.
|
|||
/// </summary>
|
|||
IVisualNode? Parent { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the transform for the node from global to control coordinates.
|
|||
/// </summary>
|
|||
Matrix Transform { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the corner radius of visual. Contents are clipped to this radius.
|
|||
/// </summary>
|
|||
CornerRadius ClipToBoundsRadius { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the bounds of the node's geometry in global coordinates.
|
|||
/// </summary>
|
|||
Rect Bounds { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the clip bounds for the node in global coordinates.
|
|||
/// </summary>
|
|||
Rect ClipBounds { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the layout bounds for the node in global coordinates.
|
|||
/// </summary>
|
|||
Rect LayoutBounds { get; } |
|||
|
|||
/// <summary>
|
|||
/// Whether the node is clipped to <see cref="ClipBounds"/>.
|
|||
/// </summary>
|
|||
bool ClipToBounds { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the node's clip geometry, if any.
|
|||
/// </summary>
|
|||
IGeometryImpl? GeometryClip { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether one of the node's ancestors has a geometry clip.
|
|||
/// </summary>
|
|||
bool HasAncestorGeometryClip { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the child scene graph nodes.
|
|||
/// </summary>
|
|||
IReadOnlyList<IVisualNode> Children { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the drawing operations for the visual.
|
|||
/// </summary>
|
|||
IReadOnlyList<IRef<IDrawOperation>> DrawOperations { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the opacity of the scene graph node.
|
|||
/// </summary>
|
|||
double Opacity { get; } |
|||
|
|||
/// <summary>
|
|||
/// Sets up the drawing context for rendering the node's geometry.
|
|||
/// </summary>
|
|||
/// <param name="context">The drawing context.</param>
|
|||
/// <param name="skipOpacity">Whether to skip pushing the control's opacity.</param>
|
|||
void BeginRender(IDrawingContextImpl context, bool skipOpacity); |
|||
|
|||
/// <summary>
|
|||
/// Resets the drawing context after rendering the node's geometry.
|
|||
/// </summary>
|
|||
/// <param name="context">The drawing context.</param>
|
|||
/// <param name="skipOpacity">Whether to skip popping the control's opacity.</param>
|
|||
void EndRender(IDrawingContextImpl context, bool skipOpacity); |
|||
|
|||
/// <summary>
|
|||
/// Hit test the geometry in this node.
|
|||
/// </summary>
|
|||
/// <param name="p">The point in global coordinates.</param>
|
|||
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
|
|||
/// <remarks>
|
|||
/// This method does not recurse to child <see cref="IVisualNode"/>s, if you want
|
|||
/// to hit test children they must be hit tested manually.
|
|||
/// </remarks>
|
|||
bool HitTest(Point p); |
|||
|
|||
bool Disposed { get; } |
|||
} |
|||
} |
|||
@ -1,352 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Collections.Pooled; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.SceneGraph |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a scene graph used by the <see cref="DeferredRenderer"/>.
|
|||
/// </summary>
|
|||
public class Scene : IDisposable |
|||
{ |
|||
private readonly Dictionary<Visual, IVisualNode> _index; |
|||
private readonly TaskCompletionSource<bool> _rendered = new TaskCompletionSource<bool>(); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Scene"/> class.
|
|||
/// </summary>
|
|||
/// <param name="rootVisual">The root visual to draw.</param>
|
|||
public Scene(Visual rootVisual) |
|||
: this( |
|||
new VisualNode(rootVisual, null), |
|||
new Dictionary<Visual, IVisualNode>(), |
|||
new SceneLayers(rootVisual), |
|||
0) |
|||
{ |
|||
_index.Add(rootVisual, Root); |
|||
} |
|||
|
|||
private Scene(VisualNode root, Dictionary<Visual, IVisualNode> index, SceneLayers layers, int generation) |
|||
{ |
|||
_ = root ?? throw new ArgumentNullException(nameof(root)); |
|||
|
|||
var renderRoot = root.Visual as IRenderRoot; |
|||
|
|||
_index = index; |
|||
Root = root; |
|||
Layers = layers; |
|||
Generation = generation; |
|||
root.LayerRoot = root.Visual; |
|||
} |
|||
|
|||
public Task Rendered => _rendered.Task; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value identifying the scene's generation. This is incremented each time the scene is cloned.
|
|||
/// </summary>
|
|||
public int Generation { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the layers for the scene.
|
|||
/// </summary>
|
|||
public SceneLayers Layers { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the root node of the scene graph.
|
|||
/// </summary>
|
|||
public IVisualNode Root { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the size of the scene in device independent pixels.
|
|||
/// </summary>
|
|||
public Size Size { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the scene scaling.
|
|||
/// </summary>
|
|||
public double Scaling { get; set; } = 1; |
|||
|
|||
/// <summary>
|
|||
/// Adds a node to the scene index.
|
|||
/// </summary>
|
|||
/// <param name="node">The node.</param>
|
|||
public void Add(IVisualNode node) |
|||
{ |
|||
_ = node ?? throw new ArgumentNullException(nameof(node)); |
|||
|
|||
_index.Add(node.Visual, node); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clones the scene.
|
|||
/// </summary>
|
|||
/// <returns>The cloned scene.</returns>
|
|||
public Scene CloneScene() |
|||
{ |
|||
var index = new Dictionary<Visual, IVisualNode>(_index.Count); |
|||
var root = Clone((VisualNode)Root, null, index); |
|||
|
|||
var result = new Scene(root, index, Layers.Clone(), Generation + 1) |
|||
{ |
|||
Size = Size, |
|||
Scaling = Scaling, |
|||
}; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_rendered.TrySetResult(false); |
|||
foreach (var node in _index.Values) |
|||
{ |
|||
node.Dispose(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to find a node in the scene graph representing the specified visual.
|
|||
/// </summary>
|
|||
/// <param name="visual">The visual.</param>
|
|||
/// <returns>
|
|||
/// The node representing the visual or null if it could not be found.
|
|||
/// </returns>
|
|||
public IVisualNode? FindNode(Visual visual) |
|||
{ |
|||
_index.TryGetValue(visual, out var node); |
|||
return node; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the visuals at a point in the scene.
|
|||
/// </summary>
|
|||
/// <param name="p">The point.</param>
|
|||
/// <param name="root">The root of the subtree to search.</param>
|
|||
/// <param name="filter">A filter. May be null.</param>
|
|||
/// <returns>The visuals at the specified point.</returns>
|
|||
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool>? filter) |
|||
{ |
|||
var node = FindNode(root); |
|||
return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty<Visual>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the visual at a point in the scene.
|
|||
/// </summary>
|
|||
/// <param name="p">The point.</param>
|
|||
/// <param name="root">The root of the subtree to search.</param>
|
|||
/// <param name="filter">A filter. May be null.</param>
|
|||
/// <returns>The visual at the specified point.</returns>
|
|||
public Visual? HitTestFirst(Point p, Visual root, Func<Visual, bool>? filter) |
|||
{ |
|||
var node = FindNode(root); |
|||
return (node != null) ? HitTestFirst(node, p, filter) : null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes a node from the scene index.
|
|||
/// </summary>
|
|||
/// <param name="node">The node.</param>
|
|||
public void Remove(IVisualNode node) |
|||
{ |
|||
_ = node ?? throw new ArgumentNullException(nameof(node)); |
|||
|
|||
_index.Remove(node.Visual); |
|||
|
|||
node.Dispose(); |
|||
} |
|||
|
|||
private VisualNode Clone(VisualNode source, IVisualNode? parent, Dictionary<Visual, IVisualNode> index) |
|||
{ |
|||
var result = source.Clone(parent); |
|||
|
|||
index.Add(result.Visual, result); |
|||
|
|||
var children = source.Children; |
|||
var childrenCount = children.Count; |
|||
|
|||
if (childrenCount > 0) |
|||
{ |
|||
result.TryPreallocateChildren(childrenCount); |
|||
|
|||
for (var i = 0; i < childrenCount; i++) |
|||
{ |
|||
var child = children[i]; |
|||
|
|||
result.AddChild(Clone((VisualNode)child, result, index)); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private Visual HitTestFirst(IVisualNode root, Point p, Func<Visual, bool>? filter) |
|||
{ |
|||
using var enumerator = new HitTestEnumerator(root, filter, p, Root); |
|||
|
|||
enumerator.MoveNext(); |
|||
|
|||
return enumerator.Current; |
|||
} |
|||
|
|||
private class HitTestEnumerable : IEnumerable<Visual> |
|||
{ |
|||
private readonly IVisualNode _root; |
|||
private readonly Func<Visual, bool>? _filter; |
|||
private readonly IVisualNode _sceneRoot; |
|||
private readonly Point _point; |
|||
|
|||
public HitTestEnumerable(IVisualNode root, Func<Visual, bool>? filter, Point point, IVisualNode sceneRoot) |
|||
{ |
|||
_root = root; |
|||
_filter = filter; |
|||
_point = point; |
|||
_sceneRoot = sceneRoot; |
|||
} |
|||
|
|||
public IEnumerator<Visual> GetEnumerator() |
|||
{ |
|||
return new HitTestEnumerator(_root, _filter, _point, _sceneRoot); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
} |
|||
|
|||
private struct HitTestEnumerator : IEnumerator<Visual> |
|||
{ |
|||
private readonly PooledStack<Entry> _nodeStack; |
|||
private readonly Func<Visual, bool>? _filter; |
|||
private readonly IVisualNode _sceneRoot; |
|||
private Visual? _current; |
|||
private readonly Point _point; |
|||
|
|||
public HitTestEnumerator(IVisualNode root, Func<Visual, bool>? filter, Point point, IVisualNode sceneRoot) |
|||
{ |
|||
_nodeStack = new PooledStack<Entry>(); |
|||
_nodeStack.Push(new Entry(root, false, null, true)); |
|||
|
|||
_filter = filter; |
|||
_point = point; |
|||
_sceneRoot = sceneRoot; |
|||
|
|||
_current = null; |
|||
} |
|||
|
|||
public bool MoveNext() |
|||
{ |
|||
while (_nodeStack.Count > 0) |
|||
{ |
|||
(var wasVisited, var isRoot, IVisualNode node, Rect? clip) = _nodeStack.Pop(); |
|||
|
|||
if (wasVisited && isRoot) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
var children = node.Children; |
|||
int childCount = children.Count; |
|||
|
|||
if (childCount == 0 || wasVisited) |
|||
{ |
|||
if ((wasVisited || FilterAndClip(node, ref clip)) && |
|||
(node.Visual is ICustomHitTest custom ? custom.HitTest(_point) : node.HitTest(_point))) |
|||
{ |
|||
_current = node.Visual; |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
else if (FilterAndClip(node, ref clip)) |
|||
{ |
|||
_nodeStack.Push(new Entry(node, true, null)); |
|||
|
|||
for (var i = 0; i < childCount; i++) |
|||
{ |
|||
_nodeStack.Push(new Entry(children[i], false, clip)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public void Reset() |
|||
{ |
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
public Visual Current => _current!; |
|||
|
|||
object IEnumerator.Current => Current; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_nodeStack.Dispose(); |
|||
} |
|||
|
|||
private bool FilterAndClip(IVisualNode node, ref Rect? clip) |
|||
{ |
|||
if (_filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree) |
|||
{ |
|||
var clipped = false; |
|||
|
|||
if (node.ClipToBounds) |
|||
{ |
|||
clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); |
|||
clipped = !clip.Value.ContainsExclusive(_point); |
|||
} |
|||
|
|||
if (node.GeometryClip != null) |
|||
{ |
|||
var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual); |
|||
clipped = !node.GeometryClip.FillContains(controlPoint!.Value); |
|||
} |
|||
|
|||
if (!clipped && node.Visual is ICustomHitTest custom) |
|||
{ |
|||
clipped = !custom.HitTest(_point); |
|||
} |
|||
|
|||
return !clipped; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private readonly struct Entry |
|||
{ |
|||
public readonly bool WasVisited; |
|||
public readonly bool IsRoot; |
|||
public readonly IVisualNode Node; |
|||
public readonly Rect? Clip; |
|||
|
|||
public Entry(IVisualNode node, bool wasVisited, Rect? clip, bool isRoot = false) |
|||
{ |
|||
Node = node; |
|||
WasVisited = wasVisited; |
|||
IsRoot = isRoot; |
|||
Clip = clip; |
|||
} |
|||
|
|||
public void Deconstruct(out bool wasVisited, out bool isRoot, out IVisualNode node, out Rect? clip) |
|||
{ |
|||
wasVisited = WasVisited; |
|||
isRoot = IsRoot; |
|||
node = Node; |
|||
clip = Clip; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void MarkAsRendered() => _rendered.TrySetResult(true); |
|||
|
|||
public List<Action>? RenderThreadJobs { get; set; } |
|||
} |
|||
} |
|||
@ -1,485 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Threading; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.SceneGraph |
|||
{ |
|||
/// <summary>
|
|||
/// Builds a scene graph from a visual tree.
|
|||
/// </summary>
|
|||
public class SceneBuilder : ISceneBuilder |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public void UpdateAll(Scene scene) |
|||
{ |
|||
_ = scene ?? throw new ArgumentNullException(nameof(scene)); |
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
|
|||
UpdateSize(scene); |
|||
scene.Layers.GetOrAdd(scene.Root.Visual); |
|||
|
|||
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) |
|||
using (var context = new DrawingContext(impl)) |
|||
{ |
|||
var clip = new Rect(scene.Root.Visual.Bounds.Size); |
|||
Update(context, scene, (VisualNode)scene.Root, clip, true); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Update(Scene scene, Visual visual) |
|||
{ |
|||
_ = scene ?? throw new ArgumentNullException(nameof(scene)); |
|||
_ = visual ?? throw new ArgumentNullException(nameof(visual)); |
|||
|
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
|
|||
if (!scene.Root.Visual.IsVisible) |
|||
{ |
|||
throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual."); |
|||
} |
|||
|
|||
var node = (VisualNode?)scene.FindNode(visual); |
|||
|
|||
if (visual == scene.Root.Visual) |
|||
{ |
|||
UpdateSize(scene); |
|||
} |
|||
|
|||
if (visual.VisualRoot == scene.Root.Visual) |
|||
{ |
|||
if (node?.Parent != null && |
|||
visual.VisualParent != null && |
|||
node.Parent.Visual != visual.VisualParent) |
|||
{ |
|||
// The control has changed parents. Remove the node and recurse into the new parent node.
|
|||
((VisualNode)node.Parent).RemoveChild(node); |
|||
Deindex(scene, node); |
|||
node = (VisualNode?)scene.FindNode(visual.VisualParent); |
|||
} |
|||
|
|||
if (visual.IsVisible) |
|||
{ |
|||
// If the node isn't yet part of the scene, find the nearest ancestor that is.
|
|||
node = node ?? FindExistingAncestor(scene, visual); |
|||
|
|||
// We don't need to do anything if this part of the tree has already been fully
|
|||
// updated.
|
|||
if (node != null && !node.SubTreeUpdated) |
|||
{ |
|||
// If the control we've been asked to update isn't part of the scene then
|
|||
// we're carrying out an add operation, so recurse and add all the
|
|||
// descendents too.
|
|||
var recurse = node.Visual != visual; |
|||
|
|||
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) |
|||
using (var context = new DrawingContext(impl)) |
|||
{ |
|||
var clip = new Rect(scene.Root.Visual.Bounds.Size); |
|||
|
|||
if (node.Parent != null) |
|||
{ |
|||
context.PushPostTransform(node.Parent.Transform); |
|||
clip = node.Parent.ClipBounds; |
|||
} |
|||
|
|||
using (context.PushTransformContainer()) |
|||
{ |
|||
Update(context, scene, node, clip, recurse); |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (node != null) |
|||
{ |
|||
// The control has been hidden so remove it from its parent and deindex the
|
|||
// node and its descendents.
|
|||
((VisualNode?)node.Parent)?.RemoveChild(node); |
|||
Deindex(scene, node); |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
else if (node != null) |
|||
{ |
|||
// The control has been removed so remove it from its parent and deindex the
|
|||
// node and its descendents.
|
|||
var trim = FindFirstDeadAncestor(scene, node); |
|||
((VisualNode)trim.Parent!).RemoveChild(trim); |
|||
Deindex(scene, trim); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private static VisualNode? FindExistingAncestor(Scene scene, Visual visual) |
|||
{ |
|||
var node = scene.FindNode(visual); |
|||
|
|||
while (node == null && visual.IsVisible) |
|||
{ |
|||
var parent = visual.VisualParent; |
|||
|
|||
if (parent is null) |
|||
return null; |
|||
|
|||
visual = parent; |
|||
node = scene.FindNode(visual); |
|||
} |
|||
|
|||
return visual.IsVisible ? (VisualNode?)node : null; |
|||
} |
|||
|
|||
private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node) |
|||
{ |
|||
var parent = node.Parent; |
|||
|
|||
while (parent!.Visual.VisualRoot == null) |
|||
{ |
|||
node = parent; |
|||
parent = node.Parent; |
|||
} |
|||
|
|||
return (VisualNode)node; |
|||
} |
|||
|
|||
private static object GetOrCreateChildNode(Scene scene, Visual child, VisualNode parent) |
|||
{ |
|||
var result = (VisualNode?)scene.FindNode(child); |
|||
|
|||
if (result != null && result.Parent != parent) |
|||
{ |
|||
Deindex(scene, result); |
|||
((VisualNode?)result.Parent)?.RemoveChild(result); |
|||
result = null; |
|||
} |
|||
|
|||
return result ?? CreateNode(scene, child, parent); |
|||
} |
|||
|
|||
private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse) |
|||
{ |
|||
var visual = node.Visual; |
|||
var opacity = visual.Opacity; |
|||
var clipToBounds = visual.ClipToBounds; |
|||
#pragma warning disable CS0618 // Type or member is obsolete
|
|||
var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? |
|||
roundRectClip.ClipToBoundsRadius : |
|||
default; |
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
|||
|
|||
var bounds = new Rect(visual.Bounds.Size); |
|||
var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; |
|||
|
|||
contextImpl.Layers.Find(node.LayerRoot!)?.Dirty.Add(node.Bounds); |
|||
|
|||
if (visual.IsVisible) |
|||
{ |
|||
var m = node != scene.Root ? |
|||
Matrix.CreateTranslation(visual.Bounds.Position) : |
|||
Matrix.Identity; |
|||
|
|||
var renderTransform = Matrix.Identity; |
|||
|
|||
// this should be calculated BEFORE renderTransform
|
|||
if (visual.HasMirrorTransform) |
|||
{ |
|||
var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); |
|||
renderTransform *= mirrorMatrix; |
|||
} |
|||
|
|||
if (visual.RenderTransform != null) |
|||
{ |
|||
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); |
|||
var offset = Matrix.CreateTranslation(origin); |
|||
var finalTransform = (-offset) * visual.RenderTransform.Value * (offset); |
|||
renderTransform *= finalTransform; |
|||
} |
|||
|
|||
m = renderTransform * m; |
|||
|
|||
using (contextImpl.BeginUpdate(node)) |
|||
using (context.PushPostTransform(m)) |
|||
using (context.PushTransformContainer()) |
|||
{ |
|||
var globalBounds = bounds.TransformToAABB(contextImpl.Transform); |
|||
var clipBounds = clipToBounds ? |
|||
globalBounds.Intersect(clip) : |
|||
clip; |
|||
|
|||
forceRecurse = forceRecurse || |
|||
node.ClipBounds != clipBounds || |
|||
node.Opacity != opacity || |
|||
node.Transform != contextImpl.Transform; |
|||
|
|||
node.Transform = contextImpl.Transform; |
|||
node.ClipBounds = clipBounds; |
|||
node.ClipToBounds = clipToBounds; |
|||
node.LayoutBounds = globalBounds; |
|||
node.ClipToBoundsRadius = clipToBoundsRadius; |
|||
node.GeometryClip = visual.Clip?.PlatformImpl; |
|||
node.Opacity = opacity; |
|||
|
|||
// TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning.
|
|||
node.OpacityMask = visual.OpacityMask?.ToImmutable(); |
|||
|
|||
if (ShouldStartLayer(visual)) |
|||
{ |
|||
if (node.LayerRoot != visual) |
|||
{ |
|||
MakeLayer(scene, node); |
|||
} |
|||
else |
|||
{ |
|||
UpdateLayer(node, scene.Layers[node.LayerRoot]); |
|||
} |
|||
} |
|||
else if (node.LayerRoot == node.Visual && node.Parent != null) |
|||
{ |
|||
ClearLayer(scene, node); |
|||
} |
|||
|
|||
if (node.ClipToBounds) |
|||
{ |
|||
clip = clip.Intersect(node.ClipBounds); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
visual.Render(context); |
|||
} |
|||
catch { } |
|||
|
|||
var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform); |
|||
visual.SetTransformedBounds(transformed); |
|||
|
|||
if (forceRecurse) |
|||
{ |
|||
var visualChildren = (IList<Visual>) visual.VisualChildren; |
|||
|
|||
node.TryPreallocateChildren(visualChildren.Count); |
|||
|
|||
if (visualChildren.Count == 1) |
|||
{ |
|||
var childNode = GetOrCreateChildNode(scene, visualChildren[0], node); |
|||
Update(context, scene, (VisualNode)childNode, clip, forceRecurse); |
|||
} |
|||
else if (visualChildren.Count > 1) |
|||
{ |
|||
var count = visualChildren.Count; |
|||
|
|||
if (visual.HasNonUniformZIndexChildren) |
|||
{ |
|||
var sortedChildren = new (Visual visual, int index)[count]; |
|||
|
|||
for (var i = 0; i < count; i++) |
|||
{ |
|||
sortedChildren[i] = (visualChildren[i], i); |
|||
} |
|||
|
|||
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
|
|||
Array.Sort(sortedChildren, (lhs, rhs) => |
|||
{ |
|||
var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); |
|||
|
|||
return result == 0 ? lhs.index.CompareTo(rhs.index) : result; |
|||
}); |
|||
|
|||
foreach (var child in sortedChildren) |
|||
{ |
|||
var childNode = GetOrCreateChildNode(scene, child.Item1, node); |
|||
Update(context, scene, (VisualNode)childNode, clip, forceRecurse); |
|||
} |
|||
} |
|||
else |
|||
foreach (var child in visualChildren) |
|||
{ |
|||
var childNode = GetOrCreateChildNode(scene, child, node); |
|||
Update(context, scene, (VisualNode)childNode, clip, forceRecurse); |
|||
} |
|||
} |
|||
|
|||
node.SubTreeUpdated = true; |
|||
contextImpl.TrimChildren(); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
contextImpl.BeginUpdate(node).Dispose(); |
|||
} |
|||
} |
|||
|
|||
private static void UpdateSize(Scene scene) |
|||
{ |
|||
var renderRoot = scene.Root.Visual as IRenderRoot; |
|||
var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size; |
|||
|
|||
scene.Scaling = renderRoot?.RenderScaling ?? 1; |
|||
|
|||
if (scene.Size != newSize) |
|||
{ |
|||
var oldSize = scene.Size; |
|||
|
|||
scene.Size = newSize; |
|||
|
|||
Rect horizontalDirtyRect = default; |
|||
Rect verticalDirtyRect = default; |
|||
|
|||
if (newSize.Width > oldSize.Width) |
|||
{ |
|||
horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height); |
|||
} |
|||
|
|||
if (newSize.Height > oldSize.Height) |
|||
{ |
|||
verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height); |
|||
} |
|||
|
|||
foreach (var layer in scene.Layers) |
|||
{ |
|||
layer.Dirty.Add(horizontalDirtyRect); |
|||
layer.Dirty.Add(verticalDirtyRect); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static VisualNode CreateNode(Scene scene, Visual visual, VisualNode parent) |
|||
{ |
|||
var node = new VisualNode(visual, parent); |
|||
node.LayerRoot = parent.LayerRoot; |
|||
scene.Add(node); |
|||
return node; |
|||
} |
|||
|
|||
private static void Deindex(Scene scene, VisualNode node) |
|||
{ |
|||
var nodeChildren = node.Children; |
|||
var nodeChildrenCount = nodeChildren.Count; |
|||
|
|||
for (var i = 0; i < nodeChildrenCount; i++) |
|||
{ |
|||
if (nodeChildren[i] is VisualNode visual) |
|||
{ |
|||
Deindex(scene, visual); |
|||
} |
|||
} |
|||
|
|||
scene.Remove(node); |
|||
|
|||
node.SubTreeUpdated = true; |
|||
|
|||
scene.Layers[node.LayerRoot!].Dirty.Add(node.Bounds); |
|||
|
|||
node.Visual.SetTransformedBounds(null); |
|||
|
|||
if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual) |
|||
{ |
|||
scene.Layers.Remove(node.LayerRoot); |
|||
} |
|||
} |
|||
|
|||
private static void ClearLayer(Scene scene, VisualNode node) |
|||
{ |
|||
var parent = (VisualNode)node.Parent!; |
|||
var oldLayerRoot = node.LayerRoot; |
|||
var newLayerRoot = parent.LayerRoot!; |
|||
var existingDirtyRects = scene.Layers[node.LayerRoot!].Dirty; |
|||
var newDirtyRects = scene.Layers[newLayerRoot].Dirty; |
|||
|
|||
existingDirtyRects.Coalesce(); |
|||
|
|||
foreach (var r in existingDirtyRects) |
|||
{ |
|||
newDirtyRects.Add(r); |
|||
} |
|||
|
|||
var oldLayer = scene.Layers[oldLayerRoot!]; |
|||
PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer); |
|||
scene.Layers.Remove(oldLayer); |
|||
} |
|||
|
|||
private static void MakeLayer(Scene scene, VisualNode node) |
|||
{ |
|||
var oldLayerRoot = node.LayerRoot!; |
|||
var layer = scene.Layers.Add(node.Visual); |
|||
var oldLayer = scene.Layers[oldLayerRoot!]; |
|||
|
|||
UpdateLayer(node, layer); |
|||
PropagateLayer(node, layer, scene.Layers[oldLayerRoot]); |
|||
} |
|||
|
|||
private static void UpdateLayer(VisualNode node, SceneLayer layer) |
|||
{ |
|||
layer.Opacity = node.Visual.Opacity; |
|||
|
|||
if (node.Visual.OpacityMask != null) |
|||
{ |
|||
layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable(); |
|||
layer.OpacityMaskRect = node.ClipBounds; |
|||
} |
|||
else |
|||
{ |
|||
layer.OpacityMask = null; |
|||
layer.OpacityMaskRect = default; |
|||
} |
|||
|
|||
layer.GeometryClip = node.HasAncestorGeometryClip ? |
|||
CreateLayerGeometryClip(node) : |
|||
null; |
|||
} |
|||
|
|||
private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer) |
|||
{ |
|||
node.LayerRoot = layer.LayerRoot; |
|||
|
|||
layer.Dirty.Add(node.Bounds); |
|||
oldLayer.Dirty.Add(node.Bounds); |
|||
|
|||
foreach (VisualNode child in node.Children) |
|||
{ |
|||
// If the child is not the start of a new layer, recurse.
|
|||
if (child.LayerRoot != child.Visual) |
|||
{ |
|||
PropagateLayer(child, layer, oldLayer); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// HACK: Disabled layers because they're broken in current renderer. See #2244.
|
|||
private static bool ShouldStartLayer(Visual visual) => false; |
|||
|
|||
private static IGeometryImpl? CreateLayerGeometryClip(VisualNode node) |
|||
{ |
|||
IGeometryImpl? result = null; |
|||
VisualNode? n = node; |
|||
|
|||
for (;;) |
|||
{ |
|||
n = (VisualNode?)n!.Parent; |
|||
|
|||
if (n == null || (n.GeometryClip == null && !n.HasAncestorGeometryClip)) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
if (n?.GeometryClip != null) |
|||
{ |
|||
var transformed = n.GeometryClip.WithTransform(n.Transform); |
|||
|
|||
result = result == null ? transformed : result.Intersect(transformed); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -1,73 +0,0 @@ |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Rendering.SceneGraph |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a layer in a <see cref="Scene"/>.
|
|||
/// </summary>
|
|||
public class SceneLayer |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="SceneLayer"/> class.
|
|||
/// </summary>
|
|||
/// <param name="layerRoot">The visual at the root of the layer.</param>
|
|||
/// <param name="distanceFromRoot">The distance from the scene root.</param>
|
|||
public SceneLayer(Visual layerRoot, int distanceFromRoot) |
|||
{ |
|||
LayerRoot = layerRoot; |
|||
Dirty = new DirtyRects(); |
|||
DistanceFromRoot = distanceFromRoot; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clones the layer.
|
|||
/// </summary>
|
|||
/// <returns>The cloned layer.</returns>
|
|||
public SceneLayer Clone() |
|||
{ |
|||
return new SceneLayer(LayerRoot, DistanceFromRoot) |
|||
{ |
|||
Opacity = Opacity, |
|||
OpacityMask = OpacityMask, |
|||
OpacityMaskRect = OpacityMaskRect, |
|||
GeometryClip = GeometryClip, |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the visual at the root of the layer.
|
|||
/// </summary>
|
|||
public Visual LayerRoot { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the distance of the layer root from the root of the scene.
|
|||
/// </summary>
|
|||
public int DistanceFromRoot { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the opacity of the layer.
|
|||
/// </summary>
|
|||
public double Opacity { get; set; } = 1; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the opacity mask for the layer.
|
|||
/// </summary>
|
|||
public IBrush? OpacityMask { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the target rectangle for the layer opacity mask.
|
|||
/// </summary>
|
|||
public Rect OpacityMaskRect { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the layer's geometry clip.
|
|||
/// </summary>
|
|||
public IGeometryImpl? GeometryClip { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the dirty rectangles for the layer.
|
|||
/// </summary>
|
|||
internal DirtyRects Dirty { get; } |
|||
} |
|||
} |
|||
@ -1,206 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.SceneGraph |
|||
{ |
|||
/// <summary>
|
|||
/// Holds a collection of layers for a <see cref="Scene"/>.
|
|||
/// </summary>
|
|||
public class SceneLayers : IEnumerable<SceneLayer> |
|||
{ |
|||
private readonly Visual _root; |
|||
private readonly List<SceneLayer> _inner; |
|||
private readonly Dictionary<Visual, SceneLayer> _index; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
|
|||
/// </summary>
|
|||
/// <param name="root">The scene's root visual.</param>
|
|||
public SceneLayers(Visual root) : this(root, 0) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
|
|||
/// </summary>
|
|||
/// <param name="root">The scene's root visual.</param>
|
|||
/// <param name="capacity">Initial layer capacity.</param>
|
|||
public SceneLayers(Visual root, int capacity) |
|||
{ |
|||
_root = root; |
|||
|
|||
_inner = new List<SceneLayer>(capacity); |
|||
_index = new Dictionary<Visual, SceneLayer>(capacity); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of layers in the scene.
|
|||
/// </summary>
|
|||
public int Count => _inner.Count; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether any of the layers have a dirty region.
|
|||
/// </summary>
|
|||
public bool HasDirty |
|||
{ |
|||
get |
|||
{ |
|||
foreach (var layer in _inner) |
|||
{ |
|||
if (!layer.Dirty.IsEmpty) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a layer by index.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the layer.</param>
|
|||
/// <returns>The layer.</returns>
|
|||
public SceneLayer this[int index] => _inner[index]; |
|||
|
|||
/// <summary>
|
|||
/// Gets a layer by its root visual.
|
|||
/// </summary>
|
|||
/// <param name="visual">The layer's root visual.</param>
|
|||
/// <returns>The layer.</returns>
|
|||
public SceneLayer this[Visual visual] => _index[visual]; |
|||
|
|||
/// <summary>
|
|||
/// Adds a layer to the scene.
|
|||
/// </summary>
|
|||
/// <param name="layerRoot">The root visual of the layer.</param>
|
|||
/// <returns>The created layer.</returns>
|
|||
public SceneLayer Add(Visual layerRoot) |
|||
{ |
|||
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); |
|||
|
|||
var distance = layerRoot.CalculateDistanceFromAncestor(_root); |
|||
var layer = new SceneLayer(layerRoot, distance); |
|||
var insert = FindInsertIndex(layer); |
|||
_index.Add(layerRoot, layer); |
|||
_inner.Insert(insert, layer); |
|||
return layer; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Makes a deep clone of the layers.
|
|||
/// </summary>
|
|||
/// <returns>The cloned layers.</returns>
|
|||
public SceneLayers Clone() |
|||
{ |
|||
var result = new SceneLayers(_root, Count); |
|||
|
|||
foreach (var src in _inner) |
|||
{ |
|||
var dest = src.Clone(); |
|||
result._index.Add(dest.LayerRoot, dest); |
|||
result._inner.Add(dest); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tests whether a layer exists with the specified root visual.
|
|||
/// </summary>
|
|||
/// <param name="layerRoot">The root visual.</param>
|
|||
/// <returns>
|
|||
/// True if a layer exists with the specified root visual, otherwise false.
|
|||
/// </returns>
|
|||
public bool Exists(Visual layerRoot) |
|||
{ |
|||
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); |
|||
|
|||
return _index.ContainsKey(layerRoot); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to find a layer with the specified root visual.
|
|||
/// </summary>
|
|||
/// <param name="layerRoot">The root visual.</param>
|
|||
/// <returns>The layer if found, otherwise null.</returns>
|
|||
public SceneLayer? Find(Visual layerRoot) |
|||
{ |
|||
_index.TryGetValue(layerRoot, out var result); |
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets an existing layer or creates a new one if no existing layer is found.
|
|||
/// </summary>
|
|||
/// <param name="layerRoot">The root visual.</param>
|
|||
/// <returns>The layer.</returns>
|
|||
public SceneLayer GetOrAdd(Visual layerRoot) |
|||
{ |
|||
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); |
|||
|
|||
if (!_index.TryGetValue(layerRoot, out var result)) |
|||
{ |
|||
result = Add(layerRoot); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes a layer from the scene.
|
|||
/// </summary>
|
|||
/// <param name="layerRoot">The root visual.</param>
|
|||
/// <returns>True if a matching layer was removed, otherwise false.</returns>
|
|||
public bool Remove(Visual layerRoot) |
|||
{ |
|||
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); |
|||
|
|||
if (_index.TryGetValue(layerRoot, out var layer)) |
|||
{ |
|||
Remove(layer); |
|||
} |
|||
|
|||
return layer != null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes a layer from the scene.
|
|||
/// </summary>
|
|||
/// <param name="layer">The layer.</param>
|
|||
/// <returns>True if the layer was part of the scene, otherwise false.</returns>
|
|||
public bool Remove(SceneLayer layer) |
|||
{ |
|||
_ = layer ?? throw new ArgumentNullException(nameof(layer)); |
|||
|
|||
_index.Remove(layer.LayerRoot); |
|||
return _inner.Remove(layer); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerator<SceneLayer> GetEnumerator() => _inner.GetEnumerator(); |
|||
|
|||
/// <inheritdoc/>
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
private int FindInsertIndex(SceneLayer insert) |
|||
{ |
|||
var index = 0; |
|||
|
|||
foreach (var layer in _inner) |
|||
{ |
|||
if (layer.DistanceFromRoot > insert.DistanceFromRoot) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
++index; |
|||
} |
|||
|
|||
return index; |
|||
} |
|||
} |
|||
} |
|||
@ -1,448 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Reactive; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.SceneGraph |
|||
{ |
|||
/// <summary>
|
|||
/// A node in the low-level scene graph representing an <see cref="Visual"/>.
|
|||
/// </summary>
|
|||
internal class VisualNode : IVisualNode |
|||
{ |
|||
private static readonly IReadOnlyList<IVisualNode> EmptyChildren = Array.Empty<IVisualNode>(); |
|||
private static readonly IReadOnlyList<IRef<IDrawOperation>> EmptyDrawOperations = Array.Empty<IRef<IDrawOperation>>(); |
|||
|
|||
private Rect? _bounds; |
|||
private double _opacity; |
|||
private List<IVisualNode>? _children; |
|||
private List<IRef<IDrawOperation>>? _drawOperations; |
|||
private IRef<IDisposable>? _drawOperationsRefCounter; |
|||
private bool _drawOperationsCloned; |
|||
private Matrix transformRestore; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="VisualNode"/> class.
|
|||
/// </summary>
|
|||
/// <param name="visual">The visual that this node represents.</param>
|
|||
/// <param name="parent">The parent scene graph node, if any.</param>
|
|||
public VisualNode(Visual visual, IVisualNode? parent) |
|||
{ |
|||
Visual = visual ?? throw new ArgumentNullException(nameof(visual)); |
|||
Parent = parent; |
|||
HasAncestorGeometryClip = parent != null && |
|||
(parent.HasAncestorGeometryClip || parent.GeometryClip != null); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Visual Visual { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public IVisualNode? Parent { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public CornerRadius ClipToBoundsRadius { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public Matrix Transform { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public Rect Bounds => _bounds ?? CalculateBounds(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public Rect ClipBounds { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public Rect LayoutBounds { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool ClipToBounds { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public IGeometryImpl? GeometryClip { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool HasAncestorGeometryClip { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public double Opacity |
|||
{ |
|||
get { return _opacity; } |
|||
set |
|||
{ |
|||
if (_opacity != value) |
|||
{ |
|||
_opacity = value; |
|||
OpacityChanged = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the opacity mask for the scene graph node.
|
|||
/// </summary>
|
|||
public IBrush? OpacityMask { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether this node in the scene graph has already
|
|||
/// been updated in the current update pass.
|
|||
/// </summary>
|
|||
public bool SubTreeUpdated { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the <see cref="Opacity"/> property has changed.
|
|||
/// </summary>
|
|||
public bool OpacityChanged { get; private set; } |
|||
|
|||
public Visual? LayerRoot { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public IReadOnlyList<IVisualNode> Children => _children ?? EmptyChildren; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IReadOnlyList<IRef<IDrawOperation>> DrawOperations => _drawOperations ?? EmptyDrawOperations; |
|||
|
|||
/// <summary>
|
|||
/// Adds a child to the <see cref="Children"/> collection.
|
|||
/// </summary>
|
|||
/// <param name="child">The child to add.</param>
|
|||
public void AddChild(IVisualNode child) |
|||
{ |
|||
if (child.Disposed) |
|||
{ |
|||
throw new ObjectDisposedException("Visual node for {node.Visual}"); |
|||
} |
|||
|
|||
if (child.Parent != this) |
|||
{ |
|||
throw new AvaloniaInternalException("VisualNode added to wrong parent."); |
|||
} |
|||
|
|||
EnsureChildrenCreated(); |
|||
_children.Add(child); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds an operation to the <see cref="DrawOperations"/> collection.
|
|||
/// </summary>
|
|||
/// <param name="operation">The operation to add.</param>
|
|||
public void AddDrawOperation(IRef<IDrawOperation> operation) |
|||
{ |
|||
EnsureDrawOperationsCreated(); |
|||
_drawOperations.Add(operation.Clone()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes a child from the <see cref="Children"/> collection.
|
|||
/// </summary>
|
|||
/// <param name="child">The child to remove.</param>
|
|||
public void RemoveChild(IVisualNode child) |
|||
{ |
|||
EnsureChildrenCreated(); |
|||
_children.Remove(child); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Replaces a child in the <see cref="Children"/> collection.
|
|||
/// </summary>
|
|||
/// <param name="index">The child to be replaced.</param>
|
|||
/// <param name="node">The child to add.</param>
|
|||
public void ReplaceChild(int index, IVisualNode node) |
|||
{ |
|||
if (node.Disposed) |
|||
{ |
|||
throw new ObjectDisposedException("Visual node for {node.Visual}"); |
|||
} |
|||
|
|||
if (node.Parent != this) |
|||
{ |
|||
throw new AvaloniaInternalException("VisualNode added to wrong parent."); |
|||
} |
|||
|
|||
EnsureChildrenCreated(); |
|||
_children[index] = node; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Replaces an item in the <see cref="DrawOperations"/> collection.
|
|||
/// </summary>
|
|||
/// <param name="index">The operation to be replaced.</param>
|
|||
/// <param name="operation">The operation to add.</param>
|
|||
public void ReplaceDrawOperation(int index, IRef<IDrawOperation> operation) |
|||
{ |
|||
EnsureDrawOperationsCreated(); |
|||
var old = _drawOperations[index]; |
|||
_drawOperations[index] = operation.Clone(); |
|||
old.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sorts the <see cref="Children"/> collection according to the order of the visual's
|
|||
/// children and their z-index.
|
|||
/// </summary>
|
|||
/// <param name="scene">The scene that the node is a part of.</param>
|
|||
public void SortChildren(Scene scene) |
|||
{ |
|||
if (_children == null || _children.Count <= 1) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var keys = new List<long>(Visual.VisualChildren.Count); |
|||
|
|||
for (var i = 0; i < Visual.VisualChildren.Count; ++i) |
|||
{ |
|||
var child = Visual.VisualChildren[i]; |
|||
var zIndex = child.ZIndex; |
|||
keys.Add(((long)zIndex << 32) + i); |
|||
} |
|||
|
|||
keys.Sort(); |
|||
_children.Clear(); |
|||
|
|||
foreach (var i in keys) |
|||
{ |
|||
var child = Visual.VisualChildren[(int)(i & 0xffffffff)]; |
|||
var node = scene.FindNode(child); |
|||
|
|||
if (node != null) |
|||
{ |
|||
_children.Add(node); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes items in the <see cref="Children"/> collection from the specified index
|
|||
/// to the end.
|
|||
/// </summary>
|
|||
/// <param name="first">The index of the first child to be removed.</param>
|
|||
public void TrimChildren(int first) |
|||
{ |
|||
if (first < _children?.Count) |
|||
{ |
|||
EnsureChildrenCreated(); |
|||
for (int i = first; i < _children.Count; i++) |
|||
{ |
|||
_children[i].Dispose(); |
|||
} |
|||
_children.RemoveRange(first, _children.Count - first); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes items in the <see cref="DrawOperations"/> collection from the specified index
|
|||
/// to the end.
|
|||
/// </summary>
|
|||
/// <param name="first">The index of the first operation to be removed.</param>
|
|||
public void TrimDrawOperations(int first) |
|||
{ |
|||
if (first < _drawOperations?.Count) |
|||
{ |
|||
EnsureDrawOperationsCreated(); |
|||
for (int i = first; i < _drawOperations.Count; i++) |
|||
{ |
|||
_drawOperations[i].Dispose(); |
|||
} |
|||
_drawOperations.RemoveRange(first, _drawOperations.Count - first); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Makes a copy of the node
|
|||
/// </summary>
|
|||
/// <param name="parent">The new parent node.</param>
|
|||
/// <returns>A cloned node.</returns>
|
|||
public VisualNode Clone(IVisualNode? parent) |
|||
{ |
|||
return new VisualNode(Visual, parent) |
|||
{ |
|||
Transform = Transform, |
|||
ClipBounds = ClipBounds, |
|||
ClipToBoundsRadius = ClipToBoundsRadius, |
|||
ClipToBounds = ClipToBounds, |
|||
LayoutBounds = LayoutBounds, |
|||
GeometryClip = GeometryClip, |
|||
_opacity = Opacity, |
|||
OpacityMask = OpacityMask, |
|||
_drawOperations = _drawOperations, |
|||
_drawOperationsRefCounter = _drawOperationsRefCounter?.Clone(), |
|||
_drawOperationsCloned = true, |
|||
LayerRoot= LayerRoot, |
|||
}; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool HitTest(Point p) |
|||
{ |
|||
var drawOperations = DrawOperations; |
|||
var drawOperationsCount = drawOperations.Count; |
|||
|
|||
for (var i = 0; i < drawOperationsCount; i++) |
|||
{ |
|||
var operation = drawOperations[i]; |
|||
|
|||
if (operation?.Item?.HitTest(p) == true) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void BeginRender(IDrawingContextImpl context, bool skipOpacity) |
|||
{ |
|||
transformRestore = context.Transform; |
|||
|
|||
if (ClipToBounds) |
|||
{ |
|||
context.Transform = Matrix.Identity; |
|||
if (ClipToBoundsRadius.IsDefault) |
|||
context.PushClip(ClipBounds); |
|||
else |
|||
context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius)); |
|||
} |
|||
|
|||
context.Transform = Transform; |
|||
|
|||
if (Opacity != 1 && !skipOpacity) |
|||
{ |
|||
context.PushOpacity(Opacity); |
|||
} |
|||
|
|||
if (GeometryClip != null) |
|||
{ |
|||
context.PushGeometryClip(GeometryClip); |
|||
} |
|||
|
|||
if (OpacityMask != null) |
|||
{ |
|||
context.PushOpacityMask(OpacityMask, LayoutBounds); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void EndRender(IDrawingContextImpl context, bool skipOpacity) |
|||
{ |
|||
if (OpacityMask != null) |
|||
{ |
|||
context.PopOpacityMask(); |
|||
} |
|||
|
|||
if (GeometryClip != null) |
|||
{ |
|||
context.PopGeometryClip(); |
|||
} |
|||
|
|||
if (Opacity != 1 && !skipOpacity) |
|||
{ |
|||
context.PopOpacity(); |
|||
} |
|||
|
|||
if (ClipToBounds) |
|||
{ |
|||
context.Transform = Matrix.Identity; |
|||
context.PopClip(); |
|||
} |
|||
|
|||
context.Transform = transformRestore; |
|||
} |
|||
|
|||
internal void TryPreallocateChildren(int count) |
|||
{ |
|||
if (count == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
EnsureChildrenCreated(count); |
|||
} |
|||
|
|||
private Rect CalculateBounds() |
|||
{ |
|||
var result = new Rect(); |
|||
|
|||
if (_drawOperations != null) |
|||
{ |
|||
foreach (var operation in _drawOperations) |
|||
{ |
|||
result = result.Union(operation.Item.Bounds); |
|||
} |
|||
} |
|||
|
|||
_bounds = result; |
|||
return result; |
|||
} |
|||
|
|||
[MemberNotNull(nameof(_children))] |
|||
private void EnsureChildrenCreated(int capacity = 0) |
|||
{ |
|||
if (_children == null) |
|||
{ |
|||
_children = new List<IVisualNode>(capacity); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Ensures that this node draw operations have been created and are mutable (in case we are using cloned operations).
|
|||
/// </summary>
|
|||
[MemberNotNull(nameof(_drawOperations))] |
|||
private void EnsureDrawOperationsCreated() |
|||
{ |
|||
if (_drawOperations == null) |
|||
{ |
|||
_drawOperations = new List<IRef<IDrawOperation>>(); |
|||
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); |
|||
_drawOperationsCloned = false; |
|||
} |
|||
else if (_drawOperationsCloned) |
|||
{ |
|||
var oldDrawOperations = _drawOperations; |
|||
|
|||
_drawOperations = new List<IRef<IDrawOperation>>(oldDrawOperations.Count); |
|||
|
|||
foreach (var drawOperation in oldDrawOperations) |
|||
{ |
|||
_drawOperations.Add(drawOperation.Clone()); |
|||
} |
|||
|
|||
_drawOperationsRefCounter?.Dispose(); |
|||
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); |
|||
_drawOperationsCloned = false; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates disposable that will dispose all items in passed draw operations after being disposed.
|
|||
/// It is crucial that we don't capture current <see cref="VisualNode"/> instance
|
|||
/// as draw operations can be cloned and may persist across subsequent scenes.
|
|||
/// </summary>
|
|||
/// <param name="drawOperations">Draw operations that need to be disposed.</param>
|
|||
/// <returns>Disposable for given draw operations.</returns>
|
|||
private static IDisposable CreateDisposeDrawOperations(List<IRef<IDrawOperation>> drawOperations) |
|||
{ |
|||
return Disposable.Create(drawOperations, operations => |
|||
{ |
|||
foreach (var operation in operations) |
|||
{ |
|||
operation.Dispose(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public bool Disposed { get; private set; } |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_drawOperationsRefCounter?.Dispose(); |
|||
|
|||
Disposed = true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
|
|||
namespace Avalonia.Utilities; |
|||
|
|||
/// <summary>
|
|||
/// Allows using <see cref="Stopwatch"/> as timestamps without allocating.
|
|||
/// </summary>
|
|||
/// <remarks>Equivalent to Stopwatch.GetElapsedTime in .NET 7.</remarks>
|
|||
internal static class StopwatchHelper |
|||
{ |
|||
private static readonly double s_timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; |
|||
|
|||
public static TimeSpan GetElapsedTime(long startingTimestamp) |
|||
=> GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp()); |
|||
|
|||
public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) |
|||
=> new((long)((endingTimestamp - startingTimestamp) * s_timestampToTicks)); |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Controls.Platform; |
|||
|
|||
[Unstable] |
|||
public interface ITopLevelImplWithStorageProvider : ITopLevelImpl |
|||
{ |
|||
public IStorageProvider StorageProvider { get; } |
|||
} |
|||
@ -1,13 +0,0 @@ |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.TextInput; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Controls.Platform |
|||
{ |
|||
[Unstable] |
|||
public interface ITopLevelImplWithTextInputMethod : ITopLevelImpl |
|||
{ |
|||
public ITextInputMethodImpl? TextInputMethod { get; } |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue