Browse Source

BiDiAlgorithm and BiDiData instances are reusable

pull/10013/head
Julien Lebosquain 4 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 public sealed class ShapedBuffer : IList<GlyphInfo>, IDisposable
{ {
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters(); private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
private bool _bufferRented;
private GlyphInfo[]? _rentedBuffer;
public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(text, public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
new ArraySlice<GlyphInfo>(ArrayPool<GlyphInfo>.Shared.Rent(bufferLength), 0, bufferLength),
glyphTypeface,
fontRenderingEmSize,
bidiLevel)
{ {
_bufferRented = true; _rentedBuffer = ArrayPool<GlyphInfo>.Shared.Rent(bufferLength);
Length = 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) internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
@ -28,12 +28,12 @@ namespace Avalonia.Media.TextFormatting
GlyphTypeface = glyphTypeface; GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize; FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel; BidiLevel = bidiLevel;
Length = GlyphInfos.Length;
} }
internal ArraySlice<GlyphInfo> GlyphInfos { get; } internal ArraySlice<GlyphInfo> GlyphInfos { get; private set; }
public int Length { get; } public int Length
=> GlyphInfos.Length;
public IGlyphTypeface GlyphTypeface { get; } public IGlyphTypeface GlyphTypeface { get; }
@ -271,18 +271,11 @@ namespace Avalonia.Media.TextFormatting
public void Dispose() public void Dispose()
{ {
GC.SuppressFinalize(this); if (_rentedBuffer is not null)
if (_bufferRented)
{
GlyphInfos.ReturnRent();
}
}
~ShapedBuffer()
{
if (_bufferRented)
{ {
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 internal class TextFormatterImpl : TextFormatter
{ {
private static readonly char[] s_empty = { ' ' }; 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"/> /// <inheritdoc cref="TextFormatter.FormatLine"/>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, 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) foreach (var textRun in textRuns)
{ {
if (textRun.Text.IsEmpty) ReadOnlySpan<char> text;
{ if (!textRun.Text.IsEmpty)
biDiData.Append(new char[textRun.Length]); text = textRun.Text.Span;
} else if (textRun.Length == TextRun.DefaultTextSourceLength)
text = s_defaultText;
else else
{ text = new char[textRun.Length];
biDiData.Append(textRun.Text.Span);
} biDiData.Append(text);
} }
using var biDi = new BidiAlgorithm(); var biDi = t_bidiAlgorithm ??= new BidiAlgorithm();
biDi.Process(biDiData); biDi.Process(biDiData);

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

@ -6,7 +6,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using Avalonia.Collections.Pooled;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
@ -28,7 +27,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// as much as possible. /// as much as possible.
/// </para> /// </para>
/// </remarks> /// </remarks>
internal struct BidiAlgorithm : IDisposable internal sealed class BidiAlgorithm
{ {
/// <summary> /// <summary>
/// The original BiDiClass classes as provided by the caller /// 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 forward mapping maps the start index to the end index.
/// The reverse mapping maps the end index to the start index. /// The reverse mapping maps the end index to the start index.
/// </remarks> /// </remarks>
private BidiDictionary<int, int>? _isolatePairs; private readonly BidiDictionary<int, int> _isolatePairs = new();
/// <summary> /// <summary>
/// The working BiDi classes /// The working BiDi classes
@ -98,7 +97,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// The status stack used during resolution of explicit /// The status stack used during resolution of explicit
/// embedding and isolating runs /// embedding and isolating runs
/// </summary> /// </summary>
private readonly Stack<Status> _statusStack = new Stack<Status>(); private readonly Stack<Status> _statusStack = new();
/// <summary> /// <summary>
/// Mapping used to virtually remove characters for rule X9 /// Mapping used to virtually remove characters for rule X9
@ -108,7 +107,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// Re-usable list of level runs /// Re-usable list of level runs
/// </summary> /// </summary>
private readonly List<LevelRun> _levelRuns = new List<LevelRun>(); private readonly List<LevelRun> _levelRuns = new();
/// <summary> /// <summary>
/// Mapping for the current isolating sequence, built /// Mapping for the current isolating sequence, built
@ -119,7 +118,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// A stack of pending isolate openings used by FindIsolatePairs() /// A stack of pending isolate openings used by FindIsolatePairs()
/// </summary> /// </summary>
private Stack<int>? _pendingIsolateOpenings; private readonly Stack<int> _pendingIsolateOpenings = new();
/// <summary> /// <summary>
/// The level of the isolating run currently being processed /// 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 /// Reusable list of pending opening brackets used by the
/// LocatePairedBrackets method /// LocatePairedBrackets method
/// </summary> /// </summary>
private readonly List<int> _pendingOpeningBrackets = new List<int>(); private readonly List<int> _pendingOpeningBrackets = new();
/// <summary> /// <summary>
/// Resolved list of paired brackets /// Resolved list of paired brackets
/// </summary> /// </summary>
private readonly List<BracketPair> _pairedBrackets = new List<BracketPair>(); private readonly List<BracketPair> _pairedBrackets = new();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BidiAlgorithm"/> class. /// Initializes a new instance of the <see cref="BidiAlgorithm"/> class.
@ -228,7 +227,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
ArraySlice<sbyte>? outLevels) ArraySlice<sbyte>? outLevels)
{ {
// Reset state // Reset state
_isolatePairs?.Clear(); _isolatePairs.Clear();
_workingClassesBuffer.Clear(); _workingClassesBuffer.Clear();
_levelRuns.Clear(); _levelRuns.Clear();
_resolvedLevelsBuffer.Clear(); _resolvedLevelsBuffer.Clear();
@ -324,7 +323,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Skip isolate pairs // Skip isolate pairs
// (Because we're working with a slice, we need to adjust the indices // (Because we're working with a slice, we need to adjust the indices
// we're using for the isolatePairs map) // 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; i -= data.Start;
} }
@ -359,7 +358,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_hasIsolates = false; _hasIsolates = false;
// BD9... // BD9...
_pendingIsolateOpenings?.Clear(); _pendingIsolateOpenings.Clear();
for (var i = 0; i < _originalClasses.Length; i++) for (var i = 0; i < _originalClasses.Length; i++)
{ {
@ -371,16 +370,14 @@ namespace Avalonia.Media.TextFormatting.Unicode
case BidiClass.RightToLeftIsolate: case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate: case BidiClass.FirstStrongIsolate:
{ {
_pendingIsolateOpenings ??= new Stack<int>();
_pendingIsolateOpenings.Push(i); _pendingIsolateOpenings.Push(i);
_hasIsolates = true; _hasIsolates = true;
break; break;
} }
case BidiClass.PopDirectionalIsolate: case BidiClass.PopDirectionalIsolate:
{ {
if (_pendingIsolateOpenings?.Count > 0) if (_pendingIsolateOpenings.Count > 0)
{ {
_isolatePairs ??= new BidiDictionary<int, int>();
_isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i);
} }
@ -501,7 +498,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
if (resolvedIsolate == BidiClass.FirstStrongIsolate) if (resolvedIsolate == BidiClass.FirstStrongIsolate)
{ {
if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate)) if (!_isolatePairs.TryGetValue(i, out var endOfIsolate))
{ {
endOfIsolate = _originalClasses.Length; endOfIsolate = _originalClasses.Length;
} }
@ -832,7 +829,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1];
var lastType = _originalClasses[lastCharacterIndex]; var lastType = _originalClasses[lastCharacterIndex];
if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && 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 // Find the continuing run index
runIndex = FindRunForIndex(nextRunIndex); runIndex = FindRunForIndex(nextRunIndex);
@ -855,13 +852,14 @@ namespace Avalonia.Media.TextFormatting.Unicode
private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel) private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel)
{ {
// Create mappings onto the underlying data // Create mappings onto the underlying data
_runResolvedClasses = new MappedArraySlice<BidiClass>(_workingClasses, _isolatedRunMapping.AsSlice()); var isolatedRunMapping = _isolatedRunMapping.AsSlice();
_runOriginalClasses = new MappedArraySlice<BidiClass>(_originalClasses, _isolatedRunMapping.AsSlice()); _runResolvedClasses = new MappedArraySlice<BidiClass>(_workingClasses, isolatedRunMapping);
_runLevels = new MappedArraySlice<sbyte>(_resolvedLevels, _isolatedRunMapping.AsSlice()); _runOriginalClasses = new MappedArraySlice<BidiClass>(_originalClasses, isolatedRunMapping);
_runLevels = new MappedArraySlice<sbyte>(_resolvedLevels, isolatedRunMapping);
if (_hasBrackets) if (_hasBrackets)
{ {
_runBiDiPairedBracketTypes = new MappedArraySlice<BidiPairedBracketType>(_pairedBracketTypes, _isolatedRunMapping.AsSlice()); _runBiDiPairedBracketTypes = new MappedArraySlice<BidiPairedBracketType>(_pairedBracketTypes, isolatedRunMapping);
_runPairedBracketValues = new MappedArraySlice<int>(_pairedBracketValues, _isolatedRunMapping.AsSlice()); _runPairedBracketValues = new MappedArraySlice<int>(_pairedBracketValues, isolatedRunMapping);
} }
_runLevel = runLevel; _runLevel = runLevel;
@ -1717,13 +1715,5 @@ namespace Avalonia.Media.TextFormatting.Unicode
public BidiClass Eos { get; } 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 /// Represents a unicode string and all associated attributes
/// for each character required for the bidirectional Unicode algorithm /// for each character required for the bidirectional Unicode algorithm
/// </summary> /// </summary>
internal struct BidiData : IDisposable internal sealed class BidiData
{ {
private ArrayBuilder<BidiClass> _classes; private ArrayBuilder<BidiClass> _classes;
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes; private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes;
@ -20,12 +20,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private ArrayBuilder<BidiPairedBracketType> _savedPairedBracketTypes; private ArrayBuilder<BidiPairedBracketType> _savedPairedBracketTypes;
private ArrayBuilder<sbyte> _tempLevelBuffer; private ArrayBuilder<sbyte> _tempLevelBuffer;
public BidiData(sbyte paragraphEmbeddingLevel) public sbyte ParagraphEmbeddingLevel { get; set; }
{
ParagraphEmbeddingLevel = paragraphEmbeddingLevel;
}
public sbyte ParagraphEmbeddingLevel { get; private set; }
public bool HasBrackets { get; private set; } public bool HasBrackets { get; private set; }
@ -36,7 +31,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// Gets the length of the data held by the BidiData /// Gets the length of the data held by the BidiData
/// </summary> /// </summary>
public int Length{get; private set; } public int Length { get; private set; }
/// <summary> /// <summary>
/// Gets the bidi character type of each code point /// Gets the bidi character type of each code point
@ -182,14 +177,27 @@ namespace Avalonia.Media.TextFormatting.Unicode
return _tempLevelBuffer.Add(length, false); return _tempLevelBuffer.Add(length, false);
} }
public void Dispose() /// <summary>
/// Resets the bidi data to a clean state.
/// </summary>
public void Reset()
{ {
_classes.Dispose(); _classes.Clear();
_pairedBracketTypes.Dispose(); _pairedBracketTypes.Clear();
_pairedBracketValues.Dispose(); _pairedBracketValues.Clear();
_savedClasses.Dispose(); _savedClasses.Clear();
_savedPairedBracketTypes.Dispose(); _savedPairedBracketTypes.Clear();
_tempLevelBuffer.Dispose(); _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/ // Ported from: https://github.com/SixLabors/Fonts/
using System; using System;
using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Avalonia.Utilities namespace Avalonia.Utilities
@ -12,7 +11,7 @@ namespace Avalonia.Utilities
/// A helper type for avoiding allocations while building arrays. /// A helper type for avoiding allocations while building arrays.
/// </summary> /// </summary>
/// <typeparam name="T">The type of item contained in the array.</typeparam> /// <typeparam name="T">The type of item contained in the array.</typeparam>
internal struct ArrayBuilder<T> : IDisposable internal struct ArrayBuilder<T>
where T : struct where T : struct
{ {
private const int DefaultCapacity = 4; private const int DefaultCapacity = 4;
@ -136,7 +135,7 @@ namespace Avalonia.Utilities
} }
// Same expansion algorithm as List<T>. // Same expansion algorithm as List<T>.
var newCapacity = length == 0 ? DefaultCapacity : length * 2; var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u;
if (newCapacity > MaxCoreClrArrayLength) if (newCapacity > MaxCoreClrArrayLength)
{ {
@ -145,15 +144,14 @@ namespace Avalonia.Utilities
if (newCapacity < min) if (newCapacity < min)
{ {
newCapacity = min; newCapacity = (uint)min;
} }
var array = ArrayPool<T>.Shared.Rent(newCapacity); var array = new T[newCapacity];
if (_size > 0) if (_size > 0)
{ {
Array.Copy(_data!, array, _size); Array.Copy(_data!, array, _size);
ArrayPool<T>.Shared.Return(_data!);
} }
_data = array; _data = array;
@ -182,13 +180,5 @@ namespace Avalonia.Utilities
/// <returns>The <see cref="ArraySlice{T}"/>.</returns> /// <returns>The <see cref="ArraySlice{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySlice<T> AsSlice(int start, int length) => new ArraySlice<T>(_data!, start, length); 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/ // Ported from: https://github.com/SixLabors/Fonts/
using System; using System;
using System.Buffers;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -186,13 +185,6 @@ namespace Avalonia.Utilities
/// <inheritdoc/> /// <inheritdoc/>
int IReadOnlyCollection<T>.Count => Length; 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(run.Length > 0)
{ {
#if NET6_0 #if NET6_0_OR_GREATER
builder.Append(run.Text.Span); builder.Append(run.Text.Span);
#else #else
builder.Append(run.Text.Span.ToArray()); 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.Runtime.InteropServices;
using System.Text; using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -32,7 +30,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
private bool Run(BiDiClassData t) private bool Run(BiDiClassData t)
{ {
var bidi = new BidiAlgorithm(); 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()); var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.CodePoints).ToArray());

Loading…
Cancel
Save