Browse Source

Removed most allocations for BidiReorder

pull/10013/head
Julien Lebosquain 4 years ago
parent
commit
290c8fe169
  1. 263
      src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
  2. 16
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  3. 236
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  4. 1
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  5. 1
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  6. 12
      src/Avalonia.Base/Utilities/ArrayBuilder.cs

263
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
}
}
}

16
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++)
{

236
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;
}
}
}
}

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
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;

12
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.

Loading…
Cancel
Save