@next_term/web 0.1.0-next.0 → 0.1.0-next.10
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/README.md +222 -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/addon.d.ts +9 -0
- package/dist/addons/fit.d.ts +23 -0
- 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/fit.d.ts +9 -0
- package/dist/fit.d.ts.map +1 -1
- package/dist/fit.js +9 -1
- package/dist/fit.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 +34 -5
- package/dist/input-handler.js.map +1 -1
- package/dist/parser-worker.d.ts +34 -0
- package/dist/parser-worker.d.ts.map +1 -1
- package/dist/parser-worker.js +4 -17
- package/dist/parser-worker.js.map +1 -1
- package/dist/render-bridge.d.ts +3 -1
- package/dist/render-bridge.d.ts.map +1 -1
- package/dist/render-bridge.js +5 -1
- package/dist/render-bridge.js.map +1 -1
- package/dist/render-worker.d.ts +4 -0
- package/dist/render-worker.d.ts.map +1 -1
- package/dist/render-worker.js +211 -98
- package/dist/render-worker.js.map +1 -1
- package/dist/renderer.d.ts +7 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +21 -6
- package/dist/renderer.js.map +1 -1
- package/dist/shared-context.d.ts +38 -9
- package/dist/shared-context.d.ts.map +1 -1
- package/dist/shared-context.js +491 -193
- package/dist/shared-context.js.map +1 -1
- package/dist/web-terminal.d.ts +26 -1
- package/dist/web-terminal.d.ts.map +1 -1
- package/dist/web-terminal.js +140 -18
- package/dist/web-terminal.js.map +1 -1
- package/dist/webgl-renderer.d.ts +33 -7
- package/dist/webgl-renderer.d.ts.map +1 -1
- package/dist/webgl-renderer.js +372 -139
- 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 +7 -2
- package/dist/worker-bridge.d.ts.map +1 -1
- package/dist/worker-bridge.js +41 -13
- 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
|
|
@@ -152,23 +188,27 @@ export class SharedWebGLContext {
|
|
|
152
188
|
themeCursorFloat = [0, 0, 0, 1];
|
|
153
189
|
fontSize;
|
|
154
190
|
fontFamily;
|
|
191
|
+
fontWeight;
|
|
192
|
+
fontWeightBold;
|
|
155
193
|
dpr;
|
|
156
194
|
cellWidth = 0;
|
|
157
195
|
cellHeight = 0;
|
|
158
196
|
constructor(options) {
|
|
159
197
|
this.fontSize = options?.fontSize ?? 14;
|
|
160
198
|
this.fontFamily = options?.fontFamily ?? "'Menlo', 'DejaVu Sans Mono', 'Consolas', monospace";
|
|
199
|
+
this.fontWeight = options?.fontWeight ?? 400;
|
|
200
|
+
this.fontWeightBold = options?.fontWeightBold ?? 700;
|
|
161
201
|
this.theme = { ...DEFAULT_THEME, ...options?.theme };
|
|
162
202
|
this.dpr =
|
|
163
203
|
options?.devicePixelRatio ?? (typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1);
|
|
164
204
|
this.palette = build256Palette(this.theme);
|
|
165
205
|
this.buildPaletteFloat();
|
|
166
206
|
this.measureCellSize();
|
|
167
|
-
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 *
|
|
207
|
+
this.atlas = new GlyphAtlas(Math.round(this.fontSize * this.dpr), this.fontFamily, this.fontWeight, this.fontWeightBold);
|
|
208
|
+
// Pre-allocate instance buffers for batched rendering (all terminals combined)
|
|
209
|
+
const maxCells = 80 * 24 * 4; // start with 4 terminals worth
|
|
210
|
+
this.bgInstances = new Float32Array(maxCells * SC_BG_INSTANCE_FLOATS);
|
|
211
|
+
this.glyphInstances = new Float32Array(maxCells * SC_GLYPH_INSTANCE_FLOATS);
|
|
172
212
|
// Create the shared canvas
|
|
173
213
|
this.canvas = document.createElement("canvas");
|
|
174
214
|
this.canvas.style.display = "block";
|
|
@@ -186,11 +226,22 @@ export class SharedWebGLContext {
|
|
|
186
226
|
alpha: true,
|
|
187
227
|
antialias: false,
|
|
188
228
|
premultipliedAlpha: false,
|
|
189
|
-
preserveDrawingBuffer:
|
|
229
|
+
preserveDrawingBuffer: true,
|
|
190
230
|
});
|
|
191
231
|
if (!this.gl) {
|
|
192
232
|
throw new Error("WebGL2 is not available");
|
|
193
233
|
}
|
|
234
|
+
// Detect software rendering (SwiftShader) — shared context is a net loss
|
|
235
|
+
// on software renderers because it concentrates all panes on one slow context.
|
|
236
|
+
// Fall back to independent per-pane rendering in that case.
|
|
237
|
+
const debugInfo = this.gl.getExtension("WEBGL_debug_renderer_info");
|
|
238
|
+
if (debugInfo) {
|
|
239
|
+
const renderer = this.gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
240
|
+
if (/swiftshader|llvmpipe|software/i.test(renderer)) {
|
|
241
|
+
this.gl = null;
|
|
242
|
+
throw new Error(`Software renderer detected (${renderer}), skipping shared context`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
194
245
|
this.initGLResources();
|
|
195
246
|
}
|
|
196
247
|
// -----------------------------------------------------------------------
|
|
@@ -215,16 +266,32 @@ export class SharedWebGLContext {
|
|
|
215
266
|
if (entry) {
|
|
216
267
|
entry.grid = grid;
|
|
217
268
|
entry.cursor = cursor;
|
|
269
|
+
// Reset dirty tracking — new grid may have different content
|
|
270
|
+
this.terminalFullyRendered.delete(id);
|
|
218
271
|
}
|
|
219
272
|
}
|
|
220
273
|
removeTerminal(id) {
|
|
221
274
|
this.terminals.delete(id);
|
|
275
|
+
// Clean up per-terminal dirty tracking state
|
|
276
|
+
this.terminalBgCounts.delete(id);
|
|
277
|
+
this.terminalGlyphCounts.delete(id);
|
|
278
|
+
this.terminalRowBgOffsets.delete(id);
|
|
279
|
+
this.terminalRowGlyphOffsets.delete(id);
|
|
280
|
+
this.terminalRowBgCounts.delete(id);
|
|
281
|
+
this.terminalRowGlyphCounts.delete(id);
|
|
282
|
+
this.terminalFullyRendered.delete(id);
|
|
283
|
+
this.terminalBgData.delete(id);
|
|
284
|
+
this.terminalGlyphData.delete(id);
|
|
222
285
|
}
|
|
223
286
|
getTerminalIds() {
|
|
224
287
|
return Array.from(this.terminals.keys());
|
|
225
288
|
}
|
|
226
289
|
/**
|
|
227
|
-
* Render all terminals in one frame.
|
|
290
|
+
* Render all terminals in one frame using batched rendering.
|
|
291
|
+
*
|
|
292
|
+
* All terminals' instance data is packed into combined buffers with
|
|
293
|
+
* per-instance viewport offsets, then uploaded and drawn in single calls.
|
|
294
|
+
* This reduces GL state changes from O(N) to O(1) per pass.
|
|
228
295
|
*/
|
|
229
296
|
render() {
|
|
230
297
|
if (this.disposed || !this.gl)
|
|
@@ -232,15 +299,151 @@ export class SharedWebGLContext {
|
|
|
232
299
|
const gl = this.gl;
|
|
233
300
|
const canvasWidth = this.canvas.width;
|
|
234
301
|
const canvasHeight = this.canvas.height;
|
|
235
|
-
//
|
|
302
|
+
// Toggle double-buffer index
|
|
303
|
+
this.bufferIndex ^= 1;
|
|
304
|
+
const bi = this.bufferIndex;
|
|
305
|
+
// --- Early out: check if any terminal has dirty rows ---
|
|
306
|
+
const cellW = this.cellWidth * this.dpr;
|
|
307
|
+
const cellH = this.cellHeight * this.dpr;
|
|
308
|
+
let anyTerminalDirty = false;
|
|
309
|
+
for (const [id, entry] of this.terminals) {
|
|
310
|
+
const { grid } = entry;
|
|
311
|
+
if (!this.terminalFullyRendered.has(id)) {
|
|
312
|
+
anyTerminalDirty = true;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
for (let row = 0; row < grid.rows; row++) {
|
|
316
|
+
if (grid.isDirty(row)) {
|
|
317
|
+
anyTerminalDirty = true;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (anyTerminalDirty)
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
// Nothing changed — skip everything, reuse last frame
|
|
325
|
+
if (!anyTerminalDirty)
|
|
326
|
+
return;
|
|
327
|
+
// --- Phase 1: Clear viewports ---
|
|
236
328
|
gl.viewport(0, 0, canvasWidth, canvasHeight);
|
|
237
329
|
gl.clearColor(0, 0, 0, 0);
|
|
238
330
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
331
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
332
|
+
const bgR = this.themeBgFloat[0];
|
|
333
|
+
const bgG = this.themeBgFloat[1];
|
|
334
|
+
const bgB = this.themeBgFloat[2];
|
|
335
|
+
gl.clearColor(bgR, bgG, bgB, 1.0);
|
|
336
|
+
for (const [, entry] of this.terminals) {
|
|
337
|
+
const { viewport } = entry;
|
|
338
|
+
const vpX = Math.round(viewport.x * this.dpr);
|
|
339
|
+
const vpY = Math.round(viewport.y * this.dpr);
|
|
340
|
+
const vpW = Math.round(viewport.width * this.dpr);
|
|
341
|
+
const vpH = Math.round(viewport.height * this.dpr);
|
|
342
|
+
const glY = canvasHeight - vpY - vpH;
|
|
343
|
+
gl.viewport(vpX, glY, vpW, vpH);
|
|
344
|
+
gl.scissor(vpX, glY, vpW, vpH);
|
|
345
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
242
346
|
}
|
|
243
347
|
gl.disable(gl.SCISSOR_TEST);
|
|
348
|
+
// --- Phase 2: Build combined instance data for all terminals ---
|
|
349
|
+
let totalBgCount = 0;
|
|
350
|
+
let totalGlyphCount = 0;
|
|
351
|
+
for (const [id, entry] of this.terminals) {
|
|
352
|
+
const { bgCount, glyphCount } = this.buildTerminalInstances(id, entry);
|
|
353
|
+
const bgData = this.terminalBgData.get(id);
|
|
354
|
+
const glyphData = this.terminalGlyphData.get(id);
|
|
355
|
+
// Ensure combined buffers are large enough
|
|
356
|
+
const neededBg = (totalBgCount + bgCount) * SC_BG_INSTANCE_FLOATS;
|
|
357
|
+
if (neededBg > this.bgInstances.length) {
|
|
358
|
+
const newBuf = new Float32Array(neededBg * 2);
|
|
359
|
+
newBuf.set(this.bgInstances.subarray(0, totalBgCount * SC_BG_INSTANCE_FLOATS));
|
|
360
|
+
this.bgInstances = newBuf;
|
|
361
|
+
}
|
|
362
|
+
const neededGlyph = (totalGlyphCount + glyphCount) * SC_GLYPH_INSTANCE_FLOATS;
|
|
363
|
+
if (neededGlyph > this.glyphInstances.length) {
|
|
364
|
+
const newBuf = new Float32Array(neededGlyph * 2);
|
|
365
|
+
newBuf.set(this.glyphInstances.subarray(0, totalGlyphCount * SC_GLYPH_INSTANCE_FLOATS));
|
|
366
|
+
this.glyphInstances = newBuf;
|
|
367
|
+
}
|
|
368
|
+
// Copy per-terminal data into combined buffer
|
|
369
|
+
if (bgData && bgCount > 0) {
|
|
370
|
+
this.bgInstances.set(bgData.subarray(0, bgCount * SC_BG_INSTANCE_FLOATS), totalBgCount * SC_BG_INSTANCE_FLOATS);
|
|
371
|
+
}
|
|
372
|
+
if (glyphData && glyphCount > 0) {
|
|
373
|
+
this.glyphInstances.set(glyphData.subarray(0, glyphCount * SC_GLYPH_INSTANCE_FLOATS), totalGlyphCount * SC_GLYPH_INSTANCE_FLOATS);
|
|
374
|
+
}
|
|
375
|
+
totalBgCount += bgCount;
|
|
376
|
+
totalGlyphCount += glyphCount;
|
|
377
|
+
}
|
|
378
|
+
// Upload atlas if dirty
|
|
379
|
+
this.atlas.upload(gl);
|
|
380
|
+
// --- Phase 3: Single viewport for all draws (full canvas) ---
|
|
381
|
+
gl.viewport(0, 0, canvasWidth, canvasHeight);
|
|
382
|
+
// --- Background pass: single upload + single draw ---
|
|
383
|
+
const activeBgVBO = this.bgInstanceVBOs[bi];
|
|
384
|
+
const activeBgVAO = this.bgVAOs[bi];
|
|
385
|
+
if (totalBgCount > 0 && this.bgProgram && activeBgVAO && activeBgVBO) {
|
|
386
|
+
gl.useProgram(this.bgProgram);
|
|
387
|
+
gl.uniform2f(this.bgUniforms.u_resolution, canvasWidth, canvasHeight);
|
|
388
|
+
gl.uniform2f(this.bgUniforms.u_cellSize, cellW, cellH);
|
|
389
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, activeBgVBO);
|
|
390
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.bgInstances.subarray(0, totalBgCount * SC_BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
391
|
+
gl.bindVertexArray(activeBgVAO);
|
|
392
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, totalBgCount);
|
|
393
|
+
}
|
|
394
|
+
// --- Glyph pass: single upload + single draw ---
|
|
395
|
+
const activeGlyphVBO = this.glyphInstanceVBOs[bi];
|
|
396
|
+
const activeGlyphVAO = this.glyphVAOs[bi];
|
|
397
|
+
if (totalGlyphCount > 0 && this.glyphProgram && activeGlyphVAO && activeGlyphVBO) {
|
|
398
|
+
gl.enable(gl.BLEND);
|
|
399
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
400
|
+
gl.useProgram(this.glyphProgram);
|
|
401
|
+
gl.uniform2f(this.glyphUniforms.u_resolution, canvasWidth, canvasHeight);
|
|
402
|
+
gl.uniform2f(this.glyphUniforms.u_cellSize, cellW, cellH);
|
|
403
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
404
|
+
gl.bindTexture(gl.TEXTURE_2D, this.atlas.getTexture());
|
|
405
|
+
gl.uniform1i(this.glyphUniforms.u_atlas, 0);
|
|
406
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, activeGlyphVBO);
|
|
407
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.glyphInstances.subarray(0, totalGlyphCount * SC_GLYPH_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
408
|
+
gl.bindVertexArray(activeGlyphVAO);
|
|
409
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, totalGlyphCount);
|
|
410
|
+
gl.disable(gl.BLEND);
|
|
411
|
+
}
|
|
412
|
+
// --- Cursor pass: batch all cursors into one draw ---
|
|
413
|
+
let cursorCount = 0;
|
|
414
|
+
const neededCursor = this.terminals.size * SC_BG_INSTANCE_FLOATS;
|
|
415
|
+
if (neededCursor > this.cursorBuffer.length) {
|
|
416
|
+
this.cursorBuffer = new Float32Array(neededCursor);
|
|
417
|
+
}
|
|
418
|
+
const cc = this.themeCursorFloat;
|
|
419
|
+
for (const [, entry] of this.terminals) {
|
|
420
|
+
const { cursor, viewport } = entry;
|
|
421
|
+
if (!cursor.visible)
|
|
422
|
+
continue;
|
|
423
|
+
const off = cursorCount * SC_BG_INSTANCE_FLOATS;
|
|
424
|
+
this.cursorBuffer[off] = cursor.col;
|
|
425
|
+
this.cursorBuffer[off + 1] = cursor.row;
|
|
426
|
+
this.cursorBuffer[off + 2] = cc[0];
|
|
427
|
+
this.cursorBuffer[off + 3] = cc[1];
|
|
428
|
+
this.cursorBuffer[off + 4] = cc[2];
|
|
429
|
+
this.cursorBuffer[off + 5] = 0.5;
|
|
430
|
+
this.cursorBuffer[off + 6] = Math.round(viewport.x * this.dpr);
|
|
431
|
+
this.cursorBuffer[off + 7] = Math.round(viewport.y * this.dpr);
|
|
432
|
+
cursorCount++;
|
|
433
|
+
}
|
|
434
|
+
if (cursorCount > 0 && this.bgProgram && activeBgVAO && activeBgVBO) {
|
|
435
|
+
gl.enable(gl.BLEND);
|
|
436
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
437
|
+
gl.useProgram(this.bgProgram);
|
|
438
|
+
gl.uniform2f(this.bgUniforms.u_resolution, canvasWidth, canvasHeight);
|
|
439
|
+
gl.uniform2f(this.bgUniforms.u_cellSize, cellW, cellH);
|
|
440
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, activeBgVBO);
|
|
441
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.cursorBuffer.subarray(0, cursorCount * SC_BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
442
|
+
gl.bindVertexArray(activeBgVAO);
|
|
443
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, cursorCount);
|
|
444
|
+
gl.disable(gl.BLEND);
|
|
445
|
+
}
|
|
446
|
+
gl.bindVertexArray(null);
|
|
244
447
|
}
|
|
245
448
|
/**
|
|
246
449
|
* Update the shared canvas size to match a container element.
|
|
@@ -290,14 +493,17 @@ export class SharedWebGLContext {
|
|
|
290
493
|
gl.deleteBuffer(this.quadVBO);
|
|
291
494
|
if (this.quadEBO)
|
|
292
495
|
gl.deleteBuffer(this.quadEBO);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
496
|
+
// Clean up double-buffered resources
|
|
497
|
+
for (let i = 0; i < 2; i++) {
|
|
498
|
+
if (this.bgInstanceVBOs[i])
|
|
499
|
+
gl.deleteBuffer(this.bgInstanceVBOs[i]);
|
|
500
|
+
if (this.glyphInstanceVBOs[i])
|
|
501
|
+
gl.deleteBuffer(this.glyphInstanceVBOs[i]);
|
|
502
|
+
if (this.bgVAOs[i])
|
|
503
|
+
gl.deleteVertexArray(this.bgVAOs[i]);
|
|
504
|
+
if (this.glyphVAOs[i])
|
|
505
|
+
gl.deleteVertexArray(this.glyphVAOs[i]);
|
|
506
|
+
}
|
|
301
507
|
}
|
|
302
508
|
this.terminals.clear();
|
|
303
509
|
if (this.canvas.parentElement) {
|
|
@@ -306,115 +512,190 @@ export class SharedWebGLContext {
|
|
|
306
512
|
this.gl = null;
|
|
307
513
|
}
|
|
308
514
|
// -----------------------------------------------------------------------
|
|
309
|
-
// Per-terminal
|
|
515
|
+
// Per-terminal instance data building (CPU-side only, no GL calls)
|
|
310
516
|
// -----------------------------------------------------------------------
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Build instance data for a single terminal into its per-terminal cache.
|
|
519
|
+
* Returns the bg and glyph instance counts. The caller assembles all
|
|
520
|
+
* terminals' data into the combined buffer before issuing GL draws.
|
|
521
|
+
*/
|
|
522
|
+
buildTerminalInstances(id, entry) {
|
|
523
|
+
const { grid, viewport } = entry;
|
|
316
524
|
const cols = grid.cols;
|
|
317
525
|
const rows = grid.rows;
|
|
318
|
-
//
|
|
526
|
+
// Viewport offset in device pixels for canvas-space coordinates
|
|
319
527
|
const vpX = Math.round(viewport.x * this.dpr);
|
|
320
528
|
const vpY = Math.round(viewport.y * this.dpr);
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
529
|
+
// Check if any rows are dirty; if not, use cached counts
|
|
530
|
+
const isFirstRender = !this.terminalFullyRendered.has(id);
|
|
531
|
+
let anyDirty = isFirstRender;
|
|
532
|
+
if (!anyDirty) {
|
|
533
|
+
for (let row = 0; row < rows; row++) {
|
|
534
|
+
if (grid.isDirty(row)) {
|
|
535
|
+
anyDirty = true;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!anyDirty) {
|
|
541
|
+
// No dirty rows — reuse cached instance data and counts
|
|
542
|
+
return {
|
|
543
|
+
bgCount: this.terminalBgCounts.get(id) ?? 0,
|
|
544
|
+
glyphCount: this.terminalGlyphCounts.get(id) ?? 0,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
// Ensure per-terminal data buffers are large enough
|
|
333
548
|
const totalCells = cols * rows;
|
|
334
|
-
|
|
335
|
-
|
|
549
|
+
let bgData = this.terminalBgData.get(id);
|
|
550
|
+
if (!bgData || bgData.length < totalCells * SC_BG_INSTANCE_FLOATS) {
|
|
551
|
+
bgData = new Float32Array(totalCells * SC_BG_INSTANCE_FLOATS);
|
|
552
|
+
this.terminalBgData.set(id, bgData);
|
|
553
|
+
}
|
|
554
|
+
let glyphData = this.terminalGlyphData.get(id);
|
|
555
|
+
if (!glyphData || glyphData.length < totalCells * SC_GLYPH_INSTANCE_FLOATS) {
|
|
556
|
+
glyphData = new Float32Array(totalCells * SC_GLYPH_INSTANCE_FLOATS);
|
|
557
|
+
this.terminalGlyphData.set(id, glyphData);
|
|
558
|
+
}
|
|
559
|
+
// Initialize or retrieve per-row offset tracking
|
|
560
|
+
let rowBgOffsets = this.terminalRowBgOffsets.get(id);
|
|
561
|
+
let rowGlyphOffsets = this.terminalRowGlyphOffsets.get(id);
|
|
562
|
+
let rowBgCounts = this.terminalRowBgCounts.get(id);
|
|
563
|
+
let rowGlyphCounts = this.terminalRowGlyphCounts.get(id);
|
|
564
|
+
if (!rowBgOffsets || rowBgOffsets.length !== rows) {
|
|
565
|
+
rowBgOffsets = new Array(rows).fill(0);
|
|
566
|
+
this.terminalRowBgOffsets.set(id, rowBgOffsets);
|
|
567
|
+
}
|
|
568
|
+
if (!rowGlyphOffsets || rowGlyphOffsets.length !== rows) {
|
|
569
|
+
rowGlyphOffsets = new Array(rows).fill(0);
|
|
570
|
+
this.terminalRowGlyphOffsets.set(id, rowGlyphOffsets);
|
|
336
571
|
}
|
|
337
|
-
if (
|
|
338
|
-
|
|
572
|
+
if (!rowBgCounts || rowBgCounts.length !== rows) {
|
|
573
|
+
rowBgCounts = new Array(rows).fill(0);
|
|
574
|
+
this.terminalRowBgCounts.set(id, rowBgCounts);
|
|
575
|
+
}
|
|
576
|
+
if (!rowGlyphCounts || rowGlyphCounts.length !== rows) {
|
|
577
|
+
rowGlyphCounts = new Array(rows).fill(0);
|
|
578
|
+
this.terminalRowGlyphCounts.set(id, rowGlyphCounts);
|
|
339
579
|
}
|
|
340
|
-
// Build instance data
|
|
341
580
|
let bgCount = 0;
|
|
342
581
|
let glyphCount = 0;
|
|
343
582
|
for (let row = 0; row < rows; row++) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
bg = tmp;
|
|
583
|
+
const rowDirty = isFirstRender || grid.isDirty(row);
|
|
584
|
+
if (!rowDirty) {
|
|
585
|
+
// Row is clean — copy previous data to new offsets if they shifted
|
|
586
|
+
const prevBgOffset = rowBgOffsets[row];
|
|
587
|
+
const prevBgCount = rowBgCounts[row];
|
|
588
|
+
const prevGlyphOffset = rowGlyphOffsets[row];
|
|
589
|
+
const prevGlyphCount = rowGlyphCounts[row];
|
|
590
|
+
if (bgCount !== prevBgOffset && prevBgCount > 0) {
|
|
591
|
+
bgData.copyWithin(bgCount * SC_BG_INSTANCE_FLOATS, prevBgOffset * SC_BG_INSTANCE_FLOATS, (prevBgOffset + prevBgCount) * SC_BG_INSTANCE_FLOATS);
|
|
592
|
+
}
|
|
593
|
+
if (glyphCount !== prevGlyphOffset && prevGlyphCount > 0) {
|
|
594
|
+
glyphData.copyWithin(glyphCount * SC_GLYPH_INSTANCE_FLOATS, prevGlyphOffset * SC_GLYPH_INSTANCE_FLOATS, (prevGlyphOffset + prevGlyphCount) * SC_GLYPH_INSTANCE_FLOATS);
|
|
357
595
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
596
|
+
rowBgOffsets[row] = bgCount;
|
|
597
|
+
rowGlyphOffsets[row] = glyphCount;
|
|
598
|
+
bgCount += prevBgCount;
|
|
599
|
+
glyphCount += prevGlyphCount;
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
// Row is dirty — re-pack cell data with viewport offsets
|
|
603
|
+
rowBgOffsets[row] = bgCount;
|
|
604
|
+
rowGlyphOffsets[row] = glyphCount;
|
|
605
|
+
let rowBg = 0;
|
|
606
|
+
let rowGlyph = 0;
|
|
607
|
+
for (let col = 0; col < cols; col++) {
|
|
608
|
+
const codepoint = grid.getCodepoint(row, col);
|
|
609
|
+
const fgIdx = grid.getFgIndex(row, col);
|
|
610
|
+
const bgIdx = grid.getBgIndex(row, col);
|
|
611
|
+
const attrs = grid.getAttrs(row, col);
|
|
612
|
+
const fgIsRGB = grid.isFgRGB(row, col);
|
|
613
|
+
const bgIsRGB = grid.isBgRGB(row, col);
|
|
614
|
+
// Skip spacer cells (right half of wide character)
|
|
615
|
+
if (grid.isSpacerCell(row, col)) {
|
|
616
|
+
// Emit transparent bg to fill the slot
|
|
617
|
+
const bOff = bgCount * SC_BG_INSTANCE_FLOATS;
|
|
618
|
+
bgData[bOff] = col;
|
|
619
|
+
bgData[bOff + 1] = row;
|
|
620
|
+
bgData[bOff + 2] = 0;
|
|
621
|
+
bgData[bOff + 3] = 0;
|
|
622
|
+
bgData[bOff + 4] = 0;
|
|
623
|
+
bgData[bOff + 5] = 0;
|
|
624
|
+
bgData[bOff + 6] = vpX;
|
|
625
|
+
bgData[bOff + 7] = vpY;
|
|
626
|
+
bgCount++;
|
|
627
|
+
rowBg++;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const wide = grid.isWide(row, col);
|
|
631
|
+
let fg = resolveColorFloat(fgIdx, fgIsRGB, grid, col, true, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
|
|
632
|
+
let bg = resolveColorFloat(bgIdx, bgIsRGB, grid, col, false, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
|
|
633
|
+
if (attrs & ATTR_INVERSE) {
|
|
634
|
+
const tmp = fg;
|
|
635
|
+
fg = bg;
|
|
636
|
+
bg = tmp;
|
|
637
|
+
}
|
|
638
|
+
// Pack bg instance with viewport offset
|
|
639
|
+
const bOff = bgCount * SC_BG_INSTANCE_FLOATS;
|
|
640
|
+
bgData[bOff] = col;
|
|
641
|
+
bgData[bOff + 1] = row;
|
|
642
|
+
bgData[bOff + 2] = bg[0];
|
|
643
|
+
bgData[bOff + 3] = bg[1];
|
|
644
|
+
bgData[bOff + 4] = bg[2];
|
|
645
|
+
bgData[bOff + 5] = bg[3];
|
|
646
|
+
bgData[bOff + 6] = vpX;
|
|
647
|
+
bgData[bOff + 7] = vpY;
|
|
648
|
+
bgCount++;
|
|
649
|
+
rowBg++;
|
|
650
|
+
// Wide char: paint bg for right-half too
|
|
651
|
+
if (wide && col + 1 < cols) {
|
|
652
|
+
const bOff2 = bgCount * SC_BG_INSTANCE_FLOATS;
|
|
653
|
+
bgData[bOff2] = col + 1;
|
|
654
|
+
bgData[bOff2 + 1] = row;
|
|
655
|
+
bgData[bOff2 + 2] = bg[0];
|
|
656
|
+
bgData[bOff2 + 3] = bg[1];
|
|
657
|
+
bgData[bOff2 + 4] = bg[2];
|
|
658
|
+
bgData[bOff2 + 5] = bg[3];
|
|
659
|
+
bgData[bOff2 + 6] = vpX;
|
|
660
|
+
bgData[bOff2 + 7] = vpY;
|
|
661
|
+
bgCount++;
|
|
662
|
+
rowBg++;
|
|
663
|
+
}
|
|
664
|
+
if (codepoint > 0x20) {
|
|
665
|
+
const bold = !!(attrs & ATTR_BOLD);
|
|
666
|
+
const italic = !!(attrs & ATTR_ITALIC);
|
|
667
|
+
const glyph = this.atlas.getGlyph(codepoint, bold, italic);
|
|
668
|
+
if (glyph) {
|
|
669
|
+
// Pack glyph instance with viewport offset
|
|
670
|
+
const gOff = glyphCount * SC_GLYPH_INSTANCE_FLOATS;
|
|
671
|
+
glyphData[gOff] = col;
|
|
672
|
+
glyphData[gOff + 1] = row;
|
|
673
|
+
glyphData[gOff + 2] = fg[0];
|
|
674
|
+
glyphData[gOff + 3] = fg[1];
|
|
675
|
+
glyphData[gOff + 4] = fg[2];
|
|
676
|
+
glyphData[gOff + 5] = fg[3];
|
|
677
|
+
glyphData[gOff + 6] = glyph.u;
|
|
678
|
+
glyphData[gOff + 7] = glyph.v;
|
|
679
|
+
glyphData[gOff + 8] = glyph.w;
|
|
680
|
+
glyphData[gOff + 9] = glyph.h;
|
|
681
|
+
glyphData[gOff + 10] = glyph.pw;
|
|
682
|
+
glyphData[gOff + 11] = glyph.ph;
|
|
683
|
+
glyphData[gOff + 12] = vpX;
|
|
684
|
+
glyphData[gOff + 13] = vpY;
|
|
685
|
+
glyphCount++;
|
|
686
|
+
rowGlyph++;
|
|
687
|
+
}
|
|
367
688
|
}
|
|
368
689
|
}
|
|
690
|
+
rowBgCounts[row] = rowBg;
|
|
691
|
+
rowGlyphCounts[row] = rowGlyph;
|
|
692
|
+
grid.clearDirty(row);
|
|
369
693
|
}
|
|
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
694
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
}
|
|
417
|
-
gl.bindVertexArray(null);
|
|
695
|
+
this.terminalBgCounts.set(id, bgCount);
|
|
696
|
+
this.terminalGlyphCounts.set(id, glyphCount);
|
|
697
|
+
this.terminalFullyRendered.add(id);
|
|
698
|
+
return { bgCount, glyphCount };
|
|
418
699
|
}
|
|
419
700
|
// -----------------------------------------------------------------------
|
|
420
701
|
// GL resource initialization
|
|
@@ -425,6 +706,27 @@ export class SharedWebGLContext {
|
|
|
425
706
|
return;
|
|
426
707
|
this.bgProgram = createProgram(gl, BG_VERTEX_SHADER, BG_FRAGMENT_SHADER);
|
|
427
708
|
this.glyphProgram = createProgram(gl, GLYPH_VERTEX_SHADER, GLYPH_FRAGMENT_SHADER);
|
|
709
|
+
// Cache all uniform locations after program creation
|
|
710
|
+
this.bgUniforms.u_resolution = gl.getUniformLocation(this.bgProgram, "u_resolution");
|
|
711
|
+
this.bgUniforms.u_cellSize = gl.getUniformLocation(this.bgProgram, "u_cellSize");
|
|
712
|
+
this.glyphUniforms.u_resolution = gl.getUniformLocation(this.glyphProgram, "u_resolution");
|
|
713
|
+
this.glyphUniforms.u_cellSize = gl.getUniformLocation(this.glyphProgram, "u_cellSize");
|
|
714
|
+
this.glyphUniforms.u_atlas = gl.getUniformLocation(this.glyphProgram, "u_atlas");
|
|
715
|
+
// Cache all attribute locations after program creation
|
|
716
|
+
this.bgAttribLocs = {
|
|
717
|
+
a_position: gl.getAttribLocation(this.bgProgram, "a_position"),
|
|
718
|
+
a_cellPos: gl.getAttribLocation(this.bgProgram, "a_cellPos"),
|
|
719
|
+
a_color: gl.getAttribLocation(this.bgProgram, "a_color"),
|
|
720
|
+
a_offset: gl.getAttribLocation(this.bgProgram, "a_offset"),
|
|
721
|
+
};
|
|
722
|
+
this.glyphAttribLocs = {
|
|
723
|
+
a_position: gl.getAttribLocation(this.glyphProgram, "a_position"),
|
|
724
|
+
a_cellPos: gl.getAttribLocation(this.glyphProgram, "a_cellPos"),
|
|
725
|
+
a_color: gl.getAttribLocation(this.glyphProgram, "a_color"),
|
|
726
|
+
a_texCoord: gl.getAttribLocation(this.glyphProgram, "a_texCoord"),
|
|
727
|
+
a_glyphSize: gl.getAttribLocation(this.glyphProgram, "a_glyphSize"),
|
|
728
|
+
a_offset: gl.getAttribLocation(this.glyphProgram, "a_offset"),
|
|
729
|
+
};
|
|
428
730
|
const quadVerts = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
|
429
731
|
const quadIndices = new Uint16Array([0, 1, 2, 2, 1, 3]);
|
|
430
732
|
this.quadVBO = gl.createBuffer();
|
|
@@ -433,94 +735,90 @@ export class SharedWebGLContext {
|
|
|
433
735
|
this.quadEBO = gl.createBuffer();
|
|
434
736
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
435
737
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices, gl.STATIC_DRAW);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
738
|
+
// Create double-buffered VBOs and VAOs
|
|
739
|
+
for (let i = 0; i < 2; i++) {
|
|
740
|
+
this.bgInstanceVBOs[i] = gl.createBuffer();
|
|
741
|
+
this.glyphInstanceVBOs[i] = gl.createBuffer();
|
|
742
|
+
this.bgVAOs[i] = gl.createVertexArray();
|
|
743
|
+
gl.bindVertexArray(this.bgVAOs[i]);
|
|
744
|
+
const bgVBO = this.bgInstanceVBOs[i];
|
|
745
|
+
if (bgVBO)
|
|
746
|
+
this.setupBgVAO(gl, bgVBO);
|
|
747
|
+
gl.bindVertexArray(null);
|
|
748
|
+
this.glyphVAOs[i] = gl.createVertexArray();
|
|
749
|
+
gl.bindVertexArray(this.glyphVAOs[i]);
|
|
750
|
+
const glyphVBO = this.glyphInstanceVBOs[i];
|
|
751
|
+
if (glyphVBO)
|
|
752
|
+
this.setupGlyphVAO(gl, glyphVBO);
|
|
753
|
+
gl.bindVertexArray(null);
|
|
754
|
+
}
|
|
446
755
|
}
|
|
447
|
-
setupBgVAO(gl) {
|
|
756
|
+
setupBgVAO(gl, instanceVBO) {
|
|
448
757
|
const FLOAT = 4;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const program = this.bgProgram;
|
|
452
|
-
const aPos = gl.getAttribLocation(program, "a_position");
|
|
758
|
+
const locs = this.bgAttribLocs;
|
|
759
|
+
// Quad position (per-vertex, from quadVBO)
|
|
453
760
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVBO);
|
|
454
|
-
gl.enableVertexAttribArray(
|
|
455
|
-
gl.vertexAttribPointer(
|
|
761
|
+
gl.enableVertexAttribArray(locs.a_position);
|
|
762
|
+
gl.vertexAttribPointer(locs.a_position, 2, gl.FLOAT, false, 0, 0);
|
|
456
763
|
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.
|
|
764
|
+
// Instance attributes (from instanceVBO)
|
|
765
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instanceVBO);
|
|
766
|
+
const stride = SC_BG_INSTANCE_FLOATS * FLOAT;
|
|
767
|
+
gl.enableVertexAttribArray(locs.a_cellPos);
|
|
768
|
+
gl.vertexAttribPointer(locs.a_cellPos, 2, gl.FLOAT, false, stride, 0);
|
|
769
|
+
gl.vertexAttribDivisor(locs.a_cellPos, 1);
|
|
770
|
+
gl.enableVertexAttribArray(locs.a_color);
|
|
771
|
+
gl.vertexAttribPointer(locs.a_color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
|
|
772
|
+
gl.vertexAttribDivisor(locs.a_color, 1);
|
|
773
|
+
gl.enableVertexAttribArray(locs.a_offset);
|
|
774
|
+
gl.vertexAttribPointer(locs.a_offset, 2, gl.FLOAT, false, stride, 6 * FLOAT);
|
|
775
|
+
gl.vertexAttribDivisor(locs.a_offset, 1);
|
|
467
776
|
}
|
|
468
|
-
setupGlyphVAO(gl) {
|
|
777
|
+
setupGlyphVAO(gl, instanceVBO) {
|
|
469
778
|
const FLOAT = 4;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const program = this.glyphProgram;
|
|
473
|
-
const aPos = gl.getAttribLocation(program, "a_position");
|
|
779
|
+
const locs = this.glyphAttribLocs;
|
|
780
|
+
// Quad position (per-vertex, from quadVBO)
|
|
474
781
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVBO);
|
|
475
|
-
gl.enableVertexAttribArray(
|
|
476
|
-
gl.vertexAttribPointer(
|
|
782
|
+
gl.enableVertexAttribArray(locs.a_position);
|
|
783
|
+
gl.vertexAttribPointer(locs.a_position, 2, gl.FLOAT, false, 0, 0);
|
|
477
784
|
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(
|
|
785
|
+
// Instance attributes (from instanceVBO)
|
|
786
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, instanceVBO);
|
|
787
|
+
const stride = SC_GLYPH_INSTANCE_FLOATS * FLOAT;
|
|
788
|
+
gl.enableVertexAttribArray(locs.a_cellPos);
|
|
789
|
+
gl.vertexAttribPointer(locs.a_cellPos, 2, gl.FLOAT, false, stride, 0);
|
|
790
|
+
gl.vertexAttribDivisor(locs.a_cellPos, 1);
|
|
791
|
+
gl.enableVertexAttribArray(locs.a_color);
|
|
792
|
+
gl.vertexAttribPointer(locs.a_color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
|
|
793
|
+
gl.vertexAttribDivisor(locs.a_color, 1);
|
|
794
|
+
gl.enableVertexAttribArray(locs.a_texCoord);
|
|
795
|
+
gl.vertexAttribPointer(locs.a_texCoord, 4, gl.FLOAT, false, stride, 6 * FLOAT);
|
|
796
|
+
gl.vertexAttribDivisor(locs.a_texCoord, 1);
|
|
797
|
+
gl.enableVertexAttribArray(locs.a_glyphSize);
|
|
798
|
+
gl.vertexAttribPointer(locs.a_glyphSize, 2, gl.FLOAT, false, stride, 10 * FLOAT);
|
|
799
|
+
gl.vertexAttribDivisor(locs.a_glyphSize, 1);
|
|
800
|
+
gl.enableVertexAttribArray(locs.a_offset);
|
|
801
|
+
gl.vertexAttribPointer(locs.a_offset, 2, gl.FLOAT, false, stride, 12 * FLOAT);
|
|
802
|
+
gl.vertexAttribDivisor(locs.a_offset, 1);
|
|
496
803
|
}
|
|
497
804
|
// -----------------------------------------------------------------------
|
|
498
805
|
// Color resolution
|
|
499
806
|
// -----------------------------------------------------------------------
|
|
807
|
+
setTheme(theme) {
|
|
808
|
+
this.theme = { ...DEFAULT_THEME, ...theme };
|
|
809
|
+
this.palette = build256Palette(this.theme);
|
|
810
|
+
this.buildPaletteFloat();
|
|
811
|
+
// Mark all terminals for full re-render with new colors
|
|
812
|
+
this.terminalFullyRendered.clear();
|
|
813
|
+
this.terminalBgData.clear();
|
|
814
|
+
this.terminalGlyphData.clear();
|
|
815
|
+
}
|
|
500
816
|
buildPaletteFloat() {
|
|
501
817
|
this.paletteFloat = this.palette.map((c) => hexToFloat4(c));
|
|
502
818
|
this.themeFgFloat = hexToFloat4(this.theme.foreground);
|
|
503
819
|
this.themeBgFloat = hexToFloat4(this.theme.background);
|
|
504
820
|
this.themeCursorFloat = hexToFloat4(this.theme.cursor);
|
|
505
821
|
}
|
|
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
822
|
// -----------------------------------------------------------------------
|
|
525
823
|
// Cell measurement
|
|
526
824
|
// -----------------------------------------------------------------------
|
|
@@ -541,7 +839,7 @@ export class SharedWebGLContext {
|
|
|
541
839
|
this.cellHeight = Math.ceil(this.fontSize * 1.2);
|
|
542
840
|
return;
|
|
543
841
|
}
|
|
544
|
-
const font = `${this.fontSize}px ${this.fontFamily}`;
|
|
842
|
+
const font = `${this.fontWeight} ${this.fontSize}px ${this.fontFamily}`;
|
|
545
843
|
measureCtx.font = font;
|
|
546
844
|
const metrics = measureCtx.measureText("M");
|
|
547
845
|
this.cellWidth = Math.ceil(metrics.width);
|