@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.
- package/dist/caches.d.ts +12 -0
- package/dist/caches.js +24 -0
- package/dist/context.d.ts +180 -12
- package/dist/context.js +377 -58
- package/dist/errors.js +8 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +29 -1
- package/dist/ledger.d.ts +106 -0
- package/dist/ledger.js +222 -0
- package/dist/node.d.ts +23 -0
- package/dist/node.js +52 -2
- package/dist/probe.d.ts +34 -0
- package/dist/probe.js +64 -0
- package/dist/surface.d.ts +51 -0
- package/dist/surface.js +110 -0
- package/dist/tier.d.ts +41 -0
- package/dist/tier.js +42 -0
- package/package.json +4 -3
package/dist/context.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { ShardError } from "@aethervtt/shard-core";
|
|
2
2
|
import { LayoutCache, PipelineCache } from "./caches.js";
|
|
3
3
|
import { toShardError } from "./errors.js";
|
|
4
|
+
import {
|
|
5
|
+
blockOf,
|
|
6
|
+
bufferCategory,
|
|
7
|
+
DATA_TEXTURE_PREFIX,
|
|
8
|
+
Ledger,
|
|
9
|
+
SHARED_OWNER,
|
|
10
|
+
textureBytes,
|
|
11
|
+
textureCategory
|
|
12
|
+
} from "./ledger.js";
|
|
13
|
+
import { Surface } from "./surface.js";
|
|
14
|
+
import {
|
|
15
|
+
capabilitiesOf,
|
|
16
|
+
isCoreDevice,
|
|
17
|
+
probeHdrSampleCount
|
|
18
|
+
} from "./tier.js";
|
|
4
19
|
const COMPRESSED_TEXTURE_FEATURES = [
|
|
5
20
|
"texture-compression-bc",
|
|
6
21
|
"texture-compression-astc",
|
|
@@ -16,45 +31,147 @@ class GpuContext {
|
|
|
16
31
|
adapter;
|
|
17
32
|
device;
|
|
18
33
|
format;
|
|
19
|
-
canvas;
|
|
20
|
-
context;
|
|
21
34
|
features;
|
|
35
|
+
/** The graphics API the device runs on (0064). */
|
|
36
|
+
backend;
|
|
37
|
+
/**
|
|
38
|
+
* The tier the engine runs at (0064): `full` on a core WebGPU device, `baseline` on anything
|
|
39
|
+
* else that runs the baseline subset. Kept across device replacement.
|
|
40
|
+
*/
|
|
41
|
+
tier;
|
|
42
|
+
/** What the device can do, as far as the tiers care. */
|
|
43
|
+
capabilities;
|
|
44
|
+
/** Why better options were skipped while opening the device (no core adapter, say). */
|
|
45
|
+
reasons;
|
|
22
46
|
pipelines;
|
|
23
47
|
layouts;
|
|
24
48
|
/** Increments when the device is replaced. */
|
|
25
49
|
generation = 0;
|
|
26
|
-
/**
|
|
27
|
-
|
|
50
|
+
/**
|
|
51
|
+
* How long the first adapter and device request took, in ms (0062's `coldStart.device`). Set by
|
|
52
|
+
* `createGpuContext`; 0 for a context built directly.
|
|
53
|
+
*/
|
|
54
|
+
deviceMs = 0;
|
|
55
|
+
/**
|
|
56
|
+
* Who new buffers and textures count against. An app sets it to its owner while it runs (the
|
|
57
|
+
* render plugin's scope); otherwise it's `'gpu'`, the device's own.
|
|
58
|
+
*/
|
|
59
|
+
owner = SHARED_OWNER;
|
|
28
60
|
/** Most recent GPU errors, newest last. */
|
|
29
61
|
errors = [];
|
|
62
|
+
/** Whether the device works now (0061). */
|
|
63
|
+
status = "ok";
|
|
30
64
|
options;
|
|
31
65
|
errorListeners = /* @__PURE__ */ new Set();
|
|
32
66
|
lostListeners = /* @__PURE__ */ new Set();
|
|
33
|
-
|
|
67
|
+
surfaces_ = [];
|
|
68
|
+
ledger = new Ledger();
|
|
69
|
+
shared_ = /* @__PURE__ */ new Map();
|
|
70
|
+
recreating;
|
|
71
|
+
destroyed = false;
|
|
72
|
+
/** The WebGL2 shim's canvas contexts, when that's the backend. */
|
|
73
|
+
canvasContext;
|
|
74
|
+
constructor(options, adapter, device, format, opened = {}) {
|
|
34
75
|
this.options = options;
|
|
35
76
|
this.adapter = adapter;
|
|
36
77
|
this.device = device;
|
|
37
78
|
this.format = format;
|
|
38
|
-
this.canvas = options.canvas;
|
|
39
|
-
this.context = context;
|
|
40
79
|
this.features = new Set(device.features);
|
|
80
|
+
this.backend = opened.backend ?? "webgpu";
|
|
81
|
+
this.tier = opened.tier ?? (isCoreDevice(device) ? "full" : "baseline");
|
|
82
|
+
this.capabilities = capabilitiesOf(device, this.backend, opened.hdrSampleCount);
|
|
83
|
+
this.reasons = opened.reasons ?? [];
|
|
84
|
+
this.canvasContext = opened.canvasContext;
|
|
41
85
|
this.pipelines = new PipelineCache(this);
|
|
42
86
|
this.layouts = new LayoutCache(this);
|
|
43
87
|
this.attach(device);
|
|
44
88
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
89
|
+
// --- surfaces ----------------------------------------------------------------
|
|
90
|
+
/** The canvases this device draws into, in the order they were added. */
|
|
91
|
+
get surfaces() {
|
|
92
|
+
return this.surfaces_;
|
|
93
|
+
}
|
|
94
|
+
/** Configures a canvas as a surface of this device. `surface.remove()` gives it back. */
|
|
95
|
+
addSurface(canvas, options = {}) {
|
|
96
|
+
const context = this.canvasContext ? this.canvasContext(canvas) : canvas.getContext("webgpu");
|
|
97
|
+
if (!context) {
|
|
98
|
+
const api = this.backend === "webgl2" ? "WebGL2" : "WebGPU";
|
|
99
|
+
throw new ShardError("gpu/no-context", `Could not get a ${api} canvas context`);
|
|
100
|
+
}
|
|
101
|
+
if (this.surfaces_.some((s) => s.canvas === canvas)) {
|
|
102
|
+
throw new ShardError("gpu/duplicate-surface", "This canvas is already a surface", {
|
|
103
|
+
hint: "Share the Surface itself (renderPlugin({ gpu, surface })), or remove it first."
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const n = this.surfaces_.length;
|
|
107
|
+
const surface = new Surface(this, canvas, context, {
|
|
108
|
+
alpha: options.alpha ?? "opaque",
|
|
109
|
+
label: options.label ?? (n === 0 ? "surface" : `surface-${n + 1}`)
|
|
110
|
+
});
|
|
111
|
+
this.surfaces_.push(surface);
|
|
112
|
+
return surface;
|
|
113
|
+
}
|
|
114
|
+
/** @internal Called by `Surface.remove()`. */
|
|
115
|
+
forgetSurface(surface) {
|
|
116
|
+
const i = this.surfaces_.indexOf(surface);
|
|
117
|
+
if (i !== -1) this.surfaces_.splice(i, 1);
|
|
118
|
+
}
|
|
119
|
+
/** WebGL2: where shader translations came from, and what naga translated (0064). */
|
|
120
|
+
shaderCache() {
|
|
121
|
+
return this.device.shaderCache;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* WebGL2: every shader translation this session used, as a baked set to ship as
|
|
125
|
+
* `webgl2.shaders` (0064). Undefined on WebGPU.
|
|
126
|
+
*/
|
|
127
|
+
exportShaderCache() {
|
|
128
|
+
return this.device.exportShaders?.();
|
|
129
|
+
}
|
|
130
|
+
// --- accounting ----------------------------------------------------------------
|
|
131
|
+
/** Runs `fn` with new buffers and textures counted against `owner`. */
|
|
132
|
+
withOwner(owner, fn) {
|
|
133
|
+
const previous = this.owner;
|
|
134
|
+
this.owner = owner;
|
|
135
|
+
try {
|
|
136
|
+
return fn();
|
|
137
|
+
} finally {
|
|
138
|
+
this.owner = previous;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Live buffers and textures this device made for `owner`, or for everyone (0052). */
|
|
142
|
+
stats(owner) {
|
|
143
|
+
return this.ledger.stats(owner);
|
|
144
|
+
}
|
|
145
|
+
/** Live bytes for `owner` (or everyone) by what they're for: targets, textures, geometry... (0062). */
|
|
146
|
+
memory(owner) {
|
|
147
|
+
return this.ledger.memory(owner);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Bytes `owner` has written to the GPU since the device was made, by upload category, and the
|
|
151
|
+
* buffers and textures it created (0055). Cumulative: diff two reads for a frame's worth.
|
|
152
|
+
*/
|
|
153
|
+
uploads(owner) {
|
|
154
|
+
return this.ledger.uploads(owner);
|
|
155
|
+
}
|
|
156
|
+
/** Owners with live objects, sorted. */
|
|
157
|
+
owners() {
|
|
158
|
+
return this.ledger.names();
|
|
57
159
|
}
|
|
160
|
+
/** Destroys every buffer and texture `owner` still has. Returns how many. */
|
|
161
|
+
release(owner) {
|
|
162
|
+
return this.ledger.release(owner);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A per-device object every app on this device shares, made once by `create` and counted against
|
|
166
|
+
* the device, not whichever app asked first. Made again after a device loss.
|
|
167
|
+
*/
|
|
168
|
+
shared(key, create) {
|
|
169
|
+
if (this.shared_.has(key)) return this.shared_.get(key);
|
|
170
|
+
const value = this.withOwner(SHARED_OWNER, () => create(this.device));
|
|
171
|
+
this.shared_.set(key, value);
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
// --- errors and loss -------------------------------------------------------------
|
|
58
175
|
onError(listener) {
|
|
59
176
|
this.errorListeners.add(listener);
|
|
60
177
|
return () => this.errorListeners.delete(listener);
|
|
@@ -63,6 +180,10 @@ class GpuContext {
|
|
|
63
180
|
this.lostListeners.add(listener);
|
|
64
181
|
return () => this.lostListeners.delete(listener);
|
|
65
182
|
}
|
|
183
|
+
/** Error and device-loss listeners, for teardown tests. */
|
|
184
|
+
get listenerCount() {
|
|
185
|
+
return { error: this.errorListeners.size, lost: this.lostListeners.size };
|
|
186
|
+
}
|
|
66
187
|
reportError(error) {
|
|
67
188
|
this.errors.push(error);
|
|
68
189
|
if (this.errors.length > MAX_ERRORS) this.errors.shift();
|
|
@@ -80,27 +201,77 @@ class GpuContext {
|
|
|
80
201
|
});
|
|
81
202
|
return result;
|
|
82
203
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Gets a fresh adapter and device after a loss and reconfigures every surface with its alpha
|
|
206
|
+
* mode. Apps sharing the device all call it; they share one recreation.
|
|
207
|
+
*/
|
|
208
|
+
recreate() {
|
|
209
|
+
this.recreating ??= this.recover().finally(() => {
|
|
210
|
+
this.recreating = void 0;
|
|
211
|
+
});
|
|
212
|
+
return this.recreating;
|
|
213
|
+
}
|
|
214
|
+
/** Replaces the device, retrying as `options.recovery` says; `failed` once every try failed. */
|
|
215
|
+
async recover() {
|
|
216
|
+
const attempts = Math.max(1, this.options.recovery?.attempts ?? 3);
|
|
217
|
+
const interval = this.options.recovery?.intervalMs ?? 1e3;
|
|
218
|
+
this.status = "lost";
|
|
219
|
+
let last;
|
|
220
|
+
for (let i = 0; i < attempts; i++) {
|
|
221
|
+
if (i > 0) await new Promise((resolve) => setTimeout(resolve, interval));
|
|
222
|
+
if (this.destroyed) return;
|
|
223
|
+
try {
|
|
224
|
+
await this.replaceDevice();
|
|
225
|
+
this.status = "ok";
|
|
226
|
+
return;
|
|
227
|
+
} catch (err) {
|
|
228
|
+
last = err;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
this.status = "failed";
|
|
232
|
+
const error = new ShardError(
|
|
233
|
+
"gpu/recovery-failed",
|
|
234
|
+
`The GPU device couldn't be replaced after ${attempts} tries`,
|
|
235
|
+
{ hint: "Tell the user 3D is unavailable; reloading the page tries again.", cause: last }
|
|
236
|
+
);
|
|
237
|
+
this.reportError(error);
|
|
238
|
+
throw error;
|
|
93
239
|
}
|
|
94
240
|
/** For tests and debugging: destroys the device and reports it as an unexpected loss. */
|
|
95
241
|
simulateDeviceLoss() {
|
|
96
242
|
this.handleLoss({ reason: "unknown", message: "Simulated device loss" });
|
|
97
243
|
this.device.destroy();
|
|
98
244
|
}
|
|
245
|
+
/** Removes every surface and destroys the device. */
|
|
99
246
|
destroy() {
|
|
247
|
+
if (this.destroyed) return;
|
|
248
|
+
this.destroyed = true;
|
|
100
249
|
this.lostListeners.clear();
|
|
250
|
+
this.errorListeners.clear();
|
|
251
|
+
for (const surface of [...this.surfaces_]) surface.remove();
|
|
252
|
+
this.ledger.clear();
|
|
253
|
+
this.shared_.clear();
|
|
101
254
|
this.device.destroy();
|
|
102
255
|
}
|
|
256
|
+
async replaceDevice() {
|
|
257
|
+
const { adapter, device, hdrSampleCount } = await openDevice(
|
|
258
|
+
this.options,
|
|
259
|
+
this.tier,
|
|
260
|
+
this.backend
|
|
261
|
+
);
|
|
262
|
+
this.adapter = adapter;
|
|
263
|
+
this.device = device;
|
|
264
|
+
this.capabilities = capabilitiesOf(device, this.backend, hdrSampleCount);
|
|
265
|
+
this.ledger.clear();
|
|
266
|
+
this.shared_.clear();
|
|
267
|
+
this.pipelines.clear();
|
|
268
|
+
this.layouts.clear();
|
|
269
|
+
this.generation++;
|
|
270
|
+
this.attach(device);
|
|
271
|
+
for (const surface of this.surfaces_) surface.configure();
|
|
272
|
+
}
|
|
103
273
|
attach(device) {
|
|
274
|
+
this.instrument(device);
|
|
104
275
|
device.addEventListener("uncapturederror", (event) => {
|
|
105
276
|
this.reportError(toShardError(event.error, void 0));
|
|
106
277
|
});
|
|
@@ -110,30 +281,184 @@ class GpuContext {
|
|
|
110
281
|
}
|
|
111
282
|
});
|
|
112
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Counts every buffer and texture the device makes against the current owner, and every byte
|
|
286
|
+
* written into one against its owner and upload category.
|
|
287
|
+
*/
|
|
288
|
+
instrument(device) {
|
|
289
|
+
const ledger = this.ledger;
|
|
290
|
+
const createBuffer = device.createBuffer.bind(device);
|
|
291
|
+
const createTexture = device.createTexture.bind(device);
|
|
292
|
+
device.createBuffer = (descriptor) => {
|
|
293
|
+
const buffer = createBuffer(descriptor);
|
|
294
|
+
const category = bufferCategory(descriptor.usage);
|
|
295
|
+
ledger.track(buffer, this.owner, false, descriptor.size, category, descriptor.label);
|
|
296
|
+
return buffer;
|
|
297
|
+
};
|
|
298
|
+
device.createTexture = (descriptor) => {
|
|
299
|
+
const texture = createTexture(descriptor);
|
|
300
|
+
const data = descriptor.label?.startsWith(DATA_TEXTURE_PREFIX) === true;
|
|
301
|
+
const category = data ? "storage" : textureCategory(descriptor.usage);
|
|
302
|
+
ledger.track(texture, this.owner, true, textureBytes(descriptor), category, descriptor.label);
|
|
303
|
+
return texture;
|
|
304
|
+
};
|
|
305
|
+
const queue = device.queue;
|
|
306
|
+
const writeBuffer = queue.writeBuffer.bind(queue);
|
|
307
|
+
const writeTexture = queue.writeTexture.bind(queue);
|
|
308
|
+
const copyExternal = queue.copyExternalImageToTexture?.bind(queue);
|
|
309
|
+
queue.writeBuffer = (buffer, offset, data, dataOffset, size) => {
|
|
310
|
+
writeBuffer(buffer, offset, data, dataOffset, size);
|
|
311
|
+
ledger.upload(buffer, writtenBytes(data, dataOffset, size));
|
|
312
|
+
};
|
|
313
|
+
queue.writeTexture = (destination, data, layout, size) => {
|
|
314
|
+
writeTexture(destination, data, layout, size);
|
|
315
|
+
ledger.upload(destination.texture, textureWriteBytes(destination.texture, data, layout, size));
|
|
316
|
+
};
|
|
317
|
+
if (copyExternal) {
|
|
318
|
+
queue.copyExternalImageToTexture = (source, destination, size) => {
|
|
319
|
+
copyExternal(source, destination, size);
|
|
320
|
+
const [w, h, d] = extent(size);
|
|
321
|
+
ledger.upload(destination.texture, w * h * d * 4);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
113
325
|
handleLoss(info) {
|
|
326
|
+
this.status = "lost";
|
|
114
327
|
for (const listener of this.lostListeners) listener(info);
|
|
115
328
|
}
|
|
116
329
|
}
|
|
117
|
-
function
|
|
330
|
+
function writtenBytes(data, dataOffset, size) {
|
|
331
|
+
if (ArrayBuffer.isView(data)) {
|
|
332
|
+
const unit = data.BYTES_PER_ELEMENT ?? 1;
|
|
333
|
+
return (size ?? data.byteLength / unit - (dataOffset ?? 0)) * unit;
|
|
334
|
+
}
|
|
335
|
+
return size ?? data.byteLength - (dataOffset ?? 0);
|
|
336
|
+
}
|
|
337
|
+
function extent(size) {
|
|
338
|
+
if (Symbol.iterator in size) {
|
|
339
|
+
const a = [...size];
|
|
340
|
+
return [a[0] ?? 1, a[1] ?? 1, a[2] ?? 1];
|
|
341
|
+
}
|
|
342
|
+
const d = size;
|
|
343
|
+
return [d.width, d.height ?? 1, d.depthOrArrayLayers ?? 1];
|
|
344
|
+
}
|
|
345
|
+
function textureWriteBytes(texture, data, layout, size) {
|
|
346
|
+
const [w, h, d] = extent(size);
|
|
347
|
+
const bytes = ArrayBuffer.isView(data) ? data.byteLength : data.byteLength;
|
|
348
|
+
const total = bytes - (layout.offset ?? 0);
|
|
349
|
+
const [bw, bh, blockBytes] = blockOf(texture.format);
|
|
350
|
+
return Math.min(total, Math.ceil(w / bw) * Math.ceil(h / bh) * blockBytes * d);
|
|
351
|
+
}
|
|
352
|
+
async function openDevice(options, keep, keepBackend) {
|
|
353
|
+
const choice = keepBackend ?? options.backend ?? "auto";
|
|
354
|
+
const reasons = [];
|
|
355
|
+
if (choice !== "webgl2") {
|
|
356
|
+
const gpu = options.gpu ?? globalThis.navigator?.gpu;
|
|
357
|
+
if (gpu) {
|
|
358
|
+
try {
|
|
359
|
+
return await openWebGpu(gpu, options, keep, []);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
if (choice === "webgpu") throw err;
|
|
362
|
+
const error = err instanceof ShardError ? err : void 0;
|
|
363
|
+
reasons.push({
|
|
364
|
+
backend: "webgpu",
|
|
365
|
+
code: error?.code.replace(/^gpu\//, "") ?? "device-failed",
|
|
366
|
+
message: error?.message ?? String(err)
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
} else if (choice === "webgpu") {
|
|
370
|
+
throw new ShardError("gpu/unsupported", "WebGPU is not available in this environment", {
|
|
371
|
+
hint: "Use a browser or webview with WebGPU, or pass `gpu` (e.g. from the `webgpu` package in Node)."
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
reasons.push({ backend: "webgpu", code: "no-webgpu", message: "This browser has no WebGPU" });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return openWebgl2(options, reasons);
|
|
378
|
+
}
|
|
379
|
+
async function openWebGpu(gpu, options, keep, reasons) {
|
|
380
|
+
const powerPreference = options.powerPreference ?? "high-performance";
|
|
381
|
+
const baseline = options.tier === "baseline" || keep === "baseline";
|
|
382
|
+
if (!baseline) {
|
|
383
|
+
const adapter2 = await gpu.requestAdapter({ powerPreference });
|
|
384
|
+
if (adapter2) {
|
|
385
|
+
const device2 = await requestDevice(adapter2, options);
|
|
386
|
+
const hdrSampleCount2 = await probeHdrSampleCount(device2);
|
|
387
|
+
return {
|
|
388
|
+
adapter: adapter2,
|
|
389
|
+
device: device2,
|
|
390
|
+
backend: "webgpu",
|
|
391
|
+
tier: tierOf(device2, false),
|
|
392
|
+
reasons,
|
|
393
|
+
hdrSampleCount: hdrSampleCount2,
|
|
394
|
+
gpu
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (keep === "full") throw new ShardError("gpu/no-adapter", "No WebGPU adapter was found");
|
|
398
|
+
reasons.push({
|
|
399
|
+
backend: "webgpu",
|
|
400
|
+
code: "no-core-adapter",
|
|
401
|
+
message: "No core WebGPU adapter; asking for a compatibility-mode one"
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const adapter = await gpu.requestAdapter({ featureLevel: "compatibility", powerPreference });
|
|
405
|
+
if (!adapter) throw new ShardError("gpu/no-adapter", "No WebGPU adapter was found");
|
|
406
|
+
const device = await requestDevice(adapter, options);
|
|
407
|
+
const hdrSampleCount = await probeHdrSampleCount(device);
|
|
118
408
|
return {
|
|
409
|
+
adapter,
|
|
119
410
|
device,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
411
|
+
backend: "webgpu",
|
|
412
|
+
tier: tierOf(device, baseline),
|
|
413
|
+
reasons,
|
|
414
|
+
hdrSampleCount,
|
|
415
|
+
gpu
|
|
124
416
|
};
|
|
125
417
|
}
|
|
126
|
-
async function
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
418
|
+
async function openWebgl2(options, reasons) {
|
|
419
|
+
const webgl2 = await import("@aethervtt/shard-gpu-webgl2");
|
|
420
|
+
const settings = options.webgl2 ?? {};
|
|
421
|
+
const gpu = webgl2.createWebgl2Gpu({
|
|
422
|
+
canvas: options.canvas,
|
|
423
|
+
context: settings.context,
|
|
424
|
+
profile: settings.profile,
|
|
425
|
+
checkErrors: settings.checkErrors,
|
|
426
|
+
shaders: settings.shaders,
|
|
427
|
+
persist: settings.persist
|
|
428
|
+
});
|
|
429
|
+
const powerPreference = options.powerPreference ?? "high-performance";
|
|
430
|
+
const adapter = await gpu.requestAdapter({ powerPreference });
|
|
431
|
+
if (!adapter) {
|
|
432
|
+
const code = gpu.unavailable ?? "no-webgl2";
|
|
433
|
+
reasons.push({ backend: "webgl2", code, message: WEBGL2_MESSAGES[code] ?? code });
|
|
434
|
+
throw new ShardError("gpu/unsupported", "Neither WebGPU nor WebGL2 can run Shard here", {
|
|
435
|
+
hint: reasons.map((r) => `${r.backend}: ${r.message}`).join("; "),
|
|
436
|
+
// Each backend's reason, its backend as the path (probeGraphics reads them back).
|
|
437
|
+
details: reasons.map((r) => new ShardError(`gpu/${r.code}`, r.message, { path: r.backend }))
|
|
131
438
|
});
|
|
132
439
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
440
|
+
const device = await requestDevice(adapter, options);
|
|
441
|
+
const hdrSampleCount = await probeHdrSampleCount(device);
|
|
442
|
+
return {
|
|
443
|
+
adapter,
|
|
444
|
+
device,
|
|
445
|
+
backend: "webgl2",
|
|
446
|
+
tier: "baseline",
|
|
447
|
+
reasons,
|
|
448
|
+
hdrSampleCount,
|
|
449
|
+
gpu,
|
|
450
|
+
canvasContext: webgl2.canvasContext
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const WEBGL2_MESSAGES = {
|
|
454
|
+
"no-webgl2": "This browser has no WebGL2",
|
|
455
|
+
"context-lost": "The WebGL2 context is lost",
|
|
456
|
+
"no-float-render-targets": "WebGL2 here has no float render targets (EXT_color_buffer_float)"
|
|
457
|
+
};
|
|
458
|
+
function tierOf(device, baseline) {
|
|
459
|
+
return baseline || !isCoreDevice(device) ? "baseline" : "full";
|
|
460
|
+
}
|
|
461
|
+
async function requestDevice(adapter, options) {
|
|
137
462
|
const missing = (options.requiredFeatures ?? []).filter((f) => !adapter.features.has(f));
|
|
138
463
|
if (missing.length > 0) {
|
|
139
464
|
throw new ShardError(
|
|
@@ -149,25 +474,19 @@ async function requestDevice(options) {
|
|
|
149
474
|
...options.compressedTextures === false ? [] : COMPRESSED_TEXTURE_FEATURES
|
|
150
475
|
].filter((f, i, all) => all.indexOf(f) === i && adapter.features.has(f))
|
|
151
476
|
];
|
|
152
|
-
|
|
153
|
-
return { adapter, device };
|
|
477
|
+
return adapter.requestDevice({ label: "shard", requiredFeatures });
|
|
154
478
|
}
|
|
155
479
|
async function createGpuContext(options = {}) {
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const format = gpu.getPreferredCanvasFormat();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!ctx) throw new ShardError("gpu/no-context", "Could not get a WebGPU canvas context");
|
|
163
|
-
ctx.configure(canvasConfig(device, format));
|
|
164
|
-
context = ctx;
|
|
165
|
-
}
|
|
166
|
-
const result = new GpuContext(options, adapter, device, format, context);
|
|
167
|
-
result.resize();
|
|
480
|
+
const start = performance.now();
|
|
481
|
+
const opened = await openDevice(options);
|
|
482
|
+
const format = opened.gpu.getPreferredCanvasFormat();
|
|
483
|
+
const result = new GpuContext(options, opened.adapter, opened.device, format, opened);
|
|
484
|
+
result.deviceMs = performance.now() - start;
|
|
485
|
+
if (options.canvas) result.addSurface(options.canvas, { alpha: options.alpha });
|
|
168
486
|
return result;
|
|
169
487
|
}
|
|
170
488
|
export {
|
|
171
489
|
GpuContext,
|
|
172
|
-
createGpuContext
|
|
490
|
+
createGpuContext,
|
|
491
|
+
openDevice
|
|
173
492
|
};
|
package/dist/errors.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { ShardError } from "@aethervtt/shard-core";
|
|
2
2
|
function toShardError(error, label) {
|
|
3
3
|
const what = label ? `"${label}"` : "an unlabeled GPU object";
|
|
4
|
-
|
|
4
|
+
if (error instanceof ShardError) {
|
|
5
|
+
return new ShardError(error.code, `${error.message} (in ${what})`, {
|
|
6
|
+
path: label,
|
|
7
|
+
hint: error.hint,
|
|
8
|
+
cause: error
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
const isOom = typeof GPUOutOfMemoryError !== "undefined" && error instanceof GPUOutOfMemoryError || error.name === "GPUOutOfMemoryError";
|
|
5
12
|
return new ShardError(
|
|
6
13
|
isOom ? "gpu/out-of-memory" : "gpu/validation",
|
|
7
14
|
`WebGPU error in ${what}: ${error.message}`,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export type { BakedTranslations } from '@aethervtt/shard-gpu-webgl2';
|
|
1
2
|
export { GpuBuffer, type GpuBufferOptions } from './buffer.js';
|
|
2
3
|
export { LayoutCache, PipelineCache } from './caches.js';
|
|
3
|
-
export { type CreateGpuContextOptions, createGpuContext, type DeviceLostInfo, GpuContext, } from './context.js';
|
|
4
|
+
export { type CreateGpuContextOptions, createGpuContext, type DeviceLostInfo, type GpuBackendChoice, GpuContext, type GpuStatus, type ShaderCacheStats, type Webgl2ContextOptions, } from './context.js';
|
|
4
5
|
export { type GpuErrorListener, toShardError } from './errors.js';
|
|
5
6
|
export { descriptorKey } from './key.js';
|
|
6
|
-
export { type
|
|
7
|
+
export { bufferCategory, DATA_TEXTURE_PREFIX, type GpuMemory, type GpuMemoryCategory, type GpuStats, type GpuUploads, SHARED_OWNER, textureBytes, textureCategory, UPLOAD_CATEGORIES, Upload, type UploadCategory, uploadCategory, } from './ledger.js';
|
|
8
|
+
export { type GraphicsSupport, type ProbeGraphicsOptions, type ProbeWebGpuOptions, probeGraphics, probeWebGpu, type WebGpuSupport, type WebGpuUnsupportedReason, } from './probe.js';
|
|
9
|
+
export { Surface, type SurfaceAlpha, type SurfaceOptions } from './surface.js';
|
|
10
|
+
export { capabilitiesOf, type GpuBackendName, type GpuCapabilities, type GpuTier, type GraphicsReason, isCoreDevice, } from './tier.js';
|
package/dist/index.js
CHANGED
|
@@ -7,15 +7,43 @@ import {
|
|
|
7
7
|
import { toShardError } from "./errors.js";
|
|
8
8
|
import { descriptorKey } from "./key.js";
|
|
9
9
|
import {
|
|
10
|
+
bufferCategory,
|
|
11
|
+
DATA_TEXTURE_PREFIX,
|
|
12
|
+
SHARED_OWNER,
|
|
13
|
+
textureBytes,
|
|
14
|
+
textureCategory,
|
|
15
|
+
UPLOAD_CATEGORIES,
|
|
16
|
+
Upload,
|
|
17
|
+
uploadCategory
|
|
18
|
+
} from "./ledger.js";
|
|
19
|
+
import {
|
|
20
|
+
probeGraphics,
|
|
10
21
|
probeWebGpu
|
|
11
22
|
} from "./probe.js";
|
|
23
|
+
import { Surface } from "./surface.js";
|
|
24
|
+
import {
|
|
25
|
+
capabilitiesOf,
|
|
26
|
+
isCoreDevice
|
|
27
|
+
} from "./tier.js";
|
|
12
28
|
export {
|
|
29
|
+
DATA_TEXTURE_PREFIX,
|
|
13
30
|
GpuBuffer,
|
|
14
31
|
GpuContext,
|
|
15
32
|
LayoutCache,
|
|
16
33
|
PipelineCache,
|
|
34
|
+
SHARED_OWNER,
|
|
35
|
+
Surface,
|
|
36
|
+
UPLOAD_CATEGORIES,
|
|
37
|
+
Upload,
|
|
38
|
+
bufferCategory,
|
|
39
|
+
capabilitiesOf,
|
|
17
40
|
createGpuContext,
|
|
18
41
|
descriptorKey,
|
|
42
|
+
isCoreDevice,
|
|
43
|
+
probeGraphics,
|
|
19
44
|
probeWebGpu,
|
|
20
|
-
|
|
45
|
+
textureBytes,
|
|
46
|
+
textureCategory,
|
|
47
|
+
toShardError,
|
|
48
|
+
uploadCategory
|
|
21
49
|
};
|