Browse Source

Merge pull request #12568 from JetBrains/fixes/clip-bounds-invalidate

Enhanced Clipping and Rendered Visuals Tracking in ServerCompositionVisual
Nikita Tsukanov 2 years ago
parent
commit
c59e63ef6f

+ 2 - 0
src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs

@@ -2,5 +2,7 @@ namespace Avalonia.Rendering.Composition;
 
 
 internal interface ICompositionTargetDebugEvents
 internal interface ICompositionTargetDebugEvents
 {
 {
+    int RenderedVisuals { get; }
+    void IncrementRenderedVisuals();
     void RectInvalidated(Rect rc);
     void RectInvalidated(Rect rc);
 }
 }

+ 1 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs

@@ -38,6 +38,7 @@ partial class ServerCompositionVisual
         CompositionVisualChangedFields.Size
         CompositionVisualChangedFields.Size
         | CompositionVisualChangedFields.SizeAnimated
         | CompositionVisualChangedFields.SizeAnimated
         | CompositionVisualChangedFields.ClipToBounds
         | CompositionVisualChangedFields.ClipToBounds
+        | CompositionVisualChangedFields.Clip
         | CompositionVisualChangedFields.ClipToBoundsAnimated;
         | CompositionVisualChangedFields.ClipToBoundsAnimated;
         
         
     partial void OnFieldsDeserialized(CompositionVisualChangedFields changed)
     partial void OnFieldsDeserialized(CompositionVisualChangedFields changed)

+ 18 - 4
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@@ -38,6 +38,7 @@ namespace Avalonia.Rendering.Composition.Server
                 return;
                 return;
 
 
             Root!.RenderedVisuals++;
             Root!.RenderedVisuals++;
+            Root!.DebugEvents?.IncrementRenderedVisuals();
 
 
             var boundsRect = new Rect(new Size(Size.X, Size.Y));
             var boundsRect = new Rect(new Size(Size.X, Size.Y));
 
 
@@ -182,11 +183,24 @@ namespace Avalonia.Rendering.Composition.Server
 
 
             if (_clipSizeDirty || positionChanged)
             if (_clipSizeDirty || positionChanged)
             {
             {
-                _transformedClipBounds = ClipToBounds
-                    ? new Rect(new Size(Size.X, Size.Y))
-                        .TransformToAABB(GlobalTransformMatrix)
-                    : null;
+                Rect? transformedVisualBounds = null;
+                Rect? transformedClipBounds = null;
                 
                 
+                if (ClipToBounds)
+                    transformedVisualBounds = new Rect(new Size(Size.X, Size.Y)).TransformToAABB(GlobalTransformMatrix);
+                
+                 if (Clip != null)
+                     transformedClipBounds = 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;
                 _clipSizeDirty = false;
             }
             }
 
 

+ 68 - 0
tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs

@@ -0,0 +1,68 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Rendering;
+/// <summary>
+/// Test class that verifies how clipping influences rendering in the compositor
+/// </summary>
+public class CompositorInvalidationClippingTests : CompositorTestsBase
+{
+    [Fact]
+    // Test case: When the ClipToBounds is false, all visuals should be rendered
+    public void Siblings_Should_Be_Rendered_On_Invalidate_Without_ClipToBounds()
+    {
+        AssertRenderedVisuals(clipToBounds: false, clipGeometry: false, expectedRenderedVisualsCount: 4);
+    }
+
+    [Fact]
+    // Test case: When the ClipToBounds is true, only visuals within the clipped boundary should be rendered
+    public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_ClipToBounds()
+    {
+        AssertRenderedVisuals(clipToBounds: true, clipGeometry: false, expectedRenderedVisualsCount: 3);
+    }
+
+    [Fact]
+    // Test case: When the Clip is used, only visuals within the clip geometry should be rendered
+    public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_Clip()
+    {
+        AssertRenderedVisuals(clipToBounds: false, clipGeometry: true, expectedRenderedVisualsCount: 3);
+    }
+
+    private void AssertRenderedVisuals(bool clipToBounds, bool clipGeometry, int expectedRenderedVisualsCount)
+    {
+        using (var s = new CompositorCanvas())
+        {
+            //#1 visual is top level
+            //#2 visual is s.Canvas
+            
+            //#3 visual is border1
+            s.Canvas.Children.Add(new Border()
+            {
+                [Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 0,
+                Width = 20, Height = 10,
+                Background = Brushes.Red,
+                ClipToBounds = clipToBounds,
+                Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
+            });
+            
+            //#4 visual is border2
+            s.Canvas.Children.Add(new Border()
+            {
+                [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50,
+                Width = 20, Height = 10,
+                Background = Brushes.Red,
+                ClipToBounds = clipToBounds,
+                Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
+            });
+            s.RunJobs();
+            s.Events.Reset();
+            
+            //invalidate border1
+            s.Canvas.Children[0].IsVisible = false;
+            s.RunJobs();
+            
+            s.AssertRenderedVisuals(expectedRenderedVisualsCount);
+        }
+    }
+}

+ 16 - 1
tests/Avalonia.UnitTests/CompositorTestServices.cs

@@ -89,6 +89,13 @@ public class CompositorTestServices : IDisposable
         Events.Rects.Clear();
         Events.Rects.Clear();
     }
     }
 
 
+    public void AssertRenderedVisuals(int renderVisuals)
+    {
+        RunJobs();
+        Assert.Equal(Events.RenderedVisuals, renderVisuals);
+        Events.Rects.Clear();
+    }
+
     public void AssertHitTest(double x, double y, Func<Visual, bool> filter, params object[] expected)
     public void AssertHitTest(double x, double y, Func<Visual, bool> filter, params object[] expected)
         => AssertHitTest(new Point(x, y), filter, expected);
         => AssertHitTest(new Point(x, y), filter, expected);
 
 
@@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable
     {
     {
         public List<Rect> Rects = new();
         public List<Rect> Rects = new();
 
 
+        public int RenderedVisuals { get; private set; }
+
+        public void IncrementRenderedVisuals()
+        {
+            RenderedVisuals++;
+        }
+
         public void RectInvalidated(Rect rc)
         public void RectInvalidated(Rect rc)
         {
         {
             Rects.Add(rc);
             Rects.Add(rc);
@@ -118,6 +132,7 @@ public class CompositorTestServices : IDisposable
         public void Reset()
         public void Reset()
         {
         {
             Rects.Clear();
             Rects.Clear();
+            RenderedVisuals = 0;
         }
         }
     }
     }
 
 
@@ -218,4 +233,4 @@ public class DispatcherCompositorScheduler : ICompositorScheduler
     {
     {
         Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender);
         Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender);
     }
     }
-}
+}