-
Notifications
You must be signed in to change notification settings - Fork 2k
Cache Brushes #36405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cache Brushes #36405
Changes from all commits
9f8df77
e6a017e
62f7add
4cde161
ff4b349
1622d0c
96f7a90
77b3c48
1c9c308
d98ec21
201e243
cbb9bfd
ed00d57
17519be
0d562df
317b1a9
e351aa4
42877ed
3c80b8a
0198100
06aacbf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| #nullable disable | ||
| using Microsoft.Maui.Controls.Internals; | ||
| using Microsoft.Maui.Graphics; | ||
| using GraphicsGradientStop = Microsoft.Maui.Graphics.PaintGradientStop; | ||
|
|
||
|
|
@@ -11,10 +12,16 @@ namespace Microsoft.Maui.Controls | |
| [System.ComponentModel.TypeConverter(typeof(BrushTypeConverter))] | ||
| public abstract partial class Brush : Element | ||
| { | ||
| static readonly ICache<Color, ImmutableBrush> _cache = new Lru64ColorVectorInlineBrushCache(64); | ||
|
|
||
| public static implicit operator Brush(Paint paint) | ||
| { | ||
| if (paint is SolidPaint solidPaint) | ||
| return new SolidColorBrush { Color = solidPaint.Color }; | ||
| { | ||
| var color = solidPaint.Color; | ||
| return color is null ? Default : _cache.Get(color); | ||
| } | ||
|
pictos marked this conversation as resolved.
Comment on lines
19
to
+23
Comment on lines
19
to
+23
Comment on lines
19
to
+23
|
||
|
|
||
|
|
||
| if (paint is GradientPaint gradientPaint) | ||
| { | ||
|
|
@@ -100,7 +107,7 @@ public static implicit operator Paint(Brush brush) | |
| /// </summary> | ||
| public static Brush Default => defaultBrush ??= new(null); | ||
|
|
||
| public static implicit operator Brush(Color color) => new SolidColorBrush(color); | ||
| public static implicit operator Brush(Color color) => color is null ? Default : _cache.Get(color); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[major] Public behavior change — The implicit |
||
|
|
||
|
pictos marked this conversation as resolved.
|
||
| /// <summary> | ||
| /// When overridden in a derived class, indicates whether the given brush represents the empty brush. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| namespace Microsoft.Maui.Controls.Internals; | ||
|
|
||
| interface ICache<TKey, TValue> | ||
| { | ||
| TValue Get(TKey key); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,299 @@ | ||
| #nullable disable | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it better to avoid this for new code? |
||
|
|
||
| // This fixed-capacity (max 64) LRU building block relies on [InlineArray] and | ||
| // Vector<T> span APIs that are only available on .NET 8+. It is intentionally | ||
| // excluded from the netstandard2.0/2.1 targets of Controls.Core. | ||
| #if !NETSTANDARD | ||
|
|
||
| using System; | ||
| using System.Numerics; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Runtime.InteropServices; | ||
| using Microsoft.Maui.Graphics; | ||
|
|
||
| namespace Microsoft.Maui.Controls.Internals; | ||
|
|
||
| internal interface ILru64LinkStore | ||
| { | ||
| byte GetPrevious(byte slot, byte head); | ||
| byte GetNext(byte slot, byte tail); | ||
| void SetPrevious(byte slot, byte previous); | ||
| void SetNext(byte slot, byte next); | ||
| } | ||
|
|
||
| internal struct InlineArrayLinkStore : ILru64LinkStore | ||
| { | ||
| PreviousBuffer _previous; | ||
| NextBuffer _next; | ||
|
|
||
| public byte GetPrevious(byte slot, byte head) => slot == head ? Lru64Constants.None : _previous[slot]; | ||
| public byte GetNext(byte slot, byte tail) => slot == tail ? Lru64Constants.None : _next[slot]; | ||
| public void SetPrevious(byte slot, byte previous) => _previous[slot] = previous; | ||
| public void SetNext(byte slot, byte next) => _next[slot] = next; | ||
|
|
||
| [InlineArray(Lru64Constants.MaxCapacity)] | ||
| struct PreviousBuffer | ||
| { | ||
| byte _element0; | ||
| } | ||
|
|
||
| [InlineArray(Lru64Constants.MaxCapacity)] | ||
| struct NextBuffer | ||
| { | ||
| byte _element0; | ||
| } | ||
| } | ||
|
|
||
| internal static class Lru64Constants | ||
| { | ||
| public const int MaxCapacity = 64; | ||
| public const byte None = byte.MaxValue; | ||
|
|
||
| public static byte ValidateCapacity(int capacity) | ||
| { | ||
| if ((uint)(capacity - 1) >= (uint)MaxCapacity) | ||
| { | ||
| throw new ArgumentOutOfRangeException(nameof(capacity), "capacity must be between 1 and 64"); | ||
| } | ||
|
|
||
| return (byte)capacity; | ||
| } | ||
| } | ||
|
|
||
| internal struct Lru64ColorVectorInline<TValue> | ||
| { | ||
| Lru64ColorVector<TValue, InlineArrayLinkStore> _cache; | ||
|
|
||
| public Lru64ColorVectorInline(int capacity) => _cache = new(capacity); | ||
| public TValue GetOrAdd(Color key, Func<Color, TValue> factory) => _cache.GetOrAdd(key, factory); | ||
| public int Count => _cache.Count; | ||
| public bool ContainsKey(Color key) => _cache.ContainsKey(key); | ||
| internal void AssertInvariants() => _cache.AssertInvariants(); | ||
| } | ||
|
|
||
| internal struct Lru64ColorVector<TValue, TLinks> | ||
| where TLinks : struct, ILru64LinkStore | ||
| { | ||
| readonly byte _capacity; | ||
| byte _count; | ||
| byte _head; | ||
| byte _tail; | ||
| UIntKeyBuffer _keys; | ||
| ValueBuffer _values; | ||
| TLinks _links; | ||
|
|
||
| public Lru64ColorVector(int capacity) | ||
| { | ||
| _capacity = Lru64Constants.ValidateCapacity(capacity); | ||
| _count = 0; | ||
| _head = Lru64Constants.None; | ||
| _tail = Lru64Constants.None; | ||
| _keys = default; | ||
| _values = default; | ||
| _links = default; | ||
| } | ||
|
|
||
| public int Count => _count; | ||
|
|
||
| public TValue GetOrAdd(Color key, Func<Color, TValue> factory) | ||
| { | ||
| var keyValue = key.ToUint(); | ||
| var slot = FindSlot(keyValue); | ||
| if (slot != Lru64Constants.None) | ||
| { | ||
| MoveToHead(slot); | ||
| return _values[slot]; | ||
| } | ||
|
|
||
| slot = GetSlotForInsert(); | ||
| _keys[slot] = keyValue; | ||
| _values[slot] = factory(key); | ||
| InsertAtHead(slot); | ||
| return _values[slot]; | ||
| } | ||
|
|
||
| public bool ContainsKey(Color key) => FindSlot(key.ToUint()) != Lru64Constants.None; | ||
|
|
||
| byte FindSlot(uint key) | ||
| { | ||
| var count = _count; | ||
| if (count == 0) | ||
| { | ||
| return Lru64Constants.None; | ||
| } | ||
|
|
||
| ref var first = ref _keys[0]; | ||
| var keys = MemoryMarshal.CreateReadOnlySpan(ref first, count); | ||
| var target = new Vector<uint>(key); | ||
| var vectorWidth = Vector<uint>.Count; | ||
| var index = 0; | ||
|
|
||
| for (; index <= count - vectorWidth; index += vectorWidth) | ||
| { | ||
| var matches = Vector.Equals(new Vector<uint>(keys.Slice(index, vectorWidth)), target); | ||
|
|
||
| if (!Vector.EqualsAll(matches, Vector<uint>.Zero)) | ||
| { | ||
| for (var lane = 0; lane < vectorWidth; lane++) | ||
| { | ||
| if (matches[lane] != 0) | ||
| { | ||
| return (byte)(index + lane); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for (; index < count; index++) | ||
| { | ||
| if (keys[index] == key) | ||
| { | ||
| return (byte)index; | ||
| } | ||
| } | ||
|
|
||
| return Lru64Constants.None; | ||
| } | ||
|
|
||
| byte GetSlotForInsert() | ||
| { | ||
| if (_count < _capacity) | ||
| { | ||
| return _count++; | ||
| } | ||
|
|
||
| var slot = _tail; | ||
| Detach(slot); | ||
| return slot; | ||
| } | ||
|
|
||
| void MoveToHead(byte slot) | ||
| { | ||
| if (slot == _head) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| Detach(slot); | ||
| InsertAtHead(slot); | ||
| } | ||
|
|
||
| void Detach(byte slot) | ||
| { | ||
| var previous = _links.GetPrevious(slot, _head); | ||
| var next = _links.GetNext(slot, _tail); | ||
|
|
||
| if (previous != Lru64Constants.None) | ||
| { | ||
| _links.SetNext(previous, next); | ||
| } | ||
| else | ||
| { | ||
| _head = next; | ||
| } | ||
|
|
||
| if (next != Lru64Constants.None) | ||
| { | ||
| _links.SetPrevious(next, previous); | ||
| } | ||
| else | ||
| { | ||
| _tail = previous; | ||
| } | ||
| } | ||
|
|
||
| void InsertAtHead(byte slot) | ||
| { | ||
| var oldHead = _head; | ||
| _links.SetPrevious(slot, Lru64Constants.None); | ||
| _links.SetNext(slot, oldHead); | ||
| _head = slot; | ||
|
|
||
| if (oldHead != Lru64Constants.None) | ||
| { | ||
| _links.SetPrevious(oldHead, slot); | ||
| } | ||
| else | ||
| { | ||
| _tail = slot; | ||
| } | ||
| } | ||
|
|
||
| internal void AssertInvariants() => Lru64InvariantHelpers.AssertInvariants(_count, _capacity, _head, _tail, ref _links); | ||
|
|
||
| [InlineArray(Lru64Constants.MaxCapacity)] | ||
| struct UIntKeyBuffer | ||
| { | ||
| uint _element0; | ||
| } | ||
|
|
||
| [InlineArray(Lru64Constants.MaxCapacity)] | ||
| struct ValueBuffer | ||
| { | ||
| TValue _element0; | ||
| } | ||
| } | ||
|
|
||
| internal static class Lru64InvariantHelpers | ||
| { | ||
| public static void AssertInvariants<TLinks>(byte count, byte capacity, byte head, byte tail, ref TLinks links) | ||
| where TLinks : struct, ILru64LinkStore | ||
| { | ||
| if (count == 0) | ||
| { | ||
| if (head != Lru64Constants.None || tail != Lru64Constants.None) | ||
| { | ||
| throw new InvalidOperationException("Empty cache should not have head or tail."); | ||
| } | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (head == Lru64Constants.None || tail == Lru64Constants.None) | ||
| { | ||
| throw new InvalidOperationException("Non-empty cache must have head and tail."); | ||
| } | ||
|
|
||
| var visited = 0UL; | ||
| var visitedCount = 0; | ||
| var slot = head; | ||
| var previous = Lru64Constants.None; | ||
|
|
||
| while (slot != Lru64Constants.None) | ||
| { | ||
| var mask = 1UL << slot; | ||
| if ((visited & mask) != 0) | ||
| { | ||
| throw new InvalidOperationException("Active list contains a cycle."); | ||
| } | ||
|
|
||
| visited |= mask; | ||
| visitedCount++; | ||
|
|
||
| if (links.GetPrevious(slot, head) != previous) | ||
| { | ||
| throw new InvalidOperationException("Previous link is inconsistent."); | ||
| } | ||
|
|
||
| previous = slot; | ||
| slot = links.GetNext(slot, tail); | ||
| } | ||
|
|
||
| if (previous != tail) | ||
| { | ||
| throw new InvalidOperationException("Tail is not the last active node."); | ||
| } | ||
|
|
||
| if (visitedCount != count) | ||
| { | ||
| throw new InvalidOperationException("Active list length does not match count."); | ||
| } | ||
|
|
||
| if (count > capacity) | ||
| { | ||
| throw new InvalidOperationException("Count exceeds capacity."); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #endif | ||
Uh oh!
There was an error while loading. Please reload this page.