Skip to content

WebGL: clearTextureAtlas() on one Terminal corrupts other Terminals sharing the atlas (clearTexture doesn't request a model clear) #6014

Closed

Description

@openwong2kim

Description

When several Terminal instances share one WebGL glyph atlas (which they do automatically whenever their render config matches — same font/theme/DPR — via CharAtlasCache.acquireTextureAtlas), calling WebglAddon.clearTextureAtlas() on one terminal renders the other terminals' text garbled or blank until something forces a full refresh on them (e.g. a resize).

Root cause: TextureAtlas.clearTexture() mutates the shared atlas but does not set _requestClearModel, so the sibling renderers are never told to rebuild their model.

Mechanism (verified against master)

  1. CharAtlasCache.acquireTextureAtlas() returns the same TextureAtlas instance to every terminal whose config is equal — the atlas is shared, only the per-terminal GlyphRenderer is not.
  2. WebglRenderer.clearTextureAtlas() does:
    this._charAtlas?.clearTexture();   // mutates the SHARED atlas
    this._clearModel(true);            // rebuilds only THIS renderer's model
    this._requestRedrawViewport();     // redraws only THIS renderer
  3. TextureAtlas.clearTexture() clears every page and the glyph caches, but does not set _requestClearModel = true.
  4. On the next frame each renderer does:
    if (this._glyphRenderer.value.beginFrame()) { this._clearModel(true); this._updateModel(0, rows - 1); }
    else { this._updateModel(start, end); }
    For a sibling terminal that didn't change, beginFrame() returns false (the flag was never set), so it only updates changed rows. Its unchanged rows keep their old per-cell vertex/texture coordinates, which now point into an emptied/repositioned atlas → garbled or blank glyphs.

The asymmetry is the tell: the page-merge / overflow-page paths in TextureAtlas (_createNewPage, the two this._requestClearModel = true assignments) deliberately set the flag because they mutate the shared atlas and every sharing renderer must rebuild. clearTexture() mutates the shared atlas the same way but skips that signal.

Reproduction

  1. Create two Terminals with identical options and load WebglAddon on each (so they share one atlas).
  2. Write content to both (CJK + box-drawing makes the corruption obvious).
  3. Call clearTextureAtlas() on terminal A only.
  4. Terminal B's unchanged rows render garbled/blank; a resize (full refresh) on B repairs it.

Suggested fix

Have TextureAtlas.clearTexture() set this._requestClearModel = true (mirroring _createNewPage), so every renderer sharing the atlas rebuilds its model on the next frame. Alternatively, WebglRenderer.clearTextureAtlas() could propagate the model-clear to all terminals sharing the atlas.

Environment

  • @xterm/addon-webgl (observed on 0.19.0; mechanism confirmed still present on master)
  • Reproduced in a multi-terminal Electron app where panes share a render config.

Activity

  1. heudev commented on Jul 16, 2026

    @heudev
    Contributor

    Some extra data on this, plus a trigger I don't think has been reported yet.

    Trigger: display switch, not just OS resume. On macOS (Apple Silicon, built-in Retina XDR, VS Code 1.123.0) the integrated terminal garbles or goes blank after moving the window between displays or switching away to another app and back. It's intermittent. The tell is that a newly opened terminal is garbled too — it re-acquires the same cached atlas via configEquals — and only closing the entire terminal panel fixes it, which drops the last owner so CharAtlasCache disposes the atlas and a fresh one gets built. Selecting the text makes it legible again because the selection forces those cells to redraw.

    The diagnosis in this issue no longer matches master. _requestClearModel was removed when #6042 landed (2026-07-13); it's now pageLayoutVersion. WebglRenderer.clearTextureAtlas() also does clear the caller's model already:

    public clearTextureAtlas(): void {
      this._charAtlas?.clearTexture();
      this._clearModel(true);
      this._requestRedrawViewport();
    }

    So the caller is fine. The actual gap is that TextureAtlas.clearTexture() wipes every page and both cache maps but never bumps _pageLayoutVersion — the invalidation channel #6042 added for exactly this class of bug. Page merges (_createNewPage) and evictions both bump it; clearTexture() was missed. Sibling owners therefore never see the change in GlyphRenderer.beginFrame() and keep vertex data pointing into the rows that were just wiped. Once the clearing terminal rasterizes new glyphs into those freed rows, the siblings draw whatever landed there — which is why the corruption looks like other text rather than blank cells.

    This also explains the VS Code report specifically: terminalNativeContribution.ts calls forceRedraw() → Terminal.clearTextureAtlas() on onDidResumeOS, and xtermTerminal.ts exposes the same on other paths.

    Repro, as a test. I extended WebglSharedAtlasGarble.test.ts (the harness from #6042) with a second terminal case. It fails on master — terminal B's header row renders terminal A's colored flood glyphs, max channel diff ~128 against a limit of 14 — and passes with a one-line bump of _pageLayoutVersion in clearTexture(). Full addon-webgl suite (60 tests) and unit tests (2403) stay green.

    PR: #6055

    Note that #6018 sets this._requestClearModel = true, a field that no longer exists post-#6042, so it can't build against master as-is.

  2. openwong2kim commented on Jul 27, 2026

    @openwong2kim
    ContributorAuthor

    🫡

  3. self-assigned this
    on Jul 27, 2026
  4. added this to the 7.0.0 milestone on Jul 27, 2026
  5. 0xFlo commented on Aug 22, 2026

    @0xFlo

    Note for anyone landing here from a stable install: the fix for this (#6055, merged 2026-07-27) has not shipped in a stable release. npm latest for @xterm/addon-webgl is still 0.19.0, published 2025-12-22, so the atlas fixes exist only in 0.20.0-beta.*.

    Upgrading is also not a cheap addon bump — 0.20.0-beta.298 hard-peer-deps @xterm/xterm ^6.1.0-beta.301, so you move the core terminal off 6.0.0 stable too.

    Userland mitigation on 0.19.0, if you cannot take the beta: stop calling clearTextureAtlas() yourself. The atlas is shared across every terminal with equal font/theme config (CharAtlasCache, ownedBy: Terminal[]), so an explicit clear from one terminal has cross-pane blast radius. In my case (Electron embedder, several visible terminals) two call sites were doing it — one on pane activation, one after loading the WebGL addon — which produced a ~1s glyph rebuild on the focused pane plus garbled cells in untouched siblings. Removing both fixed the explicit-clear half of this on stable. Config changes acquire a fresh atlas from the cache by key anyway, so an explicit clear is rarely doing useful work.

    Two honest limits on that workaround:

    1. It only covers clears you trigger. Internal atlas page merges (Fix stale rendering after shared texture atlas page merges #6042) remain exposed on 0.19.0 with no userland fix — if you see this symptom without any clearTextureAtlas() call of your own, that is the case you are in, and the beta is the only route.
    2. If the garbling comes with Too many active WebGL contexts. Oldest context will be lost. in the console, it is not this issue — that is WebglAddon.dispose() does not release the underlying WebGL2 context #6068 (dispose never releases the GL context), which the atlas fixes do not address and which needs its own separate workaround. The two look nearly identical: correct cell geometry, wrong glyph contents, on a terminal you did not touch. I hit both simultaneously on 0.19.0 and each needed its own fix; there is a triage table in #6068 for telling them apart.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions