From 34d4d89fbfee7848f02cb7f39adce6b087735da3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 18 Mar 2024 23:47:25 +0600 Subject: [PATCH] Refactored overlays, so most of them are usable without an intermediate layer (#15028) * [SKIA] Don't skip setting SKCanvas.TotalMatrix when we aren't sure what the current transform is due to Save/Restore calls * Refactored overlays, so most of them are usable without an intermediate layer --------- Co-authored-by: Max Katz --- .../Server/CompositionTargetOverlays.cs | 143 ++++++++++++++++++ .../Server/DiagnosticTextRenderer.cs | 8 +- .../Composition/Server/FpsCounter.cs | 14 +- .../Composition/Server/FrameTimeGraph.cs | 29 ++-- .../Server/ServerCompositionTarget.cs | 118 ++------------- 5 files changed, 180 insertions(+), 132 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs new file mode 100644 index 0000000000..ac21f0393b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs @@ -0,0 +1,143 @@ +using System; +using System.Diagnostics; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositionTargetOverlays +{ + private FpsCounter? _fpsCounter; + private FrameTimeGraph? _renderTimeGraph; + private FrameTimeGraph? _layoutTimeGraph; + private Rect? _oldFpsCounterRect; + private long _updateStarted; + private readonly ServerCompositionTarget _target; + private readonly DiagnosticTextRenderer? _diagnosticTextRenderer; + + public CompositionTargetOverlays( + ServerCompositionTarget target, + DiagnosticTextRenderer? diagnosticTextRenderer) + { + _target = target; + _diagnosticTextRenderer = diagnosticTextRenderer; + } + + private RendererDebugOverlays DebugOverlays { get; set; } + + private FpsCounter? FpsCounter + => _fpsCounter ??= _diagnosticTextRenderer != null ? new FpsCounter(_diagnosticTextRenderer) : null; + + private FrameTimeGraph? LayoutTimeGraph + => _layoutTimeGraph ??= CreateTimeGraph("Layout"); + + private FrameTimeGraph? RenderTimeGraph + => _renderTimeGraph ??= CreateTimeGraph("Render"); + + public bool RequireLayer => DebugOverlays.HasAnyFlag(RendererDebugOverlays.DirtyRects); + + private FrameTimeGraph? CreateTimeGraph(string title) + { + if (_diagnosticTextRenderer == null) + return null; + return new FrameTimeGraph(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, _diagnosticTextRenderer); + } + + + public void OnChanged(RendererDebugOverlays debugOverlays) + { + DebugOverlays = debugOverlays; + _oldFpsCounterRect = null; + if ((DebugOverlays & RendererDebugOverlays.Fps) == 0) + { + _fpsCounter?.Reset(); + } + + if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) == 0) + { + _layoutTimeGraph?.Reset(); + } + + if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) == 0) + { + _renderTimeGraph?.Reset(); + } + } + + + private bool CaptureTiming => (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0; + + public void Draw(IDrawingContextImpl targetContext, bool hasLayer) + { + if (DebugOverlays != RendererDebugOverlays.None) + { + if (CaptureTiming) + { + var elapsed = StopwatchHelper.GetElapsedTime(_updateStarted); + RenderTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds); + } + + + + if (DebugOverlays.HasFlag(RendererDebugOverlays.DirtyRects)) + _target.DirtyRects.Visualize(targetContext); + + targetContext.Transform = Matrix.CreateScale(_target.Scaling, _target.Scaling); + + using (var immediate = new ImmediateDrawingContext(targetContext, false)) + DrawOverlays(immediate, hasLayer, _target.PixelSize.ToSize(_target.Scaling)); + } + } + + public void MarkUpdateCallStart() => _updateStarted = CaptureTiming ? Stopwatch.GetTimestamp() : 0L; + + private void DrawOverlays(ImmediateDrawingContext targetContext, bool hasLayer, Size logicalSize) + { + if (DebugOverlays.HasFlag(RendererDebugOverlays.Fps)) + { + var nativeMem = ByteSizeHelper.ToString((ulong)( + (_target.Compositor.BatchMemoryPool.CurrentUsage + _target.Compositor.BatchMemoryPool.CurrentPool) * + _target.Compositor.BatchMemoryPool.BufferSize), false); + var managedMem = ByteSizeHelper.ToString((ulong)( + (_target.Compositor.BatchObjectPool.CurrentUsage + _target.Compositor.BatchObjectPool.CurrentPool) * + _target.Compositor.BatchObjectPool.ArraySize * + IntPtr.Size), false); + + _oldFpsCounterRect = FpsCounter?.RenderFps(targetContext, + FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{_target.RenderedVisuals:0000}"), + hasLayer, _oldFpsCounterRect); + } + + var top = 0.0; + + void DrawTimeGraph(FrameTimeGraph? graph) + { + if (graph == null) + return; + var left = logicalSize.Width - graph.Size.Width - 8.0; + top += 8.0; + if (!hasLayer) + targetContext.FillRectangle(Brushes.White, new Rect(left, top, graph.Size.Width, graph.Size.Height)); + using (targetContext.PushSetTransform(Matrix.CreateTranslation(left, top))) + graph.Render(targetContext); + top += graph.Size.Height; + } + + if (DebugOverlays.HasFlag(RendererDebugOverlays.LayoutTimeGraph)) + DrawTimeGraph(LayoutTimeGraph); + + if (DebugOverlays.HasFlag(RendererDebugOverlays.RenderTimeGraph)) + DrawTimeGraph(RenderTimeGraph); + } + + + public void OnLastLayoutPassTimingChanged(LayoutPassTiming lastLayoutPassTiming) + { + if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0) + { + LayoutTimeGraph?.AddFrameValue(lastLayoutPassTiming.Elapsed.TotalMilliseconds); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index b045bd8a53..986692176b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -58,21 +58,19 @@ namespace Avalonia.Rendering.Composition.Server return new Size(width, height); } - public void DrawAsciiText(IDrawingContextImpl context, ReadOnlySpan text, IBrush foreground) + public void DrawAsciiText(ImmediateDrawingContext context, ReadOnlySpan text, IImmutableBrush 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 = Matrix.CreateTranslation(offset, 0.0) * originalTransform; - context.DrawGlyphRun(foreground, run.PlatformImpl.Item); + using (context.PushPreTransform(Matrix.CreateTranslation(offset, 0.0))) + context.PlatformImpl.DrawGlyphRun(foreground, run.PlatformImpl.Item); offset += run.Bounds.Width; } - context.Transform = originalTransform; } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 00526eb4d1..c13e0d04ae 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -26,7 +27,7 @@ internal class FpsCounter public void FpsTick() => _framesThisSecond++; - public void RenderFps(IDrawingContextImpl context, string aux) + public Rect? RenderFps(ImmediateDrawingContext context, string aux, bool hasLayer, Rect? oldRect) { var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; @@ -48,11 +49,18 @@ internal class FpsCounter #endif 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); + + var rect = new Rect(0.0, 0.0, size.Width + 3.0, size.Height + 3.0); + if (hasLayer && oldRect.HasValue) + rect = rect.Union(oldRect.Value); + + var layerBr = new ImmutableSolidColorBrush(Colors.Black, 0.5); + context.DrawRectangle(hasLayer ? layerBr : Brushes.Black, null, rect); _textRenderer.DrawAsciiText(context, fpsLine.AsSpan(), Brushes.White); + return rect; + } public void Reset() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs index d103b068a6..8e283ba5b1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs @@ -75,37 +75,32 @@ internal sealed class FrameTimeGraph _frameCount = 0; } - public void Render(IDrawingContextImpl context) + public void Render(ImmediateDrawingContext context) { - var originalTransform = context.Transform; - context.PushClip(new Rect(_size)); + using var _ = context.PushClip(new Rect(_size)); - context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_size))); - context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_headerSize))); + context.DrawRectangle(_borderBrush, null, new Rect(_size)); + context.DrawRectangle(_borderBrush, null, new Rect(_headerSize)); - context.Transform = originalTransform * Matrix.CreateTranslation(HeaderPadding, HeaderPadding); + using var __ = context.PushPreTransform(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); + DrawLabelledValue(context, "Min", min, _headerSize.Width * 0.19); + DrawLabelledValue(context, "Avg", avg, _headerSize.Width * 0.46); + DrawLabelledValue(context, "Max", max, _headerSize.Width * 0.73); - context.Transform = originalTransform * Matrix.CreateTranslation(0.0, _headerSize.Height); - context.DrawGeometry(null, _graphPen, BuildGraphGeometry(Math.Max(max, _defaultMaxY))); + using (context.PushPreTransform(Matrix.CreateTranslation(0.0, _headerSize.Height))) + context.PlatformImpl.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) + private void DrawLabelledValue(ImmediateDrawingContext context, string label, double value, double left) { - context.Transform = originalTransform * Matrix.CreateTranslation(left + HeaderPadding, HeaderPadding); + using var _ = context.PushPreTransform(Matrix.CreateTranslation(left + HeaderPadding, HeaderPadding)); var brush = value <= _defaultMaxY ? Brushes.Black : Brushes.Red; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 8d374cb15e..5e55de8903 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -20,12 +20,9 @@ namespace Avalonia.Rendering.Composition.Server { private readonly ServerCompositor _compositor; private readonly Func> _surfaces; - private readonly DiagnosticTextRenderer? _diagnosticTextRenderer; + private CompositionTargetOverlays _overlays; private static long s_nextId = 1; private IRenderTarget? _renderTarget; - private FpsCounter? _fpsCounter; - private FrameTimeGraph? _renderTimeGraph; - private FrameTimeGraph? _layoutTimeGraph; private PixelSize _layerSize; private IDrawingContextLayerImpl? _layer; private bool _updateRequested; @@ -41,22 +38,13 @@ namespace Avalonia.Rendering.Composition.Server public ReadbackIndices Readback { get; } = new(); public int RenderedVisuals { get; set; } - private FpsCounter? FpsCounter - => _fpsCounter ??= _diagnosticTextRenderer != null ? new FpsCounter(_diagnosticTextRenderer) : null; - - private FrameTimeGraph? LayoutTimeGraph - => _layoutTimeGraph ??= CreateTimeGraph("Layout"); - - private FrameTimeGraph? RenderTimeGraph - => _renderTimeGraph ??= CreateTimeGraph("Render"); - public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces, DiagnosticTextRenderer? diagnosticTextRenderer) : base(compositor) { _compositor = compositor; _surfaces = surfaces; - _diagnosticTextRenderer = diagnosticTextRenderer; + _overlays = new CompositionTargetOverlays(this, diagnosticTextRenderer); var platformRender = AvaloniaLocator.Current.GetService(); DirtyRects = compositor.Options.UseRegionDirtyRectClipping == true && platformRender?.SupportsRegions == true @@ -64,14 +52,7 @@ namespace Avalonia.Rendering.Composition.Server : new DirtyRectTracker(); Id = Interlocked.Increment(ref s_nextId); } - - private FrameTimeGraph? CreateTimeGraph(string title) - { - if (_diagnosticTextRenderer == null) - return null; - return new FrameTimeGraph(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, _diagnosticTextRenderer); - } - + partial void OnIsEnabledChanged() { if (IsEnabled) @@ -90,29 +71,11 @@ namespace Avalonia.Rendering.Composition.Server partial void OnDebugOverlaysChanged() { - if ((DebugOverlays & RendererDebugOverlays.Fps) == 0) - { - _fpsCounter?.Reset(); - } - - if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) == 0) - { - _layoutTimeGraph?.Reset(); - } - - if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) == 0) - { - _renderTimeGraph?.Reset(); - } + _fullRedrawRequested = true; + _overlays.OnChanged(DebugOverlays); } - partial void OnLastLayoutPassTimingChanged() - { - if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0) - { - LayoutTimeGraph?.AddFrameValue(LastLayoutPassTiming.Elapsed.TotalMilliseconds); - } - } + partial void OnLastLayoutPassTimingChanged() => _overlays.OnLastLayoutPassTimingChanged(LastLayoutPassTiming); partial void DeserializeChangesExtra(BatchStreamReader c) { @@ -158,8 +121,7 @@ namespace Avalonia.Rendering.Composition.Server Revision++; - var captureTiming = (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0; - var startingTimestamp = captureTiming ? Stopwatch.GetTimestamp() : 0L; + _overlays.MarkUpdateCallStart(); var transform = Matrix.CreateScale(Scaling, Scaling); // Update happens in a separate phase to extend dirty rect if needed @@ -181,7 +143,7 @@ namespace Avalonia.Rendering.Composition.Server var renderTargetWithProperties = _renderTarget as IRenderTargetWithProperties; - var needLayer = DebugOverlays != RendererDebugOverlays.None // Check if we don't need overlays + var needLayer = _overlays.RequireLayer // Check if we don't need overlays // Check if render target can be rendered to directly and preserves the previous frame || !(renderTargetWithProperties?.Properties.RetainsPreviousFrameContents == true && renderTargetWithProperties?.Properties.IsSuitableForDirectRendering == true); @@ -224,20 +186,13 @@ namespace Avalonia.Rendering.Composition.Server var rect = new PixelRect(default, PixelSize).ToRect(1); renderTargetContext.DrawBitmap(_layer, 1, rect, rect); } + _overlays.Draw(renderTargetContext, true); } else - RenderRootToContextWithClip(renderTargetContext, Root); - } - - if (DebugOverlays != RendererDebugOverlays.None) - { - if (captureTiming) { - var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp); - RenderTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds); + RenderRootToContextWithClip(renderTargetContext, Root); + _overlays.Draw(renderTargetContext, false); } - - DrawOverlays(renderTargetContext, PixelSize.ToSize(Scaling)); } RenderedVisuals = 0; @@ -265,57 +220,6 @@ namespace Avalonia.Rendering.Composition.Server } } - - private void DrawOverlays(IDrawingContextImpl targetContext, Size logicalSize) - { - if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0) - DirtyRects.Visualize(targetContext); - - - targetContext.Transform = Matrix.CreateScale(Scaling, Scaling); - - 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) - { - if (graph == null) - return; - top += 8.0; - var oldTransform = targetContext.Transform; - - targetContext.Transform = Matrix.CreateTranslation(logicalSize.Width - graph.Size.Width - 8.0, top) * - oldTransform; - graph.Render(targetContext); - top += graph.Size.Height; - targetContext.Transform = oldTransform; - } - - if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0) - { - DrawTimeGraph(LayoutTimeGraph); - } - - if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0) - { - DrawTimeGraph(RenderTimeGraph); - } - - targetContext.Transform = Matrix.Identity; - } - public void RequestUpdate() => _updateRequested = true; public void Dispose()