Browse Source

Added FormattingObjectPool to reduce temp allocs during text layout

pull/10013/head
Julien Lebosquain 3 years ago
parent
commit
91f89c5176
  1. 5
      src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
  2. 135
      src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs
  3. 3
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  4. 4
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  5. 17
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  6. 149
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  7. 25
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  8. 90
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  9. 26
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  10. 36
      src/Avalonia.Base/Utilities/ArrayBuilder.cs
  11. 1
      src/Avalonia.Base/Utilities/ArraySlice.cs
  12. 5
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs

5
src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs

@ -10,9 +10,14 @@ namespace Avalonia.Media.TextFormatting
/// <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);

135
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;
rentedList.Clear();
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>
{
}
}
}

3
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)

4
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
{
@ -47,7 +47,7 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
/// <returns>The shapeable text characters.</returns>
internal void GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
ref TextRunProperties? previousProperties, List<TextRun> results)
ref TextRunProperties? previousProperties, RentedList<TextRun> results)
{
var properties = Properties;

17
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -111,20 +109,17 @@ namespace Avalonia.Media.TextFormatting
return new[] { shapedSymbol };
}
// perf note: the runs are very likely to come from TextLineImpl
// which already uses an array: ToArray() won't ever be called in this case
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
var objectPool = FormattingObjectPool.Instance;
var (preSplitRuns, _) = TextFormatterImpl.SplitTextRuns(textRunArray, collapsedLength);
var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
for (var i = 0; i < preSplitRuns.Count; ++i)
{
collapsedRuns[i] = preSplitRuns[i];
}
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
}
}

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

@ -1,14 +1,16 @@
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];
@ -23,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);
RentedList<TextRun>? shapedTextRuns;
if (previousLineBreak?.RemainingRuns is { } remainingRuns)
{
resolvedFlowDirection = previousLineBreak.FlowDirection;
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)
{
@ -49,25 +56,32 @@ namespace Avalonia.Media.TextFormatting
switch (textWrapping)
{
case TextWrapping.NoWrap:
{
textLine = new TextLineImpl(textRuns.ToArray(), 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;
}
@ -76,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++)
@ -94,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++)
@ -108,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)
{
@ -122,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)
{
@ -144,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>
@ -157,14 +177,16 @@ 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>();
var shapedRuns = objectPool.TextRunLists.Rent();
if (textRuns.Count == 0)
{
@ -172,13 +194,14 @@ namespace Avalonia.Media.TextFormatting
return shapedRuns;
}
var bidiData = t_bidiData ??= new BidiData();
var bidiData = t_bidiData ??= new();
bidiData.Reset();
bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection;
foreach (var textRun in textRuns)
for (var i = 0; i < textRuns.Count; ++i)
{
var textRun = textRuns[i];
ReadOnlySpan<char> text;
if (!textRun.Text.IsEmpty)
text = textRun.Text.Span;
@ -190,8 +213,7 @@ namespace Avalonia.Media.TextFormatting
bidiData.Append(text);
}
var bidiAlgorithm = t_bidiAlgorithm ??= new BidiAlgorithm();
var bidiAlgorithm = t_bidiAlgorithm ??= new();
bidiAlgorithm.Process(bidiData);
var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes);
@ -199,9 +221,11 @@ namespace Avalonia.Media.TextFormatting
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, bidiAlgorithm.ResolvedLevels, processedRuns);
var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
for (var index = 0; index < processedRuns.Count; index++)
{
@ -210,8 +234,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)
@ -253,6 +278,9 @@ namespace Avalonia.Media.TextFormatting
}
}
objectPool.TextRunLists.Return(ref processedRuns);
objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
return shapedRuns;
}
@ -319,14 +347,13 @@ 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 void ShapeTogether(IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text,
TextShaperOptions options, List<TextRun> results)
TextShaperOptions options, RentedList<TextRun> results)
{
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
@ -349,8 +376,8 @@ namespace Avalonia.Media.TextFormatting
/// <param name="levels">The bidi levels.</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)
{
@ -437,19 +464,20 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first text source index.</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);
@ -543,8 +571,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:
@ -554,15 +584,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;
@ -579,7 +609,7 @@ namespace Avalonia.Media.TextFormatting
{
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{
goto found;
return measuredLength != 0;
}
measuredLength += currentRun.Length;
@ -596,8 +626,6 @@ namespace Avalonia.Media.TextFormatting
}
}
found:
return measuredLength != 0;
}
@ -605,7 +633,8 @@ 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;
@ -618,7 +647,9 @@ namespace Avalonia.Media.TextFormatting
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>
@ -630,14 +661,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))
@ -763,10 +795,10 @@ namespace Avalonia.Media.TextFormatting
break;
}
var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength);
var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
var lineBreak = postSplitRuns?.Count > 0 ?
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns) :
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
null;
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
@ -778,7 +810,12 @@ namespace Avalonia.Media.TextFormatting
paragraphWidth, paragraphProperties, resolvedFlowDirection,
lineBreak);
return textLine.FinalizeLine();
textLine.FinalizeLine();
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
return textLine;
}
private struct TextRunEnumerator

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

