Skip to content

Fix webgl rendering corruption from atlas page merges - #5883

Merged
Tyriar merged 2 commits into
xtermjs:masterfrom
vsych:fix-webgl-atlas-merge-stale-vertices
May 21, 2026
Merged

Tyriar merged 2 commits into
xtermjs:masterfrom
vsych:fix-webgl-atlas-merge-stale-vertices

Conversation

@vsych

@vsych vsych commented May 17, 2026 •

Copy link
Copy Markdown
Contributor

Two bugs in the webgl renderer cause the corruption from #5847.

1. Texture binding stale after a same-index page swap. GlyphRenderer.render only rebinds a texture unit on version mismatch, but after _createNewPage merges 4 pages into 1, the page object at the merged index swaps to a fresh mergedPage. With per-page counters the new page's version often coincided with the version we last bound at that index (the freshly-pushed mergedPage always landed on 1). The rebind was skipped and the texture kept the old page's canvas, so glyphs sampling that unit got garbage.

Make AtlasPage.version monotonic across all pages and the existing comparison detects a same-index swap, no extra bookkeeping required.

2. Stale texturePage in the vertex buffer after a mid-update merge. When a merge fires inside _updateModel, cached glyphs get a new texturePage. Cells already written in this pass (or carried over via the model-unchanged early-exit) still point at the pre-merge index. _requestClearModel was set on merge but beginFrame never reset it, so it stayed true forever; and renderRows only checked it before _updateModel, so a mid-pass merge went unhandled.

  • reset _requestClearModel when beginFrame reads it
  • after _updateModel, re-run a full update if a merge fired during the pass (capped at MERGE_RETRY_LIMIT)

Before:
issue
After:
image

Stress repro with the script from #5847 (full-screen rewrite, many colors, frequent merges):

Before this PR:

repro-master-no-fix

After this PR:

repro-with-fix

@vsych
vsych force-pushed the fix-webgl-atlas-merge-stale-vertices branch from e7ccd52 to 15a2469 Compare May 17, 2026 08:45
@vsych vsych changed the title Fix webgl rendering corruption after mid-frame atlas page merge Fix webgl atlas merge leaving stale texture pages in vertex buffer May 17, 2026
@vsych
vsych force-pushed the fix-webgl-atlas-merge-stale-vertices branch from 15a2469 to ae77634 Compare May 18, 2026 22:58
@vsych vsych changed the title Fix webgl atlas merge leaving stale texture pages in vertex buffer Fix webgl rendering corruption from atlas page merges May 18, 2026
@vsych
vsych force-pushed the fix-webgl-atlas-merge-stale-vertices branch 2 times, most recently from 26430b0 to d9accc4 Compare May 18, 2026 23:24
@vsych
vsych force-pushed the fix-webgl-atlas-merge-stale-vertices branch from d9accc4 to dc726a2 Compare May 19, 2026 00:30
hesnotsoharry added a commit to hesnotsoharry/Ouroboros that referenced this pull request May 19, 2026
Per ADR Decision 3 (RESOLVED 2026-05-18): keep WebGL, vendor upstream
fix via self-contained Node postinstall patcher (no new deps - avoids
the per-repo lockfile-sync dance).

Root cause (Phase C sonnet-diagnostician, high confidence):
- Upstream: xtermjs/xterm.js#5847 (OPEN 2026-04-27) - WebGL atlas
  page-merge corruption with allowTransparency:true + heavy streaming.
- Fix: xtermjs/xterm.js#5883 (OPEN 2026-05-17, NOT MERGED).
- Symptom in Ouroboros: ghost cursors in random positions during
  Claude TUI thinking; ghost in front of typing cursor.
- Match: same lib version (@xterm/addon-webgl 0.19.0), same config flag,
  same workload as the upstream repro.

Vendor patch shape:
- patches/addon-webgl-0.19.0.original.{mjs,js} - shipped 0.19.0 snapshot.
- patches/addon-webgl-0.19.0.patched.{mjs,js} - PR #5883 changes applied:
  (1) AtlasPage.version becomes monotonic global (5 increment sites);
  (2) TextureAtlas.beginFrame() resets _requestClearModel before return;
  (3) WebglRenderer.renderRows() adds bounded retry loop (max 3) after
  _updateModel to handle mid-update merges.
- tools/apply-patches.mjs - SHA-256 based; copies patched bundle if
  installed matches original. Idempotent. Soft-warns and exits 0 if
  bundle SHA matches neither (upstream update detected - patch needs
  re-mapping or removal). Wired into package.json postinstall chain.
- patches/README.md - removal flow (bump dep + delete patch files when
  upstream ships >= 0.19.1).

Companion changes:
- eslint.config.mjs - tools/**/*.mjs added to Node-globals block.
- Terminal/CLAUDE.md - gotcha entry pointing to the patch + removal.
- useTerminalSetup.lifecycle.ts - structural baseline trace from
  diagnostician (3 [xterm-init] log lines, kept per debug-before-fix.md).

Live verification by Cole REQUIRED before marking phase resolved -
ghost cursor disappearance can only be confirmed by running the IDE
against a real Claude TUI streaming workload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@Tyriar Tyriar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legend 👏 I can repro the problem on master and not on your branch.

This has been an issue for a long time, I think it's popped up more recently due to agentic CLIs using a much wider range of character styles.

@Tyriar Tyriar added this to the 7.0.0 milestone May 21, 2026
@Tyriar Tyriar self-assigned this May 21, 2026
@Tyriar
Tyriar enabled auto-merge May 21, 2026 09:21
@Tyriar
Tyriar merged commit 3e2a42e into xtermjs:master May 21, 2026
12 checks passed
chloehkwong1 added a commit to chloehkwong1/alfredo that referenced this pull request May 21, 2026
… corruption

