@hex1b/web-terminal 0.167.0-alpha.1512.1.d6b2173 → 0.167.0-alpha.1519.1.b8be265
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 +41 -7
- package/dist/backend-selection.d.ts +7 -0
- package/dist/backend-selection.d.ts.map +1 -0
- package/dist/backend-selection.js +23 -0
- package/dist/backend-selection.js.map +1 -0
- package/dist/render-backend.d.ts +31 -0
- package/dist/render-backend.d.ts.map +1 -0
- package/dist/render-backend.js +5 -0
- package/dist/render-backend.js.map +1 -0
- package/dist/renderer-options.d.ts +3 -0
- package/dist/renderer-options.d.ts.map +1 -0
- package/dist/renderer-options.js +6 -0
- package/dist/renderer-options.js.map +1 -0
- package/dist/renderer.d.ts +12 -24
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +35 -173
- package/dist/renderer.js.map +1 -1
- package/dist/terminal-worker.js +7 -4
- package/dist/terminal-worker.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web-terminal.d.ts.map +1 -1
- package/dist/web-terminal.js +8 -3
- package/dist/web-terminal.js.map +1 -1
- package/dist/webgl2-backend.d.ts +38 -0
- package/dist/webgl2-backend.d.ts.map +1 -0
- package/dist/webgl2-backend.js +456 -0
- package/dist/webgl2-backend.js.map +1 -0
- package/dist/webgpu-backend.d.ts +24 -0
- package/dist/webgpu-backend.d.ts.map +1 -0
- package/dist/webgpu-backend.js +234 -0
- package/dist/webgpu-backend.js.map +1 -0
- package/dist/wire-types.d.ts +2 -1
- package/dist/wire-types.d.ts.map +1 -1
- package/dist/wire-types.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hex1b/web-terminal
|
|
2
2
|
|
|
3
|
-
The first-party
|
|
3
|
+
The first-party GPU-rendered browser terminal for Hex1b. It renders server-authoritative
|
|
4
4
|
cells and graphics in a module worker, with local input routing, producer-backed
|
|
5
5
|
history and selection, clipboard actions, and primary/secondary view sizing.
|
|
6
6
|
There are no runtime package dependencies.
|
|
@@ -53,10 +53,42 @@ connection, not the container or server-side shared terminal.
|
|
|
53
53
|
|
|
54
54
|
### Browser and deployment requirements
|
|
55
55
|
|
|
56
|
-
Use
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
Use a browser with WebGPU or WebGL2, module workers, transferable OffscreenCanvas,
|
|
57
|
+
worker animation frames, ResizeObserver, and CSS Font Loading. WebGPU requires
|
|
58
|
+
HTTPS or localhost; WebGL2 rendering also works on ordinary HTTP origins.
|
|
59
|
+
Clipboard API access still requires a secure context, browser permission and,
|
|
60
|
+
for relevant actions, a user gesture. There is no Canvas2D terminal-rendering fallback.
|
|
61
|
+
Renderer selection does not change transport security: use HTTPS/WSS to protect
|
|
62
|
+
terminal input and output.
|
|
63
|
+
|
|
64
|
+
### Renderer selection
|
|
65
|
+
|
|
66
|
+
Set the mount-time `renderer` option to `"auto"` (the default), `"webgpu"`, or
|
|
67
|
+
`"webgl2"`. Auto prefers WebGPU and uses WebGL2 if the secure context, API,
|
|
68
|
+
adapter, device acquisition, or presentation context is unavailable. Explicit
|
|
69
|
+
modes require that backend and report an error rather than falling back.
|
|
70
|
+
Shader, font, validation, and unexpected initialization errors are not
|
|
71
|
+
compatibility fallbacks. Runtime GPU/context loss terminates the view with an
|
|
72
|
+
error; it does not switch backends behind the caller's back.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const terminal = await WebTerminal.mount(container, {
|
|
76
|
+
url: "/ws/terminal",
|
|
77
|
+
renderer: "webgl2", // Use WebGL2 even if WebGPU is available.
|
|
78
|
+
onStats(stats) {
|
|
79
|
+
console.log(stats.renderer, stats.rendererFallbackReason);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`stats.renderer` identifies the active backend after initialization.
|
|
85
|
+
`stats.rendererFallbackReason` explains an automatic fallback and is absent
|
|
86
|
+
for explicit selections and successful WebGPU initialization. Both backends
|
|
87
|
+
share glyph rasterization, frame preparation, clipping, and image ordering.
|
|
88
|
+
WebGPU preference is not a performance guarantee; compare representative
|
|
89
|
+
workloads on your target browsers and devices.
|
|
90
|
+
|
|
91
|
+
### Module and worker deployment
|
|
60
92
|
|
|
61
93
|
The package contains browser ES modules, not a single bundle. For bare static
|
|
62
94
|
hosting, copy **all of `dist/`**, preserving its directory structure, and import
|
|
@@ -100,6 +132,7 @@ workers, fonts, and the intended WebSocket endpoint.
|
|
|
100
132
|
| --- | --- |
|
|
101
133
|
| `workerUrl` | Optional module-worker entry; useful when worker assets are deployed separately. |
|
|
102
134
|
| `scale` | GPU backing scale `0.5`–`3`, or `"auto"` (default, bounded device pixel ratio). |
|
|
135
|
+
| `renderer` | `"auto"` (prefer WebGPU), `"webgpu"`, or `"webgl2"`; selected once per mount. |
|
|
103
136
|
| `font` | One family and optional downloadable font faces; see below. |
|
|
104
137
|
| `sizing` | `{ mode: "auto", fontSize?: number }` or `{ mode: "fixed", columns, rows, fontSize?: number }`. |
|
|
105
138
|
| `readOnly` | Disable application input while retaining history inspection and selection. |
|
|
@@ -253,5 +286,6 @@ Only `dist/`, this README, the MIT license, and package metadata are shipped.
|
|
|
253
286
|
A prepared tarball is self-contained and can be published with
|
|
254
287
|
`npm publish ./hex1b-web-terminal-<version>.tgz --ignore-scripts`; it does not
|
|
255
288
|
need development sources or build scripts. The package name is always
|
|
256
|
-
`@hex1b/web-terminal
|
|
257
|
-
the
|
|
289
|
+
`@hex1b/web-terminal`. CI publishes main/release builds to npmjs; PR builds
|
|
290
|
+
provide the tarball as the `npm-web-terminal` workflow artifact and do not
|
|
291
|
+
publish it to a registry. No registry is pinned in `package.json`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RenderBackend } from "./render-backend.js";
|
|
2
|
+
import type { TerminalRendererPreference } from "./types.js";
|
|
3
|
+
export declare function createRenderBackend(canvas: OffscreenCanvas, onFatal: (error: Error) => void, preference?: TerminalRendererPreference): Promise<{
|
|
4
|
+
backend: RenderBackend;
|
|
5
|
+
fallbackReason?: string;
|
|
6
|
+
}>;
|
|
7
|
+
//# sourceMappingURL=backend-selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend-selection.d.ts","sourceRoot":"","sources":["../src/backend-selection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAE7D,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,EAChG,UAAU,GAAE,0BAAmC,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAe/G"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RendererUnavailableError } from "./render-backend.js";
|
|
2
|
+
import { normalizeRenderer } from "./renderer-options.js";
|
|
3
|
+
import { WebGpuBackend } from "./webgpu-backend.js";
|
|
4
|
+
import { WebGl2Backend } from "./webgl2-backend.js";
|
|
5
|
+
export async function createRenderBackend(canvas, onFatal, preference = "auto") {
|
|
6
|
+
const requested = normalizeRenderer(preference);
|
|
7
|
+
if (requested === "webgl2")
|
|
8
|
+
return { backend: await WebGl2Backend.create(canvas, onFatal) };
|
|
9
|
+
try {
|
|
10
|
+
return { backend: await WebGpuBackend.create(canvas, onFatal) };
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (requested !== "auto" || !(error instanceof RendererUnavailableError))
|
|
14
|
+
throw error;
|
|
15
|
+
try {
|
|
16
|
+
return { backend: await WebGl2Backend.create(canvas, onFatal), fallbackReason: error.message };
|
|
17
|
+
}
|
|
18
|
+
catch (fallbackError) {
|
|
19
|
+
throw new AggregateError([error, fallbackError], `WebGPU unavailable (${error.message}); WebGL2 initialization failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=backend-selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend-selection.js","sourceRoot":"","sources":["../src/backend-selection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAuB,EAAE,OAA+B,EAChG,aAAyC,MAAM;IAC/C,MAAM,SAAS,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;IAChD,IAAI,SAAS,KAAK,QAAQ;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAC5F,IAAI,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAClE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,SAAS,KAAK,MAAM,IAAI,CAAC,CAAC,KAAK,YAAY,wBAAwB,CAAC;YAAE,MAAM,KAAK,CAAC;QACtF,IAAI,CAAC;YACH,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QACjG,CAAC;QAAC,OAAO,aAAa,EAAE,CAAC;YACvB,MAAM,IAAI,cAAc,CAAC,CAAC,KAAK,EAAE,aAAa,CAAC,EAC7C,uBAAuB,KAAK,CAAC,OAAO,oCAClC,aAAa,YAAY,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import { RendererUnavailableError } from \"./render-backend.js\";\nimport type { RenderBackend } from \"./render-backend.js\";\nimport { normalizeRenderer } from \"./renderer-options.js\";\nimport { WebGpuBackend } from \"./webgpu-backend.js\";\nimport { WebGl2Backend } from \"./webgl2-backend.js\";\nimport type { TerminalRendererPreference } from \"./types.js\";\n\nexport async function createRenderBackend(canvas: OffscreenCanvas, onFatal: (error: Error) => void,\n preference: TerminalRendererPreference = \"auto\"): Promise<{ backend: RenderBackend; fallbackReason?: string }> {\n const requested = normalizeRenderer(preference);\n if (requested === \"webgl2\") return { backend: await WebGl2Backend.create(canvas, onFatal) };\n try {\n return { backend: await WebGpuBackend.create(canvas, onFatal) };\n } catch (error) {\n if (requested !== \"auto\" || !(error instanceof RendererUnavailableError)) throw error;\n try {\n return { backend: await WebGl2Backend.create(canvas, onFatal), fallbackReason: error.message };\n } catch (fallbackError) {\n throw new AggregateError([error, fallbackError],\n `WebGPU unavailable (${error.message}); WebGL2 initialization failed: ${\n fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const QUAD_STRIDE = 16;
|
|
2
|
+
export type RenderColor = [number, number, number, number];
|
|
3
|
+
export type RenderPixels = Uint8Array<ArrayBuffer> | Uint8ClampedArray<ArrayBuffer>;
|
|
4
|
+
export interface RenderTexture {
|
|
5
|
+
readonly width: number;
|
|
6
|
+
readonly height: number;
|
|
7
|
+
writePixels(pixels: RenderPixels, width: number, height: number, x?: number, y?: number): void;
|
|
8
|
+
writeBitmap(bitmap: ImageBitmap): void;
|
|
9
|
+
destroy(): void;
|
|
10
|
+
}
|
|
11
|
+
export interface RenderBatch {
|
|
12
|
+
resource: RenderTexture;
|
|
13
|
+
start: number;
|
|
14
|
+
count: number;
|
|
15
|
+
}
|
|
16
|
+
/** Internal graphics boundary; layout, rasterization and ordering belong to TerminalRenderer. */
|
|
17
|
+
export interface RenderBackend {
|
|
18
|
+
readonly kind: "webgpu" | "webgl2";
|
|
19
|
+
readonly maxTextureDimension2D: number;
|
|
20
|
+
readonly maxCanvasDimension2D: number;
|
|
21
|
+
readonly instanceBufferBytes: number;
|
|
22
|
+
createTexture(width: number, height: number, label: string): RenderTexture;
|
|
23
|
+
resize(width: number, height: number): void;
|
|
24
|
+
submit(instances: Float32Array<ArrayBuffer>, quadCount: number, batches: readonly RenderBatch[], background: RenderColor): void;
|
|
25
|
+
idle(): Promise<void>;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
/** Only capability/device acquisition failures may trigger automatic backend fallback. */
|
|
29
|
+
export declare class RendererUnavailableError extends Error {
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=render-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-backend.d.ts","sourceRoot":"","sources":["../src/render-backend.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,KAAK,CAAC;AAE9B,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,WAAW,CAAC,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;AAEpF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/F,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IACvC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAAG,QAAQ,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAEtF,iGAAiG;AACjG,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACnC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IAC3E,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,WAAW,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,WAAW,EAAE,EAC7F,UAAU,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,0FAA0F;AAC1F,qBAAa,wBAAyB,SAAQ,KAAK;CAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-backend.js","sourceRoot":"","sources":["../src/render-backend.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AA6B9B,0FAA0F;AAC1F,MAAM,OAAO,wBAAyB,SAAQ,KAAK;CAAG","sourcesContent":["export const QUAD_STRIDE = 16;\n\nexport type RenderColor = [number, number, number, number];\nexport type RenderPixels = Uint8Array<ArrayBuffer> | Uint8ClampedArray<ArrayBuffer>;\n\nexport interface RenderTexture {\n readonly width: number;\n readonly height: number;\n writePixels(pixels: RenderPixels, width: number, height: number, x?: number, y?: number): void;\n writeBitmap(bitmap: ImageBitmap): void;\n destroy(): void;\n}\n\nexport interface RenderBatch { resource: RenderTexture; start: number; count: number }\n\n/** Internal graphics boundary; layout, rasterization and ordering belong to TerminalRenderer. */\nexport interface RenderBackend {\n readonly kind: \"webgpu\" | \"webgl2\";\n readonly maxTextureDimension2D: number;\n readonly maxCanvasDimension2D: number;\n readonly instanceBufferBytes: number;\n createTexture(width: number, height: number, label: string): RenderTexture;\n resize(width: number, height: number): void;\n submit(instances: Float32Array<ArrayBuffer>, quadCount: number, batches: readonly RenderBatch[],\n background: RenderColor): void;\n idle(): Promise<void>;\n dispose(): void;\n}\n\n/** Only capability/device acquisition failures may trigger automatic backend fallback. */\nexport class RendererUnavailableError extends Error {}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer-options.d.ts","sourceRoot":"","sources":["../src/renderer-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAE7D,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,OAAgB,GAAG,0BAA0B,CAGrF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer-options.js","sourceRoot":"","sources":["../src/renderer-options.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,iBAAiB,CAAC,QAAiB,MAAM;IACvD,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC/E,MAAM,IAAI,SAAS,CAAC,gDAAgD,CAAC,CAAC;AACxE,CAAC","sourcesContent":["import type { TerminalRendererPreference } from \"./types.js\";\n\nexport function normalizeRenderer(value: unknown = \"auto\"): TerminalRendererPreference {\n if (value === \"auto\" || value === \"webgpu\" || value === \"webgl2\") return value;\n throw new TypeError('renderer must be \"auto\", \"webgpu\", or \"webgl2\"');\n}\n"]}
|
package/dist/renderer.d.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
+
import type { RenderBackend, RenderBatch, RenderColor, RenderTexture } from "./render-backend.js";
|
|
1
2
|
import type { FontMetrics, LoadedFont, NormalizedFont } from "./terminal-font.js";
|
|
2
|
-
import type { TerminalFont, TerminalSize } from "./types.js";
|
|
3
|
+
import type { TerminalFont, TerminalRendererPreference, TerminalSize } from "./types.js";
|
|
3
4
|
import type { FrameImage, FrameMetadata, ImagePlacement, TerminalCell } from "./wire-types.js";
|
|
4
|
-
type Vector4 =
|
|
5
|
-
|
|
6
|
-
texture: GPUTexture;
|
|
7
|
-
bindGroup: GPUBindGroup;
|
|
8
|
-
width: number;
|
|
9
|
-
height: number;
|
|
10
|
-
}
|
|
5
|
+
type Vector4 = RenderColor;
|
|
6
|
+
type TextureResource = RenderTexture;
|
|
11
7
|
interface Shelf {
|
|
12
8
|
x: number;
|
|
13
9
|
y: number;
|
|
@@ -28,20 +24,16 @@ interface Glyph {
|
|
|
28
24
|
u1: number;
|
|
29
25
|
v1: number;
|
|
30
26
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
start: number;
|
|
34
|
-
count: number;
|
|
35
|
-
}
|
|
36
|
-
/** WebGPU instanced quads; Canvas2D is used only to rasterize reusable glyphs. */
|
|
27
|
+
type Batch = RenderBatch;
|
|
28
|
+
/** Shared instanced-quad preparation; Canvas2D rasterizes reusable glyphs for either backend. */
|
|
37
29
|
export declare class TerminalRenderer {
|
|
38
30
|
canvas: OffscreenCanvas;
|
|
39
31
|
scale: number;
|
|
40
32
|
backingScale: number;
|
|
41
|
-
|
|
33
|
+
backend: RenderBackend;
|
|
34
|
+
fallbackReason?: string;
|
|
42
35
|
fontConfiguration: NormalizedFont;
|
|
43
36
|
fontMetrics: Map<number, FontMetrics>;
|
|
44
|
-
context: GPUCanvasContext;
|
|
45
37
|
images: Map<string, TextureResource>;
|
|
46
38
|
glyphs: Map<string, Glyph>;
|
|
47
39
|
imageUploadBytes: number;
|
|
@@ -50,16 +42,10 @@ export declare class TerminalRenderer {
|
|
|
50
42
|
atlasRebuilds: number;
|
|
51
43
|
textureBytes: number;
|
|
52
44
|
instances: Float32Array<ArrayBuffer>;
|
|
53
|
-
instanceBuffer: GPUBuffer | null;
|
|
54
|
-
instanceBufferBytes: number;
|
|
55
45
|
disposed: boolean;
|
|
56
46
|
columns: number;
|
|
57
47
|
rows: number;
|
|
58
48
|
font: LoadedFont;
|
|
59
|
-
format: GPUTextureFormat;
|
|
60
|
-
uniform: GPUBuffer;
|
|
61
|
-
sampler: GPUSampler;
|
|
62
|
-
pipeline: GPURenderPipeline;
|
|
63
49
|
rasterCanvas: OffscreenCanvas;
|
|
64
50
|
raster: OffscreenCanvasRenderingContext2D;
|
|
65
51
|
atlas: TextureResource;
|
|
@@ -70,8 +56,8 @@ export declare class TerminalRenderer {
|
|
|
70
56
|
height: number;
|
|
71
57
|
quadCount: number;
|
|
72
58
|
batches: Batch[];
|
|
73
|
-
static create(canvas: OffscreenCanvas, scale: number, onFatal: (error: Error
|
|
74
|
-
constructor(canvas: OffscreenCanvas, scale: number,
|
|
59
|
+
static create(canvas: OffscreenCanvas, scale: number, onFatal: (error: Error) => void, font?: TerminalFont, preference?: TerminalRendererPreference): Promise<TerminalRenderer>;
|
|
60
|
+
constructor(canvas: OffscreenCanvas, scale: number, backend: RenderBackend, font: NormalizedFont);
|
|
75
61
|
initialize(): Promise<void>;
|
|
76
62
|
createTexture(width: number, height: number, label: string): TextureResource;
|
|
77
63
|
resetAtlas(size: number): void;
|
|
@@ -91,6 +77,8 @@ export declare class TerminalRenderer {
|
|
|
91
77
|
drawCalls: number;
|
|
92
78
|
};
|
|
93
79
|
metrics(): {
|
|
80
|
+
renderer: "webgl2" | "webgpu";
|
|
81
|
+
rendererFallbackReason: string | undefined;
|
|
94
82
|
fontFamily: string;
|
|
95
83
|
rasterScale: number;
|
|
96
84
|
backingScale: number;
|
package/dist/renderer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}
|
package/dist/renderer.js
CHANGED
|
@@ -1,53 +1,14 @@
|
|
|
1
1
|
import { LIMITS } from "./protocol.js";
|
|
2
2
|
import { loadFont, measureFont, normalizeFont } from "./terminal-font.js";
|
|
3
|
+
import { createRenderBackend } from "./backend-selection.js";
|
|
4
|
+
import { QUAD_STRIDE } from "./render-backend.js";
|
|
3
5
|
const CELL_WIDTH = 10;
|
|
4
6
|
const CELL_HEIGHT = 20;
|
|
5
7
|
const MAX_QUADS = 1024 * 1024;
|
|
6
8
|
const MAX_GLYPHS = 16384;
|
|
7
9
|
const MAX_GLYPH_KEY_UNITS = 1024 * 1024;
|
|
8
|
-
const STRIDE =
|
|
10
|
+
const STRIDE = QUAD_STRIDE;
|
|
9
11
|
const WHITE = [1, 1, 1, 1];
|
|
10
|
-
const shader = /* wgsl */ `
|
|
11
|
-
struct Viewport { size: vec2f, padding: vec2f }
|
|
12
|
-
@group(0) @binding(0) var<uniform> viewport: Viewport;
|
|
13
|
-
@group(0) @binding(1) var image: texture_2d<f32>;
|
|
14
|
-
@group(0) @binding(2) var imageSampler: sampler;
|
|
15
|
-
|
|
16
|
-
struct VertexOut {
|
|
17
|
-
@builtin(position) position: vec4f,
|
|
18
|
-
@location(0) uv: vec2f,
|
|
19
|
-
@location(1) color: vec4f,
|
|
20
|
-
@location(2) @interpolate(flat) mode: f32,
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
@vertex fn vertex(
|
|
24
|
-
@builtin(vertex_index) index: u32,
|
|
25
|
-
@location(0) rect: vec4f,
|
|
26
|
-
@location(1) uvRect: vec4f,
|
|
27
|
-
@location(2) color: vec4f,
|
|
28
|
-
@location(3) mode: f32,
|
|
29
|
-
) -> VertexOut {
|
|
30
|
-
let corners = array<vec2f, 6>(
|
|
31
|
-
vec2f(0, 0), vec2f(1, 0), vec2f(0, 1),
|
|
32
|
-
vec2f(0, 1), vec2f(1, 0), vec2f(1, 1)
|
|
33
|
-
);
|
|
34
|
-
let corner = corners[index];
|
|
35
|
-
let position = rect.xy + corner * rect.zw;
|
|
36
|
-
var out: VertexOut;
|
|
37
|
-
out.position = vec4f(position / viewport.size * vec2f(2, -2) + vec2f(-1, 1), 0, 1);
|
|
38
|
-
out.uv = mix(uvRect.xy, uvRect.zw, corner);
|
|
39
|
-
out.color = color;
|
|
40
|
-
out.mode = mode;
|
|
41
|
-
return out;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@fragment fn fragment(in: VertexOut) -> @location(0) vec4f {
|
|
45
|
-
// Explicit LOD avoids derivative-uniformity requirements across solid/mask/image batches.
|
|
46
|
-
let texel = textureSampleLevel(image, imageSampler, in.uv, 0);
|
|
47
|
-
if (in.mode < 0.5) { return in.color; }
|
|
48
|
-
if (in.mode < 1.5) { return vec4f(in.color.rgb, in.color.a * texel.a); }
|
|
49
|
-
return texel * in.color;
|
|
50
|
-
}`;
|
|
51
12
|
function rgba(packed) {
|
|
52
13
|
return [
|
|
53
14
|
(packed & 255) / 255,
|
|
@@ -81,15 +42,15 @@ function packGlyphs(glyphs, size, scale, initial = { x: 0, y: 0, rowHeight: 0 })
|
|
|
81
42
|
}
|
|
82
43
|
return { placements, shelf: { x, y, rowHeight } };
|
|
83
44
|
}
|
|
84
|
-
/**
|
|
45
|
+
/** Shared instanced-quad preparation; Canvas2D rasterizes reusable glyphs for either backend. */
|
|
85
46
|
export class TerminalRenderer {
|
|
86
47
|
canvas;
|
|
87
48
|
scale;
|
|
88
49
|
backingScale;
|
|
89
|
-
|
|
50
|
+
backend;
|
|
51
|
+
fallbackReason;
|
|
90
52
|
fontConfiguration;
|
|
91
53
|
fontMetrics;
|
|
92
|
-
context;
|
|
93
54
|
images;
|
|
94
55
|
glyphs;
|
|
95
56
|
imageUploadBytes;
|
|
@@ -98,17 +59,11 @@ export class TerminalRenderer {
|
|
|
98
59
|
atlasRebuilds;
|
|
99
60
|
textureBytes;
|
|
100
61
|
instances;
|
|
101
|
-
instanceBuffer;
|
|
102
|
-
instanceBufferBytes;
|
|
103
62
|
disposed;
|
|
104
63
|
columns;
|
|
105
64
|
rows;
|
|
106
65
|
// Initialized by create() before the renderer can prepare or submit frames.
|
|
107
66
|
font;
|
|
108
|
-
format;
|
|
109
|
-
uniform;
|
|
110
|
-
sampler;
|
|
111
|
-
pipeline;
|
|
112
67
|
rasterCanvas;
|
|
113
68
|
raster;
|
|
114
69
|
atlas;
|
|
@@ -119,22 +74,13 @@ export class TerminalRenderer {
|
|
|
119
74
|
height = 0;
|
|
120
75
|
quadCount = 0;
|
|
121
76
|
batches = [];
|
|
122
|
-
static async create(canvas, scale, onFatal, font) {
|
|
77
|
+
static async create(canvas, scale, onFatal, font, preference = "auto") {
|
|
123
78
|
const normalizedFont = normalizeFont(font);
|
|
124
|
-
if (!
|
|
125
|
-
throw new Error("
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (!adapter)
|
|
130
|
-
throw new Error("No WebGPU adapter is available; check browser GPU support");
|
|
131
|
-
const device = await adapter.requestDevice();
|
|
132
|
-
const renderer = new TerminalRenderer(canvas, scale, device, normalizedFont);
|
|
133
|
-
device.lost.then(info => {
|
|
134
|
-
if (!renderer.disposed)
|
|
135
|
-
onFatal(new Error(`WebGPU device lost: ${info.message || info.reason}`));
|
|
136
|
-
});
|
|
137
|
-
device.addEventListener("uncapturederror", event => onFatal(event.error));
|
|
79
|
+
if (!Number.isFinite(scale) || scale < 0.5 || scale > 3)
|
|
80
|
+
throw new Error("Invalid backing scale");
|
|
81
|
+
const { backend, fallbackReason } = await createRenderBackend(canvas, onFatal, preference);
|
|
82
|
+
const renderer = new TerminalRenderer(canvas, scale, backend, normalizedFont);
|
|
83
|
+
renderer.fallbackReason = fallbackReason;
|
|
138
84
|
try {
|
|
139
85
|
await renderer.initialize();
|
|
140
86
|
return renderer;
|
|
@@ -144,19 +90,15 @@ export class TerminalRenderer {
|
|
|
144
90
|
throw error;
|
|
145
91
|
}
|
|
146
92
|
}
|
|
147
|
-
constructor(canvas, scale,
|
|
93
|
+
constructor(canvas, scale, backend, font) {
|
|
148
94
|
if (!Number.isFinite(scale) || scale < 0.5 || scale > 3)
|
|
149
95
|
throw new Error("Invalid backing scale");
|
|
150
96
|
this.canvas = canvas;
|
|
151
97
|
this.scale = scale;
|
|
152
98
|
this.backingScale = scale;
|
|
153
|
-
this.
|
|
99
|
+
this.backend = backend;
|
|
154
100
|
this.fontConfiguration = font;
|
|
155
101
|
this.fontMetrics = new Map();
|
|
156
|
-
const context = canvas.getContext("webgpu");
|
|
157
|
-
if (!context)
|
|
158
|
-
throw new Error("Could not create an OffscreenCanvas WebGPU context");
|
|
159
|
-
this.context = context;
|
|
160
102
|
this.images = new Map();
|
|
161
103
|
this.glyphs = new Map();
|
|
162
104
|
this.imageUploadBytes = 0;
|
|
@@ -165,78 +107,24 @@ export class TerminalRenderer {
|
|
|
165
107
|
this.atlasRebuilds = 0;
|
|
166
108
|
this.textureBytes = 0;
|
|
167
109
|
this.instances = new Float32Array(4096 * STRIDE);
|
|
168
|
-
this.instanceBuffer = null;
|
|
169
|
-
this.instanceBufferBytes = 0;
|
|
170
110
|
this.disposed = false;
|
|
171
111
|
this.columns = 0;
|
|
172
112
|
this.rows = 0;
|
|
173
113
|
}
|
|
174
114
|
async initialize() {
|
|
175
115
|
this.font = await loadFont(this.fontConfiguration);
|
|
176
|
-
const device = this.device;
|
|
177
|
-
this.format = navigator.gpu.getPreferredCanvasFormat();
|
|
178
|
-
this.context.configure({ device, format: this.format, alphaMode: "opaque" });
|
|
179
|
-
this.uniform = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
180
|
-
this.sampler = device.createSampler({ minFilter: "linear", magFilter: "linear" });
|
|
181
|
-
const module = device.createShaderModule({ code: shader });
|
|
182
|
-
this.pipeline = await device.createRenderPipelineAsync({
|
|
183
|
-
layout: "auto",
|
|
184
|
-
vertex: {
|
|
185
|
-
module,
|
|
186
|
-
entryPoint: "vertex",
|
|
187
|
-
buffers: [{
|
|
188
|
-
arrayStride: STRIDE * 4,
|
|
189
|
-
stepMode: "instance",
|
|
190
|
-
attributes: [
|
|
191
|
-
{ shaderLocation: 0, offset: 0, format: "float32x4" },
|
|
192
|
-
{ shaderLocation: 1, offset: 16, format: "float32x4" },
|
|
193
|
-
{ shaderLocation: 2, offset: 32, format: "float32x4" },
|
|
194
|
-
{ shaderLocation: 3, offset: 48, format: "float32" },
|
|
195
|
-
],
|
|
196
|
-
}],
|
|
197
|
-
},
|
|
198
|
-
fragment: {
|
|
199
|
-
module,
|
|
200
|
-
entryPoint: "fragment",
|
|
201
|
-
targets: [{
|
|
202
|
-
format: this.format,
|
|
203
|
-
blend: {
|
|
204
|
-
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" },
|
|
205
|
-
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
|
|
206
|
-
},
|
|
207
|
-
}],
|
|
208
|
-
},
|
|
209
|
-
primitive: { topology: "triangle-list" },
|
|
210
|
-
});
|
|
211
116
|
this.rasterCanvas = new OffscreenCanvas(1, 1);
|
|
212
117
|
const raster = this.rasterCanvas.getContext("2d", { willReadFrequently: true });
|
|
213
118
|
if (!raster)
|
|
214
119
|
throw new Error("Worker glyph rasterization is unavailable");
|
|
215
120
|
this.raster = raster;
|
|
216
|
-
this.resetAtlas(Math.min(2048,
|
|
121
|
+
this.resetAtlas(Math.min(2048, this.backend.maxTextureDimension2D));
|
|
217
122
|
}
|
|
218
123
|
createTexture(width, height, label) {
|
|
219
|
-
|
|
220
|
-
throw new Error(`${label} exceeds the GPU texture dimension limit`);
|
|
221
|
-
}
|
|
222
|
-
const texture = this.device.createTexture({
|
|
223
|
-
label,
|
|
224
|
-
size: [width, height],
|
|
225
|
-
format: "rgba8unorm",
|
|
226
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
227
|
-
});
|
|
228
|
-
const bindGroup = this.device.createBindGroup({
|
|
229
|
-
layout: this.pipeline.getBindGroupLayout(0),
|
|
230
|
-
entries: [
|
|
231
|
-
{ binding: 0, resource: { buffer: this.uniform } },
|
|
232
|
-
{ binding: 1, resource: texture.createView() },
|
|
233
|
-
{ binding: 2, resource: this.sampler },
|
|
234
|
-
],
|
|
235
|
-
});
|
|
236
|
-
return { texture, bindGroup, width, height };
|
|
124
|
+
return this.backend.createTexture(width, height, label);
|
|
237
125
|
}
|
|
238
126
|
resetAtlas(size) {
|
|
239
|
-
this.atlas?.
|
|
127
|
+
this.atlas?.destroy();
|
|
240
128
|
this.atlas = this.createTexture(size, size, "Glyph atlas");
|
|
241
129
|
this.glyphs.clear();
|
|
242
130
|
this.glyphKeyUnits = 0;
|
|
@@ -245,7 +133,7 @@ export class TerminalRenderer {
|
|
|
245
133
|
resize(columns, rows, viewport) {
|
|
246
134
|
const width = columns * CELL_WIDTH;
|
|
247
135
|
const height = rows * CELL_HEIGHT;
|
|
248
|
-
const limit = this.
|
|
136
|
+
const limit = this.backend.maxCanvasDimension2D;
|
|
249
137
|
const requested = Math.min(this.scale, viewport ? viewport.width / width : Infinity, viewport ? viewport.height / height : Infinity);
|
|
250
138
|
this.canvasLimited = width * requested > limit || height * requested > limit;
|
|
251
139
|
this.backingScale = Math.min(requested, limit / width, limit / height);
|
|
@@ -259,7 +147,7 @@ export class TerminalRenderer {
|
|
|
259
147
|
this.height = height;
|
|
260
148
|
this.canvas.width = backingWidth;
|
|
261
149
|
this.canvas.height = backingHeight;
|
|
262
|
-
this.
|
|
150
|
+
this.backend.resize(width, height);
|
|
263
151
|
}
|
|
264
152
|
/** Call only between submissions. Missing/over-budget resources terminate the session. */
|
|
265
153
|
async updateImages(incoming, retainedKeys) {
|
|
@@ -276,7 +164,7 @@ export class TerminalRenderer {
|
|
|
276
164
|
throw new Error("Retained images exceed the 256 MiB texture budget");
|
|
277
165
|
for (const [key, image] of this.images) {
|
|
278
166
|
if (!retained.has(key) || replacements.has(key)) {
|
|
279
|
-
image.
|
|
167
|
+
image.destroy();
|
|
280
168
|
this.textureBytes -= image.width * image.height * 4;
|
|
281
169
|
this.images.delete(key);
|
|
282
170
|
}
|
|
@@ -285,7 +173,7 @@ export class TerminalRenderer {
|
|
|
285
173
|
const resource = this.createTexture(image.width, image.height, `Image ${image.key}`);
|
|
286
174
|
try {
|
|
287
175
|
if (image.format === "rgba") {
|
|
288
|
-
|
|
176
|
+
resource.writePixels(image.bytes, image.width, image.height);
|
|
289
177
|
}
|
|
290
178
|
else {
|
|
291
179
|
// Check IHDR before decoding so a tiny PNG cannot claim unbounded decoded dimensions.
|
|
@@ -302,7 +190,7 @@ export class TerminalRenderer {
|
|
|
302
190
|
try {
|
|
303
191
|
if (bitmap.width !== image.width || bitmap.height !== image.height)
|
|
304
192
|
throw new Error("Decoded PNG dimension mismatch");
|
|
305
|
-
|
|
193
|
+
resource.writeBitmap(bitmap);
|
|
306
194
|
}
|
|
307
195
|
finally {
|
|
308
196
|
bitmap.close();
|
|
@@ -314,7 +202,7 @@ export class TerminalRenderer {
|
|
|
314
202
|
this.imagePayloadBytes += image.byteLength;
|
|
315
203
|
}
|
|
316
204
|
catch (error) {
|
|
317
|
-
resource.
|
|
205
|
+
resource.destroy();
|
|
318
206
|
throw error;
|
|
319
207
|
}
|
|
320
208
|
}
|
|
@@ -338,7 +226,7 @@ export class TerminalRenderer {
|
|
|
338
226
|
let plan = metadataFits ? packGlyphs(missing, this.atlas.width, this.scale, this.shelf) : null;
|
|
339
227
|
if (!plan) {
|
|
340
228
|
let size = this.atlas.width;
|
|
341
|
-
const maxSize = Math.min(4096, this.
|
|
229
|
+
const maxSize = Math.min(4096, this.backend.maxTextureDimension2D);
|
|
342
230
|
while (!(plan = packGlyphs(visible, size, this.scale)) && size < maxSize) {
|
|
343
231
|
size = Math.min(size * 2, maxSize);
|
|
344
232
|
}
|
|
@@ -378,7 +266,7 @@ export class TerminalRenderer {
|
|
|
378
266
|
break;
|
|
379
267
|
}
|
|
380
268
|
}
|
|
381
|
-
this.
|
|
269
|
+
this.atlas.writePixels(pixels.data, width, height, x, y);
|
|
382
270
|
this.glyphUploadBytes += width * height * 4;
|
|
383
271
|
this.glyphs.set(key, {
|
|
384
272
|
colored,
|
|
@@ -526,39 +414,14 @@ export class TerminalRenderer {
|
|
|
526
414
|
this.solid(x, y, CELL_WIDTH, CELL_HEIGHT, color);
|
|
527
415
|
}
|
|
528
416
|
}
|
|
529
|
-
const usedBytes = this.quadCount * STRIDE * 4;
|
|
530
|
-
if (!this.instanceBuffer || usedBytes > this.instanceBufferBytes) {
|
|
531
|
-
this.instanceBuffer?.destroy();
|
|
532
|
-
this.instanceBufferBytes = Math.max(256, this.instances.byteLength);
|
|
533
|
-
this.instanceBuffer = this.device.createBuffer({
|
|
534
|
-
size: this.instanceBufferBytes,
|
|
535
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
if (usedBytes)
|
|
539
|
-
this.device.queue.writeBuffer(this.instanceBuffer, 0, this.instances, 0, this.quadCount * STRIDE);
|
|
540
|
-
const encoder = this.device.createCommandEncoder();
|
|
541
417
|
const base = rgba(metadata.defaultBackground ?? cells[0]?.background ?? 0xff000000);
|
|
542
|
-
|
|
543
|
-
colorAttachments: [{
|
|
544
|
-
view: this.context.getCurrentTexture().createView(),
|
|
545
|
-
clearValue: { r: base[0], g: base[1], b: base[2], a: 1 },
|
|
546
|
-
loadOp: "clear",
|
|
547
|
-
storeOp: "store",
|
|
548
|
-
}],
|
|
549
|
-
});
|
|
550
|
-
pass.setPipeline(this.pipeline);
|
|
551
|
-
pass.setVertexBuffer(0, this.instanceBuffer);
|
|
552
|
-
for (const batch of this.batches) {
|
|
553
|
-
pass.setBindGroup(0, batch.resource.bindGroup);
|
|
554
|
-
pass.draw(6, batch.count, 0, batch.start);
|
|
555
|
-
}
|
|
556
|
-
pass.end();
|
|
557
|
-
this.device.queue.submit([encoder.finish()]);
|
|
418
|
+
this.backend.submit(this.instances, this.quadCount, this.batches, base);
|
|
558
419
|
return { cpuMs: performance.now() - start, quads: this.quadCount, drawCalls: this.batches.length };
|
|
559
420
|
}
|
|
560
421
|
metrics() {
|
|
561
422
|
return {
|
|
423
|
+
renderer: this.backend.kind,
|
|
424
|
+
rendererFallbackReason: this.fallbackReason,
|
|
562
425
|
fontFamily: this.font.family,
|
|
563
426
|
rasterScale: this.scale,
|
|
564
427
|
backingScale: this.backingScale,
|
|
@@ -572,24 +435,23 @@ export class TerminalRenderer {
|
|
|
572
435
|
imageUploadBytes: this.imageUploadBytes,
|
|
573
436
|
imagePayloadBytes: this.imagePayloadBytes,
|
|
574
437
|
glyphUploadBytes: this.glyphUploadBytes,
|
|
575
|
-
instanceBufferBytes: this.instanceBufferBytes,
|
|
438
|
+
instanceBufferBytes: this.backend.instanceBufferBytes,
|
|
576
439
|
};
|
|
577
440
|
}
|
|
578
441
|
async idle() {
|
|
579
|
-
await this.
|
|
442
|
+
await this.backend.idle();
|
|
580
443
|
}
|
|
581
444
|
dispose() {
|
|
445
|
+
if (this.disposed)
|
|
446
|
+
return;
|
|
582
447
|
this.disposed = true;
|
|
583
448
|
for (const image of this.images.values())
|
|
584
|
-
image.
|
|
449
|
+
image.destroy();
|
|
585
450
|
this.images.clear();
|
|
586
|
-
this.atlas?.
|
|
587
|
-
this.instanceBuffer?.destroy();
|
|
588
|
-
this.uniform?.destroy();
|
|
451
|
+
this.atlas?.destroy();
|
|
589
452
|
this.font?.dispose();
|
|
590
453
|
this.fontMetrics.clear();
|
|
591
|
-
this.
|
|
592
|
-
this.device.destroy();
|
|
454
|
+
this.backend.dispose();
|
|
593
455
|
}
|
|
594
456
|
}
|
|
595
457
|
//# sourceMappingURL=renderer.js.map
|