@xterm/addon-webgl 0.17.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@xterm/addon-webgl",
3
+ "version": "0.17.0-beta.1",
4
+ "author": {
5
+ "name": "The xterm.js authors",
6
+ "url": "https://xtermjs.org/"
7
+ },
8
+ "main": "lib/addon-webgl.js",
9
+ "types": "typings/addon-webgl.d.ts",
10
+ "repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-webgl",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "terminal",
14
+ "webgl",
15
+ "xterm",
16
+ "xterm.js"
17
+ ],
18
+ "scripts": {
19
+ "build": "../../node_modules/.bin/tsc -p .",
20
+ "prepackage": "npm run build",
21
+ "package": "../../node_modules/.bin/webpack",
22
+ "prepublishOnly": "npm run package",
23
+ "start-server-only": "node ../../demo/start-server-only"
24
+ },
25
+ "peerDependencies": {
26
+ "xterm": "^5.0.0"
27
+ }
28
+ }
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
7
+ import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
8
+ import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
9
+ import { NULL_CELL_CODE } from 'common/buffer/Constants';
10
+ import { Disposable, toDisposable } from 'common/Lifecycle';
11
+ import { Terminal } from '@xterm/xterm';
12
+ import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
13
+ import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
14
+
15
+ interface IVertices {
16
+ attributes: Float32Array;
17
+ /**
18
+ * These buffers are the ones used to bind to WebGL, the reason there are
19
+ * multiple is to allow double buffering to work as you cannot modify the
20
+ * buffer while it's being used by the GPU. Having multiple lets us start
21
+ * working on the next frame.
22
+ */
23
+ attributesBuffers: Float32Array[];
24
+ count: number;
25
+ }
26
+
27
+ const enum VertexAttribLocations {
28
+ UNIT_QUAD = 0,
29
+ CELL_POSITION = 1,
30
+ OFFSET = 2,
31
+ SIZE = 3,
32
+ TEXPAGE = 4,
33
+ TEXCOORD = 5,
34
+ TEXSIZE = 6
35
+ }
36
+
37
+ const vertexShaderSource = `#version 300 es
38
+ layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
39
+ layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos;
40
+ layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset;
41
+ layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
42
+ layout (location = ${VertexAttribLocations.TEXPAGE}) in float a_texpage;
43
+ layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord;
44
+ layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize;
45
+
46
+ uniform mat4 u_projection;
47
+ uniform vec2 u_resolution;
48
+
49
+ out vec2 v_texcoord;
50
+ flat out int v_texpage;
51
+
52
+ void main() {
53
+ vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size);
54
+ gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
55
+ v_texpage = int(a_texpage);
56
+ v_texcoord = a_texcoord + a_unitquad * a_texsize;
57
+ }`;
58
+
59
+ function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): string {
60
+ let textureConditionals = '';
61
+ for (let i = 1; i < maxFragmentShaderTextureUnits; i++) {
62
+ textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`;
63
+ }
64
+ return (`#version 300 es
65
+ precision lowp float;
66
+
67
+ in vec2 v_texcoord;
68
+ flat in int v_texpage;
69
+
70
+ uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}];
71
+
72
+ out vec4 outColor;
73
+
74
+ void main() {
75
+ if (v_texpage == 0) {
76
+ outColor = texture(u_texture[0], v_texcoord);
77
+ } ${textureConditionals}
78
+ }`);
79
+ }
80
+
81
+ const INDICES_PER_CELL = 11;
82
+ const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
83
+ const CELL_POSITION_INDICES = 2;
84
+
85
+ // Work variables to avoid garbage collection
86
+ let $i = 0;
87
+ let $glyph: IRasterizedGlyph | undefined = undefined;
88
+ let $leftCellPadding = 0;
89
+ let $clippedPixels = 0;
90
+
91
+ export class GlyphRenderer extends Disposable {
92
+ private readonly _program: WebGLProgram;
93
+ private readonly _vertexArrayObject: IWebGLVertexArrayObject;
94
+ private readonly _projectionLocation: WebGLUniformLocation;
95
+ private readonly _resolutionLocation: WebGLUniformLocation;
96
+ private readonly _textureLocation: WebGLUniformLocation;
97
+ private readonly _atlasTextures: GLTexture[];
98
+ private readonly _attributesBuffer: WebGLBuffer;
99
+
100
+ private _atlas: ITextureAtlas | undefined;
101
+ private _activeBuffer: number = 0;
102
+ private readonly _vertices: IVertices = {
103
+ count: 0,
104
+ attributes: new Float32Array(0),
105
+ attributesBuffers: [
106
+ new Float32Array(0),
107
+ new Float32Array(0)
108
+ ]
109
+ };
110
+
111
+ constructor(
112
+ private readonly _terminal: Terminal,
113
+ private readonly _gl: IWebGL2RenderingContext,
114
+ private _dimensions: IRenderDimensions
115
+ ) {
116
+ super();
117
+
118
+ const gl = this._gl;
119
+
120
+ if (TextureAtlas.maxAtlasPages === undefined) {
121
+ // Typically 8 or 16
122
+ TextureAtlas.maxAtlasPages = Math.min(32, throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number | null));
123
+ // Almost all clients will support >= 4096
124
+ TextureAtlas.maxTextureSize = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null);
125
+ }
126
+
127
+ this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages)));
128
+ this.register(toDisposable(() => gl.deleteProgram(this._program)));
129
+
130
+ // Uniform locations
131
+ this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
132
+ this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution'));
133
+ this._textureLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_texture'));
134
+
135
+ // Create and set the vertex array object
136
+ this._vertexArrayObject = gl.createVertexArray();
137
+ gl.bindVertexArray(this._vertexArrayObject);
138
+
139
+ // Setup a_unitquad, this defines the 4 vertices of a rectangle
140
+ const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
141
+ const unitQuadVerticesBuffer = gl.createBuffer();
142
+ this.register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer)));
143
+ gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
144
+ gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
145
+ gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
146
+ gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
147
+
148
+ // Setup the unit quad element array buffer, this points to indices in
149
+ // unitQuadVertices to allow is to draw 2 triangles from the vertices via a
150
+ // triangle strip
151
+ const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
152
+ const elementIndicesBuffer = gl.createBuffer();
153
+ this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
154
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
155
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
156
+
157
+ // Setup attributes
158
+ this._attributesBuffer = throwIfFalsy(gl.createBuffer());
159
+ this.register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
160
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
161
+ gl.enableVertexAttribArray(VertexAttribLocations.OFFSET);
162
+ gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0);
163
+ gl.vertexAttribDivisor(VertexAttribLocations.OFFSET, 1);
164
+ gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
165
+ gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
166
+ gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
167
+ gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE);
168
+ gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
169
+ gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1);
170
+ gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
171
+ gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
172
+ gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
173
+ gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
174
+ gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
175
+ gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
176
+ gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
177
+ gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
178
+ gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
179
+
180
+ // Setup static uniforms
181
+ gl.useProgram(this._program);
182
+ const textureUnits = new Int32Array(TextureAtlas.maxAtlasPages);
183
+ for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
184
+ textureUnits[i] = i;
185
+ }
186
+ gl.uniform1iv(this._textureLocation, textureUnits);
187
+ gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
188
+
189
+ // Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures
190
+ // is ever drawn it will show characters as red rectangles.
191
+ this._atlasTextures = [];
192
+ for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
193
+ const glTexture = new GLTexture(throwIfFalsy(gl.createTexture()));
194
+ this.register(toDisposable(() => gl.deleteTexture(glTexture.texture)));
195
+ gl.activeTexture(gl.TEXTURE0 + i);
196
+ gl.bindTexture(gl.TEXTURE_2D, glTexture.texture);
197
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
198
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
199
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
200
+ this._atlasTextures[i] = glTexture;
201
+ }
202
+
203
+ // Allow drawing of transparent texture
204
+ gl.enable(gl.BLEND);
205
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
206
+
207
+ // Set viewport
208
+ this.handleResize();
209
+ }
210
+
211
+ public beginFrame(): boolean {
212
+ return this._atlas ? this._atlas.beginFrame() : true;
213
+ }
214
+
215
+ public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
216
+ // Since this function is called for every cell (`rows*cols`), it must be very optimized. It
217
+ // should not instantiate any variables unless a new glyph is drawn to the cache where the
218
+ // slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
219
+ // each glyph.
220
+ this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
221
+ }
222
+
223
+ private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
224
+ $i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
225
+
226
+ // Exit early if this is a null character, allow space character to continue as it may have
227
+ // underline/strikethrough styles
228
+ if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
229
+ array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
230
+ return;
231
+ }
232
+
233
+ if (!this._atlas) {
234
+ return;
235
+ }
236
+
237
+ // Get the glyph
238
+ if (chars && chars.length > 1) {
239
+ $glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false);
240
+ } else {
241
+ $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false);
242
+ }
243
+
244
+ $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
245
+ if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) {
246
+ $clippedPixels = $glyph.offset.x - $leftCellPadding;
247
+ // a_origin
248
+ array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left;
249
+ array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
250
+ // a_size
251
+ array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width;
252
+ array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
253
+ // a_texpage
254
+ array[$i + 4] = $glyph.texturePage;
255
+ // a_texcoord
256
+ array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
257
+ array[$i + 6] = $glyph.texturePositionClipSpace.y;
258
+ // a_texsize
259
+ array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
260
+ array[$i + 8] = $glyph.sizeClipSpace.y;
261
+ } else {
262
+ // a_origin
263
+ array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left;
264
+ array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
265
+ // a_size
266
+ array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width;
267
+ array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
268
+ // a_texpage
269
+ array[$i + 4] = $glyph.texturePage;
270
+ // a_texcoord
271
+ array[$i + 5] = $glyph.texturePositionClipSpace.x;
272
+ array[$i + 6] = $glyph.texturePositionClipSpace.y;
273
+ // a_texsize
274
+ array[$i + 7] = $glyph.sizeClipSpace.x;
275
+ array[$i + 8] = $glyph.sizeClipSpace.y;
276
+ }
277
+ // a_cellpos only changes on resize
278
+ }
279
+
280
+ public clear(): void {
281
+ const terminal = this._terminal;
282
+ const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
283
+
284
+ // Clear vertices
285
+ if (this._vertices.count !== newCount) {
286
+ this._vertices.attributes = new Float32Array(newCount);
287
+ } else {
288
+ this._vertices.attributes.fill(0);
289
+ }
290
+ let i = 0;
291
+ for (; i < this._vertices.attributesBuffers.length; i++) {
292
+ if (this._vertices.count !== newCount) {
293
+ this._vertices.attributesBuffers[i] = new Float32Array(newCount);
294
+ } else {
295
+ this._vertices.attributesBuffers[i].fill(0);
296
+ }
297
+ }
298
+ this._vertices.count = newCount;
299
+ i = 0;
300
+ for (let y = 0; y < terminal.rows; y++) {
301
+ for (let x = 0; x < terminal.cols; x++) {
302
+ this._vertices.attributes[i + 9] = x / terminal.cols;
303
+ this._vertices.attributes[i + 10] = y / terminal.rows;
304
+ i += INDICES_PER_CELL;
305
+ }
306
+ }
307
+ }
308
+
309
+ public handleResize(): void {
310
+ const gl = this._gl;
311
+ gl.useProgram(this._program);
312
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
313
+ gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
314
+ this.clear();
315
+ }
316
+
317
+ public render(renderModel: IRenderModel): void {
318
+ if (!this._atlas) {
319
+ return;
320
+ }
321
+
322
+ const gl = this._gl;
323
+
324
+ gl.useProgram(this._program);
325
+ gl.bindVertexArray(this._vertexArrayObject);
326
+
327
+ // Alternate buffers each frame as the active buffer gets locked while it's in use by the GPU
328
+ this._activeBuffer = (this._activeBuffer + 1) % 2;
329
+ const activeBuffer = this._vertices.attributesBuffers[this._activeBuffer];
330
+
331
+ // Copy data for each cell of each line up to its line length (the last non-whitespace cell)
332
+ // from the attributes buffer into activeBuffer, which is the one that gets bound to the GPU.
333
+ // The reasons for this are as follows:
334
+ // - So the active buffer can be alternated so we don't get blocked on rendering finishing
335
+ // - To copy either the normal attributes buffer or the selection attributes buffer when there
336
+ // is a selection
337
+ // - So we don't send vertices for all the line-ending whitespace to the GPU
338
+ let bufferLength = 0;
339
+ for (let y = 0; y < renderModel.lineLengths.length; y++) {
340
+ const si = y * this._terminal.cols * INDICES_PER_CELL;
341
+ const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
342
+ activeBuffer.set(sub, bufferLength);
343
+ bufferLength += sub.length;
344
+ }
345
+
346
+ // Bind the attributes buffer
347
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
348
+ gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
349
+
350
+ // Bind the atlas page texture if they have changed
351
+ for (let i = 0; i < this._atlas.pages.length; i++) {
352
+ if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
353
+ this._bindAtlasPageTexture(gl, this._atlas, i);
354
+ }
355
+ }
356
+
357
+ // Draw the viewport
358
+ gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
359
+ }
360
+
361
+ public setAtlas(atlas: ITextureAtlas): void {
362
+ this._atlas = atlas;
363
+ for (const glTexture of this._atlasTextures) {
364
+ glTexture.version = -1;
365
+ }
366
+ }
367
+
368
+ private _bindAtlasPageTexture(gl: IWebGL2RenderingContext, atlas: ITextureAtlas, i: number): void {
369
+ gl.activeTexture(gl.TEXTURE0 + i);
370
+ gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
371
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
372
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
373
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
374
+ gl.generateMipmap(gl.TEXTURE_2D);
375
+ this._atlasTextures[i].version = atlas.pages[i].version;
376
+ }
377
+
378
+ public setDimensions(dimensions: IRenderDimensions): void {
379
+ this._dimensions = dimensions;
380
+ }
381
+ }