@aethervtt/shard-gpu 0.0.1 → 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.
@@ -0,0 +1,106 @@
1
+ /** Live GPU objects counted against one owner (`gpu.stats(owner)`). */
2
+ export interface GpuStats {
3
+ buffers: number;
4
+ textures: number;
5
+ /** Their memory: buffer sizes, and texture sizes over every mip level and sample. */
6
+ bytes: number;
7
+ }
8
+ /**
9
+ * What a GPU object is for, from its usage flags (0062): `targets` (textures rendered into),
10
+ * `textures` (sampled only), `geometry` (vertex and index buffers), `storage`, `uniforms`,
11
+ * `staging` (mappable: uploads and readbacks), and `other`.
12
+ */
13
+ export type GpuMemoryCategory = 'targets' | 'textures' | 'geometry' | 'storage' | 'uniforms' | 'staging' | 'other';
14
+ /**
15
+ * What bytes written to the GPU were for (0055), from the label of the buffer or texture written:
16
+ * `instances` (instance records), `meshes` (vertex and index data), `materials` (material
17
+ * uniforms), `textures`, `lights`, `shadows` (shadow views), `view` (camera and per-frame
18
+ * uniforms, culling parameters: derived from the view, not the scene), and `other`.
19
+ */
20
+ export declare const UPLOAD_CATEGORIES: readonly ['instances', 'meshes', 'materials', 'textures', 'lights', 'shadows', 'view', 'other'];
21
+ export type UploadCategory = (typeof UPLOAD_CATEGORIES)[number];
22
+ /** Bytes written and objects created by an owner since the device was made (`gpu.uploads(owner)`). */
23
+ export interface GpuUploads {
24
+ /** Cumulative bytes written, indexed like `UPLOAD_CATEGORIES`. */
25
+ readonly bytes: Float64Array;
26
+ /** Buffers and textures created. */
27
+ created: number;
28
+ }
29
+ /** Index of each category in `UPLOAD_CATEGORIES` (and in `GpuUploads.bytes`). */
30
+ export declare const Upload: {
31
+ readonly instances: 0;
32
+ readonly meshes: 1;
33
+ readonly materials: 2;
34
+ readonly textures: 3;
35
+ readonly lights: 4;
36
+ readonly shadows: 5;
37
+ readonly view: 6;
38
+ readonly other: 7;
39
+ };
40
+ /**
41
+ * Labels data textures (0064): on the baseline tier, a `data:instances` texture holds what the
42
+ * `instances` storage buffer would, and counts as it would.
43
+ */
44
+ export declare const DATA_TEXTURE_PREFIX = "data:";
45
+ /**
46
+ * The upload category of a buffer or texture, from its label. Textures are `textures`, except data
47
+ * textures, which count as the buffer they stand in for.
48
+ */
49
+ export declare function uploadCategory(label: string | undefined, texture: boolean): number;
50
+ /** Live bytes counted against an owner, or everyone, by category (`gpu.memory(owner)`). */
51
+ export interface GpuMemory {
52
+ bytes: number;
53
+ byCategory: Partial<Record<GpuMemoryCategory, number>>;
54
+ }
55
+ /** The category of a buffer from its usage. */
56
+ export declare function bufferCategory(usage: number): GpuMemoryCategory;
57
+ /** The category of a texture from its usage. */
58
+ export declare function textureCategory(usage: number): GpuMemoryCategory;
59
+ /** The owner of objects made while no app runs, and of per-device objects apps share. */
60
+ export declare const SHARED_OWNER = "gpu";
61
+ interface Entry {
62
+ readonly owner: Owned;
63
+ readonly texture: boolean;
64
+ readonly category: GpuMemoryCategory;
65
+ /** Index into `UPLOAD_CATEGORIES`. */
66
+ readonly upload: number;
67
+ readonly bytes: number;
68
+ readonly ref: WeakRef<GPUBuffer | GPUTexture>;
69
+ live: boolean;
70
+ }
71
+ interface Owned extends GpuStats {
72
+ readonly name: string;
73
+ readonly entries: Set<Entry>;
74
+ readonly uploads: GpuUploads;
75
+ }
76
+ /**
77
+ * Counts the buffers and textures a device creates, by owner (0052). Destroying an object uncounts
78
+ * it; so does garbage collection of one nobody destroyed. `release(owner)` destroys what an owner
79
+ * still has, which is how a disposed app gives back everything it made.
80
+ */
81
+ export declare class Ledger {
82
+ private readonly owners;
83
+ private readonly finalizer;
84
+ track(object: GPUBuffer | GPUTexture, owner: string, texture: boolean, bytes: number, category?: GpuMemoryCategory, label?: string): void;
85
+ drop(entry: Entry): void;
86
+ /** Counts bytes written into a tracked object against its owner and upload category. */
87
+ upload(object: object, bytes: number): void;
88
+ /** Cumulative bytes written and objects created by `owner`. */
89
+ uploads(owner: string): GpuUploads;
90
+ /** Destroys every object `owner` still has. Returns how many there were. */
91
+ release(owner: string): number;
92
+ /** Forgets everything without destroying it: the device it lived on is gone. */
93
+ clear(): void;
94
+ stats(owner?: string): GpuStats;
95
+ /** Live bytes of `owner` (or everyone) by category. Walks every live object: not per frame. */
96
+ memory(owner?: string): GpuMemory;
97
+ /** Owners that have live objects, sorted. */
98
+ names(): string[];
99
+ private owned;
100
+ }
101
+ /** [block width, block height, bytes per block] for compressed formats; texels are 1×1 blocks. */
102
+ /** A format's block width, height, and bytes per block (1×1 texels for uncompressed ones). */
103
+ export declare function blockOf(format: GPUTextureFormat): [number, number, number];
104
+ /** Bytes a texture takes: every mip level (and 3D slice), times its sample count. */
105
+ export declare function textureBytes(descriptor: GPUTextureDescriptor): number;
106
+ export {};
package/dist/ledger.js ADDED
@@ -0,0 +1,222 @@
1
+ const UPLOAD_CATEGORIES = [
2
+ "instances",
3
+ "meshes",
4
+ "materials",
5
+ "textures",
6
+ "lights",
7
+ "shadows",
8
+ "view",
9
+ "other"
10
+ ];
11
+ const Upload = {
12
+ instances: 0,
13
+ meshes: 1,
14
+ materials: 2,
15
+ textures: 3,
16
+ lights: 4,
17
+ shadows: 5,
18
+ view: 6,
19
+ other: 7
20
+ };
21
+ const DATA_TEXTURE_PREFIX = "data:";
22
+ function uploadCategory(label, texture) {
23
+ let l = label ?? "";
24
+ if (texture) {
25
+ if (!l.startsWith(DATA_TEXTURE_PREFIX)) return Upload.textures;
26
+ l = l.slice(DATA_TEXTURE_PREFIX.length);
27
+ }
28
+ if (l === "instances/visible") return Upload.view;
29
+ if (l.startsWith("instances")) return Upload.instances;
30
+ if (l.startsWith("mesh/") || l.startsWith("mesh ")) return Upload.meshes;
31
+ if (l.startsWith("material")) return Upload.materials;
32
+ if (l === "lights" || l.startsWith("lights/")) return Upload.lights;
33
+ if (l.startsWith("shadows/")) return Upload.shadows;
34
+ if (l.startsWith("camera:") || l.startsWith("window/") || l === "globals" || l.startsWith("cull/") || l.endsWith("/view") || l.includes("light-clusters"))
35
+ return Upload.view;
36
+ return Upload.other;
37
+ }
38
+ const MAP_READ = 1;
39
+ const MAP_WRITE = 2;
40
+ const INDEX = 16;
41
+ const VERTEX = 32;
42
+ const UNIFORM = 64;
43
+ const STORAGE = 128;
44
+ const INDIRECT = 256;
45
+ const RENDER_ATTACHMENT = 16;
46
+ function bufferCategory(usage) {
47
+ if (usage & (MAP_READ | MAP_WRITE)) return "staging";
48
+ if (usage & (VERTEX | INDEX)) return "geometry";
49
+ if (usage & (STORAGE | INDIRECT)) return "storage";
50
+ if (usage & UNIFORM) return "uniforms";
51
+ return "other";
52
+ }
53
+ function textureCategory(usage) {
54
+ return usage & RENDER_ATTACHMENT ? "targets" : "textures";
55
+ }
56
+ const SHARED_OWNER = "gpu";
57
+ const entryOf = /* @__PURE__ */ new WeakMap();
58
+ function destroyTracked() {
59
+ const tracked = entryOf.get(this);
60
+ if (tracked) tracked.ledger.drop(tracked.entry);
61
+ Object.getPrototypeOf(this).destroy.call(this);
62
+ return void 0;
63
+ }
64
+ class Ledger {
65
+ owners = /* @__PURE__ */ new Map();
66
+ finalizer = new FinalizationRegistry((entry) => this.drop(entry));
67
+ track(object, owner, texture, bytes, category = texture ? "textures" : "other", label) {
68
+ const owned = this.owned(owner);
69
+ const entry = {
70
+ owner: owned,
71
+ texture,
72
+ category,
73
+ upload: uploadCategory(label, texture),
74
+ bytes,
75
+ ref: new WeakRef(object),
76
+ live: true
77
+ };
78
+ owned.entries.add(entry);
79
+ if (texture) owned.textures++;
80
+ else owned.buffers++;
81
+ owned.bytes += bytes;
82
+ owned.uploads.created++;
83
+ entryOf.set(object, { ledger: this, entry });
84
+ this.finalizer.register(object, entry, entry);
85
+ object.destroy = destroyTracked;
86
+ }
87
+ drop(entry) {
88
+ if (!entry.live) return;
89
+ entry.live = false;
90
+ this.finalizer.unregister(entry);
91
+ const owned = entry.owner;
92
+ owned.entries.delete(entry);
93
+ if (entry.texture) owned.textures--;
94
+ else owned.buffers--;
95
+ owned.bytes -= entry.bytes;
96
+ }
97
+ /** Counts bytes written into a tracked object against its owner and upload category. */
98
+ upload(object, bytes) {
99
+ const tracked = entryOf.get(object);
100
+ if (!tracked || tracked.ledger !== this) return;
101
+ const entry = tracked.entry;
102
+ entry.owner.uploads.bytes[entry.upload] += bytes;
103
+ }
104
+ /** Cumulative bytes written and objects created by `owner`. */
105
+ uploads(owner) {
106
+ return this.owned(owner).uploads;
107
+ }
108
+ /** Destroys every object `owner` still has. Returns how many there were. */
109
+ release(owner) {
110
+ const owned = this.owners.get(owner);
111
+ if (!owned) return 0;
112
+ let n = 0;
113
+ for (const entry of [...owned.entries]) {
114
+ const object = entry.ref.deref();
115
+ this.drop(entry);
116
+ object?.destroy();
117
+ n++;
118
+ }
119
+ return n;
120
+ }
121
+ /** Forgets everything without destroying it: the device it lived on is gone. */
122
+ clear() {
123
+ for (const owned of this.owners.values()) {
124
+ for (const entry of owned.entries) {
125
+ entry.live = false;
126
+ this.finalizer.unregister(entry);
127
+ }
128
+ owned.entries.clear();
129
+ owned.buffers = owned.textures = owned.bytes = 0;
130
+ }
131
+ }
132
+ stats(owner) {
133
+ if (owner !== void 0) {
134
+ const owned = this.owners.get(owner);
135
+ return {
136
+ buffers: owned?.buffers ?? 0,
137
+ textures: owned?.textures ?? 0,
138
+ bytes: owned?.bytes ?? 0
139
+ };
140
+ }
141
+ const total = { buffers: 0, textures: 0, bytes: 0 };
142
+ for (const owned of this.owners.values()) {
143
+ total.buffers += owned.buffers;
144
+ total.textures += owned.textures;
145
+ total.bytes += owned.bytes;
146
+ }
147
+ return total;
148
+ }
149
+ /** Live bytes of `owner` (or everyone) by category. Walks every live object: not per frame. */
150
+ memory(owner) {
151
+ const result = { bytes: 0, byCategory: {} };
152
+ const by = result.byCategory;
153
+ for (const owned of this.owners.values()) {
154
+ if (owner !== void 0 && owned.name !== owner) continue;
155
+ for (const entry of owned.entries) {
156
+ result.bytes += entry.bytes;
157
+ by[entry.category] = (by[entry.category] ?? 0) + entry.bytes;
158
+ }
159
+ }
160
+ return result;
161
+ }
162
+ /** Owners that have live objects, sorted. */
163
+ names() {
164
+ return [...this.owners.values()].filter((o) => o.buffers + o.textures > 0).map((o) => o.name).sort();
165
+ }
166
+ owned(name) {
167
+ let owned = this.owners.get(name);
168
+ if (!owned) {
169
+ owned = {
170
+ name,
171
+ entries: /* @__PURE__ */ new Set(),
172
+ buffers: 0,
173
+ textures: 0,
174
+ bytes: 0,
175
+ uploads: { bytes: new Float64Array(UPLOAD_CATEGORIES.length), created: 0 }
176
+ };
177
+ this.owners.set(name, owned);
178
+ }
179
+ return owned;
180
+ }
181
+ }
182
+ function blockOf(format) {
183
+ if (format.startsWith("astc-")) {
184
+ const [w, h] = format.slice(5).split(/[x-]/).map(Number);
185
+ return [w, h, 16];
186
+ }
187
+ if (/^(bc1|bc4|etc2-rgb8|eac-r11)/.test(format)) return [4, 4, 8];
188
+ if (/^(bc|etc2|eac)/.test(format)) return [4, 4, 16];
189
+ if (/^(rgba32|rgb32)/.test(format)) return [1, 1, 16];
190
+ if (/^(rgba16|rg32)/.test(format) || format === "depth32float-stencil8") return [1, 1, 8];
191
+ if (/^(r8|stencil8)/.test(format)) return [1, 1, 1];
192
+ if (/^(rg8|r16|depth16)/.test(format)) return [1, 1, 2];
193
+ return [1, 1, 4];
194
+ }
195
+ function textureBytes(descriptor) {
196
+ const size = descriptor.size;
197
+ const width = Array.isArray(size) ? size[0] : size.width;
198
+ const height = (Array.isArray(size) ? size[1] : size.height) ?? 1;
199
+ const layers = (Array.isArray(size) ? size[2] : size.depthOrArrayLayers) ?? 1;
200
+ const is3d = descriptor.dimension === "3d";
201
+ const [bw, bh, bytes] = blockOf(descriptor.format);
202
+ let total = 0;
203
+ for (let level = 0; level < (descriptor.mipLevelCount ?? 1); level++) {
204
+ const w = Math.max(1, width >> level);
205
+ const h = Math.max(1, height >> level);
206
+ const d = is3d ? Math.max(1, layers >> level) : layers;
207
+ total += Math.ceil(w / bw) * Math.ceil(h / bh) * bytes * d;
208
+ }
209
+ return total * (descriptor.sampleCount ?? 1);
210
+ }
211
+ export {
212
+ DATA_TEXTURE_PREFIX,
213
+ Ledger,
214
+ SHARED_OWNER,
215
+ UPLOAD_CATEGORIES,
216
+ Upload,
217
+ blockOf,
218
+ bufferCategory,
219
+ textureBytes,
220
+ textureCategory,
221
+ uploadCategory
222
+ };
package/dist/node.d.ts CHANGED
@@ -1,5 +1,28 @@
1
1
  import { type CreateGpuContextOptions, type GpuContext } from './context.js';
2
2
  /** Defines `GPUBufferUsage`, `GPUTextureUsage`, etc. on globalThis, as browsers do. */
3
3
  export declare function installWebGpuGlobals(): void;
4
+ /**
5
+ * `SHARD_DAWN_OPTIONS` passes Dawn options, `;`-separated: CI's Windows job picks the software
6
+ * rasterizer with `adapter=Microsoft Basic Render Driver`, and `backend=vulkan` picks a backend.
7
+ */
4
8
  export declare function nodeGpu(): GPU;
9
+ /**
10
+ * A device through Dawn. `SHARD_TIER=baseline` runs every one at the baseline tier (0064), on a
11
+ * compatibility-mode device: the whole suite, checked against what baseline can do.
12
+ */
5
13
  export declare function createNodeGpuContext(options?: Omit<CreateGpuContextOptions, 'gpu'>): Promise<GpuContext>;
14
+ /** What a headless canvas's WebGPU context was told, for tests to check. */
15
+ export interface HeadlessCanvasContext extends GPUCanvasContext {
16
+ /** The last `configure` call's configuration; undefined while unconfigured. */
17
+ readonly configuration: GPUCanvasConfiguration | undefined;
18
+ /** `configure` calls so far. */
19
+ readonly configures: number;
20
+ }
21
+ export interface HeadlessCanvas extends OffscreenCanvas {
22
+ readonly gpuContext: HeadlessCanvasContext;
23
+ }
24
+ /**
25
+ * A canvas for headless tests (Node has none): its WebGPU context renders into a texture on the
26
+ * configured device, so a surface on it can be drawn into and captured like a real one.
27
+ */
28
+ export declare function headlessCanvas(width: number, height: number): HeadlessCanvas;
package/dist/node.js CHANGED
@@ -8,13 +8,63 @@ function installWebGpuGlobals() {
8
8
  }
9
9
  function nodeGpu() {
10
10
  installWebGpuGlobals();
11
- return create([]);
11
+ const options = process.env.SHARD_DAWN_OPTIONS?.split(";").filter(Boolean) ?? [];
12
+ return create(options);
12
13
  }
13
14
  function createNodeGpuContext(options = {}) {
14
- return createGpuContext({ ...options, gpu: nodeGpu() });
15
+ const tier = options.tier ?? (process.env.SHARD_TIER === "baseline" ? "baseline" : void 0);
16
+ return createGpuContext({ ...options, tier, gpu: nodeGpu() });
17
+ }
18
+ function headlessCanvas(width, height) {
19
+ let configuration;
20
+ let configures = 0;
21
+ let current;
22
+ const canvas = { width, height };
23
+ const context = {
24
+ get canvas() {
25
+ return canvas;
26
+ },
27
+ get configuration() {
28
+ return configuration;
29
+ },
30
+ get configures() {
31
+ return configures;
32
+ },
33
+ configure(config) {
34
+ configuration = config;
35
+ configures++;
36
+ current = void 0;
37
+ },
38
+ unconfigure() {
39
+ configuration = void 0;
40
+ current?.destroy();
41
+ current = void 0;
42
+ },
43
+ getConfiguration: () => configuration ?? null,
44
+ getCurrentTexture() {
45
+ if (!configuration) throw new Error("getCurrentTexture on an unconfigured canvas");
46
+ if (!current || current.width !== canvas.width || current.height !== canvas.height) {
47
+ current?.destroy();
48
+ const device = configuration.device;
49
+ const create2 = Object.getPrototypeOf(device).createTexture;
50
+ current = create2.call(device, {
51
+ label: "headless-canvas",
52
+ size: [canvas.width, canvas.height],
53
+ format: configuration.format,
54
+ usage: configuration.usage ?? GPUTextureUsage.RENDER_ATTACHMENT
55
+ });
56
+ }
57
+ return current;
58
+ }
59
+ };
60
+ canvas.gpuContext = context;
61
+ return Object.assign(canvas, {
62
+ getContext: (kind) => kind === "webgpu" ? context : null
63
+ });
15
64
  }
