Browse Source
* Prototype an AABB tree for hit testing * Some optimizations * Revert runtime knobs and add tests * Avoid rebuilding the whole tree * Revert an invalid optimization * Add a hit test page * Clean up * Format code * Nit * Use weak CompositionVisual reference in server-side * Bucket composition hit-test AABB tree by child order * Use readback revisions for AABB hit-test updatespull/21397/head
committed by
GitHub
9 changed files with 1678 additions and 34 deletions
@ -0,0 +1,401 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Numerics; |
|||
using Avalonia; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Animation.Easings; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Media; |
|||
using Avalonia.Rendering.Composition; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace RenderDemo.Pages |
|||
{ |
|||
public class HitTestingPage : UserControl |
|||
{ |
|||
private const int GroupColumns = 8; |
|||
private const int GroupRows = 5; |
|||
private const int CellsPerGroupSide = 10; |
|||
private const int CellStride = 10; |
|||
private const int CellSize = 8; |
|||
private const int AnimationTravel = 64; |
|||
|
|||
private readonly Canvas _scene; |
|||
private readonly TextBlock _stats; |
|||
private readonly Cell[] _cells = new Cell[GroupColumns * GroupRows * CellsPerGroupSide * CellsPerGroupSide]; |
|||
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); |
|||
private Compositor? _compositor; |
|||
private int _hitTestsPerFrame = 256; |
|||
private int _updateCount; |
|||
private int _hitTestCount; |
|||
private int _hitCount; |
|||
private int _lastSecondUpdateCount; |
|||
private int _lastSecondHitTestCount; |
|||
private int _lastSecondHitCount; |
|||
private TimeSpan _lastSecondTime; |
|||
private double _lastSecondUpdatesPerSecond; |
|||
private double _lastSecondHitTestsPerSecond; |
|||
private double _lastSecondHitsPerSecond; |
|||
private int _clickCount; |
|||
private bool _isAttached; |
|||
private bool _updateQueued; |
|||
private bool _animationsStarted; |
|||
private Cell? _lastHitCell; |
|||
private Cell? _lastClickedCell; |
|||
|
|||
public HitTestingPage() |
|||
{ |
|||
_scene = new Canvas |
|||
{ |
|||
Width = GroupColumns * CellsPerGroupSide * CellStride, |
|||
Height = GroupRows * CellsPerGroupSide * CellStride, |
|||
Background = Brushes.Transparent |
|||
}; |
|||
|
|||
_stats = new TextBlock |
|||
{ |
|||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, |
|||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top, |
|||
Margin = new Thickness(12), |
|||
Padding = new Thickness(8, 4), |
|||
Background = new SolidColorBrush(Color.FromArgb(220, 255, 255, 255)), |
|||
Foreground = Brushes.Black, |
|||
IsHitTestVisible = false |
|||
}; |
|||
|
|||
var numberOfHitTests = new NumericUpDown |
|||
{ |
|||
Minimum = 0, |
|||
Value = _hitTestsPerFrame, |
|||
Width = 200, |
|||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left, |
|||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center |
|||
}; |
|||
|
|||
numberOfHitTests.ValueChanged += (s, e) => |
|||
{ |
|||
if (numberOfHitTests.Value.HasValue) |
|||
{ |
|||
_hitTestsPerFrame = (int)numberOfHitTests.Value.Value; |
|||
} |
|||
}; |
|||
|
|||
var param = new StackPanel |
|||
{ |
|||
Orientation = Avalonia.Layout.Orientation.Horizontal, |
|||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left, |
|||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom, |
|||
Margin = new Thickness(12), |
|||
Spacing = 8, |
|||
Children = |
|||
{ |
|||
new TextBlock { Text = "Hit tests per update:" }, |
|||
numberOfHitTests |
|||
} |
|||
}; |
|||
|
|||
var root = new Grid |
|||
{ |
|||
ClipToBounds = true, |
|||
RowDefinitions = |
|||
{ |
|||
new RowDefinition(GridLength.Auto), |
|||
new RowDefinition(GridLength.Auto), |
|||
new RowDefinition(GridLength.Star), |
|||
}, |
|||
}; |
|||
|
|||
Grid.SetRow(param, 0); |
|||
root.Children.Add(param); |
|||
Grid.SetRow(_stats, 1); |
|||
root.Children.Add(_stats); |
|||
Grid.SetRow(_scene, 2); |
|||
root.Children.Add(_scene); |
|||
|
|||
Content = root; |
|||
BuildScene(); |
|||
ResetState(); |
|||
} |
|||
|
|||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnAttachedToVisualTree(e); |
|||
ResetState(); |
|||
_compositor = ElementComposition.GetElementVisual(this)?.Compositor; |
|||
_isAttached = true; |
|||
RequestNextUpdate(); |
|||
} |
|||
|
|||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
_isAttached = false; |
|||
_updateQueued = false; |
|||
_compositor = null; |
|||
ResetState(); |
|||
base.OnDetachedFromVisualTree(e); |
|||
} |
|||
|
|||
private void BuildScene() |
|||
{ |
|||
var index = 0; |
|||
var groupSize = CellsPerGroupSide * CellStride; |
|||
|
|||
for (var groupY = 0; groupY < GroupRows; groupY++) |
|||
{ |
|||
for (var groupX = 0; groupX < GroupColumns; groupX++) |
|||
{ |
|||
var group = new Canvas |
|||
{ |
|||
Width = groupSize, |
|||
Height = groupSize, |
|||
Background = Brushes.Transparent |
|||
}; |
|||
Canvas.SetLeft(group, groupX * groupSize); |
|||
Canvas.SetTop(group, groupY * groupSize); |
|||
_scene.Children.Add(group); |
|||
|
|||
for (var y = 0; y < CellsPerGroupSide; y++) |
|||
{ |
|||
for (var x = 0; x < CellsPerGroupSide; x++) |
|||
{ |
|||
var cell = new Cell(index) |
|||
{ |
|||
Width = CellSize, |
|||
Height = CellSize, |
|||
Background = CreateBrush(index), |
|||
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative) |
|||
}; |
|||
cell.PointerPressed += OnCellPointerPressed; |
|||
|
|||
Canvas.SetLeft(cell, x * CellStride); |
|||
Canvas.SetTop(cell, y * CellStride); |
|||
group.Children.Add(cell); |
|||
_cells[index++] = cell; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void OnCompositionUpdate() |
|||
{ |
|||
_updateQueued = false; |
|||
|
|||
if (!_isAttached) |
|||
return; |
|||
|
|||
if (!_animationsStarted) |
|||
StartAnimations(); |
|||
|
|||
RunHitTests(); |
|||
|
|||
_updateCount++; |
|||
if (_stopwatch.Elapsed - _lastSecondTime >= TimeSpan.FromSeconds(1)) |
|||
UpdateStats(); |
|||
|
|||
RequestNextUpdate(); |
|||
} |
|||
|
|||
private void RequestNextUpdate() |
|||
{ |
|||
if (_updateQueued || _compositor == null) |
|||
return; |
|||
|
|||
_updateQueued = true; |
|||
_compositor.RequestCompositionUpdate(OnCompositionUpdate); |
|||
} |
|||
|
|||
private void StartAnimations() |
|||
{ |
|||
var started = 0; |
|||
var easing = new SineEaseInOut(); |
|||
|
|||
for (var i = 0; i < _cells.Length; i++) |
|||
{ |
|||
if (i % 5 != 0) |
|||
continue; |
|||
|
|||
var visual = ElementComposition.GetElementVisual(_cells[i]); |
|||
if (visual == null) |
|||
continue; |
|||
|
|||
var translation = visual.Compositor.CreateVector3KeyFrameAnimation(); |
|||
translation.Target = "Translation"; |
|||
translation.Duration = TimeSpan.FromMilliseconds(900 + (i % 700)); |
|||
translation.Direction = PlaybackDirection.Alternate; |
|||
translation.IterationBehavior = AnimationIterationBehavior.Forever; |
|||
translation.InsertKeyFrame(0f, new Vector3(0, 0, 0), easing); |
|||
translation.InsertKeyFrame(1f, GetAnimationOffset(i), easing); |
|||
visual.StartAnimation("Translation", translation); |
|||
|
|||
started++; |
|||
} |
|||
|
|||
_animationsStarted = started > 0; |
|||
} |
|||
|
|||
private void StopAnimations() |
|||
{ |
|||
for (var i = 0; i < _cells.Length; i++) |
|||
{ |
|||
var visual = ElementComposition.GetElementVisual(_cells[i]); |
|||
if (visual == null) |
|||
continue; |
|||
|
|||
visual.StopAnimation("Translation"); |
|||
visual.Translation = default; |
|||
} |
|||
|
|||
_animationsStarted = false; |
|||
} |
|||
|
|||
private void RunHitTests() |
|||
{ |
|||
var width = Math.Max(1, _scene.Bounds.Width); |
|||
var height = Math.Max(1, _scene.Bounds.Height); |
|||
var baseIndex = _updateCount * 37; |
|||
|
|||
for (var i = 0; i < _hitTestsPerFrame; i++) |
|||
{ |
|||
_hitTestCount++; |
|||
var sample = baseIndex + (i * 97); |
|||
var point = new Point(sample * 17 % width, sample * 29 % height); |
|||
var hit = _scene.GetVisualAt(point); |
|||
|
|||
if (hit is Cell cell) |
|||
{ |
|||
SetLastHitCell(cell); |
|||
_hitCount++; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static Vector3 GetAnimationOffset(int index) |
|||
{ |
|||
var x = index % 4 switch |
|||
{ |
|||
0 => -AnimationTravel, |
|||
1 => AnimationTravel, |
|||
2 => -AnimationTravel / 2, |
|||
_ => AnimationTravel / 2 |
|||
}; |
|||
var y = index / 4 % 4 switch |
|||
{ |
|||
0 => -AnimationTravel, |
|||
1 => AnimationTravel, |
|||
2 => AnimationTravel / 2, |
|||
_ => -AnimationTravel / 2 |
|||
}; |
|||
|
|||
return new Vector3(x, y, 0); |
|||
} |
|||
|
|||
private void ResetState() |
|||
{ |
|||
StopAnimations(); |
|||
|
|||
_lastClickedCell?.ClearHighlight(); |
|||
_lastHitCell = null; |
|||
_lastClickedCell = null; |
|||
|
|||
_updateCount = 0; |
|||
_hitTestCount = 0; |
|||
_hitCount = 0; |
|||
_lastSecondUpdateCount = 0; |
|||
_lastSecondHitTestCount = 0; |
|||
_lastSecondHitCount = 0; |
|||
_lastSecondTime = default; |
|||
_lastSecondUpdatesPerSecond = 0; |
|||
_lastSecondHitTestsPerSecond = 0; |
|||
_lastSecondHitsPerSecond = 0; |
|||
_clickCount = 0; |
|||
_stopwatch.Restart(); |
|||
UpdateStats(); |
|||
} |
|||
|
|||
private void UpdateStats() |
|||
{ |
|||
var elapsed = _stopwatch.Elapsed; |
|||
var seconds = Math.Max(0.001, (elapsed - _lastSecondTime).TotalSeconds); |
|||
_lastSecondUpdatesPerSecond = (_updateCount - _lastSecondUpdateCount) / seconds; |
|||
_lastSecondHitTestsPerSecond = (_hitTestCount - _lastSecondHitTestCount) / seconds; |
|||
_lastSecondHitsPerSecond = (_hitCount - _lastSecondHitCount) / seconds; |
|||
_lastSecondUpdateCount = _updateCount; |
|||
_lastSecondHitTestCount = _hitTestCount; |
|||
_lastSecondHitCount = _hitCount; |
|||
_lastSecondTime = elapsed; |
|||
|
|||
_stats.Text = |
|||
$"Visuals: {_cells.Length} ({_cells.Length / 5} animated), " + |
|||
$"Hit tests/frame: {_hitTestsPerFrame}, " + |
|||
$"Composition updates/s: {_lastSecondUpdatesPerSecond:F1}, Hit tests/s: {_lastSecondHitTestsPerSecond:F0}, " + |
|||
$"Hits/s: {_lastSecondHitsPerSecond:F0}, Misses/s: {_lastSecondHitTestsPerSecond - _lastSecondHitsPerSecond:F0}, " + |
|||
$"Clicks: {_clickCount}"; |
|||
} |
|||
|
|||
private void OnCellPointerPressed(object? sender, PointerPressedEventArgs e) |
|||
{ |
|||
if (sender is not Cell cell) |
|||
return; |
|||
|
|||
SetLastClickedCell(cell); |
|||
_clickCount++; |
|||
e.Handled = true; |
|||
} |
|||
|
|||
private void SetLastHitCell(Cell cell) |
|||
{ |
|||
if (ReferenceEquals(_lastHitCell, cell)) |
|||
return; |
|||
|
|||
_lastHitCell = cell; |
|||
} |
|||
|
|||
private void SetLastClickedCell(Cell cell) |
|||
{ |
|||
if (!ReferenceEquals(_lastClickedCell, cell) && _lastClickedCell != null) |
|||
{ |
|||
_lastClickedCell.IsLatestClick = false; |
|||
_lastClickedCell.UpdateHighlight(); |
|||
} |
|||
|
|||
_lastClickedCell = cell; |
|||
cell.IsLatestClick = true; |
|||
cell.UpdateHighlight(); |
|||
} |
|||
|
|||
private static IBrush CreateBrush(int index) |
|||
{ |
|||
var r = (byte)(80 + (index * 47 % 160)); |
|||
var g = (byte)(80 + (index * 91 % 160)); |
|||
var b = (byte)(80 + (index * 137 % 160)); |
|||
return new SolidColorBrush(Color.FromRgb(r, g, b)); |
|||
} |
|||
|
|||
private sealed class Cell : Border |
|||
{ |
|||
public Cell(int index) |
|||
{ |
|||
Index = index; |
|||
BorderThickness = new Thickness(1); |
|||
} |
|||
|
|||
public int Index { get; } |
|||
public bool IsLatestClick { get; set; } |
|||
|
|||
public void ClearHighlight() |
|||
{ |
|||
IsLatestClick = false; |
|||
UpdateHighlight(); |
|||
} |
|||
|
|||
public void UpdateHighlight() |
|||
{ |
|||
BorderBrush = IsLatestClick ? Brushes.White : Brushes.Transparent; |
|||
ZIndex = IsLatestClick ? 1 : 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,894 @@ |
|||
using System; |
|||
using System.Buffers; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Collections.Pooled; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
internal sealed class CompositionHitTestAabbTree |
|||
{ |
|||
private const int Null = -1; |
|||
private const int OrderBucketSize = 32; |
|||
private const double FatBoundsPadding = 1; |
|||
private static readonly CandidateComparer s_candidateComparer = new(); |
|||
|
|||
private readonly CompositionVisualCollection _children; |
|||
private readonly Dictionary<CompositionVisual, Entry> _entries = []; |
|||
private readonly List<Bucket> _buckets = []; |
|||
private readonly List<Node> _nodes = []; |
|||
private int _freeList = Null; |
|||
|
|||
public CompositionHitTestAabbTree(CompositionVisualCollection children) |
|||
{ |
|||
_children = children; |
|||
|
|||
for (var i = 0; i < children.Count; i++) |
|||
Update(children[i], i); |
|||
} |
|||
|
|||
public void Clear() |
|||
{ |
|||
_entries.Clear(); |
|||
_buckets.Clear(); |
|||
_nodes.Clear(); |
|||
_freeList = Null; |
|||
} |
|||
|
|||
public void Update(CompositionVisual visual, int order) |
|||
{ |
|||
ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_entries, visual, out var exists); |
|||
if (!exists) |
|||
{ |
|||
var bucketIndex = GetBucketIndex(order); |
|||
entry = new Entry(order); |
|||
GetOrCreateBucket(bucketIndex); |
|||
} |
|||
else if (entry.Order != order) |
|||
{ |
|||
UpdateOrderCore(visual, ref entry, order); |
|||
} |
|||
|
|||
var state = GetBoundsState(visual, out var bounds, out var revision); |
|||
UpdateBounds(visual, ref entry, state, bounds, revision); |
|||
} |
|||
|
|||
public void Remove(CompositionVisual visual) |
|||
{ |
|||
if (!_entries.Remove(visual, out var entry)) |
|||
return; |
|||
|
|||
if (entry.Leaf != Null) |
|||
DestroyLeaf(entry.Leaf); |
|||
else if (entry.IsUnbounded) |
|||
RemoveUnbounded(visual, entry.Order); |
|||
|
|||
RemoveBucketIfEmpty(GetBucketIndex(entry.Order)); |
|||
} |
|||
|
|||
public void UpdateOrder(CompositionVisual visual, int order) |
|||
{ |
|||
ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_entries, visual, out var exists); |
|||
if (!exists) |
|||
{ |
|||
entry = new Entry(order); |
|||
return; |
|||
} |
|||
|
|||
if (entry.Order != order) |
|||
UpdateOrderCore(visual, ref entry, order); |
|||
} |
|||
|
|||
private void UpdateOrderCore(CompositionVisual visual, ref Entry entry, int order) |
|||
{ |
|||
var oldOrder = entry.Order; |
|||
var oldBucket = GetBucketIndex(oldOrder); |
|||
var newBucket = GetBucketIndex(order); |
|||
entry.Order = order; |
|||
|
|||
if (entry.Leaf != Null) |
|||
{ |
|||
MoveLeaf(entry.Leaf, order); |
|||
return; |
|||
} |
|||
|
|||
if (entry.IsUnbounded) |
|||
{ |
|||
MoveUnbounded(visual, oldOrder, order); |
|||
return; |
|||
} |
|||
|
|||
if (oldBucket != newBucket) |
|||
RemoveBucketIfEmpty(oldBucket); |
|||
} |
|||
|
|||
public void Query(Point point, PooledList<CompositionVisual> results, ulong readbackRevision) |
|||
{ |
|||
var candidates = ArrayPool<Candidate>.Shared.Rent(OrderBucketSize); |
|||
var stack = ArrayPool<int>.Shared.Rent(16); |
|||
var candidateCount = 0; |
|||
|
|||
try |
|||
{ |
|||
for (var i = _buckets.Count - 1; i >= 0; i--) |
|||
{ |
|||
UpdateBucket(i, readbackRevision); |
|||
|
|||
var bucket = _buckets[i]; |
|||
if (bucket.Root == Null && bucket.Unbounded is not { Count: > 0 }) |
|||
continue; |
|||
|
|||
candidateCount = 0; |
|||
var stackCount = 0; |
|||
QueryBucket(bucket, point, ref candidates, ref candidateCount, ref stack, ref stackCount); |
|||
candidates.AsSpan(0, candidateCount).Sort(s_candidateComparer); |
|||
|
|||
for (var j = 0; j < candidateCount; j++) |
|||
results.Add(candidates[j].Visual); |
|||
|
|||
candidates.AsSpan(0, candidateCount).Clear(); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
candidates.AsSpan(0, candidateCount).Clear(); |
|||
ArrayPool<Candidate>.Shared.Return(candidates); |
|||
ArrayPool<int>.Shared.Return(stack); |
|||
} |
|||
} |
|||
|
|||
public CompositionVisual? QueryFirst(CompositionTarget target, Point point, Func<CompositionVisual, bool>? filter, Func<CompositionVisual, bool>? resultFilter, ulong readbackRevision) |
|||
{ |
|||
var candidates = ArrayPool<Candidate>.Shared.Rent(OrderBucketSize); |
|||
var stack = ArrayPool<int>.Shared.Rent(16); |
|||
var candidateCount = 0; |
|||
|
|||
try |
|||
{ |
|||
for (var i = _buckets.Count - 1; i >= 0; i--) |
|||
{ |
|||
UpdateBucket(i, readbackRevision); |
|||
|
|||
var bucket = _buckets[i]; |
|||
if (bucket.Root == Null && bucket.Unbounded is not { Count: > 0 }) |
|||
continue; |
|||
|
|||
candidateCount = 0; |
|||
var stackCount = 0; |
|||
QueryBucket(bucket, point, ref candidates, ref candidateCount, ref stack, ref stackCount); |
|||
candidates.AsSpan(0, candidateCount).Sort(s_candidateComparer); |
|||
|
|||
for (var j = 0; j < candidateCount; j++) |
|||
{ |
|||
var hit = target.HitTestFirstCore(candidates[j].Visual, point, filter, resultFilter); |
|||
if (hit != null) |
|||
return hit; |
|||
} |
|||
|
|||
candidates.AsSpan(0, candidateCount).Clear(); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
candidates.AsSpan(0, candidateCount).Clear(); |
|||
ArrayPool<Candidate>.Shared.Return(candidates); |
|||
ArrayPool<int>.Shared.Return(stack); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private void UpdateBucket(int bucketIndex, ulong readbackRevision) |
|||
{ |
|||
ref var bucket = ref GetRef(_buckets, bucketIndex); |
|||
if (bucket.ReadbackRevision == readbackRevision) |
|||
return; |
|||
|
|||
var end = Math.Min(_children.Count, (bucketIndex + 1) * OrderBucketSize); |
|||
for (var i = bucketIndex * OrderBucketSize; i < end; i++) |
|||
{ |
|||
var visual = _children[i]; |
|||
ref var entry = ref CollectionsMarshal.GetValueRefOrNullRef(_entries, visual); |
|||
|
|||
var state = GetBoundsState(visual, out var bounds, out var revision); |
|||
if (entry.Revision != revision) |
|||
UpdateBounds(visual, ref entry, state, bounds, revision); |
|||
} |
|||
|
|||
bucket.ReadbackRevision = readbackRevision; |
|||
} |
|||
|
|||
private void UpdateBounds(CompositionVisual visual, ref Entry entry, BoundsState state, LtrbRect bounds, ulong revision) |
|||
{ |
|||
entry.Revision = revision; |
|||
|
|||
if (entry.Leaf != Null) |
|||
{ |
|||
if (state == BoundsState.Bounded) |
|||
{ |
|||
UpdateLeaf(entry.Leaf, bounds, entry.Order); |
|||
} |
|||
else |
|||
{ |
|||
DestroyLeaf(entry.Leaf); |
|||
entry.Leaf = Null; |
|||
if (state == BoundsState.Unbounded) |
|||
AddUnbounded(visual, entry.Order, ref entry); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
if (entry.IsUnbounded) |
|||
{ |
|||
if (state == BoundsState.Unbounded) |
|||
return; |
|||
|
|||
RemoveUnbounded(visual, entry.Order); |
|||
entry.IsUnbounded = false; |
|||
|
|||
if (state == BoundsState.Bounded) |
|||
entry.Leaf = CreateLeaf(visual, bounds, entry.Order); |
|||
|
|||
return; |
|||
} |
|||
|
|||
if (state == BoundsState.Bounded) |
|||
entry.Leaf = CreateLeaf(visual, bounds, entry.Order); |
|||
else if (state == BoundsState.Unbounded) |
|||
AddUnbounded(visual, entry.Order, ref entry); |
|||
} |
|||
|
|||
private void QueryBucket(Bucket bucket, Point point, ref Candidate[] candidates, ref int candidateCount, ref int[] stack, ref int stackCount) |
|||
{ |
|||
PushQueryNode(ref stack, ref stackCount, bucket.Root); |
|||
|
|||
while (stackCount > 0) |
|||
{ |
|||
var nodeIndex = stack[--stackCount]; |
|||
var node = _nodes[nodeIndex]; |
|||
|
|||
if (!node.Bounds.Contains(point)) |
|||
continue; |
|||
|
|||
if (node.IsLeaf) |
|||
{ |
|||
if (node.Visual != null) |
|||
AddCandidate(ref candidates, ref candidateCount, new Candidate(node.Visual, node.Order)); |
|||
} |
|||
else |
|||
{ |
|||
PushQueryNode(ref stack, ref stackCount, node.Child1); |
|||
PushQueryNode(ref stack, ref stackCount, node.Child2); |
|||
} |
|||
} |
|||
|
|||
if (bucket.Unbounded != null) |
|||
foreach (var visual in bucket.Unbounded) |
|||
if (_entries.TryGetValue(visual, out var entry)) |
|||
AddCandidate(ref candidates, ref candidateCount, new Candidate(visual, entry.Order)); |
|||
} |
|||
|
|||
private static void AddCandidate(ref Candidate[] candidates, ref int count, Candidate candidate) |
|||
{ |
|||
if (count == candidates.Length) |
|||
Resize(ref candidates, count); |
|||
|
|||
candidates[count++] = candidate; |
|||
} |
|||
|
|||
private static void PushQueryNode(ref int[] stack, ref int count, int nodeIndex) |
|||
{ |
|||
if (nodeIndex == Null) |
|||
return; |
|||
|
|||
if (count == stack.Length) |
|||
Resize(ref stack, count); |
|||
|
|||
stack[count++] = nodeIndex; |
|||
} |
|||
|
|||
private static void Resize<T>(ref T[] buffer, int count) |
|||
{ |
|||
var resized = ArrayPool<T>.Shared.Rent(buffer.Length * 2); |
|||
buffer.AsSpan(0, count).CopyTo(resized); |
|||
ArrayPool<T>.Shared.Return(buffer, RuntimeHelpers.IsReferenceOrContainsReferences<T>()); |
|||
buffer = resized; |
|||
} |
|||
|
|||
private static int GetBucketIndex(int order) => order / OrderBucketSize; |
|||
|
|||
private ref Bucket GetOrCreateBucket(int bucketIndex) |
|||
{ |
|||
while (_buckets.Count <= bucketIndex) |
|||
_buckets.Add(new Bucket(Null)); |
|||
|
|||
return ref GetRef(_buckets, bucketIndex); |
|||
} |
|||
|
|||
private void RemoveBucketIfEmpty(int bucketIndex) |
|||
{ |
|||
if (bucketIndex >= _buckets.Count) |
|||
return; |
|||
|
|||
var lastRequiredBucketIndex = _children.Count == 0 ? Null : GetBucketIndex(_children.Count - 1); |
|||
bool removeTrailingBucket; |
|||
{ |
|||
ref var bucket = ref GetRef(_buckets, bucketIndex); |
|||
if (bucket.Unbounded?.Count == 0) |
|||
bucket.Unbounded = null; |
|||
|
|||
removeTrailingBucket = bucket.IsEmpty && |
|||
bucketIndex == _buckets.Count - 1 && |
|||
bucketIndex > lastRequiredBucketIndex; |
|||
} |
|||
|
|||
if (!removeTrailingBucket) |
|||
return; |
|||
|
|||
do |
|||
{ |
|||
_buckets.RemoveAt(_buckets.Count - 1); |
|||
} while (_buckets.Count > 0 && |
|||
_buckets.Count - 1 > lastRequiredBucketIndex && |
|||
_buckets[^1].IsEmpty); |
|||
} |
|||
|
|||
private static ref T GetRef<T>(List<T> items, int index) => ref CollectionsMarshal.AsSpan(items)[index]; |
|||
|
|||
private int CreateLeaf(CompositionVisual visual, LtrbRect bounds, int order) |
|||
{ |
|||
var leaf = AllocateNode(); |
|||
{ |
|||
ref var node = ref GetRef(_nodes, leaf); |
|||
node.Bounds = Fatten(bounds); |
|||
node.Visual = visual; |
|||
node.Order = order; |
|||
node.Bucket = GetBucketIndex(order); |
|||
node.Height = 0; |
|||
} |
|||
|
|||
InsertLeaf(leaf); |
|||
return leaf; |
|||
} |
|||
|
|||
private void DestroyLeaf(int leaf) |
|||
{ |
|||
RemoveLeaf(leaf); |
|||
FreeNode(leaf); |
|||
} |
|||
|
|||
private void UpdateLeaf(int leaf, LtrbRect bounds, int order) |
|||
{ |
|||
var bucket = GetBucketIndex(order); |
|||
|
|||
{ |
|||
ref var node = ref GetRef(_nodes, leaf); |
|||
|
|||
// If the exact bounds still fit inside the fat bounds, the tree shape can stay unchanged.
|
|||
if (node.Bucket == bucket && node.Bounds.Contains(bounds)) |
|||
{ |
|||
node.Order = order; |
|||
return; |
|||
} |
|||
} |
|||
|
|||
RemoveLeaf(leaf); |
|||
|
|||
{ |
|||
ref var removedNode = ref GetRef(_nodes, leaf); |
|||
removedNode.Bounds = Fatten(bounds); |
|||
removedNode.Bucket = bucket; |
|||
removedNode.Order = order; |
|||
} |
|||
|
|||
InsertLeaf(leaf); |
|||
} |
|||
|
|||
private void MoveLeaf(int leaf, int order) |
|||
{ |
|||
var bucket = GetBucketIndex(order); |
|||
|
|||
{ |
|||
ref var node = ref GetRef(_nodes, leaf); |
|||
if (node.Bucket == bucket) |
|||
{ |
|||
node.Order = order; |
|||
return; |
|||
} |
|||
} |
|||
|
|||
RemoveLeaf(leaf); |
|||
|
|||
{ |
|||
ref var removedNode = ref GetRef(_nodes, leaf); |
|||
removedNode.Order = order; |
|||
removedNode.Bucket = bucket; |
|||
} |
|||
|
|||
InsertLeaf(leaf); |
|||
} |
|||
|
|||
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; |
|||
ref var node = ref GetRef(_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; |
|||
node.Bucket = 0; |
|||
return index; |
|||
} |
|||
|
|||
private void FreeNode(int index) |
|||
{ |
|||
ref var node = ref GetRef(_nodes, index); |
|||
node.Next = _freeList; |
|||
node.Parent = Null; |
|||
node.Child1 = Null; |
|||
node.Child2 = Null; |
|||
node.Height = -1; |
|||
node.Visual = null; |
|||
node.Order = 0; |
|||
node.Bucket = 0; |
|||
_freeList = index; |
|||
} |
|||
|
|||
private void InsertLeaf(int leaf) |
|||
{ |
|||
var bucketIndex = GetRef(_nodes, leaf).Bucket; |
|||
ref var bucket = ref GetOrCreateBucket(bucketIndex); |
|||
|
|||
if (bucket.Root == Null) |
|||
{ |
|||
bucket.Root = leaf; |
|||
ref var root = ref GetRef(_nodes, leaf); |
|||
root.Parent = Null; |
|||
return; |
|||
} |
|||
|
|||
var leafBounds = GetRef(_nodes, leaf).Bounds; |
|||
var sibling = FindBestSibling(bucket.Root, leafBounds); |
|||
var oldParent = GetRef(_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
|
|||
ref var parentNode = ref GetRef(_nodes, newParent); |
|||
parentNode.Parent = oldParent; |
|||
parentNode.Bounds = leafBounds.Union(GetRef(_nodes, sibling).Bounds); |
|||
parentNode.Height = GetRef(_nodes, sibling).Height + 1; |
|||
parentNode.Child1 = sibling; |
|||
parentNode.Child2 = leaf; |
|||
parentNode.Visual = null; |
|||
parentNode.Bucket = bucketIndex; |
|||
|
|||
ref var siblingNode = ref GetRef(_nodes, sibling); |
|||
siblingNode.Parent = newParent; |
|||
|
|||
ref var leafNode = ref GetRef(_nodes, leaf); |
|||
leafNode.Parent = newParent; |
|||
|
|||
if (oldParent == Null) |
|||
{ |
|||
bucket.Root = newParent; |
|||
} |
|||
else |
|||
{ |
|||
ref var oldParentNode = ref GetRef(_nodes, oldParent); |
|||
if (oldParentNode.Child1 == sibling) |
|||
oldParentNode.Child1 = newParent; |
|||
else |
|||
oldParentNode.Child2 = newParent; |
|||
} |
|||
|
|||
FixAncestors(newParent); |
|||
} |
|||
|
|||
private int FindBestSibling(int root, 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); |
|||
|
|||
// Stop descending when pairing with this internal node is already cheaper.
|
|||
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) |
|||
{ |
|||
var bucketIndex = GetRef(_nodes, leaf).Bucket; |
|||
var removedRoot = false; |
|||
{ |
|||
ref var bucket = ref GetRef(_buckets, bucketIndex); |
|||
if (leaf == bucket.Root) |
|||
{ |
|||
bucket.Root = Null; |
|||
removedRoot = true; |
|||
} |
|||
} |
|||
|
|||
if (removedRoot) |
|||
{ |
|||
RemoveBucketIfEmpty(bucketIndex); |
|||
return; |
|||
} |
|||
|
|||
var parent = GetRef(_nodes, leaf).Parent; |
|||
var parentNode = GetRef(_nodes, parent); |
|||
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
|
|||
ref var grandParentNode = ref GetRef(_nodes, grandParent); |
|||
if (grandParentNode.Child1 == parent) |
|||
grandParentNode.Child1 = sibling; |
|||
else |
|||
grandParentNode.Child2 = sibling; |
|||
|
|||
ref var siblingNode = ref GetRef(_nodes, sibling); |
|||
siblingNode.Parent = grandParent; |
|||
|
|||
FreeNode(parent); |
|||
FixAncestors(grandParent); |
|||
} |
|||
else |
|||
{ |
|||
// If the parent was the root, the sibling becomes the new root.
|
|||
//
|
|||
// Before: parent(root) After: sibling(root)
|
|||
// / \
|
|||
// leaf sibling
|
|||
{ |
|||
ref var bucket = ref GetRef(_buckets, bucketIndex); |
|||
bucket.Root = sibling; |
|||
} |
|||
|
|||
ref var siblingNode = ref GetRef(_nodes, sibling); |
|||
siblingNode.Parent = Null; |
|||
FreeNode(parent); |
|||
} |
|||
|
|||
{ |
|||
ref var leafNode = ref GetRef(_nodes, leaf); |
|||
leafNode.Parent = Null; |
|||
} |
|||
|
|||
RemoveBucketIfEmpty(bucketIndex); |
|||
} |
|||
|
|||
private void FixAncestors(int index) |
|||
{ |
|||
while (index != Null) |
|||
{ |
|||
index = Balance(index); |
|||
|
|||
ref var node = ref GetRef(_nodes, index); |
|||
var child1 = GetRef(_nodes, node.Child1); |
|||
var child2 = GetRef(_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); |
|||
|
|||
index = node.Parent; |
|||
} |
|||
} |
|||
|
|||
private int Balance(int indexA) |
|||
{ |
|||
var a = GetRef(_nodes, indexA); |
|||
if (a.IsLeaf || a.Height < 2) |
|||
return indexA; |
|||
|
|||
var indexB = a.Child1; |
|||
var indexC = a.Child2; |
|||
var b = GetRef(_nodes, indexB); |
|||
var c = GetRef(_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); |
|||
|
|||
return indexA; |
|||
} |
|||
|
|||
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
|
|||
ref var a = ref GetRef(_nodes, indexA); |
|||
ref var c = ref GetRef(_nodes, indexC); |
|||
var indexF = c.Child1; |
|||
var indexG = c.Child2; |
|||
ref var f = ref GetRef(_nodes, indexF); |
|||
ref var g = ref GetRef(_nodes, indexG); |
|||
|
|||
c.Child1 = indexA; |
|||
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; |
|||
a.Child2 = indexG; |
|||
g.Parent = indexA; |
|||
a.Bounds = GetRef(_nodes, indexB).Bounds.Union(g.Bounds); |
|||
c.Bounds = a.Bounds.Union(f.Bounds); |
|||
a.Height = 1 + Math.Max(GetRef(_nodes, indexB).Height, g.Height); |
|||
c.Height = 1 + Math.Max(a.Height, f.Height); |
|||
} |
|||
else |
|||
{ |
|||
c.Child2 = indexG; |
|||
a.Child2 = indexF; |
|||
f.Parent = indexA; |
|||
a.Bounds = GetRef(_nodes, indexB).Bounds.Union(f.Bounds); |
|||
c.Bounds = a.Bounds.Union(g.Bounds); |
|||
a.Height = 1 + Math.Max(GetRef(_nodes, indexB).Height, f.Height); |
|||
c.Height = 1 + Math.Max(a.Height, g.Height); |
|||
} |
|||
|
|||
return indexC; |
|||
} |
|||
|
|||
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
|
|||
ref var a = ref GetRef(_nodes, indexA); |
|||
ref var b = ref GetRef(_nodes, indexB); |
|||
var indexD = b.Child1; |
|||
var indexE = b.Child2; |
|||
ref var d = ref GetRef(_nodes, indexD); |
|||
ref var e = ref GetRef(_nodes, indexE); |
|||
|
|||
b.Child1 = indexA; |
|||
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; |
|||
a.Child1 = indexE; |
|||
e.Parent = indexA; |
|||
a.Bounds = GetRef(_nodes, indexC).Bounds.Union(e.Bounds); |
|||
b.Bounds = a.Bounds.Union(d.Bounds); |
|||
a.Height = 1 + Math.Max(GetRef(_nodes, indexC).Height, e.Height); |
|||
b.Height = 1 + Math.Max(a.Height, d.Height); |
|||
} |
|||
else |
|||
{ |
|||
b.Child2 = indexE; |
|||
a.Child1 = indexD; |
|||
d.Parent = indexA; |
|||
a.Bounds = GetRef(_nodes, indexC).Bounds.Union(d.Bounds); |
|||
b.Bounds = a.Bounds.Union(e.Bounds); |
|||
a.Height = 1 + Math.Max(GetRef(_nodes, indexC).Height, d.Height); |
|||
b.Height = 1 + Math.Max(a.Height, e.Height); |
|||
} |
|||
|
|||
return indexB; |
|||
} |
|||
|
|||
private void ReplaceParentChild(int oldChild, int newChild, int parent) |
|||
{ |
|||
if (parent == Null) |
|||
{ |
|||
ref var bucket = ref GetRef(_buckets, GetRef(_nodes, newChild).Bucket); |
|||
bucket.Root = newChild; |
|||
return; |
|||
} |
|||
|
|||
ref var parentNode = ref GetRef(_nodes, parent); |
|||
if (parentNode.Child1 == oldChild) |
|||
parentNode.Child1 = newChild; |
|||
else |
|||
parentNode.Child2 = newChild; |
|||
} |
|||
|
|||
private void AddUnbounded(CompositionVisual visual, int order, ref Entry entry) |
|||
{ |
|||
var bucketIndex = GetBucketIndex(order); |
|||
ref var bucket = ref GetOrCreateBucket(bucketIndex); |
|||
(bucket.Unbounded ??= []).Add(visual); |
|||
entry.IsUnbounded = true; |
|||
} |
|||
|
|||
private void MoveUnbounded(CompositionVisual visual, int oldOrder, int order) |
|||
{ |
|||
var oldBucketIndex = GetBucketIndex(oldOrder); |
|||
var newBucketIndex = GetBucketIndex(order); |
|||
|
|||
if (oldBucketIndex == newBucketIndex) |
|||
return; |
|||
|
|||
RemoveUnbounded(visual, oldOrder); |
|||
ref var newBucket = ref GetOrCreateBucket(newBucketIndex); |
|||
(newBucket.Unbounded ??= []).Add(visual); |
|||
} |
|||
|
|||
private void RemoveUnbounded(CompositionVisual visual, int order) |
|||
{ |
|||
var bucketIndex = GetBucketIndex(order); |
|||
{ |
|||
ref var bucket = ref GetRef(_buckets, bucketIndex); |
|||
bucket.Unbounded!.Remove(visual); |
|||
} |
|||
|
|||
RemoveBucketIfEmpty(bucketIndex); |
|||
} |
|||
|
|||
private static BoundsState GetBoundsState(CompositionVisual visual, out LtrbRect bounds, out ulong revision) |
|||
{ |
|||
bounds = default; |
|||
revision = 0; |
|||
|
|||
var readback = visual.TryGetValidReadback(); |
|||
if (readback == null) |
|||
return BoundsState.Empty; |
|||
revision = readback.Revision; |
|||
|
|||
if (visual.DisableSubTreeBoundsHitTestOptimization) |
|||
return BoundsState.Unbounded; |
|||
|
|||
if (readback.TransformedSubtreeBounds is not { } subtreeBounds || subtreeBounds.IsZeroSize) |
|||
return BoundsState.Empty; |
|||
|
|||
bounds = subtreeBounds; |
|||
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, |
|||
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 int Bucket; |
|||
|
|||
public readonly bool IsLeaf => Child1 == Null; |
|||
} |
|||
|
|||
private struct Bucket(int root) |
|||
{ |
|||
public int Root = root; |
|||
public List<CompositionVisual>? Unbounded = null; |
|||
public ulong ReadbackRevision; |
|||
|
|||
public readonly bool IsEmpty => Root == Null && (Unbounded == null || Unbounded.Count == 0); |
|||
} |
|||
|
|||
private struct Entry(int order) |
|||
{ |
|||
public int Order = order; |
|||
public int Leaf = Null; |
|||
public ulong Revision; |
|||
public bool IsUnbounded; |
|||
} |
|||
|
|||
private readonly struct Candidate(CompositionVisual visual, int order) |
|||
{ |
|||
public CompositionVisual Visual { get; } = visual; |
|||
public int Order { get; } = order; |
|||
} |
|||
|
|||
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); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue