11 changed files with 1096 additions and 35 deletions
@ -0,0 +1,556 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Collections.Pooled; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
internal sealed class CompositionHitTestAabbTree |
|||
{ |
|||
private const int Null = -1; |
|||
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 = []; |
|||
private readonly List<Candidate> _queryCandidates = []; |
|||
private int[] _queryStack = []; |
|||
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(); |
|||
_unbounded.Clear(); |
|||
_nodes.Clear(); |
|||
_queryCandidates.Clear(); |
|||
_root = Null; |
|||
_freeList = Null; |
|||
} |
|||
|
|||
public void Rebuild(CompositionVisualCollection children) |
|||
{ |
|||
Clear(); |
|||
|
|||
for (var i = 0; i < children.Count; i++) |
|||
Add(children[i], i); |
|||
} |
|||
|
|||
public bool Update(CompositionVisual visual) |
|||
{ |
|||
if (_leaves.TryGetValue(visual, out var leaf)) |
|||
{ |
|||
var order = _nodes[leaf].Order; |
|||
var state = GetBoundsState(visual, out var bounds); |
|||
|
|||
if (state == BoundsState.Bounded) |
|||
{ |
|||
MoveLeaf(leaf, bounds); |
|||
SetOrder(leaf, order); |
|||
} |
|||
else |
|||
{ |
|||
DestroyLeaf(visual, leaf); |
|||
if (state == BoundsState.Unbounded) |
|||
_unbounded[visual] = order; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
if (_unbounded.TryGetValue(visual, out var unboundedOrder)) |
|||
{ |
|||
var state = GetBoundsState(visual, out var bounds); |
|||
|
|||
if (state == BoundsState.Unbounded) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
_unbounded.Remove(visual); |
|||
|
|||
if (state == BoundsState.Bounded) |
|||
CreateLeaf(visual, bounds, unboundedOrder); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public void Query(Point point, PooledList<CompositionVisual> results) |
|||
{ |
|||
_queryCandidates.Clear(); |
|||
|
|||
if (_root != Null) |
|||
{ |
|||
var stackCount = 0; |
|||
PushQueryNode(ref stackCount, _root); |
|||
|
|||
while (stackCount > 0) |
|||
{ |
|||
var nodeIndex = _queryStack[--stackCount]; |
|||
var node = _nodes[nodeIndex]; |
|||
|
|||
if (!node.Bounds.Contains(point)) |
|||
continue; |
|||
|
|||
if (node.IsLeaf) |
|||
{ |
|||
if (node.Visual != null) |
|||
_queryCandidates.Add(new Candidate(node.Visual, node.Order)); |
|||
} |
|||
else |
|||
{ |
|||
PushQueryNode(ref stackCount, node.Child1); |
|||
PushQueryNode(ref stackCount, node.Child2); |
|||
} |
|||
} |
|||
} |
|||
|
|||
foreach (var candidate in _unbounded) |
|||
_queryCandidates.Add(new Candidate(candidate.Key, candidate.Value)); |
|||
|
|||
_queryCandidates.Sort(s_candidateComparer); |
|||
|
|||
foreach (var candidate in _queryCandidates) |
|||
results.Add(candidate.Visual); |
|||
|
|||
_queryCandidates.Clear(); |
|||
} |
|||
|
|||
private void Add(CompositionVisual visual, int order) |
|||
{ |
|||
var state = GetBoundsState(visual, out var bounds); |
|||
|
|||
if (state == BoundsState.Bounded) |
|||
CreateLeaf(visual, bounds, order); |
|||
else if (state == BoundsState.Unbounded) |
|||
_unbounded[visual] = order; |
|||
} |
|||
|
|||
private void PushQueryNode(ref int count, int nodeIndex) |
|||
{ |
|||
if (nodeIndex == Null) |
|||
return; |
|||
|
|||
if (count == _queryStack.Length) |
|||
Array.Resize(ref _queryStack, Math.Max(16, _queryStack.Length * 2)); |
|||
|
|||
_queryStack[count++] = nodeIndex; |
|||
} |
|||
|
|||
private void CreateLeaf(CompositionVisual visual, LtrbRect bounds, int order) |
|||
{ |
|||
var leaf = AllocateNode(); |
|||
var node = _nodes[leaf]; |
|||
node.Bounds = Fatten(bounds); |
|||
node.Visual = visual; |
|||
node.Order = order; |
|||
node.Height = 0; |
|||
_nodes[leaf] = node; |
|||
_leaves[visual] = leaf; |
|||
InsertLeaf(leaf); |
|||
} |
|||
|
|||
private void DestroyLeaf(CompositionVisual visual, int leaf) |
|||
{ |
|||
RemoveLeaf(leaf); |
|||
FreeNode(leaf); |
|||
_leaves.Remove(visual); |
|||
} |
|||
|
|||
private void MoveLeaf(int leaf, LtrbRect bounds) |
|||
{ |
|||
if (_nodes[leaf].Bounds.Contains(bounds)) |
|||
return; |
|||
|
|||
RemoveLeaf(leaf); |
|||
|
|||
var node = _nodes[leaf]; |
|||
node.Bounds = Fatten(bounds); |
|||
_nodes[leaf] = node; |
|||
|
|||
InsertLeaf(leaf); |
|||
} |
|||
|
|||
private void SetOrder(int nodeIndex, int order) |
|||
{ |
|||
var node = _nodes[nodeIndex]; |
|||
node.Order = order; |
|||
_nodes[nodeIndex] = node; |
|||
} |
|||
|
|||
private int AllocateNode() |
|||
{ |
|||
if (_freeList == Null) |
|||
{ |
|||
_nodes.Add(new Node |
|||
{ |
|||
Parent = Null, |
|||
Child1 = Null, |
|||
Child2 = Null, |
|||
Next = Null |
|||
}); |
|||
return _nodes.Count - 1; |
|||
} |
|||
|
|||
var index = _freeList; |
|||
var node = _nodes[index]; |
|||
_freeList = node.Next; |
|||
node.Parent = Null; |
|||
node.Child1 = Null; |
|||
node.Child2 = Null; |
|||
node.Next = Null; |
|||
node.Height = 0; |
|||
node.Visual = null; |
|||
node.Order = 0; |
|||
_nodes[index] = node; |
|||
return index; |
|||
} |
|||
|
|||
private void FreeNode(int index) |
|||
{ |
|||
var node = _nodes[index]; |
|||
node.Next = _freeList; |
|||
node.Parent = Null; |
|||
node.Child1 = Null; |
|||
node.Child2 = Null; |
|||
node.Height = -1; |
|||
node.Visual = null; |
|||
node.Order = 0; |
|||
_nodes[index] = node; |
|||
_freeList = index; |
|||
} |
|||
|
|||
private void InsertLeaf(int leaf) |
|||
{ |
|||
if (_root == Null) |
|||
{ |
|||
_root = leaf; |
|||
var root = _nodes[_root]; |
|||
root.Parent = Null; |
|||
_nodes[_root] = root; |
|||
return; |
|||
} |
|||
|
|||
var leafBounds = _nodes[leaf].Bounds; |
|||
var sibling = FindBestSibling(leafBounds); |
|||
var oldParent = _nodes[sibling].Parent; |
|||
var newParent = AllocateNode(); |
|||
var parentNode = _nodes[newParent]; |
|||
parentNode.Parent = oldParent; |
|||
parentNode.Bounds = leafBounds.Union(_nodes[sibling].Bounds); |
|||
parentNode.Height = _nodes[sibling].Height + 1; |
|||
parentNode.Child1 = sibling; |
|||
parentNode.Child2 = leaf; |
|||
parentNode.Visual = null; |
|||
_nodes[newParent] = parentNode; |
|||
|
|||
var siblingNode = _nodes[sibling]; |
|||
siblingNode.Parent = newParent; |
|||
_nodes[sibling] = siblingNode; |
|||
|
|||
var leafNode = _nodes[leaf]; |
|||
leafNode.Parent = newParent; |
|||
_nodes[leaf] = leafNode; |
|||
|
|||
if (oldParent == Null) |
|||
{ |
|||
_root = newParent; |
|||
} |
|||
else |
|||
{ |
|||
var oldParentNode = _nodes[oldParent]; |
|||
if (oldParentNode.Child1 == sibling) |
|||
oldParentNode.Child1 = newParent; |
|||
else |
|||
oldParentNode.Child2 = newParent; |
|||
_nodes[oldParent] = oldParentNode; |
|||
} |
|||
|
|||
FixAncestors(newParent); |
|||
} |
|||
|
|||
private int FindBestSibling(LtrbRect leafBounds) |
|||
{ |
|||
var index = _root; |
|||
while (!_nodes[index].IsLeaf) |
|||
{ |
|||
var node = _nodes[index]; |
|||
var child1 = node.Child1; |
|||
var child2 = node.Child2; |
|||
var area = Perimeter(node.Bounds); |
|||
var combinedArea = Perimeter(node.Bounds.Union(leafBounds)); |
|||
var cost = 2 * combinedArea; |
|||
var inheritanceCost = 2 * (combinedArea - area); |
|||
|
|||
var cost1 = GetInsertionCost(child1, leafBounds, inheritanceCost); |
|||
var cost2 = GetInsertionCost(child2, leafBounds, inheritanceCost); |
|||
|
|||
if (cost < cost1 && cost < cost2) |
|||
break; |
|||
|
|||
index = cost1 < cost2 ? child1 : child2; |
|||
} |
|||
|
|||
return index; |
|||
} |
|||
|
|||
private double GetInsertionCost(int nodeIndex, LtrbRect leafBounds, double inheritanceCost) |
|||
{ |
|||
var node = _nodes[nodeIndex]; |
|||
var union = node.Bounds.Union(leafBounds); |
|||
|
|||
if (node.IsLeaf) |
|||
return Perimeter(union) + inheritanceCost; |
|||
|
|||
return Perimeter(union) - Perimeter(node.Bounds) + inheritanceCost; |
|||
} |
|||
|
|||
private void RemoveLeaf(int leaf) |
|||
{ |
|||
if (leaf == _root) |
|||
{ |
|||
_root = Null; |
|||
return; |
|||
} |
|||
|
|||
var leafNode = _nodes[leaf]; |
|||
var parent = leafNode.Parent; |
|||
var parentNode = _nodes[parent]; |
|||
var grandParent = parentNode.Parent; |
|||
var sibling = parentNode.Child1 == leaf ? parentNode.Child2 : parentNode.Child1; |
|||
|
|||
if (grandParent != Null) |
|||
{ |
|||
var grandParentNode = _nodes[grandParent]; |
|||
if (grandParentNode.Child1 == parent) |
|||
grandParentNode.Child1 = sibling; |
|||
else |
|||
grandParentNode.Child2 = sibling; |
|||
_nodes[grandParent] = grandParentNode; |
|||
|
|||
var siblingNode = _nodes[sibling]; |
|||
siblingNode.Parent = grandParent; |
|||
_nodes[sibling] = siblingNode; |
|||
|
|||
FreeNode(parent); |
|||
FixAncestors(grandParent); |
|||
} |
|||
else |
|||
{ |
|||
_root = sibling; |
|||
var siblingNode = _nodes[sibling]; |
|||
siblingNode.Parent = Null; |
|||
_nodes[sibling] = siblingNode; |
|||
FreeNode(parent); |
|||
} |
|||
|
|||
leafNode.Parent = Null; |
|||
_nodes[leaf] = leafNode; |
|||
} |
|||
|
|||
private void FixAncestors(int index) |
|||
{ |
|||
while (index != Null) |
|||
{ |
|||
index = Balance(index); |
|||
|
|||
var node = _nodes[index]; |
|||
var child1 = _nodes[node.Child1]; |
|||
var child2 = _nodes[node.Child2]; |
|||
node.Bounds = child1.Bounds.Union(child2.Bounds); |
|||
node.Height = 1 + Math.Max(child1.Height, child2.Height); |
|||
_nodes[index] = node; |
|||
|
|||
index = node.Parent; |
|||
} |
|||
} |
|||
|
|||
private int Balance(int indexA) |
|||
{ |
|||
var a = _nodes[indexA]; |
|||
if (a.IsLeaf || a.Height < 2) |
|||
return indexA; |
|||
|
|||
var indexB = a.Child1; |
|||
var indexC = a.Child2; |
|||
var b = _nodes[indexB]; |
|||
var c = _nodes[indexC]; |
|||
var balance = c.Height - b.Height; |
|||
|
|||
if (balance > 1) |
|||
return RotateCUp(indexA, indexB, indexC); |
|||
|
|||
if (balance < -1) |
|||
return RotateBUp(indexA, indexB, indexC); |
|||
|
|||
return indexA; |
|||
} |
|||
|
|||
private int RotateCUp(int indexA, int indexB, int indexC) |
|||
{ |
|||
var a = _nodes[indexA]; |
|||
var c = _nodes[indexC]; |
|||
var indexF = c.Child1; |
|||
var indexG = c.Child2; |
|||
var f = _nodes[indexF]; |
|||
var g = _nodes[indexG]; |
|||
|
|||
c.Child1 = indexA; |
|||
c.Parent = a.Parent; |
|||
a.Parent = indexC; |
|||
|
|||
ReplaceParentChild(indexA, indexC, c.Parent); |
|||
|
|||
if (f.Height > g.Height) |
|||
{ |
|||
c.Child2 = indexF; |
|||
a.Child2 = indexG; |
|||
g.Parent = indexA; |
|||
_nodes[indexG] = g; |
|||
a.Bounds = _nodes[indexB].Bounds.Union(g.Bounds); |
|||
c.Bounds = a.Bounds.Union(f.Bounds); |
|||
a.Height = 1 + Math.Max(_nodes[indexB].Height, g.Height); |
|||
c.Height = 1 + Math.Max(a.Height, f.Height); |
|||
} |
|||
else |
|||
{ |
|||
c.Child2 = indexG; |
|||
a.Child2 = indexF; |
|||
f.Parent = indexA; |
|||
_nodes[indexF] = f; |
|||
a.Bounds = _nodes[indexB].Bounds.Union(f.Bounds); |
|||
c.Bounds = a.Bounds.Union(g.Bounds); |
|||
a.Height = 1 + Math.Max(_nodes[indexB].Height, f.Height); |
|||
c.Height = 1 + Math.Max(a.Height, g.Height); |
|||
} |
|||
|
|||
_nodes[indexA] = a; |
|||
_nodes[indexC] = c; |
|||
return indexC; |
|||
} |
|||
|
|||
private int RotateBUp(int indexA, int indexB, int indexC) |
|||
{ |
|||
var a = _nodes[indexA]; |
|||
var b = _nodes[indexB]; |
|||
var indexD = b.Child1; |
|||
var indexE = b.Child2; |
|||
var d = _nodes[indexD]; |
|||
var e = _nodes[indexE]; |
|||
|
|||
b.Child1 = indexA; |
|||
b.Parent = a.Parent; |
|||
a.Parent = indexB; |
|||
|
|||
ReplaceParentChild(indexA, indexB, b.Parent); |
|||
|
|||
if (d.Height > e.Height) |
|||
{ |
|||
b.Child2 = indexD; |
|||
a.Child1 = indexE; |
|||
e.Parent = indexA; |
|||
_nodes[indexE] = e; |
|||
a.Bounds = _nodes[indexC].Bounds.Union(e.Bounds); |
|||
b.Bounds = a.Bounds.Union(d.Bounds); |
|||
a.Height = 1 + Math.Max(_nodes[indexC].Height, e.Height); |
|||
b.Height = 1 + Math.Max(a.Height, d.Height); |
|||
} |
|||
else |
|||
{ |
|||
b.Child2 = indexE; |
|||
a.Child1 = indexD; |
|||
d.Parent = indexA; |
|||
_nodes[indexD] = d; |
|||
a.Bounds = _nodes[indexC].Bounds.Union(d.Bounds); |
|||
b.Bounds = a.Bounds.Union(e.Bounds); |
|||
a.Height = 1 + Math.Max(_nodes[indexC].Height, d.Height); |
|||
b.Height = 1 + Math.Max(a.Height, e.Height); |
|||
} |
|||
|
|||
_nodes[indexA] = a; |
|||
_nodes[indexB] = b; |
|||
return indexB; |
|||
} |
|||
|
|||
private void ReplaceParentChild(int oldChild, int newChild, int parent) |
|||
{ |
|||
if (parent == Null) |
|||
{ |
|||
_root = newChild; |
|||
return; |
|||
} |
|||
|
|||
var parentNode = _nodes[parent]; |
|||
if (parentNode.Child1 == oldChild) |
|||
parentNode.Child1 = newChild; |
|||
else |
|||
parentNode.Child2 = newChild; |
|||
_nodes[parent] = parentNode; |
|||
} |
|||
|
|||
private static BoundsState GetBoundsState(CompositionVisual visual, out LtrbRect bounds) |
|||
{ |
|||
bounds = default; |
|||
|
|||
var readback = visual.TryGetValidReadback(); |
|||
if (readback == null) |
|||
return BoundsState.Empty; |
|||
|
|||
if (visual.DisableSubTreeBoundsHitTestOptimization) |
|||
return BoundsState.Unbounded; |
|||
|
|||
if (readback.TransformedSubtreeBounds is not { } subtreeBounds || subtreeBounds.IsZeroSize) |
|||
return BoundsState.Empty; |
|||
|
|||
bounds = subtreeBounds; |
|||
return BoundsState.Bounded; |
|||
} |
|||
|
|||
private static LtrbRect Fatten(LtrbRect bounds) => |
|||
new(bounds.Left - FatBoundsPadding, |
|||
bounds.Top - FatBoundsPadding, |
|||
bounds.Right + FatBoundsPadding, |
|||
bounds.Bottom + FatBoundsPadding); |
|||
|
|||
private static double Perimeter(LtrbRect bounds) => 2 * (bounds.Width + bounds.Height); |
|||
|
|||
private enum BoundsState |
|||
{ |
|||
Empty, |
|||
Bounded, |
|||
Unbounded |
|||
} |
|||
|
|||
private struct Node |
|||
{ |
|||
public LtrbRect Bounds; |
|||
public CompositionVisual? Visual; |
|||
public int Parent; |
|||
public int Child1; |
|||
public int Child2; |
|||
public int Next; |
|||
public int Height; |
|||
public int Order; |
|||
|
|||
public readonly bool IsLeaf => Child1 == Null; |
|||
} |
|||
|
|||
private readonly struct Candidate(CompositionVisual visual, int order) |
|||
{ |
|||
public CompositionVisual Visual { get; } = visual; |
|||
public int Order { get; } = order; |
|||
} |
|||
|
|||
private sealed class CandidateComparer : IComparer<Candidate> |
|||
{ |
|||
public int Compare(Candidate left, Candidate right) => right.Order.CompareTo(left.Order); |
|||
} |
|||
} |
|||
@ -0,0 +1,270 @@ |
|||
#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; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue