Browse Source

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 <julien@lebosquain.net>
pull/20587/merge
Nikita Tsukanov 13 hours ago
committed by GitHub
parent
commit
697cba7193
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 40
      api/Avalonia.nupkg.xml
  2. 3
      samples/ControlCatalog/MainView.xaml
  3. 45
      samples/ControlCatalog/Pages/BitmapCachePage.axaml
  4. 13
      samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs
  5. 12
      src/Avalonia.Base/Layout/Layoutable.cs
  6. 114
      src/Avalonia.Base/Media/BitmapCache.cs
  7. 21
      src/Avalonia.Base/Media/CacheMode.cs
  8. 8
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  9. 70
      src/Avalonia.Base/Platform/LtrbRect.cs
  10. 5
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  11. 10
      src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs
  12. 5
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  13. 18
      src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs
  14. 77
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  15. 5
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs
  16. 8
      src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
  17. 6
      src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs
  18. 15
      src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs
  19. 41
      src/Avalonia.Base/Rendering/Composition/Server/CompositorPools.cs
  20. 105
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs
  21. 13
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/DebugEventsDirtyRectCollectorProxy.cs
  22. 24
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/IDirtyRectTracker.cs
  23. 348
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs
  24. 86
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.cs
  25. 60
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/RegionDirtyRectTracker.cs
  26. 50
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/SingleDirtyRectTracker.cs
  27. 45
      src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs
  28. 12
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBitmapCache.cs
  29. 24
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionCacheMode.cs
  30. 95
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  31. 15
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  32. 24
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs
  33. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs
  34. 48
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs
  35. 104
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  36. 78
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
  37. 343
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  38. 89
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs
  39. 168
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs
  40. 164
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs
  41. 191
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs
  42. 106
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs
  43. 251
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs
  44. 245
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs
  45. 135
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Walker.cs
  46. 82
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.cs
  47. 225
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs
  48. 74
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.Passes.cs
  49. 22
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs
  50. 11
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs
  51. 32
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  52. 23
      src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs
  53. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  54. 25
      src/Avalonia.Base/Rendering/Composition/Server/ServerSizeDependantVisual.cs
  55. 66
      src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs
  56. 49
      src/Avalonia.Base/Rendering/Composition/Visual.cs
  57. 2
      src/Avalonia.Base/Utilities/MathUtilities.cs
  58. 4
      src/Avalonia.Base/Visual.Composition.cs
  59. 23
      src/Avalonia.Base/Visual.cs
  60. 9
      src/Avalonia.Base/VisualTree/VisualExtensions.cs
  61. 16
      src/Avalonia.Base/composition-schema.xml
  62. 30
      src/Avalonia.Controls/BorderVisual.cs
  63. 6
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  64. 2
      src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs
  65. 17
      src/Skia/Avalonia.Skia/SkiaBackendContext.cs
  66. 9
      src/tools/DevGenerators/CompositionGenerator/Generator.cs
  67. 54
      tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
  68. 121
      tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs
  69. 4
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  70. 2
      tests/Avalonia.RenderTests/OpacityMaskTests.cs
  71. 17
      tests/Avalonia.UnitTests/CompositorTestServices.cs
  72. BIN
      tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_updated.expected.png

40
api/Avalonia.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -625,6 +625,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action)</Target>
@ -1267,6 +1273,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,System.Double)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.LockedFramebuffer.#ctor(System.IntPtr,Avalonia.PixelSize,System.Int32,Avalonia.Vector,Avalonia.Platform.PixelFormat,System.Action)</Target>
@ -1609,12 +1621,24 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,Avalonia.Vector,System.Boolean)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Platform.ILockedFramebuffer.AlphaFormat</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Platform.IPlatformRenderInterfaceContext.MaxOffscreenRenderTargetPixelSize</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Platform.IReadableBitmapImpl.AlphaFormat</Target>
@ -1759,6 +1783,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceContext.CreateOffscreenRenderTarget(Avalonia.PixelSize,Avalonia.Vector,System.Boolean)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)</Target>
@ -1777,6 +1807,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Platform.IPlatformRenderInterfaceContext.MaxOffscreenRenderTargetPixelSize</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Platform.IReadableBitmapImpl.AlphaFormat</Target>
@ -2269,4 +2305,4 @@
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>

3
samples/ControlCatalog/MainView.xaml

@ -34,6 +34,9 @@
<TabItem Header="Border">
<pages:BorderPage />
</TabItem>
<TabItem Header="BitmapCache">
<pages:BitmapCachePage />
</TabItem>
<TabItem Header="Buttons">
<pages:ButtonsPage />
</TabItem>

45
samples/ControlCatalog/Pages/BitmapCachePage.axaml

@ -0,0 +1,45 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.BitmapCachePage">
<DockPanel>
<DockPanel.Resources>
<BitmapCache x:Key="Cache"/>
<ScaleTransform x:Key="Transform" ScaleX="1" ScaleY="{Binding $self.ScaleX, Mode=OneWay}"/>
<TranslateTransform x:Key="SubPixelTransform"/>
</DockPanel.Resources>
<StackPanel DockPanel.Dock="Right" ZIndex="1">
<TextBlock>Render at scale</TextBlock>
<Slider Minimum="0.1" Maximum="4" Value="{Binding Source={StaticResource Cache}, Path=RenderAtScale, Mode=TwoWay}" Width="200"/>
<TextBlock>Scale</TextBlock>
<Slider Minimum="0.1" Maximum="4" Value="{Binding Source={StaticResource Transform}, Path=ScaleX, Mode=TwoWay}" Width="200"/>
<CheckBox IsChecked="{Binding Source={StaticResource Cache}, Path=EnableClearType, Mode=TwoWay}">Enable clear type</CheckBox>
<CheckBox IsChecked="{Binding Source={StaticResource Cache}, Path=SnapsToDevicePixels, Mode=TwoWay}">Snap to device pixels</CheckBox>
<TextBlock>Subpixel offset X</TextBlock>
<Slider Minimum="0" Maximum="1" Value="{Binding Source={StaticResource SubPixelTransform}, Path=X, Mode=TwoWay}" Width="200"/>
</StackPanel>
<Decorator RenderTransform="{StaticResource SubPixelTransform}">
<Border Background="Beige" Margin="10"
RenderTransform="{StaticResource Transform}"
RenderTransformOrigin="0.5,0.5">
<Border Margin="10"
CacheMode="{StaticResource Cache}"
TextElement.Foreground="Black">
<Border
Margin="10"
BorderThickness="2"
BorderBrush="Black"
Background="White">
<StackPanel>
<Slider MinWidth="200"/>
<TextBlock TextWrapping="Wrap">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.</TextBlock>
</StackPanel>
</Border>
</Border>
</Border>
</Decorator>
</DockPanel>
</UserControl>

13
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();
}
}

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

114
src/Avalonia.Base/Media/BitmapCache.cs

@ -0,0 +1,114 @@
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Media;
/// <summary>
/// Represents the behavior of caching a visual element or tree of elements as bitmap surfaces.
/// </summary>
public class BitmapCache : CacheMode
{
private CompositionBitmapCache? _current;
public static readonly StyledProperty<double> RenderAtScaleProperty = AvaloniaProperty.Register<BitmapCache, double>(
nameof(RenderAtScale), 1);
/// <summary>
/// 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.
/// </summary>
public double RenderAtScale
{
get => GetValue(RenderAtScaleProperty);
set => SetValue(RenderAtScaleProperty, value);
}
public static readonly StyledProperty<bool> SnapsToDevicePixelsProperty = AvaloniaProperty.Register<BitmapCache, bool>(
nameof(SnapsToDevicePixels));
/// <summary>
/// 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.
/// </summary>
public bool SnapsToDevicePixels
{
get => GetValue(SnapsToDevicePixelsProperty);
set => SetValue(SnapsToDevicePixelsProperty, value);
}
public static readonly StyledProperty<bool> EnableClearTypeProperty = AvaloniaProperty.Register<BitmapCache, bool>(
nameof(EnableClearType));
/// <summary>
/// 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.
/// </summary>
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;
}
}

