瀏覽代碼

Removed most allocations for BidiReorder

Julien Lebosquain 2 年之前
父節點
當前提交
290c8fe169

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

@@ -0,0 +1,263 @@
+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
+    {
+        private ArrayBuilder<OrderedBidiRun> _runs;
+        private ArrayBuilder<BidiRange> _ranges;
+
+        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
+            {
+                _runs.Clear();
+                _ranges.Clear();
+            }
+        }
+
+        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
+        }
+    }
+}

+ 8 - 8
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -173,9 +173,9 @@ namespace Avalonia.Media.TextFormatting
             }
 
 
-            var biDiData = t_bidiData ??= new BidiData();
-            biDiData.Reset();
-            biDiData.ParagraphEmbeddingLevel = (sbyte)flowDirection;
+            var bidiData = t_bidiData ??= new BidiData();
+            bidiData.Reset();
+            bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection;
 
             foreach (var textRun in textRuns)
             {
@@ -187,21 +187,21 @@ namespace Avalonia.Media.TextFormatting
                 else
                     text = new char[textRun.Length];
 
-                biDiData.Append(text);
+                bidiData.Append(text);
             }
 
-            var biDi = t_bidiAlgorithm ??= new BidiAlgorithm();
+            var bidiAlgorithm = t_bidiAlgorithm ??= new BidiAlgorithm();
 
-            biDi.Process(biDiData);
+            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);
 
-            CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns);
+            CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels, processedRuns);
 
             for (var index = 0; index < processedRuns.Count; index++)
             {

+ 6 - 230
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -1,12 +1,15 @@
 using System;
 using System.Collections.Generic;
+using System.Threading;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
     internal sealed class TextLineImpl : TextLine
     {
-        private TextRun[] _textRuns;
+        private static readonly ThreadLocal<BidiReorderer> s_bidiReorderer = new(() => new BidiReorderer());
+
+        private readonly TextRun[] _textRuns;
         private readonly double _paragraphWidth;
         private readonly TextParagraphProperties _paragraphProperties;
         private TextLineMetrics _textLineMetrics;
@@ -993,185 +996,12 @@ namespace Avalonia.Media.TextFormatting
         {
             _textLineMetrics = CreateLineMetrics();
 
-            BidiReorder();
+            var bidiReorderer = s_bidiReorderer.Value!;
+            bidiReorderer.BidiReorder(_textRuns, _resolvedFlowDirection);
 
             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.Length == 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.Length; 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.Length; 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 TextRun[_textRuns.Length];
-            var index = 0;
-
-            current = orderedRun;
-
-            while (current != null)
-            {
-                textRuns[index++] = 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!;
-        }
-
         /// <summary>
         /// Tries to find the next character hit.
         /// </summary>
@@ -1620,59 +1450,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;
-            }
-        }
     }
 }

+ 0 - 1
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@@ -5,7 +5,6 @@
 using System;
 using System.Collections.Generic;
 using System.Runtime.CompilerServices;
-using System.Threading;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting.Unicode

+ 1 - 0
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@@ -11,6 +11,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
     /// Represents a unicode string and all associated attributes
     /// for each character required for the bidirectional Unicode algorithm
     /// </summary>
+    /// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
     internal sealed class BidiData
     {
         private ArrayBuilder<BidiClass> _classes;

+ 11 - 1
src/Avalonia.Base/Utilities/ArrayBuilder.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Utilities
         private const int MaxCoreClrArrayLength = 0x7FeFFFFF;
 
         // Starts out null, initialized on first Add.
-        private T[] _data;
+        private T[]? _data;
         private int _size;
 
         /// <summary>
@@ -115,6 +115,16 @@ 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.