@next_term/web 0.1.0-next.0 → 0.1.0-next.11

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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/accessibility.d.ts +2 -4
  4. package/dist/accessibility.d.ts.map +1 -1
  5. package/dist/accessibility.js +2 -7
  6. package/dist/accessibility.js.map +1 -1
  7. package/dist/addon.d.ts +9 -0
  8. package/dist/addons/fit.d.ts +23 -0
  9. package/dist/addons/web-links.d.ts +0 -1
  10. package/dist/addons/web-links.d.ts.map +1 -1
  11. package/dist/addons/web-links.js +0 -4
  12. package/dist/addons/web-links.js.map +1 -1
  13. package/dist/fit.d.ts +9 -0
  14. package/dist/fit.d.ts.map +1 -1
  15. package/dist/fit.js +9 -1
  16. package/dist/fit.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/input-handler.d.ts +7 -0
  22. package/dist/input-handler.d.ts.map +1 -1
  23. package/dist/input-handler.js +36 -5
  24. package/dist/input-handler.js.map +1 -1
  25. package/dist/parser-worker.d.ts +34 -0
  26. package/dist/parser-worker.d.ts.map +1 -1
  27. package/dist/parser-worker.js +4 -17
  28. package/dist/parser-worker.js.map +1 -1
  29. package/dist/render-bridge.d.ts +3 -1
  30. package/dist/render-bridge.d.ts.map +1 -1
  31. package/dist/render-bridge.js +5 -1
  32. package/dist/render-bridge.js.map +1 -1
  33. package/dist/render-worker.d.ts +4 -0
  34. package/dist/render-worker.d.ts.map +1 -1
  35. package/dist/render-worker.js +211 -98
  36. package/dist/render-worker.js.map +1 -1
  37. package/dist/renderer.d.ts +7 -1
  38. package/dist/renderer.d.ts.map +1 -1
  39. package/dist/renderer.js +21 -6
  40. package/dist/renderer.js.map +1 -1
  41. package/dist/shared-context.d.ts +38 -9
  42. package/dist/shared-context.d.ts.map +1 -1
  43. package/dist/shared-context.js +491 -193
  44. package/dist/shared-context.js.map +1 -1
  45. package/dist/web-terminal.d.ts +26 -1
  46. package/dist/web-terminal.d.ts.map +1 -1
  47. package/dist/web-terminal.js +140 -18
  48. package/dist/web-terminal.js.map +1 -1
  49. package/dist/webgl-renderer.d.ts +33 -7
  50. package/dist/webgl-renderer.d.ts.map +1 -1
  51. package/dist/webgl-renderer.js +372 -139
  52. package/dist/webgl-renderer.js.map +1 -1
  53. package/dist/webgl-utils.d.ts +4 -0
  54. package/dist/webgl-utils.d.ts.map +1 -0
  55. package/dist/webgl-utils.js +19 -0
  56. package/dist/webgl-utils.js.map +1 -0
  57. package/dist/worker-bridge.d.ts +7 -2
  58. package/dist/worker-bridge.d.ts.map +1 -1
  59. package/dist/worker-bridge.js +41 -13
  60. package/dist/worker-bridge.js.map +1 -1
  61. package/package.json +6 -6