@ -426,16 +426,19 @@ namespace Avalonia.Media.TextFormatting
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 TextLine[] { textLine };
}
var textLines = new List<TextLine>();
var textLines = objectPool.TextLines.Rent();
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
@ -447,14 +450,15 @@ namespace Avalonia.Media.TextFormatting
while (true)
{
var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties,
previousLine?.TextLineBreak);
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);
@ -496,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));
}
@ -513,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);
@ -552,7 +556,12 @@ namespace Avalonia.Media.TextFormatting
}
}
return textLines.ToArray();
var result = textLines.ToArray();
objectPool.TextLines.Return(ref textLines);
objectPool.VerifyAllReturned();
return result;
}
/// <summary>

90
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -1,7 +1,7 @@
// ReSharper disable ForCanBeConvertedToForeach
using System;
using System.Collections.Generic;
using System.Linq;
using static Avalonia.Media.TextFormatting.FormattingObjectPool;
namespace Avalonia.Media.TextFormatting
{
@ -80,60 +80,64 @@ namespace Avalonia.Media.TextFormatting
if (measuredLength > 0)
{
var collapsedRuns = new List<TextRun>(textRuns.Count + 1);
var objectPool = FormattingObjectPool.Instance;
// perf note: the runs are very likely to come from TextLineImpl,
// which already uses an array: ToArray() won't ever be called in this case
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
var collapsedRuns = objectPool.TextRunLists.Rent();
IReadOnlyList<TextRun>? preSplitRuns;
IReadOnlyList<TextRun>? postSplitRuns;
RentedList<TextRun>? rentedPreSplitRuns = null;
RentedList<TextRun>? rentedPostSplitRuns = null;
TextRun[]? results;
if (_prefixLength > 0)
try
{
(preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(
textRunArray, Math.Min(_prefixLength, measuredLength));
IReadOnlyList<TextRun>? effectivePostSplitRuns;
for (var i = 0; i < preSplitRuns.Count; i++)
if (_prefixLength > 0)
{
var preSplitRun = preSplitRuns[i];
collapsedRuns.Add(preSplitRun);
(rentedPreSplitRuns, rentedPostSplitRuns) = TextFormatterImpl.SplitTextRuns(
textRuns, Math.Min(_prefixLength, measuredLength), objectPool);
effectivePostSplitRuns = rentedPostSplitRuns;
foreach (var preSplitRun in rentedPreSplitRuns)
{
collapsedRuns.Add(preSplitRun);
}
}
else
{
effectivePostSplitRuns = textRuns;
}
}
else
{
preSplitRuns = null;
postSplitRuns = textRunArray;
}
collapsedRuns.Add(shapedSymbol);
collapsedRuns.Add(shapedSymbol);
if (measuredLength <= _prefixLength || postSplitRuns is null)
{
return collapsedRuns.ToArray();
}
if (measuredLength <= _prefixLength || effectivePostSplitRuns is null)
{
results = collapsedRuns.ToArray();
objectPool.TextRunLists.Return(ref collapsedRuns);
return results;
}
var availableSuffixWidth = availableWidth;
var availableSuffixWidth = availableWidth;
if (preSplitRuns is not null)
{
for (var i = 0; i < preSplitRuns.Count; i++)
if (rentedPreSplitRuns is not null)
{
var run = preSplitRuns[i];
if (run is DrawableTextRun drawableTextRun)
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))
@ -151,10 +155,18 @@ namespace Avalonia.Media.TextFormatting
break;
}
}
}
}
finally
{
objectPool.TextRunLists.Return(ref rentedPreSplitRuns);
objectPool.TextRunLists.Return(ref rentedPostSplitRuns);
}
return collapsedRuns.ToArray();
results = collapsedRuns.ToArray();
objectPool.TextRunLists.Return(ref collapsedRuns);
return results;
}
return new TextRun[] { shapedSymbol };

26
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -1,14 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal sealed class TextLineImpl : TextLine
{
private static readonly ThreadLocal<BidiReorderer> s_bidiReorderer = new(() => new BidiReorderer());
private readonly TextRun[] _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
@ -570,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;
@ -583,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;
}
@ -671,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);
@ -786,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;
@ -797,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;
}
@ -992,14 +989,11 @@ namespace Avalonia.Media.TextFormatting
}
}
public TextLineImpl FinalizeLine()
public void FinalizeLine()
{
_textLineMetrics = CreateLineMetrics();
var bidiReorderer = s_bidiReorderer.Value!;
bidiReorderer.BidiReorder(_textRuns, _resolvedFlowDirection);
return this;
BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
}
/// <summary>

36
src/Avalonia.Base/Utilities/ArrayBuilder.cs

@ -12,7 +12,6 @@ namespace Avalonia.Utilities
/// </summary>
/// <typeparam name="T">The type of item contained in the array.</typeparam>
internal struct ArrayBuilder<T>
where T : struct
{
private const int DefaultCapacity = 4;
private const int MaxCoreClrArrayLength = 0x7FeFFFFF;
@ -48,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>
@ -131,8 +136,28 @@ namespace Avalonia.Utilities
/// </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)
@ -190,5 +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);
/// <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
src/Avalonia.Base/Utilities/ArraySlice.cs

@ -17,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}"/>

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

Loading…
Cancel
Save