Browse Source

Revert runtime knobs and add tests

pull/21310/head
Steven He 5 months ago
parent
commit
c33ea4346c
  1. 4
      src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs
  2. 2
      src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs
  3. 3
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs
  4. 100
      tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
  5. 4
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  6. 270
      tests/Avalonia.Benchmarks/Rendering/CompositionHitTesting.cs
  7. 2
      tests/Avalonia.UnitTests/CompositorTestServices.cs

4
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<CompositionVisual, int> _leaves = [];
private readonly Dictionary<CompositionVisual, int> _unbounded = [];
private readonly List<Node> _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();

2
src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs

@ -8,7 +8,7 @@ namespace Avalonia.Rendering.Composition
/// </summary>
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;

3
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);
}

100
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<Visual>().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);

4
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -4,7 +4,6 @@
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<XunitAutoGeneratedEntryPoint>false</XunitAutoGeneratedEntryPoint>
<DisableHitTestAabbTree>true</DisableHitTestAabbTree>
</PropertyGroup>
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
@ -16,9 +15,6 @@
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="Avalonia.Rendering.Composition.DisableHitTestAabbTree" Value="$(DisableHitTestAabbTree)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
</ItemGroup>

270
tests/Avalonia.Benchmarks/Rendering/CompositionHitTesting.cs

@ -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;
}
}

2
tests/Avalonia.UnitTests/CompositorTestServices.cs

@ -110,7 +110,7 @@ public class CompositorTestServices : IDisposable
public void AssertHitTestFirst(Point pt, Func<Visual, bool>? filter, Visual? expected)
{
RunJobs();
var tested = Renderer.HitTest(pt, TopLevel, filter).First();
var tested = Renderer.HitTestFirst(pt, TopLevel, filter);
Assert.Equal(expected, tested);
}

Loading…
Cancel
Save