Skip to content

[Text] Glyph Outline Part 7/17 - GlyphTypeface variation clones - #22180

Open
Gillibald wants to merge 7 commits into
pr4a/fvar-avar-parsingfrom
pr4b/glyph-typeface-with-variation
Open

Gillibald wants to merge 7 commits into
pr4a/fvar-avar-parsingfrom
pr4b/glyph-typeface-with-variation

Conversation

@Gillibald

@Gillibald Gillibald commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Part 7 of 17 of the glyph-outline / variable-font workstream. Stacked on Part 6 (pr4a/fvar-avar-parsing); please merge that one first. Parts 8 to 11 plug the individual variation tables into the clone this PR builds, so none of them can land before it.

What does the pull request do?

Adds the variation clone and the public front door onto it.

public GlyphTypeface WithVariations(FontVariationSettings? settings, int? instanceIndex = null);
  • WithVariations takes user-space axis values, optionally on top of a named instance, and returns a typeface bound to that position. Explicit settings override the named instance per axis.
  • An internal WithVariation(NormalizedVariationPosition) is the normalized-space counterpart the variation tables use, and an internal VariationPosition reads the clone's position back.
  • IPlatformTypeface and ITextShaperTypeface gain the same surface with default no-op overrides.
  • FontCollectionKey carries the variation, so the same Typeface at two different wght values resolves to two cache entries instead of colliding.

What is the updated/expected behavior with this PR?

  • A static font, a design-default request, and a request matching the receiver's own position all return this. Nothing is allocated.
  • Any other request returns a clone, cached on the source typeface, so repeat calls with the same settings return the same instance. Reference identity is stable, which matters because downstream caches key on GlyphTypeface.
  • Settings are normalized before they reach the cache, so two different user-space requests that resolve to the same position, for instance two values that clamp to the same axis maximum, share one clone rather than producing duplicates.
  • Going through FontCollection produces the same identity-stable clones as calling WithVariations directly.
  • A platform or shaper that does not override its WithVariation returns itself, and the clone shares the source's platform typeface rather than owning a second one. A platform that does override gets its instance owned by the clone. Ownership follows the returned reference, so disposal never double-frees.
  • Concurrent first access to a typeface's lazily created text shaper produces one shaper, not one per racing caller.

Outlines, advances and metrics are still the default instance here. A clone at wght=900 reports wght=900 and draws exactly like the default until Parts 8 to 11 land.

