Skip to content

fw/graphics: consult the font's other own resource when the routed glyph lookup misses#1771

Open
vvzvlad wants to merge 1 commit into
coredevices:mainfrom
vvzvlad:fix/langpack-glyph-routing
Open

fw/graphics: consult the font's other own resource when the routed glyph lookup misses#1771
vvzvlad wants to merge 1 commit into
coredevices:mainfrom
vvzvlad:fix/langpack-glyph-routing

Conversation

@vvzvlad

@vvzvlad vvzvlad commented Jul 22, 2026

Copy link
Copy Markdown

Fixes #1709.

When a language pack is loaded, a glyph that exists in the pack's extension font can still render as the missing-glyph box — and, in the mirror case, a glyph that exists in the base font can degrade to the 14px system fallback. ℃ (U+2103) from the issue is one instance of a whole class.

№ U+2116, ₽ U+20BD — before after
before: numero and ruble signs render as the missing-glyph box after: numero and ruble signs render from the language pack

Mechanism

prv_font_res_for_codepoint() picks base vs extension purely from the codepoint's Unicode class: codepoint_is_latin() (0x0000–0x02AF and 0x2000–0x2BFF) → base, everything else → extension. The pick happens before any glyph-presence check, and the fallback chain in prv_get_glyph() never revisits the other resource of the same font — a miss goes straight to the process-global system fallback and then the wildcard box.

So the classification acts as a hard partition, when in reality it's only a guess about which resource carries the glyph. Both directions of a wrong guess are live bugs:

  • Latin-classified codepoint that only the extension carries. 0x2000–0x2BFF spans ~2800 codepoints; the base Gothic fonts carry ~34 of them (dashes, quotes, ellipsis, €, ™, some math). Everything else in that block — letterlike symbols (℃ ℉ №), currency (₽ U+20BD, added to Unicode after the base fonts were cut), arrows, geometric shapes — is force-routed to base, misses, and renders as the wildcard box even when the loaded language pack provides the glyph. This is U+2103 (℃) in a language-pack extension is ignored by base-font routing #1709, reproduced independently by a Japanese pack (℃) and a Russian pack (№, ₽ — screenshots above).
  • Non-latin codepoint that only the base carries. With a pack loaded, π (U+03C0) and the spacing modifiers U+02C6–02DD classify as non-latin → routed to the extension. Packs typically don't carry them, so codepoints that render fine on a stock firmware degrade to the 14px system fallback (or the box) the moment a pack is installed. Installing a language pack should not break glyphs that worked without it.

The fix

prv_get_glyph_in_font() — the single-font leaf resolver — now treats classification as a lookup-order hint instead of a partition: try the routed resource first; on a miss, try the font's other own resource (base ↔ extension) before giving up. The outer fallback chain (primary → system fallback → wildcard) is untouched, and the leaf still never consults the fallback font, so the depth-1 invariant holds as documented.

Scoping decisions:

  • Guarded on owner == font_info: when the emoji font takes over, the routed resource belongs to a different FontInfo, and the rescue is skipped — the emoji substitution and baseline-adjust behavior from fw/graphics: fix tiny, top-aligned emoji at the largest text size #1766 is unchanged. The two compose: that PR places glyphs that legitimately come from another font; this one stops base-present glyphs from being outsourced at all.
  • No baseline adjustment needed: the rescue stays within the same FontInfo, so prv_baseline_adjust() yields 0 by construction — extension glyphs are already baked against their base font's own baseline.
  • Precedence is unchanged: when both resources carry the codepoint, the routed one still wins, exactly as today. Behavior changes only on paths that previously ended in the system fallback or the wildcard box, where showing the font's own glyph is strictly better.
  • Strict no-op without a language pack: every new path is gated on font_info->extended (or on being routed to the extension, which requires it). A firmware without a loaded extension resolves byte-for-byte as before.

The alternative — narrowing the 0x2000–0x2BFF "latin" range in codepoint_is_latin() — was rejected: the base fonts genuinely carry glyphs across that block (U+2013/2014, U+2018–201E, U+2026, U+20AC, U+2122, U+2212, …), and reclassifying them would regress each one to a pack lookup that packs don't cover. The nonstandard-emoji handling in the 0x25xx block also leans on the current classification.

Cost: one extra offset-table probe, only on lookups that missed their routed resource — i.e. paths that were already headed for the (more expensive) fallback-font lookup. The font cache negative-caches misses as before.

Tested

./pbl build (obelix) and the full ./pbl test suite pass.

Unit tests added in test_text_resources.c, all through the real resolver against the shipped PBF fixtures (no fixture regeneration needed):

  1. base-routed miss rescued from the extension (the U+2103 (℃) in a language-pack extension is ignored by base-font routing #1709 direction): the CJK extension fixture's only latin-classified codepoint is U+2026, and the shipped GOTHIC_18_EMOJI base font lacks it — pairing them makes U+2026 route to base, miss, and resolve from the extension. The resolver keys only on glyph presence and codepoint class, so this exercises exactly the issue's path.
  2. extension-routed miss rescued from base (mirror direction): π U+03C0 with GOTHIC_18 + CJK extension resolves to the base π glyph at native size, baseline adjust 0.
  3. Regression guards: U+2026 present in both resources resolves to the base bytes (routed resource wins; the two fixtures' ellipsis bitmaps genuinely differ), and a codepoint absent everywhere still yields the primary font's wildcard box.

Both rescue tests fail without the resolver change (verified by disabling the rescue block and re-running: got wildcard/fallback bytes instead of the expected glyphs).

On-device (obelix@pvt): a Russian language pack whose extension carries № (U+2116) and ₽ (U+20BD) — both absent from every base Gothic font — previously rendered both as the wildcard box; with this change they render from the pack (screenshots above). Latin text, base punctuation (— … « »), and emoji substitution are visually unchanged.

… misses

prv_font_res_for_codepoint() picks base vs extension purely from the
codepoint's Unicode class, before any glyph-presence check, and a miss
never revisited the other resource of the same font. With a language
pack loaded this breaks both ways: latin-classified codepoints
(0x2000-0x2BFF: U+2103, U+2116, U+20BD) present only in the pack's
extension render as the wildcard box, and base-only non-latin
codepoints (U+03C0, U+02C6-02DD) degrade to the 14px system fallback.

Make classification a lookup-order hint instead of a hard partition:
prv_get_glyph_in_font() tries the routed resource first and, on a miss,
retries the font's other own resource. The rescue is skipped when the
emoji font took over (owner != font_info), so emoji substitution and
baseline adjustment are unaffected. The rescue stays within the same
FontInfo, so the baseline adjust is 0 by construction. When a glyph
exists in both resources the routed one still wins. Without a loaded
extension the change is a strict no-op.

Cost: one extra offset-table load, only on lookups that missed their
routed resource, i.e. paths that already headed for the fallback font.
Side effect: an emoji-classified codepoint with no system emoji font
available can now also be rescued from the extension, which is the
desired outcome for packs carrying 0x2B00-0x2BFF glyphs.

Fixes coredevices#1709

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Vlad Zaytsev <git@vvzvlad.xyz>
@vvzvlad
vvzvlad requested review from gmarull and jplexer as code owners July 22, 2026 00:25
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.

U+2103 (℃) in a language-pack extension is ignored by base-font routing

1 participant