Upgrades @xterm/xterm and the six addons from 6.0.0 stable to the
6.1.0-beta.220 train, which includes xtermjs/xterm.js#5883 — fixing glyph
substitution caused by stale WebGL texture binding after atlas page merges.
That corruption hit terminals on WKWebView/Tauri (issue #5847); recovery
previously required a manual atlas rebuild via Cmd+Shift+K.
@Kitenite

Copy link
Copy Markdown

This is awesome!

Kitenite pushed a commit to ParadevAI/Paradev that referenced this pull request May 24, 2026
…e fix (#4893)

* improvement(GH-3572): capture brief

* improvement(GH-3572): scope proposal v1

* improvement(GH-3572): scope challenged + user pushback note

* improvement(GH-3572): deep-research findings (PR #5883 + beta.220)

* fix(desktop): bump @xterm/* to beta.220 for upstream WebGL atlas-merge fix

Picks up xtermjs/xterm.js#5883 "Fix webgl rendering corruption from atlas
page merges" (merged 2026-05-21 by Tyriar). The PR fixes two atlas-merge
bugs that match the reported symptom in #3572 (and the sibling cluster
#3527, #3794, #4601, #4639): texture binding stays bound to the old page's
canvas after a same-index page swap during merge, so glyphs sample garbage.

Maintainer Tyriar on the PR: "This has been an issue for a long time, I
think it's popped up more recently due to agentic CLIs using a much wider
range of character styles."

Also adds a one-line VSCode-pattern (microsoft/vscode#120393) fallback
inside both onContextLoss handlers: set the module-local
suggestedRendererType = "dom" so subsequent terminals on the same path
avoid WebGL after a context-loss event. Defense-in-depth for the rarer
GPU-process-death class that bypasses the upstream atlas-merge fix.

Refs #3572

* improvement(GH-3572): bind scope to option α (upstream bump + fallback)

* fix(web): align @xterm/* to beta.220 (matches desktop)

* chore(GH-3572): remove orchestration artifacts before PR push
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request May 25, 2026
…e fix (ParadevAI#4893)

* improvement(ParadevAIGH-3572): capture brief

* improvement(ParadevAIGH-3572): scope proposal v1

* improvement(ParadevAIGH-3572): scope challenged + user pushback note

* improvement(ParadevAIGH-3572): deep-research findings (PR ParadevAI#5883 + beta.220)

* fix(desktop): bump @xterm/* to beta.220 for upstream WebGL atlas-merge fix

Picks up xtermjs/xterm.js#5883 "Fix webgl rendering corruption from atlas
page merges" (merged 2026-05-21 by Tyriar). The PR fixes two atlas-merge
bugs that match the reported symptom in ParadevAI#3572 (and the sibling cluster
canvas after a same-index page swap during merge, so glyphs sample garbage.

Maintainer Tyriar on the PR: "This has been an issue for a long time, I
think it's popped up more recently due to agentic CLIs using a much wider
range of character styles."

Also adds a one-line VSCode-pattern (microsoft/vscode#120393) fallback
inside both onContextLoss handlers: set the module-local
suggestedRendererType = "dom" so subsequent terminals on the same path
avoid WebGL after a context-loss event. Defense-in-depth for the rarer
GPU-process-death class that bypasses the upstream atlas-merge fix.

Refs ParadevAI#3572

* improvement(ParadevAIGH-3572): bind scope to option α (upstream bump + fallback)

* fix(web): align @xterm/* to beta.220 (matches desktop)

* chore(ParadevAIGH-3572): remove orchestration artifacts before PR push
monotykamary added a commit to monotykamary/localterm that referenced this pull request Jun 9, 2026
Backport xtermjs/xterm.js#5883 to fix glyph rendering corruption
caused by stale texture bindings after atlas page merges:

- Make AtlasPage.version globally monotonic so GlyphRenderer
  detects page swaps at the same texture unit index
- Reset _requestClearModel in beginFrame() when read
- Extract GlyphRenderer.invalidateAtlasTextures() from setAtlas()
- Retry full model update when a merge fires mid-pass (capped at 32)
- Fix RectangleRenderer buffer underallocation check

Switch from CanvasAddon to WebglAddon with preserveDrawingBuffer:true
for correct box-drawing glyph connections and GPU-accelerated
rendering.
bang9 added a commit to bang9/ai-tools that referenced this pull request Jul 15, 2026
…n fixes

Stable xterm 6.0.0 ships two texture-atlas bugs that corrupt WebGL panes:
atlas page merges leave stale vertices (xtermjs/xterm.js#5883) and shared
atlas pages are not invalidated per renderer (#6042) — visible as black
cells and scattered glyph fragments while typing. Pin the 6.1.0-beta line
(webgl 0.20.0-beta.288 is built at the #6042 merge) and migrate the
WebglAddon constructor to its new options-object signature so
preserveDrawingBuffer (needed by the broadcast snapshot compositor) stays
enabled instead of silently degrading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shironex added a commit to noctcore/nightcore that referenced this pull request Jul 31, 2026
…T yet met (part of #430) (#443)

#430 assumed the blocker was `@xterm/xterm`. It isn't: **every file [xtermjs#5883](xtermjs/xterm.js#5883) touches is under `addons/addon-webgl/src`**, so the fix ships in `@xterm/addon-webgl`, which versions independently of the core. "Which xterm release contains it" was the wrong question.

**No stable release carries it.** `@xterm/addon-webgl` has three stable versions ever — 0.17.0, 0.18.0, 0.19.0 — and `dist-tags.latest` is still 0.19.0, which this repo already uses. The fix appears only from `0.20.0-beta.219` (published ~1h after the merge), confirmed by grepping the fix's own markers (`AtlasPage.nextVersion`, `invalidateAtlasTextures`, `MERGE_RETRY_LIMIT`) in the published tarballs: absent in 0.19.0 and 0.20.0-beta.218, present from beta.219.

Taking that prerelease would move the whole terminal stack to the beta channel — its `peerDependencies` demand `@xterm/xterm@^6.1.0-beta.220` — to fix a visual-only bug on our own development platform, with no gate capable of catching a regression. #430's own bar was "a **released** xterm the repo has taken", so macOS stays on DOM and nothing here changes behavior.

**D-010 is amended in place** (original intact, still active): the condition was checked and not met, with the evidence, and the trigger is restated as **a stable `@xterm/addon-webgl` ≥ 0.20.0 carrying #5883** — explicitly *not* "#5816 is closed", because an issue can outlive its fix and this one has. Re-checkable with `npm view @xterm/addon-webgl version`.

Also corrects two now-stale "fix PR unmerged" claims (in `platform.ts` and the terminal build spec) that were true when written and would have justified a future flip on a false premise.

Recorded on #430: the **WebKit ~16-context cap pooling is a prerequisite of the flip, not a follow-up** — with the 8-tab cap, flipping macOS to GPU walks toward the limit where WebKit force-loses the oldest context unrecoverably. Whoever does the flip should land the pool in the same change.

#430 stays open and is now labeled `blocked`.
huangzheng2016 added a commit to Hello-CTF/NexTerm that referenced this pull request Oct 26, 2026
…ling

After a texture atlas page merge, xterm.js 6.0 could skip re-uploading a
texture slot whose new page had the same per-page version, so every
terminal sharing the atlas drew fragments of other glyphs
(xtermjs/xterm.js#5883, #6042). xterm.js moves to 6.1.0-beta.315 and its
addons to the matching betas, pinned exactly.

overviewRuler.width becomes scrollbar.width. A selection whose start row
is trimmed now continues from the start of the first surviving row.
Transcript replay continues after a write callback returns, because
resize inside one re-parses the finished chunk (xtermjs/xterm.js#6246,
still open; a live resize during heavy output can still repeat a chunk).
Tests stub a measuring 2D canvas context that jsdom lacks.
JameZUK pushed a commit to JameZUK/ccpipe that referenced this pull request Nov 2, 2026
… replay

Garbled letters on the phone (Firefox Android): the debug snapshot showed
the buffer text intact while screenshots had colour/dim/bold glyphs broken
— the WebGL glyph atlas, not the data. It matches the @xterm/addon-webgl
page-merge bug (xtermjs/xterm.js#5883, fixed only in the 0.20 betas).
Settings -> Display -> WebGL renderer now switches it off per device,
live, falling back to the DOM renderer.

Soft keyboard: opening it shrank the session (43 -> 22 rows) and closing
it grew it back, and every resize makes Claude Code redraw, leaving
duplicated / cut-off lines in its history. While the keyboard is up the
terminal now keeps its rows and is shifted up so the prompt stays in view;
a width change (rotation) still re-fits.

History replay: capture-pane -J keeps trailing padding, so lines Claude
Code padded to a ~190-column desktop arrived on a 51-column phone as text
plus 3-4 blank wrapped rows (measured on a live session: 2,287 of 6,103
rows blank). Padding that would wrap is trimmed, except in the last
screenful, which tmux's attach redraw paints over and must keep its rows.

Snapshot diff: it compared rows bottom-aligned, which fell out of step
wherever tmux and xterm wrapped differently (373/500 "mismatches" on a
correct buffer). Schema-3 snapshots send logical lines; the server
compares them with -J history plus screen rows, drops tmux's status row,
and matches by sequence so a local difference stays local.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
JameZUK pushed a commit to JameZUK/ccpipe that referenced this pull request Nov 2, 2026
…porary)

@xterm/addon-webgl <= 0.19 garbles glyphs after an atlas page merge (the
merged page can reuse an index + version, so the GPU texture isn't
re-uploaded) — the garbled colour/bold letters on the phone. Fixed
upstream in xtermjs/xterm.js#5883, but only in the 0.20 betas (xterm 6.1).

Until then: after each frame, read the atlas page count and, three pages
short of the merge point, rebuild the WebGL addon so a fresh atlas
starts. clearTextureAtlas() isn't enough — it blanks pages but keeps
them, and full pages never become active again. Reads private internals;
switches itself off with a warning if they change. The debug snapshot
reports terminal.webglAtlas {pages, limit, merged, rebuilds}.

Verified at the phone's DPR (3.33) with thousands of distinct coloured
glyphs: the atlas grew to 24/32 pages, one rebuild, back to 5, no merge.

ROLLBACK when xterm 6.1 / addon-webgl 0.20 ship stable — steps in
docs/development.md, "Temporary workarounds".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

3 participants