Skip to content

Commit c6e0ed5

Browse files
Merge pull request #398 from codymullins/shared-glyph-cache
Introduce the concept of a shared glyph cache
2 parents b23e2ae + d4f38dc commit c6e0ed5

3 files changed

Lines changed: 28 additions & 13 deletions

File tree

src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ public sealed class DrawingCanvas<TPixel> : DrawingCanvas
5959
/// </summary>
6060
private readonly Stack<DrawingCanvasState> savedStates = new();
6161

62+
// Per-canvas glyph-outline cache: hoists RichTextGlyphRenderer's per-glyph outline cache from
63+
// per-DrawText-call scope up to the whole canvas, so a glyph outline built once is reused by
64+
// every DrawText call on this canvas (across a frame's many text runs) instead of being
65+
// rebuilt for every run on a text-heavy page.
66+
private readonly Dictionary<RichTextGlyphRenderer.CacheKey, List<RichTextGlyphRenderer.GlyphRenderData>> glyphCache = [];
67+
6268
/// <summary>
6369
/// Initializes a new instance of the <see cref="DrawingCanvas{TPixel}"/> class.
6470
/// </summary>
@@ -457,7 +463,7 @@ private void DrawTextCore(
457463
EnsureTextPaint(brush, pen);
458464

459465
RichTextOptions configuredOptions = ConfigureTextOptions(textOptions, path, out IPath? configuredPath);
460-
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, configuredPath, pen, brush);
466+
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, configuredPath, pen, brush, this.glyphCache);
461467
TextRenderer renderer = new(glyphRenderer);
462468
renderer.RenderText(text, configuredOptions);
463469

@@ -487,7 +493,7 @@ public override void DrawText(
487493
effectiveOptions.ShapeOptions,
488494
Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform);
489495

490-
using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush);
496+
using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.glyphCache);
491497
textBlock.RenderTo(glyphRenderer, wrappingLength);
492498

493499
this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions, state.ClipPaths);
@@ -509,7 +515,7 @@ public override void DrawText(
509515
DrawingCanvasState state = this.ResolveState();
510516
DrawingOptions effectiveOptions = state.Options;
511517

512-
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush);
518+
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.glyphCache);
513519
textBlock.RenderTo(glyphRenderer, wrappingLength);
514520

515521
this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths);
@@ -537,7 +543,7 @@ public override void DrawText(
537543
effectiveOptions.ShapeOptions,
538544
Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform);
539545

540-
using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush);
546+
using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.glyphCache);
541547
lineLayout.RenderTo(glyphRenderer);
542548

543549
this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions, state.ClipPaths);
@@ -558,7 +564,7 @@ public override void DrawText(
558564
DrawingCanvasState state = this.ResolveState();
559565
DrawingOptions effectiveOptions = state.Options;
560566

561-
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush);
567+
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.glyphCache);
562568
lineLayout.RenderTo(glyphRenderer);
563569

564570
this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths);
@@ -1144,6 +1150,9 @@ public override void Dispose()
11441150
this.DisposePendingImageResources();
11451151
}
11461152

1153+
// Release the per-canvas glyph-outline cache.
1154+
this.glyphCache.Clear();
1155+
11471156
this.isDisposed = true;
11481157
}
11491158
}

src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa
7272
// Benchmarked to give <0.2% image difference vs. uncached, with >60% cache hit ratio.
7373
private const float AccuracyMultiple = 8;
7474

75-
/// <summary>Maps cache keys to their list of <see cref="GlyphRenderData"/> entries (one per layer).</summary>
76-
private readonly Dictionary<CacheKey, List<GlyphRenderData>> glyphCache = [];
75+
/// <summary>Maps cache keys to their list of <see cref="GlyphRenderData"/> entries (one per layer).
76+
/// Owned by the enclosing <see cref="DrawingCanvas{TPixel}"/> and shared across every DrawText
77+
/// call on that canvas, so glyph outlines persist beyond a single text draw.</summary>
78+
private readonly Dictionary<CacheKey, List<GlyphRenderData>> glyphCache;
7779

7880
/// <summary>Read cursor into the cached layer list for layered cache hits.</summary>
7981
private int cacheReadIndex;
@@ -107,16 +109,20 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa
107109
/// <param name="path">Optional path to draw the text along.</param>
108110
/// <param name="pen">Default pen for outlined text, or <see langword="null"/> for fill-only.</param>
109111
/// <param name="brush">Default brush for filled text, or <see langword="null"/> for outline-only.</param>
112+
/// <param name="glyphCache">Caller-owned per-canvas glyph cache shared across renderer
113+
/// instances so glyph outlines persist beyond a single text draw.</param>
110114
public RichTextGlyphRenderer(
111115
DrawingOptions drawingOptions,
112116
IPath? path,
113117
Pen? pen,
114-
Brush? brush)
118+
Brush? brush,
119+
Dictionary<CacheKey, List<GlyphRenderData>> glyphCache)
115120
: base(drawingOptions.Transform)
116121
{
117122
this.drawingOptions = drawingOptions;
118123
this.defaultPen = pen;
119124
this.defaultBrush = brush;
125+
this.glyphCache = glyphCache;
120126
this.DrawingOperations = [];
121127
this.currentCompositionMode = drawingOptions.GraphicsOptions.AlphaCompositionMode;
122128
this.currentBlendingMode = drawingOptions.GraphicsOptions.ColorBlendingMode;
@@ -782,7 +788,7 @@ private void Dispose(bool disposing)
782788
{
783789
if (disposing)
784790
{
785-
this.glyphCache.Clear();
791+
// The glyph cache is owned by the canvas and outlives this renderer.
786792
this.DrawingOperations.Clear();
787793
}
788794

@@ -795,7 +801,7 @@ private void Dispose(bool disposing)
795801
/// path and the sub-pixel deltas needed to reposition the path at a different
796802
/// screen location on a cache hit.
797803
/// </summary>
798-
private struct GlyphRenderData
804+
internal struct GlyphRenderData
799805
{
800806
/// <summary>
801807
/// The fractional-pixel offset between the path's bounding-box origin
@@ -834,7 +840,7 @@ private struct GlyphRenderData
834840
/// sub-pixel position (quantized to <see cref="AccuracyMultiple"/>), and the pen reference
835841
/// (since stroke width affects the outline path).
836842
/// </summary>
837-
private readonly struct CacheKey : IEquatable<CacheKey>
843+
internal readonly struct CacheKey : IEquatable<CacheKey>
838844
{
839845
/// <summary>Gets the font family name.</summary>
840846
public string Font { get; init; }
Lines changed: 2 additions & 2 deletions
Loading

0 commit comments

Comments
 (0)