* feat(terminal): add flag-gated render-desync sentinel for WebGL panes
Detects the buffer-clean/render-stale glyph garble class in the field: per
visible WebGL pane, compare the cells the xterm buffer says hold glyphs
against the ink actually present on the canvas, sampled in the same task as
a forced synchronous redraw so a divergence proves the render model/atlas is
wrong rather than a missed present. A trip requires the same screen cells to
stay divergent across three samples (real desync is pinned; scroll lag moves),
then records a webgl-render-desync breadcrumb, stashes evidence (canvas PNG +
buffer text) for bug reports, and runs the same shared-atlas recovery a tab
reveal performs, so a stuck-garbled pane self-heals within seconds.
Off by default; arm on any build via
localStorage.setItem('orca:render-desync-sentinel', '1') and reload.
* fix(terminal): invalidate glyph cache on atlas replacement
Reproduce the WebGL atlas identity mismatch with two live terminals and force cached geometry to rebuild whenever a different shared atlas is attached. Persist flag-gated render-desync evidence and retain the investigation tooling used to validate the field signature.
* fix(terminal): harden render desync diagnostics
* docs(reliability): clarify Linux WebGL evidence gap
* fix(terminal): keep WebGL glyph atlas pages within the shader sampler budget
The fragment shader has sampler slots for maxAtlasPages (16 on most Macs)
and leaves outColor uninitialized for any higher page index, so glyphs
rasterized onto pages past the budget render as garbled pixels. Long
sessions grow past the budget via the merge fallback, and the previous
wipe fix re-activated those unbindable pages, so every atlas wipe
re-allocated glyphs onto them (post-wipe allocation prefers the last,
highest-index active page) and garbled whole panes mid-stream.
Fix, matching the direction xterm.js maintainers are pursuing upstream
(xtermjs/xterm.js#6043): a shared _evictAllPages resets the atlas to one
fresh page, called from clearTexture and from the two allocation paths
that could otherwise push a page past the budget (merge fallback and
oversized-glyph page creation), so the page count can never exceed the
renderer's texture capacity. Defensive backstops: a one-time warn plus
bind-loop clamp, and an else branch in the generated shader so an
unexpected overflow renders blank instead of undefined pixels.
* test(terminal): cover WebGL atlas sampler budget
* fix(terminal): align WebGL atlas invalidation source
Blank context lines in the @xterm/addon-webgl patch had trailing spaces,
which made git diff --check fail. Strip only that whitespace and refresh
the pnpm patchedDependencies hash so the lockfile stays consistent.
TextureAtlas.clearTexture() guarded on pages[0].currentRow being at the
origin — but merged pages are never written through currentRow, so once a
page merge lands a merged page at index 0, every atlas wipe for the rest
of the session silently no-ops: no content clear, no glyph-cache clear,
no clear-model generation bump. Recoveries degrade to placebo, the glyph
cache grows unboundedly, and the session drifts into a merge-heavy state
where stale vertex data paints the tiny-glyph garble seen after
unfocus-then-paste into Claude Code.
Fixes in @xterm/addon-webgl patch (lib bundles + readable src):
- clearTexture(): guard on any-page-has-glyphs; restore _activePages
after wiping (merges permanently shrink the active set)
- AtlasPage.clear(): reset _glyphs/_usedPixels so wiped pages stop
distorting merge-candidate selection
- renderRows merge-retry loop: check the retry budget before
beginFrame() so tripping the limit cannot consume the clear-model
notification and strand a stale model
- GlyphRenderer.render(): clamp texture binds to the sampler budget so
pages overflowing maxAtlasPages cannot TypeError and kill rendering
Verified: two-terminal shared-atlas harness (storm scenario proves the
no-op and its fix; 6-seed adversarial fuzzer clean), 405 unit tests in
pane-manager/atlas-recovery suites, terminal-rendering golden e2e.
Patch @xterm/addon-webgl so clearTexture() and page merge/delete operations
request a model clear and bump a generation counter. Without this, the renderer
keeps drawing glyphs against a stale atlas after the texture is reset, leaving
garbled or blank cells until the next full repaint.
Co-authored-by: Orca <help@stably.ai>