21
src/Avalonia.Base/Media/CacheMode.cs

@ -0,0 +1,21 @@
using System;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
namespace Avalonia.Media;
/// <summary>
/// Represents cached content modes for graphics acceleration features.
/// </summary>
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);
}
}

8
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -221,8 +221,9 @@ namespace Avalonia.Platform
/// </summary>
/// <param name="pixelSize">The size, in pixels, of the render target</param>
/// <param name="scaling">The scaling which will be reported by IBitmap.Dpi</param>
/// <param name="enableTextAntialiasing">Specifies if text antialiasing should be enabled</param>
/// <returns></returns>
IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling);
IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, Vector scaling, bool enableTextAntialiasing);
/// <summary>
/// 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)
/// </summary>
IReadOnlyDictionary<Type, object> PublicFeatures { get; }
/// <summary>
/// Maximum supported offscreen render target pixel size, or null if no limit
/// </summary>
public PixelSize? MaxOffscreenRenderTargetPixelSize { get; }
}
}

70
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
/// <summary>
/// Perform _WPF-like_ union operation
/// </summary>
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;
}
}
/// <summary>
@ -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));
}
}

5
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)));

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

5
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)

18
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
/// </summary>
public bool? UseRegionDirtyRectClipping { get; set; }
/// <summary>
/// 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.
/// </summary>
public int? MaxDirtyRects { get; set; }
/// <summary>
/// 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.
/// </summary>
[Unstable]
public double? DirtyRectMergeEagerness { get; set; }
/// <summary>
/// Enforces dirty contents to be rendered into an extra intermediate surface before being applied onto the
/// saved frame.

77
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -29,63 +29,26 @@ namespace Avalonia.Rendering.Composition
/// <returns></returns>
public PooledList<CompositionVisual>? TryHitTest(Point point, CompositionVisual? root, Func<CompositionVisual, bool>? filter)
{
point *= Scaling;
Server.Readback.NextRead();
Server.Compositor.Readback.NextRead();
root ??= Root;
if (root == null)
return null;
var res = new PooledList<CompositionVisual>();
// 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;
}
/// <summary>
/// Attempts to transform a point to a particular CompositionVisual coordinate space
/// </summary>
/// <returns></returns>
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<CompositionVisual> result,
void HitTestCore(CompositionVisual visual, Point parentPoint, PooledList<CompositionVisual> result,
Func<CompositionVisual, bool>? 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

5
src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs

@ -49,7 +49,7 @@ internal struct CompositorResourceHolder<T> where T : SimpleServerObject
public bool IsAttached => _dictionary.HasEntries;
public bool CreateOrAddRef(Compositor compositor, ICompositorSerializable owner, out T resource, Func<Compositor, T> factory)
public bool CreateOrAddRef(Compositor compositor, ICompositorSerializable? owner, out T resource, Func<Compositor, T> factory)
{
if (_dictionary.TryGetValue(compositor, out var handle))
{
@ -60,7 +60,8 @@ internal struct CompositorResourceHolder<T> where T : SimpleServerObject
resource = factory(compositor);
_dictionary.Add(compositor, new CompositorRefCountableResource<T>(resource));
compositor.RegisterForSerialization(owner);
if (owner != null)
compositor.RegisterForSerialization(owner);
return true;
}

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

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

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

41
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<T> : Stack<Stack<T>>
{
public Stack<T> Rent()
{
if (Count > 0)
return Pop()!;
return new Stack<T>();
}
public void Return(ref Stack<T> stack)
{
Return(stack);
stack = null!;
}
public void Return(Stack<T>? stack)
{
if (stack == null)
return;
stack.Clear();
Push(stack);
}
}
public StackPool<ServerCompositionVisual.TreeWalkerFrame> TreeWalkerFrameStackPool { get; } = new();
public StackPool<Matrix> MatrixStackPool { get; } = new();
public StackPool<LtrbRect> LtrbRectStackPool { get; } = new();
public StackPool<double> DoubleStackPool { get; } = new();
public StackPool<int> IntStackPool { get; } = new();
public StackPool<IDirtyRectCollector> DirtyRectCollectorStackPool { get; } = new();
}

105
src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs

@ -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<LtrbPixelRect> 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<LtrbPixelRect> Rects
{
get
{
if (_rect.IsEmpty)
return Array.Empty<LtrbPixelRect>();
_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<LtrbPixelRect> Rects => _region.Rects;
}

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

24
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
{
/// <summary>
/// Post-processes the dirty rect area (e. g. to account for anti-aliasing)
/// </summary>
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);
}

348
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
{
/// <summary>
/// This is a port of CDirtyRegion2 from WPF
/// </summary>
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);
}
}
}
/// <summary>
/// Initialize must be called before adding dirty rects. Initialize can also be called to
/// reset the dirty region.
/// </summary>
public void Initialize(LtrbRect surfaceBounds, double allowedDirtyRegionOverhead)
{
_allowedDirtyRegionOverhead = allowedDirtyRegionOverhead;
Array.Clear(_dirtyRegions);
Array.Clear(_overhead);
_optimized = false;
_maxSurfaceFallback = false;
_regionCount = 0;
_surfaceBounds = surfaceBounds;
}
/// <summary>
/// Adds a new dirty rectangle to the dirty region.
/// </summary>
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);
}
}
/// <summary>
/// Returns an array of dirty rectangles describing the dirty region.
/// </summary>
public ReadOnlySpan<LtrbRect> GetUninflatedDirtyRegions()
{
if (_maxSurfaceFallback)
{
return new ReadOnlySpan<LtrbRect>(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);
}
/// <summary>
/// Checks if the dirty region is empty.
/// </summary>
public bool IsEmpty
{
get
{
for (int i = 0; i < MaxDirtyRegionCount; i++)
{
if (!_dirtyRegions[i].IsEmpty)
{
return false;
}
}
return true;
}
}
/// <summary>
/// Returns the dirty region count.
/// NOTE: The region count is NOT VALID until GetUninflatedDirtyRegions is called.
/// </summary>
public int RegionCount => _regionCount;
}
}

86
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<LtrbRect> _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<LtrbRect> InflatedRects => _inflatedRects;
}

60
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<LtrbRect> _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; }
}

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

45
src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs

@ -1,3 +1,5 @@
using System.Threading;
namespace Avalonia.Rendering.Composition.Server
{
/// <summary>
@ -7,40 +9,29 @@ namespace Avalonia.Rendering.Composition.Server
/// </summary>
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);
}
}
}

12
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
{
}

24
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<ServerCompositionVisual> _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();
}
}

95
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@ -12,99 +12,6 @@ namespace Avalonia.Rendering.Composition.Server
/// </summary>
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!;
}
}

15
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<ServerCompositionRenderData?>();
_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()

24
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)
{

2
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)
{

48
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs

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

104
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<ServerCompositionVisual> _attachedVisuals = new();
private readonly Queue<ServerCompositionVisual> _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<IEnumerable<object>> surfaces,
DiagnosticTextRenderer? diagnosticTextRenderer)
@ -47,10 +47,19 @@ namespace Avalonia.Rendering.Composition.Server
_surfaces = surfaces;
_overlays = new CompositionTargetOverlays(this, diagnosticTextRenderer);
var platformRender = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
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);
}
}

78
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs

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

343
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -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
{
/// <summary>
/// Server-side <see cref="CompositionVisual"/> counterpart.
/// Is responsible for computing the transformation matrix, for applying various visual
/// properties before calling visual-specific drawing code and for notifying the
/// <see cref="ServerCompositionTarget"/> for new dirty rects
/// </summary>
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;
/// <summary>
/// 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
/// </summary>
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);
}
/// <summary>
/// Data that can be read from the UI thread
/// </summary>
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);
}
}

89
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<Action> 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;
}
}

168
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<int>? _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!);
}
}
}

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

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

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

251
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<double> _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<RenderContext>.Walk(ref renderContext, this);
return (renderContext.VisitedVisuals, renderContext.RenderedVisuals);
}
finally
{
renderContext.Dispose();
}
}
}

245
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<int> _dirtyRegionDisableCountStack;
private Stack<IDirtyRectCollector> _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<UpdateContext>.Walk(ref context, this);
context.Dispose();
}
}

135
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<TVisitor> 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<Matrix> _transformStack;
private Stack<LtrbRect> _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);
}
}
}

82
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
{
/// <summary>
/// Server-side <see cref="CompositionVisual"/> counterpart.
/// Is responsible for computing the transformation matrix, for applying various visual
/// properties before calling visual-specific drawing code and for notifying the
/// <see cref="ServerCompositionTarget"/> for new dirty rects
/// </summary>
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);
}
}
}

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

74
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<IServerRenderResource> _renderResourcesInvalidationQueue = new();
private readonly HashSet<IServerRenderResource> _renderResourcesInvalidationSet = new();
// TODO: parallel processing maybe
private readonly Queue<ServerCompositionVisual> _visualOwnPropertiesRecomputePass = new();
private readonly Queue<ServerCompositionVisual> _visualReadbackUpdatePassQueue = new();
private readonly Queue<ServerCompositionVisual> _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();
}
}
}

22
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs

@ -1,22 +0,0 @@
using System.Collections.Generic;
namespace Avalonia.Rendering.Composition.Server;
partial class ServerCompositor
{
private readonly Queue<IServerRenderResource> _renderResourcesInvalidationQueue = new();
private readonly HashSet<IServerRenderResource> _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);
}
}

11
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

32
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@ -30,6 +30,7 @@ namespace Avalonia.Rendering.Composition.Server
private readonly List<ServerCompositionTarget> _activeTargets = new();
internal BatchStreamObjectPool<object?> 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)

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

2
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)

25
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();
}
}

66
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<Matrix>? _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();
}
}
}
}

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

2
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;
/// <summary>
/// AreClose - Returns whether or not two doubles are "close". That is, whether or

4
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();

23
src/Avalonia.Base/Visual.cs

@ -68,6 +68,12 @@ namespace Avalonia
public static readonly StyledProperty<IBrush?> OpacityMaskProperty =
AvaloniaProperty.Register<Visual, IBrush?>(nameof(OpacityMask));
/// <summary>
/// Defines the <see cref="CacheMode"/> property.
/// </summary>
public static readonly StyledProperty<CacheMode?> CacheModeProperty = AvaloniaProperty.Register<Visual, CacheMode?>(
nameof(CacheMode));
/// <summary>
/// Defines the <see cref="Effect"/> property.
/// </summary>
@ -256,6 +262,15 @@ namespace Avalonia
set { SetValue(OpacityMaskProperty, value); }
}
/// <summary>
/// Gets or sets the cache mode of the visual.
/// </summary>
public CacheMode? CacheMode
{
get => GetValue(CacheModeProperty);
set => SetValue(CacheModeProperty, value);
}
/// <summary>
/// Gets or sets the effect of the control.
/// </summary>
@ -326,11 +341,11 @@ namespace Avalonia
/// </summary>
protected internal IRenderRoot? VisualRoot => _visualRoot;
internal RenderOptions RenderOptions
{
internal RenderOptions RenderOptions
{
get => _renderOptions;
set
{
set
{
_renderOptions = value;
InvalidateVisual();
}

9
src/Avalonia.Base/VisualTree/VisualExtensions.cs

@ -358,14 +358,7 @@ namespace Avalonia.VisualTree
return Array.Empty<Visual>();
}
var rootPoint = visual.TranslatePoint(p, (Visual)root);
if (rootPoint.HasValue)
{
return root.HitTester.HitTest(rootPoint.Value, visual, filter);
}
return Enumerable.Empty<Visual>();
return root.HitTester.HitTest(p, visual, filter);
}
/// <summary>

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

@ -35,17 +35,18 @@
<Property Name="Effect" Type="Avalonia.Media.IImmutableEffect?" Internal="true" />
<Property Name="RenderOptions" Type="Avalonia.Media.RenderOptions" />
<Property Name="TextOptions" Type="Avalonia.Media.TextOptions" />
<Property Name="ShouldExtendDirtyRect" Type="bool" Internal="true" />
<Property Name="CacheMode" Type="CompositionCacheMode?" Internal="true" />
</Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>
<Object Name="CompositionSolidColorVisual" Inherits="CompositionContainerVisual">
<Object Name="CompositionSolidColorVisual" Inherits="CompositionContainerVisual" ServerBase="ServerSizeDependantVisual">
<Property Name="Color" Type="Avalonia.Media.Color" Animated="true" />
</Object>
<Object Name="CompositionSurfaceVisual" Inherits="CompositionContainerVisual">
<Object Name="CompositionSurfaceVisual" Inherits="CompositionContainerVisual" ServerBase="ServerSizeDependantVisual">
<Property Name="Surface" Type="CompositionSurface?" />
</Object>
<Manual Name="CompositionDrawListVisual" />
<Object Name="CompositionExperimentalAcrylicVisual" Inherits="CompositionDrawListVisual" Internal="true" CustomCtor="true" CustomServerCtor="true">
<Object Name="CompositionExperimentalAcrylicVisual" Inherits="CompositionDrawListVisual"
Internal="true" CustomCtor="true" CustomServerCtor="true">
<Property Name="Material" Type="Avalonia.Media.ImmutableExperimentalAcrylicMaterial" />
<Property Name="CornerRadius" Type="CornerRadius" />
</Object>
@ -110,4 +111,11 @@
<Property Name="Stretch" Type="Avalonia.Media.Stretch" />
<Property Name="TileMode" Type="Avalonia.Media.TileMode" />
</Object>
<Object Name="CompositionCacheMode" Internal="true">
</Object>
<Object Name="CompositionBitmapCache" Internal="true" Inherits="CompositionCacheMode">
<Property Name="RenderAtScale" Type="double" Animated="true" DefaultValue="1.0"/>
<Property Name="SnapsToDevicePixels" Type="bool" Animated="true" DefaultValue="false"/>
<Property Name="EnableClearType" Type="bool" Animated="true" DefaultValue="false"/>
</Object>
</NComposition>

30
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<CornerRadius>();
}
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));
}
}
}

6
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -56,11 +56,13 @@ namespace Avalonia.Headless
=> new HeadlessGeometryStub(g1.Bounds.Union(g2.Bounds));
public IRenderTarget CreateRenderTarget(IEnumerable<object> 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<Type, object> PublicFeatures { get; } = new Dictionary<Type, object>();
public PixelSize? MaxOffscreenRenderTargetPixelSize => null;
public object? TryGetFeature(Type featureType) => null;
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)

2
src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs

@ -6,6 +6,8 @@ using SkiaSharp;
namespace Avalonia.Skia
{
//TODO12: Make it private
/// <summary>
/// Custom Skia gpu instance.
/// </summary>

17
src/Skia/Avalonia.Skia/SkiaBackendContext.cs

@ -29,6 +29,13 @@ internal class SkiaContext : IPlatformRenderInterfaceContext
// TODO12: extend ISkiaGpu with PublicFeatures instead
TryFeature<IOpenGlTextureSharingRenderInterfaceContextFeature>();
TryFeature<IExternalObjectsRenderInterfaceContextFeature>();
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,

9
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<SyntaxToken>()))
))
.AddMembers(MethodDeclaration(ParseTypeName("void"), "Validate" + prop.Name + "Change")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())
.WithParameterList(ParameterList(SeparatedList<ParameterSyntax>([
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")

54
tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs

@ -8,35 +8,35 @@ namespace Avalonia.Base.UnitTests.Rendering;
/// </summary>
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);
}
}
}

121
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<TimeSpan> 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)
{
}
}

4
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<Point>(), It.IsAny<Visual>(), It.IsAny<Func<Visual, bool>>()))
.Returns<Point, Visual, Func<Visual, bool>>((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);

2
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

17
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<Rect> 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;
}
}

BIN
tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_updated.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 395 B

Loading…
Cancel
Save