Browse Source

Implemented layout and render time graph overlays

pull/10141/head
Julien Lebosquain 3 years ago
parent
commit
5e13c5b59a
  1. 5
      samples/GpuInterop/MainWindow.axaml.cs
  2. 14
      samples/RenderDemo/MainWindow.xaml
  3. 23
      samples/RenderDemo/MainWindow.xaml.cs
  4. 45
      samples/RenderDemo/ViewModels/MainWindowViewModel.cs
  5. 20
      src/Avalonia.Base/Layout/LayoutManager.cs
  6. 31
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  7. 66
      src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs
  8. 58
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  9. 176
      src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs
  10. 131
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  11. 11
      src/Avalonia.Base/Rendering/IRenderer.cs
  12. 11
      src/Avalonia.Base/Rendering/LayoutPassTiming.cs
  13. 35
      src/Avalonia.Base/Rendering/RendererDebugOverlays.cs
  14. 57
      src/Avalonia.Base/Rendering/RendererDiagnostics.cs
  15. 19
      src/Avalonia.Base/Utilities/StopwatchHelper.cs
  16. 4
      src/Avalonia.Base/composition-schema.xml
  17. 64
      src/Avalonia.Controls/TopLevel.cs
  18. 109
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  19. 4
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
  20. 24
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  21. 14
      tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
  22. 22
      tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
  23. 8
      tests/Avalonia.Base.UnitTests/VisualTests.cs
  24. 50
      tests/Avalonia.Benchmarks/NullRenderer.cs
  25. 20
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  26. 2
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  27. 3
      tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
  28. 2
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
  29. 2
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  30. 21
      tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
  31. 12
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  32. 43
      tests/Avalonia.LeakTests/ControlTests.cs
  33. 5
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs
  34. 57
      tests/Avalonia.UnitTests/NullRenderer.cs
  35. 15
      tests/Avalonia.UnitTests/RendererMocks.cs
  36. 4
      tests/Avalonia.UnitTests/TestRoot.cs

5
samples/GpuInterop/MainWindow.axaml.cs

