From 658fd804af0835600d6c5e0fcdfa2826590f4ace Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 01:36:45 +0100 Subject: [PATCH 01/10] Removed some temporary List from text layout --- .../Media/TextFormatting/SplitResult.cs | 11 ++++ .../Media/TextFormatting/TextCharacters.cs | 9 +-- .../TextCollapsingProperties.cs | 6 +- .../TextFormatting/TextEllipsisHelper.cs | 62 ++++++++++--------- .../Media/TextFormatting/TextFormatterImpl.cs | 53 +++++++++------- .../Media/TextFormatting/TextLayout.cs | 51 ++++++++------- .../TextLeadingPrefixCharacterEllipsis.cs | 50 ++++++++------- .../Media/TextFormatting/TextLineImpl.cs | 56 ++++++++--------- .../TextTrailingCharacterEllipsis.cs | 7 +-- .../TextTrailingWordEllipsis.cs | 8 +-- 10 files changed, 169 insertions(+), 144 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs index 53021c4656..c1ac57ce46 100644 --- a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs +++ b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs @@ -26,5 +26,16 @@ /// The second part. /// public T? Second { get; } + + /// + /// Deconstructs the split results into its components. + /// + /// On return, contains the first part. + /// On return, contains the second part. + public void Deconstruct(out T first, out T? second) + { + first = First; + second = Second; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 2525f0dbf9..6454f9bfa3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -46,24 +46,21 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties) + internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, + ref TextRunProperties? previousProperties, List results) { - var shapeableCharacters = new List(2); var properties = Properties; while (!text.IsEmpty) { var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); - shapeableCharacters.Add(shapeableRun); + results.Add(shapeableRun); text = text.Slice(shapeableRun.Length); previousProperties = shapeableRun.Properties; } - - return shapeableCharacters; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 01804e1ce3..72882df0b5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing. @@ -21,6 +19,6 @@ namespace Avalonia.Media.TextFormatting /// Collapses given text line. /// /// Text line to collapse. - public abstract List? Collapse(TextLine textLine); + public abstract TextRun[]? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 528cd45581..97f8b2483b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,15 +1,18 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { internal static class TextEllipsisHelper { - public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -22,7 +25,7 @@ namespace Avalonia.Media.TextFormatting if (properties.Width < shapedSymbol.GlyphRun.Size.Width) { //Not enough space to fit in the symbol - return new List(0); + return Array.Empty(); } var availableWidth = properties.Width - shapedSymbol.Size.Width; @@ -70,18 +73,7 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= shapedRun.Size.Width; @@ -94,18 +86,7 @@ namespace Avalonia.Media.TextFormatting //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= drawableRun.Size.Width; @@ -121,5 +102,30 @@ namespace Avalonia.Media.TextFormatting return null; } + + private static TextRun[] CreateCollapsedRuns(IReadOnlyList textRuns, int collapsedLength, + TextRun shapedSymbol) + { + if (collapsedLength <= 0) + { + 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 (preSplitRuns, _) = TextFormatterImpl.SplitTextRuns(textRunArray, collapsedLength); + + var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; + + for (var i = 0; i < preSplitRuns.Count; ++i) + { + collapsedRuns[i] = preSplitRuns[i]; + } + + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 8afecb09e2..5c073452f4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -23,10 +24,10 @@ namespace Avalonia.Media.TextFormatting var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, out var textEndOfLine, out var textSourceLength); - if (previousLineBreak?.RemainingRuns != null) + if (previousLineBreak?.RemainingRuns is { } remainingRuns) { resolvedFlowDirection = previousLineBreak.FlowDirection; - textRuns = previousLineBreak.RemainingRuns; + textRuns = remainingRuns; nextLineBreak = previousLineBreak; } else @@ -45,7 +46,7 @@ namespace Avalonia.Media.TextFormatting { case TextWrapping.NoWrap: { - textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, + textLine = new TextLineImpl(textRuns.ToArray(), firstTextSourceIndex, textSourceLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); @@ -160,6 +161,14 @@ namespace Avalonia.Media.TextFormatting { var flowDirection = paragraphProperties.FlowDirection; var shapedRuns = new List(); + + if (textRuns.Count == 0) + { + resolvedFlowDirection = flowDirection; + return shapedRuns; + } + + using var biDiData = new BidiData((sbyte)flowDirection); foreach (var textRun in textRuns) @@ -224,7 +233,7 @@ namespace Avalonia.Media.TextFormatting shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); + ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); break; } @@ -309,11 +318,9 @@ namespace Avalonia.Media.TextFormatting && x.Typeface == y.Typeface && x.BaselineAlignment == y.BaselineAlignment; - private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, ReadOnlyMemory text, TextShaperOptions options) + private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text, + TextShaperOptions options, List results) { - var shapedRuns = new List(textRuns.Count); - var shapedBuffer = TextShaper.Current.ShapeText(text, options); for (var i = 0; i < textRuns.Count; i++) @@ -322,12 +329,10 @@ namespace Avalonia.Media.TextFormatting var splitResult = shapedBuffer.Split(currentRun.Length); - shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); + results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); shapedBuffer = splitResult.Second!; } - - return shapedRuns; } /// @@ -335,7 +340,7 @@ namespace Avalonia.Media.TextFormatting /// /// The text characters to form from. /// The bidi levels. - /// + /// A list that will be filled with the processed runs. /// private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, List processedRuns) @@ -385,7 +390,8 @@ namespace Avalonia.Media.TextFormatting if (j == runTextSpan.Length) { - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, + processedRuns); runLevel = levels[levelIndex]; @@ -398,7 +404,8 @@ namespace Avalonia.Media.TextFormatting } // End of this run - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, + processedRuns); runText = runText.Slice(j); runTextSpan = runText.Span; @@ -415,7 +422,7 @@ namespace Avalonia.Media.TextFormatting return; } - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns); } /// @@ -423,8 +430,8 @@ namespace Avalonia.Media.TextFormatting /// /// The text source. /// The first text source index. - /// - /// + /// On return, the end of line, if any. + /// On return, the processed text source length. /// /// The formatted text runs. /// @@ -602,7 +609,7 @@ namespace Avalonia.Media.TextFormatting var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); - var textRuns = new List { new ShapedTextRun(shapedBuffer, properties) }; + var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); } @@ -749,12 +756,10 @@ namespace Avalonia.Media.TextFormatting break; } - var splitResult = SplitTextRuns(textRuns, measuredLength); - - var remainingCharacters = splitResult.Second; + var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength); - var lineBreak = remainingCharacters?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : + var lineBreak = postSplitRuns?.Count > 0 ? + new TextLineBreak(null, resolvedFlowDirection, postSplitRuns) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) @@ -762,7 +767,7 @@ namespace Avalonia.Media.TextFormatting lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); } - var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength, + var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, lineBreak); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 468623b356..55b6f14267 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -13,6 +12,7 @@ namespace Avalonia.Media.TextFormatting private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; private readonly TextTrimming _textTrimming; + private readonly TextLine[] _textLines; private int _textSourceLength; @@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -147,7 +147,8 @@ namespace Avalonia.Media.TextFormatting /// /// The text lines. /// - public IReadOnlyList TextLines { get; private set; } + public IReadOnlyList TextLines + => _textLines; /// /// Gets the bounds of the layout. @@ -164,14 +165,14 @@ namespace Avalonia.Media.TextFormatting /// The origin. public void Draw(DrawingContext context, Point origin) { - if (!TextLines.Any()) + if (_textLines.Length == 0) { return; } var (currentX, currentY) = origin; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); @@ -186,7 +187,7 @@ namespace Avalonia.Media.TextFormatting /// public Rect HitTestTextPosition(int textPosition) { - if (TextLines.Count == 0) + if (_textLines.Length == 0) { return new Rect(); } @@ -198,7 +199,7 @@ namespace Avalonia.Media.TextFormatting var currentY = 0.0; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { var end = textLine.FirstTextSourceIndex + textLine.Length; @@ -230,11 +231,11 @@ namespace Avalonia.Media.TextFormatting return Array.Empty(); } - var result = new List(TextLines.Count); + var result = new List(_textLines.Length); var currentY = 0d; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { //Current line isn't covered. if (textLine.FirstTextSourceIndex + textLine.Length < start) @@ -284,13 +285,12 @@ namespace Avalonia.Media.TextFormatting { var currentY = 0d; - var lineIndex = 0; TextLine? currentLine = null; CharacterHit characterHit; - for (; lineIndex < TextLines.Count; lineIndex++) + for (var lineIndex = 0; lineIndex < _textLines.Length; lineIndex++) { - currentLine = TextLines[lineIndex]; + currentLine = _textLines[lineIndex]; if (currentY + currentLine.Height > point.Y) { @@ -322,12 +322,12 @@ namespace Avalonia.Media.TextFormatting if (charIndex > _textSourceLength) { - return TextLines.Count - 1; + return _textLines.Length - 1; } - for (var index = 0; index < TextLines.Count; index++) + for (var index = 0; index < _textLines.Length; index++) { - var textLine = TextLines[index]; + var textLine = _textLines[index]; if (textLine.FirstTextSourceIndex + textLine.Length < charIndex) { @@ -341,7 +341,7 @@ namespace Avalonia.Media.TextFormatting } } - return TextLines.Count - 1; + return _textLines.Length - 1; } private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point) @@ -424,7 +424,7 @@ namespace Avalonia.Media.TextFormatting height += textLine.Height; } - private IReadOnlyList CreateTextLines() + private TextLine[] CreateTextLines() { if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { @@ -432,7 +432,7 @@ namespace Avalonia.Media.TextFormatting Bounds = new Rect(0, 0, 0, textLine.Height); - return new List { textLine }; + return new TextLine[] { textLine }; } var textLines = new List(); @@ -443,12 +443,14 @@ namespace Avalonia.Media.TextFormatting TextLine? previousLine = null; + var textFormatter = TextFormatter.Current; + while (true) { - var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, + var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if(textLine == null || textLine.Length == 0) + if (textLine.Length == 0) { if (previousLine != null && previousLine.NewLineLength > 0) { @@ -524,8 +526,9 @@ namespace Avalonia.Media.TextFormatting { var whitespaceWidth = 0d; - foreach (var line in textLines) + for (var i = 0; i < textLines.Count; i++) { + var line = textLines[i]; var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; if (lineWhitespaceWidth > whitespaceWidth) @@ -549,7 +552,7 @@ namespace Avalonia.Media.TextFormatting } } - return textLines; + return textLines.ToArray(); } /// @@ -569,7 +572,7 @@ namespace Avalonia.Media.TextFormatting public void Dispose() { - foreach (var line in TextLines) + foreach (var line in _textLines) { line.Dispose(); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index e30a0fe9f4..672a15b398 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -1,5 +1,7 @@ -using System; +// ReSharper disable ForCanBeConvertedToForeach +using System; using System.Collections.Generic; +using System.Linq; namespace Avalonia.Media.TextFormatting { @@ -39,11 +41,12 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -54,7 +57,7 @@ namespace Avalonia.Media.TextFormatting if (Width < shapedSymbol.GlyphRun.Size.Width) { - return new List(0); + return Array.Empty(); } // Overview of ellipsis structure @@ -75,41 +78,48 @@ namespace Avalonia.Media.TextFormatting { shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); - var collapsedRuns = new List(textRuns.Count); - if (measuredLength > 0) { - IReadOnlyList? preSplitRuns = null; + var collapsedRuns = new List(textRuns.Count + 1); + + // 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(); + + IReadOnlyList? preSplitRuns; IReadOnlyList? postSplitRuns; if (_prefixLength > 0) { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, - Math.Min(_prefixLength, measuredLength)); + (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns( + textRunArray, Math.Min(_prefixLength, measuredLength)); - collapsedRuns.AddRange(splitResult.First); - - preSplitRuns = splitResult.First; - postSplitRuns = splitResult.Second; + for (var i = 0; i < preSplitRuns.Count; i++) + { + var preSplitRun = preSplitRuns[i]; + collapsedRuns.Add(preSplitRun); + } } else { - postSplitRuns = textRuns; + preSplitRuns = null; + postSplitRuns = textRunArray; } collapsedRuns.Add(shapedSymbol); if (measuredLength <= _prefixLength || postSplitRuns is null) { - return collapsedRuns; + return collapsedRuns.ToArray(); } var availableSuffixWidth = availableWidth; if (preSplitRuns is not null) { - foreach (var run in preSplitRuns) + for (var i = 0; i < preSplitRuns.Count; i++) { + var run = preSplitRuns[i]; if (run is DrawableTextRun drawableTextRun) { availableSuffixWidth -= drawableTextRun.Size.Width; @@ -143,13 +153,11 @@ namespace Avalonia.Media.TextFormatting } } } - } - else - { - collapsedRuns.Add(shapedSymbol); + + return collapsedRuns.ToArray(); } - return collapsedRuns; + return new TextRun[] { shapedSymbol }; } availableWidth -= shapedRun.Size.Width; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ab9686a34a..ae6df3a232 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,19 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - internal class TextLineImpl : TextLine + internal sealed class TextLineImpl : TextLine { - private IReadOnlyList _textRuns; + private TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; private readonly FlowDirection _resolvedFlowDirection; - public TextLineImpl(IReadOnlyList textRuns, int firstTextSourceIndex, int length, double paragraphWidth, + public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) { @@ -147,7 +146,7 @@ namespace Avalonia.Media.TextFormatting var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties, _resolvedFlowDirection, TextLineBreak, true); - if (collapsedRuns.Count > 0) + if (collapsedRuns.Length > 0) { collapsedLine.FinalizeLine(); } @@ -166,7 +165,7 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetCharacterHitFromDistance(double distance) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return new CharacterHit(); } @@ -182,7 +181,7 @@ namespace Avalonia.Media.TextFormatting if (distance >= WidthIncludingTrailingWhitespace) { - var lastRun = _textRuns[_textRuns.Count - 1]; + var lastRun = _textRuns[_textRuns.Length - 1]; var size = 0.0; @@ -199,7 +198,7 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - for (var i = 0; i < _textRuns.Count; i++) + for (var i = 0; i < _textRuns.Length; i++) { var currentRun = _textRuns[i]; @@ -208,7 +207,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = i; currentPosition += currentRun.Length; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + while (rightToLeftIndex + 1 <= _textRuns.Length - 1) { var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun; @@ -224,7 +223,7 @@ namespace Avalonia.Media.TextFormatting for (var j = i; i <= rightToLeftIndex; j++) { - if (j > _textRuns.Count - 1) + if (j > _textRuns.Length - 1) { break; } @@ -254,7 +253,7 @@ namespace Avalonia.Media.TextFormatting if (currentRun is DrawableTextRun drawableTextRun) { - if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance) + if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance) { currentDistance += drawableTextRun.Size.Width; @@ -328,7 +327,7 @@ namespace Avalonia.Media.TextFormatting if (flowDirection == FlowDirection.LeftToRight) { - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { var currentRun = _textRuns[index]; @@ -338,7 +337,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftWidth = shapedRun.Size.Width; - while (i + 1 <= _textRuns.Count - 1) + while (i + 1 <= _textRuns.Length - 1) { var nextRun = _textRuns[i + 1]; @@ -402,7 +401,7 @@ namespace Avalonia.Media.TextFormatting { currentDistance += WidthIncludingTrailingWhitespace; - for (var index = _textRuns.Count - 1; index >= 0; index--) + for (var index = _textRuns.Length - 1; index >= 0; index--) { var currentRun = _textRuns[index]; @@ -502,7 +501,7 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return new CharacterHit(); } @@ -637,7 +636,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = index; var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) + while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) { if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) { @@ -981,7 +980,7 @@ namespace Avalonia.Media.TextFormatting public override void Dispose() { - for (int i = 0; i < _textRuns.Count; i++) + for (int i = 0; i < _textRuns.Length; i++) { if (_textRuns[i] is ShapedTextRun shapedTextRun) { @@ -1013,7 +1012,7 @@ namespace Avalonia.Media.TextFormatting private void BidiReorder() { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return; } @@ -1025,7 +1024,7 @@ namespace Avalonia.Media.TextFormatting var current = orderedRun; - for (var i = 1; i < _textRuns.Count; i++) + for (var i = 1; i < _textRuns.Length; i++) { run = _textRuns[i]; @@ -1044,7 +1043,7 @@ namespace Avalonia.Media.TextFormatting sbyte max = 0; var min = sbyte.MaxValue; - for (var i = 0; i < _textRuns.Count; i++) + for (var i = 0; i < _textRuns.Length; i++) { var currentRun = _textRuns[i]; @@ -1095,13 +1094,14 @@ namespace Avalonia.Media.TextFormatting minLevelToReverse--; } - var textRuns = new List(_textRuns.Count); + var textRuns = new TextRun[_textRuns.Length]; + var index = 0; current = orderedRun; while (current != null) { - textRuns.Add(current.Run); + textRuns[index++] = current.Run; current = current.Next; } @@ -1197,7 +1197,7 @@ namespace Avalonia.Media.TextFormatting var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition); - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1346,7 +1346,7 @@ namespace Avalonia.Media.TextFormatting textPosition = FirstTextSourceIndex; TextRun? previousRun = null; - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1395,7 +1395,7 @@ namespace Avalonia.Media.TextFormatting } } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1411,7 +1411,7 @@ namespace Avalonia.Media.TextFormatting return runIndex; } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1448,14 +1448,14 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; - var lastRunIndex = _textRuns.Count - 1; + var lastRunIndex = _textRuns.Length - 1; if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) { lastRunIndex--; } - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { switch (_textRuns[index]) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index deecbbe476..ccae99cc75 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// A collapsing properties to collapse whole line toward the end @@ -26,7 +24,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, false); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index c291e1dfb9..c622c76a60 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using Avalonia.Utilities; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// a collapsing properties to collapse whole line toward the end @@ -31,7 +28,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, true); } From 96b423900f734c698932534dacefd59827557470 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 16 Jan 2023 11:50:14 +0100 Subject: [PATCH 02/10] TextRunProperties: don't allocate if the typeface hasn't changed --- src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 86b701cb4b..7bad99f33f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -93,6 +93,9 @@ namespace Avalonia.Media.TextFormatting internal TextRunProperties WithTypeface(Typeface typeface) { + if (this is GenericTextRunProperties other && other.Typeface == typeface) + return this; + return new GenericTextRunProperties(typeface, FontRenderingEmSize, TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment); } From 076d10fcaf6f14d14585b147ffdd72f578efaa7e Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 16 Jan 2023 14:57:06 +0100 Subject: [PATCH 03/10] BiDiAlgorithm and BiDiData instances are reusable --- .../Media/TextFormatting/ShapedBuffer.cs | 43 +++++++---------- .../Media/TextFormatting/TextFormatterImpl.cs | 25 ++++++---- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 48 ++++++++----------- .../Media/TextFormatting/Unicode/BiDiData.cs | 38 +++++++++------ src/Avalonia.Base/Utilities/ArrayBuilder.cs | 18 ++----- src/Avalonia.Base/Utilities/ArraySlice.cs | 10 +--- .../TextBoxTextInputMethodClient.cs | 2 +- .../Media/TextFormatting/BiDiClassTests.cs | 6 +-- 8 files changed, 84 insertions(+), 106 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index b05fab08fa..41bba2cd09 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -8,17 +8,17 @@ namespace Avalonia.Media.TextFormatting public sealed class ShapedBuffer : IList, IDisposable { private static readonly IComparer s_clusterComparer = new CompareClusters(); - private bool _bufferRented; - - public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(text, - new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), - glyphTypeface, - fontRenderingEmSize, - bidiLevel) + + private GlyphInfo[]? _rentedBuffer; + + public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - _bufferRented = true; - Length = bufferLength; + _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); + Text = text; + GlyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); + GlyphTypeface = glyphTypeface; + FontRenderingEmSize = fontRenderingEmSize; + BidiLevel = bidiLevel; } internal ShapedBuffer(ReadOnlyMemory text, ArraySlice 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 GlyphInfos { get; } - - public int Length { get; } + internal ArraySlice 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.Shared.Return(_rentedBuffer); + _rentedBuffer = null; + GlyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 5c073452f4..7614c8e3dc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/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; /// 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 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); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 100d381afe..e770ba9e91 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/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. /// /// - internal struct BidiAlgorithm : IDisposable + internal sealed class BidiAlgorithm { /// /// 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. /// - private BidiDictionary? _isolatePairs; + private readonly BidiDictionary _isolatePairs = new(); /// /// 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 /// - private readonly Stack _statusStack = new Stack(); + private readonly Stack _statusStack = new(); /// /// Mapping used to virtually remove characters for rule X9 @@ -108,7 +107,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Re-usable list of level runs /// - private readonly List _levelRuns = new List(); + private readonly List _levelRuns = new(); /// /// Mapping for the current isolating sequence, built @@ -119,7 +118,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// A stack of pending isolate openings used by FindIsolatePairs() /// - private Stack? _pendingIsolateOpenings; + private readonly Stack _pendingIsolateOpenings = new(); /// /// 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 /// - private readonly List _pendingOpeningBrackets = new List(); + private readonly List _pendingOpeningBrackets = new(); /// /// Resolved list of paired brackets /// - private readonly List _pairedBrackets = new List(); + private readonly List _pairedBrackets = new(); /// /// Initializes a new instance of the class. @@ -228,7 +227,7 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? 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(); _pendingIsolateOpenings.Push(i); _hasIsolates = true; break; } case BidiClass.PopDirectionalIsolate: { - if (_pendingIsolateOpenings?.Count > 0) + if (_pendingIsolateOpenings.Count > 0) { - _isolatePairs ??= new BidiDictionary(); _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(_workingClasses, _isolatedRunMapping.AsSlice()); - _runOriginalClasses = new MappedArraySlice(_originalClasses, _isolatedRunMapping.AsSlice()); - _runLevels = new MappedArraySlice(_resolvedLevels, _isolatedRunMapping.AsSlice()); + var isolatedRunMapping = _isolatedRunMapping.AsSlice(); + _runResolvedClasses = new MappedArraySlice(_workingClasses, isolatedRunMapping); + _runOriginalClasses = new MappedArraySlice(_originalClasses, isolatedRunMapping); + _runLevels = new MappedArraySlice(_resolvedLevels, isolatedRunMapping); if (_hasBrackets) { - _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, _isolatedRunMapping.AsSlice()); - _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, _isolatedRunMapping.AsSlice()); + _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, isolatedRunMapping); + _runPairedBracketValues = new MappedArraySlice(_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(); - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 0f0b3235e1..106079de8e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/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 /// - internal struct BidiData : IDisposable + internal sealed class BidiData { private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; @@ -20,12 +20,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private ArrayBuilder _savedPairedBracketTypes; private ArrayBuilder _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 /// /// Gets the length of the data held by the BidiData /// - public int Length{get; private set; } + public int Length { get; private set; } /// /// 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() + /// + /// Resets the bidi data to a clean state. + /// + 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; } } } diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index e6b67bd383..1c11966a7d 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/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. /// /// The type of item contained in the array. - internal struct ArrayBuilder : IDisposable + internal struct ArrayBuilder where T : struct { private const int DefaultCapacity = 4; @@ -136,7 +135,7 @@ namespace Avalonia.Utilities } // Same expansion algorithm as List. - 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.Shared.Rent(newCapacity); + var array = new T[newCapacity]; if (_size > 0) { Array.Copy(_data!, array, _size); - ArrayPool.Shared.Return(_data!); } _data = array; @@ -182,13 +180,5 @@ namespace Avalonia.Utilities /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); - - public void Dispose() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index b70088a907..3cffef72c5 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/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 /// int IReadOnlyCollection.Count => Length; - - public void ReturnRent() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } } } + diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index c1146cceda..10c2f36f43 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/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()); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index 9d189d1950..eb69bed1e1 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/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(t.CodePoints).ToArray()); From 290c8fe16953b5bb4dd7098752c8b6e693f6ef79 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 17 Jan 2023 03:02:22 +0100 Subject: [PATCH 04/10] Removed most allocations for BidiReorder --- .../Media/TextFormatting/BidiReorderer.cs | 263 ++++++++++++++++++ .../Media/TextFormatting/TextFormatterImpl.cs | 16 +- .../Media/TextFormatting/TextLineImpl.cs | 236 +--------------- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 1 - .../Media/TextFormatting/Unicode/BiDiData.cs | 1 + src/Avalonia.Base/Utilities/ArrayBuilder.cs | 12 +- 6 files changed, 289 insertions(+), 240 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs new file mode 100644 index 0000000000..3fcb7bf420 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -0,0 +1,263 @@ +using System; +using System.Diagnostics; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Reorders text runs according to their bidi level. + /// + /// To avoid allocations, this class is designed to be reused. + internal sealed class BidiReorderer + { + private ArrayBuilder _runs; + private ArrayBuilder _ranges; + + public void BidiReorder(Span 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; + } + + /// + /// Reorders the runs from logical to visual order. + /// + /// + /// The first run index in visual order. + 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 + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7614c8e3dc..f3cc0a714e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/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(textRuns.Count); - CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns); + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels, processedRuns); for (var index = 0; index < processedRuns.Count; index++) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ae6df3a232..7fa9155b02 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/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 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; - } - - /// - /// Reorders a series of runs from logical to visual order, returning the left most run. - /// - /// - /// The ordered bidi run. - /// The . - 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!; - } - /// /// Tries to find the next character hit. /// @@ -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; - } - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index e770ba9e91..e960a510a9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/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 diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 106079de8e..226e5ad6bd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/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 /// + /// To avoid allocations, this class is designed to be reused. internal sealed class BidiData { private ArrayBuilder _classes; diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index 1c11966a7d..3a22fc7b9c 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/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; /// @@ -115,6 +115,16 @@ namespace Avalonia.Utilities return slice; } + /// + /// Appends an item. + /// + /// The item to append. + public void AddItem(T value) + { + var index = Length++; + _data![index] = value; + } + /// /// Clears the array. /// Allocated memory is left intact for future usage. From 4144be11fefdfbe632c49752dd5276395b5a41ae Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 17 Jan 2023 17:42:54 +0100 Subject: [PATCH 05/10] Pass GlyphInfo directly to GlyphRun --- src/Avalonia.Base/Media/GlyphRun.cs | 518 +++++++----------- .../Media/TextFormatting/GlyphInfo.cs | 36 ++ .../TextFormatting/InterWordJustification.cs | 2 +- .../Media/TextFormatting/ShapedBuffer.cs | 189 +------ .../Media/TextFormatting/ShapedTextRun.cs | 24 +- .../Media/TextFormatting/TextFormatterImpl.cs | 2 +- .../Platform/IPlatformRenderInterface.cs | 9 +- .../Composition/Server/FpsCounter.cs | 2 +- .../Utilities/BinarySearchExtension.cs | 3 +- .../HeadlessPlatformRenderInterface.cs | 3 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 83 +-- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 42 +- .../Media/GlyphRunTests.cs | 14 +- .../VisualTree/MockRenderInterface.cs | 3 +- .../NullRenderingPlatform.cs | 3 +- .../Media/GlyphRunTests.cs | 10 +- .../Media/GlyphRunTests.cs | 21 +- .../TextFormatting/TextFormatterTests.cs | 10 +- .../Media/TextFormatting/TextLayoutTests.cs | 55 +- .../Media/TextFormatting/TextLineTests.cs | 30 +- .../Media/TextFormatting/TextShaperTests.cs | 10 +- .../MockPlatformRenderInterface.cs | 3 +- ...ould_Render_GlyphRun_Geometry.expected.png | Bin 4149 -> 4222 bytes 23 files changed, 390 insertions(+), 682 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index fc4bc6aa1c..b637c94d88 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utilities; @@ -11,64 +13,112 @@ namespace Avalonia.Media /// public sealed class GlyphRun : IDisposable { - private static readonly IComparer s_ascendingComparer = Comparer.Default; - private static readonly IComparer s_descendingComparer = new ReverseComparer(); - private IGlyphRunImpl? _glyphRunImpl; - private IGlyphTypeface _glyphTypeface; private double _fontRenderingEmSize; private int _biDiLevel; private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; - private ReadOnlyMemory _characters; - private IReadOnlyList _glyphIndices; - private IReadOnlyList? _glyphAdvances; - private IReadOnlyList? _glyphOffsets; - private IReadOnlyList? _glyphClusters; + private IReadOnlyList _glyphInfos; + private bool _hasOneCharPerCluster; // if true, character index and cluster are similar /// - /// Initializes a new instance of the class by specifying properties of the class. + /// Initializes a new instance of the class by specifying properties of the class. /// /// The glyph typeface. /// The rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. /// The characters. - /// The glyph clusters. + /// The glyph indices. /// The bidi level. public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, ReadOnlyMemory characters, IReadOnlyList glyphIndices, - IReadOnlyList? glyphAdvances = null, - IReadOnlyList? glyphOffsets = null, - IReadOnlyList? glyphClusters = null, + int biDiLevel = 0) + : this(glyphTypeface, fontRenderingEmSize, characters, + CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel) + { + _hasOneCharPerCluster = true; + } + + /// + /// Initializes a new instance of the class by specifying properties of the class. + /// + /// The glyph typeface. + /// The rendering em size. + /// The characters. + /// The list of glyphs used. + /// The bidi level. + public GlyphRun( + IGlyphTypeface glyphTypeface, + double fontRenderingEmSize, + ReadOnlyMemory characters, + IReadOnlyList glyphInfos, int biDiLevel = 0) { - _glyphTypeface = glyphTypeface; + GlyphTypeface = glyphTypeface; _fontRenderingEmSize = fontRenderingEmSize; _characters = characters; - _glyphIndices = glyphIndices; + _glyphInfos = glyphInfos; - _glyphAdvances = glyphAdvances; + _biDiLevel = biDiLevel; + } - _glyphOffsets = glyphOffsets; + private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices, + double fontRenderingEmSize, IGlyphTypeface glyphTypeface) + { + var glyphIndexSpan = ListToSpan(glyphIndices); + var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan); - _glyphClusters = glyphClusters; + var glyphInfos = new GlyphInfo[glyphIndexSpan.Length]; + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; - _biDiLevel = biDiLevel; + for (var i = 0; i < glyphIndexSpan.Length; ++i) + { + glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale); + } + + return glyphInfos; + } + + private static ReadOnlySpan ListToSpan(IReadOnlyList list) + { + var count = list.Count; + + if (count == 0) + { + return default; + } + + if (list is ushort[] array) + { + return array.AsSpan(); + } + +#if NET6_0_OR_GREATER + if (list is List concreteList) + { + return CollectionsMarshal.AsSpan(concreteList); + } +#endif + + array = new ushort[count]; + for (var i = 0; i < count; ++i) + { + array[i] = list[i]; + } + + return array.AsSpan(); } /// /// Gets the for the . /// - public IGlyphTypeface GlyphTypeface => _glyphTypeface; + public IGlyphTypeface GlyphTypeface { get; } /// /// Gets or sets the em size used for rendering the . @@ -88,56 +138,17 @@ namespace Avalonia.Media /// /// public GlyphRunMetrics Metrics - { - get - { - _glyphRunMetrics ??= CreateGlyphRunMetrics(); - - return _glyphRunMetrics.Value; - } - } + => _glyphRunMetrics ??= CreateGlyphRunMetrics(); /// /// Gets or sets the baseline origin of the. /// public Point BaselineOrigin { - get - { - _baselineOrigin ??= CalculateBaselineOrigin(); - - return _baselineOrigin.Value; - } + get => _baselineOrigin ??= CalculateBaselineOrigin(); set => Set(ref _baselineOrigin, value); } - /// - /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. - /// - public IReadOnlyList GlyphIndices - { - get => _glyphIndices; - set => Set(ref _glyphIndices, value); - } - - /// - /// Gets or sets an array of values that represent the advances corresponding to the glyph indices. - /// - public IReadOnlyList? GlyphAdvances - { - get => _glyphAdvances; - set => Set(ref _glyphAdvances, value); - } - - /// - /// Gets or sets an array of values representing the offsets of the glyphs in the . - /// - public IReadOnlyList? GlyphOffsets - { - get => _glyphOffsets; - set => Set(ref _glyphOffsets, value); - } - /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// @@ -148,12 +159,16 @@ namespace Avalonia.Media } /// - /// Gets or sets a list of values representing a mapping from character index to glyph index. + /// Gets or sets the list of glyphs to use to render this run. /// - public IReadOnlyList? GlyphClusters + public IReadOnlyList GlyphInfos { - get => _glyphClusters; - set => Set(ref _glyphClusters, value); + get => _glyphInfos; + set + { + Set(ref _glyphInfos, value); + _hasOneCharPerCluster = false; + } } /// @@ -179,17 +194,7 @@ namespace Avalonia.Media /// The platform implementation of the . /// public IGlyphRunImpl GlyphRunImpl - { - get - { - if (_glyphRunImpl == null) - { - Initialize(); - } - - return _glyphRunImpl!; - } - } + => _glyphRunImpl ??= CreateGlyphRunImpl(); /// /// Obtains geometry for the glyph run. @@ -221,38 +226,32 @@ namespace Avalonia.Media if (IsLeftToRight) { - if (GlyphClusters != null) + if (characterIndex < Metrics.FirstCluster) { - if (characterIndex < Metrics.FirstCluster) - { - return 0; - } + return 0; + } - if (characterIndex > Metrics.LastCluster) - { - return Metrics.WidthIncludingTrailingWhitespace; - } + if (characterIndex > Metrics.LastCluster) + { + return Metrics.WidthIncludingTrailingWhitespace; } var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null) - { - var currentCluster = GlyphClusters[glyphIndex]; + var currentCluster = _glyphInfos[glyphIndex].GlyphCluster; - //Move to the end of the glyph cluster - if (characterHit.TrailingLength > 0) + //Move to the end of the glyph cluster + if (characterHit.TrailingLength > 0) + { + while (glyphIndex + 1 < _glyphInfos.Count && _glyphInfos[glyphIndex + 1].GlyphCluster == currentCluster) { - while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster) - { - glyphIndex++; - } + glyphIndex++; } } for (var i = 0; i < glyphIndex; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return distance; @@ -262,22 +261,19 @@ namespace Avalonia.Media //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null && GlyphClusters.Count > 0) + if (characterIndex > Metrics.LastCluster) { - if (characterIndex > Metrics.LastCluster) - { - return 0; - } + return 0; + } - if (characterIndex <= Metrics.FirstCluster) - { - return Size.Width; - } + if (characterIndex <= Metrics.FirstCluster) + { + return Size.Width; } - for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++) + for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return Size.Width - distance; @@ -322,11 +318,12 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; if (distance > currentX && distance <= currentX + advance) { @@ -340,11 +337,12 @@ namespace Avalonia.Media { currentX = Size.Width; - for (var index = GlyphIndices.Count - 1; index >= 0; index--) + for (var index = _glyphInfos.Count - 1; index >= 0; index--) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; var offsetX = currentX - advance; @@ -424,7 +422,7 @@ namespace Avalonia.Media /// public int FindGlyphIndex(int characterIndex) { - if (GlyphClusters == null || GlyphClusters.Count == 0) + if (_hasOneCharPerCluster) { return characterIndex; } @@ -433,7 +431,7 @@ namespace Avalonia.Media { if (IsLeftToRight) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return 0; @@ -446,15 +444,13 @@ namespace Avalonia.Media return 0; } - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } - var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; - - var clusters = GlyphClusters; + var comparer = IsLeftToRight ? GlyphInfo.ClusterAscendingComparer : GlyphInfo.ClusterDescendingComparer; // Find the start of the cluster at the character index. - var start = clusters.BinarySearch(characterIndex, comparer); + var start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); // No cluster found. if (start < 0) @@ -463,40 +459,38 @@ namespace Avalonia.Media { characterIndex--; - start = clusters.BinarySearch(characterIndex, comparer); + start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); } if (start < 0) { - goto result; + return 0; } } if (IsLeftToRight) { - while (start > 0 && clusters[start - 1] == clusters[start]) + while (start > 0 && _glyphInfos[start - 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start--; } } else { - while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start]) + while (start + 1 < _glyphInfos.Count && _glyphInfos[start + 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start++; } } - result: - if (start < 0) { return 0; } - if (start > GlyphIndices.Count - 1) + if (start > _glyphInfos.Count - 1) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return start; @@ -516,14 +510,14 @@ namespace Avalonia.Media var glyphIndex = FindGlyphIndex(index); - if (GlyphClusters == null) + if (_hasOneCharPerCluster) { - width = GetGlyphAdvance(index, out _); + width = _glyphInfos[index].GlyphAdvance; return new CharacterHit(glyphIndex, 1); } - var cluster = GlyphClusters[glyphIndex]; + var cluster = _glyphInfos[glyphIndex].GlyphCluster; var nextCluster = cluster; @@ -531,13 +525,13 @@ namespace Avalonia.Media while (nextCluster == cluster) { - width += GetGlyphAdvance(currentIndex, out _); + width += _glyphInfos[currentIndex].GlyphAdvance; if (IsLeftToRight) { currentIndex++; - if (currentIndex == GlyphClusters.Count) + if (currentIndex == _glyphInfos.Count) { break; } @@ -552,7 +546,7 @@ namespace Avalonia.Media } } - nextCluster = GlyphClusters[currentIndex]; + nextCluster = _glyphInfos[currentIndex].GlyphCluster; } var clusterLength = Math.Max(0, nextCluster - cluster); @@ -565,9 +559,9 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (int i = 1; i < GlyphClusters.Count; i++) + for (int i = 1; i < _glyphInfos.Count; i++) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -583,9 +577,9 @@ namespace Avalonia.Media } else { - for (int i = GlyphClusters.Count - 1; i >= 0; i--) + for (int i = _glyphInfos.Count - 1; i >= 0; i--) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -613,26 +607,6 @@ namespace Avalonia.Media return new CharacterHit(cluster, clusterLength); } - /// - /// Gets a glyph's width. - /// - /// The glyph index. - /// The current cluster. - /// The glyph's width. - private double GetGlyphAdvance(int index, out int cluster) - { - cluster = GlyphClusters != null ? GlyphClusters[index] : index; - - if (GlyphAdvances != null) - { - return GlyphAdvances[index]; - } - - var glyph = GlyphIndices[index]; - - return GlyphTypeface.GetGlyphAdvance(glyph) * Scale; - } - /// /// Calculates the default baseline origin of the . /// @@ -644,20 +618,17 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { - int firstCluster = 0, lastCluster = 0; + int firstCluster, lastCluster; - if (_glyphClusters != null && _glyphClusters.Count > 0) + if (Characters.IsEmpty) { - firstCluster = _glyphClusters[0]; - lastCluster = _glyphClusters[_glyphClusters.Count - 1]; + firstCluster = 0; + lastCluster = 0; } else { - if (!Characters.IsEmpty) - { - firstCluster = 0; - lastCluster = Characters.Length - 1; - } + firstCluster = _glyphInfos[0].GlyphCluster; + lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster; } if (!IsLeftToRight) @@ -671,9 +642,9 @@ namespace Avalonia.Media var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount); - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out _); + var advance = _glyphInfos[index].GlyphAdvance; widthIncludingTrailingWhitespace += advance; } @@ -684,14 +655,14 @@ namespace Avalonia.Media { for (var index = 0; index < glyphCount; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } else { - for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) + for (var index = _glyphInfos.Count - glyphCount; index < _glyphInfos.Count; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } @@ -710,7 +681,7 @@ namespace Avalonia.Media { if (isReversed) { - return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); + return GetTrailingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); } glyphCount = 0; @@ -720,84 +691,59 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = charactersSpan.Length - 1; i >= 0;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); - - if (!codepoint.IsWhiteSpace) - { - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; + var characterIndex = charactersSpan.Length - 1; - i -= count; - glyphCount++; - } - } - else + for (var i = _glyphInfos.Count - 1; i >= 0; i--) { - var characterIndex = charactersSpan.Length - 1; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = GlyphClusters.Count - 1; i >= 0; i--) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex -= characterLength; - characterIndex -= characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + while (i - 1 >= 0) + { + var nextCluster = _glyphInfos[i - 1].GlyphCluster; - while (i - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[i - 1]; + clusterLength++; + i--; - if (currentCluster == nextCluster) + if(characterIndex >= 0) { - clusterLength++; - i--; - - if(characterIndex >= 0) - { - codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); + codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); - characterIndex -= characterLength; - } - - continue; + characterIndex -= characterLength; } - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount++; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; } } return trailingWhitespaceLength; } - private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) + private int GetTrailingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) { glyphCount = 0; newLineLength = 0; @@ -806,71 +752,46 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = 0; i < charactersSpan.Length;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); + var characterIndex = 0; - if (!codepoint.IsWhiteSpace) - { - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; - - i += count; - glyphCount++; - } - } - else + for (var i = 0; i < _glyphInfos.Count; i++) { - var characterIndex = 0; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = 0; i < GlyphClusters.Count; i++) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex += characterLength; - characterIndex += characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + var j = i; - var j = i; + while (j - 1 >= 0) + { + var nextCluster = _glyphInfos[--j].GlyphCluster; - while (j - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[--j]; - - if (currentCluster == nextCluster) - { - clusterLength++; + clusterLength++; - continue; - } - - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount += clusterLength; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount += clusterLength; } } @@ -890,44 +811,17 @@ namespace Avalonia.Media field = value; } - /// - /// Initializes the . - /// - private void Initialize() + private IGlyphRunImpl CreateGlyphRunImpl() { - if (GlyphIndices == null) - { - throw new InvalidOperationException(); - } - - var glyphCount = GlyphIndices.Count; - - if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount) - { - throw new InvalidOperationException(); - } - - if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount) - { - throw new InvalidOperationException(); - } - var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); + return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); } public void Dispose() { _glyphRunImpl?.Dispose(); - } - - private class ReverseComparer : IComparer - { - public int Compare(T? x, T? y) - { - return Comparer.Default.Compare(y, x); - } + _glyphRunImpl = null; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs new file mode 100644 index 0000000000..36a07721a6 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a single glyph. + /// + public readonly record struct GlyphInfo(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, Vector GlyphOffset = default) + { + internal static Comparer ClusterAscendingComparer { get; } = + Comparer.Create((x, y) => x.GlyphCluster.CompareTo(y.GlyphCluster)); + + internal static Comparer ClusterDescendingComparer { get; } = + Comparer.Create((x, y) => y.GlyphCluster.CompareTo(x.GlyphCluster)); + + /// + /// Get the glyph index. + /// + public ushort GlyphIndex { get; } = GlyphIndex; + + /// + /// Get the glyph cluster. + /// + public int GlyphCluster { get; } = GlyphCluster; + + /// + /// Get the glyph advance. + /// + public double GlyphAdvance { get; } = GlyphAdvance; + + /// + /// Get the glyph offset. + /// + public Vector GlyphOffset { get; } = GlyphOffset; + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index b518d47a6d..6bfcfc06f8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -111,7 +111,7 @@ namespace Avalonia.Media.TextFormatting shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } - glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; + glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; } currentPosition += textRun.Length; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 41bba2cd09..f29bdd4459 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -1,14 +1,13 @@ using System; using System.Buffers; +using System.Collections; using System.Collections.Generic; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - public sealed class ShapedBuffer : IList, IDisposable + public sealed class ShapedBuffer : IReadOnlyList, IDisposable { - private static readonly IComparer s_clusterComparer = new CompareClusters(); - private GlyphInfo[]? _rentedBuffer; public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) @@ -42,14 +41,6 @@ namespace Avalonia.Media.TextFormatting public sbyte BidiLevel { get; } public bool IsLeftToRight => (BidiLevel & 1) == 0; - - public IReadOnlyList GlyphIndices => new GlyphIndexList(GlyphInfos); - - public IReadOnlyList GlyphClusters => new GlyphClusterList(GlyphInfos); - - public IReadOnlyList GlyphAdvances => new GlyphAdvanceList(GlyphInfos); - - public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); public ReadOnlyMemory Text { get; } @@ -73,13 +64,13 @@ namespace Avalonia.Media.TextFormatting } - var comparer = s_clusterComparer; + var comparer = GlyphInfo.ClusterAscendingComparer; - var clusters = GlyphInfos.Span; + var glyphInfos = GlyphInfos.Span; - var searchValue = new GlyphInfo(0, characterIndex); + var searchValue = new GlyphInfo(default, characterIndex, default); - var start = clusters.BinarySearch(searchValue, comparer); + var start = glyphInfos.BinarySearch(searchValue, comparer); if (start < 0) { @@ -87,9 +78,9 @@ namespace Avalonia.Media.TextFormatting { characterIndex--; - searchValue = new GlyphInfo(0, characterIndex); + searchValue = new GlyphInfo(default, characterIndex, default); - start = clusters.BinarySearch(searchValue, comparer); + start = glyphInfos.BinarySearch(searchValue, comparer); } if (start < 0) @@ -98,7 +89,7 @@ namespace Avalonia.Media.TextFormatting } } - while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster) + while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster) { start--; } @@ -118,8 +109,8 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(this, null); } - var firstCluster = GlyphClusters[0]; - var lastCluster = GlyphClusters[GlyphClusters.Count - 1]; + var firstCluster = GlyphInfos[0].GlyphCluster; + var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster; var start = firstCluster < lastCluster ? firstCluster : lastCluster; @@ -134,9 +125,7 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(first, second); } - int ICollection.Count => throw new NotImplementedException(); - - bool ICollection.IsReadOnly => true; + int IReadOnlyCollection.Count => GlyphInfos.Length; public GlyphInfo this[int index] { @@ -144,130 +133,9 @@ namespace Avalonia.Media.TextFormatting set => GlyphInfos[index] = value; } - int IList.IndexOf(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.Insert(int index, GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.RemoveAt(int index) - { - throw new NotImplementedException(); - } - - void ICollection.Add(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.Clear() - { - throw new NotImplementedException(); - } - - bool ICollection.Contains(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.CopyTo(GlyphInfo[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - bool ICollection.Remove(GlyphInfo item) - { - throw new NotImplementedException(); - } public IEnumerator GetEnumerator() => GlyphInfos.GetEnumerator(); - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - - private class CompareClusters : IComparer - { - private static readonly Comparer s_intClusterComparer = Comparer.Default; - - public int Compare(GlyphInfo x, GlyphInfo y) - { - return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster); - } - } - - private readonly struct GlyphAdvanceList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphAdvanceList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public double this[int index] => _glyphInfos[index].GlyphAdvance; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphIndexList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphIndexList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public ushort this[int index] => _glyphInfos[index].GlyphIndex; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphClusterList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphClusterList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public int this[int index] => _glyphInfos[index].GlyphCluster; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphOffsetList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphOffsetList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public Vector this[int index] => _glyphInfos[index].GlyphOffset; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void Dispose() { @@ -279,35 +147,4 @@ namespace Avalonia.Media.TextFormatting } } } - - public readonly record struct GlyphInfo - { - public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default) - { - GlyphIndex = glyphIndex; - GlyphAdvance = glyphAdvance; - GlyphCluster = glyphCluster; - GlyphOffset = glyphOffset; - } - - /// - /// Get the glyph index. - /// - public ushort GlyphIndex { get; } - - /// - /// Get the glyph cluster. - /// - public int GlyphCluster { get; } - - /// - /// Get the glyph advance. - /// - public double GlyphAdvance { get; } - - /// - /// Get the glyph offset. - /// - public Vector GlyphOffset { get; } - } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 583f2e49f1..d444a58297 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -40,25 +40,14 @@ namespace Avalonia.Media.TextFormatting public override Size Size => GlyphRun.Size; - public GlyphRun GlyphRun - { - get - { - if(_glyphRun is null) - { - _glyphRun = CreateGlyphRun(); - } - - return _glyphRun; - } - } + public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun(); /// public override void Draw(DrawingContext drawingContext, Point origin) { using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) { - if (GlyphRun.GlyphIndices.Count == 0) + if (GlyphRun.GlyphInfos.Count == 0) { return; } @@ -117,7 +106,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < ShapedBuffer.Length; i++) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (currentWidth + advance > availableWidth) { @@ -141,7 +130,7 @@ namespace Avalonia.Media.TextFormatting for (var i = ShapedBuffer.Length - 1; i >= 0; i--) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (width + advance > availableWidth) { @@ -195,10 +184,7 @@ namespace Avalonia.Media.TextFormatting ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, Text, - ShapedBuffer.GlyphIndices, - ShapedBuffer.GlyphAdvances, - ShapedBuffer.GlyphOffsets, - ShapedBuffer.GlyphClusters, + ShapedBuffer, BidiLevel); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index f3cc0a714e..8ffe3e5da2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -611,7 +611,7 @@ namespace Avalonia.Media.TextFormatting var properties = paragraphProperties.DefaultTextRunProperties; var glyphTypeface = properties.Typeface.GlyphTypeface; var glyph = glyphTypeface.GetGlyph(s_empty[0]); - var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; + var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 1828f24aff..9f4e96da25 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Metadata; namespace Avalonia.Platform @@ -166,11 +167,9 @@ namespace Avalonia.Platform /// /// The glyph typeface. /// The font rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. - /// - IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets); + /// The list of glyphs. + /// An . + IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos); /// /// Creates a backend-specific object using a low-level API graphics context diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 18cb7a6308..32923a5257 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -32,7 +32,7 @@ internal class FpsCounter { var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.AsMemory(), new ushort[] { glyph }); } } diff --git a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs index b7060d2e21..defc9b1639 100644 --- a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs +++ b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs @@ -14,7 +14,6 @@ // under the License. // Copied from: https://github.com/toptensoftware/RichTextKit -using System; using System.Collections.Generic; namespace Avalonia.Utilities @@ -39,7 +38,7 @@ namespace Avalonia.Utilities /// The value to search for /// The comparer /// The index of the found item; otherwise the bitwise complement of the index of the next larger item - public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) where T : IComparable + public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) { return list.BinarySearch(0, list.Count, value, comparer); } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index cb79cc85db..e368ddc373 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -9,6 +9,7 @@ using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; namespace Avalonia.Headless { @@ -118,7 +119,7 @@ namespace Avalonia.Headless return new HeadlessGeometryStub(new Rect(glyphRun.Size)); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return new HeadlessGlyphRunStub(); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index b202b60cdf..3fb7491898 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,16 +1,11 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading; - -using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using SkiaSharp; namespace Avalonia.Skia @@ -88,9 +83,9 @@ namespace Avalonia.Skia var (currentX, currentY) = glyphRun.BaselineOrigin; - for (var i = 0; i < glyphRun.GlyphIndices.Count; i++) + for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { - var glyph = glyphRun.GlyphIndices[i]; + var glyph = glyphRun.GlyphInfos[i].GlyphIndex; var glyphPath = skFont.GetGlyphPath(glyph); if (!glyphPath.IsEmpty) @@ -98,14 +93,7 @@ namespace Avalonia.Skia path.AddPath(glyphPath, (float)currentX, (float)currentY); } - if (glyphRun.GlyphAdvances != null) - { - currentX += glyphRun.GlyphAdvances[i]; - } - else - { - currentX += glyphPath.Bounds.Right; - } + currentX += glyphRun.GlyphInfos[i].GlyphAdvance; } return new StreamGeometryImpl(path); @@ -213,17 +201,16 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format, alphaFormat); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, - IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { if (glyphTypeface == null) { throw new ArgumentNullException(nameof(glyphTypeface)); } - if (glyphIndices == null) + if (glyphInfos == null) { - throw new ArgumentNullException(nameof(glyphIndices)); + throw new ArgumentNullException(nameof(glyphInfos)); } var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; @@ -242,59 +229,25 @@ namespace Avalonia.Skia var builder = new SKTextBlobBuilder(); - var count = glyphIndices.Count; + var count = glyphInfos.Count; - if (glyphOffsets != null && glyphAdvances != null) - { - var runBuffer = builder.AllocatePositionedRun(font, count); + var runBuffer = builder.AllocatePositionedRun(font, count); - var glyphSpan = runBuffer.GetGlyphSpan(); - var positionSpan = runBuffer.GetPositionSpan(); + var glyphSpan = runBuffer.GetGlyphSpan(); + var positionSpan = runBuffer.GetPositionSpan(); - var currentX = 0.0; + var currentX = 0.0; - for (int i = 0; i < glyphOffsets.Count; i++) - { - var offset = glyphOffsets[i]; - - glyphSpan[i] = glyphIndices[i]; - - positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); - - currentX += glyphAdvances[i]; - } - } - else + for (int i = 0; i < count; i++) { - if (glyphAdvances != null) - { - var runBuffer = builder.AllocateHorizontalRun(font, count, 0); - - var glyphSpan = runBuffer.GetGlyphSpan(); - var positionSpan = runBuffer.GetPositionSpan(); - - var currentX = 0.0; + var glyphInfo = glyphInfos[i]; + var offset = glyphInfo.GlyphOffset; - for (int i = 0; i < glyphAdvances.Count; i++) - { - glyphSpan[i] = glyphIndices[i]; + glyphSpan[i] = glyphInfo.GlyphIndex; - positionSpan[i] = (float)currentX; + positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); - currentX += glyphAdvances[i]; - } - } - else - { - var runBuffer = builder.AllocateRun(font, count, 0, 0); - - var glyphSpan = runBuffer.GetGlyphSpan(); - - for (int i = 0; i < glyphIndices.Count; i++) - { - glyphSpan[i] = glyphIndices[i]; - } - } + currentX += glyphInfo.GlyphAdvance; } return new GlyphRunImpl(builder.Build()); diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5887ba2172..d9cd0590fc 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -7,6 +7,7 @@ using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; @@ -157,12 +158,11 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, - IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; - var glyphCount = glyphIndices.Count; + var glyphCount = glyphInfos.Count; var run = new SharpDX.DirectWrite.GlyphRun { @@ -174,44 +174,23 @@ namespace Avalonia.Direct2D1 for (var i = 0; i < glyphCount; i++) { - indices[i] = (short)glyphIndices[i]; + indices[i] = (short)glyphInfos[i].GlyphIndex; } run.Indices = indices; run.Advances = new float[glyphCount]; - var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight); - - if (glyphAdvances == null) - { - for (var i = 0; i < glyphCount; i++) - { - var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale; - - run.Advances[i] = advance; - } - } - else - { - for (var i = 0; i < glyphCount; i++) - { - var advance = (float)glyphAdvances[i]; - - run.Advances[i] = advance; - } - } - - if (glyphOffsets == null) + for (var i = 0; i < glyphCount; i++) { - return new GlyphRunImpl(run); + run.Advances[i] = (float)glyphInfos[i].GlyphAdvance; } run.Offsets = new GlyphOffset[glyphCount]; for (var i = 0; i < glyphCount; i++) { - var (x, y) = glyphOffsets[i]; + var (x, y) = glyphInfos[i].GlyphOffset; run.Offsets[i] = new GlyphOffset { @@ -254,11 +233,12 @@ namespace Avalonia.Direct2D1 using (var sink = pathGeometry.Open()) { - var glyphs = new short[glyphRun.GlyphIndices.Count]; + var glyphInfos = glyphRun.GlyphInfos; + var glyphs = new short[glyphInfos.Count]; - for (int i = 0; i < glyphRun.GlyphIndices.Count; i++) + for (int i = 0; i < glyphInfos.Count; i++) { - glyphs[i] = (short)glyphRun.GlyphIndices[i]; + glyphs[i] = (short)glyphInfos[i].GlyphIndex; } glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink); diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 363fb3f5b3..3573ba6b07 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System; +using System.Linq; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Utilities; @@ -179,12 +181,14 @@ namespace Avalonia.Base.UnitTests.Media private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphClusters, int bidiLevel = 0) { var count = glyphAdvances.Length; - var glyphIndices = new ushort[count]; - var characters = Enumerable.Repeat('a', count).ToArray(); + var glyphInfos = new GlyphInfo[count]; + for (var i = 0; i < count; ++i) + { + glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]); + } - return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances, - glyphClusters: glyphClusters, biDiLevel: bidiLevel); + return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel); } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 42e33729ac..93d97e6cfb 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; namespace Avalonia.Base.UnitTests.VisualTree { @@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index a272d89b8a..a802cd0958 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Microsoft.Diagnostics.Runtime; namespace Avalonia.Benchmarks @@ -120,7 +121,7 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return new MockGlyphRun(); } diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 31e485448e..772d6e1023 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Documents; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Xunit; #if AVALONIA_SKIA @@ -170,11 +171,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[0]) * scale; - var advances = new[] { advance, advance, advance}; + var glyphInfos = new[] + { + new GlyphInfo(glyphIndices[0], 0, advance), + new GlyphInfo(glyphIndices[1], 1, advance), + new GlyphInfo(glyphIndices[2], 2, advance) + }; var characters = new[] { 'A', 'B', 'C' }; - GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices, advances); + GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphInfos); } public GlyphRun GlyphRun { get; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 04bc401479..f2d6670be5 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -134,11 +134,11 @@ namespace Avalonia.Skia.UnitTests.Media foreach (var rect in rects) { - var currentCluster = glyphRun.GlyphClusters[index]; + var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster; - while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphClusters.Count) + while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphInfos.Count) { - currentCluster = glyphRun.GlyphClusters[++index]; + currentCluster = glyphRun.GlyphInfos[++index].GlyphCluster; } //Non trailing edge @@ -161,15 +161,15 @@ namespace Avalonia.Skia.UnitTests.Media var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace; - var rects = new List(glyphRun.GlyphAdvances!.Count); + var rects = new List(glyphRun.GlyphInfos!.Count); var lastCluster = -1; - for (var index = 0; index < glyphRun.GlyphAdvances.Count; index++) + for (var index = 0; index < glyphRun.GlyphInfos.Count; index++) { - var currentCluster = glyphRun.GlyphClusters![index]; + var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster; - var advance = glyphRun.GlyphAdvances[index]; + var advance = glyphRun.GlyphInfos[index].GlyphAdvance; if (lastCluster != currentCluster) { @@ -216,10 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, shapedBuffer.Text, - shapedBuffer.GlyphIndices, - shapedBuffer.GlyphAdvances, - shapedBuffer.GlyphOffsets, - shapedBuffer.GlyphClusters, + shapedBuffer.GlyphInfos, shapedBuffer.BidiLevel); if(shapedBuffer.BidiLevel == 1) @@ -233,7 +230,7 @@ namespace Avalonia.Skia.UnitTests.Media private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(), textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index b90752861c..6b9fb579b1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -585,7 +585,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expectedRuns = expectedTextLine.TextRuns.Cast().ToList(); - var expectedGlyphs = expectedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); + var expectedGlyphs = expectedRuns + .SelectMany(run => run.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex) + .ToList(); for (var i = 0; i < text.Length; i++) { @@ -604,7 +606,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedRuns = textLine.TextRuns.Cast().ToList(); - var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); + var actualGlyphs = shapedRuns + .SelectMany(x => x.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex) + .ToList(); Assert.Equal(expectedGlyphs, actualGlyphs); } @@ -706,7 +710,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(), textShaperImpl: new TextShaperImpl())); AvaloniaLocator.CurrentMutable diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 1fd26748cd..a24a0fcf70 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -142,8 +142,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting black, textWrapping: TextWrapping.Wrap); - var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() - .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); + var expectedGlyphs = GetGlyphs(expected); var outer = new GraphemeEnumerator(text); var inner = new GraphemeEnumerator(text); @@ -175,8 +174,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textWrapping: TextWrapping.Wrap, textStyleOverrides: spans); - var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() - .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); + var actualGlyphs = GetGlyphs(actual); Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count); @@ -196,6 +194,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting i += outer.Current.Text.Length; } } + + static List GetGlyphs(TextLayout textLayout) + => textLayout.TextLines + .Select(line => string.Join('|', line.TextRuns + .Cast() + .SelectMany(run => run.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphIndex))) + .ToList(); } [Fact] @@ -484,13 +489,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - var glyphClusters = shapedRun.ShapedBuffer.GlyphClusters; + var glyphClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToArray(); - var expected = clusters.Skip(index).Take(glyphClusters.Count).ToArray(); + var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray(); Assert.Equal(expected, glyphClusters); - index += glyphClusters.Count; + index += glyphClusters.Length; } } } @@ -515,13 +520,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, layout.TextLines[0].TextRuns.Count); - Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Count); + Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphInfos.Count); - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[5]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[5].GlyphCluster); if (expectedLength == 7) { - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[6]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[6].GlyphCluster); } } } @@ -562,9 +567,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); - foreach (var glyph in textRun.GlyphRun.GlyphIndices) + foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos) { - Assert.Equal(replacementGlyph, glyph); + Assert.Equal(replacementGlyph, glyphInfo.GlyphIndex); } } } @@ -776,8 +781,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width); } - var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast()) - .SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToArray(); + var rects = layout.TextLines + .SelectMany(x => x.TextRuns.Cast()) + .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance) + .ToArray(); for (var i = 0; i < SingleLineText.Length; i++) { @@ -865,10 +872,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var currentX = 0.0; - for (var i = 0; i < firstRun.GlyphRun.GlyphClusters.Count; i++) + for (var i = 0; i < firstRun.GlyphRun.GlyphInfos.Count; i++) { - var cluster = firstRun.GlyphRun.GlyphClusters[i]; - var advance = firstRun.GlyphRun.GlyphAdvances[i]; + var cluster = firstRun.GlyphRun.GlyphInfos[i].GlyphCluster; + var advance = firstRun.GlyphRun.GlyphInfos[i].GlyphAdvance; hit = layout.HitTestPoint(new Point(currentX, 0)); @@ -895,10 +902,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting currentX = firstRun.Size.Width + 0.5; - for (var i = 0; i < secondRun.GlyphRun.GlyphClusters.Count; i++) + for (var i = 0; i < secondRun.GlyphRun.GlyphInfos.Count; i++) { - var cluster = secondRun.GlyphRun.GlyphClusters[i]; - var advance = secondRun.GlyphRun.GlyphAdvances[i]; + var cluster = secondRun.GlyphRun.GlyphInfos[i].GlyphCluster; + var advance = secondRun.GlyphRun.GlyphInfos[i].GlyphAdvance; hit = layout.HitTestPoint(new Point(currentX, 0)); @@ -932,7 +939,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var firstRun = (ShapedTextRun)textLine.TextRuns[0]; - var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0]; + var firstCluster = firstRun.ShapedBuffer.GlyphInfos[0].GlyphCluster; var characterHit = textLine.GetCharacterHitFromDistance(0); @@ -946,7 +953,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); - var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0]; + var firstAdvance = firstRun.ShapedBuffer.GlyphInfos[0].GlyphAdvance; Assert.Equal(firstAdvance, distance, 5); @@ -991,9 +998,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedRuns = textLine.TextRuns.Cast().ToList(); - var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList(); + var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster).ToList(); - var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList(); + var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance).ToList(); var currentX = 0.0; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 6993c70e8b..544b84912e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -95,9 +95,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - clusters.AddRange(shapedRun.IsReversed ? - shapedRun.ShapedBuffer.GlyphClusters.Reverse() : - shapedRun.ShapedBuffer.GlyphClusters); + var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + + clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); @@ -142,9 +142,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - clusters.AddRange(shapedRun.IsReversed ? - shapedRun.ShapedBuffer.GlyphClusters.Reverse() : - shapedRun.ShapedBuffer.GlyphClusters); + var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + + clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } clusters.Reverse(); @@ -247,7 +247,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters) + var clusters = textLine.TextRuns + .Cast() + .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster) .ToArray(); var previousCharacterHit = new CharacterHit(text.Length); @@ -313,11 +315,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var glyphRun = textRun.GlyphRun; - for (var i = 0; i < glyphRun.GlyphClusters!.Count; i++) + for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { - var cluster = glyphRun.GlyphClusters[i]; + var cluster = glyphRun.GlyphInfos[i].GlyphCluster; - var advance = glyphRun.GlyphAdvances[i]; + var advance = glyphRun.GlyphInfos[i].GlyphAdvance; var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); @@ -750,7 +752,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - var currentClusters = shapedBuffer.GlyphClusters.ToList(); + var currentClusters = shapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToList(); foreach (var currentCluster in currentClusters) { @@ -783,11 +785,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++) + for (var index = 0; index < shapedBuffer.GlyphInfos.Length; index++) { - var currentCluster = shapedBuffer.GlyphClusters[index]; + var currentCluster = shapedBuffer.GlyphInfos[index].GlyphCluster; - var advance = shapedBuffer.GlyphAdvances[index]; + var advance = shapedBuffer.GlyphInfos[index].GlyphAdvance; if (lastCluster != currentCluster) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 834dce4a90..3f02867aa9 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -19,10 +19,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); - Assert.Equal(0, shapedBuffer.GlyphClusters[0]); - Assert.Equal(1, shapedBuffer.GlyphClusters[1]); - Assert.Equal(1, shapedBuffer.GlyphClusters[2]); + Assert.Equal(shapedBuffer.GlyphInfos.Length, text.Length); + Assert.Equal(0, shapedBuffer.GlyphInfos[0].GlyphCluster); + Assert.Equal(1, shapedBuffer.GlyphInfos[1].GlyphCluster); + Assert.Equal(1, shapedBuffer.GlyphInfos[2].GlyphCluster); } } @@ -36,7 +36,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(100, shapedBuffer.GlyphAdvances[0]); + Assert.Equal(100, shapedBuffer.GlyphInfos[0].GlyphAdvance); } } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index f9e1e45098..d56e360e22 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -4,6 +4,7 @@ using System.IO; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Rendering; using Moq; @@ -146,7 +147,7 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return Mock.Of(); } diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 407b67b8a0d49afd3cd0f616c45c01d9fd480222..547e9b276380f69573a79583543ae3419d6a4c0f 100644 GIT binary patch literal 4222 zcmbuD_dgVlq-?gRwfx=G} zVod!{q13Q=5)uY!h`Ne#VE%rAfd}WTzw%hY1INLDG|6!)@ucQ585zSr{NLI@9#x3A z4~c>{rPxa9bU|xr6E{(a2Bk*0#^2_L+5@`c-%yw+QC1f+@6)U<Pj3O*xh#eu__8-l}SQ5Yg ze<3Ug+M_WO1G;M=U+{dWjptq|j^`lNsb*XuVOU{UnQFHEt463jed5GRaigI4u|(GM z&#lQzuicbJ-#{}z%W_V2S*?{#z;ZH&Y76nq4Ofc(^-P1EQ4cV8CEBq#3wXp1bSIE{ zawghp`jCD1Iq%qNxh9;L(^XaKdl}GDV%`*}`A-JXTOHuHW}W%+A7pS78FY0GI1+fo z3=0#zM{|}krN9!^x-nDCRlt=P<$cboM}Rf{+Xsh9yfF-z!k{h2JeN(&-4CanJaIUBgLV>jj^q$r9S z!yPBoecqDu7BIcXJPsJQ^1ENN+?fIgLX_CtFwmUB`z8pzto}2rm9LyvX``x9pFlZ$) z>^%NfLfg@WMQ=Sc{LmjvYA*K447mQEq9{QC{63OT5wZ0&OctGula^OfZ3pU|;=9g) zRi4R!?Wd-CN>*ANY;()!Xe7x0ns4{R6Z4LRZboz2g)b!*hDfXdpiA_)GJ(I9W% z^LC$rD4QSI`^CFWu<$t4`*9u{aXbe(>lKS3;p}T^?b~WFaI@gm1Y(+3{4HzbvAhMV z#8KRpWCO)~61%v4hwNt&hN&Ep{PAxL6gEC>ZagYI7X6Tb>uYKF&vIY#GqtWJXLfj5 zBTCyY7Dy#_!R_7on8xWgx@W&k03q1eFjBB3=PGbfPp?WHGn4OX@m-kJeA})os(~ue z^v>bZ2y)(D^o1L~6zw&_Wi%0J`;@V9FX58CCzjpFPH7Dgr9u}DeWHcM$uk?ZTR_cp zxBYF9&jx3!A~^Fj6K!dc;yN1dhW3WKD3oOUdY`+n5U#bEfLniXU-*42pD&f&7iNu| zFGb4Y=ndDhGuWvoR0fdm0`{(D;OWuLXf^pZ;m&^djrH>;Gn|*CTx0frI9u_u5CQ$_ zFJP%2f4OpxIQ8EtO2LAt2i5T@cSyNUjQEh8(EOH^<~rh-&dR~>=Nl}`;$j}p^^u|! zTk;}eLcnD za|QC)*q&Vzpt!NN>Rob>e;ZOR0i`tX@~qw4XsQNM+lp0O$jw@RKptN_;eKPldF%a`1-pKrPh6-|~LjvxU3+IO^Ru`OnHShO8;*s*eGw|p#TTFF^ZPfNAc{{hD zzoE`;BTetVrG;F_rA2#tIag^@?DK|$tPTo1>^l^@z&d`!1MS9lol-zr0|kEqD?dnU zC#!I~`#HbA%w-wXX;R*8o9r)i`fu#$Z&%&McCK9NaUm{cRd)q)|8X!HPnmALP20x! z73`FI9y23ZVQkt9)`qvyf}XUZqgqN`wcc4$+N4%x$Yf>*s5=O*Fql|mvM}WVUh}t= zazE<_r^vu9XZF|hiby>q=M|w>+fIEh{`{OaKA{yGZqJ0GyDzf&b;$1plkAdniazmW zx&l}dHKTINwky6(A-pE;eeUb@g)v_kXFFYkBEh<5;C-;D0Lzm8IFc=tSC5--d!()t z*UxzSHg-g6#fKZ`s?|IuZO@bwmdlhyACa7GXA{d0=IC^VZRP~JdG<>luxZKu>G@LD z$X-(A<=qvM)?fl6EDg7Is-_|Pz>rht)oM%zP2MyIr?&UTj@kSr?qOSvPv6J9)tD_g zxf9l5gEx+CtfhXH0&@MwZB_J!?snG zhSp0DkyMp&XCE_BKI5;z&mwROJ3&cNh5@Nf65X>wC~Qx)NX0Ile1=+V=HaMLL1G^? zI#wXMU2bu-Uf}b)K1K5A)~HO%`oiT{gwn?MCyp%r1mSGfT)Oafmn?M6z;C-<1*ID9 z4L4mT`xDA!L>Xq1h7>w3_jcx0Zgx_J!h;ibr^%O+g9}sdl0fi^^UXH|wYH`8dRb%4 z#En@cEKPi##=feut(RcC<}pZyiWWm3uf1gbEMv+z(A@{7xs%+)aFrTk9I#J<(YbNR zO@}nETWyUUWh)NU_33OlP=#G2)P<%czWh3r?HfiDo&;_O2elV&-49Bwu4W4HOJ}K{ zlmBtcuG2xG+hV%4xNC_T#IiX`hDV7X)E~*|eMu;khHy~_KFE7c-uUNh{-3efI_Zu< zx{bc4oxYVtr2{Rryq{E}s5sj(8IL2rRb!lU8ZvV}q%Y5t9p=3dBb`*BXC`wFmh}=C zFh0`9BXe<9m;Dr3=xqkO^yPrfLHm-tPBVXTN+BRR{($hVt=+iC*v`C4d}->t zWzkl17M_8^!vn&b(w@V<0y(W`*yp--NuWmNv36-*rtTYIC1@bx_a{kd`0`mEnbj*y z(U!dkFY~v?^!`A8?MW}j0iCs!D1Af^X9yo3=vBGMN_p5W+bIXDHBkI_fU`*Jjl1op z%a<1w&2aATS_=W~i-x%2%N9laU;N_hFG@p{a?R??iW;q6*@rkX_Pl0zyqma=pf24j zc%avBma99Me)SCN-;US(P((;BzB>Kj&H)KLrQjuZ@nH8)0gHG00*+QcxeqYEGuoRW zJ$xlR9b+8UuVYkMf6MsO(>2EaP0omUr|^_d(A-V1wXm!@Ewd5q)Wnq!7CCuv!9HuM zBjxwQkm5`$Q9N35eeUiPhn+=9m5_&gc5Qar^y|gi|5}1h$)&ZOy%E!Y*!p8!m!uwY zP(1KVs+a+nWOHUKt)DYRlj6RHXa&~hhns#Yd5Q2sOBnAsC%sXpazNlm#cl}=e2UNz zi4z~Ys7vZ)`s~~dwnBrRQzMxxdOyPGN4TUdrM(zW*Rcr?*@8WK{R}fHxdavHI?S(z zZGYsyuA~8s6dVrLSUO>)EwZLhiD#|C4`6K_`^kE$%b8<3xqr z7TspQ67RHG!d8fo1Kl5-8neVQ17gKZc#dFQT52S(6MCe;eA9~e705i(aIU?tq!BO4 zEQ|~aT=jO;pcTDRa0|^45xV?C_+q;{$!*?UTpDyLYmb{?q!bmi9PG0%RC)vCRyWs8< z4Y1}FO|vO*Rm)j^hPLdl5#6M)3x=zT#$6Ek@O=&&TfNe7sCPo__qz|G z6COUpV(N~6q2mqXgyKcF7gSL7q3PUPW~iM!=xX2OgXZ@!Gny^$tD~nVav?+WOT;c`QhYxKX8L5zh@x@FCM^rw-^nBj`TH=Dt&0i#M%!8~&+Mr)k;w zHG$J=In$FhZ2JV>LiF@1+vmG08Ar+IS-QPTQO~o6hiA-QzSy8a75m6GLDFlv;LaIISh5#3JS*cv=77M}$AXBfaqkujq7{$Yc6ic)Fo>NeM_e!uL*Qu0MWmZYg zVOzMaX#MT8R8u)cq$L^UR1;J&JZJNDnAgxQ*nS-Lt(Iq_VN6guQO%#7?V?$MkLc?C@G!9B4|qU?mY#;t!(A1SIb zxQ|eBWhoV4V(<;=Wxt$qK;hl@qtn#g*rMb))YyUc!9qyx8Ybf*2x%v z?ju@28OIktww&-ZrDN+=b=kc^lJ*fV6KG(s(MjEo2vI_UC6pBV^l zPcGq}FiMbLb1tm*XSs2lCmS#(V*U7<`*6{-*@x@Iw+fHnd9`=?m*B8-A*T;TFOBkH zNqYS9fo&~f0O?BhUcxA4iu5_jEZHq5!}ZIT%3x+y4R@XutpyWzn_BhsNq~iz+yM#k zGNJWjks|G?)WK~=y<+A38TEK?cE+jJETEzX+09#bvGxDk>V|x~A&*d7Cy9!yIl7Sdso@G zEHFzuBdh&~K^ppH`=^)1fq_eWnqMA#D`)h4odK%rYAFbo5()Ib# zDukn7jP?&Vh@kf8gl@T*H*{Ckj!NhL+bdOn2xC%AIsRG}c9kZJM%OjdCTPBI6hcQ6 zU7+WQHwIZSS*HTrC$~=(aO^AQp8B;}c~*2+>GIqy;lVFUmqGn!o!o})=@-Zd8R?bl zY}cHbV_wGCXu#yIJf4k|qqAq~&@UEG&g--ed1|dx+dWf& z&m_~-W1-P%QLjF3*GfjHY+RAGe|sj@&8y{>Y-v{^n^Mi4}mB8K~ z4QHTzRdpPq+^N{J$KRvF*466-$-H2bW(aM<3(HuT+Qlk5N)0I9$NZ@#Ga$N5BR|k( zHxFJ2*C_1r4G9Zqz5Yt$E4-D(y5mQM*YmN7U@3c1sIO2`u4g%vUKmxNS3X}^DuY&K zu&?;J^^+aaUFp(lGbATYn58Ah3-gOn$;z7Pm`#e21@l;8j)u)Y3QhEfWKOmh51Ho~ z`4=X30TiTU3iygIL^{)XE|_Teai7oAme%8phB;P_}B8lVS1v z=7M@vnsf4}gBqkOUT+GvkNuZvG5LM)ZtEd`J$(1sU}=lN7`w(1JwIUAGK9+A{;9*k z-Ox@hAEu^)S`N3`Lzypq2;z2#@UvgW1xdxpj7cDWEJCv{+l^i2_kh43BPp$@;1|*O zW@yv66Kwl-gtJIoTjSHbsnxYVHtv@DBRYOc@)AU_q>iqDIha~2(DwPVdZ{PcX1(~1 z*r(KBA?{sPpv6lJuyy9`bg#{%?4RxIXr*H9Aa7aMT==1?AD1r~w}IP?sCzWdxl z-i!zY?+}L6i~zqcG6*8f0XsUZQcu75>FZ@tujffHY4 zacn+QRFlR#rxbsSt$(MsvI^O`YB(|E;)uTB;J?kO#lkvL5rY))`%Dz66 z`Ju$bCd%wf9f?^%h6n>hn&Hg+r2F<(45IDSeognKOxsqD>-s%Eu+~hQN3Z*G70>Zh zB}hfS0dqH=2cSdvK6}#Lo{8Q}h9k&nuMnYmT@XxuB@U4+M{7=LJdVA`Mre8C<*EA| zz`0Ta5`+sYe1;&$kGIkg&Q1{ivPqoEH|E|sPLN0f&4yHpsU`eIZ!N~t_OF*xT|xs? zX(Jr%6PIP@5Eg5yB=HoQ8!gtx^@lfh)M1f?V@cvGu%}hL(t>X8G^4vz)bJ{ie} zdk_D5+gAlEZNT4z$mW z=>$|%%bu92iF1F)&?4t?Ou&M!#1a5qZH&gy(wc{5017=(JFYGsrT3=9Zr|A4#4gp` z%@_2vcw%hnwPs}F`O)BuQDQ@!Cg|K${A7vmWD4r9B(K|AUM9!~6B(Y)as3S$mAG8$w3syjF`91VAsR8kk7F&@7qtc z43;9!RJ=o8PU?7g>t^)Tir34^Mrhi?_Eq)Riw;UvG9RbEEj(xB&5-du zb=s*h+Z~u1dmao)dN7I07>=G)ggTJ7g=WQo3L96oYN!Bci9jXH@&x{!Pw2dsgG7XA zy_3M%E=trL@fgDIsG)ED<4?~s!8QT^0_=jitOe}Ls%ncA^Vb3MPS*ltu{4LnPT~;n zR!M8aU$N^jW4RqVPV8z`Qa{k$F8D3sVVMm7Ve|Kd-L3bm=t!c|kinAxNK(If%mQ!K zrphS|RJ3n;;+8l7AB6QFnLz9G-MPSir~oGb7%HdhKLce^9x*1bI!@>1SKRTjGI*6# zEPJ4(nO=Bdkobns`$_a%O80|Hbjj=a<$GS)(j%Yd06HFKVft+^NA1t*XC?g2Q;ffC zow~b*>we2aVCf(LHQ$j!>dBUj~ubO}v(a0^T#l3s*%hMsQ$=1?8 z`5fM&c4@TgKVVDwWKeVg)M#^>KITVc{_@@MTQl&CMq(qGVtdC^<{{ilVuM6d3SVge+^Pj7vNW--AJb$vas zX2YpWD;E(b$ z>uS<|it#6?#l0wkM(49b5|-XhV?VCW98qI5MHKKOe06Xn@Jw`8hkavJe}`1(>(8DA z>55be`P=i##QF-Lax`D8@*S+2q8JF!9dm^CRZX86|ajl(g%J zStfco&0FL%`K<61;T+&ap78Wo)U~dG2>~RkzcSURR~VN1^MQ%v{61-MByGB5h{{jI zv9B?MPvf#PyqWpFik^Vn)x@5d$0!rOnZ}RT{z+HH4xq(hg#yKGcPUxLSlw zJxlgOkYDpDrFiB-PCr33~sUx5ZgozNAR=Vb9}a z4p9{c^Dek3L<>gfTei%$phyuo#ZR+wyEM^4&L1e{K$24KJd2`YyZj+y@799^cUQyG zASaI3or2y1@)M!^u3q3q6r+>N;@HU(6CQ_CLk}XsBLLl0eJ*sN__=ZPoGP!YO~kIB=W zvcD0pe0_S*P*m^W4K=mHPhv}O>rjcArEo|_i-mDL_)|(FE9zQ5BT(9`E2X(jcc$qm z2vqOl#+}{kJUPm|v6K`$=jpxqLt+Gv+`jJvxxy+qW0ff>iMFvPziw7X5_-0*wwCYc zl~ee_v`VA@I?N7~XxoYFV7?J`9%H#~h}0M|TA+7mbZf@mf3IA9T%k5qXy$EhD--5h z$-fXu!FG?`klwu~U*a3uxPzM=Up}^hRbDW&dTudkynXjkGw($}d^V_^QcEJ8K_Jb= z?46&tL^D%-Zqo4H9P1Fl%d0f2;eQWylFrje6?grNI+Rh|3>GHd2wU0&xujz0XD;cf zLo>oY2~q&M!9)GO;HLifkBkJdFGEg#qe+EkIAzA5jTe-oUN}XgY6?ynChbkgHk{(= zv6$twf^P}k0wG~ZFAiYM(o`mipAH&rDC0r|-MFS_4M1qT#I;!ZZV420$Xa(0H@WdCxf(Bd^&gsfPNv z(PUMiZB+Xr!)pj5!Nb4U%f*RCM_=z&6tR64`yu|bgsRVrdzwAQlIf3@e7{_LM7{O Date: Wed, 18 Jan 2023 12:48:46 +0100 Subject: [PATCH 06/10] Added FormattingObjectPool to reduce temp allocs during text layout --- .../Media/TextFormatting/BidiReorderer.cs | 5 + .../TextFormatting/FormattingObjectPool.cs | 135 ++++++++++++++++ .../TextFormatting/InterWordJustification.cs | 3 +- .../Media/TextFormatting/TextCharacters.cs | 4 +- .../TextFormatting/TextEllipsisHelper.cs | 17 +- .../Media/TextFormatting/TextFormatterImpl.cs | 149 +++++++++++------- .../Media/TextFormatting/TextLayout.cs | 25 ++- .../TextLeadingPrefixCharacterEllipsis.cs | 90 ++++++----- .../Media/TextFormatting/TextLineImpl.cs | 26 ++- src/Avalonia.Base/Utilities/ArrayBuilder.cs | 36 ++++- src/Avalonia.Base/Utilities/ArraySlice.cs | 1 - .../Avalonia.UnitTests/MockTextShaperImpl.cs | 5 +- 12 files changed, 359 insertions(+), 137 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs index 3fcb7bf420..2c6db4b753 100644 --- a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -10,9 +10,14 @@ namespace Avalonia.Media.TextFormatting /// To avoid allocations, this class is designed to be reused. internal sealed class BidiReorderer { + [ThreadStatic] private static BidiReorderer? t_instance; + private ArrayBuilder _runs; private ArrayBuilder _ranges; + public static BidiReorderer Instance + => t_instance ??= new(); + public void BidiReorder(Span textRuns, FlowDirection flowDirection) { Debug.Assert(_runs.Length == 0); diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs new file mode 100644 index 0000000000..0468d8f413 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Contains various list pools that are commonly used during text layout. + /// + /// 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. + /// + /// + /// + internal sealed class FormattingObjectPool + { + [ThreadStatic] private static FormattingObjectPool? t_instance; + + /// + /// Gets an instance of this class for the current thread. + /// + /// + /// 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. + /// + public static FormattingObjectPool Instance + => t_instance ??= new(); + + public ListPool TextRunLists { get; } = new(); + + public ListPool UnshapedTextRunLists { get; } = new(); + + public ListPool TextLines { get; } = new(); + + [Conditional("DEBUG")] + public void VerifyAllReturned() + { + TextRunLists.VerifyAllReturned(); + UnshapedTextRunLists.VerifyAllReturned(); + TextLines.VerifyAllReturned(); + } + + internal sealed class ListPool + { + // 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[] _lists = new RentedList[MaxSize]; + private int _size; + private int _pendingReturnCount; + + /// + /// Rents a list. + /// See for the intended usages. + /// + /// A rented list instance that must be returned to the pool. + /// + public RentedList Rent() + { + var list = _size > 0 ? _lists[--_size] : new RentedList(); + + Debug.Assert(list.Count == 0, "A RentedList has been used after being returned!"); + + ++_pendingReturnCount; + return list; + } + + /// + /// Returns a rented list to the pool. + /// + /// + /// On input, the list to return. + /// On output, the reference is set to null to avoid misuse. + /// + public void Return(ref RentedList? 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!"); + } + } + } + + /// + /// Represents a list that has been rented through . + /// + /// 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 when done. + /// + /// + /// NEVER use an instance of this type after it's been returned to the pool. + /// AVOID storing an instance of this type into a field or property. + /// AVOID casting an instance of this type to another type. + /// + /// 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. + /// + /// + /// If you call a method returning an instance of this type, + /// you're now responsible for returning it to the pool. + /// + /// + /// + /// The type of elements in the list. + internal sealed class RentedList : List + { + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 6bfcfc06f8..7afb758038 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/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) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 6454f9bfa3..c1f3816e54 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/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 /// /// The shapeable text characters. internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties, List results) + ref TextRunProperties? previousProperties, RentedList results) { var properties = Properties; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 97f8b2483b..e6743f5533 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/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; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 8ffe3e5da2..c25d530472 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/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 textRuns; + IReadOnlyList? textRuns; + var objectPool = FormattingObjectPool.Instance; - var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, + var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); + RentedList? 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 /// /// The text run's. /// The length to split at. + /// A pool used to get reusable formatting objects. /// The split text runs. - internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length) + internal static SplitResult> SplitTextRuns(IReadOnlyList 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(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(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>(first, second); + return new SplitResult>(first, second); } else { secondCount++; - var second = new List(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>(first, second); + return new SplitResult>(first, second); } } - return new SplitResult>(textRuns, null); + for (var i = 0; i < textRuns.Count; i++) + { + first.Add(textRuns[i]); + } + + return new SplitResult>(first, null); } /// @@ -157,14 +177,16 @@ namespace Avalonia.Media.TextFormatting /// The text runs to shape. /// The default paragraph properties. /// The resolved flow direction. + /// A pool used to get reusable formatting objects. /// /// A list of shaped text characters. /// - private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, + private static RentedList ShapeTextRuns(IReadOnlyList textRuns, + TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; - var shapedRuns = new List(); + 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 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(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(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 textRuns, ReadOnlyMemory text, - TextShaperOptions options, List results) + TextShaperOptions options, RentedList results) { var shapedBuffer = TextShaper.Current.ShapeText(text, options); @@ -349,8 +376,8 @@ namespace Avalonia.Media.TextFormatting /// The bidi levels. /// A list that will be filled with the processed runs. /// - private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, - List processedRuns) + private static void CoalesceLevels(IReadOnlyList textCharacters, ReadOnlySpan levels, + RentedList processedRuns) { if (levels.Length == 0) { @@ -437,19 +464,20 @@ namespace Avalonia.Media.TextFormatting /// /// The text source. /// The first text source index. + /// A pool used to get reusable formatting objects. /// On return, the end of line, if any. /// On return, the processed text source length. /// /// The formatted text runs. /// - private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, - out TextEndOfLine? endOfLine, out int textSourceLength) + private static RentedList FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, + FormattingObjectPool objectPool, out TextEndOfLine? endOfLine, out int textSourceLength) { textSourceLength = 0; endOfLine = null; - var textRuns = new List(); + 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. /// /// The empty text line. - 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; } /// @@ -630,14 +661,15 @@ namespace Avalonia.Media.TextFormatting /// The text paragraph properties. /// /// The current line break if the line was explicitly broken. + /// A pool used to get reusable formatting objects. /// The wrapped text line. private static TextLineImpl PerformTextWrapping(IReadOnlyList 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 diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 55b6f14267..7a74dc89ae 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/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(); + 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; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 672a15b398..0d777ad043 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/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(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? preSplitRuns; - IReadOnlyList? postSplitRuns; + RentedList? rentedPreSplitRuns = null; + RentedList? rentedPostSplitRuns = null; + TextRun[]? results; - if (_prefixLength > 0) + try { - (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns( - textRunArray, Math.Min(_prefixLength, measuredLength)); + IReadOnlyList? 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 }; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7fa9155b02..245104f8fe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/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 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(TextRuns.Count); + var result = new List(_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(TextRuns.Count); + var result = new List(_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); } /// diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index 3a22fc7b9c..bbbcc39ecc 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -12,7 +12,6 @@ namespace Avalonia.Utilities /// /// The type of item contained in the array. internal struct ArrayBuilder - where T : struct { private const int DefaultCapacity = 4; private const int MaxCoreClrArrayLength = 0x7FeFFFFF; @@ -48,6 +47,12 @@ namespace Avalonia.Utilities } } + /// + /// Gets the current capacity of the array. + /// + public int Capacity + => _data?.Length ?? 0; + /// /// Returns a reference to specified element of the array. /// @@ -131,8 +136,28 @@ namespace Avalonia.Utilities /// public void Clear() { - // No need to actually clear since we're not allowing reference types. +#if NET6_0_OR_GREATER + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + 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 /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); + + /// + /// Returns the current state of the array as a span. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AsSpan() => _data.AsSpan(0, _size); } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index 3cffef72c5..17e75f2f95 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -17,7 +17,6 @@ namespace Avalonia.Utilities /// /// The type of item contained in the slice. internal readonly struct ArraySlice : IReadOnlyList - where T : struct { /// /// Gets an empty diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 3218139251..b5f4777192 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/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; } From fd0720fc5661932a9f4e9246ebcbd56fa0a513bf Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 00:11:28 +0100 Subject: [PATCH 07/10] Updated text layout benchmark with trimming/wrapping --- .../Avalonia.Benchmarks/Text/HugeTextLayout.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index bf0253e9ab..8b23855fde 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -9,6 +9,8 @@ using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Text; [MemoryDiagnoser] +[MinIterationTime(150)] +[MaxWarmupCount(15)] public class HugeTextLayout : IDisposable { private readonly IDisposable _app; @@ -44,7 +46,13 @@ Admitting that the possibility of achieving the results of the constructive crit Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor. It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory. In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; - + + [Params(false, true)] + public bool UseWrapping { get; set; } + + [Params(false, true)] + public bool UseTrimming { get; set; } + [Benchmark] public TextLayout BuildTextLayout() => MakeLayout(Text); @@ -91,9 +99,12 @@ In respect that the structure of the sufficient amount poses problems and challe } } - private static TextLayout MakeLayout(string str) + private TextLayout MakeLayout(string str) { - var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120); + var wrapping = UseWrapping ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; + var trimming = UseTrimming ? TextTrimming.CharacterEllipsis : TextTrimming.None; + var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120, + textTrimming: trimming, textWrapping: wrapping); layout.Dispose(); return layout; } From 7fcfc82be0d6375e61b7b92c7307afa04633ecc3 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 00:39:50 +0100 Subject: [PATCH 08/10] Fixed TextShaperImpl when the text is backed by an array --- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 2 ++ src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs | 2 ++ tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index e0f95bac60..def2482af3 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.Skia if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index fffa5ce490..ac441108e3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.Direct2D1.Media if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 566cb0f1ac..baf5ffb07c 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.UnitTests if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } From 89a78f557b13f61b06c6eb56915b7f1a077808e7 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 18:39:25 +0100 Subject: [PATCH 09/10] Don't keep the text layout buffers around if they're too large --- .../Media/TextFormatting/BidiReorderer.cs | 4 +- .../TextFormatting/FormattingBufferHelper.cs | 62 +++++++++++++++++++ .../TextFormatting/FormattingObjectPool.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 3 + .../TextFormatting/Unicode/BiDiAlgorithm.cs | 53 ++++++++++++++-- .../Media/TextFormatting/Unicode/BiDiData.cs | 22 +++++-- src/Avalonia.Base/Utilities/BidiDictionary.cs | 29 ++++----- .../Text/HugeTextLayout.cs | 8 +-- 8 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs index 2c6db4b753..4db55fae6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -117,8 +117,8 @@ namespace Avalonia.Media.TextFormatting } finally { - _runs.Clear(); - _ranges.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs new file mode 100644 index 0000000000..0341842cb6 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + internal static class FormattingBufferHelper + { + // 1MB, arbitrary, that's 512K characters or 128K object references on x64 + private const long MaxKeptBufferSizeInBytes = 1024 * 1024; + + public static void ClearThenResetIfTooLarge(ref ArrayBuilder arrayBuilder) + { + arrayBuilder.Clear(); + + if (IsBufferTooLarge(arrayBuilder.Capacity)) + { + arrayBuilder = default; + } + } + + public static void ClearThenResetIfTooLarge(List list) + { + list.Clear(); + + if (IsBufferTooLarge(list.Capacity)) + { + list.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(Stack stack) + { + stack.Clear(); + + if (IsBufferTooLarge(stack.Count)) + { + stack.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(ref Dictionary dictionary) + where TKey : notnull + { + dictionary.Clear(); + + // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here + if (IsBufferTooLarge>(dictionary.Count)) + { +#if NET6_0_OR_GREATER + dictionary.TrimExcess(); +#else + dictionary = new Dictionary(); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsBufferTooLarge(int length) + => (long)Unsafe.SizeOf() * length > MaxKeptBufferSizeInBytes; + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs index 0468d8f413..cb8168e693 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting } --_pendingReturnCount; - rentedList.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList); if (_size < MaxSize) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index c25d530472..bf9f6f77f8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -225,6 +225,9 @@ namespace Avalonia.Media.TextFormatting CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); + bidiData.Reset(); + bidiAlgorithm.Reset(); + var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); for (var index = 0; index < processedRuns.Count; index++) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index e960a510a9..3a81784152 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -28,6 +28,11 @@ namespace Avalonia.Media.TextFormatting.Unicode /// internal sealed class BidiAlgorithm { + /// + /// Whether the state is clean and can be reused without a reset. + /// + private bool _hasCleanState = true; + /// /// The original BiDiClass classes as provided by the caller /// @@ -226,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs.Clear(); - _workingClassesBuffer.Clear(); - _levelRuns.Clear(); - _resolvedLevelsBuffer.Clear(); + Reset(); if (types.IsEmpty) { return; } + _hasCleanState = false; + // Setup original types and working types _originalClasses = types; _workingClasses = _workingClassesBuffer.Add(types); @@ -1639,6 +1643,47 @@ namespace Avalonia.Media.TextFormatting.Unicode } } + /// + /// Resets the bidi algorithm to a clean state. + /// + public void Reset() + { + if (_hasCleanState) + { + return; + } + + _originalClasses = default; + _pairedBracketTypes = default; + _pairedBracketValues = default; + _hasBrackets = default; + _hasEmbeddings = default; + _hasIsolates = default; + _isolatePairs.ClearThenResetIfTooLarge(); + _workingClasses = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _workingClassesBuffer); + _resolvedLevels = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _resolvedLevelsBuffer); + _paragraphEmbeddingLevel = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_statusStack); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _x9Map); + FormattingBufferHelper.ClearThenResetIfTooLarge(_levelRuns); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _isolatedRunMapping); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingIsolateOpenings); + _runLevel = default; + _runDirection = default; + _runLength = default; + _runResolvedClasses = default; + _runOriginalClasses = default; + _runLevels = default; + _runBiDiPairedBracketTypes = default; + _runPairedBracketValues = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingOpeningBrackets); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pairedBrackets); + + _hasCleanState = true; + } + /// /// Hold the start and end index of a pair of brackets /// diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 226e5ad6bd..8bd2171a41 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -14,6 +14,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// To avoid allocations, this class is designed to be reused. internal sealed class BidiData { + private bool _hasCleanState = true; private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; private ArrayBuilder _pairedBracketValues; @@ -62,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The text to process. public void Append(ReadOnlySpan text) { + _hasCleanState = false; + _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); _pairedBracketValues.Add(text.Length); @@ -183,12 +186,17 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void Reset() { - _classes.Clear(); - _pairedBracketTypes.Clear(); - _pairedBracketValues.Clear(); - _savedClasses.Clear(); - _savedPairedBracketTypes.Clear(); - _tempLevelBuffer.Clear(); + if (_hasCleanState) + { + return; + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _classes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketValues); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedClasses); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedPairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _tempLevelBuffer); ParagraphEmbeddingLevel = 0; HasBrackets = false; @@ -199,6 +207,8 @@ namespace Avalonia.Media.TextFormatting.Unicode Classes = default; PairedBracketTypes = default; PairedBracketValues = default; + + _hasCleanState = true; } } } diff --git a/src/Avalonia.Base/Utilities/BidiDictionary.cs b/src/Avalonia.Base/Utilities/BidiDictionary.cs index 654fbc9807..01af53ad89 100644 --- a/src/Avalonia.Base/Utilities/BidiDictionary.cs +++ b/src/Avalonia.Base/Utilities/BidiDictionary.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Media.TextFormatting; namespace Avalonia.Utilities { @@ -9,32 +11,27 @@ namespace Avalonia.Utilities /// Value type internal sealed class BidiDictionary where T1 : notnull where T2 : notnull { - public Dictionary Forward { get; } = new Dictionary(); + private Dictionary _forward = new(); + private Dictionary _reverse = new(); - public Dictionary Reverse { get; } = new Dictionary(); - - public void Clear() + public void ClearThenResetIfTooLarge() { - Forward.Clear(); - Reverse.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _forward); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _reverse); } public void Add(T1 key, T2 value) { - Forward.Add(key, value); - Reverse.Add(value, key); + _forward.Add(key, value); + _reverse.Add(value, key); } -#pragma warning disable CS8601 - public bool TryGetValue(T1 key, out T2 value) => Forward.TryGetValue(key, out value); -#pragma warning restore CS8601 + public bool TryGetValue(T1 key, [MaybeNullWhen(false)] out T2 value) => _forward.TryGetValue(key, out value); -#pragma warning disable CS8601 - public bool TryGetKey(T2 value, out T1 key) => Reverse.TryGetValue(value, out key); -#pragma warning restore CS8601 + public bool TryGetKey(T2 value, [MaybeNullWhen(false)] out T1 key) => _reverse.TryGetValue(value, out key); - public bool ContainsKey(T1 key) => Forward.ContainsKey(key); + public bool ContainsKey(T1 key) => _forward.ContainsKey(key); - public bool ContainsValue(T2 value) => Reverse.ContainsKey(value); + public bool ContainsValue(T2 value) => _reverse.ContainsKey(value); } } diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 8b23855fde..c96edbef5c 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -48,10 +48,10 @@ It may reveal how the matters of peculiar interest slowly the goals and objectiv In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; [Params(false, true)] - public bool UseWrapping { get; set; } + public bool Wrap { get; set; } [Params(false, true)] - public bool UseTrimming { get; set; } + public bool Trim { get; set; } [Benchmark] public TextLayout BuildTextLayout() => MakeLayout(Text); @@ -101,8 +101,8 @@ In respect that the structure of the sufficient amount poses problems and challe private TextLayout MakeLayout(string str) { - var wrapping = UseWrapping ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; - var trimming = UseTrimming ? TextTrimming.CharacterEllipsis : TextTrimming.None; + var wrapping = Wrap ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; + var trimming = Trim ? TextTrimming.CharacterEllipsis : TextTrimming.None; var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120, textTrimming: trimming, textWrapping: wrapping); layout.Dispose(); From 7ae394d247906b93b1347616d55bee4ac5df5890 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 20 Jan 2023 13:00:39 +0100 Subject: [PATCH 10/10] Fixed text layout buffer reset for Stack and Dictionary (+ tests) --- .../TextFormatting/FormattingBufferHelper.cs | 34 +++- .../Media/TextFormatting/Unicode/BiDiData.cs | 4 + .../FormattingBufferHelperTests.cs | 151 ++++++++++++++++++ 3 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs index 0341842cb6..c27903cd55 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Numerics; using System.Runtime.CompilerServices; using Avalonia.Utilities; @@ -13,7 +14,7 @@ namespace Avalonia.Media.TextFormatting { arrayBuilder.Clear(); - if (IsBufferTooLarge(arrayBuilder.Capacity)) + if (IsBufferTooLarge((uint) arrayBuilder.Capacity)) { arrayBuilder = default; } @@ -23,7 +24,7 @@ namespace Avalonia.Media.TextFormatting { list.Clear(); - if (IsBufferTooLarge(list.Capacity)) + if (IsBufferTooLarge((uint) list.Capacity)) { list.TrimExcess(); } @@ -31,9 +32,11 @@ namespace Avalonia.Media.TextFormatting public static void ClearThenResetIfTooLarge(Stack stack) { + var approximateCapacity = RoundUpToPowerOf2((uint)stack.Count); + stack.Clear(); - if (IsBufferTooLarge(stack.Count)) + if (IsBufferTooLarge(approximateCapacity)) { stack.TrimExcess(); } @@ -42,10 +45,12 @@ namespace Avalonia.Media.TextFormatting public static void ClearThenResetIfTooLarge(ref Dictionary dictionary) where TKey : notnull { + var approximateCapacity = RoundUpToPowerOf2((uint)dictionary.Count); + dictionary.Clear(); // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here - if (IsBufferTooLarge>(dictionary.Count)) + if (IsBufferTooLarge>(approximateCapacity)) { #if NET6_0_OR_GREATER dictionary.TrimExcess(); @@ -56,7 +61,24 @@ namespace Avalonia.Media.TextFormatting } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsBufferTooLarge(int length) - => (long)Unsafe.SizeOf() * length > MaxKeptBufferSizeInBytes; + private static bool IsBufferTooLarge(uint capacity) + => (long) (uint) Unsafe.SizeOf() * capacity > MaxKeptBufferSizeInBytes; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RoundUpToPowerOf2(uint value) + { +#if NET6_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(value); +#else + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; +#endif + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 8bd2171a41..5cc222b813 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -150,6 +150,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void SaveTypes() { + _hasCleanState = false; + // Capture the types data _savedClasses.Clear(); _savedClasses.Add(_classes.AsSlice()); @@ -162,6 +164,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void RestoreTypes() { + _hasCleanState = false; + _classes.Clear(); _classes.Add(_savedClasses.AsSlice()); _pairedBracketTypes.Clear(); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs new file mode 100644 index 0000000000..192f34eea7 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.TextFormatting +{ + public class FormattingBufferHelperTests + { + public static TheoryData SmallSizes => new() { 1, 500, 10_000, 125_000 }; + public static TheoryData LargeSizes => new() { 500_000, 1_000_000 }; + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_List(int itemCount) + { + var capacity = FillAndClearList(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_List(int itemCount) + { + var capacity = FillAndClearList(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearList(int itemCount) + { + var list = new List(); + + for (var i = 0; i < itemCount; ++i) + { + list.Add(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(list); + + return list.Capacity; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_ArrayBuilder(int itemCount) + { + var capacity = FillAndClearArrayBuilder(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_ArrayBuilder(int itemCount) + { + var capacity = FillAndClearArrayBuilder(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearArrayBuilder(int itemCount) + { + var arrayBuilder = new ArrayBuilder(); + + for (var i = 0; i < itemCount; ++i) + { + arrayBuilder.AddItem(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref arrayBuilder); + + return arrayBuilder.Capacity; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_Stack(int itemCount) + { + var capacity = FillAndClearStack(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_Stack(int itemCount) + { + var capacity = FillAndClearStack(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearStack(int itemCount) + { + var stack = new Stack(); + + for (var i = 0; i < itemCount; ++i) + { + stack.Push(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(stack); + + var array = (Array) stack.GetType() + .GetField("_array", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(stack)!; + + return array.Length; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_Dictionary(int itemCount) + { + var capacity = FillAndClearDictionary(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_Dictionary(int itemCount) + { + var capacity = FillAndClearDictionary(itemCount); + + Assert.True(capacity <= 3); // dictionary trims to the nearest prime starting with 3 + } + + private static int FillAndClearDictionary(int itemCount) + { + var dictionary = new Dictionary(); + + for (var i = 0; i < itemCount; ++i) + { + dictionary.Add(i, i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref dictionary); + + var array = (Array) dictionary.GetType() + .GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(dictionary)!; + + return array.Length; + } + } +}