@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hex1b/web-terminal
2
2
 
3
- The first-party WebGPU browser terminal for Hex1b. It renders server-authoritative
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 HTTPS or localhost and a browser with WebGPU, module workers, transferable
57
- OffscreenCanvas, worker animation frames, ResizeObserver, and CSS Font Loading.
58
- Clipboard access also requires browser permission and, for relevant actions, a
59
- user gesture. There is no Canvas2D terminal-rendering fallback.
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`, including GitHub Packages. Registry selection is left to
257
- the caller; no registry is pinned in `package.json`.
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,5 @@
1
+ export const QUAD_STRIDE = 16;
2
+ /** Only capability/device acquisition failures may trigger automatic backend fallback. */
3
+ export class RendererUnavailableError extends Error {
4
+ }
5
+ //# sourceMappingURL=render-backend.js.map
@@ -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,3 @@
1
+ import type { TerminalRendererPreference } from "./types.js";
2
+ export declare function normalizeRenderer(value?: unknown): TerminalRendererPreference;
3
+ //# sourceMappingURL=renderer-options.d.ts.map
@@ -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,6 @@
1
+ export function normalizeRenderer(value = "auto") {
2
+ if (value === "auto" || value === "webgpu" || value === "webgl2")
3
+ return value;
4
+ throw new TypeError('renderer must be "auto", "webgpu", or "webgl2"');
5
+ }
6
+ //# sourceMappingURL=renderer-options.js.map
@@ -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"]}
@@ -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 = [number, number, number, number];
5
- interface TextureResource {
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
- interface Batch {
32
- resource: TextureResource;
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
- device: GPUDevice;
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 | GPUError) => void, font: TerminalFont): Promise<TerminalRenderer>;
74
- constructor(canvas: OffscreenCanvas, scale: number, device: GPUDevice, font: NormalizedFont);
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;
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}
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 = 16;
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
- /** WebGPU instanced quads; Canvas2D is used only to rasterize reusable glyphs. */
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
- device;
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 (!self.isSecureContext)
125
- throw new Error("WebGPU requires HTTPS or localhost");
126
- if (!navigator.gpu)
127
- throw new Error("WebGPU is unavailable in this browser worker");
128
- const adapter = await navigator.gpu.requestAdapter();
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, device, font) {
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.device = device;
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, device.limits.maxTextureDimension2D));
121
+ this.resetAtlas(Math.min(2048, this.backend.maxTextureDimension2D));
217
122
  }
218
123
  createTexture(width, height, label) {
219
- if (width > this.device.limits.maxTextureDimension2D || height > this.device.limits.maxTextureDimension2D) {
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?.texture.destroy();
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.device.limits.maxTextureDimension2D;
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.device.queue.writeBuffer(this.uniform, 0, new Float32Array([width, height, 0, 0]));
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.texture.destroy();
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
- this.device.queue.writeTexture({ texture: resource.texture }, image.bytes, { bytesPerRow: image.width * 4, rowsPerImage: image.height }, [image.width, image.height]);
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
- this.device.queue.copyExternalImageToTexture({ source: bitmap }, { texture: resource.texture, premultipliedAlpha: false }, [image.width, image.height]);
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.texture.destroy();
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.device.limits.maxTextureDimension2D);
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.device.queue.writeTexture({ texture: this.atlas.texture, origin: [x, y] }, pixels.data, { bytesPerRow: width * 4, rowsPerImage: height }, [width, height]);
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
- const pass = encoder.beginRenderPass({
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.device.queue.onSubmittedWorkDone();
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.texture.destroy();
449
+ image.destroy();
585
450
  this.images.clear();
586
- this.atlas?.texture.destroy();
587
- this.instanceBuffer?.destroy();
588
- this.uniform?.destroy();
451
+ this.atlas?.destroy();
589
452
  this.font?.dispose();
590
453
  this.fontMetrics.clear();
591
- this.context.unconfigure();
592
- this.device.destroy();
454
+ this.backend.dispose();
593
455
  }
594
456
  }
595
457
  //# sourceMappingURL=renderer.js.map