fix(troika-three-text): survive WebGL context loss during SDF glyph generation#388
fix(troika-three-text): survive WebGL context loss during SDF glyph generation#388AlaricBaraou wants to merge 2 commits into
Conversation
…eneration The atlas.contextLost flag guards entry into glyph generation, but the flag only flips when the async webglcontextlost event dispatches, while context loss itself is synchronous (cap eviction, GPU process reset). Glyphs already in flight reach the atlas with a dead context in two unguarded places: - SDFGenerator's JS-worker fallback writes the worker result into the atlas via webglUtils.renderImageData inside a .then with no rejection handler downstream: one unhandled rejection per in-flight glyph, and the typeset callback never fires, so the Text keeps stale render info even after the context is restored. - TextBuilder's atlas-grow copy (resizeWebGLCanvasWithoutClearing) throws the same way when growth coincides with a lost context. Both writes are now dropped when the context reports lost (and rethrow otherwise); the existing webglcontextrestored handler already regenerates every known glyph, so dropped writes self-heal on restore. In the resize case the canvas dimensions are applied before the copy throws, and the sdfTexture.dispose() still runs, so the texture reallocates at the new size for the restore-time regeneration.
|
This was AI written from a patch I have on a local project. Feel free to close, edit, whatever is convenient / best for you! |
…was refused A null getContext probe in the catch means context creation was refused under the same GPU pressure (a canvas whose webgl creation failed stays in mode 'none', so re-attempts can also fail) - the write is equally impossible and no restore event will ever fire, so rethrowing only keeps the per-glyph rejection storm alive for that variant. The probe now also passes the generator's context attributes so a probe that happens to succeed can't stick a wrong-attribute (non-preserved drawing buffer) context onto the atlas canvas.
|
Follow-up commit: adversarial review of our production integration found two edge cases in the original guard shape, both now fixed here — (1) when context creation was refused (canvas stays in mode 'none' under the same GPU pressure), the null probe used to rethrow, keeping the per-glyph rejection storm alive for that variant; the guard now drops the write in that case too, since no restore event can ever fire for a context that never existed. (2) The catch probe now passes the generator's context attributes, so a probe that happens to succeed can't stick a wrong-attribute (non-preserved drawing buffer) context onto the atlas canvas. Re-verified the full matrix (0.52.4 / main baseline / this branch, Chromium + WebKit) with the deterministic repro from the description. |
|
looks like we see the same problem on our installation on some users ( safari) ( found with sentry ) |
Problem
atlas.contextLost(added with the context-restoration handling) guards entry into glyph generation, but the flag only flips when the asyncwebglcontextlostevent dispatches — while context loss itself is synchronous (WebKit's per-page context cap evicting the oldest context, GPU process resets, tab backgrounding on iOS). Any glyph generation already in flight reaches the atlas with a dead context in two unguarded places:SDFGenerator.js— JS-worker fallback write. The GPU generate path is caught (falls back to the JS worker), but the fallback then writes the worker's result into the same lost WebGL1 atlas canvas viawebglUtils.renderImageData, inside a.thenwith no rejection handler downstream. Result: one unhandled rejection per in-flight glyph, and thePromise.allinTextBuildernever resolves, so the typeset callback never fires — theTextkeeps stale render info even after the context is restored.TextBuilder.js— atlas grow.resizeWebGLCanvasWithoutClearingthrows the same way when growth coincides with a lost context.Which GL call throws first varies by browser build: production Safari throws at
shaderSource(TypeError: Argument 1 ('shader') to WebGLRenderingContext.shaderSource must be an instance of WebGLShader— we collected hundreds of these in Sentry from a page that holds several WebGL contexts), while current WebKit 26.4 and headless Chromium tolerate the null shader and throwError: ANGLE_instanced_arrays not supportedfrom the extension lookup a few calls later. Same path, same consequence in all cases.Fix
Drop the two atlas writes when the context reports lost (rethrow otherwise). This is safe by design: the existing
webglcontextrestoredhandler already regenerates every known glyph into the restored atlas, so dropped writes self-heal. In the resize case the canvas dimensions are applied before the copy-over throws, andsdfTexture.dispose()still runs, so the texture reallocates at the new size for the restore-time regeneration.With the fix, a typeset that overlaps a context loss completes normally (glyphs regenerate on restore) instead of rejecting and stranding the
Text.Verification
Deterministic reproduction (below) forcing the loss into the in-flight window:
gpuAccelerateSDF = falseroutes glyphs through the JS-worker fallback; after the SDF workers' 2s idle termination, a hookedWorkerconstructor loses the atlas context on a microtask right as the fresh SDF worker spawns — i.e. after the whole glyph batch is posted to workers, before any result returns.troika-three-text@0.52.4(npm dist)main, built from sourcemain+ this fix, built from sourcerestoreContext()Each row verified in both headless Chromium and WebKit 26.4 (Playwright).
npx jest: the three failing suites (troika-3d,troika-core,troika-flex-layout) fail identically on unmodifiedmain.Deterministic repro — single file, npm dists via unpkg (serve statically, watch the console)
Pristine 0.52.4 logs the unhandled rejection and
secondSyncDone: false; swap thetroika-three-textimport-map entry for a build with this fix and the same run logs no rejections andsecondSyncDone: true.