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.
This commit is contained in:
Benjamin Woodruff
2018-04-15 18:03:55 -07:00
parent 83f9d59d91
commit 6f21385cd8
6 changed files with 187 additions and 72 deletions
+12 -56
View File
@@ -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);
}
/**
+46
View File
@@ -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<any>;
/**
* 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<any> {
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<any> {
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;
}
@@ -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<ImageBitmap>;
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<ImageBitmap> {
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;
}
+100
View File
@@ -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<void> {
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;
}
}
+8
View File
@@ -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;
}
+1
View File
@@ -2,6 +2,7 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": ["DOM", "ES5", "ScriptHost", "ES2015.Promise"],
"rootDir": "src",
"outDir": "lib",
"sourceMap": true,