Browse Source

Perf: pass FontManager and typefaces around during text layout

pull/10047/head
Julien Lebosquain 4 years ago
parent
commit
63f6ef63af
  1. 2
      src/Avalonia.Base/Media/FontManager.cs
  2. 2
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  3. 54
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  4. 37
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  5. 7
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  6. 9
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  7. 5
      src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs

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

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

54
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -69,6 +69,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="text">The characters to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns>
private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory<char> 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.
/// </summary>
/// <param name="text">The characters to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param>
/// <param name="glyphTypeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultGlyphTypeface">The default typeface.</param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
internal static bool TryGetShapeableLength(
ReadOnlySpan<char> 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;
}

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

@ -27,6 +27,7 @@ namespace Avalonia.Media.TextFormatting
TextLineBreak? nextLineBreak = null;
IReadOnlyList<TextRun>? 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
/// <param name="paragraphProperties">The default paragraph properties.</param>
/// <param name="resolvedFlowDirection">The resolved flow direction.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <returns>
/// A list of shaped text characters.
/// </returns>
private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> 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
/// </summary>
/// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
/// <param name="levels">The bidi levels.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <param name="processedRuns">A list that will be filled with the processed runs.</param>
/// <returns></returns>
private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ReadOnlySpan<sbyte> levels,
RentedList<TextRun> processedRuns)
FontManager fontManager, RentedList<TextRun> processedRuns)
{
if (levels.Length == 0)
{
@ -393,7 +398,6 @@ namespace Avalonia.Media.TextFormatting
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
ReadOnlyMemory<char> runText = default;
var fontManager = FontManager.Current;
for (var i = 0; i < textCharacters.Count; i++)
{
@ -638,11 +642,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
/// <returns>The empty text line.</returns>
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
/// <param name="resolvedFlowDirection"></param>
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <returns>The wrapped text line.</returns>
private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> 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;

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

9
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)
{

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

@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting
/// </remarks>
public abstract class TextRunProperties : IEquatable<TextRunProperties>
{
private IGlyphTypeface? _cachedGlyphTypeFace;
/// <summary>
/// Run typeface
/// </summary>
@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
internal IGlyphTypeface CachedGlyphTypeface
=> _cachedGlyphTypeFace ??= Typeface.GlyphTypeface;
public bool Equals(TextRunProperties? other)
{
if (ReferenceEquals(null, other))

Loading…
Cancel
Save