From 10a3b79d128e2053161833de95e8029727614962 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 20 Jan 2023 12:18:38 +0100 Subject: [PATCH 1/7] Perf: various misc text layout optimizations --- src/Avalonia.Base/Avalonia.Base.csproj | 1 + .../Media/Fonts/FamilyNameCollection.cs | 74 ++---- .../Media/TextFormatting/TextCharacters.cs | 9 +- .../Media/TextFormatting/TextFormatterImpl.cs | 11 +- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 41 ++-- .../Media/TextFormatting/Unicode/Codepoint.cs | 117 +++++----- .../Media/TextFormatting/Unicode/Grapheme.cs | 4 + .../Unicode/GraphemeEnumerator.cs | 217 ++++++++---------- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 6 +- .../Media/TextShaperImpl.cs | 6 +- .../Text/HugeTextLayout.cs | 12 +- .../HarfBuzzTextShaperImpl.cs | 6 +- .../Avalonia.UnitTests/MockTextShaperImpl.cs | 3 +- 13 files changed, 241 insertions(+), 266 deletions(-) diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 4a67191132..35a453ce59 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs index eb42f6443b..f2350f5aea 100644 --- a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs @@ -1,13 +1,14 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Text; using Avalonia.Utilities; namespace Avalonia.Media.Fonts { public sealed class FamilyNameCollection : IReadOnlyList { + private readonly string[] _names; + /// /// Initializes a new instance of the class. /// @@ -20,13 +21,20 @@ namespace Avalonia.Media.Fonts throw new ArgumentNullException(nameof(familyNames)); } - Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim()); + _names = SplitNames(familyNames); - PrimaryFamilyName = Names[0]; + PrimaryFamilyName = _names[0]; - HasFallbacks = Names.Count > 1; + HasFallbacks = _names.Length > 1; } + private static string[] SplitNames(string names) +#if NET6_0_OR_GREATER + => names.Split(',', StringSplitOptions.TrimEntries); +#else + => Array.ConvertAll(names.Split(','), p => p.Trim()); +#endif + /// /// Gets the primary family name. /// @@ -43,14 +51,6 @@ namespace Avalonia.Media.Fonts /// public bool HasFallbacks { get; } - /// - /// Gets the internal collection of names. - /// - /// - /// The names. - /// - internal IReadOnlyList Names { get; } - /// /// Returns an enumerator for the name collection. /// @@ -76,23 +76,7 @@ namespace Avalonia.Media.Fonts /// A that represents this instance. /// public override string ToString() - { - var builder = StringBuilderCache.Acquire(); - - for (var index = 0; index < Names.Count; index++) - { - builder.Append(Names[index]); - - if (index == Names.Count - 1) - { - break; - } - - builder.Append(", "); - } - - return StringBuilderCache.GetStringAndRelease(builder); - } + => String.Join(", ", _names); /// /// Returns a hash code for this instance. @@ -102,7 +86,7 @@ namespace Avalonia.Media.Fonts /// public override int GetHashCode() { - if (Count == 0) + if (_names.Length == 0) { return 0; } @@ -111,9 +95,9 @@ namespace Avalonia.Media.Fonts { int hash = 17; - for (var i = 0; i < Names.Count; i++) + for (var i = 0; i < _names.Length; i++) { - string name = Names[i]; + string name = _names[i]; hash = hash * 23 + name.GetHashCode(); } @@ -145,30 +129,10 @@ namespace Avalonia.Media.Fonts /// true if the specified is equal to this instance; otherwise, false. /// public override bool Equals(object? obj) - { - if (!(obj is FamilyNameCollection other)) - { - return false; - } - - if (other.Count != Count) - { - return false; - } - - for (int i = 0; i < Count; i++) - { - if (Names[i] != other.Names[i]) - { - return false; - } - } - - return true; - } + => obj is FamilyNameCollection other && _names.AsSpan().SequenceEqual(other._names); - public int Count => Names.Count; + public int Count => _names.Length; - public string this[int index] => Names[index]; + public string this[int index] => _names[index]; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index c1f3816e54..9e76418ac9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -47,13 +47,13 @@ namespace Avalonia.Media.TextFormatting /// /// The shapeable text characters. internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties, RentedList results) + FontManager fontManager, ref TextRunProperties? previousProperties, RentedList results) { var properties = Properties; while (!text.IsEmpty) { - var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); + var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, fontManager, ref previousProperties); results.Add(shapeableRun); @@ -72,7 +72,8 @@ namespace Avalonia.Media.TextFormatting /// /// A list of shapeable text runs. private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, - TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) + TextRunProperties defaultProperties, sbyte biDiLevel, FontManager fontManager, + ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; var currentTypeface = defaultTypeface; @@ -121,7 +122,7 @@ namespace Avalonia.Media.TextFormatting //ToDo: Fix FontFamily fallback var matchFound = - FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, + fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index bf9f6f77f8..b0242be87e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -393,6 +393,7 @@ namespace Avalonia.Media.TextFormatting TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; ReadOnlyMemory runText = default; + var fontManager = FontManager.Current; for (var i = 0; i < textCharacters.Count; i++) { @@ -427,8 +428,8 @@ namespace Avalonia.Media.TextFormatting if (j == runTextSpan.Length) { - currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, - processedRuns); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager, + ref previousProperties, processedRuns); runLevel = levels[levelIndex]; @@ -441,8 +442,8 @@ namespace Avalonia.Media.TextFormatting } // End of this run - currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, - processedRuns); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager, + ref previousProperties, processedRuns); runText = runText.Slice(j); runTextSpan = runText.Span; @@ -459,7 +460,7 @@ namespace Avalonia.Media.TextFormatting return; } - currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns); + currentRun.GetShapeableCharacters(runText, runLevel, fontManager, ref previousProperties, processedRuns); } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 3a81784152..36e9e6eb79 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -343,6 +343,17 @@ namespace Avalonia.Media.TextFormatting.Unicode return 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsIsolateStart(BidiClass type) + { + const uint mask = + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate); + + return ((1U << (int)type) & mask) != 0U; + } + /// /// Build a list of matching isolates for a directionality slice /// Implements BD9 @@ -701,28 +712,19 @@ namespace Avalonia.Media.TextFormatting.Unicode var lastType = _workingClasses[lastCharIndex]; int nextLevel; - switch (lastType) + if (IsIsolateStart(lastType)) { - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: + nextLevel = _paragraphEmbeddingLevel; + } + else + { + i = lastCharIndex + 1; + while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i])) { - nextLevel = _paragraphEmbeddingLevel; - - break; + i++; } - default: - { - i = lastCharIndex + 1; - while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i])) - { - i++; - } - nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i]; - - break; - } + nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i]; } var eos = DirectionFromLevel(Math.Max(nextLevel, level)); @@ -831,8 +833,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // PDI and concatenate that run to this one 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)) + if (IsIsolateStart(lastType) && _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) { // Find the continuing run index runIndex = FindRunForIndex(nextRunIndex); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 22f7b50fd4..6433a37b22 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Avalonia.Media.TextFormatting.Unicode @@ -11,13 +10,19 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// The replacement codepoint that is used for non supported values. /// - public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD'); - - public Codepoint(uint value) + public static Codepoint ReplacementCodepoint { - _value = value; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new('\uFFFD'); } + /// + /// Creates a new instance of with the specified value. + /// + /// The codepoint value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Codepoint(uint value) => _value = value; + /// /// Get the codepoint's value. /// @@ -87,19 +92,17 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public bool IsWhiteSpace { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - switch (GeneralCategory) - { - case GeneralCategory.Control: - case GeneralCategory.NonspacingMark: - case GeneralCategory.Format: - case GeneralCategory.SpaceSeparator: - case GeneralCategory.SpacingMark: - return true; - } - - return false; + const ulong whiteSpaceMask = + (1UL << (int)GeneralCategory.Control) | + (1UL << (int)GeneralCategory.NonspacingMark) | + (1UL << (int)GeneralCategory.Format) | + (1UL << (int)GeneralCategory.SpaceSeparator) | + (1UL << (int)GeneralCategory.SpacingMark); + + return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0L; } } @@ -166,56 +169,62 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The index to read at. /// The count of character that were read. /// +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif public static Codepoint ReadAt(ReadOnlySpan text, int index, out int count) { + // Perf note: this method is performance critical for text layout, modify with care! + count = 1; - if (index >= text.Length) + // Perf note: uint check allows the JIT to ellide the next bound check + if ((uint)index >= (uint)text.Length) { return ReplacementCodepoint; } - var code = text[index]; - - ushort hi, low; + uint code = text[index]; - //# High surrogate - if (0xD800 <= code && code <= 0xDBFF) + //# Surrogate + if (IsInRangeInclusive(code, 0xD800U, 0xDFFFU)) { - hi = code; - - if (index + 1 == text.Length) - { - return ReplacementCodepoint; - } - - low = text[index + 1]; - - if (0xDC00 <= low && low <= 0xDFFF) - { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); - } - - return ReplacementCodepoint; - } + uint hi, low; - //# Low surrogate - if (0xDC00 <= code && code <= 0xDFFF) - { - if (index == 0) + //# High surrogate + if (code <= 0xDBFF) { - return ReplacementCodepoint; + if ((uint)(index + 1) < (uint)text.Length) + { + hi = code; + low = text[index + 1]; + + if (IsInRangeInclusive(low, 0xDC00U, 0xDFFFU)) + { + count = 2; + // Perf note: the code is written as below to become just two instructions: shl, lea. + // See https://github.com/dotnet/runtime/blob/7ec3634ee579d89b6024f72b595bfd7118093fc5/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs#L38 + return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16))); + } + } } - hi = text[index - 1]; - - low = code; - - if (0xD800 <= hi && hi <= 0xDBFF) + //# Low surrogate + else { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); + if (index > 0) + { + low = code; + hi = text[index - 1]; + + if (IsInRangeInclusive(hi, 0xD800U, 0xDBFFU)) + { + count = 2; + return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16))); + } + } } return ReplacementCodepoint; @@ -224,12 +233,16 @@ namespace Avalonia.Media.TextFormatting.Unicode return new Codepoint(code); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) + => value - lowerBound <= upperBound - lowerBound; + /// /// Returns if is between /// and , inclusive. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRangeInclusive(Codepoint cp, uint lowerBound, uint upperBound) - => (cp._value - lowerBound) <= (upperBound - lowerBound); + => IsInRangeInclusive(cp._value, lowerBound, upperBound); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index fa8e8ac976..5a4d891917 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -22,5 +22,9 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The text of the grapheme cluster /// public ReadOnlySpan Text { get; } + + /// + public override string ToString() + => Text.ToString(); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index 812bb99d99..a6a9453b8a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -4,57 +4,79 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Runtime.InteropServices; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct GraphemeEnumerator { - private ReadOnlySpan _text; + private readonly ReadOnlySpan _text; + private int _currentCodeUnitOffset; + private int _codeUnitLengthOfCurrentCodepoint; + private Codepoint _currentCodepoint; + + /// + /// Will be if invalid data or EOF reached. + /// Caller shouldn't need to special-case this since the normal rules will halt on this condition. + /// + private GraphemeBreakClass _currentType; public GraphemeEnumerator(ReadOnlySpan text) { _text = text; - Current = default; + _currentCodeUnitOffset = 0; + _codeUnitLengthOfCurrentCodepoint = 0; + _currentCodepoint = Codepoint.ReplacementCodepoint; + _currentType = GraphemeBreakClass.Other; } - /// - /// Gets the current . - /// - public Grapheme Current { get; private set; } - /// /// Moves to the next . /// /// - public bool MoveNext() + public bool MoveNext(out Grapheme grapheme) { - if (_text.IsEmpty) + var startOffset = _currentCodeUnitOffset; + + if ((uint)startOffset >= (uint)_text.Length) { + grapheme = default; return false; } // Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules. - var processor = new Processor(_text); - - processor.MoveNext(); + if (startOffset == 0) + { + ReadNextCodepoint(); + } - var firstCodepoint = processor.CurrentCodepoint; + var firstCodepoint = _currentCodepoint; // First, consume as many Prepend scalars as we can (rule GB9b). - while (processor.CurrentType == GraphemeBreakClass.Prepend) + if (_currentType == GraphemeBreakClass.Prepend) { - processor.MoveNext(); + do + { + ReadNextCodepoint(); + } while (_currentType == GraphemeBreakClass.Prepend); + + // There were only Prepend scalars in the text + if ((uint)_currentCodeUnitOffset >= (uint)_text.Length) + { + goto Return; + } } // Next, make sure we're not about to violate control character restrictions. // Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5). - if (processor.CurrentCodeUnitOffset > 0) + if (_currentCodeUnitOffset > startOffset) { - if (processor.CurrentType == GraphemeBreakClass.Control - || processor.CurrentType == GraphemeBreakClass.CR - || processor.CurrentType == GraphemeBreakClass.LF) + const uint controlCrLfMask = + (1U << (int)GraphemeBreakClass.Control) | + (1U << (int)GraphemeBreakClass.CR) | + (1U << (int)GraphemeBreakClass.LF); + + if (((1U << (int)_currentType) & controlCrLfMask) != 0U) { goto Return; } @@ -62,19 +84,19 @@ namespace Avalonia.Media.TextFormatting.Unicode // Now begin the main state machine. - var previousClusterBreakType = processor.CurrentType; + var previousClusterBreakType = _currentType; - processor.MoveNext(); + ReadNextCodepoint(); switch (previousClusterBreakType) { case GraphemeBreakClass.CR: - if (processor.CurrentType != GraphemeBreakClass.LF) + if (_currentType != GraphemeBreakClass.LF) { goto Return; // rules GB3 & GB4 (only can follow ) } - processor.MoveNext(); + ReadNextCodepoint(); goto case GraphemeBreakClass.LF; case GraphemeBreakClass.Control: @@ -82,53 +104,57 @@ namespace Avalonia.Media.TextFormatting.Unicode goto Return; // rule GB4 (no data after Control | LF) case GraphemeBreakClass.L: - if (processor.CurrentType == GraphemeBreakClass.L) + { + if (_currentType == GraphemeBreakClass.L) { - processor.MoveNext(); // rule GB6 (L x L) + ReadNextCodepoint(); // rule GB6 (L x L) goto case GraphemeBreakClass.L; } - else if (processor.CurrentType == GraphemeBreakClass.V) + else if (_currentType == GraphemeBreakClass.V) { - processor.MoveNext(); // rule GB6 (L x V) + ReadNextCodepoint(); // rule GB6 (L x V) goto case GraphemeBreakClass.V; } - else if (processor.CurrentType == GraphemeBreakClass.LV) + else if (_currentType == GraphemeBreakClass.LV) { - processor.MoveNext(); // rule GB6 (L x LV) + ReadNextCodepoint(); // rule GB6 (L x LV) goto case GraphemeBreakClass.LV; } - else if (processor.CurrentType == GraphemeBreakClass.LVT) + else if (_currentType == GraphemeBreakClass.LVT) { - processor.MoveNext(); // rule GB6 (L x LVT) + ReadNextCodepoint(); // rule GB6 (L x LVT) goto case GraphemeBreakClass.LVT; } else { break; } + } case GraphemeBreakClass.LV: case GraphemeBreakClass.V: - if (processor.CurrentType == GraphemeBreakClass.V) + { + if (_currentType == GraphemeBreakClass.V) { - processor.MoveNext(); // rule GB7 (LV | V x V) + ReadNextCodepoint(); // rule GB7 (LV | V x V) goto case GraphemeBreakClass.V; } - else if (processor.CurrentType == GraphemeBreakClass.T) + else if (_currentType == GraphemeBreakClass.T) { - processor.MoveNext(); // rule GB7 (LV | V x T) + ReadNextCodepoint(); // rule GB7 (LV | V x T) goto case GraphemeBreakClass.T; } else { break; } + } case GraphemeBreakClass.LVT: case GraphemeBreakClass.T: - if (processor.CurrentType == GraphemeBreakClass.T) + if (_currentType == GraphemeBreakClass.T) { - processor.MoveNext(); // rule GB8 (LVT | T x T) + ReadNextCodepoint(); // rule GB8 (LVT | T x T) goto case GraphemeBreakClass.T; } else @@ -139,123 +165,76 @@ namespace Avalonia.Media.TextFormatting.Unicode case GraphemeBreakClass.ExtendedPictographic: // Attempt processing extended pictographic (rules GB11, GB9). // First, drain any Extend scalars that might exist - while (processor.CurrentType == GraphemeBreakClass.Extend) + while (_currentType == GraphemeBreakClass.Extend) { - processor.MoveNext(); + ReadNextCodepoint(); } // Now see if there's a ZWJ + extended pictograph again. - if (processor.CurrentType != GraphemeBreakClass.ZWJ) + if (_currentType != GraphemeBreakClass.ZWJ) { break; } - processor.MoveNext(); - if (processor.CurrentType != GraphemeBreakClass.ExtendedPictographic) + ReadNextCodepoint(); + if (_currentType != GraphemeBreakClass.ExtendedPictographic) { break; } - processor.MoveNext(); + ReadNextCodepoint(); goto case GraphemeBreakClass.ExtendedPictographic; case GraphemeBreakClass.RegionalIndicator: // We've consumed a single RI scalar. Try to consume another (to make it a pair). - if (processor.CurrentType == GraphemeBreakClass.RegionalIndicator) + if (_currentType == GraphemeBreakClass.RegionalIndicator) { - processor.MoveNext(); + ReadNextCodepoint(); } // Standlone RI scalars (or a single pair of RI scalars) can only be followed by trailers. break; // nothing but trailers after the final RI - - default: - break; } // rules GB9, GB9a - while (processor.CurrentType == GraphemeBreakClass.Extend - || processor.CurrentType == GraphemeBreakClass.ZWJ - || processor.CurrentType == GraphemeBreakClass.SpacingMark) + while (_currentType is GraphemeBreakClass.Extend + or GraphemeBreakClass.ZWJ + or GraphemeBreakClass.SpacingMark) { - processor.MoveNext(); + ReadNextCodepoint(); } Return: - Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset)); - - _text = _text.Slice(processor.CurrentCodeUnitOffset); + var graphemeLength = _currentCodeUnitOffset - startOffset; + grapheme = new Grapheme(firstCodepoint, startOffset, graphemeLength); return true; // rules GB2, GB999 } - [StructLayout(LayoutKind.Auto)] - private ref struct Processor + private void ReadNextCodepoint() { - private readonly ReadOnlySpan _buffer; - private int _codeUnitLengthOfCurrentScalar; - - internal Processor(ReadOnlySpan buffer) - { - _buffer = buffer; - _codeUnitLengthOfCurrentScalar = 0; - CurrentCodepoint = Codepoint.ReplacementCodepoint; - CurrentType = GraphemeBreakClass.Other; - CurrentCodeUnitOffset = 0; - } - - public int CurrentCodeUnitOffset { get; private set; } - - /// - /// Will be if invalid data or EOF reached. - /// Caller shouldn't need to special-case this since the normal rules will halt on this condition. - /// - public GraphemeBreakClass CurrentType { get; private set; } - - /// - /// Get the currently processed . - /// - public Codepoint CurrentCodepoint { get; private set; } - - public void MoveNext() - { - // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on - // the decoder's default behavior of interpreting these ill-formed subsequences as - // equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property - // of Other (XX), which matches the modifications made to UAX#29, Rev. 35. - // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications - // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file - // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt - // has the line "D800..DFFF ; Control # Cs [2048] ..", - // but starting with Unicode 12.0 that line has been removed. - // - // If a later version of the Unicode Standard further modifies this guidance we should reflect - // that here. - - if (CurrentCodeUnitOffset == _buffer.Length) - { - CurrentCodepoint = Codepoint.ReplacementCodepoint; - } - else - { - CurrentCodeUnitOffset += _codeUnitLengthOfCurrentScalar; - - if (CurrentCodeUnitOffset < _buffer.Length) - { - CurrentCodepoint = Codepoint.ReadAt(_buffer, CurrentCodeUnitOffset, - out _codeUnitLengthOfCurrentScalar); - } - else - { - CurrentCodepoint = Codepoint.ReplacementCodepoint; - } - } - - CurrentType = CurrentCodepoint.GraphemeBreakClass; - } + // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on + // the decoder's default behavior of interpreting these ill-formed subsequences as + // equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property + // of Other (XX), which matches the modifications made to UAX#29, Rev. 35. + // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications + // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file + // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt + // has the line "D800..DFFF ; Control # Cs [2048] ..", + // but starting with Unicode 12.0 that line has been removed. + // + // If a later version of the Unicode Standard further modifies this guidance we should reflect + // that here. + + _currentCodeUnitOffset += _codeUnitLengthOfCurrentCodepoint; + + _currentCodepoint = Codepoint.ReadAt(_text, _currentCodeUnitOffset, + out _codeUnitLengthOfCurrentCodepoint); + + _currentType = _currentCodepoint.GraphemeBreakClass; } } } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index def2482af3..e1a6b93692 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -52,6 +52,8 @@ namespace Avalonia.Skia var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var targetInfos = shapedBuffer.GlyphInfos; + var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -77,9 +79,7 @@ namespace Avalonia.Skia 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); - - shapedBuffer[i] = targetInfo; + targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index ac441108e3..ff0fff6b14 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -52,6 +52,8 @@ namespace Avalonia.Direct2D1.Media var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var targetInfos = shapedBuffer.GlyphInfos; + var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -77,9 +79,7 @@ namespace Avalonia.Direct2D1.Media 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); - - shapedBuffer[i] = targetInfo; + targetInfos[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index c96edbef5c..0adabc75f1 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -77,7 +77,17 @@ In respect that the structure of the sufficient amount poses problems and challe public TextLayout BuildEmojisTextLayout() => MakeLayout(Emojis); [Benchmark] - public TextLayout[] BuildManySmallTexts() => _manySmallStrings.Select(MakeLayout).ToArray(); + public TextLayout[] BuildManySmallTexts() + { + var results = new TextLayout[_manySmallStrings.Length]; + + for (var i = 0; i < _manySmallStrings.Length; i++) + { + results[i] = MakeLayout(_manySmallStrings[i]); + } + + return results; + } [Benchmark] public void VirtualizeTextBlocks() diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index baf5ffb07c..0448ecd41f 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -52,6 +52,8 @@ namespace Avalonia.UnitTests var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var targetInfos = shapedBuffer.GlyphInfos; + var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -77,9 +79,7 @@ namespace Avalonia.UnitTests 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); - - shapedBuffer[i] = targetInfo; + targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index b5f4777192..b810caabd9 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -13,6 +13,7 @@ namespace Avalonia.UnitTests var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var targetInfos = shapedBuffer.GlyphInfos; var textSpan = text.Span; var textStartIndex = TextTestHelper.GetStartCharIndex(text); @@ -26,7 +27,7 @@ namespace Avalonia.UnitTests for (var j = 0; j < count; ++j) { - shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); + targetInfos[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); } i += count; From 2f429062a1dfe826351f8a48a785eaedda4c584f Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 20 Jan 2023 16:43:42 +0100 Subject: [PATCH 2/7] Perf: improved GraphemeEnumerator by avoiding double codepoint iteration --- .../TextFormatting/FormattedTextSource.cs | 6 ++---- .../Media/TextFormatting/TextCharacters.cs | 12 ++++------- .../Media/TextFormatting/Unicode/Grapheme.cs | 20 +++++++++---------- .../Unicode/GraphemeEnumerator.cs | 9 ++++++--- src/Avalonia.Controls/TextBox.cs | 6 ++---- .../GraphemeBreakClassTrieGeneratorTests.cs | 8 ++++---- .../Media/TextFormatting/TextLayoutTests.cs | 17 ++++++++-------- 7 files changed, 36 insertions(+), 42 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 5c28989c7d..2f8c4ad263 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -128,11 +128,9 @@ namespace Avalonia.Media.TextFormatting var graphemeEnumerator = new GraphemeEnumerator(text); - while (graphemeEnumerator.MoveNext()) + while (graphemeEnumerator.MoveNext(out var grapheme)) { - var grapheme = graphemeEnumerator.Current; - - finalLength += grapheme.Text.Length; + finalLength += grapheme.Length; if (finalLength >= length) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 9e76418ac9..3ccfb40c4a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -140,16 +140,14 @@ namespace Avalonia.Media.TextFormatting var enumerator = new GraphemeEnumerator(textSpan); - while (enumerator.MoveNext()) + while (enumerator.MoveNext(out var grapheme)) { - var grapheme = enumerator.Current; - if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } - count += grapheme.Text.Length; + count += grapheme.Length; } return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel); @@ -184,10 +182,8 @@ namespace Avalonia.Media.TextFormatting var enumerator = new GraphemeEnumerator(text); - while (enumerator.MoveNext()) + while (enumerator.MoveNext(out var currentGrapheme)) { - var currentGrapheme = enumerator.Current; - var currentScript = currentGrapheme.FirstCodepoint.Script; if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) @@ -217,7 +213,7 @@ namespace Avalonia.Media.TextFormatting } } - length += currentGrapheme.Text.Length; + length += currentGrapheme.Length; } return length > 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index 5a4d891917..fcc12d3526 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -1,16 +1,15 @@ -using System; - -namespace Avalonia.Media.TextFormatting.Unicode +namespace Avalonia.Media.TextFormatting.Unicode { /// /// Represents the smallest unit of a writing system of any given language. /// public readonly ref struct Grapheme { - public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text) + public Grapheme(Codepoint firstCodepoint, int offset, int length) { FirstCodepoint = firstCodepoint; - Text = text; + Offset = offset; + Length = length; } /// @@ -19,12 +18,13 @@ namespace Avalonia.Media.TextFormatting.Unicode public Codepoint FirstCodepoint { get; } /// - /// The text of the grapheme cluster + /// Gets the starting code unit offset of this grapheme inside its containing text. /// - public ReadOnlySpan Text { get; } + public int Offset { get; } - /// - public override string ToString() - => Text.ToString(); + /// + /// Gets the length of this grapheme, in code units. + /// + public int Length { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index a6a9453b8a..dd01662155 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -198,10 +198,13 @@ namespace Avalonia.Media.TextFormatting.Unicode break; // nothing but trailers after the final RI } + const uint gb9Mask = + (1U << (int)GraphemeBreakClass.Extend) | + (1U << (int)GraphemeBreakClass.ZWJ) | + (1U << (int)GraphemeBreakClass.SpacingMark); + // rules GB9, GB9a - while (_currentType is GraphemeBreakClass.Extend - or GraphemeBreakClass.ZWJ - or GraphemeBreakClass.SpacingMark) + while (((1U << (int)_currentType) & gb9Mask) != 0U) { ReadNextCodepoint(); } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 9d07fb024a..78caf350b7 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -963,10 +963,8 @@ namespace Avalonia.Controls var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan()); - while (graphemeEnumerator.MoveNext()) + while (graphemeEnumerator.MoveNext(out var grapheme)) { - var grapheme = graphemeEnumerator.Current; - if (grapheme.FirstCodepoint.IsBreakChar) { if (lineCount + 1 > MaxLines) @@ -979,7 +977,7 @@ namespace Avalonia.Controls } } - length += grapheme.Text.Length; + length += grapheme.Length; } if (length < input.Length) diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs index a022039000..0e49669a04 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -40,9 +40,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var enumerator = new GraphemeEnumerator(text); - enumerator.MoveNext(); + enumerator.MoveNext(out var g); - var actual = enumerator.Current.Text; + var actual = text.AsSpan(g.Offset, g.Length); bool pass = actual.Length == grapheme.Length; @@ -86,9 +86,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var count = 0; - while (enumerator.MoveNext()) + while (enumerator.MoveNext(out var grapheme)) { - Assert.Equal(1, enumerator.Current.Text.Length); + Assert.Equal(1, grapheme.Length); count++; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index a24a0fcf70..2b63f24cf6 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -151,9 +151,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting while (true) { - while (inner.MoveNext()) + Grapheme grapheme; + while (inner.MoveNext(out grapheme)) { - j += inner.Current.Text.Length; + j += grapheme.Length; if (j + i > text.Length) { @@ -184,14 +185,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - if (!outer.MoveNext()) + if (!outer.MoveNext(out grapheme)) { break; } inner = new GraphemeEnumerator(text); - i += outer.Current.Text.Length; + i += grapheme.Length; } } @@ -979,13 +980,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var graphemeEnumerator = new GraphemeEnumerator(text); - while (graphemeEnumerator.MoveNext()) + while (graphemeEnumerator.MoveNext(out var grapheme)) { - var grapheme = graphemeEnumerator.Current; + var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; - var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; - - i += grapheme.Text.Length; + i += grapheme.Length; var layout = new TextLayout( text, From 63f6ef63af8a8597c9efb9ddce829a12305423b7 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 20 Jan 2023 19:00:37 +0100 Subject: [PATCH 3/7] Perf: pass FontManager and typefaces around during text layout --- src/Avalonia.Base/Media/FontManager.cs | 2 +- .../Media/TextFormatting/ShapedTextRun.cs | 2 +- .../Media/TextFormatting/TextCharacters.cs | 54 +++++++++---------- .../Media/TextFormatting/TextFormatterImpl.cs | 37 +++++++------ .../Media/TextFormatting/TextLayout.cs | 7 +-- .../Media/TextFormatting/TextLineImpl.cs | 9 ++-- .../Media/TextFormatting/TextRunProperties.cs | 5 ++ 7 files changed, 64 insertions(+), 52 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index e82d5b7ba5..2dabb29e76 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -132,7 +132,7 @@ namespace Avalonia.Media { typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); - var glyphTypeface = typeface.GlyphTypeface; + var glyphTypeface = GetOrAddGlyphTypeface(typeface); if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){ return true; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index d444a58297..ac196bf7e0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting { ShapedBuffer = shapedBuffer; Properties = properties; - TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); + TextMetrics = new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize); } public bool IsReversed { get; private set; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 3ccfb40c4a..94db739d4d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -69,6 +69,7 @@ namespace Avalonia.Media.TextFormatting /// The characters to create text runs from. /// The default text run properties. /// The bidi level of the run. + /// The font manager to use. /// /// A list of shapeable text runs. private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, @@ -76,31 +77,32 @@ namespace Avalonia.Media.TextFormatting ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; - var currentTypeface = defaultTypeface; + var defaultGlyphTypeface = defaultProperties.CachedGlyphTypeface; var previousTypeface = previousProperties?.Typeface; + var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; - if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script)) { - if (script == Script.Common && previousTypeface is not null) + if (script == Script.Common && previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _)) { return new UnshapedTextRun(text.Slice(0, fallbackCount), - defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); + defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); } } - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), biDiLevel); } - if (previousTypeface is not null) + if (previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _)) { return new UnshapedTextRun(text.Slice(0, count), - defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); + defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); } } @@ -124,25 +126,23 @@ namespace Avalonia.Media.TextFormatting var matchFound = fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, - out currentTypeface); + out var fallbackTypeface); - if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _)) + var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); + + if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _)) { //Fallback found - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), biDiLevel); } // no fallback found - currentTypeface = defaultTypeface; - - var glyphTypeface = currentTypeface.GlyphTypeface; - var enumerator = new GraphemeEnumerator(textSpan); while (enumerator.MoveNext(out var grapheme)) { - if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) + if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } @@ -157,15 +157,15 @@ namespace Avalonia.Media.TextFormatting /// Tries to get a shapeable length that is supported by the specified typeface. /// /// The characters to shape. - /// The typeface that is used to find matching characters. - /// + /// The typeface that is used to find matching characters. + /// The default typeface. /// The shapeable length. /// /// internal static bool TryGetShapeableLength( ReadOnlySpan text, - Typeface typeface, - Typeface? defaultTypeface, + IGlyphTypeface glyphTypeface, + IGlyphTypeface? defaultGlyphTypeface, out int length, out Script script) { @@ -177,22 +177,22 @@ namespace Avalonia.Media.TextFormatting return false; } - var font = typeface.GlyphTypeface; - var defaultFont = defaultTypeface?.GlyphTypeface; - var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext(out var currentGrapheme)) { - var currentScript = currentGrapheme.FirstCodepoint.Script; + var currentCodepoint = currentGrapheme.FirstCodepoint; + var currentScript = currentCodepoint.Script; - if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentCodepoint.IsWhiteSpace + && defaultGlyphTypeface != null + && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } //Stop at the first missing glyph - if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index b0242be87e..bc19690196 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -27,6 +27,7 @@ namespace Avalonia.Media.TextFormatting TextLineBreak? nextLineBreak = null; IReadOnlyList? textRuns; var objectPool = FormattingObjectPool.Instance; + var fontManager = FontManager.Current; var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); @@ -42,7 +43,7 @@ namespace Avalonia.Media.TextFormatting } else { - shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection); + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection); textRuns = shapedTextRuns; if (nextLineBreak == null && textEndOfLine != null) @@ -72,7 +73,7 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.Wrap: { textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, - paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); break; } default: @@ -178,12 +179,13 @@ namespace Avalonia.Media.TextFormatting /// The default paragraph properties. /// The resolved flow direction. /// A pool used to get reusable formatting objects. + /// The font manager to use. /// /// A list of shaped text characters. /// private static RentedList ShapeTextRuns(IReadOnlyList textRuns, TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, - out FlowDirection resolvedFlowDirection) + FontManager fontManager, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; var shapedRuns = objectPool.TextRunLists.Rent(); @@ -223,7 +225,7 @@ namespace Avalonia.Media.TextFormatting var processedRuns = objectPool.TextRunLists.Rent(); - CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns); bidiData.Reset(); bidiAlgorithm.Reset(); @@ -240,7 +242,9 @@ namespace Avalonia.Media.TextFormatting { groupedRuns.Clear(); groupedRuns.Add(shapeableRun); + var text = shapeableRun.Text; + var properties = shapeableRun.Properties; while (index + 1 < processedRuns.Count) { @@ -251,7 +255,7 @@ namespace Avalonia.Media.TextFormatting if (shapeableRun.BidiLevel == nextRun.BidiLevel && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) - && CanShapeTogether(shapeableRun.Properties, nextRun.Properties)) + && CanShapeTogether(properties, nextRun.Properties)) { groupedRuns.Add(nextRun); index++; @@ -263,10 +267,10 @@ namespace Avalonia.Media.TextFormatting break; } - var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, - currentRun.Properties.FontRenderingEmSize, - shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, - paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); + var shaperOptions = new TextShaperOptions( + properties.CachedGlyphTypeface, + properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, + paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); @@ -377,10 +381,11 @@ namespace Avalonia.Media.TextFormatting /// /// The text characters to form from. /// The bidi levels. + /// The font manager to use. /// A list that will be filled with the processed runs. /// private static void CoalesceLevels(IReadOnlyList textCharacters, ReadOnlySpan levels, - RentedList processedRuns) + FontManager fontManager, RentedList processedRuns) { if (levels.Length == 0) { @@ -393,7 +398,6 @@ namespace Avalonia.Media.TextFormatting TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; ReadOnlyMemory runText = default; - var fontManager = FontManager.Current; for (var i = 0; i < textCharacters.Count; i++) { @@ -638,11 +642,11 @@ namespace Avalonia.Media.TextFormatting /// /// The empty text line. public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, - TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool) + TextParagraphProperties paragraphProperties, FontManager fontManager) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; - var glyphTypeface = properties.Typeface.GlyphTypeface; + var glyphTypeface = properties.CachedGlyphTypeface; var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; @@ -666,14 +670,15 @@ namespace Avalonia.Media.TextFormatting /// /// The current line break if the line was explicitly broken. /// A pool used to get reusable formatting objects. + /// The font manager to use. /// The wrapped text line. private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, - TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) + TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager) { if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -869,7 +874,7 @@ namespace Avalonia.Media.TextFormatting { var textShaper = TextShaper.Current; - var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface; + var glyphTypeface = textRun.Properties!.CachedGlyphTypeface; var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 7a74dc89ae..bb58e0d692 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -427,11 +427,12 @@ namespace Avalonia.Media.TextFormatting private TextLine[] CreateTextLines() { var objectPool = FormattingObjectPool.Instance; + var fontManager = FontManager.Current; if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, - FormattingObjectPool.Instance); + fontManager); Bounds = new Rect(0, 0, 0, textLine.Height); @@ -458,7 +459,7 @@ namespace Avalonia.Media.TextFormatting if (previousLine != null && previousLine.NewLineLength > 0) { var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, - _paragraphProperties, objectPool); + _paragraphProperties, fontManager); textLines.Add(emptyTextLine); @@ -517,7 +518,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, objectPool); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager); textLines.Add(textLine); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 260fcaccbe..ad3244a3a5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1256,7 +1256,7 @@ namespace Avalonia.Media.TextFormatting private TextLineMetrics CreateLineMetrics() { - var fontMetrics = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface.Metrics; + var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; @@ -1285,12 +1285,13 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextRun textRun: { + var properties = textRun.Properties; var textMetrics = - new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); + new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize); - if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize) + if (fontRenderingEmSize < properties.FontRenderingEmSize) { - fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; + fontRenderingEmSize = properties.FontRenderingEmSize; if (ascent > textMetrics.Ascent) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 7bad99f33f..1622bc3b6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting /// public abstract class TextRunProperties : IEquatable { + private IGlyphTypeface? _cachedGlyphTypeFace; + /// /// Run typeface /// @@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting /// public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline; + internal IGlyphTypeface CachedGlyphTypeface + => _cachedGlyphTypeFace ??= Typeface.GlyphTypeface; + public bool Equals(TextRunProperties? other) { if (ReferenceEquals(null, other)) From 900299b6a8b002a77d1bff970ce140a9f26cd00d Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 21 Jan 2023 00:31:07 +0100 Subject: [PATCH 4/7] Perf: improved LineBreakEnumerator by using less branches --- .../Media/TextFormatting/Unicode/Codepoint.cs | 2 +- .../Unicode/LineBreakEnumerator.cs | 229 ++++++++++++------ 2 files changed, 155 insertions(+), 76 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 6433a37b22..23a1e4a275 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -102,7 +102,7 @@ namespace Avalonia.Media.TextFormatting.Unicode (1UL << (int)GeneralCategory.SpaceSeparator) | (1UL << (int)GeneralCategory.SpacingMark); - return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0L; + return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0UL; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 877ab76ce5..31ef47f47b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -3,6 +3,7 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; +using System.Runtime.CompilerServices; namespace Avalonia.Media.TextFormatting.Unicode { @@ -118,13 +119,14 @@ namespace Avalonia.Media.TextFormatting.Unicode return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static LineBreakClass MapClass(Codepoint cp) { if (cp.Value == 327685) { return LineBreakClass.Alphabetic; } - + // LB 1 // ========================================== // Resolved Original General_Category @@ -133,26 +135,38 @@ namespace Avalonia.Media.TextFormatting.Unicode // CM SA Only Mn or Mc // AL SA Any except Mn and Mc // NS CJ Any - switch (cp.LineBreakClass) - { - case LineBreakClass.Ambiguous: - case LineBreakClass.Surrogate: - case LineBreakClass.Unknown: - return LineBreakClass.Alphabetic; + var cls = cp.LineBreakClass; - case LineBreakClass.ComplexContext: - return cp.GeneralCategory == GeneralCategory.NonspacingMark || cp.GeneralCategory == GeneralCategory.SpacingMark - ? LineBreakClass.CombiningMark - : LineBreakClass.Alphabetic; + const ulong specialMask = + (1UL << (int)LineBreakClass.Ambiguous) | + (1UL << (int)LineBreakClass.Surrogate) | + (1UL << (int)LineBreakClass.Unknown) | + (1UL << (int)LineBreakClass.ComplexContext) | + (1UL << (int)LineBreakClass.ConditionalJapaneseStarter); - case LineBreakClass.ConditionalJapaneseStarter: - return LineBreakClass.Nonstarter; - - default: - return cp.LineBreakClass; + if (((1UL << (int)cls) & specialMask) != 0UL) + { + switch (cls) + { + case LineBreakClass.Ambiguous: + case LineBreakClass.Surrogate: + case LineBreakClass.Unknown: + return LineBreakClass.Alphabetic; + + case LineBreakClass.ComplexContext: + return cp.GeneralCategory is GeneralCategory.NonspacingMark or GeneralCategory.SpacingMark + ? LineBreakClass.CombiningMark + : LineBreakClass.Alphabetic; + + case LineBreakClass.ConditionalJapaneseStarter: + return LineBreakClass.Nonstarter; + } } + + return cls; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static LineBreakClass MapFirst(LineBreakClass c) { switch (c) @@ -169,10 +183,80 @@ namespace Avalonia.Media.TextFormatting.Unicode } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsAlphaNumeric(LineBreakClass cls) - => cls == LineBreakClass.Alphabetic - || cls == LineBreakClass.HebrewLetter - || cls == LineBreakClass.Numeric; + { + const ulong mask = + (1UL << (int)LineBreakClass.Alphabetic) | + (1UL << (int)LineBreakClass.HebrewLetter) | + (1UL << (int)LineBreakClass.Numeric); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsPrefixPostfixNumericOrSpace(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.PostfixNumeric) | + (1UL << (int)LineBreakClass.PrefixNumeric) | + (1UL << (int)LineBreakClass.Space); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsPrefixPostfixNumeric(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.PostfixNumeric) | + (1UL << (int)LineBreakClass.PrefixNumeric); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsClosePunctuationOrParenthesis(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.ClosePunctuation) | + (1UL << (int)LineBreakClass.CloseParenthesis); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsClosePunctuationOrInfixNumericOrBreakSymbols(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.ClosePunctuation) | + (1UL << (int)LineBreakClass.InfixNumeric) | + (1UL << (int)LineBreakClass.BreakSymbols); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSpaceOrWordJoinerOrAlphabetic(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.Space) | + (1UL << (int)LineBreakClass.WordJoiner) | + (1UL << (int)LineBreakClass.Alphabetic); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMandatoryBreakOrLineFeedOrCarriageReturn(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.MandatoryBreak) | + (1UL << (int)LineBreakClass.LineFeed) | + (1UL << (int)LineBreakClass.CarriageReturn); + + return ((1UL << (int)cls) & mask) != 0UL; + } private LineBreakClass PeekNextCharClass() { @@ -198,83 +282,77 @@ namespace Avalonia.Media.TextFormatting.Unicode // Track combining mark exceptions. LB22 if (cls == LineBreakClass.CombiningMark) { - switch (_currentClass) + const ulong lb22ExMask = + (1UL << (int)LineBreakClass.MandatoryBreak) | + (1UL << (int)LineBreakClass.ContingentBreak) | + (1UL << (int)LineBreakClass.Exclamation) | + (1UL << (int)LineBreakClass.LineFeed) | + (1UL << (int)LineBreakClass.NextLine) | + (1UL << (int)LineBreakClass.Space) | + (1UL << (int)LineBreakClass.ZWSpace) | + (1UL << (int)LineBreakClass.CarriageReturn); + + if (((1UL << (int)_currentClass) & lb22ExMask) != 0UL) { - case LineBreakClass.MandatoryBreak: - case LineBreakClass.ContingentBreak: - case LineBreakClass.Exclamation: - case LineBreakClass.LineFeed: - case LineBreakClass.NextLine: - case LineBreakClass.Space: - case LineBreakClass.ZWSpace: - case LineBreakClass.CarriageReturn: - _lb22ex = true; - break; + _lb22ex = true; } - } - // Track combining mark exceptions. LB31 - if (_first && cls == LineBreakClass.CombiningMark) - { - _lb31 = true; + const ulong lb31Mask = + (1UL << (int)LineBreakClass.MandatoryBreak) | + (1UL << (int)LineBreakClass.ContingentBreak) | + (1UL << (int)LineBreakClass.Exclamation) | + (1UL << (int)LineBreakClass.LineFeed) | + (1UL << (int)LineBreakClass.NextLine) | + (1UL << (int)LineBreakClass.Space) | + (1UL << (int)LineBreakClass.ZWSpace) | + (1UL << (int)LineBreakClass.CarriageReturn) | + (1UL << (int)LineBreakClass.ZWJ); + + // Track combining mark exceptions. LB31 + if (_first || ((1UL << (int)_currentClass) & lb31Mask) != 0UL) + { + _lb31 = true; + } } - if (cls == LineBreakClass.CombiningMark) + if (_first) { - switch (_currentClass) + // Rule LB24 + if (IsClosePunctuationOrParenthesis(cls)) { - case LineBreakClass.MandatoryBreak: - case LineBreakClass.ContingentBreak: - case LineBreakClass.Exclamation: - case LineBreakClass.LineFeed: - case LineBreakClass.NextLine: - case LineBreakClass.Space: - case LineBreakClass.ZWSpace: - case LineBreakClass.CarriageReturn: - case LineBreakClass.ZWJ: - _lb31 = true; - break; + _lb24ex = true; } - } - if (_first - && (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space)) - { - _lb31 = true; + // Rule LB25 + if (IsClosePunctuationOrInfixNumericOrBreakSymbols(cls)) + { + _lb25ex = true; + } + + if (IsPrefixPostfixNumericOrSpace(cls)) + { + _lb31 = true; + } } - if (_currentClass == LineBreakClass.Alphabetic && - (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space)) + if (_currentClass == LineBreakClass.Alphabetic && IsPrefixPostfixNumericOrSpace(cls)) { _lb31 = true; } // Reset LB31 if next is U+0028 (Left Opening Parenthesis) if (_lb31 - && _currentClass != LineBreakClass.PostfixNumeric - && _currentClass != LineBreakClass.PrefixNumeric - && cls == LineBreakClass.OpenPunctuation && cp.Value == 0x0028) + && !IsPrefixPostfixNumeric(_currentClass) + && cls == LineBreakClass.OpenPunctuation + && cp.Value == 0x0028) { _lb31 = false; } - // Rule LB24 - if (_first && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.CloseParenthesis)) - { - _lb24ex = true; - } - - // Rule LB25 - if (_first - && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.InfixNumeric || cls == LineBreakClass.BreakSymbols)) - { - _lb25ex = true; - } - - if (cls == LineBreakClass.Space || cls == LineBreakClass.WordJoiner || cls == LineBreakClass.Alphabetic) + if (IsSpaceOrWordJoinerOrAlphabetic(cls)) { var next = PeekNextCharClass(); - if (next == LineBreakClass.ClosePunctuation || next == LineBreakClass.InfixNumeric || next == LineBreakClass.BreakSymbols) + if (IsClosePunctuationOrInfixNumericOrBreakSymbols(next)) { _lb25ex = true; } @@ -295,6 +373,7 @@ namespace Avalonia.Media.TextFormatting.Unicode return cls; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool? GetSimpleBreak() { // handle classes not handled by the pair table @@ -317,6 +396,7 @@ namespace Avalonia.Media.TextFormatting.Unicode return null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] // quite long but only one usage private bool GetPairTableBreak(LineBreakClass lastClass) { // If not handled already, use the pair table @@ -477,8 +557,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var cls = cp.LineBreakClass; - if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || - cls == LineBreakClass.CarriageReturn) + if (IsMandatoryBreakOrLineFeedOrCarriageReturn(cls)) { from -= count; } From dccad9aa5714e3b34a1e73f3dde8b7d4e75a27c3 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 21 Jan 2023 02:08:50 +0100 Subject: [PATCH 5/7] Perf: improved BidiAlgorithm by using less branches --- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 170 ++++++++---------- .../Media/TextFormatting/Unicode/BiDiData.cs | 37 ++-- 2 files changed, 89 insertions(+), 118 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 36e9e6eb79..3406432ce7 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -870,74 +870,59 @@ namespace Avalonia.Media.TextFormatting.Unicode _runDirection = DirectionFromLevel(runLevel); _runLength = _runResolvedClasses.Length; - // By tracking the types of characters known to be in the current run, we can - // skip some of the rules that we know won't apply. The flags will be - // initialized while we're processing rule W1 below. - var hasEN = false; - var hasAL = false; - var hasES = false; - var hasCS = false; - var hasAN = false; - var hasET = false; - // Rule W1 // Also, set hasXX flags int i; var previousClass = sos; + const uint isolateMask = + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate); + + const uint wRulesMask = + (1U << (int)BidiClass.EuropeanNumber) | + (1U << (int)BidiClass.ArabicLetter) | + (1U << (int)BidiClass.EuropeanSeparator) | + (1U << (int)BidiClass.CommonSeparator) | + (1U << (int)BidiClass.ArabicNumber) | + (1U << (int)BidiClass.EuropeanTerminator); + + uint wRules = 0; + for (i = 0; i < _runLength; i++) { var resolvedClass = _runResolvedClasses[i]; - - switch (resolvedClass) - { - case BidiClass.NonspacingMark: - _runResolvedClasses[i] = previousClass; - break; - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: + if (resolvedClass == BidiClass.NonspacingMark) + { + _runResolvedClasses[i] = previousClass; + } + else + { + var classBit = 1U << (int)resolvedClass; + if ((classBit & isolateMask) != 0U) + { previousClass = BidiClass.OtherNeutral; - break; - - case BidiClass.EuropeanNumber: - hasEN = true; - previousClass = resolvedClass; - break; - - case BidiClass.ArabicLetter: - hasAL = true; - previousClass = resolvedClass; - break; - - case BidiClass.EuropeanSeparator: - hasES = true; - previousClass = resolvedClass; - break; - - case BidiClass.CommonSeparator: - hasCS = true; - previousClass = resolvedClass; - break; - - case BidiClass.ArabicNumber: - hasAN = true; - previousClass = resolvedClass; - break; - - case BidiClass.EuropeanTerminator: - hasET = true; - previousClass = resolvedClass; - break; - - default: + } + else + { + wRules |= classBit & wRulesMask; previousClass = resolvedClass; - break; + } } } + // By tracking the types of characters known to be in the current run, we can + // skip some of the rules that we know won't apply. + var hasEN = (wRules & (1U << (int)BidiClass.EuropeanNumber)) != 0U; + var hasAL = (wRules & (1U << (int)BidiClass.ArabicLetter)) != 0U; + var hasES = (wRules & (1U << (int)BidiClass.EuropeanSeparator)) != 0U; + var hasCS = (wRules & (1U << (int)BidiClass.CommonSeparator)) != 0U; + var hasAN = (wRules & (1U << (int)BidiClass.ArabicNumber)) != 0U; + var hasET = (wRules & (1U << (int)BidiClass.EuropeanTerminator)) != 0U; + // Rule W2 if (hasEN) { @@ -1549,23 +1534,20 @@ namespace Avalonia.Media.TextFormatting.Unicode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsWhitespace(BidiClass biDiClass) { - switch (biDiClass) - { - case BidiClass.LeftToRightEmbedding: - case BidiClass.RightToLeftEmbedding: - case BidiClass.LeftToRightOverride: - case BidiClass.RightToLeftOverride: - case BidiClass.PopDirectionalFormat: - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: - case BidiClass.BoundaryNeutral: - case BidiClass.WhiteSpace: - return true; - default: - return false; - } + const uint mask = + (1U << (int)BidiClass.LeftToRightEmbedding) | + (1U << (int)BidiClass.RightToLeftEmbedding) | + (1U << (int)BidiClass.LeftToRightOverride) | + (1U << (int)BidiClass.RightToLeftOverride) | + (1U << (int)BidiClass.PopDirectionalFormat) | + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate) | + (1U << (int)BidiClass.BoundaryNeutral) | + (1U << (int)BidiClass.WhiteSpace); + + return ((1U << (int)biDiClass) & mask) != 0U; } /// @@ -1586,18 +1568,15 @@ namespace Avalonia.Media.TextFormatting.Unicode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsRemovedByX9(BidiClass biDiClass) { - switch (biDiClass) - { - case BidiClass.LeftToRightEmbedding: - case BidiClass.RightToLeftEmbedding: - case BidiClass.LeftToRightOverride: - case BidiClass.RightToLeftOverride: - case BidiClass.PopDirectionalFormat: - case BidiClass.BoundaryNeutral: - return true; - default: - return false; - } + const uint mask = + (1U << (int)BidiClass.LeftToRightEmbedding) | + (1U << (int)BidiClass.RightToLeftEmbedding) | + (1U << (int)BidiClass.LeftToRightOverride) | + (1U << (int)BidiClass.RightToLeftOverride) | + (1U << (int)BidiClass.PopDirectionalFormat) | + (1U << (int)BidiClass.BoundaryNeutral); + + return ((1U << (int)biDiClass) & mask) != 0U; } /// @@ -1606,20 +1585,17 @@ namespace Avalonia.Media.TextFormatting.Unicode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsNeutralClass(BidiClass direction) { - switch (direction) - { - case BidiClass.ParagraphSeparator: - case BidiClass.SegmentSeparator: - case BidiClass.WhiteSpace: - case BidiClass.OtherNeutral: - case BidiClass.RightToLeftIsolate: - case BidiClass.LeftToRightIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: - return true; - default: - return false; - } + const uint mask = + (1U << (int)BidiClass.ParagraphSeparator) | + (1U << (int)BidiClass.SegmentSeparator) | + (1U << (int)BidiClass.WhiteSpace) | + (1U << (int)BidiClass.OtherNeutral) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate); + + return ((1U << (int)direction) & mask) != 0U; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 5cc222b813..214ea07c98 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -73,6 +73,19 @@ namespace Avalonia.Media.TextFormatting.Unicode // bracket values for all code points int i = Length; + + const uint embeddingMask = + (1U << (int)BidiClass.LeftToRightEmbedding) | + (1U << (int)BidiClass.LeftToRightOverride) | + (1U << (int)BidiClass.RightToLeftEmbedding) | + (1U << (int)BidiClass.RightToLeftOverride) | + (1U << (int)BidiClass.PopDirectionalFormat); + + const uint isolateMask = + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate); var codePointEnumerator = new CodepointEnumerator(text); @@ -85,27 +98,9 @@ namespace Avalonia.Media.TextFormatting.Unicode _classes[i] = dir; - switch (dir) - { - case BidiClass.LeftToRightEmbedding: - case BidiClass.LeftToRightOverride: - case BidiClass.RightToLeftEmbedding: - case BidiClass.RightToLeftOverride: - case BidiClass.PopDirectionalFormat: - { - HasEmbeddings = true; - break; - } - - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: - { - HasIsolates = true; - break; - } - } + var dirBit = 1U << (int)dir; + HasEmbeddings = (dirBit & embeddingMask) != 0U; + HasIsolates = (dirBit & isolateMask) != 0U; // Lookup paired bracket types var pbt = codepoint.PairedBracketType; From f951929d54ec51213c9db26dd85302350c05918c Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 21 Jan 2023 02:56:34 +0100 Subject: [PATCH 6/7] Perf: improved CodepointEnumerator --- .../TextFormatting/InterWordJustification.cs | 4 +- .../Media/TextFormatting/TextCharacters.cs | 6 +-- .../TextFormatting/TextEllipsisHelper.cs | 4 +- .../Media/TextFormatting/TextFormatterImpl.cs | 47 +++++++++---------- .../Media/TextFormatting/Unicode/BiDiData.cs | 4 +- .../Unicode/CodepointEnumerator.cs | 24 ++++------ .../Unicode/LineBreakEnumerator.cs | 15 +++--- .../LineBreakEnumuratorTests.cs | 47 ++++++++++--------- .../TextFormatting/TextFormatterTests.cs | 4 +- 9 files changed, 70 insertions(+), 85 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 7afb758038..efcd866bbc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -60,10 +60,8 @@ namespace Avalonia.Media.TextFormatting var lineBreakEnumerator = new LineBreakEnumerator(text.Span); - while (lineBreakEnumerator.MoveNext()) + while (lineBreakEnumerator.MoveNext(out var currentBreak)) { - var currentBreak = lineBreakEnumerator.Current; - if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length) { breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 94db739d4d..82cf3297fd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -110,14 +110,14 @@ namespace Avalonia.Media.TextFormatting var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span); - while (codepointEnumerator.MoveNext()) + while (codepointEnumerator.MoveNext(out var cp)) { - if (codepointEnumerator.Current.IsWhiteSpace) + if (cp.IsWhiteSpace) { continue; } - codepoint = codepointEnumerator.Current; + codepoint = cp; break; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index e6743f5533..47973e37b5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -48,9 +48,9 @@ namespace Avalonia.Media.TextFormatting var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) { - var nextBreakPosition = lineBreaker.Current.PositionMeasure; + var nextBreakPosition = lineBreak.PositionMeasure; if (nextBreakPosition == 0) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index bc19690196..7de842ab39 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -231,6 +231,7 @@ namespace Avalonia.Media.TextFormatting bidiAlgorithm.Reset(); var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); + var textShaper = TextShaper.Current; for (var index = 0; index < processedRuns.Count; index++) { @@ -272,7 +273,7 @@ namespace Avalonia.Media.TextFormatting properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); + ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns); break; } @@ -360,9 +361,9 @@ namespace Avalonia.Media.TextFormatting && x.BaselineAlignment == y.BaselineAlignment; private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text, - TextShaperOptions options, RentedList results) + TextShaperOptions options, TextShaper textShaper, RentedList results) { - var shapedBuffer = TextShaper.Current.ShapeText(text, options); + var shapedBuffer = textShaper.ShapeText(text, options); for (var i = 0; i < textRuns.Count; i++) { @@ -559,15 +560,13 @@ namespace Avalonia.Media.TextFormatting var lineBreakEnumerator = new LineBreakEnumerator(text.Span); - while (lineBreakEnumerator.MoveNext()) + while (lineBreakEnumerator.MoveNext(out lineBreak)) { - if (!lineBreakEnumerator.Current.Required) + if (!lineBreak.Required) { continue; } - lineBreak = lineBreakEnumerator.Current; - return lineBreak.PositionWrap >= textRun.Length || true; } @@ -704,20 +703,20 @@ namespace Avalonia.Media.TextFormatting { var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - while (lineBreaker.MoveNext()) + while (lineBreaker.MoveNext(out var lineBreak)) { - if (lineBreaker.Current.Required && - currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) + if (lineBreak.Required && + currentLength + lineBreak.PositionMeasure <= measuredLength) { //Explicit break found breakFound = true; - currentPosition = currentLength + lineBreaker.Current.PositionWrap; + currentPosition = currentLength + lineBreak.PositionWrap; break; } - if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) + if (currentLength + lineBreak.PositionMeasure > measuredLength) { if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) { @@ -733,21 +732,21 @@ namespace Avalonia.Media.TextFormatting //Find next possible wrap position (overflow) if (index < textRuns.Count - 1) { - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (lineBreak.PositionWrap != currentRun.Length) { //We already found the next possible wrap position. breakFound = true; - currentPosition = currentLength + lineBreaker.Current.PositionWrap; + currentPosition = currentLength + lineBreak.PositionWrap; break; } - while (lineBreaker.MoveNext() && index < textRuns.Count) + while (lineBreaker.MoveNext(out lineBreak) && index < textRuns.Count) { - currentPosition += lineBreaker.Current.PositionWrap; + currentPosition += lineBreak.PositionWrap; - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (lineBreak.PositionWrap != currentRun.Length) { break; } @@ -766,7 +765,7 @@ namespace Avalonia.Media.TextFormatting } else { - currentPosition = currentLength + lineBreaker.Current.PositionWrap; + currentPosition = currentLength + lineBreak.PositionWrap; } breakFound = true; @@ -782,9 +781,9 @@ namespace Avalonia.Media.TextFormatting break; } - if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) + if (lineBreak.PositionMeasure != lineBreak.PositionWrap) { - lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; + lastWrapPosition = currentLength + lineBreak.PositionWrap; } } @@ -806,18 +805,18 @@ namespace Avalonia.Media.TextFormatting var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); - var lineBreak = postSplitRuns?.Count > 0 ? + var textLineBreak = postSplitRuns?.Count > 0 ? new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : null; - if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) + if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null) { - lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); + textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); } var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, - lineBreak); + textLineBreak); textLine.FinalizeLine(); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 214ea07c98..b8094056f2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -89,10 +89,8 @@ namespace Avalonia.Media.TextFormatting.Unicode var codePointEnumerator = new CodepointEnumerator(text); - while (codePointEnumerator.MoveNext()) + while (codePointEnumerator.MoveNext(out var codepoint)) { - var codepoint = codePointEnumerator.Current; - // Look up BiDiClass var dir = codepoint.BiDiClass; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index d21f30ab7e..47a2b7d46a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -4,35 +4,27 @@ namespace Avalonia.Media.TextFormatting.Unicode { public ref struct CodepointEnumerator { - private ReadOnlySpan _text; + private readonly ReadOnlySpan _text; + private int _offset; public CodepointEnumerator(ReadOnlySpan text) - { - _text = text; - Current = Codepoint.ReplacementCodepoint; - } - - /// - /// Gets the current . - /// - public Codepoint Current { get; private set; } + => _text = text; /// /// Moves to the next . /// /// - public bool MoveNext() + public bool MoveNext(out Codepoint codepoint) { - if (_text.IsEmpty) + if ((uint)_offset >= (uint)_text.Length) { - Current = Codepoint.ReplacementCodepoint; - + codepoint = Codepoint.ReplacementCodepoint; return false; } - Current = Codepoint.ReadAt(_text, 0, out var count); + codepoint = Codepoint.ReadAt(_text, _offset, out var count); - _text = _text.Slice(count); + _offset += count; return true; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 31ef47f47b..5e12b7458e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -47,10 +47,8 @@ namespace Avalonia.Media.TextFormatting.Unicode _lb30 = false; _lb30a = 0; } - - public LineBreak Current { get; private set; } - - public bool MoveNext() + + public bool MoveNext(out LineBreak lineBreak) { // Get the first char if we're at the beginning of the string. if (_first) @@ -76,7 +74,7 @@ namespace Avalonia.Media.TextFormatting.Unicode case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed: { _currentClass = MapFirst(_nextClass); - Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true); + lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true); return true; } } @@ -88,7 +86,7 @@ namespace Avalonia.Media.TextFormatting.Unicode if (shouldBreak) { - Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); + lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); return true; } } @@ -109,13 +107,12 @@ namespace Avalonia.Media.TextFormatting.Unicode break; } - Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required); + lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required); return true; } } - Current = default; - + lineBreak = default; return false; } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs index d198fe81a6..3db9a32b65 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs @@ -24,32 +24,33 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting public void BasicLatinTest() { var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test."); + LineBreak lineBreak; - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(6, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(6, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(13, lineBreaker.Current.PositionWrap); - Assert.True(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(13, lineBreak.PositionWrap); + Assert.True(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(18, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(18, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(21, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(21, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(23, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(23, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(28, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(28, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.False(lineBreaker.MoveNext()); + Assert.False(lineBreaker.MoveNext(out lineBreak)); } @@ -72,9 +73,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { var breaks = new List(); - while (lineBreaker.MoveNext()) + while (lineBreaker.MoveNext(out var lineBreak)) { - breaks.Add(lineBreaker.Current); + breaks.Add(lineBreak); } return breaks; @@ -104,9 +105,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var foundBreaks = new List(); - while (lineBreaker.MoveNext()) + while (lineBreaker.MoveNext(out var lineBreak)) { - foundBreaks.Add(lineBreaker.Current.PositionWrap); + foundBreaks.Add(lineBreak.PositionWrap); } // Check the same diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 6b9fb579b1..7822d6624b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -283,9 +283,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expected = new List(); - while (lineBreaker.MoveNext()) + while (lineBreaker.MoveNext(out var lineBreak)) { - expected.Add(lineBreaker.Current.PositionWrap - 1); + expected.Add(lineBreak.PositionWrap - 1); } var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + From 7a1f74a3d3952141aaeaee886865d384321176b1 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 22 Jan 2023 13:36:46 +0100 Subject: [PATCH 7/7] Benchmarks: option to use Skia for text layout --- src/Skia/Avalonia.Skia/Avalonia.Skia.csproj | 1 + .../Avalonia.Benchmarks.csproj | 1 + .../Text/HugeTextLayout.cs | 32 +++++++++++++------ tests/Avalonia.UnitTests/MockGlyphRun.cs | 10 ++++-- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index ffe8352865..4c3cfe2ef4 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 941d377a17..0ddee2ad7a 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 0adabc75f1..4dad8442de 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -3,6 +3,7 @@ using System.Linq; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.TextFormatting; +using Avalonia.Skia; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; @@ -13,24 +14,35 @@ namespace Avalonia.Benchmarks.Text; [MaxWarmupCount(15)] public class HugeTextLayout : IDisposable { + private static readonly Random s_rand = new(); + private static readonly bool s_useSkia = true; + private readonly IDisposable _app; - private string[] _manySmallStrings; - private static Random _rand = new Random(); - + private readonly string[] _manySmallStrings; + private static string RandomString(int length) { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&?%$@"; - return new string(Enumerable.Repeat(chars, length).Select(s => s[_rand.Next(s.Length)]).ToArray()); + return new string(Enumerable.Repeat(chars, length).Select(s => s[s_rand.Next(s.Length)]).ToArray()); } public HugeTextLayout() { - _manySmallStrings = Enumerable.Range(0, 1000).Select(x => RandomString(_rand.Next(2, 15))).ToArray(); - _app = UnitTestApplication.Start( - TestServices.StyledWindow.With( - renderInterface: new NullRenderingPlatform(), - threadingInterface: new NullThreadingPlatform(), - standardCursorFactory: new NullCursorFactory())); + _manySmallStrings = Enumerable.Range(0, 1000).Select(_ => RandomString(s_rand.Next(2, 15))).ToArray(); + + var testServices = TestServices.StyledWindow.With( + renderInterface: new NullRenderingPlatform(), + threadingInterface: new NullThreadingPlatform(), + standardCursorFactory: new NullCursorFactory()); + + if (s_useSkia) + { + testServices = testServices.With( + textShaperImpl: new TextShaperImpl(), + fontManagerImpl: new FontManagerImpl()); + } + + _app = UnitTestApplication.Start(testServices); } private const string Text = @"Though, the objectives of the development of the prominent landmarks can be neglected in most cases, it should be realized that after the completion of the strategic decision gives rise to The Expertise of Regular Program (Carlton Cartwright in The Book of the Key Factor) diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index 45e7b47f62..477f34565f 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Avalonia.Media.TextFormatting; using Avalonia.Platform; @@ -9,7 +8,14 @@ namespace Avalonia.UnitTests { public MockGlyphRun(IReadOnlyList glyphInfos) { - Size = new Size(glyphInfos.Sum(x=> x.GlyphAdvance), 10); + var width = 0.0; + + for (var i = 0; i < glyphInfos.Count; ++i) + { + width += glyphInfos[i].GlyphAdvance; + } + + Size = new Size(width, 10); } public Size Size { get; }