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
+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;
}