From 6f21385cd8f730358c40f1e95f6fae4eb78b036c Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 7 Mar 2018 22:41:48 -0800 Subject: [PATCH] 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 | 68 +++--------- 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, 187 insertions(+), 72 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 fbffd547..1747c428 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; + 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 + ); - // 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 { + 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 00000000..21d5f833 --- /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 2e417c09..ea887a1a 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 00000000..de808716 --- /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 34f01d39..fe52f5a5 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 ffd9ab68..6d329658 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,