From dc726a2ae6287e1aa09d410f4f11b4d73ed3fe4b Mon Sep 17 00:00:00 2001 From: Victor Sych Date: Sun, 17 May 2026 10:41:37 +0200 Subject: [PATCH 1/2] Fix webgl rendering corruption from atlas page merges --- addons/addon-webgl/src/GlyphRenderer.ts | 4 +++- addons/addon-webgl/src/TextureAtlas.ts | 20 +++++++++++++------- addons/addon-webgl/src/WebglRenderer.ts | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts index 568d3a4f96..b412f2d89f 100644 --- a/addons/addon-webgl/src/GlyphRenderer.ts +++ b/addons/addon-webgl/src/GlyphRenderer.ts @@ -358,7 +358,9 @@ export class GlyphRenderer extends Disposable { gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW); - // Bind the atlas page texture if they have changed + // Bind the atlas page texture if they have changed. AtlasPage.version is globally + // monotonic, so a page object swap at the same index (which happens after a page merge) + // is detected by the same comparison. for (let i = 0; i < this._atlas.pages.length; i++) { if (this._atlas.pages[i].version !== this._atlasTextures[i].version) { this._bindAtlasPageTexture(gl, this._atlas, i); diff --git a/addons/addon-webgl/src/TextureAtlas.ts b/addons/addon-webgl/src/TextureAtlas.ts index 5629508f25..34188235c7 100644 --- a/addons/addon-webgl/src/TextureAtlas.ts +++ b/addons/addon-webgl/src/TextureAtlas.ts @@ -133,7 +133,9 @@ export class TextureAtlas implements ITextureAtlas { private _requestClearModel = false; public beginFrame(): boolean { - return this._requestClearModel; + const result = this._requestClearModel; + this._requestClearModel = false; + return result; } public clearTexture(): void { @@ -193,7 +195,7 @@ export class TextureAtlas implements ITextureAtlas { // Merge into the new page const mergedPage = this._mergePages(mergingPages, mergedPageIndex); - mergedPage.version++; + mergedPage.version = ++AtlasPage.nextVersion; // Delete the pages, shifting glyph texture pages as needed for (let i = sortedMergingPagesIndexes.length - 1; i >= 0; i--) { @@ -251,7 +253,7 @@ export class TextureAtlas implements ITextureAtlas { for (const g of adjustingPage.glyphs) { g.texturePage--; } - adjustingPage.version++; + adjustingPage.version = ++AtlasPage.nextVersion; } } @@ -937,7 +939,7 @@ export class TextureAtlas implements ITextureAtlas { rasterizedGlyph.size.y ); activePage.addGlyph(rasterizedGlyph); - activePage.version++; + activePage.version = ++AtlasPage.nextVersion; return rasterizedGlyph; } @@ -1047,9 +1049,13 @@ class AtlasPage { } /** - * Used to check whether the canvas of the atlas page has changed. + * Monotonically increasing across all atlas pages globally. Used to detect when the texture + * unit at a given index needs to be re-uploaded — both for content changes within the same + * page and for a page object swap at the same index (which happens after a page merge, + * where a per-page counter could coincide with the previously-bound page's value). */ - public version = 0; + public static nextVersion: number = 0; + public version = ++AtlasPage.nextVersion; // Texture atlas current positioning data. The texture packing strategy used is to fill from // left-to-right and top-to-bottom. When the glyph being written is less than half of the current @@ -1092,7 +1098,7 @@ class AtlasPage { this.currentRow.y = 0; this.currentRow.height = 0; this.fixedRows.length = 0; - this.version++; + this.version = ++AtlasPage.nextVersion; } } diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 91867ea261..578955fc12 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -28,6 +28,16 @@ import { addDisposableListener } from 'browser/Dom'; import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; +const enum Constants { + /** + * Upper bound on how many times `renderRows` re-runs `_updateModel` after detecting a + * mid-update atlas page merge. A single retry should always suffice once every glyph + * needed by the frame is cached; the cap exists purely as a safety net against an + * unexpected runaway, not as a tuned value. + */ + MERGE_RETRY_LIMIT = 3 +} + export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; private _cursorBlinkStateManager: MutableDisposable = this._register(new MutableDisposable()); @@ -375,6 +385,14 @@ export class WebglRenderer extends Disposable implements IRenderer { this._updateModel(start, end); } + // A mid-update atlas page merge shifts cached glyphs to new texture pages, leaving + // stale texturePage values in the vertex buffer. Re-run a full update if that happened. + let mergeRetries = 0; + while (this._charAtlas && this._glyphRenderer.value.beginFrame() && mergeRetries++ < Constants.MERGE_RETRY_LIMIT) { + this._clearModel(true); + this._updateModel(0, this._terminal.rows - 1); + } + // Render this._rectangleRenderer.value.renderBackgrounds(); this._glyphRenderer.value.render(this._model); From 3bcb57545e3d2db44555f9128f4fefb66555fd98 Mon Sep 17 00:00:00 2001 From: Victor Sych Date: Thu, 21 May 2026 04:59:37 +0200 Subject: [PATCH 2/2] Re-upload atlas textures after page merges --- addons/addon-webgl/src/GlyphRenderer.ts | 4 ++++ addons/addon-webgl/src/RectangleRenderer.ts | 5 +++-- addons/addon-webgl/src/WebglRenderer.ts | 17 ++++++++--------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts index b412f2d89f..46b8efeb90 100644 --- a/addons/addon-webgl/src/GlyphRenderer.ts +++ b/addons/addon-webgl/src/GlyphRenderer.ts @@ -373,6 +373,10 @@ export class GlyphRenderer extends Disposable { public setAtlas(atlas: ITextureAtlas): void { this._atlas = atlas; + this.invalidateAtlasTextures(); + } + + public invalidateAtlasTextures(): void { for (const glTexture of this._atlasTextures) { glTexture.version = -1; } diff --git a/addons/addon-webgl/src/RectangleRenderer.ts b/addons/addon-webgl/src/RectangleRenderer.ts index 4ad8d66ff0..81e129e5c6 100644 --- a/addons/addon-webgl/src/RectangleRenderer.ts +++ b/addons/addon-webgl/src/RectangleRenderer.ts @@ -336,8 +336,9 @@ export class RectangleRenderer extends Disposable { } } - if (vertices.attributes.length < offset + 4) { - vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE); + if (vertices.attributes.length < offset + INDICES_PER_RECTANGLE) { + // +1 for the viewport-clear rectangle at offset 0. + vertices.attributes = expandFloat32Array(vertices.attributes, (this._terminal.rows * this._terminal.cols + 1) * INDICES_PER_RECTANGLE); } $x1 = startX * this._dimensions.device.cell.width; $y1 = y * this._dimensions.device.cell.height; diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 578955fc12..500308bd1d 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -29,13 +29,7 @@ import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; const enum Constants { - /** - * Upper bound on how many times `renderRows` re-runs `_updateModel` after detecting a - * mid-update atlas page merge. A single retry should always suffice once every glyph - * needed by the frame is cached; the cap exists purely as a safety net against an - * unexpected runaway, not as a tuned value. - */ - MERGE_RETRY_LIMIT = 3 + MERGE_RETRY_LIMIT = 32 } export class WebglRenderer extends Disposable implements IRenderer { @@ -385,13 +379,18 @@ export class WebglRenderer extends Disposable implements IRenderer { this._updateModel(start, end); } - // A mid-update atlas page merge shifts cached glyphs to new texture pages, leaving - // stale texturePage values in the vertex buffer. Re-run a full update if that happened. + // A mid-update atlas page merge invalidates vertex data and may not bump the host + // page's version, so re-run the update and force a full texture rebind. + let merged = false; let mergeRetries = 0; while (this._charAtlas && this._glyphRenderer.value.beginFrame() && mergeRetries++ < Constants.MERGE_RETRY_LIMIT) { + merged = true; this._clearModel(true); this._updateModel(0, this._terminal.rows - 1); } + if (merged) { + this._glyphRenderer.value.invalidateAtlasTextures(); + } // Render this._rectangleRenderer.value.renderBackgrounds();