diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
index c8efb9b931..be04e13251 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Diagnostics;
@@ -129,8 +128,16 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
///
public Visual? HitTestFirst(Point p, Visual root, Func? filter)
{
- // TODO: Optimize
- return HitTest(p, root, filter).FirstOrDefault();
+ using var _ = Diagnostic.PerformingHitTest();
+
+ if (root.CompositionVisual == null)
+ return null;
+
+ Func? f = filter is null
+ ? null :
+ v => v is not CompositionDrawListVisual dlv || filter(dlv.Visual);
+
+ return CompositionTarget.TryHitTestFirst(p, root.CompositionVisual, f, static v => v is CompositionDrawListVisual) is CompositionDrawListVisual dv ? dv.Visual : null;
}
///
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs b/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs
new file mode 100644
index 0000000000..c439d26a9b
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs
@@ -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 _leaves = [];
+ private readonly Dictionary _unbounded = [];
+ private readonly List _nodes = [];
+ private readonly List _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 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
+ {
+ public int Compare(Candidate left, Candidate right) => right.Order.CompareTo(left.Order);
+ }
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
index 659d379517..5dd99734bc 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
@@ -11,6 +11,9 @@ namespace Avalonia.Rendering.Composition
///
internal partial class CompositionTarget
{
+ private readonly PooledList _hitTestChildCandidates = new();
+ private bool _hitTestChildCandidatesInUse;
+
partial void OnRootChanged()
{
if (Root != null)
@@ -29,13 +32,13 @@ namespace Avalonia.Rendering.Composition
///
public PooledList? TryHitTest(Point point, CompositionVisual? root, Func? filter)
{
- Server.Compositor.Readback.NextRead();
+ var readbackUpdates = Server.Compositor.Readback.NextRead();
+ ProcessHitTestReadbackUpdates(readbackUpdates);
+
root ??= Root;
if (root == null)
return null;
-
- var res = new PooledList();
-
+
// Need to convert transform the point using visual's readback since HitTestCore will use its inverse matrix
// NOTE: it can technically break hit-testing of the root visual itself if it has a non-identity transform,
// need to investigate that possibility later. We might want a separate mode for root hit-testing.
@@ -43,53 +46,181 @@ namespace Avalonia.Rendering.Composition
if (readback == null)
return null;
point = point.Transform(readback.Matrix);
-
+
+ var res = new PooledList();
HitTestCore(root, point, res, filter);
return res;
}
-
- void HitTestCore(CompositionVisual visual, Point parentPoint, PooledList result,
- Func? filter)
+
+ private static void ProcessHitTestReadbackUpdates(IReadOnlyList readbackUpdates)
{
- if (visual.Visible == false)
+ foreach (var visual in readbackUpdates)
+ {
+ if (visual.Parent is CompositionContainerVisual parent)
+ parent.UpdateHitTestChildBounds(visual);
+ }
+ }
+
+ PooledList RentHitTestChildCandidates(out bool releaseToField)
+ {
+ if (!_hitTestChildCandidatesInUse)
+ {
+ _hitTestChildCandidatesInUse = true;
+ releaseToField = true;
+ _hitTestChildCandidates.Clear();
+ return _hitTestChildCandidates;
+ }
+
+ releaseToField = false;
+ return new PooledList();
+ }
+
+ void ReleaseHitTestChildCandidates(PooledList candidates, bool releaseToField)
+ {
+ if (releaseToField)
+ {
+ candidates.Clear();
+ _hitTestChildCandidatesInUse = false;
+ }
+ else
+ {
+ candidates.Dispose();
+ }
+ }
+
+ void HitTestCore(CompositionVisual visual, Point parentPoint, PooledList result, Func? filter)
+ {
+ if (!HitTestVisual(visual, parentPoint, filter, out var point))
return;
+
+ // Inspect children
+ if (visual is CompositionContainerVisual cv)
+ HitTestChildren(cv, point, result, filter);
+ // Hit-test the current node
+ if (visual.HitTest(point))
+ result.Add(visual);
+ }
+
+ void HitTestChildren(CompositionContainerVisual visual, Point point, PooledList result, Func? filter)
+ {
+ if (visual.Children.Count >= CompositionContainerVisual.HitTestAabbTreeThreshold)
+ {
+ var candidates = RentHitTestChildCandidates(out var releaseToField);
+ try
+ {
+ if (visual.TryQueryHitTestChildren(point, candidates))
+ {
+ foreach (var child in candidates)
+ HitTestCore(child, point, result, filter);
+ return;
+ }
+ }
+ finally
+ {
+ ReleaseHitTestChildCandidates(candidates, releaseToField);
+ }
+ }
+
+ for (var c = visual.Children.Count - 1; c >= 0; c--)
+ HitTestCore(visual.Children[c], point, result, filter);
+ }
+
+ static bool HitTestVisual(CompositionVisual visual, Point parentPoint, Func? filter, out Point point)
+ {
+ point = default;
+
+ if (visual.Visible == false)
+ return false;
+
if (filter != null && !filter(visual))
- return;
+ return false;
var readback = visual.TryGetValidReadback();
if(readback == null)
- return;
-
+ return false;
if (!visual.DisableSubTreeBoundsHitTestOptimization &&
(readback.TransformedSubtreeBounds == null ||
!readback.TransformedSubtreeBounds.Value.Contains(parentPoint)))
- return;
-
+ return false;
+
if(!readback.Matrix.TryInvert(out var invMatrix))
- return;
+ return false;
+
+ point = parentPoint.Transform(invMatrix);
- var point = parentPoint.Transform(invMatrix);
-
if (visual.ClipToBounds
&& (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y))
- return;
+ return false;
if (visual.Clip?.FillContains(point) == false)
- return;
-
- // Inspect children
+ return false;
+
+ return true;
+ }
+
+ public CompositionVisual? TryHitTestFirst(Point point, CompositionVisual? root, Func? filter, Func? resultFilter)
+ {
+ var readbackUpdates = Server.Compositor.Readback.NextRead();
+ ProcessHitTestReadbackUpdates(readbackUpdates);
+
+ root ??= Root;
+ if (root == null)
+ return null;
+
+ // Need to convert transform the point using visual's readback since HitTestCore will use its inverse matrix
+ // NOTE: it can technically break hit-testing of the root visual itself if it has a non-identity transform,
+ // need to investigate that possibility later. We might want a separate mode for root hit-testing.
+ var readback = root.TryGetValidReadback();
+ if (readback == null)
+ return null;
+
+ return HitTestFirstCore(root, point.Transform(readback.Matrix), filter, resultFilter);
+ }
+
+ CompositionVisual? HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func? filter, Func? resultFilter)
+ {
+ if (!HitTestVisual(visual, parentPoint, filter, out var point))
+ return null;
+
if (visual is CompositionContainerVisual cv)
- for (var c = cv.Children.Count - 1; c >= 0; c--)
+ {
+ var queriedIndexedChildren = false;
+ if (cv.Children.Count >= CompositionContainerVisual.HitTestAabbTreeThreshold)
{
- var ch = cv.Children[c];
- HitTestCore(ch, point, result, filter);
+ var candidates = RentHitTestChildCandidates(out var releaseToField);
+ try
+ {
+ if (cv.TryQueryHitTestChildren(point, candidates))
+ {
+ queriedIndexedChildren = true;
+ foreach (var child in candidates)
+ {
+ var hit = HitTestFirstCore(child, point, filter, resultFilter);
+ if (hit != null)
+ return hit;
+ }
+ }
+ }
+ finally
+ {
+ ReleaseHitTestChildCandidates(candidates, releaseToField);
+ }
}
-
- // Hit-test the current node
- if (visual.HitTest(point))
- result.Add(visual);
+
+ if (!queriedIndexedChildren)
+ {
+ for (var c = cv.Children.Count - 1; c >= 0; c--)
+ {
+ var hit = HitTestFirstCore(cv.Children[c], point, filter, resultFilter);
+ if (hit != null)
+ return hit;
+ }
+ }
+ }
+
+ return visual.HitTest(point) && (resultFilter == null || resultFilter(visual)) ? visual : null;
}
///
diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs
index f79828c9b4..94a8424666 100644
--- a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs
@@ -1,3 +1,4 @@
+using Avalonia.Collections.Pooled;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition
@@ -7,6 +8,10 @@ namespace Avalonia.Rendering.Composition
///
public partial class CompositionContainerVisual : CompositionVisual
{
+ internal static readonly int HitTestAabbTreeThreshold = CompositionHitTestAabbTree.IsEnabled ? 1 : int.MaxValue;
+ private CompositionHitTestAabbTree? _hitTestChildren;
+ private bool _hitTestChildrenDirty = true;
+
public CompositionVisualCollection Children { get; private set; } = null!;
partial void InitializeDefaultsExtra()
@@ -20,5 +25,47 @@ namespace Avalonia.Rendering.Composition
ch.Root = Root;
base.OnRootChangedCore();
}
+
+ internal void InvalidateHitTestChildren()
+ {
+ _hitTestChildrenDirty = true;
+ }
+
+ internal void UpdateHitTestChildBounds(CompositionVisual child)
+ {
+ if (_hitTestChildren == null || _hitTestChildrenDirty)
+ return;
+
+ if (Children.Count < HitTestAabbTreeThreshold)
+ {
+ _hitTestChildren.Clear();
+ _hitTestChildren = null;
+ return;
+ }
+
+ if (!_hitTestChildren.Update(child))
+ _hitTestChildrenDirty = true;
+ }
+
+ internal bool TryQueryHitTestChildren(Point point, PooledList results)
+ {
+ if (Children.Count < HitTestAabbTreeThreshold)
+ {
+ _hitTestChildren?.Clear();
+ _hitTestChildren = null;
+ return false;
+ }
+
+ _hitTestChildren ??= new CompositionHitTestAabbTree();
+
+ if (_hitTestChildrenDirty)
+ {
+ _hitTestChildren.Rebuild(Children);
+ _hitTestChildrenDirty = false;
+ }
+
+ _hitTestChildren.Query(point, results);
+ return true;
+ }
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs
index ce5d80650d..c06506c530 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Threading;
namespace Avalonia.Rendering.Composition.Server
@@ -15,11 +16,25 @@ namespace Avalonia.Rendering.Composition.Server
private ulong _nextWriteRevision = 1;
public ulong WriteRevision { get; private set; }
public ulong LastCompletedWrite { get; private set; }
+ private readonly HashSet _pendingHitTestUpdateSet = new();
+ private List _pendingHitTestUpdates = new();
+ private List _readHitTestUpdates = new();
- public void NextRead()
+ public IReadOnlyList NextRead()
{
lock (_lock)
+ {
ReadRevision = LastCompletedWrite;
+ _readHitTestUpdates.Clear();
+
+ if (_pendingHitTestUpdates.Count > 0)
+ {
+ (_pendingHitTestUpdates, _readHitTestUpdates) = (_readHitTestUpdates, _pendingHitTestUpdates);
+ _pendingHitTestUpdateSet.Clear();
+ }
+
+ return _readHitTestUpdates;
+ }
}
public void BeginWrite()
@@ -33,5 +48,11 @@ namespace Avalonia.Rendering.Composition.Server
LastCompletedWrite = WriteRevision;
Monitor.Exit(_lock);
}
+
+ internal void AddHitTestUpdate(CompositionVisual? visual)
+ {
+ if (visual != null && _pendingHitTestUpdateSet.Add(visual))
+ _pendingHitTestUpdates.Add(visual);
+ }
}
}
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 a054446a5a..d64a60d369 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Readback.cs
@@ -100,7 +100,10 @@ partial class ServerCompositionVisual
slot.TargetId = Root?.Id ?? -1;
slot.TransformedSubtreeBounds = _transformedSubTreeBounds;
slot.Visible = Visible;
+
+ if (CompositionHitTestAabbTree.IsEnabled)
+ Compositor.Readback.AddHitTestUpdate(Client);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.cs
index 5e1b91f7a4..5182006a74 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.cs
@@ -20,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server
{
public ServerCompositionVisualCollection? Children { get; private set; } = null!;
public ServerCompositionVisualCache? Cache { get; private set; }
+ internal CompositionVisual? Client { get; set; }
partial void OnRootChanging()
{
diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs
index e4b469d327..552548d158 100644
--- a/src/Avalonia.Base/Rendering/Composition/Visual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs
@@ -20,6 +20,11 @@ namespace Avalonia.Rendering.Composition
{
}
+ partial void InitializeDefaultsExtra()
+ {
+ Server.Client = this;
+ }
+
partial void OnRootChanged() => OnRootChangedCore();
partial void OnParentChanging()
diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs
index 5663b86af3..e89c3837ff 100644
--- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs
+++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs
@@ -38,7 +38,11 @@ namespace Avalonia.Rendering.Composition
public void RemoveAll() => Clear();
- partial void OnAdded(CompositionVisual item) => item.Parent = _owner;
+ partial void OnAdded(CompositionVisual item)
+ {
+ item.Parent = _owner;
+ InvalidateHitTestChildren();
+ }
partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem)
{
@@ -55,7 +59,11 @@ namespace Avalonia.Rendering.Composition
}
}
- partial void OnRemoved(CompositionVisual item) => item.Parent = null;
+ partial void OnRemoved(CompositionVisual item)
+ {
+ item.Parent = null;
+ InvalidateHitTestChildren();
+ }
partial void OnBeforeClear()
{
@@ -63,11 +71,19 @@ namespace Avalonia.Rendering.Composition
i.Parent = null;
}
+ partial void OnClear() => InvalidateHitTestChildren();
+
partial void OnBeforeAdded(CompositionVisual item)
{
if (item.Parent != null)
throw new InvalidOperationException("Visual already has a parent");
item.Parent = _owner;
}
+
+ private void InvalidateHitTestChildren()
+ {
+ if (_owner is CompositionContainerVisual container)
+ container.InvalidateHitTestChildren();
+ }
}
}
diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
index b4d7435a28..5746766b7d 100644
--- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
+++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
@@ -4,6 +4,7 @@
Exe
false
false
+ true
@@ -15,6 +16,9 @@
+
+
+
diff --git a/tests/Avalonia.Benchmarks/Rendering/CompositionHitTesting.cs b/tests/Avalonia.Benchmarks/Rendering/CompositionHitTesting.cs
new file mode 100644
index 0000000000..e4906cf8f2
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Rendering/CompositionHitTesting.cs
@@ -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;
+ }
+}