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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +21 -3
  33. package/dist/webgl-renderer.d.ts.map +1 -1
  34. package/dist/webgl-renderer.js +255 -109
  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
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { DEFAULT_THEME, normalizeSelection } from "@next_term/core";
10
10
  import { build256Palette, Canvas2DRenderer } from "./renderer.js";
11
+ import { resolveColorFloat } from "./webgl-utils.js";
11
12
  // ---------------------------------------------------------------------------
12
13
  // Attribute bit positions (mirrors renderer.ts / cell-grid.ts)
13
14
  // ---------------------------------------------------------------------------
@@ -361,6 +362,7 @@ export class WebGLRenderer {
361
362
  cursor = null;
362
363
  cellWidth = 0;
363
364
  cellHeight = 0;
365
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: read via destructuring in render()
364
366
  baselineOffset = 0;
365
367
  fontSize;
366
368
  fontFamily;
@@ -380,15 +382,36 @@ export class WebGLRenderer {
380
382
  glyphProgram = null;
381
383
  quadVBO = null;
382
384
  quadEBO = null;
383
- bgInstanceVBO = null;
384
- glyphInstanceVBO = null;
385
+ // Double-buffered instance VBOs to avoid GPU read/write conflicts
386
+ bgInstanceVBOs = [null, null];
387
+ glyphInstanceVBOs = [null, null];
388
+ activeBufferIdx = 0;
389
+ // Dedicated overlay VBO for cursor/selection/highlights so we don't
390
+ // overwrite the active bg VBO and neutralize double-buffering.
391
+ overlayVBO = null;
385
392
  bgVAO = null;
386
393
  glyphVAO = null;
394
+ // Cached uniform locations (populated in initGLResources)
395
+ bgResolutionLoc = null;
396
+ bgCellSizeLoc = null;
397
+ glyphResolutionLoc = null;
398
+ glyphCellSizeLoc = null;
399
+ glyphAtlasLoc = null;
387
400
  // Instance data (CPU side)
388
401
  bgInstances;
389
402
  glyphInstances;
390
403
  bgCount = 0;
391
404
  glyphCount = 0;
405
+ // Per-row dirty tracking for incremental instance rebuilds
406
+ rowBgOffsets = []; // starting bgCount index per row
407
+ rowBgCounts = []; // number of bg instances per row
408
+ rowGlyphOffsets = []; // starting glyphCount index per row
409
+ rowGlyphCounts = []; // number of glyph instances per row
410
+ hasRenderedOnce = false;
411
+ // Pre-allocated overlay buffers (reused each frame)
412
+ cursorData = new Float32Array(BG_INSTANCE_FLOATS);
413
+ selBuffer = new Float32Array(256 * BG_INSTANCE_FLOATS);
414
+ hlBuffer = new Float32Array(256 * BG_INSTANCE_FLOATS);
392
415
  // Glyph atlas
393
416
  atlas;
394
417
  // Palette as float arrays (cached for performance)
@@ -421,6 +444,7 @@ export class WebGLRenderer {
421
444
  this.canvas = canvas;
422
445
  this.grid = grid;
423
446
  this.cursor = cursor;
447
+ this.hasRenderedOnce = false;
424
448
  // Get WebGL2 context
425
449
  this.gl = canvas.getContext("webgl2", {
426
450
  alpha: false,
@@ -482,10 +506,38 @@ export class WebGLRenderer {
482
506
  if (!anyDirty) {
483
507
  return;
484
508
  }
485
- // Rebuild instance data
486
- this.bgCount = 0;
487
- this.glyphCount = 0;
509
+ // Flip active double-buffer index
510
+ this.activeBufferIdx = 1 - this.activeBufferIdx;
511
+ // Incremental rebuild — only re-pack dirty rows
512
+ // On first render or if grid dimensions changed, initialize per-row tracking
513
+ if (!this.hasRenderedOnce || this.rowBgOffsets.length !== rows) {
514
+ this.rowBgOffsets = new Array(rows).fill(0);
515
+ this.rowBgCounts = new Array(rows).fill(0);
516
+ this.rowGlyphOffsets = new Array(rows).fill(0);
517
+ this.rowGlyphCounts = new Array(rows).fill(0);
518
+ // Compute fixed offsets: each row has exactly `cols` bg instances
519
+ // Glyph offsets are variable, so on first pass we do a full rebuild
520
+ let bgOff = 0;
521
+ let glyphOff = 0;
522
+ for (let r = 0; r < rows; r++) {
523
+ this.rowBgOffsets[r] = bgOff;
524
+ this.rowBgCounts[r] = cols; // one bg instance per cell
525
+ bgOff += cols;
526
+ // For glyphs, allocate max possible (cols) per row on first pass
527
+ this.rowGlyphOffsets[r] = glyphOff;
528
+ this.rowGlyphCounts[r] = 0;
529
+ glyphOff += cols;
530
+ }
531
+ this.bgCount = bgOff;
532
+ this.glyphCount = 0; // will be summed below
533
+ }
488
534
  for (let row = 0; row < rows; row++) {
535
+ // Skip non-dirty rows — their data persists in the arrays
536
+ if (!grid.isDirty(row))
537
+ continue;
538
+ const bgBase = this.rowBgOffsets[row] * BG_INSTANCE_FLOATS;
539
+ const glyphBase = this.rowGlyphOffsets[row] * GLYPH_INSTANCE_FLOATS;
540
+ let rowGlyphCount = 0;
489
541
  for (let col = 0; col < cols; col++) {
490
542
  const codepoint = grid.getCodepoint(row, col);
491
543
  const fgIdx = grid.getFgIndex(row, col);
@@ -493,9 +545,8 @@ export class WebGLRenderer {
493
545
  const attrs = grid.getAttrs(row, col);
494
546
  const fgIsRGB = grid.isFgRGB(row, col);
495
547
  const bgIsRGB = grid.isBgRGB(row, col);
496
- const isWide = grid.isWide(row, col);
497
- let fg = this.resolveColorFloat(fgIdx, fgIsRGB, grid, col, true);
498
- let bg = this.resolveColorFloat(bgIdx, bgIsRGB, grid, col, false);
548
+ let fg = resolveColorFloat(fgIdx, fgIsRGB, grid, col, true, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
549
+ let bg = resolveColorFloat(bgIdx, bgIsRGB, grid, col, false, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
499
550
  // Handle inverse
500
551
  if (attrs & ATTR_INVERSE) {
501
552
  const tmp = fg;
@@ -503,23 +554,39 @@ export class WebGLRenderer {
503
554
  bg = tmp;
504
555
  }
505
556
  // Background instance — emit for all cells to paint default bg too
506
- packBgInstance(this.bgInstances, this.bgCount * BG_INSTANCE_FLOATS, col, row, bg[0], bg[1], bg[2], bg[3]);
507
- this.bgCount++;
557
+ packBgInstance(this.bgInstances, bgBase + col * BG_INSTANCE_FLOATS, col, row, bg[0], bg[1], bg[2], bg[3]);
508
558
  // Glyph instance — skip spaces and control chars
509
559
  if (codepoint > 0x20) {
510
560
  const bold = !!(attrs & ATTR_BOLD);
511
561
  const italic = !!(attrs & ATTR_ITALIC);
512
562
  const glyph = this.atlas.getGlyph(codepoint, bold, italic);
513
563
  if (glyph) {
514
- const glyphPw = isWide ? glyph.pw : glyph.pw;
515
564
  const glyphPh = glyph.ph;
516
- packGlyphInstance(this.glyphInstances, this.glyphCount * GLYPH_INSTANCE_FLOATS, col, row, fg[0], fg[1], fg[2], fg[3], glyph.u, glyph.v, glyph.w, glyph.h, glyphPw, glyphPh);
517
- this.glyphCount++;
565
+ packGlyphInstance(this.glyphInstances, glyphBase + rowGlyphCount * GLYPH_INSTANCE_FLOATS, col, row, fg[0], fg[1], fg[2], fg[3], glyph.u, glyph.v, glyph.w, glyph.h, glyph.pw, glyphPh);
566
+ rowGlyphCount++;
518
567
  }
519
568
  }
520
569
  }
570
+ // Zero out remaining glyph slots for this row (if fewer glyphs than last time)
571
+ const maxGlyphSlots = cols;
572
+ for (let i = rowGlyphCount; i < this.rowGlyphCounts[row]; i++) {
573
+ // Zero the codepoint/color to make invisible (alpha=0 effectively)
574
+ const off = glyphBase + i * GLYPH_INSTANCE_FLOATS;
575
+ for (let j = 0; j < GLYPH_INSTANCE_FLOATS; j++) {
576
+ this.glyphInstances[off + j] = 0;
577
+ }
578
+ }
579
+ // Keep max of old and new count to ensure we still upload zeroed slots
580
+ if (rowGlyphCount > maxGlyphSlots)
581
+ rowGlyphCount = maxGlyphSlots;
582
+ this.rowGlyphCounts[row] = rowGlyphCount;
521
583
  grid.clearDirty(row);
522
584
  }
585
+ this.hasRenderedOnce = true;
586
+ // Both bg and glyph instance arrays are sized for rows * cols slots;
587
+ // data is packed per-row at fixed offsets so we always upload the full region.
588
+ this.bgCount = rows * cols;
589
+ this.glyphCount = rows * cols;
523
590
  // Upload atlas if dirty
524
591
  this.atlas.upload(gl);
525
592
  // Set up GL state
@@ -530,39 +597,54 @@ export class WebGLRenderer {
530
597
  gl.clear(gl.COLOR_BUFFER_BIT);
531
598
  const cellW = this.cellWidth * this.dpr;
532
599
  const cellH = this.cellHeight * this.dpr;
600
+ const _FLOAT = 4;
601
+ const activeBgVBO = this.bgInstanceVBOs[this.activeBufferIdx];
602
+ const activeGlyphVBO = this.glyphInstanceVBOs[this.activeBufferIdx];
533
603
  // --- Background pass ---
534
- if (this.bgCount > 0 && this.bgProgram && this.bgVAO && this.bgInstanceVBO) {
604
+ if (this.bgCount > 0 && this.bgProgram && this.bgVAO && activeBgVBO) {
535
605
  gl.useProgram(this.bgProgram);
536
- gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), canvasWidth, canvasHeight);
537
- gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), cellW, cellH);
538
- gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
539
- gl.bufferData(gl.ARRAY_BUFFER, this.bgInstances.subarray(0, this.bgCount * BG_INSTANCE_FLOATS), gl.DYNAMIC_DRAW);
606
+ // Use cached uniform locations
607
+ gl.uniform2f(this.bgResolutionLoc, canvasWidth, canvasHeight);
608
+ gl.uniform2f(this.bgCellSizeLoc, cellW, cellH);
609
+ // Bind active double-buffered VBO and re-setup instance attrib pointers
610
+ gl.bindBuffer(gl.ARRAY_BUFFER, activeBgVBO);
611
+ gl.bufferData(gl.ARRAY_BUFFER, this.bgInstances.subarray(0, this.bgCount * BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
540
612
  gl.bindVertexArray(this.bgVAO);
613
+ // Rebind instance attribs to the active VBO (VAO captured the old one)
614
+ this.rebindBgInstanceAttribs(gl, activeBgVBO);
541
615
  gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, this.bgCount);
542
616
  }
543
617
  // --- Glyph pass ---
544
- if (this.glyphCount > 0 && this.glyphProgram && this.glyphVAO && this.glyphInstanceVBO) {
618
+ if (this.glyphCount > 0 && this.glyphProgram && this.glyphVAO && activeGlyphVBO) {
545
619
  gl.enable(gl.BLEND);
546
620
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
547
621
  gl.useProgram(this.glyphProgram);
548
- gl.uniform2f(gl.getUniformLocation(this.glyphProgram, "u_resolution"), canvasWidth, canvasHeight);
549
- gl.uniform2f(gl.getUniformLocation(this.glyphProgram, "u_cellSize"), cellW, cellH);
622
+ // Use cached uniform locations
623
+ gl.uniform2f(this.glyphResolutionLoc, canvasWidth, canvasHeight);
624
+ gl.uniform2f(this.glyphCellSizeLoc, cellW, cellH);
550
625
  // Bind atlas texture
551
626
  gl.activeTexture(gl.TEXTURE0);
552
627
  gl.bindTexture(gl.TEXTURE_2D, this.atlas.getTexture());
553
- gl.uniform1i(gl.getUniformLocation(this.glyphProgram, "u_atlas"), 0);
554
- gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBO);
555
- gl.bufferData(gl.ARRAY_BUFFER, this.glyphInstances.subarray(0, this.glyphCount * GLYPH_INSTANCE_FLOATS), gl.DYNAMIC_DRAW);
628
+ gl.uniform1i(this.glyphAtlasLoc, 0);
629
+ // Bind active double-buffered VBO
630
+ gl.bindBuffer(gl.ARRAY_BUFFER, activeGlyphVBO);
631
+ gl.bufferData(gl.ARRAY_BUFFER, this.glyphInstances.subarray(0, this.glyphCount * GLYPH_INSTANCE_FLOATS), gl.STREAM_DRAW);
556
632
  gl.bindVertexArray(this.glyphVAO);
633
+ // Rebind instance attribs to the active VBO
634
+ this.rebindGlyphInstanceAttribs(gl, activeGlyphVBO);
557
635
  gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, this.glyphCount);
558
636
  gl.disable(gl.BLEND);
559
637
  }
638
+ // Enable BLEND once for all overlay passes
639
+ gl.enable(gl.BLEND);
640
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
560
641
  // --- Highlights (search results) ---
561
642
  this.drawHighlights();
562
643
  // --- Selection overlay ---
563
644
  this.drawSelection();
564
645
  // --- Cursor ---
565
646
  this.drawCursor();
647
+ gl.disable(gl.BLEND);
566
648
  gl.bindVertexArray(null);
567
649
  }
568
650
  resize(_cols, _rows) {
@@ -631,10 +713,16 @@ export class WebGLRenderer {
631
713
  gl.deleteBuffer(this.quadVBO);
632
714
  if (this.quadEBO)
633
715
  gl.deleteBuffer(this.quadEBO);
634
- if (this.bgInstanceVBO)
635
- gl.deleteBuffer(this.bgInstanceVBO);
636
- if (this.glyphInstanceVBO)
637
- gl.deleteBuffer(this.glyphInstanceVBO);
716
+ if (this.bgInstanceVBOs[0])
717
+ gl.deleteBuffer(this.bgInstanceVBOs[0]);
718
+ if (this.bgInstanceVBOs[1])
719
+ gl.deleteBuffer(this.bgInstanceVBOs[1]);
720
+ if (this.glyphInstanceVBOs[0])
721
+ gl.deleteBuffer(this.glyphInstanceVBOs[0]);
722
+ if (this.glyphInstanceVBOs[1])
723
+ gl.deleteBuffer(this.glyphInstanceVBOs[1]);
724
+ if (this.overlayVBO)
725
+ gl.deleteBuffer(this.overlayVBO);
638
726
  if (this.bgVAO)
639
727
  gl.deleteVertexArray(this.bgVAO);
640
728
  if (this.glyphVAO)
@@ -693,19 +781,40 @@ export class WebGLRenderer {
693
781
  this.quadEBO = gl.createBuffer();
694
782
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
695
783
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices, gl.STATIC_DRAW);
696
- // Instance buffers
697
- this.bgInstanceVBO = gl.createBuffer();
698
- this.glyphInstanceVBO = gl.createBuffer();
699
- // Set up background VAO
784
+ // Double-buffered instance VBOs
785
+ this.bgInstanceVBOs = [gl.createBuffer(), gl.createBuffer()];
786
+ this.glyphInstanceVBOs = [gl.createBuffer(), gl.createBuffer()];
787
+ // Dedicated overlay VBO for cursor/selection/highlights
788
+ this.overlayVBO = gl.createBuffer();
789
+ // Set up background VAO (quad + EBO only; instance buffer bound per-frame)
700
790
  this.bgVAO = gl.createVertexArray();
701
791
  gl.bindVertexArray(this.bgVAO);
702
792
  this.setupBgVAO(gl);
703
793
  gl.bindVertexArray(null);
704
- // Set up glyph VAO
794
+ // Set up glyph VAO (quad + EBO only; instance buffer bound per-frame)
705
795
  this.glyphVAO = gl.createVertexArray();
706
796
  gl.bindVertexArray(this.glyphVAO);
707
797
  this.setupGlyphVAO(gl);
708
798
  gl.bindVertexArray(null);
799
+ // Cache all uniform locations after programs are compiled
800
+ this.bgResolutionLoc = gl.getUniformLocation(this.bgProgram, "u_resolution");
801
+ this.bgCellSizeLoc = gl.getUniformLocation(this.bgProgram, "u_cellSize");
802
+ this.glyphResolutionLoc = gl.getUniformLocation(this.glyphProgram, "u_resolution");
803
+ this.glyphCellSizeLoc = gl.getUniformLocation(this.glyphProgram, "u_cellSize");
804
+ this.glyphAtlasLoc = gl.getUniformLocation(this.glyphProgram, "u_atlas");
805
+ // Cache attribute locations
806
+ this.bgAttribLocs = {
807
+ cellPos: gl.getAttribLocation(this.bgProgram, "a_cellPos"),
808
+ color: gl.getAttribLocation(this.bgProgram, "a_color"),
809
+ };
810
+ this.glyphAttribLocs = {
811
+ cellPos: gl.getAttribLocation(this.glyphProgram, "a_cellPos"),
812
+ color: gl.getAttribLocation(this.glyphProgram, "a_color"),
813
+ texCoord: gl.getAttribLocation(this.glyphProgram, "a_texCoord"),
814
+ glyphSize: gl.getAttribLocation(this.glyphProgram, "a_glyphSize"),
815
+ };
816
+ // Reset dirty-row tracking state on GL reinit
817
+ this.hasRenderedOnce = false;
709
818
  // Recreate atlas texture
710
819
  this.atlas.recreateTexture();
711
820
  }
@@ -721,8 +830,8 @@ export class WebGLRenderer {
721
830
  gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
722
831
  // Element buffer
723
832
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
724
- // Instance data
725
- gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
833
+ // Instance data — bind initial buffer; will be rebound per-frame for double buffering
834
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBOs[0]);
726
835
  const stride = BG_INSTANCE_FLOATS * FLOAT;
727
836
  const aCellPos = gl.getAttribLocation(program, "a_cellPos");
728
837
  gl.enableVertexAttribArray(aCellPos);
@@ -745,8 +854,8 @@ export class WebGLRenderer {
745
854
  gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
746
855
  // Element buffer
747
856
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
748
- // Instance data
749
- gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBO);
857
+ // Instance data — bind initial buffer; will be rebound per-frame for double buffering
858
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBOs[0]);
750
859
  const stride = GLYPH_INSTANCE_FLOATS * FLOAT;
751
860
  const aCellPos = gl.getAttribLocation(program, "a_cellPos");
752
861
  gl.enableVertexAttribArray(aCellPos);
@@ -776,9 +885,11 @@ export class WebGLRenderer {
776
885
  const neededGlyph = totalCells * GLYPH_INSTANCE_FLOATS;
777
886
  if (this.bgInstances.length < neededBg) {
778
887
  this.bgInstances = new Float32Array(neededBg);
888
+ this.hasRenderedOnce = false; // force full rebuild on resize
779
889
  }
780
890
  if (this.glyphInstances.length < neededGlyph) {
781
891
  this.glyphInstances = new Float32Array(neededGlyph);
892
+ this.hasRenderedOnce = false;
782
893
  }
783
894
  }
784
895
  // -----------------------------------------------------------------------
@@ -790,24 +901,6 @@ export class WebGLRenderer {
790
901
  this.themeBgFloat = hexToFloat4(this.theme.background);
791
902
  this.themeCursorFloat = hexToFloat4(this.theme.cursor);
792
903
  }
793
- resolveColorFloat(colorIdx, isRGB, grid, col, isForeground) {
794
- if (isRGB) {
795
- const offset = isForeground ? col : 256 + col;
796
- const rgb = grid.rgbColors[offset];
797
- const r = ((rgb >> 16) & 0xff) / 255;
798
- const g = ((rgb >> 8) & 0xff) / 255;
799
- const b = (rgb & 0xff) / 255;
800
- return [r, g, b, 1.0];
801
- }
802
- if (isForeground && colorIdx === 7)
803
- return this.themeFgFloat;
804
- if (!isForeground && colorIdx === 0)
805
- return this.themeBgFloat;
806
- if (colorIdx >= 0 && colorIdx < 256) {
807
- return this.paletteFloat[colorIdx];
808
- }
809
- return isForeground ? this.themeFgFloat : this.themeBgFloat;
810
- }
811
904
  // -----------------------------------------------------------------------
812
905
  // Cursor
813
906
  // -----------------------------------------------------------------------
@@ -815,33 +908,44 @@ export class WebGLRenderer {
815
908
  if (!this.gl || !this.highlights.length)
816
909
  return;
817
910
  const gl = this.gl;
818
- if (!this.bgProgram || !this.bgVAO || !this.bgInstanceVBO)
911
+ if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
819
912
  return;
820
- const hlInstances = [];
913
+ // Pack into pre-allocated hlBuffer, growing only if needed
914
+ let hlIdx = 0;
821
915
  for (const hl of this.highlights) {
822
- // Current match: orange, other matches: semi-transparent yellow
823
916
  const r = hl.isCurrent ? 1.0 : 1.0;
824
917
  const g = hl.isCurrent ? 0.647 : 1.0;
825
918
  const b = hl.isCurrent ? 0.0 : 0.0;
826
919
  const a = hl.isCurrent ? 0.5 : 0.3;
827
920
  for (let col = hl.startCol; col <= hl.endCol; col++) {
828
- hlInstances.push(col, hl.row, r, g, b, a);
921
+ const needed = (hlIdx + 1) * BG_INSTANCE_FLOATS;
922
+ if (needed > this.hlBuffer.length) {
923
+ const newBuf = new Float32Array(this.hlBuffer.length * 2);
924
+ newBuf.set(this.hlBuffer);
925
+ this.hlBuffer = newBuf;
926
+ }
927
+ const off = hlIdx * BG_INSTANCE_FLOATS;
928
+ this.hlBuffer[off] = col;
929
+ this.hlBuffer[off + 1] = hl.row;
930
+ this.hlBuffer[off + 2] = r;
931
+ this.hlBuffer[off + 3] = g;
932
+ this.hlBuffer[off + 4] = b;
933
+ this.hlBuffer[off + 5] = a;
934
+ hlIdx++;
829
935
  }
830
936
  }
831
- if (hlInstances.length === 0)
937
+ if (hlIdx === 0)
832
938
  return;
833
- const hlData = new Float32Array(hlInstances);
834
- const hlCount = hlInstances.length / BG_INSTANCE_FLOATS;
835
- gl.enable(gl.BLEND);
836
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
939
+ // BLEND already enabled by caller
837
940
  gl.useProgram(this.bgProgram);
838
- gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), this.canvas?.width ?? 0, this.canvas?.height ?? 0);
839
- gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), this.cellWidth * this.dpr, this.cellHeight * this.dpr);
840
- gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
841
- gl.bufferData(gl.ARRAY_BUFFER, hlData, gl.DYNAMIC_DRAW);
941
+ // Use cached uniform locations
942
+ gl.uniform2f(this.bgResolutionLoc, this.canvas?.width ?? 0, this.canvas?.height ?? 0);
943
+ gl.uniform2f(this.bgCellSizeLoc, this.cellWidth * this.dpr, this.cellHeight * this.dpr);
944
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayVBO);
945
+ gl.bufferData(gl.ARRAY_BUFFER, this.hlBuffer.subarray(0, hlIdx * BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
842
946
  gl.bindVertexArray(this.bgVAO);
843
- gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, hlCount);
844
- gl.disable(gl.BLEND);
947
+ this.rebindBgInstanceAttribs(gl, this.overlayVBO);
948
+ gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, hlIdx);
845
949
  }
846
950
  drawSelection() {
847
951
  if (!this.gl || !this.grid || !this.selection)
@@ -854,12 +958,12 @@ export class WebGLRenderer {
854
958
  // Skip if selection is empty (same cell)
855
959
  if (sr === er && sel.startCol === sel.endCol)
856
960
  return;
857
- if (!this.bgProgram || !this.bgVAO || !this.bgInstanceVBO)
961
+ if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
858
962
  return;
859
963
  // Parse the selection background color
860
964
  const selColor = hexToFloat4(this.theme.selectionBackground);
861
- // Build instance data for selection rects
862
- const selInstances = [];
965
+ // Pack into pre-allocated selBuffer, growing only if needed
966
+ let selIdx = 0;
863
967
  for (let row = sr; row <= er; row++) {
864
968
  let colStart;
865
969
  let colEnd;
@@ -880,23 +984,34 @@ export class WebGLRenderer {
880
984
  colEnd = grid.cols - 1;
881
985
  }
882
986
  for (let col = colStart; col <= colEnd; col++) {
883
- selInstances.push(col, row, selColor[0], selColor[1], selColor[2], 0.5);
987
+ const needed = (selIdx + 1) * BG_INSTANCE_FLOATS;
988
+ if (needed > this.selBuffer.length) {
989
+ const newBuf = new Float32Array(this.selBuffer.length * 2);
990
+ newBuf.set(this.selBuffer);
991
+ this.selBuffer = newBuf;
992
+ }
993
+ const off = selIdx * BG_INSTANCE_FLOATS;
994
+ this.selBuffer[off] = col;
995
+ this.selBuffer[off + 1] = row;
996
+ this.selBuffer[off + 2] = selColor[0];
997
+ this.selBuffer[off + 3] = selColor[1];
998
+ this.selBuffer[off + 4] = selColor[2];
999
+ this.selBuffer[off + 5] = 0.5;
1000
+ selIdx++;
884
1001
  }
885
1002
  }
886
- if (selInstances.length === 0)
1003
+ if (selIdx === 0)
887
1004
  return;
888
- const selData = new Float32Array(selInstances);
889
- const selCount = selInstances.length / BG_INSTANCE_FLOATS;
890
- gl.enable(gl.BLEND);
891
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1005
+ // BLEND already enabled by caller
892
1006
  gl.useProgram(this.bgProgram);
893
- gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), this.canvas?.width ?? 0, this.canvas?.height ?? 0);
894
- gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), this.cellWidth * this.dpr, this.cellHeight * this.dpr);
895
- gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
896
- gl.bufferData(gl.ARRAY_BUFFER, selData, gl.DYNAMIC_DRAW);
1007
+ // Use cached uniform locations
1008
+ gl.uniform2f(this.bgResolutionLoc, this.canvas?.width ?? 0, this.canvas?.height ?? 0);
1009
+ gl.uniform2f(this.bgCellSizeLoc, this.cellWidth * this.dpr, this.cellHeight * this.dpr);
1010
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayVBO);
1011
+ gl.bufferData(gl.ARRAY_BUFFER, this.selBuffer.subarray(0, selIdx * BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
897
1012
  gl.bindVertexArray(this.bgVAO);
898
- gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, selCount);
899
- gl.disable(gl.BLEND);
1013
+ this.rebindBgInstanceAttribs(gl, this.overlayVBO);
1014
+ gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, selIdx);
900
1015
  }
901
1016
  drawCursor() {
902
1017
  if (!this.gl || !this.cursor || !this.cursor.visible)
@@ -907,50 +1022,81 @@ export class WebGLRenderer {
907
1022
  const cellH = this.cellHeight * this.dpr;
908
1023
  const cc = this.themeCursorFloat;
909
1024
  // Use the bg program to draw a simple colored rect for the cursor
910
- if (!this.bgProgram || !this.bgVAO || !this.bgInstanceVBO)
1025
+ if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
911
1026
  return;
912
- gl.enable(gl.BLEND);
913
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1027
+ // BLEND already enabled by caller
914
1028
  gl.useProgram(this.bgProgram);
915
- gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), this.canvas?.width ?? 0, this.canvas?.height ?? 0);
916
- gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), cellW, cellH);
1029
+ // Use cached uniform locations
1030
+ gl.uniform2f(this.bgResolutionLoc, this.canvas?.width ?? 0, this.canvas?.height ?? 0);
1031
+ gl.uniform2f(this.bgCellSizeLoc, cellW, cellH);
1032
+ // Write into pre-allocated cursorData instead of allocating
917
1033
  // For bar and underline styles, we draw a thin rect.
918
1034
  // We abuse cellPos with fractional values to position correctly.
919
- let cursorData;
920
1035
  switch (cursor.style) {
921
1036
  case "block":
922
- cursorData = new Float32Array([
923
- cursor.col,
924
- cursor.row,
925
- cc[0],
926
- cc[1],
927
- cc[2],
928
- 0.5, // 50% alpha for block
929
- ]);
1037
+ this.cursorData[0] = cursor.col;
1038
+ this.cursorData[1] = cursor.row;
1039
+ this.cursorData[2] = cc[0];
1040
+ this.cursorData[3] = cc[1];
1041
+ this.cursorData[4] = cc[2];
1042
+ this.cursorData[5] = 0.5; // 50% alpha for block
930
1043
  break;
931
1044
  case "underline": {
932
1045
  // Draw a thin line at the bottom of the cell
933
- // We position it by adjusting cellPos row to be near bottom
934
1046
  const lineH = Math.max(2 * this.dpr, 1);
935
1047
  const fractionalRow = cursor.row + (cellH - lineH) / cellH;
936
- cursorData = new Float32Array([cursor.col, fractionalRow, cc[0], cc[1], cc[2], cc[3]]);
937
- // We'd need a different cell size for this, but we can approximate
938
- // by using the full cell width and adjusting position
1048
+ this.cursorData[0] = cursor.col;
1049
+ this.cursorData[1] = fractionalRow;
1050
+ this.cursorData[2] = cc[0];
1051
+ this.cursorData[3] = cc[1];
1052
+ this.cursorData[4] = cc[2];
1053
+ this.cursorData[5] = cc[3];
939
1054
  break;
940
1055
  }
941
1056
  case "bar": {
942
- // Draw a thin vertical bar at the left of the cell
943
- cursorData = new Float32Array([cursor.col, cursor.row, cc[0], cc[1], cc[2], cc[3]]);
1057
+ this.cursorData[0] = cursor.col;
1058
+ this.cursorData[1] = cursor.row;
1059
+ this.cursorData[2] = cc[0];
1060
+ this.cursorData[3] = cc[1];
1061
+ this.cursorData[4] = cc[2];
1062
+ this.cursorData[5] = cc[3];
944
1063
  break;
945
1064
  }
946
1065
  default:
947
- cursorData = new Float32Array([cursor.col, cursor.row, cc[0], cc[1], cc[2], 0.5]);
1066
+ this.cursorData[0] = cursor.col;
1067
+ this.cursorData[1] = cursor.row;
1068
+ this.cursorData[2] = cc[0];
1069
+ this.cursorData[3] = cc[1];
1070
+ this.cursorData[4] = cc[2];
1071
+ this.cursorData[5] = 0.5;
948
1072
  }
949
- gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
950
- gl.bufferData(gl.ARRAY_BUFFER, cursorData, gl.DYNAMIC_DRAW);
1073
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayVBO);
1074
+ gl.bufferData(gl.ARRAY_BUFFER, this.cursorData, gl.STREAM_DRAW);
951
1075
  gl.bindVertexArray(this.bgVAO);
1076
+ this.rebindBgInstanceAttribs(gl, this.overlayVBO);
952
1077
  gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, 1);
953
- gl.disable(gl.BLEND);
1078
+ }
1079
+ // -----------------------------------------------------------------------
1080
+ // Instance attribute rebinding helpers for double-buffered VBOs
1081
+ // -----------------------------------------------------------------------
1082
+ // Cached attribute locations (populated in initGLResources)
1083
+ bgAttribLocs = { cellPos: -1, color: -1 };
1084
+ glyphAttribLocs = { cellPos: -1, color: -1, texCoord: -1, glyphSize: -1 };
1085
+ rebindBgInstanceAttribs(gl, vbo) {
1086
+ const FLOAT = 4;
1087
+ const stride = BG_INSTANCE_FLOATS * FLOAT;
1088
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
1089
+ gl.vertexAttribPointer(this.bgAttribLocs.cellPos, 2, gl.FLOAT, false, stride, 0);
1090
+ gl.vertexAttribPointer(this.bgAttribLocs.color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
1091
+ }
1092
+ rebindGlyphInstanceAttribs(gl, vbo) {
1093
+ const FLOAT = 4;
1094
+ const stride = GLYPH_INSTANCE_FLOATS * FLOAT;
1095
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
1096
+ gl.vertexAttribPointer(this.glyphAttribLocs.cellPos, 2, gl.FLOAT, false, stride, 0);
1097
+ gl.vertexAttribPointer(this.glyphAttribLocs.color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
1098
+ gl.vertexAttribPointer(this.glyphAttribLocs.texCoord, 4, gl.FLOAT, false, stride, 6 * FLOAT);
1099
+ gl.vertexAttribPointer(this.glyphAttribLocs.glyphSize, 2, gl.FLOAT, false, stride, 10 * FLOAT);
954
1100
  }
955
1101
  // -----------------------------------------------------------------------
956
1102
  // Internal helpers