@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.
- 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 +22 -5
- package/dist/webgl-renderer.d.ts.map +1 -1
- package/dist/webgl-renderer.js +326 -129
- 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
|
// ---------------------------------------------------------------------------
|
|
@@ -19,31 +20,82 @@ const ATTR_INVERSE = 0x40;
|
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Color helpers
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Parse any CSS color string to [r, g, b, a] in 0-1 range.
|
|
25
|
+
*
|
|
26
|
+
* Fast path for #rrggbb/#rgb hex (no canvas overhead).
|
|
27
|
+
* All other formats (rgb(), rgba(), hsl(), oklch(), color(), named colors)
|
|
28
|
+
* are resolved by the browser's native CSS engine via a 1x1 canvas.
|
|
29
|
+
*/
|
|
30
|
+
// Singleton canvas context for CSS color resolution (works in main thread
|
|
31
|
+
// and Web Workers via OffscreenCanvas)
|
|
32
|
+
let _colorCtx = null;
|
|
33
|
+
let _colorCtxFailed = false;
|
|
34
|
+
function getColorCtx() {
|
|
35
|
+
if (_colorCtx || _colorCtxFailed)
|
|
36
|
+
return _colorCtx;
|
|
37
|
+
try {
|
|
38
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
39
|
+
_colorCtx = new OffscreenCanvas(1, 1).getContext("2d");
|
|
31
40
|
}
|
|
32
|
-
else if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
else if (typeof document !== "undefined") {
|
|
42
|
+
const c = document.createElement("canvas");
|
|
43
|
+
c.width = 1;
|
|
44
|
+
c.height = 1;
|
|
45
|
+
_colorCtx = c.getContext("2d");
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
catch {
|
|
49
|
+
// No canvas available (SSR / test environment)
|
|
50
|
+
}
|
|
51
|
+
if (!_colorCtx)
|
|
52
|
+
_colorCtxFailed = true;
|
|
53
|
+
return _colorCtx;
|
|
54
|
+
}
|
|
55
|
+
export function hexToFloat4(color) {
|
|
56
|
+
// Fast path: #rrggbb (most common — default theme + 256-palette are all hex)
|
|
57
|
+
if (color.length === 7 && color.charCodeAt(0) === 0x23 /* # */) {
|
|
58
|
+
return [
|
|
59
|
+
parseInt(color.slice(1, 3), 16) / 255,
|
|
60
|
+
parseInt(color.slice(3, 5), 16) / 255,
|
|
61
|
+
parseInt(color.slice(5, 7), 16) / 255,
|
|
62
|
+
1.0,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
// Fast path: #rgb
|
|
66
|
+
if (color.length === 4 && color.charCodeAt(0) === 0x23) {
|
|
67
|
+
return [
|
|
68
|
+
parseInt(color[1] + color[1], 16) / 255,
|
|
69
|
+
parseInt(color[2] + color[2], 16) / 255,
|
|
70
|
+
parseInt(color[3] + color[3], 16) / 255,
|
|
71
|
+
1.0,
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
// Universal path: let the browser parse any CSS color
|
|
75
|
+
const ctx = getColorCtx();
|
|
76
|
+
if (ctx) {
|
|
77
|
+
try {
|
|
78
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
79
|
+
ctx.fillStyle = "#000";
|
|
80
|
+
ctx.fillStyle = color;
|
|
81
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
82
|
+
const d = ctx.getImageData(0, 0, 1, 1).data;
|
|
83
|
+
const a = d[3] / 255;
|
|
84
|
+
if (a === 0) {
|
|
85
|
+
// Invalid color or fully transparent — return opaque black
|
|
86
|
+
return [0, 0, 0, 1.0];
|
|
87
|
+
}
|
|
88
|
+
// Unpremultiply RGB (getImageData returns premultiplied on some browsers)
|
|
89
|
+
return a >= 1
|
|
90
|
+
? [d[0] / 255, d[1] / 255, d[2] / 255, 1.0]
|
|
91
|
+
: [d[0] / 255 / a, d[1] / 255 / a, d[2] / 255 / a, a];
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Partial canvas implementation (e.g., jsdom) — fall through
|
|
44
95
|
}
|
|
45
96
|
}
|
|
46
|
-
|
|
97
|
+
// No canvas fallback — return black
|
|
98
|
+
return [0, 0, 0, 1.0];
|
|
47
99
|
}
|
|
48
100
|
/** Build a glyph cache key from codepoint and style flags. */
|
|
49
101
|
export function glyphCacheKey(codepoint, bold, italic) {
|
|
@@ -361,6 +413,7 @@ export class WebGLRenderer {
|
|
|
361
413
|
cursor = null;
|
|
362
414
|
cellWidth = 0;
|
|
363
415
|
cellHeight = 0;
|
|
416
|
+
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: read via destructuring in render()
|
|
364
417
|
baselineOffset = 0;
|
|
365
418
|
fontSize;
|
|
366
419
|
fontFamily;
|
|
@@ -380,15 +433,36 @@ export class WebGLRenderer {
|
|
|
380
433
|
glyphProgram = null;
|
|
381
434
|
quadVBO = null;
|
|
382
435
|
quadEBO = null;
|
|
383
|
-
|
|
384
|
-
|
|
436
|
+
// Double-buffered instance VBOs to avoid GPU read/write conflicts
|
|
437
|
+
bgInstanceVBOs = [null, null];
|
|
438
|
+
glyphInstanceVBOs = [null, null];
|
|
439
|
+
activeBufferIdx = 0;
|
|
440
|
+
// Dedicated overlay VBO for cursor/selection/highlights so we don't
|
|
441
|
+
// overwrite the active bg VBO and neutralize double-buffering.
|
|
442
|
+
overlayVBO = null;
|
|
385
443
|
bgVAO = null;
|
|
386
444
|
glyphVAO = null;
|
|
445
|
+
// Cached uniform locations (populated in initGLResources)
|
|
446
|
+
bgResolutionLoc = null;
|
|
447
|
+
bgCellSizeLoc = null;
|
|
448
|
+
glyphResolutionLoc = null;
|
|
449
|
+
glyphCellSizeLoc = null;
|
|
450
|
+
glyphAtlasLoc = null;
|
|
387
451
|
// Instance data (CPU side)
|
|
388
452
|
bgInstances;
|
|
389
453
|
glyphInstances;
|
|
390
454
|
bgCount = 0;
|
|
391
455
|
glyphCount = 0;
|
|
456
|
+
// Per-row dirty tracking for incremental instance rebuilds
|
|
457
|
+
rowBgOffsets = []; // starting bgCount index per row
|
|
458
|
+
rowBgCounts = []; // number of bg instances per row
|
|
459
|
+
rowGlyphOffsets = []; // starting glyphCount index per row
|
|
460
|
+
rowGlyphCounts = []; // number of glyph instances per row
|
|
461
|
+
hasRenderedOnce = false;
|
|
462
|
+
// Pre-allocated overlay buffers (reused each frame)
|
|
463
|
+
cursorData = new Float32Array(BG_INSTANCE_FLOATS);
|
|
464
|
+
selBuffer = new Float32Array(256 * BG_INSTANCE_FLOATS);
|
|
465
|
+
hlBuffer = new Float32Array(256 * BG_INSTANCE_FLOATS);
|
|
392
466
|
// Glyph atlas
|
|
393
467
|
atlas;
|
|
394
468
|
// Palette as float arrays (cached for performance)
|
|
@@ -421,6 +495,7 @@ export class WebGLRenderer {
|
|
|
421
495
|
this.canvas = canvas;
|
|
422
496
|
this.grid = grid;
|
|
423
497
|
this.cursor = cursor;
|
|
498
|
+
this.hasRenderedOnce = false;
|
|
424
499
|
// Get WebGL2 context
|
|
425
500
|
this.gl = canvas.getContext("webgl2", {
|
|
426
501
|
alpha: false,
|
|
@@ -482,10 +557,38 @@ export class WebGLRenderer {
|
|
|
482
557
|
if (!anyDirty) {
|
|
483
558
|
return;
|
|
484
559
|
}
|
|
485
|
-
//
|
|
486
|
-
this.
|
|
487
|
-
|
|
560
|
+
// Flip active double-buffer index
|
|
561
|
+
this.activeBufferIdx = 1 - this.activeBufferIdx;
|
|
562
|
+
// Incremental rebuild — only re-pack dirty rows
|
|
563
|
+
// On first render or if grid dimensions changed, initialize per-row tracking
|
|
564
|
+
if (!this.hasRenderedOnce || this.rowBgOffsets.length !== rows) {
|
|
565
|
+
this.rowBgOffsets = new Array(rows).fill(0);
|
|
566
|
+
this.rowBgCounts = new Array(rows).fill(0);
|
|
567
|
+
this.rowGlyphOffsets = new Array(rows).fill(0);
|
|
568
|
+
this.rowGlyphCounts = new Array(rows).fill(0);
|
|
569
|
+
// Compute fixed offsets: each row has exactly `cols` bg instances
|
|
570
|
+
// Glyph offsets are variable, so on first pass we do a full rebuild
|
|
571
|
+
let bgOff = 0;
|
|
572
|
+
let glyphOff = 0;
|
|
573
|
+
for (let r = 0; r < rows; r++) {
|
|
574
|
+
this.rowBgOffsets[r] = bgOff;
|
|
575
|
+
this.rowBgCounts[r] = cols; // one bg instance per cell
|
|
576
|
+
bgOff += cols;
|
|
577
|
+
// For glyphs, allocate max possible (cols) per row on first pass
|
|
578
|
+
this.rowGlyphOffsets[r] = glyphOff;
|
|
579
|
+
this.rowGlyphCounts[r] = 0;
|
|
580
|
+
glyphOff += cols;
|
|
581
|
+
}
|
|
582
|
+
this.bgCount = bgOff;
|
|
583
|
+
this.glyphCount = 0; // will be summed below
|
|
584
|
+
}
|
|
488
585
|
for (let row = 0; row < rows; row++) {
|
|
586
|
+
// Skip non-dirty rows — their data persists in the arrays
|
|
587
|
+
if (!grid.isDirty(row))
|
|
588
|
+
continue;
|
|
589
|
+
const bgBase = this.rowBgOffsets[row] * BG_INSTANCE_FLOATS;
|
|
590
|
+
const glyphBase = this.rowGlyphOffsets[row] * GLYPH_INSTANCE_FLOATS;
|
|
591
|
+
let rowGlyphCount = 0;
|
|
489
592
|
for (let col = 0; col < cols; col++) {
|
|
490
593
|
const codepoint = grid.getCodepoint(row, col);
|
|
491
594
|
const fgIdx = grid.getFgIndex(row, col);
|
|
@@ -493,9 +596,8 @@ export class WebGLRenderer {
|
|
|
493
596
|
const attrs = grid.getAttrs(row, col);
|
|
494
597
|
const fgIsRGB = grid.isFgRGB(row, col);
|
|
495
598
|
const bgIsRGB = grid.isBgRGB(row, col);
|
|
496
|
-
|
|
497
|
-
let
|
|
498
|
-
let bg = this.resolveColorFloat(bgIdx, bgIsRGB, grid, col, false);
|
|
599
|
+
let fg = resolveColorFloat(fgIdx, fgIsRGB, grid, col, true, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
|
|
600
|
+
let bg = resolveColorFloat(bgIdx, bgIsRGB, grid, col, false, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
|
|
499
601
|
// Handle inverse
|
|
500
602
|
if (attrs & ATTR_INVERSE) {
|
|
501
603
|
const tmp = fg;
|
|
@@ -503,23 +605,39 @@ export class WebGLRenderer {
|
|
|
503
605
|
bg = tmp;
|
|
504
606
|
}
|
|
505
607
|
// Background instance — emit for all cells to paint default bg too
|
|
506
|
-
packBgInstance(this.bgInstances,
|
|
507
|
-
this.bgCount++;
|
|
608
|
+
packBgInstance(this.bgInstances, bgBase + col * BG_INSTANCE_FLOATS, col, row, bg[0], bg[1], bg[2], bg[3]);
|
|
508
609
|
// Glyph instance — skip spaces and control chars
|
|
509
610
|
if (codepoint > 0x20) {
|
|
510
611
|
const bold = !!(attrs & ATTR_BOLD);
|
|
511
612
|
const italic = !!(attrs & ATTR_ITALIC);
|
|
512
613
|
const glyph = this.atlas.getGlyph(codepoint, bold, italic);
|
|
513
614
|
if (glyph) {
|
|
514
|
-
const glyphPw = isWide ? glyph.pw : glyph.pw;
|
|
515
615
|
const glyphPh = glyph.ph;
|
|
516
|
-
packGlyphInstance(this.glyphInstances,
|
|
517
|
-
|
|
616
|
+
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);
|
|
617
|
+
rowGlyphCount++;
|
|
518
618
|
}
|
|
519
619
|
}
|
|
520
620
|
}
|
|
621
|
+
// Zero out remaining glyph slots for this row (if fewer glyphs than last time)
|
|
622
|
+
const maxGlyphSlots = cols;
|
|
623
|
+
for (let i = rowGlyphCount; i < this.rowGlyphCounts[row]; i++) {
|
|
624
|
+
// Zero the codepoint/color to make invisible (alpha=0 effectively)
|
|
625
|
+
const off = glyphBase + i * GLYPH_INSTANCE_FLOATS;
|
|
626
|
+
for (let j = 0; j < GLYPH_INSTANCE_FLOATS; j++) {
|
|
627
|
+
this.glyphInstances[off + j] = 0;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Keep max of old and new count to ensure we still upload zeroed slots
|
|
631
|
+
if (rowGlyphCount > maxGlyphSlots)
|
|
632
|
+
rowGlyphCount = maxGlyphSlots;
|
|
633
|
+
this.rowGlyphCounts[row] = rowGlyphCount;
|
|
521
634
|
grid.clearDirty(row);
|
|
522
635
|
}
|
|
636
|
+
this.hasRenderedOnce = true;
|
|
637
|
+
// Both bg and glyph instance arrays are sized for rows * cols slots;
|
|
638
|
+
// data is packed per-row at fixed offsets so we always upload the full region.
|
|
639
|
+
this.bgCount = rows * cols;
|
|
640
|
+
this.glyphCount = rows * cols;
|
|
523
641
|
// Upload atlas if dirty
|
|
524
642
|
this.atlas.upload(gl);
|
|
525
643
|
// Set up GL state
|
|
@@ -530,39 +648,54 @@ export class WebGLRenderer {
|
|
|
530
648
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
531
649
|
const cellW = this.cellWidth * this.dpr;
|
|
532
650
|
const cellH = this.cellHeight * this.dpr;
|
|
651
|
+
const _FLOAT = 4;
|
|
652
|
+
const activeBgVBO = this.bgInstanceVBOs[this.activeBufferIdx];
|
|
653
|
+
const activeGlyphVBO = this.glyphInstanceVBOs[this.activeBufferIdx];
|
|
533
654
|
// --- Background pass ---
|
|
534
|
-
if (this.bgCount > 0 && this.bgProgram && this.bgVAO &&
|
|
655
|
+
if (this.bgCount > 0 && this.bgProgram && this.bgVAO && activeBgVBO) {
|
|
535
656
|
gl.useProgram(this.bgProgram);
|
|
536
|
-
|
|
537
|
-
gl.uniform2f(
|
|
538
|
-
gl.
|
|
539
|
-
|
|
657
|
+
// Use cached uniform locations
|
|
658
|
+
gl.uniform2f(this.bgResolutionLoc, canvasWidth, canvasHeight);
|
|
659
|
+
gl.uniform2f(this.bgCellSizeLoc, cellW, cellH);
|
|
660
|
+
// Bind active double-buffered VBO and re-setup instance attrib pointers
|
|
661
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, activeBgVBO);
|
|
662
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.bgInstances.subarray(0, this.bgCount * BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
540
663
|
gl.bindVertexArray(this.bgVAO);
|
|
664
|
+
// Rebind instance attribs to the active VBO (VAO captured the old one)
|
|
665
|
+
this.rebindBgInstanceAttribs(gl, activeBgVBO);
|
|
541
666
|
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, this.bgCount);
|
|
542
667
|
}
|
|
543
668
|
// --- Glyph pass ---
|
|
544
|
-
if (this.glyphCount > 0 && this.glyphProgram && this.glyphVAO &&
|
|
669
|
+
if (this.glyphCount > 0 && this.glyphProgram && this.glyphVAO && activeGlyphVBO) {
|
|
545
670
|
gl.enable(gl.BLEND);
|
|
546
671
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
547
672
|
gl.useProgram(this.glyphProgram);
|
|
548
|
-
|
|
549
|
-
gl.uniform2f(
|
|
673
|
+
// Use cached uniform locations
|
|
674
|
+
gl.uniform2f(this.glyphResolutionLoc, canvasWidth, canvasHeight);
|
|
675
|
+
gl.uniform2f(this.glyphCellSizeLoc, cellW, cellH);
|
|
550
676
|
// Bind atlas texture
|
|
551
677
|
gl.activeTexture(gl.TEXTURE0);
|
|
552
678
|
gl.bindTexture(gl.TEXTURE_2D, this.atlas.getTexture());
|
|
553
|
-
gl.uniform1i(
|
|
554
|
-
|
|
555
|
-
gl.
|
|
679
|
+
gl.uniform1i(this.glyphAtlasLoc, 0);
|
|
680
|
+
// Bind active double-buffered VBO
|
|
681
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, activeGlyphVBO);
|
|
682
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.glyphInstances.subarray(0, this.glyphCount * GLYPH_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
556
683
|
gl.bindVertexArray(this.glyphVAO);
|
|
684
|
+
// Rebind instance attribs to the active VBO
|
|
685
|
+
this.rebindGlyphInstanceAttribs(gl, activeGlyphVBO);
|
|
557
686
|
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, this.glyphCount);
|
|
558
687
|
gl.disable(gl.BLEND);
|
|
559
688
|
}
|
|
689
|
+
// Enable BLEND once for all overlay passes
|
|
690
|
+
gl.enable(gl.BLEND);
|
|
691
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
560
692
|
// --- Highlights (search results) ---
|
|
561
693
|
this.drawHighlights();
|
|
562
694
|
// --- Selection overlay ---
|
|
563
695
|
this.drawSelection();
|
|
564
696
|
// --- Cursor ---
|
|
565
697
|
this.drawCursor();
|
|
698
|
+
gl.disable(gl.BLEND);
|
|
566
699
|
gl.bindVertexArray(null);
|
|
567
700
|
}
|
|
568
701
|
resize(_cols, _rows) {
|
|
@@ -631,10 +764,16 @@ export class WebGLRenderer {
|
|
|
631
764
|
gl.deleteBuffer(this.quadVBO);
|
|
632
765
|
if (this.quadEBO)
|
|
633
766
|
gl.deleteBuffer(this.quadEBO);
|
|
634
|
-
if (this.
|
|
635
|
-
gl.deleteBuffer(this.
|
|
636
|
-
if (this.
|
|
637
|
-
gl.deleteBuffer(this.
|
|
767
|
+
if (this.bgInstanceVBOs[0])
|
|
768
|
+
gl.deleteBuffer(this.bgInstanceVBOs[0]);
|
|
769
|
+
if (this.bgInstanceVBOs[1])
|
|
770
|
+
gl.deleteBuffer(this.bgInstanceVBOs[1]);
|
|
771
|
+
if (this.glyphInstanceVBOs[0])
|
|
772
|
+
gl.deleteBuffer(this.glyphInstanceVBOs[0]);
|
|
773
|
+
if (this.glyphInstanceVBOs[1])
|
|
774
|
+
gl.deleteBuffer(this.glyphInstanceVBOs[1]);
|
|
775
|
+
if (this.overlayVBO)
|
|
776
|
+
gl.deleteBuffer(this.overlayVBO);
|
|
638
777
|
if (this.bgVAO)
|
|
639
778
|
gl.deleteVertexArray(this.bgVAO);
|
|
640
779
|
if (this.glyphVAO)
|
|
@@ -693,19 +832,40 @@ export class WebGLRenderer {
|
|
|
693
832
|
this.quadEBO = gl.createBuffer();
|
|
694
833
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
695
834
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices, gl.STATIC_DRAW);
|
|
696
|
-
//
|
|
697
|
-
this.
|
|
698
|
-
this.
|
|
699
|
-
//
|
|
835
|
+
// Double-buffered instance VBOs
|
|
836
|
+
this.bgInstanceVBOs = [gl.createBuffer(), gl.createBuffer()];
|
|
837
|
+
this.glyphInstanceVBOs = [gl.createBuffer(), gl.createBuffer()];
|
|
838
|
+
// Dedicated overlay VBO for cursor/selection/highlights
|
|
839
|
+
this.overlayVBO = gl.createBuffer();
|
|
840
|
+
// Set up background VAO (quad + EBO only; instance buffer bound per-frame)
|
|
700
841
|
this.bgVAO = gl.createVertexArray();
|
|
701
842
|
gl.bindVertexArray(this.bgVAO);
|
|
702
843
|
this.setupBgVAO(gl);
|
|
703
844
|
gl.bindVertexArray(null);
|
|
704
|
-
// Set up glyph VAO
|
|
845
|
+
// Set up glyph VAO (quad + EBO only; instance buffer bound per-frame)
|
|
705
846
|
this.glyphVAO = gl.createVertexArray();
|
|
706
847
|
gl.bindVertexArray(this.glyphVAO);
|
|
707
848
|
this.setupGlyphVAO(gl);
|
|
708
849
|
gl.bindVertexArray(null);
|
|
850
|
+
// Cache all uniform locations after programs are compiled
|
|
851
|
+
this.bgResolutionLoc = gl.getUniformLocation(this.bgProgram, "u_resolution");
|
|
852
|
+
this.bgCellSizeLoc = gl.getUniformLocation(this.bgProgram, "u_cellSize");
|
|
853
|
+
this.glyphResolutionLoc = gl.getUniformLocation(this.glyphProgram, "u_resolution");
|
|
854
|
+
this.glyphCellSizeLoc = gl.getUniformLocation(this.glyphProgram, "u_cellSize");
|
|
855
|
+
this.glyphAtlasLoc = gl.getUniformLocation(this.glyphProgram, "u_atlas");
|
|
856
|
+
// Cache attribute locations
|
|
857
|
+
this.bgAttribLocs = {
|
|
858
|
+
cellPos: gl.getAttribLocation(this.bgProgram, "a_cellPos"),
|
|
859
|
+
color: gl.getAttribLocation(this.bgProgram, "a_color"),
|
|
860
|
+
};
|
|
861
|
+
this.glyphAttribLocs = {
|
|
862
|
+
cellPos: gl.getAttribLocation(this.glyphProgram, "a_cellPos"),
|
|
863
|
+
color: gl.getAttribLocation(this.glyphProgram, "a_color"),
|
|
864
|
+
texCoord: gl.getAttribLocation(this.glyphProgram, "a_texCoord"),
|
|
865
|
+
glyphSize: gl.getAttribLocation(this.glyphProgram, "a_glyphSize"),
|
|
866
|
+
};
|
|
867
|
+
// Reset dirty-row tracking state on GL reinit
|
|
868
|
+
this.hasRenderedOnce = false;
|
|
709
869
|
// Recreate atlas texture
|
|
710
870
|
this.atlas.recreateTexture();
|
|
711
871
|
}
|
|
@@ -721,8 +881,8 @@ export class WebGLRenderer {
|
|
|
721
881
|
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
|
722
882
|
// Element buffer
|
|
723
883
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
724
|
-
// Instance data
|
|
725
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.
|
|
884
|
+
// Instance data — bind initial buffer; will be rebound per-frame for double buffering
|
|
885
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBOs[0]);
|
|
726
886
|
const stride = BG_INSTANCE_FLOATS * FLOAT;
|
|
727
887
|
const aCellPos = gl.getAttribLocation(program, "a_cellPos");
|
|
728
888
|
gl.enableVertexAttribArray(aCellPos);
|
|
@@ -745,8 +905,8 @@ export class WebGLRenderer {
|
|
|
745
905
|
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
|
746
906
|
// Element buffer
|
|
747
907
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
748
|
-
// Instance data
|
|
749
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.
|
|
908
|
+
// Instance data — bind initial buffer; will be rebound per-frame for double buffering
|
|
909
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBOs[0]);
|
|
750
910
|
const stride = GLYPH_INSTANCE_FLOATS * FLOAT;
|
|
751
911
|
const aCellPos = gl.getAttribLocation(program, "a_cellPos");
|
|
752
912
|
gl.enableVertexAttribArray(aCellPos);
|
|
@@ -776,9 +936,11 @@ export class WebGLRenderer {
|
|
|
776
936
|
const neededGlyph = totalCells * GLYPH_INSTANCE_FLOATS;
|
|
777
937
|
if (this.bgInstances.length < neededBg) {
|
|
778
938
|
this.bgInstances = new Float32Array(neededBg);
|
|
939
|
+
this.hasRenderedOnce = false; // force full rebuild on resize
|
|
779
940
|
}
|
|
780
941
|
if (this.glyphInstances.length < neededGlyph) {
|
|
781
942
|
this.glyphInstances = new Float32Array(neededGlyph);
|
|
943
|
+
this.hasRenderedOnce = false;
|
|
782
944
|
}
|
|
783
945
|
}
|
|
784
946
|
// -----------------------------------------------------------------------
|
|
@@ -790,24 +952,6 @@ export class WebGLRenderer {
|
|
|
790
952
|
this.themeBgFloat = hexToFloat4(this.theme.background);
|
|
791
953
|
this.themeCursorFloat = hexToFloat4(this.theme.cursor);
|
|
792
954
|
}
|
|
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
955
|
// -----------------------------------------------------------------------
|
|
812
956
|
// Cursor
|
|
813
957
|
// -----------------------------------------------------------------------
|
|
@@ -815,33 +959,44 @@ export class WebGLRenderer {
|
|
|
815
959
|
if (!this.gl || !this.highlights.length)
|
|
816
960
|
return;
|
|
817
961
|
const gl = this.gl;
|
|
818
|
-
if (!this.bgProgram || !this.bgVAO || !this.
|
|
962
|
+
if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
|
|
819
963
|
return;
|
|
820
|
-
|
|
964
|
+
// Pack into pre-allocated hlBuffer, growing only if needed
|
|
965
|
+
let hlIdx = 0;
|
|
821
966
|
for (const hl of this.highlights) {
|
|
822
|
-
// Current match: orange, other matches: semi-transparent yellow
|
|
823
967
|
const r = hl.isCurrent ? 1.0 : 1.0;
|
|
824
968
|
const g = hl.isCurrent ? 0.647 : 1.0;
|
|
825
969
|
const b = hl.isCurrent ? 0.0 : 0.0;
|
|
826
970
|
const a = hl.isCurrent ? 0.5 : 0.3;
|
|
827
971
|
for (let col = hl.startCol; col <= hl.endCol; col++) {
|
|
828
|
-
|
|
972
|
+
const needed = (hlIdx + 1) * BG_INSTANCE_FLOATS;
|
|
973
|
+
if (needed > this.hlBuffer.length) {
|
|
974
|
+
const newBuf = new Float32Array(this.hlBuffer.length * 2);
|
|
975
|
+
newBuf.set(this.hlBuffer);
|
|
976
|
+
this.hlBuffer = newBuf;
|
|
977
|
+
}
|
|
978
|
+
const off = hlIdx * BG_INSTANCE_FLOATS;
|
|
979
|
+
this.hlBuffer[off] = col;
|
|
980
|
+
this.hlBuffer[off + 1] = hl.row;
|
|
981
|
+
this.hlBuffer[off + 2] = r;
|
|
982
|
+
this.hlBuffer[off + 3] = g;
|
|
983
|
+
this.hlBuffer[off + 4] = b;
|
|
984
|
+
this.hlBuffer[off + 5] = a;
|
|
985
|
+
hlIdx++;
|
|
829
986
|
}
|
|
830
987
|
}
|
|
831
|
-
if (
|
|
988
|
+
if (hlIdx === 0)
|
|
832
989
|
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);
|
|
990
|
+
// BLEND already enabled by caller
|
|
837
991
|
gl.useProgram(this.bgProgram);
|
|
838
|
-
|
|
839
|
-
gl.uniform2f(
|
|
840
|
-
gl.
|
|
841
|
-
gl.
|
|
992
|
+
// Use cached uniform locations
|
|
993
|
+
gl.uniform2f(this.bgResolutionLoc, this.canvas?.width ?? 0, this.canvas?.height ?? 0);
|
|
994
|
+
gl.uniform2f(this.bgCellSizeLoc, this.cellWidth * this.dpr, this.cellHeight * this.dpr);
|
|
995
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayVBO);
|
|
996
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.hlBuffer.subarray(0, hlIdx * BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
842
997
|
gl.bindVertexArray(this.bgVAO);
|
|
843
|
-
|
|
844
|
-
gl.
|
|
998
|
+
this.rebindBgInstanceAttribs(gl, this.overlayVBO);
|
|
999
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, hlIdx);
|
|
845
1000
|
}
|
|
846
1001
|
drawSelection() {
|
|
847
1002
|
if (!this.gl || !this.grid || !this.selection)
|
|
@@ -854,12 +1009,12 @@ export class WebGLRenderer {
|
|
|
854
1009
|
// Skip if selection is empty (same cell)
|
|
855
1010
|
if (sr === er && sel.startCol === sel.endCol)
|
|
856
1011
|
return;
|
|
857
|
-
if (!this.bgProgram || !this.bgVAO || !this.
|
|
1012
|
+
if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
|
|
858
1013
|
return;
|
|
859
1014
|
// Parse the selection background color
|
|
860
1015
|
const selColor = hexToFloat4(this.theme.selectionBackground);
|
|
861
|
-
//
|
|
862
|
-
|
|
1016
|
+
// Pack into pre-allocated selBuffer, growing only if needed
|
|
1017
|
+
let selIdx = 0;
|
|
863
1018
|
for (let row = sr; row <= er; row++) {
|
|
864
1019
|
let colStart;
|
|
865
1020
|
let colEnd;
|
|
@@ -880,23 +1035,34 @@ export class WebGLRenderer {
|
|
|
880
1035
|
colEnd = grid.cols - 1;
|
|
881
1036
|
}
|
|
882
1037
|
for (let col = colStart; col <= colEnd; col++) {
|
|
883
|
-
|
|
1038
|
+
const needed = (selIdx + 1) * BG_INSTANCE_FLOATS;
|
|
1039
|
+
if (needed > this.selBuffer.length) {
|
|
1040
|
+
const newBuf = new Float32Array(this.selBuffer.length * 2);
|
|
1041
|
+
newBuf.set(this.selBuffer);
|
|
1042
|
+
this.selBuffer = newBuf;
|
|
1043
|
+
}
|
|
1044
|
+
const off = selIdx * BG_INSTANCE_FLOATS;
|
|
1045
|
+
this.selBuffer[off] = col;
|
|
1046
|
+
this.selBuffer[off + 1] = row;
|
|
1047
|
+
this.selBuffer[off + 2] = selColor[0];
|
|
1048
|
+
this.selBuffer[off + 3] = selColor[1];
|
|
1049
|
+
this.selBuffer[off + 4] = selColor[2];
|
|
1050
|
+
this.selBuffer[off + 5] = 0.5;
|
|
1051
|
+
selIdx++;
|
|
884
1052
|
}
|
|
885
1053
|
}
|
|
886
|
-
if (
|
|
1054
|
+
if (selIdx === 0)
|
|
887
1055
|
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);
|
|
1056
|
+
// BLEND already enabled by caller
|
|
892
1057
|
gl.useProgram(this.bgProgram);
|
|
893
|
-
|
|
894
|
-
gl.uniform2f(
|
|
895
|
-
gl.
|
|
896
|
-
gl.
|
|
1058
|
+
// Use cached uniform locations
|
|
1059
|
+
gl.uniform2f(this.bgResolutionLoc, this.canvas?.width ?? 0, this.canvas?.height ?? 0);
|
|
1060
|
+
gl.uniform2f(this.bgCellSizeLoc, this.cellWidth * this.dpr, this.cellHeight * this.dpr);
|
|
1061
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayVBO);
|
|
1062
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.selBuffer.subarray(0, selIdx * BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
|
|
897
1063
|
gl.bindVertexArray(this.bgVAO);
|
|
898
|
-
|
|
899
|
-
gl.
|
|
1064
|
+
this.rebindBgInstanceAttribs(gl, this.overlayVBO);
|
|
1065
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, selIdx);
|
|
900
1066
|
}
|
|
901
1067
|
drawCursor() {
|
|
902
1068
|
if (!this.gl || !this.cursor || !this.cursor.visible)
|
|
@@ -907,50 +1073,81 @@ export class WebGLRenderer {
|
|
|
907
1073
|
const cellH = this.cellHeight * this.dpr;
|
|
908
1074
|
const cc = this.themeCursorFloat;
|
|
909
1075
|
// Use the bg program to draw a simple colored rect for the cursor
|
|
910
|
-
if (!this.bgProgram || !this.bgVAO || !this.
|
|
1076
|
+
if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
|
|
911
1077
|
return;
|
|
912
|
-
|
|
913
|
-
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
1078
|
+
// BLEND already enabled by caller
|
|
914
1079
|
gl.useProgram(this.bgProgram);
|
|
915
|
-
|
|
916
|
-
gl.uniform2f(
|
|
1080
|
+
// Use cached uniform locations
|
|
1081
|
+
gl.uniform2f(this.bgResolutionLoc, this.canvas?.width ?? 0, this.canvas?.height ?? 0);
|
|
1082
|
+
gl.uniform2f(this.bgCellSizeLoc, cellW, cellH);
|
|
1083
|
+
// Write into pre-allocated cursorData instead of allocating
|
|
917
1084
|
// For bar and underline styles, we draw a thin rect.
|
|
918
1085
|
// We abuse cellPos with fractional values to position correctly.
|
|
919
|
-
let cursorData;
|
|
920
1086
|
switch (cursor.style) {
|
|
921
1087
|
case "block":
|
|
922
|
-
cursorData =
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
0.5, // 50% alpha for block
|
|
929
|
-
]);
|
|
1088
|
+
this.cursorData[0] = cursor.col;
|
|
1089
|
+
this.cursorData[1] = cursor.row;
|
|
1090
|
+
this.cursorData[2] = cc[0];
|
|
1091
|
+
this.cursorData[3] = cc[1];
|
|
1092
|
+
this.cursorData[4] = cc[2];
|
|
1093
|
+
this.cursorData[5] = 0.5; // 50% alpha for block
|
|
930
1094
|
break;
|
|
931
1095
|
case "underline": {
|
|
932
1096
|
// Draw a thin line at the bottom of the cell
|
|
933
|
-
// We position it by adjusting cellPos row to be near bottom
|
|
934
1097
|
const lineH = Math.max(2 * this.dpr, 1);
|
|
935
1098
|
const fractionalRow = cursor.row + (cellH - lineH) / cellH;
|
|
936
|
-
cursorData =
|
|
937
|
-
|
|
938
|
-
|
|
1099
|
+
this.cursorData[0] = cursor.col;
|
|
1100
|
+
this.cursorData[1] = fractionalRow;
|
|
1101
|
+
this.cursorData[2] = cc[0];
|
|
1102
|
+
this.cursorData[3] = cc[1];
|
|
1103
|
+
this.cursorData[4] = cc[2];
|
|
1104
|
+
this.cursorData[5] = cc[3];
|
|
939
1105
|
break;
|
|
940
1106
|
}
|
|
941
1107
|
case "bar": {
|
|
942
|
-
|
|
943
|
-
cursorData =
|
|
1108
|
+
this.cursorData[0] = cursor.col;
|
|
1109
|
+
this.cursorData[1] = cursor.row;
|
|
1110
|
+
this.cursorData[2] = cc[0];
|
|
1111
|
+
this.cursorData[3] = cc[1];
|
|
1112
|
+
this.cursorData[4] = cc[2];
|
|
1113
|
+
this.cursorData[5] = cc[3];
|
|
944
1114
|
break;
|
|
945
1115
|
}
|
|
946
1116
|
default:
|
|
947
|
-
cursorData =
|
|
1117
|
+
this.cursorData[0] = cursor.col;
|
|
1118
|
+
this.cursorData[1] = cursor.row;
|
|
1119
|
+
this.cursorData[2] = cc[0];
|
|
1120
|
+
this.cursorData[3] = cc[1];
|
|
1121
|
+
this.cursorData[4] = cc[2];
|
|
1122
|
+
this.cursorData[5] = 0.5;
|
|
948
1123
|
}
|
|
949
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.
|
|
950
|
-
gl.bufferData(gl.ARRAY_BUFFER, cursorData, gl.
|
|
1124
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayVBO);
|
|
1125
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.cursorData, gl.STREAM_DRAW);
|
|
951
1126
|
gl.bindVertexArray(this.bgVAO);
|
|
1127
|
+
this.rebindBgInstanceAttribs(gl, this.overlayVBO);
|
|
952
1128
|
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, 1);
|
|
953
|
-
|
|
1129
|
+
}
|
|
1130
|
+
// -----------------------------------------------------------------------
|
|
1131
|
+
// Instance attribute rebinding helpers for double-buffered VBOs
|
|
1132
|
+
// -----------------------------------------------------------------------
|
|
1133
|
+
// Cached attribute locations (populated in initGLResources)
|
|
1134
|
+
bgAttribLocs = { cellPos: -1, color: -1 };
|
|
1135
|
+
glyphAttribLocs = { cellPos: -1, color: -1, texCoord: -1, glyphSize: -1 };
|
|
1136
|
+
rebindBgInstanceAttribs(gl, vbo) {
|
|
1137
|
+
const FLOAT = 4;
|
|
1138
|
+
const stride = BG_INSTANCE_FLOATS * FLOAT;
|
|
1139
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
1140
|
+
gl.vertexAttribPointer(this.bgAttribLocs.cellPos, 2, gl.FLOAT, false, stride, 0);
|
|
1141
|
+
gl.vertexAttribPointer(this.bgAttribLocs.color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
|
|
1142
|
+
}
|
|
1143
|
+
rebindGlyphInstanceAttribs(gl, vbo) {
|
|
1144
|
+
const FLOAT = 4;
|
|
1145
|
+
const stride = GLYPH_INSTANCE_FLOATS * FLOAT;
|
|
1146
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
1147
|
+
gl.vertexAttribPointer(this.glyphAttribLocs.cellPos, 2, gl.FLOAT, false, stride, 0);
|
|
1148
|
+
gl.vertexAttribPointer(this.glyphAttribLocs.color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
|
|
1149
|
+
gl.vertexAttribPointer(this.glyphAttribLocs.texCoord, 4, gl.FLOAT, false, stride, 6 * FLOAT);
|
|
1150
|
+
gl.vertexAttribPointer(this.glyphAttribLocs.glyphSize, 2, gl.FLOAT, false, stride, 10 * FLOAT);
|
|
954
1151
|
}
|
|
955
1152
|
// -----------------------------------------------------------------------
|
|
956
1153
|
// Internal helpers
|