Browse Source

Address review

pull/20497/head
Nikita Tsukanov 1 day ago
parent
commit
e473db1630
  1. 23
      src/Avalonia.Base/Media/BitmapCache.cs
  2. 3
      src/Avalonia.Base/Media/CacheMode.cs
  3. 5
      src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs
  4. 5
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs
  5. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  6. 42
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs
  7. 10
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs
  8. 16
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs
  9. 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs
  10. 32
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs
  11. 12
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs
  12. 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs
  13. 12
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs
  14. 5
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs
  15. 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  16. 6
      src/Avalonia.Base/Visual.cs

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

@ -3,12 +3,15 @@ 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>(
"RenderAtScale", 1);
nameof(RenderAtScale), 1);
/// <summary>
/// Use the RenderAtScale property to render the BitmapCache at a multiple of the normal bitmap size.
@ -31,7 +34,7 @@ public class BitmapCache : CacheMode
}
public static readonly StyledProperty<bool> SnapsToDevicePixelsProperty = AvaloniaProperty.Register<BitmapCache, bool>(
"SnapsToDevicePixels");
nameof(SnapsToDevicePixels));
/// <summary>
/// Set the SnapsToDevicePixels property when the cache displays content that requires pixel-alignment to render correctly.
@ -55,8 +58,22 @@ public class BitmapCache : CacheMode
}
public static readonly StyledProperty<bool> EnableClearTypeProperty = AvaloniaProperty.Register<BitmapCache, bool>(
"EnableClearType");
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);

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

@ -4,6 +4,9 @@ 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

5
src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs

@ -17,6 +17,11 @@ public class CompositionOptions
/// </summary>
public int? MaxDirtyRects { get; set; }
/// <summary>
/// Controls the eagerness of merging dirty rects. WPF uses 50000, Avalonia currently has a different default
/// that's a subject to change. You can play with this property to find the best value for your application.
/// </summary>
[Unstable]
public double? DirtyRectMergeEagerness { get; set; }

5
src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs

@ -17,7 +17,6 @@ partial class MultiDirtyRectTracker
private readonly double[,] _overhead = new double[MaxDirtyRegionCount + 1, MaxDirtyRegionCount];
private LtrbRect _surfaceBounds;
private double _allowedDirtyRegionOverhead;
private double _accumulatedOverhead;
private int _regionCount;
private bool _optimized;
private bool _maxSurfaceFallback;
@ -25,6 +24,7 @@ partial class MultiDirtyRectTracker
private readonly struct UnionResult
{
public readonly double Overhead;
// Left here for debugging purposes
public readonly double Area;
public readonly LtrbRect Union;
@ -120,7 +120,6 @@ partial class MultiDirtyRectTracker
_allowedDirtyRegionOverhead = allowedDirtyRegionOverhead;
Array.Clear(_dirtyRegions);
Array.Clear(_overhead);
_accumulatedOverhead = 0;
_optimized = false;
_maxSurfaceFallback = false;
_regionCount = 0;
@ -236,7 +235,6 @@ partial class MultiDirtyRectTracker
return;
}
_accumulatedOverhead += ur.Overhead;
_dirtyRegions[bestMatchK] = unioned;
UpdateOverhead(bestMatchK);
}
@ -244,7 +242,6 @@ partial class MultiDirtyRectTracker
{
// Case B: Merge region N with region K, store new region slot K
var ur = ComputeUnion(_dirtyRegions[bestMatchN], _dirtyRegions[bestMatchK]);
_accumulatedOverhead += ur.Overhead;
_dirtyRegions[bestMatchN] = ur.Union;
_dirtyRegions[bestMatchK] = clippedNewRegion;
UpdateOverhead(bestMatchN);

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -31,7 +31,7 @@ namespace Avalonia.Rendering.Composition.Server
private bool _fullRedrawRequested;
private bool _disposed;
private readonly HashSet<ServerCompositionVisual> _attachedVisuals = new();
public readonly IDirtyRectTracker DirtyRects;
public IDirtyRectTracker DirtyRects { get; }
public long Id { get; }
public ulong Revision { get; private set; }

42
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs

@ -5,13 +5,13 @@ namespace Avalonia.Rendering.Composition.Server;
partial class ServerCompositionVisual
{
// ACT = Ancestor Transform Tracker
// 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 ActHelper
class AttHelper
{
public readonly HashSet<Action> AncestorChainTransformSubscribers = new();
public required Action ParentActSubscriptionAction;
@ -21,51 +21,51 @@ partial class ServerCompositionVisual
public bool EnqueuedForAdornerUpdate;
}
private ActHelper? _actHelper;
private AttHelper? _AttHelper;
private ActHelper GetActHelper() => _actHelper ??= new()
private AttHelper GetAttHelper() => _AttHelper ??= new()
{
ParentActSubscriptionAction = ActHelper_CombinedTransformChanged,
AdornedVisualActSubscriptionAction = ActHelper_OnAdornedVisualWorldTransformChanged
ParentActSubscriptionAction = AttHelper_CombinedTransformChanged,
AdornedVisualActSubscriptionAction = AttHelper_OnAdornedVisualWorldTransformChanged
};
private void ActHelper_CombinedTransformChanged()
private void AttHelper_CombinedTransformChanged()
{
if(_actHelper == null || _actHelper.AncestorChainTransformSubscribers.Count == 0)
if(_AttHelper == null || _AttHelper.AncestorChainTransformSubscribers.Count == 0)
return;
foreach (var sub in _actHelper.AncestorChainTransformSubscribers)
foreach (var sub in _AttHelper.AncestorChainTransformSubscribers)
sub();
}
private void ActHelper_ParentChanging()
private void AttHelper_ParentChanging()
{
if(Parent != null && _actHelper?.AncestorChainTransformSubscribers.Count > 0)
Parent.ActHelper_UnsubscribeFromActNotification(_actHelper.ParentActSubscriptionAction);
if(Parent != null && _AttHelper?.AncestorChainTransformSubscribers.Count > 0)
Parent.AttHelper_UnsubscribeFromActNotification(_AttHelper.ParentActSubscriptionAction);
}
private void ActHelper_ParentChanged()
private void AttHelper_ParentChanged()
{
if(Parent != null && _actHelper?.AncestorChainTransformSubscribers.Count > 0)
Parent.ActHelper_SubscribeToActNotification(_actHelper.ParentActSubscriptionAction);
if(Parent != null && _AttHelper?.AncestorChainTransformSubscribers.Count > 0)
Parent.AttHelper_SubscribeToActNotification(_AttHelper.ParentActSubscriptionAction);
if(Parent != null && AdornedVisual != null)
AdornerHelper_EnqueueForAdornerUpdate();
}
protected void ActHelper_SubscribeToActNotification(Action cb)
protected void AttHelper_SubscribeToActNotification(Action cb)
{
var h = GetActHelper();
var h = GetAttHelper();
(h.AncestorChainTransformSubscribers).Add(cb);
if (h.AncestorChainTransformSubscribers.Count == 1)
Parent?.ActHelper_SubscribeToActNotification(h.ParentActSubscriptionAction);
Parent?.AttHelper_SubscribeToActNotification(h.ParentActSubscriptionAction);
}
protected void ActHelper_UnsubscribeFromActNotification(Action cb)
protected void AttHelper_UnsubscribeFromActNotification(Action cb)
{
var h = GetActHelper();
var h = GetAttHelper();
h.AncestorChainTransformSubscribers.Remove(cb);
if(h.AncestorChainTransformSubscribers.Count == 0)
Parent?.ActHelper_UnsubscribeFromActNotification(h.ParentActSubscriptionAction);
Parent?.AttHelper_UnsubscribeFromActNotification(h.ParentActSubscriptionAction);
}
protected static bool ComputeTransformFromAncestor(ServerCompositionVisual visual,

10
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs

@ -9,7 +9,7 @@ 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 ActHelper_OnAdornedVisualWorldTransformChanged() => AdornerHelper_EnqueueForAdornerUpdate();
private void AttHelper_OnAdornedVisualWorldTransformChanged() => AdornerHelper_EnqueueForAdornerUpdate();
private void AdornerHelper_AttachedToRoot()
{
@ -19,7 +19,7 @@ partial class ServerCompositionVisual
public void AdornerHelper_EnqueueForAdornerUpdate()
{
var helper = GetActHelper();
var helper = GetAttHelper();
if(helper.EnqueuedForAdornerUpdate)
return;
Compositor.EnqueueAdornerUpdate(this);
@ -27,11 +27,11 @@ partial class ServerCompositionVisual
}
partial void OnAdornedVisualChanging() =>
AdornedVisual?.ActHelper_UnsubscribeFromActNotification(GetActHelper().AdornedVisualActSubscriptionAction);
AdornedVisual?.AttHelper_UnsubscribeFromActNotification(GetAttHelper().AdornedVisualActSubscriptionAction);
partial void OnAdornedVisualChanged()
{
AdornedVisual?.ActHelper_SubscribeToActNotification(GetActHelper().AdornedVisualActSubscriptionAction);
AdornedVisual?.AttHelper_SubscribeToActNotification(GetAttHelper().AdornedVisualActSubscriptionAction);
AdornerHelper_EnqueueForAdornerUpdate();
}
@ -45,7 +45,7 @@ partial class ServerCompositionVisual
public void UpdateAdorner()
{
GetActHelper().EnqueuedForAdornerUpdate = false;
GetAttHelper().EnqueuedForAdornerUpdate = false;
var ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale,
RotationAngle, Orientation, Offset);

16
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs

@ -87,27 +87,27 @@ partial class ServerCompositionVisual
// ReplaceChild | Y | Y | Y(N)
// -----------------+---------------+-----------------+-----------------------
// RemoveChild | Y | Y | Y(N)
private void PropagateFlags(bool fNeedsBoundingBoxUpdate, bool fDirtyForRender, bool fAdditionalDirtyRegion = false)
private void PropagateFlags(bool needsBoundingBoxUpdate, bool dirtyForRender, bool additionalDirtyRegion = false)
{
Root?.RequestUpdate();
var parent = Parent;
var setIsDirtyForRenderInSubgraph = fAdditionalDirtyRegion || fDirtyForRender;
var setIsDirtyForRenderInSubgraph = additionalDirtyRegion || dirtyForRender;
while (parent != null &&
((fNeedsBoundingBoxUpdate && !parent._needsBoundingBoxUpdate) ||
((needsBoundingBoxUpdate && !parent._needsBoundingBoxUpdate) ||
(setIsDirtyForRenderInSubgraph && !parent._isDirtyForRenderInSubgraph)))
{
parent._needsBoundingBoxUpdate |= fNeedsBoundingBoxUpdate;
parent._needsBoundingBoxUpdate |= needsBoundingBoxUpdate;
parent._isDirtyForRenderInSubgraph |= setIsDirtyForRenderInSubgraph;
parent = parent.Parent;
}
_needsBoundingBoxUpdate |= fNeedsBoundingBoxUpdate;
_isDirtyForRender |= fDirtyForRender;
_needsBoundingBoxUpdate |= needsBoundingBoxUpdate;
_isDirtyForRender |= dirtyForRender;
// If node itself is dirty for render, we don't need to keep track of extra dirty rects
_hasExtraDirtyRect = !fDirtyForRender && (_hasExtraDirtyRect || fAdditionalDirtyRegion);
_hasExtraDirtyRect = !dirtyForRender && (_hasExtraDirtyRect || additionalDirtyRegion);
}
public void RecomputeOwnProperties()
@ -152,7 +152,7 @@ partial class ServerCompositionVisual
setDirtyForRender = setDirtyBounds = true;
ActHelper_CombinedTransformChanged();
AttHelper_CombinedTransformChanged();
}

4
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs

@ -148,7 +148,7 @@ partial class ServerCompositionVisual
{
if (Parent != null && _transformedSubTreeBounds.HasValue)
Parent.AddExtraDirtyRect(_transformedSubTreeBounds.Value);
ActHelper_ParentChanging();
AttHelper_ParentChanging();
}
partial void OnParentChanged()
@ -158,7 +158,7 @@ partial class ServerCompositionVisual
_delayPropagateNeedsBoundsUpdate = _delayPropagateIsDirtyForRender = true;
EnqueueOwnPropertiesRecompute();
}
ActHelper_ParentChanged();
AttHelper_ParentChanged();
}
protected void AddExtraDirtyRect(LtrbRect rect)

32
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs

@ -7,6 +7,32 @@ 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;
@ -57,10 +83,12 @@ partial class ServerCompositionVisual
{
_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 slot0
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

12
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs

@ -81,10 +81,12 @@ partial class ServerCompositionVisual
if (visual._ownClipRect != null)
effectiveClip = effectiveClip.IntersectOrEmpty(visual._ownClipRect.Value.TransformToAABB(effectiveNewTransform));
if (!effectiveClip.Intersects(visual._transformedSubTreeBounds.Value.TransformToAABB(_walkContext.Transform)))
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
@ -149,6 +151,9 @@ partial class ServerCompositionVisual
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());
@ -180,6 +185,9 @@ partial class ServerCompositionVisual
if (visual.OpacityMaskBrush != null)
_canvas.PopOpacityMask();
if (visual.TextOptions != default)
_canvas.PopTextOptions();
if (visual.RenderOptions != default)
_canvas.PopRenderOptions();
}

6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs

@ -69,7 +69,8 @@ internal partial class ServerCompositionVisual
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 TBD
// 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
@ -157,7 +158,8 @@ internal partial class ServerCompositionVisual
}
// 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 TBD
// 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--;

