@vibecook/ghosttea-react 0.2.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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/TerminalSurface.d.ts +28 -0
  4. package/dist/TerminalSurface.d.ts.map +1 -0
  5. package/dist/TerminalSurface.js +614 -0
  6. package/dist/TerminalSurface.js.map +1 -0
  7. package/dist/bindings/action-route.d.ts +155 -0
  8. package/dist/bindings/action-route.d.ts.map +1 -0
  9. package/dist/bindings/action-route.js +196 -0
  10. package/dist/bindings/action-route.js.map +1 -0
  11. package/dist/bindings/fixtures/extensions.json +10 -0
  12. package/dist/bindings/fixtures/keybinds-linux-default.json +505 -0
  13. package/dist/bindings/fixtures/keybinds-macos-default.json +660 -0
  14. package/dist/bindings/ghostty-actions.d.ts +247 -0
  15. package/dist/bindings/ghostty-actions.d.ts.map +1 -0
  16. package/dist/bindings/ghostty-actions.js +431 -0
  17. package/dist/bindings/ghostty-actions.js.map +1 -0
  18. package/dist/bindings/ghostty-bindings.d.ts +88 -0
  19. package/dist/bindings/ghostty-bindings.d.ts.map +1 -0
  20. package/dist/bindings/ghostty-bindings.js +122 -0
  21. package/dist/bindings/ghostty-bindings.js.map +1 -0
  22. package/dist/bindings/ghostty-triggers.d.ts +47 -0
  23. package/dist/bindings/ghostty-triggers.d.ts.map +1 -0
  24. package/dist/bindings/ghostty-triggers.js +378 -0
  25. package/dist/bindings/ghostty-triggers.js.map +1 -0
  26. package/dist/bindings/index.d.ts +5 -0
  27. package/dist/bindings/index.d.ts.map +1 -0
  28. package/dist/bindings/index.js +5 -0
  29. package/dist/bindings/index.js.map +1 -0
  30. package/dist/catalog-budget.d.ts +22 -0
  31. package/dist/catalog-budget.d.ts.map +1 -0
  32. package/dist/catalog-budget.js +53 -0
  33. package/dist/catalog-budget.js.map +1 -0
  34. package/dist/cell-width.d.ts +3 -0
  35. package/dist/cell-width.d.ts.map +1 -0
  36. package/dist/cell-width.js +19 -0
  37. package/dist/cell-width.js.map +1 -0
  38. package/dist/context.d.ts +9 -0
  39. package/dist/context.d.ts.map +1 -0
  40. package/dist/context.js +13 -0
  41. package/dist/context.js.map +1 -0
  42. package/dist/cursor-invalidation.d.ts +3 -0
  43. package/dist/cursor-invalidation.d.ts.map +1 -0
  44. package/dist/cursor-invalidation.js +4 -0
  45. package/dist/cursor-invalidation.js.map +1 -0
  46. package/dist/frame-resync.d.ts +16 -0
  47. package/dist/frame-resync.d.ts.map +1 -0
  48. package/dist/frame-resync.js +164 -0
  49. package/dist/frame-resync.js.map +1 -0
  50. package/dist/frame-sequence.d.ts +14 -0
  51. package/dist/frame-sequence.d.ts.map +1 -0
  52. package/dist/frame-sequence.js +16 -0
  53. package/dist/frame-sequence.js.map +1 -0
  54. package/dist/index.d.ts +6 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +5 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/keyboard-input.d.ts +18 -0
  59. package/dist/keyboard-input.d.ts.map +1 -0
  60. package/dist/keyboard-input.js +101 -0
  61. package/dist/keyboard-input.js.map +1 -0
  62. package/dist/performance.d.ts +66 -0
  63. package/dist/performance.d.ts.map +1 -0
  64. package/dist/performance.js +21 -0
  65. package/dist/performance.js.map +1 -0
  66. package/dist/renderers/canvas-renderer.d.ts +12 -0
  67. package/dist/renderers/canvas-renderer.d.ts.map +1 -0
  68. package/dist/renderers/canvas-renderer.js +117 -0
  69. package/dist/renderers/canvas-renderer.js.map +1 -0
  70. package/dist/renderers/render-damage.d.ts +7 -0
  71. package/dist/renderers/render-damage.d.ts.map +1 -0
  72. package/dist/renderers/render-damage.js +16 -0
  73. package/dist/renderers/render-damage.js.map +1 -0
  74. package/dist/renderers/types.d.ts +65 -0
  75. package/dist/renderers/types.d.ts.map +1 -0
  76. package/dist/renderers/types.js +31 -0
  77. package/dist/renderers/types.js.map +1 -0
  78. package/dist/renderers/webgpu-renderer.d.ts +27 -0
  79. package/dist/renderers/webgpu-renderer.d.ts.map +1 -0
  80. package/dist/renderers/webgpu-renderer.js +1746 -0
  81. package/dist/renderers/webgpu-renderer.js.map +1 -0
  82. package/dist/runtime.d.ts +70 -0
  83. package/dist/runtime.d.ts.map +1 -0
  84. package/dist/runtime.js +1021 -0
  85. package/dist/runtime.js.map +1 -0
  86. package/dist/scroll-input.d.ts +6 -0
  87. package/dist/scroll-input.d.ts.map +1 -0
  88. package/dist/scroll-input.js +14 -0
  89. package/dist/scroll-input.js.map +1 -0
  90. package/dist/selection-input.d.ts +16 -0
  91. package/dist/selection-input.d.ts.map +1 -0
  92. package/dist/selection-input.js +67 -0
  93. package/dist/selection-input.js.map +1 -0
  94. package/dist/selection-text.d.ts +4 -0
  95. package/dist/selection-text.d.ts.map +1 -0
  96. package/dist/selection-text.js +28 -0
  97. package/dist/selection-text.js.map +1 -0
  98. package/dist/styles.css +75 -0
  99. package/dist/terminal-bindings.d.ts +22 -0
  100. package/dist/terminal-bindings.d.ts.map +1 -0
  101. package/dist/terminal-bindings.js +22 -0
  102. package/dist/terminal-bindings.js.map +1 -0
  103. package/dist/terminal-render.worker.d.ts +2 -0
  104. package/dist/terminal-render.worker.d.ts.map +1 -0
  105. package/dist/terminal-render.worker.js +3165 -0
  106. package/dist/terminal-render.worker.js.map +7 -0
  107. package/dist/worker-messages.d.ts +111 -0
  108. package/dist/worker-messages.d.ts.map +1 -0
  109. package/dist/worker-messages.js +2 -0
  110. package/dist/worker-messages.js.map +1 -0
  111. package/dist/workspace/RemoteSessionPalette.d.ts +12 -0
  112. package/dist/workspace/RemoteSessionPalette.d.ts.map +1 -0
  113. package/dist/workspace/RemoteSessionPalette.js +135 -0
  114. package/dist/workspace/RemoteSessionPalette.js.map +1 -0
  115. package/dist/workspace/Workspace.d.ts +69 -0
  116. package/dist/workspace/Workspace.d.ts.map +1 -0
  117. package/dist/workspace/Workspace.js +536 -0
  118. package/dist/workspace/Workspace.js.map +1 -0
  119. package/dist/workspace/hotkeys.d.ts +27 -0
  120. package/dist/workspace/hotkeys.d.ts.map +1 -0
  121. package/dist/workspace/hotkeys.js +29 -0
  122. package/dist/workspace/hotkeys.js.map +1 -0
  123. package/dist/workspace/index.d.ts +8 -0
  124. package/dist/workspace/index.d.ts.map +1 -0
  125. package/dist/workspace/index.js +8 -0
  126. package/dist/workspace/index.js.map +1 -0
  127. package/dist/workspace/pane-layout.d.ts +35 -0
  128. package/dist/workspace/pane-layout.d.ts.map +1 -0
  129. package/dist/workspace/pane-layout.js +139 -0
  130. package/dist/workspace/pane-layout.js.map +1 -0
  131. package/dist/workspace/pending-cache.d.ts +5 -0
  132. package/dist/workspace/pending-cache.d.ts.map +1 -0
  133. package/dist/workspace/pending-cache.js +22 -0
  134. package/dist/workspace/pending-cache.js.map +1 -0
  135. package/dist/workspace/session-scope.d.ts +3 -0
  136. package/dist/workspace/session-scope.d.ts.map +1 -0
  137. package/dist/workspace/session-scope.js +6 -0
  138. package/dist/workspace/session-scope.js.map +1 -0
  139. package/dist/workspace/themes.d.ts +18 -0
  140. package/dist/workspace/themes.d.ts.map +1 -0
  141. package/dist/workspace/themes.js +19 -0
  142. package/dist/workspace/themes.js.map +1 -0
  143. package/dist/workspace/workspace-model.d.ts +59 -0
  144. package/dist/workspace/workspace-model.d.ts.map +1 -0
  145. package/dist/workspace/workspace-model.js +257 -0
  146. package/dist/workspace/workspace-model.js.map +1 -0
  147. package/dist/workspace/workspace-tabs.d.ts +41 -0
  148. package/dist/workspace/workspace-tabs.d.ts.map +1 -0
  149. package/dist/workspace/workspace-tabs.js +133 -0
  150. package/dist/workspace/workspace-tabs.js.map +1 -0
  151. package/dist/workspace.css +453 -0
  152. package/package.json +75 -0
@@ -0,0 +1,1746 @@
1
+ /// <reference types="@webgpu/types" />
2
+ import { CursorStyle, GlyphFormat } from "@vibecook/ghosttea-frame";
3
+ import { CELL_WIDTH, LINE_HEIGHT, ORIGIN_X, ORIGIN_Y, effectiveCursorStyle, renderedSizeChanged, } from "./types.js";
4
+ import { graphemeCellWidth, splitGraphemes } from "../cell-width.js";
5
+ import { rowsForDamage } from "./render-damage.js";
6
+ const ATLAS_SIZE = 2048;
7
+ const GEOMETRY_CACHE_LIMIT = 8;
8
+ const FONT_STACK = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
9
+ const RECT_SHADER = /* wgsl */ `
10
+ struct VertexOutput {
11
+ @builtin(position) position: vec4f,
12
+ @location(0) color: vec4f,
13
+ }
14
+
15
+ @vertex fn vertex_main(
16
+ @builtin(vertex_index) vertex_index: u32,
17
+ @location(0) p0: vec2f,
18
+ @location(1) p1: vec2f,
19
+ @location(2) p2: vec2f,
20
+ @location(3) color: vec4f,
21
+ ) -> VertexOutput {
22
+ let p3 = p1 + p2 - p0;
23
+ let positions = array<vec2f, 6>(
24
+ p0,
25
+ p1,
26
+ p2,
27
+ p2,
28
+ p1,
29
+ p3,
30
+ );
31
+ var output: VertexOutput;
32
+ output.position = vec4f(positions[vertex_index], 0.0, 1.0);
33
+ output.color = color;
34
+ return output;
35
+ }
36
+
37
+ @fragment fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
38
+ return vec4f(input.color.rgb * input.color.a, input.color.a);
39
+ }
40
+ `;
41
+ function glyphShader(coverageChannel) {
42
+ return /* wgsl */ `
43
+ @group(0) @binding(0) var atlas: texture_2d<f32>;
44
+ @group(0) @binding(1) var atlas_sampler: sampler;
45
+
46
+ struct VertexOutput {
47
+ @builtin(position) position: vec4f,
48
+ @location(0) uv: vec2f,
49
+ @location(1) color: vec4f,
50
+ }
51
+
52
+ @vertex fn vertex_main(
53
+ @builtin(vertex_index) vertex_index: u32,
54
+ @location(0) bounds: vec4f,
55
+ @location(1) uv_bounds: vec4f,
56
+ @location(2) color: vec4f,
57
+ ) -> VertexOutput {
58
+ let positions = array<vec2f, 6>(
59
+ vec2f(bounds.x, bounds.z),
60
+ vec2f(bounds.y, bounds.z),
61
+ vec2f(bounds.x, bounds.w),
62
+ vec2f(bounds.x, bounds.w),
63
+ vec2f(bounds.y, bounds.z),
64
+ vec2f(bounds.y, bounds.w),
65
+ );
66
+ let uvs = array<vec2f, 6>(
67
+ vec2f(uv_bounds.x, uv_bounds.z),
68
+ vec2f(uv_bounds.y, uv_bounds.z),
69
+ vec2f(uv_bounds.x, uv_bounds.w),
70
+ vec2f(uv_bounds.x, uv_bounds.w),
71
+ vec2f(uv_bounds.y, uv_bounds.z),
72
+ vec2f(uv_bounds.y, uv_bounds.w),
73
+ );
74
+ var output: VertexOutput;
75
+ output.position = vec4f(positions[vertex_index], 0.0, 1.0);
76
+ output.uv = uvs[vertex_index];
77
+ output.color = color;
78
+ return output;
79
+ }
80
+
81
+ @fragment fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
82
+ let coverage = textureSample(atlas, atlas_sampler, input.uv).${coverageChannel};
83
+ let alpha = input.color.a * coverage;
84
+ return vec4f(input.color.rgb * alpha, alpha);
85
+ }
86
+ `;
87
+ }
88
+ const MONO_GLYPH_SHADER = glyphShader("r");
89
+ const FALLBACK_GLYPH_SHADER = glyphShader("a");
90
+ const COLOR_GLYPH_SHADER = /* wgsl */ `
91
+ @group(0) @binding(0) var atlas: texture_2d<f32>;
92
+ @group(0) @binding(1) var atlas_sampler: sampler;
93
+
94
+ struct VertexOutput {
95
+ @builtin(position) position: vec4f,
96
+ @location(0) uv: vec2f,
97
+ @location(1) color: vec4f,
98
+ }
99
+
100
+ @vertex fn vertex_main(
101
+ @builtin(vertex_index) vertex_index: u32,
102
+ @location(0) bounds: vec4f,
103
+ @location(1) uv_bounds: vec4f,
104
+ @location(2) color: vec4f,
105
+ ) -> VertexOutput {
106
+ let positions = array<vec2f, 6>(
107
+ vec2f(bounds.x, bounds.z),
108
+ vec2f(bounds.y, bounds.z),
109
+ vec2f(bounds.x, bounds.w),
110
+ vec2f(bounds.x, bounds.w),
111
+ vec2f(bounds.y, bounds.z),
112
+ vec2f(bounds.y, bounds.w),
113
+ );
114
+ let uvs = array<vec2f, 6>(
115
+ vec2f(uv_bounds.x, uv_bounds.z),
116
+ vec2f(uv_bounds.y, uv_bounds.z),
117
+ vec2f(uv_bounds.x, uv_bounds.w),
118
+ vec2f(uv_bounds.x, uv_bounds.w),
119
+ vec2f(uv_bounds.y, uv_bounds.z),
120
+ vec2f(uv_bounds.y, uv_bounds.w),
121
+ );
122
+ var output: VertexOutput;
123
+ output.position = vec4f(positions[vertex_index], 0.0, 1.0);
124
+ output.uv = uvs[vertex_index];
125
+ output.color = color;
126
+ return output;
127
+ }
128
+
129
+ @fragment fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
130
+ return textureSample(atlas, atlas_sampler, input.uv);
131
+ }
132
+ `;
133
+ // WebGPU port of ~/.config/ghostty/shaders/bettercrt.glsl. Ghostty applies
134
+ // custom shaders to the completed terminal image, so this must remain a
135
+ // separate pass rather than being approximated per glyph.
136
+ const BETTER_CRT_SHADER = /* wgsl */ `
137
+ @group(0) @binding(0) var terminal_image: texture_2d<f32>;
138
+ @group(0) @binding(1) var terminal_sampler: sampler;
139
+
140
+ @vertex fn vertex_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4f {
141
+ let positions = array<vec2f, 3>(
142
+ vec2f(-1.0, -1.0),
143
+ vec2f(3.0, -1.0),
144
+ vec2f(-1.0, 3.0),
145
+ );
146
+ return vec4f(positions[vertex_index], 0.0, 1.0);
147
+ }
148
+
149
+ @fragment fn fragment_main(@builtin(position) position: vec4f) -> @location(0) vec4f {
150
+ let dimensions = vec2f(textureDimensions(terminal_image));
151
+ var uv = position.xy / dimensions;
152
+ var dc = abs(vec2f(0.5) - uv);
153
+ dc *= dc;
154
+
155
+ // These constants are the active Ghostty shader's warp = 0.25 and
156
+ // scan = 0.50 values.
157
+ uv.x -= 0.5;
158
+ uv.x *= 1.0 + dc.y * 0.075;
159
+ uv.x += 0.5;
160
+ uv.y -= 0.5;
161
+ uv.y *= 1.0 + dc.x * 0.10;
162
+ uv.y += 0.5;
163
+
164
+ let scanline = abs(sin(position.y) * 0.125);
165
+ let color = textureSample(terminal_image, terminal_sampler, uv).rgb;
166
+ return vec4f(mix(color, vec3f(0.0), scanline), 1.0);
167
+ }
168
+ `;
169
+ const premultipliedBlend = {
170
+ color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
171
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
172
+ };
173
+ class DynamicVertexBuffer {
174
+ device;
175
+ label;
176
+ #buffer;
177
+ #capacity = 0;
178
+ constructor(device, label) {
179
+ this.device = device;
180
+ this.label = label;
181
+ }
182
+ write(vertices) {
183
+ if (vertices.byteLength === 0)
184
+ return undefined;
185
+ if (!this.#buffer || this.#capacity < vertices.byteLength) {
186
+ this.#buffer?.destroy();
187
+ this.#capacity = Math.max(4096, 2 ** Math.ceil(Math.log2(vertices.byteLength)));
188
+ this.#buffer = this.device.createBuffer({
189
+ label: this.label,
190
+ size: this.#capacity,
191
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
192
+ });
193
+ }
194
+ this.device.queue.writeBuffer(this.#buffer, 0, vertices);
195
+ return this.#buffer;
196
+ }
197
+ destroy() {
198
+ this.#buffer?.destroy();
199
+ this.#buffer = undefined;
200
+ }
201
+ }
202
+ class FallbackGlyphAtlas {
203
+ device;
204
+ #texture;
205
+ #sampler;
206
+ #bindGroup;
207
+ #cache = new Map();
208
+ #scratch = new OffscreenCanvas(1, 1);
209
+ #x = 1;
210
+ #y = 1;
211
+ #rowHeight = 0;
212
+ #uploadBytes = 0;
213
+ #uploadCalls = 0;
214
+ #generation = 0;
215
+ constructor(device, bindGroupLayout) {
216
+ this.device = device;
217
+ this.#texture = device.createTexture({
218
+ label: "terminal glyph atlas",
219
+ size: [ATLAS_SIZE, ATLAS_SIZE],
220
+ format: "rgba8unorm",
221
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
222
+ });
223
+ this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
224
+ this.#bindGroup = device.createBindGroup({
225
+ label: "terminal glyph atlas bindings",
226
+ layout: bindGroupLayout,
227
+ entries: [
228
+ { binding: 0, resource: this.#texture.createView() },
229
+ { binding: 1, resource: this.#sampler },
230
+ ],
231
+ });
232
+ }
233
+ get bindGroup() {
234
+ return this.#bindGroup;
235
+ }
236
+ get generation() {
237
+ return this.#generation;
238
+ }
239
+ prepare(values, dpr) {
240
+ const scale = Math.max(1, Math.min(3, dpr));
241
+ const all = [...new Set(values)].filter((value) => value.trim().length > 0);
242
+ const missing = all.filter((value) => !this.#cache.has(`${scale.toFixed(2)}:${value}`));
243
+ if (this.#fits(missing, scale, this.#x, this.#y, this.#rowHeight))
244
+ return;
245
+ this.#cache.clear();
246
+ this.#generation += 1;
247
+ this.#x = 1;
248
+ this.#y = 1;
249
+ this.#rowHeight = 0;
250
+ if (!this.#fits(all, scale, this.#x, this.#y, this.#rowHeight)) {
251
+ throw new Error("The diagnostic glyph atlas cannot fit the text visible in one frame");
252
+ }
253
+ }
254
+ #fits(values, scale, startX, startY, startRowHeight) {
255
+ let x = startX;
256
+ let y = startY;
257
+ let rowHeight = startRowHeight;
258
+ for (const value of values) {
259
+ const width = Math.max(1, Math.ceil(CELL_WIDTH * graphemeCellWidth(value) * scale));
260
+ const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
261
+ if (x + width + 1 > ATLAS_SIZE) {
262
+ x = 1;
263
+ y += rowHeight + 1;
264
+ rowHeight = 0;
265
+ }
266
+ if (y + height + 1 > ATLAS_SIZE)
267
+ return false;
268
+ x += width + 1;
269
+ rowHeight = Math.max(rowHeight, height);
270
+ }
271
+ return true;
272
+ }
273
+ glyph(grapheme, dpr) {
274
+ if (grapheme.trim().length === 0)
275
+ return undefined;
276
+ const scale = Math.max(1, Math.min(3, dpr));
277
+ const cells = graphemeCellWidth(grapheme);
278
+ const key = `${scale.toFixed(2)}:${grapheme}`;
279
+ const cached = this.#cache.get(key);
280
+ if (cached)
281
+ return cached;
282
+ const width = Math.max(1, Math.ceil(CELL_WIDTH * cells * scale));
283
+ const height = Math.max(1, Math.ceil(LINE_HEIGHT * scale));
284
+ if (this.#x + width + 1 > ATLAS_SIZE) {
285
+ this.#x = 1;
286
+ this.#y += this.#rowHeight + 1;
287
+ this.#rowHeight = 0;
288
+ }
289
+ if (this.#y + height + 1 > ATLAS_SIZE) {
290
+ throw new Error("Temporary glyph atlas exhausted");
291
+ }
292
+ this.#scratch.width = width;
293
+ this.#scratch.height = height;
294
+ const context = this.#scratch.getContext("2d", { willReadFrequently: true });
295
+ if (!context)
296
+ throw new Error("Glyph raster canvas unavailable");
297
+ context.clearRect(0, 0, width, height);
298
+ context.fillStyle = "white";
299
+ context.font = `${13 * scale}px ${FONT_STACK}`;
300
+ context.textBaseline = "top";
301
+ context.fillText(grapheme, 0, Math.max(0, Math.floor((height - 15 * scale) / 2)));
302
+ const image = context.getImageData(0, 0, width, height);
303
+ this.device.queue.writeTexture({ texture: this.#texture, origin: [this.#x, this.#y] }, image.data, { bytesPerRow: width * 4, rowsPerImage: height }, [width, height]);
304
+ this.#uploadBytes += image.data.byteLength;
305
+ this.#uploadCalls += 1;
306
+ const location = {
307
+ u0: this.#x / ATLAS_SIZE,
308
+ v0: this.#y / ATLAS_SIZE,
309
+ u1: (this.#x + width) / ATLAS_SIZE,
310
+ v1: (this.#y + height) / ATLAS_SIZE,
311
+ cells,
312
+ };
313
+ this.#cache.set(key, location);
314
+ this.#x += width + 1;
315
+ this.#rowHeight = Math.max(this.#rowHeight, height);
316
+ return location;
317
+ }
318
+ destroy() {
319
+ this.#texture.destroy();
320
+ }
321
+ uploadMetrics() {
322
+ return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
323
+ }
324
+ }
325
+ class NativeGlyphAtlas {
326
+ device;
327
+ label;
328
+ format;
329
+ #texture;
330
+ #sampler;
331
+ #bindGroup;
332
+ #cache = new Map();
333
+ #x = 1;
334
+ #y = 1;
335
+ #rowHeight = 0;
336
+ #uploadBytes = 0;
337
+ #uploadCalls = 0;
338
+ #generation = 0;
339
+ constructor(device, bindGroupLayout, label, format) {
340
+ this.device = device;
341
+ this.label = label;
342
+ this.format = format;
343
+ this.#texture = device.createTexture({
344
+ label,
345
+ size: [ATLAS_SIZE, ATLAS_SIZE],
346
+ format,
347
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
348
+ });
349
+ this.#sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
350
+ this.#bindGroup = device.createBindGroup({
351
+ label: `${label} bindings`,
352
+ layout: bindGroupLayout,
353
+ entries: [
354
+ { binding: 0, resource: this.#texture.createView() },
355
+ { binding: 1, resource: this.#sampler },
356
+ ],
357
+ });
358
+ }
359
+ get bindGroup() {
360
+ return this.#bindGroup;
361
+ }
362
+ get generation() {
363
+ return this.#generation;
364
+ }
365
+ prepare(definitions) {
366
+ const all = new Map();
367
+ const missing = new Map();
368
+ for (const definition of definitions) {
369
+ all.set(definition.id, definition);
370
+ if (!this.#cache.has(definition.id))
371
+ missing.set(definition.id, definition);
372
+ }
373
+ if (this.#fits(missing.values(), this.#x, this.#y, this.#rowHeight))
374
+ return;
375
+ this.#cache.clear();
376
+ this.#generation += 1;
377
+ this.#x = 1;
378
+ this.#y = 1;
379
+ this.#rowHeight = 0;
380
+ if (!this.#fits(all.values(), this.#x, this.#y, this.#rowHeight)) {
381
+ throw new Error(`${this.label} cannot fit the glyphs visible in one frame`);
382
+ }
383
+ }
384
+ #fits(definitions, startX, startY, startRowHeight) {
385
+ let x = startX;
386
+ let y = startY;
387
+ let rowHeight = startRowHeight;
388
+ for (const { width, height } of definitions) {
389
+ if (width + 2 > ATLAS_SIZE || height + 2 > ATLAS_SIZE)
390
+ return false;
391
+ if (x + width + 1 > ATLAS_SIZE) {
392
+ x = 1;
393
+ y += rowHeight + 1;
394
+ rowHeight = 0;
395
+ }
396
+ if (y + height + 1 > ATLAS_SIZE)
397
+ return false;
398
+ x += width + 1;
399
+ rowHeight = Math.max(rowHeight, height);
400
+ }
401
+ return true;
402
+ }
403
+ glyph(definition) {
404
+ const cached = this.#cache.get(definition.id);
405
+ if (cached)
406
+ return cached;
407
+ const { width, height } = definition;
408
+ if (this.#x + width + 1 > ATLAS_SIZE) {
409
+ this.#x = 1;
410
+ this.#y += this.#rowHeight + 1;
411
+ this.#rowHeight = 0;
412
+ }
413
+ if (this.#y + height + 1 > ATLAS_SIZE)
414
+ throw new Error(`${this.label} exhausted`);
415
+ const pixels = definition.pixels;
416
+ const bytesPerPixel = this.format === "r8unorm" ? 1 : 4;
417
+ this.device.queue.writeTexture({ texture: this.#texture, origin: [this.#x, this.#y] }, pixels, { bytesPerRow: width * bytesPerPixel, rowsPerImage: height }, [width, height]);
418
+ this.#uploadBytes += pixels.byteLength;
419
+ this.#uploadCalls += 1;
420
+ const location = {
421
+ u0: this.#x / ATLAS_SIZE,
422
+ v0: this.#y / ATLAS_SIZE,
423
+ u1: (this.#x + width) / ATLAS_SIZE,
424
+ v1: (this.#y + height) / ATLAS_SIZE,
425
+ cells: 0,
426
+ };
427
+ this.#cache.set(definition.id, location);
428
+ this.#x += width + 1;
429
+ this.#rowHeight = Math.max(this.#rowHeight, height);
430
+ return location;
431
+ }
432
+ destroy() {
433
+ this.#texture.destroy();
434
+ }
435
+ uploadMetrics() {
436
+ return { bytes: this.#uploadBytes, calls: this.#uploadCalls };
437
+ }
438
+ }
439
+ function destroyCachedGeometry(geometry) {
440
+ geometry.rectangleBuffer?.destroy();
441
+ geometry.glyphBuffer?.destroy();
442
+ geometry.colorGlyphBuffer?.destroy();
443
+ geometry.fallbackGlyphBuffer?.destroy();
444
+ }
445
+ function clearGeometryCache(surface) {
446
+ for (const geometry of surface.geometryCache.values())
447
+ destroyCachedGeometry(geometry);
448
+ surface.geometryCache.clear();
449
+ surface.geometryCandidates.clear();
450
+ }
451
+ function createCachedVertexBuffer(device, label, data) {
452
+ if (data.byteLength === 0)
453
+ return undefined;
454
+ const buffer = device.createBuffer({
455
+ label,
456
+ size: data.byteLength,
457
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
458
+ });
459
+ device.queue.writeBuffer(buffer, 0, data);
460
+ return buffer;
461
+ }
462
+ function geometryCacheKey(view, rows, hasNativeRows, atlasGenerations) {
463
+ const selection = view.selection
464
+ ? `${view.selection.anchor.row},${view.selection.anchor.column},${view.selection.focus.row},${view.selection.focus.column}`
465
+ : "-";
466
+ return [
467
+ view.sessionEpoch.toString(),
468
+ view.layoutEpoch.toString(),
469
+ hasNativeRows ? "native" : "fallback",
470
+ rows.map((row) => `${row}:${view.rowRevisions[row]?.toString() ?? "-"}`).join(","),
471
+ [
472
+ ...view.theme.background,
473
+ ...view.theme.foreground,
474
+ ...view.theme.selection,
475
+ ...view.theme.selectionForeground,
476
+ ].join(","),
477
+ selection,
478
+ atlasGenerations.join(","),
479
+ ].join("|");
480
+ }
481
+ function pushCursorVertices(vertices, view, renderRowSet, scale, viewportWidth, viewportHeight) {
482
+ const cursorStyle = effectiveCursorStyle(view);
483
+ if (cursorStyle === null || !renderRowSet.has(view.cursor.y))
484
+ return;
485
+ const x = (ORIGIN_X + view.cursor.x * CELL_WIDTH) * scale;
486
+ const y = (ORIGIN_Y + view.cursor.y * LINE_HEIGHT) * scale;
487
+ const width = CELL_WIDTH * scale;
488
+ const height = LINE_HEIGHT * scale;
489
+ const stroke = Math.max(1, Math.round(scale));
490
+ const cursorColor = cursorStyle === CursorStyle.HollowBlock
491
+ ? [view.theme.cursor[0], view.theme.cursor[1], view.theme.cursor[2], 1]
492
+ : view.theme.cursor;
493
+ if (cursorStyle === CursorStyle.Bar) {
494
+ pushRectangle(vertices, x, y, Math.max(2, Math.round(2 * scale)), height, cursorColor, viewportWidth, viewportHeight);
495
+ }
496
+ else if (cursorStyle === CursorStyle.Underline) {
497
+ const thickness = Math.max(2, Math.round(2 * scale));
498
+ pushRectangle(vertices, x, y + height - thickness, width, thickness, cursorColor, viewportWidth, viewportHeight);
499
+ }
500
+ else if (cursorStyle === CursorStyle.HollowBlock) {
501
+ pushRectangle(vertices, x, y, width, stroke, cursorColor, viewportWidth, viewportHeight);
502
+ pushRectangle(vertices, x, y + height - stroke, width, stroke, cursorColor, viewportWidth, viewportHeight);
503
+ pushRectangle(vertices, x, y + stroke, stroke, Math.max(0, height - stroke * 2), cursorColor, viewportWidth, viewportHeight);
504
+ pushRectangle(vertices, x + width - stroke, y + stroke, stroke, Math.max(0, height - stroke * 2), cursorColor, viewportWidth, viewportHeight);
505
+ }
506
+ else {
507
+ pushRectangle(vertices, x, y, width, height, cursorColor, viewportWidth, viewportHeight);
508
+ }
509
+ }
510
+ function clipX(pixel, width) {
511
+ return (pixel / Math.max(1, width)) * 2 - 1;
512
+ }
513
+ function clipY(pixel, height) {
514
+ return 1 - (pixel / Math.max(1, height)) * 2;
515
+ }
516
+ function pushRectangle(output, x, y, width, height, color, viewportWidth, viewportHeight) {
517
+ const left = clipX(x, viewportWidth);
518
+ const right = clipX(x + width, viewportWidth);
519
+ const top = clipY(y, viewportHeight);
520
+ const bottom = clipY(y + height, viewportHeight);
521
+ output.push(left, top, right, top, left, bottom, ...color);
522
+ }
523
+ function pushGlyph(output, x, y, width, height, glyph, color, viewportWidth, viewportHeight) {
524
+ const left = clipX(x, viewportWidth);
525
+ const right = clipX(x + width, viewportWidth);
526
+ const top = clipY(y, viewportHeight);
527
+ const bottom = clipY(y + height, viewportHeight);
528
+ output.push(left, right, top, bottom, glyph.u0, glyph.u1, glyph.v0, glyph.v1, ...color);
529
+ }
530
+ function ordered(selection) {
531
+ if (!selection)
532
+ return null;
533
+ const { anchor, focus } = selection;
534
+ return anchor.row < focus.row || (anchor.row === focus.row && anchor.column <= focus.column)
535
+ ? [anchor, focus]
536
+ : [focus, anchor];
537
+ }
538
+ function cellLength(text) {
539
+ return splitGraphemes(text).reduce((total, grapheme) => total + graphemeCellWidth(grapheme), 0);
540
+ }
541
+ const BOX_DIRECTIONS = new Map([
542
+ ["─", "lr"],
543
+ ["━", "lr"],
544
+ ["│", "ud"],
545
+ ["┃", "ud"],
546
+ ["┌", "rd"],
547
+ ["┍", "rd"],
548
+ ["┎", "rd"],
549
+ ["┏", "rd"],
550
+ ["┐", "ld"],
551
+ ["┑", "ld"],
552
+ ["┒", "ld"],
553
+ ["┓", "ld"],
554
+ ["└", "ru"],
555
+ ["┕", "ru"],
556
+ ["┖", "ru"],
557
+ ["┗", "ru"],
558
+ ["┘", "lu"],
559
+ ["┙", "lu"],
560
+ ["┚", "lu"],
561
+ ["┛", "lu"],
562
+ ["├", "udr"],
563
+ ["┝", "udr"],
564
+ ["┞", "udr"],
565
+ ["┣", "udr"],
566
+ ["┤", "udl"],
567
+ ["┥", "udl"],
568
+ ["┦", "udl"],
569
+ ["┫", "udl"],
570
+ ["┬", "lrd"],
571
+ ["┯", "lrd"],
572
+ ["┰", "lrd"],
573
+ ["┳", "lrd"],
574
+ ["┴", "lru"],
575
+ ["┷", "lru"],
576
+ ["┸", "lru"],
577
+ ["┻", "lru"],
578
+ ["┼", "lrud"],
579
+ ["╋", "lrud"],
580
+ ["╴", "l"],
581
+ ["╶", "r"],
582
+ ["╵", "u"],
583
+ ["╷", "d"],
584
+ ["═", "lr"],
585
+ ["║", "ud"],
586
+ ["╔", "rd"],
587
+ ["╗", "ld"],
588
+ ["╚", "ru"],
589
+ ["╝", "lu"],
590
+ ["╠", "udr"],
591
+ ["╣", "udl"],
592
+ ["╦", "lrd"],
593
+ ["╩", "lru"],
594
+ ["╬", "lrud"],
595
+ ["╭", "rd"],
596
+ ["╮", "ld"],
597
+ ["╯", "lu"],
598
+ ["╰", "ru"],
599
+ ]);
600
+ const ROUNDED_BOX_CORNERS = new Set(["╭", "╮", "╯", "╰"]);
601
+ export function specialDrawingCells(text) {
602
+ const box = new Map();
603
+ const block = new Map();
604
+ let column = 0;
605
+ let simple = true;
606
+ for (let index = 0; index < text.length; index += 1) {
607
+ const codepoint = text.charCodeAt(index);
608
+ if (codepoint <= 0x7f) {
609
+ column += 1;
610
+ }
611
+ else if (codepoint >= 0x2580 && codepoint <= 0x259f) {
612
+ block.set(column, text[index]);
613
+ column += 1;
614
+ }
615
+ else if (BOX_DIRECTIONS.has(text[index])) {
616
+ box.set(column, text[index]);
617
+ column += 1;
618
+ }
619
+ else {
620
+ simple = false;
621
+ break;
622
+ }
623
+ }
624
+ if (simple)
625
+ return { box, block };
626
+ box.clear();
627
+ block.clear();
628
+ column = 0;
629
+ for (const grapheme of splitGraphemes(text)) {
630
+ const codepoint = grapheme.codePointAt(0) ?? 0;
631
+ if (codepoint >= 0x2580 && codepoint <= 0x259f)
632
+ block.set(column, grapheme);
633
+ else if (BOX_DIRECTIONS.has(grapheme))
634
+ box.set(column, grapheme);
635
+ column += graphemeCellWidth(grapheme);
636
+ }
637
+ return { box, block };
638
+ }
639
+ function rgb(color) {
640
+ return [color[0] / 255, color[1] / 255, color[2] / 255, 1];
641
+ }
642
+ function over(source, backdrop) {
643
+ if (source[3] >= 1)
644
+ return source;
645
+ if (source[3] <= 0)
646
+ return backdrop;
647
+ const alpha = source[3] + backdrop[3] * (1 - source[3]);
648
+ if (alpha <= Number.EPSILON)
649
+ return [0, 0, 0, 0];
650
+ return [
651
+ (source[0] * source[3] + backdrop[0] * backdrop[3] * (1 - source[3])) / alpha,
652
+ (source[1] * source[3] + backdrop[1] * backdrop[3] * (1 - source[3])) / alpha,
653
+ (source[2] * source[3] + backdrop[2] * backdrop[3] * (1 - source[3])) / alpha,
654
+ alpha,
655
+ ];
656
+ }
657
+ function selectionContains(selection, row, column) {
658
+ if (!selection)
659
+ return false;
660
+ const [start, end] = selection;
661
+ if (row < start.row || row > end.row)
662
+ return false;
663
+ const first = row === start.row ? start.column : 0;
664
+ const last = row === end.row ? end.column : Number.MAX_SAFE_INTEGER;
665
+ return column >= first && column <= last;
666
+ }
667
+ function resolveStyle(style, theme) {
668
+ let foreground = style?.foreground ? rgb(style.foreground) : theme.foreground;
669
+ let background = style?.background ? rgb(style.background) : null;
670
+ if (style?.inverse) {
671
+ const originalForeground = foreground;
672
+ foreground = background ?? theme.background;
673
+ background = originalForeground;
674
+ }
675
+ if (style?.faint)
676
+ foreground = [foreground[0], foreground[1], foreground[2], foreground[3] * 0.55];
677
+ return {
678
+ foreground,
679
+ background,
680
+ underline: style?.underline ?? false,
681
+ strikethrough: style?.strikethrough ?? false,
682
+ invisible: style?.invisible ?? false,
683
+ };
684
+ }
685
+ function pushBoxDrawing(output, grapheme, column, row, scale, color, viewportWidth, viewportHeight) {
686
+ const directions = BOX_DIRECTIONS.get(grapheme);
687
+ if (!directions)
688
+ return;
689
+ const left = (ORIGIN_X + column * CELL_WIDTH) * scale;
690
+ const right = (ORIGIN_X + (column + 1) * CELL_WIDTH) * scale;
691
+ const top = (ORIGIN_Y + row * LINE_HEIGHT) * scale;
692
+ const bottom = (ORIGIN_Y + (row + 1) * LINE_HEIGHT) * scale;
693
+ const stroke = Math.max(1, Math.round(scale));
694
+ const centerX = Math.round((left + right - stroke) / 2);
695
+ const centerY = Math.round((top + bottom - stroke) / 2);
696
+ if (ROUNDED_BOX_CORNERS.has(grapheme)) {
697
+ const x = centerX + stroke / 2;
698
+ const y = centerY + stroke / 2;
699
+ const points = [];
700
+ const [originX, originY, startAngle, endAngle] = grapheme === "╭"
701
+ ? [right, bottom, -Math.PI / 2, -Math.PI]
702
+ : grapheme === "╮"
703
+ ? [left, bottom, -Math.PI / 2, 0]
704
+ : grapheme === "╯"
705
+ ? [left, top, Math.PI / 2, 0]
706
+ : [right, top, Math.PI, Math.PI / 2];
707
+ const radiusX = Math.abs(right - x);
708
+ const radiusY = Math.abs(bottom - y);
709
+ const steps = Math.max(5, Math.ceil(Math.max(radiusX, radiusY) / 2));
710
+ for (let step = 0; step <= steps; step += 1) {
711
+ const angle = startAngle + (endAngle - startAngle) * (step / steps);
712
+ points.push([originX + Math.cos(angle) * radiusX, originY + Math.sin(angle) * radiusY]);
713
+ }
714
+ for (let index = 1; index < points.length; index += 1) {
715
+ const from = points[index - 1];
716
+ const to = points[index];
717
+ const dx = to[0] - from[0];
718
+ const dy = to[1] - from[1];
719
+ const length = Math.hypot(dx, dy);
720
+ if (length <= Number.EPSILON)
721
+ continue;
722
+ const nx = ((-dy / length) * stroke) / 2;
723
+ const ny = ((dx / length) * stroke) / 2;
724
+ output.push(clipX(from[0] + nx, viewportWidth), clipY(from[1] + ny, viewportHeight), clipX(to[0] + nx, viewportWidth), clipY(to[1] + ny, viewportHeight), clipX(from[0] - nx, viewportWidth), clipY(from[1] - ny, viewportHeight), ...color);
725
+ }
726
+ return;
727
+ }
728
+ if (directions.includes("u"))
729
+ pushRectangle(output, centerX, top, stroke, centerY + stroke - top, color, viewportWidth, viewportHeight);
730
+ if (directions.includes("d"))
731
+ pushRectangle(output, centerX, centerY, stroke, bottom - centerY, color, viewportWidth, viewportHeight);
732
+ if (directions.includes("l"))
733
+ pushRectangle(output, left, centerY, centerX + stroke - left, stroke, color, viewportWidth, viewportHeight);
734
+ if (directions.includes("r"))
735
+ pushRectangle(output, centerX, centerY, right - centerX, stroke, color, viewportWidth, viewportHeight);
736
+ }
737
+ const LOWER_BLOCK_RECTANGLES = Array.from({ length: 7 }, (_, index) => [
738
+ [0, 1 - (index + 1) / 8, 1, 1],
739
+ ]);
740
+ const LEFT_BLOCK_RECTANGLES = Array.from({ length: 7 }, (_, index) => [
741
+ [0, 0, (7 - index) / 8, 1],
742
+ ]);
743
+ const FULL_BLOCK_RECTANGLES = [[0, 0, 1, 1]];
744
+ const RIGHT_HALF_RECTANGLES = [[0.5, 0, 1, 1]];
745
+ const UPPER_EIGHTH_RECTANGLES = [[0, 0, 1, 1 / 8]];
746
+ const RIGHT_EIGHTH_RECTANGLES = [[7 / 8, 0, 1, 1]];
747
+ const QUADRANT_RECTANGLES = [
748
+ [0, 0, 0.5, 0.5],
749
+ [0.5, 0, 1, 0.5],
750
+ [0, 0.5, 0.5, 1],
751
+ [0.5, 0.5, 1, 1],
752
+ ];
753
+ const QUADRANT_BLOCK_RECTANGLES = new Map([
754
+ [0x2596, [2]],
755
+ [0x2597, [3]],
756
+ [0x2598, [0]],
757
+ [0x2599, [0, 2, 3]],
758
+ [0x259a, [0, 3]],
759
+ [0x259b, [0, 1, 2]],
760
+ [0x259c, [0, 1, 3]],
761
+ [0x259d, [1]],
762
+ [0x259e, [1, 2]],
763
+ [0x259f, [1, 2, 3]],
764
+ ].map(([codepoint, quadrants]) => [
765
+ codepoint,
766
+ quadrants.map((quadrant) => QUADRANT_RECTANGLES[quadrant]),
767
+ ]));
768
+ function blockElementRectangles(codepoint) {
769
+ if (codepoint >= 0x2581 && codepoint <= 0x2587)
770
+ return LOWER_BLOCK_RECTANGLES[codepoint - 0x2581];
771
+ if (codepoint === 0x2588)
772
+ return FULL_BLOCK_RECTANGLES;
773
+ if (codepoint >= 0x2589 && codepoint <= 0x258f)
774
+ return LEFT_BLOCK_RECTANGLES[codepoint - 0x2589];
775
+ if (codepoint === 0x2590)
776
+ return RIGHT_HALF_RECTANGLES;
777
+ if (codepoint === 0x2594)
778
+ return UPPER_EIGHTH_RECTANGLES;
779
+ if (codepoint === 0x2595)
780
+ return RIGHT_EIGHTH_RECTANGLES;
781
+ return QUADRANT_BLOCK_RECTANGLES.get(codepoint);
782
+ }
783
+ function pushBlockElement(output, grapheme, column, row, scale, color, viewportWidth, viewportHeight) {
784
+ const codepoint = grapheme.charCodeAt(0);
785
+ if (codepoint < 0x2580 || codepoint > 0x259f)
786
+ return false;
787
+ const cellLeft = (ORIGIN_X + column * CELL_WIDTH) * scale;
788
+ const cellRight = (ORIGIN_X + (column + 1) * CELL_WIDTH) * scale;
789
+ const cellTop = (ORIGIN_Y + row * LINE_HEIGHT) * scale;
790
+ const cellBottom = (ORIGIN_Y + (row + 1) * LINE_HEIGHT) * scale;
791
+ if (codepoint === 0x2580) {
792
+ const x0 = Math.round(cellLeft);
793
+ const x1 = Math.round(cellRight);
794
+ const y0 = Math.round(cellTop);
795
+ const y1 = Math.round(cellTop + (cellBottom - cellTop) * 0.5);
796
+ pushRectangle(output, x0, y0, x1 - x0, y1 - y0, color, viewportWidth, viewportHeight);
797
+ return true;
798
+ }
799
+ const rectangles = blockElementRectangles(codepoint);
800
+ if (!rectangles)
801
+ return false;
802
+ for (const [left, top, right, bottom] of rectangles) {
803
+ const x0 = Math.round(cellLeft + (cellRight - cellLeft) * left);
804
+ const x1 = Math.round(cellLeft + (cellRight - cellLeft) * right);
805
+ const y0 = Math.round(cellTop + (cellBottom - cellTop) * top);
806
+ const y1 = Math.round(cellTop + (cellBottom - cellTop) * bottom);
807
+ pushRectangle(output, x0, y0, x1 - x0, y1 - y0, color, viewportWidth, viewportHeight);
808
+ }
809
+ return true;
810
+ }
811
+ export class WebGpuTerminalRenderer {
812
+ device;
813
+ format;
814
+ kind = "webgpu";
815
+ #surfaces = new Map();
816
+ #rectanglePipeline;
817
+ #glyphPipeline;
818
+ #fallbackGlyphPipeline;
819
+ #colorGlyphPipeline;
820
+ #postProcessPipeline;
821
+ #postProcessSampler;
822
+ #monoAtlas;
823
+ #colorAtlas;
824
+ #fallbackAtlas;
825
+ #performanceMeasurementEnabled = false;
826
+ constructor(device, format) {
827
+ this.device = device;
828
+ this.format = format;
829
+ const rectangleModule = device.createShaderModule({ label: "terminal rectangle shader", code: RECT_SHADER });
830
+ const glyphModule = device.createShaderModule({ label: "terminal glyph shader", code: MONO_GLYPH_SHADER });
831
+ const fallbackGlyphModule = device.createShaderModule({
832
+ label: "terminal fallback glyph shader",
833
+ code: FALLBACK_GLYPH_SHADER,
834
+ });
835
+ const colorGlyphModule = device.createShaderModule({
836
+ label: "terminal color glyph shader",
837
+ code: COLOR_GLYPH_SHADER,
838
+ });
839
+ const postProcessModule = device.createShaderModule({ label: "Ghostty bettercrt shader", code: BETTER_CRT_SHADER });
840
+ this.#rectanglePipeline = device.createRenderPipeline({
841
+ label: "terminal rectangle pipeline",
842
+ layout: "auto",
843
+ vertex: {
844
+ module: rectangleModule,
845
+ entryPoint: "vertex_main",
846
+ buffers: [
847
+ {
848
+ arrayStride: 40,
849
+ stepMode: "instance",
850
+ attributes: [
851
+ { shaderLocation: 0, offset: 0, format: "float32x2" },
852
+ { shaderLocation: 1, offset: 8, format: "float32x2" },
853
+ { shaderLocation: 2, offset: 16, format: "float32x2" },
854
+ { shaderLocation: 3, offset: 24, format: "float32x4" },
855
+ ],
856
+ },
857
+ ],
858
+ },
859
+ fragment: {
860
+ module: rectangleModule,
861
+ entryPoint: "fragment_main",
862
+ targets: [{ format, blend: premultipliedBlend }],
863
+ },
864
+ primitive: { topology: "triangle-list" },
865
+ });
866
+ this.#glyphPipeline = device.createRenderPipeline({
867
+ label: "terminal glyph pipeline",
868
+ layout: "auto",
869
+ vertex: {
870
+ module: glyphModule,
871
+ entryPoint: "vertex_main",
872
+ buffers: [
873
+ {
874
+ arrayStride: 48,
875
+ stepMode: "instance",
876
+ attributes: [
877
+ { shaderLocation: 0, offset: 0, format: "float32x4" },
878
+ { shaderLocation: 1, offset: 16, format: "float32x4" },
879
+ { shaderLocation: 2, offset: 32, format: "float32x4" },
880
+ ],
881
+ },
882
+ ],
883
+ },
884
+ fragment: { module: glyphModule, entryPoint: "fragment_main", targets: [{ format, blend: premultipliedBlend }] },
885
+ primitive: { topology: "triangle-list" },
886
+ });
887
+ this.#fallbackGlyphPipeline = device.createRenderPipeline({
888
+ label: "terminal fallback glyph pipeline",
889
+ layout: "auto",
890
+ vertex: {
891
+ module: fallbackGlyphModule,
892
+ entryPoint: "vertex_main",
893
+ buffers: [
894
+ {
895
+ arrayStride: 48,
896
+ stepMode: "instance",
897
+ attributes: [
898
+ { shaderLocation: 0, offset: 0, format: "float32x4" },
899
+ { shaderLocation: 1, offset: 16, format: "float32x4" },
900
+ { shaderLocation: 2, offset: 32, format: "float32x4" },
901
+ ],
902
+ },
903
+ ],
904
+ },
905
+ fragment: {
906
+ module: fallbackGlyphModule,
907
+ entryPoint: "fragment_main",
908
+ targets: [{ format, blend: premultipliedBlend }],
909
+ },
910
+ primitive: { topology: "triangle-list" },
911
+ });
912
+ this.#colorGlyphPipeline = device.createRenderPipeline({
913
+ label: "terminal color glyph pipeline",
914
+ layout: "auto",
915
+ vertex: {
916
+ module: colorGlyphModule,
917
+ entryPoint: "vertex_main",
918
+ buffers: [
919
+ {
920
+ arrayStride: 48,
921
+ stepMode: "instance",
922
+ attributes: [
923
+ { shaderLocation: 0, offset: 0, format: "float32x4" },
924
+ { shaderLocation: 1, offset: 16, format: "float32x4" },
925
+ { shaderLocation: 2, offset: 32, format: "float32x4" },
926
+ ],
927
+ },
928
+ ],
929
+ },
930
+ fragment: {
931
+ module: colorGlyphModule,
932
+ entryPoint: "fragment_main",
933
+ targets: [{ format, blend: premultipliedBlend }],
934
+ },
935
+ primitive: { topology: "triangle-list" },
936
+ });
937
+ this.#postProcessPipeline = device.createRenderPipeline({
938
+ label: "Ghostty custom shader pipeline",
939
+ layout: "auto",
940
+ vertex: { module: postProcessModule, entryPoint: "vertex_main" },
941
+ fragment: { module: postProcessModule, entryPoint: "fragment_main", targets: [{ format }] },
942
+ primitive: { topology: "triangle-list" },
943
+ });
944
+ this.#postProcessSampler = device.createSampler({
945
+ label: "Ghostty custom shader sampler",
946
+ addressModeU: "clamp-to-edge",
947
+ addressModeV: "clamp-to-edge",
948
+ minFilter: "linear",
949
+ magFilter: "linear",
950
+ });
951
+ this.#monoAtlas = new NativeGlyphAtlas(device, this.#glyphPipeline.getBindGroupLayout(0), "native monochrome glyph atlas", "r8unorm");
952
+ this.#colorAtlas = new NativeGlyphAtlas(device, this.#colorGlyphPipeline.getBindGroupLayout(0), "native color glyph atlas", "rgba8unorm");
953
+ this.#fallbackAtlas = new FallbackGlyphAtlas(device, this.#fallbackGlyphPipeline.getBindGroupLayout(0));
954
+ }
955
+ static async create(onLost) {
956
+ if (!navigator.gpu)
957
+ throw new Error("WebGPU is unavailable");
958
+ const adapter = await navigator.gpu.requestAdapter();
959
+ if (!adapter)
960
+ throw new Error("No WebGPU adapter is available");
961
+ const device = await adapter.requestDevice({ label: "Electron Ghostty terminal renderer" });
962
+ device.addEventListener("uncapturederror", (event) => {
963
+ console.error("[terminal-renderer] WebGPU validation error", event.error);
964
+ });
965
+ void device.lost.then((info) => {
966
+ if (info.reason !== "destroyed")
967
+ onLost(info);
968
+ });
969
+ return new WebGpuTerminalRenderer(device, navigator.gpu.getPreferredCanvasFormat());
970
+ }
971
+ setPerformanceMeasurementEnabled(enabled) {
972
+ this.#performanceMeasurementEnabled = enabled;
973
+ }
974
+ mount(id, canvas) {
975
+ const context = canvas.getContext("webgpu");
976
+ if (!context)
977
+ throw new Error("WebGPU canvas context unavailable");
978
+ const sceneTexture = this.#createSceneTexture(canvas.width, canvas.height);
979
+ const surface = {
980
+ canvas,
981
+ context,
982
+ sceneTexture,
983
+ postProcessBindGroup: this.#createPostProcessBindGroup(sceneTexture),
984
+ width: 1,
985
+ height: 1,
986
+ dpr: 1,
987
+ rectangleBuffer: new DynamicVertexBuffer(this.device, `rectangles ${id}`),
988
+ glyphBuffer: new DynamicVertexBuffer(this.device, `glyphs ${id}`),
989
+ colorGlyphBuffer: new DynamicVertexBuffer(this.device, `color glyphs ${id}`),
990
+ fallbackGlyphBuffer: new DynamicVertexBuffer(this.device, `fallback glyphs ${id}`),
991
+ cursorBuffer: new DynamicVertexBuffer(this.device, `cursor ${id}`),
992
+ geometryCache: new Map(),
993
+ geometryCandidates: new Map(),
994
+ sceneValid: false,
995
+ };
996
+ this.#surfaces.set(id, surface);
997
+ this.#configure(surface);
998
+ }
999
+ unmount(id) {
1000
+ const surface = this.#surfaces.get(id);
1001
+ if (!surface)
1002
+ return;
1003
+ surface.context.unconfigure();
1004
+ surface.sceneTexture.destroy();
1005
+ surface.rectangleBuffer.destroy();
1006
+ surface.glyphBuffer.destroy();
1007
+ surface.colorGlyphBuffer.destroy();
1008
+ surface.fallbackGlyphBuffer.destroy();
1009
+ surface.cursorBuffer.destroy();
1010
+ clearGeometryCache(surface);
1011
+ this.#surfaces.delete(id);
1012
+ }
1013
+ resize(id, size) {
1014
+ const surface = this.#surfaces.get(id);
1015
+ if (!surface)
1016
+ return;
1017
+ const changed = renderedSizeChanged(surface, size);
1018
+ Object.assign(surface, size);
1019
+ if (!changed)
1020
+ return;
1021
+ surface.canvas.width = Math.max(1, Math.round(size.width * size.dpr));
1022
+ surface.canvas.height = Math.max(1, Math.round(size.height * size.dpr));
1023
+ this.#configure(surface);
1024
+ }
1025
+ #configure(surface) {
1026
+ surface.context.configure({ device: this.device, format: this.format, alphaMode: "premultiplied" });
1027
+ surface.sceneTexture.destroy();
1028
+ surface.sceneTexture = this.#createSceneTexture(surface.canvas.width, surface.canvas.height);
1029
+ surface.postProcessBindGroup = this.#createPostProcessBindGroup(surface.sceneTexture);
1030
+ clearGeometryCache(surface);
1031
+ surface.sceneValid = false;
1032
+ }
1033
+ #createSceneTexture(width, height) {
1034
+ return this.device.createTexture({
1035
+ label: "terminal scene before Ghostty custom shaders",
1036
+ size: [Math.max(1, width), Math.max(1, height)],
1037
+ format: this.format,
1038
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1039
+ });
1040
+ }
1041
+ #createPostProcessBindGroup(texture) {
1042
+ return this.device.createBindGroup({
1043
+ label: "Ghostty custom shader bindings",
1044
+ layout: this.#postProcessPipeline.getBindGroupLayout(0),
1045
+ entries: [
1046
+ { binding: 0, resource: texture.createView() },
1047
+ { binding: 1, resource: this.#postProcessSampler },
1048
+ ],
1049
+ });
1050
+ }
1051
+ render(id, view) {
1052
+ if (!this.#surfaces.has(id))
1053
+ return;
1054
+ const encoder = this.device.createCommandEncoder({ label: `terminal frame ${id}` });
1055
+ const metrics = view.damage?.geometryChanged === false
1056
+ ? this.#encodeCachedRender(id, view, encoder)
1057
+ : this.#encodeRender(id, view, encoder);
1058
+ this.device.queue.submit([encoder.finish()]);
1059
+ if (metrics)
1060
+ metrics.queueSubmits = 1;
1061
+ return metrics;
1062
+ }
1063
+ renderBatch(entries) {
1064
+ const active = entries.filter(({ id }) => this.#surfaces.has(id));
1065
+ if (active.length === 0)
1066
+ return entries.map(() => undefined);
1067
+ const encoder = this.device.createCommandEncoder({ label: `terminal frame batch (${active.length} panes)` });
1068
+ const byId = new Map();
1069
+ for (const { id, view } of active) {
1070
+ byId.set(id, view.damage?.geometryChanged === false
1071
+ ? this.#encodeCachedRender(id, view, encoder)
1072
+ : this.#encodeRender(id, view, encoder));
1073
+ }
1074
+ this.device.queue.submit([encoder.finish()]);
1075
+ const firstMetrics = [...byId.values()].find((metrics) => metrics !== undefined);
1076
+ if (firstMetrics)
1077
+ firstMetrics.queueSubmits = 1;
1078
+ return entries.map(({ id }) => byId.get(id));
1079
+ }
1080
+ #buildGeometry(view, renderRows, rowCount, fullRedraw, hasNativeRows, scale, viewportWidth, viewportHeight) {
1081
+ const rectangleVertices = [];
1082
+ const glyphVertices = [];
1083
+ const colorGlyphVertices = [];
1084
+ const fallbackGlyphVertices = [];
1085
+ const resolvedStyles = new Map();
1086
+ const styleFor = (styleId) => {
1087
+ const cached = resolvedStyles.get(styleId);
1088
+ if (cached)
1089
+ return cached;
1090
+ const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
1091
+ resolvedStyles.set(styleId, resolved);
1092
+ return resolved;
1093
+ };
1094
+ const monoDefinitions = new Map();
1095
+ const colorDefinitions = new Map();
1096
+ for (const row of renderRows) {
1097
+ for (const glyph of view.nativeRows[row] ?? []) {
1098
+ const definition = view.glyphDefinitions.get(glyph.glyphId);
1099
+ if (!definition)
1100
+ continue;
1101
+ (definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(definition.id, definition);
1102
+ }
1103
+ }
1104
+ this.#monoAtlas.prepare(monoDefinitions.values());
1105
+ this.#colorAtlas.prepare(colorDefinitions.values());
1106
+ if (!fullRedraw) {
1107
+ for (const row of renderRows) {
1108
+ const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
1109
+ const bottom = row === rowCount - 1
1110
+ ? viewportHeight
1111
+ : Math.min(viewportHeight, (ORIGIN_Y + (row + 1) * LINE_HEIGHT) * scale);
1112
+ pushRectangle(rectangleVertices, 0, top, viewportWidth, Math.max(0, bottom - top),
1113
+ // The rectangle pipeline blends. Force the row reset opaque so it
1114
+ // replaces stale scene pixels even when a theme carries alpha.
1115
+ [view.theme.background[0], view.theme.background[1], view.theme.background[2], 1], viewportWidth, viewportHeight);
1116
+ }
1117
+ }
1118
+ for (const row of renderRows) {
1119
+ for (const run of view.nativeStyleRows[row] ?? []) {
1120
+ const style = styleFor(run.styleId);
1121
+ if (!style.background)
1122
+ continue;
1123
+ pushRectangle(rectangleVertices, (ORIGIN_X + run.cellStart * CELL_WIDTH) * scale, (ORIGIN_Y + row * LINE_HEIGHT) * scale, run.cellSpan * CELL_WIDTH * scale, LINE_HEIGHT * scale, style.background, viewportWidth, viewportHeight);
1124
+ }
1125
+ }
1126
+ const backgroundInstanceCount = rectangleVertices.length / 10;
1127
+ const selection = ordered(view.selection);
1128
+ if (selection) {
1129
+ const [start, end] = selection;
1130
+ for (const row of renderRows) {
1131
+ if (row < start.row || row > end.row)
1132
+ continue;
1133
+ const first = row === start.row ? start.column : 0;
1134
+ const last = row === end.row ? end.column : Math.max(0, cellLength(view.rows[row] ?? "") - 1);
1135
+ pushRectangle(rectangleVertices, (ORIGIN_X + first * CELL_WIDTH) * scale, (ORIGIN_Y + row * LINE_HEIGHT) * scale, Math.max(1, last - first + 1) * CELL_WIDTH * scale, LINE_HEIGHT * scale, view.theme.selection, viewportWidth, viewportHeight);
1136
+ }
1137
+ }
1138
+ const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
1139
+ if (!hasNativeRows) {
1140
+ this.#fallbackAtlas.prepare(renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")), scale);
1141
+ }
1142
+ if (hasNativeRows) {
1143
+ for (const row of renderRows) {
1144
+ const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
1145
+ for (const instance of view.nativeRows[row] ?? []) {
1146
+ const definition = view.glyphDefinitions.get(instance.glyphId);
1147
+ if (!definition)
1148
+ continue;
1149
+ const style = styleFor(instance.styleId);
1150
+ if (style.invisible)
1151
+ continue;
1152
+ const isSelected = selectionContains(selection, row, instance.cellStart);
1153
+ const foreground = isSelected ? view.theme.selectionForeground : style.foreground;
1154
+ const boxDrawing = boxCells.get(instance.cellStart);
1155
+ if (boxDrawing) {
1156
+ let backdrop = style.background ?? view.theme.background;
1157
+ if (isSelected)
1158
+ backdrop = view.theme.selection;
1159
+ // Box glyphs are overlapping primitives. Flatten faint opacity
1160
+ // against the cell backdrop so joins are blended only once.
1161
+ pushBoxDrawing(rectangleVertices, boxDrawing, instance.cellStart, row, scale, over(foreground, backdrop), viewportWidth, viewportHeight);
1162
+ continue;
1163
+ }
1164
+ const blockElement = blockCells.get(instance.cellStart);
1165
+ if (blockElement) {
1166
+ let backdrop = style.background ?? view.theme.background;
1167
+ if (isSelected)
1168
+ backdrop = view.theme.selection;
1169
+ if (pushBlockElement(rectangleVertices, blockElement, instance.cellStart, row, scale, over(foreground, backdrop), viewportWidth, viewportHeight))
1170
+ continue;
1171
+ }
1172
+ const atlas = definition.format === GlyphFormat.Alpha8 ? this.#monoAtlas : this.#colorAtlas;
1173
+ const vertices = definition.format === GlyphFormat.Alpha8 ? glyphVertices : colorGlyphVertices;
1174
+ pushGlyph(vertices, (ORIGIN_X + instance.x) * scale, (ORIGIN_Y + row * LINE_HEIGHT + instance.y) * scale, instance.width * scale, instance.height * scale, atlas.glyph(definition), foreground, viewportWidth, viewportHeight);
1175
+ }
1176
+ }
1177
+ }
1178
+ else {
1179
+ for (const row of renderRows) {
1180
+ let column = 0;
1181
+ for (const grapheme of splitGraphemes(view.rows[row] ?? "")) {
1182
+ const glyph = this.#fallbackAtlas.glyph(grapheme, scale);
1183
+ const cells = glyph?.cells ?? graphemeCellWidth(grapheme);
1184
+ if (glyph) {
1185
+ const foreground = selectionContains(selection, row, column)
1186
+ ? view.theme.selectionForeground
1187
+ : view.theme.foreground;
1188
+ pushGlyph(fallbackGlyphVertices, (ORIGIN_X + column * CELL_WIDTH) * scale, (ORIGIN_Y + row * LINE_HEIGHT) * scale, CELL_WIDTH * cells * scale, LINE_HEIGHT * scale, glyph, foreground, viewportWidth, viewportHeight);
1189
+ }
1190
+ column += cells;
1191
+ }
1192
+ }
1193
+ }
1194
+ for (const row of renderRows) {
1195
+ for (const run of view.nativeStyleRows[row] ?? []) {
1196
+ const style = styleFor(run.styleId);
1197
+ if (style.invisible)
1198
+ continue;
1199
+ const x = (ORIGIN_X + run.cellStart * CELL_WIDTH) * scale;
1200
+ const width = run.cellSpan * CELL_WIDTH * scale;
1201
+ const rowTop = (ORIGIN_Y + row * LINE_HEIGHT) * scale;
1202
+ const stroke = Math.max(1, Math.round(scale));
1203
+ if (style.underline) {
1204
+ pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.foreground, viewportWidth, viewportHeight);
1205
+ }
1206
+ if (style.strikethrough) {
1207
+ pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.foreground, viewportWidth, viewportHeight);
1208
+ }
1209
+ }
1210
+ }
1211
+ const decorationInstanceStart = backgroundInstanceCount + selectionInstanceCount;
1212
+ const decorationInstanceCount = rectangleVertices.length / 10 - decorationInstanceStart;
1213
+ return {
1214
+ rectangleData: new Float32Array(rectangleVertices),
1215
+ glyphData: new Float32Array(glyphVertices),
1216
+ colorGlyphData: new Float32Array(colorGlyphVertices),
1217
+ fallbackGlyphData: new Float32Array(fallbackGlyphVertices),
1218
+ backgroundInstanceCount,
1219
+ selectionInstanceCount,
1220
+ decorationInstanceStart,
1221
+ decorationInstanceCount,
1222
+ rectangleInstanceCount: rectangleVertices.length / 10,
1223
+ glyphInstanceCount: glyphVertices.length / 12,
1224
+ colorGlyphInstanceCount: colorGlyphVertices.length / 12,
1225
+ fallbackGlyphInstanceCount: fallbackGlyphVertices.length / 12,
1226
+ };
1227
+ }
1228
+ #encodeRender(id, view, encoder) {
1229
+ const surface = this.#surfaces.get(id);
1230
+ if (!surface)
1231
+ return;
1232
+ const monoUploadsBefore = this.#performanceMeasurementEnabled ? this.#monoAtlas.uploadMetrics() : undefined;
1233
+ const colorUploadsBefore = this.#performanceMeasurementEnabled ? this.#colorAtlas.uploadMetrics() : undefined;
1234
+ const fallbackUploadsBefore = this.#performanceMeasurementEnabled ? this.#fallbackAtlas.uploadMetrics() : undefined;
1235
+ const scale = surface.dpr;
1236
+ const viewportWidth = surface.canvas.width;
1237
+ const viewportHeight = surface.canvas.height;
1238
+ const rowCount = Math.max(view.rows.length, view.nativeRows.length, view.nativeStyleRows.length);
1239
+ const { full: fullRedraw, rows: renderRows } = rowsForDamage(rowCount, view.damage, surface.sceneValid);
1240
+ const renderRowSet = new Set(renderRows);
1241
+ const rectangleVertices = [];
1242
+ const glyphVertices = [];
1243
+ const colorGlyphVertices = [];
1244
+ const fallbackGlyphVertices = [];
1245
+ const resolvedStyles = new Map();
1246
+ const styleFor = (styleId) => {
1247
+ const cached = resolvedStyles.get(styleId);
1248
+ if (cached)
1249
+ return cached;
1250
+ const resolved = resolveStyle(view.styleDefinitions.get(styleId), view.theme);
1251
+ resolvedStyles.set(styleId, resolved);
1252
+ return resolved;
1253
+ };
1254
+ const monoDefinitions = new Map();
1255
+ const colorDefinitions = new Map();
1256
+ for (const row of renderRows) {
1257
+ for (const glyph of view.nativeRows[row] ?? []) {
1258
+ const definition = view.glyphDefinitions.get(glyph.glyphId);
1259
+ if (!definition)
1260
+ continue;
1261
+ (definition.format === GlyphFormat.Rgba8Premultiplied ? colorDefinitions : monoDefinitions).set(definition.id, definition);
1262
+ }
1263
+ }
1264
+ this.#monoAtlas.prepare(monoDefinitions.values());
1265
+ this.#colorAtlas.prepare(colorDefinitions.values());
1266
+ if (!fullRedraw) {
1267
+ for (const row of renderRows) {
1268
+ const top = row === 0 ? 0 : (ORIGIN_Y + row * LINE_HEIGHT) * scale;
1269
+ const bottom = row === rowCount - 1
1270
+ ? viewportHeight
1271
+ : Math.min(viewportHeight, (ORIGIN_Y + (row + 1) * LINE_HEIGHT) * scale);
1272
+ pushRectangle(rectangleVertices, 0, top, viewportWidth, Math.max(0, bottom - top),
1273
+ // The rectangle pipeline blends. Force the row reset opaque so it
1274
+ // replaces stale scene pixels even when a theme carries alpha.
1275
+ [view.theme.background[0], view.theme.background[1], view.theme.background[2], 1], viewportWidth, viewportHeight);
1276
+ }
1277
+ }
1278
+ for (const row of renderRows) {
1279
+ for (const run of view.nativeStyleRows[row] ?? []) {
1280
+ const style = styleFor(run.styleId);
1281
+ if (!style.background)
1282
+ continue;
1283
+ pushRectangle(rectangleVertices, (ORIGIN_X + run.cellStart * CELL_WIDTH) * scale, (ORIGIN_Y + row * LINE_HEIGHT) * scale, run.cellSpan * CELL_WIDTH * scale, LINE_HEIGHT * scale, style.background, viewportWidth, viewportHeight);
1284
+ }
1285
+ }
1286
+ const backgroundInstanceCount = rectangleVertices.length / 10;
1287
+ const selection = ordered(view.selection);
1288
+ if (selection) {
1289
+ const [start, end] = selection;
1290
+ for (const row of renderRows) {
1291
+ if (row < start.row || row > end.row)
1292
+ continue;
1293
+ const first = row === start.row ? start.column : 0;
1294
+ const last = row === end.row ? end.column : Math.max(0, cellLength(view.rows[row] ?? "") - 1);
1295
+ pushRectangle(rectangleVertices, (ORIGIN_X + first * CELL_WIDTH) * scale, (ORIGIN_Y + row * LINE_HEIGHT) * scale, Math.max(1, last - first + 1) * CELL_WIDTH * scale, LINE_HEIGHT * scale, view.theme.selection, viewportWidth, viewportHeight);
1296
+ }
1297
+ }
1298
+ const selectionInstanceCount = rectangleVertices.length / 10 - backgroundInstanceCount;
1299
+ const hasNativeRows = view.nativeRows.some((row) => row.length > 0);
1300
+ if (!hasNativeRows) {
1301
+ this.#fallbackAtlas.prepare(renderRows.flatMap((row) => splitGraphemes(view.rows[row] ?? "")), scale);
1302
+ }
1303
+ if (hasNativeRows) {
1304
+ for (const row of renderRows) {
1305
+ const { box: boxCells, block: blockCells } = specialDrawingCells(view.rows[row] ?? "");
1306
+ for (const instance of view.nativeRows[row] ?? []) {
1307
+ const definition = view.glyphDefinitions.get(instance.glyphId);
1308
+ if (!definition)
1309
+ continue;
1310
+ const style = styleFor(instance.styleId);
1311
+ if (style.invisible)
1312
+ continue;
1313
+ const isSelected = selectionContains(selection, row, instance.cellStart);
1314
+ const foreground = isSelected ? view.theme.selectionForeground : style.foreground;
1315
+ const boxDrawing = boxCells.get(instance.cellStart);
1316
+ if (boxDrawing) {
1317
+ let backdrop = style.background ?? view.theme.background;
1318
+ if (isSelected)
1319
+ backdrop = view.theme.selection;
1320
+ // Box glyphs are assembled from several overlapping primitives. Flatten
1321
+ // faint opacity against the cell backdrop first so joins are blended once,
1322
+ // instead of producing a brighter seam at every cell boundary.
1323
+ const boxColor = over(foreground, backdrop);
1324
+ pushBoxDrawing(rectangleVertices, boxDrawing, instance.cellStart, row, scale, boxColor, viewportWidth, viewportHeight);
1325
+ continue;
1326
+ }
1327
+ const blockElement = blockCells.get(instance.cellStart);
1328
+ if (blockElement) {
1329
+ let backdrop = style.background ?? view.theme.background;
1330
+ if (isSelected)
1331
+ backdrop = view.theme.selection;
1332
+ const blockColor = over(foreground, backdrop);
1333
+ if (pushBlockElement(rectangleVertices, blockElement, instance.cellStart, row, scale, blockColor, viewportWidth, viewportHeight))
1334
+ continue;
1335
+ }
1336
+ const atlas = definition.format === GlyphFormat.Alpha8 ? this.#monoAtlas : this.#colorAtlas;
1337
+ const vertices = definition.format === GlyphFormat.Alpha8 ? glyphVertices : colorGlyphVertices;
1338
+ pushGlyph(vertices, (ORIGIN_X + instance.x) * scale, (ORIGIN_Y + row * LINE_HEIGHT + instance.y) * scale, instance.width * scale, instance.height * scale, atlas.glyph(definition), foreground, viewportWidth, viewportHeight);
1339
+ }
1340
+ }
1341
+ }
1342
+ else {
1343
+ for (const row of renderRows) {
1344
+ let column = 0;
1345
+ for (const grapheme of splitGraphemes(view.rows[row] ?? "")) {
1346
+ const glyph = this.#fallbackAtlas.glyph(grapheme, scale);
1347
+ const cells = glyph?.cells ?? graphemeCellWidth(grapheme);
1348
+ if (glyph) {
1349
+ const foreground = selectionContains(selection, row, column)
1350
+ ? view.theme.selectionForeground
1351
+ : view.theme.foreground;
1352
+ pushGlyph(fallbackGlyphVertices, (ORIGIN_X + column * CELL_WIDTH) * scale, (ORIGIN_Y + row * LINE_HEIGHT) * scale, CELL_WIDTH * cells * scale, LINE_HEIGHT * scale, glyph, foreground, viewportWidth, viewportHeight);
1353
+ }
1354
+ column += cells;
1355
+ }
1356
+ }
1357
+ }
1358
+ for (const row of renderRows) {
1359
+ for (const run of view.nativeStyleRows[row] ?? []) {
1360
+ const style = styleFor(run.styleId);
1361
+ if (style.invisible)
1362
+ continue;
1363
+ const x = (ORIGIN_X + run.cellStart * CELL_WIDTH) * scale;
1364
+ const width = run.cellSpan * CELL_WIDTH * scale;
1365
+ const rowTop = (ORIGIN_Y + row * LINE_HEIGHT) * scale;
1366
+ const stroke = Math.max(1, Math.round(scale));
1367
+ if (style.underline) {
1368
+ pushRectangle(rectangleVertices, x, Math.round(rowTop + 16 * scale), width, stroke, style.foreground, viewportWidth, viewportHeight);
1369
+ }
1370
+ if (style.strikethrough) {
1371
+ pushRectangle(rectangleVertices, x, Math.round(rowTop + 9 * scale), width, stroke, style.foreground, viewportWidth, viewportHeight);
1372
+ }
1373
+ }
1374
+ }
1375
+ const decorationInstanceStart = backgroundInstanceCount + selectionInstanceCount;
1376
+ const decorationInstanceCount = rectangleVertices.length / 10 - decorationInstanceStart;
1377
+ const cursorStyle = effectiveCursorStyle(view);
1378
+ if (cursorStyle !== null && renderRowSet.has(view.cursor.y)) {
1379
+ const x = (ORIGIN_X + view.cursor.x * CELL_WIDTH) * scale;
1380
+ const y = (ORIGIN_Y + view.cursor.y * LINE_HEIGHT) * scale;
1381
+ const width = CELL_WIDTH * scale;
1382
+ const height = LINE_HEIGHT * scale;
1383
+ const stroke = Math.max(1, Math.round(scale));
1384
+ const cursorColor = cursorStyle === CursorStyle.HollowBlock
1385
+ ? [view.theme.cursor[0], view.theme.cursor[1], view.theme.cursor[2], 1]
1386
+ : view.theme.cursor;
1387
+ if (cursorStyle === CursorStyle.Bar) {
1388
+ pushRectangle(rectangleVertices, x, y, Math.max(2, Math.round(2 * scale)), height, cursorColor, viewportWidth, viewportHeight);
1389
+ }
1390
+ else if (cursorStyle === CursorStyle.Underline) {
1391
+ const thickness = Math.max(2, Math.round(2 * scale));
1392
+ pushRectangle(rectangleVertices, x, y + height - thickness, width, thickness, cursorColor, viewportWidth, viewportHeight);
1393
+ }
1394
+ else if (cursorStyle === CursorStyle.HollowBlock) {
1395
+ pushRectangle(rectangleVertices, x, y, width, stroke, cursorColor, viewportWidth, viewportHeight);
1396
+ pushRectangle(rectangleVertices, x, y + height - stroke, width, stroke, cursorColor, viewportWidth, viewportHeight);
1397
+ pushRectangle(rectangleVertices, x, y + stroke, stroke, Math.max(0, height - stroke * 2), cursorColor, viewportWidth, viewportHeight);
1398
+ pushRectangle(rectangleVertices, x + width - stroke, y + stroke, stroke, Math.max(0, height - stroke * 2), cursorColor, viewportWidth, viewportHeight);
1399
+ }
1400
+ else {
1401
+ pushRectangle(rectangleVertices, x, y, width, height, cursorColor, viewportWidth, viewportHeight);
1402
+ }
1403
+ }
1404
+ const rectangleData = new Float32Array(rectangleVertices);
1405
+ const glyphData = new Float32Array(glyphVertices);
1406
+ const colorGlyphData = new Float32Array(colorGlyphVertices);
1407
+ const fallbackGlyphData = new Float32Array(fallbackGlyphVertices);
1408
+ const rectangleBuffer = surface.rectangleBuffer.write(rectangleData);
1409
+ const glyphBuffer = surface.glyphBuffer.write(glyphData);
1410
+ const colorGlyphBuffer = surface.colorGlyphBuffer.write(colorGlyphData);
1411
+ const fallbackGlyphBuffer = surface.fallbackGlyphBuffer.write(fallbackGlyphData);
1412
+ const cursorInstanceStart = decorationInstanceStart + decorationInstanceCount;
1413
+ const cursorInstanceCount = rectangleVertices.length / 10 - cursorInstanceStart;
1414
+ const pass = encoder.beginRenderPass({
1415
+ label: `terminal pass ${id}`,
1416
+ colorAttachments: [
1417
+ {
1418
+ view: surface.sceneTexture.createView(),
1419
+ clearValue: {
1420
+ r: view.theme.background[0],
1421
+ g: view.theme.background[1],
1422
+ b: view.theme.background[2],
1423
+ a: view.theme.background[3],
1424
+ },
1425
+ loadOp: fullRedraw ? "clear" : "load",
1426
+ storeOp: "store",
1427
+ },
1428
+ ],
1429
+ });
1430
+ if (rectangleBuffer && backgroundInstanceCount > 0) {
1431
+ pass.setPipeline(this.#rectanglePipeline);
1432
+ pass.setVertexBuffer(0, rectangleBuffer);
1433
+ pass.draw(6, backgroundInstanceCount);
1434
+ }
1435
+ if (rectangleBuffer && selectionInstanceCount > 0) {
1436
+ pass.setPipeline(this.#rectanglePipeline);
1437
+ pass.setVertexBuffer(0, rectangleBuffer);
1438
+ pass.draw(6, selectionInstanceCount, 0, backgroundInstanceCount);
1439
+ }
1440
+ if (glyphBuffer && glyphData.length > 0) {
1441
+ pass.setPipeline(this.#glyphPipeline);
1442
+ pass.setBindGroup(0, this.#monoAtlas.bindGroup);
1443
+ pass.setVertexBuffer(0, glyphBuffer);
1444
+ pass.draw(6, glyphVertices.length / 12);
1445
+ }
1446
+ if (colorGlyphBuffer && colorGlyphData.length > 0) {
1447
+ pass.setPipeline(this.#colorGlyphPipeline);
1448
+ pass.setBindGroup(0, this.#colorAtlas.bindGroup);
1449
+ pass.setVertexBuffer(0, colorGlyphBuffer);
1450
+ pass.draw(6, colorGlyphVertices.length / 12);
1451
+ }
1452
+ if (fallbackGlyphBuffer && fallbackGlyphData.length > 0) {
1453
+ pass.setPipeline(this.#fallbackGlyphPipeline);
1454
+ pass.setBindGroup(0, this.#fallbackAtlas.bindGroup);
1455
+ pass.setVertexBuffer(0, fallbackGlyphBuffer);
1456
+ pass.draw(6, fallbackGlyphVertices.length / 12);
1457
+ }
1458
+ if (rectangleBuffer && decorationInstanceCount > 0) {
1459
+ pass.setPipeline(this.#rectanglePipeline);
1460
+ pass.setVertexBuffer(0, rectangleBuffer);
1461
+ pass.draw(6, decorationInstanceCount, 0, decorationInstanceStart);
1462
+ }
1463
+ if (rectangleBuffer && cursorInstanceCount > 0) {
1464
+ pass.setPipeline(this.#rectanglePipeline);
1465
+ pass.setVertexBuffer(0, rectangleBuffer);
1466
+ pass.draw(6, cursorInstanceCount, 0, cursorInstanceStart);
1467
+ }
1468
+ pass.end();
1469
+ const postProcessPass = encoder.beginRenderPass({
1470
+ label: `Ghostty custom shader pass ${id}`,
1471
+ colorAttachments: [
1472
+ {
1473
+ view: surface.context.getCurrentTexture().createView(),
1474
+ clearValue: {
1475
+ r: view.theme.background[0],
1476
+ g: view.theme.background[1],
1477
+ b: view.theme.background[2],
1478
+ a: 1,
1479
+ },
1480
+ loadOp: "clear",
1481
+ storeOp: "store",
1482
+ },
1483
+ ],
1484
+ });
1485
+ postProcessPass.setPipeline(this.#postProcessPipeline);
1486
+ postProcessPass.setBindGroup(0, surface.postProcessBindGroup);
1487
+ postProcessPass.draw(3);
1488
+ postProcessPass.end();
1489
+ surface.sceneValid = true;
1490
+ if (!this.#performanceMeasurementEnabled)
1491
+ return undefined;
1492
+ const monoUploadsAfter = this.#monoAtlas.uploadMetrics();
1493
+ const colorUploadsAfter = this.#colorAtlas.uploadMetrics();
1494
+ const fallbackUploadsAfter = this.#fallbackAtlas.uploadMetrics();
1495
+ return {
1496
+ queueSubmits: 0,
1497
+ fullRenders: Number(fullRedraw),
1498
+ partialRenders: Number(!fullRedraw),
1499
+ damagedRows: renderRows.length,
1500
+ geometryCacheHits: 0,
1501
+ geometryCacheMisses: 0,
1502
+ canvasPixels: viewportWidth * viewportHeight,
1503
+ renderPasses: 2,
1504
+ drawCalls: Number(backgroundInstanceCount > 0) +
1505
+ Number(selectionInstanceCount > 0) +
1506
+ Number(glyphData.length > 0) +
1507
+ Number(colorGlyphData.length > 0) +
1508
+ Number(fallbackGlyphData.length > 0) +
1509
+ Number(decorationInstanceCount > 0) +
1510
+ Number(cursorInstanceCount > 0) +
1511
+ 1,
1512
+ rectangleVertices: (rectangleVertices.length / 10) * 6,
1513
+ monoGlyphVertices: (glyphVertices.length / 12) * 6,
1514
+ colorGlyphVertices: (colorGlyphVertices.length / 12) * 6,
1515
+ fallbackGlyphVertices: (fallbackGlyphVertices.length / 12) * 6,
1516
+ vertexUploadBytes: rectangleData.byteLength + glyphData.byteLength + colorGlyphData.byteLength + fallbackGlyphData.byteLength,
1517
+ atlasUploadBytes: monoUploadsAfter.bytes -
1518
+ monoUploadsBefore.bytes +
1519
+ (colorUploadsAfter.bytes - colorUploadsBefore.bytes) +
1520
+ (fallbackUploadsAfter.bytes - fallbackUploadsBefore.bytes),
1521
+ atlasUploadCalls: monoUploadsAfter.calls -
1522
+ monoUploadsBefore.calls +
1523
+ (colorUploadsAfter.calls - colorUploadsBefore.calls) +
1524
+ (fallbackUploadsAfter.calls - fallbackUploadsBefore.calls),
1525
+ };
1526
+ }
1527
+ #encodeCachedRender(id, view, encoder) {
1528
+ const surface = this.#surfaces.get(id);
1529
+ if (!surface)
1530
+ return;
1531
+ if (!surface.sceneValid || !view.damage || view.damage.full)
1532
+ return this.#encodeRender(id, view, encoder);
1533
+ const rowCount = Math.max(view.rows.length, view.nativeRows.length, view.nativeStyleRows.length);
1534
+ const damage = rowsForDamage(rowCount, view.damage, surface.sceneValid);
1535
+ const monoUploadsBefore = this.#performanceMeasurementEnabled ? this.#monoAtlas.uploadMetrics() : undefined;
1536
+ const colorUploadsBefore = this.#performanceMeasurementEnabled ? this.#colorAtlas.uploadMetrics() : undefined;
1537
+ const fallbackUploadsBefore = this.#performanceMeasurementEnabled ? this.#fallbackAtlas.uploadMetrics() : undefined;
1538
+ const scale = surface.dpr;
1539
+ const viewportWidth = surface.canvas.width;
1540
+ const viewportHeight = surface.canvas.height;
1541
+ const renderRowSet = new Set(damage.rows);
1542
+ const hasNativeRows = view.nativeRows.some((row) => row.length > 0);
1543
+ let geometry;
1544
+ let geometryUploadBytes = 0;
1545
+ const generations = () => [
1546
+ this.#monoAtlas.generation,
1547
+ this.#colorAtlas.generation,
1548
+ this.#fallbackAtlas.generation,
1549
+ ];
1550
+ let key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
1551
+ const cached = surface.geometryCache.get(key);
1552
+ const cacheHit = cached !== undefined;
1553
+ if (cached) {
1554
+ geometry = cached;
1555
+ surface.geometryCache.delete(key);
1556
+ surface.geometryCache.set(key, cached);
1557
+ }
1558
+ else {
1559
+ const admitted = surface.geometryCandidates.has(key);
1560
+ const candidateKey = key;
1561
+ const cpu = this.#buildGeometry(view, damage.rows, rowCount, false, hasNativeRows, scale, viewportWidth, viewportHeight);
1562
+ key = geometryCacheKey(view, damage.rows, hasNativeRows, generations());
1563
+ const promote = admitted && key === candidateKey;
1564
+ geometry = {
1565
+ rectangleBuffer: promote
1566
+ ? createCachedVertexBuffer(this.device, `cached rectangles ${id}`, cpu.rectangleData)
1567
+ : surface.rectangleBuffer.write(cpu.rectangleData),
1568
+ glyphBuffer: promote
1569
+ ? createCachedVertexBuffer(this.device, `cached glyphs ${id}`, cpu.glyphData)
1570
+ : surface.glyphBuffer.write(cpu.glyphData),
1571
+ colorGlyphBuffer: promote
1572
+ ? createCachedVertexBuffer(this.device, `cached color glyphs ${id}`, cpu.colorGlyphData)
1573
+ : surface.colorGlyphBuffer.write(cpu.colorGlyphData),
1574
+ fallbackGlyphBuffer: promote
1575
+ ? createCachedVertexBuffer(this.device, `cached fallback glyphs ${id}`, cpu.fallbackGlyphData)
1576
+ : surface.fallbackGlyphBuffer.write(cpu.fallbackGlyphData),
1577
+ backgroundInstanceCount: cpu.backgroundInstanceCount,
1578
+ selectionInstanceCount: cpu.selectionInstanceCount,
1579
+ decorationInstanceStart: cpu.decorationInstanceStart,
1580
+ decorationInstanceCount: cpu.decorationInstanceCount,
1581
+ rectangleInstanceCount: cpu.rectangleInstanceCount,
1582
+ glyphInstanceCount: cpu.glyphInstanceCount,
1583
+ colorGlyphInstanceCount: cpu.colorGlyphInstanceCount,
1584
+ fallbackGlyphInstanceCount: cpu.fallbackGlyphInstanceCount,
1585
+ };
1586
+ geometryUploadBytes =
1587
+ cpu.rectangleData.byteLength +
1588
+ cpu.glyphData.byteLength +
1589
+ cpu.colorGlyphData.byteLength +
1590
+ cpu.fallbackGlyphData.byteLength;
1591
+ if (promote) {
1592
+ surface.geometryCandidates.delete(key);
1593
+ surface.geometryCache.set(key, geometry);
1594
+ while (surface.geometryCache.size > GEOMETRY_CACHE_LIMIT) {
1595
+ const oldestKey = surface.geometryCache.keys().next().value;
1596
+ if (oldestKey === undefined)
1597
+ break;
1598
+ const oldest = surface.geometryCache.get(oldestKey);
1599
+ if (oldest)
1600
+ destroyCachedGeometry(oldest);
1601
+ surface.geometryCache.delete(oldestKey);
1602
+ }
1603
+ }
1604
+ else {
1605
+ surface.geometryCandidates.delete(key);
1606
+ surface.geometryCandidates.set(key, true);
1607
+ while (surface.geometryCandidates.size > GEOMETRY_CACHE_LIMIT) {
1608
+ const oldestKey = surface.geometryCandidates.keys().next().value;
1609
+ if (oldestKey === undefined)
1610
+ break;
1611
+ surface.geometryCandidates.delete(oldestKey);
1612
+ }
1613
+ }
1614
+ }
1615
+ const cursorVertices = [];
1616
+ pushCursorVertices(cursorVertices, view, renderRowSet, scale, viewportWidth, viewportHeight);
1617
+ const cursorData = new Float32Array(cursorVertices);
1618
+ const cursorBuffer = surface.cursorBuffer.write(cursorData);
1619
+ const cursorInstanceStart = 0;
1620
+ const cursorInstanceCount = cursorData.length / 10;
1621
+ const pass = encoder.beginRenderPass({
1622
+ label: `terminal pass ${id}`,
1623
+ colorAttachments: [
1624
+ {
1625
+ view: surface.sceneTexture.createView(),
1626
+ clearValue: {
1627
+ r: view.theme.background[0],
1628
+ g: view.theme.background[1],
1629
+ b: view.theme.background[2],
1630
+ a: view.theme.background[3],
1631
+ },
1632
+ loadOp: "load",
1633
+ storeOp: "store",
1634
+ },
1635
+ ],
1636
+ });
1637
+ if (geometry.rectangleBuffer && geometry.backgroundInstanceCount > 0) {
1638
+ pass.setPipeline(this.#rectanglePipeline);
1639
+ pass.setVertexBuffer(0, geometry.rectangleBuffer);
1640
+ pass.draw(6, geometry.backgroundInstanceCount);
1641
+ }
1642
+ if (geometry.rectangleBuffer && geometry.selectionInstanceCount > 0) {
1643
+ pass.setPipeline(this.#rectanglePipeline);
1644
+ pass.setVertexBuffer(0, geometry.rectangleBuffer);
1645
+ pass.draw(6, geometry.selectionInstanceCount, 0, geometry.backgroundInstanceCount);
1646
+ }
1647
+ if (geometry.glyphBuffer && geometry.glyphInstanceCount > 0) {
1648
+ pass.setPipeline(this.#glyphPipeline);
1649
+ pass.setBindGroup(0, this.#monoAtlas.bindGroup);
1650
+ pass.setVertexBuffer(0, geometry.glyphBuffer);
1651
+ pass.draw(6, geometry.glyphInstanceCount);
1652
+ }
1653
+ if (geometry.colorGlyphBuffer && geometry.colorGlyphInstanceCount > 0) {
1654
+ pass.setPipeline(this.#colorGlyphPipeline);
1655
+ pass.setBindGroup(0, this.#colorAtlas.bindGroup);
1656
+ pass.setVertexBuffer(0, geometry.colorGlyphBuffer);
1657
+ pass.draw(6, geometry.colorGlyphInstanceCount);
1658
+ }
1659
+ if (geometry.fallbackGlyphBuffer && geometry.fallbackGlyphInstanceCount > 0) {
1660
+ pass.setPipeline(this.#fallbackGlyphPipeline);
1661
+ pass.setBindGroup(0, this.#fallbackAtlas.bindGroup);
1662
+ pass.setVertexBuffer(0, geometry.fallbackGlyphBuffer);
1663
+ pass.draw(6, geometry.fallbackGlyphInstanceCount);
1664
+ }
1665
+ if (geometry.rectangleBuffer && geometry.decorationInstanceCount > 0) {
1666
+ pass.setPipeline(this.#rectanglePipeline);
1667
+ pass.setVertexBuffer(0, geometry.rectangleBuffer);
1668
+ pass.draw(6, geometry.decorationInstanceCount, 0, geometry.decorationInstanceStart);
1669
+ }
1670
+ if (cursorBuffer && cursorInstanceCount > 0) {
1671
+ pass.setPipeline(this.#rectanglePipeline);
1672
+ pass.setVertexBuffer(0, cursorBuffer);
1673
+ pass.draw(6, cursorInstanceCount, 0, cursorInstanceStart);
1674
+ }
1675
+ pass.end();
1676
+ const postProcessPass = encoder.beginRenderPass({
1677
+ label: `Ghostty custom shader pass ${id}`,
1678
+ colorAttachments: [
1679
+ {
1680
+ view: surface.context.getCurrentTexture().createView(),
1681
+ clearValue: {
1682
+ r: view.theme.background[0],
1683
+ g: view.theme.background[1],
1684
+ b: view.theme.background[2],
1685
+ a: 1,
1686
+ },
1687
+ loadOp: "clear",
1688
+ storeOp: "store",
1689
+ },
1690
+ ],
1691
+ });
1692
+ postProcessPass.setPipeline(this.#postProcessPipeline);
1693
+ postProcessPass.setBindGroup(0, surface.postProcessBindGroup);
1694
+ postProcessPass.draw(3);
1695
+ postProcessPass.end();
1696
+ surface.sceneValid = true;
1697
+ if (!this.#performanceMeasurementEnabled)
1698
+ return undefined;
1699
+ const monoUploadsAfter = this.#monoAtlas.uploadMetrics();
1700
+ const colorUploadsAfter = this.#colorAtlas.uploadMetrics();
1701
+ const fallbackUploadsAfter = this.#fallbackAtlas.uploadMetrics();
1702
+ return {
1703
+ queueSubmits: 0,
1704
+ fullRenders: 0,
1705
+ partialRenders: 1,
1706
+ damagedRows: damage.rows.length,
1707
+ geometryCacheHits: Number(cacheHit),
1708
+ geometryCacheMisses: Number(!cacheHit),
1709
+ canvasPixels: viewportWidth * viewportHeight,
1710
+ renderPasses: 2,
1711
+ drawCalls: Number(geometry.backgroundInstanceCount > 0) +
1712
+ Number(geometry.selectionInstanceCount > 0) +
1713
+ Number(geometry.glyphInstanceCount > 0) +
1714
+ Number(geometry.colorGlyphInstanceCount > 0) +
1715
+ Number(geometry.fallbackGlyphInstanceCount > 0) +
1716
+ Number(geometry.decorationInstanceCount > 0) +
1717
+ Number(cursorInstanceCount > 0) +
1718
+ 1,
1719
+ rectangleVertices: (geometry.rectangleInstanceCount + cursorInstanceCount) * 6,
1720
+ monoGlyphVertices: geometry.glyphInstanceCount * 6,
1721
+ colorGlyphVertices: geometry.colorGlyphInstanceCount * 6,
1722
+ fallbackGlyphVertices: geometry.fallbackGlyphInstanceCount * 6,
1723
+ vertexUploadBytes: geometryUploadBytes + cursorData.byteLength,
1724
+ atlasUploadBytes: monoUploadsAfter.bytes -
1725
+ monoUploadsBefore.bytes +
1726
+ (colorUploadsAfter.bytes - colorUploadsBefore.bytes) +
1727
+ (fallbackUploadsAfter.bytes - fallbackUploadsBefore.bytes),
1728
+ atlasUploadCalls: monoUploadsAfter.calls -
1729
+ monoUploadsBefore.calls +
1730
+ (colorUploadsAfter.calls - colorUploadsBefore.calls) +
1731
+ (fallbackUploadsAfter.calls - fallbackUploadsBefore.calls),
1732
+ };
1733
+ }
1734
+ settle() {
1735
+ return this.device.queue.onSubmittedWorkDone();
1736
+ }
1737
+ destroy() {
1738
+ for (const id of [...this.#surfaces.keys()])
1739
+ this.unmount(id);
1740
+ this.#monoAtlas.destroy();
1741
+ this.#colorAtlas.destroy();
1742
+ this.#fallbackAtlas.destroy();
1743
+ this.device.destroy();
1744
+ }
1745
+ }
1746
+ //# sourceMappingURL=webgpu-renderer.js.map