16
65
  export {
17
66
  createNodeGpuContext,
67
+ headlessCanvas,
18
68
  installWebGpuGlobals,
19
69
  nodeGpu
20
70
  };
package/dist/probe.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { ShardError } from '@aethervtt/shard-core';
2
+ import { type GpuBackendChoice, type Webgl2ContextOptions } from './context.js';
3
+ import { type GpuBackendName, type GpuCapabilities, type GpuTier, type GraphicsReason } from './tier.js';
2
4
  export interface ProbeWebGpuOptions {
3
5
  /** The WebGPU entry point. Defaults to `navigator.gpu`; in Node pass the `webgpu` package's. */
4
6
  gpu?: GPU;
@@ -36,3 +38,35 @@ export interface WebGpuSupport {
36
38
  * throws, so a host can show an honest message instead of a blank canvas.
37
39
  */
38
40
  export declare function probeWebGpu(options?: ProbeWebGpuOptions): Promise<WebGpuSupport>;
41
+ export interface ProbeGraphicsOptions {
42
+ /** The WebGPU entry point. Defaults to `navigator.gpu`; in Node pass the `webgpu` package's. */
43
+ gpu?: GPU;
44
+ /** Which backends to try, as `createGpuContext` has it. Default 'auto'. */
45
+ backend?: GpuBackendChoice;
46
+ /** WebGL2 settings (a fake context, in tests). */
47
+ webgl2?: Webgl2ContextOptions;
48
+ /** `'baseline'`: classify the device the baseline tier would get (a compatibility-mode one). */
49
+ tier?: 'baseline';
50
+ powerPreference?: GPUPowerPreference;
51
+ /** Gives up after this long: some drivers never answer. Default 5000 ms. */
52
+ timeoutMs?: number;
53
+ }
54
+ /** What `probeGraphics` found: the backend and tier an app would get, and why not better (0064). */
55
+ export interface GraphicsSupport {
56
+ backend: GpuBackendName | 'none';
57
+ tier: GpuTier | 'none';
58
+ /** Undefined when nothing opened. */
59
+ capabilities: GpuCapabilities | undefined;
60
+ /** Why a better option was skipped, or why none opened. */
61
+ reasons: GraphicsReason[];
62
+ adapter?: {
63
+ vendor: string;
64
+ architecture: string;
65
+ description: string;
66
+ };
67
+ }
68
+ /**
69
+ * Opens the device an app would get, the same way `createGpuContext` does, classifies it from what
70
+ * it can actually do, and destroys it again. Never throws: an honest `none` with reasons instead.
71
+ */
72
+ export declare function probeGraphics(options?: ProbeGraphicsOptions): Promise<GraphicsSupport>;
package/dist/probe.js CHANGED
@@ -1,4 +1,8 @@
1
1
  import { ShardError } from "@aethervtt/shard-core";
2
+ import { openDevice } from "./context.js";
3
+ import {
4
+ capabilitiesOf
5
+ } from "./tier.js";
2
6
  const HINTS = {
3
7
  "no-webgpu": "This browser has no WebGPU. Use a current Chrome, Edge, or Safari, or enable WebGPU in its settings.",
4
8
  "no-adapter": "WebGPU is present but found no usable GPU. It may be blocklisted or disabled.",
@@ -75,6 +79,66 @@ function unsupported(reason, features, missing, detail) {
75
79
  missing
76
80
  };
77
81
  }
82
+ async function probeGraphics(options = {}) {
83
+ const timeoutMs = options.timeoutMs ?? 5e3;
84
+ let timer;
85
+ const timeout = new Promise((resolve) => {
86
+ timer = setTimeout(() => resolve("timeout"), timeoutMs);
87
+ });
88
+ const opening = openDevice({
89
+ gpu: options.gpu,
90
+ tier: options.tier,
91
+ powerPreference: options.powerPreference,
92
+ backend: options.backend,
93
+ webgl2: options.webgl2
94
+ });
95
+ try {
96
+ const opened = await Promise.race([opening, timeout]);
97
+ if (opened === "timeout") {
98
+ void opening.then((o) => o.device.destroy()).catch(() => {
99
+ });
100
+ return none([{ backend: "webgpu", code: "timeout", message: MESSAGES.timeout }]);
101
+ }
102
+ const info = opened.adapter.info;
103
+ const support = {
104
+ backend: opened.backend,
105
+ tier: opened.tier,
106
+ capabilities: capabilitiesOf(opened.device, opened.backend, opened.hdrSampleCount),
107
+ reasons: opened.reasons,
108
+ adapter: {
109
+ vendor: info?.vendor ?? "",
110
+ architecture: info?.architecture ?? "",
111
+ description: info?.description ?? ""
112
+ }
113
+ };
114
+ opened.device.destroy();
115
+ return support;
116
+ } catch (err) {
117
+ const error = err instanceof ShardError ? err : void 0;
118
+ if (error?.details?.length) {
119
+ return none(
120
+ error.details.map((d) => ({
121
+ backend: d.path,
122
+ code: d.code.replace(/^gpu\//, ""),
123
+ message: d.message
124
+ }))
125
+ );
126
+ }
127
+ return none([
128
+ {
129
+ backend: "webgpu",
130
+ code: error?.code.replace(/^gpu\//, "") ?? "device-failed",
131
+ message: error?.message ?? String(err)
132
+ }
133
+ ]);
134
+ } finally {
135
+ clearTimeout(timer);
136
+ }
137
+ }
138
+ function none(reasons) {
139
+ return { backend: "none", tier: "none", capabilities: void 0, reasons };
140
+ }
78
141
  export {
142
+ probeGraphics,
79
143
  probeWebGpu
80
144
  };
@@ -0,0 +1,51 @@
1
+ import type { GpuContext } from './context.js';
2
+ /**
3
+ * How a surface composites over the page. premultiplied: its pixels are premultiplied RGBA, so a
4
+ * clear alpha of 0 shows what's under the canvas (0052).
5
+ */
6
+ export type SurfaceAlpha = 'opaque' | 'premultiplied';
7
+ export interface SurfaceOptions {
8
+ /** Default 'opaque'. */
9
+ alpha?: SurfaceAlpha;
10
+ /** Names it in errors and `render.describe`. Default `surface`, `surface-2`, … */
11
+ label?: string;
12
+ }
13
+ /**
14
+ * A configured canvas on a `GpuContext` (0052): its own canvas context and alpha mode, sized from a
15
+ * ResizeObserver, reconfigured after a device loss. It's a render target: cameras without a target
16
+ * render into their app's surface.
17
+ */
18
+ export declare class Surface {
19
+ readonly label: string;
20
+ readonly canvas: HTMLCanvasElement | OffscreenCanvas;
21
+ readonly alpha: SurfaceAlpha;
22
+ readonly context: GPUCanvasContext;
23
+ /** Cameras showing on a surface render at the RenderScale and are upscaled (0051). */
24
+ readonly renderScale = true;
25
+ /** Canvas pixels per CSS pixel (devicePixelRatio) at the last resize. */
26
+ pixelRatio: number;
27
+ /** The device it's configured for. */
28
+ readonly gpu: GpuContext;
29
+ private readonly observer;
30
+ private readonly resizeListeners;
31
+ private removed_;
32
+ /** @internal Made by `GpuContext.addSurface`. */
33
+ constructor(gpu: GpuContext, canvas: HTMLCanvasElement | OffscreenCanvas, context: GPUCanvasContext, options: Required<SurfaceOptions>);
34
+ get format(): GPUTextureFormat;
35
+ get width(): number;
36
+ get height(): number;
37
+ /** True once `remove()` was called. */
38
+ get removed(): boolean;
39
+ /** The texture to render into this frame. */
40
+ texture(): GPUTexture;
41
+ /** Calls `listener` after the canvas's backing size changes. Returns an unsubscribe function. */
42
+ onResize(listener: () => void): () => void;
43
+ /** Unconfigures the canvas, stops observing it, and takes it off the device. Idempotent. */
44
+ remove(): void;
45
+ /** @internal Configures the canvas for the current device, keeping its alpha mode. */
46
+ configure(): void;
47
+ /** Sizes the backing store from the element's CSS size (no-op for an OffscreenCanvas). */
48
+ private measure;
49
+ private onObserved;
50
+ private resize;
51
+ }