@ -1,6 +1,7 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
namespace GpuInterop namespace GpuInterop
{ {
@ -8,9 +9,9 @@ namespace GpuInterop
{ {
public MainWindow() public MainWindow()
{ {
this.InitializeComponent(); InitializeComponent();
this.AttachDevTools(); this.AttachDevTools();
this.Renderer.DrawFps = true; Renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.Fps;
} }
private void InitializeComponent() private void InitializeComponent()

14
samples/RenderDemo/MainWindow.xaml

@ -26,6 +26,20 @@
IsHitTestVisible="False" /> IsHitTestVisible="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Command="{Binding ToggleDrawLayoutTimeGraph}" Header="Draw layout time graph">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding DrawLayoutTimeGraph}"
IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Command="{Binding ToggleDrawRenderTimeGraph}" Header="Draw render time graph">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding DrawRenderTimeGraph}"
IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
</MenuItem> </MenuItem>
<MenuItem Header="Tests"> <MenuItem Header="Tests">
<MenuItem Command="{Binding ResizeWindow}" Header="Resize window" /> <MenuItem Command="{Binding ResizeWindow}" Header="Resize window" />

23
samples/RenderDemo/MainWindow.xaml.cs

@ -1,7 +1,9 @@
using System; using System;
using System.Linq.Expressions;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
using RenderDemo.ViewModels; using RenderDemo.ViewModels;
using MiniMvvm; using MiniMvvm;
@ -11,13 +13,26 @@ namespace RenderDemo
{ {
public MainWindow() public MainWindow()
{ {
this.InitializeComponent(); InitializeComponent();
this.AttachDevTools(); this.AttachDevTools();
var vm = new MainWindowViewModel(); var vm = new MainWindowViewModel();
vm.WhenAnyValue(x => x.DrawDirtyRects).Subscribe(x => Renderer.DrawDirtyRects = x);
vm.WhenAnyValue(x => x.DrawFps).Subscribe(x => Renderer.DrawFps = x); void BindOverlay(Expression<Func<MainWindowViewModel, bool>> expr, RendererDebugOverlays overlay)
this.DataContext = vm; => vm.WhenAnyValue(expr).Subscribe(x =>
{
var diagnostics = Renderer.Diagnostics;
diagnostics.DebugOverlays = x ?
diagnostics.DebugOverlays | overlay :
diagnostics.DebugOverlays & ~overlay;
});
BindOverlay(x => x.DrawDirtyRects, RendererDebugOverlays.DirtyRects);
BindOverlay(x => x.DrawFps, RendererDebugOverlays.Fps);
BindOverlay(x => x.DrawLayoutTimeGraph, RendererDebugOverlays.LayoutTimeGraph);
BindOverlay(x => x.DrawRenderTimeGraph, RendererDebugOverlays.RenderTimeGraph);
DataContext = vm;
} }
private void InitializeComponent() private void InitializeComponent()

45
samples/RenderDemo/ViewModels/MainWindowViewModel.cs

@ -1,49 +1,66 @@
using System.Reactive; using System.Threading.Tasks;
using System.Threading.Tasks;
using MiniMvvm; using MiniMvvm;
namespace RenderDemo.ViewModels namespace RenderDemo.ViewModels
{ {
public class MainWindowViewModel : ViewModelBase public class MainWindowViewModel : ViewModelBase
{ {
private bool drawDirtyRects = false; private bool _drawDirtyRects;
private bool drawFps = true; private bool _drawFps = true;
private double width = 800; private bool _drawLayoutTimeGraph;
private double height = 600; private bool _drawRenderTimeGraph;
private double _width = 800;
private double _height = 600;
public MainWindowViewModel() public MainWindowViewModel()
{ {
ToggleDrawDirtyRects = MiniCommand.Create(() => DrawDirtyRects = !DrawDirtyRects); ToggleDrawDirtyRects = MiniCommand.Create(() => DrawDirtyRects = !DrawDirtyRects);
ToggleDrawFps = MiniCommand.Create(() => DrawFps = !DrawFps); ToggleDrawFps = MiniCommand.Create(() => DrawFps = !DrawFps);
ToggleDrawLayoutTimeGraph = MiniCommand.Create(() => DrawLayoutTimeGraph = !DrawLayoutTimeGraph);
ToggleDrawRenderTimeGraph = MiniCommand.Create(() => DrawRenderTimeGraph = !DrawRenderTimeGraph);
ResizeWindow = MiniCommand.CreateFromTask(ResizeWindowAsync); ResizeWindow = MiniCommand.CreateFromTask(ResizeWindowAsync);
} }
public bool DrawDirtyRects public bool DrawDirtyRects
{ {
get => drawDirtyRects; get => _drawDirtyRects;
set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value); set => RaiseAndSetIfChanged(ref _drawDirtyRects, value);
} }
public bool DrawFps public bool DrawFps
{ {
get => drawFps; get => _drawFps;
set => this.RaiseAndSetIfChanged(ref drawFps, value); set => RaiseAndSetIfChanged(ref _drawFps, value);
}
public bool DrawLayoutTimeGraph
{
get => _drawLayoutTimeGraph;
set => RaiseAndSetIfChanged(ref _drawLayoutTimeGraph, value);
}
public bool DrawRenderTimeGraph
{
get => _drawRenderTimeGraph;
set => RaiseAndSetIfChanged(ref _drawRenderTimeGraph, value);
} }
public double Width public double Width
{ {
get => width; get => _width;
set => this.RaiseAndSetIfChanged(ref width, value); set => RaiseAndSetIfChanged(ref _width, value);
} }
public double Height public double Height
{ {
get => height; get => _height;
set => this.RaiseAndSetIfChanged(ref height, value); set => RaiseAndSetIfChanged(ref _height, value);
} }
public MiniCommand ToggleDrawDirtyRects { get; } public MiniCommand ToggleDrawDirtyRects { get; }
public MiniCommand ToggleDrawFps { get; } public MiniCommand ToggleDrawFps { get; }
public MiniCommand ToggleDrawLayoutTimeGraph { get; }
public MiniCommand ToggleDrawRenderTimeGraph { get; }
public MiniCommand ResizeWindow { get; } public MiniCommand ResizeWindow { get; }
private async Task ResizeWindowAsync() private async Task ResizeWindowAsync()

20
src/Avalonia.Base/Layout/LayoutManager.cs

@ -3,8 +3,9 @@ using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Rendering;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.Utilities;
#nullable enable #nullable enable
@ -24,6 +25,7 @@ namespace Avalonia.Layout
private bool _disposed; private bool _disposed;
private bool _queued; private bool _queued;
private bool _running; private bool _running;
private int _totalPassCount;
public LayoutManager(ILayoutRoot owner) public LayoutManager(ILayoutRoot owner)
{ {
@ -33,6 +35,8 @@ namespace Avalonia.Layout
public virtual event EventHandler? LayoutUpdated; public virtual event EventHandler? LayoutUpdated;
internal Action<LayoutPassTiming>? LayoutPassTimed { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public virtual void InvalidateMeasure(Layoutable control) public virtual void InvalidateMeasure(Layoutable control)
{ {
@ -116,10 +120,9 @@ namespace Avalonia.Layout
if (!_running) if (!_running)
{ {
Stopwatch? stopwatch = null;
const LogEventLevel timingLogLevel = LogEventLevel.Information; const LogEventLevel timingLogLevel = LogEventLevel.Information;
bool captureTiming = Logger.IsEnabled(timingLogLevel, LogArea.Layout); var captureTiming = LayoutPassTimed is not null || Logger.IsEnabled(timingLogLevel, LogArea.Layout);
var startingTimestamp = 0L;
if (captureTiming) if (captureTiming)
{ {
@ -129,8 +132,7 @@ namespace Avalonia.Layout
_toMeasure.Count, _toMeasure.Count,
_toArrange.Count); _toArrange.Count);
stopwatch = new Stopwatch(); startingTimestamp = Stopwatch.GetTimestamp();
stopwatch.Start();
} }
_toMeasure.BeginLoop(MaxPasses); _toMeasure.BeginLoop(MaxPasses);
@ -139,6 +141,7 @@ namespace Avalonia.Layout
try try
{ {
_running = true; _running = true;
++_totalPassCount;
for (var pass = 0; pass < MaxPasses; ++pass) for (var pass = 0; pass < MaxPasses; ++pass)
{ {
@ -160,9 +163,10 @@ namespace Avalonia.Layout
if (captureTiming) if (captureTiming)
{ {
stopwatch!.Stop(); var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp);
LayoutPassTimed?.Invoke(new LayoutPassTiming(_totalPassCount, elapsed));
Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", stopwatch.Elapsed); Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", elapsed);
} }
} }

31
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -1,15 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Collections.Pooled; using Avalonia.Collections.Pooled;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see> // Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -38,6 +35,9 @@ public class CompositingRenderer : IRendererWithCompositor
/// </summary> /// </summary>
public bool RenderOnlyOnRenderThread { get; set; } = true; public bool RenderOnlyOnRenderThread { get; set; } = true;
/// <inheritdoc/>
public RendererDiagnostics Diagnostics { get; }
public CompositingRenderer(IRenderRoot root, Compositor compositor, Func<IEnumerable<object>> surfaces) public CompositingRenderer(IRenderRoot root, Compositor compositor, Func<IEnumerable<object>> surfaces)
{ {
_root = root; _root = root;
@ -46,20 +46,21 @@ public class CompositingRenderer : IRendererWithCompositor
CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget = compositor.CreateCompositionTarget(surfaces);
CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor);
_update = Update; _update = Update;
Diagnostics = new RendererDiagnostics();
Diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged;
} }
/// <inheritdoc/> private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e)
public bool DrawFps
{ {
get => CompositionTarget.DrawFps; switch (e.PropertyName)
set => CompositionTarget.DrawFps = value; {
} case nameof(RendererDiagnostics.DebugOverlays):
CompositionTarget.DebugOverlays = Diagnostics.DebugOverlays;
/// <inheritdoc/> break;
public bool DrawDirtyRects case nameof(RendererDiagnostics.LastLayoutPassTiming):
{ CompositionTarget.LastLayoutPassTiming = Diagnostics.LastLayoutPassTiming;
get => CompositionTarget.DrawDirtyRects; break;
set => CompositionTarget.DrawDirtyRects = value; }
} }
/// <inheritdoc/> /// <inheritdoc/>

66
src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs

@ -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;
}
}
}

58
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@ -1,11 +1,8 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see> // Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -17,26 +14,18 @@ namespace Avalonia.Rendering.Composition.Server;
internal class FpsCounter internal class FpsCounter
{ {
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private readonly DiagnosticTextRenderer _textRenderer;
private int _framesThisSecond; private int _framesThisSecond;
private int _totalFrames; private int _totalFrames;
private int _fps; private int _fps;
private TimeSpan _lastFpsUpdate; private TimeSpan _lastFpsUpdate;
const int FirstChar = 32;
const int LastChar = 126;
// ASCII chars
private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1];
public FpsCounter(IGlyphTypeface typeface)
{
for (var c = FirstChar; c <= LastChar; c++)
{
var s = new string((char)c, 1);
var glyph = typeface.GetGlyph((uint)(s[0]));
_runs[c - FirstChar] = new GlyphRun(typeface, 18, s.AsMemory(), new ushort[] { glyph });
}
}
public void FpsTick() => _framesThisSecond++; public FpsCounter(DiagnosticTextRenderer textRenderer)
=> _textRenderer = textRenderer;
public void FpsTick()
=> _framesThisSecond++;
public void RenderFps(IDrawingContextImpl context, string aux) public void RenderFps(IDrawingContextImpl context, string aux)
{ {
@ -53,27 +42,24 @@ internal class FpsCounter
_lastFpsUpdate = now; _lastFpsUpdate = now;
} }
var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} ") + aux; #if NET6_0_OR_GREATER
double width = 0; var fpsLine = string.Create(CultureInfo.InvariantCulture, $"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}");
double height = 0; #else
foreach (var ch in fpsLine) var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}");
{ #endif
var run = _runs[ch - FirstChar];
width += run.Size.Width;
height = Math.Max(height, run.Size.Height);
}
var rect = new Rect(0, 0, width + 3, height + 3); var size = _textRenderer.MeasureAsciiText(fpsLine.AsSpan());
var rect = new Rect(0.0, 0.0, size.Width + 3.0, size.Height + 3.0);
context.DrawRectangle(Brushes.Black, null, rect); context.DrawRectangle(Brushes.Black, null, rect);
double offset = 0; _textRenderer.DrawAsciiText(context, fpsLine.AsSpan(), Brushes.White);
foreach (var ch in fpsLine) }
{
var run = _runs[ch - FirstChar]; public void Reset()
context.Transform = Matrix.CreateTranslation(offset, 0); {
context.DrawGlyphRun(Brushes.White, run.PlatformImpl); _framesThisSecond = 0;
offset += run.Size.Width; _totalFrames = 0;
} _fps = 0;
} }
} }

176
src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs

@ -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];
}

131
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -1,6 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Diagnostics;
using System.Threading; using System.Threading;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
@ -22,10 +22,11 @@ namespace Avalonia.Rendering.Composition.Server
private readonly ServerCompositor _compositor; private readonly ServerCompositor _compositor;
private readonly Func<IEnumerable<object>> _surfaces; private readonly Func<IEnumerable<object>> _surfaces;
private static long s_nextId = 1; private static long s_nextId = 1;
public long Id { get; }
public ulong Revision { get; private set; }
private IRenderTarget? _renderTarget; private IRenderTarget? _renderTarget;
private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); private DiagnosticTextRenderer? _diagnosticTextRenderer;
private FpsCounter? _fpsCounter;
private FrameTimeGraph? _renderTimeGraph;
private FrameTimeGraph? _layoutTimeGraph;
private Rect _dirtyRect; private Rect _dirtyRect;
private Random _random = new(); private Random _random = new();
private Size _layerSize; private Size _layerSize;
@ -35,10 +36,24 @@ namespace Avalonia.Rendering.Composition.Server
private HashSet<ServerCompositionVisual> _attachedVisuals = new(); private HashSet<ServerCompositionVisual> _attachedVisuals = new();
private Queue<ServerCompositionVisual> _adornerUpdateQueue = new(); private Queue<ServerCompositionVisual> _adornerUpdateQueue = new();
public long Id { get; }
public ulong Revision { get; private set; }
public ICompositionTargetDebugEvents? DebugEvents { get; set; } public ICompositionTargetDebugEvents? DebugEvents { get; set; }
public ReadbackIndices Readback { get; } = new(); public ReadbackIndices Readback { get; } = new();
public int RenderedVisuals { get; set; } public int RenderedVisuals { get; set; }
private DiagnosticTextRenderer DiagnosticTextRenderer
=> _diagnosticTextRenderer ??= new DiagnosticTextRenderer(Typeface.Default.GlyphTypeface, 12.0);
private FpsCounter FpsCounter
=> _fpsCounter ??= new FpsCounter(DiagnosticTextRenderer);
private FrameTimeGraph LayoutTimeGraph
=> _layoutTimeGraph ??= CreateTimeGraph("Layout");
private FrameTimeGraph RenderTimeGraph
=> _renderTimeGraph ??= CreateTimeGraph("Render");
public ServerCompositionTarget(ServerCompositor compositor, Func<IEnumerable<object>> surfaces) : public ServerCompositionTarget(ServerCompositor compositor, Func<IEnumerable<object>> surfaces) :
base(compositor) base(compositor)
{ {
@ -47,6 +62,9 @@ namespace Avalonia.Rendering.Composition.Server
Id = Interlocked.Increment(ref s_nextId); Id = Interlocked.Increment(ref s_nextId);
} }
private FrameTimeGraph CreateTimeGraph(string title)
=> new(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, DiagnosticTextRenderer);
partial void OnIsEnabledChanged() partial void OnIsEnabledChanged()
{ {
if (IsEnabled) if (IsEnabled)
@ -62,7 +80,33 @@ namespace Avalonia.Rendering.Composition.Server
v.Deactivate(); v.Deactivate();
} }
} }
partial void OnDebugOverlaysChanged()
{
if ((DebugOverlays & RendererDebugOverlays.Fps) == 0)
{
_fpsCounter?.Reset();
}
if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) == 0)
{
_layoutTimeGraph?.Reset();
}
if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) == 0)
{
_renderTimeGraph?.Reset();
}
}
partial void OnLastLayoutPassTimingChanged()
{
if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0)
{
LayoutTimeGraph.AddFrameValue(LastLayoutPassTiming.Elapsed.TotalMilliseconds);
}
}
partial void DeserializeChangesExtra(BatchStreamReader c) partial void DeserializeChangesExtra(BatchStreamReader c)
{ {
_redrawRequested = true; _redrawRequested = true;
@ -92,7 +136,10 @@ namespace Avalonia.Rendering.Composition.Server
return; return;
Revision++; Revision++;
var captureTiming = (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0;
var startingTimestamp = captureTiming ? Stopwatch.GetTimestamp() : 0L;
// Update happens in a separate phase to extend dirty rect if needed // Update happens in a separate phase to extend dirty rect if needed
Root.Update(this); Root.Update(this);
@ -137,33 +184,69 @@ namespace Avalonia.Rendering.Composition.Server
targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1,
new Rect(_layerSize), new Rect(_layerSize),
new Rect(Size), BitmapInterpolationMode.LowQuality); new Rect(Size), BitmapInterpolationMode.LowQuality);
if (DrawDirtyRects)
{
targetContext.DrawRectangle(new ImmutableSolidColorBrush(
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255),
(byte)_random.Next(255)))
, null, _dirtyRect);
}
if (DrawFps) if (DebugOverlays != RendererDebugOverlays.None)
{ {
var nativeMem = ByteSizeHelper.ToString((ulong)( if (captureTiming)
(Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) * {
Compositor.BatchMemoryPool.BufferSize), false); var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp);
var managedMem = ByteSizeHelper.ToString((ulong)( RenderTimeGraph.AddFrameValue(elapsed.TotalMilliseconds);
(Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * }
Compositor.BatchObjectPool.ArraySize *
IntPtr.Size), false); DrawOverlays(targetContext, layerSize);
_fpsCounter.RenderFps(targetContext, FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}"));
} }
RenderedVisuals = 0; RenderedVisuals = 0;
_dirtyRect = default; _dirtyRect = default;
} }
} }
private void DrawOverlays(IDrawingContextImpl targetContext, Size layerSize)
{
if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0)
{
targetContext.DrawRectangle(
new ImmutableSolidColorBrush(
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null,
_dirtyRect);
}
if ((DebugOverlays & RendererDebugOverlays.Fps) != 0)
{
var nativeMem = ByteSizeHelper.ToString((ulong) (
(Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) *
Compositor.BatchMemoryPool.BufferSize), false);
var managedMem = ByteSizeHelper.ToString((ulong) (
(Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) *
Compositor.BatchObjectPool.ArraySize *
IntPtr.Size), false);
FpsCounter.RenderFps(targetContext,
FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}"));
}
var top = 0.0;
void DrawTimeGraph(FrameTimeGraph graph)
{
top += 8.0;
targetContext.Transform = Matrix.CreateTranslation(layerSize.Width - graph.Size.Width - 8.0, top);
graph.Render(targetContext);
top += graph.Size.Height;
}
if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0)
{
DrawTimeGraph(LayoutTimeGraph);
}
if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0)
{
DrawTimeGraph(RenderTimeGraph);
}
}
public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling); public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling);
private static Rect SnapToDevicePixels(Rect rect, double scale) private static Rect SnapToDevicePixels(Rect rect, double scale)

