From 697cba719370a83ccd6656ff7df433c47edba545 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 6 Feb 2026 16:15:47 +0500 Subject: [PATCH] Composition system SP1 (#20497) * baseline benchmark * Composition system SP1 * Allow reusing the same BitmapCache object for multiple visuals * Fix warnings * Address review * API suppressions --------- Co-authored-by: Julien Lebosquain --- api/Avalonia.nupkg.xml | 40 +- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/BitmapCachePage.axaml | 45 +++ .../Pages/BitmapCachePage.axaml.cs | 13 + src/Avalonia.Base/Layout/Layoutable.cs | 12 - src/Avalonia.Base/Media/BitmapCache.cs | 114 ++++++ src/Avalonia.Base/Media/CacheMode.cs | 21 ++ .../Platform/IPlatformRenderInterface.cs | 8 +- src/Avalonia.Base/Platform/LtrbRect.cs | 70 +++- .../Composition/CompositingRenderer.cs | 5 +- .../CompositionCustomVisualHandler.cs | 10 +- .../Composition/CompositionDrawListVisual.cs | 5 + .../Composition/CompositionOptions.cs | 18 + .../Composition/CompositionTarget.cs | 77 ++-- .../Drawing/CompositorResourceHelpers.cs | 5 +- .../ICompositionTargetDebugEvents.cs | 8 +- .../Rendering/Composition/MatrixUtils.cs | 6 +- .../Server/CompositionTargetOverlays.cs | 15 +- .../Composition/Server/CompositorPools.cs | 41 +++ .../Composition/Server/DirtyRectTracker.cs | 105 ------ .../DebugEventsDirtyRectCollectorProxy.cs | 13 + .../Server/DirtyRects/IDirtyRectTracker.cs | 24 ++ .../MultiDirtyRectTracker.CDirtyRegion.cs | 348 ++++++++++++++++++ .../DirtyRects/MultiDirtyRectTracker.cs | 86 +++++ .../DirtyRects/RegionDirtyRectTracker.cs | 60 +++ .../DirtyRects/SingleDirtyRectTracker.cs | 50 +++ .../Composition/Server/ReadbackIndices.cs | 45 +-- .../Server/ServerCompositionBitmapCache.cs | 12 + .../Server/ServerCompositionCacheMode.cs | 24 ++ .../ServerCompositionContainerVisual.cs | 95 +---- .../Server/ServerCompositionDrawListVisual.cs | 15 +- ...verCompositionExperimentalAcrylicVisual.cs | 24 +- .../Server/ServerCompositionSurfaceVisual.cs | 2 +- .../ServerCompositionTarget.DirtyRects.cs | 48 --- .../Server/ServerCompositionTarget.cs | 104 ++++-- ...ServerCompositionVisual.DirtyProperties.cs | 78 ---- .../Server/ServerCompositionVisual.cs | 343 ----------------- .../ServerCompositionVisual.Act.cs | 89 +++++ .../ServerCompositionVisual.Adorners.cs | 168 +++++++++ ...verCompositionVisual.ComputedProperties.cs | 164 +++++++++ .../ServerCompositionVisual.DirtyInputs.cs | 191 ++++++++++ .../ServerCompositionVisual.Readback.cs | 106 ++++++ .../ServerCompositionVisual.Render.cs | 251 +++++++++++++ .../ServerCompositionVisual.Update.cs | 245 ++++++++++++ .../ServerCompositionVisual.Walker.cs | 135 +++++++ .../ServerCompositionVisual.cs | 82 +++++ .../ServerCompositionVisualCache.cs | 225 +++++++++++ .../Server/ServerCompositor.Passes.cs | 74 ++++ .../ServerCompositor.RenderResources.cs | 22 -- .../Server/ServerCompositor.UserApis.cs | 11 +- .../Composition/Server/ServerCompositor.cs | 32 +- .../Server/ServerCustomCompositionVisual.cs | 23 +- .../Composition/Server/ServerObject.cs | 2 +- .../Server/ServerSizeDependantVisual.cs | 25 ++ .../Server/ServerVisualRenderContext.cs | 66 +--- .../Rendering/Composition/Visual.cs | 49 ++- src/Avalonia.Base/Utilities/MathUtilities.cs | 2 +- src/Avalonia.Base/Visual.Composition.cs | 4 + src/Avalonia.Base/Visual.cs | 23 +- .../VisualTree/VisualExtensions.cs | 9 +- src/Avalonia.Base/composition-schema.xml | 16 +- src/Avalonia.Controls/BorderVisual.cs | 30 +- .../HeadlessPlatformRenderInterface.cs | 6 +- src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 2 + src/Skia/Avalonia.Skia/SkiaBackendContext.cs | 17 +- .../CompositionGenerator/Generator.cs | 9 +- .../CompositorInvalidationClippingTests.cs | 54 +-- .../Compositor/CompositionTargetUpdate.cs | 121 ++++++ .../ButtonTests.cs | 4 +- .../Avalonia.RenderTests/OpacityMaskTests.cs | 2 + .../CompositorTestServices.cs | 17 +- ...tised_advertized-True_updated.expected.png | Bin 358 -> 395 bytes 72 files changed, 3222 insertions(+), 1046 deletions(-) create mode 100644 samples/ControlCatalog/Pages/BitmapCachePage.axaml create mode 100644 samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs create mode 100644 src/Avalonia.Base/Media/BitmapCache.cs create mode 100644 src/Avalonia.Base/Media/CacheMode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/CompositorPools.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/DebugEventsDirtyRectCollectorProxy.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/IDirtyRectTracker.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/RegionDirtyRectTracker.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/SingleDirtyRectTracker.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBitmapCache.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionCacheMode.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Walker.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.Passes.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerSizeDependantVisual.cs create mode 100644 tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 75154ef969..3d33c5de44 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -625,6 +625,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0002 M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action) @@ -1267,6 +1273,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0002 M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action) @@ -1609,12 +1621,24 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,Avalonia.Vector,System.Boolean) + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 P:Avalonia.Platform.ILockedFramebuffer.AlphaFormat baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + P:Avalonia.Platform.IPlatformRenderInterfaceContext.MaxOffscreenRenderTargetPixelSize + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 P:Avalonia.Platform.IReadableBitmapImpl.AlphaFormat @@ -1759,6 +1783,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,Avalonia.Vector,System.Boolean) + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64) @@ -1777,6 +1807,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + P:Avalonia.Platform.IPlatformRenderInterfaceContext.MaxOffscreenRenderTargetPixelSize + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 P:Avalonia.Platform.IReadableBitmapImpl.AlphaFormat @@ -2269,4 +2305,4 @@ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll - + \ No newline at end of file diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 984daf1200..803ca52254 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -34,6 +34,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/BitmapCachePage.axaml b/samples/ControlCatalog/Pages/BitmapCachePage.axaml new file mode 100644 index 0000000000..5902be8e51 --- /dev/null +++ b/samples/ControlCatalog/Pages/BitmapCachePage.axaml @@ -0,0 +1,45 @@ + + + + + + + + + Render at scale + + Scale + + Enable clear type + + Snap to device pixels + Subpixel offset X + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + + + + + diff --git a/samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs b/samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs new file mode 100644 index 0000000000..92b3cbec68 --- /dev/null +++ b/samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages; + +public partial class BitmapCachePage : UserControl +{ + public BitmapCachePage() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 277548b07e..9fa0f7689f 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -951,17 +951,5 @@ namespace Avalonia.Layout { return new Size(Math.Max(size.Width, 0), Math.Max(size.Height, 0)); } - - internal override void SynchronizeCompositionProperties() - { - base.SynchronizeCompositionProperties(); - - if (CompositionVisual is { } visual) - { - // If the visual isn't using layout rounding, it's possible that antialiasing renders to pixels - // outside the current bounds. Extend the dirty rect by 1px in all directions in this case. - visual.ShouldExtendDirtyRect = !UseLayoutRounding; - } - } } } diff --git a/src/Avalonia.Base/Media/BitmapCache.cs b/src/Avalonia.Base/Media/BitmapCache.cs new file mode 100644 index 0000000000..94caa1f557 --- /dev/null +++ b/src/Avalonia.Base/Media/BitmapCache.cs @@ -0,0 +1,114 @@ +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Media; + +/// +/// Represents the behavior of caching a visual element or tree of elements as bitmap surfaces. +/// +public class BitmapCache : CacheMode +{ + private CompositionBitmapCache? _current; + + public static readonly StyledProperty RenderAtScaleProperty = AvaloniaProperty.Register( + nameof(RenderAtScale), 1); + + /// + /// Use the RenderAtScale property to render the BitmapCache at a multiple of the normal bitmap size. + /// The normal size is determined by the local size of the element. + /// + /// Values greater than 1 increase the resolution of the bitmap relative to the native resolution of the element, + /// and values less than 1 decrease the resolution. + /// For example, if the RenderAtScale property is set to 2.0, and you apply a scale transform that + /// enlarges the content by a factor of 2, the content will have the same visual quality as the same content + /// with RenderAtScale set to 1.0 and a transform scale of 1. + /// + /// When RenderAtScale is set to 0, no bitmap is rendered. Negative values are clamped to 0. + /// + /// If you change this value, the cache is regenerated at the appropriate new resolution. + /// + public double RenderAtScale + { + get => GetValue(RenderAtScaleProperty); + set => SetValue(RenderAtScaleProperty, value); + } + + public static readonly StyledProperty SnapsToDevicePixelsProperty = AvaloniaProperty.Register( + nameof(SnapsToDevicePixels)); + + /// + /// Set the SnapsToDevicePixels property when the cache displays content that requires pixel-alignment to render correctly. + /// This is the case for text with subpixel antialiasing. If you set the EnableClearType property to true, + /// consider setting SnapsToDevicePixels to true to ensure proper rendering. + /// + /// When the SnapsToDevicePixels property is set to false, + /// you can move and scale the cached element by a fraction of a pixel. + /// + /// When the SnapsToDevicePixels property is set to true, + /// the bitmap cache is aligned with pixel boundaries of the destination. + /// If you move or scale the cached element by a fraction of a pixel, + /// the bitmap snaps to the pixel grid + /// . In this case, the top-left corner of the bitmap is rounded up and snapped to the pixel grid, + /// but the bottom-right corner is on a fractional pixel boundary. + /// + public bool SnapsToDevicePixels + { + get => GetValue(SnapsToDevicePixelsProperty); + set => SetValue(SnapsToDevicePixelsProperty, value); + } + + public static readonly StyledProperty EnableClearTypeProperty = AvaloniaProperty.Register( + nameof(EnableClearType)); + + /// + /// Set the EnableClearType property to allow subpixel text to be rendered in the cache. + /// When the EnableClearType property is true, your application MUST render all + /// of its subpixel text on an opaque background. + /// + /// When the EnableClearType property is false, text in the cache is rendered with grayscale antialiasing. + /// + /// ClearType text requires correct pixel alignment of rendered characters, + /// so you should set the SnapsToDevicePixels property to true. + /// If you do not set this property, the content may not blend correctly. + /// + /// Use the EnableClearType property when you know the cache is rendered on pixel boundaries, + /// so it is safe to cache ClearType text. This situation occurs commonly in text-scrolling scenarios. + /// + public bool EnableClearType + { + get => GetValue(EnableClearTypeProperty); + set => SetValue(EnableClearTypeProperty, value); + } + + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.IsEffectiveValueChange && _current != null) + { + if (change.Property == RenderAtScaleProperty) + _current.RenderAtScale = RenderAtScale; + else if (change.Property == SnapsToDevicePixelsProperty) + _current.SnapsToDevicePixels = SnapsToDevicePixels; + else if (change.Property == EnableClearTypeProperty) + _current.EnableClearType = EnableClearType; + } + + base.OnPropertyChanged(change); + } + + // We currently only allow visual to be attached to one compositor at a time, so keep it simple for now + internal override CompositionCacheMode GetForCompositor(Compositor c) + { + // TODO: Make it to be a multi-compositor resource once we support visuals being attached to multiple + // compositor instances (e. g. referenced via visual brush from a different WASM toplevel). + if(_current?.Compositor != c) + { + _current = new CompositionBitmapCache(c, new ServerCompositionBitmapCache(c.Server)); + _current.EnableClearType = EnableClearType; + _current.RenderAtScale = RenderAtScale; + _current.SnapsToDevicePixels = SnapsToDevicePixels; + } + + return _current; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/CacheMode.cs b/src/Avalonia.Base/Media/CacheMode.cs new file mode 100644 index 0000000000..44784eb29c --- /dev/null +++ b/src/Avalonia.Base/Media/CacheMode.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; + +namespace Avalonia.Media; + +/// +/// Represents cached content modes for graphics acceleration features. +/// +public abstract class CacheMode : StyledElement +{ + // We currently only allow visual to be attached to one compositor at a time, so keep it simple for now + internal abstract CompositionCacheMode GetForCompositor(Compositor c); + + public static CacheMode Parse(string s) + { + if(s == "BitmapCache") + return new BitmapCache(); + throw new ArgumentException("Unknown CacheMode: " + s); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 2deb289ddf..3a42a88aed 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -221,8 +221,9 @@ namespace Avalonia.Platform /// /// The size, in pixels, of the render target /// The scaling which will be reported by IBitmap.Dpi + /// Specifies if text antialiasing should be enabled /// - IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling); + IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, Vector scaling, bool enableTextAntialiasing); /// /// Indicates that the context is no longer usable. This method should be thread-safe @@ -233,5 +234,10 @@ namespace Avalonia.Platform /// Exposes features that should be available for consumption while context isn't active (e. g. from the UI thread) /// IReadOnlyDictionary PublicFeatures { get; } + + /// + /// Maximum supported offscreen render target pixel size, or null if no limit + /// + public PixelSize? MaxOffscreenRenderTargetPixelSize { get; } } } diff --git a/src/Avalonia.Base/Platform/LtrbRect.cs b/src/Avalonia.Base/Platform/LtrbRect.cs index 188f75c010..97cbec0ab2 100644 --- a/src/Avalonia.Base/Platform/LtrbRect.cs +++ b/src/Avalonia.Base/Platform/LtrbRect.cs @@ -23,6 +23,9 @@ public struct LtrbRect { public double Left, Top, Right, Bottom; + public double Width => Right - Left; + public double Height => Bottom - Top; + internal LtrbRect(double x, double y, double right, double bottom) { Left = x; @@ -40,9 +43,37 @@ public struct LtrbRect Bottom = rc.Bottom; } - internal bool IsZeroSize => Left == Right && Top == Bottom; + internal static LtrbRect Infinite { get; } = new(double.NegativeInfinity, double.NegativeInfinity, + double.PositiveInfinity, double.PositiveInfinity); + + internal bool IsWellOrdered => Left <= Right && Top <= Bottom; + + internal bool IsZeroSize => Left == Right || Top == Bottom; + internal bool IsEmpty => IsZeroSize; + + + + internal LtrbRect? NullIfZeroSize() => IsZeroSize ? null : this; - internal LtrbRect Intersect(LtrbRect rect) + internal LtrbRect? IntersectOrNull(LtrbRect rect) + { + var newLeft = (rect.Left > Left) ? rect.Left : Left; + var newTop = (rect.Top > Top) ? rect.Top : Top; + var newRight = (rect.Right < Right) ? rect.Right : Right; + var newBottom = (rect.Bottom < Bottom) ? rect.Bottom : Bottom; + + if ((newRight > newLeft) && (newBottom > newTop)) + { + return new LtrbRect(newLeft, newTop, newRight, newBottom); + } + else + { + return default; + } + } + + + internal LtrbRect IntersectOrEmpty(LtrbRect rect) { var newLeft = (rect.Left > Left) ? rect.Left : Left; var newTop = (rect.Top > Top) ? rect.Top : Top; @@ -118,7 +149,7 @@ public struct LtrbRect /// /// Perform _WPF-like_ union operation /// - private LtrbRect FullUnionCore(LtrbRect rect) + public LtrbRect Union(LtrbRect rect) { var x1 = Math.Min(Left, rect.Left); var x2 = Math.Max(Right, rect.Right); @@ -134,7 +165,7 @@ public struct LtrbRect return right; if (right == null) return left; - return right.Value.FullUnionCore(left.Value); + return right.Value.Union(left.Value); } internal static LtrbRect? FullUnion(LtrbRect? left, Rect? right) @@ -143,7 +174,7 @@ public struct LtrbRect return left; if (left == null) return new(right.Value); - return left.Value.FullUnionCore(new(right.Value)); + return left.Value.Union(new(right.Value)); } public override bool Equals(object? obj) @@ -165,6 +196,18 @@ public struct LtrbRect return hash; } } + + public override string ToString() => $"{Left}:{Top}-{Right}:{Bottom} ({Width}x{Height})"; + + public bool Contains(Point point) + { + return point.X >= Left && point.X <= Right && point.Y >= Top && point.Y <= Bottom; + } + + public bool Contains(LtrbRect rect) + { + return rect.Left >= Left && rect.Right <= Right && rect.Top >= Top && rect.Bottom <= Bottom; + } } /// @@ -184,7 +227,7 @@ public struct LtrbRect public struct LtrbPixelRect { public int Left, Top, Right, Bottom; - + internal LtrbPixelRect(int x, int y, int right, int bottom) { Left = x; @@ -218,16 +261,10 @@ public struct LtrbPixelRect return new(x1, y1, x2, y2); } - internal Rect ToRectWithNoScaling() => new(Left, Top, (Right - Left), (Bottom - Top)); - internal bool Contains(int x, int y) { return x >= Left && x <= Right && y >= Top && y <= Bottom; } - - internal static LtrbPixelRect FromRectWithNoScaling(LtrbRect rect) => - new((int)rect.Left, (int)rect.Top, (int)Math.Ceiling(rect.Right), - (int)Math.Ceiling(rect.Bottom)); public static bool operator ==(LtrbPixelRect left, LtrbPixelRect right)=> left.Left == right.Left && left.Top == right.Top && left.Right == right.Right && left.Bottom == right.Bottom; @@ -259,10 +296,9 @@ public struct LtrbPixelRect } internal Rect ToRectUnscaled() => new(Left, Top, Right - Left, Bottom - Top); - - internal static LtrbPixelRect FromRectUnscaled(LtrbRect rect) - { - return new LtrbPixelRect((int)rect.Left, (int)rect.Top, (int)Math.Ceiling(rect.Right), + internal LtrbRect ToLtrbRectUnscaled() => new(Left, Top, Right, Bottom); + + internal static LtrbPixelRect FromRectUnscaled(LtrbRect rect) => + new((int)rect.Left, (int)rect.Top, (int)Math.Ceiling(rect.Right), (int)Math.Ceiling(rect.Bottom)); - } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 6a0396e52a..dd81219168 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -180,11 +180,12 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester CompositionTarget.PixelSize = PixelSize.FromSizeRounded(_root.ClientSize, _root.RenderScaling); CompositionTarget.Scaling = _root.RenderScaling; - var commit = _compositor.RequestCommitAsync(); + var commit = _compositor.RequestCompositionBatchCommitAsync(); if (!_queuedSceneInvalidation) { _queuedSceneInvalidation = true; - commit.ContinueWith(_ => Dispatcher.UIThread.Post(() => + // Updated hit-test information is available after full render + commit.Rendered.ContinueWith(_ => Dispatcher.UIThread.Post(() => { _queuedSceneInvalidation = false; SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs index 8cf4cca0a5..319226a6ab 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs @@ -12,6 +12,7 @@ public abstract class CompositionCustomVisualHandler private ServerCompositionCustomVisual? _host; private bool _inRender; private Rect _currentTransformedClip; + private Matrix _currentTransform; public virtual void OnMessage(object message) { @@ -27,6 +28,7 @@ public abstract class CompositionCustomVisualHandler { _inRender = true; _currentTransformedClip = currentTransformedClip; + _currentTransform = drawingContext.CurrentTransform; try { OnRender(drawingContext); @@ -97,14 +99,14 @@ public abstract class CompositionCustomVisualHandler protected bool RenderClipContains(Point pt) { VerifyInRender(); - pt *= _host!.GlobalTransformMatrix; - return _currentTransformedClip.Contains(pt) && _host.Root!.DirtyRects.Contains(pt); + pt = pt.Transform(_currentTransform); + return _currentTransformedClip.Contains(pt); } protected bool RenderClipIntersectes(Rect rc) { VerifyInRender(); - rc = rc.TransformToAABB(_host!.GlobalTransformMatrix); - return _currentTransformedClip.Intersects(rc) && _host.Root!.DirtyRects.Intersects(new (rc)); + rc = rc.TransformToAABB(_currentTransform); + return _currentTransformedClip.Intersects(rc); } } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index c4e2038450..6f4bb3b882 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -25,6 +25,10 @@ internal class CompositionDrawListVisual : CompositionContainerVisual get => _drawList; set { + // Nothing to do + if (value == null && _drawList == null) + return; + _drawList?.Dispose(); _drawList = value; _drawListChanged = true; @@ -46,6 +50,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) { Visual = visual; + CustomHitTestCountInSubTree = visual is ICustomHitTest ? 1 : 0; } internal override bool HitTest(Point pt) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs b/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs index ad40a2abed..f474f9382d 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs @@ -1,3 +1,5 @@ +using Avalonia.Metadata; + namespace Avalonia.Rendering.Composition; public class CompositionOptions @@ -7,6 +9,22 @@ public class CompositionOptions /// drawing context /// public bool? UseRegionDirtyRectClipping { get; set; } + + /// + /// The maximum number of dirty rects to track when region clip is in use. Setting this to zero or negative + /// value will remove the smarter algorithm and will use underlying drawing context region support directly. + /// Default value is 8. + /// + public int? MaxDirtyRects { get; set; } + + + /// + /// Controls the eagerness of merging dirty rects. WPF uses 50000, Avalonia currently has a different default + /// that's a subject to change. You can play with this property to find the best value for your application. + /// + [Unstable] + public double? DirtyRectMergeEagerness { get; set; } + /// /// Enforces dirty contents to be rendered into an extra intermediate surface before being applied onto the /// saved frame. diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index f00eef0d10..659d379517 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -29,63 +29,26 @@ namespace Avalonia.Rendering.Composition /// public PooledList? TryHitTest(Point point, CompositionVisual? root, Func? filter) { - point *= Scaling; - Server.Readback.NextRead(); + Server.Compositor.Readback.NextRead(); root ??= Root; if (root == null) return null; + var res = new PooledList(); + + // Need to convert transform the point using visual's readback since HitTestCore will use its inverse matrix + // NOTE: it can technically break hit-testing of the root visual itself if it has a non-identity transform, + // need to investigate that possibility later. We might want a separate mode for root hit-testing. + var readback = root.TryGetValidReadback(); + if (readback == null) + return null; + point = point.Transform(readback.Matrix); + HitTestCore(root, point, res, filter); return res; } - - /// - /// Attempts to transform a point to a particular CompositionVisual coordinate space - /// - /// - public Point? TryTransformToVisual(CompositionVisual visual, Point point) - { - if (visual.Root != this) - return null; - var v = visual; - var m = Matrix.Identity; - while (v != null) - { - if (!TryGetInvertedTransform(v, out var cm)) - return null; - m = m * cm; - v = v.Parent; - } - - return point * m; - } - - static bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix) - { - var m = visual.TryGetServerGlobalTransform(); - if (m == null) - { - matrix = default; - return false; - } - - var m33 = m.Value; - return m33.TryInvert(out matrix); - } - - static bool TryTransformTo(CompositionVisual visual, Point globalPoint, out Point v) - { - v = default; - if (TryGetInvertedTransform(visual, out var m)) - { - v = globalPoint * m; - return true; - } - - return false; - } - void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList result, + void HitTestCore(CompositionVisual visual, Point parentPoint, PooledList result, Func? filter) { if (visual.Visible == false) @@ -93,10 +56,22 @@ namespace Avalonia.Rendering.Composition if (filter != null && !filter(visual)) return; + + var readback = visual.TryGetValidReadback(); + if(readback == null) + return; + + + if (!visual.DisableSubTreeBoundsHitTestOptimization && + (readback.TransformedSubtreeBounds == null || + !readback.TransformedSubtreeBounds.Value.Contains(parentPoint))) + return; - if (!TryTransformTo(visual, globalPoint, out var point)) + if(!readback.Matrix.TryInvert(out var invMatrix)) return; + var point = parentPoint.Transform(invMatrix); + if (visual.ClipToBounds && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) return; @@ -109,7 +84,7 @@ namespace Avalonia.Rendering.Composition for (var c = cv.Children.Count - 1; c >= 0; c--) { var ch = cv.Children[c]; - HitTestCore(ch, globalPoint, result, filter); + HitTestCore(ch, point, result, filter); } // Hit-test the current node diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs index 59e0bc848e..78ddea2362 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs @@ -49,7 +49,7 @@ internal struct CompositorResourceHolder where T : SimpleServerObject public bool IsAttached => _dictionary.HasEntries; - public bool CreateOrAddRef(Compositor compositor, ICompositorSerializable owner, out T resource, Func factory) + public bool CreateOrAddRef(Compositor compositor, ICompositorSerializable? owner, out T resource, Func factory) { if (_dictionary.TryGetValue(compositor, out var handle)) { @@ -60,7 +60,8 @@ internal struct CompositorResourceHolder where T : SimpleServerObject resource = factory(compositor); _dictionary.Add(compositor, new CompositorRefCountableResource(resource)); - compositor.RegisterForSerialization(owner); + if (owner != null) + compositor.RegisterForSerialization(owner); return true; } diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs index 27aca436b8..cd15f96bee 100644 --- a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs +++ b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs @@ -1,8 +1,10 @@ +using Avalonia.Platform; + namespace Avalonia.Rendering.Composition; internal interface ICompositionTargetDebugEvents { - int RenderedVisuals { get; } - void IncrementRenderedVisuals(); - void RectInvalidated(Rect rc); + int RenderedVisuals { get; set; } + int VisitedVisuals { get; set; } + void RectInvalidated(LtrbRect rc); } diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs index 4ec1326103..b568177acf 100644 --- a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -4,13 +4,13 @@ namespace Avalonia.Rendering.Composition { static class MatrixUtils { - public static Matrix ComputeTransform(Vector size, Vector anchorPoint, Vector3D centerPoint, + public static Matrix? ComputeTransform(Vector size, Vector anchorPoint, Vector3D centerPoint, Matrix transformMatrix, Vector3D scale, float rotationAngle, Quaternion orientation, Vector3D offset) { // The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work var anchor = Vector.Multiply(size, anchorPoint); - var mat = Matrix.CreateTranslation(-anchor.X, -anchor.Y); + var mat = Matrix.CreateTranslation(-anchor.X, -anchor.Y); var center = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z); @@ -45,6 +45,8 @@ namespace Avalonia.Rendering.Composition mat *= ToMatrix(Matrix4x4.CreateTranslation(offset.ToVector3())); } + if (mat.IsIdentity) + return null; return mat; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs index 7038e9cd46..a8c2908b88 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs @@ -11,6 +11,7 @@ internal class CompositionTargetOverlays { private FpsCounter? _fpsCounter; private FrameTimeGraph? _renderTimeGraph; + private FrameTimeGraph? _compositorUpdateTimeGraph; private FrameTimeGraph? _updateTimeGraph; private FrameTimeGraph? _layoutTimeGraph; private Rect? _oldFpsCounterRect; @@ -36,9 +37,12 @@ internal class CompositionTargetOverlays private FrameTimeGraph? RenderTimeGraph => _renderTimeGraph ??= CreateTimeGraph("Render"); + + private FrameTimeGraph? CompositorUpdateTimeGraph + => _compositorUpdateTimeGraph ??= CreateTimeGraph("GUpdate"); private FrameTimeGraph? UpdateTimeGraph - => _updateTimeGraph ??= CreateTimeGraph("RUpdate"); + => _updateTimeGraph ??= CreateTimeGraph("TUpdate"); @@ -108,6 +112,12 @@ internal class CompositionTargetOverlays if (CaptureTiming) UpdateTimeGraph?.AddFrameValue(StopwatchHelper.GetElapsedTime(_updateStarted).TotalMilliseconds); } + + public void RecordGlobalCompositorUpdateTime(TimeSpan elapsed) + { + if (CaptureTiming) + CompositorUpdateTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds); + } private void DrawOverlays(ImmediateDrawingContext targetContext, bool hasLayer, Size logicalSize) { @@ -122,7 +132,7 @@ internal class CompositionTargetOverlays IntPtr.Size), false); _oldFpsCounterRect = FpsCounter?.RenderFps(targetContext, - FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{_target.RenderedVisuals:0000}"), + FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} V:{_target.VisitedVisuals:0000} R:{_target.RenderedVisuals:0000}"), hasLayer, _oldFpsCounterRect); } @@ -147,6 +157,7 @@ internal class CompositionTargetOverlays if (DebugOverlays.HasFlag(RendererDebugOverlays.RenderTimeGraph)) { DrawTimeGraph(RenderTimeGraph); + DrawTimeGraph(CompositorUpdateTimeGraph); DrawTimeGraph(UpdateTimeGraph); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositorPools.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositorPools.cs new file mode 100644 index 0000000000..5d280059c5 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositorPools.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositorPools +{ + public class StackPool : Stack> + { + public Stack Rent() + { + if (Count > 0) + return Pop()!; + return new Stack(); + } + + public void Return(ref Stack stack) + { + Return(stack); + stack = null!; + } + + public void Return(Stack? stack) + { + if (stack == null) + return; + + stack.Clear(); + Push(stack); + } + } + + public StackPool TreeWalkerFrameStackPool { get; } = new(); + public StackPool MatrixStackPool { get; } = new(); + public StackPool LtrbRectStackPool { get; } = new(); + public StackPool DoubleStackPool { get; } = new(); + public StackPool IntStackPool { get; } = new(); + public StackPool DirtyRectCollectorStackPool { get; } = new(); + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs deleted file mode 100644 index 0861384c2f..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs +++ /dev/null @@ -1,105 +0,0 @@ -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(LtrbPixelRect rect); - IDisposable BeginDraw(IDrawingContextImpl ctx); - bool IsEmpty { get; } - bool Intersects(LtrbRect rect); - bool Contains(Point pt); - void Reset(); - void Visualize(IDrawingContextImpl context); - LtrbPixelRect CombinedRect { get; } - IList Rects { get; } -} - -internal class DirtyRectTracker : IDirtyRectTracker -{ - private LtrbPixelRect _rect; - private Rect _doubleRect; - private LtrbRect _normalRect; - private LtrbPixelRect[] _rectsForApi = new LtrbPixelRect[1]; - private Random _random = new(); - public void AddRect(LtrbPixelRect rect) - { - _rect = _rect.Union(rect); - } - - public IDisposable BeginDraw(IDrawingContextImpl ctx) - { - ctx.PushClip(_rect.ToRectWithNoScaling()); - _doubleRect = _rect.ToRectWithNoScaling(); - _normalRect = new(_doubleRect); - return Disposable.Create(ctx.PopClip); - } - - public bool IsEmpty => _rect.IsEmpty; - public bool Intersects(LtrbRect rect) => _normalRect.Intersects(rect); - public bool Contains(Point pt) => _rect.Contains((int)pt.X, (int)pt.Y); - - 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 LtrbPixelRect CombinedRect => _rect; - - public IList Rects - { - get - { - if (_rect.IsEmpty) - 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(LtrbPixelRect 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(LtrbRect 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 LtrbPixelRect CombinedRect => _region.Bounds; - public IList Rects => _region.Rects; -} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/DebugEventsDirtyRectCollectorProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/DebugEventsDirtyRectCollectorProxy.cs new file mode 100644 index 0000000000..e4ba44d56b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/DebugEventsDirtyRectCollectorProxy.cs @@ -0,0 +1,13 @@ +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +internal class DebugEventsDirtyRectCollectorProxy(IDirtyRectCollector inner, ICompositionTargetDebugEvents events) + : IDirtyRectCollector +{ + public void AddRect(LtrbRect rect) + { + inner.AddRect(rect); + events.RectInvalidated(rect); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/IDirtyRectTracker.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/IDirtyRectTracker.cs new file mode 100644 index 0000000000..0f7c705468 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/IDirtyRectTracker.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +internal interface IDirtyRectTracker : IDirtyRectCollector +{ + /// + /// Post-processes the dirty rect area (e. g. to account for anti-aliasing) + /// + void FinalizeFrame(LtrbRect bounds); + IDisposable BeginDraw(IDrawingContextImpl ctx); + bool IsEmpty { get; } + bool Intersects(LtrbRect rect); + void Initialize(LtrbRect bounds); + void Visualize(IDrawingContextImpl context); + LtrbRect CombinedRect { get; } +} + +internal interface IDirtyRectCollector +{ + void AddRect(LtrbRect rect); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs new file mode 100644 index 0000000000..d89ca0a9f0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs @@ -0,0 +1,348 @@ +using System; +using System.Diagnostics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +partial class MultiDirtyRectTracker +{ + /// + /// This is a port of CDirtyRegion2 from WPF + /// + class CDirtyRegion2(int MaxDirtyRegionCount) + { + + private readonly LtrbRect[] _dirtyRegions = new LtrbRect[MaxDirtyRegionCount]; + private readonly LtrbRect[] _resolvedRegions = new LtrbRect[MaxDirtyRegionCount]; + private readonly double[,] _overhead = new double[MaxDirtyRegionCount + 1, MaxDirtyRegionCount]; + private LtrbRect _surfaceBounds; + private double _allowedDirtyRegionOverhead; + private int _regionCount; + private bool _optimized; + private bool _maxSurfaceFallback; + + private readonly struct UnionResult + { + public readonly double Overhead; + // Left here for debugging purposes + public readonly double Area; + public readonly LtrbRect Union; + + public UnionResult(double overhead, double area, LtrbRect union) + { + Overhead = overhead; + Area = area; + Union = union; + } + } + + private static double RectArea(LtrbRect r) + { + return (r.Right - r.Left) * (r.Bottom - r.Top); + } + + private static LtrbRect RectUnion(LtrbRect left, LtrbRect right) + { + if (left.IsZeroSize) + return right; + if (right.IsZeroSize) + return left; + return left.Union(right); + } + + private static UnionResult ComputeUnion(LtrbRect r0, LtrbRect r1) + { + var unioned = RectUnion(r0, r1); + var intersected = r0.IntersectOrEmpty(r1); + + double areaOfUnion = RectArea(unioned); + double overhead = areaOfUnion - (RectArea(r0) + RectArea(r1) - RectArea(intersected)); + + + // Use 0 as overhead if computed overhead is negative or overhead + // computation returns a nan. (If more than one of the previous + // area computations overflowed then overhead could be not a + // number.) + if (!(overhead > 0)) + { + overhead = 0; + } + + return new UnionResult(overhead, areaOfUnion, unioned); + } + + private void SetOverhead(int i, int j, double value) + { + if (i > j) + { + _overhead[i, j] = value; + } + else if (i < j) + { + _overhead[j, i] = value; + } + } + + private double GetOverhead(int i, int j) + { + if (i > j) + { + return _overhead[i, j]; + } + + if (i < j) + { + return _overhead[j, i]; + } + + return double.MaxValue; + } + + private void UpdateOverhead(int regionIndex) + { + ref readonly var regionAtIndex = ref _dirtyRegions[regionIndex]; + for (int i = 0; i < MaxDirtyRegionCount; i++) + { + if (regionIndex != i) + { + var ur = ComputeUnion(_dirtyRegions[i], regionAtIndex); + SetOverhead(i, regionIndex, ur.Overhead); + } + } + } + + /// + /// Initialize must be called before adding dirty rects. Initialize can also be called to + /// reset the dirty region. + /// + public void Initialize(LtrbRect surfaceBounds, double allowedDirtyRegionOverhead) + { + _allowedDirtyRegionOverhead = allowedDirtyRegionOverhead; + Array.Clear(_dirtyRegions); + Array.Clear(_overhead); + _optimized = false; + _maxSurfaceFallback = false; + _regionCount = 0; + + _surfaceBounds = surfaceBounds; + } + + /// + /// Adds a new dirty rectangle to the dirty region. + /// + public void Add(LtrbRect newRegion) + { + + // // We've already fallen back to setting the whole surface as a dirty region + // // because of invalid dirty rects, so no need to add any new ones + if (_maxSurfaceFallback) + { + return; + } + + // // Check if rectangle is well formed before we try to intersect it, + // // because Intersect will fail for badly formed rects + if (!newRegion.IsWellOrdered) + { + // If we're here it means that we've been passed an invalid rectangle as a dirty + // region, containing NAN or a non well ordered rectangle. + // In this case, make the dirty region the full surface size and warn in the debugger + // since this could cause a serious perf regression. + // + Debug.Assert(false); + + // + // Remove all dirty regions from this object, since + // they're no longer relevant. + // + Initialize(_surfaceBounds, _allowedDirtyRegionOverhead); + _maxSurfaceFallback = true; + _regionCount = 1; + return; + } + + var clippedNewRegion = newRegion.IntersectOrEmpty(_surfaceBounds); + + if (clippedNewRegion.IsEmpty) + { + return; + } + + // Always keep bounding boxes device space integer. + clippedNewRegion = new LtrbRect( + Math.Floor(clippedNewRegion.Left), + Math.Floor(clippedNewRegion.Top), + Math.Ceiling(clippedNewRegion.Right), + Math.Ceiling(clippedNewRegion.Bottom)); + + // Compute the overhead for the new region combined with all existing regions + for (int n = 0; n < MaxDirtyRegionCount; n++) + { + var ur = ComputeUnion(_dirtyRegions[n], clippedNewRegion); + SetOverhead(MaxDirtyRegionCount, n, ur.Overhead); + } + + // Find the pair of dirty regions that if merged create the minimal overhead. A overhead + // of 0 is perfect in the sense that it can not get better. In that case we break early + // out of the loop. + double minimalOverhead = double.MaxValue; + int bestMatchN = 0; + int bestMatchK = 0; + bool matchFound = false; + + for (int n = MaxDirtyRegionCount; n > 0; n--) + { + for (int k = 0; k < n; k++) + { + double overheadNK = GetOverhead(n, k); + if (minimalOverhead >= overheadNK) + { + minimalOverhead = overheadNK; + bestMatchN = n; + bestMatchK = k; + matchFound = true; + + if (overheadNK < _allowedDirtyRegionOverhead) + { + // If the overhead is very small, we bail out early since this + // saves us some valuable cycles. Note that "small" means really + // nothing here. In fact we don't always know if that number is + // actually small. However, it the algorithm stays still correct + // in the sense that we render everything that is necessary. It + // might just be not optimal. + goto LoopExit; + } + } + } + } + + if (!matchFound) + { + return; + } + + LoopExit: + + // Case A: The new dirty region can be combined with an existing one + if (bestMatchN == MaxDirtyRegionCount) + { + var ur = ComputeUnion(clippedNewRegion, _dirtyRegions[bestMatchK]); + var unioned = ur.Union; + + if (_dirtyRegions[bestMatchK].Contains(unioned)) + { + // newDirtyRegion is enclosed by dirty region bestMatchK + return; + } + + _dirtyRegions[bestMatchK] = unioned; + UpdateOverhead(bestMatchK); + } + else + { + // Case B: Merge region N with region K, store new region slot K + var ur = ComputeUnion(_dirtyRegions[bestMatchN], _dirtyRegions[bestMatchK]); + _dirtyRegions[bestMatchN] = ur.Union; + _dirtyRegions[bestMatchK] = clippedNewRegion; + UpdateOverhead(bestMatchN); + UpdateOverhead(bestMatchK); + } + } + + /// + /// Returns an array of dirty rectangles describing the dirty region. + /// + public ReadOnlySpan GetUninflatedDirtyRegions() + { + if (_maxSurfaceFallback) + { + return new ReadOnlySpan(in _surfaceBounds); + } + + if (!_optimized) + { + Array.Clear(_resolvedRegions); + + // Consolidate the dirtyRegions array + int addedDirtyRegionCount = 0; + for (int i = 0; i < MaxDirtyRegionCount; i++) + { + if (!_dirtyRegions[i].IsEmpty) + { + if (i != addedDirtyRegionCount) + { + _dirtyRegions[addedDirtyRegionCount] = _dirtyRegions[i]; + UpdateOverhead(addedDirtyRegionCount); + } + + addedDirtyRegionCount++; + } + } + + // Merge all dirty rects that we can + bool couldMerge = true; + while (couldMerge) + { + couldMerge = false; + for (int n = 0; n < addedDirtyRegionCount; n++) + { + for (int k = n + 1; k < addedDirtyRegionCount; k++) + { + if (!_dirtyRegions[n].IsEmpty + && !_dirtyRegions[k].IsEmpty + && GetOverhead(n, k) < _allowedDirtyRegionOverhead) + { + var ur = ComputeUnion(_dirtyRegions[n], _dirtyRegions[k]); + _dirtyRegions[n] = ur.Union; + _dirtyRegions[k] = default; + UpdateOverhead(n); + couldMerge = true; + } + } + } + } + + // Consolidate and copy into resolvedRegions + int finalRegionCount = 0; + for (int i = 0; i < addedDirtyRegionCount; i++) + { + if (!_dirtyRegions[i].IsEmpty) + { + _resolvedRegions[finalRegionCount] = _dirtyRegions[i]; + finalRegionCount++; + } + } + + _regionCount = finalRegionCount; + _optimized = true; + } + + return _resolvedRegions.AsSpan(0, _regionCount); + } + + /// + /// Checks if the dirty region is empty. + /// + public bool IsEmpty + { + get + { + for (int i = 0; i < MaxDirtyRegionCount; i++) + { + if (!_dirtyRegions[i].IsEmpty) + { + return false; + } + } + + return true; + } + } + + /// + /// Returns the dirty region count. + /// NOTE: The region count is NOT VALID until GetUninflatedDirtyRegions is called. + /// + public int RegionCount => _regionCount; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.cs new file mode 100644 index 0000000000..191712cf6a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Reactive; + +namespace Avalonia.Rendering.Composition.Server; + + +internal partial class MultiDirtyRectTracker : IDirtyRectTracker +{ + private readonly double _maxOverhead; + private readonly CDirtyRegion2 _regions; + private readonly IPlatformRenderInterfaceRegion _clipRegion; + private readonly List _inflatedRects = new(); + private Random _random = new(); + + public MultiDirtyRectTracker(IPlatformRenderInterface platformRender, int maxDirtyRects, double maxOverhead) + { + _maxOverhead = maxOverhead; + _regions = new CDirtyRegion2(maxDirtyRects); + _clipRegion = platformRender.CreateRegion(); + } + + public void AddRect(LtrbRect rect) => _regions.Add(rect); + + public void FinalizeFrame(LtrbRect bounds) + { + _inflatedRects.Clear(); + _clipRegion.Reset(); + + var dirtyRegions = _regions.GetUninflatedDirtyRegions(); + + LtrbRect? combined = default; + foreach (var rect in dirtyRegions) + { + var inflated = rect.Inflate(new(1)).IntersectOrEmpty(bounds); + _inflatedRects.Add(inflated); + _clipRegion.AddRect(LtrbPixelRect.FromRectUnscaled(inflated)); + combined = LtrbRect.FullUnion(combined, inflated); + } + + CombinedRect = combined ?? default; + } + + public IDisposable BeginDraw(IDrawingContextImpl ctx) + { + ctx.PushClip(_clipRegion); + return Disposable.Create(ctx.PopClip); + } + + public bool IsEmpty => _regions.IsEmpty; + + public bool Intersects(LtrbRect rect) + { + foreach(var r in _inflatedRects) + { + if (r.Intersects(rect)) + return true; + } + + return false; + } + + public void Initialize(LtrbRect bounds) + { + + _regions.Initialize(bounds, _maxOverhead); + _inflatedRects.Clear(); + _clipRegion.Reset(); + CombinedRect = default; + } + + 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, _clipRegion); + } + + public LtrbRect CombinedRect { get; private set; } + + public IReadOnlyList InflatedRects => _inflatedRects; +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/RegionDirtyRectTracker.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/RegionDirtyRectTracker.cs new file mode 100644 index 0000000000..54a615cb00 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/RegionDirtyRectTracker.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Reactive; + +namespace Avalonia.Rendering.Composition.Server; + +internal class RegionDirtyRectTracker : IDirtyRectTracker +{ + private readonly IPlatformRenderInterfaceRegion _region; + private readonly List _rects = new(); + private Random _random = new(); + + public RegionDirtyRectTracker(IPlatformRenderInterface platformRender) + { + _region = platformRender.CreateRegion(); + } + + public void AddRect(LtrbRect rect) => _rects.Add(rect); + + private LtrbPixelRect GetInflatedPixelRect(LtrbRect rc) + { + var inflated = rc.Inflate(new Thickness(1)).IntersectOrEmpty(rc); + var pixelRect = LtrbPixelRect.FromRectUnscaled(inflated); + return pixelRect; + } + + public void FinalizeFrame(LtrbRect bounds) + { + _region.Reset(); + foreach (var rc in _rects) + _region.AddRect(GetInflatedPixelRect(rc)); + CombinedRect = _region.Bounds.ToLtrbRectUnscaled(); + } + + public IDisposable BeginDraw(IDrawingContextImpl ctx) + { + ctx.PushClip(_region); + return Disposable.Create(ctx.PopClip); + } + + public bool IsEmpty => _rects.Count == 0; + + public bool Intersects(LtrbRect rect) => _region.Intersects(rect); + + public void Initialize(LtrbRect bounds) => _rects.Clear(); + + 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 LtrbRect CombinedRect { get; private set; } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/SingleDirtyRectTracker.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/SingleDirtyRectTracker.cs new file mode 100644 index 0000000000..e02b9ec0d6 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/SingleDirtyRectTracker.cs @@ -0,0 +1,50 @@ +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 class SingleDirtyRectTracker : IDirtyRectTracker +{ + private LtrbRect? _rect; + private LtrbRect _extendedRect; + + private readonly Random _random = new(); + public void AddRect(LtrbRect rect) + { + _rect = LtrbRect.FullUnion(_rect, rect); + } + + public void FinalizeFrame(LtrbRect bounds) + { + + _extendedRect = _rect.HasValue + ? LtrbPixelRect.FromRectUnscaled(_rect.Value.Inflate(new Thickness(1)).IntersectOrEmpty(bounds)) + .ToLtrbRectUnscaled() + : default; + } + + public IDisposable BeginDraw(IDrawingContextImpl ctx) + { + ctx.PushClip(_extendedRect.ToRect()); + return Disposable.Create(ctx.PopClip); + } + + public bool IsEmpty => _rect?.IsZeroSize ?? true; + public bool Intersects(LtrbRect rect) => _extendedRect.Intersects(rect); + + public void Initialize(LtrbRect bounds) => _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, _extendedRect.ToRect()); + } + + public LtrbRect CombinedRect => _extendedRect; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs index 9f0341826a..ce5d80650d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -1,3 +1,5 @@ +using System.Threading; + namespace Avalonia.Rendering.Composition.Server { /// @@ -7,40 +9,29 @@ namespace Avalonia.Rendering.Composition.Server /// internal class ReadbackIndices { - private readonly object _lock = new object(); - public int ReadIndex { get; private set; } = 0; - public int WriteIndex { get; private set; } = 1; - public int WrittenIndex { get; private set; } = 0; + public readonly object _lock = new object(); + public ulong ReadRevision { get; private set; } - public ulong LastWrittenRevision { get; private set; } - + private ulong _nextWriteRevision = 1; + public ulong WriteRevision { get; private set; } + public ulong LastCompletedWrite { get; private set; } + public void NextRead() { lock (_lock) - { - if (ReadRevision < LastWrittenRevision) - { - ReadIndex = WrittenIndex; - ReadRevision = LastWrittenRevision; - } - } + ReadRevision = LastCompletedWrite; + } + + public void BeginWrite() + { + Monitor.Enter(_lock); + WriteRevision = _nextWriteRevision++; } - public void CompleteWrite(ulong writtenRevision) + public void EndWrite() { - lock (_lock) - { - for (var c = 0; c < 3; c++) - { - if (c != WriteIndex && c != ReadIndex) - { - WrittenIndex = WriteIndex; - LastWrittenRevision = writtenRevision; - WriteIndex = c; - return; - } - } - } + LastCompletedWrite = WriteRevision; + Monitor.Exit(_lock); } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBitmapCache.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBitmapCache.cs new file mode 100644 index 0000000000..854795f3a1 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBitmapCache.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionBitmapCache +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionCacheMode.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionCacheMode.cs new file mode 100644 index 0000000000..f2471dabb5 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionCacheMode.cs @@ -0,0 +1,24 @@ +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionCacheMode +{ + private readonly WeakHashList _attachedVisuals = new(); + + public void Subscribe(ServerCompositionVisual visual) => _attachedVisuals.Add(visual); + + public void Unsubscribe(ServerCompositionVisual visual) => _attachedVisuals.Remove(visual); + + protected override void ValuesInvalidated() + { + using var alive = _attachedVisuals.GetAlive(); + if (alive != null) + { + foreach (var v in alive.Span) + v.OnCacheModeStateChanged(); + } + + base.ValuesInvalidated(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs index 396009841b..34076adf1a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs @@ -12,99 +12,6 @@ namespace Avalonia.Rendering.Composition.Server /// internal partial class ServerCompositionContainerVisual : ServerCompositionVisual { - public ServerCompositionVisualCollection Children { get; private set; } = null!; - private LtrbRect? _transformedContentBounds; - private IImmutableEffect? _oldEffect; - - protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) - { - base.RenderCore(context, currentTransformedClip); - - if (context.RenderChildren) - { - foreach (var ch in Children) - { - ch.Render(context, currentTransformedClip); - } - } - } - - public override UpdateResult Update(ServerCompositionTarget root, Matrix parentCombinedTransform) - { - 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, GlobalTransformMatrix); - oldInvalidated |= res.InvalidatedOld; - newInvalidated |= res.InvalidatedNew; - combinedBounds = LtrbRect.FullUnion(combinedBounds, res.Bounds); - } - } - - // If effect is changed, we need to clean both old and new bounds - var effectChanged = !Effect.EffectEquals(_oldEffect); - if (effectChanged) - oldInvalidated = newInvalidated = true; - - // Expand invalidated bounds to the whole content area since we don't actually know what is being sampled - // We also ignore clip for now since we don't have means to reset it? - if (_oldEffect != null && oldInvalidated && _transformedContentBounds.HasValue) - AddEffectPaddedDirtyRect(_oldEffect, _transformedContentBounds.Value); - - if (Effect != null && newInvalidated && combinedBounds.HasValue) - AddEffectPaddedDirtyRect(Effect, combinedBounds.Value); - - _oldEffect = Effect; - _transformedContentBounds = combinedBounds; - - IsDirtyComposition = false; - return new(_transformedContentBounds, oldInvalidated, newInvalidated); - } - - protected override LtrbRect GetEffectBounds() => _transformedContentBounds ?? default; - - void AddEffectPaddedDirtyRect(IImmutableEffect effect, LtrbRect transformedBounds) - { - var padding = effect.GetEffectOutputPadding(); - if (padding == default) - { - AddDirtyRect(transformedBounds); - return; - } - - // We are in a weird position here: bounds are in global coordinates while padding gets applied in local ones - // Since we have optimizations to AVOID recomputing transformed bounds and since visuals with effects are relatively rare - // we instead apply the transformation matrix to rescale the bounds - - - // If we only have translation and scale, just scale the padding - if (CombinedTransformMatrix is - { - M12: 0, M13: 0, - M21: 0, M23: 0, - M31: 0, M32: 0 - }) - padding = new Thickness(padding.Left * CombinedTransformMatrix.M11, - padding.Top * CombinedTransformMatrix.M22, - padding.Right * CombinedTransformMatrix.M11, - padding.Bottom * CombinedTransformMatrix.M22); - else - { - // Conservatively use the transformed rect size - var transformedPaddingRect = new Rect().Inflate(padding).TransformToAABB(CombinedTransformMatrix); - padding = new(Math.Max(transformedPaddingRect.Width, transformedPaddingRect.Height)); - } - - AddDirtyRect(transformedBounds.Inflate(padding)); - } - - partial void Initialize() - { - Children = new ServerCompositionVisualCollection(Compositor); - } + public new ServerCompositionVisualCollection Children => base.Children!; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index ba9b042ad3..98fd9e4873 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -27,7 +27,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua #endif } - public override LtrbRect OwnContentBounds => _renderCommands?.Bounds ?? default; + public override LtrbRect? ComputeOwnContentBounds() => _renderCommands?.Bounds; protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { @@ -36,22 +36,17 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua _renderCommands?.Dispose(); _renderCommands = reader.ReadObject(); _renderCommands?.AddObserver(this); + InvalidateContent(); } base.DeserializeChangesCore(reader, committedAt); } protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { - if (_renderCommands != null - && context.ShouldRenderOwnContent(this, currentTransformedClip)) - { - _renderCommands.Render(context.Canvas); - } - - base.RenderCore(context, currentTransformedClip); + _renderCommands?.Render(context.Canvas); } - - public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated(); + + public void DependencyQueuedInvalidate(IServerRenderResource sender) => InvalidateContent(); #if DEBUG public override string ToString() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs index 20a2501ca7..d3fa053416 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs @@ -8,17 +8,25 @@ internal partial class ServerCompositionExperimentalAcrylicVisual protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { var cornerRadius = CornerRadius; - context.Canvas.DrawRectangle( - Material, - new RoundedRect( - new Rect(0, 0, Size.X, Size.Y), - cornerRadius.TopLeft, cornerRadius.TopRight, - cornerRadius.BottomRight, cornerRadius.BottomLeft)); - + if(context.Canvas is IDrawingContextWithAcrylicLikeSupport supported) + supported.DrawRectangle( + Material, + new RoundedRect( + new Rect(0, 0, Size.X, Size.Y), + cornerRadius.TopLeft, cornerRadius.TopRight, + cornerRadius.BottomRight, cornerRadius.BottomLeft)); + base.RenderCore(context, currentTransformedClip); } - public override LtrbRect OwnContentBounds => new(0, 0, Size.X, Size.Y); + public override LtrbRect? ComputeOwnContentBounds() => + LtrbRect.FullUnion(base.ComputeOwnContentBounds(), new LtrbRect(0, 0, Size.X, Size.Y)); + + protected override void SizeChanged() + { + EnqueueForOwnBoundsRecompute(); + base.SizeChanged(); + } public ServerCompositionExperimentalAcrylicVisual(ServerCompositor compositor, Visual v) : base(compositor, v) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs index 28663ce342..73b8ebc56b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs @@ -19,7 +19,7 @@ internal partial class ServerCompositionSurfaceVisual } - private void OnSurfaceInvalidated() => ValuesInvalidated(); + private void OnSurfaceInvalidated() => InvalidateContent(); protected override void OnAttachedToRoot(ServerCompositionTarget target) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs deleted file mode 100644 index 1b5cac520e..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Collections.Pooled; -using Avalonia.Platform; - -namespace Avalonia.Rendering.Composition.Server; - -internal partial class ServerCompositionTarget -{ - public readonly IDirtyRectTracker DirtyRects; - - static int Clamp0(int value, int max) => Math.Max(Math.Min(value, max), 0); - - public void AddDirtyRect(LtrbRect rect) - { - if (rect.IsZeroSize) - return; - - DebugEvents?.RectInvalidated(rect.ToRect()); - - var snapped = LtrbPixelRect.FromRectWithNoScaling(SnapToDevicePixels(rect, Scaling)); - - var clamped = new LtrbPixelRect( - Clamp0(snapped.Left, _pixelSize.Width), - Clamp0(snapped.Top, _pixelSize.Height), - Clamp0(snapped.Right, _pixelSize.Width), - Clamp0(snapped.Bottom, _pixelSize.Height) - ); - - if (!clamped.IsEmpty) - DirtyRects.AddRect(clamped); - _redrawRequested = true; - } - - public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(new(rect), Scaling).ToRect(); - public LtrbRect SnapToDevicePixels(LtrbRect rect) => SnapToDevicePixels(rect, Scaling); - - public static LtrbRect SnapToDevicePixels(LtrbRect rect, double scale) - { - return new LtrbRect( - Math.Floor(rect.Left * scale) / scale, - Math.Floor(rect.Top * scale) / scale, - 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 90b973c3a8..19a5f0e881 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -31,13 +31,13 @@ namespace Avalonia.Rendering.Composition.Server private bool _fullRedrawRequested; private bool _disposed; private readonly HashSet _attachedVisuals = new(); - private readonly Queue _adornerUpdateQueue = new(); + public IDirtyRectTracker DirtyRects { get; } public long Id { get; } public ulong Revision { get; private set; } public ICompositionTargetDebugEvents? DebugEvents { get; set; } - public ReadbackIndices Readback { get; } = new(); public int RenderedVisuals { get; set; } + public int VisitedVisuals { get; set; } public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces, DiagnosticTextRenderer? diagnosticTextRenderer) @@ -47,10 +47,19 @@ namespace Avalonia.Rendering.Composition.Server _surfaces = surfaces; _overlays = new CompositionTargetOverlays(this, diagnosticTextRenderer); var platformRender = AvaloniaLocator.Current.GetService(); - DirtyRects = compositor.Options.UseRegionDirtyRectClipping == true && - platformRender?.SupportsRegions == true - ? new RegionDirtyRectTracker(platformRender) - : new DirtyRectTracker(); + + if (platformRender?.SupportsRegions == true && compositor.Options.UseRegionDirtyRectClipping != false) + { + var maxRects = compositor.Options.MaxDirtyRects ?? 8; + DirtyRects = maxRects <= 0 + ? new RegionDirtyRectTracker(platformRender) + : new MultiDirtyRectTracker(platformRender, maxRects, + // WPF uses 50K, but that merges stuff rather aggressively + compositor.Options.DirtyRectMergeEagerness ?? 1000); + } + + DirtyRects ??= new SingleDirtyRectTracker(); + Id = Interlocked.Increment(ref s_nextId); } @@ -83,8 +92,9 @@ namespace Avalonia.Rendering.Composition.Server _redrawRequested = true; _fullRedrawRequested = true; } - - public void Render() + + + public void Update(TimeSpan diagnosticsCompositorGlobalUpdateElapsedTime = default) { if (_disposed) { @@ -92,6 +102,32 @@ namespace Avalonia.Rendering.Composition.Server return; } + if (Root == null) + return; + + _overlays.RecordGlobalCompositorUpdateTime(diagnosticsCompositorGlobalUpdateElapsedTime); + _overlays.MarkUpdateCallStart(); + using (Diagnostic.BeginCompositorUpdatePass()) + { + var transform = Matrix.CreateScale(Scaling, Scaling); + + var collector = DebugEvents != null + ? new DebugEventsDirtyRectCollectorProxy(DirtyRects, DebugEvents) + : (IDirtyRectCollector)DirtyRects; + + Root.UpdateRoot(collector, transform, new LtrbRect(0, 0, PixelSize.Width, PixelSize.Height)); + + _updateRequested = false; + + _overlays.MarkUpdateCallEnd(); + } + } + + public void Render() + { + if (_disposed) + return; + if (Root == null) return; @@ -120,26 +156,7 @@ namespace Avalonia.Rendering.Composition.Server if (DirtyRects.IsEmpty && !_redrawRequested && !_updateRequested) return; - Revision++; - - _overlays.MarkUpdateCallStart(); - using (Diagnostic.BeginCompositorUpdatePass()) - { - var transform = Matrix.CreateScale(Scaling, Scaling); - // Update happens in a separate phase to extend dirty rect if needed - Root.Update(this, transform); - - while (_adornerUpdateQueue.Count > 0) - { - var adorner = _adornerUpdateQueue.Dequeue(); - adorner.Update(this, transform); - } - - _updateRequested = false; - Readback.CompleteWrite(Revision); - - _overlays.MarkUpdateCallEnd(); - } + _redrawRequested |= !DirtyRects.IsEmpty; if (!_redrawRequested) return; @@ -156,13 +173,15 @@ namespace Avalonia.Rendering.Composition.Server this.PixelSize, out var properties)) using (var renderTiming = Diagnostic.BeginCompositorRenderPass()) { + var fullRedraw = false; + if(needLayer && (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted)) { _layer?.Dispose(); _layer = null; _layer = renderTargetContext.CreateLayer(PixelSize); _layerSize = PixelSize; - DirtyRects.AddRect(new LtrbPixelRect(_layerSize)); + fullRedraw = true; } else if (!needLayer) { @@ -172,12 +191,20 @@ namespace Avalonia.Rendering.Composition.Server if (_fullRedrawRequested || (!needLayer && !properties.PreviousFrameIsRetained)) { - DirtyRects.AddRect(new LtrbPixelRect(_layerSize)); _fullRedrawRequested = false; + fullRedraw = true; + } + + var renderBounds = new LtrbRect(0, 0, PixelSize.Width, PixelSize.Height); + if (fullRedraw) + { + DirtyRects.Initialize(renderBounds); + DirtyRects.AddRect(renderBounds); } if (!DirtyRects.IsEmpty) { + DirtyRects.FinalizeFrame(renderBounds); if (_layer != null) { using (var context = _layer.CreateDrawingContext(false)) @@ -202,9 +229,10 @@ namespace Avalonia.Rendering.Composition.Server } RenderedVisuals = 0; + VisitedVisuals = 0; _redrawRequested = false; - DirtyRects.Reset(); + DirtyRects.Initialize(renderBounds); } } @@ -216,12 +244,14 @@ namespace Avalonia.Rendering.Composition.Server { context.Clear(Colors.Transparent); if (useLayerClip) - context.PushLayer(DirtyRects.CombinedRect.ToRectUnscaled()); + context.PushLayer(DirtyRects.CombinedRect.ToRect()); - using (var proxy = new CompositorDrawingContextProxy(context)) + context.Transform = Matrix.CreateScale(Scaling, Scaling); + (VisitedVisuals, RenderedVisuals) = root.Render(context, new LtrbRect(0,0, PixelSize.Width, PixelSize.Height), DirtyRects); + if (DebugEvents != null) { - var ctx = new ServerVisualRenderContext(proxy, DirtyRects, false, true); - root.Render(ctx, null); + DebugEvents.RenderedVisuals = RenderedVisuals; + DebugEvents.VisitedVisuals = VisitedVisuals; } if (useLayerClip) @@ -260,10 +290,6 @@ namespace Avalonia.Rendering.Composition.Server { if (_attachedVisuals.Remove(visual) && IsEnabled) visual.Deactivate(); - if (visual.IsVisibleInFrame) - AddDirtyRect(visual.TransformedOwnContentBounds); } - - public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs deleted file mode 100644 index 9d17756f2b..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace Avalonia.Rendering.Composition.Server; - -partial class ServerCompositionVisual -{ - protected bool IsDirtyComposition; - private bool _combinedTransformDirty; - private bool _clipSizeDirty; - - private const CompositionVisualChangedFields CompositionFieldsMask - = CompositionVisualChangedFields.Opacity - | CompositionVisualChangedFields.OpacityAnimated - | CompositionVisualChangedFields.OpacityMaskBrush - | CompositionVisualChangedFields.Clip - | CompositionVisualChangedFields.ClipToBounds - | CompositionVisualChangedFields.ClipToBoundsAnimated - | CompositionVisualChangedFields.Size - | CompositionVisualChangedFields.SizeAnimated - | CompositionVisualChangedFields.RenderOptions; - - private const CompositionVisualChangedFields CombinedTransformFieldsMask = - CompositionVisualChangedFields.Size - | CompositionVisualChangedFields.SizeAnimated - | CompositionVisualChangedFields.AnchorPoint - | CompositionVisualChangedFields.AnchorPointAnimated - | CompositionVisualChangedFields.CenterPoint - | CompositionVisualChangedFields.CenterPointAnimated - | CompositionVisualChangedFields.AdornedVisual - | CompositionVisualChangedFields.TransformMatrix - | CompositionVisualChangedFields.Scale - | CompositionVisualChangedFields.ScaleAnimated - | CompositionVisualChangedFields.RotationAngle - | CompositionVisualChangedFields.RotationAngleAnimated - | CompositionVisualChangedFields.Orientation - | CompositionVisualChangedFields.OrientationAnimated - | CompositionVisualChangedFields.Offset - | CompositionVisualChangedFields.OffsetAnimated; - - private const CompositionVisualChangedFields ClipSizeDirtyMask = - CompositionVisualChangedFields.Size - | CompositionVisualChangedFields.SizeAnimated - | CompositionVisualChangedFields.ClipToBounds - | CompositionVisualChangedFields.Clip - | CompositionVisualChangedFields.ClipToBoundsAnimated; - - partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) - { - if ((changed & CompositionFieldsMask) != 0) - IsDirtyComposition = true; - if ((changed & CombinedTransformFieldsMask) != 0) - _combinedTransformDirty = true; - if ((changed & ClipSizeDirtyMask) != 0) - _clipSizeDirty = true; - } - - public override void NotifyAnimatedValueChanged(CompositionProperty offset) - { - base.NotifyAnimatedValueChanged(offset); - if (offset == s_IdOfClipToBoundsProperty - || offset == s_IdOfOpacityProperty - || offset == s_IdOfSizeProperty) - IsDirtyComposition = true; - - if (offset == s_IdOfSizeProperty - || offset == s_IdOfAnchorPointProperty - || offset == s_IdOfCenterPointProperty - || offset == s_IdOfAdornedVisualProperty - || offset == s_IdOfTransformMatrixProperty - || offset == s_IdOfScaleProperty - || offset == s_IdOfRotationAngleProperty - || offset == s_IdOfOrientationProperty - || offset == s_IdOfOffsetProperty) - _combinedTransformDirty = true; - - if (offset == s_IdOfClipToBoundsProperty - || offset == s_IdOfSizeProperty) - _clipSizeDirty = true; - } -} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs deleted file mode 100644 index 9225dd6ac6..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ /dev/null @@ -1,343 +0,0 @@ -using System; -using System.Numerics; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Animations; -using Avalonia.Rendering.Composition.Transport; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Server -{ - /// - /// Server-side counterpart. - /// Is responsible for computing the transformation matrix, for applying various visual - /// properties before calling visual-specific drawing code and for notifying the - /// for new dirty rects - /// - partial class ServerCompositionVisual : ServerObject - { - private bool _isDirtyForUpdate; - private LtrbRect _oldOwnContentBounds; - private bool _isBackface; - private LtrbRect? _transformedClipBounds; - private LtrbRect _combinedTransformedClipBounds; - - protected virtual void RenderCore(ServerVisualRenderContext canvas, LtrbRect currentTransformedClip) - { - } - - public void Render(ServerVisualRenderContext context, LtrbRect? parentTransformedClip) - { - if (Visible == false || IsVisibleInFrame == false) - return; - if (Opacity == 0) - return; - var canvas = context.Canvas; - - var currentTransformedClip = parentTransformedClip.HasValue - ? parentTransformedClip.Value.Intersect(_combinedTransformedClipBounds) - : _combinedTransformedClipBounds; - if(!context.ShouldRender(this, currentTransformedClip)) - return; - - Root!.RenderedVisuals++; - Root!.DebugEvents?.IncrementRenderedVisuals(); - - var boundsRect = new Rect(new Size(Size.X, Size.Y)); - - if (AdornedVisual != null) - { - // Adorners are currently not supported in detached rendering mode - if(context.DetachedRendering) - return; - - canvas.Transform = Matrix.Identity; - if (AdornerIsClipped) - canvas.PushClip(AdornedVisual._combinedTransformedClipBounds.ToRect()); - } - - using var _ = context.SetOrPushTransform(this); - - var applyRenderOptions = RenderOptions != default; - - if (applyRenderOptions) - canvas.PushRenderOptions(RenderOptions); - - var applyTextOptions = TextOptions != default; - - if (applyTextOptions) - canvas.PushTextOptions(TextOptions); - - var needPopEffect = PushEffect(canvas); - - if (Opacity != 1) - canvas.PushOpacity(Opacity, ClipToBounds ? boundsRect : null); - if (ClipToBounds && !HandlesClipToBounds) - canvas.PushClip(boundsRect); - if (Clip != null) - canvas.PushGeometryClip(Clip); - if (OpacityMaskBrush != null) - canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - - RenderCore(context, currentTransformedClip); - - if (OpacityMaskBrush != null) - canvas.PopOpacityMask(); - if (Clip != null) - canvas.PopGeometryClip(); - if (ClipToBounds && !HandlesClipToBounds) - canvas.PopClip(); - if (AdornedVisual != null && AdornerIsClipped) - canvas.PopClip(); - if (Opacity != 1) - canvas.PopOpacity(); - - if (needPopEffect) - canvas.PopEffect(); - if (applyTextOptions) - canvas.PopTextOptions(); - if (applyRenderOptions) - canvas.PopRenderOptions(); - } - - protected virtual LtrbRect GetEffectBounds() => TransformedOwnContentBounds; - - private bool PushEffect(CompositorDrawingContextProxy canvas) - { - if (Effect == null) - return false; - var clip = GetEffectBounds(); - if (clip.IsZeroSize) - return false; - var oldMatrix = canvas.Transform; - canvas.Transform = Matrix.Identity; - canvas.PushEffect(GetEffectBounds().ToRect(), Effect!); - canvas.Transform = oldMatrix; - return true; - } - - protected virtual bool HandlesClipToBounds => false; - - private ReadbackData _readback0, _readback1, _readback2; - - /// - /// Obtains "readback" data - the data that is sent from the render thread to the UI thread - /// in non-blocking manner. Used mostly by hit-testing - /// - public ref ReadbackData GetReadback(int idx) - { - if (idx == 0) - return ref _readback0; - if (idx == 1) - return ref _readback1; - return ref _readback2; - } - - public Matrix CombinedTransformMatrix { get; private set; } = Matrix.Identity; - public Matrix GlobalTransformMatrix { get; private set; } - - public record struct UpdateResult(LtrbRect? Bounds, bool InvalidatedOld, bool InvalidatedNew) - { - public UpdateResult() : this(null, false, false) - { - - } - } - - public virtual UpdateResult Update(ServerCompositionTarget root, Matrix parentVisualTransform) - { - if (Parent == null && Root == null) - return default; - - var wasVisible = IsVisibleInFrame; - - // Calculate new parent-relative transform - if (_combinedTransformDirty) - { - CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, - // HACK: Ignore RenderTransform set by the adorner layer - AdornedVisual != null ? Matrix.Identity : TransformMatrix, - Scale, RotationAngle, Orientation, Offset); - _combinedTransformDirty = false; - } - - var parentTransform = AdornedVisual?.GlobalTransformMatrix ?? parentVisualTransform; - - var newTransform = CombinedTransformMatrix * parentTransform; - - // Check if visual was moved and recalculate face orientation - var positionChanged = false; - if (GlobalTransformMatrix != newTransform) - { - _isBackface = Vector3.Transform( - new Vector3(0, 0, float.PositiveInfinity), MatrixUtils.ToMatrix4x4(GlobalTransformMatrix)).Z <= 0; - positionChanged = true; - } - - var oldTransformedContentBounds = TransformedOwnContentBounds; - var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; - - if (_parent?.IsDirtyComposition == true) - { - IsDirtyComposition = true; - _isDirtyForUpdate = true; - } - - var invalidateOldBounds = _isDirtyForUpdate; - var invalidateNewBounds = _isDirtyForUpdate; - - GlobalTransformMatrix = newTransform; - - var ownBounds = OwnContentBounds; - - // Since padding is applied in the current visual's coordinate space we expand bounds before transforming them - if (Effect != null) - ownBounds = ownBounds.Inflate(Effect.GetEffectOutputPadding()); - - if (ownBounds != _oldOwnContentBounds || positionChanged) - { - _oldOwnContentBounds = ownBounds; - if (ownBounds.IsZeroSize) - TransformedOwnContentBounds = default; - else - TransformedOwnContentBounds = - ownBounds.TransformToAABB(GlobalTransformMatrix); - } - - if (_clipSizeDirty || positionChanged) - { - LtrbRect? transformedVisualBounds = null; - LtrbRect? transformedClipBounds = null; - - if (ClipToBounds) - transformedVisualBounds = - new LtrbRect(0, 0, Size.X, Size.Y).TransformToAABB(GlobalTransformMatrix); - - if (Clip != null) - transformedClipBounds = new LtrbRect(Clip.Bounds).TransformToAABB(GlobalTransformMatrix); - - if (transformedVisualBounds != null && transformedClipBounds != null) - _transformedClipBounds = transformedVisualBounds.Value.Intersect(transformedClipBounds.Value); - else if (transformedVisualBounds != null) - _transformedClipBounds = transformedVisualBounds; - else if (transformedClipBounds != null) - _transformedClipBounds = transformedClipBounds; - else - _transformedClipBounds = null; - - _clipSizeDirty = false; - } - - _combinedTransformedClipBounds = - (AdornerIsClipped ? AdornedVisual?._combinedTransformedClipBounds : null) - ?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null) - ?? new LtrbRect(0, 0, Root!.PixelSize.Width, Root!.PixelSize.Height); - - if (_transformedClipBounds != null) - _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); - - EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); - - IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false - && Visible - && !_isBackface - && !(_combinedTransformedClipBounds.IsZeroSize); - - IsVisibleInFrame = IsHitTestVisibleInFrame - && _parent?.IsVisibleInFrame != false - && EffectiveOpacity > 0.003; - - if (wasVisible != IsVisibleInFrame || positionChanged) - { - invalidateOldBounds |= wasVisible; - invalidateNewBounds |= IsVisibleInFrame; - } - - // Invalidate new bounds - if (invalidateNewBounds) - AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds)); - - if (invalidateOldBounds) - AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds)); - - - _isDirtyForUpdate = false; - - // Update readback indices - var i = Root!.Readback; - ref var readback = ref GetReadback(i.WriteIndex); - readback.Revision = root.Revision; - readback.Matrix = GlobalTransformMatrix; - readback.TargetId = Root.Id; - readback.Visible = IsHitTestVisibleInFrame; - return new(TransformedOwnContentBounds, invalidateNewBounds, invalidateOldBounds); - } - - protected void AddDirtyRect(LtrbRect rc) - { - if (rc == default) - return; - - // If the visual isn't using layout rounding, it's possible that antialiasing renders to pixels - // outside the current bounds. Extend the dirty rect by 1px in all directions in this case. - if (ShouldExtendDirtyRect && RenderOptions.EdgeMode != EdgeMode.Aliased) - rc = rc.Inflate(new Thickness(1)); - - Root?.AddDirtyRect(rc); - } - - /// - /// Data that can be read from the UI thread - /// - public struct ReadbackData - { - public Matrix Matrix; - public ulong Revision; - public long TargetId; - public bool Visible; - } - - partial void DeserializeChangesExtra(BatchStreamReader c) - { - ValuesInvalidated(); - } - - partial void OnRootChanging() - { - if (Root != null) - { - Root.RemoveVisual(this); - OnDetachedFromRoot(Root); - } - } - - protected virtual void OnDetachedFromRoot(ServerCompositionTarget target) - { - } - - partial void OnRootChanged() - { - if (Root != null) - { - Root.AddVisual(this); - OnAttachedToRoot(Root); - } - } - - protected virtual void OnAttachedToRoot(ServerCompositionTarget target) - { - } - - protected override void ValuesInvalidated() - { - _isDirtyForUpdate = true; - Root?.RequestUpdate(); - } - - public bool IsVisibleInFrame { get; set; } - public bool IsHitTestVisibleInFrame { get; set; } - public double EffectiveOpacity { get; set; } - public LtrbRect TransformedOwnContentBounds { get; set; } - public virtual LtrbRect OwnContentBounds => new (0, 0, Size.X, Size.Y); - } -} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs new file mode 100644 index 0000000000..c0fd5ab777 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + // ATT = Ancestor Transform Tracker + // While we generally avoid dealing with keeping world transforms up to date, + // we still need it for cases like adorners. + // Instead of updating world transforms eagerly, we use a subscription model where + // visuals can subscribe to notifications when any ancestor's world transform changes. + + class AttHelper + { + public readonly HashSet AncestorChainTransformSubscribers = new(); + public required Action ParentActSubscriptionAction; + + // We keep adorner stuff here too + public required Action AdornedVisualActSubscriptionAction; + public bool EnqueuedForAdornerUpdate; + } + + private AttHelper? _AttHelper; + + private AttHelper GetAttHelper() => _AttHelper ??= new() + { + ParentActSubscriptionAction = AttHelper_CombinedTransformChanged, + AdornedVisualActSubscriptionAction = AttHelper_OnAdornedVisualWorldTransformChanged + }; + + private void AttHelper_CombinedTransformChanged() + { + if(_AttHelper == null || _AttHelper.AncestorChainTransformSubscribers.Count == 0) + return; + foreach (var sub in _AttHelper.AncestorChainTransformSubscribers) + sub(); + } + + private void AttHelper_ParentChanging() + { + if(Parent != null && _AttHelper?.AncestorChainTransformSubscribers.Count > 0) + Parent.AttHelper_UnsubscribeFromActNotification(_AttHelper.ParentActSubscriptionAction); + } + + private void AttHelper_ParentChanged() + { + if(Parent != null && _AttHelper?.AncestorChainTransformSubscribers.Count > 0) + Parent.AttHelper_SubscribeToActNotification(_AttHelper.ParentActSubscriptionAction); + if(Parent != null && AdornedVisual != null) + AdornerHelper_EnqueueForAdornerUpdate(); + } + + protected void AttHelper_SubscribeToActNotification(Action cb) + { + var h = GetAttHelper(); + + (h.AncestorChainTransformSubscribers).Add(cb); + if (h.AncestorChainTransformSubscribers.Count == 1) + Parent?.AttHelper_SubscribeToActNotification(h.ParentActSubscriptionAction); + } + + protected void AttHelper_UnsubscribeFromActNotification(Action cb) + { + var h = GetAttHelper(); + h.AncestorChainTransformSubscribers.Remove(cb); + if(h.AncestorChainTransformSubscribers.Count == 0) + Parent?.AttHelper_UnsubscribeFromActNotification(h.ParentActSubscriptionAction); + } + + protected static bool ComputeTransformFromAncestor(ServerCompositionVisual visual, + ServerCompositionVisual ancestor, out Matrix transform) + { + transform = visual._ownTransform ?? Matrix.Identity; + while (visual.Parent != null) + { + visual = visual.Parent; + + if (visual == ancestor) // Walked up to ancestor + return true; + + if (visual._ownTransform.HasValue) + transform = transform * visual._ownTransform.Value; + } + + // Visual is a part of a different subtree, this is not supported + return false; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs new file mode 100644 index 0000000000..6f5f7b7572 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + // Support for adorners is a rather cancerou^W invasive thing, so we isolate all related code in this file + // and prefix it with AdornerHelper_. + + private void AttHelper_OnAdornedVisualWorldTransformChanged() => AdornerHelper_EnqueueForAdornerUpdate(); + + private void AdornerHelper_AttachedToRoot() + { + if(AdornedVisual != null) + AdornerHelper_EnqueueForAdornerUpdate(); + } + + public void AdornerHelper_EnqueueForAdornerUpdate() + { + var helper = GetAttHelper(); + if(helper.EnqueuedForAdornerUpdate) + return; + Compositor.EnqueueAdornerUpdate(this); + helper.EnqueuedForAdornerUpdate = true; + } + + partial void OnAdornedVisualChanging() => + AdornedVisual?.AttHelper_UnsubscribeFromActNotification(GetAttHelper().AdornedVisualActSubscriptionAction); + + partial void OnAdornedVisualChanged() + { + AdornedVisual?.AttHelper_SubscribeToActNotification(GetAttHelper().AdornedVisualActSubscriptionAction); + AdornerHelper_EnqueueForAdornerUpdate(); + } + + private static ServerCompositionVisual? AdornerLayer_GetExpectedSharedAncestor(ServerCompositionVisual adorner) + { + // This is hardcoded to VisualLayerManager -> AdornerLayer -> adorner + // Since AdornedVisual is a private API that's only supposed to be accessible from AdornerLayer + // it's a safe assumption to make + return adorner?.Parent?.Parent; + } + + public void UpdateAdorner() + { + GetAttHelper().EnqueuedForAdornerUpdate = false; + var ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale, + RotationAngle, Orientation, Offset); + + if (AdornedVisual != null && Parent != null) + { + if ( + AdornerLayer_GetExpectedSharedAncestor(this) is {} sharedAncestor + && ComputeTransformFromAncestor(AdornedVisual, sharedAncestor, out var adornerLayerToAdornedVisual)) + ownTransform = (ownTransform ?? Matrix.Identity) * adornerLayerToAdornedVisual; + else + ownTransform = default(Matrix); // Don't render, something is broken + + } + _ownTransform = ownTransform; + + PropagateFlags(true, true); + } + + partial struct RenderContext + { + private enum Op + { + PopClip, + PopGeometryClip, + Stop + } + private Stack? _adornerPushedClipStack; + private ServerCompositionVisual? _currentAdornerLayer; + + private bool AdornerLayer_WalkAdornerParentClipRecursive(ServerCompositionVisual? visual) + { + if (visual != _currentAdornerLayer!) + { + // AdornedVisual is a part of a different subtree, this is not supported + if (visual == null) + return false; + + if (!AdornerLayer_WalkAdornerParentClipRecursive(visual.Parent)) + return false; + } + + if (visual._ownTransform.HasValue) + _canvas.Transform = visual._ownTransform.Value * _canvas.Transform; + + if (visual.ClipToBounds) + { + _canvas.PushClip(new Rect(0, 0, visual.Size.X, visual.Size.Y)); + _adornerPushedClipStack!.Push((int)Op.PopClip); + } + + if (visual.Clip != null) + { + _canvas.PushGeometryClip(visual.Clip); + _adornerPushedClipStack!.Push((int)Op.PopGeometryClip); + } + + return true; + } + + bool SkipAdornerClip(ServerCompositionVisual visual) + { + if (!visual.AdornerIsClipped + || visual == _rootVisual + || visual._parent == _rootVisual // Root visual is AdornerLayer + || AdornerLayer_GetExpectedSharedAncestor(visual) == null) + return true; + return false; + } + + private void AdornerHelper_RenderPreGraphPushAdornerClip(ServerCompositionVisual visual) + { + if (SkipAdornerClip(visual)) + return; + + _adornerPushedClipStack ??= _pools.IntStackPool.Rent(); + _adornerPushedClipStack.Push((int)Op.Stop); + + var originalTransform = _canvas.Transform; + var transform = originalTransform; + if (visual._ownTransform.HasValue) + { + if (!visual._ownTransform.Value.TryInvert(out var transformToAdornerLayer)) + return; + transform = transformToAdornerLayer * transform; + } + + _canvas.Transform = transform; + _currentAdornerLayer = AdornerLayer_GetExpectedSharedAncestor(visual); + + AdornerLayer_WalkAdornerParentClipRecursive(visual.AdornedVisual); + + _canvas.Transform = originalTransform; + } + + private void AdornerHelper_RenderPostGraphPushAdornerClip(ServerCompositionVisual visual) + { + if (SkipAdornerClip(visual)) + return; + + if (_adornerPushedClipStack == null) + return; + + while (_adornerPushedClipStack.Count > 0) + { + var op = (Op)_adornerPushedClipStack.Pop(); + if (op == Op.Stop) + break; + if (op == Op.PopGeometryClip) + _canvas.PopGeometryClip(); + else if (op == Op.PopClip) + _canvas.PopClip(); + } + } + + private void AdornerHelper_Dispose() + { + _pools.IntStackPool.Return(ref _adornerPushedClipStack!); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs new file mode 100644 index 0000000000..4b98b0f80e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs @@ -0,0 +1,164 @@ +using System.Diagnostics; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + // Dirty flags, handled by RecomputeOwnProperties + private bool _combinedTransformDirty; + private bool _clipSizeDirty; + private bool _ownBoundsDirty; + private bool _compositionFieldsDirty; + private bool _contentChanged; + + private bool _delayPropagateNeedsBoundsUpdate; + private bool _delayPropagateIsDirtyForRender; + private bool _delayPropagateHasExtraDirtyRects; + + // Dirty rect, re-render flags, set by PropagateFlags + private bool _needsBoundingBoxUpdate; + private bool _isDirtyForRender; + private bool _isDirtyForRenderInSubgraph; + + // Transform that accounts for offset, RenderTransform and other properties of _this_ visual that is + // used to transform to parent's coordinate space + // Updated by RecomputeOwnProperties pass + private Matrix? _ownTransform; + public Matrix? OwnTransform => _ownTransform; + + // The bounds of this visual's own content, excluding children + // Coordinate space: local + // Updated by RecomputeOwnProperties pass + private LtrbRect? _ownContentBounds; + + // The bounds of this visual and its subtree + // Coordinate space: local + // Updated by: PreSubgraph, PostSubraph (recursive) + private LtrbRect? _subTreeBounds; + + public LtrbRect? SubTreeBounds => _subTreeBounds; + + // The bounds of this visual and its subtree + // Coordinate space: parent + // Updated by: PostSubgraph + private LtrbRect? _transformedSubTreeBounds; + + // Visual's own clip area + // Coordinate space: local + private LtrbRect? _ownClipRect; + + + private bool _hasExtraDirtyRect; + private LtrbRect _extraDirtyRect; + + public virtual LtrbRect? ComputeOwnContentBounds() => null; + + public Matrix CombinedTransformMatrix { get; private set; } = Matrix.Identity; + + + // WPF's cheatsheet + //----------------------------------------------------------------------------- + // Node Operation | NeedsToBe | NeedsBBoxUpdate | HasNodeThat | Visit + // | AddedToDirty | (parent chain) | NeedsToBeAdded | child + // | Region | | ToDirtyRegion | + // | | | (parent chain) | + //============================================================================= + // Set transform | Y | Y | Y(N) + // -----------------+---------------+-----------------+----------------------- + // Set opacity | Y | N | Y(N) + // -----------------+---------------+-----------------+----------------------- + // Set clip | Y | Y | Y(N) + // -----------------+---------------+-----------------+----------------------- + // AttachRenderData | Y | Y | Y(N) + // -----------------+---------------+-----------------+----------------------- + // FreeRenderData | Y | Y | Y(N) + // -----------------+---------------+-----------------+----------------------- + // InsertChild | N | Y | Y + // | Y(child) | N | Y(N) + // -----------------+---------------+-----------------+----------------------- + // InsertChildAt | N | Y | Y + // | Y(child) | N | Y(N) + // -----------------+---------------+-----------------+----------------------- + // ZOrderChild | N | N | Y + // | Y(child) | N | Y(N) + // -----------------+---------------+-----------------+----------------------- + // ReplaceChild | Y | Y | Y(N) + // -----------------+---------------+-----------------+----------------------- + // RemoveChild | Y | Y | Y(N) + private void PropagateFlags(bool needsBoundingBoxUpdate, bool dirtyForRender, bool additionalDirtyRegion = false) + { + Root?.RequestUpdate(); + + var parent = Parent; + var setIsDirtyForRenderInSubgraph = additionalDirtyRegion || dirtyForRender; + while (parent != null && + ((needsBoundingBoxUpdate && !parent._needsBoundingBoxUpdate) || + (setIsDirtyForRenderInSubgraph && !parent._isDirtyForRenderInSubgraph))) + { + parent._needsBoundingBoxUpdate |= needsBoundingBoxUpdate; + parent._isDirtyForRenderInSubgraph |= setIsDirtyForRenderInSubgraph; + + parent = parent.Parent; + } + + _needsBoundingBoxUpdate |= needsBoundingBoxUpdate; + _isDirtyForRender |= dirtyForRender; + + // If node itself is dirty for render, we don't need to keep track of extra dirty rects + _hasExtraDirtyRect = !dirtyForRender && (_hasExtraDirtyRect || additionalDirtyRegion); + } + + public void RecomputeOwnProperties() + { + var setDirtyBounds = _contentChanged || _delayPropagateNeedsBoundsUpdate; + var setDirtyForRender = _contentChanged || _delayPropagateIsDirtyForRender; + var setHasExtraDirtyRect = _delayPropagateHasExtraDirtyRects; + + _delayPropagateIsDirtyForRender = + _delayPropagateHasExtraDirtyRects = + _delayPropagateIsDirtyForRender = false; + + _enqueuedForOwnPropertiesRecompute = false; + if (_ownBoundsDirty) + { + _ownContentBounds = ComputeOwnContentBounds()?.NullIfZeroSize(); + setDirtyForRender = setDirtyBounds = true; + } + + if (_clipSizeDirty) + { + LtrbRect? clip = null; + if (Clip != null) + clip = new(Clip.Bounds); + if (ClipToBounds) + { + var bounds = new LtrbRect(0, 0, Size.X, Size.Y); + clip = clip?.IntersectOrEmpty(bounds) ?? bounds; + } + + if (_ownClipRect != clip) + { + _ownClipRect = clip; + setDirtyForRender = setDirtyBounds = true; + } + } + + if (_combinedTransformDirty) + { + _ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale, + RotationAngle, Orientation, Offset); + + setDirtyForRender = setDirtyBounds = true; + + AttHelper_CombinedTransformChanged(); + } + + + setDirtyForRender |= _compositionFieldsDirty; + + _ownBoundsDirty = _clipSizeDirty = _combinedTransformDirty = _compositionFieldsDirty = false; + PropagateFlags(setDirtyBounds, setDirtyForRender, setHasExtraDirtyRect); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs new file mode 100644 index 0000000000..fa8c6047fc --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs @@ -0,0 +1,191 @@ +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + private bool _enqueuedForOwnPropertiesRecompute; + + private const CompositionVisualChangedFields CompositionFieldsMask + = CompositionVisualChangedFields.Opacity + | CompositionVisualChangedFields.OpacityAnimated + | CompositionVisualChangedFields.OpacityMaskBrush + | CompositionVisualChangedFields.Clip + | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.ClipToBoundsAnimated + | CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.RenderOptions + | CompositionVisualChangedFields.Effect; + + private const CompositionVisualChangedFields OwnBoundsUpdateFieldsMask = + CompositionVisualChangedFields.Clip + | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.ClipToBoundsAnimated + | CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.Effect; + + private const CompositionVisualChangedFields CombinedTransformFieldsMask = + CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.AnchorPoint + | CompositionVisualChangedFields.AnchorPointAnimated + | CompositionVisualChangedFields.CenterPoint + | CompositionVisualChangedFields.CenterPointAnimated + | CompositionVisualChangedFields.AdornedVisual + | CompositionVisualChangedFields.TransformMatrix + | CompositionVisualChangedFields.Scale + | CompositionVisualChangedFields.ScaleAnimated + | CompositionVisualChangedFields.RotationAngle + | CompositionVisualChangedFields.RotationAngleAnimated + | CompositionVisualChangedFields.Orientation + | CompositionVisualChangedFields.OrientationAnimated + | CompositionVisualChangedFields.Offset + | CompositionVisualChangedFields.OffsetAnimated; + + private const CompositionVisualChangedFields ClipSizeDirtyMask = + CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.Clip + | CompositionVisualChangedFields.ClipToBoundsAnimated; + + private const CompositionVisualChangedFields ReadbackDirtyMask = + CombinedTransformFieldsMask + | CompositionVisualChangedFields.Root + | CompositionVisualChangedFields.Visible + | CompositionVisualChangedFields.VisibleAnimated; + + partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) + { + if ((changed & CompositionFieldsMask) != 0) + TriggerCompositionFieldsDirty(); + if ((changed & CombinedTransformFieldsMask) != 0) + TriggerCombinedTransformDirty(); + + if ((changed & ClipSizeDirtyMask) != 0) + TriggerClipSizeDirty(); + if((changed & OwnBoundsUpdateFieldsMask) != 0) + { + _ownBoundsDirty = true; + EnqueueOwnPropertiesRecompute(); + } + + if((changed & ReadbackDirtyMask) != 0) + EnqueueForReadbackUpdate(); + + if ((changed & (CompositionVisualChangedFields.Visible | CompositionVisualChangedFields.VisibleAnimated)) != 0) + TriggerVisibleDirty(); + + if ((changed & (CompositionVisualChangedFields.SizeAnimated | CompositionVisualChangedFields.Size)) != 0) + SizeChanged(); + } + + + public override void NotifyAnimatedValueChanged(CompositionProperty property) + { + base.NotifyAnimatedValueChanged(property); + if (property == s_IdOfClipToBoundsProperty + || property == s_IdOfOpacityProperty + || property == s_IdOfSizeProperty) + TriggerCompositionFieldsDirty(); + + if (property == s_IdOfSizeProperty + || property == s_IdOfAnchorPointProperty + || property == s_IdOfCenterPointProperty + || property == s_IdOfAdornedVisualProperty + || property == s_IdOfTransformMatrixProperty + || property == s_IdOfScaleProperty + || property == s_IdOfRotationAngleProperty + || property == s_IdOfOrientationProperty + || property == s_IdOfOffsetProperty) + TriggerCombinedTransformDirty(); + + if (property == s_IdOfClipToBoundsProperty + || property == s_IdOfSizeProperty + ) TriggerClipSizeDirty(); + + if (property == s_IdOfSizeProperty) + SizeChanged(); + + if (property == s_IdOfVisibleProperty) + TriggerVisibleDirty(); + } + + protected virtual void SizeChanged() + { + + } + + protected void TriggerCompositionFieldsDirty() + { + _compositionFieldsDirty = true; + EnqueueOwnPropertiesRecompute(); + } + + protected void TriggerCombinedTransformDirty() + { + _combinedTransformDirty = true; + EnqueueOwnPropertiesRecompute(); + EnqueueForReadbackUpdate(); + } + + protected void TriggerClipSizeDirty() + { + EnqueueOwnPropertiesRecompute(); + _clipSizeDirty = true; + } + + protected void TriggerVisibleDirty() + { + EnqueueForReadbackUpdate(); + EnqueueForOwnBoundsRecompute(); + } + + partial void OnParentChanging() + { + if (Parent != null && _transformedSubTreeBounds.HasValue) + Parent.AddExtraDirtyRect(_transformedSubTreeBounds.Value); + AttHelper_ParentChanging(); + } + + partial void OnParentChanged() + { + if (Parent != null) + { + _delayPropagateNeedsBoundsUpdate = _delayPropagateIsDirtyForRender = true; + EnqueueOwnPropertiesRecompute(); + } + AttHelper_ParentChanged(); + } + + protected void AddExtraDirtyRect(LtrbRect rect) + { + _extraDirtyRect = _hasExtraDirtyRect ? _extraDirtyRect.Union(rect) : rect; + _delayPropagateHasExtraDirtyRects = true; + EnqueueOwnPropertiesRecompute(); + } + + + protected void EnqueueForOwnBoundsRecompute() + { + _ownBoundsDirty = true; + EnqueueOwnPropertiesRecompute(); + } + + protected void InvalidateContent() + { + _contentChanged = true; + EnqueueForOwnBoundsRecompute(); + } + + private void EnqueueOwnPropertiesRecompute() + { + if(_enqueuedForOwnPropertiesRecompute) + return; + _enqueuedForOwnPropertiesRecompute = true; + Compositor.EnqueueVisualForOwnPropertiesUpdatePass(this); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs new file mode 100644 index 0000000000..a054446a5a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics; +using System.Threading; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + // Here we are using a simplified version Multi-Version Concurrency Control with only one reader + // and only one writer. + // + // The goal is to provide un-teared view of a particular revision for the UI thread + // + // We are taking a shared lock before switching reader's revision and are using the same lock + // to produce a new revision, so we know for sure that reader can't switch to a newer revision + // while we are writing. + // + // Reader's behavior: + // 1) reader will only pick slots with revision <= its current revision + // 2) reader will pick the newest revision among slots from (1) + // There are two scenarios that can be encountered by the writer: + // 1) both slots contain data for revisions older than the reader's current revision, + // in that case we pick the slot with the oldest revision and update it. + // 1.1) if reader comes before update it will pick the newer one + // 1.2) if reader comes after update, the overwritten slot would have a revision that's higher than the reader's + // one, so it will still pick the same slot + // 2) one of the slots contains data for a revision newer than the reader's current revision. In that case + // we simply pick the slot with revision the reader isn't allowed to touch anyway. + // Both before and after update the reader will see only one (same) slot it's allowed to touch + // + // While having to hold a lock for the entire time we are writing the revision may seem suboptimal, + // the UI thread isn't likely to contend for that lock and we update pre-enqueued visuals, so it won't take much time. + + + public class ReadbackData + { + public Matrix Matrix; + public ulong Revision; + public long TargetId; + public bool Visible; + public LtrbRect? TransformedSubtreeBounds; + } + + private ReadbackData + _readback0 = new() { Revision = ulong.MaxValue }, + _readback1 = new() { Revision = ulong.MaxValue }; + + private bool _enqueuedForReadbackUpdate = false; + + private void EnqueueForReadbackUpdate() + { + if (!_enqueuedForReadbackUpdate) + { + _enqueuedForReadbackUpdate = true; + Compositor.EnqueueVisualForReadbackUpdatePass(this); + } + } + + public ReadbackData? GetReadback(ulong readerRevision) + { + // Prevent ulong tearing + var slot0Revision = Interlocked.Read(ref _readback0.Revision); + var slot1Revision = Interlocked.Read(ref _readback1.Revision); + + if (slot0Revision <= readerRevision && slot1Revision <= readerRevision) + { + // Pick the newest one, it's guaranteed to be not touched by the writer + return slot1Revision > slot0Revision ? _readback1 : _readback0; + } + + if (slot0Revision <= readerRevision) + return _readback0; + + if (slot1Revision <= readerRevision) + return _readback1; + + // No readback was written for this visual yet + return null; + } + + public void UpdateReadback(ulong writerRevision, ulong readerRevision) + { + _enqueuedForReadbackUpdate = false; + ReadbackData slot; + + // We don't need to use Interlocked.Read here since we are the only writer + + if (_readback0.Revision > readerRevision) // Future revision is in slot0 + slot = _readback0; + else if (_readback1.Revision > readerRevision) // Future revision is in slot1 + slot = _readback1; + else + // No future revisions, overwrite the oldest one since reader will always pick the newest + slot = (_readback0.Revision < _readback1.Revision) ? _readback0 : _readback1; + + // Prevent ulong tearing + Interlocked.Exchange(ref slot.Revision, writerRevision); + slot.Matrix = _ownTransform ?? Matrix.Identity; + slot.TargetId = Root?.Id ?? -1; + slot.TransformedSubtreeBounds = _transformedSubTreeBounds; + slot.Visible = Visible; + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs new file mode 100644 index 0000000000..ad828b068a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + [StructLayout(LayoutKind.Auto)] + partial struct RenderContext : IServerTreeVisitor, IDisposable + { + private readonly IDrawingContextImpl _canvas; + private readonly IDirtyRectTracker? _dirtyRects; + private readonly CompositorPools _pools; + private readonly bool _renderChildren; + private TreeWalkContext _walkContext; + private Stack _opacityStack; + private double _opacity; + private bool _fullSkip; + private bool _usedCache; + public int RenderedVisuals; + public int VisitedVisuals; + private ServerVisualRenderContext _publicContext; + private readonly ServerCompositionVisual _rootVisual; + private bool _skipNextVisualTransform; + private bool _renderingToBitmapCache; + + public RenderContext(ServerCompositionVisual rootVisual, IDrawingContextImpl canvas, + IDirtyRectTracker? dirtyRects, CompositorPools pools, Matrix matrix, LtrbRect clip, + bool renderChildren, bool skipRootVisualTransform, bool renderingToBitmapCache) + { + _publicContext = new ServerVisualRenderContext(canvas); + + if (dirtyRects != null) + { + var dirtyClip = dirtyRects.CombinedRect; + if (dirtyRects is SingleDirtyRectTracker) + dirtyRects = null; + clip = clip.IntersectOrEmpty(dirtyClip); + } + + _canvas = canvas; + _dirtyRects = dirtyRects; + _pools = pools; + _renderChildren = renderChildren; + + _rootVisual = rootVisual; + + _walkContext = new TreeWalkContext(pools, matrix, clip); + + _opacity = 1; + _opacityStack = pools.DoubleStackPool.Rent(); + _skipNextVisualTransform = skipRootVisualTransform; + _renderingToBitmapCache = renderingToBitmapCache; + } + + + private bool HandlePreGraphTransformClipOpacity(ServerCompositionVisual visual) + { + if (!visual.Visible || visual._transformedSubTreeBounds == null) + return false; + var effectiveOpacity = visual.Opacity * _opacity; + if (effectiveOpacity <= 0.003) + return false; + + ref var effectiveNewTransform = ref _walkContext.Transform; + Matrix transformToPush; + if (visual._ownTransform.HasValue) + { + if (!_skipNextVisualTransform) + { + transformToPush = visual._ownTransform.Value * _walkContext.Transform; + effectiveNewTransform = ref transformToPush; + } + } + + _skipNextVisualTransform = false; + + var effectiveClip = _walkContext.Clip; + if (visual._ownClipRect != null) + effectiveClip = effectiveClip.IntersectOrEmpty(visual._ownClipRect.Value.TransformToAABB(effectiveNewTransform)); + + var worldBounds = visual._transformedSubTreeBounds.Value.TransformToAABB(_walkContext.Transform); + if (!effectiveClip.Intersects(worldBounds) + || _dirtyRects?.Intersects(worldBounds) == false) + return false; + + + RenderedVisuals++; + + // We are still in parent's coordinate space here + + + if (visual.Opacity != 1) + { + _opacityStack.Push(effectiveOpacity); + _canvas.PushOpacity(visual.Opacity, visual._transformedSubTreeBounds.Value.ToRect()); + } + + // Switch coordinate space to this visual's space + + if (visual._ownTransform.HasValue) + { + _walkContext.PushSetTransform(effectiveNewTransform); // Reuse one computed before + _canvas.Transform = effectiveNewTransform; + } + + if (visual._ownClipRect.HasValue) + _walkContext.PushClip(effectiveClip); + + if (visual.ClipToBounds) + _canvas.PushClip(new Rect(0, 0, visual.Size.X, visual.Size.Y)); + + if (visual.Clip != null) + _canvas.PushGeometryClip(visual.Clip); + + return true; + } + + public void PreSubgraph(ServerCompositionVisual visual, out bool visitChildren) + { + VisitedVisuals++; + var bitmapCacheRoot = _renderingToBitmapCache && visual == _rootVisual; + + if (!bitmapCacheRoot) // Skip those for the root visual if we are rendering to bitmap cache + { + // Push transform, clip, opacity and check if those make the visual effectively invisible + if (!HandlePreGraphTransformClipOpacity(visual)) + { + _fullSkip = true; + visitChildren = false; + return; + } + + // Push adorner clip + if (visual.AdornedVisual != null) + AdornerHelper_RenderPreGraphPushAdornerClip(visual); + + // If caching is enabled, draw from cache and skip rendering + if (visual.Cache != null) + { + var (visited, rendered) = visual.Cache.Draw(_canvas); + VisitedVisuals += visited; + RenderedVisuals += rendered; + _usedCache = true; + visitChildren = false; + return; + } + } + + if(visual.RenderOptions != default) + _canvas.PushRenderOptions(visual.RenderOptions); + + if (visual.TextOptions != default) + _canvas.PushTextOptions(visual.TextOptions); + + if (visual.OpacityMaskBrush != null) + _canvas.PushOpacityMask(visual.OpacityMaskBrush, visual._subTreeBounds!.Value.ToRect()); + + if (visual.Effect != null && _canvas is IDrawingContextImplWithEffects effects) + effects.PushEffect(visual._subTreeBounds!.Value.ToRect(), visual.Effect); + + visual.RenderCore(_publicContext, _walkContext.Clip); + + visitChildren = _renderChildren; + } + + public void PostSubgraph(ServerCompositionVisual visual) + { + if (_fullSkip) + { + _fullSkip = false; + return; + } + + var bitmapCacheRoot = _renderingToBitmapCache && visual == _rootVisual; + + // If we've used cache, those never got pushed in PreSubgraph + if (!_usedCache) + { + if (visual.Effect != null && _canvas is IDrawingContextImplWithEffects effects) + effects.PopEffect(); + + if (visual.OpacityMaskBrush != null) + _canvas.PopOpacityMask(); + + if (visual.TextOptions != default) + _canvas.PopTextOptions(); + + if (visual.RenderOptions != default) + _canvas.PopRenderOptions(); + } + + // If we are rendering to bitmap cache, PreSubgraph skipped those for the root visual + if (!bitmapCacheRoot) + { + if (visual.AdornedVisual != null) + AdornerHelper_RenderPostGraphPushAdornerClip(visual); + + if (visual.Clip != null) + _canvas.PopGeometryClip(); + + if (visual.ClipToBounds) + _canvas.PopClip(); + + if (visual._ownClipRect.HasValue) + _walkContext.PopClip(); + + if (visual._ownTransform.HasValue) + { + _walkContext.PopTransform(); + _canvas.Transform = _walkContext.Transform; + } + + if (visual.Opacity != 1) + { + _canvas.PopOpacity(); + _opacity = _opacityStack.Pop(); + } + } + } + + public void Dispose() + { + _walkContext.Dispose(); + _pools.DoubleStackPool.Return(ref _opacityStack); + AdornerHelper_Dispose(); + } + } + + protected virtual void PushClipToBounds(IDrawingContextImpl canvas) => + canvas.PushClip(new Rect(0, 0, Size.X, Size.Y)); + + public (int visited, int rendered) Render(IDrawingContextImpl canvas, LtrbRect clip, IDirtyRectTracker? dirtyRects, + bool renderChildren = true, bool skipRootVisualTransform = false, bool renderingToBitmapCache = false) + { + var renderContext = new RenderContext(this, canvas, dirtyRects, Compositor.Pools, canvas.Transform, + clip, renderChildren, skipRootVisualTransform, renderingToBitmapCache); + try + { + ServerTreeWalker.Walk(ref renderContext, this); + return (renderContext.VisitedVisuals, renderContext.RenderedVisuals); + } + finally + { + renderContext.Dispose(); + } + } + +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs new file mode 100644 index 0000000000..b8322225bd --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionVisual +{ + protected virtual bool HasEffect => Effect != null; + + struct UpdateContext : IServerTreeVisitor, IDisposable + { + private TreeWalkContext _context; + + private IDirtyRectCollector _dirtyRegion; + private int _dirtyRegionDisableCount; + private Stack _dirtyRegionDisableCountStack; + private Stack _dirtyRegionCollectorStack; + private bool AreDirtyRegionsDisabled() => _dirtyRegionDisableCount != 0; + + public UpdateContext(CompositorPools pools, IDirtyRectCollector dirtyRects, Matrix transform, LtrbRect clip) + { + _dirtyRegion = dirtyRects; + _context = new TreeWalkContext(pools, transform, clip); + _dirtyRegionDisableCountStack = pools.IntStackPool.Rent(); + _dirtyRegionCollectorStack = pools.DirtyRectCollectorStackPool.Rent(); + } + + private void PushCacheIfNeeded(ServerCompositionVisual visual) + { + if (visual.Cache != null) + { + _dirtyRegionCollectorStack.Push(_dirtyRegion); + _dirtyRegion = visual.Cache.DirtyRectCollector; + _dirtyRegionDisableCountStack.Push(_dirtyRegionDisableCount); + _dirtyRegionDisableCount = 0; + + _context.PushSetTransform(Matrix.Identity); + _context.ResetClip(LtrbRect.Infinite); + } + } + + private void PopCacheIfNeeded(ServerCompositionVisual visual) + { + if (visual.Cache != null) + { + _context.PopClip(); + _context.PopTransform(); + _dirtyRegion = _dirtyRegionCollectorStack.Pop(); + _dirtyRegionDisableCount = _dirtyRegionDisableCountStack.Pop(); + if (visual.Cache.IsDirty) + AddToDirtyRegion(visual._subTreeBounds); + } + } + + private bool NeedToPushBoundsAffectingProperties(ServerCompositionVisual node) + { + return (node._isDirtyForRenderInSubgraph || node._hasExtraDirtyRect || node._contentChanged); + } + + public void PreSubgraph(ServerCompositionVisual node, out bool visitChildren) + { + visitChildren = node._isDirtyForRenderInSubgraph || node._needsBoundingBoxUpdate; + + // If this node has an alpha mask an we caused its inner bounds to change + // then treat the node as if _isDirtyForRender was set. + if (node is { _needsBoundingBoxUpdate: true, OpacityMaskBrush: not null }) + node._isDirtyForRender = true; + + // Special handling for effects: just add the entire node's old subtree bounds as a dirty region + // WPF does this because they had legacy effects with non-affine transforms, we do this because + // it's something to be done in the future (maybe) + if (node._isDirtyForRender || node is { _isDirtyForRenderInSubgraph: true, HasEffect: true }) + { + // If bounds haven't actually changed, there is no point in adding them now since they will be added + // again in PostSubgraph. + if (node._needsBoundingBoxUpdate && !AreDirtyRegionsDisabled()) + { + // We add this node's bbox to the dirty region. Alternatively we could walk the sub-graph and add the + // bbox of each node's content to the dirty region. Note that this is much harder to do because if the + // transform changes we don't know anymore the old transform. We would have to use to a two phased dirty + // region algorithm. + AddToDirtyRegion(node._transformedSubTreeBounds); + } + + // If we added a node in the parent chain to the bbox we don't need to add anything below this node + // to the dirty region. + _dirtyRegionDisableCount++; + } + + // If a node in the sub-graph of this node is dirty for render and we haven't collected the bbox of one of pNode's + // ascendants as dirty region, then we need to maintain the transform and clip stack so that we have a world transform + // when we need to collect the bbox of the descendant node that is dirty for render. If something has changed + // in the contents or subgraph, we need to update the cache on this node. + if (NeedToPushBoundsAffectingProperties(node)) + { + // Dirty regions will be enabled if we haven't collected an ancestor's bbox or if they were re-enabled + // by an ancestor's cache. + if (!AreDirtyRegionsDisabled()) + { + PushBoundsAffectingProperties(node); + } + + PushCacheIfNeeded(node); + } + + if (node._needsBoundingBoxUpdate) + { + // This node's bbox needs to be updated. We start out by setting his bbox to the bbox of its content. All its + // children will union their bbox into their parent's bbox. PostSubgraph will clip the bbox and transform it + // to outer space. + node._subTreeBounds = node._ownContentBounds; + } + } + + + public void PostSubgraph(ServerCompositionVisual node) + { + var parent = node.Parent; + if (node._needsBoundingBoxUpdate) + { + // + // If pNode's bbox got recomputed it is at this point still in inner + // space. We need to apply the clip and transform. + // + FinalizeSubtreeBounds(node); + } + + // + // Update state on the parent node if we have a parent. + + if (parent != null) + { + // Update the bounding box on the parent. + if (parent._needsBoundingBoxUpdate) + parent._subTreeBounds = LtrbRect.FullUnion(parent._subTreeBounds, node._transformedSubTreeBounds); + } + + // + // If there are additional dirty regions, pick them up. (Additional dirty regions are + // specified before the tranform, i.e. in inner space, hence we have to pick them + // up before we pop the transform from the transform stack. + // + if (node._hasExtraDirtyRect) + { + AddToDirtyRegion(node._extraDirtyRect); + } + + // If we pushed transforms here, we need to pop them again. If we're handling a cache we need + // to finish handling it here as well. + if (NeedToPushBoundsAffectingProperties(node)) + { + PopCacheIfNeeded(node); + if(!AreDirtyRegionsDisabled()) + PopBoundsAffectingProperties(node); + + } + + // Special handling for effects: just add the entire node's old subtree bounds as a dirty region + // WPF does this because they had legacy effects with non-affine transforms, we do this because + // it's something to be done in the future (maybe) + if(node._isDirtyForRender || node is { _isDirtyForRenderInSubgraph: true, Effect: not null }) + { + _dirtyRegionDisableCount--; + AddToDirtyRegion(node._transformedSubTreeBounds); + } + + node._isDirtyForRender = false; + node._isDirtyForRenderInSubgraph = false; + node._needsBoundingBoxUpdate = false; + node._hasExtraDirtyRect = false; + node._contentChanged = false; + } + + private void FinalizeSubtreeBounds(ServerCompositionVisual node) + { + // WPF simply removes drawing commands from every visual in invisible subtree (on UI thread). + // We set the bounds to null when computing subtree bounds for invisible nodes. + if (!node.Visible) + node._subTreeBounds = null; + + if (node._subTreeBounds != null) + { + if (node.Effect != null) + node._subTreeBounds = node._subTreeBounds.Value.Inflate(node.Effect.GetEffectOutputPadding()); + + if (node._ownClipRect.HasValue) + node._subTreeBounds = node._subTreeBounds.Value.IntersectOrNull(node._ownClipRect.Value); + } + + if (node._subTreeBounds == null) + node._transformedSubTreeBounds = null; + else if (node._ownTransform.HasValue) + node._transformedSubTreeBounds = node._subTreeBounds?.TransformToAABB(node._ownTransform.Value); + else + node._transformedSubTreeBounds = node._subTreeBounds; + + node.EnqueueForReadbackUpdate(); + } + + private void AddToDirtyRegion(LtrbRect? bounds) + { + if(_dirtyRegionDisableCount != 0 || !bounds.HasValue) + return; + + var transformed = bounds.Value.TransformToAABB(_context.Transform).IntersectOrEmpty(_context.Clip); + if(transformed.IsZeroSize) + return; + + _dirtyRegion.AddRect(transformed); + } + + private void PushBoundsAffectingProperties(ServerCompositionVisual node) + { + if (node._ownTransform.HasValue) + _context.PushTransform(node._ownTransform.Value); + if (node._ownClipRect.HasValue) + _context.PushClip(node._ownClipRect.Value.TransformToAABB(_context.Transform)); + } + + private void PopBoundsAffectingProperties(ServerCompositionVisual node) + { + if (node._ownTransform.HasValue) + _context.PopTransform(); + if (node._ownClipRect.HasValue) + _context.PopClip(); + } + + public void Dispose() + { + _context.Pools.IntStackPool.Return(ref _dirtyRegionDisableCountStack); + _context.Pools.DirtyRectCollectorStackPool.Return(ref _dirtyRegionCollectorStack); + _context.Dispose(); + } + } + + public void UpdateRoot(IDirtyRectCollector tracker, Matrix transform, LtrbRect clip) + { + var context = new UpdateContext(Compositor.Pools, tracker, transform, clip); + ServerTreeWalker.Walk(ref context, this); + context.Dispose(); + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Walker.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Walker.cs new file mode 100644 index 0000000000..8551371f41 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Walker.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + + interface IServerTreeVisitor + { + void PreSubgraph(ServerCompositionVisual visual, out bool visitChildren); + void PostSubgraph(ServerCompositionVisual visual); + } + + public record struct TreeWalkerFrame(ServerCompositionVisual Visual, int CurrentIndex); + + static class ServerTreeWalker where TVisitor : struct, IServerTreeVisitor + { + public static void Walk(ref TVisitor visitor, ServerCompositionVisual root) + { + var stackPool = root.Compositor.Pools.TreeWalkerFrameStackPool; + var frames = stackPool.Rent(); + try + { + visitor.PreSubgraph(root, out var visitChildren); + + var container = root; + if(!visitChildren + || container.Children == null + || container.Children.List.Count == 0) + { + visitor.PostSubgraph(root); + return; + } + + int currentIndex = 0; + + while (true) + { + if (currentIndex >= container.Children!.List.Count) + { + // Exiting "recursion" + + visitor.PostSubgraph(container); + + if(!frames.TryPop(out var frame)) + break; + (container, currentIndex) = frame; + continue; + } + var child = container.Children.List[currentIndex]; + visitor.PreSubgraph(child, out visitChildren); + if (visitChildren && child.Children!.List.Count > 0) + { + // Go deeper + frames.Push(new TreeWalkerFrame(container, currentIndex + 1)); + container = child; + currentIndex = 0; + continue; // Enter "recursion" + } + + // Haven't entered recursion, still call PostSubgraph and go to the next sibling + visitor.PostSubgraph(child); + currentIndex++; + } + } + finally + { + stackPool.Return(frames); + } + } + + } + + struct TreeWalkContext : IDisposable + { + private readonly CompositorPools _pools; + public CompositorPools Pools => _pools; + public Matrix Transform; + public LtrbRect Clip; + + private Stack _transformStack; + private Stack _clipStack; + + public TreeWalkContext(CompositorPools pools, Matrix transform, LtrbRect clip) + { + _pools = pools; + Transform = transform; + Clip = clip; + _transformStack = pools.MatrixStackPool.Rent(); + _clipStack = pools.LtrbRectStackPool.Rent(); + } + + public void PushTransform(in Matrix m) + { + _transformStack.Push(Transform); + Transform = m * Transform; + } + + public void PushSetTransform(in Matrix m) + { + _transformStack.Push(Transform); + Transform = m; + } + + public void PushClip(LtrbRect rect) + { + _clipStack.Push(Clip); + Clip = Clip.IntersectOrEmpty(rect); + } + + public void ResetClip(LtrbRect rect) + { + _clipStack.Push(Clip); + Clip = rect; + } + + public void PopTransform() + { + Transform = _transformStack.Pop(); + } + + public void PopClip() + { + Clip = _clipStack.Pop(); + } + + public void Dispose() + { + _pools.MatrixStackPool.Return(ref _transformStack); + _pools.LtrbRectStackPool.Return(ref _clipStack); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.cs new file mode 100644 index 0000000000..5e1b91f7a4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.Intrinsics; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// Server-side counterpart. + /// Is responsible for computing the transformation matrix, for applying various visual + /// properties before calling visual-specific drawing code and for notifying the + /// for new dirty rects + /// + partial class ServerCompositionVisual : ServerObject + { + public ServerCompositionVisualCollection? Children { get; private set; } = null!; + public ServerCompositionVisualCache? Cache { get; private set; } + + partial void OnRootChanging() + { + if (Root != null) + { + Root.RemoveVisual(this); + OnDetachedFromRoot(Root); + } + } + + protected virtual void OnDetachedFromRoot(ServerCompositionTarget target) + { + } + + partial void OnRootChanged() + { + if (Root != null) + { + Root.AddVisual(this); + OnAttachedToRoot(Root); + AdornerHelper_AttachedToRoot(); + } + Cache?.FreeResources(); + } + + protected virtual void OnAttachedToRoot(ServerCompositionTarget target) + { + } + + partial void OnCacheModeChanging() + { + CacheMode?.Unsubscribe(this); + Cache?.FreeResources(); + Cache = null; + } + + partial void OnCacheModeChanged() + { + Cache = CacheMode is ServerCompositionBitmapCache bitmapCache ? new ServerCompositionVisualCache(this, bitmapCache) : null; + CacheMode?.Subscribe(this); + OnCacheModeStateChanged(); + } + + public void OnCacheModeStateChanged() + { + Cache?.InvalidateProperties(); + InvalidateContent(); + } + + + protected virtual void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) + { + } + + partial void Initialize() + { + Children = new ServerCompositionVisualCollection(Compositor); + } + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs new file mode 100644 index 0000000000..a9a481b5e3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs @@ -0,0 +1,225 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionVisualCache +{ + private readonly ServerCompositionBitmapCache _cacheMode; + private bool _needsFullReRender; + private IDrawingContextLayerImpl? _layer; + private IPlatformRenderInterfaceContext? _layerCreatedWithContext; + private bool _layerHasTextAntialiasing; + private PixelSize _desiredLayerSize; + private double _scaleX, _scaleY; + private Vector _drawAtOffset; + private bool _needToFinalizeFrame = true; + + public IDirtyRectCollector DirtyRectCollector { get; private set; } = null!; + public bool IsDirty => !_dirtyRectTracker.IsEmpty; + + public ServerCompositionVisualCache(ServerCompositionVisual visual, ServerCompositionBitmapCache cacheMode) + { + _cacheMode = cacheMode; + TargetVisual = visual; + DirtyRectCollector = new DirtyRectCollectorProxy(this); + MarkForFullReRender(); + } + + public ServerCompositionVisual TargetVisual { get; } + private ServerCompositor Compositor => TargetVisual.Compositor; + private double RenderAtScale => _cacheMode.RenderAtScale; + private bool SnapsToDevicePixels => _cacheMode.SnapsToDevicePixels; + private bool EnableClearType => _cacheMode.EnableClearType; + + public void FreeResources() + { + _layer?.Dispose(); + _layerCreatedWithContext = null; + } + + + public void InvalidateProperties() + { + MarkForFullReRender(); + } + + void ResetDirtyRects() + { + _needToFinalizeFrame = true; + _dirtyRectTracker.Initialize(LtrbRect.Infinite); + } + + void MarkForFullReRender() + { + _needsFullReRender = true; + ResetDirtyRects(); + } + + class DirtyRectCollectorProxy(ServerCompositionVisualCache parent) : IDirtyRectCollector + { + public void AddRect(LtrbRect rect) + { + parent._needToFinalizeFrame = true; + + // scale according to our render transform, since those values come in local space of the visual + parent._dirtyRectTracker.AddRect(new LtrbRect((rect.Left + parent._drawAtOffset.X) * parent._scaleX, + (rect.Top + parent._drawAtOffset.Y) * parent._scaleY, + (rect.Right + parent._drawAtOffset.X) * parent._scaleX, + (rect.Bottom + parent._drawAtOffset.Y) * parent._scaleY)); + } + } + + private readonly IDirtyRectTracker _dirtyRectTracker = new SingleDirtyRectTracker(); + + static bool IsCloseReal(double a, double b) + { + // Underlying rendering platform is using floats anyway, so we use float epsilon here + return (Math.Abs((a - b) / ((b == 0.0f) ? 1.0f : b)) < 10.0f * MathUtilities.FloatEpsilon); + } + + + bool UpdateRealizationDimensions() + { + var targetVisual = TargetVisual; + if(!(targetVisual is { Root: not null, SubTreeBounds: {} visualBounds })) + return false; + + // Since the cache relies only on local space bounds, the DPI isn't taken into account (as it's the root + // transform of the visual tree). Scale for DPI if needed here. + var scale = targetVisual.Root.Scaling * RenderAtScale; + + + + // Caches are not clipped to the window bounds, they use local space bounds, + // so (especially in combination with RenderScale) a very large intermediate + // surface could be requested. Instead of failing in this case, we clamp the + // surface to the max texture size, which can cause some pixelation but will + // allow the app to render in hardware and still benefit from a cache. + var maxSize = Compositor.RenderInterface.Value.MaxOffscreenRenderTargetPixelSize + ?? new PixelSize(16384, 16384); + + // We round our bounds up to integral values for consistency here, since we need to do so when creating the surface anyway. + // This also ensures that our content will always be drawn in its entirety in the texture. + // Future Consideration: Note that if we want to use the cache texture for TextureBrush or as input to Effects, we'll + // need to be able to toggle this "snap-out" behavior to avoid seams since Effects by default + // do NOT snap the size out, they round down to integral bounds. + var fWidth = visualBounds.Width * scale; + var uWidth = (int)fWidth; + // If our width was non-integer, round up. + if (!IsCloseReal(fWidth, fWidth)) + uWidth++; + + var fHeight = visualBounds.Height * scale; + var uHeight = (int)fHeight; + + // If our height was non-integer, round up. + if (!IsCloseReal(fHeight, fHeight)) + uHeight++; + + _scaleX = _scaleY = scale; + if (uWidth > maxSize.Width) + { + _scaleX *= (double)maxSize.Width / uWidth; + uWidth = maxSize.Width; + } + + if(uHeight > maxSize.Height) + { + _scaleY *= (double)maxSize.Height / uHeight; + uHeight = maxSize.Height; + } + + _drawAtOffset = new Vector(-visualBounds.Left, -visualBounds.Top); + + _desiredLayerSize = new PixelSize(uWidth, uHeight); + + return true; + } + + public (int visitedVisuals, int renderedVisuals) Draw(IDrawingContextImpl outerCanvas) + { + if (TargetVisual.SubTreeBounds == null) + return default; + + UpdateRealizationDimensions(); + + var renderContext = Compositor.RenderInterface.Value; + + // Re-create layer if needed + if (_layer == null + || _layerHasTextAntialiasing != EnableClearType + || _layer.PixelSize != _desiredLayerSize + || _layerCreatedWithContext != renderContext) + { + _layer?.Dispose(); + _layer = null; + _layerCreatedWithContext = null; + + if (_desiredLayerSize.Width < 1 || _desiredLayerSize.Height < 1) + { + ResetDirtyRects(); + return default; + } + + _layer = renderContext.CreateOffscreenRenderTarget(_desiredLayerSize, new Vector(_scaleX, _scaleX), + EnableClearType); + _layerHasTextAntialiasing = EnableClearType; + _layerCreatedWithContext = renderContext; + _needsFullReRender = true; + } + + var fullFrameRect = new LtrbRect(0, 0, + _layer.PixelSize.Width, _layer.PixelSize.Height); + + // Extend the dirty rect area if needed + if (_needsFullReRender) + { + ResetDirtyRects(); + DirtyRectCollector.AddRect(LtrbRect.Infinite); + } + + // Compute the final dirty rect set that accounts for antialiasing effects + if (_needToFinalizeFrame) + { + _dirtyRectTracker.FinalizeFrame(fullFrameRect); + _needToFinalizeFrame = false; + } + + var visualLocalBounds = TargetVisual.SubTreeBounds.Value.ToRect(); + (int, int) rv = default; + // Render to layer if needed + if (!_dirtyRectTracker.IsEmpty) + { + using var ctx = _layer.CreateDrawingContext(false); + using (_needsFullReRender ? null : _dirtyRectTracker.BeginDraw(ctx)) + { + ctx.Clear(Colors.Transparent); + ctx.Transform = Matrix.CreateTranslation(_drawAtOffset) * Matrix.CreateScale(_scaleX, _scaleY); + rv = TargetVisual.Render(ctx, _dirtyRectTracker.CombinedRect, _dirtyRectTracker, renderingToBitmapCache: true); + } + } + _needsFullReRender = false; + + var originalTransform = outerCanvas.Transform; + if (SnapsToDevicePixels) + { + var worldBounds = visualLocalBounds.TransformToAABB(originalTransform); + var snapOffsetX = worldBounds.Left - Math.Floor(worldBounds.Left); + var snapOffsetY = worldBounds.Top - Math.Floor(worldBounds.Top); + outerCanvas.Transform = originalTransform * Matrix.CreateTranslation(-snapOffsetX, -snapOffsetY); + } + + //TODO: Maybe adjust for that extra pixel added due to rounding? + outerCanvas.DrawBitmap(_layer, 1, new Rect(0,0, _layer.PixelSize.Width, _layer.PixelSize.Height), + visualLocalBounds); + if (SnapsToDevicePixels) + outerCanvas.Transform = originalTransform; + + // Set empty dirty rects for next frame + ResetDirtyRects(); + return rv; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.Passes.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.Passes.cs new file mode 100644 index 0000000000..c08dfb6e67 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.Passes.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositor +{ + private readonly Queue _renderResourcesInvalidationQueue = new(); + private readonly HashSet _renderResourcesInvalidationSet = new(); + // TODO: parallel processing maybe + private readonly Queue _visualOwnPropertiesRecomputePass = new(); + private readonly Queue _visualReadbackUpdatePassQueue = new(); + private readonly Queue _adornerUpdateQueue = new(); + + private void ApplyEnqueuedRenderResourceChangesPass() + { + while (_renderResourcesInvalidationQueue.TryDequeue(out var obj)) + obj.QueuedInvalidate(); + _renderResourcesInvalidationSet.Clear(); + } + + public void EnqueueRenderResourceForInvalidation(IServerRenderResource resource) + { + if (_renderResourcesInvalidationSet.Add(resource)) + _renderResourcesInvalidationQueue.Enqueue(resource); + } + + private void VisualOwnPropertiesUpdatePass() + { + while (_visualOwnPropertiesRecomputePass.TryDequeue(out var obj)) + obj.RecomputeOwnProperties(); + } + + public void EnqueueVisualForOwnPropertiesUpdatePass(ServerCompositionVisual visual) => + _visualOwnPropertiesRecomputePass.Enqueue(visual); + + + private void VisualReadbackUpdatePass() + { + if(_visualReadbackUpdatePassQueue.Count == 0) + return; + + // visual.HitTest is waiting for this lock to be released, so we need to be quick + // this is why we have a queue in the first place + Readback.BeginWrite(); + try + { + var read = Readback.ReadRevision; + var write = Readback.WriteRevision; + while (_visualReadbackUpdatePassQueue.TryDequeue(out var obj)) + obj.UpdateReadback(write, read); + } + finally + { + Readback.EndWrite(); + } + } + + public void EnqueueVisualForReadbackUpdatePass(ServerCompositionVisual visual) => + _visualReadbackUpdatePassQueue.Enqueue(visual); + + + public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual); + + private void AdornerUpdatePass() + { + while (_adornerUpdateQueue.Count > 0) + { + var adorner = _adornerUpdateQueue.Dequeue(); + adorner.UpdateAdorner(); + } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs deleted file mode 100644 index 85bed78db8..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; - -namespace Avalonia.Rendering.Composition.Server; - -partial class ServerCompositor -{ - private readonly Queue _renderResourcesInvalidationQueue = new(); - private readonly HashSet _renderResourcesInvalidationSet = new(); - - public void ApplyEnqueuedRenderResourceChanges() - { - while (_renderResourcesInvalidationQueue.TryDequeue(out var obj)) - obj.QueuedInvalidate(); - _renderResourcesInvalidationSet.Clear(); - } - - public void EnqueueRenderResourceForInvalidation(IServerRenderResource resource) - { - if (_renderResourcesInvalidationSet.Add(resource)) - _renderResourcesInvalidationQueue.Enqueue(resource); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs index cb4da6723f..d299bed384 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs @@ -61,16 +61,11 @@ internal partial class ServerCompositor IDrawingContextLayerImpl? target = null; try { - target = RenderInterface.Value.CreateOffscreenRenderTarget(pixelSize, scaling); + target = RenderInterface.Value.CreateOffscreenRenderTarget(pixelSize, new(scaling, scaling), true); using (var canvas = target.CreateDrawingContext(false)) { - var proxy = new CompositorDrawingContextProxy(canvas) - { - PostTransform = invertRootTransform * scaleTransform, - Transform = Matrix.Identity - }; - var ctx = new ServerVisualRenderContext(proxy, null, true, renderChildren); - visual.Render(ctx, null); + canvas.Transform = scaleTransform; + visual.Render(canvas, LtrbRect.Infinite, null, renderChildren); } if (target is IDrawingContextLayerWithRenderContextAffinityImpl affined diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index dd540ecf9f..e23197ff13 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -30,6 +30,7 @@ namespace Avalonia.Rendering.Composition.Server private readonly List _activeTargets = new(); internal BatchStreamObjectPool BatchObjectPool; internal BatchStreamMemoryPool BatchMemoryPool; + public CompositorPools Pools { get; } = new(); private readonly object _lock = new object(); private Thread? _safeThread; private bool _uiThreadIsInsideRender; @@ -41,6 +42,7 @@ namespace Avalonia.Rendering.Composition.Server internal static readonly object RenderThreadPostTargetJobsEndMarker = new(); public CompositionOptions Options { get; } public ServerCompositorAnimations Animations { get; } + public ReadbackIndices Readback { get; } = new(); public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, CompositionOptions options, @@ -212,17 +214,32 @@ namespace Avalonia.Rendering.Composition.Server } } } - - private void RenderCore(bool catchExceptions) + + private TimeSpan ExecuteGlobalPasses() { - UpdateServerTime(); + var compositorGlobalPassesStarted = Stopwatch.GetTimestamp(); ApplyPendingBatches(); NotifyBatchesProcessed(); Animations.Process(); + ApplyEnqueuedRenderResourceChangesPass(); + + VisualOwnPropertiesUpdatePass(); + + // Adorners need to be updated after own properties recompute pass, + // because they may depend on ancestor's transform chain to be consistent + AdornerUpdatePass(); - ApplyEnqueuedRenderResourceChanges(); + return Stopwatch.GetElapsedTime(compositorGlobalPassesStarted); + } + + private void RenderCore(bool catchExceptions) + { + + UpdateServerTime(); + + var compositorGlobalPassesElapsed = ExecuteGlobalPasses(); try { @@ -230,8 +247,15 @@ namespace Avalonia.Rendering.Composition.Server return; RenderInterface.EnsureValidBackendContext(); ExecuteServerJobs(_receivedJobQueue); + foreach (var t in _activeTargets) + { + t.Update(compositorGlobalPassesElapsed); t.Render(); + } + + VisualReadbackUpdatePass(); + ExecuteServerJobs(_receivedPostTargetJobQueue); } catch (Exception e) when(RT_OnContextLostExceptionFilterObserver(e) && catchExceptions) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs index 8ba5470e7f..0b9c6889c5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs @@ -42,7 +42,7 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer Compositor.Animations.RemoveFromClock(this); } - public override LtrbRect OwnContentBounds => new(_handler.GetRenderBounds()); + public override LtrbRect? ComputeOwnContentBounds() => new LtrbRect(_handler.GetRenderBounds()); protected override void OnAttachedToRoot(ServerCompositionTarget target) { @@ -57,13 +57,10 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer base.OnDetachedFromRoot(target); } - internal void HandlerInvalidate() => ValuesInvalidated(); + internal void HandlerInvalidate() => InvalidateContent(); + + internal void HandlerInvalidate(Rect rc) => AddExtraDirtyRect(new LtrbRect(rc)); - internal void HandlerInvalidate(Rect rc) - { - Root?.AddDirtyRect(new LtrbRect(rc).TransformToAABB(GlobalTransformMatrix)); - } - internal void HandlerRegisterForNextAnimationFrameUpdate() { _wantsNextAnimationFrameAfterTick = true; @@ -73,8 +70,14 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer protected override void RenderCore(ServerVisualRenderContext ctx, LtrbRect currentTransformedClip) { - ctx.Canvas.AutoFlush = true; - using var context = new ImmediateDrawingContext(ctx.Canvas, GlobalTransformMatrix, false); + var proxy = ctx.Canvas as CompositorDrawingContextProxy; + if (proxy != null) + { + proxy.AutoFlush = true; + proxy.Flush(); + } + + using var context = new ImmediateDrawingContext(ctx.Canvas, ctx.Canvas.Transform, false); try { _handler.Render(context, currentTransformedClip.ToRect()); @@ -85,6 +88,6 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnRender)} {{0}}", e); } - ctx.Canvas.AutoFlush = false; + proxy?.AutoFlush = false; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index c845321cc9..f975e8e726 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -65,7 +65,7 @@ namespace Avalonia.Rendering.Composition.Server _animations?.RemoveAnimationForProperty(property); } - public virtual void NotifyAnimatedValueChanged(CompositionProperty prop) => ValuesInvalidated(); + public virtual void NotifyAnimatedValueChanged(CompositionProperty property) => ValuesInvalidated(); public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null; ExpressionVariant IExpressionObject.GetProperty(string name) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSizeDependantVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSizeDependantVisual.cs new file mode 100644 index 0000000000..bd8a568073 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSizeDependantVisual.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server; + +class ServerSizeDependantVisual : ServerCompositionContainerVisual +{ + public ServerSizeDependantVisual(ServerCompositor compositor) : base(compositor) + { + } + + public override LtrbRect? ComputeOwnContentBounds() + { + if (Size.X == 0 || Size.Y == 0) + return null; + return new LtrbRect(0, 0, Size.X, Size.Y); + } + + protected override void SizeChanged() + { + EnqueueForOwnBoundsRecompute(); + base.SizeChanged(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs index 4ee22d3bff..08302cfda1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs @@ -6,72 +6,10 @@ namespace Avalonia.Rendering.Composition.Server; internal class ServerVisualRenderContext { - public IDirtyRectTracker? DirtyRects { get; } - public bool DetachedRendering { get; } - public bool RenderChildren { get; } - public CompositorDrawingContextProxy Canvas { get; } - private readonly Stack? _transformStack; + public IDrawingContextImpl Canvas { get; } - public ServerVisualRenderContext(CompositorDrawingContextProxy canvas, IDirtyRectTracker? dirtyRects, - bool detachedRendering, bool renderChildren) + public ServerVisualRenderContext(IDrawingContextImpl canvas) { Canvas = canvas; - DirtyRects = dirtyRects; - DetachedRendering = detachedRendering; - RenderChildren = renderChildren; - if (detachedRendering) - { - _transformStack = new(); - _transformStack.Push(canvas.Transform); - } } - - - public bool ShouldRender(ServerCompositionVisual visual, LtrbRect currentTransformedClip) - { - if (DetachedRendering) - return true; - if (currentTransformedClip.IsZeroSize) - return false; - if (DirtyRects?.Intersects(currentTransformedClip) == false) - return false; - return true; - } - - public bool ShouldRenderOwnContent(ServerCompositionVisual visual, LtrbRect currentTransformedClip) - { - if (DetachedRendering) - return true; - return currentTransformedClip.Intersects(visual.TransformedOwnContentBounds) - && DirtyRects?.Intersects(visual.TransformedOwnContentBounds) != false; - } - - public RestoreTransform SetOrPushTransform(ServerCompositionVisual visual) - { - if (!DetachedRendering) - { - Canvas.Transform = visual.GlobalTransformMatrix; - return default; - } - else - { - var transform = visual.CombinedTransformMatrix * _transformStack!.Peek(); - Canvas.Transform = transform; - _transformStack.Push(transform); - return new RestoreTransform(this); - } - } - - public struct RestoreTransform(ServerVisualRenderContext? parent) : IDisposable - { - public void Dispose() - { - if (parent != null) - { - parent._transformStack!.Pop(); - parent.Canvas.Transform = parent._transformStack.Peek(); - } - } - } - } diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs index 35be380425..e4b469d327 100644 --- a/src/Avalonia.Base/Rendering/Composition/Visual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -2,6 +2,7 @@ using System; using System.Numerics; using Avalonia.Media; using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; using Avalonia.VisualTree; namespace Avalonia.Rendering.Composition @@ -12,14 +13,44 @@ namespace Avalonia.Rendering.Composition public abstract partial class CompositionVisual { private IBrush? _opacityMask; - + protected int CustomHitTestCountInSubTree; + public bool DisableSubTreeBoundsHitTestOptimization => CustomHitTestCountInSubTree != 0; + private protected virtual void OnRootChangedCore() { } partial void OnRootChanged() => OnRootChangedCore(); - partial void OnParentChanged() => Root = Parent?.Root; + partial void OnParentChanging() + { + // Propagate the blight + if (CustomHitTestCountInSubTree != 0) + { + var parent = Parent; + while (parent != null) + { + parent.CustomHitTestCountInSubTree -= CustomHitTestCountInSubTree; + parent = parent.Parent; + } + } + } + + partial void OnParentChanged() + { + Root = Parent?.Root; + // Propagate the blight + if (CustomHitTestCountInSubTree != 0) + { + var parent = Parent; + while (parent != null) + { + parent.CustomHitTestCountInSubTree -= CustomHitTestCountInSubTree; + parent = parent.Parent; + } + } + } + public IBrush? OpacityMask { @@ -48,22 +79,24 @@ namespace Avalonia.Rendering.Composition } } - internal Matrix? TryGetServerGlobalTransform() + internal ServerCompositionVisual.ReadbackData? TryGetValidReadback() { if (Root == null) return null; - var i = Root.Server.Readback; - ref var readback = ref Server.GetReadback(i.ReadIndex); + var i = Server.Compositor.Readback; + var readback = Server.GetReadback(i.ReadRevision); + if (readback == null) + return null; // CompositionVisual wasn't visible or wasn't even attached to the composition target during the lat frame - if (!readback.Visible || readback.Revision < i.ReadRevision) + if (!readback.Visible || readback.TargetId != Root.Server.Id) return null; // CompositionVisual was reparented (potential race here) if (readback.TargetId != Root.Server.Id) return null; - - return readback.Matrix; + + return readback; } internal object? Tag { get; set; } diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 01f041c5ee..1ebf4f6457 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -18,7 +18,7 @@ namespace Avalonia.Utilities // smallest such that 1.0+DoubleEpsilon != 1.0 internal const double DoubleEpsilon = 2.2204460492503131e-016; - private const float FloatEpsilon = 1.192092896e-07F; + internal const float FloatEpsilon = 1.192092896e-07F; /// /// AreClose - Returns whether or not two doubles are "close". That is, whether or diff --git a/src/Avalonia.Base/Visual.Composition.cs b/src/Avalonia.Base/Visual.Composition.cs index a1f90a5407..be2d42782d 100644 --- a/src/Avalonia.Base/Visual.Composition.cs +++ b/src/Avalonia.Base/Visual.Composition.cs @@ -144,6 +144,10 @@ public partial class Visual if (!Equals(comp.OpacityMask, OpacityMask)) comp.OpacityMask = OpacityMask; + var cacheMode = CacheMode?.GetForCompositor(comp.Compositor); + if (!ReferenceEquals(comp.CacheMode, cacheMode)) + comp.CacheMode = cacheMode; + if (!comp.Effect.EffectEquals(Effect)) comp.Effect = Effect?.ToImmutable(); diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index ee960cee1e..843914dc1d 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -68,6 +68,12 @@ namespace Avalonia public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); + /// + /// Defines the property. + /// + public static readonly StyledProperty CacheModeProperty = AvaloniaProperty.Register( + nameof(CacheMode)); + /// /// Defines the property. /// @@ -256,6 +262,15 @@ namespace Avalonia set { SetValue(OpacityMaskProperty, value); } } + /// + /// Gets or sets the cache mode of the visual. + /// + public CacheMode? CacheMode + { + get => GetValue(CacheModeProperty); + set => SetValue(CacheModeProperty, value); + } + /// /// Gets or sets the effect of the control. /// @@ -326,11 +341,11 @@ namespace Avalonia /// protected internal IRenderRoot? VisualRoot => _visualRoot; - internal RenderOptions RenderOptions - { + internal RenderOptions RenderOptions + { get => _renderOptions; - set - { + set + { _renderOptions = value; InvalidateVisual(); } diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index 670d879f29..55176a6502 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualTree/VisualExtensions.cs @@ -358,14 +358,7 @@ namespace Avalonia.VisualTree return Array.Empty(); } - var rootPoint = visual.TranslatePoint(p, (Visual)root); - - if (rootPoint.HasValue) - { - return root.HitTester.HitTest(rootPoint.Value, visual, filter); - } - - return Enumerable.Empty(); + return root.HitTester.HitTest(p, visual, filter); } /// diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index dd1d284cd5..febf465042 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -35,17 +35,18 @@ - + - + - + - + @@ -110,4 +111,11 @@ + + + + + + + diff --git a/src/Avalonia.Controls/BorderVisual.cs b/src/Avalonia.Controls/BorderVisual.cs index 3046e7e875..908c44db6b 100644 --- a/src/Avalonia.Controls/BorderVisual.cs +++ b/src/Avalonia.Controls/BorderVisual.cs @@ -46,25 +46,6 @@ class CompositionBorderVisual : CompositionDrawListVisual { } - protected override void RenderCore(ServerVisualRenderContext ctx, LtrbRect currentTransformedClip) - { - var canvas = ctx.Canvas; - if (ClipToBounds) - { - var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y))); - if (_cornerRadius == default) - canvas.PushClip(clipRect); - else - canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); - } - - base.RenderCore(ctx, currentTransformedClip); - - if(ClipToBounds) - canvas.PopClip(); - - } - protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { base.DeserializeChangesCore(reader, committedAt); @@ -72,7 +53,16 @@ class CompositionBorderVisual : CompositionDrawListVisual _cornerRadius = reader.Read(); } - protected override bool HandlesClipToBounds => true; + + protected override void PushClipToBounds(IDrawingContextImpl canvas) + { + var clipRect = new Rect(new Size(Size.X, Size.Y)); + if (_cornerRadius == default) + canvas.PushClip(clipRect); + else + canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); + } + } } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 05d835c57c..2759db05c2 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -56,11 +56,13 @@ namespace Avalonia.Headless => new HeadlessGeometryStub(g1.Bounds.Union(g2.Bounds)); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); - public IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling) => - new HeadlessBitmapStub(pixelSize, new Vector(96 * scaling, 96 * scaling)); + public IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, Vector scaling, + bool enableTextAntialiasing) => + new HeadlessBitmapStub(pixelSize, scaling * 96); public bool IsLost => false; public IReadOnlyDictionary PublicFeatures { get; } = new Dictionary(); + public PixelSize? MaxOffscreenRenderTargetPixelSize => null; public object? TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 85a2cbdd22..a4d8bafa56 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -6,6 +6,8 @@ using SkiaSharp; namespace Avalonia.Skia { + //TODO12: Make it private + /// /// Custom Skia gpu instance. /// diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs index db50c1e424..ee952457dc 100644 --- a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -29,6 +29,13 @@ internal class SkiaContext : IPlatformRenderInterfaceContext // TODO12: extend ISkiaGpu with PublicFeatures instead TryFeature(); TryFeature(); + using (var gr = (_gpu as ISkiaGpuWithPlatformGraphicsContext)?.TryGetGrContext()) + { + var renderTargetSize = gr?.Value.MaxRenderTargetSize; + if (renderTargetSize.HasValue) + MaxOffscreenRenderTargetPixelSize = + new PixelSize(renderTargetSize.Value, renderTargetSize.Value); + } } PublicFeatures = features; @@ -61,7 +68,11 @@ internal class SkiaContext : IPlatformRenderInterfaceContext "Don't know how to create a Skia render target from any of provided surfaces"); } - public IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling) + + public PixelSize? MaxOffscreenRenderTargetPixelSize { get; } + + public IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, Vector scaling, + bool enableTextAntialiasing) { using (var gr = (_gpu as ISkiaGpuWithPlatformGraphicsContext)?.TryGetGrContext()) { @@ -69,9 +80,9 @@ internal class SkiaContext : IPlatformRenderInterfaceContext { Width = pixelSize.Width, Height = pixelSize.Height, - Dpi = new Vector(96 * scaling, 96 * scaling), + Dpi = scaling * 96, Format = null, - DisableTextLcdRendering = false, + DisableTextLcdRendering = !enableTextAntialiasing, GrContext = gr?.Value, Gpu = _gpu, DisableManualFbo = true, diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index 2f27a752e2..4210dfc308 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -228,7 +228,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator server = server.AddMembers(DeclareField($"CompositionProperty<{serverPropertyType}>", CompositionPropertyField(prop), EqualsValueClause(ParseExpression( $"CompositionProperty.Register<{serverName}, {serverPropertyType}>(\"{prop.Name}\", obj => (({serverName})obj).{fieldName}, (obj, v) => (({serverName})obj).{fieldName} = v, {compositionPropertyVariantGetter})")), - SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword)); + SyntaxKind.InternalKeyword, SyntaxKind.ReadOnlyKeyword, SyntaxKind.StaticKeyword)); if (prop.DefaultValue != null) { @@ -323,6 +323,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator IdentifierName(fieldName), IdentifierName("value")), Block( + ParseStatement($"Validate{prop.Name}Change({fieldName}, value);"), ParseStatement("On" + prop.Name + "Changing();"), ParseStatement("changed = true;"), GeneratePropertySetterAssignment(cl, prop, isObject, isNullable)) @@ -332,6 +333,12 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ParseStatement($"if(changed) On" + prop.Name + "Changed();") )).WithModifiers(TokenList(prop.InternalSet ? new[]{Token(SyntaxKind.InternalKeyword)} : Array.Empty())) )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "Validate" + prop.Name + "Change") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()) + .WithParameterList(ParameterList(SeparatedList([ + Parameter(Identifier("oldValue")).WithType(propType), + Parameter(Identifier("newValue")).WithType(propType) + ])))) .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs index c158ff4e75..78a3d3e5bc 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs @@ -8,35 +8,35 @@ namespace Avalonia.Base.UnitTests.Rendering; /// public class CompositorInvalidationClippingTests : CompositorTestsBase { - [Fact] - // Test case: When the ClipToBounds is false, all visuals should be rendered - public void Siblings_Should_Be_Rendered_On_Invalidate_Without_ClipToBounds() + int CountVisuals(Visual visual) { - AssertRenderedVisuals(clipToBounds: false, clipGeometry: false, expectedRenderedVisualsCount: 4); + int count = 1; // Count the current visual + foreach (var child in visual.VisualChildren) count += CountVisuals(child); + return count; } - - [Fact] - // Test case: When the ClipToBounds is true, only visuals within the clipped boundary should be rendered - public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_ClipToBounds() - { - AssertRenderedVisuals(clipToBounds: true, clipGeometry: false, expectedRenderedVisualsCount: 3); - } - - [Fact] - // Test case: When the Clip is used, only visuals within the clip geometry should be rendered - public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_Clip() - { - AssertRenderedVisuals(clipToBounds: false, clipGeometry: true, expectedRenderedVisualsCount: 3); - } - - private void AssertRenderedVisuals(bool clipToBounds, bool clipGeometry, int expectedRenderedVisualsCount) + + [Theory, + // If canvas itself has no background, the second render won't draw any visuals at all, since + // root visual's subtree bounds will exactly match the second visual + InlineData(false, false, false, 1, 0), + InlineData(true, false, false, 1, 0), + InlineData(false, true, false, 1, 0), + // If canvas has background, the second render will draw only the canvas visual itself + InlineData(false, false, true, 5, 4), + InlineData(true, false, true,5, 4), + InlineData(false, true, true, 5, 4), + ] + public void Do_Not_Re_Render_Unaffected_Visual_Trees(bool clipToBounds, bool clipGeometry, + bool canvasHasContent, + int expectedVisitedVisualsCount, int expectedRenderedVisualsCount) { using (var s = new CompositorCanvas()) { - //#1 visual is top level - //#2 visual is s.Canvas + // #1 visual is top level + // #2 is ContentPresenter + // #3 visual is s.Canvas - //#3 visual is border1 + //# 4 visual is border1 s.Canvas.Children.Add(new Border() { [Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 0, @@ -46,7 +46,7 @@ public class CompositorInvalidationClippingTests : CompositorTestsBase Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null }); - //#4 visual is border2 + //# 5 visual is border2 s.Canvas.Children.Add(new Border() { [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50, @@ -55,14 +55,18 @@ public class CompositorInvalidationClippingTests : CompositorTestsBase ClipToBounds = clipToBounds, Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null }); + if (canvasHasContent) + s.Canvas.Background = Brushes.Green; s.RunJobs(); s.Events.Reset(); + if (CountVisuals(s.TopLevel) != 5) + Assert.Fail("Layout part of the test is broken, expected 5 visuals in the tree"); //invalidate border1 s.Canvas.Children[0].IsVisible = false; s.RunJobs(); - s.AssertRenderedVisuals(expectedRenderedVisualsCount); + s.AssertRenderedVisuals(expectedVisitedVisualsCount, expectedRenderedVisualsCount); } } } diff --git a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs new file mode 100644 index 0000000000..a066b7a4ab --- /dev/null +++ b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs @@ -0,0 +1,121 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Threading; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks; + +public class CompositionTargetUpdateOnly : IDisposable +{ + private readonly bool _includeRender; + private readonly IDisposable _app; + private readonly Compositor _compositor; + private readonly CompositionTarget _target; + + class Timer : IRenderTimer + { + event Action IRenderTimer.Tick { add { } remove { } } + public bool RunsInBackground => false; + } + + class ManualScheduler : ICompositorScheduler + { + public void CommitRequested(Compositor compositor) + { + + } + } + + class NullFramebuffer : IFramebufferPlatformSurface + { + private static readonly IntPtr Buffer = Marshal.AllocHGlobal(4); + public IFramebufferRenderTarget CreateFramebufferRenderTarget() => + new FuncFramebufferRenderTarget(() => new LockedFramebuffer(Buffer, new PixelSize(1, 1), 4, new Vector(96, 96), PixelFormat.Rgba8888, + AlphaFormat.Premul, null)); + } + + + public CompositionTargetUpdateOnly() : this(false) + { + + } + + protected CompositionTargetUpdateOnly(bool includeRender) + { + _includeRender = includeRender; + _app = UnitTestApplication.Start(TestServices.StyledWindow); + _compositor = new Compositor(new RenderLoop(new Timer()), null, true, new ManualScheduler(), true, + Dispatcher.UIThread, null); + _target = _compositor.CreateCompositionTarget(() => [new NullFramebuffer()]); + _target.PixelSize = new PixelSize(1000, 1000); + _target.Scaling = 1; + var root = _compositor.CreateContainerVisual(); + root.Size = new Vector(1000, 1000); + _target.Root = root; + if (includeRender) + _target.IsEnabled = true; + CreatePyramid(root, 7); + _compositor.Commit(); + } + + void CreatePyramid(CompositionContainerVisual visual, int depth) + { + for (var c = 0; c < 4; c++) + { + var child = new CompositionDrawListVisual(visual.Compositor, + new ServerCompositionDrawListVisual(visual.Compositor.Server, null!), null!); + double right = c == 1 || c == 3 ? 1 : 0; + double bottom = c > 1 ? 1 : 0; + + var rect = new Rect( + visual.Size.X / 2 * right, + visual.Size.Y / 2 * bottom, + visual.Size.X / 2, + visual.Size.Y / 2 + ); + child.Offset = new(rect.X, rect.Y, 0); + child.Size = new Vector(rect.Width, rect.Height); + + var ctx = new RenderDataDrawingContext(child.Compositor); + ctx.DrawRectangle(Brushes.Aqua, null, new Rect(rect.Size)); + child.DrawList = ctx.GetRenderResults(); + child.Visible = true; + visual.Children.Add(child); + if (depth > 0) + CreatePyramid(child, depth - 1); + } + } + + [Benchmark] + public void TargetUpdate() + { + _target.Root.Offset = new Vector3D(_target.Root.Offset.X == 0 ? 1 : 0, 0, 0); + _compositor.Commit(); + _compositor.Server.Render(); + if (!_includeRender) + _target.Server.Update(); + + } + + + public void Dispose() + { + _app.Dispose(); + + } +} + +public class CompositionTargetUpdateWithRender : CompositionTargetUpdateOnly +{ + public CompositionTargetUpdateWithRender() : base(true) + { + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 92bf013725..c8b985afda 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -212,7 +212,7 @@ namespace Avalonia.Controls.UnitTests var pt = new Point(150, 50); renderer.Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => - r.Bounds.Contains(p.Transform(r.RenderTransform!.Value.Invert())) ? + r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]); using var _ = UnitTestApplication.Start(TestServices.StyledWindow); @@ -238,6 +238,8 @@ namespace Avalonia.Controls.UnitTests bool clicked = false; + Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); + target.Click += (s, e) => clicked = true; RaisePointerEntered(target); diff --git a/tests/Avalonia.RenderTests/OpacityMaskTests.cs b/tests/Avalonia.RenderTests/OpacityMaskTests.cs index a9348e3512..ea639333f9 100644 --- a/tests/Avalonia.RenderTests/OpacityMaskTests.cs +++ b/tests/Avalonia.RenderTests/OpacityMaskTests.cs @@ -33,6 +33,7 @@ namespace Avalonia.Skia.RenderTests }, Width = 76, Height = 76, + Background = Brushes.Transparent, Children = { new Path @@ -70,6 +71,7 @@ namespace Avalonia.Skia.RenderTests RenderTransform = new RotateTransform(90), Width = 76, Height = 76, + Background = Brushes.Transparent, Children = { new Path diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index 96b3248ed1..e227b9622b 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -88,10 +88,11 @@ public class CompositorTestServices : IDisposable Events.Rects.Clear(); } - public void AssertRenderedVisuals(int renderVisuals) + public void AssertRenderedVisuals(int visitedVisuals, int renderVisuals) { RunJobs(); - Assert.Equal(Events.RenderedVisuals, renderVisuals); + Assert.Equal(visitedVisuals, Events.VisitedVisuals); + Assert.Equal(renderVisuals, Events.RenderedVisuals); Events.Rects.Clear(); } @@ -116,22 +117,20 @@ public class CompositorTestServices : IDisposable { public List Rects = new(); - public int RenderedVisuals { get; private set; } + public int RenderedVisuals { get; set; } + public int VisitedVisuals { get; set; } - public void IncrementRenderedVisuals() - { - RenderedVisuals++; - } - public void RectInvalidated(Rect rc) + public void RectInvalidated(LtrbRect rc) { - Rects.Add(rc); + Rects.Add(rc.ToRect()); } public void Reset() { Rects.Clear(); RenderedVisuals = 0; + VisitedVisuals = 0; } } diff --git a/tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_updated.expected.png b/tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_updated.expected.png index 96fbb171c9691c1316422468c9e32b3c15a70ef7..9f616b4724cfa6e27d3a44ddddf47ee5304d4507 100644 GIT binary patch literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`0|TRzr;B4q z#hkZy4`w<$3a}oGefVKcds2|xsrFSxX1?{;a_<7$y5-_qZhv9eVbtg#z`;T@qUE$*L45Yw t;%hVSedyb}zS8|WGj4ZNEc3woFXK7|{?Lza)`Wo!^K|udS?83{1OOuLTPXkl literal 358 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`0|TS1r;B4q z#hka79eEEMFt9kP|M5~@;H1nT%NG4LHGGz1fmP;lpf-Hq$Jvcl2h2Z96@P6jx>H?W zVO#yby#Cw2_c68%WoiN(OpOY(A{g;Gi&D7;`%g?)mDvJV|K_`c%<^>gb6Mw<&;$Ur CT|5~8