From e882f4a46a099a9ab5339009f57bd55684d896a6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:50:08 -0800 Subject: [PATCH 1/3] Prototype of adding a large atlas page for overflow glyphs Part of #5246 --- src/browser/renderer/shared/TextureAtlas.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index a86cd459..0eca83e2 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -75,6 +75,8 @@ export class TextureAtlas implements ITextureAtlas { private _workAttributeData: AttributeData = new AttributeData(); private _textureSize: number = 512; + // TODO: Use actual value + private _deviceMaxTextureSize: number = 2048; public static maxAtlasPages: number | undefined; public static maxTextureSize: number | undefined; @@ -431,7 +433,7 @@ export class TextureAtlas implements ITextureAtlas { // Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used // to draw the glyph to the canvas as well as to restrict the bounding box search to ensure // giant ligatures (eg. =====>) don't impact overall performance. - const allowedWidth = Math.min(this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2, this._textureSize); + const allowedWidth = Math.min(this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2, this._deviceMaxTextureSize); if (this._tmpCanvas.width < allowedWidth) { this._tmpCanvas.width = allowedWidth; } @@ -772,6 +774,21 @@ export class TextureAtlas implements ITextureAtlas { } } + // Create a new page for oversized glyphs as they come up + if (rasterizedGlyph.size.x > this._textureSize) { + // TODO: Move below after page merging to ensure page limit isn't hit + const newPage = new AtlasPage(this._document, this._deviceMaxTextureSize); + this.pages.push(newPage); + + // Request the model to be cleared to refresh all texture pages. + this._requestClearModel = true; + this._onAddTextureAtlasCanvas.fire(newPage.canvas); + + newPage.addGlyph(rasterizedGlyph); + activePage.fixedRows.push(newPage.currentRow); + break; + } + // Create a new page if too much vertical space would be wasted or there is not enough room // left in the page. The previous active row will become fixed in the process as it now has a // fixed height From 7541cf2fd74c93821b0b5cf5980fd626de665b9a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:52:20 -0800 Subject: [PATCH 2/3] Add todo --- src/browser/renderer/shared/TextureAtlas.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 0eca83e2..01a53c8a 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -774,9 +774,10 @@ export class TextureAtlas implements ITextureAtlas { } } + // TODO: Move below after page merging to ensure page limit isn't hit // Create a new page for oversized glyphs as they come up if (rasterizedGlyph.size.x > this._textureSize) { - // TODO: Move below after page merging to ensure page limit isn't hit + // TODO: Reuse a single oversized glyphs page const newPage = new AtlasPage(this._document, this._deviceMaxTextureSize); this.pages.push(newPage); From a7b98de83308934ea5df874bd592101ad16f0b51 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 7 Jan 2025 06:28:59 -0800 Subject: [PATCH 3/3] Handle overflowing to new row --- addons/addon-webgl/src/WebglRenderer.ts | 6 +++- .../src/renderLayer/BaseRenderLayer.ts | 3 +- src/browser/renderer/shared/CharAtlasCache.ts | 5 +-- src/browser/renderer/shared/CharAtlasUtils.ts | 3 +- src/browser/renderer/shared/TextureAtlas.ts | 31 +++++++++++-------- src/browser/renderer/shared/Types.ts | 1 + 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 3eb13761..4be864c1 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -33,6 +33,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _charAtlasDisposable = this._register(new MutableDisposable()); private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; + private _deviceMaxTextureSize: number; private _observerDisposable = this._register(new MutableDisposable()); private _model: RenderModel = new RenderModel(); @@ -102,6 +103,8 @@ export class WebglRenderer extends Disposable implements IRenderer { throw new Error('WebGL2 not supported ' + this._gl); } + this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE); + this._register(addDisposableListener(this._canvas, 'webglcontextlost', (e) => { console.log('webglcontextlost event received'); // Prevent the default behavior in order to enable WebGL context restoration. @@ -272,7 +275,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this.dimensions.device.cell.height, this.dimensions.device.char.width, this.dimensions.device.char.height, - this._coreBrowserService.dpr + this._coreBrowserService.dpr, + this._deviceMaxTextureSize ); if (this._charAtlas !== atlas) { this._onChangeTextureAtlas.fire(atlas.pages[0].canvas); diff --git a/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts index 3d9a1aa5..0cac7eb3 100644 --- a/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -94,7 +94,8 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) { return; } - this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr); + + this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr, 2048); this._charAtlas.warmUp(); } diff --git a/src/browser/renderer/shared/CharAtlasCache.ts b/src/browser/renderer/shared/CharAtlasCache.ts index 0a871483..33e58078 100644 --- a/src/browser/renderer/shared/CharAtlasCache.ts +++ b/src/browser/renderer/shared/CharAtlasCache.ts @@ -31,9 +31,10 @@ export function acquireTextureAtlas( deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, - devicePixelRatio: number + devicePixelRatio: number, + deviceMaxTextureSize: number ): ITextureAtlas { - const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio); + const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio, deviceMaxTextureSize); // Check to see if the terminal already owns this config for (let i = 0; i < charAtlasCache.length; i++) { diff --git a/src/browser/renderer/shared/CharAtlasUtils.ts b/src/browser/renderer/shared/CharAtlasUtils.ts index fb3fae88..db601383 100644 --- a/src/browser/renderer/shared/CharAtlasUtils.ts +++ b/src/browser/renderer/shared/CharAtlasUtils.ts @@ -9,7 +9,7 @@ import { ITerminalOptions } from '@xterm/xterm'; import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { NULL_COLOR } from 'common/Color'; -export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required, colors: ReadonlyColorSet, devicePixelRatio: number): ICharAtlasConfig { +export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number): ICharAtlasConfig { // null out some fields that don't matter const clonedColors: IColorSet = { foreground: colors.foreground, @@ -34,6 +34,7 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number return { customGlyphs: options.customGlyphs, devicePixelRatio, + deviceMaxTextureSize, letterSpacing: options.letterSpacing, lineHeight: options.lineHeight, deviceCellWidth: deviceCellWidth, diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 01a53c8a..c692aece 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -66,6 +66,7 @@ export class TextureAtlas implements ITextureAtlas { // The set of atlas pages that can be written to private _activePages: AtlasPage[] = []; + private _overflowSizePage: AtlasPage | undefined; private _tmpCanvas: HTMLCanvasElement; // A temporary context that glyphs are drawn to before being transfered to the atlas. @@ -75,8 +76,6 @@ export class TextureAtlas implements ITextureAtlas { private _workAttributeData: AttributeData = new AttributeData(); private _textureSize: number = 512; - // TODO: Use actual value - private _deviceMaxTextureSize: number = 2048; public static maxAtlasPages: number | undefined; public static maxTextureSize: number | undefined; @@ -433,7 +432,7 @@ export class TextureAtlas implements ITextureAtlas { // Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used // to draw the glyph to the canvas as well as to restrict the bounding box search to ensure // giant ligatures (eg. =====>) don't impact overall performance. - const allowedWidth = Math.min(this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2, this._deviceMaxTextureSize); + const allowedWidth = Math.min(this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2, this._config.deviceMaxTextureSize); if (this._tmpCanvas.width < allowedWidth) { this._tmpCanvas.width = allowedWidth; } @@ -774,19 +773,24 @@ export class TextureAtlas implements ITextureAtlas { } } - // TODO: Move below after page merging to ensure page limit isn't hit // Create a new page for oversized glyphs as they come up if (rasterizedGlyph.size.x > this._textureSize) { - // TODO: Reuse a single oversized glyphs page - const newPage = new AtlasPage(this._document, this._deviceMaxTextureSize); - this.pages.push(newPage); + if (!this._overflowSizePage) { + this._overflowSizePage = new AtlasPage(this._document, this._config.deviceMaxTextureSize); + this.pages.push(this._overflowSizePage); - // Request the model to be cleared to refresh all texture pages. - this._requestClearModel = true; - this._onAddTextureAtlasCanvas.fire(newPage.canvas); - - newPage.addGlyph(rasterizedGlyph); - activePage.fixedRows.push(newPage.currentRow); + // Request the model to be cleared to refresh all texture pages. + this._requestClearModel = true; + this._onAddTextureAtlasCanvas.fire(this._overflowSizePage.canvas); + } + activePage = this._overflowSizePage; + activeRow = this._overflowSizePage.currentRow; + // Move to next row if necessary + if (activeRow.x + rasterizedGlyph.size.x >= activePage.canvas.width) { + activeRow.x = 0; + activeRow.y += activeRow.height; + activeRow.height = 0; + } break; } @@ -800,6 +804,7 @@ export class TextureAtlas implements ITextureAtlas { if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) { // Find the first page with room to create the new row on let candidatePage: AtlasPage | undefined; + for (const p of this._activePages) { if (p.currentRow.y + p.currentRow.height + rasterizedGlyph.size.y < p.canvas.height) { candidatePage = p; diff --git a/src/browser/renderer/shared/Types.ts b/src/browser/renderer/shared/Types.ts index 3b3c7659..47681a4c 100644 --- a/src/browser/renderer/shared/Types.ts +++ b/src/browser/renderer/shared/Types.ts @@ -11,6 +11,7 @@ import type { Event } from 'vs/base/common/event'; export interface ICharAtlasConfig { customGlyphs: boolean; devicePixelRatio: number; + deviceMaxTextureSize: number; letterSpacing: number; lineHeight: number; fontSize: number;