Browse Source

BiDiAlgorithm and BiDiData instances are reusable

pull/10013/head
Julien Lebosquain 3 years ago
parent
commit
076d10fcaf
  1. 43
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  2. 25
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  3. 48
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  4. 38
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  5. 18
      src/Avalonia.Base/Utilities/ArrayBuilder.cs
  6. 10
      src/Avalonia.Base/Utilities/ArraySlice.cs
  7. 2
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  8. 6
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs

43
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -8,17 +8,17 @@ namespace Avalonia.Media.TextFormatting
public sealed class ShapedBuffer : IList<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 +28,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; }
@ -271,18 +271,11 @@ namespace Avalonia.Media.TextFormatting
public void Dispose()
{
GC.SuppressFinalize(this);
if (_bufferRented)
{
GlyphInfos.ReturnRent();
}
}
~ShapedBuffer()
{
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
}
}
}

25
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -11,6 +11,10 @@ namespace Avalonia.Media.TextFormatting
internal 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,
@ -169,21 +173,24 @@ namespace Avalonia.Media.TextFormatting
}
using var biDiData = new BidiData((sbyte)flowDirection);
var biDiData = t_bidiData ??= new BidiData();
biDiData.Reset();
biDiData.ParagraphEmbeddingLevel = (sbyte)flowDirection;
foreach (var textRun in textRuns)
{
if (textRun.Text.IsEmpty)
{
biDiData.Append(new char[textRun.Length]);
}
ReadOnlySpan<char> text;
if (!textRun.Text.IsEmpty)
text = textRun.Text.Span;
else if (textRun.Length == TextRun.DefaultTextSourceLength)
text = s_defaultText;
else
{
biDiData.Append(textRun.Text.Span);
}
text = new char[textRun.Length];
biDiData.Append(text);
}
using var biDi = new BidiAlgorithm();
var biDi = t_bidiAlgorithm ??= new BidiAlgorithm();
biDi.Process(biDiData);

48
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@ -6,7 +6,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,7 +27,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// as much as possible.
/// </para>
/// </remarks>
internal struct BidiAlgorithm : IDisposable
internal sealed class BidiAlgorithm
{
/// <summary>
/// The original BiDiClass classes as provided by the caller
@ -67,7 +66,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 +97,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 +107,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 +118,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 +174,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,7 +227,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
ArraySlice<sbyte>? outLevels)
{
// Reset state
_isolatePairs?.Clear();
_isolatePairs.Clear();
_workingClassesBuffer.Clear();
_levelRuns.Clear();
_resolvedLevelsBuffer.Clear();
@ -324,7 +323,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 +358,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_hasIsolates = false;
// BD9...
_pendingIsolateOpenings?.Clear();
_pendingIsolateOpenings.Clear();
for (var i = 0; i < _originalClasses.Length; i++)
{
@ -371,16 +370,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 +498,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 +829,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 +852,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;
@ -1717,13 +1715,5 @@ namespace Avalonia.Media.TextFormatting.Unicode
public BidiClass Eos { get; }
}
public void Dispose()
{
_workingClassesBuffer.Dispose();
_resolvedLevelsBuffer.Dispose();
_x9Map.Dispose();
_isolatedRunMapping.Dispose();
}
}
}

38
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@ -11,7 +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>
internal struct BidiData : IDisposable
internal sealed class BidiData
{
private ArrayBuilder<BidiClass> _classes;
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes;
@ -20,12 +20,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 +31,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
@ -182,14 +177,27 @@ 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();
_classes.Clear();
_pairedBracketTypes.Clear();
_pairedBracketValues.Clear();
_savedClasses.Clear();
_savedPairedBracketTypes.Clear();
_tempLevelBuffer.Clear();
ParagraphEmbeddingLevel = 0;
HasBrackets = false;
HasEmbeddings = false;
HasIsolates = false;
Length = 0;
Classes = default;
PairedBracketTypes = default;
PairedBracketValues = default;
}
}
}

18
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,7 +11,7 @@ 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
internal struct ArrayBuilder<T>
where T : struct
{
private const int DefaultCapacity = 4;
@ -136,7 +135,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 +144,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 +180,5 @@ 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);
}
}
}
}

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;
@ -186,13 +185,6 @@ namespace Avalonia.Utilities
/// <inheritdoc/>
int IReadOnlyCollection<T>.Count => Length;
public void ReturnRent()
{
if (_data != null)
{
ArrayPool<T>.Shared.Return(_data);
}
}
}
}

2
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());

6
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());

Loading…
Cancel
Save