11
src/Avalonia.Base/Rendering/IRenderer.cs

@ -1,5 +1,4 @@
using System; using System;
using Avalonia.VisualTree;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
@ -12,15 +11,9 @@ namespace Avalonia.Rendering
public interface IRenderer : IDisposable public interface IRenderer : IDisposable
{ {
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the renderer should draw an FPS counter. /// Gets a value indicating whether the renderer should draw specific diagnostics.
/// </summary> /// </summary>
bool DrawFps { get; set; } RendererDiagnostics Diagnostics { get; }
/// <summary>
/// Gets or sets a value indicating whether the renderer should draw a visual representation
/// of its dirty rectangles.
/// </summary>
bool DrawDirtyRects { get; set; }
/// <summary> /// <summary>
/// Raised when a portion of the scene has been invalidated. /// Raised when a portion of the scene has been invalidated.

11
src/Avalonia.Base/Rendering/LayoutPassTiming.cs

@ -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);
}

35
src/Avalonia.Base/Rendering/RendererDebugOverlays.cs

@ -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
}

57
src/Avalonia.Base/Rendering/RendererDiagnostics.cs

@ -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);
}
}

19
src/Avalonia.Base/Utilities/StopwatchHelper.cs

@ -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));
}

4
src/Avalonia.Base/composition-schema.xml

