From 6f21385cd8f730358c40f1e95f6fae4eb78b036c Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 7 Mar 2018 22:41:48 -0800 Subject: [PATCH 01/15] Add BaseCharAtlas and implement StaticCharAtlas The API defined by BaseCharAtlas will let us support multiple char atlas implementations at once. I tested this by running the demo with Chrome's profiler, which shows that calls are going to StaticCharAtlas/drawImage instead of _drawUncachedChar. Applications using 256 colors also still work fine, via _drawUncachedChar. --- src/renderer/BaseRenderLayer.ts | 70 +++--------- src/renderer/atlas/BaseCharAtlas.ts | 46 ++++++++ .../atlas/{CharAtlas.ts => CharAtlasCache.ts} | 36 ++++--- src/renderer/atlas/StaticCharAtlas.ts | 100 ++++++++++++++++++ src/renderer/atlas/Types.ts | 8 ++ tsconfig.json | 1 + 6 files changed, 188 insertions(+), 73 deletions(-) create mode 100644 src/renderer/atlas/BaseCharAtlas.ts rename src/renderer/atlas/{CharAtlas.ts => CharAtlasCache.ts} (69%) create mode 100644 src/renderer/atlas/StaticCharAtlas.ts diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index fbffd54748..1747c428a5 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -6,8 +6,8 @@ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; import { CharData, ITerminal } from '../Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types'; -import { CHAR_ATLAS_CELL_SPACING } from '../shared/atlas/Types'; -import { acquireCharAtlas } from './atlas/CharAtlas'; +import BaseCharAtlas from './atlas/BaseCharAtlas'; +import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { CHAR_DATA_CHAR_INDEX } from '../Buffer'; export abstract class BaseRenderLayer implements IRenderLayer { @@ -20,7 +20,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; - private _charAtlas: HTMLCanvasElement | ImageBitmap; + private _charAtlas: BaseCharAtlas; constructor( private _container: HTMLElement, @@ -83,13 +83,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = null; - const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight); - if (result instanceof HTMLCanvasElement) { - this._charAtlas = result; - } else { - result.then(bitmap => this._charAtlas = bitmap); - } + this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight); + this._charAtlas.warmUp(); } public resize(terminal: ITerminal, dim: IRenderDimensions): void { @@ -243,55 +238,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param bold Whether the text is bold. */ protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean): void { - let colorIndex = 0; - if (fg < 256) { - colorIndex = fg + 2; - } else { - // If default color and bold - if (bold && terminal.options.enableBold) { - colorIndex = 1; - } - } - const isAscii = code < 256; - // A color is basic if it is one of the standard normal or bold weight - // colors of the characters held in the char atlas. Note that this excludes - // the normal weight _light_ color characters. - const isBasicColor = (colorIndex > 1 && fg < 16) && (fg < 8 || bold); - const isDefaultColor = fg >= 256; - const isDefaultBackground = bg >= 256; - if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) { - // ImageBitmap's draw about twice as fast as from a canvas - const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING; - const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING; - - // Apply alpha to dim the character - if (dim) { - this._ctx.globalAlpha = DIM_OPACITY; - } - - // Draw the non-bold version of the same color if bold is not enabled - if (bold && !terminal.options.enableBold) { - // Ignore default color as it's not touched above - if (colorIndex > 1) { - colorIndex -= 8; - } - } - - this._ctx.drawImage(this._charAtlas, - code * charAtlasCellWidth, - colorIndex * charAtlasCellHeight, - charAtlasCellWidth, - this._scaledCharHeight, - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop, - charAtlasCellWidth, - this._scaledCharHeight); - } else { + const atlasDidDraw = this._charAtlas && this._charAtlas.draw( + this._ctx, + {char, bg, fg, bold: bold && terminal.options.enableBold, dim}, + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop + ); + + if (!atlasDidDraw) { this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim); } - // This draws the atlas (for debugging purposes) - // this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - // this._ctx.drawImage(this._charAtlas, 0, 0); } /** diff --git a/src/renderer/atlas/BaseCharAtlas.ts b/src/renderer/atlas/BaseCharAtlas.ts new file mode 100644 index 0000000000..21d5f833e3 --- /dev/null +++ b/src/renderer/atlas/BaseCharAtlas.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IGlyphIdentifier } from './Types'; + +export default abstract class BaseCharAtlas { + private _didWarmUp: Promise; + + /** + * Perform any work needed to warm the cache before it can be used. May be called multiple times. + * Implement _doWarmUp instead if you only want to get called once. + */ + public warmUp(): Promise { + if (this._didWarmUp == null) { + this._didWarmUp = this._doWarmUp(); + } + return this._didWarmUp; + } + + /** + * Perform any work needed to warm the cache before it can be used. Used by the default + * implementation of warmUp(), and will only be called once. + */ + protected _doWarmUp(): Promise { + return Promise.resolve(); + } + + /** + * May be called before warmUp finishes, however it is okay for the implementation to + * do nothing and return false in that case. + * + * @param ctx Where to draw the character onto. + * @param glyph Information about what to draw + * @param x The position on the context to start drawing at + * @param y The position on the context to start drawing at + * @returns The success state. True if we drew the character. + */ + public abstract draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number, + ): boolean; +} diff --git a/src/renderer/atlas/CharAtlas.ts b/src/renderer/atlas/CharAtlasCache.ts similarity index 69% rename from src/renderer/atlas/CharAtlas.ts rename to src/renderer/atlas/CharAtlasCache.ts index 2e417c094b..ea887a1af6 100644 --- a/src/renderer/atlas/CharAtlas.ts +++ b/src/renderer/atlas/CharAtlasCache.ts @@ -4,18 +4,21 @@ */ import { ITerminal } from '../../Types'; +import BaseCharAtlas from './BaseCharAtlas'; +import StaticCharAtlas from './StaticCharAtlas'; import { IColorSet } from '../Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; -import { generateCharAtlas } from '../../shared/atlas/CharAtlasGenerator'; import { generateConfig, configEquals } from './CharAtlasUtils'; interface ICharAtlasCacheEntry { - bitmap: HTMLCanvasElement | Promise; + atlas: BaseCharAtlas; config: ICharAtlasConfig; + // N.B. This implementation potentially holds onto copies of the terminal forever, so + // this may cause memory leaks. ownedBy: ITerminal[]; } -let charAtlasCache: ICharAtlasCacheEntry[] = []; +const charAtlasCache: ICharAtlasCacheEntry[] = []; /** * Acquires a char atlas, either generating a new one or returning an existing @@ -23,7 +26,12 @@ let charAtlasCache: ICharAtlasCacheEntry[] = []; * @param terminal The terminal. * @param colors The colors to use. */ -export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise { +export function acquireCharAtlas( + terminal: ITerminal, + colors: IColorSet, + scaledCharWidth: number, + scaledCharHeight: number, +): BaseCharAtlas { const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); // Check to see if the terminal already owns this config @@ -32,7 +40,7 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC const ownedByIndex = entry.ownedBy.indexOf(terminal); if (ownedByIndex >= 0) { if (configEquals(entry.config, newConfig)) { - return entry.bitmap; + return entry.atlas; } else { // The configs differ, release the terminal from the entry if (entry.ownedBy.length === 1) { @@ -51,22 +59,18 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC if (configEquals(entry.config, newConfig)) { // Add the terminal to the cache entry and return entry.ownedBy.push(terminal); - return entry.bitmap; + return entry.atlas; } } - const canvasFactory = (width: number, height: number) => { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - return canvas; - }; - const newEntry: ICharAtlasCacheEntry = { - bitmap: generateCharAtlas(window, canvasFactory, newConfig), + atlas: new StaticCharAtlas( + document, + newConfig, + ), config: newConfig, - ownedBy: [terminal] + ownedBy: [terminal], }; charAtlasCache.push(newEntry); - return newEntry.bitmap; + return newEntry.atlas; } diff --git a/src/renderer/atlas/StaticCharAtlas.ts b/src/renderer/atlas/StaticCharAtlas.ts new file mode 100644 index 0000000000..de808716ab --- /dev/null +++ b/src/renderer/atlas/StaticCharAtlas.ts @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { DIM_OPACITY, IGlyphIdentifier } from './Types'; +import { ICharAtlasConfig } from '../../shared/atlas/Types'; +import { CHAR_ATLAS_CELL_SPACING } from '../../shared/atlas/Types'; +import { generateCharAtlas } from '../../shared/atlas/CharAtlasGenerator'; +import BaseCharAtlas from './BaseCharAtlas'; + +export default class StaticCharAtlas extends BaseCharAtlas { + private _texture: HTMLCanvasElement | ImageBitmap; + + constructor(private _document: Document, private _config: ICharAtlasConfig) { + super(); + } + + private _canvasFactory = (width: number, height: number) => { + const canvas = this._document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + } + + public async _doWarmUp(): Promise { + const result = generateCharAtlas(window, this._canvasFactory, this._config); + if (result instanceof Promise) { + this._texture = await result; + } else { + this._texture = result; + } + } + + private _isCached(glyph: IGlyphIdentifier, colorIndex: number): boolean { + const isAscii = glyph.char.charCodeAt(0) < 256; + // A color is basic if it is one of the standard normal or bold weight + // colors of the characters held in the char atlas. Note that this excludes + // the normal weight _light_ color characters. + const isBasicColor = (colorIndex > 1 && glyph.fg < 16) && (glyph.fg < 8 || glyph.bold); + const isDefaultColor = glyph.fg >= 256; + const isDefaultBackground = glyph.bg >= 256; + return isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground; + } + + public draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number, + ): boolean { + // we're not warmed up yet + if (this._texture == null) { + return false; + } + + let colorIndex = 0; + if (glyph.fg < 256) { + colorIndex = glyph.fg + 2; + } else { + // If default color and bold + if (glyph.bold) { + colorIndex = 1; + } + } + if (!this._isCached(glyph, colorIndex)) { + return false; + } + // ImageBitmap's draw about twice as fast as from a canvas + const charAtlasCellWidth = this._config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const charAtlasCellHeight = this._config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + + // Apply alpha to dim the character + if (glyph.dim) { + ctx.globalAlpha = DIM_OPACITY; + } + + // Draw the non-bold version of the same color if bold is not enabled + /*if (glyph.bold && !terminal.options.enableBold) { + // Ignore default color as it's not touched above + if (colorIndex > 1) { + colorIndex -= 8; + } + }*/ + + ctx.drawImage( + this._texture, + glyph.char.charCodeAt(0) * charAtlasCellWidth, + colorIndex * charAtlasCellHeight, + charAtlasCellWidth, + this._config.scaledCharHeight, + x, + y, + charAtlasCellWidth, + this._config.scaledCharHeight + ); + + return true; + } +} diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts index 34f01d39cd..fe52f5a5e4 100644 --- a/src/renderer/atlas/Types.ts +++ b/src/renderer/atlas/Types.ts @@ -5,3 +5,11 @@ export const INVERTED_DEFAULT_COLOR = -1; export const DIM_OPACITY = 0.5; + +export interface IGlyphIdentifier { + char: string; + bg: number; + fg: number; + bold: boolean; + dim: boolean; +} diff --git a/tsconfig.json b/tsconfig.json index ffd9ab6862..6d329658c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "module": "commonjs", "target": "es5", + "lib": ["DOM", "ES5", "ScriptHost", "ES2015.Promise"], "rootDir": "src", "outDir": "lib", "sourceMap": true, From 8c980bc30d6d1b460a2cbbfccffb610dcf7d8da4 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Sat, 10 Mar 2018 20:08:09 -0800 Subject: [PATCH 02/15] Add a dummy NoneCharAtlas implementation This adds the logic needed to switch between multiple char atlas implementations, and an example second implementation to test it out with. This also offers an escape hatch if something goes wrong with the char atlas implementation(s). --- demo/index.html | 9 +++++++++ demo/main.js | 4 ++++ src/Terminal.ts | 2 ++ src/renderer/atlas/CharAtlasCache.ts | 12 +++++++++--- src/renderer/atlas/CharAtlasUtils.ts | 4 +++- src/renderer/atlas/NoneCharAtlas.ts | 25 +++++++++++++++++++++++++ src/shared/atlas/Types.ts | 1 + typings/xterm.d.ts | 14 ++++++++++++++ 8 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 src/renderer/atlas/NoneCharAtlas.ts diff --git a/demo/index.html b/demo/index.html index 760d5990c5..e18614a48b 100644 --- a/demo/index.html +++ b/demo/index.html @@ -53,6 +53,15 @@

Options

+

+ +

Size

diff --git a/demo/main.js b/demo/main.js index d9c0151ad1..f7c863f5ca 100644 --- a/demo/main.js +++ b/demo/main.js @@ -32,6 +32,7 @@ var terminalContainer = document.getElementById('terminal-container'), macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'), scrollback: document.querySelector('#option-scrollback'), tabstopwidth: document.querySelector('#option-tabstopwidth'), + experimentalCharAtlas: document.querySelector('#option-experimental-char-atlas'), bellStyle: document.querySelector('#option-bell-style'), screenReaderMode: document.querySelector('#option-screen-reader-mode') }, @@ -89,6 +90,9 @@ optionElements.scrollback.addEventListener('change', function () { optionElements.tabstopwidth.addEventListener('change', function () { term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10)); }); +optionElements.experimentalCharAtlas.addEventListener('change', function () { + term.setOption('experimentalCharAtlas', optionElements.experimentalCharAtlas.value); +}); optionElements.screenReaderMode.addEventListener('change', function () { term.setOption('screenReaderMode', optionElements.screenReaderMode.checked); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index 4b58f47d46..809d0dd887 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -102,6 +102,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', enableBold: true, + experimentalCharAtlas: 'static', fontFamily: 'courier-new, courier, monospace', fontSize: 15, fontWeight: 'normal', @@ -454,6 +455,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.charMeasure.measure(this.options); } break; + case 'experimentalCharAtlas': case 'enableBold': case 'letterSpacing': case 'lineHeight': diff --git a/src/renderer/atlas/CharAtlasCache.ts b/src/renderer/atlas/CharAtlasCache.ts index ea887a1af6..14037acf3e 100644 --- a/src/renderer/atlas/CharAtlasCache.ts +++ b/src/renderer/atlas/CharAtlasCache.ts @@ -4,11 +4,17 @@ */ import { ITerminal } from '../../Types'; -import BaseCharAtlas from './BaseCharAtlas'; -import StaticCharAtlas from './StaticCharAtlas'; import { IColorSet } from '../Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; import { generateConfig, configEquals } from './CharAtlasUtils'; +import BaseCharAtlas from './BaseCharAtlas'; +import NoneCharAtlas from './NoneCharAtlas'; +import StaticCharAtlas from './StaticCharAtlas'; + +const charAtlasImplementations = { + 'none': NoneCharAtlas, + 'static': StaticCharAtlas, +}; interface ICharAtlasCacheEntry { atlas: BaseCharAtlas; @@ -64,7 +70,7 @@ export function acquireCharAtlas( } const newEntry: ICharAtlasCacheEntry = { - atlas: new StaticCharAtlas( + atlas: new charAtlasImplementations[terminal.options.experimentalCharAtlas]( document, newConfig, ), diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index 24ed5a488f..c0c54783ce 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -17,6 +17,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number ansi: colors.ansi.slice(0, 16) }; return { + type: terminal.options.experimentalCharAtlas, devicePixelRatio: window.devicePixelRatio, scaledCharWidth, scaledCharHeight, @@ -35,7 +36,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean return false; } } - return a.devicePixelRatio === b.devicePixelRatio && + return a.type === b.type && + a.devicePixelRatio === b.devicePixelRatio && a.fontFamily === b.fontFamily && a.fontSize === b.fontSize && a.fontWeight === b.fontWeight && diff --git a/src/renderer/atlas/NoneCharAtlas.ts b/src/renderer/atlas/NoneCharAtlas.ts new file mode 100644 index 0000000000..1cbc9eea83 --- /dev/null +++ b/src/renderer/atlas/NoneCharAtlas.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + * + * A dummy CharAtlas implementation that always fails to draw characters. + */ + +import { IGlyphIdentifier } from './Types'; +import { ICharAtlasConfig } from '../../shared/atlas/Types'; +import BaseCharAtlas from './BaseCharAtlas'; + +export default class NoneCharAtlas extends BaseCharAtlas { + constructor(document: Document, config: ICharAtlasConfig) { + super(); + } + + public draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number + ): boolean { + return false; + } +} diff --git a/src/shared/atlas/Types.ts b/src/shared/atlas/Types.ts index 4a66d55428..2bec4194df 100644 --- a/src/shared/atlas/Types.ts +++ b/src/shared/atlas/Types.ts @@ -9,6 +9,7 @@ import { IColorSet } from '../Types'; export const CHAR_ATLAS_CELL_SPACING = 1; export interface ICharAtlasConfig { + type: 'none' | 'static'; devicePixelRatio: number; fontSize: number; fontFamily: string; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 5c9e95e5ec..a208b7edc0 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -61,6 +61,20 @@ declare module 'xterm' { */ enableBold?: boolean; + /** + * What character atlas implementation to use. The character atlas caches drawn characters, + * speeding up rendering significantly. However, it can introduce some minor rendering + * artifacts. + * + * - 'none': Don't use an atlas. + * - 'static': Generate an atlas when the terminal starts or is reconfigured. This atlas will + * only contain ASCII characters in 16 colors. + * + * Currently defaults to 'static'. This option may be removed in the future. If it is, passed + * parameters will be ignored. + */ + experimentalCharAtlas?: 'none' | 'static'; + /** * The font size used to render text. */ From 24feaabd9ac771526b66301d43366d71c27dc210 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Sat, 10 Mar 2018 21:33:09 -0800 Subject: [PATCH 03/15] Add a basic dynamic character atlas implementation This adds a very minimal implementation of DynamicCharAtlas using a LRU cache. I've got some optimizations that I'll add on top of this, but this proves the concept. --- demo/index.html | 1 + src/renderer/atlas/CharAtlasCache.ts | 2 + src/renderer/atlas/CharAtlasUtils.ts | 5 +- src/renderer/atlas/DynamicCharAtlas.ts | 195 +++++++++++++++++++++++++ src/renderer/atlas/StaticCharAtlas.ts | 4 +- src/shared/atlas/CharAtlasGenerator.ts | 15 +- src/shared/atlas/Types.ts | 2 +- tsconfig.json | 9 +- typings/xterm.d.ts | 6 +- 9 files changed, 226 insertions(+), 13 deletions(-) create mode 100644 src/renderer/atlas/DynamicCharAtlas.ts diff --git a/demo/index.html b/demo/index.html index e18614a48b..e4db41185d 100644 --- a/demo/index.html +++ b/demo/index.html @@ -58,6 +58,7 @@

Options

experimentalCharAtlas diff --git a/src/renderer/atlas/CharAtlasCache.ts b/src/renderer/atlas/CharAtlasCache.ts index 14037acf3e..c44f718aa2 100644 --- a/src/renderer/atlas/CharAtlasCache.ts +++ b/src/renderer/atlas/CharAtlasCache.ts @@ -8,12 +8,14 @@ import { IColorSet } from '../Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; import { generateConfig, configEquals } from './CharAtlasUtils'; import BaseCharAtlas from './BaseCharAtlas'; +import DynamicCharAtlas from './DynamicCharAtlas'; import NoneCharAtlas from './NoneCharAtlas'; import StaticCharAtlas from './StaticCharAtlas'; const charAtlasImplementations = { 'none': NoneCharAtlas, 'static': StaticCharAtlas, + 'dynamic': DynamicCharAtlas, }; interface ICharAtlasCacheEntry { diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index c0c54783ce..ded04085a9 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -8,13 +8,16 @@ import { IColorSet } from '../Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { + // null out some fields that don't matter const clonedColors = { foreground: colors.foreground, background: colors.background, cursor: null, cursorAccent: null, selection: null, - ansi: colors.ansi.slice(0, 16) + // For the static char atlas, we only use the first 16 colors, but we need all 256 for the + // dynamic character atlas. + ansi: colors.ansi, }; return { type: terminal.options.experimentalCharAtlas, diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts new file mode 100644 index 0000000000..8992fa8a06 --- /dev/null +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR } from './Types'; +import { ICharAtlasConfig } from '../../shared/atlas/Types'; +import BaseCharAtlas from './BaseCharAtlas'; +import { clearColor } from '../../shared/atlas/CharAtlasGenerator'; + +// In practice we're probably never going to exhaust a texture this large. For debugging purposes, +// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. +const TEXTURE_WIDTH = 1024; +const TEXTURE_HEIGHT = 1024; + +type GlyphCacheKey = string; + +/** + * Removes and returns the oldest element in a map. + */ +function mapShift(map: Map): [K, V] { + // Map guarantees insertion-order iteration. + const entry = map.entries().next().value; + if (entry === undefined) { + return undefined; + } + map.delete(entry[0]); + return entry; +} + +function getGlyphCacheKey(glyph: IGlyphIdentifier): GlyphCacheKey { + return `${glyph.bg}_${glyph.fg}_${glyph.bold ? 0 : 1}${glyph.dim ? 0 : 1}${glyph.char}`; +} + +export default class DynamicCharAtlas extends BaseCharAtlas { + // An ordered map that we're using to keep track of where each glyph is in the atlas texture. + // It's ordered so that we can determine when to remove the old entries. + private _cacheMap: Map = new Map(); + + // The texture that the atlas is drawn to + private _cacheCanvas: HTMLCanvasElement; + private _cacheCtx: CanvasRenderingContext2D; + + // A temporary canvas that glyphs are drawn to before being transfered over to the atlas. + private _tmpCanvas: HTMLCanvasElement; + private _tmpCtx: CanvasRenderingContext2D; + + // The number of characters stored in the atlas by width/height + private _capacity: number; + private _width: number; + private _height: number; + + constructor(document: Document, private _config: ICharAtlasConfig) { + super(); + this._cacheCanvas = document.createElement('canvas'); + this._cacheCanvas.width = TEXTURE_WIDTH; + this._cacheCanvas.height = TEXTURE_HEIGHT; + // The canvas needs alpha because we use clearColor to convert the background color to alpha. + this._cacheCtx = this._cacheCanvas.getContext('2d', {alpha: true}); + + this._tmpCanvas = document.createElement('canvas'); + this._tmpCanvas.width = this._config.scaledCharWidth; + this._tmpCanvas.height = this._config.scaledCharHeight; + this._tmpCtx = this._tmpCanvas.getContext('2d', {alpha: true}); + + this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth); + this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight); + this._capacity = this._width * this._height; + + // This is useful for debugging + // document.body.appendChild(this._cacheCanvas); + } + + public draw( + ctx: CanvasRenderingContext2D, + glyph: IGlyphIdentifier, + x: number, + y: number, + ): boolean { + const glyphKey = getGlyphCacheKey(glyph); + const index = this._cacheMap.get(glyphKey); + if (index != null) { + // move to end of insertion order, so this can behave like an LRU cache + this._cacheMap.delete(glyphKey); + this._cacheMap.set(glyphKey, index); + this._drawFromCache(ctx, index, x, y); + return true; + } else if (this._canCache(glyph)) { + let index; + if (this._cacheMap.size < this._capacity) { + index = this._cacheMap.size; + } else { + index = mapShift(this._cacheMap)[1]; + } + this._drawToCache(glyph, index); + this._cacheMap.set(glyphKey, index); + this._drawFromCache(ctx, index, x, y); + return true; + } else { + return false; + } + } + + private _canCache(glyph: IGlyphIdentifier): boolean { + // Only cache ascii and extended characters for now, to be safe. In the future, we could do + // something more complicated to determine the expected width of a character. + // + // If we switch the renderer over to webgl at some point, we may be able to use blending modes + // to draw overlapping glyphs from the atlas: + // https://github.com/servo/webrender/issues/464#issuecomment-255632875 + // https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html + return glyph.char.charCodeAt(0) < 256; + } + + private _toCoordinates(index: number): [number, number] { + return [ + (index % this._width) * this._config.scaledCharWidth, + Math.floor(index / this._width) * this._config.scaledCharHeight + ]; + } + + private _drawFromCache( + ctx: CanvasRenderingContext2D, + index: number, + x: number, + y: number + ): void { + const [cacheX, cacheY] = this._toCoordinates(index); + ctx.drawImage( + this._cacheCanvas, + cacheX, + cacheY, + this._config.scaledCharWidth, + this._config.scaledCharHeight, + x, + y, + this._config.scaledCharWidth, + this._config.scaledCharHeight, + ); + } + + // TODO: We do this (or something similar) in multiple places. We should split this off + // into a shared function. + private _drawToCache(glyph: IGlyphIdentifier, index: number): void { + this._tmpCtx.save(); + // no need to clear _tmpCtx, since we're going to draw a fully opaque background + + // draw the background + let backgroundColor = this._config.colors.background; + if (glyph.bg === INVERTED_DEFAULT_COLOR) { + backgroundColor = this._config.colors.foreground; + } else if (glyph.bg < 256) { + backgroundColor = this._config.colors.ansi[glyph.bg]; + } + this._tmpCtx.fillStyle = backgroundColor.css; + this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight); + + // draw the foreground/glyph + this._tmpCtx.font = + `${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; + if (glyph.bold) { + this._tmpCtx.font = `bold ${this._tmpCtx.font}`; + } + this._tmpCtx.textBaseline = 'top'; + + if (glyph.fg === INVERTED_DEFAULT_COLOR) { + this._tmpCtx.fillStyle = this._config.colors.background.css; + } else if (glyph.fg < 256) { + // 256 color support + this._tmpCtx.fillStyle = this._config.colors.ansi[glyph.fg].css; + } else { + this._tmpCtx.fillStyle = this._config.colors.foreground.css; + } + + // Apply alpha to dim the character + if (glyph.dim) { + this._tmpCtx.globalAlpha = DIM_OPACITY; + } + // Draw the character + this._tmpCtx.fillText(glyph.char, 0, 0); + this._tmpCtx.restore(); + + // clear the background from the character to avoid issues with drawing over the previous + // character if it extends past it's bounds + const imageData = this._tmpCtx.getImageData( + 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight, + ); + clearColor(imageData, backgroundColor); + + // copy the data from _tmpCanvas to _cacheCanvas + const [x, y] = this._toCoordinates(index); + // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us + this._cacheCtx.putImageData(imageData, x, y); + } +} diff --git a/src/renderer/atlas/StaticCharAtlas.ts b/src/renderer/atlas/StaticCharAtlas.ts index de808716ab..746369fc4a 100644 --- a/src/renderer/atlas/StaticCharAtlas.ts +++ b/src/renderer/atlas/StaticCharAtlas.ts @@ -6,7 +6,7 @@ import { DIM_OPACITY, IGlyphIdentifier } from './Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; import { CHAR_ATLAS_CELL_SPACING } from '../../shared/atlas/Types'; -import { generateCharAtlas } from '../../shared/atlas/CharAtlasGenerator'; +import { generateStaticCharAtlasTexture } from '../../shared/atlas/CharAtlasGenerator'; import BaseCharAtlas from './BaseCharAtlas'; export default class StaticCharAtlas extends BaseCharAtlas { @@ -24,7 +24,7 @@ export default class StaticCharAtlas extends BaseCharAtlas { } public async _doWarmUp(): Promise { - const result = generateCharAtlas(window, this._canvasFactory, this._config); + const result = generateStaticCharAtlasTexture(window, this._canvasFactory, this._config); if (result instanceof Promise) { this._texture = await result; } else { diff --git a/src/shared/atlas/CharAtlasGenerator.ts b/src/shared/atlas/CharAtlasGenerator.ts index fc83c7ce3f..874020c3d9 100644 --- a/src/shared/atlas/CharAtlasGenerator.ts +++ b/src/shared/atlas/CharAtlasGenerator.ts @@ -5,6 +5,7 @@ import { FontWeight } from 'xterm'; import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from './Types'; +import { IColor } from '../Types'; import { isFirefox } from '../utils/Browser'; declare const Promise: any; @@ -20,9 +21,9 @@ export interface IOffscreenCanvas { * Generates a char atlas. * @param context The window or worker context. * @param canvasFactory A function to generate a canvas with a width or height. - * @param request The config for the new char atlas. + * @param config The config for the new char atlas. */ -export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, config: ICharAtlasConfig): HTMLCanvasElement | Promise { +export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, config: ICharAtlasConfig): HTMLCanvasElement | Promise { const cellWidth = config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const cellHeight = config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; const canvas = canvasFactory( @@ -100,10 +101,7 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // Remove the background color from the image so characters may overlap - const r = config.colors.background.rgba >>> 24; - const g = config.colors.background.rgba >>> 16 & 0xFF; - const b = config.colors.background.rgba >>> 8 & 0xFF; - clearColor(charAtlasImageData, r, g, b); + clearColor(charAtlasImageData, config.colors.background); return context.createImageBitmap(charAtlasImageData); } @@ -111,7 +109,10 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number /** * Makes a partiicular rgb color in an ImageData completely transparent. */ -function clearColor(imageData: ImageData, r: number, g: number, b: number): void { +export function clearColor(imageData: ImageData, color: IColor): void { + const r = color.rgba >>> 24; + const g = color.rgba >>> 16 & 0xFF; + const b = color.rgba >>> 8 & 0xFF; for (let offset = 0; offset < imageData.data.length; offset += 4) { if (imageData.data[offset] === r && imageData.data[offset + 1] === g && diff --git a/src/shared/atlas/Types.ts b/src/shared/atlas/Types.ts index 2bec4194df..25eaa716e4 100644 --- a/src/shared/atlas/Types.ts +++ b/src/shared/atlas/Types.ts @@ -9,7 +9,7 @@ import { IColorSet } from '../Types'; export const CHAR_ATLAS_CELL_SPACING = 1; export interface ICharAtlasConfig { - type: 'none' | 'static'; + type: 'none' | 'static' | 'dynamic'; devicePixelRatio: number; fontSize: number; fontFamily: string; diff --git a/tsconfig.json b/tsconfig.json index 6d329658c5..7525c3def6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,14 @@ "compilerOptions": { "module": "commonjs", "target": "es5", - "lib": ["DOM", "ES5", "ScriptHost", "ES2015.Promise"], + "lib": [ + "DOM", + "ES5", + "ScriptHost", + "ES2015.Promise", + "ES2015.Collection", + "ES2015.Iterable" + ], "rootDir": "src", "outDir": "lib", "sourceMap": true, diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a208b7edc0..f7898d6e28 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -69,11 +69,15 @@ declare module 'xterm' { * - 'none': Don't use an atlas. * - 'static': Generate an atlas when the terminal starts or is reconfigured. This atlas will * only contain ASCII characters in 16 colors. + * - 'dynamic': Generate an atlas using a LRU cache as characters are requested. Limited to + * ASCII characters (for now), but supports 256 colors. For characters covered by the static + * cache, it's slightly slower in comparison, since there's more overhead involved in + * managing the cache. * * Currently defaults to 'static'. This option may be removed in the future. If it is, passed * parameters will be ignored. */ - experimentalCharAtlas?: 'none' | 'static'; + experimentalCharAtlas?: 'none' | 'static' | 'dynamic'; /** * The font size used to render text. From c8b500c182797e22e153faca84469b7613a4f526 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Sun, 11 Mar 2018 12:36:50 -0700 Subject: [PATCH 04/15] Skip drawing whitespace from DynamicCharAtlas When we remove the background from a glyph, we can keep track of if we made all the pixels fully transparent. If we did, we can just skip future calls to _drawFromCache, since it would be a noop anyways. --- src/renderer/atlas/DynamicCharAtlas.ts | 40 +++++++++++++++++--------- src/shared/atlas/CharAtlasGenerator.ts | 7 ++++- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 8992fa8a06..4b0d9f52d4 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -15,6 +15,11 @@ const TEXTURE_HEIGHT = 1024; type GlyphCacheKey = string; +interface IGlyphCacheValue { + index: number; + isEmpty: boolean; +} + /** * Removes and returns the oldest element in a map. */ @@ -35,7 +40,7 @@ function getGlyphCacheKey(glyph: IGlyphIdentifier): GlyphCacheKey { export default class DynamicCharAtlas extends BaseCharAtlas { // An ordered map that we're using to keep track of where each glyph is in the atlas texture. // It's ordered so that we can determine when to remove the old entries. - private _cacheMap: Map = new Map(); + private _cacheMap: Map = new Map(); // The texture that the atlas is drawn to private _cacheCanvas: HTMLCanvasElement; @@ -78,23 +83,23 @@ export default class DynamicCharAtlas extends BaseCharAtlas { y: number, ): boolean { const glyphKey = getGlyphCacheKey(glyph); - const index = this._cacheMap.get(glyphKey); - if (index != null) { + const cacheValue = this._cacheMap.get(glyphKey); + if (cacheValue != null) { // move to end of insertion order, so this can behave like an LRU cache this._cacheMap.delete(glyphKey); - this._cacheMap.set(glyphKey, index); - this._drawFromCache(ctx, index, x, y); + this._cacheMap.set(glyphKey, cacheValue); + this._drawFromCache(ctx, cacheValue, x, y); return true; } else if (this._canCache(glyph)) { let index; if (this._cacheMap.size < this._capacity) { index = this._cacheMap.size; } else { - index = mapShift(this._cacheMap)[1]; + index = mapShift(this._cacheMap)[1].index; } - this._drawToCache(glyph, index); - this._cacheMap.set(glyphKey, index); - this._drawFromCache(ctx, index, x, y); + const cacheValue = this._drawToCache(glyph, index); + this._cacheMap.set(glyphKey, cacheValue); + this._drawFromCache(ctx, cacheValue, x, y); return true; } else { return false; @@ -121,11 +126,15 @@ export default class DynamicCharAtlas extends BaseCharAtlas { private _drawFromCache( ctx: CanvasRenderingContext2D, - index: number, + cacheValue: IGlyphCacheValue, x: number, y: number ): void { - const [cacheX, cacheY] = this._toCoordinates(index); + // We don't actually need to do anything if this is whitespace. + if (cacheValue.isEmpty) { + return; + } + const [cacheX, cacheY] = this._toCoordinates(cacheValue.index); ctx.drawImage( this._cacheCanvas, cacheX, @@ -141,7 +150,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { // TODO: We do this (or something similar) in multiple places. We should split this off // into a shared function. - private _drawToCache(glyph: IGlyphIdentifier, index: number): void { + private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue { this._tmpCtx.save(); // no need to clear _tmpCtx, since we're going to draw a fully opaque background @@ -185,11 +194,16 @@ export default class DynamicCharAtlas extends BaseCharAtlas { const imageData = this._tmpCtx.getImageData( 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight, ); - clearColor(imageData, backgroundColor); + const isEmpty = clearColor(imageData, backgroundColor); // copy the data from _tmpCanvas to _cacheCanvas const [x, y] = this._toCoordinates(index); // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us this._cacheCtx.putImageData(imageData, x, y); + + return { + index, + isEmpty, + }; } } diff --git a/src/shared/atlas/CharAtlasGenerator.ts b/src/shared/atlas/CharAtlasGenerator.ts index 874020c3d9..3e62c39dd5 100644 --- a/src/shared/atlas/CharAtlasGenerator.ts +++ b/src/shared/atlas/CharAtlasGenerator.ts @@ -108,8 +108,10 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( /** * Makes a partiicular rgb color in an ImageData completely transparent. + * @returns True if the result is "empty", meaning all pixels are fully transparent. */ -export function clearColor(imageData: ImageData, color: IColor): void { +export function clearColor(imageData: ImageData, color: IColor): boolean { + let isEmpty = true; const r = color.rgba >>> 24; const g = color.rgba >>> 16 & 0xFF; const b = color.rgba >>> 8 & 0xFF; @@ -118,8 +120,11 @@ export function clearColor(imageData: ImageData, color: IColor): void { imageData.data[offset + 1] === g && imageData.data[offset + 2] === b) { imageData.data[offset + 3] = 0; + } else { + isEmpty = false; } } + return isEmpty; } function getFont(fontWeight: FontWeight, config: ICharAtlasConfig): string { From 8b3b7535c6f720c754d51923e06bf40fe526973d Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Fri, 16 Mar 2018 23:24:45 -0700 Subject: [PATCH 05/15] Add a new LRUMap class, remove uses of Map LRUMap implements the parts of an ordered map that we need to efficiently implement DynamicCharAtlas. It's more code, but there's a few advantages of this approach: - Map isn't available on some older browsers, so this removes the need for a polyfill. - Moving an item to the end of the map's iteration order now only requires unlinking and linking a linked-list node, whereas before we had to delete and re-insert our value. - Peeking at the oldest entry in the map no longer requires allocating and destroying an iterator. - We can preallocate the linked-list nodes we want to improve cache locality. Similarly, we can recycle linked-list nodes to reduce allocations and the GC pauses those allocations may cause. - LRUMap seems to give slightly better results in Chrome's profiler than Map did. We now spend about 5% of our time on map operations instead of about 10%. - In my (limited) testing, it doesn't look like LRUMap is slowing down over time. Map appeared to get slightly slower the longer I ran the terminal for, either due to memory fragmentation or some sort of leak. I still need to write some tests for LRUMap, but I've been using this implementation for the last hour without problems. --- src/renderer/atlas/DynamicCharAtlas.ts | 33 ++----- src/renderer/atlas/LRUMap.ts | 121 +++++++++++++++++++++++++ tsconfig.json | 4 +- 3 files changed, 131 insertions(+), 27 deletions(-) create mode 100644 src/renderer/atlas/LRUMap.ts diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 4b0d9f52d4..25dbca182d 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -7,40 +7,26 @@ import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR } from './Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; import BaseCharAtlas from './BaseCharAtlas'; import { clearColor } from '../../shared/atlas/CharAtlasGenerator'; +import LRUMap from './LRUMap'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. const TEXTURE_WIDTH = 1024; const TEXTURE_HEIGHT = 1024; -type GlyphCacheKey = string; - interface IGlyphCacheValue { index: number; isEmpty: boolean; } -/** - * Removes and returns the oldest element in a map. - */ -function mapShift(map: Map): [K, V] { - // Map guarantees insertion-order iteration. - const entry = map.entries().next().value; - if (entry === undefined) { - return undefined; - } - map.delete(entry[0]); - return entry; -} - -function getGlyphCacheKey(glyph: IGlyphIdentifier): GlyphCacheKey { +function getGlyphCacheKey(glyph: IGlyphIdentifier): string { return `${glyph.bg}_${glyph.fg}_${glyph.bold ? 0 : 1}${glyph.dim ? 0 : 1}${glyph.char}`; } export default class DynamicCharAtlas extends BaseCharAtlas { // An ordered map that we're using to keep track of where each glyph is in the atlas texture. // It's ordered so that we can determine when to remove the old entries. - private _cacheMap: Map = new Map(); + private _cacheMap: LRUMap; // The texture that the atlas is drawn to private _cacheCanvas: HTMLCanvasElement; @@ -51,7 +37,6 @@ export default class DynamicCharAtlas extends BaseCharAtlas { private _tmpCtx: CanvasRenderingContext2D; // The number of characters stored in the atlas by width/height - private _capacity: number; private _width: number; private _height: number; @@ -70,7 +55,9 @@ export default class DynamicCharAtlas extends BaseCharAtlas { this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth); this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight); - this._capacity = this._width * this._height; + const capacity = this._width * this._height; + this._cacheMap = new LRUMap(capacity); + this._cacheMap.prealloc(capacity); // This is useful for debugging // document.body.appendChild(this._cacheCanvas); @@ -85,17 +72,15 @@ export default class DynamicCharAtlas extends BaseCharAtlas { const glyphKey = getGlyphCacheKey(glyph); const cacheValue = this._cacheMap.get(glyphKey); if (cacheValue != null) { - // move to end of insertion order, so this can behave like an LRU cache - this._cacheMap.delete(glyphKey); - this._cacheMap.set(glyphKey, cacheValue); this._drawFromCache(ctx, cacheValue, x, y); return true; } else if (this._canCache(glyph)) { let index; - if (this._cacheMap.size < this._capacity) { + if (this._cacheMap.size < this._cacheMap.capacity) { index = this._cacheMap.size; } else { - index = mapShift(this._cacheMap)[1].index; + // we're out of space, so our call to set will delete this item + index = this._cacheMap.peek().index; } const cacheValue = this._drawToCache(glyph, index); this._cacheMap.set(glyphKey, cacheValue); diff --git a/src/renderer/atlas/LRUMap.ts b/src/renderer/atlas/LRUMap.ts new file mode 100644 index 0000000000..942641780b --- /dev/null +++ b/src/renderer/atlas/LRUMap.ts @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +interface ILinkedListNode { + prev: ILinkedListNode, + next: ILinkedListNode, + key: string, + value: T, +} + +export default class LRUMap { + private _map = {}; + private _head: ILinkedListNode = null; + private _tail: ILinkedListNode = null; + private _nodePool: ILinkedListNode[] = []; + public size: number = 0; + + constructor(public capacity: number) { } + + private _unlinkNode(node: ILinkedListNode): void { + const prev = node.prev; + const next = node.next; + if (node === this._head) { + this._head = next; + } + if (node === this._tail) { + this._tail = prev; + } + if (prev !== null) { + prev.next = next; + } + if (next !== null) { + next.prev = prev; + } + } + + private _appendNode(node: ILinkedListNode): void { + node.prev = this._tail; + node.next = null; + this._tail = node; + if (this._head === null) { + this._head = node; + } + } + + /** + * Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that + * they're more likely to live next to each other in memory, which seems to improve performance. + * + * Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for + * large maps. + */ + public prealloc(count: number) { + const nodePool = this._nodePool; + for (let i = 0; i < count; i++) { + nodePool.push({ + prev: null, + next: null, + key: null, + value: null, + }); + } + } + + public get(key: string): T | null { + // This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However, + // it's faster than calling hasOwnProperty, and in our case, it would never overlap. + const node = this._map[key]; + if (node !== undefined) { + this._unlinkNode(node); + this._appendNode(node); + return node.value; + } + return null; + } + + public peek(): T | null { + const head = this._head; + return head === null ? null : head.value; + } + + public set(key: string, value: T): void { + // This is unsafe: See note above. + let node = this._map[key]; + if (node !== undefined) { + // already exists, we just need to mutate it and move it to the end of the list + node = this._map[key]; + this._unlinkNode(node); + node.value = value; + } else if (this.size >= this.capacity) { + // we're out of space: recycle the head node, move it to the tail + node = this._head; + this._unlinkNode(node); + delete this._map[node.key]; + node.key = key; + node.value = value; + this._map[key] = node; + } else { + // make a new element + const nodePool = this._nodePool; + if (nodePool.length > 0) { + // use a preallocated node if we can + node = nodePool.pop(); + node.key = key; + node.value = value; + } else { + node = { + prev: null, + next: null, + key, + value, + }; + } + this._map[key] = node; + this.size++; + } + this._appendNode(node); + } +} diff --git a/tsconfig.json b/tsconfig.json index 7525c3def6..e56930e639 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,9 +6,7 @@ "DOM", "ES5", "ScriptHost", - "ES2015.Promise", - "ES2015.Collection", - "ES2015.Iterable" + "ES2015.Promise" ], "rootDir": "src", "outDir": "lib", From ac7db87fd0b4d7399ce2ea210f9388d0ed71d0d5 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Sat, 17 Mar 2018 23:54:18 -0700 Subject: [PATCH 06/15] Remove use of Promises in {Base,Static}CharAtlas Nothing actually cares about the return value, so we can avoid depending on `Promise` by not returning a promise. This should make this work for IE11 (though I haven't tested that). I tried this in Chrome and Firefox to test both codepaths (ImageBitmap and HTMLCanvasElement). --- src/renderer/atlas/BaseCharAtlas.ts | 14 ++++++-------- src/renderer/atlas/StaticCharAtlas.ts | 13 +++++++------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/renderer/atlas/BaseCharAtlas.ts b/src/renderer/atlas/BaseCharAtlas.ts index 21d5f833e3..8656f2123d 100644 --- a/src/renderer/atlas/BaseCharAtlas.ts +++ b/src/renderer/atlas/BaseCharAtlas.ts @@ -6,26 +6,24 @@ import { IGlyphIdentifier } from './Types'; export default abstract class BaseCharAtlas { - private _didWarmUp: Promise; + private _didWarmUp: boolean = false; /** * Perform any work needed to warm the cache before it can be used. May be called multiple times. * Implement _doWarmUp instead if you only want to get called once. */ - public warmUp(): Promise { - if (this._didWarmUp == null) { - this._didWarmUp = this._doWarmUp(); + public warmUp(): void { + if (!this._didWarmUp) { + this._doWarmUp(); + this._didWarmUp = true; } - return this._didWarmUp; } /** * Perform any work needed to warm the cache before it can be used. Used by the default * implementation of warmUp(), and will only be called once. */ - protected _doWarmUp(): Promise { - return Promise.resolve(); - } + protected _doWarmUp(): void { } /** * May be called before warmUp finishes, however it is okay for the implementation to diff --git a/src/renderer/atlas/StaticCharAtlas.ts b/src/renderer/atlas/StaticCharAtlas.ts index 746369fc4a..f68c7c6c67 100644 --- a/src/renderer/atlas/StaticCharAtlas.ts +++ b/src/renderer/atlas/StaticCharAtlas.ts @@ -4,8 +4,7 @@ */ import { DIM_OPACITY, IGlyphIdentifier } from './Types'; -import { ICharAtlasConfig } from '../../shared/atlas/Types'; -import { CHAR_ATLAS_CELL_SPACING } from '../../shared/atlas/Types'; +import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from '../../shared/atlas/Types'; import { generateStaticCharAtlasTexture } from '../../shared/atlas/CharAtlasGenerator'; import BaseCharAtlas from './BaseCharAtlas'; @@ -23,12 +22,14 @@ export default class StaticCharAtlas extends BaseCharAtlas { return canvas; } - public async _doWarmUp(): Promise { + public _doWarmUp() { const result = generateStaticCharAtlasTexture(window, this._canvasFactory, this._config); - if (result instanceof Promise) { - this._texture = await result; - } else { + if (result instanceof HTMLCanvasElement) { this._texture = result; + } else { + result.then(texture => { + this._texture = texture + }); } } From 9c0ce1d7424886f90f0f25035c9e3f87ba719d16 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Sun, 18 Mar 2018 14:41:46 -0700 Subject: [PATCH 07/15] Fix DynamicCharAtlas with transparency There were two bugs: - We need to clear the background of _tmpCtx instead of just drawing on top if the background color is transparent. - We should set the background to fully transparent if it's partially transparent, to avoid drawing the transparent background twice. --- demo/index.html | 3 +++ demo/main.js | 12 ++++++++--- src/renderer/Renderer.ts | 1 + src/renderer/atlas/DynamicCharAtlas.ts | 28 ++++++++++++++++++++++++-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/demo/index.html b/demo/index.html index e4db41185d..29b6fcf0f1 100644 --- a/demo/index.html +++ b/demo/index.html @@ -26,6 +26,9 @@

Options

+

+ +