@@ -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 by using `gl.viewport` and
7
- * `gl.scissor` to partition the canvas into regions.
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 { BG_INSTANCE_FLOATS, GLYPH_INSTANCE_FLOATS, GlyphAtlas, hexToFloat4, packBgInstance, packGlyphInstance, } from "./webgl-renderer.js";
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
- bgInstanceVBO = null;
138
- glyphInstanceVBO = null;
139
- bgVAO = null;
140
- glyphVAO = null;
141
- // Instance data (CPU side)
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 * BG_INSTANCE_FLOATS);
171
- this.glyphInstances = new Float32Array(maxCells * GLYPH_INSTANCE_FLOATS);
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: false,
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
- // Full-canvas clear
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
- // Render each terminal in its viewport
240
- for (const [_id, entry] of this.terminals) {
241
- this.renderTerminal(entry);
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
- if (this.bgInstanceVBO)
294
- gl.deleteBuffer(this.bgInstanceVBO);
295
- if (this.glyphInstanceVBO)
296
- gl.deleteBuffer(this.glyphInstanceVBO);
297
- if (this.bgVAO)
298
- gl.deleteVertexArray(this.bgVAO);
299
- if (this.glyphVAO)
300
- gl.deleteVertexArray(this.glyphVAO);
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 rendering
515
+ // Per-terminal instance data building (CPU-side only, no GL calls)
310
516
  // -----------------------------------------------------------------------
311
- renderTerminal(entry) {
312
- if (!this.gl)
313
- return;
314
- const gl = this.gl;
315
- const { grid, cursor, viewport } = entry;
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
- // Convert CSS viewport to device pixels
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
- const vpW = Math.round(viewport.width * this.dpr);
322
- const vpH = Math.round(viewport.height * this.dpr);
323
- // WebGL viewport Y is from bottom; canvas Y is from top
324
- const canvasHeight = this.canvas.height;
325
- const glY = canvasHeight - vpY - vpH;
326
- gl.viewport(vpX, glY, vpW, vpH);
327
- gl.enable(gl.SCISSOR_TEST);
328
- gl.scissor(vpX, glY, vpW, vpH);
329
- // Clear this region with background color
330
- gl.clearColor(this.themeBgFloat[0], this.themeBgFloat[1], this.themeBgFloat[2], 1.0);
331
- gl.clear(gl.COLOR_BUFFER_BIT);
332
- // Ensure instance buffers are large enough
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
- if (this.bgInstances.length < totalCells * BG_INSTANCE_FLOATS) {
335
- this.bgInstances = new Float32Array(totalCells * BG_INSTANCE_FLOATS);
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 (this.glyphInstances.length < totalCells * GLYPH_INSTANCE_FLOATS) {
338
- this.glyphInstances = new Float32Array(totalCells * GLYPH_INSTANCE_FLOATS);
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
- for (let col = 0; col < cols; col++) {
345
- const codepoint = grid.getCodepoint(row, col);
346
- const fgIdx = grid.getFgIndex(row, col);
347
- const bgIdx = grid.getBgIndex(row, col);
348
- const attrs = grid.getAttrs(row, col);
349
- const fgIsRGB = grid.isFgRGB(row, col);
350
- const bgIsRGB = grid.isBgRGB(row, col);
351
- let fg = this.resolveColorFloat(fgIdx, fgIsRGB, grid, col, true);
352
- let bg = this.resolveColorFloat(bgIdx, bgIsRGB, grid, col, false);
353
- if (attrs & ATTR_INVERSE) {
354
- const tmp = fg;
355
- fg = bg;
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
- packBgInstance(this.bgInstances, bgCount * BG_INSTANCE_FLOATS, col, row, bg[0], bg[1], bg[2], bg[3]);
359
- bgCount++;
360
- if (codepoint > 0x20) {
361
- const bold = !!(attrs & ATTR_BOLD);
362
- const italic = !!(attrs & ATTR_ITALIC);
363
- const glyph = this.atlas.getGlyph(codepoint, bold, italic);
364
- if (glyph) {
365
- packGlyphInstance(this.glyphInstances, glyphCount * GLYPH_INSTANCE_FLOATS, col, row, fg[0], fg[1], fg[2], fg[3], glyph.u, glyph.v, glyph.w, glyph.h, glyph.pw, glyph.ph);
366
- glyphCount++;
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
- // --- 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
- }
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
- this.bgInstanceVBO = gl.createBuffer();
437
- this.glyphInstanceVBO = gl.createBuffer();
438
- this.bgVAO = gl.createVertexArray();
439
- gl.bindVertexArray(this.bgVAO);
440
- this.setupBgVAO(gl);
441
- gl.bindVertexArray(null);
442
- this.glyphVAO = gl.createVertexArray();
443
- gl.bindVertexArray(this.glyphVAO);
444
- this.setupGlyphVAO(gl);
445
- gl.bindVertexArray(null);
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
- if (!this.bgProgram)
450
- return;
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(aPos);
455
- gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
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
- gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
458
- const stride = BG_INSTANCE_FLOATS * FLOAT;
459
- const aCellPos = gl.getAttribLocation(program, "a_cellPos");
460
- gl.enableVertexAttribArray(aCellPos);
461
- gl.vertexAttribPointer(aCellPos, 2, gl.FLOAT, false, stride, 0);
462
- gl.vertexAttribDivisor(aCellPos, 1);
463
- const aColor = gl.getAttribLocation(program, "a_color");
464
- gl.enableVertexAttribArray(aColor);
465
- gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 2 * FLOAT);
466
- gl.vertexAttribDivisor(aColor, 1);
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
- if (!this.glyphProgram)
471
- return;
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(aPos);
476
- gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
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
- gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBO);
479
- const stride = GLYPH_INSTANCE_FLOATS * FLOAT;
480
- const aCellPos = gl.getAttribLocation(program, "a_cellPos");
481
- gl.enableVertexAttribArray(aCellPos);
482
- gl.vertexAttribPointer(aCellPos, 2, gl.FLOAT, false, stride, 0);
483
- gl.vertexAttribDivisor(aCellPos, 1);
484
- const aColor = gl.getAttribLocation(program, "a_color");
485
- gl.enableVertexAttribArray(aColor);
486
- gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 2 * FLOAT);
487
- gl.vertexAttribDivisor(aColor, 1);
488
- const aTexCoord = gl.getAttribLocation(program, "a_texCoord");
489
- gl.enableVertexAttribArray(aTexCoord);
490
- gl.vertexAttribPointer(aTexCoord, 4, gl.FLOAT, false, stride, 6 * FLOAT);
491
- gl.vertexAttribDivisor(aTexCoord, 1);
492
- const aGlyphSize = gl.getAttribLocation(program, "a_glyphSize");
493
- gl.enableVertexAttribArray(aGlyphSize);
494
- gl.vertexAttribPointer(aGlyphSize, 2, gl.FLOAT, false, stride, 10 * FLOAT);
495
- gl.vertexAttribDivisor(aGlyphSize, 1);
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);