From c33ea4346c14fc864729d128b10f6e6654652648 Mon Sep 17 00:00:00 2001 From: Steven He Date: Mon, 4 May 2026 23:03:58 +0900 Subject: [PATCH] Revert runtime knobs and add tests --- .../Composition/CompositionHitTestAabbTree.cs | 4 - .../Rendering/Composition/ContainerVisual.cs | 2 +- .../ServerCompositionVisual.Readback.cs | 3 +- .../Rendering/CompositorHitTestingTests.cs | 100 +++++++ .../Avalonia.Benchmarks.csproj | 4 - .../Rendering/CompositionHitTesting.cs | 270 ------------------ .../CompositorTestServices.cs | 2 +- 7 files changed, 103 insertions(+), 282 deletions(-) delete mode 100644 tests/Avalonia.Benchmarks/Rendering/CompositionHitTesting.cs diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs b/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs index b7f460754f..cef3ca6792 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs @@ -16,8 +16,6 @@ internal sealed class CompositionHitTestAabbTree private const double FatBoundsPadding = 1; private static readonly CandidateComparer s_candidateComparer = new(); - internal static bool IsEnabled { get; } = InitializeIsEnabled(); - private readonly Dictionary _leaves = []; private readonly Dictionary _unbounded = []; private readonly List _nodes = []; @@ -26,8 +24,6 @@ internal sealed class CompositionHitTestAabbTree private int _root = Null; private int _freeList = Null; - private static bool InitializeIsEnabled() => !AppContext.TryGetSwitch("Avalonia.Rendering.Composition.DisableHitTestAabbTree", out var disabled) || !disabled; - public void Clear() { _leaves.Clear(); diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs index 34a55f2ad5..26937fb323 100644 --- a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.Composition /// public partial class CompositionContainerVisual : CompositionVisual { - internal static readonly int HitTestAabbTreeThreshold = CompositionHitTestAabbTree.IsEnabled ? 32 : int.MaxValue; + internal const int HitTestAabbTreeThreshold = 32; private CompositionHitTestAabbTree? _hitTestChildren; private bool _hitTestChildrenDirty = true; 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 d64a60d369..cc75b78b6d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs @@ -101,8 +101,7 @@ partial class ServerCompositionVisual slot.TransformedSubtreeBounds = _transformedSubTreeBounds; slot.Visible = Visible; - if (CompositionHitTestAabbTree.IsEnabled) - Compositor.Readback.AddHitTestUpdate(Client); + Compositor.Readback.AddHitTestUpdate(Client); } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs index 7cd02d2907..052a9b10e7 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs @@ -8,6 +8,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.UnitTests; using Avalonia.VisualTree; using Moq; @@ -494,6 +495,105 @@ public class CompositorHitTestingTests : CompositorTestsBase } } + [Fact] + public void HitTestFirst_Should_Skip_Element_Child_Composition_Visual() + { + using (var s = new CompositorTestServices(new Size(200, 200))) + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.Red + }; + + s.TopLevel.Content = target; + s.RunJobs(); + + var childVisual = s.Compositor.CreateSolidColorVisual(); + childVisual.Size = new Vector(200, 200); + childVisual.Color = Colors.Blue; + ElementComposition.SetElementChildVisual(target, childVisual); + + s.AssertHitTestFirst(new Point(100, 100), null, target); + } + } + + [Fact] + public void HitTest_Should_Find_Control_With_Many_Siblings() + { + using (var s = new CompositorTestServices(new Size(1000, 200))) + { + Border target = null!; + var canvas = new Canvas { Width = 1000, Height = 200 }; + + for (var i = 0; i < 70; i++) + { + var child = new Border { Width = 8, Height = 8, Background = Brushes.Red }; + Canvas.SetLeft(child, i * 12); + canvas.Children.Add(child); + + if (i == 0) + target = child; + } + + s.TopLevel.Content = canvas; + s.AssertHitTestFirst(new Point(4, 4), null, target); + } + } + + [Fact] + public void HitTest_Should_Return_Top_Controls_First_With_Many_Overlapping_Siblings() + { + using (var s = new CompositorTestServices(new Size(200, 200))) + { + Border top = null!; + var canvas = new Canvas { Width = 200, Height = 200 }; + + for (var i = 0; i < 70; i++) + { + var child = new Border { Width = 100, Height = 100, Background = Brushes.Red }; + Canvas.SetLeft(child, 50); + Canvas.SetTop(child, 50); + canvas.Children.Add(child); + + if (i == 69) + top = child; + } + + s.TopLevel.Content = canvas; + s.AssertHitTestFirst(new Point(100, 100), null, top); + s.AssertHitTest(new Point(100, 100), null, canvas.Children.Cast().Reverse().ToArray()); + } + } + + [Fact] + public void HitTest_Should_Update_Many_Sibling_Index_When_Child_Moves() + { + using (var s = new CompositorTestServices(new Size(1000, 200))) + { + Border moving = null!; + var canvas = new Canvas { Width = 1000, Height = 200 }; + + for (var i = 0; i < 70; i++) + { + var child = new Border { Width = 8, Height = 8, Background = Brushes.Red }; + Canvas.SetLeft(child, i * 12); + canvas.Children.Add(child); + + if (i == 69) + moving = child; + } + + s.TopLevel.Content = canvas; + s.AssertHitTestFirst(new Point(69 * 12 + 4, 4), null, moving); + + Canvas.SetLeft(moving, 10); + Canvas.SetTop(moving, 100); + s.AssertHitTestFirst(new Point(14, 104), null, moving); + } + } + private static IDisposable TestApplication() { return UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 5746766b7d..b4d7435a28 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -4,7 +4,6 @@ Exe false false - true @@ -16,9 +15,6 @@ - - - diff --git a/tests/Avalonia.Benchmarks/Rendering/CompositionHitTesting.cs b/tests/Avalonia.Benchmarks/Rendering/CompositionHitTesting.cs deleted file mode 100644 index e4906cf8f2..0000000000 --- a/tests/Avalonia.Benchmarks/Rendering/CompositionHitTesting.cs +++ /dev/null @@ -1,270 +0,0 @@ -#nullable enable - -using System; -using System.Numerics; -using Avalonia.Animation; -using Avalonia.Animation.Easings; -using Avalonia.Controls; -using Avalonia.Media; -using Avalonia.Rendering.Composition; -using Avalonia.Rendering.Composition.Animations; -using Avalonia.UnitTests; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; - -namespace Avalonia.Benchmarks.Rendering; - -class BenchmarkConfig : ManualConfig -{ - public BenchmarkConfig() - { - AddJob(Job.Default - .WithMsBuildArguments("/p:DisableHitTestAabbTree=true") - .WithId("Linear")); - AddJob(Job.Default - .WithMsBuildArguments("/p:DisableHitTestAabbTree=false") - .WithId("AabbTree")); - HideColumns(Column.Arguments); - } -} - -[MemoryDiagnoser] -[Config(typeof(BenchmarkConfig))] -public class CompositionHitTesting -{ - private const int CellSize = 8; - private const int CellStride = 12; - private const int TreeDepth = 4; - - private CompositorTestServices? _services; - private Point _hitPoint; - private Border? _expectedHit; - - [Params(1, 2, 4, 8, 16, 32, 64, 1024, 4096, 16384)] - public int VisualCount { get; set; } - - [GlobalSetup] - public void Setup() - { - var canvas = BuildGrid(VisualCount, out var size, out _expectedHit); - - _services = new CompositorTestServices(size); - _services.TopLevel.Content = canvas; - _services.RunJobs(); - - _hitPoint = new Point(CellSize / 2d, CellSize / 2d); - - if (!ReferenceEquals(HitTestFirst(), _expectedHit)) - throw new InvalidOperationException("Hit test returned an unexpected visual."); - } - - [GlobalCleanup] - public void Cleanup() - { - _services?.Dispose(); - _services = null; - _expectedHit = null; - } - - [Benchmark] - public Visual? HitTestFirst() - { - return _services!.Renderer.HitTestFirst(_hitPoint, _services.TopLevel, null); - } - - internal static Canvas BuildGrid(int visualCount, out Size size, out Border? firstChild) - { - var columns = (int)Math.Ceiling(Math.Sqrt(visualCount)); - var rows = (visualCount + columns - 1) / columns; - size = new Size(columns * CellStride, rows * CellStride); - firstChild = null; - - var root = new Canvas - { - Width = size.Width, - Height = size.Height - }; - - var leafHost = root; - for (var depth = 0; depth < TreeDepth; depth++) - { - var nested = new Canvas - { - Width = size.Width, - Height = size.Height - }; - leafHost.Children.Add(nested); - leafHost = nested; - } - - for (var i = 0; i < visualCount; i++) - { - var child = new Border - { - Width = CellSize, - Height = CellSize, - Background = Brushes.Red - }; - - Canvas.SetLeft(child, i % columns * CellStride); - Canvas.SetTop(child, i / columns * CellStride); - leafHost.Children.Add(child); - - if (i == 0) - firstChild = child; - } - - return root; - } -} - -[MemoryDiagnoser] -[Config(typeof(BenchmarkConfig))] -public class CompositionHitTestingAnimated -{ - private const int CellSize = 8; - private const int CellStride = 12; - private const int TreeDepth = 4; - - private CompositorTestServices? _services; - private CompositionVisual? _animatedVisual; - private Border? _expectedHit; - private Point _hitPoint; - - [Params(1, 2, 4, 8, 16, 32, 64, 1024, 4096, 16384)] - public int VisualCount { get; set; } - - [GlobalSetup] - public void Setup() - { - var canvas = BuildDeepAnimatedGrid(VisualCount, out var size, out _expectedHit); - - _services = new CompositorTestServices(size); - _services.TopLevel.Content = canvas; - _services.RunJobs(); - - _animatedVisual = _expectedHit!.CompositionVisual; - StartOffsetAnimation(); - _services.RunJobs(); - UpdateHitPoint(); - - if (!ReferenceEquals(HitTestAnimatedChild(), _expectedHit)) - throw new InvalidOperationException("Hit test returned an unexpected visual."); - } - - [GlobalCleanup] - public void Cleanup() - { - _services?.Dispose(); - _services = null; - _animatedVisual = null; - _expectedHit = null; - } - - [Benchmark] - public Visual? HitTestAnimatedChild() - { - _services!.RunJobs(); - UpdateHitPoint(); - return _services.Renderer.HitTestFirst(_hitPoint, _services.TopLevel, null); - } - - private void StartOffsetAnimation() - { - var animation = _animatedVisual!.Compositor.CreateVector3KeyFrameAnimation(); - animation.Target = "Offset"; - animation.InsertKeyFrame(0f, new Vector3(CellStride, CellStride, 0), new LinearEasing()); - animation.InsertKeyFrame(1f, new Vector3(CellStride * 3, CellStride * 3, 0), new LinearEasing()); - animation.Duration = TimeSpan.FromSeconds(1); - animation.Direction = PlaybackDirection.Alternate; - animation.IterationBehavior = AnimationIterationBehavior.Forever; - _animatedVisual.StartAnimation("Offset", animation); - } - - private void UpdateHitPoint() - { - var server = _animatedVisual!.Server; - var bounds = server.GetReadback(server.Compositor.Readback.LastCompletedWrite)!.TransformedSubtreeBounds!.Value; - _hitPoint = new Point((bounds.Left + bounds.Right) / 2, (bounds.Top + bounds.Bottom) / 2); - } - - private static Canvas BuildDeepAnimatedGrid(int visualCount, out Size size, out Border target) - { - var branchCount = Math.Min(8, Math.Max(1, visualCount / 64)); - var leavesPerBranch = (visualCount + branchCount - 1) / branchCount; - var columns = (int)Math.Ceiling(Math.Sqrt(leavesPerBranch + 1)); - var rows = (leavesPerBranch + columns - 1) / columns; - var branchSize = new Size(columns * CellStride + CellStride * 4, rows * CellStride + CellStride * 4); - - size = new Size(branchSize.Width * branchCount, branchSize.Height); - target = null!; - - var root = new Canvas - { - Width = size.Width, - Height = size.Height - }; - - var remaining = visualCount; - for (var branch = 0; branch < branchCount; branch++) - { - var branchRoot = new Canvas - { - Width = branchSize.Width, - Height = branchSize.Height - }; - Canvas.SetLeft(branchRoot, branch * branchSize.Width); - root.Children.Add(branchRoot); - - var leafHost = branchRoot; - for (var depth = 0; depth < TreeDepth; depth++) - { - var nested = new Canvas - { - Width = branchSize.Width, - Height = branchSize.Height - }; - leafHost.Children.Add(nested); - leafHost = nested; - } - - var count = Math.Min(leavesPerBranch, remaining); - remaining -= count; - - if (branch == 0) - count--; - - for (var i = 0; i < count; i++) - { - var child = new Border - { - Width = CellSize, - Height = CellSize, - Background = Brushes.Red - }; - - Canvas.SetLeft(child, (i % columns) * CellStride); - Canvas.SetTop(child, (i / columns) * CellStride); - leafHost.Children.Add(child); - } - - if (branch == 0) - { - target = new Border - { - Width = CellSize, - Height = CellSize, - Background = Brushes.Blue - }; - - Canvas.SetLeft(target, CellStride); - Canvas.SetTop(target, CellStride); - leafHost.Children.Add(target); - } - } - - return root; - } -} diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index 7fd5ae6ad8..14320ee4da 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -110,7 +110,7 @@ public class CompositorTestServices : IDisposable public void AssertHitTestFirst(Point pt, Func? filter, Visual? expected) { RunJobs(); - var tested = Renderer.HitTest(pt, TopLevel, filter).First(); + var tested = Renderer.HitTestFirst(pt, TopLevel, filter); Assert.Equal(expected, tested); }