2 * Copyright (c) 2017 The xterm.js authors. All rights reserved.
6 import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
7 import { IGlyphIdentifier, ICharAtlasConfig } from 'browser/renderer/atlas/Types';
8 import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
9 import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
10 import { LRUMap } from 'browser/renderer/atlas/LRUMap';
11 import { isFirefox, isSafari } from 'common/Platform';
12 import { IColor } from 'browser/Types';
13 import { throwIfFalsy } from 'browser/renderer/RendererUtils';
15 // In practice we're probably never going to exhaust a texture this large. For debugging purposes,
16 // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
17 const TEXTURE_WIDTH = 1024;
18 const TEXTURE_HEIGHT = 1024;
20 const TRANSPARENT_COLOR = {
21 css: 'rgba(0, 0, 0, 0)',
25 // Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the
26 // cache in a single frame, give up on trying to cache anything else, and try to finish the current
29 // This helps to limit the amount of damage a program can do when it would otherwise thrash the
31 const FRAME_CACHE_DRAW_LIMIT = 100;
34 * The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
35 * the operation as window.createImageBitmap is asynchronous.
37 const GLYPH_BITMAP_COMMIT_DELAY = 100;
39 interface IGlyphCacheValue {
45 export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
46 // Note that this only returns a valid key when code < 256
48 // 0b00000000000000000000000000000001: italic (1)
49 // 0b00000000000000000000000000000010: dim (1)
50 // 0b00000000000000000000000000000100: bold (1)
51 // 0b00000000000000000000111111111000: fg (9)
52 // 0b00000000000111111111000000000000: bg (9)
53 // 0b00011111111000000000000000000000: code (8)
54 // 0b11100000000000000000000000000000: unused (3)
55 return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
58 export class DynamicCharAtlas extends BaseCharAtlas {
59 // An ordered map that we're using to keep track of where each glyph is in the atlas texture.
60 // It's ordered so that we can determine when to remove the old entries.
61 private _cacheMap: LRUMap<IGlyphCacheValue>;
63 // The texture that the atlas is drawn to
64 private _cacheCanvas: HTMLCanvasElement;
65 private _cacheCtx: CanvasRenderingContext2D;
67 // A temporary context that glyphs are drawn to before being transfered to the atlas.
68 private _tmpCtx: CanvasRenderingContext2D;
70 // The number of characters stored in the atlas by width/height
71 private _width: number;
72 private _height: number;
74 private _drawToCacheCount: number = 0;
76 // An array of glyph keys that are waiting on the bitmap to be generated.
77 private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
79 // The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
80 private _bitmapCommitTimeout: number | null = null;
82 // The bitmap to draw from, this is much faster on other browsers than others.
83 private _bitmap: ImageBitmap | null = null;
85 constructor(document: Document, private _config: ICharAtlasConfig) {
87 this._cacheCanvas = document.createElement('canvas');
88 this._cacheCanvas.width = TEXTURE_WIDTH;
89 this._cacheCanvas.height = TEXTURE_HEIGHT;
90 // The canvas needs alpha because we use clearColor to convert the background color to alpha.
91 // It might also contain some characters with transparent backgrounds if allowTransparency is
93 this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', {alpha: true}));
95 const tmpCanvas = document.createElement('canvas');
96 tmpCanvas.width = this._config.scaledCharWidth;
97 tmpCanvas.height = this._config.scaledCharHeight;
98 this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}));
100 this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
101 this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
102 const capacity = this._width * this._height;
103 this._cacheMap = new LRUMap(capacity);
104 this._cacheMap.prealloc(capacity);
106 // This is useful for debugging
107 // document.body.appendChild(this._cacheCanvas);
110 public dispose(): void {
111 if (this._bitmapCommitTimeout !== null) {
112 window.clearTimeout(this._bitmapCommitTimeout);
113 this._bitmapCommitTimeout = null;
117 public beginFrame(): void {
118 this._drawToCacheCount = 0;
122 ctx: CanvasRenderingContext2D,
123 glyph: IGlyphIdentifier,
127 // Space is always an empty cell, special case this as it's so common
128 if (glyph.code === 32) {
132 // Exit early for uncachable glyphs
133 if (!this._canCache(glyph)) {
137 const glyphKey = getGlyphCacheKey(glyph);
138 const cacheValue = this._cacheMap.get(glyphKey);
139 if (cacheValue !== null && cacheValue !== undefined) {
140 this._drawFromCache(ctx, cacheValue, x, y);
142 } else if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
144 if (this._cacheMap.size < this._cacheMap.capacity) {
145 index = this._cacheMap.size;
147 // we're out of space, so our call to set will delete this item
148 index = this._cacheMap.peek()!.index;
150 const cacheValue = this._drawToCache(glyph, index);
151 this._cacheMap.set(glyphKey, cacheValue);
152 this._drawFromCache(ctx, cacheValue, x, y);
158 private _canCache(glyph: IGlyphIdentifier): boolean {
159 // Only cache ascii and extended characters for now, to be safe. In the future, we could do
160 // something more complicated to determine the expected width of a character.
162 // If we switch the renderer over to webgl at some point, we may be able to use blending modes
163 // to draw overlapping glyphs from the atlas:
164 // https://github.com/servo/webrender/issues/464#issuecomment-255632875
165 // https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html
166 return glyph.code < 256;
169 private _toCoordinateX(index: number): number {
170 return (index % this._width) * this._config.scaledCharWidth;
173 private _toCoordinateY(index: number): number {
174 return Math.floor(index / this._width) * this._config.scaledCharHeight;
177 private _drawFromCache(
178 ctx: CanvasRenderingContext2D,
179 cacheValue: IGlyphCacheValue,
183 // We don't actually need to do anything if this is whitespace.
184 if (cacheValue.isEmpty) {
187 const cacheX = this._toCoordinateX(cacheValue.index);
188 const cacheY = this._toCoordinateY(cacheValue.index);
190 cacheValue.inBitmap ? this._bitmap! : this._cacheCanvas,
193 this._config.scaledCharWidth,
194 this._config.scaledCharHeight,
197 this._config.scaledCharWidth,
198 this._config.scaledCharHeight
202 private _getColorFromAnsiIndex(idx: number): IColor {
203 if (idx < this._config.colors.ansi.length) {
204 return this._config.colors.ansi[idx];
206 return DEFAULT_ANSI_COLORS[idx];
209 private _getBackgroundColor(glyph: IGlyphIdentifier): IColor {
210 if (this._config.allowTransparency) {
211 // The background color might have some transparency, so we need to render it as fully
212 // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
213 // around the anti-aliased edges of the glyph, and it would look too dark.
214 return TRANSPARENT_COLOR;
215 } else if (glyph.bg === INVERTED_DEFAULT_COLOR) {
216 return this._config.colors.foreground;
217 } else if (glyph.bg < 256) {
218 return this._getColorFromAnsiIndex(glyph.bg);
220 return this._config.colors.background;
223 private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
224 if (glyph.fg === INVERTED_DEFAULT_COLOR) {
225 return this._config.colors.background;
226 } else if (glyph.fg < 256) {
228 return this._getColorFromAnsiIndex(glyph.fg);
230 return this._config.colors.foreground;
233 // TODO: We do this (or something similar) in multiple places. We should split this off
234 // into a shared function.
235 private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue {
236 this._drawToCacheCount++;
240 // draw the background
241 const backgroundColor = this._getBackgroundColor(glyph);
242 // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
243 // transparency in backgroundColor
244 this._tmpCtx.globalCompositeOperation = 'copy';
245 this._tmpCtx.fillStyle = backgroundColor.css;
246 this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
247 this._tmpCtx.globalCompositeOperation = 'source-over';
249 // draw the foreground/glyph
250 const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
251 const fontStyle = glyph.italic ? 'italic' : '';
253 `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
254 this._tmpCtx.textBaseline = 'middle';
256 this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
258 // Apply alpha to dim the character
260 this._tmpCtx.globalAlpha = DIM_OPACITY;
262 // Draw the character
263 this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2);
264 this._tmpCtx.restore();
266 // clear the background from the character to avoid issues with drawing over the previous
267 // character if it extends past it's bounds
268 const imageData = this._tmpCtx.getImageData(
269 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
272 if (!this._config.allowTransparency) {
273 isEmpty = clearColor(imageData, backgroundColor);
276 // copy the data from imageData to _cacheCanvas
277 const x = this._toCoordinateX(index);
278 const y = this._toCoordinateY(index);
279 // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
280 this._cacheCtx.putImageData(imageData, x, y);
282 // Add the glyph and queue it to the bitmap (if the browser supports it)
288 this._addGlyphToBitmap(cacheValue);
293 private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
294 // Support is patchy for createImageBitmap at the moment, pass a canvas back
295 // if support is lacking as drawImage works there too. Firefox is also
296 // included here as ImageBitmap appears both buggy and has horrible
297 // performance (tested on v55).
298 if (!('createImageBitmap' in window) || isFirefox || isSafari) {
302 // Add the glyph to the queue
303 this._glyphsWaitingOnBitmap.push(cacheValue);
305 // Check if bitmap generation timeout already exists
306 if (this._bitmapCommitTimeout !== null) {
310 this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
313 private _generateBitmap(): void {
314 const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
315 this._glyphsWaitingOnBitmap = [];
316 window.createImageBitmap(this._cacheCanvas).then(bitmap => {
318 this._bitmap = bitmap;
320 // Mark all new glyphs as in bitmap, excluding glyphs that came in after
321 // the bitmap was requested
322 for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
323 const value = glyphsMovingToBitmap[i];
324 // It doesn't matter if the value was already evicted, it will be
325 // released from memory after this block if so.
326 value.inBitmap = true;
329 this._bitmapCommitTimeout = null;
333 // This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with
334 // `new NoneCharAtlas`.
335 export class NoneCharAtlas extends BaseCharAtlas {
336 constructor(document: Document, config: ICharAtlasConfig) {
341 ctx: CanvasRenderingContext2D,
342 glyph: IGlyphIdentifier,
351 * Makes a partiicular rgb color in an ImageData completely transparent.
352 * @returns True if the result is "empty", meaning all pixels are fully transparent.
354 function clearColor(imageData: ImageData, color: IColor): boolean {
356 const r = color.rgba >>> 24;
357 const g = color.rgba >>> 16 & 0xFF;
358 const b = color.rgba >>> 8 & 0xFF;
359 for (let offset = 0; offset < imageData.data.length; offset += 4) {
360 if (imageData.data[offset] === r &&
361 imageData.data[offset + 1] === g &&
362 imageData.data[offset + 2] === b) {
363 imageData.data[offset + 3] = 0;