How was the solution implemented (if it's not obvious)?

  • The clone constructor pre-projects the normalized position onto the fvar axis order once and caches the resulting array, so per-glyph variation work later is a scaler multiply rather than a projection. Every later variation PR builds on that cached array.
  • Clones hold a backpointer to their source and share its table parsers and its variation cache. Disposal walks to the source, so a clone never frees tables another clone is still reading.
  • FontCollectionKey includes the variation in its ordering as well as its equality, so keys remain totally ordered when a collection sorts them.

Checklist

Breaking changes

None. WithVariations is new, the interface members have default implementations, and FontCollectionKey's new field defaults to no variation.

Obsoletions / Deprecations

None.

Fixed issues

None.


🤖 Generated with Claude Code

@Gillibald Gillibald changed the title pr4b/glyph typeface with variation [Text] Glyph Outline Part 7/15 - GlyphTypeface variation clones Sep 8, 2026
@Gillibald Gillibald added enhancement area-textprocessing api-needs-review The PR adds new public APIs that should be reviewed. labels Sep 8, 2026
Adds the variation-tracking layer on GlyphTypeface without yet plumbing
the variation into Skia rendering or HarfBuzz shaping (those are pr4e1
and pr4e2, gated on the SkiaSharp 4.147 bump). After this slice, code
holding a GlyphTypeface can call WithVariation(settings) to obtain a
variation-bound clone that tracks the variation point; downstream
slices that read GlyphTypeface.VariationSettings (gvar in pr4c, HVAR
in pr4d, COLRv1 paint resolution) become variation-correct as they
land.

Interface additions:
- IPlatformTypeface gains VariationSettings (default => default) and
  WithVariation (default => this) — the no-op contract that keeps Tier 1
  shippable without a platform override. Skia implements the real Clone
  in PR4e1.
- ITextShaperTypeface gains the symmetric pair with the same defaults
  for the HarfBuzz path in PR4e2.

GlyphTypeface additions:
- VariationSettings property exposes the bound variation point.
- WithVariation(FontVariationSettings) returns a variation clone, with
  a per-source cache so repeat requests for equal settings return the
  same instance. The cache is unbounded in pr4b — LRU eviction is a
  follow-up (planning doc, "Cache sizing").
- Private clone constructor reference-shares every parsed table
  (cmap, glyf, fvar, avar, hmtx, vmtx, name, os2, etc.) and only
  varies the platform typeface (no-op in pr4b), variation settings,
  and lazy shaper. Memory cost per variation is a few dozen bytes,
  not the full table set.
- TextShaperTypeface getter on clones derives from the source's
  shaper via ITextShaperTypeface.WithVariation so face-level shaping
  state stays shared.
- Dispose disposes cached variation clones before the platform
  typeface to keep ownership clean. Clones skip platform typeface
  disposal in pr4b (shared with source); the _ownsPlatformTypeface
  flag flips on automatically once pr4e1 lands a real Skia clone.

FontCollectionKey extension:
- Adds an init-only Variation property (FontVariationSettings).
  Default-instance / static-font case uses default(...) so existing
  three-arg constructions and cache equality stay unchanged.
- FontCollectionKeyExtensions.ToFontCollectionKey propagates the
  variation from both GlyphTypeface and IPlatformTypeface so future
  consumers building keys from a varied typeface get the right
  cache entry automatically.

Behavior:
- WithVariation(default) on a varied clone returns the source — the
  source IS the default-instance typeface, so cache identity holds.
- WithVariation on a static font returns this unchanged (silent
  ignore — matches CSS and matches CreateVariationSettings).
- WithVariation(equal settings) returns the same cached instance.

The FontCollection-level projection (FontWeight=Bold on a variable
font automatically resolves via wght axis) is deferred. The cache
infrastructure is in place; pr4d will wire the projection through
FontCollectionBase when HVAR makes it visibly correct.

Tested with 15 unit tests covering static/variable behavior, cache
identity, clone-routes-through-source, table sharing, and
FontCollectionKey equality. All 493 Media tests still pass; render
tests still green.
The lazy getter raced: concurrent first access could create two shapers and
leak the loser. Use double-checked locking over a dedicated lock with the
backing field marked volatile, so the lock-free fast path observes a fully
published instance and exactly one shaper is ever created.
Exercises the synchronized TextShaperTypeface getter from many threads racing
into the first access, asserting exactly one shaper is created (no losing-thread
leak).
The record-synthesized equality includes the new Variation property but
the hand-written CompareTo did not, so keys differing only in variation
compared equal while Equals said otherwise - an IComparable contract
violation. Compare the variation coordinates lexicographically (they
are sorted by axis and zero-canonicalized at construction, so the
element-wise order is a total order that agrees with Equals).
WithVariation and the platform/shaper seams traffic in normalized
coordinates, which are internal after the user-space refactor. Give users
a public entry point that speaks user space and normalizes inside:

- public GlyphTypeface.WithVariations(FontVariationSettings, int?
  instanceIndex) = CreateNormalizedPosition + internal WithVariation
- normalization runs before the cache lookup, so user-space requests that
  clamp to the same axis position share one clone (pinned by test)
- named-instance selection by index rides the same path
A shaper without variation support returns the source's own shaper
typeface from WithVariation, so the clone and its source hold the same
instance. Disposing the clone must leave the source's shaper usable.

- shared instance: the source's shaper must survive the clone's Dispose
- distinct instance: the clone disposes the shaper it owns
ITextShaperTypeface.WithVariation defaults to returning the same
instance, so a clone of a font whose shaper has no variation support
holds its source's shaper typeface. Disposing the clone disposed that
shared instance and left the source with a dead shaping font.

- the clone records whether WithVariation produced a distinct instance
- Dispose releases the shaper only when the clone owns it
@Gillibald
Gillibald force-pushed the pr4b/glyph-typeface-with-variation branch from 43f07e6 to abe5493 Compare September 29, 2026 14:37
@Gillibald Gillibald changed the title [Text] Glyph Outline Part 7/15 - GlyphTypeface variation clones [Text] Glyph Outline Part 7/16 - GlyphTypeface variation clones Sep 29, 2026
@github-actions

Copy link
Copy Markdown

NuGet packages

The packages for this pull request have been published (workflow run):

@Gillibald Gillibald changed the title [Text] Glyph Outline Part 7/16 - GlyphTypeface variation clones [Text] Glyph Outline Part 7/17 - GlyphTypeface variation clones Sep 29, 2026

This branch has not been deployed

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

Labels

api-needs-review The PR adds new public APIs that should be reviewed. area-textprocessing enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants