@vibecook/ghosttea-react 0.12.0 → 0.13.0
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/README.md +59 -4
- package/dist/TerminalSurface.d.ts +53 -0
- package/dist/TerminalSurface.d.ts.map +1 -1
- package/dist/TerminalSurface.js +76 -9
- package/dist/TerminalSurface.js.map +1 -1
- package/dist/appearance/AppearanceSettings.d.ts +2 -0
- package/dist/appearance/AppearanceSettings.d.ts.map +1 -1
- package/dist/appearance/AppearanceSettings.js +12 -2
- package/dist/appearance/AppearanceSettings.js.map +1 -1
- package/dist/appearance/types.d.ts +4 -0
- package/dist/appearance/types.d.ts.map +1 -1
- package/dist/catalog-budget.d.ts +2 -2
- package/dist/catalog-budget.d.ts.map +1 -1
- package/dist/catalog-budget.js +2 -2
- package/dist/catalog-budget.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/renderers/glyph-atlas.d.ts +43 -0
- package/dist/renderers/glyph-atlas.d.ts.map +1 -0
- package/dist/renderers/glyph-atlas.js +175 -0
- package/dist/renderers/glyph-atlas.js.map +1 -0
- package/dist/renderers/light-adaptation.d.ts +36 -0
- package/dist/renderers/light-adaptation.d.ts.map +1 -0
- package/dist/renderers/light-adaptation.js +332 -0
- package/dist/renderers/light-adaptation.js.map +1 -0
- package/dist/renderers/types.d.ts +21 -0
- package/dist/renderers/types.d.ts.map +1 -1
- package/dist/renderers/types.js.map +1 -1
- package/dist/renderers/webgpu-renderer.d.ts +4 -0
- package/dist/renderers/webgpu-renderer.d.ts.map +1 -1
- package/dist/renderers/webgpu-renderer.js +321 -237
- package/dist/renderers/webgpu-renderer.js.map +1 -1
- package/dist/routed-control.d.ts +26 -2
- package/dist/routed-control.d.ts.map +1 -1
- package/dist/routed-control.js +145 -27
- package/dist/routed-control.js.map +1 -1
- package/dist/routed-frames.d.ts +1 -1
- package/dist/routed-frames.d.ts.map +1 -1
- package/dist/routed-frames.js +3 -7
- package/dist/routed-frames.js.map +1 -1
- package/dist/row-damage.d.ts +21 -0
- package/dist/row-damage.d.ts.map +1 -0
- package/dist/row-damage.js +76 -0
- package/dist/row-damage.js.map +1 -0
- package/dist/runtime.d.ts +92 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +439 -43
- package/dist/runtime.js.map +1 -1
- package/dist/styles.css +11 -0
- package/dist/terminal-render.worker.js +908 -288
- package/dist/terminal-render.worker.js.map +4 -4
- package/dist/worker-messages.d.ts +22 -1
- package/dist/worker-messages.d.ts.map +1 -1
- package/dist/workspace/Workspace.d.ts +15 -2
- package/dist/workspace/Workspace.d.ts.map +1 -1
- package/dist/workspace/Workspace.js +36 -12
- package/dist/workspace/Workspace.js.map +1 -1
- package/dist/workspace/pane-layout.d.ts +3 -1
- package/dist/workspace/pane-layout.d.ts.map +1 -1
- package/dist/workspace/pane-layout.js +16 -4
- package/dist/workspace/pane-layout.js.map +1 -1
- package/package.json +4 -4
|
@@ -6,7 +6,8 @@ import { graphemeCellWidth, splitGraphemes } from "../cell-width.js";
|
|
|
6
6
|
import { backgroundRunBounds } from "./background-geometry.js";
|
|
7
7
|
import { rowsForDamage } from "./render-damage.js";
|
|
8
8
|
import { SHADER_EFFECT_WGSL } from "./shader-effects.js";
|
|
9
|
-
|
|
9
|
+
import { createLightAdaptation, darkPaintedBase, ensureContrast } from "./light-adaptation.js";
|
|
10
|
+
import { AtlasBatchFullError, AtlasCapacityError, GlyphAtlasPages } from "./glyph-atlas.js";
|
|
10
11
|
const GEOMETRY_CACHE_LIMIT = 8;
|
|
11
12
|
const FONT_STACK = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
|
|
12
13
|
const EFFECT_MODES = {
|
|
@@ -180,74 +181,29 @@ class DynamicVertexBuffer {
|
|
|
180
181
|
}
|
|
181
182
|
class FallbackGlyphAtlas {
|
|
182
183
|
device;
|
|
183
|
-
|
|
184
|
-
#sampler;
|
|
185
|
-
#bindGroup;
|
|
186
|
-
#cache = new Map();
|
|
184
|
+
pages;
|
|
187
185
|
#scratch = new OffscreenCanvas(1, 1);
|
|
188
|
-
#x = 1;
|
|
189
|
-
#y = 1;
|
|
190
|
-
#rowHeight = 0;
|
|
191
186
|
#uploadBytes = 0;
|
|
192
187
|
#uploadCalls = 0;
|
|
193
|
-
#generation = 0;
|
|
194
188
|
constructor(device, bindGroupLayout) {
|
|
195
189
|
this.device = device;
|
|
196
|
-
this
|
|
197
|
-
label: "terminal glyph atlas",
|
|
198
|
-
size: [ATLAS_SIZE, ATLAS_SIZE],
|
|
199
|
-
format: "rgba8unorm",
|
|
200
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
201
|
-
});
|
|
202
|
-
this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
|
|
203
|
-
this.#bindGroup = device.createBindGroup({
|
|
204
|
-
label: "terminal glyph atlas bindings",
|
|
205
|
-
layout: bindGroupLayout,
|
|
206
|
-
entries: [
|
|
207
|
-
{ binding: 0, resource: this.#texture.createView() },
|
|
208
|
-
{ binding: 1, resource: this.#sampler },
|
|
209
|
-
],
|
|
210
|
-
});
|
|
190
|
+
this.pages = new GlyphAtlasPages(device, bindGroupLayout, "terminal diagnostic glyph atlas", "rgba8unorm", 1);
|
|
211
191
|
}
|
|
212
192
|
get bindGroup() {
|
|
213
|
-
return this
|
|
193
|
+
return this.pages.bindGroup;
|
|
214
194
|
}
|
|
215
195
|
get generation() {
|
|
216
|
-
return this
|
|
196
|
+
return this.pages.generation;
|
|
217
197
|
}
|
|
218
198
|
prepare(values, dpr) {
|
|
219
199
|
const scale = Math.max(1, Math.min(3, dpr));
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
this.#y = 1;
|
|
228
|
-
this.#rowHeight = 0;
|
|
229
|
-
if (!this.#fits(all, scale, this.#x, this.#y, this.#rowHeight)) {
|
|
230
|
-
throw new Error("The diagnostic glyph atlas cannot fit the text visible in one frame");
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
#fits(values, scale, startX, startY, startRowHeight) {
|
|
234
|
-
let x = startX;
|
|
235
|
-
let y = startY;
|
|
236
|
-
let rowHeight = startRowHeight;
|
|
237
|
-
for (const value of values) {
|
|
238
|
-
const width = Math.max(1, Math.ceil(CELL_WIDTH * graphemeCellWidth(value) * scale));
|
|
239
|
-
const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
|
|
240
|
-
if (x + width + 1 > ATLAS_SIZE) {
|
|
241
|
-
x = 1;
|
|
242
|
-
y += rowHeight + 1;
|
|
243
|
-
rowHeight = 0;
|
|
244
|
-
}
|
|
245
|
-
if (y + height + 1 > ATLAS_SIZE)
|
|
246
|
-
return false;
|
|
247
|
-
x += width + 1;
|
|
248
|
-
rowHeight = Math.max(rowHeight, height);
|
|
249
|
-
}
|
|
250
|
-
return true;
|
|
200
|
+
this.pages.prepare([...new Set(values)]
|
|
201
|
+
.filter((value) => value.trim().length > 0)
|
|
202
|
+
.map((value) => ({
|
|
203
|
+
key: `${scale.toFixed(2)}:${value}`,
|
|
204
|
+
width: Math.max(1, Math.ceil(CELL_WIDTH * graphemeCellWidth(value) * scale)),
|
|
205
|
+
height: Math.max(1, Math.ceil(LINE_HEIGHT * scale)),
|
|
206
|
+
})));
|
|
251
207
|
}
|
|
252
208
|
glyph(grapheme, dpr) {
|
|
253
209
|
if (grapheme.trim().length === 0)
|
|
@@ -255,19 +211,11 @@ class FallbackGlyphAtlas {
|
|
|
255
211
|
const scale = Math.max(1, Math.min(3, dpr));
|
|
256
212
|
const cells = graphemeCellWidth(grapheme);
|
|
257
213
|
const key = `${scale.toFixed(2)}:${grapheme}`;
|
|
258
|
-
const cached = this
|
|
214
|
+
const cached = this.pages.get(key);
|
|
259
215
|
if (cached)
|
|
260
216
|
return cached;
|
|
261
217
|
const width = Math.max(1, Math.ceil(CELL_WIDTH * cells * scale));
|
|
262
218
|
const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
|
|
263
|
-
if (this.#x + width + 1 > ATLAS_SIZE) {
|
|
264
|
-
this.#x = 1;
|
|
265
|
-
this.#y += this.#rowHeight + 1;
|
|
266
|
-
this.#rowHeight = 0;
|
|
267
|
-
}
|
|
268
|
-
if (this.#y + height + 1 > ATLAS_SIZE) {
|
|
269
|
-
throw new Error("Temporary glyph atlas exhausted");
|
|
270
|
-
}
|
|
271
219
|
this.#scratch.width = width;
|
|
272
220
|
this.#scratch.height = height;
|
|
273
221
|
const context = this.#scratch.getContext("2d", { willReadFrequently: true });
|
|
@@ -279,23 +227,17 @@ class FallbackGlyphAtlas {
|
|
|
279
227
|
context.textBaseline = "top";
|
|
280
228
|
context.fillText(grapheme, 0, Math.max(0, Math.floor((height - 15 * scale) / 2)));
|
|
281
229
|
const image = context.getImageData(0, 0, width, height);
|
|
282
|
-
|
|
230
|
+
const { location, texture, origin } = this.pages.insert(key, width, height, cells);
|
|
231
|
+
this.device.queue.writeTexture({ texture, origin }, image.data, { bytesPerRow: width * 4, rowsPerImage: height }, [
|
|
232
|
+
width,
|
|
233
|
+
height,
|
|
234
|
+
]);
|
|
283
235
|
this.#uploadBytes += image.data.byteLength;
|
|
284
236
|
this.#uploadCalls += 1;
|
|
285
|
-
const location = {
|
|
286
|
-
u0: this.#x / ATLAS_SIZE,
|
|
287
|
-
v0: this.#y / ATLAS_SIZE,
|
|
288
|
-
u1: (this.#x + width) / ATLAS_SIZE,
|
|
289
|
-
v1: (this.#y + height) / ATLAS_SIZE,
|
|
290
|
-
cells,
|
|
291
|
-
};
|
|
292
|
-
this.#cache.set(key, location);
|
|
293
|
-
this.#x += width + 1;
|
|
294
|
-
this.#rowHeight = Math.max(this.#rowHeight, height);
|
|
295
237
|
return location;
|
|
296
238
|
}
|
|
297
239
|
destroy() {
|
|
298
|
-
this
|
|
240
|
+
this.pages.destroy();
|
|
299
241
|
}
|
|
300
242
|
uploadMetrics() {
|
|
301
243
|
return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
|
|
@@ -305,111 +247,44 @@ class NativeGlyphAtlas {
|
|
|
305
247
|
device;
|
|
306
248
|
label;
|
|
307
249
|
format;
|
|
308
|
-
|
|
309
|
-
#sampler;
|
|
310
|
-
#bindGroup;
|
|
311
|
-
#cache = new Map();
|
|
312
|
-
#x = 1;
|
|
313
|
-
#y = 1;
|
|
314
|
-
#rowHeight = 0;
|
|
250
|
+
pages;
|
|
315
251
|
#uploadBytes = 0;
|
|
316
252
|
#uploadCalls = 0;
|
|
317
|
-
#generation = 0;
|
|
318
253
|
constructor(device, bindGroupLayout, label, format) {
|
|
319
254
|
this.device = device;
|
|
320
255
|
this.label = label;
|
|
321
256
|
this.format = format;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
format,
|
|
326
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
327
|
-
});
|
|
328
|
-
this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
|
|
329
|
-
this.#bindGroup = device.createBindGroup({
|
|
330
|
-
label: `${label} bindings`,
|
|
331
|
-
layout: bindGroupLayout,
|
|
332
|
-
entries: [
|
|
333
|
-
{ binding: 0, resource: this.#texture.createView() },
|
|
334
|
-
{ binding: 1, resource: this.#sampler },
|
|
335
|
-
],
|
|
336
|
-
});
|
|
257
|
+
// At most 16 MiB mono + 32 MiB color + 16 MiB diagnostic pages. Empty
|
|
258
|
+
// formats allocate no textures. The ordinary ASCII working set uses 4 MiB.
|
|
259
|
+
this.pages = new GlyphAtlasPages(device, bindGroupLayout, label, format, format === "r8unorm" ? 4 : 2);
|
|
337
260
|
}
|
|
338
261
|
get bindGroup() {
|
|
339
|
-
return this
|
|
262
|
+
return this.pages.bindGroup;
|
|
340
263
|
}
|
|
341
264
|
get generation() {
|
|
342
|
-
return this
|
|
265
|
+
return this.pages.generation;
|
|
343
266
|
}
|
|
344
267
|
prepare(definitions) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
missing.set(definition.id, definition);
|
|
351
|
-
}
|
|
352
|
-
if (this.#fits(missing.values(), this.#x, this.#y, this.#rowHeight))
|
|
353
|
-
return;
|
|
354
|
-
this.#cache.clear();
|
|
355
|
-
this.#generation += 1;
|
|
356
|
-
this.#x = 1;
|
|
357
|
-
this.#y = 1;
|
|
358
|
-
this.#rowHeight = 0;
|
|
359
|
-
if (!this.#fits(all.values(), this.#x, this.#y, this.#rowHeight)) {
|
|
360
|
-
throw new Error(`${this.label} cannot fit the glyphs visible in one frame`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
#fits(definitions, startX, startY, startRowHeight) {
|
|
364
|
-
let x = startX;
|
|
365
|
-
let y = startY;
|
|
366
|
-
let rowHeight = startRowHeight;
|
|
367
|
-
for (const { width, height } of definitions) {
|
|
368
|
-
if (width + 2 > ATLAS_SIZE || height + 2 > ATLAS_SIZE)
|
|
369
|
-
return false;
|
|
370
|
-
if (x + width + 1 > ATLAS_SIZE) {
|
|
371
|
-
x = 1;
|
|
372
|
-
y += rowHeight + 1;
|
|
373
|
-
rowHeight = 0;
|
|
374
|
-
}
|
|
375
|
-
if (y + height + 1 > ATLAS_SIZE)
|
|
376
|
-
return false;
|
|
377
|
-
x += width + 1;
|
|
378
|
-
rowHeight = Math.max(rowHeight, height);
|
|
379
|
-
}
|
|
380
|
-
return true;
|
|
268
|
+
this.pages.prepare(Array.from(definitions, (definition) => ({
|
|
269
|
+
key: definition.id,
|
|
270
|
+
width: definition.width,
|
|
271
|
+
height: definition.height,
|
|
272
|
+
})));
|
|
381
273
|
}
|
|
382
274
|
glyph(definition) {
|
|
383
|
-
const cached = this
|
|
275
|
+
const cached = this.pages.get(definition.id);
|
|
384
276
|
if (cached)
|
|
385
277
|
return cached;
|
|
386
278
|
const { width, height } = definition;
|
|
387
|
-
|
|
388
|
-
this.#x = 1;
|
|
389
|
-
this.#y += this.#rowHeight + 1;
|
|
390
|
-
this.#rowHeight = 0;
|
|
391
|
-
}
|
|
392
|
-
if (this.#y + height + 1 > ATLAS_SIZE)
|
|
393
|
-
throw new Error(`${this.label} exhausted`);
|
|
394
|
-
const pixels = definition.pixels;
|
|
279
|
+
const { location, texture, origin } = this.pages.insert(definition.id, width, height, 0);
|
|
395
280
|
const bytesPerPixel = this.format === "r8unorm" ? 1 : 4;
|
|
396
|
-
this.device.queue.writeTexture({ texture
|
|
397
|
-
this.#uploadBytes += pixels.byteLength;
|
|
281
|
+
this.device.queue.writeTexture({ texture, origin }, definition.pixels, { bytesPerRow: width * bytesPerPixel, rowsPerImage: height }, [width, height]);
|
|
282
|
+
this.#uploadBytes += definition.pixels.byteLength;
|
|
398
283
|
this.#uploadCalls += 1;
|
|
399
|
-
const location = {
|
|
400
|
-
u0: this.#x / ATLAS_SIZE,
|
|
401
|
-
v0: this.#y / ATLAS_SIZE,
|
|
402
|
-
u1: (this.#x + width) / ATLAS_SIZE,
|
|
403
|
-
v1: (this.#y + height) / ATLAS_SIZE,
|
|
404
|
-
cells: 0,
|
|
405
|
-
};
|
|
406
|
-
this.#cache.set(definition.id, location);
|
|
407
|
-
this.#x += width + 1;
|
|
408
|
-
this.#rowHeight = Math.max(this.#rowHeight, height);
|
|
409
284
|
return location;
|
|
410
285
|
}
|
|
411
286
|
destroy() {
|
|
412
|
-
this
|
|
287
|
+
this.pages.destroy();
|
|
413
288
|
}
|
|
414
289
|
uploadMetrics() {
|
|
415
290
|
return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
|
|
@@ -438,7 +313,7 @@ function createCachedVertexBuffer(device, label, data) {
|
|
|
438
313
|
device.queue.writeBuffer(buffer, 0, data);
|
|
439
314
|
return buffer;
|
|
440
315
|
}
|
|
441
|
-
function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
|
|
316
|
+
function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations, adaptationKey) {
|
|
442
317
|
const selection = view.selection
|
|
443
318
|
? `${view.selection.anchor.row},${view.selection.anchor.column},${view.selection.focus.row},${view.selection.focus.column}`
|
|
444
319
|
: "-";
|
|
@@ -457,6 +332,8 @@ function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
|
|
|
457
332
|
...view.theme.selectionForeground,
|
|
458
333
|
].join(","),
|
|
459
334
|
view.theme.backgroundOpacityCells ? "opacity-cells" : "opaque-cells",
|
|
335
|
+
view.theme.minimumContrast ?? 1,
|
|
336
|
+
adaptationKey,
|
|
460
337
|
selection,
|
|
461
338
|
effectiveCursorStyle(view) === CursorStyle.Block ? `${view.cursor.x},${view.cursor.y}` : "-",
|
|
462
339
|
atlasGenerations.join(","),
|
|
@@ -659,22 +536,51 @@ function pushBlockCursorBackground(vertices, view, renderRows, scale, viewportWi
|
|
|
659
536
|
return;
|
|
660
537
|
pushRectangle(vertices, (ORIGIN_X + view.cursor.x * CELL_WIDTH) * scale, (ORIGIN_Y + view.cursor.y * LINE_HEIGHT) * scale, CELL_WIDTH * scale, LINE_HEIGHT * scale, view.theme.cursor, viewportWidth, viewportHeight);
|
|
661
538
|
}
|
|
662
|
-
function resolveStyle(style, theme) {
|
|
539
|
+
function resolveStyle(style, theme, adaptation = null) {
|
|
663
540
|
let foreground = style?.foreground ? rgb(style.foreground) : theme.foreground;
|
|
664
541
|
let background = style?.background ? rgb(style.background) : null;
|
|
665
|
-
|
|
542
|
+
let fill = foreground;
|
|
543
|
+
if (adaptation) {
|
|
544
|
+
// Application colors were designed on the app's dark base; theme-owned
|
|
545
|
+
// defaults already belong to the light theme and pass through.
|
|
546
|
+
const source = { foreground: style?.foreground, background: style?.background };
|
|
547
|
+
if (style?.inverse) {
|
|
548
|
+
background = source.foreground ? adaptation.surface(source.foreground) : theme.foreground;
|
|
549
|
+
foreground = source.background
|
|
550
|
+
? adaptation.ink(source.background, source.foreground ?? adaptation.base, style?.backgroundPalette)
|
|
551
|
+
: theme.background;
|
|
552
|
+
fill = foreground;
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
foreground = source.foreground
|
|
556
|
+
? adaptation.ink(source.foreground, source.background ?? adaptation.base, style?.foregroundPalette)
|
|
557
|
+
: theme.foreground;
|
|
558
|
+
background = source.background ? adaptation.surface(source.background) : null;
|
|
559
|
+
fill = source.foreground ? adaptation.surface(source.foreground) : theme.foreground;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else if (style?.inverse) {
|
|
666
563
|
const originalForeground = foreground;
|
|
667
564
|
foreground = background ?? theme.background;
|
|
668
565
|
background = originalForeground;
|
|
566
|
+
fill = foreground;
|
|
669
567
|
}
|
|
670
568
|
if (background && theme.backgroundOpacityCells) {
|
|
671
569
|
background = [background[0], background[1], background[2], theme.background[3]];
|
|
672
570
|
}
|
|
673
|
-
|
|
571
|
+
// Graphics (box drawing, block elements) and color glyphs keep the raw
|
|
572
|
+
// foreground, as in Ghostty; the backdrop ignores window transparency.
|
|
573
|
+
let text = ensureContrast(foreground, over(background ?? theme.background, theme.background), theme.minimumContrast ?? 1);
|
|
574
|
+
if (style?.faint) {
|
|
674
575
|
foreground = [foreground[0], foreground[1], foreground[2], foreground[3] * 0.55];
|
|
576
|
+
fill = [fill[0], fill[1], fill[2], fill[3] * 0.55];
|
|
577
|
+
text = [text[0], text[1], text[2], text[3] * 0.55];
|
|
578
|
+
}
|
|
675
579
|
return {
|
|
676
580
|
foreground,
|
|
677
581
|
background,
|
|
582
|
+
fill,
|
|
583
|
+
text,
|
|
678
584
|
underline: style?.underline ?? false,
|
|
679
585
|
strikethrough: style?.strikethrough ?? false,
|
|
680
586
|
invisible: style?.invisible ?? false,
|
|
@@ -821,7 +727,10 @@ export class WebGpuTerminalRenderer {
|
|
|
821
727
|
#monoAtlas;
|
|
822
728
|
#colorAtlas;
|
|
823
729
|
#fallbackAtlas;
|
|
730
|
+
#adaptations = new Map();
|
|
824
731
|
#performanceMeasurementEnabled = false;
|
|
732
|
+
#submission = { queueSubmits: 0, surfaceIds: [] };
|
|
733
|
+
#scrollScratch;
|
|
825
734
|
constructor(device, format) {
|
|
826
735
|
this.device = device;
|
|
827
736
|
this.format = format;
|
|
@@ -1020,6 +929,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1020
929
|
geometryCache: new Map(),
|
|
1021
930
|
geometryCandidates: new Map(),
|
|
1022
931
|
sceneValid: false,
|
|
932
|
+
adaptation: null,
|
|
933
|
+
sceneHadSelection: false,
|
|
934
|
+
sceneHadOverhang: false,
|
|
935
|
+
sceneCursorRow: null,
|
|
1023
936
|
};
|
|
1024
937
|
this.#surfaces.set(id, surface);
|
|
1025
938
|
this.#configure(surface);
|
|
@@ -1041,6 +954,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1041
954
|
surface.cursorBuffer.destroy();
|
|
1042
955
|
clearGeometryCache(surface);
|
|
1043
956
|
this.#surfaces.delete(id);
|
|
957
|
+
if (this.#surfaces.size === 0) {
|
|
958
|
+
this.#scrollScratch?.destroy();
|
|
959
|
+
this.#scrollScratch = undefined;
|
|
960
|
+
}
|
|
1044
961
|
}
|
|
1045
962
|
resize(id, size) {
|
|
1046
963
|
const surface = this.#surfaces.get(id);
|
|
@@ -1073,7 +990,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1073
990
|
label: "terminal scene before Ghostty custom shaders",
|
|
1074
991
|
size: [Math.max(1, width), Math.max(1, height)],
|
|
1075
992
|
format: this.format,
|
|
1076
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT |
|
|
993
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT |
|
|
994
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
995
|
+
GPUTextureUsage.COPY_SRC |
|
|
996
|
+
GPUTextureUsage.COPY_DST,
|
|
1077
997
|
});
|
|
1078
998
|
}
|
|
1079
999
|
#createEffectTexture(width, height, suffix) {
|
|
@@ -1190,37 +1110,227 @@ export class WebGpuTerminalRenderer {
|
|
|
1190
1110
|
drawCalls: effectPassCount,
|
|
1191
1111
|
};
|
|
1192
1112
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1113
|
+
/**
|
|
1114
|
+
* Engage, re-key, or release light adaptation for a pane. Any change remaps
|
|
1115
|
+
* every color, so the persistent scene and cached geometry are invalidated.
|
|
1116
|
+
*/
|
|
1117
|
+
#prepareAdaptation(surface, view) {
|
|
1118
|
+
const base = darkPaintedBase(view, surface.adaptation !== null);
|
|
1119
|
+
let next = null;
|
|
1120
|
+
if (base) {
|
|
1121
|
+
const key = `${base.join(",")}|${view.theme.background.join(",")}|${view.theme.foreground.join(",")}`;
|
|
1122
|
+
next = this.#adaptations.get(key) ?? null;
|
|
1123
|
+
if (!next) {
|
|
1124
|
+
next = createLightAdaptation(base, view.theme.background, view.theme.foreground);
|
|
1125
|
+
this.#adaptations.set(key, next);
|
|
1126
|
+
if (this.#adaptations.size > 8)
|
|
1127
|
+
this.#adaptations.delete(this.#adaptations.keys().next().value);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if ((next?.key ?? null) === (surface.adaptation?.key ?? null))
|
|
1196
1131
|
return;
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1132
|
+
surface.adaptation = next;
|
|
1133
|
+
surface.sceneValid = false;
|
|
1134
|
+
clearGeometryCache(surface);
|
|
1135
|
+
}
|
|
1136
|
+
submission() {
|
|
1137
|
+
return this.#submission;
|
|
1138
|
+
}
|
|
1139
|
+
#releaseAtlasBatch() {
|
|
1140
|
+
this.#monoAtlas.pages.releaseBatch();
|
|
1141
|
+
this.#colorAtlas.pages.releaseBatch();
|
|
1142
|
+
this.#fallbackAtlas.pages.releaseBatch();
|
|
1143
|
+
}
|
|
1144
|
+
#prepareAtlases(view, renderRows, dpr) {
|
|
1145
|
+
const mono = new Map();
|
|
1146
|
+
const color = new Map();
|
|
1147
|
+
for (const row of renderRows) {
|
|
1148
|
+
for (const glyph of view.nativeRows[row] ?? []) {
|
|
1149
|
+
const definition = view.glyphDefinitions.get(glyph.glyphId);
|
|
1150
|
+
if (definition)
|
|
1151
|
+
(definition.format === GlyphFormat.Alpha8 ? mono : color).set(definition.id, definition);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
this.#monoAtlas.prepare(mono.values());
|
|
1155
|
+
this.#colorAtlas.prepare(color.values());
|
|
1156
|
+
const fallback = !view.nativeRows.some((row) => row.length > 0);
|
|
1157
|
+
if (fallback) {
|
|
1158
|
+
this.#fallbackAtlas.prepare(renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")), dpr);
|
|
1159
|
+
}
|
|
1160
|
+
// Pin only after every format fits, so a rejected pane cannot occupy the
|
|
1161
|
+
// batch budget with pages that no pending draw actually references.
|
|
1162
|
+
if (mono.size > 0)
|
|
1163
|
+
this.#monoAtlas.pages.pin();
|
|
1164
|
+
if (color.size > 0)
|
|
1165
|
+
this.#colorAtlas.pages.pin();
|
|
1166
|
+
if (fallback && renderRows.length > 0)
|
|
1167
|
+
this.#fallbackAtlas.pages.pin();
|
|
1168
|
+
}
|
|
1169
|
+
render(id, view) {
|
|
1170
|
+
return this.renderBatch([{ id, view }])[0];
|
|
1171
|
+
}
|
|
1172
|
+
#canCopyScroll(surface, view) {
|
|
1173
|
+
const shift = view.damage?.scrollRows ?? 0;
|
|
1174
|
+
const top = ORIGIN_Y * surface.dpr;
|
|
1175
|
+
const line = LINE_HEIGHT * surface.dpr;
|
|
1176
|
+
const count = view.rows.length;
|
|
1177
|
+
return (surface.sceneValid &&
|
|
1178
|
+
!view.damage?.full &&
|
|
1179
|
+
Number.isSafeInteger(shift) &&
|
|
1180
|
+
shift !== 0 &&
|
|
1181
|
+
Math.abs(shift) < count &&
|
|
1182
|
+
!view.selection &&
|
|
1183
|
+
!surface.sceneHadSelection &&
|
|
1184
|
+
Number.isInteger(top) &&
|
|
1185
|
+
Number.isInteger(line) &&
|
|
1186
|
+
top + count * line <= surface.canvas.height);
|
|
1187
|
+
}
|
|
1188
|
+
#prepareScrollScratch(entries) {
|
|
1189
|
+
let width = 0, height = 0;
|
|
1190
|
+
for (const { id, view } of entries) {
|
|
1191
|
+
const surface = this.#surfaces.get(id);
|
|
1192
|
+
if (!surface || !this.#canCopyScroll(surface, view))
|
|
1193
|
+
continue;
|
|
1194
|
+
width = Math.max(width, surface.canvas.width);
|
|
1195
|
+
height = Math.max(height, view.rows.length * LINE_HEIGHT * surface.dpr);
|
|
1196
|
+
}
|
|
1197
|
+
if (width === 0)
|
|
1198
|
+
return false;
|
|
1199
|
+
// One shared staging texture, never one per pane. Fall back to redraw if
|
|
1200
|
+
// the batch's maximum extent would exceed this fixed memory ceiling.
|
|
1201
|
+
if (width * height * 4 > 64 * 1024 * 1024)
|
|
1202
|
+
return false;
|
|
1203
|
+
if (!this.#scrollScratch || this.#scrollScratch.width < width || this.#scrollScratch.height < height) {
|
|
1204
|
+
this.#scrollScratch?.destroy();
|
|
1205
|
+
this.#scrollScratch = this.device.createTexture({
|
|
1206
|
+
label: "terminal shared scroll staging",
|
|
1207
|
+
size: [width, height],
|
|
1208
|
+
format: this.format,
|
|
1209
|
+
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1214
|
+
#copyScroll(surface, view, encoder) {
|
|
1215
|
+
const dy = view.damage.scrollRows * LINE_HEIGHT * surface.dpr;
|
|
1216
|
+
const top = ORIGIN_Y * surface.dpr;
|
|
1217
|
+
const size = [surface.canvas.width, view.rows.length * LINE_HEIGHT * surface.dpr - Math.abs(dy)];
|
|
1218
|
+
// A texture cannot copy overlapping regions to itself. Encoder ordering
|
|
1219
|
+
// lets all panes safely share this scratch texture, even within one batch.
|
|
1220
|
+
encoder.copyTextureToTexture({ texture: surface.sceneTexture, origin: [0, top + Math.max(0, dy)] }, { texture: this.#scrollScratch }, size);
|
|
1221
|
+
encoder.copyTextureToTexture({ texture: this.#scrollScratch }, { texture: surface.sceneTexture, origin: [0, top + Math.max(0, -dy)] }, size);
|
|
1205
1222
|
}
|
|
1206
1223
|
renderBatch(entries) {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1224
|
+
this.#submission = { queueSubmits: 0, surfaceIds: [] };
|
|
1225
|
+
this.#releaseAtlasBatch();
|
|
1226
|
+
const prepared = entries.map(({ id, view }) => {
|
|
1227
|
+
const surface = this.#surfaces.get(id);
|
|
1228
|
+
// Light adaptation first, for the whole batch: a change remaps every color
|
|
1229
|
+
// and invalidates the scene, so every decision below (the overhang
|
|
1230
|
+
// shortcut, the scroll staging, the scroll copy) sees it invalidated.
|
|
1231
|
+
if (surface)
|
|
1232
|
+
this.#prepareAdaptation(surface, view);
|
|
1233
|
+
const overhang = view.damage?.geometryChanged === false && surface?.sceneValid
|
|
1234
|
+
? surface.sceneHadOverhang
|
|
1235
|
+
: view.nativeRows.some((row) => row.some((glyph) => glyph.y < 0 || glyph.y + glyph.height > LINE_HEIGHT));
|
|
1236
|
+
// Row clears cannot reconstruct cross-row alpha composition at the
|
|
1237
|
+
// repair boundary. Preserve those glyphs with a complete scene redraw.
|
|
1238
|
+
if (view.damage &&
|
|
1239
|
+
!view.damage.full &&
|
|
1240
|
+
(view.damage.scrollRows || view.damage.rows.size > 0) &&
|
|
1241
|
+
(overhang || surface?.sceneHadOverhang)) {
|
|
1242
|
+
view = { ...view, damage: { full: true, rows: new Set(), geometryChanged: true } };
|
|
1243
|
+
}
|
|
1244
|
+
return { id, view, overhang };
|
|
1245
|
+
});
|
|
1246
|
+
const canStageScroll = this.#prepareScrollScratch(prepared);
|
|
1247
|
+
let encoder;
|
|
1248
|
+
let pendingIds = [];
|
|
1211
1249
|
const byId = new Map();
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1250
|
+
const submit = () => {
|
|
1251
|
+
if (encoder && pendingIds.length > 0) {
|
|
1252
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1253
|
+
this.#submission.queueSubmits += 1;
|
|
1254
|
+
this.#submission.surfaceIds.push(...pendingIds);
|
|
1255
|
+
const first = pendingIds.map((id) => byId.get(id)).find((metric) => metric !== undefined);
|
|
1256
|
+
if (first)
|
|
1257
|
+
first.queueSubmits += 1;
|
|
1258
|
+
}
|
|
1259
|
+
encoder = undefined;
|
|
1260
|
+
pendingIds = [];
|
|
1261
|
+
this.#releaseAtlasBatch();
|
|
1262
|
+
};
|
|
1263
|
+
try {
|
|
1264
|
+
for (const { id, view: originalView, overhang } of prepared) {
|
|
1265
|
+
const surface = this.#surfaces.get(id);
|
|
1266
|
+
if (!surface)
|
|
1267
|
+
continue;
|
|
1268
|
+
let view = originalView;
|
|
1269
|
+
const copyScroll = canStageScroll && this.#canCopyScroll(surface, view);
|
|
1270
|
+
if (view.damage?.scrollRows) {
|
|
1271
|
+
if (copyScroll) {
|
|
1272
|
+
const rows = new Set(view.damage.rows);
|
|
1273
|
+
const shift = view.damage.scrollRows;
|
|
1274
|
+
// Repaint exposed rows, the two clipped edges and the cursor
|
|
1275
|
+
// baked into the last submitted scene (possibly several frames ago).
|
|
1276
|
+
rows.add(0);
|
|
1277
|
+
rows.add(view.rows.length - 1);
|
|
1278
|
+
for (let row = 0; row < view.rows.length; row++) {
|
|
1279
|
+
if (row + shift < 0 || row + shift >= view.rows.length)
|
|
1280
|
+
rows.add(row);
|
|
1281
|
+
}
|
|
1282
|
+
if (surface.sceneCursorRow !== null)
|
|
1283
|
+
rows.add(surface.sceneCursorRow - shift);
|
|
1284
|
+
if (effectiveCursorStyle(view) !== null)
|
|
1285
|
+
rows.add(view.cursor.y);
|
|
1286
|
+
view = { ...view, damage: { ...view.damage, rows, geometryChanged: true } };
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
view = { ...view, damage: { full: true, rows: new Set(), geometryChanged: true } };
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const count = Math.max(view.rows.length, view.nativeRows.length, view.nativeStyleRows.length);
|
|
1293
|
+
const { rows } = rowsForDamage(count, view.damage, surface.sceneValid);
|
|
1294
|
+
try {
|
|
1295
|
+
try {
|
|
1296
|
+
this.#prepareAtlases(view, rows, surface.dpr);
|
|
1297
|
+
}
|
|
1298
|
+
catch (error) {
|
|
1299
|
+
if (!(error instanceof AtlasBatchFullError) || pendingIds.length === 0)
|
|
1300
|
+
throw error;
|
|
1301
|
+
// All recyclable pages are referenced by pending draws. Submit
|
|
1302
|
+
// them before overwriting any texels, then retry this pane once.
|
|
1303
|
+
submit();
|
|
1304
|
+
this.#prepareAtlases(view, rows, surface.dpr);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
if (!(error instanceof AtlasCapacityError))
|
|
1309
|
+
throw error;
|
|
1310
|
+
// Capacity failure is isolated to this pane and preserves its old
|
|
1311
|
+
// scene. Other panes still submit; new content/resize can retry it.
|
|
1312
|
+
console.error(`[terminal-renderer] cannot render ${id}`, error);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
encoder ??= this.device.createCommandEncoder({ label: "terminal frame batch" });
|
|
1316
|
+
if (copyScroll)
|
|
1317
|
+
this.#copyScroll(surface, view, encoder);
|
|
1318
|
+
byId.set(id, view.damage?.geometryChanged === false
|
|
1319
|
+
? this.#encodeCachedRender(id, view, encoder)
|
|
1320
|
+
: this.#encodeRender(id, view, encoder));
|
|
1321
|
+
surface.sceneHadSelection = view.selection !== null;
|
|
1322
|
+
surface.sceneHadOverhang = overhang;
|
|
1323
|
+
surface.sceneCursorRow = effectiveCursorStyle(view) === null ? null : view.cursor.y;
|
|
1324
|
+
pendingIds.push(id);
|
|
1325
|
+
}
|
|
1326
|
+
submit();
|
|
1327
|
+
}
|
|
1328
|
+
finally {
|
|
1329
|
+
this.#releaseAtlasBatch();
|
|
1216
1330
|
}
|
|
1217
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1218
|
-
const firstMetrics = [...byId.values()].find((metrics) => metrics !== undefined);
|
|
1219
|
-
if (firstMetrics)
|
|
1220
|
-
firstMetrics.queueSubmits = 1;
|
|
1221
1331
|
return entries.map(({ id }) => byId.get(id));
|
|
1222
1332
|
}
|
|
1223
|
-
#buildGeometry(view, renderRows, rowCount, fullRedraw, hasNativeRows, scale, viewportWidth, viewportHeight) {
|
|
1333
|
+
#buildGeometry(view, renderRows, rowCount, fullRedraw, hasNativeRows, scale, viewportWidth, viewportHeight, adaptation) {
|
|
1224
1334
|
const rectangleVertices = [];
|
|
1225
1335
|
const glyphVertices = [];
|
|
1226
1336
|
const colorGlyphVertices = [];
|
|
@@ -1230,22 +1340,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1230
1340
|
const cached = resolvedStyles.get(styleId);
|
|
1231
1341
|
if (cached)
|
|
1232
1342
|
return cached;
|
|
1233
|
-
const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
|
|
1343
|
+
const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme, adaptation);
|
|
1234
1344
|
resolvedStyles.set(styleId, resolved);
|
|
1235
1345
|
return resolved;
|
|
1236
1346
|
};
|
|
1237
|
-
const monoDefinitions = new Map();
|
|
1238
|
-
const colorDefinitions = new Map();
|
|
1239
|
-
for (const row of renderRows) {
|
|
1240
|
-
for (const glyph of view.nativeRows[row] ?? []) {
|
|
1241
|
-
const definition = view.glyphDefinitions.get(glyph.glyphId);
|
|
1242
|
-
if (!definition)
|
|
1243
|
-
continue;
|
|
1244
|
-
(definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(definition.id, definition);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
this.#monoAtlas.prepare(monoDefinitions.values());
|
|
1248
|
-
this.#colorAtlas.prepare(colorDefinitions.values());
|
|
1249
1347
|
if (!fullRedraw) {
|
|
1250
1348
|
for (const row of renderRows) {
|
|
1251
1349
|
const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
|
|
@@ -1281,9 +1379,6 @@ export class WebGpuTerminalRenderer {
|
|
|
1281
1379
|
}
|
|
1282
1380
|
pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
|
|
1283
1381
|
const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
|
|
1284
|
-
if (!hasNativeRows) {
|
|
1285
|
-
this.#fallbackAtlas.prepare(renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")), scale);
|
|
1286
|
-
}
|
|
1287
1382
|
if (hasNativeRows) {
|
|
1288
1383
|
for (const row of renderRows) {
|
|
1289
1384
|
const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
|
|
@@ -1315,12 +1410,14 @@ export class WebGpuTerminalRenderer {
|
|
|
1315
1410
|
let backdrop = style.background ?? view.theme.background;
|
|
1316
1411
|
if (isSelected)
|
|
1317
1412
|
backdrop = view.theme.selection;
|
|
1318
|
-
|
|
1413
|
+
const blockForeground = foreground === style.foreground ? style.fill : foreground;
|
|
1414
|
+
if (pushBlockElement(rectangleVertices, blockElement, instance.cellStart, row, scale, over(blockForeground, backdrop), viewportWidth, viewportHeight))
|
|
1319
1415
|
continue;
|
|
1320
1416
|
}
|
|
1321
|
-
const
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1417
|
+
const mono = definition.format === GlyphFormat.Alpha8;
|
|
1418
|
+
const atlas = mono ? this.#monoAtlas : this.#colorAtlas;
|
|
1419
|
+
const vertices = mono ? glyphVertices : colorGlyphVertices;
|
|
1420
|
+
pushGlyph(vertices, (ORIGIN_X + instance.x) * scale, (ORIGIN_Y + row * LINE_HEIGHT + instance.y) * scale, instance.width * scale, instance.height * scale, atlas.glyph(definition), mono && foreground === style.foreground ? style.text : foreground, viewportWidth, viewportHeight);
|
|
1324
1421
|
}
|
|
1325
1422
|
}
|
|
1326
1423
|
}
|
|
@@ -1352,10 +1449,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1352
1449
|
const rowTop = (ORIGIN_Y + row * LINE_HEIGHT) * scale;
|
|
1353
1450
|
const stroke = Math.max(1, Math.round(scale));
|
|
1354
1451
|
if (style.underline) {
|
|
1355
|
-
pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.
|
|
1452
|
+
pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.text, viewportWidth, viewportHeight);
|
|
1356
1453
|
}
|
|
1357
1454
|
if (style.strikethrough) {
|
|
1358
|
-
pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.
|
|
1455
|
+
pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.text, viewportWidth, viewportHeight);
|
|
1359
1456
|
}
|
|
1360
1457
|
}
|
|
1361
1458
|
}
|
|
@@ -1399,22 +1496,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1399
1496
|
const cached = resolvedStyles.get(styleId);
|
|
1400
1497
|
if (cached)
|
|
1401
1498
|
return cached;
|
|
1402
|
-
const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
|
|
1499
|
+
const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme, surface.adaptation);
|
|
1403
1500
|
resolvedStyles.set(styleId, resolved);
|
|
1404
1501
|
return resolved;
|
|
1405
1502
|
};
|
|
1406
|
-
const monoDefinitions = new Map();
|
|
1407
|
-
const colorDefinitions = new Map();
|
|
1408
|
-
for (const row of renderRows) {
|
|
1409
|
-
for (const glyph of view.nativeRows[row] ?? []) {
|
|
1410
|
-
const definition = view.glyphDefinitions.get(glyph.glyphId);
|
|
1411
|
-
if (!definition)
|
|
1412
|
-
continue;
|
|
1413
|
-
(definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(definition.id, definition);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
this.#monoAtlas.prepare(monoDefinitions.values());
|
|
1417
|
-
this.#colorAtlas.prepare(colorDefinitions.values());
|
|
1418
1503
|
if (!fullRedraw) {
|
|
1419
1504
|
for (const row of renderRows) {
|
|
1420
1505
|
const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
|
|
@@ -1451,9 +1536,6 @@ export class WebGpuTerminalRenderer {
|
|
|
1451
1536
|
pushBlockCursorBackground(rectangleVertices, view, renderRows, scale, viewportWidth, viewportHeight);
|
|
1452
1537
|
const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
|
|
1453
1538
|
const hasNativeRows = view.nativeRows.some((row) => row.length > 0);
|
|
1454
|
-
if (!hasNativeRows) {
|
|
1455
|
-
this.#fallbackAtlas.prepare(renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")), scale);
|
|
1456
|
-
}
|
|
1457
1539
|
if (hasNativeRows) {
|
|
1458
1540
|
for (const row of renderRows) {
|
|
1459
1541
|
const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
|
|
@@ -1487,13 +1569,15 @@ export class WebGpuTerminalRenderer {
|
|
|
1487
1569
|
let backdrop = style.background ?? view.theme.background;
|
|
1488
1570
|
if (isSelected)
|
|
1489
1571
|
backdrop = view.theme.selection;
|
|
1490
|
-
const
|
|
1572
|
+
const blockForeground = foreground === style.foreground ? style.fill : foreground;
|
|
1573
|
+
const blockColor = over(blockForeground, backdrop);
|
|
1491
1574
|
if (pushBlockElement(rectangleVertices, blockElement, instance.cellStart, row, scale, blockColor, viewportWidth, viewportHeight))
|
|
1492
1575
|
continue;
|
|
1493
1576
|
}
|
|
1494
|
-
const
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1577
|
+
const mono = definition.format === GlyphFormat.Alpha8;
|
|
1578
|
+
const atlas = mono ? this.#monoAtlas : this.#colorAtlas;
|
|
1579
|
+
const vertices = mono ? glyphVertices : colorGlyphVertices;
|
|
1580
|
+
pushGlyph(vertices, (ORIGIN_X + instance.x) * scale, (ORIGIN_Y + row * LINE_HEIGHT + instance.y) * scale, instance.width * scale, instance.height * scale, atlas.glyph(definition), mono && foreground === style.foreground ? style.text : foreground, viewportWidth, viewportHeight);
|
|
1497
1581
|
}
|
|
1498
1582
|
}
|
|
1499
1583
|
}
|
|
@@ -1525,10 +1609,10 @@ export class WebGpuTerminalRenderer {
|
|
|
1525
1609
|
const rowTop = (ORIGIN_Y + row * LINE_HEIGHT) * scale;
|
|
1526
1610
|
const stroke = Math.max(1, Math.round(scale));
|
|
1527
1611
|
if (style.underline) {
|
|
1528
|
-
pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.
|
|
1612
|
+
pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.text, viewportWidth, viewportHeight);
|
|
1529
1613
|
}
|
|
1530
1614
|
if (style.strikethrough) {
|
|
1531
|
-
pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.
|
|
1615
|
+
pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.text, viewportWidth, viewportHeight);
|
|
1532
1616
|
}
|
|
1533
1617
|
}
|
|
1534
1618
|
}
|
|
@@ -1692,7 +1776,7 @@ export class WebGpuTerminalRenderer {
|
|
|
1692
1776
|
this.#colorAtlas.generation,
|
|
1693
1777
|
this.#fallbackAtlas.generation,
|
|
1694
1778
|
];
|
|
1695
|
-
let key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
|
|
1779
|
+
let key = geometryCacheKey(view, damage.rows, hasNativeRows, generations(), surface.adaptation?.key ?? "-");
|
|
1696
1780
|
const cached = surface.geometryCache.get(key);
|
|
1697
1781
|
const cacheHit = cached !== undefined;
|
|
1698
1782
|
if (cached) {
|
|
@@ -1703,8 +1787,8 @@ export class WebGpuTerminalRenderer {
|
|
|
1703
1787
|
else {
|
|
1704
1788
|
const admitted = surface.geometryCandidates.has(key);
|
|
1705
1789
|
const candidateKey = key;
|
|
1706
|
-
const cpu = this.#buildGeometry(view, damage.rows, rowCount, false, hasNativeRows, scale, viewportWidth, viewportHeight);
|
|
1707
|
-
key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
|
|
1790
|
+
const cpu = this.#buildGeometry(view, damage.rows, rowCount, false, hasNativeRows, scale, viewportWidth, viewportHeight, surface.adaptation);
|
|
1791
|
+
key = geometryCacheKey(view, damage.rows, hasNativeRows, generations(), surface.adaptation?.key ?? "-");
|
|
1708
1792
|
const promote = admitted && key === candidateKey;
|
|
1709
1793
|
geometry = {
|
|
1710
1794
|
rectangleBuffer: promote
|