From 4dbb165a7bcb338ef3324690549fd3012d3f8a92 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 6 Mar 2024 03:37:53 +0600 Subject: [PATCH] Reworked the way we are clipping dirty rects with CompositionTarget (#14806) * Reworked dirty rect tracking to support regions * Fixed FPS counter --- samples/ControlCatalog.NetCore/Program.cs | 5 + .../Pages/CompositionPage.axaml | 4 + .../Pages/CompositionPage.axaml.cs | 64 +++++++-- .../Media/Imaging/RenderTargetBitmap.cs | 2 +- src/Avalonia.Base/PixelSize.cs | 11 ++ .../Platform/IDrawingContextImpl.cs | 33 ++++- .../Platform/IPlatformRenderInterface.cs | 3 + .../IPlatformRenderInterfaceRegion.cs | 17 +++ src/Avalonia.Base/Platform/IRenderTarget.cs | 3 +- .../Composition/CompositingRenderer.cs | 3 +- .../Composition/CompositionCustomVisual.cs | 32 +++-- .../CompositionCustomVisualHandler.cs | 47 ++++++- .../Composition/CompositionOptions.cs | 16 +++ .../Composition/CompositionTarget.cs | 1 + .../Server/DiagnosticTextRenderer.cs | 2 +- .../Composition/Server/DirtyRectTracker.cs | 103 +++++++++++++++ .../Composition/Server/DrawingContextProxy.cs | 11 +- .../ServerCompositionContainerVisual.cs | 13 +- .../Server/ServerCompositionDrawListVisual.cs | 6 +- ...verCompositionExperimentalAcrylicVisual.cs | 5 +- .../ServerCompositionSolidColorVisual.cs | 3 +- .../Server/ServerCompositionSurfaceVisual.cs | 3 +- .../ServerCompositionTarget.DirtyRects.cs | 35 +++++ .../Server/ServerCompositionTarget.cs | 121 +++++++++--------- .../Server/ServerCompositionVisual.cs | 21 +-- .../Composition/Server/ServerCompositor.cs | 1 + .../Server/ServerCustomCompositionVisual.cs | 19 ++- src/Avalonia.Base/composition-schema.xml | 2 +- src/Avalonia.Controls/BorderVisual.cs | 5 +- src/Avalonia.X11/X11CursorFactory.cs | 2 +- src/Avalonia.X11/X11IconLoader.cs | 2 +- .../HeadlessPlatformRenderInterface.cs | 26 +++- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 95 +++++++++++--- .../Avalonia.Skia/FramebufferRenderTarget.cs | 5 +- .../Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs | 3 +- src/Skia/Avalonia.Skia/PictureRenderTarget.cs | 1 + .../Avalonia.Skia/PlatformRenderInterface.cs | 3 + .../Avalonia.Skia/RenderTargetBitmapImpl.cs | 3 +- src/Skia/Avalonia.Skia/SkiaRegionImpl.cs | 53 ++++++++ src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs | 10 ++ src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 3 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 3 + .../ExternalRenderTarget.cs | 4 +- .../FramebufferShimRenderTarget.cs | 8 +- .../Media/DrawingContextImpl.cs | 58 ++++++++- .../Media/ImageBrushImpl.cs | 2 +- .../Imaging/D2DRenderTargetBitmapImpl.cs | 7 +- .../Imaging/WicRenderTargetBitmapImpl.cs | 8 +- .../Avalonia.Direct2D1/RenderTarget.cs | 4 +- .../SwapChainRenderTarget.cs | 4 +- .../Avalonia.RenderTests/Media/BitmapTests.cs | 2 +- tests/Avalonia.UnitTests/TestRoot.cs | 6 +- 52 files changed, 719 insertions(+), 184 deletions(-) create mode 100644 src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs create mode 100644 src/Skia/Avalonia.Skia/SkiaRegionImpl.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 5e3e301461..fa5b78c278 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Fonts.Inter; using Avalonia.Headless; using Avalonia.LogicalTree; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using ControlCatalog.Pages; @@ -130,6 +131,10 @@ namespace ControlCatalog.NetCore UseDBusMenu = true, EnableIme = true }) + .With(new CompositionOptions() + { + UseRegionDirtyRectClipping = true + }) .UseSkia() .WithInterFont() .AfterSetup(builder => diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml index 602b9b768d..4d9bb41781 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml @@ -50,6 +50,10 @@ + Precise dirty rects diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs index 5bf46510dc..da12accd58 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -24,7 +24,7 @@ public partial class CompositionPage : UserControl public CompositionPage() { - AvaloniaXamlLoader.Load(this); + InitializeComponent(); AttachAnimatedSolidVisual(this.FindControl("SolidVisualHost")!); AttachCustomVisual(this.FindControl("CustomVisualHost")!); } @@ -206,6 +206,7 @@ public partial class CompositionPage : UserControl _customVisual = compositor.CreateCustomVisual(new CustomVisualHandler()); ElementComposition.SetElementChildVisual(v, _customVisual); _customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage); + PreciseDirtyRectsCheckboxCustomVisualChanged(this, new()); Update(); }; @@ -221,10 +222,16 @@ public partial class CompositionPage : UserControl private TimeSpan _animationElapsed; private TimeSpan? _lastServerTime; private bool _running; + private bool _preciseDirtyRects; - public static readonly object StopMessage = new(), StartMessage = new(); - - public override void OnRender(ImmediateDrawingContext drawingContext) + public static readonly object StopMessage = new(), + StartMessage = new(), + UsePreciseDirtyRects = new(), + UseNonPreciseDirtyRects = new(); + + private List<(Point center, double size, ImmutableSolidColorBrush brush)> _ellipses = new(); + + void UpdateRects() { if (_running) { @@ -232,6 +239,8 @@ public partial class CompositionPage : UserControl _lastServerTime = CompositionNow; } + _ellipses.Clear(); + const int cnt = 20; var maxPointSizeX = EffectiveSize.X / (cnt * 1.6); var maxPointSizeY = EffectiveSize.Y / 4; @@ -250,16 +259,22 @@ public partial class CompositionPage : UserControl var posY = (EffectiveSize.Y - pointSize) * (1 + Math.Sin(stage * 3.14 * 3 + sinOffset)) / 2 + pointSize / 2; var opacity = Math.Sin(stage * 3.14); - - drawingContext.DrawEllipse(new ImmutableSolidColorBrush(Color.FromArgb( - 255, - (byte)(255 - 255 * colorStage), - (byte)(255 * Math.Abs(0.5 - colorStage) * 2), - (byte)(255 * colorStage) - ), opacity), null, - new Point(posX, posY), pointSize / 2, pointSize / 2); + _ellipses.Add((new Point(posX, posY), pointSize / 2, new ImmutableSolidColorBrush(Color.FromArgb( + 255, + (byte)(255 - 255 * colorStage), + (byte)(255 * Math.Abs(0.5 - colorStage) * 2), + (byte)(255 * colorStage) + ), opacity))); } + } + + public override void OnRender(ImmediateDrawingContext drawingContext) + { + if (_ellipses.Count == 0) + UpdateRects(); + foreach(var e in _ellipses) + drawingContext.DrawEllipse(e.brush, null, e.center, e.size, e.size); } public override void OnMessage(object message) @@ -272,13 +287,29 @@ public partial class CompositionPage : UserControl } else if (message == StopMessage) _running = false; + else if (message == UsePreciseDirtyRects) + _preciseDirtyRects = true; + else if (message == UseNonPreciseDirtyRects) + _preciseDirtyRects = false; } + void InvalidateCurrentEllipseRects() + { + foreach (var e in _ellipses) + Invalidate(new Rect(e.center.X - e.size, e.center.Y - e.size, e.size * 2, e.size * 2)); + } + public override void OnAnimationFrameUpdate() { if (_running) { - Invalidate(); + if (_preciseDirtyRects) + InvalidateCurrentEllipseRects(); + else + Invalidate(); + UpdateRects(); + if(_preciseDirtyRects) + InvalidateCurrentEllipseRects(); RegisterForNextAnimationFrameUpdate(); } } @@ -298,6 +329,13 @@ public partial class CompositionPage : UserControl { _customVisual?.SendHandlerMessage(CustomVisualHandler.StopMessage); } + + private void PreciseDirtyRectsCheckboxCustomVisualChanged(object sender, RoutedEventArgs e) + { + _customVisual?.SendHandlerMessage(PreciseDirtyRectsCheckboxCustomVisual?.IsChecked == true + ? CustomVisualHandler.UsePreciseDirtyRects + : CustomVisualHandler.UseNonPreciseDirtyRects); + } } public class CompositionPageColorItem diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index b1c9c81b39..9f41177223 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -77,7 +77,7 @@ namespace Avalonia.Media.Imaging /// The drawing context. public DrawingContext CreateDrawingContext(bool clear) { - var platform = PlatformImpl.Item.CreateDrawingContext(); + var platform = PlatformImpl.Item.CreateDrawingContext(true); if(clear) platform.Clear(Colors.Transparent); return new PlatformDrawingContext(platform); diff --git a/src/Avalonia.Base/PixelSize.cs b/src/Avalonia.Base/PixelSize.cs index 5a34c6f6b5..251fe6ff85 100644 --- a/src/Avalonia.Base/PixelSize.cs +++ b/src/Avalonia.Base/PixelSize.cs @@ -166,6 +166,17 @@ namespace Avalonia public static PixelSize FromSize(Size size, double scale) => new PixelSize( (int)Math.Ceiling(size.Width * scale), (int)Math.Ceiling(size.Height * scale)); + + /// + /// A reversible variant of that uses Round instead of Ceiling to make it reversible from ToSize + /// + /// The size. + /// The scaling factor. + /// The device-independent size. + internal static PixelSize FromSizeRounded(Size size, double scale) => new PixelSize( + (int)Math.Round(size.Width * scale), + (int)Math.Round(size.Height * scale)); + /// /// Converts a to device pixels using the specified scaling factor. diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index fe411c350d..f367ed89ca 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -3,6 +3,7 @@ using Avalonia.Media; using Avalonia.Utilities; using Avalonia.Metadata; using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; namespace Avalonia.Platform { @@ -75,6 +76,18 @@ namespace Avalonia.Platform /// void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default); + + /// + /// Draws the specified region with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The region to draw. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region); /// /// Draws an ellipse with the specified Brush and Pen. @@ -108,7 +121,7 @@ namespace Avalonia.Platform /// has to do a format conversion each time a standard render target bitmap is rendered, /// but a layer created via this method has no such overhead. /// - IDrawingContextLayerImpl CreateLayer(Size size); + IDrawingContextLayerImpl CreateLayer(PixelSize size); /// /// Pushes a clip rectangle. @@ -121,12 +134,28 @@ namespace Avalonia.Platform /// /// The clip rounded rectangle void PushClip(RoundedRect clip); - + + /// + /// Pushes a clip region. + /// + /// The clip region + void PushClip(IPlatformRenderInterfaceRegion region); + /// /// Pops the latest pushed clip rectangle. /// void PopClip(); + /// + /// Enforces rendering to happen on an intermediate surface + /// + void PushLayer(Rect bounds); + + /// + /// Pops the latest pushed intermediate surface layer. + /// + void PopLayer(); + /// /// Pushes an opacity value. /// diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index c783a5ea65..0ead242a27 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -199,6 +199,9 @@ namespace Avalonia.Platform public PixelFormat DefaultPixelFormat { get; } bool IsSupportedBitmapPixelFormat(PixelFormat format); + + bool SupportsRegions { get; } + IPlatformRenderInterfaceRegion CreateRegion(); } [Unstable, PrivateApi] diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs new file mode 100644 index 0000000000..682609391c --- /dev/null +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Avalonia.Metadata; + +namespace Avalonia.Platform; + +[Unstable, PrivateApi] +public interface IPlatformRenderInterfaceRegion : IDisposable +{ + void AddRect(PixelRect rect); + void Reset(); + bool IsEmpty { get; } + PixelRect Bounds { get; } + IList Rects { get; } + bool Intersects(Rect rect); + bool Contains(Point pt); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 2b4584483e..a1f33db6c7 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -16,7 +16,8 @@ namespace Avalonia.Platform /// /// Creates an for a rendering session. /// - IDrawingContextImpl CreateDrawingContext(); + /// Apply DPI reported by the render target as a hidden transform matrix + IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing); /// /// Indicates if the render target is no longer usable and needs to be recreated diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index eef5188564..2a4345f702 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -172,7 +172,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester v.SynchronizeCompositionChildVisuals(); _dirty.Clear(); _recalculateChildren.Clear(); - CompositionTarget.Size = _root.ClientSize; + + CompositionTarget.PixelSize = PixelSize.FromSizeRounded(_root.ClientSize, _root.RenderScaling); CompositionTarget.Scaling = _root.RenderScaling; var commit = _compositor.RequestCommitAsync(); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs index 1d7887cd0e..79f2cc3fb9 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs @@ -1,12 +1,15 @@ using System.Collections.Generic; using System.Numerics; +using Avalonia.Media; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Threading; namespace Avalonia.Rendering.Composition; public sealed class CompositionCustomVisual : CompositionContainerVisual { + private static readonly ThreadSafeObjectPool> s_messageListPool = new(); private List? _messages; internal CompositionCustomVisual(Compositor compositor, CompositionCustomVisualHandler handler) @@ -17,21 +20,26 @@ public sealed class CompositionCustomVisual : CompositionContainerVisual public void SendHandlerMessage(object message) { - (_messages ??= new()).Add(message); - RegisterForSerialization(); + if (_messages == null) + { + _messages = s_messageListPool.Get(); + Compositor.RequestCompositionUpdate(OnCompositionUpdate); + } + _messages.Add(message); } - private protected override void SerializeChangesCore(BatchStreamWriter writer) + private void OnCompositionUpdate() { - base.SerializeChangesCore(writer); - if (_messages == null || _messages.Count == 0) - writer.Write(0); - else + if(_messages == null) + return; + + var messages = _messages; + _messages = null; + Compositor.PostServerJob(()=> { - writer.Write(_messages.Count); - foreach (var m in _messages) - writer.WriteObject(m); - _messages.Clear(); - } + ((ServerCompositionCustomVisual)Server).DispatchMessages(messages); + messages.Clear(); + s_messageListPool.ReturnAndSetNull(ref messages); + }); } } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs index 598d4163d1..d508d2e860 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Numerics; +using Avalonia.Collections.Pooled; using Avalonia.Media; using Avalonia.Rendering.Composition.Server; @@ -8,6 +10,8 @@ namespace Avalonia.Rendering.Composition; public abstract class CompositionCustomVisualHandler { private ServerCompositionCustomVisual? _host; + private bool _inRender; + private Rect _currentTransformedClip; public virtual void OnMessage(object message) { @@ -18,7 +22,21 @@ public abstract class CompositionCustomVisualHandler { } - + + internal void Render(ImmediateDrawingContext drawingContext, Rect currentTransformedClip) + { + _inRender = true; + _currentTransformedClip = currentTransformedClip; + try + { + OnRender(drawingContext); + } + finally + { + _inRender = false; + } + } + public abstract void OnRender(ImmediateDrawingContext drawingContext); void VerifyAccess() @@ -28,6 +46,13 @@ public abstract class CompositionCustomVisualHandler _host.Compositor.VerifyAccess(); } + void VerifyInRender() + { + VerifyAccess(); + if (!_inRender) + throw new InvalidOperationException("This API is only available from OnRender"); + } + protected Vector EffectiveSize { get @@ -57,9 +82,29 @@ public abstract class CompositionCustomVisualHandler _host!.HandlerInvalidate(); } + protected void Invalidate(Rect rc) + { + VerifyAccess(); + _host!.HandlerInvalidate(rc); + } + protected void RegisterForNextAnimationFrameUpdate() { VerifyAccess(); _host!.HandlerRegisterForNextAnimationFrameUpdate(); } + + protected bool RenderClipContains(Point pt) + { + VerifyInRender(); + pt *= _host!.GlobalTransformMatrix; + return _currentTransformedClip.Contains(pt) && _host.Root!.DirtyRects.Contains(pt); + } + + protected bool RenderClipIntersectes(Rect rc) + { + VerifyInRender(); + rc = rc.TransformToAABB(_host!.GlobalTransformMatrix); + return _currentTransformedClip.Intersects(rc) && _host.Root!.DirtyRects.Intersects(rc); + } } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs b/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs new file mode 100644 index 0000000000..ad40a2abed --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs @@ -0,0 +1,16 @@ +namespace Avalonia.Rendering.Composition; + +public class CompositionOptions +{ + /// + /// Enables more accurate tracking of dirty rects by utilizing regions if supported by the underlying + /// drawing context + /// + public bool? UseRegionDirtyRectClipping { get; set; } + /// + /// Enforces dirty contents to be rendered into an extra intermediate surface before being applied onto the + /// saved frame. + /// Required as a workaround for Skia bug https://issues.skia.org/issues/327877721 + /// + public bool? UseSaveLayerRootClip { get; set; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 8ffdd649d5..f00eef0d10 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -29,6 +29,7 @@ namespace Avalonia.Rendering.Composition /// public PooledList? TryHitTest(Point point, CompositionVisual? root, Func? filter) { + point *= Scaling; Server.Readback.NextRead(); root ??= Root; if (root == null) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index 138b791019..b045bd8a53 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -67,7 +67,7 @@ namespace Avalonia.Rendering.Composition.Server { var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; var run = _runs[effectiveChar - FirstChar]; - context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); + context.Transform = Matrix.CreateTranslation(offset, 0.0) * originalTransform; context.DrawGlyphRun(foreground, run.PlatformImpl.Item); offset += run.Bounds.Width; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs new file mode 100644 index 0000000000..e9b4071d79 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Reactive; + +namespace Avalonia.Rendering.Composition.Server; + +internal interface IDirtyRectTracker +{ + void AddRect(PixelRect rect); + IDisposable BeginDraw(IDrawingContextImpl ctx); + bool IsEmpty { get; } + bool Intersects(Rect rect); + bool Contains(Point pt); + void Reset(); + void Visualize(IDrawingContextImpl context); + PixelRect CombinedRect { get; } + IList Rects { get; } +} + +internal class DirtyRectTracker : IDirtyRectTracker +{ + private PixelRect _rect; + private Rect _doubleRect; + private PixelRect[] _rectsForApi = new PixelRect[1]; + private Random _random = new(); + public void AddRect(PixelRect rect) + { + _rect = _rect.Union(rect); + } + + public IDisposable BeginDraw(IDrawingContextImpl ctx) + { + ctx.PushClip(_rect.ToRect(1)); + _doubleRect = _rect.ToRect(1); + return Disposable.Create(ctx.PopClip); + } + + public bool IsEmpty => _rect.Width == 0 | _rect.Height == 0; + public bool Intersects(Rect rect) => _doubleRect.Intersects(rect); + public bool Contains(Point pt) => _rect.Contains(PixelPoint.FromPoint(pt, 1)); + + public void Reset() => _rect = default; + public void Visualize(IDrawingContextImpl context) + { + context.DrawRectangle( + new ImmutableSolidColorBrush( + new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))), + null, _doubleRect); + } + + public PixelRect CombinedRect => _rect; + + public IList Rects + { + get + { + if (_rect.Width == 0 || _rect.Height == 0) + return Array.Empty(); + _rectsForApi[0] = _rect; + return _rectsForApi; + } + } +} + +internal class RegionDirtyRectTracker : IDirtyRectTracker +{ + private readonly IPlatformRenderInterfaceRegion _region; + private Random _random = new(); + + public RegionDirtyRectTracker(IPlatformRenderInterface platformRender) + { + _region = platformRender.CreateRegion(); + } + + public void AddRect(PixelRect rect) => _region.AddRect(rect); + + public IDisposable BeginDraw(IDrawingContextImpl ctx) + { + ctx.PushClip(_region); + return Disposable.Create(ctx.PopClip); + } + + public bool IsEmpty => _region.IsEmpty; + public bool Intersects(Rect rect) => _region.Intersects(rect); + public bool Contains(Point pt) => _region.Contains(pt); + + public void Reset() => _region.Reset(); + + public void Visualize(IDrawingContextImpl context) + { + context.DrawRegion( + new ImmutableSolidColorBrush( + new Color(150, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))), + null, _region); + } + + public PixelRect CombinedRect => _region.Bounds; + public IList Rects => _region.Rects; +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 59ee8f6556..980c1413c6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -77,6 +77,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, _impl.DrawRectangle(brush, pen, rect, boxShadows); } + public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region) => + _impl.DrawRegion(brush, pen, region); + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { _impl.DrawEllipse(brush, pen, rect); @@ -87,7 +90,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, _impl.DrawGlyphRun(foreground, glyphRun); } - public IDrawingContextLayerImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(PixelSize size) { return _impl.CreateLayer(size); } @@ -102,11 +105,17 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, _impl.PushClip(clip); } + public void PushClip(IPlatformRenderInterfaceRegion region) => _impl.PushClip(region); + public void PopClip() { _impl.PopClip(); } + public void PushLayer(Rect bounds) => _impl.PushLayer(bounds); + + public void PopLayer() => _impl.PopLayer(); + public void PushOpacity(double opacity, Rect? bounds) { _impl.PushOpacity(opacity, bounds); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs index f2a777b4bd..6e6ce31206 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs @@ -16,26 +16,27 @@ namespace Avalonia.Rendering.Composition.Server private Rect? _transformedContentBounds; private IImmutableEffect? _oldEffect; - protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip, + IDirtyRectTracker dirtyRects) { - base.RenderCore(canvas, currentTransformedClip); + base.RenderCore(canvas, currentTransformedClip, dirtyRects); foreach (var ch in Children) { - ch.Render(canvas, currentTransformedClip); + ch.Render(canvas, currentTransformedClip, dirtyRects); } } - public override UpdateResult Update(ServerCompositionTarget root) + public override UpdateResult Update(ServerCompositionTarget root, Matrix parentCombinedTransform) { - var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root); + var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root, parentCombinedTransform); foreach (var child in Children) { if (child.AdornedVisual != null) root.EnqueueAdornerUpdate(child); else { - var res = child.Update(root); + var res = child.Update(root, GlobalTransformMatrix); oldInvalidated |= res.InvalidatedOld; newInvalidated |= res.InvalidatedNew; combinedBounds = Rect.Union(combinedBounds, res.Bounds); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 5da8afd9cd..bf6b71bbd2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -40,13 +40,15 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua base.DeserializeChangesCore(reader, committedAt); } - protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip, + IDirtyRectTracker dirtyRects) { if (_renderCommands != null) { _renderCommands.Render(canvas); } - base.RenderCore(canvas, currentTransformedClip); + + base.RenderCore(canvas, currentTransformedClip, dirtyRects); } public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs index f0bcd7bc92..cd0c8d1092 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs @@ -5,7 +5,8 @@ namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionExperimentalAcrylicVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip, + IDirtyRectTracker dirtyRects) { var cornerRadius = CornerRadius; canvas.DrawRectangle( @@ -15,7 +16,7 @@ internal partial class ServerCompositionExperimentalAcrylicVisual cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.BottomRight, cornerRadius.BottomLeft)); - base.RenderCore(canvas, currentTransformedClip); + base.RenderCore(canvas, currentTransformedClip, dirtyRects); } public override Rect OwnContentBounds => new(0, 0, Size.X, Size.Y); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs index 79abd7ee17..ad3b62805f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs @@ -4,7 +4,8 @@ namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionSolidColorVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip, + IDirtyRectTracker dirtyRects) { canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new Rect(0, 0, Size.X, Size.Y)); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs index e7e6875193..5f9936874f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs @@ -4,7 +4,8 @@ namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionSurfaceVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip, + IDirtyRectTracker dirtyRects) { if (Surface == null) return; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs new file mode 100644 index 0000000000..317452e658 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionTarget +{ + public readonly IDirtyRectTracker DirtyRects; + + public void AddDirtyRect(Rect rect) + { + if (rect.Width == 0 && rect.Height == 0) + return; + var snapped = PixelRect.FromRect(SnapToDevicePixels(rect, Scaling), 1); + DebugEvents?.RectInvalidated(rect); + DirtyRects.AddRect(snapped); + _redrawRequested = true; + } + + public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling); + + private static Rect SnapToDevicePixels(Rect rect, double scale) + { + return new Rect( + new Point( + Math.Floor(rect.X * scale) / scale, + Math.Floor(rect.Y * scale) / scale), + new Point( + Math.Ceiling(rect.Right * scale) / scale, + Math.Ceiling(rect.Bottom * scale) / scale)); + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index d9c0e78169..e39ffc5e7f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; +using Avalonia.Collections.Pooled; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; @@ -25,10 +26,9 @@ namespace Avalonia.Rendering.Composition.Server private FpsCounter? _fpsCounter; private FrameTimeGraph? _renderTimeGraph; private FrameTimeGraph? _layoutTimeGraph; - private Rect _dirtyRect; - private readonly Random _random = new(); - private Size _layerSize; + private PixelSize _layerSize; private IDrawingContextLayerImpl? _layer; + private bool _updateRequested; private bool _redrawRequested; private bool _disposed; private readonly HashSet _attachedVisuals = new(); @@ -56,6 +56,11 @@ namespace Avalonia.Rendering.Composition.Server _compositor = compositor; _surfaces = surfaces; _diagnosticTextRenderer = diagnosticTextRenderer; + var platformRender = AvaloniaLocator.Current.GetService(); + DirtyRects = compositor.Options.UseRegionDirtyRectClipping == true && + platformRender?.SupportsRegions == true + ? new RegionDirtyRectTracker(platformRender) + : new DirtyRectTracker(); Id = Interlocked.Increment(ref s_nextId); } @@ -146,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server return; } - if ((_dirtyRect.Width == 0 && _dirtyRect.Height == 0) && !_redrawRequested) + if (DirtyRects.IsEmpty && !_redrawRequested && !_updateRequested) return; Revision++; @@ -154,39 +159,51 @@ namespace Avalonia.Rendering.Composition.Server var captureTiming = (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0; var startingTimestamp = captureTiming ? Stopwatch.GetTimestamp() : 0L; + var transform = Matrix.CreateScale(Scaling, Scaling); // Update happens in a separate phase to extend dirty rect if needed - Root.Update(this); + Root.Update(this, transform); while (_adornerUpdateQueue.Count > 0) { var adorner = _adornerUpdateQueue.Dequeue(); - adorner.Update(this); + adorner.Update(this, transform); } - + + _updateRequested = false; Readback.CompleteWrite(Revision); + if (!_redrawRequested) + return; _redrawRequested = false; - using (var targetContext = _renderTarget.CreateDrawingContext()) + using (var targetContext = _renderTarget.CreateDrawingContext(false)) { - var size = Size; - var layerSize = size * Scaling; - if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) + if (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted) { _layer?.Dispose(); _layer = null; - _layer = targetContext.CreateLayer(size); - _layerSize = layerSize; - _dirtyRect = new Rect(0, 0, size.Width, size.Height); + _layer = targetContext.CreateLayer(PixelSize); + _layerSize = PixelSize; + DirtyRects.AddRect(new PixelRect(_layerSize)); } - if (_dirtyRect.Width != 0 || _dirtyRect.Height != 0) + if (!DirtyRects.IsEmpty) { - using (var context = _layer.CreateDrawingContext()) + var useLayerClip = Compositor.Options.UseSaveLayerRootClip ?? + Compositor.RenderInterface.GpuContext != null; + using (var context = _layer.CreateDrawingContext(false)) { - context.PushClip(_dirtyRect); - context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect); - context.PopClip(); + using (DirtyRects.BeginDraw(context)) + { + context.Clear(Colors.Transparent); + if (useLayerClip) + context.PushLayer(DirtyRects.CombinedRect.ToRect(1)); + + + Root.Render(new CompositorDrawingContextProxy(context), null, DirtyRects); + + if (useLayerClip) + context.PopLayer(); + } } } @@ -195,9 +212,10 @@ namespace Avalonia.Rendering.Composition.Server if (_layer.CanBlit) _layer.Blit(targetContext); else - targetContext.DrawBitmap(_layer, 1, - new Rect(_layerSize), - new Rect(size)); + { + var rect = new PixelRect(default, PixelSize).ToRect(1); + targetContext.DrawBitmap(_layer, 1, rect, rect); + } if (DebugOverlays != RendererDebugOverlays.None) { @@ -206,27 +224,24 @@ namespace Avalonia.Rendering.Composition.Server var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp); RenderTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds); } - - DrawOverlays(targetContext); + + DrawOverlays(targetContext, PixelSize.ToSize(Scaling)); } RenderedVisuals = 0; - _dirtyRect = default; + DirtyRects.Reset(); } } - private void DrawOverlays(IDrawingContextImpl targetContext) + private void DrawOverlays(IDrawingContextImpl targetContext, Size logicalSize) { - 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.DirtyRects) != 0) + DirtyRects.Visualize(targetContext); + + targetContext.Transform = Matrix.CreateScale(Scaling, Scaling); + if ((DebugOverlays & RendererDebugOverlays.Fps) != 0) { var nativeMem = ByteSizeHelper.ToString((ulong) ( @@ -247,9 +262,13 @@ namespace Avalonia.Rendering.Composition.Server if (graph == null) return; top += 8.0; - targetContext.Transform = Matrix.CreateTranslation(Size.Width - graph.Size.Width - 8.0, top); + 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) @@ -261,35 +280,11 @@ namespace Avalonia.Rendering.Composition.Server { DrawTimeGraph(RenderTimeGraph); } - } - - public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling); - - private static Rect SnapToDevicePixels(Rect rect, double scale) - { - return new Rect( - new Point( - Math.Floor(rect.X * scale) / scale, - Math.Floor(rect.Y * scale) / scale), - new Point( - Math.Ceiling(rect.Right * scale) / scale, - Math.Ceiling(rect.Bottom * scale) / scale)); + + targetContext.Transform = Matrix.Identity; } - public void AddDirtyRect(Rect rect) - { - if (rect.Width == 0 && rect.Height == 0) - return; - var snapped = SnapToDevicePixels(rect, Scaling); - DebugEvents?.RectInvalidated(rect); - _dirtyRect = _dirtyRect.Union(snapped); - _redrawRequested = true; - } - - public void Invalidate() - { - _redrawRequested = true; - } + public void RequestUpdate() => _updateRequested = true; public void Dispose() { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index d7bdde11e9..1e19ee0051 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -22,20 +22,25 @@ namespace Avalonia.Rendering.Composition.Server private Rect? _transformedClipBounds; private Rect _combinedTransformedClipBounds; - protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip, + IDirtyRectTracker dirtyRects) { } - public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + public void Render(CompositorDrawingContextProxy canvas, Rect? parentTransformedClip, IDirtyRectTracker dirtyRects) { if (Visible == false || IsVisibleInFrame == false) return; if (Opacity == 0) return; - currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); + var currentTransformedClip = parentTransformedClip.HasValue + ? parentTransformedClip.Value.Intersect(_combinedTransformedClipBounds) + : _combinedTransformedClipBounds; if (currentTransformedClip.Width == 0 && currentTransformedClip.Height == 0) return; + if(!dirtyRects.Intersects(currentTransformedClip)) + return; Root!.RenderedVisuals++; Root!.DebugEvents?.IncrementRenderedVisuals(); @@ -67,7 +72,7 @@ namespace Avalonia.Rendering.Composition.Server canvas.RenderOptions = RenderOptions; - RenderCore(canvas, currentTransformedClip); + RenderCore(canvas, currentTransformedClip, dirtyRects); // Hack to force invalidation of SKMatrix canvas.PostTransform = transform; @@ -116,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server } } - public virtual UpdateResult Update(ServerCompositionTarget root) + public virtual UpdateResult Update(ServerCompositionTarget root, Matrix parentVisualTransform) { if (Parent == null && Root == null) return default; @@ -138,7 +143,7 @@ namespace Avalonia.Rendering.Composition.Server _combinedTransformDirty = false; } - var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix.Identity; + var parentTransform = AdornedVisual?.GlobalTransformMatrix ?? parentVisualTransform; var newTransform = CombinedTransformMatrix * parentTransform; @@ -207,7 +212,7 @@ namespace Avalonia.Rendering.Composition.Server _combinedTransformedClipBounds = (AdornerIsClipped ? AdornedVisual?._combinedTransformedClipBounds : null) ?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null) - ?? new Rect(Root!.Size); + ?? new Rect(Root!.PixelSize.ToSize(1)); if (_transformedClipBounds != null) _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); @@ -301,7 +306,7 @@ namespace Avalonia.Rendering.Composition.Server protected override void ValuesInvalidated() { _isDirtyForUpdate = true; - Root?.Invalidate(); + Root?.RequestUpdate(); } public bool IsVisibleInFrame { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index cd4ef0317e..c9f4474654 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -38,6 +38,7 @@ namespace Avalonia.Rendering.Composition.Server internal static readonly object RenderThreadDisposeStartMarker = new(); internal static readonly object RenderThreadJobsStartMarker = new(); internal static readonly object RenderThreadJobsEndMarker = new(); + public CompositionOptions Options { get; } = AvaloniaLocator.Current.GetService() ?? new(); public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs index 165d08b282..483c4b26d0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Numerics; using Avalonia.Logging; using Avalonia.Media; @@ -16,15 +17,13 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer _handler.Attach(this); } - protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) + public void DispatchMessages(List messages) { - base.DeserializeChangesCore(reader, committedAt); - var count = reader.Read(); - for (var c = 0; c < count; c++) + foreach(var message in messages) { try { - _handler.OnMessage(reader.ReadObject()!); + _handler.OnMessage(message); } catch (Exception e) { @@ -58,6 +57,11 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer } internal void HandlerInvalidate() => ValuesInvalidated(); + + internal void HandlerInvalidate(Rect rc) + { + Root?.AddDirtyRect(rc.TransformToAABB(GlobalTransformMatrix)); + } internal void HandlerRegisterForNextAnimationFrameUpdate() { @@ -66,12 +70,13 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer Compositor.AddToClock(this); } - protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip, + IDirtyRectTracker dirtyRects) { using var context = new ImmediateDrawingContext(canvas, false); try { - _handler.OnRender(context); + _handler.Render(context, currentTransformedClip); } catch (Exception e) { diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 1b8aa9107e..d4a1278fe7 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -54,7 +54,7 @@ - + diff --git a/src/Avalonia.Controls/BorderVisual.cs b/src/Avalonia.Controls/BorderVisual.cs index 591604a3c5..85e3a64df3 100644 --- a/src/Avalonia.Controls/BorderVisual.cs +++ b/src/Avalonia.Controls/BorderVisual.cs @@ -45,7 +45,8 @@ class CompositionBorderVisual : CompositionDrawListVisual { } - protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip, + IDirtyRectTracker dirtyRects) { if (ClipToBounds) { @@ -56,7 +57,7 @@ class CompositionBorderVisual : CompositionDrawListVisual canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); } - base.RenderCore(canvas, currentTransformedClip); + base.RenderCore(canvas, currentTransformedClip, dirtyRects); if(ClipToBounds) canvas.PopClip(); diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 9db7694604..0fc2fd9343 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -115,7 +115,7 @@ namespace Avalonia.X11 using (var cpuContext = platformRenderInterface.CreateBackendContext(null)) using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this })) - using (var ctx = renderTarget.CreateDrawingContext()) + using (var ctx = renderTarget.CreateDrawingContext(true)) { var r = new Rect(_pixelSize.ToSize(1)); ctx.DrawBitmap(bitmap, 1, r, r); diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index cb09802d76..73b24bb4c5 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -43,7 +43,7 @@ namespace Avalonia.X11 _bdata = new uint[_width * _height]; using(var cpuContext = AvaloniaLocator.Current.GetRequiredService().CreateBackendContext(null)) using(var rt = cpuContext.CreateRenderTarget(new[]{this})) - using (var ctx = rt.CreateDrawingContext()) + using (var ctx = rt.CreateDrawingContext(true)) ctx.DrawBitmap(bitmap.PlatformImpl.Item, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); Data = new UIntPtr[_width * _height + 2]; diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index da46b45998..7c7eea1c6f 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -33,6 +33,8 @@ namespace Avalonia.Headless public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true; + public bool SupportsRegions => false; + public IPlatformRenderInterfaceRegion CreateRegion() => throw new NotSupportedException(); public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect); @@ -398,7 +400,7 @@ namespace Avalonia.Headless } - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool _) { return new HeadlessDrawingContextStub(); } @@ -454,7 +456,7 @@ namespace Avalonia.Headless } - public IDrawingContextLayerImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(PixelSize size) { return new HeadlessBitmapStub(size, new Vector(96, 96)); } @@ -464,11 +466,24 @@ namespace Avalonia.Headless } + public void PushClip(IPlatformRenderInterfaceRegion region) + { + + } + public void PopClip() { } + public void PushLayer(Rect bounds) + { + } + + public void PopLayer() + { + } + public void PushOpacity(double opacity, Rect? rect) { @@ -541,6 +556,11 @@ namespace Avalonia.Headless } + public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region) + { + + } + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { } @@ -573,7 +593,7 @@ namespace Avalonia.Headless } - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool _) { return new HeadlessDrawingContextStub(); } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index df82e7635a..be53f21af4 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -21,7 +21,9 @@ namespace Avalonia.Skia IDrawingContextImplWithEffects { private IDisposable?[]? _disposables; - private readonly Vector _dpi; + // TODO: Get rid of this value, it's currently used to calculate intermediate sizes for tile brushes + // but does so ignoring the current transform + private readonly Vector _intermediateSurfaceDpi; private readonly Stack _maskStack = new(); private readonly Stack _opacityStack = new(); private readonly Stack _renderOptionsStack = new(); @@ -57,7 +59,12 @@ namespace Avalonia.Skia public SKSurface? Surface; /// - /// Dpi of drawings. + /// Makes DPI to be applied as a hidden matrix transform + /// + public bool ScaleDrawingToDpi; + + /// + /// Dpi for intermediate surfaces /// public Vector Dpi; @@ -180,7 +187,7 @@ namespace Avalonia.Skia Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); - _dpi = createInfo.Dpi; + _intermediateSurfaceDpi = createInfo.Dpi; _disposables = disposables; _disableSubpixelTextRendering = createInfo.DisableSubpixelTextRendering; _grContext = createInfo.GrContext; @@ -191,10 +198,12 @@ namespace Avalonia.Skia _session = createInfo.CurrentSession; - if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi)) + + if (createInfo.ScaleDrawingToDpi && !createInfo.Dpi.NearlyEquals(SkiaPlatform.DefaultDpi)) { _postTransform = - Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); + Matrix.CreateScale(createInfo.Dpi.X / SkiaPlatform.DefaultDpi.X, + createInfo.Dpi.Y / SkiaPlatform.DefaultDpi.Y); } Transform = Matrix.Identity; @@ -526,6 +535,32 @@ namespace Avalonia.Skia SKRoundRectCache.Shared.Return(skRoundRect); } + /// + public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region) + { + var r = (SkiaRegionImpl)region; + if(r.IsEmpty) + return; + CheckLease(); + + if (brush != null) + { + using (var fill = CreatePaint(_fillPaint, brush, r.Bounds.ToRect(1))) + { + Canvas.DrawRegion(r.Region, fill.Paint); + } + } + + if (pen is not null + && TryCreatePaint(_strokePaint, pen, r.Bounds.ToRect(1).Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) + { + using (stroke) + { + Canvas.DrawRegion(r.Region, stroke.Paint); + } + } + } + /// public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { @@ -591,7 +626,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextLayerImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(PixelSize size) { CheckLease(); return CreateRenderTarget(size, true); @@ -629,6 +664,14 @@ namespace Avalonia.Skia SKRoundRectCache.Shared.Return(roundRect); } + public void PushClip(IPlatformRenderInterfaceRegion region) + { + var r = ((SkiaRegionImpl)region).Region; + CheckLease(); + Canvas.Save(); + Canvas.ClipRegion(r); + } + /// public void PopClip() { @@ -636,6 +679,18 @@ namespace Avalonia.Skia Canvas.Restore(); } + public void PushLayer(Rect bounds) + { + CheckLease(); + Canvas.SaveLayer(bounds.ToSKRect(), null!); + } + + public void PopLayer() + { + CheckLease(); + Canvas.Restore(); + } + /// public void PushOpacity(double opacity, Rect? bounds) { @@ -976,15 +1031,16 @@ namespace Avalonia.Skia /// Tile brush image. private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Rect targetBox, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage) { - var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSizeWithDpi(_dpi), targetBox.Size); - var intermediate = CreateRenderTarget(calc.IntermediateSize, false); + var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSizeWithDpi(_intermediateSurfaceDpi), targetBox.Size); + var intermediate = CreateRenderTarget( + PixelSize.FromSizeWithDpi(calc.IntermediateSize, _intermediateSurfaceDpi), false); paintWrapper.AddDisposable(intermediate); - using (var context = intermediate.CreateDrawingContext()) + using (var context = intermediate.CreateDrawingContext(true)) { var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96)); - var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi)); + var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_intermediateSurfaceDpi)); context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); @@ -1028,7 +1084,7 @@ namespace Avalonia.Skia SKMatrix.Concat( ref paintTransform, tileTransform, - SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + SKMatrix.CreateScale((float)(96.0 / _intermediateSurfaceDpi.X), (float)(96.0 / _intermediateSurfaceDpi.Y))); if (tileBrush.Transform is { }) { @@ -1066,9 +1122,10 @@ namespace Avalonia.Skia if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { - using var intermediate = CreateRenderTarget(intermediateSize, false); + using var intermediate = CreateRenderTarget( + PixelSize.FromSizeWithDpi(intermediateSize, _intermediateSurfaceDpi), false); - using (var ctx = intermediate.CreateDrawingContext()) + using (var ctx = intermediate.CreateDrawingContext(true)) { ctx.RenderOptions = RenderOptions; ctx.Clear(Colors.Transparent); @@ -1096,7 +1153,7 @@ namespace Avalonia.Skia var calc = new TileBrushCalculator(tileBrush, contentSize, targetRect.Size); transform *= calc.IntermediateTransform; - using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi); + using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _intermediateSurfaceDpi); using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize)) { ctx.RenderOptions = RenderOptions; @@ -1127,7 +1184,7 @@ namespace Avalonia.Skia : SKShaderTileMode.Repeat; paintTransform = SKMatrix.Concat(paintTransform, - SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + SKMatrix.CreateScale((float)(96.0 / _intermediateSurfaceDpi.X), (float)(96.0 / _intermediateSurfaceDpi.Y))); if (tileBrush.Transform is { }) { @@ -1339,18 +1396,18 @@ namespace Avalonia.Skia /// /// Create new render target compatible with this drawing context. /// - /// The size of the render target in DIPs. + /// The size of the render target. + /// The DPI of the render target. /// Whether the render target is being created for a layer. /// Pixel format. /// - private SurfaceRenderTarget CreateRenderTarget(Size size, bool isLayer, PixelFormat? format = null) + private SurfaceRenderTarget CreateRenderTarget(PixelSize pixelSize, bool isLayer, PixelFormat? format = null) { - var pixelSize = PixelSize.FromSizeWithDpi(size, _dpi); var createInfo = new SurfaceRenderTarget.CreateInfo { Width = pixelSize.Width, Height = pixelSize.Height, - Dpi = _dpi, + Dpi = _intermediateSurfaceDpi, Format = format, DisableTextLcdRendering = isLayer ? _disableSubpixelTextRendering : true, GrContext = _grContext, diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 2cdc9a2d82..8b50cd8295 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -37,7 +37,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool scaleDrawingToDpi) { if (_renderTarget == null) throw new ObjectDisposedException(nameof(FramebufferRenderTarget)); @@ -58,7 +58,8 @@ namespace Avalonia.Skia var createInfo = new DrawingContextImpl.CreateInfo { Surface = _framebufferSurface, - Dpi = framebuffer.Dpi + Dpi = framebuffer.Dpi, + ScaleDrawingToDpi = scaleDrawingToDpi }; return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer); diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 6db6083ba7..8cc7ed6868 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -21,7 +21,7 @@ namespace Avalonia.Skia _renderTarget.Dispose(); } - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) { var session = _renderTarget.BeginRenderingSession(); @@ -30,6 +30,7 @@ namespace Avalonia.Skia GrContext = session.GrContext, Surface = session.SkSurface, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, + ScaleDrawingToDpi = useScaledDrawing, Gpu = _skiaGpu, CurrentSession = session }; diff --git a/src/Skia/Avalonia.Skia/PictureRenderTarget.cs b/src/Skia/Avalonia.Skia/PictureRenderTarget.cs index 02cc1c0676..5d5494452a 100644 --- a/src/Skia/Avalonia.Skia/PictureRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/PictureRenderTarget.cs @@ -38,6 +38,7 @@ internal class PictureRenderTarget : IDisposable var createInfo = new DrawingContextImpl.CreateInfo { Canvas = canvas, + ScaleDrawingToDpi = true, Dpi = _dpi, DisableSubpixelTextRendering = true, GrContext = _grContext, diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 19df124910..e65d9c15e7 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -50,6 +50,9 @@ namespace Avalonia.Skia || format == PixelFormats.Bgra8888 || format == PixelFormats.Rgba8888; + public bool SupportsRegions => true; + public IPlatformRenderInterfaceRegion CreateRegion() => new SkiaRegionImpl(); + public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2); diff --git a/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs b/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs index a6a8fd1def..32b7340993 100644 --- a/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs @@ -19,7 +19,8 @@ internal class RenderTargetBitmapImpl : WriteableBitmapImpl, _renderTarget = new FramebufferRenderTarget(this); } - public IDrawingContextImpl CreateDrawingContext() => _renderTarget.CreateDrawingContext(); + IDrawingContextImpl IRenderTarget.CreateDrawingContext(bool useScaledDrawing) => + _renderTarget.CreateDrawingContext(useScaledDrawing); public bool IsCorrupted => false; diff --git a/src/Skia/Avalonia.Skia/SkiaRegionImpl.cs b/src/Skia/Avalonia.Skia/SkiaRegionImpl.cs new file mode 100644 index 0000000000..317cd932aa --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaRegionImpl.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal class SkiaRegionImpl : IPlatformRenderInterfaceRegion +{ + private SKRegion? _region = new(); + public SKRegion Region => _region ?? throw new ObjectDisposedException(nameof(SkiaRegionImpl)); + private bool _rectsValid; + private List? _rects; + public void Dispose() + { + _region?.Dispose(); + _region = null; + } + + public void AddRect(PixelRect rect) + { + _rectsValid = false; + Region.Op(rect.X, rect.Y, rect.Right, rect.Bottom, SKRegionOperation.Union); + } + + public void Reset() + { + _rectsValid = false; + Region.SetEmpty(); + } + + public bool IsEmpty => Region.IsEmpty; + public PixelRect Bounds => Region.Bounds.ToAvaloniaPixelRect(); + + public IList Rects + { + get + { + _rects ??= new(); + if (!_rectsValid) + { + _rects.Clear(); + using var iter = Region.CreateRectIterator(); + while (iter.Next(out var rc)) + _rects.Add(rc.ToAvaloniaPixelRect()); + } + return _rects; + } + } + + public bool Intersects(Rect rect) => Region.Intersects(PixelRect.FromRect(rect, 1).ToSKRectI()); + public bool Contains(Point pt) => Region.Contains((int)pt.X, (int)pt.Y); +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 44fe7aed89..32a89a58a5 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -75,6 +75,11 @@ namespace Avalonia.Skia { return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom); } + + public static SKRectI ToSKRectI(this PixelRect r) + { + return new SKRectI(r.X, r.Y, r.Right, r.Bottom); + } public static SKRoundRect ToSKRoundRect(this RoundedRect r) { @@ -95,6 +100,11 @@ namespace Avalonia.Skia { return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top); } + + public static PixelRect ToAvaloniaPixelRect(this SKRectI r) + { + return new PixelRect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top); + } public static SKMatrix ToSKMatrix(this Matrix m) { diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index c695e8ba41..9b5d104aad 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -97,7 +97,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) { _canvas.RestoreToCount(-1); _canvas.ResetMatrix(); @@ -106,6 +106,7 @@ namespace Avalonia.Skia { Surface = _surface.Surface, Dpi = Dpi, + ScaleDrawingToDpi = useScaledDrawing, DisableSubpixelTextRendering = _disableLcdRendering, GrContext = _grContext, Gpu = _gpu, diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 1c2682607d..21c5f51980 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -296,5 +296,8 @@ namespace Avalonia.Direct2D1 public bool IsSupportedBitmapPixelFormat(PixelFormat format) => format == PixelFormats.Bgra8888 || format == PixelFormats.Rgba8888; + + public bool SupportsRegions => false; + public IPlatformRenderInterfaceRegion CreateRegion() => throw new NotSupportedException(); } } diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 10f9239b1a..8aa6e5c0e1 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1 _externalRenderTargetProvider.DestroyRenderTarget(); } - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) { var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); _externalRenderTargetProvider.BeforeDrawing(); - return new DrawingContextImpl( null, target, null, () => + return new DrawingContextImpl( null, target, useScaledDrawing, null, () => { try { diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index fd21d9b5e1..23faf79e9f 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -25,7 +25,7 @@ namespace Avalonia.Direct2D1 _target = null; } - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) { if (_target == null) throw new ObjectDisposedException(nameof(FramebufferShimRenderTarget)); @@ -37,7 +37,7 @@ namespace Avalonia.Direct2D1 } return new FramebufferShim(locked) - .CreateDrawingContext(); + .CreateDrawingContext(useScaledDrawing); } public bool IsCorrupted => false; @@ -52,9 +52,9 @@ namespace Avalonia.Direct2D1 _target = target; } - public override IDrawingContextImpl CreateDrawingContext() + public override IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) { - return base.CreateDrawingContext(() => + return base.CreateDrawingContext(useScaledDrawing, () => { using (var l = WicImpl.Lock(BitmapLockFlags.Read)) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 1ac90dd5f9..e1d51f7054 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -39,6 +39,7 @@ namespace Avalonia.Direct2D1.Media public DrawingContextImpl( ILayerFactory layerFactory, SharpDX.Direct2D1.RenderTarget renderTarget, + bool useScaledDrawing, SharpDX.DXGI.SwapChain1 swapChain = null, Action finishedCallback = null) { @@ -58,6 +59,13 @@ namespace Avalonia.Direct2D1.Media _ownsDeviceContext = true; } + if (!useScaledDrawing) + { + var scaling = _renderTarget.DotsPerInch.Width / 96; + if (!MathUtilities.AreClose(1, scaling)) + _postTransform = Matrix.CreateScale(1 / scaling, 1 / scaling); + } + _deviceContext.BeginDraw(); } @@ -66,8 +74,13 @@ namespace Avalonia.Direct2D1.Media /// public Matrix Transform { - get { return _deviceContext.Transform.ToAvalonia(); } - set { _deviceContext.Transform = value.ToDirect2D(); } + get { return _transform; } + set + { + _transform = value; + _deviceContext.Transform = + (_postTransform.HasValue ? value * _postTransform.Value : value).ToDirect2D(); + } } public Matrix4x4 Transform4x4 @@ -353,6 +366,11 @@ namespace Avalonia.Direct2D1.Media } } + public void DrawRegion(IBrush brush, IPen pen, IPlatformRenderInterfaceRegion region) + { + throw new NotSupportedException(); + } + /// public void DrawEllipse(IBrush brush, IPen pen, Rect rect) { @@ -410,17 +428,17 @@ namespace Avalonia.Direct2D1.Media } } - public IDrawingContextLayerImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(PixelSize pixelSize) { + var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height); if (_layerFactory != null) { - return _layerFactory.CreateLayer(size); + return _layerFactory.CreateLayer(pixelSize.ToSizeWithDpi(dpi)); } else { var platform = AvaloniaLocator.Current.GetRequiredService(); - var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height); - var pixelSize = PixelSize.FromSizeWithDpi(size, dpi); + return (IDrawingContextLayerImpl)platform.CreateRenderTargetBitmap(pixelSize, dpi); } } @@ -441,14 +459,40 @@ namespace Avalonia.Direct2D1.Media _deviceContext.PushAxisAlignedClip(clip.Rect.ToDirect2D(), AntialiasMode.PerPrimitive); } + public void PushClip(IPlatformRenderInterfaceRegion region) + { + throw new NotSupportedException(); + } + public void PopClip() { _deviceContext.PopAxisAlignedClip(); } + public void PushLayer(Rect bounds) + { + var parameters = new LayerParameters + { + ContentBounds = bounds.ToDirect2D(), + MaskTransform = PrimitiveExtensions.Matrix3x2Identity, + Opacity = 1 + }; + var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext); + _deviceContext.PushLayer(ref parameters, layer); + + _layers.Push(layer); + } + + void IDrawingContextImpl.PopLayer() + { + PopLayer(); + } + readonly Stack _layers = new Stack(); private readonly Stack _layerPool = new Stack(); private RenderOptions _renderOptions; + private readonly Matrix? _postTransform; + private Matrix _transform = Matrix.Identity; /// /// Pushes an opacity value. @@ -574,7 +618,7 @@ namespace Avalonia.Direct2D1.Media CompatibleRenderTargetOptions.None, pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) { - using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(true)) { intermediate.Clear(null); sceneBrushContent.Render(ctx, diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index fdefe21acd..ef4d91df9d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -98,7 +98,7 @@ namespace Avalonia.Direct2D1.Media CompatibleRenderTargetOptions.None, calc.IntermediateSize.ToSharpDX()); - using (var context = new RenderTarget(result).CreateDrawingContext()) + using (var context = new RenderTarget(result).CreateDrawingContext(true)) { var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height); var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi)); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 5d38751bde..6d2d12504b 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -30,9 +30,10 @@ namespace Avalonia.Direct2D1.Media.Imaging return new D2DRenderTargetBitmapImpl(bitmapRenderTarget); } - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) { - return new DrawingContextImpl( this, _renderTarget, null, () => Version++); + return new DrawingContextImpl( this, _renderTarget, useScaledDrawing, + null, () => Version++); } public bool IsCorrupted => false; @@ -60,7 +61,7 @@ namespace Avalonia.Direct2D1.Media.Imaging { using (var wic = new WicRenderTargetBitmapImpl(PixelSize, Dpi)) { - using (var dc = wic.CreateDrawingContext(null)) + using (var dc = wic.CreateDrawingContext(true, null)) { dc.DrawBitmap( this, diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index fa40e75fa7..1120366a8e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media base.Dispose(); } - public virtual IDrawingContextImpl CreateDrawingContext() - => CreateDrawingContext(null); + public virtual IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) + => CreateDrawingContext(useScaledDrawing, null); public bool IsCorrupted => false; - public IDrawingContextImpl CreateDrawingContext(Action finishedCallback) + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing, Action finishedCallback) { - return new DrawingContextImpl(null, _renderTarget, finishedCallback: () => + return new DrawingContextImpl(null, _renderTarget, useScaledDrawing, finishedCallback: () => { Version++; finishedCallback?.Invoke(); diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index 4392e35058..ecefcd9dad 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -25,9 +25,9 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) { - return new DrawingContextImpl(this, _renderTarget); + return new DrawingContextImpl(this, _renderTarget, useScaledDrawing); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index 385120505c..228ae6e460 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -19,7 +19,7 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) { var size = GetWindowSize(); var dpi = GetWindowDpi(); @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 Resize(); } - return new DrawingContextImpl(this, _deviceContext, _swapChain); + return new DrawingContextImpl(this, _deviceContext, useScaledDrawing, _swapChain); } public bool IsCorrupted => false; diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 1b16617f87..6ddeda1b5d 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -72,7 +72,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media var r = AvaloniaLocator.Current.GetRequiredService(); using(var cpuContext = r.CreateBackendContext(null)) using (var target = cpuContext.CreateRenderTarget(new object[] { fb })) - using (var ctx = target.CreateDrawingContext()) + using (var ctx = target.CreateDrawingContext(false)) { ctx.Clear(Colors.Transparent); ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80)); diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index f62afaf74d..ae62301d69 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -79,16 +79,16 @@ namespace Avalonia.UnitTests public IRenderTarget CreateRenderTarget() { var dc = new Mock(); - dc.Setup(x => x.CreateLayer(It.IsAny())).Returns(() => + dc.Setup(x => x.CreateLayer(It.IsAny())).Returns(() => { var layerDc = new Mock(); var layer = new Mock(); - layer.Setup(x => x.CreateDrawingContext()).Returns(layerDc.Object); + layer.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(layerDc.Object); return layer.Object; }); var result = new Mock(); - result.Setup(x => x.CreateDrawingContext()).Returns(dc.Object); + result.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(dc.Object); return result.Object; }