@next_term/web 0.1.0-next.3 → 0.1.0-next.5
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/LICENSE +21 -0
- package/dist/accessibility.d.ts +2 -4
- package/dist/accessibility.d.ts.map +1 -1
- package/dist/accessibility.js +2 -7
- package/dist/accessibility.js.map +1 -1
- package/dist/addons/web-links.d.ts +0 -1
- package/dist/addons/web-links.d.ts.map +1 -1
- package/dist/addons/web-links.js +0 -4
- package/dist/addons/web-links.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/input-handler.d.ts +7 -0
- package/dist/input-handler.d.ts.map +1 -1
- package/dist/input-handler.js +26 -5
- package/dist/input-handler.js.map +1 -1
- package/dist/render-worker.d.ts.map +1 -1
- package/dist/render-worker.js +186 -95
- package/dist/render-worker.js.map +1 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +1 -0
- package/dist/renderer.js.map +1 -1
- package/dist/shared-context.d.ts +34 -9
- package/dist/shared-context.d.ts.map +1 -1
- package/dist/shared-context.js +433 -189
- package/dist/shared-context.js.map +1 -1
- package/dist/web-terminal.d.ts +15 -0
- package/dist/web-terminal.d.ts.map +1 -1
- package/dist/web-terminal.js +79 -13
- package/dist/web-terminal.js.map +1 -1
- package/dist/webgl-renderer.d.ts +21 -3
- package/dist/webgl-renderer.d.ts.map +1 -1
- package/dist/webgl-renderer.js +255 -109
- package/dist/webgl-renderer.js.map +1 -1
- package/dist/webgl-utils.d.ts +4 -0
- package/dist/webgl-utils.d.ts.map +1 -0
- package/dist/webgl-utils.js +19 -0
- package/dist/webgl-utils.js.map +1 -0
- package/dist/worker-bridge.d.ts +3 -0
- package/dist/worker-bridge.d.ts.map +1 -1
- package/dist/worker-bridge.js +17 -4
- package/dist/worker-bridge.js.map +1 -1
- package/package.json +6 -6
package/dist/shared-context.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* multiple terminal panes.
|
|
4
4
|
*
|
|
5
5
|
* Chrome limits WebGL contexts to 16 per page. This class allows any number
|
|
6
|
-
* of terminal panes to render through one context
|
|
7
|
-
*
|
|
6
|
+
* of terminal panes to render through one context using **batched rendering**:
|
|
7
|
+
* all terminals' instance data is packed into a single buffer with per-instance
|
|
8
|
+
* viewport offsets, then uploaded and drawn in one call per pass.
|
|
8
9
|
*
|
|
9
10
|
* The shared canvas is positioned as an overlay by the consumer (typically
|
|
10
11
|
* TerminalPane). Each registered terminal provides its CellGrid, CursorState,
|
|
@@ -12,7 +13,8 @@
|
|
|
12
13
|
*/
|
|
13
14
|
import { DEFAULT_THEME } from "@next_term/core";
|
|
14
15
|
import { build256Palette } from "./renderer.js";
|
|
15
|
-
import {
|
|
16
|
+
import { GlyphAtlas, hexToFloat4 } from "./webgl-renderer.js";
|
|
17
|
+
import { resolveColorFloat } from "./webgl-utils.js";
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
17
19
|
// Attribute bit positions
|
|
18
20
|
// ---------------------------------------------------------------------------
|
|
@@ -22,10 +24,14 @@ const ATTR_INVERSE = 0x40;
|
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
23
25
|
// Shader sources (same as webgl-renderer.ts)
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
27
|
+
// Instance floats for shared-context batched rendering (include viewport offset)
|
|
28
|
+
const SC_BG_INSTANCE_FLOATS = 8; // cellCol, cellRow, r, g, b, a, offsetX, offsetY
|
|
29
|
+
const SC_GLYPH_INSTANCE_FLOATS = 14; // cellCol, cellRow, r, g, b, a, u, v, tw, th, pw, ph, offsetX, offsetY
|
|
25
30
|
const BG_VERTEX_SHADER = `#version 300 es
|
|
26
31
|
in vec2 a_position;
|
|
27
32
|
in vec2 a_cellPos;
|
|
28
33
|
in vec4 a_color;
|
|
34
|
+
in vec2 a_offset;
|
|
29
35
|
|
|
30
36
|
uniform vec2 u_resolution;
|
|
31
37
|
uniform vec2 u_cellSize;
|
|
@@ -33,7 +39,7 @@ uniform vec2 u_cellSize;
|
|
|
33
39
|
out vec4 v_color;
|
|
34
40
|
|
|
35
41
|
void main() {
|
|
36
|
-
vec2 cellPixelPos = a_cellPos * u_cellSize;
|
|
42
|
+
vec2 cellPixelPos = a_cellPos * u_cellSize + a_offset;
|
|
37
43
|
vec2 pos = cellPixelPos + a_position * u_cellSize;
|
|
38
44
|
vec2 clipPos = (pos / u_resolution) * 2.0 - 1.0;
|
|
39
45
|
clipPos.y = -clipPos.y;
|
|
@@ -55,6 +61,7 @@ in vec2 a_cellPos;
|
|
|
55
61
|
in vec4 a_color;
|
|
56
62
|
in vec4 a_texCoord;
|
|
57
63
|
in vec2 a_glyphSize;
|
|
64
|
+
in vec2 a_offset;
|
|
58
65
|
|
|
59
66
|
uniform vec2 u_resolution;
|
|
60
67
|
uniform vec2 u_cellSize;
|
|
@@ -63,7 +70,7 @@ out vec4 v_color;
|
|
|
63
70
|
out vec2 v_texCoord;
|
|
64
71
|
|
|
65
72
|
void main() {
|
|
66
|
-
vec2 cellPixelPos = a_cellPos * u_cellSize;
|
|
73
|
+
vec2 cellPixelPos = a_cellPos * u_cellSize + a_offset;
|
|
67
74
|
vec2 size = (a_glyphSize.x > 0.0) ? a_glyphSize : u_cellSize;
|
|
68
75
|
vec2 pos = cellPixelPos + a_position * size;
|
|
69
76
|
vec2 clipPos = (pos / u_resolution) * 2.0 - 1.0;
|
|
@@ -134,13 +141,42 @@ export class SharedWebGLContext {
|
|
|
134
141
|
glyphProgram = null;
|
|
135
142
|
quadVBO = null;
|
|
136
143
|
quadEBO = null;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
// Double-buffered VBOs — two each for bg and glyph instance data
|
|
145
|
+
bgInstanceVBOs = [null, null];
|
|
146
|
+
glyphInstanceVBOs = [null, null];
|
|
147
|
+
bufferIndex = 0; // toggles 0/1 each frame
|
|
148
|
+
bgVAOs = [null, null];
|
|
149
|
+
glyphVAOs = [null, null];
|
|
150
|
+
// Cached uniform locations
|
|
151
|
+
bgUniforms = { u_resolution: null, u_cellSize: null };
|
|
152
|
+
glyphUniforms = { u_resolution: null, u_cellSize: null, u_atlas: null };
|
|
153
|
+
// Cached attribute locations (looked up once in initGLResources)
|
|
154
|
+
bgAttribLocs = { a_position: -1, a_cellPos: -1, a_color: -1, a_offset: -1 };
|
|
155
|
+
glyphAttribLocs = {
|
|
156
|
+
a_position: -1,
|
|
157
|
+
a_cellPos: -1,
|
|
158
|
+
a_color: -1,
|
|
159
|
+
a_texCoord: -1,
|
|
160
|
+
a_glyphSize: -1,
|
|
161
|
+
a_offset: -1,
|
|
162
|
+
};
|
|
163
|
+
// Instance data (CPU side) — persistent across frames for dirty-row optimization
|
|
164
|
+
// Sized for ALL terminals combined (not per-terminal)
|
|
142
165
|
bgInstances;
|
|
143
166
|
glyphInstances;
|
|
167
|
+
// Per-terminal dirty tracking state
|
|
168
|
+
terminalBgCounts = new Map();
|
|
169
|
+
terminalGlyphCounts = new Map();
|
|
170
|
+
terminalRowBgOffsets = new Map(); // bgCount at start of each row
|
|
171
|
+
terminalRowGlyphOffsets = new Map(); // glyphCount at start of each row
|
|
172
|
+
terminalRowBgCounts = new Map(); // bg instances per row
|
|
173
|
+
terminalRowGlyphCounts = new Map(); // glyph instances per row
|
|
174
|
+
terminalFullyRendered = new Set(); // tracks if terminal has had initial full render
|
|
175
|
+
// Per-terminal cached instance data (for reuse when terminal is clean)
|
|
176
|
+
terminalBgData = new Map();
|
|
177
|
+
terminalGlyphData = new Map();
|
|
178
|
+
// Reusable cursor data buffer — grows as needed, never shrinks
|
|
179
|
+
cursorBuffer = new Float32Array(4 * SC_BG_INSTANCE_FLOATS);
|
|
144
180
|
// Glyph atlas
|
|
145
181
|
atlas;
|
|
146
182
|
// Theme / palette
|
|
@@ -165,10 +201,10 @@ export class SharedWebGLContext {
|
|
|
165
201
|
this.buildPaletteFloat();
|
|
166
202
|
this.measureCellSize();
|
|
167
203
|
this.atlas = new GlyphAtlas(Math.round(this.fontSize * this.dpr), this.fontFamily);
|
|
168
|
-
// Pre-allocate instance buffers
|
|
169
|
-
const maxCells = 80 * 24;
|
|
170
|
-
this.bgInstances = new Float32Array(maxCells *
|
|
171
|
-
this.glyphInstances = new Float32Array(maxCells *
|
|
204
|
+
// Pre-allocate instance buffers for batched rendering (all terminals combined)
|
|
205
|
+
const maxCells = 80 * 24 * 4; // start with 4 terminals worth
|
|
206
|
+
this.bgInstances = new Float32Array(maxCells * SC_BG_INSTANCE_FLOATS);
|
|
207
|
+
this.glyphInstances = new Float32Array(maxCells * SC_GLYPH_INSTANCE_FLOATS);
|
|
172
208
|
// Create the shared canvas
|
|
173
209
|
this.canvas = document.createElement("canvas");
|
|
174
210
|
this.canvas.style.display = "block";
|
|
@@ -191,6 +227,17 @@ export class SharedWebGLContext {
|
|
|
191
227
|
if (!this.gl) {
|
|
192
228
|
throw new Error("WebGL2 is not available");
|
|
193
229
|
}
|
|
230
|
+
// Detect software rendering (SwiftShader) — shared context is a net loss
|
|
231
|
+
// on software renderers because it concentrates all panes on one slow context.
|
|
232
|
+
// Fall back to independent per-pane rendering in that case.
|
|
233
|
+
const debugInfo = this.gl.getExtension("WEBGL_debug_renderer_info");
|
|
234
|
+
if (debugInfo) {
|
|
235
|
+
const renderer = this.gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
236
|
+
if (/swiftshader|llvmpipe|software/i.test(renderer)) {
|
|
237
|
+
this.gl = null;
|
|
238
|
+
throw new Error(`Software renderer detected (${renderer}), skipping shared context`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
194
241
|
this.initGLResources();
|
|
195
242
|
}
|
|
196
243
|
// -----------------------------------------------------------------------
|
|
@@ -215,16 +262,32 @@ export class SharedWebGLContext {
|
|
|
215
262
|
if (entry) {
|
|
216
263
|
entry.grid = grid;
|
|
217
264
|
entry.cursor = cursor;
|
|
265
|
+
// Reset dirty tracking — new grid may have different content
|
|
266
|
+
this.terminalFullyRendered.delete(id);
|
|
218
267
|
}
|
|
219
268
|
}
|
|
220
269
|
removeTerminal(id) {
|
|
221
270
|
this.terminals.delete(id);
|
|
271
|
+
// Clean up per-terminal dirty tracking state
|
|
272
|
+
this.terminalBgCounts.delete(id);
|
|
273
|
+
this.terminalGlyphCounts.delete(id);
|
|
274
|
+
this.terminalRowBgOffsets.delete(id);
|
|
275
|
+
this.terminalRowGlyphOffsets.delete(id);
|
|
276
|
+
this.terminalRowBgCounts.delete(id);
|
|
277
|
+
this.terminalRowGlyphCounts.delete(id);
|
|
278
|
+
this.terminalFullyRendered.delete(id);
|
|
279
|
+
this.terminalBgData.delete(id);
|
|
280
|
+
this.terminalGlyphData.delete(id);
|
|
222
281
|
}
|
|
223
282
|
getTerminalIds() {
|
|
224
283
|
return Array.from(this.terminals.keys());
|
|
225
284
|
}
|
|
226
285
|
/**
|
|
227
|
-
* Render all terminals in one frame.
|
|
286
|
+
* Render all terminals in one frame using batched rendering.
|
|
287
|
+
*
|
|
288
|
+
* All terminals' instance data is packed into combined buffers with
|
|
289
|
+
* per-instance viewport offsets, then uploaded and drawn in single calls.
|
|
290
|
+
* This reduces GL state changes from O(N) to O(1) per pass.
|
|
228
291
|
*/
|
|
229
292
|
render() {
|
|
230
293
|
if (this.disposed || !this.gl)
|
|
@@ -232,15 +295,132 @@ export class SharedWebGLContext {
|
|
|
232
295
|
const gl = this.gl;
|
|
233
296
|
const canvasWidth = this.canvas.width;
|
|
234
297
|
const canvasHeight = this.canvas.height;
|
|
298
|
+
// Toggle double-buffer index
|
|
299
|
+
this.bufferIndex ^= 1;
|
|
300
|
+
const bi = this.bufferIndex;
|
|
235
301
|
// Full-canvas clear
|
|
236
302
|
gl.viewport(0, 0, canvasWidth, canvasHeight);
|
|
237
303
|
gl.clearColor(0, 0, 0, 0);
|
|
238
304
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
305
|
+
// --- Phase 1: Clear each terminal's viewport with theme bg color ---
|
|
306
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
307
|
+
const bgR = this.themeBgFloat[0];
|
|
308
|
+
const bgG = this.themeBgFloat[1];
|
|
309
|
+
const bgB = this.themeBgFloat[2];
|
|
310
|
+
gl.clearColor(bgR, bgG, bgB, 1.0);
|
|
311
|
+
for (const [, entry] of this.terminals) {
|
|
312
|
+
const { viewport } = entry;
|
|
313
|
+
const vpX = Math.round(viewport.x * this.dpr);
|
|
314
|
+
const vpY = Math.round(viewport.y * this.dpr);
|
|
315
|
+
const vpW = Math.round(viewport.width * this.dpr);
|
|
316
|
+
const vpH = Math.round(viewport.height * this.dpr);
|
|
317
|
+
const glY = canvasHeight - vpY - vpH;
|
|
318
|
+
gl.viewport(vpX, glY, vpW, vpH);
|
|
319
|
+
gl.scissor(vpX, glY, vpW, vpH);
|
|
320
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
242
321
|
}
|
|
243
322
|
gl.disable(gl.SCISSOR_TEST);
|
|
323
|
+
// --- Phase 2: Build combined instance data for all terminals ---
|
|
324
|
+
const cellW = this.cellWidth * this.dpr;
|
|
325
|
+
const cellH = this.cellHeight * this.dpr;
|
|
326
|
+
let totalBgCount = 0;
|
|
327
|
+
let totalGlyphCount = 0;
|
|
328
|
+
for (const [id, entry] of this.terminals) {
|
|
329
|
+
const { bgCount, glyphCount } = this.buildTerminalInstances(id, entry);
|
|
330
|
+
const bgData = this.terminalBgData.get(id);
|
|
331
|
+
const glyphData = this.terminalGlyphData.get(id);
|
|
332
|
+
// Ensure combined buffers are large enough
|
|
333
|
+
const neededBg = (totalBgCount + bgCount) * SC_BG_INSTANCE_FLOATS;
|
|
334
|
+
if (neededBg > this.bgInstances.length) {
|
|
335
|
+
const newBuf = new Float32Array(neededBg * 2);
|
|
336
|
+
newBuf.set(this.bgInstances.subarray(0, totalBgCount * SC_BG_INSTANCE_FLOATS));
|
|
337
|
+
this.bgInstances = newBuf;
|
|
338
|
+
}
|
|
339
|
+
const neededGlyph = (totalGlyphCount + glyphCount) * SC_GLYPH_INSTANCE_FLOATS;
|
|
340
|
+
if (neededGlyph > this.glyphInstances.length) {
|
|
341
|
+
const newBuf = new Float32Array(neededGlyph * 2);
|
|
342
|
+
newBuf.set(this.glyphInstances.subarray(0, totalGlyphCount * SC_GLYPH_INSTANCE_FLOATS));
|
|
343
|
+
this.glyphInstances = newBuf;
|
|
344
|
+
}
|
|
345
|
+
// Copy per-terminal data into combined buffer
|
|
346
|
+
if (bgData && bgCount > 0) {
|
|
347
|
+
this.bgInstances.set(bgData.subarray(0, bgCount * SC_BG_INSTANCE_FLOATS), totalBgCount * SC_BG_INSTANCE_FLOATS);
|
|
348
|
+
}
|
|
349
|
+
if (glyphData && glyphCount > 0) {
|
|
350
|
+
this.glyphInstances.set(glyphData.subarray(0, glyphCount * SC_GLYPH_INSTANCE_FLOATS), totalGlyphCount * SC_GLYPH_INSTANCE_FLOATS);
|
|
351
|
+
}
|
|
352
|
+
totalBgCount += bgCount;
|
|
353
|
+
totalGlyphCount += glyphCount;
|
|
354
|
+
}
|
|
355
|
+
// Upload atlas if dirty
|
|
356
|
+
this.atlas.upload(gl);
|
|
357
|
+
// --- Phase 3: Single viewport for all draws (full canvas) ---
|
|
358
|
+
gl.viewport(0, 0, canvasWidth, canvasHeight);
|
|
359
|
+
// --- Background pass: single upload + single draw ---
|
|
360
|
+
const activeBgVBO = this.bgInstanceVBOs[bi];
|
|
361
|
+
const activeBgVAO = this.bgVAOs[bi];
|
|
362
|
+
if (totalBgCount > 0 && this.bgProgram && activeBgVAO && activeBgVBO) {
|
|
363
|
+
gl.useProgram(this.bgProgram);
|
|
364
|
+
gl.uniform2f(this.bgUniforms.u_resolution, canvasWidth, canvasHeight);
|
|
365
|
+
gl.uniform2f(this.bgUniforms.u_cellSize, cellW, cellH);
|
|
366
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, activeBgVBO);
|
|
367
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.bgInstances.subarray(0, totalBgCount * SC_BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
368
|
+
gl.bindVertexArray(activeBgVAO);
|
|
369
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, totalBgCount);
|
|
370
|
+
}
|
|
371
|
+
// --- Glyph pass: single upload + single draw ---
|
|
372
|
+
const activeGlyphVBO = this.glyphInstanceVBOs[bi];
|
|
373
|
+
const activeGlyphVAO = this.glyphVAOs[bi];
|
|
374
|
+
if (totalGlyphCount > 0 && this.glyphProgram && activeGlyphVAO && activeGlyphVBO) {
|
|
375
|
+
gl.enable(gl.BLEND);
|
|
376
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
377
|
+
gl.useProgram(this.glyphProgram);
|
|
378
|
+
gl.uniform2f(this.glyphUniforms.u_resolution, canvasWidth, canvasHeight);
|
|
379
|
+
gl.uniform2f(this.glyphUniforms.u_cellSize, cellW, cellH);
|
|
380
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
381
|
+
gl.bindTexture(gl.TEXTURE_2D, this.atlas.getTexture());
|
|
382
|
+
gl.uniform1i(this.glyphUniforms.u_atlas, 0);
|
|
383
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, activeGlyphVBO);
|
|
384
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.glyphInstances.subarray(0, totalGlyphCount * SC_GLYPH_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
385
|
+
gl.bindVertexArray(activeGlyphVAO);
|
|
386
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, totalGlyphCount);
|
|
387
|
+
gl.disable(gl.BLEND);
|
|
388
|
+
}
|
|
389
|
+
// --- Cursor pass: batch all cursors into one draw ---
|
|
390
|
+
let cursorCount = 0;
|
|
391
|
+
const neededCursor = this.terminals.size * SC_BG_INSTANCE_FLOATS;
|
|
392
|
+
if (neededCursor > this.cursorBuffer.length) {
|
|
393
|
+
this.cursorBuffer = new Float32Array(neededCursor);
|
|
394
|
+
}
|
|
395
|
+
const cc = this.themeCursorFloat;
|
|
396
|
+
for (const [, entry] of this.terminals) {
|
|
397
|
+
const { cursor, viewport } = entry;
|
|
398
|
+
if (!cursor.visible)
|
|
399
|
+
continue;
|
|
400
|
+
const off = cursorCount * SC_BG_INSTANCE_FLOATS;
|
|
401
|
+
this.cursorBuffer[off] = cursor.col;
|
|
402
|
+
this.cursorBuffer[off + 1] = cursor.row;
|
|
403
|
+
this.cursorBuffer[off + 2] = cc[0];
|
|
404
|
+
this.cursorBuffer[off + 3] = cc[1];
|
|
405
|
+
this.cursorBuffer[off + 4] = cc[2];
|
|
406
|
+
this.cursorBuffer[off + 5] = 0.5;
|
|
407
|
+
this.cursorBuffer[off + 6] = Math.round(viewport.x * this.dpr);
|
|
408
|
+
this.cursorBuffer[off + 7] = Math.round(viewport.y * this.dpr);
|
|
409
|
+
cursorCount++;
|
|
410
|
+
}
|
|
411
|
+
if (cursorCount > 0 && this.bgProgram && activeBgVAO && activeBgVBO) {
|
|
412
|
+
gl.enable(gl.BLEND);
|
|
413
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
414
|
+
gl.useProgram(this.bgProgram);
|
|
415
|
+
gl.uniform2f(this.bgUniforms.u_resolution, canvasWidth, canvasHeight);
|
|
416
|
+
gl.uniform2f(this.bgUniforms.u_cellSize, cellW, cellH);
|
|
417
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, activeBgVBO);
|
|
418
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.cursorBuffer.subarray(0, cursorCount * SC_BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
419
|
+
gl.bindVertexArray(activeBgVAO);
|
|
420
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, cursorCount);
|
|
421
|
+
gl.disable(gl.BLEND);
|
|
422
|
+
}
|
|
423
|
+
gl.bindVertexArray(null);
|
|
244
424
|
}
|
|
245
425
|
/**
|
|
246
426
|
* Update the shared canvas size to match a container element.
|
|
@@ -290,14 +470,17 @@ export class SharedWebGLContext {
|
|
|
290
470
|
gl.deleteBuffer(this.quadVBO);
|
|
291
471
|
if (this.quadEBO)
|
|
292
472
|
gl.deleteBuffer(this.quadEBO);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
473
|
+
// Clean up double-buffered resources
|
|
474
|
+
for (let i = 0; i < 2; i++) {
|
|
475
|
+
if (this.bgInstanceVBOs[i])
|
|
476
|
+
gl.deleteBuffer(this.bgInstanceVBOs[i]);
|
|
477
|
+
if (this.glyphInstanceVBOs[i])
|
|
478
|
+
gl.deleteBuffer(this.glyphInstanceVBOs[i]);
|
|
479
|
+
if (this.bgVAOs[i])
|
|
480
|
+
gl.deleteVertexArray(this.bgVAOs[i]);
|
|
481
|
+
if (this.glyphVAOs[i])
|
|
482
|
+
gl.deleteVertexArray(this.glyphVAOs[i]);
|
|
483
|
+
}
|
|
301
484
|
}
|
|
302
485
|
this.terminals.clear();
|
|
303
486
|
if (this.canvas.parentElement) {
|
|
@@ -306,115 +489,159 @@ export class SharedWebGLContext {
|
|
|
306
489
|
this.gl = null;
|
|
307
490
|
}
|
|
308
491
|
// -----------------------------------------------------------------------
|
|
309
|
-
// Per-terminal
|
|
492
|
+
// Per-terminal instance data building (CPU-side only, no GL calls)
|
|
310
493
|
// -----------------------------------------------------------------------
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
494
|
+
/**
|
|
495
|
+
* Build instance data for a single terminal into its per-terminal cache.
|
|
496
|
+
* Returns the bg and glyph instance counts. The caller assembles all
|
|
497
|
+
* terminals' data into the combined buffer before issuing GL draws.
|
|
498
|
+
*/
|
|
499
|
+
buildTerminalInstances(id, entry) {
|
|
500
|
+
const { grid, viewport } = entry;
|
|
316
501
|
const cols = grid.cols;
|
|
317
502
|
const rows = grid.rows;
|
|
318
|
-
//
|
|
503
|
+
// Viewport offset in device pixels for canvas-space coordinates
|
|
319
504
|
const vpX = Math.round(viewport.x * this.dpr);
|
|
320
505
|
const vpY = Math.round(viewport.y * this.dpr);
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
506
|
+
// Check if any rows are dirty; if not, use cached counts
|
|
507
|
+
const isFirstRender = !this.terminalFullyRendered.has(id);
|
|
508
|
+
let anyDirty = isFirstRender;
|
|
509
|
+
if (!anyDirty) {
|
|
510
|
+
for (let row = 0; row < rows; row++) {
|
|
511
|
+
if (grid.isDirty(row)) {
|
|
512
|
+
anyDirty = true;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!anyDirty) {
|
|
518
|
+
// No dirty rows — reuse cached instance data and counts
|
|
519
|
+
return {
|
|
520
|
+
bgCount: this.terminalBgCounts.get(id) ?? 0,
|
|
521
|
+
glyphCount: this.terminalGlyphCounts.get(id) ?? 0,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// Ensure per-terminal data buffers are large enough
|
|
333
525
|
const totalCells = cols * rows;
|
|
334
|
-
|
|
335
|
-
|
|
526
|
+
let bgData = this.terminalBgData.get(id);
|
|
527
|
+
if (!bgData || bgData.length < totalCells * SC_BG_INSTANCE_FLOATS) {
|
|
528
|
+
bgData = new Float32Array(totalCells * SC_BG_INSTANCE_FLOATS);
|
|
529
|
+
this.terminalBgData.set(id, bgData);
|
|
530
|
+
}
|
|
531
|
+
let glyphData = this.terminalGlyphData.get(id);
|
|
532
|
+
if (!glyphData || glyphData.length < totalCells * SC_GLYPH_INSTANCE_FLOATS) {
|
|
533
|
+
glyphData = new Float32Array(totalCells * SC_GLYPH_INSTANCE_FLOATS);
|
|
534
|
+
this.terminalGlyphData.set(id, glyphData);
|
|
535
|
+
}
|
|
536
|
+
// Initialize or retrieve per-row offset tracking
|
|
537
|
+
let rowBgOffsets = this.terminalRowBgOffsets.get(id);
|
|
538
|
+
let rowGlyphOffsets = this.terminalRowGlyphOffsets.get(id);
|
|
539
|
+
let rowBgCounts = this.terminalRowBgCounts.get(id);
|
|
540
|
+
let rowGlyphCounts = this.terminalRowGlyphCounts.get(id);
|
|
541
|
+
if (!rowBgOffsets || rowBgOffsets.length !== rows) {
|
|
542
|
+
rowBgOffsets = new Array(rows).fill(0);
|
|
543
|
+
this.terminalRowBgOffsets.set(id, rowBgOffsets);
|
|
544
|
+
}
|
|
545
|
+
if (!rowGlyphOffsets || rowGlyphOffsets.length !== rows) {
|
|
546
|
+
rowGlyphOffsets = new Array(rows).fill(0);
|
|
547
|
+
this.terminalRowGlyphOffsets.set(id, rowGlyphOffsets);
|
|
548
|
+
}
|
|
549
|
+
if (!rowBgCounts || rowBgCounts.length !== rows) {
|
|
550
|
+
rowBgCounts = new Array(rows).fill(0);
|
|
551
|
+
this.terminalRowBgCounts.set(id, rowBgCounts);
|
|
336
552
|
}
|
|
337
|
-
if (
|
|
338
|
-
|
|
553
|
+
if (!rowGlyphCounts || rowGlyphCounts.length !== rows) {
|
|
554
|
+
rowGlyphCounts = new Array(rows).fill(0);
|
|
555
|
+
this.terminalRowGlyphCounts.set(id, rowGlyphCounts);
|
|
339
556
|
}
|
|
340
|
-
// Build instance data
|
|
341
557
|
let bgCount = 0;
|
|
342
558
|
let glyphCount = 0;
|
|
343
559
|
for (let row = 0; row < rows; row++) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (attrs & ATTR_INVERSE) {
|
|
354
|
-
const tmp = fg;
|
|
355
|
-
fg = bg;
|
|
356
|
-
bg = tmp;
|
|
560
|
+
const rowDirty = isFirstRender || grid.isDirty(row);
|
|
561
|
+
if (!rowDirty) {
|
|
562
|
+
// Row is clean — copy previous data to new offsets if they shifted
|
|
563
|
+
const prevBgOffset = rowBgOffsets[row];
|
|
564
|
+
const prevBgCount = rowBgCounts[row];
|
|
565
|
+
const prevGlyphOffset = rowGlyphOffsets[row];
|
|
566
|
+
const prevGlyphCount = rowGlyphCounts[row];
|
|
567
|
+
if (bgCount !== prevBgOffset && prevBgCount > 0) {
|
|
568
|
+
bgData.copyWithin(bgCount * SC_BG_INSTANCE_FLOATS, prevBgOffset * SC_BG_INSTANCE_FLOATS, (prevBgOffset + prevBgCount) * SC_BG_INSTANCE_FLOATS);
|
|
357
569
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
570
|
+
if (glyphCount !== prevGlyphOffset && prevGlyphCount > 0) {
|
|
571
|
+
glyphData.copyWithin(glyphCount * SC_GLYPH_INSTANCE_FLOATS, prevGlyphOffset * SC_GLYPH_INSTANCE_FLOATS, (prevGlyphOffset + prevGlyphCount) * SC_GLYPH_INSTANCE_FLOATS);
|
|
572
|
+
}
|
|
573
|
+
rowBgOffsets[row] = bgCount;
|
|
574
|
+
rowGlyphOffsets[row] = glyphCount;
|
|
575
|
+
bgCount += prevBgCount;
|
|
576
|
+
glyphCount += prevGlyphCount;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
// Row is dirty — re-pack cell data with viewport offsets
|
|
580
|
+
rowBgOffsets[row] = bgCount;
|
|
581
|
+
rowGlyphOffsets[row] = glyphCount;
|
|
582
|
+
let rowBg = 0;
|
|
583
|
+
let rowGlyph = 0;
|
|
584
|
+
for (let col = 0; col < cols; col++) {
|
|
585
|
+
const codepoint = grid.getCodepoint(row, col);
|
|
586
|
+
const fgIdx = grid.getFgIndex(row, col);
|
|
587
|
+
const bgIdx = grid.getBgIndex(row, col);
|
|
588
|
+
const attrs = grid.getAttrs(row, col);
|
|
589
|
+
const fgIsRGB = grid.isFgRGB(row, col);
|
|
590
|
+
const bgIsRGB = grid.isBgRGB(row, col);
|
|
591
|
+
let fg = resolveColorFloat(fgIdx, fgIsRGB, grid, col, true, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
|
|
592
|
+
let bg = resolveColorFloat(bgIdx, bgIsRGB, grid, col, false, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
|
|
593
|
+
if (attrs & ATTR_INVERSE) {
|
|
594
|
+
const tmp = fg;
|
|
595
|
+
fg = bg;
|
|
596
|
+
bg = tmp;
|
|
597
|
+
}
|
|
598
|
+
// Pack bg instance with viewport offset
|
|
599
|
+
const bOff = bgCount * SC_BG_INSTANCE_FLOATS;
|
|
600
|
+
bgData[bOff] = col;
|
|
601
|
+
bgData[bOff + 1] = row;
|
|
602
|
+
bgData[bOff + 2] = bg[0];
|
|
603
|
+
bgData[bOff + 3] = bg[1];
|
|
604
|
+
bgData[bOff + 4] = bg[2];
|
|
605
|
+
bgData[bOff + 5] = bg[3];
|
|
606
|
+
bgData[bOff + 6] = vpX;
|
|
607
|
+
bgData[bOff + 7] = vpY;
|
|
608
|
+
bgCount++;
|
|
609
|
+
rowBg++;
|
|
610
|
+
if (codepoint > 0x20) {
|
|
611
|
+
const bold = !!(attrs & ATTR_BOLD);
|
|
612
|
+
const italic = !!(attrs & ATTR_ITALIC);
|
|
613
|
+
const glyph = this.atlas.getGlyph(codepoint, bold, italic);
|
|
614
|
+
if (glyph) {
|
|
615
|
+
// Pack glyph instance with viewport offset
|
|
616
|
+
const gOff = glyphCount * SC_GLYPH_INSTANCE_FLOATS;
|
|
617
|
+
glyphData[gOff] = col;
|
|
618
|
+
glyphData[gOff + 1] = row;
|
|
619
|
+
glyphData[gOff + 2] = fg[0];
|
|
620
|
+
glyphData[gOff + 3] = fg[1];
|
|
621
|
+
glyphData[gOff + 4] = fg[2];
|
|
622
|
+
glyphData[gOff + 5] = fg[3];
|
|
623
|
+
glyphData[gOff + 6] = glyph.u;
|
|
624
|
+
glyphData[gOff + 7] = glyph.v;
|
|
625
|
+
glyphData[gOff + 8] = glyph.w;
|
|
626
|
+
glyphData[gOff + 9] = glyph.h;
|
|
627
|
+
glyphData[gOff + 10] = glyph.pw;
|
|
628
|
+
glyphData[gOff + 11] = glyph.ph;
|
|
629
|
+
glyphData[gOff + 12] = vpX;
|
|
630
|
+
glyphData[gOff + 13] = vpY;
|
|
631
|
+
glyphCount++;
|
|
632
|
+
rowGlyph++;
|
|
633
|
+
}
|
|
367
634
|
}
|
|
368
635
|
}
|
|
636
|
+
rowBgCounts[row] = rowBg;
|
|
637
|
+
rowGlyphCounts[row] = rowGlyph;
|
|
638
|
+
grid.clearDirty(row);
|
|
369
639
|
}
|
|
370
|
-
grid.clearDirty(row);
|
|
371
|
-
}
|
|
372
|
-
// Upload atlas if dirty
|
|
373
|
-
this.atlas.upload(gl);
|
|
374
|
-
const cellW = this.cellWidth * this.dpr;
|
|
375
|
-
const cellH = this.cellHeight * this.dpr;
|
|
376
|
-
// --- Background pass ---
|
|
377
|
-
if (bgCount > 0 && this.bgProgram && this.bgVAO && this.bgInstanceVBO) {
|
|
378
|
-
gl.useProgram(this.bgProgram);
|
|
379
|
-
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), vpW, vpH);
|
|
380
|
-
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), cellW, cellH);
|
|
381
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
|
|
382
|
-
gl.bufferData(gl.ARRAY_BUFFER, this.bgInstances.subarray(0, bgCount * BG_INSTANCE_FLOATS), gl.DYNAMIC_DRAW);
|
|
383
|
-
gl.bindVertexArray(this.bgVAO);
|
|
384
|
-
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, bgCount);
|
|
385
|
-
}
|
|
386
|
-
// --- Glyph pass ---
|
|
387
|
-
if (glyphCount > 0 && this.glyphProgram && this.glyphVAO && this.glyphInstanceVBO) {
|
|
388
|
-
gl.enable(gl.BLEND);
|
|
389
|
-
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
390
|
-
gl.useProgram(this.glyphProgram);
|
|
391
|
-
gl.uniform2f(gl.getUniformLocation(this.glyphProgram, "u_resolution"), vpW, vpH);
|
|
392
|
-
gl.uniform2f(gl.getUniformLocation(this.glyphProgram, "u_cellSize"), cellW, cellH);
|
|
393
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
394
|
-
gl.bindTexture(gl.TEXTURE_2D, this.atlas.getTexture());
|
|
395
|
-
gl.uniform1i(gl.getUniformLocation(this.glyphProgram, "u_atlas"), 0);
|
|
396
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBO);
|
|
397
|
-
gl.bufferData(gl.ARRAY_BUFFER, this.glyphInstances.subarray(0, glyphCount * GLYPH_INSTANCE_FLOATS), gl.DYNAMIC_DRAW);
|
|
398
|
-
gl.bindVertexArray(this.glyphVAO);
|
|
399
|
-
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, glyphCount);
|
|
400
|
-
gl.disable(gl.BLEND);
|
|
401
|
-
}
|
|
402
|
-
// --- Cursor ---
|
|
403
|
-
if (cursor.visible && this.bgProgram && this.bgVAO && this.bgInstanceVBO) {
|
|
404
|
-
const cc = this.themeCursorFloat;
|
|
405
|
-
gl.enable(gl.BLEND);
|
|
406
|
-
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
407
|
-
gl.useProgram(this.bgProgram);
|
|
408
|
-
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), vpW, vpH);
|
|
409
|
-
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), cellW, cellH);
|
|
410
|
-
const cursorData = new Float32Array([cursor.col, cursor.row, cc[0], cc[1], cc[2], 0.5]);
|
|
411
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
|
|
412
|
-
gl.bufferData(gl.ARRAY_BUFFER, cursorData, gl.DYNAMIC_DRAW);
|
|
413
|
-
gl.bindVertexArray(this.bgVAO);
|
|
414
|
-
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, 1);
|
|
415
|
-
gl.disable(gl.BLEND);
|
|
416
640
|
}
|
|
417
|
-
|
|
641
|
+
this.terminalBgCounts.set(id, bgCount);
|
|
642
|
+
this.terminalGlyphCounts.set(id, glyphCount);
|
|
643
|
+
this.terminalFullyRendered.add(id);
|
|
644
|
+
return { bgCount, glyphCount };
|
|
418
645
|
}
|
|
419
646
|
// -----------------------------------------------------------------------
|
|
420
647
|
// GL resource initialization
|
|
@@ -425,6 +652,27 @@ export class SharedWebGLContext {
|
|
|
425
652
|
return;
|
|
426
653
|
this.bgProgram = createProgram(gl, BG_VERTEX_SHADER, BG_FRAGMENT_SHADER);
|
|
427
654
|
this.glyphProgram = createProgram(gl, GLYPH_VERTEX_SHADER, GLYPH_FRAGMENT_SHADER);
|
|
655
|
+
// Cache all uniform locations after program creation
|
|
656
|
+
this.bgUniforms.u_resolution = gl.getUniformLocation(this.bgProgram, "u_resolution");
|
|
657
|
+
this.bgUniforms.u_cellSize = gl.getUniformLocation(this.bgProgram, "u_cellSize");
|
|
658
|
+
this.glyphUniforms.u_resolution = gl.getUniformLocation(this.glyphProgram, "u_resolution");
|
|
659
|
+
this.glyphUniforms.u_cellSize = gl.getUniformLocation(this.glyphProgram, "u_cellSize");
|
|
660
|
+
this.glyphUniforms.u_atlas = gl.getUniformLocation(this.glyphProgram, "u_atlas");
|
|
661
|
+
// Cache all attribute locations after program creation
|
|
662
|
+
this.bgAttribLocs = {
|
|
663
|
+
a_position: gl.getAttribLocation(this.bgProgram, "a_position"),
|
|
664
|
+
a_cellPos: gl.getAttribLocation(this.bgProgram, "a_cellPos"),
|
|
665
|
+
a_color: gl.getAttribLocation(this.bgProgram, "a_color"),
|
|
666
|
+
a_offset: gl.getAttribLocation(this.bgProgram, "a_offset"),
|
|
667
|
+
};
|
|
668
|
+
this.glyphAttribLocs = {
|
|
669
|
+
a_position: gl.getAttribLocation(this.glyphProgram, "a_position"),
|
|
670
|
+
a_cellPos: gl.getAttribLocation(this.glyphProgram, "a_cellPos"),
|
|
671
|
+
a_color: gl.getAttribLocation(this.glyphProgram, "a_color"),
|
|
672
|
+
a_texCoord: gl.getAttribLocation(this.glyphProgram, "a_texCoord"),
|
|
673
|
+
a_glyphSize: gl.getAttribLocation(this.glyphProgram, "a_glyphSize"),
|
|
674
|
+
a_offset: gl.getAttribLocation(this.glyphProgram, "a_offset"),
|
|
675
|
+
};
|
|
428
676
|
const quadVerts = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
|
429
677
|
const quadIndices = new Uint16Array([0, 1, 2, 2, 1, 3]);
|
|
430
678
|
this.quadVBO = gl.createBuffer();
|
|
@@ -433,94 +681,90 @@ export class SharedWebGLContext {
|
|
|
433
681
|
this.quadEBO = gl.createBuffer();
|
|
434
682
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
435
683
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices, gl.STATIC_DRAW);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
684
|
+
// Create double-buffered VBOs and VAOs
|
|
685
|
+
for (let i = 0; i < 2; i++) {
|
|
686
|
+
this.bgInstanceVBOs[i] = gl.createBuffer();
|
|
687
|
+
this.glyphInstanceVBOs[i] = gl.createBuffer();
|
|
688
|
+
this.bgVAOs[i] = gl.createVertexArray();
|
|
689
|
+
gl.bindVertexArray(this.bgVAOs[i]);
|
|
690
|
+
const bgVBO = this.bgInstanceVBOs[i];
|
|
691
|
+
if (bgVBO)
|
|
692
|
+
this.setupBgVAO(gl, bgVBO);
|
|
693
|
+
gl.bindVertexArray(null);
|
|
694
|
+
this.glyphVAOs[i] = gl.createVertexArray();
|
|
695
|
+
gl.bindVertexArray(this.glyphVAOs[i]);
|
|
696
|
+
const glyphVBO = this.glyphInstanceVBOs[i];
|
|
697
|
+
if (glyphVBO)
|
|
698
|
+
this.setupGlyphVAO(gl, glyphVBO);
|
|
699
|
+
gl.bindVertexArray(null);
|
|
700
|
+
}
|
|
446
701
|
}
|
|
447
|
-
setupBgVAO(gl) {
|
|
702
|
+
setupBgVAO(gl, instanceVBO) {
|
|
448
703
|
const FLOAT = 4;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const program = this.bgProgram;
|
|
452
|
-
const aPos = gl.getAttribLocation(program, "a_position");
|
|
704
|
+
const locs = this.bgAttribLocs;
|
|
705
|
+
// Quad position (per-vertex, from quadVBO)
|
|
453
706
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVBO);
|
|
454
|
-
gl.enableVertexAttribArray(
|
|
455
|
-
gl.vertexAttribPointer(
|
|
707
|
+
gl.enableVertexAttribArray(locs.a_position);
|
|
708
|
+
gl.vertexAttribPointer(locs.a_position, 2, gl.FLOAT, false, 0, 0);
|
|
456
709
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
gl.enableVertexAttribArray(
|
|
461
|
-
gl.vertexAttribPointer(
|
|
462
|
-
gl.vertexAttribDivisor(
|
|
463
|
-
|
|
464
|
-
gl.
|
|
465
|
-
gl.
|
|
466
|
-
gl.
|
|
710
|
+
// Instance attributes (from instanceVBO)
|
|
711
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instanceVBO);
|
|
712
|
+
const stride = SC_BG_INSTANCE_FLOATS * FLOAT;
|
|
713
|
+
gl.enableVertexAttribArray(locs.a_cellPos);
|
|
714
|
+
gl.vertexAttribPointer(locs.a_cellPos, 2, gl.FLOAT, false, stride, 0);
|
|
715
|
+
gl.vertexAttribDivisor(locs.a_cellPos, 1);
|
|
716
|
+
gl.enableVertexAttribArray(locs.a_color);
|
|
717
|
+
gl.vertexAttribPointer(locs.a_color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
|
|
718
|
+
gl.vertexAttribDivisor(locs.a_color, 1);
|
|
719
|
+
gl.enableVertexAttribArray(locs.a_offset);
|
|
720
|
+
gl.vertexAttribPointer(locs.a_offset, 2, gl.FLOAT, false, stride, 6 * FLOAT);
|
|
721
|
+
gl.vertexAttribDivisor(locs.a_offset, 1);
|
|
467
722
|
}
|
|
468
|
-
setupGlyphVAO(gl) {
|
|
723
|
+
setupGlyphVAO(gl, instanceVBO) {
|
|
469
724
|
const FLOAT = 4;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const program = this.glyphProgram;
|
|
473
|
-
const aPos = gl.getAttribLocation(program, "a_position");
|
|
725
|
+
const locs = this.glyphAttribLocs;
|
|
726
|
+
// Quad position (per-vertex, from quadVBO)
|
|
474
727
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVBO);
|
|
475
|
-
gl.enableVertexAttribArray(
|
|
476
|
-
gl.vertexAttribPointer(
|
|
728
|
+
gl.enableVertexAttribArray(locs.a_position);
|
|
729
|
+
gl.vertexAttribPointer(locs.a_position, 2, gl.FLOAT, false, 0, 0);
|
|
477
730
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
gl.enableVertexAttribArray(
|
|
482
|
-
gl.vertexAttribPointer(
|
|
483
|
-
gl.vertexAttribDivisor(
|
|
484
|
-
|
|
485
|
-
gl.
|
|
486
|
-
gl.
|
|
487
|
-
gl.
|
|
488
|
-
|
|
489
|
-
gl.
|
|
490
|
-
gl.
|
|
491
|
-
gl.
|
|
492
|
-
|
|
493
|
-
gl.enableVertexAttribArray(
|
|
494
|
-
gl.vertexAttribPointer(
|
|
495
|
-
gl.vertexAttribDivisor(
|
|
731
|
+
// Instance attributes (from instanceVBO)
|
|
732
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instanceVBO);
|
|
733
|
+
const stride = SC_GLYPH_INSTANCE_FLOATS * FLOAT;
|
|
734
|
+
gl.enableVertexAttribArray(locs.a_cellPos);
|
|
735
|
+
gl.vertexAttribPointer(locs.a_cellPos, 2, gl.FLOAT, false, stride, 0);
|
|
736
|
+
gl.vertexAttribDivisor(locs.a_cellPos, 1);
|
|
737
|
+
gl.enableVertexAttribArray(locs.a_color);
|
|
738
|
+
gl.vertexAttribPointer(locs.a_color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
|
|
739
|
+
gl.vertexAttribDivisor(locs.a_color, 1);
|
|
740
|
+
gl.enableVertexAttribArray(locs.a_texCoord);
|
|
741
|
+
gl.vertexAttribPointer(locs.a_texCoord, 4, gl.FLOAT, false, stride, 6 * FLOAT);
|
|
742
|
+
gl.vertexAttribDivisor(locs.a_texCoord, 1);
|
|
743
|
+
gl.enableVertexAttribArray(locs.a_glyphSize);
|
|
744
|
+
gl.vertexAttribPointer(locs.a_glyphSize, 2, gl.FLOAT, false, stride, 10 * FLOAT);
|
|
745
|
+
gl.vertexAttribDivisor(locs.a_glyphSize, 1);
|
|
746
|
+
gl.enableVertexAttribArray(locs.a_offset);
|
|
747
|
+
gl.vertexAttribPointer(locs.a_offset, 2, gl.FLOAT, false, stride, 12 * FLOAT);
|
|
748
|
+
gl.vertexAttribDivisor(locs.a_offset, 1);
|
|
496
749
|
}
|
|
497
750
|
// -----------------------------------------------------------------------
|
|
498
751
|
// Color resolution
|
|
499
752
|
// -----------------------------------------------------------------------
|
|
753
|
+
setTheme(theme) {
|
|
754
|
+
this.theme = { ...DEFAULT_THEME, ...theme };
|
|
755
|
+
this.palette = build256Palette(this.theme);
|
|
756
|
+
this.buildPaletteFloat();
|
|
757
|
+
// Mark all terminals for full re-render with new colors
|
|
758
|
+
this.terminalFullyRendered.clear();
|
|
759
|
+
this.terminalBgData.clear();
|
|
760
|
+
this.terminalGlyphData.clear();
|
|
761
|
+
}
|
|
500
762
|
buildPaletteFloat() {
|
|
501
763
|
this.paletteFloat = this.palette.map((c) => hexToFloat4(c));
|
|
502
764
|
this.themeFgFloat = hexToFloat4(this.theme.foreground);
|
|
503
765
|
this.themeBgFloat = hexToFloat4(this.theme.background);
|
|
504
766
|
this.themeCursorFloat = hexToFloat4(this.theme.cursor);
|
|
505
767
|
}
|
|
506
|
-
resolveColorFloat(colorIdx, isRGB, grid, col, isForeground) {
|
|
507
|
-
if (isRGB) {
|
|
508
|
-
const offset = isForeground ? col : 256 + col;
|
|
509
|
-
const rgb = grid.rgbColors[offset];
|
|
510
|
-
const r = ((rgb >> 16) & 0xff) / 255;
|
|
511
|
-
const g = ((rgb >> 8) & 0xff) / 255;
|
|
512
|
-
const b = (rgb & 0xff) / 255;
|
|
513
|
-
return [r, g, b, 1.0];
|
|
514
|
-
}
|
|
515
|
-
if (isForeground && colorIdx === 7)
|
|
516
|
-
return this.themeFgFloat;
|
|
517
|
-
if (!isForeground && colorIdx === 0)
|
|
518
|
-
return this.themeBgFloat;
|
|
519
|
-
if (colorIdx >= 0 && colorIdx < 256) {
|
|
520
|
-
return this.paletteFloat[colorIdx];
|
|
521
|
-
}
|
|
522
|
-
return isForeground ? this.themeFgFloat : this.themeBgFloat;
|
|
523
|
-
}
|
|
524
768
|
// -----------------------------------------------------------------------
|
|
525
769
|
// Cell measurement
|
|
526
770
|
// -----------------------------------------------------------------------
|