Browse Source
* 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
committed by
GitHub
72 changed files with 3222 additions and 1046 deletions
@ -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> |
|||
@ -0,0 +1,13 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class BitmapCachePage : UserControl |
|||
{ |
|||
public BitmapCachePage() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
|
|||
@ -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(); |
|||
|
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; } |
|||
|
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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 |
|||
{ |
|||
|
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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!); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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(); |
|||
} |
|||
|
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
} |
|||
} |
|||
|
Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 395 B |
Loading…
Reference in new issue