-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathWebglRenderer.ts
410 lines (332 loc) · 15.3 KB
/
WebglRenderer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal } from '../../../src/Types';
import { GlyphRenderer } from './GlyphRenderer';
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel';
import { Disposable } from 'common/Lifecycle';
import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { getLuminance } from './ColorUtils';
import { IRenderLayer } from './renderLayer/Types';
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
import { IColorSet } from 'browser/Types';
import { FLAGS } from './Constants';
import { getCompatAttr } from './CharDataCompat';
export const INDICIES_PER_CELL = 4;
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _charAtlas: WebglCharAtlas | undefined;
private _devicePixelRatio: number;
private _model: RenderModel = new RenderModel();
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
private _rectangleRenderer: RectangleRenderer;
private _glyphRenderer: GlyphRenderer;
public dimensions: IRenderDimensions;
private _core: ITerminal;
constructor(
private _terminal: Terminal,
private _colors: IColorSet,
preserveDrawingBuffer?: boolean
) {
super();
this._core = (<any>this._terminal)._core;
this._applyBgLuminanceBasedSelection();
this._renderLayers = [
new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core),
new CursorRenderLayer(this._core.screenElement, 3, this._colors)
];
this.dimensions = {
scaledCharWidth: 0,
scaledCharHeight: 0,
scaledCellWidth: 0,
scaledCellHeight: 0,
scaledCharLeft: 0,
scaledCharTop: 0,
scaledCanvasWidth: 0,
scaledCanvasHeight: 0,
canvasWidth: 0,
canvasHeight: 0,
actualCellWidth: 0,
actualCellHeight: 0
};
this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions();
this._canvas = document.createElement('canvas');
const contextAttributes = {
antialias: false,
depth: false,
preserveDrawingBuffer
};
this._gl = this._canvas.getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
if (!this._gl) {
throw new Error('WebGL2 not supported');
}
this._core.screenElement.appendChild(this._canvas);
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);
// Update dimensions and acquire char atlas
this.onCharSizeChanged();
}
public dispose(): void {
this._renderLayers.forEach(l => l.dispose());
this._core.screenElement.removeChild(this._canvas);
super.dispose();
}
private _applyBgLuminanceBasedSelection(): void {
// HACK: This is needed until webgl renderer adds support for selection colors
if (getLuminance(this._colors.background) > 0.5) {
this._colors.selection = { css: '#000', rgba: 255 };
} else {
this._colors.selection = { css: '#fff', rgba: 4294967295 };
}
}
public setColors(colors: IColorSet): void {
this._colors = colors;
this._applyBgLuminanceBasedSelection();
// Clear layers and force a full render
this._renderLayers.forEach(l => {
l.setColors(this._terminal, this._colors);
l.reset(this._terminal);
});
this._rectangleRenderer.setColors();
this._glyphRenderer.setColors();
this._refreshCharAtlas();
// Force a full refresh
this._model.clear();
}
public onDevicePixelRatioChange(): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== window.devicePixelRatio) {
this._devicePixelRatio = window.devicePixelRatio;
this.onResize(this._terminal.cols, this._terminal.rows);
}
}
public onResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
this._model.resize(this._terminal.cols, this._terminal.rows);
this._rectangleRenderer.onResize();
// Resize all render layers
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
// Resize the canvas
this._canvas.width = this.dimensions.scaledCanvasWidth;
this._canvas.height = this.dimensions.scaledCanvasHeight;
this._canvas.style.width = `${this.dimensions.canvasWidth}px`;
this._canvas.style.height = `${this.dimensions.canvasHeight}px`;
// Resize the screen
this._core.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._core.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this._glyphRenderer.setDimensions(this.dimensions);
this._glyphRenderer.onResize();
this._refreshCharAtlas();
// Force a full refresh
this._model.clear();
}
public onCharSizeChanged(): void {
this.onResize(this._terminal.cols, this._terminal.rows);
}
public onBlur(): void {
this._renderLayers.forEach(l => l.onBlur(this._terminal));
}
public onFocus(): void {
this._renderLayers.forEach(l => l.onFocus(this._terminal));
}
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
this._updateSelectionModel(start, end);
this._rectangleRenderer.updateSelection(this._model.selection, columnSelectMode);
this._glyphRenderer.updateSelection(this._model, columnSelectMode);
// TODO: #2102 Should this move to RenderCoordinator?
this._core.refresh(0, this._terminal.rows - 1);
}
public onCursorMove(): void {
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
}
public onOptionsChanged(): void {
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
this._updateDimensions();
this._refreshCharAtlas();
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal.
* @param colorSet The color set to use for the char atlas.
*/
private _refreshCharAtlas(): void {
if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) {
return;
}
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
if (!('getRasterizedGlyph' in atlas)) {
throw new Error('The webgl renderer only works with the webgl char atlas');
}
this._charAtlas = atlas as WebglCharAtlas;
this._charAtlas.warmUp();
this._glyphRenderer.setAtlas(this._charAtlas);
}
public clear(): void {
this._renderLayers.forEach(l => l.reset(this._terminal));
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
return -1;
}
public deregisterCharacterJoiner(joinerId: number): boolean {
return false;
}
public renderRows(start: number, end: number): void {
// Update render layers
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
// Tell renderer the frame is beginning
if (this._glyphRenderer.beginFrame()) {
this._model.clear();
}
// Update model to reflect what's drawn
this._updateModel(start, end);
// Render
this._rectangleRenderer.render();
this._glyphRenderer.render(this._model, this._model.selection.hasSelection);
}
private _updateModel(start: number, end: number): void {
const terminal = this._core;
for (let y = start; y <= end; y++) {
const row = y + terminal.buffer.ydisp;
const line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
for (let x = 0; x < terminal.cols; x++) {
const charData = line.get(x);
const chars = charData[CHAR_DATA_CHAR_INDEX];
let code = charData[CHAR_DATA_CODE_INDEX];
const attr = getCompatAttr(line, x); // charData[CHAR_DATA_ATTR_INDEX];
const i = ((y * terminal.cols) + x) * INDICIES_PER_CELL;
if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
}
// Nothing has changed, no updates needed
if (this._model.cells[i] === code && this._model.cells[i + 1] === attr) {
continue;
}
// Resolve bg and fg and cache in the model
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
// If inverse flag is on, the foreground should become the background.
if (flags & FLAGS.INVERSE) {
const temp = bg;
bg = fg;
fg = temp;
if (fg === DEFAULT_COLOR) {
fg = INVERTED_DEFAULT_COLOR;
}
if (bg === DEFAULT_COLOR) {
bg = INVERTED_DEFAULT_COLOR;
}
}
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && !!(flags & FLAGS.BOLD) && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
fg += drawInBrightColor ? 8 : 0;
// Flag combined chars with a bit mask so they're easily identifiable
if (chars.length > 1) {
code = code | COMBINED_CHAR_BIT_MASK;
}
this._model.cells[i ] = code;
this._model.cells[i + 1] = attr;
this._model.cells[i + 2] = bg;
this._model.cells[i + 3] = fg;
this._glyphRenderer.updateCell(x, y, code, attr, bg, fg, chars);
}
}
this._rectangleRenderer.updateBackgrounds(this._model);
}
private _updateSelectionModel(start: [number, number], end: [number, number]): void {
const terminal = this._terminal;
// Selection does not exist
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
this._model.clearSelection();
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - terminal.buffer.viewportY;
const viewportEndRow = end[1] - terminal.buffer.viewportY;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
this._model.clearSelection();
return;
}
this._model.selection.hasSelection = true;
this._model.selection.viewportStartRow = viewportStartRow;
this._model.selection.viewportEndRow = viewportEndRow;
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
this._model.selection.startCol = start[0];
this._model.selection.endCol = end[0];
}
/**
* Recalculates the character and canvas dimensions.
*/
private _updateDimensions(): void {
// TODO: Acquire CharSizeService properly
// Perform a new measure if the CharMeasure dimensions are not yet available
if (!(<any>this._core)._charSizeService.width || !(<any>this._core)._charSizeService.height) {
return;
}
// Calculate the scaled character width. Width is floored as it must be
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
// blurry. When text is drawn to the grid not using the CharAtlas, it is
// clipped to ensure there is no overlap with the next cell.
// NOTE: ceil fixes sometime, floor does others :s
this.dimensions.scaledCharWidth = Math.floor((<any>this._core)._charSizeService.width * this._devicePixelRatio);
// Calculate the scaled character height. Height is ceiled in case
// devicePixelRatio is a floating point number in order to ensure there is
// enough space to draw the character to the cell.
this.dimensions.scaledCharHeight = Math.ceil((<any>this._core)._charSizeService.height * this._devicePixelRatio);
// Calculate the scaled cell height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.getOption('lineHeight'));
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharTop = this._terminal.getOption('lineHeight') === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.getOption('letterSpacing'));
// Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharLeft = Math.floor(this._terminal.getOption('letterSpacing') / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
// The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set
// window.devicePixelRatio as something like 1.100000023841858, when it's
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
// pixel too large for the canvas element size.
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio);
// this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio;
// this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio;
// Get the _actual_ dimensions of an individual cell. This needs to be
// derived from the canvasWidth/Height calculated above which takes into
// account window.devicePixelRatio. CharMeasure.width/height by itself is
// insufficient when the page is not at 100% zoom level as CharMeasure is
// measured in CSS pixels, but the actual char size on the canvas can
// differ.
// this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
// this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
// This fixes 110% and 125%, not 150% or 175% though
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
}
}