Skip to content

webgl: stop the link render layer from acquiring a texture atlas - #6156

Open
andylizf wants to merge 1 commit into
xtermjs:masterfrom
andylizf:fix/webgl-link-layer-atlas-thrash
Open

webgl: stop the link render layer from acquiring a texture atlas#6156
andylizf wants to merge 1 commit into
xtermjs:masterfrom
andylizf:fix/webgl-link-layer-atlas-thrash

Conversation

@andylizf

@andylizf andylizf commented Sep 6, 2026

Copy link
Copy Markdown

Summary

BaseRenderLayer in the WebGL addon acquires a texture atlas on every resize, colour change and transparency change, passing a hard-coded 2048 as the device max texture size. Nothing in the layer reads that atlas: LinkRenderLayer, its only subclass, draws underlines with fillRect and never touches _charAtlas.

Since #5929 added deviceMaxTextureSize to configEquals, that acquisition stopped being a cache hit. On any GPU whose MAX_TEXTURE_SIZE is not 2048, the layer's config never equals the renderer's, so acquireTextureAtlas:

  1. finds the terminal owning the real atlas, sees a differing config, and releases it — calling atlas.dispose() when the terminal is the sole owner;
  2. builds a fresh atlas with deviceMaxTextureSize: 2048 and warms it up;
  3. is then called again from WebglRenderer._refreshCharAtlas with the real size, which disposes the 2048 atlas and rebuilds the real one.

WebglRenderer.handleResize calls layer.resize() before its own _refreshCharAtlas(), so every terminal resize pays this in full. Embedders that resize on tab switch (VS Code does, via TerminalInstance._resizexterm.resize) pay it on every tab switch.

Each cycle discards two atlases' worth of 512×512 page canvases and a warm-up of 93 glyphs. On a VS Code session with ~30 CJK-heavy terminals, the GPU process's IOSurface count climbs with tab switches and heavy output and only comes back on a full atlas evict; this PR removes one of the sources feeding that.

Changes

  • BaseRenderLayer.ts: remove _charAtlas, _refreshCharAtlas and its three call sites (constructor colour listener, _setTransparency, resize), plus the now-unused imports. −24 lines, no behaviour change for the link layer, which never used the atlas.
  • test/WebglLinkLayerAtlas.test.ts: two Playwright tests that tag the renderer's atlas and wrap its dispose, then assert the same instance survives (a) four resizes and (b) an unchanged theme reassignment, with dispose never called. Both fail on master with the atlas replaced.

Testing

  • npm run test-unit: 2403 passing
  • npm run lint: clean
  • npm run test-integration --suite=addon-webgl (Chromium): 64 passed, 9 skipped (Firefox/WebKit projects)
  • A/B on the new tests: 2 failed on master, 2 passed with this change

Why this is low risk

The removed code produced a value nothing consumed. acquireTextureAtlas / removeTerminalFromCache pairing stays with WebglRenderer, which already owns both sides. LinkRenderLayer keeps its own canvas and its onChangeColorsreset() path.

Related: #5929 (made the mismatch observable), #6074 (atlas growth from per-cell background colours, a separate source), microsoft/vscode#329118 (maintainer trace attributing renderer load to rapid atlas invalidation).

BaseRenderLayer acquired a texture atlas on every resize, colour change and
transparency change, passing a hard-coded 2048 as the device max texture size.
Nothing in the layer ever read that atlas: LinkRenderLayer, its only subclass,
draws underlines with fillRect and never touches _charAtlas.

Since xtermjs#5929 made deviceMaxTextureSize part of atlas cache equality, that call
stopped being a harmless cache hit. On any GPU whose MAX_TEXTURE_SIZE is not
2048, the layer's config never equals the renderer's, so acquireTextureAtlas
released the terminal from its real atlas -- disposing it outright when the
terminal was the sole owner -- and built a 2048 one, after which the renderer's
own refresh disposed that and rebuilt the real one. WebglRenderer.handleResize
calls the layer's resize before its own atlas refresh, so every terminal resize
paid this twice over, and embedders that resize on tab switch (VS Code does)
paid it on every tab switch.

Remove the acquisition. The two integration tests pin the renderer to the same
atlas instance across resizes and across an unchanged theme reassignment; both
fail on the previous code with the atlas replaced.
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.

1 participant