Skip to content

Introduce the concept of a shared glyph cache#398

Open
codymullins wants to merge 2 commits into
SixLabors:mainfrom
codymullins:shared-glyph-cache
Open

Introduce the concept of a shared glyph cache#398
codymullins wants to merge 2 commits into
SixLabors:mainfrom
codymullins:shared-glyph-cache

Conversation

@codymullins
Copy link
Copy Markdown

@codymullins codymullins commented May 22, 2026

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

tl;dr: Adds an opt-in per-canvas glyph-outline cache (Configuration.SetSharedGlyphCache(true)) that reuses glyph outlines across DrawText calls on a canvas, making text-heavy rendering up to ~3× faster and cutting allocations ~2.6× (404 MB → 155 MB at 1000 text runs).

Draft pull request for a possible shared glyph cache. Shows promising performance benchmarks in Starling, especially when rendering an entire screen of text.

Hoping to see if you had appetite for merging in something like this -- it would be great to have. Or if there's anything I'm missing please LMK!

Notes:

  • I tried to find the best way to make it "backwards compatible" since it did seem to slip tolerance a bit for one of the cache tests.
  • The "configuration" route matched an existing pattern and seemed to cause the least downstream effects and not make any breaking changes.

Early benchmarks here, the full suite is still running:
image
image

EDIT - benchmarks from full suite finished:

  ┌──────────┬─────────────────────────┬───────────────────┬───────┬──────────────────────────────┐
  │ RunCount │   Default (per-call)    │ Shared per-canvas │ Ratio │ Allocated (default → shared) │
  ├──────────┼─────────────────────────┼───────────────────┼───────┼──────────────────────────────┤
  │ 50       │ 6.56 ms                 │ 3.25 ms           │ 0.50  │ 20 MB → 10 MB                │
  ├──────────┼─────────────────────────┼───────────────────┼───────┼──────────────────────────────┤
  │ 250      │ 29.74 ms                │ 12.95 ms          │ 0.44  │ 103 MB → 40 MB               │
  ├──────────┼─────────────────────────┼───────────────────┼───────┼──────────────────────────────┤
  │ 1000     │ 139.4 ms (median 117.5) │ 45.04 ms          │ 0.34  │ 404 MB → 155 MB              │
  └──────────┴─────────────────────────┴───────────────────┴───────┴──────────────────────────────┘

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 22, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Member

@JimBobSquarePants JimBobSquarePants left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks fantastic. I wish I had thought of it!

I think we can drop the configuration and use truthy default behaviour. The current differences are imperceptible to the human eye.

For the updated reference image just reuse your new output. I use pinga with lossless optimization to optimize them for storage. https://css-ig.net/pinga

@codymullins codymullins marked this pull request as ready for review May 25, 2026 17:14
Copilot AI review requested due to automatic review settings May 25, 2026 17:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduces an opt-in per-canvas shared glyph-outline cache for DrawText calls, allowing glyph outlines built once to be reused across multiple text draws on the same canvas.

Changes:

  • Adds a new GlyphCacheDefaultsExtensions API to enable/disable the shared glyph cache via Configuration.
  • Threads an optional shared Dictionary<CacheKey, List<GlyphRenderData>> from DrawingCanvas<TPixel> into RichTextGlyphRenderer, with ownership tracking so injected caches aren't cleared on dispose.
  • Adds a benchmark comparing the per-call cache against the per-canvas shared cache.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/ImageSharp.Drawing/Processing/GlyphCacheDefaultsExtensions.cs New public extensions to set/get a shared-glyph-cache flag on Configuration.
src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs Accepts an optional shared cache; conditionally clears it on dispose; promotes CacheKey/GlyphRenderData visibility to internal.
src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs Lazily creates and passes a per-canvas shared cache to each renderer instance when enabled.
tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextSharedGlyphCache.cs New benchmark exercising shared vs. per-call glyph cache across many runs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +34 to +35
internal static bool GetSharedGlyphCache(this Configuration configuration)
=> configuration.Properties.TryGetValue(typeof(SharedGlyphCacheKey), out object? value) && value is true;
this.DisposePendingImageResources();
}

// Release the per-canvas shared glyph-outline cache.
/// screen location on a cache hit.
/// </summary>
private struct GlyphRenderData
internal struct GlyphRenderData
/// <para>
/// Unlike <see cref="DrawTextRepeatedGlyphs"/> (a single <c>DrawText</c> call, where the
/// per-call cache already captures repeats), this draws <see cref="RunCount"/> separate
/// <c>DrawText</c> calls that reuse a common alphabet across runs - the cross-call reuse the
@codymullins
Copy link
Copy Markdown
Author

Thanks @JimBobSquarePants -- I'll work on updating. Just to be clear, use truthy default behaviour - you mean just use the shared glyph cache by canvas by default?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants