fix(terminal): gate the WebGL attach on the font instead of clearing the atlas (#70)#100
Conversation
|
@Orellius it happens on non-retina display, and gets fixed if I move once the window between retina and non retina displays, just like it gets fixed when i change the font. |
) The bundled JetBrains Mono is a lazy @font-face and xterm's WebGL atlas keys glyphs per (char,fg,bg,ext) with no font in the key, so a glyph rasterized before the face is active caches at the fallback height and never corrects (selecting text re-keys it, which is why that "fixed" it). The prior gate polled document.fonts for the family, but on WKWebView check() returns true and load() resolves with zero faces for a family not yet REGISTERED in the FontFaceSet (vacuous-true). fontsource registers "JetBrains Mono" via async CSS @font-face, so that pre-registration window is the poison window on a cold spawn: the gate passes, the atlas builds against the fallback, and the mixed-height waves stick. Own the latin 400/700 faces as explicit FontFace handles (lib/terminalFontReady), registered and load()-awaited at boot. Gate the WebGL attach and the PTY spawn on those real handles instead of document.fonts. The atlas is then built against a genuinely-loaded face, so nothing is ever cleared under a live renderer. Also drops the 800ms race + poller. tsc + suite + build green.
|
Thanks, that detail nailed it: "heals when I move between displays, just like changing the font" means this is transient atlas poisoning, not a DPR rendering bug. A display round-trip that ends back on the same non-retina DPR would reproduce a DPR-deterministic bug, but it heals instead, because any atlas rebuild (display move or font change) clears it. And the root cause is different from what this PR gated on. The gate used So I reworked it to stop trusting Validated in WKWebView: the old |
494f8c9 to
444da44
Compare
|
@simion pushed the reworked fix ( |
|
Merged. What I tested: the race never repros in dev naturally (the fontsource CSS sits in the ES module graph, so the family always registers before app code), so I forced the production window deterministically: pulled the four jetbrains-mono @imports out of index.css and injected the @font-face rules 3s after boot from a plain script in index.html. On main that reproduces #70 on demand, and worse than any cold launch ever showed: every glyph rasterized before the late registration keeps the fallback height forever, output after 3s comes out correct, selection heals per-cell. On this branch, same delay, clean from the first glyph: the owned handles fetch the woff2 directly and the WebGL attach waits for them, so the CSS timing stops mattering entirely. That confirms the vacuous check() diagnosis end to end. Not cutting a release yet. I'll run this locally for two days on the machine that hits the cold non-retina spawn, then ship it. |




What
Fixes #70 (character heights render wrong until an atlas rebuild). This supersedes the earlier
document.fonts.check()-based approach on this branch, which @simion confirmed still poisoned the atlas on a cold non-retina spawn.Root cause (reproduced on WKWebView)
xterm's WebGL atlas keys glyphs per
(char, fg, bg, ext)with no font in the key, so a glyph rasterized before the real face is active caches at the fallback height and never corrects until the cell re-rasterizes (selecting text re-keys it, which is why that "fixed" it).The gate meant to prevent that used
document.fonts.check()for "JetBrains Mono". On WKWebView I reproduced thatcheck()(andload()) report a family READY before it is registered in the FontFaceSet:check('400 1em "JetBrains Mono"')returnstrue, andload()resolves with zero faces, for an unregistered family (vacuous-true). fontsource registers the family via async CSS@font-face, so that pre-registration window is the poison window on a cold spawn: the gate passes, the atlas builds against the fallback, heights stick.check()is honest for a registered face mid-load, so it only bites before registration, which is the cold-resume timing.The "heals on a display move, just like changing the font" report confirms this is transient atlas poisoning, not a DPR rendering bug: a round-trip ending back on the same non-retina DPR would reproduce a DPR-deterministic bug, but it heals, because any atlas rebuild clears it.
Fix
Stop trusting
document.fontsstatus. Own the latin 400/700 faces as explicitFontFacehandles (src/lib/terminalFontReady.ts), registered andload()-awaited at boot. Gate the WebGL attach (loadTerminalRenderer) and the PTY spawn (awaitTerminalFonts) on those real handles instead ofdocument.fonts.check(). The atlas can only build once the exact face it rasterizes with is genuinely loaded, so nothing is ever cleared under a live renderer (no scramble). Drops the 800ms race + poller.The owned faces duplicate the fontsource latin
@font-facerules (same family/weight/URL, so the byte fetch is shared). That is intentional and sound: xterm rasterizes glyphs through Canvas 2D, which draws only with FontFaceSet faces that are actually loaded and ignores unloaded ones, so once the awaited handles load the atlas rasterizes against the real face regardless of the CSS faces' state. Cyrillic stays on the CSS@font-face(terminals are overwhelmingly latin).Validation
checkgate returnstruein the pre-registration window (would attach early); the owned-handle gate staysfalseuntil the face genuinely loads, thentrue.tsc -b+ test suite +vite buildall green.I couldn't force the exact cold non-retina spawn on a warm dev instance (a warm instance can't be forced cold), so a retest on the affected machine is the real confirmation.