فهرست منبع

Merge pull request #10013 from MrJul/textlayout-allocs

Reduced text layout allocations
Benedikt Stebner 2 سال پیش
والد
کامیت
53bc1ff2d6
49فایلهای تغییر یافته به همراه1569 افزوده شده و 1273 حذف شده
  1. 206 312
      src/Avalonia.Base/Media/GlyphRun.cs
  2. 268 0
      src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
  3. 84 0
      src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs
  4. 135 0
      src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs
  5. 36 0
      src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs
  6. 3 2
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  7. 30 200
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  8. 5 19
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  9. 11 0
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  10. 4 7
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  11. 2 4
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  12. 29 28
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  13. 137 85
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  14. 42 30
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  15. 62 42
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  16. 32 262
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  17. 3 0
      src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs
  18. 3 4
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  19. 3 5
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  20. 67 33
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  21. 38 15
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  22. 4 5
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  23. 1 1
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  24. 49 17
      src/Avalonia.Base/Utilities/ArrayBuilder.cs
  25. 1 10
      src/Avalonia.Base/Utilities/ArraySlice.cs
  26. 13 16
      src/Avalonia.Base/Utilities/BidiDictionary.cs
  27. 1 2
      src/Avalonia.Base/Utilities/BinarySearchExtension.cs
  28. 1 1
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  29. 2 1
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  30. 18 65
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  31. 2 0
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  32. 11 31
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  33. 2 0
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  34. 9 5
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  35. 2 4
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs
  36. 151 0
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs
  37. 2 1
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  38. 2 1
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  39. 14 3
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
  40. 8 2
      tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
  41. 9 12
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  42. 7 3
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  43. 31 24
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  44. 16 14
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  45. 5 5
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
  46. 2 0
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  47. 2 1
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  48. 4 1
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs
  49. BIN
      tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

+ 206 - 312
src/Avalonia.Base/Media/GlyphRun.cs

@@ -1,5 +1,7 @@
 using System;
 using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Platform;
 using Avalonia.Utilities;
@@ -11,64 +13,112 @@ namespace Avalonia.Media
     /// </summary>
     public sealed class GlyphRun : IDisposable
     {
-        private static readonly IComparer<int> s_ascendingComparer = Comparer<int>.Default;
-        private static readonly IComparer<int> s_descendingComparer = new ReverseComparer<int>();
-
         private IGlyphRunImpl? _glyphRunImpl;
-        private IGlyphTypeface _glyphTypeface;
         private double _fontRenderingEmSize;
         private int _biDiLevel;
         private Point? _baselineOrigin;
         private GlyphRunMetrics? _glyphRunMetrics;
-
         private ReadOnlyMemory<char> _characters;
-        private IReadOnlyList<ushort> _glyphIndices;
-        private IReadOnlyList<double>? _glyphAdvances;
-        private IReadOnlyList<Vector>? _glyphOffsets;
-        private IReadOnlyList<int>? _glyphClusters;
+        private IReadOnlyList<GlyphInfo> _glyphInfos;
+        private bool _hasOneCharPerCluster; // if true, character index and cluster are similar
 
         /// <summary>
-        ///     Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
+        /// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
         /// </summary>
         /// <param name="glyphTypeface">The glyph typeface.</param>
         /// <param name="fontRenderingEmSize">The rendering em size.</param>
-        /// <param name="glyphIndices">The glyph indices.</param>
-        /// <param name="glyphAdvances">The glyph advances.</param>
-        /// <param name="glyphOffsets">The glyph offsets.</param>
         /// <param name="characters">The characters.</param>
-        /// <param name="glyphClusters">The glyph clusters.</param>
+        /// <param name="glyphIndices">The glyph indices.</param>
         /// <param name="biDiLevel">The bidi level.</param>
         public GlyphRun(
             IGlyphTypeface glyphTypeface,
             double fontRenderingEmSize,
             ReadOnlyMemory<char> characters,
             IReadOnlyList<ushort> glyphIndices,
-            IReadOnlyList<double>? glyphAdvances = null,
-            IReadOnlyList<Vector>? glyphOffsets = null,
-            IReadOnlyList<int>? glyphClusters = null,
+            int biDiLevel = 0)
+            : this(glyphTypeface, fontRenderingEmSize, characters,
+                CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel)
+        {
+            _hasOneCharPerCluster = true;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
+        /// </summary>
+        /// <param name="glyphTypeface">The glyph typeface.</param>
+        /// <param name="fontRenderingEmSize">The rendering em size.</param>
+        /// <param name="characters">The characters.</param>
+        /// <param name="glyphInfos">The list of glyphs used.</param>
+        /// <param name="biDiLevel">The bidi level.</param>
+        public GlyphRun(
+            IGlyphTypeface glyphTypeface,
+            double fontRenderingEmSize,
+            ReadOnlyMemory<char> characters,
+            IReadOnlyList<GlyphInfo> glyphInfos,
             int biDiLevel = 0)
         {
-            _glyphTypeface = glyphTypeface;
+            GlyphTypeface = glyphTypeface;
 
             _fontRenderingEmSize = fontRenderingEmSize;
 
             _characters = characters;
 
-            _glyphIndices = glyphIndices;
+            _glyphInfos = glyphInfos;
 
-            _glyphAdvances = glyphAdvances;
+            _biDiLevel = biDiLevel;
+        }
 
-            _glyphOffsets = glyphOffsets;
+        private static IReadOnlyList<GlyphInfo> CreateGlyphInfos(IReadOnlyList<ushort> glyphIndices,
+            double fontRenderingEmSize, IGlyphTypeface glyphTypeface)
+        {
+            var glyphIndexSpan = ListToSpan(glyphIndices);
+            var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan);
 
-            _glyphClusters = glyphClusters;
+            var glyphInfos = new GlyphInfo[glyphIndexSpan.Length];
+            var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
 
-            _biDiLevel = biDiLevel;
+            for (var i = 0; i < glyphIndexSpan.Length; ++i)
+            {
+                glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale);
+            }
+
+            return glyphInfos;
+        }
+
+        private static ReadOnlySpan<ushort> ListToSpan(IReadOnlyList<ushort> list)
+        {
+            var count = list.Count;
+
+            if (count == 0)
+            {
+                return default;
+            }
+
+            if (list is ushort[] array)
+            {
+                return array.AsSpan();
+            }
+
+#if NET6_0_OR_GREATER
+            if (list is List<ushort> concreteList)
+            {
+                return CollectionsMarshal.AsSpan(concreteList);
+            }
+#endif
+
+            array = new ushort[count];
+            for (var i = 0; i < count; ++i)
+            {
+                array[i] = list[i];
+            }
+
+            return array.AsSpan();
         }
 
         /// <summary>
         ///     Gets the <see cref="IGlyphTypeface"/> for the <see cref="GlyphRun"/>.
         /// </summary>
-        public IGlyphTypeface GlyphTypeface => _glyphTypeface;
+        public IGlyphTypeface GlyphTypeface { get; }
 
         /// <summary>
         ///     Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
@@ -88,56 +138,17 @@ namespace Avalonia.Media
         /// 
         /// </summary>
         public GlyphRunMetrics Metrics
-        {
-            get
-            {
-                _glyphRunMetrics ??= CreateGlyphRunMetrics();
-
-                return _glyphRunMetrics.Value;
-            }
-        }
+            => _glyphRunMetrics ??= CreateGlyphRunMetrics();
 
         /// <summary>
         ///     Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
         /// </summary>
         public Point BaselineOrigin
         {
-            get
-            {
-                _baselineOrigin ??= CalculateBaselineOrigin();
-
-                return _baselineOrigin.Value;
-            }
+            get => _baselineOrigin ??= CalculateBaselineOrigin();
             set => Set(ref _baselineOrigin, value);
         }
 
-        /// <summary>
-        ///     Gets or sets an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font.
-        /// </summary>
-        public IReadOnlyList<ushort> GlyphIndices
-        {
-            get => _glyphIndices;
-            set => Set(ref _glyphIndices, value);
-        }
-
-        /// <summary>
-        ///     Gets or sets an array of <see cref="double"/> values that represent the advances corresponding to the glyph indices.
-        /// </summary>
-        public IReadOnlyList<double>? GlyphAdvances
-        {
-            get => _glyphAdvances;
-            set => Set(ref _glyphAdvances, value);
-        }
-
-        /// <summary>
-        ///     Gets or sets an array of <see cref="Vector"/> values representing the offsets of the glyphs in the <see cref="GlyphRun"/>.
-        /// </summary>
-        public IReadOnlyList<Vector>? GlyphOffsets
-        {
-            get => _glyphOffsets;
-            set => Set(ref _glyphOffsets, value);
-        }
-
         /// <summary>
         ///     Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
         /// </summary>
@@ -148,12 +159,16 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        ///     Gets or sets a list of <see cref="int"/> values representing a mapping from character index to glyph index.
+        /// Gets or sets the list of glyphs to use to render this run.
         /// </summary>
-        public IReadOnlyList<int>? GlyphClusters
+        public IReadOnlyList<GlyphInfo> GlyphInfos
         {
-            get => _glyphClusters;
-            set => Set(ref _glyphClusters, value);
+            get => _glyphInfos;
+            set
+            {
+                Set(ref _glyphInfos, value);
+                _hasOneCharPerCluster = false;
+            }
         }
 
         /// <summary>
@@ -179,17 +194,7 @@ namespace Avalonia.Media
         /// The platform implementation of the <see cref="GlyphRun"/>.
         /// </summary>
         public IGlyphRunImpl GlyphRunImpl
-        {
-            get
-            {
-                if (_glyphRunImpl == null)
-                {
-                    Initialize();
-                }
-
-                return _glyphRunImpl!;
-            }
-        }
+            => _glyphRunImpl ??= CreateGlyphRunImpl();
 
         /// <summary>
         /// Obtains geometry for the glyph run.
@@ -221,38 +226,32 @@ namespace Avalonia.Media
 
             if (IsLeftToRight)
             {
-                if (GlyphClusters != null)
+                if (characterIndex < Metrics.FirstCluster)
                 {
-                    if (characterIndex < Metrics.FirstCluster)
-                    {
-                        return 0;
-                    }
+                    return 0;
+                }
 
-                    if (characterIndex > Metrics.LastCluster)
-                    {
-                        return Metrics.WidthIncludingTrailingWhitespace;
-                    }
+                if (characterIndex > Metrics.LastCluster)
+                {
+                    return Metrics.WidthIncludingTrailingWhitespace;
                 }
 
                 var glyphIndex = FindGlyphIndex(characterIndex);
 
-                if (GlyphClusters != null)
-                {
-                    var currentCluster = GlyphClusters[glyphIndex];
+                var currentCluster = _glyphInfos[glyphIndex].GlyphCluster;
 
-                    //Move to the end of the glyph cluster
-                    if (characterHit.TrailingLength > 0)
+                //Move to the end of the glyph cluster
+                if (characterHit.TrailingLength > 0)
+                {
+                    while (glyphIndex + 1 < _glyphInfos.Count && _glyphInfos[glyphIndex + 1].GlyphCluster == currentCluster)
                     {
-                        while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster)
-                        {
-                            glyphIndex++;
-                        }
+                        glyphIndex++;
                     }
                 }
 
                 for (var i = 0; i < glyphIndex; i++)
                 {
-                    distance += GetGlyphAdvance(i, out _);
+                    distance += _glyphInfos[i].GlyphAdvance;
                 }
 
                 return distance;
@@ -262,22 +261,19 @@ namespace Avalonia.Media
                 //RightToLeft
                 var glyphIndex = FindGlyphIndex(characterIndex);
 
-                if (GlyphClusters != null && GlyphClusters.Count > 0)
+                if (characterIndex > Metrics.LastCluster)
                 {
-                    if (characterIndex > Metrics.LastCluster)
-                    {
-                        return 0;
-                    }
+                    return 0;
+                }
 
-                    if (characterIndex <= Metrics.FirstCluster)
-                    {
-                        return Size.Width;
-                    }
+                if (characterIndex <= Metrics.FirstCluster)
+                {
+                    return Size.Width;
                 }
 
-                for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++)
+                for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++)
                 {
-                    distance += GetGlyphAdvance(i, out _);
+                    distance += _glyphInfos[i].GlyphAdvance;
                 }
 
                 return Size.Width - distance;
@@ -322,11 +318,12 @@ namespace Avalonia.Media
 
             if (IsLeftToRight)
             {
-                for (var index = 0; index < GlyphIndices.Count; index++)
+                for (var index = 0; index < _glyphInfos.Count; index++)
                 {
-                    var advance = GetGlyphAdvance(index, out var cluster);
+                    var glyphInfo = _glyphInfos[index];
+                    var advance = glyphInfo.GlyphAdvance;
 
-                    characterIndex = cluster;
+                    characterIndex = glyphInfo.GlyphCluster;
 
                     if (distance > currentX && distance <= currentX + advance)
                     {
@@ -340,11 +337,12 @@ namespace Avalonia.Media
             {
                 currentX = Size.Width;
 
-                for (var index = GlyphIndices.Count - 1; index >= 0; index--)
+                for (var index = _glyphInfos.Count - 1; index >= 0; index--)
                 {
-                    var advance = GetGlyphAdvance(index, out var cluster);
+                    var glyphInfo = _glyphInfos[index];
+                    var advance = glyphInfo.GlyphAdvance;
 
-                    characterIndex = cluster;
+                    characterIndex = glyphInfo.GlyphCluster;
 
                     var offsetX = currentX - advance;
 
@@ -424,7 +422,7 @@ namespace Avalonia.Media
         /// </returns>
         public int FindGlyphIndex(int characterIndex)
         {
-            if (GlyphClusters == null || GlyphClusters.Count == 0)
+            if (_hasOneCharPerCluster)
             {
                 return characterIndex;
             }
@@ -433,7 +431,7 @@ namespace Avalonia.Media
             {
                 if (IsLeftToRight)
                 {
-                    return GlyphIndices.Count - 1;
+                    return _glyphInfos.Count - 1;
                 }
 
                 return 0;
@@ -446,15 +444,13 @@ namespace Avalonia.Media
                     return 0;
                 }
 
-                return GlyphIndices.Count - 1;
+                return _glyphInfos.Count - 1;
             }
 
-            var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
-
-            var clusters = GlyphClusters;
+            var comparer = IsLeftToRight ? GlyphInfo.ClusterAscendingComparer : GlyphInfo.ClusterDescendingComparer;
 
             // Find the start of the cluster at the character index.
-            var start = clusters.BinarySearch(characterIndex, comparer);
+            var start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer);
 
             // No cluster found.
             if (start < 0)
@@ -463,40 +459,38 @@ namespace Avalonia.Media
                 {
                     characterIndex--;
 
-                    start = clusters.BinarySearch(characterIndex, comparer);
+                    start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer);
                 }
 
                 if (start < 0)
                 {
-                    goto result;
+                    return 0;
                 }
             }
 
             if (IsLeftToRight)
             {
-                while (start > 0 && clusters[start - 1] == clusters[start])
+                while (start > 0 && _glyphInfos[start - 1].GlyphCluster == _glyphInfos[start].GlyphCluster)
                 {
                     start--;
                 }
             }
             else
             {
-                while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start])
+                while (start + 1 < _glyphInfos.Count && _glyphInfos[start + 1].GlyphCluster == _glyphInfos[start].GlyphCluster)
                 {
                     start++;
                 }
             }
 
-        result:
-
             if (start < 0)
             {
                 return 0;
             }
 
-            if (start > GlyphIndices.Count - 1)
+            if (start > _glyphInfos.Count - 1)
             {
-                return GlyphIndices.Count - 1;
+                return _glyphInfos.Count - 1;
             }
 
             return start;
@@ -516,14 +510,14 @@ namespace Avalonia.Media
 
             var glyphIndex = FindGlyphIndex(index);
 
-            if (GlyphClusters == null)
+            if (_hasOneCharPerCluster)
             {
-                width = GetGlyphAdvance(index, out _);
+                width = _glyphInfos[index].GlyphAdvance;
 
                 return new CharacterHit(glyphIndex, 1);
             }
 
-            var cluster = GlyphClusters[glyphIndex];
+            var cluster = _glyphInfos[glyphIndex].GlyphCluster;
 
             var nextCluster = cluster;
 
@@ -531,13 +525,13 @@ namespace Avalonia.Media
 
             while (nextCluster == cluster)
             {
-                width += GetGlyphAdvance(currentIndex, out _);
+                width += _glyphInfos[currentIndex].GlyphAdvance;
 
                 if (IsLeftToRight)
                 {
                     currentIndex++;
 
-                    if (currentIndex == GlyphClusters.Count)
+                    if (currentIndex == _glyphInfos.Count)
                     {
                         break;
                     }
@@ -552,7 +546,7 @@ namespace Avalonia.Media
                     }
                 }
 
-                nextCluster = GlyphClusters[currentIndex];
+                nextCluster = _glyphInfos[currentIndex].GlyphCluster;
             }
 
             var clusterLength = Math.Max(0, nextCluster - cluster);
