Browse Source

Avoid rebuilding the whole tree

pull/21310/head
Steven He 4 months ago
parent
commit
5cd484de0d
  1. 110
      src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs
  2. 62
      src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs
  3. 22
      src/Avalonia.Base/Rendering/Composition/VisualCollection.cs

110
src/Avalonia.Base/Rendering/Composition/CompositionHitTestAabbTree.cs

@ -24,6 +24,12 @@ internal sealed class CompositionHitTestAabbTree
private int _root = Null;
private int _freeList = Null;
public CompositionHitTestAabbTree(CompositionVisualCollection children)
{
for (var i = 0; i < children.Count; i++)
Add(children[i], i);
}
public void Clear()
{
_leaves.Clear();
@ -34,19 +40,10 @@ internal sealed class CompositionHitTestAabbTree
_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)
public void Update(CompositionVisual visual, int order)
{
if (_leaves.TryGetValue(visual, out var leaf))
{
var order = _nodes[leaf].Order;
var state = GetBoundsState(visual, out var bounds);
if (state == BoundsState.Bounded)
@ -61,27 +58,51 @@ internal sealed class CompositionHitTestAabbTree
_unbounded[visual] = order;
}
return true;
return;
}
if (_unbounded.TryGetValue(visual, out var unboundedOrder))
if (_unbounded.ContainsKey(visual))
{
var state = GetBoundsState(visual, out var bounds);
if (state == BoundsState.Unbounded)
{
return true;
_unbounded[visual] = order;
return;
}
_unbounded.Remove(visual);
if (state == BoundsState.Bounded)
CreateLeaf(visual, bounds, unboundedOrder);
CreateLeaf(visual, bounds, order);
return;
}
Add(visual, order);
}
public void Remove(CompositionVisual visual)
{
if (_leaves.TryGetValue(visual, out var leaf))
{
DestroyLeaf(visual, leaf);
return;
}
_unbounded.Remove(visual);
}
return true;
public void UpdateOrder(CompositionVisual visual, int order)
{
if (_leaves.TryGetValue(visual, out var leaf))
{
SetOrder(leaf, order);
return;
}
return false;
if (_unbounded.ContainsKey(visual))
_unbounded[visual] = order;
}
public void Query(Point point, PooledList<CompositionVisual> results)
@ -218,6 +239,7 @@ internal sealed class CompositionHitTestAabbTree
private void MoveLeaf(int leaf, LtrbRect bounds)
{
// If the exact bounds still fit inside the fat bounds, the tree shape can stay unchanged.
if (_nodes[leaf].Bounds.Contains(bounds))
return;
@ -294,6 +316,14 @@ internal sealed class CompositionHitTestAabbTree
var sibling = FindBestSibling(leafBounds);
var oldParent = _nodes[sibling].Parent;
var newParent = AllocateNode();
// Insert by replacing the chosen sibling with a new internal parent:
//
// Before: oldParent After: oldParent
// | |
// sibling newParent
// / \
// sibling leaf
var parentNode = _nodes[newParent];
parentNode.Parent = oldParent;
parentNode.Bounds = leafBounds.Union(_nodes[sibling].Bounds);
@ -344,6 +374,7 @@ internal sealed class CompositionHitTestAabbTree
var cost1 = GetInsertionCost(child1, leafBounds, inheritanceCost);
var cost2 = GetInsertionCost(child2, leafBounds, inheritanceCost);
// Stop descending when pairing with this internal node is already cheaper.
if (cost < cost1 && cost < cost2)
break;
@ -378,8 +409,14 @@ internal sealed class CompositionHitTestAabbTree
var grandParent = parentNode.Parent;
var sibling = parentNode.Child1 == leaf ? parentNode.Child2 : parentNode.Child1;
// Collapse the removed leaf's parent and promote the sibling.
if (grandParent != Null)
{
// Before: grandParent After: grandParent
// | |
// parent sibling
// / \
// leaf sibling
var grandParentNode = _nodes[grandParent];
if (grandParentNode.Child1 == parent)
grandParentNode.Child1 = sibling;
@ -396,6 +433,11 @@ internal sealed class CompositionHitTestAabbTree
}
else
{
// If the parent was the root, the sibling becomes the new root.
//
// Before: parent(root) After: sibling(root)
// / \
// leaf sibling
_root = sibling;
var siblingNode = _nodes[sibling];
siblingNode.Parent = Null;
@ -416,6 +458,8 @@ internal sealed class CompositionHitTestAabbTree
var node = _nodes[index];
var child1 = _nodes[node.Child1];
var child2 = _nodes[node.Child2];
// Ancestor bounds always cover both children after insert/remove/rotate.
node.Bounds = child1.Bounds.Union(child2.Bounds);
node.Height = 1 + Math.Max(child1.Height, child2.Height);
_nodes[index] = node;
@ -436,9 +480,11 @@ internal sealed class CompositionHitTestAabbTree
var c = _nodes[indexC];
var balance = c.Height - b.Height;
// The right subtree is heavier than the left. Rotate C up.
if (balance > 1)
return RotateCUp(indexA, indexB, indexC);
// The left subtree is heavier than the right. Rotate B up.
if (balance < -1)
return RotateBUp(indexA, indexB, indexC);
@ -447,6 +493,19 @@ internal sealed class CompositionHitTestAabbTree
private int RotateCUp(int indexA, int indexB, int indexC)
{
// Rotate C above A:
//
// Before: A After, if F taller: C
// / \ / \
// B C A F
// / \ / \
// F G B G
//
// After, otherwise: C
// / \
// A G
// / \
// B F
var a = _nodes[indexA];
var c = _nodes[indexC];
var indexF = c.Child1;
@ -458,8 +517,10 @@ internal sealed class CompositionHitTestAabbTree
c.Parent = a.Parent;
a.Parent = indexC;
// C takes A's old place in the parent chain.
ReplaceParentChild(indexA, indexC, c.Parent);
// Keep the taller C child with C, and move the other child under A.
if (f.Height > g.Height)
{
c.Child2 = indexF;
@ -490,6 +551,19 @@ internal sealed class CompositionHitTestAabbTree
private int RotateBUp(int indexA, int indexB, int indexC)
{
// Rotate B above A:
//
// Before: A After, if D taller: B
// / \ / \
// B C A D
// / \ / \
// D E E C
//
// After, otherwise: B
// / \
// A E
// / \
// D C
var a = _nodes[indexA];
var b = _nodes[indexB];
var indexD = b.Child1;
@ -501,8 +575,10 @@ internal sealed class CompositionHitTestAabbTree
b.Parent = a.Parent;
a.Parent = indexB;
// B takes A's old place in the parent chain.
ReplaceParentChild(indexA, indexB, b.Parent);
// Keep the taller B child with B, and move the other child under A.
if (d.Height > e.Height)
{
b.Child2 = indexD;
@ -565,6 +641,7 @@ internal sealed class CompositionHitTestAabbTree
return BoundsState.Bounded;
}
// Fatten the bounds by a small amount to avoid having to update the tree for every tiny movement.
private static LtrbRect Fatten(LtrbRect bounds) =>
new(bounds.Left - FatBoundsPadding,
bounds.Top - FatBoundsPadding,
@ -602,6 +679,7 @@ internal sealed class CompositionHitTestAabbTree
private sealed class CandidateComparer : IComparer<Candidate>
{
// Higher child order is topmost, sort descending.
public int Compare(Candidate left, Candidate right) => right.Order.CompareTo(left.Order);
}
}

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

@ -10,7 +10,6 @@ namespace Avalonia.Rendering.Composition
{
internal const int HitTestAabbTreeThreshold = 32;
private CompositionHitTestAabbTree? _hitTestChildren;
private bool _hitTestChildrenDirty = true;
public CompositionVisualCollection Children { get; private set; } = null!;
@ -26,43 +25,58 @@ namespace Avalonia.Rendering.Composition
base.OnRootChangedCore();
}
internal void InvalidateHitTestChildren()
internal void AddHitTestChild(CompositionVisual child)
{
_hitTestChildrenDirty = true;
if (_hitTestChildren == null)
return;
var order = Children.IndexOf(child);
if (order >= 0)
_hitTestChildren.Update(child, order);
UpdateHitTestChildOrder();
}
internal void RemoveHitTestChild(CompositionVisual child)
{
if (_hitTestChildren == null)
return;
_hitTestChildren.Remove(child);
UpdateHitTestChildOrder();
}
internal void ClearHitTestChildren()
{
_hitTestChildren?.Clear();
}
internal void UpdateHitTestChildBounds(CompositionVisual child)
{
if (_hitTestChildren == null || _hitTestChildrenDirty)
if (_hitTestChildren == null)
return;
if (Children.Count < HitTestAabbTreeThreshold)
{
_hitTestChildren.Clear();
_hitTestChildren = null;
return;
}
if (!_hitTestChildren.Update(child))
_hitTestChildrenDirty = true;
var order = Children.IndexOf(child);
if (order >= 0)
_hitTestChildren.Update(child, order);
}
internal bool TryQueryHitTestChildren(Point point, PooledList<CompositionVisual> results)
{
if (Children.Count < HitTestAabbTreeThreshold)
{
_hitTestChildren?.Clear();
_hitTestChildren = null;
return false;
}
_hitTestChildren ??= new CompositionHitTestAabbTree();
if (_hitTestChildrenDirty)
{
_hitTestChildren.Rebuild(Children);
_hitTestChildrenDirty = false;
}
_hitTestChildren ??= new CompositionHitTestAabbTree(Children);
_hitTestChildren.Query(point, results);
return true;
@ -73,22 +87,24 @@ namespace Avalonia.Rendering.Composition
{
if (Children.Count < HitTestAabbTreeThreshold)
{
_hitTestChildren?.Clear();
_hitTestChildren = null;
hit = null;
return false;
}
_hitTestChildren ??= new CompositionHitTestAabbTree();
if (_hitTestChildrenDirty)
{
_hitTestChildren.Rebuild(Children);
_hitTestChildrenDirty = false;
}
_hitTestChildren ??= new CompositionHitTestAabbTree(Children);
hit = _hitTestChildren.QueryFirst(point, ref hitTest);
return true;
}
private void UpdateHitTestChildOrder()
{
if (_hitTestChildren == null)
return;
for (var i = 0; i < Children.Count; i++)
_hitTestChildren.UpdateOrder(Children[i], i);
}
}
}

22
src/Avalonia.Base/Rendering/Composition/VisualCollection.cs

@ -41,7 +41,7 @@ namespace Avalonia.Rendering.Composition
partial void OnAdded(CompositionVisual item)
{
item.Parent = _owner;
InvalidateHitTestChildren();
AddHitTestChild(item);
}
partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem)
@ -62,7 +62,7 @@ namespace Avalonia.Rendering.Composition
partial void OnRemoved(CompositionVisual item)
{
item.Parent = null;
InvalidateHitTestChildren();
RemoveHitTestChild(item);
}
partial void OnBeforeClear()
@ -71,7 +71,7 @@ namespace Avalonia.Rendering.Composition
i.Parent = null;
}
partial void OnClear() => InvalidateHitTestChildren();
partial void OnClear() => ClearHitTestChildren();
partial void OnBeforeAdded(CompositionVisual item)
{
@ -80,10 +80,22 @@ namespace Avalonia.Rendering.Composition
item.Parent = _owner;
}
private void InvalidateHitTestChildren()
private void AddHitTestChild(CompositionVisual item)
{
if (_owner is CompositionContainerVisual container)
container.InvalidateHitTestChildren();
container.AddHitTestChild(item);
}
private void RemoveHitTestChild(CompositionVisual item)
{
if (_owner is CompositionContainerVisual container)
container.RemoveHitTestChild(item);
}
private void ClearHitTestChildren()
{
if (_owner is CompositionContainerVisual container)
container.ClearHitTestChildren();
}
}
}

Loading…
Cancel
Save