36 changed files with 867 additions and 320 deletions
@ -0,0 +1,66 @@ |
|||||
|
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 MaxHeight { get; } |
||||
|
|
||||
|
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); |
||||
|
var run = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph }); |
||||
|
_runs[index] = run; |
||||
|
MaxHeight = Math.Max(run.Size.Height, MaxHeight); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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.MaxHeight + 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]; |
||||
|
} |
||||
@ -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>
|
||||
|
public readonly record struct LayoutPassTiming(int PassCounter, TimeSpan Elapsed); |
||||
|
} |
||||
@ -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>
|
||||
|
public 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); |
||||
|
} |
||||
|
} |
||||
@ -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,50 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using Avalonia.Rendering; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Benchmarks |
|
||||
{ |
|
||||
internal class NullRenderer : IRenderer |
|
||||
{ |
|
||||
public bool DrawFps { get; set; } |
|
||||
public bool DrawDirtyRects { get; set; } |
|
||||
#pragma warning disable CS0067
|
|
||||
public event EventHandler<SceneInvalidatedEventArgs> SceneInvalidated; |
|
||||
#pragma warning restore CS0067
|
|
||||
public void AddDirty(Visual visual) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool> filter) => null; |
|
||||
|
|
||||
public Visual HitTestFirst(Point p, Visual root, Func<Visual, bool> filter) => null; |
|
||||
|
|
||||
public void Paint(Rect rect) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void RecalculateChildren(Visual visual) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void Resized(Size size) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void Start() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void Stop() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public ValueTask<object> TryGetRenderInterfaceFeature(Type featureType) => new(0); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,57 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Rendering; |
||||
|
|
||||
|
namespace Avalonia.UnitTests; |
||||
|
|
||||
|
public sealed class NullRenderer : IRenderer |
||||
|
{ |
||||
|
public RendererDiagnostics Diagnostics { get; } = new(); |
||||
|
|
||||
|
public event EventHandler<SceneInvalidatedEventArgs>? SceneInvalidated; |
||||
|
|
||||
|
public NullRenderer() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void AddDirty(Visual visual) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool> filter) |
||||
|
=> Enumerable.Empty<Visual>(); |
||||
|
|
||||
|
public Visual? HitTestFirst(Point p, Visual root, Func<Visual, bool> filter) |
||||
|
=> null; |
||||
|
|
||||
|
public void Paint(Rect rect) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void RecalculateChildren(Visual visual) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void Resized(Size size) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void Start() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void Stop() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType) |
||||
|
=> new((object?) null); |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
using Avalonia.Rendering; |
||||
|
using Moq; |
||||
|
|
||||
|
namespace Avalonia.UnitTests |
||||
|
{ |
||||
|
public static class RendererMocks |
||||
|
{ |
||||
|
public static Mock<IRenderer> CreateRenderer() |
||||
|
{ |
||||
|
var renderer = new Mock<IRenderer>(); |
||||
|
renderer.SetupGet(r => r.Diagnostics).Returns(new RendererDiagnostics()); |
||||
|
return renderer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue