I found that getting advances of a text takes unnecesasry long, due to HarfBuzz's works (HarfBuzzShaper.Shape).
This is a real world issue in my GUI framework (guigui-gui/guigui#390), when putting a big (10M or so) text data in a text input, as this needs to calculate text advances to show appropriate text glyphs.
Claude Code helped me to make this proposal, and I'll paste the summary as following:
Title: Add an advance-only shaping path that skips per-glyph GlyphExtents
Body:
Summary
shaping.HarfbuzzShaper.Shape unconditionally fills each glyph's Width, Height, XBearing, and YBearing by calling font.Face.GlyphExtents once per glyph. Callers that only need the total advance (text measurement, line-wrapping width queries, etc.) have no way to opt out, even though the advance data is already produced by harfbuzz.Buffer.Shape in buf.Pos[i].XAdvance / YAdvance.
For variable fonts this is expensive: every GlyphExtents miss flows through getExtentsFromGlyf → getGlyfPoints → gvar.applyDeltasToPoints. The cost is amplified because Face.SetCoords (called every time SetVariations runs) resets the extentsCache, so back-to-back shape calls usually start cold.
Profile evidence
CPU profile from a text-editor workload (Measure calling Advance per visible line) on a variable font:
github.com/hajimehoshi/ebiten/v2/text/v2.(*GoTextFace).advance 3.75s 65% cum
shaping.(*HarfbuzzShaper).Shape 2.25s 39%
harfbuzz.(*Buffer).Shape 0.40s 7% ← actual layout
font.(*Face).GlyphExtents loop 1.74s 30% ← unused by advance
font.(*Face).getGlyfPoints 1.26s 22%
font.gvar.applyDeltasToPoints 0.42s 7%
GlyphExtents accounts for ~30% of total CPU on this profile and is dead work for the caller.
Workaround
Drop to harfbuzz directly: cache a *harfbuzz.Font and *harfbuzz.Buffer, replicate the positioning prelude of HarfbuzzShaper.Shape (segmenter run handling, sideways axis switch, font scale, feature array), call Buffer.Shape, then sum Pos[i].XAdvance/YAdvance. This works but duplicates ~50 lines of HarfbuzzShaper.Shape's setup, and the duplication is fragile against upstream changes (e.g. how sideways advances are signed).
Proposed API
A way to ask HarfbuzzShaper to skip per-glyph extent population. A few options, in rough order of preference:
- New method:
(*HarfbuzzShaper).ShapeAdvance(Input) fixed.Int26_6 — returns just the total advance, with the same direction/sideways semantics Output.Advance would have.
- Flag on
Input (or on HarfbuzzShaper): e.g. Input.SkipGlyphExtents bool. Shape returns the same Output but with Width/Height/XBearing/YBearing left zero. Output.Advance and Glyph.Advance are still populated. Less invasive but couples the contract to a field.
- New method returning a stripped
Output: e.g. ShapeNoExtents(Input) Output. Cheaper than (1) for callers that also want per-glyph advances, but more API surface than (2).
Option (1) is the simplest in usage and makes the cost difference explicit at the call site. Option (2) reuses existing types and integrates with Output.RecomputeAdvance callers.
Notes
- The advance data is already present in
buf.Pos[i] after Buffer.Shape, so the implementation is mostly "skip the extents loop, return early."
- Sideways vertical text needs the same sign convention
Output.sideways() applies (per-glyph advance negated), so ShapeAdvance should return Output.Advance semantics, not raw Pos.XAdvance sums.
I found that getting advances of a text takes unnecesasry long, due to HarfBuzz's works (HarfBuzzShaper.Shape).
This is a real world issue in my GUI framework (guigui-gui/guigui#390), when putting a big (10M or so) text data in a text input, as this needs to calculate text advances to show appropriate text glyphs.
Claude Code helped me to make this proposal, and I'll paste the summary as following:
Title: Add an advance-only shaping path that skips per-glyph
GlyphExtentsBody:
Summary
shaping.HarfbuzzShaper.Shapeunconditionally fills each glyph'sWidth,Height,XBearing, andYBearingby callingfont.Face.GlyphExtentsonce per glyph. Callers that only need the total advance (text measurement, line-wrapping width queries, etc.) have no way to opt out, even though the advance data is already produced byharfbuzz.Buffer.Shapeinbuf.Pos[i].XAdvance/YAdvance.For variable fonts this is expensive: every
GlyphExtentsmiss flows throughgetExtentsFromGlyf→getGlyfPoints→gvar.applyDeltasToPoints. The cost is amplified becauseFace.SetCoords(called every timeSetVariationsruns) resets theextentsCache, so back-to-back shape calls usually start cold.Profile evidence
CPU profile from a text-editor workload (
MeasurecallingAdvanceper visible line) on a variable font:GlyphExtentsaccounts for ~30% of total CPU on this profile and is dead work for the caller.Workaround
Drop to
harfbuzzdirectly: cache a*harfbuzz.Fontand*harfbuzz.Buffer, replicate the positioning prelude ofHarfbuzzShaper.Shape(segmenter run handling, sideways axis switch, font scale, feature array), callBuffer.Shape, then sumPos[i].XAdvance/YAdvance. This works but duplicates ~50 lines ofHarfbuzzShaper.Shape's setup, and the duplication is fragile against upstream changes (e.g. how sideways advances are signed).Proposed API
A way to ask
HarfbuzzShaperto skip per-glyph extent population. A few options, in rough order of preference:(*HarfbuzzShaper).ShapeAdvance(Input) fixed.Int26_6— returns just the total advance, with the same direction/sideways semanticsOutput.Advancewould have.Input(or onHarfbuzzShaper): e.g.Input.SkipGlyphExtents bool.Shapereturns the sameOutputbut withWidth/Height/XBearing/YBearingleft zero.Output.AdvanceandGlyph.Advanceare still populated. Less invasive but couples the contract to a field.Output: e.g.ShapeNoExtents(Input) Output. Cheaper than (1) for callers that also want per-glyph advances, but more API surface than (2).Option (1) is the simplest in usage and makes the cost difference explicit at the call site. Option (2) reuses existing types and integrates with
Output.RecomputeAdvancecallers.Notes
buf.Pos[i]afterBuffer.Shape, so the implementation is mostly "skip the extents loop, return early."Output.sideways()applies (per-glyph advance negated), soShapeAdvanceshould returnOutput.Advancesemantics, not rawPos.XAdvancesums.