From e473db16301e182065bc41bb2cb7ad1905d4e7aa Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 5 Feb 2026 21:47:26 +0500 Subject: [PATCH] Address review --- src/Avalonia.Base/Media/BitmapCache.cs | 23 ++++++++-- src/Avalonia.Base/Media/CacheMode.cs | 3 ++ .../Composition/CompositionOptions.cs | 5 +++ .../MultiDirtyRectTracker.CDirtyRegion.cs | 5 +-- .../Server/ServerCompositionTarget.cs | 2 +- .../ServerCompositionVisual.Act.cs | 42 +++++++++---------- .../ServerCompositionVisual.Adorners.cs | 10 ++--- ...verCompositionVisual.ComputedProperties.cs | 16 +++---- .../ServerCompositionVisual.DirtyInputs.cs | 4 +- .../ServerCompositionVisual.Readback.cs | 32 +++++++++++++- .../ServerCompositionVisual.Render.cs | 12 +++++- .../ServerCompositionVisual.Update.cs | 6 ++- .../ServerCompositionVisualCache.cs | 12 ++---- .../Server/ServerCompositor.UserApis.cs | 5 +-- .../Composition/Server/ServerCompositor.cs | 4 +- src/Avalonia.Base/Visual.cs | 6 +-- 16 files changed, 120 insertions(+), 67 deletions(-) diff --git a/src/Avalonia.Base/Media/BitmapCache.cs b/src/Avalonia.Base/Media/BitmapCache.cs index c96c95bedd..94caa1f557 100644 --- a/src/Avalonia.Base/Media/BitmapCache.cs +++ b/src/Avalonia.Base/Media/BitmapCache.cs @@ -3,12 +3,15 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Media; +/// +/// Represents the behavior of caching a visual element or tree of elements as bitmap surfaces. +/// public class BitmapCache : CacheMode { private CompositionBitmapCache? _current; public static readonly StyledProperty RenderAtScaleProperty = AvaloniaProperty.Register( - "RenderAtScale", 1); + nameof(RenderAtScale), 1); /// /// 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 SnapsToDevicePixelsProperty = AvaloniaProperty.Register( - "SnapsToDevicePixels"); + nameof(SnapsToDevicePixels)); /// /// 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 EnableClearTypeProperty = AvaloniaProperty.Register( - "EnableClearType"); + nameof(EnableClearType)); + /// + /// Set the EnableClearType property to allow subpixel text to be rendered in the cache. + /// When the EnableClearType property is true, your application MUST render all + /// of its subpixel text on an opaque background. + /// + /// When the EnableClearType property is false, text in the cache is rendered with grayscale antialiasing. + /// + /// ClearType text requires correct pixel alignment of rendered characters, + /// so you should set the SnapsToDevicePixels property to true. + /// If you do not set this property, the content may not blend correctly. + /// + /// Use the EnableClearType property when you know the cache is rendered on pixel boundaries, + /// so it is safe to cache ClearType text. This situation occurs commonly in text-scrolling scenarios. + /// public bool EnableClearType { get => GetValue(EnableClearTypeProperty); diff --git a/src/Avalonia.Base/Media/CacheMode.cs b/src/Avalonia.Base/Media/CacheMode.cs index 76be3e3579..44784eb29c 100644 --- a/src/Avalonia.Base/Media/CacheMode.cs +++ b/src/Avalonia.Base/Media/CacheMode.cs @@ -4,6 +4,9 @@ using Avalonia.Rendering.Composition.Drawing; namespace Avalonia.Media; +/// +/// Represents cached content modes for graphics acceleration features. +/// public abstract class CacheMode : StyledElement { // We currently only allow visual to be attached to one compositor at a time, so keep it simple for now diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs b/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs index 4c9a693b0f..f474f9382d 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs @@ -17,6 +17,11 @@ public class CompositionOptions /// public int? MaxDirtyRects { get; set; } + + /// + /// Controls the eagerness of merging dirty rects. WPF uses 50000, Avalonia currently has a different default + /// that's a subject to change. You can play with this property to find the best value for your application. + /// [Unstable] public double? DirtyRectMergeEagerness { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs index d4d993b531..d89ca0a9f0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs +++ b/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); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index cf04ef52c1..19a5f0e881 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/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 _attachedVisuals = new(); - public readonly IDirtyRectTracker DirtyRects; + public IDirtyRectTracker DirtyRects { get; } public long Id { get; } public ulong Revision { get; private set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs index 073a7a4bef..c0fd5ab777 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Act.cs +++ b/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 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, diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs index 228530f570..6f5f7b7572 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs +++ b/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); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs index 7a9ed98787..4b98b0f80e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs +++ b/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(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs index 75f0185b84..fa8c6047fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs +++ b/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) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs index 9c5e83a4cd..a054446a5a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs +++ b/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 diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs index fb4fa4a116..ad828b068a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs +++ b/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(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs index 336f26ac7c..b8322225bd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Update.cs +++ b/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--; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs index a958b04570..a9a481b5e3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisualCache.cs +++ b/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; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs index 1cfd200068..d299bed384 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs +++ b/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 diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 57c430ec1f..e23197ff13 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/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) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index dcee2369f7..843914dc1d 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -71,8 +71,8 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly StyledProperty CacheModeProperty = AvaloniaProperty.Register( - "CacheMode"); + public static readonly StyledProperty CacheModeProperty = AvaloniaProperty.Register( + nameof(CacheMode)); /// /// Defines the property. @@ -265,7 +265,7 @@ namespace Avalonia /// /// Gets or sets the cache mode of the visual. /// - public CacheMode CacheMode + public CacheMode? CacheMode { get => GetValue(CacheModeProperty); set => SetValue(CacheModeProperty, value);