/
GlyphRenderer.ts
380 lines (330 loc) · 15.8 KB
/
GlyphRenderer.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
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from './Types';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
interface IVertices {
attributes: Float32Array;
/**
* These buffers are the ones used to bind to WebGL, the reason there are
* multiple is to allow double buffering to work as you cannot modify the
* buffer while it's being used by the GPU. Having multiple lets us start
* working on the next frame.
*/
attributesBuffers: Float32Array[];
count: number;
}
const enum VertexAttribLocations {
UNIT_QUAD = 0,
CELL_POSITION = 1,
OFFSET = 2,
SIZE = 3,
TEXPAGE = 4,
TEXCOORD = 5,
TEXSIZE = 6
}
const vertexShaderSource = `#version 300 es
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos;
layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset;
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
layout (location = ${VertexAttribLocations.TEXPAGE}) in float a_texpage;
layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord;
layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize;
uniform mat4 u_projection;
uniform vec2 u_resolution;
out vec2 v_texcoord;
flat out int v_texpage;
void main() {
vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size);
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
v_texpage = int(a_texpage);
v_texcoord = a_texcoord + a_unitquad * a_texsize;
}`;
function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): string {
let textureConditionals = '';
for (let i = 1; i < maxFragmentShaderTextureUnits; i++) {
textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`;
}
return (`#version 300 es
precision lowp float;
in vec2 v_texcoord;
flat in int v_texpage;
uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}];
out vec4 outColor;
void main() {
if (v_texpage == 0) {
outColor = texture(u_texture[0], v_texcoord);
} ${textureConditionals}
}`);
}
const INDICES_PER_CELL = 11;
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
const CELL_POSITION_INDICES = 2;
// Work variables to avoid garbage collection
let $i = 0;
let $glyph: IRasterizedGlyph | undefined = undefined;
let $leftCellPadding = 0;
let $clippedPixels = 0;
export class GlyphRenderer extends Disposable {
private readonly _program: WebGLProgram;
private readonly _vertexArrayObject: IWebGLVertexArrayObject;
private readonly _projectionLocation: WebGLUniformLocation;
private readonly _resolutionLocation: WebGLUniformLocation;
private readonly _textureLocation: WebGLUniformLocation;
private readonly _atlasTextures: GLTexture[];
private readonly _attributesBuffer: WebGLBuffer;
private _atlas: ITextureAtlas | undefined;
private _activeBuffer: number = 0;
private readonly _vertices: IVertices = {
count: 0,
attributes: new Float32Array(0),
attributesBuffers: [
new Float32Array(0),
new Float32Array(0)
]
};
constructor(
private readonly _terminal: Terminal,
private readonly _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions
) {
super();
const gl = this._gl;
if (TextureAtlas.maxAtlasPages === undefined) {
// Typically 8 or 16
TextureAtlas.maxAtlasPages = Math.min(32, throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number | null));
// Almost all clients will support >= 4096
TextureAtlas.maxTextureSize = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null);
}
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages)));
this.register(toDisposable(() => gl.deleteProgram(this._program)));
// Uniform locations
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution'));
this._textureLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_texture'));
// Create and set the vertex array object
this._vertexArrayObject = gl.createVertexArray();
gl.bindVertexArray(this._vertexArrayObject);
// Setup a_unitquad, this defines the 4 vertices of a rectangle
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
const unitQuadVerticesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
// triangle strip
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
const elementIndicesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
// Setup attributes
this._attributesBuffer = throwIfFalsy(gl.createBuffer());
this.register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.enableVertexAttribArray(VertexAttribLocations.OFFSET);
gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0);
gl.vertexAttribDivisor(VertexAttribLocations.OFFSET, 1);
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE);
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
// Setup static uniforms
gl.useProgram(this._program);
const textureUnits = new Int32Array(TextureAtlas.maxAtlasPages);
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
textureUnits[i] = i;
}
gl.uniform1iv(this._textureLocation, textureUnits);
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
// Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures
// is ever drawn it will show characters as red rectangles.
this._atlasTextures = [];
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
const glTexture = new GLTexture(throwIfFalsy(gl.createTexture()));
this.register(toDisposable(() => gl.deleteTexture(glTexture.texture)));
gl.activeTexture(gl.TEXTURE0 + i);
gl.bindTexture(gl.TEXTURE_2D, glTexture.texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
this._atlasTextures[i] = glTexture;
}
// Allow drawing of transparent texture
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Set viewport
this.handleResize();
}
public beginFrame(): boolean {
return this._atlas ? this._atlas.beginFrame() : true;
}
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
// should not instantiate any variables unless a new glyph is drawn to the cache where the
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
// each glyph.
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
}
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
$i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
// Exit early if this is a null character, allow space character to continue as it may have
// underline/strikethrough styles
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
}
if (!this._atlas) {
return;
}
// Get the glyph
if (chars && chars.length > 1) {
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
} else {
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
}
$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) {
$clippedPixels = $glyph.offset.x - $leftCellPadding;
// a_origin
array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left;
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
// a_size
array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width;
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
// a_texpage
array[$i + 4] = $glyph.texturePage;
// a_texcoord
array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
array[$i + 6] = $glyph.texturePositionClipSpace.y;
// a_texsize
array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
array[$i + 8] = $glyph.sizeClipSpace.y;
} else {
// a_origin
array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left;
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
// a_size
array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width;
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
// a_texpage
array[$i + 4] = $glyph.texturePage;
// a_texcoord
array[$i + 5] = $glyph.texturePositionClipSpace.x;
array[$i + 6] = $glyph.texturePositionClipSpace.y;
// a_texsize
array[$i + 7] = $glyph.sizeClipSpace.x;
array[$i + 8] = $glyph.sizeClipSpace.y;
}
// a_cellpos only changes on resize
}
public clear(): void {
const terminal = this._terminal;
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
// Clear vertices
if (this._vertices.count !== newCount) {
this._vertices.attributes = new Float32Array(newCount);
} else {
this._vertices.attributes.fill(0);
}
let i = 0;
for (; i < this._vertices.attributesBuffers.length; i++) {
if (this._vertices.count !== newCount) {
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
} else {
this._vertices.attributesBuffers[i].fill(0);
}
}
this._vertices.count = newCount;
i = 0;
for (let y = 0; y < terminal.rows; y++) {
for (let x = 0; x < terminal.cols; x++) {
this._vertices.attributes[i + 9] = x / terminal.cols;
this._vertices.attributes[i + 10] = y / terminal.rows;
i += INDICES_PER_CELL;
}
}
}
public handleResize(): void {
const gl = this._gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
this.clear();
}
public render(renderModel: IRenderModel): void {
if (!this._atlas) {
return;
}
const gl = this._gl;
gl.useProgram(this._program);
gl.bindVertexArray(this._vertexArrayObject);
// Alternate buffers each frame as the active buffer gets locked while it's in use by the GPU
this._activeBuffer = (this._activeBuffer + 1) % 2;
const activeBuffer = this._vertices.attributesBuffers[this._activeBuffer];
// Copy data for each cell of each line up to its line length (the last non-whitespace cell)
// from the attributes buffer into activeBuffer, which is the one that gets bound to the GPU.
// The reasons for this are as follows:
// - So the active buffer can be alternated so we don't get blocked on rendering finishing
// - To copy either the normal attributes buffer or the selection attributes buffer when there
// is a selection
// - So we don't send vertices for all the line-ending whitespace to the GPU
let bufferLength = 0;
for (let y = 0; y < renderModel.lineLengths.length; y++) {
const si = y * this._terminal.cols * INDICES_PER_CELL;
const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
activeBuffer.set(sub, bufferLength);
bufferLength += sub.length;
}
// Bind the attributes buffer
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
// Bind the atlas page texture if they have changed
for (let i = 0; i < this._atlas.pages.length; i++) {
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
this._bindAtlasPageTexture(gl, this._atlas, i);
}
}
// Draw the viewport
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
}
public setAtlas(atlas: ITextureAtlas): void {
this._atlas = atlas;
for (const glTexture of this._atlasTextures) {
glTexture.version = -1;
}
}
private _bindAtlasPageTexture(gl: IWebGL2RenderingContext, atlas: ITextureAtlas, i: number): void {
gl.activeTexture(gl.TEXTURE0 + i);
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
gl.generateMipmap(gl.TEXTURE_2D);
this._atlasTextures[i].version = atlas.pages[i].version;
}
public setDimensions(dimensions: IRenderDimensions): void {
this._dimensions = dimensions;
}
}