Decode GPOS context positioning and mark-to-base subtables once per document - #323
Open
jakejackson1 wants to merge 4 commits into
Open
jakejackson1 wants to merge 4 commits into
jakejackson1 wants to merge 4 commits into
Conversation
The fixtures for GPOS Lookup Types 7 and 8 carry a handful of glyphs each: enough to pin one rule, too few to write a paragraph in, so a document could not be timed through them. These four cover a whole script each, and a run of ordinary text reaches the lookups glyph after glyph: - Noto Sans Gurmukhi UI 2.001, cut down to Gurmukhi: Type 7 Format 1 and Type 8 Format 2 - Noto Sans Sharada 2.002, cut down to Sharada: Type 7 Format 2 and Type 8 Format 2 - Noto Sans Takri 2.005, cut down to Takri: six Type 8 Format 1 lookups - Noto Sans cut down to printable ASCII, with a GPOS of two Type 7 Format 3 subtables written for it, since no font surveyed carries that format ContextualPositioningTextTest pins one run per format in which the lookup moves a glyph. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GPOS Lookup Type 7 read each subtable from the font every time a glyph its Coverage table matched was offered to it: the rule set offsets, then each rule and its glyph or class list, then for Format 2 the ClassDef and its class 0 exclusions, then for Format 3 the Coverage offsets and sets. Type 7 has the layout of GSUB Type 5, so each format now shares its decoder with the matching substitution format: plainRuleSet for Format 1, plainClassContext and plainClassRules for Format 2, plainCoverageContext for Format 3. The entries are kept under the font's GPOS key, apart from its GSUB ones. Format 1 had not tested the first glyph's Coverage Index against the rule set count, and read past the offset array for a font whose Coverage table is longer than it. plainRuleSet finds no rule set there, as GSUB Type 5 always has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…per document GPOS Lookup Type 8 Formats 1 and 2 read each subtable from the font every time a glyph its Coverage table matched was offered to it. Format 3 has been decoded once since it was given the decoder GSUB Type 6 Format 3 uses. The other two formats now share theirs too: chainedRuleSet for Format 1, and chainedClassContext and chainedClassRules for Format 2. Where GPOS read a null rule set offset as a glyph that begins no context and GSUB followed it, each still does, so chainedRuleSet is told which. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GPOS Lookup Type 4 read its header from the font every time a mark its Coverage table matched was offered to it, and for each mark it attached, the mark's record, the base's anchor offset and the anchor itself. The header is now decoded once per subtable (markToBase), keeping the base Coverage table as coverageIndexByHex() already did. The mark record (markRecord) and the base's anchor for each mark class (baseAnchor) are decoded the first time they are asked for. Types 5 and 6 have the same layout and are left as they were: decoding them the same way made no difference that could be measured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #314. That PR decoded most OTL subtables once per document through
LuDataCache, but it left out three things: GPOS 7 (all formats), GPOS 8 formats 1 and 2, and mark attachment. The GPOS formats were skipped because no bundled font has them, so they could not be timed or checked against a real font. Mark attachment was under 4% of the profile. This PR adds fixtures for those formats, converts them, and converts mark-to-base. There is one commit for the fixtures and one per lookup type.What was slow
What changes
plainRuleSet,plainClassContext+plainClassRules,plainCoverageContext. The layout is the same.chainedRuleSet,chainedClassContext+chainedClassRules. 8.3 already sharedchainedCoverageContext.markToBase(the header, plus the base Coverage table ascoverageIndexByHex()already caches it),markRecord, andbaseAnchor(per base, per mark class, filled on first use)checkContextMatch,checkContextMatchMultiple,_applyGPOSlookupRecords) are not changed. When a rule matches, the reader is seeked to its lookup records, as Decode each OTL subtable once per document #314 does for GSUB./GPOSkey, so they never collide with the GSUB entries from the same decoders.chainedRuleSetnow takes$followNull, which only the GSUB caller passes, so each keeps its old behaviour.plainRuleSet, which finds no rule set there, as GSUB 5.1 always has. No font I tested reaches that case.applyOTLchanged by −1% to +1% on hebrew-niqqud, thai, khmer, arabic-cursive, myanmar and syriac. Both changes were dropped.CACHE_FORMATbump.Fixtures
The four fonts are Noto, SIL OFL 1.1. Noto reserves no font name. Each subset keeps its name and licence strings. Each was cut down with fontTools to one script plus the space, the danda, the joiners and the dotted circle, with hinting dropped. The provenance is also in the docblock of
ContextualPositioningTextTest.NotoSansGurmukhiUI-SubsetNotoSansSharada-Subsetmkmk,dist), 8.2NotoSansTakri-SubsetNotoSans-ContextCoverage-Syntheticdistlookup with two 7.3 subtables: vowel + consonant, and consonant + consonant + vowel._applyGPOSsubtableoffers per type and format in an instrumented copy. For each format I also rendered a run with that lookup switched off to confirm the lookup is what moves the glyph. Those runs are pinned inContextualPositioningTextTest.fontcache,otldump,shaping,subset) were written before any conversion. They still hold after it.Performance
compare.sh, time insideOtl::applyOTL, interleaved cold-base / cold-new / hot-base / hot-new, one PHP process per run. My copy of the harness adds one scenario per fixture font (ctx-*): 60 paragraphs of four repeats of that script's text.Against
gravitypdf(9d585a1), N=10:total) moves by the same milliseconds: ctx-sharada 3419 → 1111 ms cold (−67%), ctx-takri 917 → 748 ms (−18%).Per commit, N=10, each commit measured against the one before it:
Where the time goes now. I profiled on #314 before converting (Xdebug, hot cache). The whole of GPOS was 15–25% of shaping in hebrew-niqqud, 8% in myanmar, 15% in hindi (pair adjustment), 3% in tibetan and 7% in arabic. Mark attachment was 5% inclusive at most (hebrew-niqqud: mark-to-base 3.6%, mark-to-mark 1.4%), and under 1.5% everywhere else. That is why the gain above is small.
GSUBsubstituteand ligature substitution still lead every profile.Cumulative against
/Users/jakejackson/Sites/mpdf-perf-baseline(8c443ff, before #314), N=5,applyOTL:This baseline also predates #311–#313 and #321, so the table is the combined effect of everything merged since 8c443ff, not of this PR alone. Hot runs track cold. Hindi is left out because its baseline cells were at ±64% and ±48%.
Output
kern. That covers every bundled OTL font plus every font intests/data/ttf: 89 fonts, 15,636 runs. The hash of the returned string plus serialisedOTLdata(includingGPOSinfo) is identical togravitypdffor every run. I also checked it after each commit.creationDate,exposeVersionoff, compression off and a warm cache: the fourctx-*, hebrew-niqqud, myanmar, tibetan, hindi, arabic, arabic-cursive, thai, khmer, syriac, latin-otl, latin-nootl, telugu, kannada, bengali, sinhala, ethiopic and multilingual-auto. All are byte-identical.Tests
DecodedSubtableTestgains six cases, each checking a cold decode followed by a cached decode: same result, bucket filled, nothing decoded on the second pass.baseAnchorContextualPositioningTextTestis new. It pins one run per format where the lookup moves a glyph, on the new fixtures.composer test,composer csand PHPStan all pass.Left out
Uninitialized string offsetwarnings fromFileReader::readUInt16. That bug is separate from this PR and not fixed here.🤖 Generated with Claude Code