@ -39,8 +39,8 @@
<Object Name="CompositionTarget" CustomServerCtor="true"> <Object Name="CompositionTarget" CustomServerCtor="true">
<Property Name="Root" Type="CompositionVisual?"/> <Property Name="Root" Type="CompositionVisual?"/>
<Property Name="IsEnabled" Type="bool"/> <Property Name="IsEnabled" Type="bool"/>
<Property Name="DrawDirtyRects" Type="bool"/> <Property Name="DebugOverlays" Type="RendererDebugOverlays"/>
<Property Name="DrawFps" Type="bool"/> <Property Name="LastLayoutPassTiming" Type="LayoutPassTiming"/>
<Property Name="Scaling" Type="double"/> <Property Name="Scaling" Type="double"/>
<Property Name="Size" Type="Size" /> <Property Name="Size" Type="Size" />
</Object> </Object>

64
src/Avalonia.Controls/TopLevel.cs

@ -1,7 +1,7 @@
using System; using System;
using System.ComponentModel;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
@ -17,7 +17,6 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using System.Linq; using System.Linq;
@ -106,6 +105,7 @@ namespace Avalonia.Controls
private Border? _transparencyFallbackBorder; private Border? _transparencyFallbackBorder;
private TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>? _resourcesChangesSubscriber; private TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>? _resourcesChangesSubscriber;
private IStorageProvider? _storageProvider; private IStorageProvider? _storageProvider;
private LayoutDiagnosticBridge? _layoutDiagnosticBridge;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TopLevel"/> class. /// Initializes static members of the <see cref="TopLevel"/> class.
@ -194,7 +194,7 @@ namespace Avalonia.Controls
ClientSize = impl.ClientSize; ClientSize = impl.ClientSize;
FrameSize = impl.FrameSize; FrameSize = impl.FrameSize;
this.GetObservable(PointerOverElementProperty) this.GetObservable(PointerOverElementProperty)
.Select( .Select(
x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>()) x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>())
@ -328,8 +328,17 @@ namespace Avalonia.Controls
{ {
get get
{ {
if (_layoutManager == null) if (_layoutManager is null)
{
_layoutManager = CreateLayoutManager(); _layoutManager = CreateLayoutManager();
if (_layoutManager is LayoutManager typedLayoutManager && Renderer is not null)
{
_layoutDiagnosticBridge = new LayoutDiagnosticBridge(Renderer.Diagnostics, typedLayoutManager);
_layoutDiagnosticBridge.SetupBridge();
}
}
return _layoutManager; return _layoutManager;
} }
} }
@ -435,6 +444,9 @@ namespace Avalonia.Controls
Renderer?.Dispose(); Renderer?.Dispose();
Renderer = null!; Renderer = null!;
_layoutDiagnosticBridge?.Dispose();
_layoutDiagnosticBridge = null;
_pointerOverPreProcessor?.OnCompleted(); _pointerOverPreProcessor?.OnCompleted();
_pointerOverPreProcessorSubscription?.Dispose(); _pointerOverPreProcessorSubscription?.Dispose();
_backGestureSubscription?.Dispose(); _backGestureSubscription?.Dispose();
@ -617,5 +629,49 @@ namespace Avalonia.Controls
} }
ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => PlatformImpl?.TryGetFeature<ITextInputMethodImpl>(); ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => PlatformImpl?.TryGetFeature<ITextInputMethodImpl>();
/// <summary>
/// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes.
/// </summary>
private sealed class LayoutDiagnosticBridge : IDisposable
{
private readonly RendererDiagnostics _diagnostics;
private readonly LayoutManager _layoutManager;
private bool _isHandling;
public LayoutDiagnosticBridge(RendererDiagnostics diagnostics, LayoutManager layoutManager)
{
_diagnostics = diagnostics;
_layoutManager = layoutManager;
diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged;
}
public void SetupBridge()
{
var needsHandling = (_diagnostics.DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0;
if (needsHandling != _isHandling)
{
_isHandling = needsHandling;
_layoutManager.LayoutPassTimed = needsHandling
? timing => _diagnostics.LastLayoutPassTiming = timing
: null;
}
}
private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(RendererDiagnostics.DebugOverlays))
{
SetupBridge();
}
}
public void Dispose()
{
_diagnostics.PropertyChanged -= OnDiagnosticsPropertyChanged;
_layoutManager.LayoutPassTimed = null;
}
}
} }
} }

109
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -1,11 +1,13 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics.Models; using Avalonia.Diagnostics.Models;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Rendering;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
@ -21,8 +23,6 @@ namespace Avalonia.Diagnostics.ViewModels
private string? _focusedControl; private string? _focusedControl;
private IInputElement? _pointerOverElement; private IInputElement? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
private bool _freezePopups; private bool _freezePopups;
private string? _pointerOverElementName; private string? _pointerOverElementName;
private IInputRoot? _pointerOverRoot; private IInputRoot? _pointerOverRoot;
@ -75,69 +75,76 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
} }
public bool ShouldVisualizeDirtyRects public void ToggleVisualizeMarginPadding()
=> ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
private IRenderer? TryGetRenderer()
=> _root switch
{
TopLevel topLevel => topLevel.Renderer,
Controls.Application app => app.RendererRoot,
_ => null
};
private bool GetDebugOverlay(RendererDebugOverlays overlay)
=> ((TryGetRenderer()?.Diagnostics.DebugOverlays ?? RendererDebugOverlays.None) & overlay) != 0;
private void SetDebugOverlay(RendererDebugOverlays overlay, bool enable,
[CallerMemberName] string? propertyName = null)
{ {
get => _shouldVisualizeDirtyRects; if (TryGetRenderer() is not { } renderer)
set
{ {
var changed = true; return;
if (_root is TopLevel topLevel && topLevel.Renderer is { })
{
topLevel.Renderer.DrawDirtyRects = value;
}
else if (_root is Controls.Application app && app.RendererRoot is { })
{
app.RendererRoot.DrawDirtyRects = value;
}
else
{
changed = false;
}
if (changed)
{
RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
}
} }
}
public void ToggleVisualizeDirtyRects() var oldValue = renderer.Diagnostics.DebugOverlays;
{ var newValue = enable ? oldValue | overlay : oldValue & ~overlay;
ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects;
if (oldValue == newValue)
{
return;
}
renderer.Diagnostics.DebugOverlays = newValue;
RaisePropertyChanged(propertyName);
} }
public void ToggleVisualizeMarginPadding() public bool ShowDirtyRectsOverlay
{ {
ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; get => GetDebugOverlay(RendererDebugOverlays.DirtyRects);
set => SetDebugOverlay(RendererDebugOverlays.DirtyRects, value);
} }
public void ToggleDirtyRectsOverlay()
=> ShowDirtyRectsOverlay = !ShowDirtyRectsOverlay;
public bool ShowFpsOverlay public bool ShowFpsOverlay
{ {
get => _showFpsOverlay; get => GetDebugOverlay(RendererDebugOverlays.Fps);
set set => SetDebugOverlay(RendererDebugOverlays.Fps, value);
{
var changed = true;
if (_root is TopLevel topLevel && topLevel.Renderer is { })
{
topLevel.Renderer.DrawFps = value;
}
else if (_root is Controls.Application app && app.RendererRoot is { })
{
app.RendererRoot.DrawFps = value;
}
else
{
changed = false;
}
if(changed)
RaiseAndSetIfChanged(ref _showFpsOverlay, value);
}
} }
public void ToggleFpsOverlay() public void ToggleFpsOverlay()
=> ShowFpsOverlay = !ShowFpsOverlay;
public bool ShowLayoutTimeGraphOverlay
{ {
ShowFpsOverlay = !ShowFpsOverlay; get => GetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph);
set => SetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph, value);
} }
public void ToggleLayoutTimeGraphOverlay()
=> ShowLayoutTimeGraphOverlay = !ShowLayoutTimeGraphOverlay;
public bool ShowRenderTimeGraphOverlay
{
get => GetDebugOverlay(RendererDebugOverlays.RenderTimeGraph);
set => SetDebugOverlay(RendererDebugOverlays.RenderTimeGraph, value);
}
public void ToggleRenderTimeGraphOverlay()
=> ShowRenderTimeGraphOverlay = !ShowRenderTimeGraphOverlay;
public ConsoleViewModel Console { get; } public ConsoleViewModel Console { get; }
public ViewModelBase? Content public ViewModelBase? Content
@ -254,10 +261,10 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose(); _pointerOverSubscription.Dispose();
_logicalTree.Dispose(); _logicalTree.Dispose();
_visualTree.Dispose(); _visualTree.Dispose();
if (_root is TopLevel top)
if (TryGetRenderer() is { } renderer)
{ {
top.Renderer.DrawDirtyRects = false; renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.None;
top.Renderer.DrawFps = false;
} }
} }

4
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs

@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
} }
protected bool RaiseAndSetIfChanged<T>([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!) protected bool RaiseAndSetIfChanged<T>([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string? propertyName = null)
{ {
if (!EqualityComparer<T>.Default.Equals(field, value)) if (!EqualityComparer<T>.Default.Equals(field, value))
{ {
@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels
return false; return false;
} }
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!) protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{ {
var e = new PropertyChangedEventArgs(propertyName); var e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e); OnPropertyChanged(e);

24
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -65,28 +65,42 @@
</MenuItem> </MenuItem>
</MenuItem> </MenuItem>
</MenuItem> </MenuItem>
<MenuItem Header="_Options"> <MenuItem Header="_Overlays">
<MenuItem Header="Visualize margin/padding" Command="{Binding ToggleVisualizeMarginPadding}"> <MenuItem Header="Margin/padding" Command="{Binding ToggleVisualizeMarginPadding}">
<MenuItem.Icon> <MenuItem.Icon>
<CheckBox BorderThickness="0" <CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeMarginPadding}" IsChecked="{Binding ShouldVisualizeMarginPadding}"
IsEnabled="False" /> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Visualize dirty rects" Command="{Binding ToggleVisualizeDirtyRects}"> <MenuItem Header="Dirty rects" Command="{Binding ToggleDirtyRectsOverlay}">
<MenuItem.Icon> <MenuItem.Icon>
<CheckBox BorderThickness="0" <CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeDirtyRects}" IsChecked="{Binding ShowDirtyRectsOverlay}"
IsEnabled="False" /> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Show fps overlay" Command="{Binding ToggleFpsOverlay}"> <MenuItem Header="FPS" Command="{Binding ToggleFpsOverlay}">
<MenuItem.Icon> <MenuItem.Icon>
<CheckBox BorderThickness="0" <CheckBox BorderThickness="0"
IsChecked="{Binding ShowFpsOverlay}" IsChecked="{Binding ShowFpsOverlay}"
IsEnabled="False" /> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Layout time graph" Command="{Binding ToggleLayoutTimeGraphOverlay}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowLayoutTimeGraphOverlay}"
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Render time graph" Command="{Binding ToggleRenderTimeGraphOverlay}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowRenderTimeGraphOverlay}"
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
</MenuItem> </MenuItem>
</Menu> </Menu>

14
tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs

@ -1,15 +1,7 @@
using System; using Avalonia.Controls;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests.Input namespace Avalonia.Base.UnitTests.Input
@ -21,7 +13,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var device = new MouseDevice(); var device = new MouseDevice();
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
@ -59,7 +51,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var device = new MouseDevice(); var device = new MouseDevice();
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);

22
tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs

@ -22,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var device = CreatePointerDeviceMock().Object; var device = CreatePointerDeviceMock().Object;
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
@ -50,7 +50,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var device = CreatePointerDeviceMock().Object; var device = CreatePointerDeviceMock().Object;
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
@ -93,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object; var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object;
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
@ -119,7 +119,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var pointer = new Mock<IPointer>(); var pointer = new Mock<IPointer>();
var device = CreatePointerDeviceMock(pointer.Object).Object; var device = CreatePointerDeviceMock(pointer.Object).Object;
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
@ -155,7 +155,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var device = CreatePointerDeviceMock().Object; var device = CreatePointerDeviceMock().Object;
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
@ -201,7 +201,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock(); var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
var result = new List<(object?, string)>(); var result = new List<(object?, string)>();
@ -256,7 +256,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock(); var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
var result = new List<(object?, string)>(); var result = new List<(object?, string)>();
@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests.Input
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var expectedPosition = new Point(15, 15); var expectedPosition = new Point(15, 15);
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock(); var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
var result = new List<(object?, string, Point)>(); var result = new List<(object?, string, Point)>();
@ -351,7 +351,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock(); var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
@ -405,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock(); var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);
@ -442,7 +442,7 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock(); var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object); var impl = CreateTopLevelImplMock(renderer.Object);

8
tests/Avalonia.Base.UnitTests/VisualTests.cs

@ -150,7 +150,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Attaching_To_Visual_Tree_Should_Invalidate_Visual() public void Attaching_To_Visual_Tree_Should_Invalidate_Visual()
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var child = new Decorator(); var child = new Decorator();
var root = new TestRoot var root = new TestRoot
{ {
@ -165,7 +165,7 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Detaching_From_Visual_Tree_Should_Invalidate_Visual() public void Detaching_From_Visual_Tree_Should_Invalidate_Visual()
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var child = new Decorator(); var child = new Decorator();
var root = new TestRoot var root = new TestRoot
{ {
@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests
public void Changing_ZIndex_Should_InvalidateVisual() public void Changing_ZIndex_Should_InvalidateVisual()
{ {
Canvas canvas1; Canvas canvas1;
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var root = new TestRoot var root = new TestRoot
{ {
Child = new StackPanel Child = new StackPanel
@ -331,7 +331,7 @@ namespace Avalonia.Base.UnitTests
{ {
Canvas canvas1; Canvas canvas1;
StackPanel stackPanel; StackPanel stackPanel;
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var root = new TestRoot var root = new TestRoot
{ {
Child = stackPanel = new StackPanel Child = stackPanel = new StackPanel

50
tests/Avalonia.Benchmarks/NullRenderer.cs

@ -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);
}
}

20
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@ -134,16 +134,16 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Button_Raises_Click() public void Button_Raises_Click()
{ {
var renderer = Mock.Of<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var pt = new Point(50, 50); var pt = new Point(50, 50);
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<Visual>(), It.IsAny<Func<Visual, bool>>())) renderer.Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<Visual>(), It.IsAny<Func<Visual, bool>>()))
.Returns<Point, Visual, Func<Visual, bool>>((p, r, f) => .Returns<Point, Visual, Func<Visual, bool>>((p, r, f) =>
r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]); r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]);
var target = new TestButton() var target = new TestButton()
{ {
Bounds = new Rect(0, 0, 100, 100), Bounds = new Rect(0, 0, 100, 100),
Renderer = renderer Renderer = renderer.Object
}; };
bool clicked = false; bool clicked = false;
@ -166,16 +166,16 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside() public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside()
{ {
var renderer = Mock.Of<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<Visual>(), It.IsAny<Func<Visual, bool>>())) renderer.Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<Visual>(), It.IsAny<Func<Visual, bool>>()))
.Returns<Point, Visual, Func<Visual, bool>>((p, r, f) => .Returns<Point, Visual, Func<Visual, bool>>((p, r, f) =>
r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]); r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]);
var target = new TestButton() var target = new TestButton()
{ {
Bounds = new Rect(0, 0, 100, 100), Bounds = new Rect(0, 0, 100, 100),
Renderer = renderer Renderer = renderer.Object
}; };
bool clicked = false; bool clicked = false;
@ -199,9 +199,9 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Button_With_RenderTransform_Raises_Click() public void Button_With_RenderTransform_Raises_Click()
{ {
var renderer = Mock.Of<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var pt = new Point(150, 50); var pt = new Point(150, 50);
Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<Visual>(), It.IsAny<Func<Visual, bool>>())) renderer.Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<Visual>(), It.IsAny<Func<Visual, bool>>()))
.Returns<Point, Visual, Func<Visual, bool>>((p, r, f) => .Returns<Point, Visual, Func<Visual, bool>>((p, r, f) =>
r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ? r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ?
new Visual[] { r } : new Visual[0]); new Visual[] { r } : new Visual[0]);
@ -210,7 +210,7 @@ namespace Avalonia.Controls.UnitTests
{ {
Bounds = new Rect(0, 0, 100, 100), Bounds = new Rect(0, 0, 100, 100),
RenderTransform = new TranslateTransform { X = 100, Y = 0 }, RenderTransform = new TranslateTransform { X = 100, Y = 0 },
Renderer = renderer Renderer = renderer.Object
}; };
//actual bounds of button should be 100,0,100,100 x -> translated 100 pixels //actual bounds of button should be 100,0,100,100 x -> translated 100 pixels

2
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -595,7 +595,7 @@ namespace Avalonia.Controls.UnitTests
private static Window PreparedWindow(object content = null) private static Window PreparedWindow(object content = null)
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var platform = AvaloniaLocator.Current.GetRequiredService<IWindowingPlatform>(); var platform = AvaloniaLocator.Current.GetRequiredService<IWindowingPlatform>();
var windowImpl = Mock.Get(platform.CreateWindow()); var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object); windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);

3
tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq; using Moq;
using Xunit; using Xunit;
@ -189,6 +190,8 @@ namespace Avalonia.Controls.UnitTests
public void Impl_Closing_Should_Remove_Window_From_OpenWindows() public void Impl_Closing_Should_Remove_Window_From_OpenWindows()
{ {
var windowImpl = new Mock<IWindowImpl>(); var windowImpl = new Mock<IWindowImpl>();
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
.Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.SetupProperty(x => x.Closed); windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);

2
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@ -569,7 +569,7 @@ namespace Avalonia.Controls.UnitTests
private static Window PreparedWindow(object content = null) private static Window PreparedWindow(object content = null)
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var platform = AvaloniaLocator.Current.GetRequiredService<IWindowingPlatform>(); var platform = AvaloniaLocator.Current.GetRequiredService<IWindowingPlatform>();
var windowImpl = Mock.Get(platform.CreateWindow()); var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object); windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);

2
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -563,7 +563,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{ {
using (CreateServices()) using (CreateServices())
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var platform = AvaloniaLocator.Current.GetRequiredService<IWindowingPlatform>(); var platform = AvaloniaLocator.Current.GetRequiredService<IWindowingPlatform>();
var windowImpl = Mock.Get(platform.CreateWindow()); var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object); windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);

21
tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs

@ -110,6 +110,8 @@ namespace Avalonia.Controls.UnitTests
public void IsVisible_Should_Be_False_Atfer_Impl_Signals_Close() public void IsVisible_Should_Be_False_Atfer_Impl_Signals_Close()
{ {
var windowImpl = new Mock<IPopupImpl>(); var windowImpl = new Mock<IPopupImpl>();
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
.Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);
windowImpl.SetupProperty(x => x.Closed); windowImpl.SetupProperty(x => x.Closed);
@ -129,6 +131,8 @@ namespace Avalonia.Controls.UnitTests
public void Setting_IsVisible_True_Shows_Window() public void Setting_IsVisible_True_Shows_Window()
{ {
var windowImpl = new Mock<IPopupImpl>(); var windowImpl = new Mock<IPopupImpl>();
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
.Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);
@ -145,6 +149,8 @@ namespace Avalonia.Controls.UnitTests
public void Setting_IsVisible_False_Hides_Window() public void Setting_IsVisible_False_Hides_Window()
{ {
var windowImpl = new Mock<IPopupImpl>(); var windowImpl = new Mock<IPopupImpl>();
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
.Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);
@ -163,7 +169,7 @@ namespace Avalonia.Controls.UnitTests
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var target = new TestWindowBase(renderer.Object); var target = new TestWindowBase(renderer.Object);
target.Show(); target.Show();
@ -194,7 +200,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var target = new TestWindowBase(renderer.Object); var target = new TestWindowBase(renderer.Object);
target.Show(); target.Show();
@ -209,7 +215,7 @@ namespace Avalonia.Controls.UnitTests
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var windowImpl = new Mock<IPopupImpl>(); var windowImpl = new Mock<IPopupImpl>();
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);
@ -240,12 +246,15 @@ namespace Avalonia.Controls.UnitTests
public bool IsClosed { get; private set; } public bool IsClosed { get; private set; }
public TestWindowBase(IRenderer renderer = null) public TestWindowBase(IRenderer renderer = null)
: base(Mock.Of<IWindowBaseImpl>(x => : base(CreateWindowsBaseImplMock(renderer ?? RendererMocks.CreateRenderer().Object))
x.RenderScaling == 1 &&
x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer))
{ {
} }
private static IWindowBaseImpl CreateWindowsBaseImplMock(IRenderer renderer)
=> Mock.Of<IWindowBaseImpl>(x =>
x.RenderScaling == 1 &&
x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer);
public TestWindowBase(IWindowBaseImpl impl) public TestWindowBase(IWindowBaseImpl impl)
: base(impl) : base(impl)
{ {

12
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -98,6 +98,8 @@ namespace Avalonia.Controls.UnitTests
public void IsVisible_Should_Be_False_After_Impl_Signals_Close() public void IsVisible_Should_Be_False_After_Impl_Signals_Close()
{ {
var windowImpl = new Mock<IWindowImpl>(); var windowImpl = new Mock<IWindowImpl>();
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
.Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.SetupProperty(x => x.Closed); windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);
@ -269,7 +271,7 @@ namespace Avalonia.Controls.UnitTests
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var target = new Window(CreateImpl(renderer)); var target = new Window(CreateImpl(renderer));
target.Show(); target.Show();
@ -284,7 +286,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var parent = new Window(); var parent = new Window();
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var target = new Window(CreateImpl(renderer)); var target = new Window(CreateImpl(renderer));
parent.Show(); parent.Show();
@ -317,7 +319,7 @@ namespace Avalonia.Controls.UnitTests
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
var target = new Window(CreateImpl(renderer)); var target = new Window(CreateImpl(renderer));
target.Show(); target.Show();
@ -334,6 +336,8 @@ namespace Avalonia.Controls.UnitTests
{ {
var parent = new Window(); var parent = new Window();
var windowImpl = new Mock<IWindowImpl>(); var windowImpl = new Mock<IWindowImpl>();
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
.Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.SetupProperty(x => x.Closed); windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);
@ -375,6 +379,8 @@ namespace Avalonia.Controls.UnitTests
{ {
var parent = new Window(); var parent = new Window();
var windowImpl = new Mock<IWindowImpl>(); var windowImpl = new Mock<IWindowImpl>();
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
.Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.SetupProperty(x => x.Closed); windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1);

43
tests/Avalonia.LeakTests/ControlTests.cs

@ -462,7 +462,7 @@ namespace Avalonia.LeakTests
{ {
using (Start()) using (Start())
{ {
var renderer = new Mock<IRenderer>(); var renderer = RendererMocks.CreateRenderer();
renderer.Setup(x => x.Dispose()); renderer.Setup(x => x.Dispose());
var impl = new Mock<IWindowImpl>(); var impl = new Mock<IWindowImpl>();
impl.Setup(r => r.TryGetFeature(It.IsAny<Type>())).Returns(null); impl.Setup(r => r.TryGetFeature(It.IsAny<Type>())).Returns(null);
@ -1029,46 +1029,5 @@ namespace Avalonia.LeakTests
public IEnumerable<Node> Children { get; set; } public IEnumerable<Node> Children { get; set; }
} }
private 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(null);
}
} }
} }

5
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -1,6 +1,5 @@
using System; using System;
using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Moq; using Moq;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
@ -28,6 +27,8 @@ namespace Avalonia.UnitTests
var clientSize = new Size(initialWidth, initialHeight); var clientSize = new Size(initialWidth, initialHeight);
windowImpl.SetupAllProperties(); windowImpl.SetupAllProperties();
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
.Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.DesktopScaling).Returns(1);
@ -92,6 +93,8 @@ namespace Avalonia.UnitTests
var positioner = new ManagedPopupPositioner(positionerHelper); var positioner = new ManagedPopupPositioner(positionerHelper);
popupImpl.SetupAllProperties(); popupImpl.SetupAllProperties();
popupImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
.Returns(() => RendererMocks.CreateRenderer().Object);
popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize); popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
popupImpl.Setup(x => x.RenderScaling).Returns(1); popupImpl.Setup(x => x.RenderScaling).Returns(1);

57
tests/Avalonia.UnitTests/NullRenderer.cs

@ -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);
}

15
tests/Avalonia.UnitTests/RendererMocks.cs

@ -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;
}
}
}

4
tests/Avalonia.UnitTests/TestRoot.cs

@ -1,9 +1,7 @@
using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
@ -18,7 +16,7 @@ namespace Avalonia.UnitTests
public TestRoot() public TestRoot()
{ {
Renderer = Mock.Of<IRenderer>(); Renderer = RendererMocks.CreateRenderer().Object;
LayoutManager = new LayoutManager(this); LayoutManager = new LayoutManager(this);
IsVisible = true; IsVisible = true;
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);

Loading…
Cancel
Save