Skip to content

Commit 4717619

Browse files
authored
Merge pull request #1327 from bgw/dynamic-char-atlas
Modularize the character atlas system, add a LRU-cache based dynamic character atlas
2 parents 7334e6a + 7f761b5 commit 4717619

19 files changed

+730
-76
lines changed

demo/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ <h2>Options</h2>
2626
<p>
2727
<label><input type="checkbox" id="option-mac-option-is-meta"> macOptionIsMeta</label>
2828
</p>
29+
<p>
30+
<label><input type="checkbox" id="option-transparency"> transparency</label>
31+
</p>
2932
<p>
3033
<label>
3134
cursorStyle
@@ -53,6 +56,16 @@ <h2>Options</h2>
5356
<p>
5457
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
5558
</p>
59+
<p>
60+
<label>
61+
experimentalCharAtlas
62+
<select id="option-experimental-char-atlas">
63+
<option value="static" selected>static</option>
64+
<option value="dynamic">dynamic</option>
65+
<option value="none">none</option>
66+
</select>
67+
</label>
68+
</p>
5669
<div>
5770
<h3>Size</h3>
5871
<div>

demo/main.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ var terminalContainer = document.getElementById('terminal-container'),
3131
cursorStyle: document.querySelector('#option-cursor-style'),
3232
macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'),
3333
scrollback: document.querySelector('#option-scrollback'),
34+
transparency: document.querySelector('#option-transparency'),
3435
tabstopwidth: document.querySelector('#option-tabstopwidth'),
36+
experimentalCharAtlas: document.querySelector('#option-experimental-char-atlas'),
3537
bellStyle: document.querySelector('#option-bell-style'),
3638
screenReaderMode: document.querySelector('#option-screen-reader-mode')
3739
},
@@ -74,21 +76,29 @@ actionElements.findPrevious.addEventListener('keypress', function (e) {
7476
optionElements.cursorBlink.addEventListener('change', function () {
7577
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
7678
});
79+
optionElements.macOptionIsMeta.addEventListener('change', function () {
80+
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
81+
});
82+
optionElements.transparency.addEventListener('change', function () {
83+
var checked = optionElements.transparency.checked;
84+
term.setOption('allowTransparency', checked);
85+
term.setOption('theme', checked ? {background: 'rgba(0, 0, 0, .5)'} : {});
86+
});
7787
optionElements.cursorStyle.addEventListener('change', function () {
7888
term.setOption('cursorStyle', optionElements.cursorStyle.value);
7989
});
8090
optionElements.bellStyle.addEventListener('change', function () {
8191
term.setOption('bellStyle', optionElements.bellStyle.value);
8292
});
83-
optionElements.macOptionIsMeta.addEventListener('change', function () {
84-
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
85-
});
8693
optionElements.scrollback.addEventListener('change', function () {
8794
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
8895
});
8996
optionElements.tabstopwidth.addEventListener('change', function () {
9097
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
9198
});
99+
optionElements.experimentalCharAtlas.addEventListener('change', function () {
100+
term.setOption('experimentalCharAtlas', optionElements.experimentalCharAtlas.value);
101+
});
92102
optionElements.screenReaderMode.addEventListener('change', function () {
93103
term.setOption('screenReaderMode', optionElements.screenReaderMode.checked);
94104
});

src/Terminal.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import { MouseZoneManager } from './input/MouseZoneManager';
4848
import { AccessibilityManager } from './AccessibilityManager';
4949
import { ScreenDprMonitor } from './utils/ScreenDprMonitor';
5050
import { ITheme, ILocalizableStrings, IMarker, IDisposable } from 'xterm';
51-
import { removeTerminalFromCache } from './renderer/atlas/CharAtlas';
51+
import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache';
5252

5353
// reg + shift key mappings for digits and special chars
5454
const KEYCODE_KEY_MAPPINGS = {
@@ -105,6 +105,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
105105
bellStyle: 'none',
106106
drawBoldTextInBrightColors: true,
107107
enableBold: true,
108+
experimentalCharAtlas: 'static',
108109
fontFamily: 'courier-new, courier, monospace',
109110
fontSize: 15,
110111
fontWeight: 'normal',
@@ -478,6 +479,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
478479
this.charMeasure.measure(this.options);
479480
}
480481
break;
482+
case 'experimentalCharAtlas':
481483
case 'enableBold':
482484
case 'letterSpacing':
483485
case 'lineHeight':

src/renderer/BaseRenderLayer.ts

Lines changed: 16 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
77
import { CharData, ITerminal } from '../Types';
88
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
9-
import { CHAR_ATLAS_CELL_SPACING } from '../shared/atlas/Types';
10-
import { acquireCharAtlas } from './atlas/CharAtlas';
9+
import BaseCharAtlas from './atlas/BaseCharAtlas';
10+
import { acquireCharAtlas } from './atlas/CharAtlasCache';
1111
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
1212

1313
export abstract class BaseRenderLayer implements IRenderLayer {
@@ -20,7 +20,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
2020
private _scaledCharLeft: number = 0;
2121
private _scaledCharTop: number = 0;
2222

23-
private _charAtlas: HTMLCanvasElement | ImageBitmap;
23+
protected _charAtlas: BaseCharAtlas;
2424

2525
constructor(
2626
private _container: HTMLElement,
@@ -83,13 +83,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
8383
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
8484
return;
8585
}
86-
this._charAtlas = null;
87-
const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
88-
if (result instanceof HTMLCanvasElement) {
89-
this._charAtlas = result;
90-
} else {
91-
result.then(bitmap => this._charAtlas = bitmap);
92-
}
86+
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
87+
this._charAtlas.warmUp();
9388
}
9489

9590
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
@@ -243,46 +238,18 @@ export abstract class BaseRenderLayer implements IRenderLayer {
243238
* @param bold Whether the text is bold.
244239
*/
245240
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
246-
const isAscii = code < 256;
247-
// A color is basic if it is one of the 4 bit ANSI colors.
248-
const isBasicColor = fg < 16;
249-
const isDefaultColor = fg >= 256;
250-
const isDefaultBackground = bg >= 256;
251-
const drawInBrightColor = (terminal.options.drawBoldTextInBrightColors && bold && fg < 8);
252-
if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground && !italic) {
253-
this._ctx.save(); // we may set globalAlpha, so we need to be able to restore
254-
let colorIndex: number;
255-
if (isDefaultColor) {
256-
colorIndex = (bold && terminal.options.enableBold ? 1 : 0);
257-
} else {
258-
colorIndex = 2 + fg + (bold && terminal.options.enableBold ? 16 : 0) + (drawInBrightColor ? 8 : 0);
259-
}
260-
261-
// ImageBitmap's draw about twice as fast as from a canvas
262-
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
263-
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
264-
265-
// Apply alpha to dim the character
266-
if (dim) {
267-
this._ctx.globalAlpha = DIM_OPACITY;
268-
}
269-
270-
this._ctx.drawImage(this._charAtlas,
271-
code * charAtlasCellWidth,
272-
colorIndex * charAtlasCellHeight,
273-
charAtlasCellWidth,
274-
this._scaledCharHeight,
275-
x * this._scaledCellWidth + this._scaledCharLeft,
276-
y * this._scaledCellHeight + this._scaledCharTop,
277-
charAtlasCellWidth,
278-
this._scaledCharHeight);
279-
this._ctx.restore();
280-
} else {
281-
this._drawUncachedChar(terminal, char, width, fg + (drawInBrightColor ? 8 : 0), x, y, bold && terminal.options.enableBold, dim, italic);
241+
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
242+
fg += drawInBrightColor ? 8 : 0;
243+
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
244+
this._ctx,
245+
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
246+
x * this._scaledCellWidth + this._scaledCharLeft,
247+
y * this._scaledCellHeight + this._scaledCharTop
248+
);
249+
250+
if (!atlasDidDraw) {
251+
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
282252
}
283-
// This draws the atlas (for debugging purposes)
284-
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
285-
// this._ctx.drawImage(this._charAtlas, 0, 0);
286253
}
287254

288255
/**

src/renderer/Renderer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export class Renderer extends EventEmitter implements IRenderer {
150150
}
151151

152152
public onOptionsChanged(): void {
153+
this.colorManager.allowTransparency = this._terminal.options.allowTransparency;
153154
this._runOperation(l => l.onOptionsChanged(this._terminal));
154155
}
155156

src/renderer/TextRenderLayer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ export class TextRenderLayer extends BaseRenderLayer {
208208
return;
209209
}
210210

211+
this._charAtlas.beginFrame();
212+
211213
this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1);
212214
this._drawBackground(terminal, firstRow, lastRow);
213215
this._drawForeground(terminal, firstRow, lastRow);

src/renderer/atlas/BaseCharAtlas.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import { IGlyphIdentifier } from './Types';
7+
8+
export default abstract class BaseCharAtlas {
9+
private _didWarmUp: boolean = false;
10+
11+
/**
12+
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
13+
* Implement _doWarmUp instead if you only want to get called once.
14+
*/
15+
public warmUp(): void {
16+
if (!this._didWarmUp) {
17+
this._doWarmUp();
18+
this._didWarmUp = true;
19+
}
20+
}
21+
22+
/**
23+
* Perform any work needed to warm the cache before it can be used. Used by the default
24+
* implementation of warmUp(), and will only be called once.
25+
*/
26+
protected _doWarmUp(): void { }
27+
28+
/**
29+
* Called when we start drawing a new frame.
30+
*
31+
* TODO: We rely on this getting called by TextRenderLayer. This should really be called by
32+
* Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
33+
* of BaseRenderLayer.
34+
*/
35+
public beginFrame(): void { }
36+
37+
/**
38+
* May be called before warmUp finishes, however it is okay for the implementation to
39+
* do nothing and return false in that case.
40+
*
41+
* @param ctx Where to draw the character onto.
42+
* @param glyph Information about what to draw
43+
* @param x The position on the context to start drawing at
44+
* @param y The position on the context to start drawing at
45+
* @returns The success state. True if we drew the character.
46+
*/
47+
public abstract draw(
48+
ctx: CanvasRenderingContext2D,
49+
glyph: IGlyphIdentifier,
50+
x: number,
51+
y: number
52+
): boolean;
53+
}

src/renderer/atlas/CharAtlas.ts renamed to src/renderer/atlas/CharAtlasCache.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,40 @@
66
import { ITerminal } from '../../Types';
77
import { IColorSet } from '../Types';
88
import { ICharAtlasConfig } from '../../shared/atlas/Types';
9-
import { generateCharAtlas } from '../../shared/atlas/CharAtlasGenerator';
109
import { generateConfig, configEquals } from './CharAtlasUtils';
10+
import BaseCharAtlas from './BaseCharAtlas';
11+
import DynamicCharAtlas from './DynamicCharAtlas';
12+
import NoneCharAtlas from './NoneCharAtlas';
13+
import StaticCharAtlas from './StaticCharAtlas';
14+
15+
const charAtlasImplementations = {
16+
'none': NoneCharAtlas,
17+
'static': StaticCharAtlas,
18+
'dynamic': DynamicCharAtlas
19+
};
1120

1221
interface ICharAtlasCacheEntry {
13-
bitmap: HTMLCanvasElement | Promise<ImageBitmap>;
22+
atlas: BaseCharAtlas;
1423
config: ICharAtlasConfig;
24+
// N.B. This implementation potentially holds onto copies of the terminal forever, so
25+
// this may cause memory leaks.
1526
ownedBy: ITerminal[];
1627
}
1728

18-
let charAtlasCache: ICharAtlasCacheEntry[] = [];
29+
const charAtlasCache: ICharAtlasCacheEntry[] = [];
1930

2031
/**
2132
* Acquires a char atlas, either generating a new one or returning an existing
2233
* one that is in use by another terminal.
2334
* @param terminal The terminal.
2435
* @param colors The colors to use.
2536
*/
26-
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise<ImageBitmap> {
37+
export function acquireCharAtlas(
38+
terminal: ITerminal,
39+
colors: IColorSet,
40+
scaledCharWidth: number,
41+
scaledCharHeight: number
42+
): BaseCharAtlas {
2743
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
2844

2945
// TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed)
@@ -34,7 +50,7 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
3450
const ownedByIndex = entry.ownedBy.indexOf(terminal);
3551
if (ownedByIndex >= 0) {
3652
if (configEquals(entry.config, newConfig)) {
37-
return entry.bitmap;
53+
return entry.atlas;
3854
}
3955
// The configs differ, release the terminal from the entry
4056
if (entry.ownedBy.length === 1) {
@@ -52,24 +68,20 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
5268
if (configEquals(entry.config, newConfig)) {
5369
// Add the terminal to the cache entry and return
5470
entry.ownedBy.push(terminal);
55-
return entry.bitmap;
71+
return entry.atlas;
5672
}
5773
}
5874

59-
const canvasFactory = (width: number, height: number) => {
60-
const canvas = document.createElement('canvas');
61-
canvas.width = width;
62-
canvas.height = height;
63-
return canvas;
64-
};
65-
6675
const newEntry: ICharAtlasCacheEntry = {
67-
bitmap: generateCharAtlas(window, canvasFactory, newConfig),
76+
atlas: new charAtlasImplementations[terminal.options.experimentalCharAtlas](
77+
document,
78+
newConfig
79+
),
6880
config: newConfig,
6981
ownedBy: [terminal]
7082
};
7183
charAtlasCache.push(newEntry);
72-
return newEntry.bitmap;
84+
return newEntry.atlas;
7385
}
7486

7587
/**

src/renderer/atlas/CharAtlasUtils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@ import { IColorSet } from '../Types';
88
import { ICharAtlasConfig } from '../../shared/atlas/Types';
99

1010
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
11+
// null out some fields that don't matter
1112
const clonedColors = {
1213
foreground: colors.foreground,
1314
background: colors.background,
1415
cursor: null,
1516
cursorAccent: null,
1617
selection: null,
18+
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
19+
// dynamic character atlas.
1720
ansi: colors.ansi.slice(0, 16)
1821
};
1922
return {
23+
type: terminal.options.experimentalCharAtlas,
2024
devicePixelRatio: window.devicePixelRatio,
2125
scaledCharWidth,
2226
scaledCharHeight,
@@ -35,7 +39,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
3539
return false;
3640
}
3741
}
38-
return a.devicePixelRatio === b.devicePixelRatio &&
42+
return a.type === b.type &&
43+
a.devicePixelRatio === b.devicePixelRatio &&
3944
a.fontFamily === b.fontFamily &&
4045
a.fontSize === b.fontSize &&
4146
a.fontWeight === b.fontWeight &&

0 commit comments

Comments
 (0)