@graphty/webgpu-graph-algorithms 0.0.0 → 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/README.md +344 -23
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +32 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunks/context-E6iKaeuJ.js +3136 -0
- package/dist/chunks/context-E6iKaeuJ.js.map +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +131 -0
- package/dist/node.js.map +1 -0
- package/dist/src/accelerator.d.ts +26 -0
- package/dist/src/accelerator.d.ts.map +1 -0
- package/dist/src/accelerator.js +101 -0
- package/dist/src/accelerator.js.map +1 -0
- package/dist/src/algorithms/degree.d.ts +35 -0
- package/dist/src/algorithms/degree.d.ts.map +1 -0
- package/dist/src/algorithms/degree.js +119 -0
- package/dist/src/algorithms/degree.js.map +1 -0
- package/dist/src/browser/index.d.ts +23 -0
- package/dist/src/browser/index.d.ts.map +1 -0
- package/dist/src/browser/index.js +48 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/constants.d.ts +92 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +92 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/context.d.ts +84 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +304 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/device/acquire.d.ts +57 -0
- package/dist/src/device/acquire.d.ts.map +1 -0
- package/dist/src/device/acquire.js +232 -0
- package/dist/src/device/acquire.js.map +1 -0
- package/dist/src/device/caps.d.ts +43 -0
- package/dist/src/device/caps.d.ts.map +1 -0
- package/dist/src/device/caps.js +104 -0
- package/dist/src/device/caps.js.map +1 -0
- package/dist/src/device/error-scope.d.ts +75 -0
- package/dist/src/device/error-scope.d.ts.map +1 -0
- package/dist/src/device/error-scope.js +152 -0
- package/dist/src/device/error-scope.js.map +1 -0
- package/dist/src/device/lost.d.ts +51 -0
- package/dist/src/device/lost.d.ts.map +1 -0
- package/dist/src/device/lost.js +130 -0
- package/dist/src/device/lost.js.map +1 -0
- package/dist/src/device/webgpu-constants.d.ts +31 -0
- package/dist/src/device/webgpu-constants.d.ts.map +1 -0
- package/dist/src/device/webgpu-constants.js +31 -0
- package/dist/src/device/webgpu-constants.js.map +1 -0
- package/dist/src/errors.d.ts +56 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +57 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +27 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/kernel/batch.d.ts +116 -0
- package/dist/src/kernel/batch.d.ts.map +1 -0
- package/dist/src/kernel/batch.js +335 -0
- package/dist/src/kernel/batch.js.map +1 -0
- package/dist/src/kernel/dispatch.d.ts +59 -0
- package/dist/src/kernel/dispatch.d.ts.map +1 -0
- package/dist/src/kernel/dispatch.js +139 -0
- package/dist/src/kernel/dispatch.js.map +1 -0
- package/dist/src/kernel/kernel.d.ts +84 -0
- package/dist/src/kernel/kernel.d.ts.map +1 -0
- package/dist/src/kernel/kernel.js +239 -0
- package/dist/src/kernel/kernel.js.map +1 -0
- package/dist/src/kernel/pipeline-cache.d.ts +90 -0
- package/dist/src/kernel/pipeline-cache.d.ts.map +1 -0
- package/dist/src/kernel/pipeline-cache.js +251 -0
- package/dist/src/kernel/pipeline-cache.js.map +1 -0
- package/dist/src/kernel/prelude.d.ts +35 -0
- package/dist/src/kernel/prelude.d.ts.map +1 -0
- package/dist/src/kernel/prelude.js +211 -0
- package/dist/src/kernel/prelude.js.map +1 -0
- package/dist/src/kernel/profiler.d.ts +64 -0
- package/dist/src/kernel/profiler.d.ts.map +1 -0
- package/dist/src/kernel/profiler.js +120 -0
- package/dist/src/kernel/profiler.js.map +1 -0
- package/dist/src/kernel/struct-block.d.ts +122 -0
- package/dist/src/kernel/struct-block.d.ts.map +1 -0
- package/dist/src/kernel/struct-block.js +353 -0
- package/dist/src/kernel/struct-block.js.map +1 -0
- package/dist/src/kernel/uniform-ring.d.ts +70 -0
- package/dist/src/kernel/uniform-ring.d.ts.map +1 -0
- package/dist/src/kernel/uniform-ring.js +146 -0
- package/dist/src/kernel/uniform-ring.js.map +1 -0
- package/dist/src/kernel/wgsl.d.ts +88 -0
- package/dist/src/kernel/wgsl.d.ts.map +1 -0
- package/dist/src/kernel/wgsl.js +390 -0
- package/dist/src/kernel/wgsl.js.map +1 -0
- package/dist/src/kernels.d.ts +81 -0
- package/dist/src/kernels.d.ts.map +1 -0
- package/dist/src/kernels.js +417 -0
- package/dist/src/kernels.js.map +1 -0
- package/dist/src/layouts/force-simulation.d.ts +498 -0
- package/dist/src/layouts/force-simulation.d.ts.map +1 -0
- package/dist/src/layouts/force-simulation.js +1650 -0
- package/dist/src/layouts/force-simulation.js.map +1 -0
- package/dist/src/layouts/forceatlas2.d.ts +210 -0
- package/dist/src/layouts/forceatlas2.d.ts.map +1 -0
- package/dist/src/layouts/forceatlas2.js +759 -0
- package/dist/src/layouts/forceatlas2.js.map +1 -0
- package/dist/src/layouts/inputs.d.ts +40 -0
- package/dist/src/layouts/inputs.d.ts.map +1 -0
- package/dist/src/layouts/inputs.js +185 -0
- package/dist/src/layouts/inputs.js.map +1 -0
- package/dist/src/layouts/repulsion-exact.d.ts +85 -0
- package/dist/src/layouts/repulsion-exact.d.ts.map +1 -0
- package/dist/src/layouts/repulsion-exact.js +134 -0
- package/dist/src/layouts/repulsion-exact.js.map +1 -0
- package/dist/src/layouts/seed.d.ts +56 -0
- package/dist/src/layouts/seed.d.ts.map +1 -0
- package/dist/src/layouts/seed.js +173 -0
- package/dist/src/layouts/seed.js.map +1 -0
- package/dist/src/memory/buffer-pool.d.ts +73 -0
- package/dist/src/memory/buffer-pool.d.ts.map +1 -0
- package/dist/src/memory/buffer-pool.js +170 -0
- package/dist/src/memory/buffer-pool.js.map +1 -0
- package/dist/src/memory/lease.d.ts +53 -0
- package/dist/src/memory/lease.d.ts.map +1 -0
- package/dist/src/memory/lease.js +85 -0
- package/dist/src/memory/lease.js.map +1 -0
- package/dist/src/memory/readback.d.ts +143 -0
- package/dist/src/memory/readback.d.ts.map +1 -0
- package/dist/src/memory/readback.js +375 -0
- package/dist/src/memory/readback.js.map +1 -0
- package/dist/src/memory/residency.d.ts +83 -0
- package/dist/src/memory/residency.d.ts.map +1 -0
- package/dist/src/memory/residency.js +573 -0
- package/dist/src/memory/residency.js.map +1 -0
- package/dist/src/memory/upload-plan.d.ts +101 -0
- package/dist/src/memory/upload-plan.d.ts.map +1 -0
- package/dist/src/memory/upload-plan.js +265 -0
- package/dist/src/memory/upload-plan.js.map +1 -0
- package/dist/src/node/index.d.ts +64 -0
- package/dist/src/node/index.d.ts.map +1 -0
- package/dist/src/node/index.js +183 -0
- package/dist/src/node/index.js.map +1 -0
- package/dist/src/primitives/reduce.d.ts +57 -0
- package/dist/src/primitives/reduce.d.ts.map +1 -0
- package/dist/src/primitives/reduce.js +161 -0
- package/dist/src/primitives/reduce.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts +38 -0
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -0
- package/dist/src/primitives/segmented-reduce.js +211 -0
- package/dist/src/primitives/segmented-reduce.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +209 -0
- package/dist/src/types/accelerator.d.ts.map +1 -0
- package/dist/src/types/accelerator.js +8 -0
- package/dist/src/types/accelerator.js.map +1 -0
- package/dist/src/types/context.d.ts +114 -0
- package/dist/src/types/context.d.ts.map +1 -0
- package/dist/src/types/context.js +7 -0
- package/dist/src/types/context.js.map +1 -0
- package/dist/src/types/layout.d.ts +95 -0
- package/dist/src/types/layout.d.ts.map +1 -0
- package/dist/src/types/layout.js +6 -0
- package/dist/src/types/layout.js.map +1 -0
- package/dist/src/types/memory.d.ts +22 -0
- package/dist/src/types/memory.d.ts.map +1 -0
- package/dist/src/types/memory.js +7 -0
- package/dist/src/types/memory.js.map +1 -0
- package/dist/src/types/options.d.ts +78 -0
- package/dist/src/types/options.d.ts.map +1 -0
- package/dist/src/types/options.js +7 -0
- package/dist/src/types/options.js.map +1 -0
- package/dist/src/types/run.d.ts +13 -0
- package/dist/src/types/run.d.ts.map +1 -0
- package/dist/src/types/run.js +6 -0
- package/dist/src/types/run.js.map +1 -0
- package/dist/src/wgsl/degree.wgsl.d.ts +10 -0
- package/dist/src/wgsl/degree.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/degree.wgsl.js +25 -0
- package/dist/src/wgsl/degree.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +12 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.js +37 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +13 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.js +69 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +12 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +79 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts +15 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.js +54 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +14 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +57 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts +11 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.js +19 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.js.map +1 -0
- package/dist/src/wgsl/fill.wgsl.d.ts +7 -0
- package/dist/src/wgsl/fill.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fill.wgsl.js +14 -0
- package/dist/src/wgsl/fill.wgsl.js.map +1 -0
- package/dist/src/wgsl/reduce.wgsl.d.ts +10 -0
- package/dist/src/wgsl/reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/reduce.wgsl.js +63 -0
- package/dist/src/wgsl/reduce.wgsl.js.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +13 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.js +35 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/webgpu-graph-algorithms.d.ts +1 -0
- package/dist/webgpu-graph-algorithms.js +4454 -0
- package/dist/webgpu-graph-algorithms.js.map +1 -0
- package/package.json +108 -17
- package/src/accelerator.ts +117 -0
- package/src/algorithms/degree.ts +142 -0
- package/src/browser/index.ts +57 -0
- package/src/constants.ts +116 -0
- package/src/context.ts +399 -0
- package/src/device/acquire.ts +256 -0
- package/src/device/caps.ts +122 -0
- package/src/device/error-scope.ts +171 -0
- package/src/device/lost.ts +142 -0
- package/src/device/webgpu-constants.ts +44 -0
- package/src/errors.ts +94 -0
- package/src/index.ts +102 -0
- package/src/kernel/batch.ts +427 -0
- package/src/kernel/dispatch.ts +162 -0
- package/src/kernel/kernel.ts +311 -0
- package/src/kernel/pipeline-cache.ts +288 -0
- package/src/kernel/prelude.ts +229 -0
- package/src/kernel/profiler.ts +148 -0
- package/src/kernel/struct-block.ts +439 -0
- package/src/kernel/uniform-ring.ts +184 -0
- package/src/kernel/wgsl.ts +490 -0
- package/src/kernels.ts +511 -0
- package/src/layouts/force-simulation.ts +2111 -0
- package/src/layouts/forceatlas2.ts +942 -0
- package/src/layouts/inputs.ts +252 -0
- package/src/layouts/repulsion-exact.ts +183 -0
- package/src/layouts/seed.ts +198 -0
- package/src/memory/buffer-pool.ts +204 -0
- package/src/memory/lease.ts +93 -0
- package/src/memory/readback.ts +429 -0
- package/src/memory/residency.ts +753 -0
- package/src/memory/upload-plan.ts +350 -0
- package/src/node/index.ts +230 -0
- package/src/primitives/reduce.ts +233 -0
- package/src/primitives/segmented-reduce.ts +270 -0
- package/src/types/accelerator.ts +236 -0
- package/src/types/context.ts +135 -0
- package/src/types/layout.ts +103 -0
- package/src/types/memory.ts +23 -0
- package/src/types/options.ts +84 -0
- package/src/types/run.ts +13 -0
- package/src/wgsl/degree.wgsl.ts +24 -0
- package/src/wgsl/fa2-attraction.wgsl.ts +37 -0
- package/src/wgsl/fa2-integrate.wgsl.ts +69 -0
- package/src/wgsl/fa2-repulsion-exact.wgsl.ts +78 -0
- package/src/wgsl/fa2-speed-finalize.wgsl.ts +53 -0
- package/src/wgsl/fa2-stats-finalize.wgsl.ts +57 -0
- package/src/wgsl/fa2-to-scene.wgsl.ts +19 -0
- package/src/wgsl/fill.wgsl.ts +13 -0
- package/src/wgsl/reduce.wgsl.ts +62 -0
- package/src/wgsl/segmented-reduce.wgsl.ts +35 -0
package/src/context.ts
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GpuContext (spec 2.2, 2.8, 3.3, 5.7; contract 3.5): one device, its caps and the singletons built on it --
|
|
3
|
+
* the composition root of D26. probe() never creates a device; create() runs the six steps of spec 2.2 in
|
|
4
|
+
* order; from() adopts a device the caller owns. Step 5 (the uncapturederror sink and the device.lost
|
|
5
|
+
* watcher) and step 6 (the AllocationTracker, GraphResidency, BufferPool, Readback and PipelineCache) run
|
|
6
|
+
* in the private constructor. The Profiler is constructed when "timestamp-query" is on the device (contract
|
|
7
|
+
* 3.5; P2-T1) with `quantised` = runtime !== "node" (Dawn-node is the only runtime whose ticks are known
|
|
8
|
+
* unquantised, spec 2.6). This file never imports ./kernels.js or ./wgsl/**.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { GraphSnapshot } from "@graphty/graph-format";
|
|
12
|
+
|
|
13
|
+
import { DEFAULT_WARN_UNRELEASED_SNAPSHOTS } from "./constants.js";
|
|
14
|
+
import {
|
|
15
|
+
buildRequiredFeatures,
|
|
16
|
+
buildRequiredLimits,
|
|
17
|
+
requestAdapter,
|
|
18
|
+
requestDevice,
|
|
19
|
+
summarizeAdapter,
|
|
20
|
+
} from "./device/acquire.js";
|
|
21
|
+
import { assertPlanLimits, capsFromDevice, captureCaps, workgroupSizeFor } from "./device/caps.js";
|
|
22
|
+
import { AllocationTracker } from "./device/error-scope.js";
|
|
23
|
+
import { deviceLostError, installUncapturedErrorSink, PendingErrorSlot, watchDeviceLost } from "./device/lost.js";
|
|
24
|
+
import { isWebGpuGraphError, WebGpuGraphError } from "./errors.js";
|
|
25
|
+
import { PipelineCache } from "./kernel/pipeline-cache.js";
|
|
26
|
+
import { Profiler } from "./kernel/profiler.js";
|
|
27
|
+
import { BufferPool } from "./memory/buffer-pool.js";
|
|
28
|
+
import { Readback } from "./memory/readback.js";
|
|
29
|
+
import { GraphResidency } from "./memory/residency.js";
|
|
30
|
+
import type { GpuCaps, GpuContextOptions, GpuDebugFlags, ProbeOptions, ProbeResult } from "./types/context.js";
|
|
31
|
+
|
|
32
|
+
/** The label a context and its device carry when create() is given none. */
|
|
33
|
+
const DEFAULT_LABEL = "webgpu-graph-algorithms";
|
|
34
|
+
/** The optionalFeatures default of spec 2.2. */
|
|
35
|
+
const DEFAULT_OPTIONAL_FEATURES: readonly GPUFeatureName[] = Object.freeze(["subgroups", "timestamp-query"]);
|
|
36
|
+
/** The powerPreference default of spec 2.2 (Chrome 145 needs it; Dawn-node ignores it). */
|
|
37
|
+
const DEFAULT_POWER_PREFERENCE: GPUPowerPreference = "high-performance";
|
|
38
|
+
|
|
39
|
+
/** What the private constructor takes: the device, its caps and the create-time choices. */
|
|
40
|
+
interface ContextInit {
|
|
41
|
+
readonly device: GPUDevice;
|
|
42
|
+
readonly caps: GpuCaps;
|
|
43
|
+
readonly ownsDevice: boolean;
|
|
44
|
+
readonly label: string;
|
|
45
|
+
readonly onError: ((error: WebGpuGraphError) => void) | null;
|
|
46
|
+
readonly warnUnreleasedSnapshots: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Step 1 of create(): the caller's UNUSED adapter, else one requested from `gpu`; neither -> E_NO_WEBGPU.
|
|
51
|
+
* @param options - the create options
|
|
52
|
+
* @returns an adapter no device was requested from
|
|
53
|
+
*/
|
|
54
|
+
async function adapterOf(options: GpuContextOptions): Promise<GPUAdapter> {
|
|
55
|
+
if (options.adapter !== undefined) {
|
|
56
|
+
return options.adapter;
|
|
57
|
+
}
|
|
58
|
+
if (options.gpu !== undefined) {
|
|
59
|
+
return requestAdapter(options.gpu, options.powerPreference ?? DEFAULT_POWER_PREFERENCE);
|
|
60
|
+
}
|
|
61
|
+
throw new WebGpuGraphError("E_NO_WEBGPU", "GpuContext.create needs one of gpu, adapter or device", {
|
|
62
|
+
reason: "no gpu, adapter or device given",
|
|
63
|
+
hint: "pass navigator.gpu (browser) or the gpu of createNodeGpu() (Node), or use the ./browser and ./node entries",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One device, its caps and the singletons built on it (spec 3.3; the composition root of D26). */
|
|
68
|
+
export class GpuContext {
|
|
69
|
+
/** The device (owned when create() made it, adopted otherwise). */
|
|
70
|
+
readonly device: GPUDevice;
|
|
71
|
+
/** The capability record captured from device.limits / device.features (spec 3.3). */
|
|
72
|
+
readonly caps: GpuCaps;
|
|
73
|
+
/** Resolves with the lost info once the device is lost or destroyed (spec 2.8). */
|
|
74
|
+
readonly lost: Promise<GPUDeviceLostInfo>;
|
|
75
|
+
/**
|
|
76
|
+
* The upload cache (spec 3.3).
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
readonly residency: GraphResidency;
|
|
80
|
+
/** Non-null when "timestamp-query" was granted (P2-T1 makes it functional; P1 always null). */
|
|
81
|
+
readonly profiler: Profiler | null;
|
|
82
|
+
/**
|
|
83
|
+
* The compile-once pipeline cache.
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
readonly pipelines: PipelineCache;
|
|
87
|
+
/**
|
|
88
|
+
* The scratch pool.
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
readonly pool: BufferPool;
|
|
92
|
+
/**
|
|
93
|
+
* The staging ring.
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
readonly readback: Readback;
|
|
97
|
+
/**
|
|
98
|
+
* The OOM-scoped allocator shared by residency and pool.
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
readonly allocator: AllocationTracker;
|
|
102
|
+
/**
|
|
103
|
+
* The workgroup size of this device (spec 5.1).
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
readonly workgroupSize: number;
|
|
107
|
+
/**
|
|
108
|
+
* True when create() made the device (dispose() destroys it).
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
readonly ownsDevice: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* The label given to create().
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
readonly label: string;
|
|
117
|
+
/**
|
|
118
|
+
* Mutable test-build flags (GpuDebugFlags).
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
readonly debug: GpuDebugFlags;
|
|
122
|
+
|
|
123
|
+
private currentState: "ready" | "lost" | "disposed" = "ready";
|
|
124
|
+
private lostError: WebGpuGraphError | null = null;
|
|
125
|
+
private readonly slot = new PendingErrorSlot();
|
|
126
|
+
private readonly uninstallSink: () => void;
|
|
127
|
+
private readonly lostListeners = new Set<(info: GPUDeviceLostInfo) => void>();
|
|
128
|
+
private readonly disposers: (() => void)[] = [];
|
|
129
|
+
private batchCounter = 0;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Steps 5 and 6 of create() (spec 2.2): the sink, the loss watcher and the singletons.
|
|
133
|
+
* @param init - the device, its caps and the create-time choices
|
|
134
|
+
*/
|
|
135
|
+
private constructor(init: ContextInit) {
|
|
136
|
+
this.device = init.device;
|
|
137
|
+
this.caps = init.caps;
|
|
138
|
+
this.ownsDevice = init.ownsDevice;
|
|
139
|
+
this.label = init.label;
|
|
140
|
+
this.debug = { inspect: false };
|
|
141
|
+
this.workgroupSize = workgroupSizeFor(init.caps);
|
|
142
|
+
this.allocator = new AllocationTracker(init.device);
|
|
143
|
+
this.residency = new GraphResidency(init.device, init.caps, this.allocator, {
|
|
144
|
+
warnUnreleasedSnapshots: init.warnUnreleasedSnapshots,
|
|
145
|
+
});
|
|
146
|
+
this.pool = new BufferPool(init.device, this.allocator, init.caps.limits.maxBufferSize);
|
|
147
|
+
this.readback = new Readback(init.device, this.allocator);
|
|
148
|
+
this.pipelines = new PipelineCache(init.device, init.caps);
|
|
149
|
+
this.profiler = init.device.features.has("timestamp-query")
|
|
150
|
+
? new Profiler(init.device, true, init.caps.runtime !== "node")
|
|
151
|
+
: null;
|
|
152
|
+
this.uninstallSink = installUncapturedErrorSink(init.device, this.slot, init.onError);
|
|
153
|
+
this.lost = watchDeviceLost(init.device, (info) => {
|
|
154
|
+
this.handleLost(info);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Probe without creating a device (spec 2.2); never throws. `gpu === undefined` -> E_NO_WEBGPU;
|
|
160
|
+
* requestAdapter() null or a throw -> E_NO_ADAPTER; `rejectSoftware && software` -> E_SOFTWARE_ONLY (the
|
|
161
|
+
* adapter and its summary are still reported); else OK with the UNUSED adapter and its summary.
|
|
162
|
+
* @param options - the GPU object, the power preference and the software policy
|
|
163
|
+
* @returns the probe result
|
|
164
|
+
*/
|
|
165
|
+
static async probe(options: ProbeOptions): Promise<ProbeResult> {
|
|
166
|
+
const { gpu } = options;
|
|
167
|
+
if (gpu === undefined) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
code: "E_NO_WEBGPU",
|
|
171
|
+
reason: "no GPU object: navigator.gpu is undefined (no WebGPU in this runtime)",
|
|
172
|
+
adapter: null,
|
|
173
|
+
summary: null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
let adapter: GPUAdapter;
|
|
177
|
+
try {
|
|
178
|
+
adapter = await requestAdapter(gpu, options.powerPreference ?? DEFAULT_POWER_PREFERENCE);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const reason = isWebGpuGraphError(err) ? err.message : String(err);
|
|
181
|
+
return { ok: false, code: "E_NO_ADAPTER", reason, adapter: null, summary: null };
|
|
182
|
+
}
|
|
183
|
+
const summary = summarizeAdapter(adapter);
|
|
184
|
+
if (options.rejectSoftware === true && summary.software) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
code: "E_SOFTWARE_ONLY",
|
|
188
|
+
reason: `software adapter rejected: ${summary.vendor}/${summary.architecture}`,
|
|
189
|
+
adapter,
|
|
190
|
+
summary,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return { ok: true, code: "OK", reason: null, adapter, summary };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The six-step acquisition of spec 2.2: adopt `device`, else take `adapter`, else request one from `gpu`
|
|
198
|
+
* (step 1); the software test (step 2); the limits / features and requestDevice (step 3); the caps from
|
|
199
|
+
* device.limits plus assertPlanLimits (step 4); the sink, the loss watcher and the singletons (steps 5-6,
|
|
200
|
+
* in the constructor).
|
|
201
|
+
* @param options - see GpuContextOptions
|
|
202
|
+
* @returns the context
|
|
203
|
+
*/
|
|
204
|
+
static async create(options: GpuContextOptions): Promise<GpuContext> {
|
|
205
|
+
const runtime = options.runtime ?? "unknown";
|
|
206
|
+
const label = options.label ?? DEFAULT_LABEL;
|
|
207
|
+
const onError = options.onError ?? null;
|
|
208
|
+
const warnUnreleasedSnapshots = options.warnUnreleasedSnapshots ?? DEFAULT_WARN_UNRELEASED_SNAPSHOTS;
|
|
209
|
+
if (options.device !== undefined) {
|
|
210
|
+
const adopted = capsFromDevice(options.device, { runtime });
|
|
211
|
+
assertPlanLimits(adopted);
|
|
212
|
+
return new GpuContext({
|
|
213
|
+
device: options.device,
|
|
214
|
+
caps: adopted,
|
|
215
|
+
ownsDevice: false,
|
|
216
|
+
label,
|
|
217
|
+
onError,
|
|
218
|
+
warnUnreleasedSnapshots,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const adapter = await adapterOf(options);
|
|
222
|
+
const summary = summarizeAdapter(adapter);
|
|
223
|
+
if (options.rejectSoftware === true && summary.software) {
|
|
224
|
+
throw new WebGpuGraphError(
|
|
225
|
+
"E_SOFTWARE_ONLY",
|
|
226
|
+
`software adapter rejected by rejectSoftware: ${summary.vendor}/${summary.architecture}`,
|
|
227
|
+
{ adapter: summary },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const requiredLimits = buildRequiredLimits(adapter, options.limits ?? "raise");
|
|
231
|
+
const requiredFeatures = buildRequiredFeatures(
|
|
232
|
+
adapter,
|
|
233
|
+
options.requiredFeatures ?? [],
|
|
234
|
+
options.optionalFeatures ?? DEFAULT_OPTIONAL_FEATURES,
|
|
235
|
+
);
|
|
236
|
+
const device = await requestDevice(adapter, { label, requiredLimits, requiredFeatures });
|
|
237
|
+
const caps = captureCaps(device, adapter.info, runtime, options.gpu?.wgslLanguageFeatures ?? []);
|
|
238
|
+
try {
|
|
239
|
+
assertPlanLimits(caps);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
device.destroy();
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
return new GpuContext({ device, caps, ownsDevice: true, label, onError, warnUnreleasedSnapshots });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Zero-cost adoption of a device the caller owns (ownsDevice false, runtime "unknown" unless info says
|
|
249
|
+
* otherwise). The context is labelled with the device's label, or "GpuContext.from" when it has none.
|
|
250
|
+
* @param device - the device to adopt
|
|
251
|
+
* @param info - what the caller knows about the adapter, if anything
|
|
252
|
+
* @returns the context
|
|
253
|
+
*/
|
|
254
|
+
static from(device: GPUDevice, info?: Partial<GpuCaps>): GpuContext {
|
|
255
|
+
const caps = capsFromDevice(device, info);
|
|
256
|
+
assertPlanLimits(caps);
|
|
257
|
+
const deviceLabel: string = device.label ?? "";
|
|
258
|
+
return new GpuContext({
|
|
259
|
+
device,
|
|
260
|
+
caps,
|
|
261
|
+
ownsDevice: false,
|
|
262
|
+
label: deviceLabel === "" ? "GpuContext.from" : deviceLabel,
|
|
263
|
+
onError: null,
|
|
264
|
+
warnUnreleasedSnapshots: DEFAULT_WARN_UNRELEASED_SNAPSHOTS,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* "ready" | "lost" | "disposed".
|
|
270
|
+
* @returns the lifecycle state
|
|
271
|
+
*/
|
|
272
|
+
get state(): "ready" | "lost" | "disposed" {
|
|
273
|
+
return this.currentState;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Throws E_DEVICE_LOST / E_DISPOSED by state and rethrows a pending uncaptured error (spec 5.7: "thrown
|
|
278
|
+
* from the next public call").
|
|
279
|
+
* @internal
|
|
280
|
+
*/
|
|
281
|
+
assertReady(): void {
|
|
282
|
+
if (this.currentState === "disposed") {
|
|
283
|
+
throw new WebGpuGraphError("E_DISPOSED", `GpuContext "${this.label}" is disposed`, { label: this.label });
|
|
284
|
+
}
|
|
285
|
+
if (this.lostError !== null) {
|
|
286
|
+
throw this.lostError;
|
|
287
|
+
}
|
|
288
|
+
const pending = this.slot.take();
|
|
289
|
+
if (pending !== null) {
|
|
290
|
+
throw pending;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Takes the pending uncaptured error (used by CommandBatch right after submit).
|
|
296
|
+
* @internal
|
|
297
|
+
* @returns the error, or null
|
|
298
|
+
*/
|
|
299
|
+
takePendingError(): WebGpuGraphError | null {
|
|
300
|
+
return this.slot.take();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Monotonically increasing batch ids, starting at 1.
|
|
305
|
+
* @internal
|
|
306
|
+
* @returns the next id
|
|
307
|
+
*/
|
|
308
|
+
nextBatchId(): number {
|
|
309
|
+
this.batchCounter += 1;
|
|
310
|
+
return this.batchCounter;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Registers a device-loss listener (simulations enter "disposed"); returns the unregister function.
|
|
315
|
+
* @internal
|
|
316
|
+
* @param listener - called once with the lost info
|
|
317
|
+
* @returns removes the listener
|
|
318
|
+
*/
|
|
319
|
+
onLost(listener: (info: GPUDeviceLostInfo) => void): () => void {
|
|
320
|
+
this.lostListeners.add(listener);
|
|
321
|
+
return () => {
|
|
322
|
+
this.lostListeners.delete(listener);
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A disposer run by dispose() (the Node entry attaches the GPU handle's dispose); attached after
|
|
328
|
+
* dispose() it runs immediately.
|
|
329
|
+
* @internal
|
|
330
|
+
* @param dispose - the disposer
|
|
331
|
+
*/
|
|
332
|
+
attachDisposer(dispose: () => void): void {
|
|
333
|
+
if (this.currentState === "disposed") {
|
|
334
|
+
dispose();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.disposers.push(dispose);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Destroys every buffer recorded for the snapshot (spec 4.5), then `pool.trim()` (spec 4.4: idle scratch
|
|
342
|
+
* goes with the graph); idempotent; safe on a snapshot never uploaded; a no-op after dispose().
|
|
343
|
+
* @param snapshot - the snapshot whose buffers are released
|
|
344
|
+
*/
|
|
345
|
+
release(snapshot: GraphSnapshot): void {
|
|
346
|
+
if (this.currentState === "disposed") {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.residency.release(snapshot);
|
|
350
|
+
this.pool.trim();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Rejects pending work with E_DISPOSED (every later public call), destroys residency, pool and staging
|
|
355
|
+
* ring, destroys the device when owned, runs attached disposers; idempotent (spec 2.8).
|
|
356
|
+
*/
|
|
357
|
+
dispose(): void {
|
|
358
|
+
if (this.currentState === "disposed") {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.currentState = "disposed";
|
|
362
|
+
this.uninstallSink();
|
|
363
|
+
this.residency.destroyAll();
|
|
364
|
+
this.pool.destroyAll();
|
|
365
|
+
this.readback.destroyAll();
|
|
366
|
+
if (this.profiler !== null) {
|
|
367
|
+
this.profiler.destroy();
|
|
368
|
+
}
|
|
369
|
+
if (this.ownsDevice) {
|
|
370
|
+
this.device.destroy();
|
|
371
|
+
}
|
|
372
|
+
const disposers = this.disposers.splice(0);
|
|
373
|
+
for (const dispose of disposers) {
|
|
374
|
+
dispose();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Step 5 of spec 2.2 on loss: state "lost", the residency cleared without destroying (the buffers are
|
|
380
|
+
* gone with the device), every registered listener run once; a loss reported after dispose() (the owned
|
|
381
|
+
* device was destroyed by dispose) changes nothing.
|
|
382
|
+
* @param info - the device.lost result
|
|
383
|
+
*/
|
|
384
|
+
private handleLost(info: GPUDeviceLostInfo): void {
|
|
385
|
+
if (this.currentState === "disposed") {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.currentState = "lost";
|
|
389
|
+
this.lostError = deviceLostError(info);
|
|
390
|
+
this.residency.clearOnLoss();
|
|
391
|
+
for (const listener of Array.from(this.lostListeners)) {
|
|
392
|
+
try {
|
|
393
|
+
listener(info);
|
|
394
|
+
} catch {
|
|
395
|
+
// a throwing listener must not stop the fan-out; the others still learn of the loss
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device acquisition (spec 2.2 steps 1-3, 2.6; contract 3.4): the adapter request, the software-adapter
|
|
3
|
+
* test, the adapter summary, the requiredLimits / requiredFeatures builders and the device request. Nothing
|
|
4
|
+
* above the device layer is constructed here (D26); src/context.ts sequences these calls. An adapter is
|
|
5
|
+
* consumed by its first requestDevice (WebGPU 3.5.1; Dawn-node rejects the second call with
|
|
6
|
+
* `OperationError: adapter is "consumed"`), so every device the package creates comes from a fresh adapter.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
10
|
+
import type { AdapterInfoLike, AdapterSummary, LimitPolicy, RaisableLimit } from "../types/context.js";
|
|
11
|
+
|
|
12
|
+
/** The six raisable limits in the order create() requests them (spec 2.2). */
|
|
13
|
+
export const RAISABLE_LIMITS: readonly RaisableLimit[] = Object.freeze([
|
|
14
|
+
"maxBufferSize",
|
|
15
|
+
"maxStorageBufferBindingSize",
|
|
16
|
+
"maxStorageBuffersPerShaderStage",
|
|
17
|
+
"maxComputeWorkgroupStorageSize",
|
|
18
|
+
"maxComputeInvocationsPerWorkgroup",
|
|
19
|
+
"maxComputeWorkgroupSizeX",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The WebGPU limit names summarizeAdapter reads (the GPUSupportedLimits members of @webgpu/types 0.1.72).
|
|
24
|
+
* A fixed list because Chromium exposes the limits as prototype getters (Object.keys is empty) and Dawn-node as an
|
|
25
|
+
* addon object; reading each name works identically on both, and a name the runtime lacks is simply absent.
|
|
26
|
+
*/
|
|
27
|
+
const LIMIT_NAMES: readonly string[] = [
|
|
28
|
+
"maxTextureDimension1D",
|
|
29
|
+
"maxTextureDimension2D",
|
|
30
|
+
"maxTextureDimension3D",
|
|
31
|
+
"maxTextureArrayLayers",
|
|
32
|
+
"maxBindGroups",
|
|
33
|
+
"maxBindGroupsPlusVertexBuffers",
|
|
34
|
+
"maxBindingsPerBindGroup",
|
|
35
|
+
"maxDynamicUniformBuffersPerPipelineLayout",
|
|
36
|
+
"maxDynamicStorageBuffersPerPipelineLayout",
|
|
37
|
+
"maxSampledTexturesPerShaderStage",
|
|
38
|
+
"maxSamplersPerShaderStage",
|
|
39
|
+
"maxStorageBuffersPerShaderStage",
|
|
40
|
+
"maxStorageBuffersInVertexStage",
|
|
41
|
+
"maxStorageBuffersInFragmentStage",
|
|
42
|
+
"maxStorageTexturesPerShaderStage",
|
|
43
|
+
"maxStorageTexturesInVertexStage",
|
|
44
|
+
"maxStorageTexturesInFragmentStage",
|
|
45
|
+
"maxUniformBuffersPerShaderStage",
|
|
46
|
+
"maxUniformBufferBindingSize",
|
|
47
|
+
"maxStorageBufferBindingSize",
|
|
48
|
+
"minUniformBufferOffsetAlignment",
|
|
49
|
+
"minStorageBufferOffsetAlignment",
|
|
50
|
+
"maxVertexBuffers",
|
|
51
|
+
"maxBufferSize",
|
|
52
|
+
"maxVertexAttributes",
|
|
53
|
+
"maxVertexBufferArrayStride",
|
|
54
|
+
"maxInterStageShaderVariables",
|
|
55
|
+
"maxColorAttachments",
|
|
56
|
+
"maxColorAttachmentBytesPerSample",
|
|
57
|
+
"maxComputeWorkgroupStorageSize",
|
|
58
|
+
"maxComputeInvocationsPerWorkgroup",
|
|
59
|
+
"maxComputeWorkgroupSizeX",
|
|
60
|
+
"maxComputeWorkgroupSizeY",
|
|
61
|
+
"maxComputeWorkgroupSizeZ",
|
|
62
|
+
"maxComputeWorkgroupsPerDimension",
|
|
63
|
+
"maxImmediateSize",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** Adapters that created a device through requestDevice(): a second create({ adapter }) fails without a round trip. */
|
|
67
|
+
const consumedAdapters = new WeakSet<GPUAdapter>();
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The message of a thrown value.
|
|
71
|
+
* @param err - what was caught
|
|
72
|
+
* @returns the Error message, or the value as a string
|
|
73
|
+
*/
|
|
74
|
+
function messageOf(err: unknown): string {
|
|
75
|
+
return err instanceof Error ? err.message : String(err);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Software adapter test (spec 2.2 step 2): architecture "software" (Dawn llvmpipe) or "swiftshader", or
|
|
80
|
+
* isFallbackAdapter === true (Chromium); the ONE reader of these fields.
|
|
81
|
+
* @param info - the adapter info (structural, so tests can fake it)
|
|
82
|
+
* @returns true for a software adapter
|
|
83
|
+
*/
|
|
84
|
+
export function isSoftwareAdapter(info: AdapterInfoLike): boolean {
|
|
85
|
+
// "software" = Dawn-node on llvmpipe, "swiftshader" = Chromium's fallback, "warp" = Dawn's D3D12 backend on
|
|
86
|
+
// Microsoft's WARP rasterizer (the Windows host lane: vendor "microsoft", isFallbackAdapter absent under Dawn-node).
|
|
87
|
+
return (
|
|
88
|
+
info.architecture === "software" ||
|
|
89
|
+
info.architecture === "swiftshader" ||
|
|
90
|
+
info.architecture === "warp" ||
|
|
91
|
+
info.isFallbackAdapter === true
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The AdapterSummary of an adapter (info + features + limits); never requests a device.
|
|
97
|
+
* @param adapter - an adapter (unused or not; only its info, features and limits are read)
|
|
98
|
+
* @returns a frozen summary; `subgroupMinSize` / `subgroupMaxSize` are 0 when the info lacks them
|
|
99
|
+
*/
|
|
100
|
+
export function summarizeAdapter(adapter: GPUAdapter): AdapterSummary {
|
|
101
|
+
const { info } = adapter;
|
|
102
|
+
const source = adapter.limits as unknown as Readonly<Record<string, unknown>>;
|
|
103
|
+
const limits: Record<string, number> = {};
|
|
104
|
+
for (const name of LIMIT_NAMES) {
|
|
105
|
+
const value = source[name];
|
|
106
|
+
if (typeof value === "number") {
|
|
107
|
+
limits[name] = value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
vendor: info.vendor,
|
|
112
|
+
architecture: info.architecture,
|
|
113
|
+
device: info.device,
|
|
114
|
+
description: info.description,
|
|
115
|
+
software: isSoftwareAdapter(info),
|
|
116
|
+
subgroupMinSize: info.subgroupMinSize ?? 0,
|
|
117
|
+
subgroupMaxSize: info.subgroupMaxSize ?? 0,
|
|
118
|
+
features: Object.freeze(Array.from(adapter.features).sort()),
|
|
119
|
+
limits: Object.freeze(limits),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Step 1 of create(): requestAdapter with the power preference; null -> E_NO_ADAPTER.
|
|
125
|
+
* @param gpu - navigator.gpu or the Dawn handle
|
|
126
|
+
* @param powerPreference - honoured by browsers, ignored by Dawn-node (spec 2.6)
|
|
127
|
+
* @returns an UNUSED adapter
|
|
128
|
+
*/
|
|
129
|
+
export async function requestAdapter(gpu: GPU, powerPreference: GPUPowerPreference): Promise<GPUAdapter> {
|
|
130
|
+
let adapter: GPUAdapter | null;
|
|
131
|
+
try {
|
|
132
|
+
adapter = await gpu.requestAdapter({ powerPreference });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const reason = `requestAdapter() threw: ${messageOf(err)}`;
|
|
135
|
+
throw new WebGpuGraphError("E_NO_ADAPTER", reason, { reason });
|
|
136
|
+
}
|
|
137
|
+
if (adapter === null) {
|
|
138
|
+
const reason = "requestAdapter() returned null: no usable WebGPU adapter";
|
|
139
|
+
throw new WebGpuGraphError("E_NO_ADAPTER", reason, { reason });
|
|
140
|
+
}
|
|
141
|
+
return adapter;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Step 3 of create(): the requiredLimits record for a policy, clamped to the adapter; an explicit value
|
|
146
|
+
* above the adapter -> E_NO_DEVICE { reason: "limit" } before any requestDevice call, so the failure is
|
|
147
|
+
* diagnosable.
|
|
148
|
+
* @param adapter - the adapter the device will come from
|
|
149
|
+
* @param policy - "default" (nothing), "raise" (every raisable limit at the adapter's value) or explicit values
|
|
150
|
+
* @returns the record to pass as requiredLimits
|
|
151
|
+
*/
|
|
152
|
+
export function buildRequiredLimits(adapter: GPUAdapter, policy: LimitPolicy): Record<string, number> {
|
|
153
|
+
const limits: Record<string, number> = {};
|
|
154
|
+
if (policy === "default") {
|
|
155
|
+
return limits;
|
|
156
|
+
}
|
|
157
|
+
if (policy === "raise") {
|
|
158
|
+
for (const name of RAISABLE_LIMITS) {
|
|
159
|
+
limits[name] = adapter.limits[name];
|
|
160
|
+
}
|
|
161
|
+
return limits;
|
|
162
|
+
}
|
|
163
|
+
for (const name of RAISABLE_LIMITS) {
|
|
164
|
+
const requested = policy[name];
|
|
165
|
+
if (requested === undefined) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const available = adapter.limits[name];
|
|
169
|
+
if (requested > available) {
|
|
170
|
+
throw new WebGpuGraphError(
|
|
171
|
+
"E_NO_DEVICE",
|
|
172
|
+
`requested ${name} = ${requested} exceeds the adapter's ${available}`,
|
|
173
|
+
{ reason: "limit", adapter: summarizeAdapter(adapter), limit: name, requested, available },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
limits[name] = requested;
|
|
177
|
+
}
|
|
178
|
+
return limits;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Step 3 of create(): the requiredFeatures list = required + (optional intersect adapter.features); a missing
|
|
183
|
+
* required feature -> E_NO_DEVICE { reason: "feature", requested: <name> }.
|
|
184
|
+
* @param adapter - the adapter the device will come from
|
|
185
|
+
* @param required - features the caller cannot do without
|
|
186
|
+
* @param optional - features requested only when the adapter has them
|
|
187
|
+
* @returns the deduplicated list, required first
|
|
188
|
+
*/
|
|
189
|
+
export function buildRequiredFeatures(
|
|
190
|
+
adapter: GPUAdapter,
|
|
191
|
+
required: readonly GPUFeatureName[],
|
|
192
|
+
optional: readonly GPUFeatureName[],
|
|
193
|
+
): GPUFeatureName[] {
|
|
194
|
+
const features: GPUFeatureName[] = [];
|
|
195
|
+
for (const feature of required) {
|
|
196
|
+
if (!adapter.features.has(feature)) {
|
|
197
|
+
throw new WebGpuGraphError("E_NO_DEVICE", `required feature "${feature}" is not supported by the adapter`, {
|
|
198
|
+
reason: "feature",
|
|
199
|
+
adapter: summarizeAdapter(adapter),
|
|
200
|
+
requested: feature,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (!features.includes(feature)) {
|
|
204
|
+
features.push(feature);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const feature of optional) {
|
|
208
|
+
if (adapter.features.has(feature) && !features.includes(feature)) {
|
|
209
|
+
features.push(feature);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return features;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The E_NO_DEVICE of a consumed adapter; the runtime's text lives in the error message only (contract 3.1
|
|
217
|
+
* documents `details` as { reason, adapter, limit?, requested?, available? }).
|
|
218
|
+
* @param adapter - the adapter that already created a device
|
|
219
|
+
* @param message - the runtime's rejection message when there was a round trip
|
|
220
|
+
* @returns E_NO_DEVICE { reason: "consumed", adapter }
|
|
221
|
+
*/
|
|
222
|
+
function consumedError(adapter: GPUAdapter, message: string): WebGpuGraphError {
|
|
223
|
+
return new WebGpuGraphError(
|
|
224
|
+
"E_NO_DEVICE",
|
|
225
|
+
`the adapter already created a device (an adapter is consumed by one requestDevice, spec 2.2 step 1): ${message}`,
|
|
226
|
+
{ reason: "consumed", adapter: summarizeAdapter(adapter) },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Step 3 of create(): requestDevice; a rejection -> E_NO_DEVICE with the adapter summary ("consumed" when
|
|
232
|
+
* the adapter already created a device, detected by the OperationError message or a prior-use record).
|
|
233
|
+
* @param adapter - an UNUSED adapter
|
|
234
|
+
* @param descriptor - requiredLimits, requiredFeatures and the label
|
|
235
|
+
* @returns the device
|
|
236
|
+
*/
|
|
237
|
+
export async function requestDevice(adapter: GPUAdapter, descriptor: GPUDeviceDescriptor): Promise<GPUDevice> {
|
|
238
|
+
if (consumedAdapters.has(adapter)) {
|
|
239
|
+
throw consumedError(adapter, "recorded by this package");
|
|
240
|
+
}
|
|
241
|
+
let device: GPUDevice;
|
|
242
|
+
try {
|
|
243
|
+
device = await adapter.requestDevice(descriptor);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const message = messageOf(err);
|
|
246
|
+
if (/consumed|already been used/i.test(message)) {
|
|
247
|
+
throw consumedError(adapter, message);
|
|
248
|
+
}
|
|
249
|
+
throw new WebGpuGraphError("E_NO_DEVICE", `requestDevice() rejected: ${message}`, {
|
|
250
|
+
reason: "requestDevice",
|
|
251
|
+
adapter: summarizeAdapter(adapter),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
consumedAdapters.add(adapter);
|
|
255
|
+
return device;
|
|
256
|
+
}
|