diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 6817cb21..6c8df65c 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -70,6 +70,14 @@ const INDICES_PER_CELL = 10; const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT; const CELL_POSITION_INDICES = 2; +/** Work variables to avoid garbage collection. */ +const w: { i: number, glyph: IRasterizedGlyph | undefined, leftCellPadding: number, clippedPixels: number } = { + i: 0, + glyph: undefined, + leftCellPadding: 0, + clippedPixels: 0 +}; + export class GlyphRenderer extends Disposable { private _atlas: WebglCharAtlas | undefined; @@ -170,18 +178,20 @@ export class GlyphRenderer extends Disposable { } public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void { + // Since this function is called for every cell (`rows*cols`), it must be very optimized. It + // should not instantiate any variables unless a new glyph is drawn to the cache where the + // slight slowdown is acceptable for the developer ergonomics provided as it's a once of for + // each glyph. this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg); } private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void { - const terminal = this._terminal; - - const i = (y * terminal.cols + x) * INDICES_PER_CELL; + w.i = (y * this._terminal.cols + x) * INDICES_PER_CELL; // Exit early if this is a null character, allow space character to continue as it may have // underline/strikethrough styles if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) { - fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); + fill(array, 0, w.i, w.i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); return; } @@ -190,47 +200,40 @@ export class GlyphRenderer extends Disposable { } // Get the glyph - let rasterizedGlyph: IRasterizedGlyph; if (chars && chars.length > 1) { - rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); + w.glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); } else { - rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); + w.glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } - // Fill empty if no glyph was found - if (!rasterizedGlyph) { - fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); - return; - } - - const leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2); - if (bg !== lastBg && rasterizedGlyph.offset.x > leftCellPadding) { - const clippedPixels = rasterizedGlyph.offset.x - leftCellPadding; + w.leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2); + if (bg !== lastBg && w.glyph.offset.x > w.leftCellPadding) { + w.clippedPixels = w.glyph.offset.x - w.leftCellPadding; // a_origin - array[i ] = -(rasterizedGlyph.offset.x - clippedPixels) + this._dimensions.scaledCharLeft; - array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop; + array[w.i ] = -(w.glyph.offset.x - w.clippedPixels) + this._dimensions.scaledCharLeft; + array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop; // a_size - array[i + 2] = (rasterizedGlyph.size.x - clippedPixels) / this._dimensions.scaledCanvasWidth; - array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight; + array[w.i + 2] = (w.glyph.size.x - w.clippedPixels) / this._dimensions.scaledCanvasWidth; + array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight; // a_texcoord - array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x + clippedPixels / this._atlas.cacheCanvas.width; - array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y; + array[w.i + 4] = w.glyph.texturePositionClipSpace.x + w.clippedPixels / this._atlas.cacheCanvas.width; + array[w.i + 5] = w.glyph.texturePositionClipSpace.y; // a_texsize - array[i + 6] = rasterizedGlyph.sizeClipSpace.x - clippedPixels / this._atlas.cacheCanvas.width; - array[i + 7] = rasterizedGlyph.sizeClipSpace.y; + array[w.i + 6] = w.glyph.sizeClipSpace.x - w.clippedPixels / this._atlas.cacheCanvas.width; + array[w.i + 7] = w.glyph.sizeClipSpace.y; } else { // a_origin - array[i ] = -rasterizedGlyph.offset.x + this._dimensions.scaledCharLeft; - array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop; + array[w.i ] = -w.glyph.offset.x + this._dimensions.scaledCharLeft; + array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop; // a_size - array[i + 2] = rasterizedGlyph.size.x / this._dimensions.scaledCanvasWidth; - array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight; + array[w.i + 2] = w.glyph.size.x / this._dimensions.scaledCanvasWidth; + array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight; // a_texcoord - array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x; - array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y; + array[w.i + 4] = w.glyph.texturePositionClipSpace.x; + array[w.i + 5] = w.glyph.texturePositionClipSpace.y; // a_texsize - array[i + 6] = rasterizedGlyph.sizeClipSpace.x; - array[i + 7] = rasterizedGlyph.sizeClipSpace.y; + array[w.i + 6] = w.glyph.sizeClipSpace.x; + array[w.i + 7] = w.glyph.sizeClipSpace.y; } // a_cellpos only changes on resize } diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index e1d56b78..c17c54a5 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -15,6 +15,7 @@ import { color, rgba } from 'common/Color'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils'; import { IUnicodeService } from 'common/services/Services'; +import { FourKeyMap } from 'common/MultiKeyMap'; // For debugging purposes, it can be useful to set this to a really tiny value, // to verify that LRU eviction works. @@ -52,11 +53,16 @@ interface ICharAtlasActiveRow { height: number; } +/** Work variables to avoid garbage collection. */ +const w: { glyph: IRasterizedGlyph | undefined } = { + glyph: undefined +}; + export class WebglCharAtlas implements IDisposable { private _didWarmUp: boolean = false; - private _cacheMap: { [code: number]: IRasterizedGlyphSet } = {}; - private _cacheMapCombined: { [chars: string]: IRasterizedGlyphSet } = {}; + private _cacheMap: FourKeyMap = new FourKeyMap(); + private _cacheMapCombined: FourKeyMap = new FourKeyMap(); // The texture that the atlas is drawn to public cacheCanvas: HTMLCanvasElement; @@ -124,13 +130,7 @@ export class WebglCharAtlas implements IDisposable { // Pre-fill with ASCII 33-126 for (let i = 33; i < 126; i++) { const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); - this._cacheMap[i] = { - [DEFAULT_COLOR]: { - [DEFAULT_COLOR]: { - [DEFAULT_EXT]: rasterizedGlyph - } - } - }; + this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph); } } @@ -148,8 +148,8 @@ export class WebglCharAtlas implements IDisposable { return; } this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); - this._cacheMap = {}; - this._cacheMapCombined = {}; + this._cacheMap.clear(); + this._cacheMapCombined.clear(); this._currentRow.x = 0; this._currentRow.y = 0; this._currentRow.height = 0; @@ -169,39 +169,18 @@ export class WebglCharAtlas implements IDisposable { * Gets the glyphs texture coords, drawing the texture if it's not already */ private _getFromCacheMap( - cacheMap: { [key: string | number]: IRasterizedGlyphSet }, + cacheMap: FourKeyMap, key: string | number, bg: number, fg: number, ext: number ): IRasterizedGlyph { - let rasterizedGlyphSet = cacheMap[key]; - if (!rasterizedGlyphSet) { - rasterizedGlyphSet = {}; - cacheMap[key] = rasterizedGlyphSet; + w.glyph = cacheMap.get(key, bg, fg, ext); + if (!w.glyph) { + w.glyph = this._drawToCache(key, bg, fg, ext); + cacheMap.set(key, bg, fg, ext, w.glyph); } - - let rasterizedGlyphSetBg = rasterizedGlyphSet[bg]; - if (!rasterizedGlyphSetBg) { - rasterizedGlyphSetBg = {}; - rasterizedGlyphSet[bg] = rasterizedGlyphSetBg; - } - - let rasterizedGlyph: IRasterizedGlyph | undefined; - let rasterizedGlyphSetFg = rasterizedGlyphSetBg[fg]; - if (!rasterizedGlyphSetFg) { - rasterizedGlyphSetFg = {}; - rasterizedGlyphSetBg[fg] = rasterizedGlyphSetFg; - } else { - rasterizedGlyph = rasterizedGlyphSetFg[ext]; - } - - if (!rasterizedGlyph) { - rasterizedGlyph = this._drawToCache(key, bg, fg, ext); - rasterizedGlyphSetFg[ext] = rasterizedGlyph; - } - - return rasterizedGlyph; + return w.glyph; } private _getColorFromAnsiIndex(idx: number): IColor { diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts index 73b7a0b7..0c60e8db 100644 --- a/src/browser/ColorContrastCache.ts +++ b/src/browser/ColorContrastCache.ts @@ -5,35 +5,30 @@ import { IColorContrastCache } from 'browser/Types'; import { IColor } from 'common/Types'; +import { TwoKeyMap } from 'common/MultiKeyMap'; export class ColorContrastCache implements IColorContrastCache { - private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {}; - private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {}; - - public clear(): void { - this._color = {}; - this._rgba = {}; - } + private _color: TwoKeyMap = new TwoKeyMap(); + private _css: TwoKeyMap = new TwoKeyMap(); public setCss(bg: number, fg: number, value: string | null): void { - if (!this._rgba[bg]) { - this._rgba[bg] = {}; - } - this._rgba[bg]![fg] = value; + this._css.set(bg, fg, value); } public getCss(bg: number, fg: number): string | null | undefined { - return this._rgba[bg] ? this._rgba[bg]![fg] : undefined; + return this._css.get(bg, fg); } public setColor(bg: number, fg: number, value: IColor | null): void { - if (!this._color[bg]) { - this._color[bg] = {}; - } - this._color[bg]![fg] = value; + this._color.set(bg, fg, value); } public getColor(bg: number, fg: number): IColor | null | undefined { - return this._color[bg] ? this._color[bg]![fg] : undefined; + return this._color.get(bg, fg); + } + + public clear(): void { + this._color.clear(); + this._css.clear(); } } diff --git a/src/common/MultiKeyMap.test.ts b/src/common/MultiKeyMap.test.ts new file mode 100644 index 00000000..b908321c --- /dev/null +++ b/src/common/MultiKeyMap.test.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { FourKeyMap, TwoKeyMap } from 'common/MultiKeyMap'; + +const strictEqual = assert.strictEqual; + +describe('TwoKeyMap', () => { + let map: TwoKeyMap; + + beforeEach(() => { + map = new TwoKeyMap(); + }); + + it('set, get', () => { + strictEqual(map.get(1, 2), undefined); + map.set(1, 2, 'foo'); + strictEqual(map.get(1, 2), 'foo'); + map.set(1, 3, 'bar'); + strictEqual(map.get(1, 2), 'foo'); + strictEqual(map.get(1, 3), 'bar'); + map.set(2, 2, 'foo2'); + map.set(2, 3, 'bar2'); + strictEqual(map.get(1, 2), 'foo'); + strictEqual(map.get(1, 3), 'bar'); + strictEqual(map.get(2, 2), 'foo2'); + strictEqual(map.get(2, 3), 'bar2'); + }); + it('clear', () => { + strictEqual(map.get(1, 2), undefined); + map.set(1, 2, 'foo'); + strictEqual(map.get(1, 2), 'foo'); + map.clear(); + strictEqual(map.get(1, 2), undefined); + }); +}); + +describe('FourKeyMap', () => { + let map: FourKeyMap; + + beforeEach(() => { + map = new FourKeyMap(); + }); + + it('set, get', () => { + strictEqual(map.get(1, 2, 3, 4), undefined); + map.set(1, 2, 3, 4, 'foo'); + strictEqual(map.get(1, 2, 3, 4), 'foo'); + map.set(1, 3, 3, 4, 'bar'); + strictEqual(map.get(1, 2, 3, 4), 'foo'); + strictEqual(map.get(1, 3, 3, 4), 'bar'); + map.set(2, 2, 3, 4, 'foo2'); + map.set(2, 3, 3, 4, 'bar2'); + strictEqual(map.get(1, 2, 3, 4), 'foo'); + strictEqual(map.get(1, 3, 3, 4), 'bar'); + strictEqual(map.get(2, 2, 3, 4), 'foo2'); + strictEqual(map.get(2, 3, 3, 4), 'bar2'); + }); + it('clear', () => { + strictEqual(map.get(1, 2, 3, 4), undefined); + map.set(1, 2, 3, 4, 'foo'); + strictEqual(map.get(1, 2, 3, 4), 'foo'); + map.clear(); + strictEqual(map.get(1, 2, 3, 4), undefined); + }); +}); diff --git a/src/common/MultiKeyMap.ts b/src/common/MultiKeyMap.ts new file mode 100644 index 00000000..6287a8f2 --- /dev/null +++ b/src/common/MultiKeyMap.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export class TwoKeyMap { + private _data: { [bg: string | number]: { [fg: string | number]: TValue | undefined } | undefined } = {}; + + public set(first: TFirst, second: TSecond, value: TValue): void { + if (!this._data[first]) { + this._data[first] = {}; + } + this._data[first as string | number]![second] = value; + } + + public get(first: TFirst, second: TSecond): TValue | undefined { + return this._data[first as string | number] ? this._data[first as string | number]![second] : undefined; + } + + public clear(): void { + this._data = {}; + } +} + +export class FourKeyMap { + private _data: TwoKeyMap> = new TwoKeyMap(); + + public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void { + if (!this._data.get(first, second)) { + this._data.set(first, second, new TwoKeyMap()); + } + this._data.get(first, second)!.set(third, fourth, value); + } + + public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined { + return this._data.get(first, second)?.get(third, fourth); + } + + public clear(): void { + this._data.clear(); + } +}