12
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs

@ -141,9 +141,6 @@ internal class ServerCompositionVisualCache
public (int visitedVisuals, int renderedVisuals) Draw(IDrawingContextImpl outerCanvas)
{
if (TargetVisual == null)
return default;
if (TargetVisual.SubTreeBounds == null)
return default;
@ -191,7 +188,7 @@ internal class ServerCompositionVisualCache
_needToFinalizeFrame = false;
}
var visualLocalBounds = TargetVisual.SubTreeBounds.Value;
var visualLocalBounds = TargetVisual.SubTreeBounds.Value.ToRect();
(int, int) rv = default;
// Render to layer if needed
if (!_dirtyRectTracker.IsEmpty)
@ -205,12 +202,11 @@ internal class ServerCompositionVisualCache
}
}
_needsFullReRender = false;
var renderBitmapAtBounds = TargetVisual.SubTreeBounds.Value.ToRect();
var originalTransform = outerCanvas.Transform;
if (SnapsToDevicePixels)
{
var worldBounds = renderBitmapAtBounds.TransformToAABB(originalTransform);
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);
@ -218,7 +214,7 @@ internal class ServerCompositionVisualCache
//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),
TargetVisual.SubTreeBounds.Value.ToRect());
visualLocalBounds);
if (SnapsToDevicePixels)
outerCanvas.Transform = originalTransform;

5
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs

@ -65,10 +65,7 @@ internal partial class ServerCompositor
using (var canvas = target.CreateDrawingContext(false))
{
canvas.Transform = scaleTransform;
visual.Render(canvas,
new LtrbRect(double.NegativeInfinity, double.NegativeInfinity, double.PositiveInfinity,
double.PositiveInfinity),
null, renderChildren);
visual.Render(canvas, LtrbRect.Infinite, null, renderChildren);
}
if (target is IDrawingContextLayerWithRenderContextAffinityImpl affined

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

@ -217,7 +217,7 @@ namespace Avalonia.Rendering.Composition.Server
private TimeSpan ExecuteGlobalPasses()
{
var compositorGlobalPassesStopwatch = Stopwatch.StartNew();
var compositorGlobalPassesStarted = Stopwatch.GetTimestamp();
ApplyPendingBatches();
NotifyBatchesProcessed();
@ -231,7 +231,7 @@ namespace Avalonia.Rendering.Composition.Server
// because they may depend on ancestor's transform chain to be consistent
AdornerUpdatePass();
return compositorGlobalPassesStopwatch.Elapsed;
return Stopwatch.GetElapsedTime(compositorGlobalPassesStarted);
}
private void RenderCore(bool catchExceptions)

6
src/Avalonia.Base/Visual.cs

@ -71,8 +71,8 @@ namespace Avalonia
/// <summary>
/// Defines the <see cref="CacheMode"/> property.
/// </summary>
public static readonly StyledProperty<CacheMode> CacheModeProperty = AvaloniaProperty.Register<Visual, CacheMode>(
"CacheMode");
public static readonly StyledProperty<CacheMode?> CacheModeProperty = AvaloniaProperty.Register<Visual, CacheMode?>(
nameof(CacheMode));
/// <summary>
/// Defines the <see cref="Effect"/> property.
@ -265,7 +265,7 @@ namespace Avalonia
/// <summary>
/// Gets or sets the cache mode of the visual.
/// </summary>
public CacheMode CacheMode
public CacheMode? CacheMode
{
get => GetValue(CacheModeProperty);
set => SetValue(CacheModeProperty, value);

Loading…
Cancel
Save