@next_term/web 0.1.0-next.3 → 0.1.0-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/accessibility.d.ts +2 -4
- package/dist/accessibility.d.ts.map +1 -1
- package/dist/accessibility.js +2 -7
- package/dist/accessibility.js.map +1 -1
- package/dist/addons/web-links.d.ts +0 -1
- package/dist/addons/web-links.d.ts.map +1 -1
- package/dist/addons/web-links.js +0 -4
- package/dist/addons/web-links.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/input-handler.d.ts +7 -0
- package/dist/input-handler.d.ts.map +1 -1
- package/dist/input-handler.js +26 -5
- package/dist/input-handler.js.map +1 -1
- package/dist/render-worker.d.ts.map +1 -1
- package/dist/render-worker.js +186 -95
- package/dist/render-worker.js.map +1 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +1 -0
- package/dist/renderer.js.map +1 -1
- package/dist/shared-context.d.ts +34 -9
- package/dist/shared-context.d.ts.map +1 -1
- package/dist/shared-context.js +433 -189
- package/dist/shared-context.js.map +1 -1
- package/dist/web-terminal.d.ts +15 -0
- package/dist/web-terminal.d.ts.map +1 -1
- package/dist/web-terminal.js +79 -13
- package/dist/web-terminal.js.map +1 -1
- package/dist/webgl-renderer.d.ts +21 -3
- package/dist/webgl-renderer.d.ts.map +1 -1
- package/dist/webgl-renderer.js +255 -109
- package/dist/webgl-renderer.js.map +1 -1
- package/dist/webgl-utils.d.ts +4 -0
- package/dist/webgl-utils.d.ts.map +1 -0
- package/dist/webgl-utils.js +19 -0
- package/dist/webgl-utils.js.map +1 -0
- package/dist/worker-bridge.d.ts +3 -0
- package/dist/worker-bridge.d.ts.map +1 -1
- package/dist/worker-bridge.js +17 -4
- package/dist/worker-bridge.js.map +1 -1
- package/package.json +6 -6
package/dist/webgl-renderer.js
CHANGED
|
@@ -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
|
-
|
|
384
|
-
|
|
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
|
-
//
|
|
486
|
-
this.
|
|
487
|
-
|
|
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
|
-
|
|
497
|
-
let
|
|
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,
|
|
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,
|
|
517
|
-
|
|
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 &&
|
|
604
|
+
if (this.bgCount > 0 && this.bgProgram && this.bgVAO && activeBgVBO) {
|
|
535
605
|
gl.useProgram(this.bgProgram);
|
|
536
|
-
|
|
537
|
-
gl.uniform2f(
|
|
538
|
-
gl.
|
|
539
|
-
|
|
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 &&
|
|
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
|
-
|
|
549
|
-
gl.uniform2f(
|
|
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(
|
|
554
|
-
|
|
555
|
-
gl.
|
|
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.
|
|
635
|
-
gl.deleteBuffer(this.
|
|
636
|
-
if (this.
|
|
637
|
-
gl.deleteBuffer(this.
|
|
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
|
-
//
|
|
697
|
-
this.
|
|
698
|
-
this.
|
|
699
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
911
|
+
if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
|
|
819
912
|
return;
|
|
820
|
-
|
|
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
|
-
|
|
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 (
|
|
937
|
+
if (hlIdx === 0)
|
|
832
938
|
return;
|
|
833
|
-
|
|
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
|
-
|
|
839
|
-
gl.uniform2f(
|
|
840
|
-
gl.
|
|
841
|
-
gl.
|
|
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
|
-
|
|
844
|
-
gl.
|
|
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.
|
|
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
|
-
//
|
|
862
|
-
|
|
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
|
-
|
|
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 (
|
|
1003
|
+
if (selIdx === 0)
|
|
887
1004
|
return;
|
|
888
|
-
|
|
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
|
-
|
|
894
|
-
gl.uniform2f(
|
|
895
|
-
gl.
|
|
896
|
-
gl.
|
|
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
|
-
|
|
899
|
-
gl.
|
|
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.
|
|
1025
|
+
if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
|
|
911
1026
|
return;
|
|
912
|
-
|
|
913
|
-
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
1027
|
+
// BLEND already enabled by caller
|
|
914
1028
|
gl.useProgram(this.bgProgram);
|
|
915
|
-
|
|
916
|
-
gl.uniform2f(
|
|
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 =
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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 =
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
943
|
-
cursorData =
|
|
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 =
|
|
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.
|
|
950
|
-
gl.bufferData(gl.ARRAY_BUFFER, cursorData, gl.
|
|
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
|
-
|
|
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
|