xterm
[VSoRC/.git] / node_modules / xterm / src / browser / renderer / atlas / DynamicCharAtlas.ts
1 /**
2  * Copyright (c) 2017 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
5
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';
14
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;
19
20 const TRANSPARENT_COLOR = {
21   css: 'rgba(0, 0, 0, 0)',
22   rgba: 0
23 };
24
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
27 // frame ASAP.
28 //
29 // This helps to limit the amount of damage a program can do when it would otherwise thrash the
30 // cache.
31 const FRAME_CACHE_DRAW_LIMIT = 100;
32
33 /**
34  * The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
35  * the operation as window.createImageBitmap is asynchronous.
36  */
37 const GLYPH_BITMAP_COMMIT_DELAY = 100;
38
39 interface IGlyphCacheValue {
40   index: number;
41   isEmpty: boolean;
42   inBitmap: boolean;
43 }
44
45 export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
46   // Note that this only returns a valid key when code < 256
47   // Layout:
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);
56 }
57
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>;
62
63   // The texture that the atlas is drawn to
64   private _cacheCanvas: HTMLCanvasElement;
65   private _cacheCtx: CanvasRenderingContext2D;
66
67   // A temporary context that glyphs are drawn to before being transfered to the atlas.
68   private _tmpCtx: CanvasRenderingContext2D;
69
70   // The number of characters stored in the atlas by width/height
71   private _width: number;
72   private _height: number;
73
74   private _drawToCacheCount: number = 0;
75
76   // An array of glyph keys that are waiting on the bitmap to be generated.
77   private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
78
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;
81
82   // The bitmap to draw from, this is much faster on other browsers than others.
83   private _bitmap: ImageBitmap | null = null;
84
85   constructor(document: Document, private _config: ICharAtlasConfig) {
86     super();
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
92     // set.
93     this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', {alpha: true}));
94
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}));
99
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);
105
106     // This is useful for debugging
107     // document.body.appendChild(this._cacheCanvas);
108   }
109
110   public dispose(): void {
111     if (this._bitmapCommitTimeout !== null) {
112       window.clearTimeout(this._bitmapCommitTimeout);
113       this._bitmapCommitTimeout = null;
114     }
115   }
116
117   public beginFrame(): void {
118     this._drawToCacheCount = 0;
119   }
120
121   public draw(
122     ctx: CanvasRenderingContext2D,
123     glyph: IGlyphIdentifier,
124     x: number,
125     y: number
126   ): boolean {
127     // Space is always an empty cell, special case this as it's so common
128     if (glyph.code === 32) {
129       return true;
130     }
131
132     // Exit early for uncachable glyphs
133     if (!this._canCache(glyph)) {
134       return false;
135     }
136
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);
141       return true;
142     } else if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
143       let index;
144       if (this._cacheMap.size < this._cacheMap.capacity) {
145         index = this._cacheMap.size;
146       } else {
147         // we're out of space, so our call to set will delete this item
148         index = this._cacheMap.peek()!.index;
149       }
150       const cacheValue = this._drawToCache(glyph, index);
151       this._cacheMap.set(glyphKey, cacheValue);
152       this._drawFromCache(ctx, cacheValue, x, y);
153       return true;
154     }
155     return false;
156   }
157
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.
161     //
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;
167   }
168
169   private _toCoordinateX(index: number): number {
170     return (index % this._width) * this._config.scaledCharWidth;
171   }
172
173   private _toCoordinateY(index: number): number {
174     return Math.floor(index / this._width) * this._config.scaledCharHeight;
175   }
176
177   private _drawFromCache(
178     ctx: CanvasRenderingContext2D,
179     cacheValue: IGlyphCacheValue,
180     x: number,
181     y: number
182   ): void {
183     // We don't actually need to do anything if this is whitespace.
184     if (cacheValue.isEmpty) {
185       return;
186     }
187     const cacheX = this._toCoordinateX(cacheValue.index);
188     const cacheY = this._toCoordinateY(cacheValue.index);
189     ctx.drawImage(
190       cacheValue.inBitmap ? this._bitmap! : this._cacheCanvas,
191       cacheX,
192       cacheY,
193       this._config.scaledCharWidth,
194       this._config.scaledCharHeight,
195       x,
196       y,
197       this._config.scaledCharWidth,
198       this._config.scaledCharHeight
199     );
200   }
201
202   private _getColorFromAnsiIndex(idx: number): IColor {
203     if (idx < this._config.colors.ansi.length) {
204       return this._config.colors.ansi[idx];
205     }
206     return DEFAULT_ANSI_COLORS[idx];
207   }
208
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);
219     }
220     return this._config.colors.background;
221   }
222
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) {
227       // 256 color support
228       return this._getColorFromAnsiIndex(glyph.fg);
229     }
230     return this._config.colors.foreground;
231   }
232
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++;
237
238     this._tmpCtx.save();
239
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';
248
249     // draw the foreground/glyph
250     const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
251     const fontStyle = glyph.italic ? 'italic' : '';
252     this._tmpCtx.font =
253       `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
254     this._tmpCtx.textBaseline = 'middle';
255
256     this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
257
258     // Apply alpha to dim the character
259     if (glyph.dim) {
260       this._tmpCtx.globalAlpha = DIM_OPACITY;
261     }
262     // Draw the character
263     this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2);
264     this._tmpCtx.restore();
265
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
270     );
271     let isEmpty = false;
272     if (!this._config.allowTransparency) {
273       isEmpty = clearColor(imageData, backgroundColor);
274     }
275
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);
281
282     // Add the glyph and queue it to the bitmap (if the browser supports it)
283     const cacheValue = {
284       index,
285       isEmpty,
286       inBitmap: false
287     };
288     this._addGlyphToBitmap(cacheValue);
289
290     return cacheValue;
291   }
292
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) {
299       return;
300     }
301
302     // Add the glyph to the queue
303     this._glyphsWaitingOnBitmap.push(cacheValue);
304
305     // Check if bitmap generation timeout already exists
306     if (this._bitmapCommitTimeout !== null) {
307       return;
308     }
309
310     this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
311   }
312
313   private _generateBitmap(): void {
314     const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
315     this._glyphsWaitingOnBitmap = [];
316     window.createImageBitmap(this._cacheCanvas).then(bitmap => {
317       // Set bitmap
318       this._bitmap = bitmap;
319
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;
327       }
328     });
329     this._bitmapCommitTimeout = null;
330   }
331 }
332
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) {
337     super();
338   }
339
340   public draw(
341     ctx: CanvasRenderingContext2D,
342     glyph: IGlyphIdentifier,
343     x: number,
344     y: number
345   ): boolean {
346     return false;
347   }
348 }
349
350 /**
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.
353  */
354 function clearColor(imageData: ImageData, color: IColor): boolean {
355   let isEmpty = true;
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;
364     } else {
365       isEmpty = false;
366     }
367   }
368   return isEmpty;
369 }