@@ -565,9 +559,9 @@ namespace Avalonia.Media
 
                 if (IsLeftToRight)
                 {
-                    for (int i = 1; i < GlyphClusters.Count; i++)
+                    for (int i = 1; i < _glyphInfos.Count; i++)
                     {
-                        nextCluster = GlyphClusters[i];
+                        nextCluster = _glyphInfos[i].GlyphCluster;
 
                         if (currentCluster > cluster)
                         {
@@ -583,9 +577,9 @@ namespace Avalonia.Media
                 }
                 else
                 {
-                    for (int i = GlyphClusters.Count - 1; i >= 0; i--)
+                    for (int i = _glyphInfos.Count - 1; i >= 0; i--)
                     {
-                        nextCluster = GlyphClusters[i];
+                        nextCluster = _glyphInfos[i].GlyphCluster;
 
                         if (currentCluster > cluster)
                         {
@@ -613,26 +607,6 @@ namespace Avalonia.Media
             return new CharacterHit(cluster, clusterLength);
         }
 
-        /// <summary>
-        /// Gets a glyph's width.
-        /// </summary>
-        /// <param name="index">The glyph index.</param>
-        /// <param name="cluster">The current cluster.</param>
-        /// <returns>The glyph's width.</returns>
-        private double GetGlyphAdvance(int index, out int cluster)
-        {
-            cluster = GlyphClusters != null ? GlyphClusters[index] : index;
-
-            if (GlyphAdvances != null)
-            {
-                return GlyphAdvances[index];
-            }
-
-            var glyph = GlyphIndices[index];
-
-            return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
-        }
-
         /// <summary>
         /// Calculates the default baseline origin of the <see cref="GlyphRun"/>.
         /// </summary>
@@ -644,20 +618,17 @@ namespace Avalonia.Media
 
         private GlyphRunMetrics CreateGlyphRunMetrics()
         {
-            int firstCluster = 0, lastCluster = 0;
+            int firstCluster, lastCluster;
 
-            if (_glyphClusters != null && _glyphClusters.Count > 0)
+            if (Characters.IsEmpty)
             {
-                firstCluster = _glyphClusters[0];
-                lastCluster = _glyphClusters[_glyphClusters.Count - 1];
+                firstCluster = 0;
+                lastCluster = 0;
             }
             else
             {
-                if (!Characters.IsEmpty)
-                {
-                    firstCluster = 0;
-                    lastCluster = Characters.Length - 1;
-                }
+                firstCluster = _glyphInfos[0].GlyphCluster;
+                lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster;
             }
 
             if (!IsLeftToRight)
@@ -671,9 +642,9 @@ namespace Avalonia.Media
 
             var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount);
 
-            for (var index = 0; index < GlyphIndices.Count; index++)
+            for (var index = 0; index < _glyphInfos.Count; index++)
             {
-                var advance = GetGlyphAdvance(index, out _);
+                var advance = _glyphInfos[index].GlyphAdvance;
 
                 widthIncludingTrailingWhitespace += advance;
             }
@@ -684,14 +655,14 @@ namespace Avalonia.Media
             {
                 for (var index = 0; index < glyphCount; index++)
                 {
-                    width -= GetGlyphAdvance(index, out _);
+                    width -= _glyphInfos[index].GlyphAdvance;
                 }
             }
             else
             {
-                for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
+                for (var index = _glyphInfos.Count - glyphCount; index < _glyphInfos.Count; index++)
                 {
-                    width -= GetGlyphAdvance(index, out _);
+                    width -= _glyphInfos[index].GlyphAdvance;
                 }
             }
 
@@ -710,7 +681,7 @@ namespace Avalonia.Media
         {
             if (isReversed)
             {
-                return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
+                return GetTrailingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
             }
 
             glyphCount = 0;
@@ -720,84 +691,59 @@ namespace Avalonia.Media
 
             if (!charactersSpan.IsEmpty)
             {
-                if (GlyphClusters == null)
-                {
-                    for (var i = charactersSpan.Length - 1; i >= 0;)
-                    {
-                        var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
-
-                        if (!codepoint.IsWhiteSpace)
-                        {
-                            break;
-                        }
-
-                        if (codepoint.IsBreakChar)
-                        {
-                            newLineLength++;
-                        }
-
-                        trailingWhitespaceLength++;
+                var characterIndex = charactersSpan.Length - 1;
 
-                        i -= count;
-                        glyphCount++;
-                    }
-                }
-                else
+                for (var i = _glyphInfos.Count - 1; i >= 0; i--)
                 {
-                    var characterIndex = charactersSpan.Length - 1;
+                    var currentCluster = _glyphInfos[i].GlyphCluster;
+                    var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
 
-                    for (var i = GlyphClusters.Count - 1; i >= 0; i--)
-                    {
-                        var currentCluster = GlyphClusters[i];
-                        var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
+                    characterIndex -= characterLength;
 
-                        characterIndex -= characterLength;
+                    if (!codepoint.IsWhiteSpace)
+                    {
+                        break;
+                    }
 
-                        if (!codepoint.IsWhiteSpace)
-                        {
-                            break;
-                        }
+                    var clusterLength = 1;
 
-                        var clusterLength = 1;
+                    while (i - 1 >= 0)
+                    {
+                        var nextCluster = _glyphInfos[i - 1].GlyphCluster;
 
-                        while (i - 1 >= 0)
+                        if (currentCluster == nextCluster)
                         {
-                            var nextCluster = GlyphClusters[i - 1];
+                            clusterLength++;
+                            i--;
 
-                            if (currentCluster == nextCluster)
+                            if(characterIndex >= 0)
                             {
-                                clusterLength++;
-                                i--;
-
-                                if(characterIndex >= 0)
-                                {
-                                    codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
+                                codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
 
-                                    characterIndex -= characterLength;
-                                }
-
-                                continue;
+                                characterIndex -= characterLength;
                             }
 
-                            break;
-                        }
-
-                        if (codepoint.IsBreakChar)
-                        {
-                            newLineLength += clusterLength;
+                            continue;
                         }
 
-                        trailingWhitespaceLength += clusterLength;
+                        break;
+                    }
 
-                        glyphCount++;
+                    if (codepoint.IsBreakChar)
+                    {
+                        newLineLength += clusterLength;
                     }
+
+                    trailingWhitespaceLength += clusterLength;
+
+                    glyphCount++;
                 }
             }
 
             return trailingWhitespaceLength;
         }
 
-        private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount)
+        private int GetTrailingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount)
         {
             glyphCount = 0;
             newLineLength = 0;
@@ -806,71 +752,46 @@ namespace Avalonia.Media
 
             if (!charactersSpan.IsEmpty)
             {
-                if (GlyphClusters == null)
-                {
-                    for (var i = 0; i < charactersSpan.Length;)
-                    {
-                        var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
+                var characterIndex = 0;
 
-                        if (!codepoint.IsWhiteSpace)
-                        {
-                            break;
-                        }
-
-                        if (codepoint.IsBreakChar)
-                        {
-                            newLineLength++;
-                        }
-
-                        trailingWhitespaceLength++;
-
-                        i += count;
-                        glyphCount++;
-                    }
-                }
-                else
+                for (var i = 0; i < _glyphInfos.Count; i++)
                 {
-                    var characterIndex = 0;
+                    var currentCluster = _glyphInfos[i].GlyphCluster;
+                    var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
 
-                    for (var i = 0; i < GlyphClusters.Count; i++)
-                    {
-                        var currentCluster = GlyphClusters[i];
-                        var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
+                    characterIndex += characterLength;
 
-                        characterIndex += characterLength;
+                    if (!codepoint.IsWhiteSpace)
+                    {
+                        break;
+                    }
 
-                        if (!codepoint.IsWhiteSpace)
-                        {
-                            break;
-                        }
+                    var clusterLength = 1;
 
-                        var clusterLength = 1;
+                    var j = i;
 
-                        var j = i;
+                    while (j - 1 >= 0)
+                    {
+                        var nextCluster = _glyphInfos[--j].GlyphCluster;
 
-                        while (j - 1 >= 0)
+                        if (currentCluster == nextCluster)
                         {
-                            var nextCluster = GlyphClusters[--j];
-
-                            if (currentCluster == nextCluster)
-                            {
-                                clusterLength++;
+                            clusterLength++;
 
-                                continue;
-                            }
-
-                            break;
-                        }
-
-                        if (codepoint.IsBreakChar)
-                        {
-                            newLineLength += clusterLength;
+                            continue;
                         }
 
-                        trailingWhitespaceLength += clusterLength;
+                        break;
+                    }
 
-                        glyphCount += clusterLength;
+                    if (codepoint.IsBreakChar)
+                    {
+                        newLineLength += clusterLength;
                     }
+
+                    trailingWhitespaceLength += clusterLength;
+
+                    glyphCount += clusterLength;
                 }
             }
 
@@ -890,44 +811,17 @@ namespace Avalonia.Media
             field = value;
         }
 
-        /// <summary>
-        /// Initializes the <see cref="GlyphRun"/>.
-        /// </summary>
-        private void Initialize()
+        private IGlyphRunImpl CreateGlyphRunImpl()
         {
-            if (GlyphIndices == null)
-            {
-                throw new InvalidOperationException();
-            }
-
-            var glyphCount = GlyphIndices.Count;
-
-            if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount)
-            {
-                throw new InvalidOperationException();
-            }
-
-            if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount)
-            {
-                throw new InvalidOperationException();
-            }
-
             var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
 
-            _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
+            return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos);
         }
 
         public void Dispose()
         {
             _glyphRunImpl?.Dispose();
-        }
-
-        private class ReverseComparer<T> : IComparer<T>
-        {
-            public int Compare(T? x, T? y)
-            {
-                return Comparer<T>.Default.Compare(y, x);
-            }
+            _glyphRunImpl = null;
         }
     }
 }

+ 268 - 0
src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs

@@ -0,0 +1,268 @@
+using System;
+using System.Diagnostics;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Reorders text runs according to their bidi level.
+    /// </summary>
+    /// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
+    internal sealed class BidiReorderer
+    {
+        [ThreadStatic] private static BidiReorderer? t_instance;
+
+        private ArrayBuilder<OrderedBidiRun> _runs;
+        private ArrayBuilder<BidiRange> _ranges;
+
+        public static BidiReorderer Instance
+            => t_instance ??= new();
+
+        public void BidiReorder(Span<TextRun> textRuns, FlowDirection flowDirection)
+        {
+            Debug.Assert(_runs.Length == 0);
+            Debug.Assert(_ranges.Length == 0);
+
+            if (textRuns.IsEmpty)
+            {
+                return;
+            }
+
+            try
+            {
+                _runs.Add(textRuns.Length);
+
+                // Build up the collection of ordered runs.
+                for (var i = 0; i < textRuns.Length; i++)
+                {
+                    var textRun = textRuns[i];
+                    _runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection));
+
+                    if (i > 0)
+                    {
+                        _runs[i - 1].NextRunIndex = i;
+                    }
+                }
+
+                // Reorder them into visual order.
+                var firstIndex = LinearReorder();
+
+                // Now perform a recursive reversal of each run.
+                // From the highest level found in the text to the lowest odd level on each line, including intermediate levels
+                // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
+                // https://unicode.org/reports/tr9/#L2
+                sbyte max = 0;
+                var min = sbyte.MaxValue;
+
+                for (var i = 0; i < textRuns.Length; i++)
+                {
+                    var level = GetRunBidiLevel(textRuns[i], flowDirection);
+                    if (level > max)
+                    {
+                        max = level;
+                    }
+
+                    if ((level & 1) != 0 && level < min)
+                    {
+                        min = level;
+                    }
+                }
+
+                if (min > max)
+                {
+                    min = max;
+                }
+
+                if (max == 0 || (min == max && (max & 1) == 0))
+                {
+                    // Nothing to reverse.
+                    return;
+                }
+
+                // Now apply the reversal and replace the original contents.
+                var minLevelToReverse = max;
+                int currentIndex;
+
+                while (minLevelToReverse >= min)
+                {
+                    currentIndex = firstIndex;
+
+                    while (currentIndex >= 0)
+                    {
+                        ref var current = ref _runs[currentIndex];
+                        if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
+                        {
+                            if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
+                            {
+                                shapedTextCharacters.Reverse();
+                            }
+                        }
+
+                        currentIndex = current.NextRunIndex;
+                    }
+
+                    minLevelToReverse--;
+                }
+
+                var index = 0;
+
+                currentIndex = firstIndex;
+                while (currentIndex >= 0)
+                {
+                    ref var current = ref _runs[currentIndex];
+                    textRuns[index++] = current.Run;
+
+                    currentIndex = current.NextRunIndex;
+                }
+            }
+            finally
+            {
+                FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs);
+                FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges);
+            }
+        }
+
+        private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
+        {
+            if (run is ShapedTextRun shapedTextRun)
+            {
+                return shapedTextRun.BidiLevel;
+            }
+
+            var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
+            return (sbyte)defaultLevel;
+        }
+
+        /// <summary>
+        /// Reorders the runs from logical to visual order.
+        /// <see href="https://github.com/fribidi/linear-reorder/blob/f2f872257d4d8b8e137fcf831f254d6d4db79d3c/linear-reorder.c"/>
+        /// </summary>
+        /// <returns>The first run index in visual order.</returns>
+        private int LinearReorder()
+        {
+            var runIndex = 0;
+            var rangeIndex = -1;
+
+            while (runIndex >= 0)
+            {
+                ref var run = ref _runs[runIndex];
+                var nextRunIndex = run.NextRunIndex;
+
+                while (rangeIndex >= 0
+                    && _ranges[rangeIndex].Level > run.Level
+                    && _ranges[rangeIndex].PreviousRangeIndex >= 0
+                    && _ranges[_ranges[rangeIndex].PreviousRangeIndex].Level >= run.Level)
+                {
+
+                    rangeIndex = MergeRangeWithPrevious(rangeIndex);
+                }
+
+                if (rangeIndex >= 0 && _ranges[rangeIndex].Level >= run.Level)
+                {
+                    // Attach run to the range.
+                    if ((run.Level & 1) != 0)
+                    {
+                        // Odd, range goes to the right of run.
+                        run.NextRunIndex = _ranges[rangeIndex].LeftRunIndex;
+                        _ranges[rangeIndex].LeftRunIndex = runIndex;
+                    }
+                    else
+                    {
+                        // Even, range goes to the left of run.
+                        _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = runIndex;
+                        _ranges[rangeIndex].RightRunIndex = runIndex;
+                    }
+
+                    _ranges[rangeIndex].Level = run.Level;
+                }
+                else
+                {
+                    var r = new BidiRange(run.Level, runIndex, runIndex, previousRangeIndex: rangeIndex);
+                    _ranges.AddItem(r);
+                    rangeIndex = _ranges.Length - 1;
+                }
+
+                runIndex = nextRunIndex;
+            }
+
+            while (rangeIndex >= 0 && _ranges[rangeIndex].PreviousRangeIndex >= 0)
+            {
+                rangeIndex = MergeRangeWithPrevious(rangeIndex);
+            }
+
+            // Terminate.
+            _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = -1;
+
+            return _runs[_ranges[rangeIndex].LeftRunIndex].RunIndex;
+        }
+
+        private int MergeRangeWithPrevious(int index)
+        {
+            var previousIndex = _ranges[index].PreviousRangeIndex;
+            ref var previous = ref _ranges[previousIndex];
+
+            int leftIndex;
+            int rightIndex;
+
+            if ((previous.Level & 1) != 0)
+            {
+                // Odd, previous goes to the right of range.
+                leftIndex = index;
+                rightIndex = previousIndex;
+            }
+            else
+            {
+                // Even, previous goes to the left of range.
+                leftIndex = previousIndex;
+                rightIndex = index;
+            }
+
+            // Stitch them
+            ref var left = ref _ranges[leftIndex];
+            ref var right = ref _ranges[rightIndex];
+            _runs[left.RightRunIndex].NextRunIndex = _runs[right.LeftRunIndex].RunIndex;
+            previous.LeftRunIndex = left.LeftRunIndex;
+            previous.RightRunIndex = right.RightRunIndex;
+
+            return previousIndex;
+        }
+
+        private struct OrderedBidiRun
+        {
+            public OrderedBidiRun(int runIndex, TextRun run, sbyte level)
+            {
+                RunIndex = runIndex;
+                Run = run;
+                Level = level;
+                NextRunIndex = -1;
+            }
+
+            public int RunIndex { get; }
+
+            public sbyte Level { get; }
+
+            public TextRun Run { get; }
+
+            public int NextRunIndex { get; set; } // -1 if none
+        }
+
+        private struct BidiRange
+        {
+            public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex)
+            {
+                Level = level;
+                LeftRunIndex = leftRunIndex;
+                RightRunIndex = rightRunIndex;
+                PreviousRangeIndex = previousRangeIndex;
+            }
+
+            public sbyte Level { get; set; }
+
+            public int LeftRunIndex { get; set; }
+
+            public int RightRunIndex { get; set; }
+
+            public int PreviousRangeIndex { get; } // -1 if none
+        }
+    }
+}

+ 84 - 0
src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs

@@ -0,0 +1,84 @@
+using System.Collections.Generic;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+    internal static class FormattingBufferHelper
+    {
+        // 1MB, arbitrary, that's 512K characters or 128K object references on x64
+        private const long MaxKeptBufferSizeInBytes = 1024 * 1024;
+
+        public static void ClearThenResetIfTooLarge<T>(ref ArrayBuilder<T> arrayBuilder)
+        {
+            arrayBuilder.Clear();
+
+            if (IsBufferTooLarge<T>((uint) arrayBuilder.Capacity))
+            {
+                arrayBuilder = default;
+            }
+        }
+
+        public static void ClearThenResetIfTooLarge<T>(List<T> list)
+        {
+            list.Clear();
+
+            if (IsBufferTooLarge<T>((uint) list.Capacity))
+            {
+                list.TrimExcess();
+            }
+        }
+
+        public static void ClearThenResetIfTooLarge<T>(Stack<T> stack)
+        {
+            var approximateCapacity = RoundUpToPowerOf2((uint)stack.Count);
+
+            stack.Clear();
+
+            if (IsBufferTooLarge<T>(approximateCapacity))
+            {
+                stack.TrimExcess();
+            }
+        }
+
+        public static void ClearThenResetIfTooLarge<TKey, TValue>(ref Dictionary<TKey, TValue> dictionary)
+            where TKey : notnull
+        {
+            var approximateCapacity = RoundUpToPowerOf2((uint)dictionary.Count);
+
+            dictionary.Clear();
+
+            // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here
+            if (IsBufferTooLarge<KeyValuePair<TKey, TValue>>(approximateCapacity))
+            {
+#if NET6_0_OR_GREATER
+                dictionary.TrimExcess();
+#else
+                dictionary = new Dictionary<TKey, TValue>();
+#endif
+            }
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static bool IsBufferTooLarge<T>(uint capacity)
+            => (long) (uint) Unsafe.SizeOf<T>() * capacity > MaxKeptBufferSizeInBytes;
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static uint RoundUpToPowerOf2(uint value)
+        {
+#if NET6_0_OR_GREATER
+            return BitOperations.RoundUpToPowerOf2(value);
+#else
+            // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
+            --value;
+            value |= value >> 1;
+            value |= value >> 2;
+            value |= value >> 4;
+            value |= value >> 8;
+            value |= value >> 16;
+            return value + 1;
+#endif
+        }
+    }
+}

+ 135 - 0
src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs

@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// <para>Contains various list pools that are commonly used during text layout.</para>
+    /// <para>
+    /// This class provides an instance per thread.
+    /// In most applications, there'll be only one instance: on the UI thread, which is responsible for layout.
+    /// </para>
+    /// </summary>
+    /// <seealso cref="RentedList{T}"/>
+    internal sealed class FormattingObjectPool
+    {
+        [ThreadStatic] private static FormattingObjectPool? t_instance;
+
+        /// <summary>
+        /// Gets an instance of this class for the current thread.
+        /// </summary>
+        /// <remarks>
+        /// Since this is backed by a thread static field which is slower than a normal static field,
+        /// prefer passing the instance around when possible instead of calling this property each time.
+        /// </remarks>
+        public static FormattingObjectPool Instance
+            => t_instance ??= new();
+
+        public ListPool<TextRun> TextRunLists { get; } = new();
+
+        public ListPool<UnshapedTextRun> UnshapedTextRunLists { get; } = new();
+
+        public ListPool<TextLine> TextLines { get; } = new();
+
+        [Conditional("DEBUG")]
+        public void VerifyAllReturned()
+        {
+            TextRunLists.VerifyAllReturned();
+            UnshapedTextRunLists.VerifyAllReturned();
+            TextLines.VerifyAllReturned();
+        }
+
+        internal sealed class ListPool<T>
+        {
+            // we don't need a big number here, these are for temporary usages only which should quickly be returned
+            private const int MaxSize = 16;
+
+            private readonly RentedList<T>[] _lists = new RentedList<T>[MaxSize];
+            private int _size;
+            private int _pendingReturnCount;
+
+            /// <summary>
+            /// Rents a list.
+            /// See <see cref="RentedList{T}"/> for the intended usages.
+            /// </summary>
+            /// <returns>A rented list instance that must be returned to the pool.</returns>
+            /// <seealso cref="RentedList{T}"/>
+            public RentedList<T> Rent()
+            {
+                var list = _size > 0 ? _lists[--_size] : new RentedList<T>();
+
+                Debug.Assert(list.Count == 0, "A RentedList has been used after being returned!");
+
+                ++_pendingReturnCount;
+                return list;
+            }
+
+            /// <summary>
+            /// Returns a rented list to the pool.
+            /// </summary>
+            /// <param name="rentedList">
+            /// On input, the list to return.
+            /// On output, the reference is set to null to avoid misuse.
+            /// </param>
+            public void Return(ref RentedList<T>? rentedList)
+            {
+                if (rentedList is null)
+                {
+                    return;
+                }
+
+                --_pendingReturnCount;
+                FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList);
+
+                if (_size < MaxSize)
+                {
+                    _lists[_size++] = rentedList;
+                }
+
+                rentedList = null;
+            }
+
+            [Conditional("DEBUG")]
+            public void VerifyAllReturned()
+            {
+                if (_pendingReturnCount > 0)
+                {
+                    throw new InvalidOperationException(
+                        $"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!");
+                }
+
+                if (_pendingReturnCount < 0)
+                {
+                    throw new InvalidOperationException(
+                        $"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!");
+                }
+            }
+        }
+
+        /// <summary>
+        /// <para>Represents a list that has been rented through <see cref="FormattingObjectPool"/>.</para>
+        /// <para>
+        /// This class can be used when a temporary list is needed to store some items during text layout.
+        /// It can also be used as a reusable array builder by calling <see cref="List{T}.ToArray"/> when done.
+        /// </para>
+        /// <list type="bullet">
+        ///   <item>NEVER use an instance of this type after it's been returned to the pool.</item>
+        ///   <item>AVOID storing an instance of this type into a field or property.</item>
+        ///   <item>AVOID casting an instance of this type to another type.</item>
+        ///   <item>
+        ///     AVOID passing an instance of this type as an argument to a method expecting a standard list,
+        ///     unless you're absolutely sure it won't store it.
+        ///   </item>
+        ///   <item>
+        ///     If you call a method returning an instance of this type,
+        ///     you're now responsible for returning it to the pool.
+        ///   </item>
+        /// </list>
+        /// </summary>
+        /// <typeparam name="T">The type of elements in the list.</typeparam>
+        internal sealed class RentedList<T> : List<T>
+        {
+        }
+    }
+}

+ 36 - 0
src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs

@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Represents a single glyph.
+    /// </summary>
+    public readonly record struct GlyphInfo(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, Vector GlyphOffset = default)
+    {
+        internal static Comparer<GlyphInfo> ClusterAscendingComparer { get; } =
+            Comparer<GlyphInfo>.Create((x, y) => x.GlyphCluster.CompareTo(y.GlyphCluster));
+
+        internal static Comparer<GlyphInfo> ClusterDescendingComparer { get; } =
+            Comparer<GlyphInfo>.Create((x, y) => y.GlyphCluster.CompareTo(x.GlyphCluster));
+
+        /// <summary>
+        /// Get the glyph index.
+        /// </summary>
+        public ushort GlyphIndex { get; } = GlyphIndex;
+
+        /// <summary>
+        /// Get the glyph cluster.
+        /// </summary>
+        public int GlyphCluster { get; } = GlyphCluster;
+
+        /// <summary>
+        /// Get the glyph advance.
+        /// </summary>
+        public double GlyphAdvance { get; } = GlyphAdvance;
+
+        /// <summary>
+        /// Get the glyph offset.
+        /// </summary>
+        public Vector GlyphOffset { get; } = GlyphOffset;
+    }
+}

+ 3 - 2
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@@ -48,8 +48,9 @@ namespace Avalonia.Media.TextFormatting
 
             var currentPosition = textLine.FirstTextSourceIndex;
 
-            foreach (var textRun in lineImpl.TextRuns)
+            for (var i = 0; i < lineImpl.TextRuns.Count; ++i)
             {
+                var textRun = lineImpl.TextRuns[i];
                 var text = textRun.Text;
 
                 if (text.IsEmpty)
@@ -111,7 +112,7 @@ namespace Avalonia.Media.TextFormatting
                         shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing);
                     }
 
-                    glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances;
+                    glyphRun.GlyphInfos = shapedBuffer.GlyphInfos;
                 }
 
                 currentPosition += textRun.Length;

+ 30 - 200
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@@ -1,24 +1,23 @@
 using System;
 using System.Buffers;
+using System.Collections;
 using System.Collections.Generic;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
-    public sealed class ShapedBuffer : IList<GlyphInfo>, IDisposable
+    public sealed class ShapedBuffer : IReadOnlyList<GlyphInfo>, IDisposable
     {
-        private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
-        private bool _bufferRented;
-
-        public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
-            this(text,
-                new ArraySlice<GlyphInfo>(ArrayPool<GlyphInfo>.Shared.Rent(bufferLength), 0, bufferLength),
-                glyphTypeface,
-                fontRenderingEmSize,
-                bidiLevel)
+        private GlyphInfo[]? _rentedBuffer;
+
+        public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
         {
-            _bufferRented = true;
-            Length = bufferLength;
+            _rentedBuffer = ArrayPool<GlyphInfo>.Shared.Rent(bufferLength);
+            Text = text;
+            GlyphInfos = new ArraySlice<GlyphInfo>(_rentedBuffer, 0, bufferLength);
+            GlyphTypeface = glyphTypeface;
+            FontRenderingEmSize = fontRenderingEmSize;
+            BidiLevel = bidiLevel;
         }
 
         internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
@@ -28,12 +27,12 @@ namespace Avalonia.Media.TextFormatting
             GlyphTypeface = glyphTypeface;
             FontRenderingEmSize = fontRenderingEmSize;
             BidiLevel = bidiLevel;
-            Length = GlyphInfos.Length;
         }
 
-        internal ArraySlice<GlyphInfo> GlyphInfos { get; }
-        
-        public int Length { get; }
+        internal ArraySlice<GlyphInfo> GlyphInfos { get; private set; }
+
+        public int Length
+            => GlyphInfos.Length;
 
         public IGlyphTypeface GlyphTypeface { get; }
 
@@ -42,14 +41,6 @@ namespace Avalonia.Media.TextFormatting
         public sbyte BidiLevel { get; }
 
         public bool IsLeftToRight => (BidiLevel & 1) == 0;
-        
-        public IReadOnlyList<ushort> GlyphIndices => new GlyphIndexList(GlyphInfos);
-
-        public IReadOnlyList<int> GlyphClusters => new GlyphClusterList(GlyphInfos);
-
-        public IReadOnlyList<double> GlyphAdvances => new GlyphAdvanceList(GlyphInfos);
-
-        public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
 
         public ReadOnlyMemory<char> Text { get; }
         
@@ -73,13 +64,13 @@ namespace Avalonia.Media.TextFormatting
             }
 
 
-            var comparer = s_clusterComparer;
+            var comparer = GlyphInfo.ClusterAscendingComparer;
 
-            var clusters = GlyphInfos.Span;
+            var glyphInfos = GlyphInfos.Span;
 
-            var searchValue = new GlyphInfo(0, characterIndex);
+            var searchValue = new GlyphInfo(default, characterIndex, default);
 
-            var start = clusters.BinarySearch(searchValue, comparer);
+            var start = glyphInfos.BinarySearch(searchValue, comparer);
 
             if (start < 0)
             {
@@ -87,9 +78,9 @@ namespace Avalonia.Media.TextFormatting
                 {
                     characterIndex--;
 
-                    searchValue = new GlyphInfo(0, characterIndex);
+                    searchValue = new GlyphInfo(default, characterIndex, default);
 
-                    start = clusters.BinarySearch(searchValue, comparer);
+                    start = glyphInfos.BinarySearch(searchValue, comparer);
                 }
 
                 if (start < 0)
@@ -98,7 +89,7 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster)
+            while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster)
             {
                 start--;
             }
@@ -118,8 +109,8 @@ namespace Avalonia.Media.TextFormatting
                 return new SplitResult<ShapedBuffer>(this, null);
             }
 
-            var firstCluster = GlyphClusters[0];
-            var lastCluster = GlyphClusters[GlyphClusters.Count - 1];
+            var firstCluster = GlyphInfos[0].GlyphCluster;
+            var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster;
 
             var start = firstCluster < lastCluster ? firstCluster : lastCluster;
 
@@ -134,9 +125,7 @@ namespace Avalonia.Media.TextFormatting
             return new SplitResult<ShapedBuffer>(first, second);
         }
 
-        int ICollection<GlyphInfo>.Count => throw new NotImplementedException();
-
-        bool ICollection<GlyphInfo>.IsReadOnly => true;
+        int IReadOnlyCollection<GlyphInfo>.Count => GlyphInfos.Length;
 
         public GlyphInfo this[int index]
         {
@@ -144,177 +133,18 @@ namespace Avalonia.Media.TextFormatting
             set => GlyphInfos[index] = value;
         }
 
-        int IList<GlyphInfo>.IndexOf(GlyphInfo item)
-        {
-            throw new NotImplementedException();
-        }
-
-        void IList<GlyphInfo>.Insert(int index, GlyphInfo item)
-        {
-            throw new NotImplementedException();
-        }
-
-        void IList<GlyphInfo>.RemoveAt(int index)
-        {
-            throw new NotImplementedException();
-        }
-
-        void ICollection<GlyphInfo>.Add(GlyphInfo item)
-        {
-            throw new NotImplementedException();
-        }
-
-        void ICollection<GlyphInfo>.Clear()
-        {
-            throw new NotImplementedException();
-        }
-
-        bool ICollection<GlyphInfo>.Contains(GlyphInfo item)
-        {
-            throw new NotImplementedException();
-        }
-
-        void ICollection<GlyphInfo>.CopyTo(GlyphInfo[] array, int arrayIndex)
-        {
-            throw new NotImplementedException();
-        }
-
-        bool ICollection<GlyphInfo>.Remove(GlyphInfo item)
-        {
-            throw new NotImplementedException();
-        }
         public IEnumerator<GlyphInfo> GetEnumerator() => GlyphInfos.GetEnumerator();
 
-        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
-
-        private class CompareClusters : IComparer<GlyphInfo>
-        {
-            private static readonly Comparer<int> s_intClusterComparer = Comparer<int>.Default;
-
-            public int Compare(GlyphInfo x, GlyphInfo y)
-            {
-                return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster);
-            }
-        }
-
-        private readonly struct GlyphAdvanceList : IReadOnlyList<double>
-        {
-            private readonly ArraySlice<GlyphInfo> _glyphInfos;
-
-            public GlyphAdvanceList(ArraySlice<GlyphInfo> glyphInfos)
-            {
-                _glyphInfos = glyphInfos;
-            }
-
-            public double this[int index] => _glyphInfos[index].GlyphAdvance;
-
-            public int Count => _glyphInfos.Length;
-
-            public IEnumerator<double> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<double>(this);
-
-            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
-        }
-
-        private readonly struct GlyphIndexList : IReadOnlyList<ushort>
-        {
-            private readonly ArraySlice<GlyphInfo> _glyphInfos;
-
-            public GlyphIndexList(ArraySlice<GlyphInfo> glyphInfos)
-            {
-                _glyphInfos = glyphInfos;
-            }
-
-            public ushort this[int index] => _glyphInfos[index].GlyphIndex;
-
-            public int Count => _glyphInfos.Length;
-
-            public IEnumerator<ushort> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<ushort>(this);
-
-            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
-        }
-
-        private readonly struct GlyphClusterList : IReadOnlyList<int>
-        {
-            private readonly ArraySlice<GlyphInfo> _glyphInfos;
-
-            public GlyphClusterList(ArraySlice<GlyphInfo> glyphInfos)
-            {
-                _glyphInfos = glyphInfos;
-            }
-
-            public int this[int index] => _glyphInfos[index].GlyphCluster;
-
-            public int Count => _glyphInfos.Length;
-
-            public IEnumerator<int> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<int>(this);
-
-            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
-        }
-
-        private readonly struct GlyphOffsetList : IReadOnlyList<Vector>
-        {
-            private readonly ArraySlice<GlyphInfo> _glyphInfos;
-
-            public GlyphOffsetList(ArraySlice<GlyphInfo> glyphInfos)
-            {
-                _glyphInfos = glyphInfos;
-            }
-
-            public Vector this[int index] => _glyphInfos[index].GlyphOffset;
-
-            public int Count => _glyphInfos.Length;
-
-            public IEnumerator<Vector> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<Vector>(this);
-
-            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
-        }
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 
         public void Dispose()
         {
-            GC.SuppressFinalize(this);
-            if (_bufferRented)
+            if (_rentedBuffer is not null)
             {
-                GlyphInfos.ReturnRent();
+                ArrayPool<GlyphInfo>.Shared.Return(_rentedBuffer);
+                _rentedBuffer = null;
+                GlyphInfos = ArraySlice<GlyphInfo>.Empty; // ensure we don't misuse the returned array
             }
         }
-
-        ~ShapedBuffer()
-        {
-            if (_bufferRented)
-            {
-                GlyphInfos.ReturnRent();
-            }
-        }
-    }
-
-    public readonly record struct GlyphInfo
-    {
-        public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default)
-        {
-            GlyphIndex = glyphIndex;
-            GlyphAdvance = glyphAdvance;
-            GlyphCluster = glyphCluster;
-            GlyphOffset = glyphOffset;
-        }
-
-        /// <summary>
-        /// Get the glyph index.
-        /// </summary>
-        public ushort GlyphIndex { get; }
-
-        /// <summary>
-        /// Get the glyph cluster.
-        /// </summary>
-        public int GlyphCluster { get; }
-
-        /// <summary>
-        /// Get the glyph advance.
-        /// </summary>
-        public double GlyphAdvance { get; }
-
-        /// <summary>
-        /// Get the glyph offset.
-        /// </summary>
-        public Vector GlyphOffset { get; }
     }
 }

+ 5 - 19
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@@ -40,25 +40,14 @@ namespace Avalonia.Media.TextFormatting
 
         public override Size Size => GlyphRun.Size;
 
-        public GlyphRun GlyphRun
-        {
-            get
-            {
-                if(_glyphRun is null)
-                {
-                    _glyphRun = CreateGlyphRun();
-                }
-
-                return _glyphRun;
-            }
-        }
+        public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun();
 
         /// <inheritdoc/>
         public override void Draw(DrawingContext drawingContext, Point origin)
         {
             using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin)))
             {
-                if (GlyphRun.GlyphIndices.Count == 0)
+                if (GlyphRun.GlyphInfos.Count == 0)
                 {
                     return;
                 }
@@ -117,7 +106,7 @@ namespace Avalonia.Media.TextFormatting
 
             for (var i = 0; i < ShapedBuffer.Length; i++)
             {
-                var advance = ShapedBuffer.GlyphAdvances[i];
+                var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
 
                 if (currentWidth + advance > availableWidth)
                 {
@@ -141,7 +130,7 @@ namespace Avalonia.Media.TextFormatting
 
             for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
             {
-                var advance = ShapedBuffer.GlyphAdvances[i];
+                var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
 
                 if (width + advance > availableWidth)
                 {
@@ -195,10 +184,7 @@ namespace Avalonia.Media.TextFormatting
                 ShapedBuffer.GlyphTypeface,
                 ShapedBuffer.FontRenderingEmSize,
                 Text,
-                ShapedBuffer.GlyphIndices,
-                ShapedBuffer.GlyphAdvances,
-                ShapedBuffer.GlyphOffsets,
-                ShapedBuffer.GlyphClusters,
+                ShapedBuffer,
                 BidiLevel);
         }
 

+ 11 - 0
src/Avalonia.Base/Media/TextFormatting/SplitResult.cs

@@ -26,5 +26,16 @@
         /// The second part.
         /// </value>
         public T? Second { get; }
+
+        /// <summary>
+        /// Deconstructs the split results into its components.
+        /// </summary>
+        /// <param name="first">On return, contains the first part.</param>
+        /// <param name="second">On return, contains the second part.</param>
+        public void Deconstruct(out T first, out T? second)
+        {
+            first = First;
+            second = Second;
+        }
     }
 }

+ 4 - 7
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -1,6 +1,6 @@
 using System;
-using System.Collections.Generic;
 using Avalonia.Media.TextFormatting.Unicode;
+using static Avalonia.Media.TextFormatting.FormattingObjectPool;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -46,24 +46,21 @@ namespace Avalonia.Media.TextFormatting
         /// Gets a list of <see cref="UnshapedTextRun"/>.
         /// </summary>
         /// <returns>The shapeable text characters.</returns>
-        internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
-            ref TextRunProperties? previousProperties)
+        internal void GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
+            ref TextRunProperties? previousProperties, RentedList<TextRun> results)
         {
-            var shapeableCharacters = new List<UnshapedTextRun>(2);
             var properties = Properties;
 
             while (!text.IsEmpty)
             {
                 var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties);
 
-                shapeableCharacters.Add(shapeableRun);
+                results.Add(shapeableRun);
 
                 text = text.Slice(shapeableRun.Length);
 
                 previousProperties = shapeableRun.Properties;
             }
-
-            return shapeableCharacters;
         }
 
         /// <summary>

+ 2 - 4
src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs

@@ -1,6 +1,4 @@
-using System.Collections.Generic;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
 {
     /// <summary>
     /// Properties of text collapsing.
@@ -21,6 +19,6 @@ namespace Avalonia.Media.TextFormatting
         /// Collapses given text line.
         /// </summary>
         /// <param name="textLine">Text line to collapse.</param>
-        public abstract List<TextRun>? Collapse(TextLine textLine);
+        public abstract TextRun[]? Collapse(TextLine textLine);
     }
 }

+ 29 - 28
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@@ -1,15 +1,16 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using Avalonia.Media.TextFormatting.Unicode;
 
 namespace Avalonia.Media.TextFormatting
 {
     internal static class TextEllipsisHelper
     {
-        public static List<TextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
+        public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
         {
             var textRuns = textLine.TextRuns;
 
-            if (textRuns == null || textRuns.Count == 0)
+            if (textRuns.Count == 0)
             {
                 return null;
             }
@@ -22,7 +23,7 @@ namespace Avalonia.Media.TextFormatting
             if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
             {
                 //Not enough space to fit in the symbol
-                return new List<TextRun>(0);
+                return Array.Empty<TextRun>();
             }
 
             var availableWidth = properties.Width - shapedSymbol.Size.Width;
@@ -70,18 +71,7 @@ namespace Avalonia.Media.TextFormatting
 
                                 collapsedLength += measuredLength;
 
-                                var collapsedRuns = new List<TextRun>(textRuns.Count);
-
-                                if (collapsedLength > 0)
-                                {
-                                    var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
-
-                                    collapsedRuns.AddRange(splitResult.First);
-                                }
-
-                                collapsedRuns.Add(shapedSymbol);
-
-                                return collapsedRuns;
+                                return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
                             }
 
                             availableWidth -= shapedRun.Size.Width;
@@ -94,18 +84,7 @@ namespace Avalonia.Media.TextFormatting
                             //The whole run needs to fit into available space
                             if (currentWidth + drawableRun.Size.Width > availableWidth)
                             {
-                                var collapsedRuns = new List<TextRun>(textRuns.Count);
-
-                                if (collapsedLength > 0)
-                                {
-                                    var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
-
-                                    collapsedRuns.AddRange(splitResult.First);
-                                }
-
-                                collapsedRuns.Add(shapedSymbol);
-
-                                return collapsedRuns;
+                                return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
                             }
 
                             availableWidth -= drawableRun.Size.Width;
@@ -121,5 +100,27 @@ namespace Avalonia.Media.TextFormatting
 
             return null;
         }
+
+        private static TextRun[] CreateCollapsedRuns(IReadOnlyList<TextRun> textRuns, int collapsedLength,
+            TextRun shapedSymbol)
+        {
+            if (collapsedLength <= 0)
+            {
+                return new[] { shapedSymbol };
+            }
+
+            var objectPool = FormattingObjectPool.Instance;
+
+            var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
+
+            var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
+            preSplitRuns.CopyTo(collapsedRuns);
+            collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
+
+            objectPool.TextRunLists.Return(ref preSplitRuns);
+            objectPool.TextRunLists.Return(ref postSplitRuns);
+
+            return collapsedRuns;
+        }
     }
 }

+ 137 - 85
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -1,15 +1,22 @@
-using System;
+// ReSharper disable ForCanBeConvertedToForeach
+using System;
 using System.Buffers;
 using System.Collections.Generic;
+using System.Linq;
 using System.Runtime.InteropServices;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Utilities;
+using static Avalonia.Media.TextFormatting.FormattingObjectPool;
 
 namespace Avalonia.Media.TextFormatting
 {
-    internal class TextFormatterImpl : TextFormatter
+    internal sealed class TextFormatterImpl : TextFormatter
     {
         private static readonly char[] s_empty = { ' ' };
+        private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength];
+
+        [ThreadStatic] private static BidiData? t_bidiData;
+        [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm;
 
         /// <inheritdoc cref="TextFormatter.FormatLine"/>
         public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
@@ -18,20 +25,25 @@ namespace Avalonia.Media.TextFormatting
             var textWrapping = paragraphProperties.TextWrapping;
             FlowDirection resolvedFlowDirection;
             TextLineBreak? nextLineBreak = null;
-            IReadOnlyList<TextRun> textRuns;
+            IReadOnlyList<TextRun>? textRuns;
+            var objectPool = FormattingObjectPool.Instance;
 
-            var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex,
+            var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
                 out var textEndOfLine, out var textSourceLength);
 
-            if (previousLineBreak?.RemainingRuns != null)
+            RentedList<TextRun>? shapedTextRuns;
+
+            if (previousLineBreak?.RemainingRuns is { } remainingRuns)
             {
                 resolvedFlowDirection = previousLineBreak.FlowDirection;
-                textRuns = previousLineBreak.RemainingRuns;
+                textRuns = remainingRuns;
                 nextLineBreak = previousLineBreak;
+                shapedTextRuns = null;
             }
             else
             {
-                textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection);
+                shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection);
+                textRuns = shapedTextRuns;
 
                 if (nextLineBreak == null && textEndOfLine != null)
                 {
@@ -44,25 +56,32 @@ namespace Avalonia.Media.TextFormatting
             switch (textWrapping)
             {
                 case TextWrapping.NoWrap:
-                    {
-                        textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength,
-                            paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
+                {
+                    // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
+                    // which already uses an array: ToArray() won't ever be called in this case
+                    var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
 
-                        textLine.FinalizeLine();
+                    textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
+                        paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
 
-                        break;
-                    }
+                    textLine.FinalizeLine();
+
+                    break;
+                }
                 case TextWrapping.WrapWithOverflow:
                 case TextWrapping.Wrap:
-                    {
-                        textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
-                            resolvedFlowDirection, nextLineBreak);
-                        break;
-                    }
+                {
+                    textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
+                        paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
+                    break;
+                }
                 default:
                     throw new ArgumentOutOfRangeException(nameof(textWrapping));
             }
 
+            objectPool.TextRunLists.Return(ref shapedTextRuns);
+            objectPool.TextRunLists.Return(ref fetchedRuns);
+
             return textLine;
         }
 
@@ -71,9 +90,12 @@ namespace Avalonia.Media.TextFormatting
         /// </summary>
         /// <param name="textRuns">The text run's.</param>
         /// <param name="length">The length to split at.</param>
+        /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
         /// <returns>The split text runs.</returns>
-        internal static SplitResult<IReadOnlyList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length)
+        internal static SplitResult<RentedList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length,
+            FormattingObjectPool objectPool)
         {
+            var first = objectPool.TextRunLists.Rent();
             var currentLength = 0;
 
             for (var i = 0; i < textRuns.Count; i++)
@@ -89,8 +111,6 @@ namespace Avalonia.Media.TextFormatting
 
                 var firstCount = currentRun.Length >= 1 ? i + 1 : i;
 
-                var first = new List<TextRun>(firstCount);
-
                 if (firstCount > 1)
                 {
                     for (var j = 0; j < i; j++)
@@ -103,7 +123,7 @@ namespace Avalonia.Media.TextFormatting
 
                 if (currentLength + currentRun.Length == length)
                 {
-                    var second = secondCount > 0 ? new List<TextRun>(secondCount) : null;
+                    var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null;
 
                     if (second != null)
                     {
@@ -117,13 +137,13 @@ namespace Avalonia.Media.TextFormatting
 
                     first.Add(currentRun);
 
-                    return new SplitResult<IReadOnlyList<TextRun>>(first, second);
+                    return new SplitResult<RentedList<TextRun>>(first, second);
                 }
                 else
                 {
                     secondCount++;
 
-                    var second = new List<TextRun>(secondCount);
+                    var second = objectPool.TextRunLists.Rent();
 
                     if (currentRun is ShapedTextRun shapedTextCharacters)
                     {
@@ -139,11 +159,16 @@ namespace Avalonia.Media.TextFormatting
                         second.Add(textRuns[i + j]);
                     }
 
-                    return new SplitResult<IReadOnlyList<TextRun>>(first, second);
+                    return new SplitResult<RentedList<TextRun>>(first, second);
                 }
             }
 
-            return new SplitResult<IReadOnlyList<TextRun>>(textRuns, null);
+            for (var i = 0; i < textRuns.Count; i++)
+            {
+                first.Add(textRuns[i]);
+            }
+
+            return new SplitResult<RentedList<TextRun>>(first, null);
         }
 
         /// <summary>
@@ -152,40 +177,58 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="textRuns">The text runs to shape.</param>
         /// <param name="paragraphProperties">The default paragraph properties.</param>
         /// <param name="resolvedFlowDirection">The resolved flow direction.</param>
+        /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
         /// <returns>
         /// A list of shaped text characters.
         /// </returns>
-        private static List<TextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
+        private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> textRuns,
+            TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool,
             out FlowDirection resolvedFlowDirection)
         {
             var flowDirection = paragraphProperties.FlowDirection;
-            var shapedRuns = new List<TextRun>();
-            using var biDiData = new BidiData((sbyte)flowDirection);
+            var shapedRuns = objectPool.TextRunLists.Rent();
 
-            foreach (var textRun in textRuns)
+            if (textRuns.Count == 0)
             {
-                if (textRun.Text.IsEmpty)
-                {
-                    biDiData.Append(new char[textRun.Length]);
-                }
-                else
-                {
-                    biDiData.Append(textRun.Text.Span);
-                }
+                resolvedFlowDirection = flowDirection;
+                return shapedRuns;
             }
 
-            using var biDi = new BidiAlgorithm();
+            var bidiData = t_bidiData ??= new();
+            bidiData.Reset();
+            bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection;
+
+            for (var i = 0; i < textRuns.Count; ++i)
+            {
+                var textRun = textRuns[i];
+
+                ReadOnlySpan<char> text;
+                if (!textRun.Text.IsEmpty)
+                    text = textRun.Text.Span;
+                else if (textRun.Length == TextRun.DefaultTextSourceLength)
+                    text = s_defaultText;
+                else
+                    text = new char[textRun.Length];
+
+                bidiData.Append(text);
+            }
 
-            biDi.Process(biDiData);
+            var bidiAlgorithm = t_bidiAlgorithm ??= new();
+            bidiAlgorithm.Process(bidiData);
 
-            var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes);
+            var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes);
 
             resolvedFlowDirection =
                 (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
 
-            var processedRuns = new List<TextRun>(textRuns.Count);
+            var processedRuns = objectPool.TextRunLists.Rent();
+
+            CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns);
 
-            CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns);
+            bidiData.Reset();
+            bidiAlgorithm.Reset();
+
+            var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
 
             for (var index = 0; index < processedRuns.Count; index++)
             {
@@ -194,8 +237,9 @@ namespace Avalonia.Media.TextFormatting
                 switch (currentRun)
                 {
                     case UnshapedTextRun shapeableRun:
-                        {
-                            var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
+                    {
+                            groupedRuns.Clear();
+                            groupedRuns.Add(shapeableRun);
                             var text = shapeableRun.Text;
 
                             while (index + 1 < processedRuns.Count)
@@ -224,7 +268,7 @@ namespace Avalonia.Media.TextFormatting
                                          shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
                                          paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
 
-                            shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
+                            ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns);
 
                             break;
                         }
@@ -237,6 +281,9 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
+            objectPool.TextRunLists.Return(ref processedRuns);
+            objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
+
             return shapedRuns;
         }
 
@@ -303,17 +350,14 @@ namespace Avalonia.Media.TextFormatting
             }
         }
 
-
         private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y)
             => MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize)
                && x.Typeface == y.Typeface
                && x.BaselineAlignment == y.BaselineAlignment;
 
-        private static IReadOnlyList<ShapedTextRun> ShapeTogether(
-            IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text, TextShaperOptions options)
+        private static void ShapeTogether(IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text,
+            TextShaperOptions options, RentedList<TextRun> results)
         {
-            var shapedRuns = new List<ShapedTextRun>(textRuns.Count);
-
             var shapedBuffer = TextShaper.Current.ShapeText(text, options);
 
             for (var i = 0; i < textRuns.Count; i++)
@@ -322,12 +366,10 @@ namespace Avalonia.Media.TextFormatting
 
                 var splitResult = shapedBuffer.Split(currentRun.Length);
 
-                shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
+                results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
 
                 shapedBuffer = splitResult.Second!;
             }
-
-            return shapedRuns;
         }
 
         /// <summary>
@@ -335,10 +377,10 @@ namespace Avalonia.Media.TextFormatting
         /// </summary>
         /// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
         /// <param name="levels">The bidi levels.</param>
-        /// <param name="processedRuns"></param>
+        /// <param name="processedRuns">A list that will be filled with the processed runs.</param>
         /// <returns></returns>
-        private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels,
-            List<TextRun> processedRuns)
+        private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ReadOnlySpan<sbyte> levels,
+            RentedList<TextRun> processedRuns)
         {
             if (levels.Length == 0)
             {
@@ -385,7 +427,8 @@ namespace Avalonia.Media.TextFormatting
 
                     if (j == runTextSpan.Length)
                     {
-                        processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
+                        currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties,
+                            processedRuns);
 
                         runLevel = levels[levelIndex];
 
@@ -398,7 +441,8 @@ namespace Avalonia.Media.TextFormatting
                     }
 
                     // End of this run
-                    processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
+                    currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties,
+                        processedRuns);
 
                     runText = runText.Slice(j);
                     runTextSpan = runText.Span;
@@ -415,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
                 return;
             }
 
-            processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties));
+            currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns);
         }
 
         /// <summary>
@@ -423,19 +467,20 @@ namespace Avalonia.Media.TextFormatting
         /// </summary>
         /// <param name="textSource">The text source.</param>
         /// <param name="firstTextSourceIndex">The first text source index.</param>
-        /// <param name="endOfLine"></param>
-        /// <param name="textSourceLength"></param>
+        /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
+        /// <param name="endOfLine">On return, the end of line, if any.</param>
+        /// <param name="textSourceLength">On return, the processed text source length.</param>
         /// <returns>
         /// The formatted text runs.
         /// </returns>
-        private static List<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
-            out TextEndOfLine? endOfLine, out int textSourceLength)
+        private static RentedList<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
+            FormattingObjectPool objectPool, out TextEndOfLine? endOfLine, out int textSourceLength)
         {
             textSourceLength = 0;
 
             endOfLine = null;
 
-            var textRuns = new List<TextRun>();
+            var textRuns = objectPool.TextRunLists.Rent();
 
             var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
 
@@ -529,8 +574,10 @@ namespace Avalonia.Media.TextFormatting
             measuredLength = 0;
             var currentWidth = 0.0;
 
-            foreach (var currentRun in textRuns)
+            for (var i = 0; i < textRuns.Count; ++i)
             {
+                var currentRun = textRuns[i];
+
                 switch (currentRun)
                 {
                     case ShapedTextRun shapedTextCharacters:
@@ -540,15 +587,15 @@ namespace Avalonia.Media.TextFormatting
                                 var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
                                 var lastCluster = firstCluster;
 
-                                for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
+                                for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++)
                                 {
-                                    var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
+                                    var glyphInfo = shapedTextCharacters.ShapedBuffer[j];
 
                                     if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
                                     {
                                         measuredLength += Math.Max(0, lastCluster - firstCluster);
 
-                                        goto found;
+                                        return measuredLength != 0;
                                     }
 
                                     lastCluster = glyphInfo.GlyphCluster;
@@ -565,7 +612,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
                             {
-                                goto found;
+                                return measuredLength != 0;
                             }
 
                             measuredLength += currentRun.Length;
@@ -582,8 +629,6 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-        found:
-
             return measuredLength != 0;
         }
 
@@ -591,20 +636,23 @@ namespace Avalonia.Media.TextFormatting
         /// Creates an empty text line.
         /// </summary>
         /// <returns>The empty text line.</returns>
-        public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties)
+        public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth,
+            TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool)
         {
             var flowDirection = paragraphProperties.FlowDirection;
             var properties = paragraphProperties.DefaultTextRunProperties;
             var glyphTypeface = properties.Typeface.GlyphTypeface;
             var glyph = glyphTypeface.GetGlyph(s_empty[0]);
-            var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
+            var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) };
 
             var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
                 (sbyte)flowDirection);
 
-            var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) };
+            var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) };
 
-            return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
+            var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection);
+            line.FinalizeLine();
+            return line;
         }
 
         /// <summary>
@@ -616,14 +664,15 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="paragraphProperties">The text paragraph properties.</param>
         /// <param name="resolvedFlowDirection"></param>
         /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
+        /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
         /// <returns>The wrapped text line.</returns>
         private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex,
             double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
-            TextLineBreak? currentLineBreak)
+            TextLineBreak? currentLineBreak, FormattingObjectPool objectPool)
         {
             if (textRuns.Count == 0)
             {
-                return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
+                return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool);
             }
 
             if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@@ -749,12 +798,10 @@ namespace Avalonia.Media.TextFormatting
                 break;
             }
 
-            var splitResult = SplitTextRuns(textRuns, measuredLength);
-
-            var remainingCharacters = splitResult.Second;
+            var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
 
-            var lineBreak = remainingCharacters?.Count > 0 ?
-                new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) :
+            var lineBreak = postSplitRuns?.Count > 0 ?
+                new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
                 null;
 
             if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
@@ -762,11 +809,16 @@ namespace Avalonia.Media.TextFormatting
                 lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
             }
 
-            var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength,
+            var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
                 paragraphWidth, paragraphProperties, resolvedFlowDirection,
                 lineBreak);
 
-            return textLine.FinalizeLine();
+            textLine.FinalizeLine();
+
+            objectPool.TextRunLists.Return(ref preSplitRuns);
+            objectPool.TextRunLists.Return(ref postSplitRuns);
+
+            return textLine;
         }
 
         private struct TextRunEnumerator

+ 42 - 30
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
@@ -13,6 +12,7 @@ namespace Avalonia.Media.TextFormatting
         private readonly ITextSource _textSource;
         private readonly TextParagraphProperties _paragraphProperties;
         private readonly TextTrimming _textTrimming;
+        private readonly TextLine[] _textLines;
 
         private int _textSourceLength;
 
@@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
 
             MaxLines = maxLines;
 
-            TextLines = CreateTextLines();
+            _textLines = CreateTextLines();
         }
 
         /// <summary>
@@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting
 
             MaxLines = maxLines;
 
-            TextLines = CreateTextLines();
+            _textLines = CreateTextLines();
         }
 
         /// <summary>
@@ -147,7 +147,8 @@ namespace Avalonia.Media.TextFormatting
         /// <value>
         /// The text lines.
         /// </value>
-        public IReadOnlyList<TextLine> TextLines { get; private set; }
+        public IReadOnlyList<TextLine> TextLines
+            => _textLines;
 
         /// <summary>
         /// Gets the bounds of the layout.
