mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": ["DOM", "ES5", "ScriptHost", "ES2015.Promise"],
|
||||
"rootDir": "src",
|
||||
"outDir": "lib",
|
||||
"sourceMap": true,
|
||||
|
||||
Reference in New Issue
Block a user