@next_term/web 0.1.0-next.3 → 0.1.0-next.6

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/dist/accessibility.d.ts +2 -4
  3. package/dist/accessibility.d.ts.map +1 -1
  4. package/dist/accessibility.js +2 -7
  5. package/dist/accessibility.js.map +1 -1
  6. package/dist/addons/web-links.d.ts +0 -1
  7. package/dist/addons/web-links.d.ts.map +1 -1
  8. package/dist/addons/web-links.js +0 -4
  9. package/dist/addons/web-links.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/input-handler.d.ts +7 -0
  15. package/dist/input-handler.d.ts.map +1 -1
  16. package/dist/input-handler.js +26 -5
  17. package/dist/input-handler.js.map +1 -1
  18. package/dist/render-worker.d.ts.map +1 -1
  19. package/dist/render-worker.js +186 -95
  20. package/dist/render-worker.js.map +1 -1
  21. package/dist/renderer.d.ts.map +1 -1
  22. package/dist/renderer.js +1 -0
  23. package/dist/renderer.js.map +1 -1
  24. package/dist/shared-context.d.ts +34 -9
  25. package/dist/shared-context.d.ts.map +1 -1
  26. package/dist/shared-context.js +433 -189
  27. package/dist/shared-context.js.map +1 -1
  28. package/dist/web-terminal.d.ts +15 -0
  29. package/dist/web-terminal.d.ts.map +1 -1
  30. package/dist/web-terminal.js +79 -13
  31. package/dist/web-terminal.js.map +1 -1
  32. package/dist/webgl-renderer.d.ts +22 -5
  33. package/dist/webgl-renderer.d.ts.map +1 -1
  34. package/dist/webgl-renderer.js +326 -129
  35. package/dist/webgl-renderer.js.map +1 -1
  36. package/dist/webgl-utils.d.ts +4 -0
  37. package/dist/webgl-utils.d.ts.map +1 -0
  38. package/dist/webgl-utils.js +19 -0
  39. package/dist/webgl-utils.js.map +1 -0
  40. package/dist/worker-bridge.d.ts +3 -0
  41. package/dist/worker-bridge.d.ts.map +1 -1
  42. package/dist/worker-bridge.js +17 -4
  43. package/dist/worker-bridge.js.map +1 -1
  44. 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
@@ -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 * BG_INSTANCE_FLOATS);
171
- this.glyphInstances = new Float32Array(maxCells * GLYPH_INSTANCE_FLOATS);
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
- // Render each terminal in its viewport
240
- for (const [_id, entry] of this.terminals) {
241
- this.renderTerminal(entry);
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
- 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);
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 rendering
492
+ // Per-terminal instance data building (CPU-side only, no GL calls)
310
493
  // -----------------------------------------------------------------------
311
- renderTerminal(entry) {
312
- if (!this.gl)
313
- return;
314
- const gl = this.gl;
315
- const { grid, cursor, viewport } = entry;
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
- // Convert CSS viewport to device pixels
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
- 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
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
- if (this.bgInstances.length < totalCells * BG_INSTANCE_FLOATS) {
335
- this.bgInstances = new Float32Array(totalCells * BG_INSTANCE_FLOATS);
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 (this.glyphInstances.length < totalCells * GLYPH_INSTANCE_FLOATS) {
338
- this.glyphInstances = new Float32Array(totalCells * GLYPH_INSTANCE_FLOATS);
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
- 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;
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
- 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++;
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
- gl.bindVertexArray(null);
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
- 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);
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
- if (!this.bgProgram)
450
- return;
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(aPos);
455
- gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
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
- 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);
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
- if (!this.glyphProgram)
471
- return;
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(aPos);
476
- gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
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
- 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);
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
  // -----------------------------------------------------------------------