@@ -164,14 +165,14 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="origin">The origin.</param>
         public void Draw(DrawingContext context, Point origin)
         {
-            if (!TextLines.Any())
+            if (_textLines.Length == 0)
             {
                 return;
             }
 
             var (currentX, currentY) = origin;
 
-            foreach (var textLine in TextLines)
+            foreach (var textLine in _textLines)
             {
                 textLine.Draw(context, new Point(currentX + textLine.Start, currentY));
 
@@ -186,7 +187,7 @@ namespace Avalonia.Media.TextFormatting
         /// <returns></returns>
         public Rect HitTestTextPosition(int textPosition)
         {
-            if (TextLines.Count == 0)
+            if (_textLines.Length == 0)
             {
                 return new Rect();
             }
@@ -198,7 +199,7 @@ namespace Avalonia.Media.TextFormatting
 
             var currentY = 0.0;
 
-            foreach (var textLine in TextLines)
+            foreach (var textLine in _textLines)
             {
                 var end = textLine.FirstTextSourceIndex + textLine.Length;
 
@@ -230,11 +231,11 @@ namespace Avalonia.Media.TextFormatting
                 return Array.Empty<Rect>();
             }
 
-            var result = new List<Rect>(TextLines.Count);
+            var result = new List<Rect>(_textLines.Length);
 
             var currentY = 0d;
 
-            foreach (var textLine in TextLines)
+            foreach (var textLine in _textLines)
             {
                 //Current line isn't covered.
                 if (textLine.FirstTextSourceIndex + textLine.Length < start)
@@ -284,13 +285,12 @@ namespace Avalonia.Media.TextFormatting
         {
             var currentY = 0d;
 
-            var lineIndex = 0;
             TextLine? currentLine = null;
             CharacterHit characterHit;
 
-            for (; lineIndex < TextLines.Count; lineIndex++)
+            for (var lineIndex = 0; lineIndex < _textLines.Length; lineIndex++)
             {
-                currentLine = TextLines[lineIndex];
+                currentLine = _textLines[lineIndex];
 
                 if (currentY + currentLine.Height > point.Y)
                 {
@@ -322,12 +322,12 @@ namespace Avalonia.Media.TextFormatting
 
             if (charIndex > _textSourceLength)
             {
-                return TextLines.Count - 1;
+                return _textLines.Length - 1;
             }
 
-            for (var index = 0; index < TextLines.Count; index++)
+            for (var index = 0; index < _textLines.Length; index++)
             {
-                var textLine = TextLines[index];
+                var textLine = _textLines[index];
 
                 if (textLine.FirstTextSourceIndex + textLine.Length < charIndex)
                 {
@@ -341,7 +341,7 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            return TextLines.Count - 1;
+            return _textLines.Length - 1;
         }
 
         private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
@@ -424,18 +424,21 @@ namespace Avalonia.Media.TextFormatting
             height += textLine.Height;
         }
 
-        private IReadOnlyList<TextLine> CreateTextLines()
+        private TextLine[] CreateTextLines()
         {
+            var objectPool = FormattingObjectPool.Instance;
+
             if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
             {
-                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
+                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties,
+                    FormattingObjectPool.Instance);
 
                 Bounds = new Rect(0, 0, 0, textLine.Height);
 
-                return new List<TextLine> { textLine };
+                return new TextLine[] { textLine };
             }
 
-            var textLines = new List<TextLine>();
+            var textLines = objectPool.TextLines.Rent();
 
             double left = double.PositiveInfinity, width = 0.0, height = 0.0;
 
@@ -443,16 +446,19 @@ namespace Avalonia.Media.TextFormatting
 
             TextLine? previousLine = null;
 
+            var textFormatter = TextFormatter.Current;
+
             while (true)
             {
-                var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
-                    _paragraphProperties, previousLine?.TextLineBreak);
+                var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties,
+                    previousLine?.TextLineBreak);
 
-                if(textLine == null || textLine.Length == 0)
+                if (textLine.Length == 0)
                 {
                     if (previousLine != null && previousLine.NewLineLength > 0)
                     {
-                        var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
+                        var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
+                            _paragraphProperties, objectPool);
 
                         textLines.Add(emptyTextLine);
 
@@ -494,7 +500,7 @@ namespace Avalonia.Media.TextFormatting
                 //Fulfill max lines constraint
                 if (MaxLines > 0 && textLines.Count >= MaxLines)
                 {
-                    if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null)
+                    if(textLine.TextLineBreak?.RemainingRuns is not null)
                     {
                         textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
                     }
@@ -511,7 +517,7 @@ namespace Avalonia.Media.TextFormatting
             //Make sure the TextLayout always contains at least on empty line
             if (textLines.Count == 0)
             {
-                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
+                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool);
 
                 textLines.Add(textLine);
 
@@ -524,8 +530,9 @@ namespace Avalonia.Media.TextFormatting
             {
                 var whitespaceWidth = 0d;
 
-                foreach (var line in textLines)
+                for (var i = 0; i < textLines.Count; i++)
                 {
+                    var line = textLines[i];
                     var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
 
                     if (lineWhitespaceWidth > whitespaceWidth)
@@ -549,7 +556,12 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            return textLines;
+            var result = textLines.ToArray();
+
+            objectPool.TextLines.Return(ref textLines);
+            objectPool.VerifyAllReturned();
+
+            return result;
         }
 
         /// <summary>
@@ -569,7 +581,7 @@ namespace Avalonia.Media.TextFormatting
 
         public void Dispose()
         {
-            foreach (var line in TextLines)
+            foreach (var line in _textLines)
             {
                 line.Dispose();
             }

+ 62 - 42
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -1,5 +1,7 @@
-using System;
+// ReSharper disable ForCanBeConvertedToForeach
+using System;
 using System.Collections.Generic;
+using static Avalonia.Media.TextFormatting.FormattingObjectPool;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -39,11 +41,12 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextRun Symbol { get; }
 
-        public override List<TextRun>? Collapse(TextLine textLine)
+        /// <inheritdoc />
+        public override TextRun[]? Collapse(TextLine textLine)
         {
             var textRuns = textLine.TextRuns;
 
-            if (textRuns == null || textRuns.Count == 0)
+            if (textRuns.Count == 0)
             {
                 return null;
             }
@@ -54,7 +57,7 @@ namespace Avalonia.Media.TextFormatting
 
             if (Width < shapedSymbol.GlyphRun.Size.Width)
             {
-                return new List<TextRun>(0);
+                return Array.Empty<TextRun>();
             }
 
             // Overview of ellipsis structure
@@ -75,55 +78,66 @@ namespace Avalonia.Media.TextFormatting
                             {
                                 shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
 
-                                var collapsedRuns = new List<TextRun>(textRuns.Count);
-
                                 if (measuredLength > 0)
                                 {
-                                    IReadOnlyList<TextRun>? preSplitRuns = null;
-                                    IReadOnlyList<TextRun>? postSplitRuns;
+                                    var objectPool = FormattingObjectPool.Instance;
 
-                                    if (_prefixLength > 0)
-                                    {
-                                        var splitResult = TextFormatterImpl.SplitTextRuns(textRuns,
-                                            Math.Min(_prefixLength, measuredLength));
+                                    var collapsedRuns = objectPool.TextRunLists.Rent();
 
-                                        collapsedRuns.AddRange(splitResult.First);
+                                    RentedList<TextRun>? rentedPreSplitRuns = null;
+                                    RentedList<TextRun>? rentedPostSplitRuns = null;
+                                    TextRun[]? results;
 
-                                        preSplitRuns = splitResult.First;
-                                        postSplitRuns = splitResult.Second;
-                                    }
-                                    else
+                                    try
                                     {
-                                        postSplitRuns = textRuns;
-                                    }
+                                        IReadOnlyList<TextRun>? effectivePostSplitRuns;
 
-                                    collapsedRuns.Add(shapedSymbol);
+                                        if (_prefixLength > 0)
+                                        {
+                                            (rentedPreSplitRuns, rentedPostSplitRuns) = TextFormatterImpl.SplitTextRuns(
+                                                textRuns, Math.Min(_prefixLength, measuredLength), objectPool);
 
-                                    if (measuredLength <= _prefixLength || postSplitRuns is null)
-                                    {
-                                        return collapsedRuns;
-                                    }
+                                            effectivePostSplitRuns = rentedPostSplitRuns;
+
+                                            foreach (var preSplitRun in rentedPreSplitRuns)
+                                            {
+                                                collapsedRuns.Add(preSplitRun);
+                                            }
+                                        }
+                                        else
+                                        {
+                                            effectivePostSplitRuns = textRuns;
+                                        }
 
-                                    var availableSuffixWidth = availableWidth;
+                                        collapsedRuns.Add(shapedSymbol);
 
-                                    if (preSplitRuns is not null)
-                                    {
-                                        foreach (var run in preSplitRuns)
+                                        if (measuredLength <= _prefixLength || effectivePostSplitRuns is null)
                                         {
-                                            if (run is DrawableTextRun drawableTextRun)
+                                            results = collapsedRuns.ToArray();
+                                            objectPool.TextRunLists.Return(ref collapsedRuns);
+                                            return results;
+                                        }
+
+                                        var availableSuffixWidth = availableWidth;
+
+                                        if (rentedPreSplitRuns is not null)
+                                        {
+                                            foreach (var run in rentedPreSplitRuns)
                                             {
-                                                availableSuffixWidth -= drawableTextRun.Size.Width;
+                                                if (run is DrawableTextRun drawableTextRun)
+                                                {
+                                                    availableSuffixWidth -= drawableTextRun.Size.Width;
+                                                }
                                             }
                                         }
-                                    }
-
-                                    for (var i = postSplitRuns.Count - 1; i >= 0; i--)
-                                    {
-                                        var run = postSplitRuns[i];
 
-                                        switch (run)
+                                        for (var i = effectivePostSplitRuns.Count - 1; i >= 0; i--)
                                         {
-                                            case ShapedTextRun endShapedRun:
+                                            var run = effectivePostSplitRuns[i];
+
+                                            switch (run)
+                                            {
+                                                case ShapedTextRun endShapedRun:
                                                 {
                                                     if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
                                                             out var suffixCount, out var suffixWidth))
@@ -141,15 +155,21 @@ namespace Avalonia.Media.TextFormatting
 
                                                     break;
                                                 }
+                                            }
                                         }
                                     }
-                                }
-                                else
-                                {
-                                    collapsedRuns.Add(shapedSymbol);
+                                    finally
+                                    {
+                                        objectPool.TextRunLists.Return(ref rentedPreSplitRuns);
+                                        objectPool.TextRunLists.Return(ref rentedPostSplitRuns);
+                                    }
+
+                                    results = collapsedRuns.ToArray();
+                                    objectPool.TextRunLists.Return(ref collapsedRuns);
+                                    return results;
                                 }
 
-                                return collapsedRuns;
+                                return new TextRun[] { shapedSymbol };
                             }
 
                             availableWidth -= shapedRun.Size.Width;

+ 32 - 262
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -1,19 +1,18 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
-    internal class TextLineImpl : TextLine
+    internal sealed class TextLineImpl : TextLine
     {
-        private IReadOnlyList<TextRun> _textRuns;
+        private readonly TextRun[] _textRuns;
         private readonly double _paragraphWidth;
         private readonly TextParagraphProperties _paragraphProperties;
         private TextLineMetrics _textLineMetrics;
         private readonly FlowDirection _resolvedFlowDirection;
 
-        public TextLineImpl(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
+        public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
             TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
             TextLineBreak? lineBreak = null, bool hasCollapsed = false)
         {
@@ -147,7 +146,7 @@ namespace Avalonia.Media.TextFormatting
             var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
                 _resolvedFlowDirection, TextLineBreak, true);
 
-            if (collapsedRuns.Count > 0)
+            if (collapsedRuns.Length > 0)
             {
                 collapsedLine.FinalizeLine();
             }
@@ -166,7 +165,7 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override CharacterHit GetCharacterHitFromDistance(double distance)
         {
-            if (_textRuns.Count == 0)
+            if (_textRuns.Length == 0)
             {
                 return new CharacterHit();
             }
@@ -182,7 +181,7 @@ namespace Avalonia.Media.TextFormatting
 
             if (distance >= WidthIncludingTrailingWhitespace)
             {
-                var lastRun = _textRuns[_textRuns.Count - 1];
+                var lastRun = _textRuns[_textRuns.Length - 1];
 
                 var size = 0.0;
 
@@ -199,7 +198,7 @@ namespace Avalonia.Media.TextFormatting
             var currentPosition = FirstTextSourceIndex;
             var currentDistance = 0.0;
 
-            for (var i = 0; i < _textRuns.Count; i++)
+            for (var i = 0; i < _textRuns.Length; i++)
             {
                 var currentRun = _textRuns[i];
 
@@ -208,7 +207,7 @@ namespace Avalonia.Media.TextFormatting
                     var rightToLeftIndex = i;
                     currentPosition += currentRun.Length;
 
-                    while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
+                    while (rightToLeftIndex + 1 <= _textRuns.Length - 1)
                     {
                         var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun;
 
@@ -224,7 +223,7 @@ namespace Avalonia.Media.TextFormatting
 
                     for (var j = i; i <= rightToLeftIndex; j++)
                     {
-                        if (j > _textRuns.Count - 1)
+                        if (j > _textRuns.Length - 1)
                         {
                             break;
                         }
@@ -254,7 +253,7 @@ namespace Avalonia.Media.TextFormatting
 
                 if (currentRun is DrawableTextRun drawableTextRun)
                 {
-                    if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance)
+                    if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance)
                     {
                         currentDistance += drawableTextRun.Size.Width;
 
@@ -328,7 +327,7 @@ namespace Avalonia.Media.TextFormatting
 
             if (flowDirection == FlowDirection.LeftToRight)
             {
-                for (var index = 0; index < _textRuns.Count; index++)
+                for (var index = 0; index < _textRuns.Length; index++)
                 {
                     var currentRun = _textRuns[index];
 
@@ -338,7 +337,7 @@ namespace Avalonia.Media.TextFormatting
 
                         var rightToLeftWidth = shapedRun.Size.Width;
 
-                        while (i + 1 <= _textRuns.Count - 1)
+                        while (i + 1 <= _textRuns.Length - 1)
                         {
                             var nextRun = _textRuns[i + 1];
 
@@ -402,7 +401,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 currentDistance += WidthIncludingTrailingWhitespace;
 
-                for (var index = _textRuns.Count - 1; index >= 0; index--)
+                for (var index = _textRuns.Length - 1; index >= 0; index--)
                 {
                     var currentRun = _textRuns[index];
 
@@ -502,7 +501,7 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
         {
-            if (_textRuns.Count == 0)
+            if (_textRuns.Length == 0)
             {
                 return new CharacterHit();
             }
@@ -568,7 +567,7 @@ namespace Avalonia.Media.TextFormatting
         {
             var characterIndex = firstTextSourceIndex + textLength;
 
-            var result = new List<TextBounds>(TextRuns.Count);
+            var result = new List<TextBounds>(_textRuns.Length);
             var lastDirection = FlowDirection.LeftToRight;
             var currentDirection = lastDirection;
 
@@ -581,9 +580,9 @@ namespace Avalonia.Media.TextFormatting
 
             TextRunBounds lastRunBounds = default;
 
-            for (var index = 0; index < TextRuns.Count; index++)
+            for (var index = 0; index < _textRuns.Length; index++)
             {
-                if (TextRuns[index] is not DrawableTextRun currentRun)
+                if (_textRuns[index] is not DrawableTextRun currentRun)
                 {
                     continue;
                 }
@@ -637,7 +636,7 @@ namespace Avalonia.Media.TextFormatting
                         var rightToLeftIndex = index;
                         var rightToLeftWidth = currentShapedRun.Size.Width;
 
-                        while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
+                        while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
                         {
                             if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
                             {
@@ -669,12 +668,12 @@ namespace Avalonia.Media.TextFormatting
 
                         for (int i = rightToLeftIndex - 1; i >= index; i--)
                         {
-                            if (TextRuns[i] is not ShapedTextRun)
+                            if (_textRuns[i] is not ShapedTextRun shapedRun)
                             {
                                 continue;
                             }
 
-                            currentShapedRun = (ShapedTextRun)TextRuns[i];
+                            currentShapedRun = shapedRun;
 
                             currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
 
@@ -784,7 +783,7 @@ namespace Avalonia.Media.TextFormatting
         {
             var characterIndex = firstTextSourceIndex + textLength;
 
-            var result = new List<TextBounds>(TextRuns.Count);
+            var result = new List<TextBounds>(_textRuns.Length);
             var lastDirection = FlowDirection.LeftToRight;
             var currentDirection = lastDirection;
 
@@ -795,9 +794,9 @@ namespace Avalonia.Media.TextFormatting
             double currentWidth = 0;
             var currentRect = default(Rect);
 
-            for (var index = TextRuns.Count - 1; index >= 0; index--)
+            for (var index = _textRuns.Length - 1; index >= 0; index--)
             {
-                if (TextRuns[index] is not DrawableTextRun currentRun)
+                if (_textRuns[index] is not DrawableTextRun currentRun)
                 {
                     continue;
                 }
@@ -981,7 +980,7 @@ namespace Avalonia.Media.TextFormatting
 
         public override void Dispose()
         {
-            for (int i = 0; i < _textRuns.Count; i++)
+            for (int i = 0; i < _textRuns.Length; i++)
             {
                 if (_textRuns[i] is ShapedTextRun shapedTextRun)
                 {
@@ -990,186 +989,11 @@ namespace Avalonia.Media.TextFormatting
             }
         }
 
-        public TextLineImpl FinalizeLine()
+        public void FinalizeLine()
         {
             _textLineMetrics = CreateLineMetrics();
 
-            BidiReorder();
-
-            return this;
-        }
-
-        private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
-        {
-            if (run is ShapedTextRun shapedTextCharacters)
-            {
-                return shapedTextCharacters.BidiLevel;
-            }
-
-            var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
-
-            return (sbyte)defaultLevel;
-        }
-
-        private void BidiReorder()
-        {
-            if (_textRuns.Count == 0)
-            {
-                return;
-            }
-
-            // Build up the collection of ordered runs.
-            var run = _textRuns[0];
-
-            OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection));
-
-            var current = orderedRun;
-
-            for (var i = 1; i < _textRuns.Count; i++)
-            {
-                run = _textRuns[i];
-
-                current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection));
-
-                current = current.Next;
-            }
-
-            // Reorder them into visual order.
-            orderedRun = LinearReOrder(orderedRun);
-
-            // Now perform a recursive reversal of each run.
-            // From the highest level found in the text to the lowest odd level on each line, including intermediate levels
-            // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
-            // https://unicode.org/reports/tr9/#L2
-            sbyte max = 0;
-            var min = sbyte.MaxValue;
-
-            for (var i = 0; i < _textRuns.Count; i++)
-            {
-                var currentRun = _textRuns[i];
-
-                var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection);
-
-                if (level > max)
-                {
-                    max = level;
-                }
-
-                if ((level & 1) != 0 && level < min)
-                {
-                    min = level;
-                }
-            }
-
-            if (min > max)
-            {
-                min = max;
-            }
-
-            if (max == 0 || (min == max && (max & 1) == 0))
-            {
-                // Nothing to reverse.
-                return;
-            }
-
-            // Now apply the reversal and replace the original contents.
-            var minLevelToReverse = max;
-
-            while (minLevelToReverse >= min)
-            {
-                current = orderedRun;
-
-                while (current != null)
-                {
-                    if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
-                    {
-                        if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
-                        {
-                            shapedTextCharacters.Reverse();
-                        }
-                    }
-
-                    current = current.Next;
-                }
-
-                minLevelToReverse--;
-            }
-
-            var textRuns = new List<TextRun>(_textRuns.Count);
-
-            current = orderedRun;
-
-            while (current != null)
-            {
-                textRuns.Add(current.Run);
-
-                current = current.Next;
-            }
-
-            _textRuns = textRuns;
-        }
-
-        /// <summary>
-        /// Reorders a series of runs from logical to visual order, returning the left most run.
-        /// <see href="https://github.com/fribidi/linear-reorder/blob/f2f872257d4d8b8e137fcf831f254d6d4db79d3c/linear-reorder.c"/>
-        /// </summary>
-        /// <param name="run">The ordered bidi run.</param>
-        /// <returns>The <see cref="OrderedBidiRun"/>.</returns>
-        private static OrderedBidiRun LinearReOrder(OrderedBidiRun? run)
-        {
-            BidiRange? range = null;
-
-            while (run != null)
-            {
-                var next = run.Next;
-
-                while (range != null && range.Level > run.Level
-                    && range.Previous != null && range.Previous.Level >= run.Level)
-                {
-                    range = BidiRange.MergeWithPrevious(range);
-                }
-
-                if (range != null && range.Level >= run.Level)
-                {
-                    // Attach run to the range.
-                    if ((run.Level & 1) != 0)
-                    {
-                        // Odd, range goes to the right of run.
-                        run.Next = range.Left;
-                        range.Left = run;
-                    }
-                    else
-                    {
-                        // Even, range goes to the left of run.
-                        range.Right!.Next = run;
-                        range.Right = run;
-                    }
-
-                    range.Level = run.Level;
-                }
-                else
-                {
-                    var r = new BidiRange();
-
-                    r.Left = r.Right = run;
-                    r.Level = run.Level;
-                    r.Previous = range;
-
-                    range = r;
-                }
-
-                run = next;
-            }
-
-            while (range?.Previous != null)
-            {
-                range = BidiRange.MergeWithPrevious(range);
-            }
-
-            // Terminate.
-            range!.Right!.Next = null;
-
-            return range.Left!;
+            BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
         }
 
         /// <summary>
@@ -1197,7 +1021,7 @@ namespace Avalonia.Media.TextFormatting
 
             var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
 
-            while (runIndex < _textRuns.Count)
+            while (runIndex < _textRuns.Length)
             {
                 var currentRun = _textRuns[runIndex];
 
@@ -1346,7 +1170,7 @@ namespace Avalonia.Media.TextFormatting
             textPosition = FirstTextSourceIndex;
             TextRun? previousRun = null;
 
-            while (runIndex < _textRuns.Count)
+            while (runIndex < _textRuns.Length)
             {
                 var currentRun = _textRuns[runIndex];
 
@@ -1395,7 +1219,7 @@ namespace Avalonia.Media.TextFormatting
                                 }
                             }
 
-                            if (runIndex + 1 >= _textRuns.Count)
+                            if (runIndex + 1 >= _textRuns.Length)
                             {
                                 return runIndex;
                             }
@@ -1411,7 +1235,7 @@ namespace Avalonia.Media.TextFormatting
                                 return runIndex;
                             }
 
-                            if (runIndex + 1 >= _textRuns.Count)
+                            if (runIndex + 1 >= _textRuns.Length)
                             {
                                 return runIndex;
                             }
@@ -1448,14 +1272,14 @@ namespace Avalonia.Media.TextFormatting
 
             var lineHeight = _paragraphProperties.LineHeight;
 
-            var lastRunIndex = _textRuns.Count - 1;
+            var lastRunIndex = _textRuns.Length - 1;
 
             if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine)
             {
                 lastRunIndex--;
             }
 
-            for (var index = 0; index < _textRuns.Count; index++)
+            for (var index = 0; index < _textRuns.Length; index++)
             {
                 switch (_textRuns[index])
                 {
@@ -1620,59 +1444,5 @@ namespace Avalonia.Media.TextFormatting
                     return 0;
             }
         }
-
-        private sealed class OrderedBidiRun
-        {
-            public OrderedBidiRun(TextRun run, sbyte level)
-            {
-                Run = run;
-                Level = level;
-            }
-
-            public sbyte Level { get; }
-
-            public TextRun Run { get; }
-
-            public OrderedBidiRun? Next { get; set; }
-        }
-
-        private sealed class BidiRange
-        {
-            public int Level { get; set; }
-
-            public OrderedBidiRun? Left { get; set; }
-
-            public OrderedBidiRun? Right { get; set; }
-
-            public BidiRange? Previous { get; set; }
-
-            public static BidiRange MergeWithPrevious(BidiRange range)
-            {
-                var previous = range.Previous;
-
-                BidiRange left;
-                BidiRange right;
-
-                if ((previous!.Level & 1) != 0)
-                {
-                    // Odd, previous goes to the right of range.
-                    left = range;
-                    right = previous;
-                }
-                else
-                {
-                    // Even, previous goes to the left of range.
-                    left = previous;
-                    right = range;
-                }
-
-                // Stitch them
-                left.Right!.Next = right.Left;
-                previous.Left = left.Left;
-                previous.Right = right.Right;
-
-                return previous;
-            }
-        }
     }
 }

+ 3 - 0
src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs

@@ -93,6 +93,9 @@ namespace Avalonia.Media.TextFormatting
         
         internal TextRunProperties WithTypeface(Typeface typeface)
         {
+            if (this is GenericTextRunProperties other && other.Typeface == typeface)
+                return this;
+
             return new GenericTextRunProperties(typeface, FontRenderingEmSize,
                 TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment);
         }

+ 3 - 4
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@@ -1,6 +1,4 @@
-using System.Collections.Generic;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
 {
     /// <summary>
     /// A collapsing properties to collapse whole line toward the end
@@ -26,7 +24,8 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextRun Symbol { get; }
 
-        public override List<TextRun>? Collapse(TextLine textLine)
+        /// <inheritdoc />
+        public override TextRun[]? Collapse(TextLine textLine)
         {
             return TextEllipsisHelper.Collapse(textLine, this, false);
         }

+ 3 - 5
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@@ -1,7 +1,4 @@
-using System.Collections.Generic;
-using Avalonia.Utilities;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
 {
     /// <summary>
     /// a collapsing properties to collapse whole line toward the end
@@ -31,7 +28,8 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextRun Symbol { get; }
 
-        public override List<TextRun>? Collapse(TextLine textLine)
+        /// <inheritdoc />
+        public override TextRun[]? Collapse(TextLine textLine)
         {
             return TextEllipsisHelper.Collapse(textLine, this, true);
         }

+ 67 - 33
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@@ -5,8 +5,6 @@
 using System;
 using System.Collections.Generic;
 using System.Runtime.CompilerServices;
-using System.Threading;
-using Avalonia.Collections.Pooled;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting.Unicode
@@ -28,8 +26,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
     /// as much as possible.
     /// </para>
     /// </remarks>
-    internal struct BidiAlgorithm : IDisposable
+    internal sealed class BidiAlgorithm
     {
+        /// <summary>
+        /// Whether the state is clean and can be reused without a reset.
+        /// </summary>
+        private bool _hasCleanState = true;
+
         /// <summary>
         /// The original BiDiClass classes as provided by the caller
         /// </summary>
@@ -67,7 +70,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// The forward mapping maps the start index to the end index.
         /// The reverse mapping maps the end index to the start index.
         /// </remarks>
-        private BidiDictionary<int, int>? _isolatePairs;
+        private readonly BidiDictionary<int, int> _isolatePairs = new();
 
         /// <summary>
         /// The working BiDi classes
@@ -98,7 +101,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// The status stack used during resolution of explicit
         /// embedding and isolating runs
         /// </summary>
-        private readonly Stack<Status> _statusStack = new Stack<Status>();
+        private readonly Stack<Status> _statusStack = new();
 
         /// <summary>
         /// Mapping used to virtually remove characters for rule X9
@@ -108,7 +111,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// <summary>
         /// Re-usable list of level runs
         /// </summary>
-        private readonly List<LevelRun> _levelRuns = new List<LevelRun>();
+        private readonly List<LevelRun> _levelRuns = new();
 
         /// <summary>
         /// Mapping for the current isolating sequence, built
@@ -119,7 +122,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// <summary>
         /// A stack of pending isolate openings used by FindIsolatePairs()
         /// </summary>
-        private Stack<int>? _pendingIsolateOpenings;
+        private readonly Stack<int> _pendingIsolateOpenings = new();
 
         /// <summary>
         /// The level of the isolating run currently being processed
@@ -175,12 +178,12 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// Reusable list of pending opening brackets used by the
         /// LocatePairedBrackets method
         /// </summary>
-        private readonly List<int> _pendingOpeningBrackets = new List<int>();
+        private readonly List<int> _pendingOpeningBrackets = new();
 
         /// <summary>
         /// Resolved list of paired brackets
         /// </summary>
-        private readonly List<BracketPair> _pairedBrackets = new List<BracketPair>();
+        private readonly List<BracketPair> _pairedBrackets = new();
 
         /// <summary>
         /// Initializes a new instance of the <see cref="BidiAlgorithm"/> class.
@@ -228,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
             ArraySlice<sbyte>? outLevels)
         {
             // Reset state
-            _isolatePairs?.Clear();
-            _workingClassesBuffer.Clear();
-            _levelRuns.Clear();
-            _resolvedLevelsBuffer.Clear();
+            Reset();
 
             if (types.IsEmpty)
             {
                 return;
             }
 
+            _hasCleanState = false;
+
             // Setup original types and working types
             _originalClasses = types;
             _workingClasses = _workingClassesBuffer.Add(types);
@@ -324,7 +326,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
                         // Skip isolate pairs
                         // (Because we're working with a slice, we need to adjust the indices
                         //  we're using for the isolatePairs map)
-                        if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true)
+                        if (_isolatePairs.TryGetValue(data.Start + i, out i))
                         {
                             i -= data.Start;
                         }
@@ -359,7 +361,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
             _hasIsolates = false;
 
             // BD9...
-            _pendingIsolateOpenings?.Clear();
+            _pendingIsolateOpenings.Clear();
             
             for (var i = 0; i < _originalClasses.Length; i++)
             {
@@ -371,16 +373,14 @@ namespace Avalonia.Media.TextFormatting.Unicode
                     case BidiClass.RightToLeftIsolate:
                     case BidiClass.FirstStrongIsolate:
                     {
-                        _pendingIsolateOpenings ??= new Stack<int>();
                         _pendingIsolateOpenings.Push(i);
                         _hasIsolates = true;
                         break;
                     }
                     case BidiClass.PopDirectionalIsolate:
                     {
-                        if (_pendingIsolateOpenings?.Count > 0)
+                        if (_pendingIsolateOpenings.Count > 0)
                         {
-                            _isolatePairs ??= new BidiDictionary<int, int>();
                             _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i);
                         }
 
@@ -501,7 +501,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
 
                         if (resolvedIsolate == BidiClass.FirstStrongIsolate)
                         {
-                            if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate))
+                            if (!_isolatePairs.TryGetValue(i, out var endOfIsolate))
                             {
                                 endOfIsolate = _originalClasses.Length;
                             }
@@ -832,7 +832,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
                     var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1];
                     var lastType = _originalClasses[lastCharacterIndex];
                     if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) &&
-                            _isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true)
+                            _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex))
                     {
                         // Find the continuing run index
                         runIndex = FindRunForIndex(nextRunIndex);
@@ -855,13 +855,14 @@ namespace Avalonia.Media.TextFormatting.Unicode
         private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel)
         {
             // Create mappings onto the underlying data
-            _runResolvedClasses = new MappedArraySlice<BidiClass>(_workingClasses, _isolatedRunMapping.AsSlice());
-            _runOriginalClasses = new MappedArraySlice<BidiClass>(_originalClasses, _isolatedRunMapping.AsSlice());
-            _runLevels = new MappedArraySlice<sbyte>(_resolvedLevels, _isolatedRunMapping.AsSlice());
+            var isolatedRunMapping = _isolatedRunMapping.AsSlice();
+            _runResolvedClasses = new MappedArraySlice<BidiClass>(_workingClasses, isolatedRunMapping);
+            _runOriginalClasses = new MappedArraySlice<BidiClass>(_originalClasses, isolatedRunMapping);
+            _runLevels = new MappedArraySlice<sbyte>(_resolvedLevels, isolatedRunMapping);
             if (_hasBrackets)
             {
-                _runBiDiPairedBracketTypes = new MappedArraySlice<BidiPairedBracketType>(_pairedBracketTypes, _isolatedRunMapping.AsSlice());
-                _runPairedBracketValues = new MappedArraySlice<int>(_pairedBracketValues, _isolatedRunMapping.AsSlice());
+                _runBiDiPairedBracketTypes = new MappedArraySlice<BidiPairedBracketType>(_pairedBracketTypes, isolatedRunMapping);
+                _runPairedBracketValues = new MappedArraySlice<int>(_pairedBracketValues, isolatedRunMapping);
             }
 
             _runLevel = runLevel;
@@ -1642,6 +1643,47 @@ namespace Avalonia.Media.TextFormatting.Unicode
             }
         }
 
+        /// <summary>
+        /// Resets the bidi algorithm to a clean state.
+        /// </summary>
+        public void Reset()
+        {
+            if (_hasCleanState)
+            {
+                return;
+            }
+
+            _originalClasses = default;
+            _pairedBracketTypes = default;
+            _pairedBracketValues = default;
+            _hasBrackets = default;
+            _hasEmbeddings = default;
+            _hasIsolates = default;
+            _isolatePairs.ClearThenResetIfTooLarge();
+            _workingClasses = default;
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _workingClassesBuffer);
+            _resolvedLevels = default;
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _resolvedLevelsBuffer);
+            _paragraphEmbeddingLevel = default;
+            FormattingBufferHelper.ClearThenResetIfTooLarge(_statusStack);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _x9Map);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(_levelRuns);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _isolatedRunMapping);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingIsolateOpenings);
+            _runLevel = default;
+            _runDirection = default;
+            _runLength = default;
+            _runResolvedClasses = default;
+            _runOriginalClasses = default;
+            _runLevels = default;
+            _runBiDiPairedBracketTypes = default;
+            _runPairedBracketValues = default;
+            FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingOpeningBrackets);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(_pairedBrackets);
+
+            _hasCleanState = true;
+        }
+
         /// <summary>
         /// Hold the start and end index of a pair of brackets
         /// </summary>
@@ -1717,13 +1759,5 @@ namespace Avalonia.Media.TextFormatting.Unicode
 
             public BidiClass Eos { get; }
         }
-
-        public void Dispose()
-        {
-            _workingClassesBuffer.Dispose();
-            _resolvedLevelsBuffer.Dispose();
-            _x9Map.Dispose();
-            _isolatedRunMapping.Dispose();
-        }
     }
 }

+ 38 - 15
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@@ -11,8 +11,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
     /// Represents a unicode string and all associated attributes
     /// for each character required for the bidirectional Unicode algorithm
     /// </summary>
-    internal struct BidiData : IDisposable
+    /// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
+    internal sealed class BidiData
     {
+        private bool _hasCleanState = true;
         private ArrayBuilder<BidiClass> _classes;
         private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes;
         private ArrayBuilder<int> _pairedBracketValues;
@@ -20,12 +22,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         private ArrayBuilder<BidiPairedBracketType> _savedPairedBracketTypes;
         private ArrayBuilder<sbyte> _tempLevelBuffer;
 
-        public BidiData(sbyte paragraphEmbeddingLevel)
-        {
-            ParagraphEmbeddingLevel = paragraphEmbeddingLevel;
-        }
-
-        public sbyte ParagraphEmbeddingLevel { get; private set; }
+        public sbyte ParagraphEmbeddingLevel { get; set; }
 
         public bool HasBrackets { get; private set; }
 
@@ -36,7 +33,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// <summary>
         /// Gets the length of the data held by the BidiData
         /// </summary>
-        public int Length{get; private set; }
+        public int Length { get; private set; }
 
         /// <summary>
         /// Gets the bidi character type of each code point
@@ -66,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// <param name="text">The text to process.</param>
         public void Append(ReadOnlySpan<char> text)
         {
+            _hasCleanState = false;
+
             _classes.Add(text.Length);
             _pairedBracketTypes.Add(text.Length);
             _pairedBracketValues.Add(text.Length);
@@ -151,6 +150,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// </remarks>
         public void SaveTypes()
         {
+            _hasCleanState = false;
+
             // Capture the types data
             _savedClasses.Clear();
             _savedClasses.Add(_classes.AsSlice());
@@ -163,6 +164,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// </summary>
         public void RestoreTypes()
         {
+            _hasCleanState = false;
+
             _classes.Clear();
             _classes.Add(_savedClasses.AsSlice());
             _pairedBracketTypes.Clear();
@@ -182,14 +185,34 @@ namespace Avalonia.Media.TextFormatting.Unicode
             return _tempLevelBuffer.Add(length, false);
         }
 
-        public void Dispose()
+        /// <summary>
+        /// Resets the bidi data to a clean state.
+        /// </summary>
+        public void Reset()
         {
-            _classes.Dispose();
-            _pairedBracketTypes.Dispose();
-            _pairedBracketValues.Dispose();
-            _savedClasses.Dispose();
-            _savedPairedBracketTypes.Dispose();
-            _tempLevelBuffer.Dispose();
+            if (_hasCleanState)
+            {
+                return;
+            }
+
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _classes);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketTypes);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketValues);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedClasses);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedPairedBracketTypes);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _tempLevelBuffer);
+
+            ParagraphEmbeddingLevel = 0;
+            HasBrackets = false;
+            HasEmbeddings = false;
+            HasIsolates = false;
+            Length = 0;
+
+            Classes = default;
+            PairedBracketTypes = default;
+            PairedBracketValues = default;
+
+            _hasCleanState = true;
         }
     }
 }

+ 4 - 5
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.IO;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
+using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
 
 namespace Avalonia.Platform
@@ -166,11 +167,9 @@ namespace Avalonia.Platform
         /// </summary>
         /// <param name="glyphTypeface">The glyph typeface.</param>
         /// <param name="fontRenderingEmSize">The font rendering em size.</param>
-        /// <param name="glyphIndices">The glyph indices.</param>
-        /// <param name="glyphAdvances">The glyph advances.</param>
-        /// <param name="glyphOffsets">The glyph offsets.</param>
-        /// <returns></returns>
-        IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets);
+        /// <param name="glyphInfos">The list of glyphs.</param>
+        /// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
+        IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos);
 
         /// <summary>
         /// Creates a backend-specific object using a low-level API graphics context

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@@ -32,7 +32,7 @@ internal class FpsCounter
         {
             var s = new string((char)c, 1);
             var glyph = typeface.GetGlyph((uint)(s[0]));
-            _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph });
+            _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.AsMemory(), new ushort[] { glyph });
         }
     }
 

+ 49 - 17
src/Avalonia.Base/Utilities/ArrayBuilder.cs

@@ -3,7 +3,6 @@
 // Ported from: https://github.com/SixLabors/Fonts/
 
 using System;
-using System.Buffers;
 using System.Runtime.CompilerServices;
 
 namespace Avalonia.Utilities
@@ -12,14 +11,13 @@ namespace Avalonia.Utilities
     /// A helper type for avoiding allocations while building arrays.
     /// </summary>
     /// <typeparam name="T">The type of item contained in the array.</typeparam>
-    internal struct ArrayBuilder<T> : IDisposable
-        where T : struct
+    internal struct ArrayBuilder<T>
     {
         private const int DefaultCapacity = 4;
         private const int MaxCoreClrArrayLength = 0x7FeFFFFF;
 
         // Starts out null, initialized on first Add.
-        private T[] _data;
+        private T[]? _data;
         private int _size;
 
         /// <summary>
@@ -49,6 +47,12 @@ namespace Avalonia.Utilities
             }
         }
 
+        /// <summary>
+        /// Gets the current capacity of the array.
+        /// </summary>
+        public int Capacity
+            => _data?.Length ?? 0;
+
         /// <summary>
         /// Returns a reference to specified element of the array.
         /// </summary>
@@ -116,14 +120,44 @@ namespace Avalonia.Utilities
             return slice;
         }
 
+        /// <summary>
+        /// Appends an item.
+        /// </summary>
+        /// <param name="value">The item to append.</param>
+        public void AddItem(T value)
+        {
+            var index = Length++;
+            _data![index] = value;
+        }
+
         /// <summary>
         /// Clears the array.
         /// Allocated memory is left intact for future usage.
         /// </summary>
         public void Clear()
         {
-            // No need to actually clear since we're not allowing reference types.
+#if NET6_0_OR_GREATER
+            if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
+            {
+                ClearArray();
+            }
+            else
+            {
+                _size = 0;
+            }
+#else
+            ClearArray();
+#endif
+        }
+
+        private void ClearArray()
+        {
+            var size = _size;
             _size = 0;
+            if (size > 0)
+            {
+                Array.Clear(_data!, 0, size);
+            }
         }
 
         private void EnsureCapacity(int min)
@@ -136,7 +170,7 @@ namespace Avalonia.Utilities
             }
 
             // Same expansion algorithm as List<T>.
-            var newCapacity = length == 0 ? DefaultCapacity : length * 2;
+            var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u;
 
             if (newCapacity > MaxCoreClrArrayLength)
             {
@@ -145,15 +179,14 @@ namespace Avalonia.Utilities
 
             if (newCapacity < min)
             {
-                newCapacity = min;
+                newCapacity = (uint)min;
             }
             
-            var array = ArrayPool<T>.Shared.Rent(newCapacity);
+            var array = new T[newCapacity];
 
             if (_size > 0)
             {
                 Array.Copy(_data!, array, _size);
-                ArrayPool<T>.Shared.Return(_data!);
             }
 
             _data = array;
@@ -182,13 +215,12 @@ namespace Avalonia.Utilities
         /// <returns>The <see cref="ArraySlice{T}"/>.</returns>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public ArraySlice<T> AsSlice(int start, int length) => new ArraySlice<T>(_data!, start, length);
-        
-        public void Dispose()
-        {
-            if (_data != null)
-            {
-                ArrayPool<T>.Shared.Return(_data);
-            }
-        }
+
+        /// <summary>
+        /// Returns the current state of the array as a span.
+        /// </summary>
+        /// <returns>The <see cref="Span{T}"/>.</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public Span<T> AsSpan() => _data.AsSpan(0, _size);
     }
 }

+ 1 - 10
src/Avalonia.Base/Utilities/ArraySlice.cs

@@ -3,7 +3,6 @@
 // Ported from: https://github.com/SixLabors/Fonts/
 
 using System;
-using System.Buffers;
 using System.Collections;
 using System.Collections.Generic;
 using System.Runtime.CompilerServices;
@@ -18,7 +17,6 @@ namespace Avalonia.Utilities
     /// </summary>
     /// <typeparam name="T">The type of item contained in the slice.</typeparam>
     internal readonly struct ArraySlice<T> : IReadOnlyList<T>
-        where T : struct
     {
         /// <summary>
         /// Gets an empty <see cref="ArraySlice{T}"/>
@@ -186,13 +184,6 @@ namespace Avalonia.Utilities
 
         /// <inheritdoc/>
         int IReadOnlyCollection<T>.Count => Length;
-
-        public void ReturnRent()
-        {
-            if (_data != null)
-            {
-                ArrayPool<T>.Shared.Return(_data);
-            }
-        }
     }
 }
+

+ 13 - 16
src/Avalonia.Base/Utilities/BidiDictionary.cs

@@ -1,4 +1,6 @@
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Media.TextFormatting;
 
 namespace Avalonia.Utilities
 {
@@ -9,32 +11,27 @@ namespace Avalonia.Utilities
     /// <typeparam name="T2">Value type</typeparam>
     internal sealed class BidiDictionary<T1, T2> where T1 : notnull where T2 : notnull
     {
-        public Dictionary<T1, T2> Forward { get; } = new Dictionary<T1, T2>();
+        private Dictionary<T1, T2> _forward = new();
+        private Dictionary<T2, T1> _reverse = new();
 
-        public Dictionary<T2, T1> Reverse { get; } = new Dictionary<T2, T1>();
-
-        public void Clear()
+        public void ClearThenResetIfTooLarge()
         {
-            Forward.Clear();
-            Reverse.Clear();
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _forward);
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref _reverse);
         }
 
         public void Add(T1 key, T2 value)
         {
-            Forward.Add(key, value);
-            Reverse.Add(value, key);
+            _forward.Add(key, value);
+            _reverse.Add(value, key);
         }
 
-#pragma warning disable CS8601
-        public bool TryGetValue(T1 key, out T2 value) => Forward.TryGetValue(key, out value);
-#pragma warning restore CS8601
+        public bool TryGetValue(T1 key, [MaybeNullWhen(false)] out T2 value) => _forward.TryGetValue(key, out value);
 
-#pragma warning disable CS8601
-        public bool TryGetKey(T2 value, out T1 key) => Reverse.TryGetValue(value, out key);
-#pragma warning restore CS8601
+        public bool TryGetKey(T2 value, [MaybeNullWhen(false)] out T1 key) => _reverse.TryGetValue(value, out key);
 
-        public bool ContainsKey(T1 key) => Forward.ContainsKey(key);
+        public bool ContainsKey(T1 key) => _forward.ContainsKey(key);
 
-        public bool ContainsValue(T2 value) => Reverse.ContainsKey(value);
+        public bool ContainsValue(T2 value) => _reverse.ContainsKey(value);
     }
 }

+ 1 - 2
src/Avalonia.Base/Utilities/BinarySearchExtension.cs

@@ -14,7 +14,6 @@
 // under the License.
 // Copied from: https://github.com/toptensoftware/RichTextKit
 
-using System;
 using System.Collections.Generic;
 
 namespace Avalonia.Utilities
@@ -39,7 +38,7 @@ namespace Avalonia.Utilities
         /// <param name="value">The value to search for</param>
         /// <param name="comparer">The comparer</param>
         /// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
-        public static int BinarySearch<T>(this IReadOnlyList<T> list, T value, IComparer<T> comparer) where T : IComparable
+        public static int BinarySearch<T>(this IReadOnlyList<T> list, T value, IComparer<T> comparer)
         {
             return list.BinarySearch(0, list.Count, value, comparer);
         }

+ 1 - 1
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@@ -79,7 +79,7 @@ namespace Avalonia.Controls
             {
                 if(run.Length > 0)
                 {
-#if NET6_0
+#if NET6_0_OR_GREATER
                     builder.Append(run.Text.Span);
 #else
                     builder.Append(run.Text.Span.ToArray());

+ 2 - 1
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -9,6 +9,7 @@ using Avalonia.Rendering;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Utilities;
 using Avalonia.Media.Imaging;
+using Avalonia.Media.TextFormatting;
 
 namespace Avalonia.Headless
 {
@@ -118,7 +119,7 @@ namespace Avalonia.Headless
             return new HeadlessGeometryStub(new Rect(glyphRun.Size));
         }
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
         {
             return new HeadlessGlyphRunStub();
         }

+ 18 - 65
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -1,16 +1,11 @@
 using System;
-using System.Collections;
 using System.Collections.Generic;
 using System.IO;
-using System.Linq;
-using System.Threading;
-
-using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Media;
 using Avalonia.OpenGL;
-using Avalonia.OpenGL.Imaging;
 using Avalonia.Platform;
 using Avalonia.Media.Imaging;
+using Avalonia.Media.TextFormatting;
 using SkiaSharp;
 
 namespace Avalonia.Skia
@@ -88,9 +83,9 @@ namespace Avalonia.Skia
 
             var (currentX, currentY) = glyphRun.BaselineOrigin;
 
-            for (var i = 0; i < glyphRun.GlyphIndices.Count; i++)
+            for (var i = 0; i < glyphRun.GlyphInfos.Count; i++)
             {
-                var glyph = glyphRun.GlyphIndices[i];
+                var glyph = glyphRun.GlyphInfos[i].GlyphIndex;
                 var glyphPath = skFont.GetGlyphPath(glyph);
 
                 if (!glyphPath.IsEmpty)
@@ -98,14 +93,7 @@ namespace Avalonia.Skia
                     path.AddPath(glyphPath, (float)currentX, (float)currentY);
                 }
 
-                if (glyphRun.GlyphAdvances != null)
-                {
-                    currentX += glyphRun.GlyphAdvances[i];
-                }
-                else
-                {
-                    currentX += glyphPath.Bounds.Right;
-                }
+                currentX += glyphRun.GlyphInfos[i].GlyphAdvance;
             }
 
             return new StreamGeometryImpl(path);
@@ -213,17 +201,16 @@ namespace Avalonia.Skia
             return new WriteableBitmapImpl(size, dpi, format, alphaFormat);
         }
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices,
-            IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
         {
             if (glyphTypeface == null)
             {
                 throw new ArgumentNullException(nameof(glyphTypeface));
             }
 
-            if (glyphIndices == null)
+            if (glyphInfos == null)
             {
-                throw new ArgumentNullException(nameof(glyphIndices));
+                throw new ArgumentNullException(nameof(glyphInfos));
             }
 
             var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl;
@@ -242,59 +229,25 @@ namespace Avalonia.Skia
 
             var builder = new SKTextBlobBuilder();
 
-            var count = glyphIndices.Count;
+            var count = glyphInfos.Count;
 
-            if (glyphOffsets != null && glyphAdvances != null)
-            {
-                var runBuffer = builder.AllocatePositionedRun(font, count);
+            var runBuffer = builder.AllocatePositionedRun(font, count);
 
-                var glyphSpan = runBuffer.GetGlyphSpan();
-                var positionSpan = runBuffer.GetPositionSpan();
+            var glyphSpan = runBuffer.GetGlyphSpan();
+            var positionSpan = runBuffer.GetPositionSpan();
 
-                var currentX = 0.0;
+            var currentX = 0.0;
 
-                for (int i = 0; i < glyphOffsets.Count; i++)
-                {
-                    var offset = glyphOffsets[i];
-
-                    glyphSpan[i] = glyphIndices[i];
-
-                    positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y);
-
-                    currentX += glyphAdvances[i];
-                }
-            }
-            else
+            for (int i = 0; i < count; i++)
             {
-                if (glyphAdvances != null)
-                {
-                    var runBuffer = builder.AllocateHorizontalRun(font, count, 0);
-
-                    var glyphSpan = runBuffer.GetGlyphSpan();
-                    var positionSpan = runBuffer.GetPositionSpan();
-
-                    var currentX = 0.0;
+                var glyphInfo = glyphInfos[i];
+                var offset = glyphInfo.GlyphOffset;
 
-                    for (int i = 0; i < glyphAdvances.Count; i++)
-                    {
-                        glyphSpan[i] = glyphIndices[i];
+                glyphSpan[i] = glyphInfo.GlyphIndex;
 
-                        positionSpan[i] = (float)currentX;
+                positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y);
 
-                        currentX += glyphAdvances[i];
-                    }
-                }
-                else
-                {
-                    var runBuffer = builder.AllocateRun(font, count, 0, 0);
-
-                    var glyphSpan = runBuffer.GetGlyphSpan();
-
-                    for (int i = 0; i < glyphIndices.Count; i++)
-                    {
-                        glyphSpan[i] = glyphIndices[i];
-                    }
-                }
+                currentX += glyphInfo.GlyphAdvance;
             }
 
             return new GlyphRunImpl(builder.Build());

+ 2 - 0
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@@ -161,6 +161,8 @@ namespace Avalonia.Skia
 
             if (MemoryMarshal.TryGetArray(memory, out var segment))
             {
+                start = segment.Offset;
+                length = segment.Count;
                 return segment.Array.AsMemory();
             }
 

+ 11 - 31
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -7,6 +7,7 @@ using Avalonia.Direct2D1.Media;
 using Avalonia.Direct2D1.Media.Imaging;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
+using Avalonia.Media.TextFormatting;
 using Avalonia.Platform;
 using SharpDX.DirectWrite;
 using GlyphRun = Avalonia.Media.GlyphRun;
@@ -157,12 +158,11 @@ namespace Avalonia.Direct2D1
         public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
         public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, 
-            IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
         {
             var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
 
-            var glyphCount = glyphIndices.Count;
+            var glyphCount = glyphInfos.Count;
 
             var run = new SharpDX.DirectWrite.GlyphRun
             {
@@ -174,44 +174,23 @@ namespace Avalonia.Direct2D1
 
             for (var i = 0; i < glyphCount; i++)
             {
-                indices[i] = (short)glyphIndices[i];
+                indices[i] = (short)glyphInfos[i].GlyphIndex;
             }
 
             run.Indices = indices;
 
             run.Advances = new float[glyphCount];
 
-            var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight);
-
-            if (glyphAdvances == null)
-            {
-                for (var i = 0; i < glyphCount; i++)
-                {
-                    var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale;
-
-                    run.Advances[i] = advance;
-                }
-            }
-            else
-            {
-                for (var i = 0; i < glyphCount; i++)
-                {
-                    var advance = (float)glyphAdvances[i];
-
-                    run.Advances[i] = advance;
-                }
-            }
-
-            if (glyphOffsets == null)
+            for (var i = 0; i < glyphCount; i++)
             {
-                return new GlyphRunImpl(run);
+                run.Advances[i] = (float)glyphInfos[i].GlyphAdvance;
             }
 
             run.Offsets = new GlyphOffset[glyphCount];
 
             for (var i = 0; i < glyphCount; i++)
             {
-                var (x, y) = glyphOffsets[i];
+                var (x, y) = glyphInfos[i].GlyphOffset;
 
                 run.Offsets[i] = new GlyphOffset
                 {
@@ -254,11 +233,12 @@ namespace Avalonia.Direct2D1
 
             using (var sink = pathGeometry.Open())
             {
-                var glyphs = new short[glyphRun.GlyphIndices.Count];
+                var glyphInfos = glyphRun.GlyphInfos;
+                var glyphs = new short[glyphInfos.Count];
 
-                for (int i = 0; i < glyphRun.GlyphIndices.Count; i++)
+                for (int i = 0; i < glyphInfos.Count; i++)
                 {
-                    glyphs[i] = (short)glyphRun.GlyphIndices[i];
+                    glyphs[i] = (short)glyphInfos[i].GlyphIndex;
                 }
 
                 glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink);

+ 2 - 0
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@@ -161,6 +161,8 @@ namespace Avalonia.Direct2D1.Media
 
             if (MemoryMarshal.TryGetArray(memory, out var segment))
             {
+                start = segment.Offset;
+                length = segment.Count;
                 return segment.Array.AsMemory();
             }
 

+ 9 - 5
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@@ -1,5 +1,7 @@
-using System.Linq;
+using System;
+using System.Linq;
 using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
 using Avalonia.Utilities;
@@ -179,12 +181,14 @@ namespace Avalonia.Base.UnitTests.Media
         private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphClusters, int bidiLevel = 0)
         {
             var count = glyphAdvances.Length;
-            var glyphIndices = new ushort[count];
 
-            var characters = Enumerable.Repeat('a', count).ToArray();
+            var glyphInfos = new GlyphInfo[count];
+            for (var i = 0; i < count; ++i)
+            {
+                glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]);
+            }
 
-            return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances,
-                glyphClusters: glyphClusters, biDiLevel: bidiLevel);
+            return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel);
         }
     }
 }

+ 2 - 4
tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs

@@ -1,8 +1,6 @@
-using System;
-using System.Linq;
+using System.Linq;
 using System.Runtime.InteropServices;
 using System.Text;
-using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Xunit;
 using Xunit.Abstractions;
@@ -32,7 +30,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
         private bool Run(BiDiClassData t)
         {
             var bidi = new BidiAlgorithm();
-            var bidiData = new BidiData(t.ParagraphLevel);
+            var bidiData = new BidiData { ParagraphEmbeddingLevel = t.ParagraphLevel };
         
             var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.CodePoints).ToArray());
 

+ 151 - 0
tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs

@@ -0,0 +1,151 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Utilities;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Media.TextFormatting
+{
+    public class FormattingBufferHelperTests
+    {
+        public static TheoryData<int> SmallSizes => new() { 1, 500, 10_000, 125_000 };
+        public static TheoryData<int> LargeSizes => new() { 500_000, 1_000_000 };
+
+        [Theory]
+        [MemberData(nameof(SmallSizes))]
+        public void Should_Keep_Small_Buffer_List(int itemCount)
+        {
+            var capacity = FillAndClearList(itemCount);
+
+            Assert.True(capacity >= itemCount);
+        }
+
+        [Theory]
+        [MemberData(nameof(LargeSizes))]
+        public void Should_Reset_Large_Buffer_List(int itemCount)
+        {
+            var capacity = FillAndClearList(itemCount);
+
+            Assert.Equal(0, capacity);
+        }
+
+        private static int FillAndClearList(int itemCount)
+        {
+            var list = new List<int>();
+
+            for (var i = 0; i < itemCount; ++i)
+            {
+                list.Add(i);
+            }
+
+            FormattingBufferHelper.ClearThenResetIfTooLarge(list);
+
+            return list.Capacity;
+        }
+
+        [Theory]
+        [MemberData(nameof(SmallSizes))]
+        public void Should_Keep_Small_Buffer_ArrayBuilder(int itemCount)
+        {
+            var capacity = FillAndClearArrayBuilder(itemCount);
+
+            Assert.True(capacity >= itemCount);
+        }
+
+        [Theory]
+        [MemberData(nameof(LargeSizes))]
+        public void Should_Reset_Large_Buffer_ArrayBuilder(int itemCount)
+        {
+            var capacity = FillAndClearArrayBuilder(itemCount);
+
+            Assert.Equal(0, capacity);
+        }
+
+        private static int FillAndClearArrayBuilder(int itemCount)
+        {
+            var arrayBuilder = new ArrayBuilder<int>();
+
+            for (var i = 0; i < itemCount; ++i)
+            {
+                arrayBuilder.AddItem(i);
+            }
+
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref arrayBuilder);
+
+            return arrayBuilder.Capacity;
+        }
+
+        [Theory]
+        [MemberData(nameof(SmallSizes))]
+        public void Should_Keep_Small_Buffer_Stack(int itemCount)
+        {
+            var capacity = FillAndClearStack(itemCount);
+
+            Assert.True(capacity >= itemCount);
+        }
+
+        [Theory]
+        [MemberData(nameof(LargeSizes))]
+        public void Should_Reset_Large_Buffer_Stack(int itemCount)
+        {
+            var capacity = FillAndClearStack(itemCount);
+
+            Assert.Equal(0, capacity);
+        }
+
+        private static int FillAndClearStack(int itemCount)
+        {
+            var stack = new Stack<int>();
+
+            for (var i = 0; i < itemCount; ++i)
+            {
+                stack.Push(i);
+            }
+
+            FormattingBufferHelper.ClearThenResetIfTooLarge(stack);
+
+            var array = (Array) stack.GetType()
+                .GetField("_array", BindingFlags.NonPublic | BindingFlags.Instance)!
+                .GetValue(stack)!;
+
+            return array.Length;
+        }
+
+        [Theory]
+        [MemberData(nameof(SmallSizes))]
+        public void Should_Keep_Small_Buffer_Dictionary(int itemCount)
+        {
+            var capacity = FillAndClearDictionary(itemCount);
+
+            Assert.True(capacity >= itemCount);
+        }
+
+        [Theory]
+        [MemberData(nameof(LargeSizes))]
+        public void Should_Reset_Large_Buffer_Dictionary(int itemCount)
+        {
+            var capacity = FillAndClearDictionary(itemCount);
+
+            Assert.True(capacity <= 3); // dictionary trims to the nearest prime starting with 3
+        }
+
+        private static int FillAndClearDictionary(int itemCount)
+        {
+            var dictionary = new Dictionary<int, int>();
+
+            for (var i = 0; i < itemCount; ++i)
+            {
+                dictionary.Add(i, i);
+            }
+
+            FormattingBufferHelper.ClearThenResetIfTooLarge(ref dictionary);
+
+            var array = (Array) dictionary.GetType()
+                .GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)!
+                .GetValue(dictionary)!;
+
+            return array.Length;
+        }
+    }
+}

+ 2 - 1
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@@ -5,6 +5,7 @@ using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
 using Avalonia.Media.Imaging;
+using Avalonia.Media.TextFormatting;
 
 namespace Avalonia.Base.UnitTests.VisualTree
 {
@@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
             throw new NotImplementedException();
         }
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
         {
             throw new NotImplementedException();
         }

+ 2 - 1
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@@ -5,6 +5,7 @@ using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
 using Avalonia.Media.Imaging;
+using Avalonia.Media.TextFormatting;
 using Microsoft.Diagnostics.Runtime;
 
 namespace Avalonia.Benchmarks
@@ -120,7 +121,7 @@ namespace Avalonia.Benchmarks
             return new MockStreamGeometryImpl();
         }
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
         {
             return new MockGlyphRun();
         }

+ 14 - 3
tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs

@@ -9,6 +9,8 @@ using BenchmarkDotNet.Attributes;
 namespace Avalonia.Benchmarks.Text;
 
 [MemoryDiagnoser]
+[MinIterationTime(150)]
+[MaxWarmupCount(15)]
 public class HugeTextLayout : IDisposable
 {
     private readonly IDisposable _app;
@@ -44,7 +46,13 @@ Admitting that the possibility of achieving the results of the constructive crit
 Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor. 
 It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory.  
 In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias.";
-    
+
+    [Params(false, true)]
+    public bool Wrap { get; set; }
+
+    [Params(false, true)]
+    public bool Trim { get; set; }
+
     [Benchmark]
     public TextLayout BuildTextLayout() => MakeLayout(Text);
 
@@ -91,9 +99,12 @@ In respect that the structure of the sufficient amount poses problems and challe
         }
     }
 
-    private static TextLayout MakeLayout(string str)
+    private TextLayout MakeLayout(string str)
     {
-        var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120);
+        var wrapping = Wrap ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap;
+        var trimming = Trim ? TextTrimming.CharacterEllipsis : TextTrimming.None;
+        var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120,
+            textTrimming: trimming, textWrapping: wrapping);
         layout.Dispose();
         return layout;
     }

+ 8 - 2
tests/Avalonia.RenderTests/Media/GlyphRunTests.cs

@@ -5,6 +5,7 @@ using Avalonia.Controls.Documents;
 using Avalonia.Controls.Shapes;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
+using Avalonia.Media.TextFormatting;
 using Xunit;
 
 #if AVALONIA_SKIA
@@ -170,11 +171,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media
 
                 var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[0]) * scale;
 
-                var advances = new[] { advance, advance, advance};
+                var glyphInfos = new[]
+                {
+                    new GlyphInfo(glyphIndices[0], 0, advance),
+                    new GlyphInfo(glyphIndices[1], 1, advance),
+                    new GlyphInfo(glyphIndices[2], 2, advance)
+                };
 
                 var characters = new[] { 'A', 'B', 'C' };
 
-                GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices, advances);
+                GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphInfos);
             }
 
             public GlyphRun GlyphRun { get; }

+ 9 - 12
tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs

@@ -134,11 +134,11 @@ namespace Avalonia.Skia.UnitTests.Media
 
                 foreach (var rect in rects)
                 {
-                    var currentCluster = glyphRun.GlyphClusters[index];
+                    var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster;
 
-                    while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphClusters.Count)
+                    while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphInfos.Count)
                     {
-                        currentCluster = glyphRun.GlyphClusters[++index];
+                        currentCluster = glyphRun.GlyphInfos[++index].GlyphCluster;
                     }
 
                     //Non trailing edge
@@ -161,15 +161,15 @@ namespace Avalonia.Skia.UnitTests.Media
 
             var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace;
             
-            var rects = new List<Rect>(glyphRun.GlyphAdvances!.Count);
+            var rects = new List<Rect>(glyphRun.GlyphInfos!.Count);
 
             var lastCluster = -1;
 
-            for (var index = 0; index < glyphRun.GlyphAdvances.Count; index++)
+            for (var index = 0; index < glyphRun.GlyphInfos.Count; index++)
             {
-                var currentCluster = glyphRun.GlyphClusters![index];
+                var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster;
                 
-                var advance = glyphRun.GlyphAdvances[index];
+                var advance = glyphRun.GlyphInfos[index].GlyphAdvance;
 
                 if (lastCluster != currentCluster)
                 {
@@ -216,10 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media
                 shapedBuffer.GlyphTypeface,
                 shapedBuffer.FontRenderingEmSize,
                 shapedBuffer.Text,
-                shapedBuffer.GlyphIndices,
-                shapedBuffer.GlyphAdvances,
-                shapedBuffer.GlyphOffsets,
-                shapedBuffer.GlyphClusters,
+                shapedBuffer.GlyphInfos,
                 shapedBuffer.BidiLevel);
 
             if(shapedBuffer.BidiLevel == 1)
@@ -233,7 +230,7 @@ namespace Avalonia.Skia.UnitTests.Media
         private static IDisposable Start()
         {
             var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
-                .With(renderInterface: new PlatformRenderInterface(null),
+                .With(renderInterface: new PlatformRenderInterface(),
                     textShaperImpl: new TextShaperImpl(),
                     fontManagerImpl: new CustomFontManagerImpl()));
 

+ 7 - 3
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@@ -585,7 +585,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var expectedRuns = expectedTextLine.TextRuns.Cast<ShapedTextRun>().ToList();
 
-                var expectedGlyphs = expectedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList();
+                var expectedGlyphs = expectedRuns
+                    .SelectMany(run => run.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex)
+                    .ToList();
 
                 for (var i = 0; i < text.Length; i++)
                 {
@@ -604,7 +606,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                         var shapedRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
 
-                        var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList();
+                        var actualGlyphs = shapedRuns
+                            .SelectMany(x => x.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex)
+                            .ToList();
 
                         Assert.Equal(expectedGlyphs, actualGlyphs);
                     }
@@ -706,7 +710,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         public static IDisposable Start()
         {
             var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
-                .With(renderInterface: new PlatformRenderInterface(null),
+                .With(renderInterface: new PlatformRenderInterface(),
                     textShaperImpl: new TextShaperImpl()));
 
             AvaloniaLocator.CurrentMutable

+ 31 - 24
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@@ -142,8 +142,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     black,
                     textWrapping: TextWrapping.Wrap);
 
-                var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextRun>()
-                    .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
+                var expectedGlyphs = GetGlyphs(expected);
 
                 var outer = new GraphemeEnumerator(text);
                 var inner = new GraphemeEnumerator(text);
@@ -175,8 +174,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                             textWrapping: TextWrapping.Wrap,
                             textStyleOverrides: spans);
 
-                        var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextRun>()
-                            .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
+                        var actualGlyphs = GetGlyphs(actual);
 
                         Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count);
 
@@ -196,6 +194,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     i += outer.Current.Text.Length;
                 }
             }
+
+            static List<string> GetGlyphs(TextLayout textLayout)
+                => textLayout.TextLines
+                    .Select(line => string.Join('|', line.TextRuns
+                        .Cast<ShapedTextRun>()
+                        .SelectMany(run => run.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphIndex)))
+                    .ToList();
         }
 
         [Fact]
@@ -484,13 +489,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 {
                     var shapedRun = (ShapedTextRun)textRun;
 
-                    var glyphClusters = shapedRun.ShapedBuffer.GlyphClusters;
+                    var glyphClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToArray();
 
-                    var expected = clusters.Skip(index).Take(glyphClusters.Count).ToArray();
+                    var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray();
 
                     Assert.Equal(expected, glyphClusters);
 
-                    index += glyphClusters.Count;
+                    index += glyphClusters.Length;
                 }
             }
         }
@@ -515,13 +520,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
 
-                Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Count);
+                Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphInfos.Count);
 
-                Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[5]);
+                Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[5].GlyphCluster);
 
                 if (expectedLength == 7)
                 {
-                    Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[6]);
+                    Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[6].GlyphCluster);
                 }
             }
         }
@@ -562,9 +567,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint);
 
-                foreach (var glyph in textRun.GlyphRun.GlyphIndices)
+                foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos)
                 {
-                    Assert.Equal(replacementGlyph, glyph);
+                    Assert.Equal(replacementGlyph, glyphInfo.GlyphIndex);
                 }
             }
         }
@@ -776,8 +781,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width);
                 }
 
-                var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast<ShapedTextRun>())
-                    .SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToArray();
+                var rects = layout.TextLines
+                    .SelectMany(x => x.TextRuns.Cast<ShapedTextRun>())
+                    .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance)
+                    .ToArray();
 
                 for (var i = 0; i < SingleLineText.Length; i++)
                 {
@@ -865,10 +872,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var currentX = 0.0;
 
-                for (var i = 0; i < firstRun.GlyphRun.GlyphClusters.Count; i++)
+                for (var i = 0; i < firstRun.GlyphRun.GlyphInfos.Count; i++)
                 {
-                    var cluster = firstRun.GlyphRun.GlyphClusters[i];
-                    var advance = firstRun.GlyphRun.GlyphAdvances[i];
+                    var cluster = firstRun.GlyphRun.GlyphInfos[i].GlyphCluster;
+                    var advance = firstRun.GlyphRun.GlyphInfos[i].GlyphAdvance;
 
                     hit = layout.HitTestPoint(new Point(currentX, 0));
 
@@ -895,10 +902,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 currentX = firstRun.Size.Width + 0.5;
 
-                for (var i = 0; i < secondRun.GlyphRun.GlyphClusters.Count; i++)
+                for (var i = 0; i < secondRun.GlyphRun.GlyphInfos.Count; i++)
                 {
-                    var cluster = secondRun.GlyphRun.GlyphClusters[i];
-                    var advance = secondRun.GlyphRun.GlyphAdvances[i];
+                    var cluster = secondRun.GlyphRun.GlyphInfos[i].GlyphCluster;
+                    var advance = secondRun.GlyphRun.GlyphInfos[i].GlyphAdvance;
 
                     hit = layout.HitTestPoint(new Point(currentX, 0));
 
@@ -932,7 +939,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var firstRun = (ShapedTextRun)textLine.TextRuns[0];
 
-                var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0];
+                var firstCluster = firstRun.ShapedBuffer.GlyphInfos[0].GlyphCluster;
 
                 var characterHit = textLine.GetCharacterHitFromDistance(0);
 
@@ -946,7 +953,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
 
-                var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0];
+                var firstAdvance = firstRun.ShapedBuffer.GlyphInfos[0].GlyphAdvance;
 
                 Assert.Equal(firstAdvance, distance, 5);
 
@@ -991,9 +998,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                     var shapedRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
 
-                    var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList();
+                    var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster).ToList();
 
-                    var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList();
+                    var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance).ToList();
 
                     var currentX = 0.0;
 

+ 16 - 14
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@@ -95,9 +95,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 {
                     var shapedRun = (ShapedTextRun)textRun;
 
-                    clusters.AddRange(shapedRun.IsReversed ?
-                        shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
-                        shapedRun.ShapedBuffer.GlyphClusters);
+                    var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster);
+
+                    clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters);
                 }
 
                 var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
@@ -142,9 +142,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 {
                     var shapedRun = (ShapedTextRun)textRun;
 
-                    clusters.AddRange(shapedRun.IsReversed ?
-                        shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
-                        shapedRun.ShapedBuffer.GlyphClusters);
+                    var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster);
+
+                    clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters);
                 }
 
                 clusters.Reverse();
@@ -247,7 +247,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     formatter.FormatLine(textSource, 0, double.PositiveInfinity,
                         new GenericTextParagraphProperties(defaultProperties));
 
-                var clusters = textLine.TextRuns.Cast<ShapedTextRun>().SelectMany(x => x.ShapedBuffer.GlyphClusters)
+                var clusters = textLine.TextRuns
+                    .Cast<ShapedTextRun>()
+                    .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster)
                     .ToArray();
 
                 var previousCharacterHit = new CharacterHit(text.Length);
@@ -313,11 +315,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                     var glyphRun = textRun.GlyphRun;
 
-                    for (var i = 0; i < glyphRun.GlyphClusters!.Count; i++)
+                    for (var i = 0; i < glyphRun.GlyphInfos.Count; i++)
                     {
-                        var cluster = glyphRun.GlyphClusters[i];
+                        var cluster = glyphRun.GlyphInfos[i].GlyphCluster;
 
-                        var advance = glyphRun.GlyphAdvances[i];
+                        var advance = glyphRun.GlyphInfos[i].GlyphAdvance;
 
                         var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
 
@@ -750,7 +752,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             {
                 var shapedBuffer = textRun.ShapedBuffer;
 
-                var currentClusters = shapedBuffer.GlyphClusters.ToList();
+                var currentClusters = shapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToList();
 
                 foreach (var currentCluster in currentClusters)
                 {
@@ -783,11 +785,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             {
                 var shapedBuffer = textRun.ShapedBuffer;
 
-                for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++)
+                for (var index = 0; index < shapedBuffer.GlyphInfos.Length; index++)
                 {
-                    var currentCluster = shapedBuffer.GlyphClusters[index];
+                    var currentCluster = shapedBuffer.GlyphInfos[index].GlyphCluster;
 
-                    var advance = shapedBuffer.GlyphAdvances[index];
+                    var advance = shapedBuffer.GlyphInfos[index].GlyphAdvance;
 
                     if (lastCluster != currentCluster)
                     {

+ 5 - 5
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs

@@ -19,10 +19,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 var shapedBuffer = TextShaper.Current.ShapeText(text, options);
                 
                 Assert.Equal(shapedBuffer.Length, text.Length);
-                Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length);
-                Assert.Equal(0, shapedBuffer.GlyphClusters[0]);
-                Assert.Equal(1, shapedBuffer.GlyphClusters[1]);
-                Assert.Equal(1, shapedBuffer.GlyphClusters[2]);
+                Assert.Equal(shapedBuffer.GlyphInfos.Length, text.Length);
+                Assert.Equal(0, shapedBuffer.GlyphInfos[0].GlyphCluster);
+                Assert.Equal(1, shapedBuffer.GlyphInfos[1].GlyphCluster);
+                Assert.Equal(1, shapedBuffer.GlyphInfos[2].GlyphCluster);
             }
         }
 
@@ -36,7 +36,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 var shapedBuffer = TextShaper.Current.ShapeText(text, options);
 
                 Assert.Equal(shapedBuffer.Length, text.Length);
-                Assert.Equal(100, shapedBuffer.GlyphAdvances[0]);
+                Assert.Equal(100, shapedBuffer.GlyphInfos[0].GlyphAdvance);
             }
         }
 

+ 2 - 0
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@@ -161,6 +161,8 @@ namespace Avalonia.UnitTests
 
             if (MemoryMarshal.TryGetArray(memory, out var segment))
             {
+                start = segment.Offset;
+                length = segment.Count;
                 return segment.Array.AsMemory();
             }
 

+ 2 - 1
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -4,6 +4,7 @@ using System.IO;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Media.Imaging;
+using Avalonia.Media.TextFormatting;
 using Avalonia.Rendering;
 using Moq;
 
@@ -146,7 +147,7 @@ namespace Avalonia.UnitTests
             throw new NotImplementedException();
         }
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
         {
             return Mock.Of<IGlyphRunImpl>();
         }

+ 4 - 1
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@@ -24,7 +24,10 @@ namespace Avalonia.UnitTests
 
                 var glyphIndex = typeface.GetGlyph(codepoint);
 
-                shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster, 10);
+                for (var j = 0; j < count; ++j)
+                {
+                    shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
+                }
 
                 i += count;
             }

BIN
tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png