@graphty/webgpu-graph-algorithms 0.6.26 → 0.6.28
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 +56 -6
- package/dist/acquire.d.ts +2 -0
- package/dist/browser.js +18 -1
- package/dist/browser.js.map +1 -1
- package/dist/chunks/accelerator-B-FjQwaA.js +19173 -0
- package/dist/chunks/accelerator-B-FjQwaA.js.map +1 -0
- package/dist/chunks/managed-D_GdQtnu.js +98 -0
- package/dist/chunks/managed-D_GdQtnu.js.map +1 -0
- package/dist/node.js +18 -1
- package/dist/node.js.map +1 -1
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +5 -3
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/algorithms/all-pairs.d.ts.map +1 -1
- package/dist/src/algorithms/all-pairs.js +72 -47
- package/dist/src/algorithms/all-pairs.js.map +1 -1
- package/dist/src/algorithms/betweenness.d.ts +1 -1
- package/dist/src/algorithms/betweenness.js +2 -2
- package/dist/src/algorithms/closeness.d.ts +45 -42
- package/dist/src/algorithms/closeness.d.ts.map +1 -1
- package/dist/src/algorithms/closeness.js +295 -226
- package/dist/src/algorithms/closeness.js.map +1 -1
- package/dist/src/browser/index.d.ts +10 -0
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +22 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/constants.d.ts +13 -3
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +13 -3
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernels.d.ts +14 -4
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +62 -28
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/layouts/force-simulation.d.ts.map +1 -1
- package/dist/src/layouts/force-simulation.js +0 -1
- package/dist/src/layouts/force-simulation.js.map +1 -1
- package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
- package/dist/src/layouts/forceatlas2.js +0 -1
- package/dist/src/layouts/forceatlas2.js.map +1 -1
- package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -1
- package/dist/src/layouts/fruchterman-reingold.js +0 -1
- package/dist/src/layouts/fruchterman-reingold.js.map +1 -1
- package/dist/src/layouts/repulsion-grid.d.ts +1 -3
- package/dist/src/layouts/repulsion-grid.d.ts.map +1 -1
- package/dist/src/layouts/repulsion-grid.js +1 -6
- package/dist/src/layouts/repulsion-grid.js.map +1 -1
- package/dist/src/layouts/spring-electrical.d.ts.map +1 -1
- package/dist/src/layouts/spring-electrical.js +0 -1
- package/dist/src/layouts/spring-electrical.js.map +1 -1
- package/dist/src/managed.d.ts +11 -0
- package/dist/src/managed.d.ts.map +1 -0
- package/dist/src/managed.js +129 -0
- package/dist/src/managed.js.map +1 -0
- package/dist/src/node/index.d.ts +11 -0
- package/dist/src/node/index.d.ts.map +1 -1
- package/dist/src/node/index.js +21 -0
- package/dist/src/node/index.js.map +1 -1
- package/dist/src/primitives/grid-pyramid.d.ts +16 -15
- package/dist/src/primitives/grid-pyramid.d.ts.map +1 -1
- package/dist/src/primitives/grid-pyramid.js +20 -28
- package/dist/src/primitives/grid-pyramid.js.map +1 -1
- package/dist/src/types/accelerator.d.ts +2 -0
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/managed.d.ts +81 -0
- package/dist/src/types/managed.d.ts.map +1 -0
- package/dist/src/types/managed.js +7 -0
- package/dist/src/types/managed.js.map +1 -0
- package/dist/src/wgsl/bc-forward.wgsl.d.ts +1 -1
- package/dist/src/wgsl/bc-forward.wgsl.js +1 -1
- package/dist/src/wgsl/closeness-level.wgsl.d.ts +37 -0
- package/dist/src/wgsl/closeness-level.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/closeness-level.wgsl.js +204 -0
- package/dist/src/wgsl/closeness-level.wgsl.js.map +1 -0
- package/dist/src/wgsl/closeness-rowsum.wgsl.d.ts +11 -0
- package/dist/src/wgsl/closeness-rowsum.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/closeness-rowsum.wgsl.js +42 -0
- package/dist/src/wgsl/closeness-rowsum.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts +2 -2
- package/dist/src/wgsl/grid-centroid-hub.wgsl.js +2 -2
- package/dist/webgpu-graph-algorithms.js +142 -15586
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +10 -4
- package/src/accelerator.ts +5 -3
- package/src/algorithms/all-pairs.ts +86 -56
- package/src/algorithms/betweenness.ts +2 -2
- package/src/algorithms/closeness.ts +353 -256
- package/src/browser/index.ts +37 -0
- package/src/constants.ts +13 -3
- package/src/kernels.ts +65 -36
- package/src/layouts/force-simulation.ts +0 -1
- package/src/layouts/forceatlas2.ts +0 -1
- package/src/layouts/fruchterman-reingold.ts +0 -1
- package/src/layouts/repulsion-grid.ts +2 -7
- package/src/layouts/spring-electrical.ts +0 -1
- package/src/managed.ts +172 -0
- package/src/node/index.ts +36 -0
- package/src/primitives/grid-pyramid.ts +29 -41
- package/src/types/accelerator.ts +2 -0
- package/src/types/managed.ts +86 -0
- package/src/wgsl/bc-forward.wgsl.ts +1 -1
- package/src/wgsl/closeness-level.wgsl.ts +203 -0
- package/src/wgsl/closeness-rowsum.wgsl.ts +41 -0
- package/src/wgsl/grid-centroid-hub.wgsl.ts +2 -2
- package/dist/chunks/context-BZY6SMsM.js +0 -3615
- package/dist/chunks/context-BZY6SMsM.js.map +0 -1
- package/dist/src/wgsl/closeness-reduce.wgsl.d.ts +0 -20
- package/dist/src/wgsl/closeness-reduce.wgsl.d.ts.map +0 -1
- package/dist/src/wgsl/closeness-reduce.wgsl.js +0 -69
- package/dist/src/wgsl/closeness-reduce.wgsl.js.map +0 -1
- package/dist/src/wgsl/closeness-sweep.wgsl.d.ts +0 -22
- package/dist/src/wgsl/closeness-sweep.wgsl.d.ts.map +0 -1
- package/dist/src/wgsl/closeness-sweep.wgsl.js +0 -106
- package/dist/src/wgsl/closeness-sweep.wgsl.js.map +0 -1
- package/src/wgsl/closeness-reduce.wgsl.ts +0 -68
- package/src/wgsl/closeness-sweep.wgsl.ts +0 -105
|
@@ -1,3615 +0,0 @@
|
|
|
1
|
-
import { INVALID_INDEX } from "@graphty/graph-format";
|
|
2
|
-
const WORKGROUP_SIZE = 256;
|
|
3
|
-
const MAX_WORKGROUPS_PER_DIM = 65535;
|
|
4
|
-
const MAX_1D_ITEMS = MAX_WORKGROUPS_PER_DIM * WORKGROUP_SIZE;
|
|
5
|
-
const U32_MAX = 4294967295;
|
|
6
|
-
const RADIX_BINS = 256;
|
|
7
|
-
const ARC_WINDOW_ALIGN = 64;
|
|
8
|
-
const STORAGE_ALIGN = 256;
|
|
9
|
-
const UNIFORM_SLOT_BYTES = 256;
|
|
10
|
-
const EXACT_MAX_NODES = 32768;
|
|
11
|
-
const DEFAULT_STAGING_SLOTS = 3;
|
|
12
|
-
const PROFILER_QUERY_SLOTS = 256;
|
|
13
|
-
const OOM_SCOPE_THRESHOLD_BYTES = 16 * 1024 * 1024;
|
|
14
|
-
const POOL_MAX_IDLE_PER_CLASS = 4;
|
|
15
|
-
const POOL_MIN_CLASS_BYTES = 4 * 1024;
|
|
16
|
-
const POOL_MAX_POW2_CLASS_BYTES = 64 * 1024 * 1024;
|
|
17
|
-
const POOL_LINEAR_STEP_BYTES = 16 * 1024 * 1024;
|
|
18
|
-
const DEFAULT_WARN_UNRELEASED_SNAPSHOTS = 2;
|
|
19
|
-
const MAX_ITERATIONS_PER_STEP = 256;
|
|
20
|
-
const TRACE_RECORD_BYTES = 32;
|
|
21
|
-
const STATE_HEADER_BYTES = 256;
|
|
22
|
-
const PARTIAL_BYTES = 64;
|
|
23
|
-
const FA2_DEFAULTS = Object.freeze({
|
|
24
|
-
maxIter: 100,
|
|
25
|
-
jitterTolerance: 1,
|
|
26
|
-
scalingRatio: 2,
|
|
27
|
-
gravity: 1,
|
|
28
|
-
strongGravity: false,
|
|
29
|
-
distributedAction: false,
|
|
30
|
-
linlog: false,
|
|
31
|
-
dissuadeHubs: false,
|
|
32
|
-
dim: 2,
|
|
33
|
-
scale: 1,
|
|
34
|
-
settleThreshold: 1e-3,
|
|
35
|
-
settleWindow: 10,
|
|
36
|
-
iterationsPerStep: 1,
|
|
37
|
-
maxInFlight: 2
|
|
38
|
-
});
|
|
39
|
-
const LAYOUT_TUNING_DEFAULTS = Object.freeze({
|
|
40
|
-
repulsion: "auto",
|
|
41
|
-
exactMaxNodes: EXACT_MAX_NODES,
|
|
42
|
-
nearMax: 64,
|
|
43
|
-
deterministic: true,
|
|
44
|
-
gridMax2D: 512,
|
|
45
|
-
gridMax3D: 128,
|
|
46
|
-
extentFactor: 6,
|
|
47
|
-
compat: "paper"
|
|
48
|
-
});
|
|
49
|
-
const FA2_DISTANCE_FLOOR = 0.01;
|
|
50
|
-
const FA2_DISTANCE_FLOOR_SQ = 1e-4;
|
|
51
|
-
const FA2_COINCIDENT_SQ = 1e-8;
|
|
52
|
-
const EXACT_TILES_PER_PASS = 128;
|
|
53
|
-
const FA2_FLAG_FIRST = 1;
|
|
54
|
-
const FA2_FLAG_ADAPTIVE = 2;
|
|
55
|
-
const FR_START_TEMPERATURE = 0.1;
|
|
56
|
-
const FR_REHEAT_FRACTION = 0.7;
|
|
57
|
-
const FR_COOLING_STEP = 0.9;
|
|
58
|
-
const FR_COOLING_PATIENCE = 5;
|
|
59
|
-
const FR_ADAPTIVE_MAX_ITERATIONS = 1e4;
|
|
60
|
-
const FR_DEFAULTS = Object.freeze({
|
|
61
|
-
k: null,
|
|
62
|
-
iterations: 50,
|
|
63
|
-
fixed: null,
|
|
64
|
-
dim: 2,
|
|
65
|
-
scale: 1,
|
|
66
|
-
settleThreshold: 1e-3,
|
|
67
|
-
settleWindow: 10,
|
|
68
|
-
iterationsPerStep: 1,
|
|
69
|
-
maxInFlight: 2,
|
|
70
|
-
cooling: "linear"
|
|
71
|
-
});
|
|
72
|
-
const SE_SCALE_REFERENCE_NODES = 300;
|
|
73
|
-
const SE_DEFAULTS = Object.freeze({
|
|
74
|
-
springLength: 10,
|
|
75
|
-
springCoefficient: 0.8,
|
|
76
|
-
gravity: -12,
|
|
77
|
-
dragCoefficient: 0.9,
|
|
78
|
-
timeStep: 0.5,
|
|
79
|
-
dim: 2,
|
|
80
|
-
scale: 1,
|
|
81
|
-
settleThreshold: 1e-3,
|
|
82
|
-
settleWindow: 10,
|
|
83
|
-
iterationsPerStep: 1,
|
|
84
|
-
maxInFlight: 2
|
|
85
|
-
});
|
|
86
|
-
const SETTLE_FLOOR_FRACTION = Object.freeze(
|
|
87
|
-
{
|
|
88
|
-
springElectrical: 3e-3,
|
|
89
|
-
fruchtermanReingold: 2e-3
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
const SETTLE_FLOOR_REFERENCE_NODES = 2e3;
|
|
93
|
-
const SETTLE_FLOOR_UNBOUNDED = 2 ** 128 - 2 ** 104;
|
|
94
|
-
const GRID_MIN_SIDE = 8;
|
|
95
|
-
const GRID_COARSEST_SIDE = 4;
|
|
96
|
-
const GRID_HUB_CELL = 1024;
|
|
97
|
-
const GRID_EXTENT_FLOOR = 1e-6;
|
|
98
|
-
const GRID_BBOX_MARGIN = 1.01;
|
|
99
|
-
const GRID_SORT_BITS = 24;
|
|
100
|
-
const MAX_LEVELS_PER_SUBMIT = 32;
|
|
101
|
-
const FUSED_FRONTIER_MAX = 4096;
|
|
102
|
-
const BEAMER_BETA = 24;
|
|
103
|
-
const SSSP_DELTA_FACTOR = 32;
|
|
104
|
-
const F32_INF_BITS = 2139095040;
|
|
105
|
-
const BC_BATCH_BUDGET_FRACTION = 0.25;
|
|
106
|
-
const BC_MAX_BATCH = 64;
|
|
107
|
-
const BC_EDGE_PARALLEL_GAMMA = 2;
|
|
108
|
-
const BC_SIGMA_EXPONENT_CAP = 100;
|
|
109
|
-
const BC_COUNT_MAX_GROUPS = 256;
|
|
110
|
-
const BC_BACKWARD_LEVELS_PER_SUBMIT = 64;
|
|
111
|
-
const APSP_TILE = 32;
|
|
112
|
-
const APSP_MAX_DISPATCHES_PER_SUBMIT = 4096;
|
|
113
|
-
const LABEL_PROP_PASSES_PER_SUBMIT = 8;
|
|
114
|
-
const BORUVKA_ROUNDS_PER_SUBMIT = 4;
|
|
115
|
-
const F32_SIGN_BIT = 2147483648;
|
|
116
|
-
const GROUP_ROW_THREAD_MAX = 32;
|
|
117
|
-
const GROUP_ROW_THREAD_LIMIT = 128;
|
|
118
|
-
const PARALLEL_MERGE_LIMIT = 65e3;
|
|
119
|
-
const GROUP_HASH_LOAD_FACTOR = 2;
|
|
120
|
-
const TRIANGLE_BINARY_SEARCH_RATIO = 32;
|
|
121
|
-
const PASSTHROUGH_FORMAT_CODES = Object.freeze(["E_GPU_INELIGIBLE", "E_UNKNOWN_NODE", "E_UNKNOWN_COLUMN", "E_COLUMN_LENGTH"]);
|
|
122
|
-
const EMPTY_DETAILS = Object.freeze({});
|
|
123
|
-
class WebGpuGraphError extends Error {
|
|
124
|
-
/**
|
|
125
|
-
* Create a WebGpuGraphError.
|
|
126
|
-
* @param code - the stable code the caller can branch on
|
|
127
|
-
* @param message - a plain-ASCII human-readable message naming the offending label, limit or value
|
|
128
|
-
* @param details - optional machine-readable context; copied shallowly and frozen
|
|
129
|
-
*/
|
|
130
|
-
constructor(code, message, details) {
|
|
131
|
-
super(message);
|
|
132
|
-
this.name = "WebGpuGraphError";
|
|
133
|
-
this.code = code;
|
|
134
|
-
this.details = details === void 0 ? EMPTY_DETAILS : Object.freeze({ ...details });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function isWebGpuGraphError(x) {
|
|
138
|
-
if (typeof x !== "object" || x === null) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
const candidate = x;
|
|
142
|
-
return candidate.name === "WebGpuGraphError" && typeof candidate.code === "string";
|
|
143
|
-
}
|
|
144
|
-
function hasErrorCode(x, code) {
|
|
145
|
-
return isWebGpuGraphError(x) && x.code === code;
|
|
146
|
-
}
|
|
147
|
-
const RAISABLE_LIMITS = Object.freeze([
|
|
148
|
-
"maxBufferSize",
|
|
149
|
-
"maxStorageBufferBindingSize",
|
|
150
|
-
"maxStorageBuffersPerShaderStage",
|
|
151
|
-
"maxComputeWorkgroupStorageSize",
|
|
152
|
-
"maxComputeInvocationsPerWorkgroup",
|
|
153
|
-
"maxComputeWorkgroupSizeX"
|
|
154
|
-
]);
|
|
155
|
-
const LIMIT_NAMES = [
|
|
156
|
-
"maxTextureDimension1D",
|
|
157
|
-
"maxTextureDimension2D",
|
|
158
|
-
"maxTextureDimension3D",
|
|
159
|
-
"maxTextureArrayLayers",
|
|
160
|
-
"maxBindGroups",
|
|
161
|
-
"maxBindGroupsPlusVertexBuffers",
|
|
162
|
-
"maxBindingsPerBindGroup",
|
|
163
|
-
"maxDynamicUniformBuffersPerPipelineLayout",
|
|
164
|
-
"maxDynamicStorageBuffersPerPipelineLayout",
|
|
165
|
-
"maxSampledTexturesPerShaderStage",
|
|
166
|
-
"maxSamplersPerShaderStage",
|
|
167
|
-
"maxStorageBuffersPerShaderStage",
|
|
168
|
-
"maxStorageBuffersInVertexStage",
|
|
169
|
-
"maxStorageBuffersInFragmentStage",
|
|
170
|
-
"maxStorageTexturesPerShaderStage",
|
|
171
|
-
"maxStorageTexturesInVertexStage",
|
|
172
|
-
"maxStorageTexturesInFragmentStage",
|
|
173
|
-
"maxUniformBuffersPerShaderStage",
|
|
174
|
-
"maxUniformBufferBindingSize",
|
|
175
|
-
"maxStorageBufferBindingSize",
|
|
176
|
-
"minUniformBufferOffsetAlignment",
|
|
177
|
-
"minStorageBufferOffsetAlignment",
|
|
178
|
-
"maxVertexBuffers",
|
|
179
|
-
"maxBufferSize",
|
|
180
|
-
"maxVertexAttributes",
|
|
181
|
-
"maxVertexBufferArrayStride",
|
|
182
|
-
"maxInterStageShaderVariables",
|
|
183
|
-
"maxColorAttachments",
|
|
184
|
-
"maxColorAttachmentBytesPerSample",
|
|
185
|
-
"maxComputeWorkgroupStorageSize",
|
|
186
|
-
"maxComputeInvocationsPerWorkgroup",
|
|
187
|
-
"maxComputeWorkgroupSizeX",
|
|
188
|
-
"maxComputeWorkgroupSizeY",
|
|
189
|
-
"maxComputeWorkgroupSizeZ",
|
|
190
|
-
"maxComputeWorkgroupsPerDimension",
|
|
191
|
-
"maxImmediateSize"
|
|
192
|
-
];
|
|
193
|
-
const consumedAdapters = /* @__PURE__ */ new WeakSet();
|
|
194
|
-
function messageOf$1(err) {
|
|
195
|
-
return err instanceof Error ? err.message : String(err);
|
|
196
|
-
}
|
|
197
|
-
function isSoftwareAdapter(info) {
|
|
198
|
-
return info.architecture === "software" || info.architecture === "swiftshader" || info.architecture === "warp" || info.isFallbackAdapter === true;
|
|
199
|
-
}
|
|
200
|
-
function summarizeAdapter(adapter) {
|
|
201
|
-
const { info } = adapter;
|
|
202
|
-
const source = adapter.limits;
|
|
203
|
-
const limits = {};
|
|
204
|
-
for (const name of LIMIT_NAMES) {
|
|
205
|
-
const value = source[name];
|
|
206
|
-
if (typeof value === "number") {
|
|
207
|
-
limits[name] = value;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return Object.freeze({
|
|
211
|
-
vendor: info.vendor,
|
|
212
|
-
architecture: info.architecture,
|
|
213
|
-
device: info.device,
|
|
214
|
-
description: info.description,
|
|
215
|
-
software: isSoftwareAdapter(info),
|
|
216
|
-
subgroupMinSize: info.subgroupMinSize ?? 0,
|
|
217
|
-
subgroupMaxSize: info.subgroupMaxSize ?? 0,
|
|
218
|
-
features: Object.freeze(Array.from(adapter.features).sort()),
|
|
219
|
-
limits: Object.freeze(limits)
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
async function requestAdapter(gpu, powerPreference) {
|
|
223
|
-
let adapter;
|
|
224
|
-
try {
|
|
225
|
-
adapter = await gpu.requestAdapter({ powerPreference });
|
|
226
|
-
} catch (err) {
|
|
227
|
-
const reason = `requestAdapter() threw: ${messageOf$1(err)}`;
|
|
228
|
-
throw new WebGpuGraphError("E_NO_ADAPTER", reason, { reason });
|
|
229
|
-
}
|
|
230
|
-
if (adapter === null) {
|
|
231
|
-
const reason = "requestAdapter() returned null: no usable WebGPU adapter";
|
|
232
|
-
throw new WebGpuGraphError("E_NO_ADAPTER", reason, { reason });
|
|
233
|
-
}
|
|
234
|
-
return adapter;
|
|
235
|
-
}
|
|
236
|
-
function buildRequiredLimits(adapter, policy) {
|
|
237
|
-
const limits = {};
|
|
238
|
-
if (policy === "default") {
|
|
239
|
-
return limits;
|
|
240
|
-
}
|
|
241
|
-
if (policy === "raise") {
|
|
242
|
-
for (const name of RAISABLE_LIMITS) {
|
|
243
|
-
limits[name] = adapter.limits[name];
|
|
244
|
-
}
|
|
245
|
-
return limits;
|
|
246
|
-
}
|
|
247
|
-
for (const name of RAISABLE_LIMITS) {
|
|
248
|
-
const requested = policy[name];
|
|
249
|
-
if (requested === void 0) {
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
const available = adapter.limits[name];
|
|
253
|
-
if (requested > available) {
|
|
254
|
-
throw new WebGpuGraphError(
|
|
255
|
-
"E_NO_DEVICE",
|
|
256
|
-
`requested ${name} = ${requested} exceeds the adapter's ${available}`,
|
|
257
|
-
{ reason: "limit", adapter: summarizeAdapter(adapter), limit: name, requested, available }
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
limits[name] = requested;
|
|
261
|
-
}
|
|
262
|
-
return limits;
|
|
263
|
-
}
|
|
264
|
-
function buildRequiredFeatures(adapter, required, optional) {
|
|
265
|
-
const features = [];
|
|
266
|
-
for (const feature of required) {
|
|
267
|
-
if (!adapter.features.has(feature)) {
|
|
268
|
-
throw new WebGpuGraphError("E_NO_DEVICE", `required feature "${feature}" is not supported by the adapter`, {
|
|
269
|
-
reason: "feature",
|
|
270
|
-
adapter: summarizeAdapter(adapter),
|
|
271
|
-
requested: feature
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
if (!features.includes(feature)) {
|
|
275
|
-
features.push(feature);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
for (const feature of optional) {
|
|
279
|
-
if (adapter.features.has(feature) && !features.includes(feature)) {
|
|
280
|
-
features.push(feature);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return features;
|
|
284
|
-
}
|
|
285
|
-
function consumedError(adapter, message) {
|
|
286
|
-
return new WebGpuGraphError(
|
|
287
|
-
"E_NO_DEVICE",
|
|
288
|
-
`the adapter already created a device (an adapter is consumed by one requestDevice, spec 2.2 step 1): ${message}`,
|
|
289
|
-
{ reason: "consumed", adapter: summarizeAdapter(adapter) }
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
async function requestDevice(adapter, descriptor) {
|
|
293
|
-
if (consumedAdapters.has(adapter)) {
|
|
294
|
-
throw consumedError(adapter, "recorded by this package");
|
|
295
|
-
}
|
|
296
|
-
let device;
|
|
297
|
-
try {
|
|
298
|
-
device = await adapter.requestDevice(descriptor);
|
|
299
|
-
} catch (err) {
|
|
300
|
-
const message = messageOf$1(err);
|
|
301
|
-
if (/consumed|already been used/i.test(message)) {
|
|
302
|
-
throw consumedError(adapter, message);
|
|
303
|
-
}
|
|
304
|
-
throw new WebGpuGraphError("E_NO_DEVICE", `requestDevice() rejected: ${message}`, {
|
|
305
|
-
reason: "requestDevice",
|
|
306
|
-
adapter: summarizeAdapter(adapter)
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
consumedAdapters.add(adapter);
|
|
310
|
-
return device;
|
|
311
|
-
}
|
|
312
|
-
function captureCaps(device, info, runtime, wgslFeatures) {
|
|
313
|
-
const features = new Set(device.features);
|
|
314
|
-
const subgroups = features.has("subgroups");
|
|
315
|
-
return {
|
|
316
|
-
limits: device.limits,
|
|
317
|
-
features,
|
|
318
|
-
wgslFeatures: new Set(wgslFeatures),
|
|
319
|
-
subgroupMinSize: subgroups ? info.subgroupMinSize ?? 0 : 0,
|
|
320
|
-
subgroupMaxSize: subgroups ? info.subgroupMaxSize ?? 0 : 0,
|
|
321
|
-
software: isSoftwareAdapter(info),
|
|
322
|
-
runtime,
|
|
323
|
-
vendor: info.vendor,
|
|
324
|
-
architecture: info.architecture,
|
|
325
|
-
device: info.device,
|
|
326
|
-
description: info.description
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
function capsFromDevice(device, info) {
|
|
330
|
-
const features = new Set(device.features);
|
|
331
|
-
const subgroups = features.has("subgroups");
|
|
332
|
-
return {
|
|
333
|
-
limits: device.limits,
|
|
334
|
-
features,
|
|
335
|
-
wgslFeatures: info?.wgslFeatures ?? /* @__PURE__ */ new Set(),
|
|
336
|
-
subgroupMinSize: subgroups ? info?.subgroupMinSize ?? 0 : 0,
|
|
337
|
-
subgroupMaxSize: subgroups ? info?.subgroupMaxSize ?? 0 : 0,
|
|
338
|
-
software: info?.software ?? false,
|
|
339
|
-
runtime: info?.runtime ?? "unknown",
|
|
340
|
-
vendor: info?.vendor ?? "",
|
|
341
|
-
architecture: info?.architecture ?? "",
|
|
342
|
-
device: info?.device ?? "",
|
|
343
|
-
description: info?.description ?? ""
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
function workgroupSizeFor(caps) {
|
|
347
|
-
const cap = Math.min(WORKGROUP_SIZE, caps.limits.maxComputeInvocationsPerWorkgroup);
|
|
348
|
-
let size = 1;
|
|
349
|
-
while (size * 2 <= cap) {
|
|
350
|
-
size *= 2;
|
|
351
|
-
}
|
|
352
|
-
return size;
|
|
353
|
-
}
|
|
354
|
-
function assertPlanLimits(caps) {
|
|
355
|
-
const perDim = caps.limits.maxComputeWorkgroupsPerDimension;
|
|
356
|
-
if (perDim !== MAX_WORKGROUPS_PER_DIM) {
|
|
357
|
-
throw new WebGpuGraphError(
|
|
358
|
-
"E_NO_DEVICE",
|
|
359
|
-
`maxComputeWorkgroupsPerDimension is ${perDim}; the dispatch planner and the WGSL prelude bake ${MAX_WORKGROUPS_PER_DIM}`,
|
|
360
|
-
{
|
|
361
|
-
reason: "maxComputeWorkgroupsPerDimension",
|
|
362
|
-
adapter: null,
|
|
363
|
-
requested: MAX_WORKGROUPS_PER_DIM,
|
|
364
|
-
available: perDim
|
|
365
|
-
}
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
const wg = workgroupSizeFor(caps);
|
|
369
|
-
if (wg < 64) {
|
|
370
|
-
throw new WebGpuGraphError(
|
|
371
|
-
"E_NO_DEVICE",
|
|
372
|
-
`the device allows a workgroup of ${wg} invocations; the kernels need a power of two >= 64`,
|
|
373
|
-
{
|
|
374
|
-
reason: "limit",
|
|
375
|
-
adapter: null,
|
|
376
|
-
limit: "maxComputeInvocationsPerWorkgroup",
|
|
377
|
-
requested: 64,
|
|
378
|
-
available: wg
|
|
379
|
-
}
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
function formatCompilationInfo(info, preludeLines) {
|
|
384
|
-
return info.messages.map((m) => {
|
|
385
|
-
if (m.lineNum === 0) {
|
|
386
|
-
return `${m.type} ${m.message}`;
|
|
387
|
-
}
|
|
388
|
-
const line = m.lineNum - preludeLines;
|
|
389
|
-
if (line >= 1) {
|
|
390
|
-
return `${m.type} ${line}:${m.linePos} ${m.message}`;
|
|
391
|
-
}
|
|
392
|
-
return `${m.type} prelude:${m.lineNum}:${m.linePos} ${m.message}`;
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
class AllocationTracker {
|
|
396
|
-
/**
|
|
397
|
-
* Creates a tracker over one device.
|
|
398
|
-
* @param device - the device buffers are created on
|
|
399
|
-
* @param thresholdBytes - the byte size from which createBuffer runs inside an out-of-memory scope (default OOM_SCOPE_THRESHOLD_BYTES)
|
|
400
|
-
*/
|
|
401
|
-
constructor(device, thresholdBytes) {
|
|
402
|
-
this.live = /* @__PURE__ */ new Map();
|
|
403
|
-
this.residentBytes = 0;
|
|
404
|
-
this.pending = [];
|
|
405
|
-
this.failure = null;
|
|
406
|
-
this.serial = 0;
|
|
407
|
-
this.device = device;
|
|
408
|
-
this.threshold = thresholdBytes ?? OOM_SCOPE_THRESHOLD_BYTES;
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* createBuffer with the scope rule; every buffer is labelled (an unlabelled descriptor becomes `buffer-<n>`).
|
|
412
|
-
* @param descriptor - the buffer descriptor
|
|
413
|
-
* @returns the buffer
|
|
414
|
-
*/
|
|
415
|
-
createBuffer(descriptor) {
|
|
416
|
-
this.serial += 1;
|
|
417
|
-
const label = descriptor.label === void 0 || descriptor.label === "" ? `buffer-${this.serial}` : descriptor.label;
|
|
418
|
-
const { size } = descriptor;
|
|
419
|
-
const scoped = size >= this.threshold;
|
|
420
|
-
if (scoped) {
|
|
421
|
-
this.device.pushErrorScope("out-of-memory");
|
|
422
|
-
}
|
|
423
|
-
const buffer = this.device.createBuffer({ ...descriptor, label });
|
|
424
|
-
this.live.set(buffer, size);
|
|
425
|
-
this.residentBytes += size;
|
|
426
|
-
if (scoped) {
|
|
427
|
-
const pop = this.device.popErrorScope().then(
|
|
428
|
-
(error) => {
|
|
429
|
-
if (error === null) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (this.live.has(buffer)) {
|
|
433
|
-
this.live.delete(buffer);
|
|
434
|
-
this.residentBytes -= size;
|
|
435
|
-
}
|
|
436
|
-
if (this.failure === null) {
|
|
437
|
-
this.failure = new WebGpuGraphError(
|
|
438
|
-
"E_OUT_OF_MEMORY",
|
|
439
|
-
`out of memory creating "${label}" (${size} bytes, ${this.residentBytes} resident): ${error.message}`,
|
|
440
|
-
{ requested: size, resident: this.residentBytes, label }
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
},
|
|
444
|
-
() => void 0
|
|
445
|
-
);
|
|
446
|
-
this.pending.push(pop);
|
|
447
|
-
}
|
|
448
|
-
return buffer;
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Destroys a buffer created here and decrements `resident`; a buffer the tracker does not know is destroyed
|
|
452
|
-
* without touching the bookkeeping, so destroying twice is harmless.
|
|
453
|
-
* @param buffer - the buffer to destroy
|
|
454
|
-
*/
|
|
455
|
-
destroy(buffer) {
|
|
456
|
-
const size = this.live.get(buffer);
|
|
457
|
-
if (size !== void 0) {
|
|
458
|
-
this.live.delete(buffer);
|
|
459
|
-
this.residentBytes -= size;
|
|
460
|
-
}
|
|
461
|
-
buffer.destroy();
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Awaits every outstanding scope pop; the first OOM -> E_OUT_OF_MEMORY { requested, resident, label }; later
|
|
465
|
-
* calls after a failure throw the same error again until `reset()`.
|
|
466
|
-
*/
|
|
467
|
-
async check() {
|
|
468
|
-
while (this.pending.length > 0) {
|
|
469
|
-
const batch = this.pending.splice(0);
|
|
470
|
-
await Promise.all(batch);
|
|
471
|
-
}
|
|
472
|
-
if (this.failure !== null) {
|
|
473
|
-
throw this.failure;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
/** Forgets a recorded OOM after the caller released what it allocated. */
|
|
477
|
-
reset() {
|
|
478
|
-
this.failure = null;
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Bytes of live buffers created through this tracker.
|
|
482
|
-
* @returns the resident byte count
|
|
483
|
-
*/
|
|
484
|
-
get resident() {
|
|
485
|
-
return this.residentBytes;
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Number of live buffers created through this tracker.
|
|
489
|
-
* @returns the live buffer count
|
|
490
|
-
*/
|
|
491
|
-
get liveBuffers() {
|
|
492
|
-
return this.live.size;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
class PendingErrorSlot {
|
|
496
|
-
constructor() {
|
|
497
|
-
this.queue = [];
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Stores an error; a second error before `take()` is kept as `details.next` of the first.
|
|
501
|
-
* @param error - the error to hold until the next public call
|
|
502
|
-
*/
|
|
503
|
-
set(error) {
|
|
504
|
-
this.queue.push(error);
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Returns and clears the pending error, or null. With several errors queued the FIRST is returned and
|
|
508
|
-
* each later one is chained as `details.next` of its predecessor (a copy: `details` is frozen).
|
|
509
|
-
* @returns the first pending error with the chain attached, or null
|
|
510
|
-
*/
|
|
511
|
-
take() {
|
|
512
|
-
if (this.queue.length === 0) {
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
const errors = this.queue.splice(0);
|
|
516
|
-
let chained = null;
|
|
517
|
-
for (let i = errors.length - 1; i >= 0; i -= 1) {
|
|
518
|
-
const error = errors[i];
|
|
519
|
-
chained = chained === null ? error : new WebGpuGraphError(error.code, error.message, { ...error.details, next: chained });
|
|
520
|
-
}
|
|
521
|
-
return chained;
|
|
522
|
-
}
|
|
523
|
-
/**
|
|
524
|
-
* True when an error is pending.
|
|
525
|
-
* @returns whether `take()` would return an error
|
|
526
|
-
*/
|
|
527
|
-
get pending() {
|
|
528
|
-
return this.queue.length > 0;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
function uncapturedError(error, label) {
|
|
532
|
-
const { message } = error;
|
|
533
|
-
if (error.constructor.name === "GPUOutOfMemoryError") {
|
|
534
|
-
return new WebGpuGraphError("E_OUT_OF_MEMORY", `uncaptured out-of-memory error on "${label}": ${message}`, {
|
|
535
|
-
requested: 0,
|
|
536
|
-
resident: 0,
|
|
537
|
-
label
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
return new WebGpuGraphError("E_VALIDATION", `uncaptured validation error on "${label}": ${message}`, {
|
|
541
|
-
label,
|
|
542
|
-
message
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
function installUncapturedErrorSink(device, slot, onError) {
|
|
546
|
-
let installed = true;
|
|
547
|
-
const label = device.label ?? "";
|
|
548
|
-
const listener = (event) => {
|
|
549
|
-
if (!installed) {
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
const wrapped = uncapturedError(event.error, label);
|
|
553
|
-
if (onError !== null) {
|
|
554
|
-
onError(wrapped);
|
|
555
|
-
} else {
|
|
556
|
-
slot.set(wrapped);
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
device.addEventListener("uncapturederror", listener);
|
|
560
|
-
return () => {
|
|
561
|
-
installed = false;
|
|
562
|
-
if (typeof device.removeEventListener === "function") {
|
|
563
|
-
device.removeEventListener("uncapturederror", listener);
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
function watchDeviceLost(device, onLost) {
|
|
568
|
-
let delivered = false;
|
|
569
|
-
return device.lost.then((info) => {
|
|
570
|
-
if (!delivered) {
|
|
571
|
-
delivered = true;
|
|
572
|
-
onLost(info);
|
|
573
|
-
}
|
|
574
|
-
return info;
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
function deviceLostError(info) {
|
|
578
|
-
return new WebGpuGraphError("E_DEVICE_LOST", `device lost (${info.reason}): ${info.message}`, {
|
|
579
|
-
reason: info.reason,
|
|
580
|
-
message: info.message
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
const INDIRECT_ARGS_STRIDE = 16;
|
|
584
|
-
const BIND_GROUP_CACHE_LIMIT = 32;
|
|
585
|
-
const bufferIds = /* @__PURE__ */ new WeakMap();
|
|
586
|
-
let nextBufferId = 1;
|
|
587
|
-
function bufferId(buffer) {
|
|
588
|
-
let id = bufferIds.get(buffer);
|
|
589
|
-
if (id === void 0) {
|
|
590
|
-
id = nextBufferId;
|
|
591
|
-
nextBufferId += 1;
|
|
592
|
-
bufferIds.set(buffer, id);
|
|
593
|
-
}
|
|
594
|
-
return id;
|
|
595
|
-
}
|
|
596
|
-
function rangesIntersect(a, b) {
|
|
597
|
-
return a.offset < b.offset + b.size && b.offset < a.offset + a.size;
|
|
598
|
-
}
|
|
599
|
-
class Kernel {
|
|
600
|
-
/**
|
|
601
|
-
* Wraps a compiled pipeline.
|
|
602
|
-
* @param device - the device the bind groups are created on
|
|
603
|
-
* @param spec - the module
|
|
604
|
-
* @param composed - the composed text (workgroup size and entry point)
|
|
605
|
-
* @param pipeline - the compiled pipeline
|
|
606
|
-
* @param layouts - the explicit bind-group layouts, one per group 0..maxGroup
|
|
607
|
-
*/
|
|
608
|
-
constructor(device, spec, composed, pipeline, layouts) {
|
|
609
|
-
this.cache = /* @__PURE__ */ new Map();
|
|
610
|
-
this.device = device;
|
|
611
|
-
this.spec = spec;
|
|
612
|
-
this.pipeline = pipeline;
|
|
613
|
-
this.layouts = layouts;
|
|
614
|
-
this.workgroupSize = composed.overrides.WG;
|
|
615
|
-
this.entryPoint = composed.entryPoint;
|
|
616
|
-
const groups = layouts.map(() => []);
|
|
617
|
-
for (const decl of spec.bindings) {
|
|
618
|
-
groups[decl.group].push(decl);
|
|
619
|
-
}
|
|
620
|
-
for (const group of groups) {
|
|
621
|
-
group.sort((a, b) => a.binding - b.binding);
|
|
622
|
-
}
|
|
623
|
-
this.groups = groups;
|
|
624
|
-
this.dynamicCounts = groups.map((group) => group.filter((decl) => decl.kind === "uniform").length);
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* Creates (or reuses, keyed by every buffer identity + offset + size) the bind groups, each labelled
|
|
628
|
-
* `<spec.id>/<group>`; a missing or extra name is E_INVALID_ARGUMENT; two bindings of one call whose ranges
|
|
629
|
-
* intersect on one buffer while either slot is `storage` (read_write) is E_INVALID_ARGUMENT
|
|
630
|
-
* { argument: "aliasing" } (the host-side mirror of WebGPU's writable buffer-binding-aliasing rule, 3.10.1, so
|
|
631
|
-
* the failure is synchronous and labelled). CONTRACT DECISION: bind() is synchronous, so a createBindGroup
|
|
632
|
-
* validation error (a wrong-size uniform binding, a usage mismatch) is NOT thrown here -- it reaches the
|
|
633
|
-
* pending-error slot as E_VALIDATION { label: "<spec.id>/<group>" } and is thrown by the batch's readback
|
|
634
|
-
* (Dawn-node) or the next assertReady() (browser), spec 5.7. An empty bind group is created for every empty
|
|
635
|
-
* layout index so setBindGroup is called for 0..maxGroup.
|
|
636
|
-
* PLAN DECISION (verified on Dawn 0.4.0 / lavapipe): one buffer bound with two DIFFERENT access modes when one is
|
|
637
|
-
* `storage` is also E_INVALID_ARGUMENT { argument: "aliasing", reason: "usage" } even with disjoint ranges, because
|
|
638
|
-
* WebGPU's usage-scope rule rejects "writable usage and another usage in the same synchronization scope" per
|
|
639
|
-
* BUFFER; two `storage` slots with disjoint ranges (the state header / trace pattern) are accepted; a zero-size
|
|
640
|
-
* binding (never bind a zero-length buffer, spec 3.6) and an offset that is not a multiple of STORAGE_ALIGN = 256
|
|
641
|
-
* (spec 2.6; Dawn: "does not satisfy the minimum ... alignment (256)") are E_INVALID_ARGUMENT.
|
|
642
|
-
* @param resources - one Binding per declared name
|
|
643
|
-
* @returns the bound kernel (the same object for the same buffers, offsets and sizes until invalidate())
|
|
644
|
-
*/
|
|
645
|
-
bind(resources) {
|
|
646
|
-
const { id } = this.spec;
|
|
647
|
-
const declared = new Map(this.spec.bindings.map((decl) => [decl.name, decl]));
|
|
648
|
-
for (const name of Object.keys(resources)) {
|
|
649
|
-
if (!declared.has(name)) {
|
|
650
|
-
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${id}: no binding named "${name}"`, {
|
|
651
|
-
argument: "bindings",
|
|
652
|
-
value: name,
|
|
653
|
-
expected: [...declared.keys()].join(", ")
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
const resolved = [];
|
|
658
|
-
for (const decl of this.spec.bindings) {
|
|
659
|
-
const binding = resources[decl.name];
|
|
660
|
-
if (binding === void 0) {
|
|
661
|
-
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${id}: binding "${decl.name}" is missing`, {
|
|
662
|
-
argument: "bindings",
|
|
663
|
-
value: decl.name,
|
|
664
|
-
expected: "a Binding for every declared name"
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
if (!(Number.isInteger(binding.size) && binding.size > 0)) {
|
|
668
|
-
throw new WebGpuGraphError(
|
|
669
|
-
"E_INVALID_ARGUMENT",
|
|
670
|
-
`${id}: binding "${decl.name}" has size ${binding.size}`,
|
|
671
|
-
{
|
|
672
|
-
argument: decl.name,
|
|
673
|
-
value: binding.size,
|
|
674
|
-
expected: "size > 0 (a zero-length buffer is never bound)"
|
|
675
|
-
}
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
if (!(Number.isInteger(binding.offset) && binding.offset >= 0 && binding.offset % STORAGE_ALIGN === 0)) {
|
|
679
|
-
throw new WebGpuGraphError(
|
|
680
|
-
"E_INVALID_ARGUMENT",
|
|
681
|
-
`${id}: binding "${decl.name}" has offset ${binding.offset}`,
|
|
682
|
-
{
|
|
683
|
-
argument: decl.name,
|
|
684
|
-
value: binding.offset,
|
|
685
|
-
expected: `a multiple of ${STORAGE_ALIGN} (spec 2.6: the offset alignment the package always honours)`
|
|
686
|
-
}
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
resolved.push({ decl, binding });
|
|
690
|
-
}
|
|
691
|
-
for (let i = 0; i < resolved.length; i++) {
|
|
692
|
-
for (let j = i + 1; j < resolved.length; j++) {
|
|
693
|
-
const a = resolved[i];
|
|
694
|
-
const b = resolved[j];
|
|
695
|
-
if (a.binding.buffer !== b.binding.buffer) {
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
const writable = a.decl.kind === "storage" || b.decl.kind === "storage";
|
|
699
|
-
if (!writable) {
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
if (a.decl.kind !== b.decl.kind) {
|
|
703
|
-
throw new WebGpuGraphError(
|
|
704
|
-
"E_INVALID_ARGUMENT",
|
|
705
|
-
`${id}: "${a.decl.name}" and "${b.decl.name}" bind one buffer with different access modes (writable usage next to another usage)`,
|
|
706
|
-
{
|
|
707
|
-
argument: "aliasing",
|
|
708
|
-
value: [a.decl.name, b.decl.name],
|
|
709
|
-
expected: "one access mode per buffer when a slot is read_write",
|
|
710
|
-
reason: "usage"
|
|
711
|
-
}
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
if (rangesIntersect(a.binding, b.binding)) {
|
|
715
|
-
throw new WebGpuGraphError(
|
|
716
|
-
"E_INVALID_ARGUMENT",
|
|
717
|
-
`${id}: "${a.decl.name}" and "${b.decl.name}" bind intersecting ranges of one read_write buffer`,
|
|
718
|
-
{
|
|
719
|
-
argument: "aliasing",
|
|
720
|
-
value: [a.decl.name, b.decl.name],
|
|
721
|
-
expected: "disjoint ranges",
|
|
722
|
-
reason: "range"
|
|
723
|
-
}
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
const key = resolved.map(({ binding }) => `${bufferId(binding.buffer)}:${binding.offset}:${binding.size}`).join("|");
|
|
729
|
-
const cached = this.cache.get(key);
|
|
730
|
-
if (cached !== void 0) {
|
|
731
|
-
this.cache.delete(key);
|
|
732
|
-
this.cache.set(key, cached);
|
|
733
|
-
return cached;
|
|
734
|
-
}
|
|
735
|
-
const byName = new Map(resolved.map(({ decl, binding }) => [decl.name, binding]));
|
|
736
|
-
const bindGroups = this.layouts.map(
|
|
737
|
-
(layout, group) => this.device.createBindGroup({
|
|
738
|
-
label: `${id}/${group}`,
|
|
739
|
-
layout,
|
|
740
|
-
entries: this.groups[group].map((decl) => {
|
|
741
|
-
const binding = byName.get(decl.name);
|
|
742
|
-
return {
|
|
743
|
-
binding: decl.binding,
|
|
744
|
-
resource: { buffer: binding.buffer, offset: binding.offset, size: binding.size }
|
|
745
|
-
};
|
|
746
|
-
})
|
|
747
|
-
})
|
|
748
|
-
);
|
|
749
|
-
const dynamicGroups = [];
|
|
750
|
-
this.dynamicCounts.forEach((count, group) => {
|
|
751
|
-
if (count > 0) {
|
|
752
|
-
dynamicGroups.push(group);
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
const bound = Object.freeze({
|
|
756
|
-
kernel: this,
|
|
757
|
-
bindGroups: Object.freeze(bindGroups),
|
|
758
|
-
dynamicGroups: Object.freeze(dynamicGroups)
|
|
759
|
-
});
|
|
760
|
-
this.cache.set(key, bound);
|
|
761
|
-
if (this.cache.size > BIND_GROUP_CACHE_LIMIT) {
|
|
762
|
-
this.cache.delete(this.cache.keys().next().value);
|
|
763
|
-
}
|
|
764
|
-
return bound;
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* setPipeline + setBindGroup for every group (dynamic offsets in dynamicGroups order) + dispatchWorkgroups(plan.x, plan.y, plan.z); a plan with x === 0 records nothing (spec 5.6).
|
|
768
|
-
* PLAN DECISION: one dynamic offset per dynamic GROUP, replicated over every uniform binding of that group (every
|
|
769
|
-
* P1-P3 kernel has exactly one params uniform per group); absent offsets mean 0; a BoundKernel of another kernel
|
|
770
|
-
* or an offset list of the wrong length is E_INVALID_ARGUMENT.
|
|
771
|
-
* PLAN DECISION: `dynamicOffsets?` is spelled `?: readonly number[]` rather than the contract's
|
|
772
|
-
* `?: readonly number[] | undefined` because the root ESLint rule no-duplicate-type-constituents rejects the
|
|
773
|
-
* explicit undefined on an optional parameter (the call signature is identical).
|
|
774
|
-
* @param pass - the open compute pass
|
|
775
|
-
* @param bound - a BoundKernel of THIS kernel
|
|
776
|
-
* @param plan - the dispatch shape
|
|
777
|
-
* @param dynamicOffsets - one byte offset per entry of bound.dynamicGroups
|
|
778
|
-
*/
|
|
779
|
-
dispatch(pass, bound, plan, dynamicOffsets) {
|
|
780
|
-
this.check(bound, dynamicOffsets);
|
|
781
|
-
if (plan.x === 0) {
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
this.setUp(pass, bound, dynamicOffsets);
|
|
785
|
-
pass.dispatchWorkgroups(plan.x, plan.y, plan.z);
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* setPipeline + the bind groups exactly as dispatch(), then dispatchWorkgroupsIndirect(args.buffer, args.offset +
|
|
789
|
-
* INDIRECT_ARGS_STRIDE * slot) (spec 5.4): the (x, y, 1) of the slot were written by the indirect-finalize kernel
|
|
790
|
-
* earlier in the same pass. A slot beyond the binding is E_INVALID_ARGUMENT.
|
|
791
|
-
* @param pass - the open compute pass
|
|
792
|
-
* @param bound - a BoundKernel of THIS kernel
|
|
793
|
-
* @param args - the args buffer range (usage INDIRECT | STORAGE)
|
|
794
|
-
* @param slot - the 16-byte slot index inside the range
|
|
795
|
-
* @param dynamicOffsets - one byte offset per entry of bound.dynamicGroups
|
|
796
|
-
*/
|
|
797
|
-
dispatchIndirect(pass, bound, args, slot, dynamicOffsets) {
|
|
798
|
-
this.check(bound, dynamicOffsets);
|
|
799
|
-
if (!Number.isInteger(slot) || slot < 0 || (slot + 1) * INDIRECT_ARGS_STRIDE > args.size) {
|
|
800
|
-
throw new WebGpuGraphError(
|
|
801
|
-
"E_INVALID_ARGUMENT",
|
|
802
|
-
`${this.spec.id}: args slot ${slot} is outside the binding`,
|
|
803
|
-
{
|
|
804
|
-
argument: "slot",
|
|
805
|
-
value: slot,
|
|
806
|
-
expected: `0 <= slot < ${Math.floor(args.size / INDIRECT_ARGS_STRIDE)}`
|
|
807
|
-
}
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
this.setUp(pass, bound, dynamicOffsets);
|
|
811
|
-
pass.dispatchWorkgroupsIndirect(args.buffer, args.offset + INDIRECT_ARGS_STRIDE * slot);
|
|
812
|
-
}
|
|
813
|
-
/**
|
|
814
|
-
* The argument checks dispatch() and dispatchIndirect() share: a BoundKernel of another kernel or an offset list
|
|
815
|
-
* of the wrong length is E_INVALID_ARGUMENT.
|
|
816
|
-
* @param bound - the bound kernel
|
|
817
|
-
* @param dynamicOffsets - the offsets, if any
|
|
818
|
-
*/
|
|
819
|
-
check(bound, dynamicOffsets) {
|
|
820
|
-
const { id } = this.spec;
|
|
821
|
-
if (bound.kernel !== this) {
|
|
822
|
-
throw new WebGpuGraphError(
|
|
823
|
-
"E_INVALID_ARGUMENT",
|
|
824
|
-
`${id}: the bound kernel belongs to "${bound.kernel.spec.id}"`,
|
|
825
|
-
{
|
|
826
|
-
argument: "bound",
|
|
827
|
-
value: bound.kernel.spec.id,
|
|
828
|
-
expected: id
|
|
829
|
-
}
|
|
830
|
-
);
|
|
831
|
-
}
|
|
832
|
-
if (dynamicOffsets !== void 0 && dynamicOffsets.length !== bound.dynamicGroups.length) {
|
|
833
|
-
throw new WebGpuGraphError(
|
|
834
|
-
"E_INVALID_ARGUMENT",
|
|
835
|
-
`${id}: ${dynamicOffsets.length} dynamic offsets for ${bound.dynamicGroups.length} dynamic groups`,
|
|
836
|
-
{
|
|
837
|
-
argument: "dynamicOffsets",
|
|
838
|
-
value: dynamicOffsets.length,
|
|
839
|
-
expected: bound.dynamicGroups.length
|
|
840
|
-
}
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* setPipeline + setBindGroup for every group (the dynamic offsets replicated over the uniform bindings of each
|
|
846
|
-
* dynamic group), the prefix dispatch() and dispatchIndirect() share.
|
|
847
|
-
* @param pass - the open compute pass
|
|
848
|
-
* @param bound - a BoundKernel of this kernel
|
|
849
|
-
* @param dynamicOffsets - one byte offset per entry of bound.dynamicGroups (absent = 0)
|
|
850
|
-
*/
|
|
851
|
-
setUp(pass, bound, dynamicOffsets) {
|
|
852
|
-
pass.setPipeline(this.pipeline);
|
|
853
|
-
let next = 0;
|
|
854
|
-
bound.bindGroups.forEach((bindGroup, group) => {
|
|
855
|
-
const count = this.dynamicCounts[group];
|
|
856
|
-
if (count > 0) {
|
|
857
|
-
const offset = dynamicOffsets?.[next] ?? 0;
|
|
858
|
-
next += 1;
|
|
859
|
-
const offsets = [];
|
|
860
|
-
for (let k = 0; k < count; k++) {
|
|
861
|
-
offsets.push(offset);
|
|
862
|
-
}
|
|
863
|
-
pass.setBindGroup(group, bindGroup, offsets);
|
|
864
|
-
} else {
|
|
865
|
-
pass.setBindGroup(group, bindGroup);
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
/** Drops cached bind groups (a layout's buffers changed). */
|
|
870
|
-
invalidate() {
|
|
871
|
-
this.cache.clear();
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
const BufferUsage = Object.freeze({
|
|
875
|
-
MAP_READ: 1,
|
|
876
|
-
// GPUBufferUsage.MAP_READ
|
|
877
|
-
MAP_WRITE: 2,
|
|
878
|
-
// GPUBufferUsage.MAP_WRITE
|
|
879
|
-
COPY_SRC: 4,
|
|
880
|
-
// GPUBufferUsage.COPY_SRC
|
|
881
|
-
COPY_DST: 8,
|
|
882
|
-
// GPUBufferUsage.COPY_DST
|
|
883
|
-
INDEX: 16,
|
|
884
|
-
// GPUBufferUsage.INDEX
|
|
885
|
-
VERTEX: 32,
|
|
886
|
-
// GPUBufferUsage.VERTEX
|
|
887
|
-
UNIFORM: 64,
|
|
888
|
-
// GPUBufferUsage.UNIFORM
|
|
889
|
-
STORAGE: 128,
|
|
890
|
-
// GPUBufferUsage.STORAGE
|
|
891
|
-
INDIRECT: 256,
|
|
892
|
-
// GPUBufferUsage.INDIRECT
|
|
893
|
-
QUERY_RESOLVE: 512
|
|
894
|
-
// GPUBufferUsage.QUERY_RESOLVE
|
|
895
|
-
});
|
|
896
|
-
const MapMode = Object.freeze({
|
|
897
|
-
READ: 1,
|
|
898
|
-
// GPUMapMode.READ
|
|
899
|
-
WRITE: 2
|
|
900
|
-
// GPUMapMode.WRITE
|
|
901
|
-
});
|
|
902
|
-
const ShaderStage = Object.freeze({
|
|
903
|
-
VERTEX: 1,
|
|
904
|
-
// GPUShaderStage.VERTEX
|
|
905
|
-
FRAGMENT: 2,
|
|
906
|
-
// GPUShaderStage.FRAGMENT
|
|
907
|
-
COMPUTE: 4
|
|
908
|
-
// GPUShaderStage.COMPUTE
|
|
909
|
-
});
|
|
910
|
-
function wgslF32Literal(value) {
|
|
911
|
-
if (!Number.isFinite(value)) {
|
|
912
|
-
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `wgslF32Literal: ${value} is not a finite number`, {
|
|
913
|
-
argument: "value",
|
|
914
|
-
value,
|
|
915
|
-
expected: "a finite number"
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
const text = String(value);
|
|
919
|
-
return /[.e]/.test(text) ? text : `${text}.0`;
|
|
920
|
-
}
|
|
921
|
-
const PRELUDE_WGSL = (
|
|
922
|
-
/* wgsl */
|
|
923
|
-
`// ---- prelude: constants, standard overrides, helpers (every module receives this text first)
|
|
924
|
-
const INVALID_INDEX: u32 = ${INVALID_INDEX}u;
|
|
925
|
-
const U32_MAX: u32 = ${U32_MAX}u;
|
|
926
|
-
const F32_INF_BITS: u32 = ${F32_INF_BITS}u;
|
|
927
|
-
const F32_SIGN_BIT: u32 = ${F32_SIGN_BIT}u;
|
|
928
|
-
const MAX_WORKGROUPS_PER_DIM: u32 = ${MAX_WORKGROUPS_PER_DIM}u;
|
|
929
|
-
const EXACT_TILES_PER_PASS: u32 = ${EXACT_TILES_PER_PASS}u;
|
|
930
|
-
const FA2_DIST_FLOOR: f32 = ${wgslF32Literal(FA2_DISTANCE_FLOOR)};
|
|
931
|
-
const FA2_DIST_FLOOR_SQ: f32 = ${wgslF32Literal(FA2_DISTANCE_FLOOR_SQ)};
|
|
932
|
-
const FA2_COINCIDENT_SQ: f32 = ${wgslF32Literal(FA2_COINCIDENT_SQ)};
|
|
933
|
-
const FA2_FLAG_FIRST: u32 = ${FA2_FLAG_FIRST}u;
|
|
934
|
-
const FA2_FLAG_ADAPTIVE: u32 = ${FA2_FLAG_ADAPTIVE}u;
|
|
935
|
-
const FR_COOLING_STEP: f32 = ${wgslF32Literal(FR_COOLING_STEP)};
|
|
936
|
-
const FR_COOLING_PATIENCE: u32 = ${FR_COOLING_PATIENCE}u;
|
|
937
|
-
const GRID_HUB_CELL: u32 = ${GRID_HUB_CELL}u;
|
|
938
|
-
const GRID_EXTENT_FLOOR: f32 = ${wgslF32Literal(GRID_EXTENT_FLOOR)};
|
|
939
|
-
const GRID_BBOX_MARGIN: f32 = ${wgslF32Literal(GRID_BBOX_MARGIN)};
|
|
940
|
-
const RADIX_BINS: u32 = ${RADIX_BINS}u;
|
|
941
|
-
const RADIX_DIGIT_MASK: u32 = ${RADIX_BINS - 1}u;
|
|
942
|
-
const APSP_TILE: u32 = ${APSP_TILE}u;
|
|
943
|
-
const GROUP_HASH_LOAD_FACTOR: u32 = ${GROUP_HASH_LOAD_FACTOR}u;
|
|
944
|
-
const TRIANGLE_BINARY_SEARCH_RATIO: u32 = ${TRIANGLE_BINARY_SEARCH_RATIO}u;
|
|
945
|
-
const BC_SIGMA_EXPONENT_CAP: i32 = ${BC_SIGMA_EXPONENT_CAP}i;
|
|
946
|
-
const F32_MAX: f32 = 0x1.fffffep+127;
|
|
947
|
-
override WG: u32 = ${WORKGROUP_SIZE}u;
|
|
948
|
-
override USE_PERM: bool = false;
|
|
949
|
-
override HAS_WEIGHTS: bool = false;
|
|
950
|
-
override SUBGROUP_MIN: u32 = 4u;
|
|
951
|
-
override SUBGROUP_MAX: u32 = 0u;
|
|
952
|
-
|
|
953
|
-
fn linear_id(wid: vec3<u32>, lid: u32) -> u32 { return (wid.x + wid.y * MAX_WORKGROUPS_PER_DIM) * WG + lid; }
|
|
954
|
-
fn group_id(wid: vec3<u32>) -> u32 { return wid.x + wid.y * MAX_WORKGROUPS_PER_DIM; }
|
|
955
|
-
// betweenness: the power of two the counts of depth L + 1 are divided by, from the f32 bits of depth L's largest count
|
|
956
|
-
fn sigma_shift(maxBits: u32) -> i32 { return max(0i, i32((maxBits >> 23u) & 0xffu) - 127i - BC_SIGMA_EXPONENT_CAP); }
|
|
957
|
-
fn lowbias32(x0: u32) -> u32 {
|
|
958
|
-
var x = x0;
|
|
959
|
-
x = x ^ (x >> 16u);
|
|
960
|
-
x = x * 0x7feb352du;
|
|
961
|
-
x = x ^ (x >> 15u);
|
|
962
|
-
x = x * 0x846ca68bu;
|
|
963
|
-
x = x ^ (x >> 16u);
|
|
964
|
-
return x;
|
|
965
|
-
}
|
|
966
|
-
fn mask_bit(w: u32, i: u32) -> bool { return ((w >> (i & 31u)) & 1u) == 1u; }
|
|
967
|
-
fn unpack_u8(w: u32, i: u32) -> u32 { return (w >> (8u * (i & 3u))) & 0xFFu; }
|
|
968
|
-
fn pair_hash(i: u32, j: u32) -> u32 { return lowbias32((min(i, j) * 0x9E3779B9u) ^ max(i, j)); }
|
|
969
|
-
fn order_key(w: f32) -> u32 {
|
|
970
|
-
let raw = bitcast<u32>(w);
|
|
971
|
-
let bits = select(raw, 0u, raw == F32_SIGN_BIT);
|
|
972
|
-
return select(bits | F32_SIGN_BIT, ~bits, (bits & F32_SIGN_BIT) != 0u);
|
|
973
|
-
}
|
|
974
|
-
fn hash_unit(h: u32) -> f32 { return f32(h >> 8u) * (1.0 / 16777216.0); }
|
|
975
|
-
fn hash_dir(h: u32, dim: u32) -> vec3f {
|
|
976
|
-
let phi = 6.283185307179586 * hash_unit(h);
|
|
977
|
-
if (dim == 2u) { return vec3f(cos(phi), sin(phi), 0.0); }
|
|
978
|
-
let z = 2.0 * hash_unit(lowbias32(h ^ 0x5bd1e995u)) - 1.0;
|
|
979
|
-
let r = sqrt(max(0.0, 1.0 - z * z));
|
|
980
|
-
return vec3f(r * cos(phi), r * sin(phi), z);
|
|
981
|
-
}
|
|
982
|
-
fn kick_dir(i: u32, j: u32, dim: u32) -> vec3f {
|
|
983
|
-
let d = hash_dir(pair_hash(i, j), dim);
|
|
984
|
-
return select(d, -d, i > j);
|
|
985
|
-
}`
|
|
986
|
-
);
|
|
987
|
-
PRELUDE_WGSL.split("\n").length;
|
|
988
|
-
const REDUCE_HELPERS_WORKGROUP_WGSL = (
|
|
989
|
-
/* wgsl */
|
|
990
|
-
`var<workgroup> wg_scratch_v: array<vec4f, WG>;
|
|
991
|
-
var<workgroup> wg_scratch_u: array<u32, WG>;
|
|
992
|
-
fn combine_v(a: vec4f, b: vec4f, op: u32) -> vec4f {
|
|
993
|
-
if (op == 1u) { return min(a, b); }
|
|
994
|
-
if (op == 2u) { return max(a, b); }
|
|
995
|
-
return a + b;
|
|
996
|
-
}
|
|
997
|
-
fn combine_u(a: u32, b: u32, op: u32) -> u32 {
|
|
998
|
-
if (op == 1u) { return min(a, b); }
|
|
999
|
-
if (op == 2u) { return max(a, b); }
|
|
1000
|
-
return a + b;
|
|
1001
|
-
}
|
|
1002
|
-
fn wg_reduce_vec4(v: vec4f, lid: u32, op: u32) -> vec4f {
|
|
1003
|
-
workgroupBarrier();
|
|
1004
|
-
wg_scratch_v[lid] = v;
|
|
1005
|
-
workgroupBarrier();
|
|
1006
|
-
for (var s = WG / 2u; s > 0u; s = s >> 1u) {
|
|
1007
|
-
if (lid < s) { wg_scratch_v[lid] = combine_v(wg_scratch_v[lid], wg_scratch_v[lid + s], op); }
|
|
1008
|
-
workgroupBarrier();
|
|
1009
|
-
}
|
|
1010
|
-
let total = wg_scratch_v[0];
|
|
1011
|
-
workgroupBarrier();
|
|
1012
|
-
return total;
|
|
1013
|
-
}
|
|
1014
|
-
fn wg_reduce_u32(v: u32, lid: u32, op: u32) -> u32 {
|
|
1015
|
-
workgroupBarrier();
|
|
1016
|
-
wg_scratch_u[lid] = v;
|
|
1017
|
-
workgroupBarrier();
|
|
1018
|
-
for (var s = WG / 2u; s > 0u; s = s >> 1u) {
|
|
1019
|
-
if (lid < s) { wg_scratch_u[lid] = combine_u(wg_scratch_u[lid], wg_scratch_u[lid + s], op); }
|
|
1020
|
-
workgroupBarrier();
|
|
1021
|
-
}
|
|
1022
|
-
let total = wg_scratch_u[0];
|
|
1023
|
-
workgroupBarrier();
|
|
1024
|
-
return total;
|
|
1025
|
-
}
|
|
1026
|
-
fn wg_scan_u32(v: u32, lid: u32) -> u32 {
|
|
1027
|
-
workgroupBarrier();
|
|
1028
|
-
wg_scratch_u[lid] = v;
|
|
1029
|
-
workgroupBarrier();
|
|
1030
|
-
for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan (scan-block's body); uniform: every lane runs every round
|
|
1031
|
-
var t = 0u;
|
|
1032
|
-
if (lid >= s) { t = wg_scratch_u[lid - s]; }
|
|
1033
|
-
workgroupBarrier();
|
|
1034
|
-
wg_scratch_u[lid] = wg_scratch_u[lid] + t;
|
|
1035
|
-
workgroupBarrier();
|
|
1036
|
-
}
|
|
1037
|
-
let inclusive = wg_scratch_u[lid];
|
|
1038
|
-
workgroupBarrier(); // the scratch is free for the next call
|
|
1039
|
-
return inclusive;
|
|
1040
|
-
}
|
|
1041
|
-
fn wg_reduce_f32(v: f32, lid: u32, op: u32) -> f32 { return wg_reduce_vec4(vec4f(v, 0.0, 0.0, 0.0), lid, op).x; }`
|
|
1042
|
-
);
|
|
1043
|
-
const REDUCE_HELPERS_SUBGROUP_WGSL = (
|
|
1044
|
-
/* wgsl */
|
|
1045
|
-
`override SG_SLOTS: u32 = (WG + SUBGROUP_MIN - 1u) / SUBGROUP_MIN; // one slot per subgroup; the count is largest when the compiler picks the SMALLEST size
|
|
1046
|
-
var<workgroup> sg_counter: atomic<u32>;
|
|
1047
|
-
var<workgroup> sg_val_v: array<vec4f, SG_SLOTS>;
|
|
1048
|
-
var<workgroup> sg_val_u: array<u32, SG_SLOTS>;
|
|
1049
|
-
var<workgroup> sg_key: array<u32, SG_SLOTS>;
|
|
1050
|
-
var<workgroup> sg_sorted_v: array<vec4f, SG_SLOTS>;
|
|
1051
|
-
var<workgroup> sg_sorted_u: array<u32, SG_SLOTS>;
|
|
1052
|
-
fn combine_v(a: vec4f, b: vec4f, op: u32) -> vec4f {
|
|
1053
|
-
if (op == 1u) { return min(a, b); }
|
|
1054
|
-
if (op == 2u) { return max(a, b); }
|
|
1055
|
-
return a + b;
|
|
1056
|
-
}
|
|
1057
|
-
fn combine_u(a: u32, b: u32, op: u32) -> u32 {
|
|
1058
|
-
if (op == 1u) { return min(a, b); }
|
|
1059
|
-
if (op == 2u) { return max(a, b); }
|
|
1060
|
-
return a + b;
|
|
1061
|
-
}
|
|
1062
|
-
fn wg_reduce_vec4(v: vec4f, lid: u32, op: u32) -> vec4f {
|
|
1063
|
-
workgroupBarrier();
|
|
1064
|
-
if (lid == 0u) { atomicStore(&sg_counter, 0u); }
|
|
1065
|
-
workgroupBarrier();
|
|
1066
|
-
let s_add = subgroupAdd(v);
|
|
1067
|
-
let s_min = subgroupMin(v);
|
|
1068
|
-
let s_max = subgroupMax(v);
|
|
1069
|
-
var partial = s_add;
|
|
1070
|
-
if (op == 1u) { partial = s_min; }
|
|
1071
|
-
if (op == 2u) { partial = s_max; }
|
|
1072
|
-
let key = subgroupMin(lid); // the smallest local id of this subgroup: a stable identity without @builtin(subgroup_id)
|
|
1073
|
-
var slot = 0u;
|
|
1074
|
-
if (subgroupElect()) { slot = atomicAdd(&sg_counter, 1u); } // D16: the elected lane takes a slot from the counter
|
|
1075
|
-
slot = subgroupBroadcast(slot, 0u); // and broadcasts it (lane 0 is the elected lane in uniform control flow)
|
|
1076
|
-
if (subgroupElect()) { sg_val_v[slot] = partial; sg_key[slot] = key; }
|
|
1077
|
-
workgroupBarrier();
|
|
1078
|
-
let count = atomicLoad(&sg_counter);
|
|
1079
|
-
if (lid < count) { // rank the slots by key so the final sum has a fixed order (11.9 item 4)
|
|
1080
|
-
let mine = sg_key[lid];
|
|
1081
|
-
var rank = 0u;
|
|
1082
|
-
for (var k = 0u; k < count; k = k + 1u) { if (sg_key[k] < mine) { rank = rank + 1u; } }
|
|
1083
|
-
sg_sorted_v[rank] = sg_val_v[lid];
|
|
1084
|
-
}
|
|
1085
|
-
workgroupBarrier();
|
|
1086
|
-
var total = sg_sorted_v[0];
|
|
1087
|
-
for (var k = 1u; k < count; k = k + 1u) { total = combine_v(total, sg_sorted_v[k], op); }
|
|
1088
|
-
workgroupBarrier();
|
|
1089
|
-
return total;
|
|
1090
|
-
}
|
|
1091
|
-
fn wg_reduce_u32(v: u32, lid: u32, op: u32) -> u32 {
|
|
1092
|
-
workgroupBarrier();
|
|
1093
|
-
if (lid == 0u) { atomicStore(&sg_counter, 0u); }
|
|
1094
|
-
workgroupBarrier();
|
|
1095
|
-
let s_add = subgroupAdd(v);
|
|
1096
|
-
let s_min = subgroupMin(v);
|
|
1097
|
-
let s_max = subgroupMax(v);
|
|
1098
|
-
var partial = s_add;
|
|
1099
|
-
if (op == 1u) { partial = s_min; }
|
|
1100
|
-
if (op == 2u) { partial = s_max; }
|
|
1101
|
-
let key = subgroupMin(lid);
|
|
1102
|
-
var slot = 0u;
|
|
1103
|
-
if (subgroupElect()) { slot = atomicAdd(&sg_counter, 1u); }
|
|
1104
|
-
slot = subgroupBroadcast(slot, 0u);
|
|
1105
|
-
if (subgroupElect()) { sg_val_u[slot] = partial; sg_key[slot] = key; }
|
|
1106
|
-
workgroupBarrier();
|
|
1107
|
-
let count = atomicLoad(&sg_counter);
|
|
1108
|
-
if (lid < count) {
|
|
1109
|
-
let mine = sg_key[lid];
|
|
1110
|
-
var rank = 0u;
|
|
1111
|
-
for (var k = 0u; k < count; k = k + 1u) { if (sg_key[k] < mine) { rank = rank + 1u; } }
|
|
1112
|
-
sg_sorted_u[rank] = sg_val_u[lid];
|
|
1113
|
-
}
|
|
1114
|
-
workgroupBarrier();
|
|
1115
|
-
var total = sg_sorted_u[0];
|
|
1116
|
-
for (var k = 1u; k < count; k = k + 1u) { total = combine_u(total, sg_sorted_u[k], op); }
|
|
1117
|
-
workgroupBarrier();
|
|
1118
|
-
return total;
|
|
1119
|
-
}
|
|
1120
|
-
fn wg_scan_u32(v: u32, lid: u32) -> u32 {
|
|
1121
|
-
workgroupBarrier();
|
|
1122
|
-
if (lid == 0u) { atomicStore(&sg_counter, 0u); }
|
|
1123
|
-
workgroupBarrier();
|
|
1124
|
-
let inSub = subgroupExclusiveAdd(v) + v; // the lane's inclusive sum within its subgroup
|
|
1125
|
-
let total = subgroupAdd(v);
|
|
1126
|
-
let key = subgroupMin(lid); // the subgroup's identity: its smallest local id (D16)
|
|
1127
|
-
var slot = 0u;
|
|
1128
|
-
if (subgroupElect()) { slot = atomicAdd(&sg_counter, 1u); }
|
|
1129
|
-
slot = subgroupBroadcast(slot, 0u);
|
|
1130
|
-
if (subgroupElect()) { sg_val_u[slot] = total; sg_key[slot] = key; }
|
|
1131
|
-
workgroupBarrier();
|
|
1132
|
-
let count = atomicLoad(&sg_counter);
|
|
1133
|
-
var carry = 0u; // the totals of every subgroup whose key is below this lane's: LANE order, whatever order the slots were taken in
|
|
1134
|
-
for (var k = 0u; k < count; k = k + 1u) { if (sg_key[k] < key) { carry = carry + sg_val_u[k]; } }
|
|
1135
|
-
workgroupBarrier(); // the slots are free for the next call
|
|
1136
|
-
return inSub + carry;
|
|
1137
|
-
}
|
|
1138
|
-
fn wg_reduce_f32(v: f32, lid: u32, op: u32) -> f32 { return wg_reduce_vec4(vec4f(v, 0.0, 0.0, 0.0), lid, op).x; }`
|
|
1139
|
-
);
|
|
1140
|
-
const REDUCE_HELPER_NAMES = ["wg_reduce_f32", "wg_reduce_u32", "wg_reduce_vec4", "wg_scan_u32"];
|
|
1141
|
-
const WGSL_RESERVED_WORDS = Object.freeze(
|
|
1142
|
-
`NULL Self abstract active alignas alignof as asm asm_fragment async attribute auto await become cast catch class
|
|
1143
|
-
co_await co_return co_yield coherent column_major common compile compile_fragment concept const_cast consteval
|
|
1144
|
-
constexpr constinit crate debugger decltype delete demote demote_to_helper do dynamic_cast enum explicit export
|
|
1145
|
-
extends extern external fallthrough filter final finally friend from fxgroup get goto groupshared highp impl
|
|
1146
|
-
implements import inline instanceof interface layout lowp macro macro_rules match mediump meta mod module move
|
|
1147
|
-
mut mutable namespace new nil noexcept noinline nointerpolation non_coherent noncoherent noperspective null
|
|
1148
|
-
nullptr of operator package packoffset partition pass patch pixelfragment precise precision premerge priv
|
|
1149
|
-
protected pub public readonly ref regardless register reinterpret_cast require resource restrict self set shared
|
|
1150
|
-
sizeof smooth snorm static static_assert static_cast std subroutine super target template this thread_local
|
|
1151
|
-
throw trait try type typedef typeid typename typeof union unless unorm unsafe unsized use using varying virtual
|
|
1152
|
-
volatile wgsl where with writeonly yield`.trim().split(/\s+/)
|
|
1153
|
-
);
|
|
1154
|
-
const STANDARD_OVERRIDES = ["WG", "USE_PERM", "HAS_WEIGHTS", "SUBGROUP_MIN", "SUBGROUP_MAX"];
|
|
1155
|
-
const STANDARD_OVERRIDE_TYPES = Object.freeze({
|
|
1156
|
-
WG: "u32",
|
|
1157
|
-
USE_PERM: "bool",
|
|
1158
|
-
HAS_WEIGHTS: "bool",
|
|
1159
|
-
SUBGROUP_MIN: "u32",
|
|
1160
|
-
SUBGROUP_MAX: "u32"
|
|
1161
|
-
});
|
|
1162
|
-
const KNOWN_NEEDS = Object.freeze(["subgroups"]);
|
|
1163
|
-
const SUBGROUP_FLOOR = 4;
|
|
1164
|
-
const BUILTIN_TYPES = /* @__PURE__ */ new Set([
|
|
1165
|
-
"u32",
|
|
1166
|
-
"i32",
|
|
1167
|
-
"f32",
|
|
1168
|
-
"f16",
|
|
1169
|
-
"bool",
|
|
1170
|
-
"vec2f",
|
|
1171
|
-
"vec3f",
|
|
1172
|
-
"vec4f",
|
|
1173
|
-
"vec2u",
|
|
1174
|
-
"vec3u",
|
|
1175
|
-
"vec4u",
|
|
1176
|
-
"vec2i",
|
|
1177
|
-
"vec3i",
|
|
1178
|
-
"vec4i",
|
|
1179
|
-
"vec2h",
|
|
1180
|
-
"vec3h",
|
|
1181
|
-
"vec4h"
|
|
1182
|
-
]);
|
|
1183
|
-
const TEMPLATED_BUILTIN = /^(vec[234]|mat[234]x[234]|atomic|array)</;
|
|
1184
|
-
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1185
|
-
const ADDRESS_SPACE = Object.freeze({
|
|
1186
|
-
storage: "var<storage, read_write>",
|
|
1187
|
-
"storage-ro": "var<storage, read>",
|
|
1188
|
-
uniform: "var<uniform>"
|
|
1189
|
-
});
|
|
1190
|
-
const RESERVED = new Set(WGSL_RESERVED_WORDS);
|
|
1191
|
-
const MARKER = /\/\/@@([A-Za-z0-9_]+)@@/;
|
|
1192
|
-
const IDENTIFIER_TOKENS = new RegExp("(?<![A-Za-z0-9_])[A-Za-z_][A-Za-z0-9_]*", "g");
|
|
1193
|
-
const COMPUTE_ATTRIBUTE = /@compute\b/g;
|
|
1194
|
-
const ENTRY_FN = /\bfn\s+([A-Za-z_][A-Za-z0-9_]*)/;
|
|
1195
|
-
function composeError(id, slot, message) {
|
|
1196
|
-
return new WebGpuGraphError("E_SHADER_COMPILE", `${id}: ${message}`, { id, stage: "compose", slot });
|
|
1197
|
-
}
|
|
1198
|
-
function stripComments(text) {
|
|
1199
|
-
return text.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, "");
|
|
1200
|
-
}
|
|
1201
|
-
function structNameOf(wgslType) {
|
|
1202
|
-
const trimmed = wgslType.trim();
|
|
1203
|
-
const inner = trimmed.startsWith("array<") ? trimmed.slice("array<".length).replace(/\s*(,[^>]*)?>\s*$/, "") : trimmed;
|
|
1204
|
-
if (BUILTIN_TYPES.has(inner) || TEMPLATED_BUILTIN.test(inner)) {
|
|
1205
|
-
return null;
|
|
1206
|
-
}
|
|
1207
|
-
return inner;
|
|
1208
|
-
}
|
|
1209
|
-
function assertOverrideValue(id, name, type, value) {
|
|
1210
|
-
const ok = type === "bool" ? typeof value === "boolean" : typeof value === "number" && Number.isFinite(value) && (type === "f32" || Number.isInteger(value) && value >= 0);
|
|
1211
|
-
if (!ok) {
|
|
1212
|
-
throw composeError(id, `override:${name}`, `override ${name} (${type}) cannot take the value ${String(value)}`);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
function overrideLiteral(decl) {
|
|
1216
|
-
if (decl.type === "bool") {
|
|
1217
|
-
return decl.default ? "true" : "false";
|
|
1218
|
-
}
|
|
1219
|
-
const value = decl.default;
|
|
1220
|
-
return decl.type === "u32" ? `${value}u` : wgslF32Literal(value);
|
|
1221
|
-
}
|
|
1222
|
-
function bindingLine(decl) {
|
|
1223
|
-
const space = ADDRESS_SPACE[decl.kind];
|
|
1224
|
-
return `@group(${decl.group}) @binding(${decl.binding}) ${space} ${decl.name}: ${decl.wgslType};`;
|
|
1225
|
-
}
|
|
1226
|
-
function findEntryPoint(body) {
|
|
1227
|
-
const stripped = stripComments(body);
|
|
1228
|
-
const matches = [...stripped.matchAll(COMPUTE_ATTRIBUTE)];
|
|
1229
|
-
if (matches.length !== 1 || matches[0].index === void 0) {
|
|
1230
|
-
return null;
|
|
1231
|
-
}
|
|
1232
|
-
const fn = ENTRY_FN.exec(stripped.slice(matches[0].index));
|
|
1233
|
-
return fn === null ? null : fn[1];
|
|
1234
|
-
}
|
|
1235
|
-
function bindGroupLayoutDescriptors(spec) {
|
|
1236
|
-
const maxGroup = spec.bindings.reduce((acc, b) => Math.max(acc, b.group), -1);
|
|
1237
|
-
const blocks = new Map(spec.uniforms.map((block) => [block.name, block]));
|
|
1238
|
-
const descriptors = [];
|
|
1239
|
-
for (let group = 0; group <= maxGroup; group++) {
|
|
1240
|
-
const entries = spec.bindings.filter((b) => b.group === group).sort((a, b) => a.binding - b.binding).map((b) => {
|
|
1241
|
-
if (b.kind === "uniform") {
|
|
1242
|
-
const block = blocks.get(b.wgslType);
|
|
1243
|
-
const buffer = { type: "uniform", hasDynamicOffset: true };
|
|
1244
|
-
if (block !== void 0) {
|
|
1245
|
-
buffer.minBindingSize = block.byteLength;
|
|
1246
|
-
}
|
|
1247
|
-
return { binding: b.binding, visibility: ShaderStage.COMPUTE, buffer };
|
|
1248
|
-
}
|
|
1249
|
-
return {
|
|
1250
|
-
binding: b.binding,
|
|
1251
|
-
visibility: ShaderStage.COMPUTE,
|
|
1252
|
-
buffer: { type: b.kind === "storage" ? "storage" : "read-only-storage" }
|
|
1253
|
-
};
|
|
1254
|
-
});
|
|
1255
|
-
descriptors.push({ entries });
|
|
1256
|
-
}
|
|
1257
|
-
return descriptors;
|
|
1258
|
-
}
|
|
1259
|
-
function validateDeclarations(spec) {
|
|
1260
|
-
const { id } = spec;
|
|
1261
|
-
for (const need of spec.needs) {
|
|
1262
|
-
if (!KNOWN_NEEDS.includes(need)) {
|
|
1263
|
-
throw composeError(id, `needs:${need}`, `unknown feature "${need}" in needs`);
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
const declared = /* @__PURE__ */ new Map();
|
|
1267
|
-
for (const decl of spec.overrideDecls) {
|
|
1268
|
-
if (!IDENTIFIER.test(decl.name) || RESERVED.has(decl.name)) {
|
|
1269
|
-
throw composeError(id, `override:${decl.name}`, `override name "${decl.name}" is not a usable identifier`);
|
|
1270
|
-
}
|
|
1271
|
-
if (STANDARD_OVERRIDES.includes(decl.name) || declared.has(decl.name)) {
|
|
1272
|
-
throw composeError(
|
|
1273
|
-
id,
|
|
1274
|
-
`override:${decl.name}`,
|
|
1275
|
-
`override "${decl.name}" is declared twice (the prelude declares the standard ones)`
|
|
1276
|
-
);
|
|
1277
|
-
}
|
|
1278
|
-
assertOverrideValue(id, decl.name, decl.type, decl.default);
|
|
1279
|
-
declared.set(decl.name, decl.type);
|
|
1280
|
-
}
|
|
1281
|
-
for (const [key, value] of Object.entries(spec.overrides)) {
|
|
1282
|
-
const standard = STANDARD_OVERRIDE_TYPES[key];
|
|
1283
|
-
const type = standard ?? declared.get(key);
|
|
1284
|
-
if (type === void 0) {
|
|
1285
|
-
throw composeError(
|
|
1286
|
-
id,
|
|
1287
|
-
`override:${key}`,
|
|
1288
|
-
`override "${key}" is neither a standard override nor declared in overrideDecls`
|
|
1289
|
-
);
|
|
1290
|
-
}
|
|
1291
|
-
assertOverrideValue(id, key, type, value);
|
|
1292
|
-
}
|
|
1293
|
-
const slots = /* @__PURE__ */ new Set();
|
|
1294
|
-
const names = /* @__PURE__ */ new Set();
|
|
1295
|
-
const blocks = new Set(spec.uniforms.map((block) => block.name));
|
|
1296
|
-
for (const b of spec.bindings) {
|
|
1297
|
-
if (!IDENTIFIER.test(b.name) || RESERVED.has(b.name)) {
|
|
1298
|
-
throw composeError(id, `binding:${b.name}`, `binding name "${b.name}" is not a usable identifier`);
|
|
1299
|
-
}
|
|
1300
|
-
const slot = `${b.group}.${b.binding}`;
|
|
1301
|
-
if (slots.has(slot)) {
|
|
1302
|
-
throw composeError(id, `binding:${b.name}`, `two bindings share group ${b.group} binding ${b.binding}`);
|
|
1303
|
-
}
|
|
1304
|
-
if (names.has(b.name)) {
|
|
1305
|
-
throw composeError(id, `binding:${b.name}`, `two bindings share the name "${b.name}"`);
|
|
1306
|
-
}
|
|
1307
|
-
slots.add(slot);
|
|
1308
|
-
names.add(b.name);
|
|
1309
|
-
const struct = structNameOf(b.wgslType);
|
|
1310
|
-
if (struct !== null && !blocks.has(struct)) {
|
|
1311
|
-
throw composeError(
|
|
1312
|
-
id,
|
|
1313
|
-
`type:${struct}`,
|
|
1314
|
-
`binding "${b.name}" names struct "${struct}" but no block in spec.uniforms declares it`
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
function substituteSnippets(spec) {
|
|
1320
|
-
const { id } = spec;
|
|
1321
|
-
let { body } = spec;
|
|
1322
|
-
for (const [key, snippet] of Object.entries(spec.snippets ?? {})) {
|
|
1323
|
-
const marker = `//@@${key}@@`;
|
|
1324
|
-
if (!body.includes(marker)) {
|
|
1325
|
-
throw composeError(id, `snippet:${key}`, `snippet "${key}" has no ${marker} marker in the body`);
|
|
1326
|
-
}
|
|
1327
|
-
body = body.split(marker).join(snippet);
|
|
1328
|
-
}
|
|
1329
|
-
const left = MARKER.exec(body);
|
|
1330
|
-
if (left !== null) {
|
|
1331
|
-
throw composeError(id, `marker:${left[1]}`, `marker //@@${left[1]}@@ is not filled by any snippet`);
|
|
1332
|
-
}
|
|
1333
|
-
return body;
|
|
1334
|
-
}
|
|
1335
|
-
function validateBody(spec, body) {
|
|
1336
|
-
const { id } = spec;
|
|
1337
|
-
for (const forbidden of ["@group(", "override "]) {
|
|
1338
|
-
if (body.includes(forbidden)) {
|
|
1339
|
-
throw composeError(
|
|
1340
|
-
id,
|
|
1341
|
-
`body:${forbidden}`,
|
|
1342
|
-
`a body must not contain "${forbidden}" (the composer emits those lines)`
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
const stripped = stripComments(body);
|
|
1347
|
-
const helpers = REDUCE_HELPER_NAMES;
|
|
1348
|
-
for (const match of stripped.matchAll(IDENTIFIER_TOKENS)) {
|
|
1349
|
-
const word = match[0];
|
|
1350
|
-
if (RESERVED.has(word)) {
|
|
1351
|
-
throw composeError(
|
|
1352
|
-
id,
|
|
1353
|
-
`reserved:${word}`,
|
|
1354
|
-
`"${word}" is a WGSL reserved word (spec 16.2) and cannot be an identifier`
|
|
1355
|
-
);
|
|
1356
|
-
}
|
|
1357
|
-
if (helpers.includes(word) && !spec.needs.includes("subgroups")) {
|
|
1358
|
-
throw composeError(id, `helper:${word}`, `the body calls ${word} but spec.needs does not list "subgroups"`);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
function composeWgsl(spec, caps) {
|
|
1363
|
-
validateDeclarations(spec);
|
|
1364
|
-
const entryPoint = findEntryPoint(spec.body);
|
|
1365
|
-
if (entryPoint === null) {
|
|
1366
|
-
throw composeError(
|
|
1367
|
-
spec.id,
|
|
1368
|
-
"entry",
|
|
1369
|
-
"a body must carry exactly one @compute attribute followed by `fn <name>`"
|
|
1370
|
-
);
|
|
1371
|
-
}
|
|
1372
|
-
const body = substituteSnippets(spec);
|
|
1373
|
-
validateBody(spec, body);
|
|
1374
|
-
const wantsHelpers = spec.needs.includes("subgroups");
|
|
1375
|
-
const subgroups = wantsHelpers && caps.features.has("subgroups");
|
|
1376
|
-
const overrides = { ...spec.overrides };
|
|
1377
|
-
if (overrides.WG === void 0) {
|
|
1378
|
-
overrides.WG = workgroupSizeFor(caps);
|
|
1379
|
-
}
|
|
1380
|
-
if (subgroups) {
|
|
1381
|
-
overrides.SUBGROUP_MAX = caps.subgroupMaxSize;
|
|
1382
|
-
overrides.SUBGROUP_MIN = Math.max(SUBGROUP_FLOOR, caps.subgroupMinSize);
|
|
1383
|
-
}
|
|
1384
|
-
const parts = [];
|
|
1385
|
-
if (subgroups) {
|
|
1386
|
-
parts.push("enable subgroups;");
|
|
1387
|
-
}
|
|
1388
|
-
parts.push(PRELUDE_WGSL);
|
|
1389
|
-
for (const decl of spec.overrideDecls) {
|
|
1390
|
-
parts.push(`override ${decl.name}: ${decl.type} = ${overrideLiteral(decl)};`);
|
|
1391
|
-
}
|
|
1392
|
-
for (const block of spec.uniforms) {
|
|
1393
|
-
parts.push(block.wgsl);
|
|
1394
|
-
}
|
|
1395
|
-
if (wantsHelpers) {
|
|
1396
|
-
parts.push(subgroups ? REDUCE_HELPERS_SUBGROUP_WGSL : REDUCE_HELPERS_WORKGROUP_WGSL);
|
|
1397
|
-
}
|
|
1398
|
-
for (const decl of spec.bindings) {
|
|
1399
|
-
parts.push(bindingLine(decl));
|
|
1400
|
-
}
|
|
1401
|
-
const head = parts.join("\n");
|
|
1402
|
-
const code = `${head}
|
|
1403
|
-
${body}`;
|
|
1404
|
-
return Object.freeze({
|
|
1405
|
-
id: spec.id,
|
|
1406
|
-
code,
|
|
1407
|
-
bodyLine: head.split("\n").length + 1,
|
|
1408
|
-
overrides: Object.freeze(overrides),
|
|
1409
|
-
constants: Object.freeze(referencedOverrides(code, overrides)),
|
|
1410
|
-
entryPoint,
|
|
1411
|
-
subgroups
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
function referencedOverrides(code, overrides) {
|
|
1415
|
-
const stripped = code.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ").replace(/^\s*override\s+\w+\s*:[^\n]*$/gm, " ");
|
|
1416
|
-
const referenced = {};
|
|
1417
|
-
for (const [name, value] of Object.entries(overrides)) {
|
|
1418
|
-
if (new RegExp(`\\b${name}\\b`).test(stripped)) {
|
|
1419
|
-
referenced[name] = value;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
return referenced;
|
|
1423
|
-
}
|
|
1424
|
-
function stableJson(value) {
|
|
1425
|
-
if (Array.isArray(value)) {
|
|
1426
|
-
return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
|
1427
|
-
}
|
|
1428
|
-
if (typeof value === "object" && value !== null) {
|
|
1429
|
-
const record = value;
|
|
1430
|
-
const keys = Object.keys(record).sort();
|
|
1431
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
1432
|
-
}
|
|
1433
|
-
return JSON.stringify(value);
|
|
1434
|
-
}
|
|
1435
|
-
function hashSnippets(snippets) {
|
|
1436
|
-
if (snippets === void 0 || Object.keys(snippets).length === 0) {
|
|
1437
|
-
return "0";
|
|
1438
|
-
}
|
|
1439
|
-
const text = stableJson(snippets);
|
|
1440
|
-
let hash = 2166136261;
|
|
1441
|
-
for (let i = 0; i < text.length; i++) {
|
|
1442
|
-
hash ^= text.charCodeAt(i);
|
|
1443
|
-
hash = Math.imul(hash, 16777619) >>> 0;
|
|
1444
|
-
}
|
|
1445
|
-
return hash.toString(16).padStart(8, "0");
|
|
1446
|
-
}
|
|
1447
|
-
function pipelineKey(spec, caps) {
|
|
1448
|
-
const present = spec.needs.filter((need) => caps.features.has(need)).join(",");
|
|
1449
|
-
return `${spec.id}|${stableJson(spec.overrides)}|${present}|${hashSnippets(spec.snippets)}`;
|
|
1450
|
-
}
|
|
1451
|
-
function messageOf(err) {
|
|
1452
|
-
return err instanceof Error ? err.message : String(err);
|
|
1453
|
-
}
|
|
1454
|
-
class PipelineCache {
|
|
1455
|
-
/**
|
|
1456
|
-
* Builds a cache over a device.
|
|
1457
|
-
* @param device - the device pipelines are created on
|
|
1458
|
-
* @param caps - its capabilities (WG, subgroup sizes, features)
|
|
1459
|
-
*/
|
|
1460
|
-
constructor(device, caps) {
|
|
1461
|
-
this.pending = /* @__PURE__ */ new Map();
|
|
1462
|
-
this.entries = /* @__PURE__ */ new Map();
|
|
1463
|
-
this.order = [];
|
|
1464
|
-
this.disposed = false;
|
|
1465
|
-
this.device = device;
|
|
1466
|
-
this.caps = caps;
|
|
1467
|
-
}
|
|
1468
|
-
/**
|
|
1469
|
-
* The cache key of a spec on this device: `id + "|" + stableJson(overrides) + "|" + needs.filter(present).join(",") + "|" + hash(snippets)`.
|
|
1470
|
-
* @param spec - the module
|
|
1471
|
-
* @returns the key
|
|
1472
|
-
*/
|
|
1473
|
-
key(spec) {
|
|
1474
|
-
return pipelineKey(spec, this.caps);
|
|
1475
|
-
}
|
|
1476
|
-
/**
|
|
1477
|
-
* createComputePipelineAsync inside a validation scope with the layouts derived from spec.bindings; compilation messages become E_SHADER_COMPILE { id, stage: "compile", messages } with body-relative lines.
|
|
1478
|
-
* @param spec - the module
|
|
1479
|
-
* @returns the pipeline (the same object for the same key)
|
|
1480
|
-
*/
|
|
1481
|
-
async get(spec) {
|
|
1482
|
-
return (await this.entry(spec)).pipeline;
|
|
1483
|
-
}
|
|
1484
|
-
/**
|
|
1485
|
-
* The Kernel (pipeline + layouts + binding names) for a spec; cached with the pipeline.
|
|
1486
|
-
* @param spec - the module
|
|
1487
|
-
* @returns the kernel (the same object for the same key)
|
|
1488
|
-
*/
|
|
1489
|
-
async kernel(spec) {
|
|
1490
|
-
return (await this.entry(spec)).kernel;
|
|
1491
|
-
}
|
|
1492
|
-
/**
|
|
1493
|
-
* Compiles every spec not yet cached (load() calls it so the first step() does not compile).
|
|
1494
|
-
* @param specs - the modules
|
|
1495
|
-
*/
|
|
1496
|
-
async warm(specs) {
|
|
1497
|
-
await Promise.all(specs.map((spec) => this.entry(spec)));
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Number of cached pipelines.
|
|
1501
|
-
* @returns the count of finished compiles
|
|
1502
|
-
*/
|
|
1503
|
-
get size() {
|
|
1504
|
-
return this.entries.size;
|
|
1505
|
-
}
|
|
1506
|
-
/**
|
|
1507
|
-
* Every key created so far, in creation order (the override-matrix coverage test reads it).
|
|
1508
|
-
* PLAN DECISION: keys are listed in compile-START order and only those whose compile finished (warm() compiles
|
|
1509
|
-
* concurrently, so completion order is not deterministic; a failed compile is removed).
|
|
1510
|
-
* @returns the keys
|
|
1511
|
-
* @internal
|
|
1512
|
-
*/
|
|
1513
|
-
keys() {
|
|
1514
|
-
return this.order.filter((key) => this.entries.has(key));
|
|
1515
|
-
}
|
|
1516
|
-
/**
|
|
1517
|
-
* The bind-group layouts of a cached kernel by key.
|
|
1518
|
-
* @param key - a key from keys()
|
|
1519
|
-
* @returns the layouts, or null when the key is not cached
|
|
1520
|
-
* @internal
|
|
1521
|
-
*/
|
|
1522
|
-
layoutsOf(key) {
|
|
1523
|
-
return this.entries.get(key)?.layouts ?? null;
|
|
1524
|
-
}
|
|
1525
|
-
/**
|
|
1526
|
-
* PLAN DECISION: forgets every pipeline and refuses further compiles with E_DISPOSED (the context's dispose()
|
|
1527
|
-
* may call it; a GPUComputePipeline has no destroy(), so the objects are simply released).
|
|
1528
|
-
*/
|
|
1529
|
-
dispose() {
|
|
1530
|
-
this.disposed = true;
|
|
1531
|
-
this.pending.clear();
|
|
1532
|
-
this.entries.clear();
|
|
1533
|
-
this.order.length = 0;
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
|
-
* The cached entry of a spec, compiling it once per key.
|
|
1537
|
-
* @param spec - the module
|
|
1538
|
-
* @returns the entry
|
|
1539
|
-
*/
|
|
1540
|
-
entry(spec) {
|
|
1541
|
-
if (this.disposed) {
|
|
1542
|
-
return Promise.reject(
|
|
1543
|
-
new WebGpuGraphError("E_DISPOSED", "PipelineCache: disposed", { label: "PipelineCache" })
|
|
1544
|
-
);
|
|
1545
|
-
}
|
|
1546
|
-
const key = this.key(spec);
|
|
1547
|
-
const inFlight = this.pending.get(key);
|
|
1548
|
-
if (inFlight !== void 0) {
|
|
1549
|
-
return inFlight;
|
|
1550
|
-
}
|
|
1551
|
-
this.order.push(key);
|
|
1552
|
-
const compile = this.compile(spec, key).then(
|
|
1553
|
-
(entry) => {
|
|
1554
|
-
if (!this.disposed) {
|
|
1555
|
-
this.entries.set(key, entry);
|
|
1556
|
-
}
|
|
1557
|
-
return entry;
|
|
1558
|
-
},
|
|
1559
|
-
(err) => {
|
|
1560
|
-
this.pending.delete(key);
|
|
1561
|
-
const at = this.order.indexOf(key);
|
|
1562
|
-
if (at >= 0) {
|
|
1563
|
-
this.order.splice(at, 1);
|
|
1564
|
-
}
|
|
1565
|
-
throw err;
|
|
1566
|
-
}
|
|
1567
|
-
);
|
|
1568
|
-
this.pending.set(key, compile);
|
|
1569
|
-
return compile;
|
|
1570
|
-
}
|
|
1571
|
-
/**
|
|
1572
|
-
* One compile: compose, create the shader module inside a synchronous push / pop validation pair (so concurrent
|
|
1573
|
-
* compiles never interleave scopes), check the compilation info, create the layouts and the pipeline.
|
|
1574
|
-
* PLAN DECISION: `E_SHADER_COMPILE` at stage "compile" also carries `lines` (the body-relative 1-based line of every
|
|
1575
|
-
* error message, from `composed.bodyLine`) next to the contract's `messages`; a `createComputePipelineAsync`
|
|
1576
|
-
* rejection (a GPUPipelineError, which pushes nothing into a validation scope on Dawn 0.4.0) is
|
|
1577
|
-
* `E_VALIDATION { label: "<id>|pipeline", message }`; bool overrides travel as the pipeline constants 1 / 0
|
|
1578
|
-
* because dawn.node's WebIDL layer rejects a boolean constant.
|
|
1579
|
-
* @param spec - the module
|
|
1580
|
-
* @param key - its key (the pipeline label)
|
|
1581
|
-
* @returns the entry
|
|
1582
|
-
*/
|
|
1583
|
-
async compile(spec, key) {
|
|
1584
|
-
const composed = composeWgsl(spec, this.caps);
|
|
1585
|
-
const { device } = this;
|
|
1586
|
-
const label = `${spec.id}|pipeline`;
|
|
1587
|
-
device.pushErrorScope("validation");
|
|
1588
|
-
const module = device.createShaderModule({ code: composed.code, label: spec.id });
|
|
1589
|
-
const layouts = bindGroupLayoutDescriptors(spec).map(
|
|
1590
|
-
(descriptor, group) => device.createBindGroupLayout({ ...descriptor, label: `${spec.id}/layout${group}` })
|
|
1591
|
-
);
|
|
1592
|
-
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: layouts, label: `${spec.id}|layout` });
|
|
1593
|
-
const popped = device.popErrorScope();
|
|
1594
|
-
const info = await module.getCompilationInfo();
|
|
1595
|
-
const scopeError = await popped;
|
|
1596
|
-
const errors = info.messages.filter((message) => message.type === "error");
|
|
1597
|
-
if (errors.length > 0) {
|
|
1598
|
-
const preludeLines = composed.bodyLine - 1;
|
|
1599
|
-
throw new WebGpuGraphError(
|
|
1600
|
-
"E_SHADER_COMPILE",
|
|
1601
|
-
`${spec.id}: WGSL compilation failed (${errors.length} error(s))`,
|
|
1602
|
-
{
|
|
1603
|
-
id: spec.id,
|
|
1604
|
-
stage: "compile",
|
|
1605
|
-
messages: formatCompilationInfo(info, preludeLines),
|
|
1606
|
-
lines: errors.map((message) => message.lineNum - preludeLines)
|
|
1607
|
-
}
|
|
1608
|
-
);
|
|
1609
|
-
}
|
|
1610
|
-
if (scopeError !== null) {
|
|
1611
|
-
throw new WebGpuGraphError("E_VALIDATION", `${spec.id}: ${scopeError.message}`, {
|
|
1612
|
-
label,
|
|
1613
|
-
message: scopeError.message
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
const constants = {};
|
|
1617
|
-
for (const [name, value] of Object.entries(composed.constants)) {
|
|
1618
|
-
if (typeof value === "boolean") {
|
|
1619
|
-
constants[name] = value ? 1 : 0;
|
|
1620
|
-
} else {
|
|
1621
|
-
constants[name] = value;
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
let pipeline;
|
|
1625
|
-
try {
|
|
1626
|
-
pipeline = await device.createComputePipelineAsync({
|
|
1627
|
-
label: key,
|
|
1628
|
-
layout: pipelineLayout,
|
|
1629
|
-
compute: { module, entryPoint: composed.entryPoint, constants }
|
|
1630
|
-
});
|
|
1631
|
-
} catch (err) {
|
|
1632
|
-
const message = messageOf(err);
|
|
1633
|
-
throw new WebGpuGraphError("E_VALIDATION", `${spec.id}: pipeline creation failed: ${message}`, {
|
|
1634
|
-
label,
|
|
1635
|
-
message
|
|
1636
|
-
});
|
|
1637
|
-
}
|
|
1638
|
-
const kernel = new Kernel(device, spec, composed, pipeline, layouts);
|
|
1639
|
-
return { key, composed, pipeline, layouts, kernel };
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
class Profiler {
|
|
1643
|
-
/**
|
|
1644
|
-
* Creates the query set and the resolve buffer when usable; `slots` defaults to PROFILER_QUERY_SLOTS = 256
|
|
1645
|
-
* (spec 5.5); 2 query slots per pass. `enabled` is also false when the device lacks "timestamp-query" or
|
|
1646
|
-
* `slots < 2`, so no caller can make the profiler raise a validation error.
|
|
1647
|
-
* @param device - the device the query set is created on
|
|
1648
|
-
* @param enabled - whether the caller wants profiling (the context passes `features.has("timestamp-query")`)
|
|
1649
|
-
* @param quantised - true in browsers
|
|
1650
|
-
* @param slots - query slots, two per pass (default PROFILER_QUERY_SLOTS)
|
|
1651
|
-
*/
|
|
1652
|
-
constructor(device, enabled, quantised, slots) {
|
|
1653
|
-
this.labels = /* @__PURE__ */ new WeakMap();
|
|
1654
|
-
this.partialRequests = /* @__PURE__ */ new WeakSet();
|
|
1655
|
-
this.pending = [];
|
|
1656
|
-
this.cursor = 0;
|
|
1657
|
-
this.refused = 0;
|
|
1658
|
-
this.destroyed = false;
|
|
1659
|
-
const count = slots ?? PROFILER_QUERY_SLOTS;
|
|
1660
|
-
this.quantised = quantised;
|
|
1661
|
-
this.slots = count;
|
|
1662
|
-
const usable = enabled && count >= 2 && device.features.has("timestamp-query");
|
|
1663
|
-
this.enabled = usable;
|
|
1664
|
-
if (usable) {
|
|
1665
|
-
this.querySet = device.createQuerySet({ label: "profiler/timestamps", type: "timestamp", count });
|
|
1666
|
-
this.resolveBuffer = device.createBuffer({
|
|
1667
|
-
label: "profiler/resolve",
|
|
1668
|
-
size: count * 8,
|
|
1669
|
-
usage: BufferUsage.QUERY_RESOLVE | BufferUsage.COPY_SRC
|
|
1670
|
-
});
|
|
1671
|
-
} else {
|
|
1672
|
-
this.querySet = null;
|
|
1673
|
-
this.resolveBuffer = null;
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
/**
|
|
1677
|
-
* The timestampWrites descriptor for a new pass, or undefined when disabled / out of slots. A pass refused for
|
|
1678
|
-
* want of slots is counted, and the next `resolveInto()` marks its request partial.
|
|
1679
|
-
* @param label - the pass name reported by `timings()`
|
|
1680
|
-
* @returns the descriptor a CommandBatch puts on beginComputePass, or undefined
|
|
1681
|
-
*/
|
|
1682
|
-
beginPass(label) {
|
|
1683
|
-
if (this.querySet === null || this.destroyed) {
|
|
1684
|
-
return void 0;
|
|
1685
|
-
}
|
|
1686
|
-
if (this.cursor + 2 > this.slots) {
|
|
1687
|
-
this.refused++;
|
|
1688
|
-
return void 0;
|
|
1689
|
-
}
|
|
1690
|
-
const begin = this.cursor;
|
|
1691
|
-
this.cursor += 2;
|
|
1692
|
-
this.pending.push({ label, begin });
|
|
1693
|
-
return { querySet: this.querySet, beginningOfPassWriteIndex: begin, endOfPassWriteIndex: begin + 1 };
|
|
1694
|
-
}
|
|
1695
|
-
/**
|
|
1696
|
-
* Records the resolve + copy into the batch's staging slot; returns the request's byte range, or null when
|
|
1697
|
-
* nothing was written (disabled, destroyed, or no pass begun since the last resolve).
|
|
1698
|
-
* @param batch - the batch whose passes were timed (it must still be open)
|
|
1699
|
-
* @returns the readback request `timings()` decodes, or null
|
|
1700
|
-
*/
|
|
1701
|
-
resolveInto(batch) {
|
|
1702
|
-
if (this.querySet === null || this.resolveBuffer === null || this.destroyed || this.pending.length === 0) {
|
|
1703
|
-
return null;
|
|
1704
|
-
}
|
|
1705
|
-
const count = this.cursor;
|
|
1706
|
-
batch.resolveQuerySet(this.querySet, 0, count, this.resolveBuffer);
|
|
1707
|
-
const request = batch.readback(this.resolveBuffer, 0, count * 8);
|
|
1708
|
-
this.labels.set(
|
|
1709
|
-
request,
|
|
1710
|
-
this.pending.map((pass) => pass.label)
|
|
1711
|
-
);
|
|
1712
|
-
if (this.refused > 0) {
|
|
1713
|
-
this.partialRequests.add(request);
|
|
1714
|
-
}
|
|
1715
|
-
this.pending = [];
|
|
1716
|
-
this.cursor = 0;
|
|
1717
|
-
this.refused = 0;
|
|
1718
|
-
return request;
|
|
1719
|
-
}
|
|
1720
|
-
/**
|
|
1721
|
-
* Whether a pass of the batch was refused a slot pair, so `timings()` covers only its first slots / 2 passes
|
|
1722
|
-
* and their sum is not the batch's duration.
|
|
1723
|
-
* @param request - the request `resolveInto()` returned
|
|
1724
|
-
* @returns true when at least one pass of that batch went untimed
|
|
1725
|
-
*/
|
|
1726
|
-
partial(request) {
|
|
1727
|
-
return this.partialRequests.has(request);
|
|
1728
|
-
}
|
|
1729
|
-
/**
|
|
1730
|
-
* Decodes the timings of a batch from its readback bytes.
|
|
1731
|
-
* @param bytes - the resolved readback of the batch
|
|
1732
|
-
* @param request - the request `resolveInto()` returned for it
|
|
1733
|
-
* @returns one timing per pass in pass order (empty for a request this profiler did not create)
|
|
1734
|
-
*/
|
|
1735
|
-
timings(bytes, request) {
|
|
1736
|
-
const labels = this.labels.get(request);
|
|
1737
|
-
if (labels === void 0) {
|
|
1738
|
-
return [];
|
|
1739
|
-
}
|
|
1740
|
-
const view = new DataView(bytes);
|
|
1741
|
-
const out = [];
|
|
1742
|
-
for (let i = 0; i < labels.length; i++) {
|
|
1743
|
-
const at = request.offset + i * 16;
|
|
1744
|
-
if (at + 16 > bytes.byteLength) {
|
|
1745
|
-
break;
|
|
1746
|
-
}
|
|
1747
|
-
const begin = view.getBigUint64(at, true);
|
|
1748
|
-
const end = view.getBigUint64(at + 8, true);
|
|
1749
|
-
out.push({ label: labels[i], ns: Number(end - begin) });
|
|
1750
|
-
}
|
|
1751
|
-
return out;
|
|
1752
|
-
}
|
|
1753
|
-
/** Destroys the query set and the resolve buffer; idempotent. */
|
|
1754
|
-
destroy() {
|
|
1755
|
-
if (this.destroyed) {
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
this.destroyed = true;
|
|
1759
|
-
this.pending = [];
|
|
1760
|
-
this.cursor = 0;
|
|
1761
|
-
this.refused = 0;
|
|
1762
|
-
if (this.querySet !== null) {
|
|
1763
|
-
this.querySet.destroy();
|
|
1764
|
-
}
|
|
1765
|
-
if (this.resolveBuffer !== null) {
|
|
1766
|
-
this.resolveBuffer.destroy();
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
class Lease {
|
|
1771
|
-
/**
|
|
1772
|
-
* Creates a lease over a pool; `pool.lease()` is the usual way to get one.
|
|
1773
|
-
* @param pool - the pool every acquisition of this lease goes through
|
|
1774
|
-
*/
|
|
1775
|
-
constructor(pool) {
|
|
1776
|
-
this.buffers = /* @__PURE__ */ new Set();
|
|
1777
|
-
this.released = false;
|
|
1778
|
-
this.pool = pool;
|
|
1779
|
-
}
|
|
1780
|
-
/**
|
|
1781
|
-
* pool.acquire(byteLength, STORAGE | COPY_SRC | COPY_DST, label).
|
|
1782
|
-
* @param byteLength - bytes needed; the pool rounds up to its size class
|
|
1783
|
-
* @param label - the buffer label
|
|
1784
|
-
* @returns the buffer, returned to the pool by `release()`
|
|
1785
|
-
*/
|
|
1786
|
-
storage(byteLength, label) {
|
|
1787
|
-
return this.acquire(byteLength, BufferUsage.STORAGE | BufferUsage.COPY_SRC | BufferUsage.COPY_DST, label);
|
|
1788
|
-
}
|
|
1789
|
-
/**
|
|
1790
|
-
* pool.acquire(byteLength, UNIFORM | COPY_DST, label).
|
|
1791
|
-
* @param byteLength - bytes needed; the pool rounds up to its size class
|
|
1792
|
-
* @param label - the buffer label
|
|
1793
|
-
* @returns the buffer, returned to the pool by `release()`
|
|
1794
|
-
*/
|
|
1795
|
-
uniform(byteLength, label) {
|
|
1796
|
-
return this.acquire(byteLength, BufferUsage.UNIFORM | BufferUsage.COPY_DST, label);
|
|
1797
|
-
}
|
|
1798
|
-
/**
|
|
1799
|
-
* pool.acquire with explicit usage.
|
|
1800
|
-
* @param byteLength - bytes needed; the pool rounds up to its size class
|
|
1801
|
-
* @param usage - the GPUBufferUsage bits
|
|
1802
|
-
* @param label - the buffer label
|
|
1803
|
-
* @returns the buffer, returned to the pool by `release()`
|
|
1804
|
-
*/
|
|
1805
|
-
acquire(byteLength, usage, label) {
|
|
1806
|
-
if (this.released) {
|
|
1807
|
-
throw new WebGpuGraphError("E_DISPOSED", `lease already released: cannot acquire "${label}"`, { label });
|
|
1808
|
-
}
|
|
1809
|
-
const buffer = this.pool.acquire(byteLength, usage, label);
|
|
1810
|
-
this.buffers.add(buffer);
|
|
1811
|
-
return buffer;
|
|
1812
|
-
}
|
|
1813
|
-
/**
|
|
1814
|
-
* Releases every buffer acquired through this lease; idempotent. A pool that `ctx.dispose()` already
|
|
1815
|
-
* destroyed has nothing left to take back, so its E_DISPOSED is swallowed here: the lease sits in a
|
|
1816
|
-
* `finally` block and must never mask the error that unwound the algorithm.
|
|
1817
|
-
*/
|
|
1818
|
-
release() {
|
|
1819
|
-
if (this.released) {
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
this.released = true;
|
|
1823
|
-
const owned = [...this.buffers];
|
|
1824
|
-
this.buffers.clear();
|
|
1825
|
-
for (const buffer of owned) {
|
|
1826
|
-
try {
|
|
1827
|
-
this.pool.release(buffer);
|
|
1828
|
-
} catch (error) {
|
|
1829
|
-
if (!hasErrorCode(error, "E_DISPOSED")) {
|
|
1830
|
-
throw error;
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
/**
|
|
1836
|
-
* Live buffers of this lease.
|
|
1837
|
-
* @returns the number of buffers acquired through the lease and not yet released
|
|
1838
|
-
*/
|
|
1839
|
-
get count() {
|
|
1840
|
-
return this.buffers.size;
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
function classKey(sizeClass, usage) {
|
|
1844
|
-
return `${sizeClass}:${usage}`;
|
|
1845
|
-
}
|
|
1846
|
-
class BufferPool {
|
|
1847
|
-
/**
|
|
1848
|
-
* Creates an empty pool.
|
|
1849
|
-
* @param _device - the device (buffers are created through the allocator, which owns it; the parameter keeps
|
|
1850
|
-
* the contract's constructor shape)
|
|
1851
|
-
* @param allocator - the context's OOM-scoped allocator
|
|
1852
|
-
* @param maxBufferSize - the device's maxBufferSize (classes above it are never created)
|
|
1853
|
-
* @param options - pool options
|
|
1854
|
-
* @param options.maxIdlePerClass - idle buffers kept per (class, usage); default POOL_MAX_IDLE_PER_CLASS
|
|
1855
|
-
*/
|
|
1856
|
-
constructor(_device, allocator, maxBufferSize, options) {
|
|
1857
|
-
this.idle = /* @__PURE__ */ new Map();
|
|
1858
|
-
this.live = /* @__PURE__ */ new Map();
|
|
1859
|
-
this.liveBytesValue = 0;
|
|
1860
|
-
this.idleBytesValue = 0;
|
|
1861
|
-
this.disposed = false;
|
|
1862
|
-
this.allocator = allocator;
|
|
1863
|
-
this.maxBufferSize = maxBufferSize - maxBufferSize % 4;
|
|
1864
|
-
this.maxIdlePerClass = options?.maxIdlePerClass ?? POOL_MAX_IDLE_PER_CLASS;
|
|
1865
|
-
}
|
|
1866
|
-
/**
|
|
1867
|
-
* The size class a byte length rounds up to: powers of two from 4 KiB to 64 MiB, then 16 MiB steps (pure).
|
|
1868
|
-
* @param byteLength - a positive integer
|
|
1869
|
-
* @returns the class in bytes
|
|
1870
|
-
*/
|
|
1871
|
-
static sizeClass(byteLength) {
|
|
1872
|
-
if (!Number.isInteger(byteLength) || byteLength <= 0) {
|
|
1873
|
-
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "byteLength: expected a positive integer", {
|
|
1874
|
-
argument: "byteLength",
|
|
1875
|
-
value: byteLength,
|
|
1876
|
-
expected: "a positive integer"
|
|
1877
|
-
});
|
|
1878
|
-
}
|
|
1879
|
-
if (byteLength > POOL_MAX_POW2_CLASS_BYTES) {
|
|
1880
|
-
return Math.ceil(byteLength / POOL_LINEAR_STEP_BYTES) * POOL_LINEAR_STEP_BYTES;
|
|
1881
|
-
}
|
|
1882
|
-
let sizeClass = POOL_MIN_CLASS_BYTES;
|
|
1883
|
-
while (sizeClass < byteLength) {
|
|
1884
|
-
sizeClass *= 2;
|
|
1885
|
-
}
|
|
1886
|
-
return sizeClass;
|
|
1887
|
-
}
|
|
1888
|
-
/**
|
|
1889
|
-
* Acquires a buffer of at least byteLength (an idle one of the class and usage, else a new one through the
|
|
1890
|
-
* allocator); E_TOO_LARGE above maxBufferSize.
|
|
1891
|
-
* @param byteLength - the bytes needed
|
|
1892
|
-
* @param usage - the GPUBufferUsage flags
|
|
1893
|
-
* @param label - the buffer label (a reused buffer is relabelled)
|
|
1894
|
-
* @returns a buffer of the class size
|
|
1895
|
-
*/
|
|
1896
|
-
acquire(byteLength, usage, label) {
|
|
1897
|
-
if (this.disposed) {
|
|
1898
|
-
throw new WebGpuGraphError("E_DISPOSED", "the buffer pool was disposed", { label: "pool" });
|
|
1899
|
-
}
|
|
1900
|
-
const rounded = BufferPool.sizeClass(byteLength);
|
|
1901
|
-
if (byteLength > this.maxBufferSize) {
|
|
1902
|
-
throw new WebGpuGraphError(
|
|
1903
|
-
"E_TOO_LARGE",
|
|
1904
|
-
`pool: ${byteLength} bytes exceed the device maxBufferSize of ${this.maxBufferSize} bytes`,
|
|
1905
|
-
{ needed: byteLength, limit: this.maxBufferSize, path: "pool", algorithm: null }
|
|
1906
|
-
);
|
|
1907
|
-
}
|
|
1908
|
-
const sizeClass = Math.min(rounded, this.maxBufferSize);
|
|
1909
|
-
const key = classKey(sizeClass, usage);
|
|
1910
|
-
const idle = this.idle.get(key);
|
|
1911
|
-
let buffer = idle?.pop();
|
|
1912
|
-
if (buffer === void 0) {
|
|
1913
|
-
buffer = this.allocator.createBuffer({ label, size: sizeClass, usage });
|
|
1914
|
-
} else {
|
|
1915
|
-
this.idleBytesValue -= sizeClass;
|
|
1916
|
-
buffer.label = label;
|
|
1917
|
-
}
|
|
1918
|
-
this.live.set(buffer, { sizeClass, usage });
|
|
1919
|
-
this.liveBytesValue += sizeClass;
|
|
1920
|
-
return buffer;
|
|
1921
|
-
}
|
|
1922
|
-
/**
|
|
1923
|
-
* Returns a buffer to its class; destroys it when the class already holds maxIdlePerClass idle buffers. A no-op
|
|
1924
|
-
* after destroyAll() (so a `finally` never masks the original error).
|
|
1925
|
-
* @param buffer - a buffer acquired from this pool and not yet released
|
|
1926
|
-
*/
|
|
1927
|
-
release(buffer) {
|
|
1928
|
-
if (this.disposed) {
|
|
1929
|
-
return;
|
|
1930
|
-
}
|
|
1931
|
-
const entry = this.live.get(buffer);
|
|
1932
|
-
if (entry === void 0) {
|
|
1933
|
-
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "buffer: not a live buffer of this pool", {
|
|
1934
|
-
argument: "buffer",
|
|
1935
|
-
value: buffer.label,
|
|
1936
|
-
expected: "a buffer acquired from this pool and not yet released"
|
|
1937
|
-
});
|
|
1938
|
-
}
|
|
1939
|
-
this.live.delete(buffer);
|
|
1940
|
-
this.liveBytesValue -= entry.sizeClass;
|
|
1941
|
-
const key = classKey(entry.sizeClass, entry.usage);
|
|
1942
|
-
let idle = this.idle.get(key);
|
|
1943
|
-
if (idle === void 0) {
|
|
1944
|
-
idle = [];
|
|
1945
|
-
this.idle.set(key, idle);
|
|
1946
|
-
}
|
|
1947
|
-
if (idle.length >= this.maxIdlePerClass) {
|
|
1948
|
-
this.allocator.destroy(buffer);
|
|
1949
|
-
return;
|
|
1950
|
-
}
|
|
1951
|
-
idle.push(buffer);
|
|
1952
|
-
this.idleBytesValue += entry.sizeClass;
|
|
1953
|
-
}
|
|
1954
|
-
/** Destroys every idle buffer. */
|
|
1955
|
-
trim() {
|
|
1956
|
-
for (const buffers of this.idle.values()) {
|
|
1957
|
-
for (const buffer of buffers) {
|
|
1958
|
-
this.allocator.destroy(buffer);
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
this.idle.clear();
|
|
1962
|
-
this.idleBytesValue = 0;
|
|
1963
|
-
}
|
|
1964
|
-
/** Destroys everything, idle and live (ctx.dispose()); idempotent. @internal */
|
|
1965
|
-
destroyAll() {
|
|
1966
|
-
this.trim();
|
|
1967
|
-
for (const buffer of this.live.keys()) {
|
|
1968
|
-
this.allocator.destroy(buffer);
|
|
1969
|
-
}
|
|
1970
|
-
this.live.clear();
|
|
1971
|
-
this.liveBytesValue = 0;
|
|
1972
|
-
this.disposed = true;
|
|
1973
|
-
}
|
|
1974
|
-
/**
|
|
1975
|
-
* A scope object that releases everything acquired through it (spec 4.4, P2).
|
|
1976
|
-
* @returns a fresh lease over this pool
|
|
1977
|
-
*/
|
|
1978
|
-
lease() {
|
|
1979
|
-
return new Lease(this);
|
|
1980
|
-
}
|
|
1981
|
-
/**
|
|
1982
|
-
* Bytes of buffers acquired and not yet released (class sizes).
|
|
1983
|
-
* @returns the live byte count
|
|
1984
|
-
*/
|
|
1985
|
-
get liveBytes() {
|
|
1986
|
-
return this.liveBytesValue;
|
|
1987
|
-
}
|
|
1988
|
-
/**
|
|
1989
|
-
* Bytes of idle buffers (class sizes).
|
|
1990
|
-
* @returns the idle byte count
|
|
1991
|
-
*/
|
|
1992
|
-
get idleBytes() {
|
|
1993
|
-
return this.idleBytesValue;
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
const DEFAULT_SLOT_BYTES = 4 * 1024 * 1024;
|
|
1997
|
-
const STAGING_USAGE = BufferUsage.MAP_READ | BufferUsage.COPY_DST;
|
|
1998
|
-
const SLOT_GRANULE = 256;
|
|
1999
|
-
function isSharedBuffer$1(buffer) {
|
|
2000
|
-
return Object.prototype.toString.call(buffer) === "[object SharedArrayBuffer]";
|
|
2001
|
-
}
|
|
2002
|
-
function invalid$2(argument, value, expected) {
|
|
2003
|
-
return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument}: expected ${expected}`, {
|
|
2004
|
-
argument,
|
|
2005
|
-
value,
|
|
2006
|
-
expected
|
|
2007
|
-
});
|
|
2008
|
-
}
|
|
2009
|
-
class Readback {
|
|
2010
|
-
/**
|
|
2011
|
-
* Creates the ring with `slots` slots of `slotBytes` each.
|
|
2012
|
-
* @param device - the device the copies run on
|
|
2013
|
-
* @param allocator - the context's OOM-scoped allocator (used for slots at or above OOM_SCOPE_THRESHOLD_BYTES)
|
|
2014
|
-
* @param options - ring options
|
|
2015
|
-
* @param options.slots - slots created up front; default DEFAULT_STAGING_SLOTS
|
|
2016
|
-
* @param options.slotBytes - capacity of each slot and the chunk size of read(); default 4 MiB; a positive multiple of 4
|
|
2017
|
-
*/
|
|
2018
|
-
constructor(device, allocator, options) {
|
|
2019
|
-
this.ring = [];
|
|
2020
|
-
this.disposed = false;
|
|
2021
|
-
const slotBytes = options?.slotBytes ?? DEFAULT_SLOT_BYTES;
|
|
2022
|
-
if (!Number.isInteger(slotBytes) || slotBytes <= 0 || slotBytes % 4 !== 0) {
|
|
2023
|
-
throw invalid$2("slotBytes", slotBytes, "a positive multiple of 4");
|
|
2024
|
-
}
|
|
2025
|
-
const slots = options?.slots ?? DEFAULT_STAGING_SLOTS;
|
|
2026
|
-
if (!Number.isInteger(slots) || slots < 0) {
|
|
2027
|
-
throw invalid$2("slots", slots, "a non-negative integer");
|
|
2028
|
-
}
|
|
2029
|
-
this.device = device;
|
|
2030
|
-
this.allocator = allocator;
|
|
2031
|
-
this.slotBytes = slotBytes;
|
|
2032
|
-
for (let i = 0; i < slots; i++) {
|
|
2033
|
-
this.addSlot(slotBytes);
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
/**
|
|
2037
|
-
* Copies `byteLength` bytes from `src` at `srcOffset` (own encoder, own submit), maps, copies out BEFORE unmap;
|
|
2038
|
-
* resolves the bytes (a fresh ArrayBuffer, or `dest.buffer` after `dest.set` when given). Requests above the slot
|
|
2039
|
-
* size are chunked.
|
|
2040
|
-
* @param src - a COPY_SRC buffer
|
|
2041
|
-
* @param byteLength - bytes to read (a positive multiple of 4, within src)
|
|
2042
|
-
* @param dest - an optional destination view of at least byteLength bytes over a plain ArrayBuffer
|
|
2043
|
-
* @param srcOffset - the byte offset in src (default 0; a multiple of 4)
|
|
2044
|
-
* @returns the bytes read
|
|
2045
|
-
*/
|
|
2046
|
-
async read(src, byteLength, dest, srcOffset) {
|
|
2047
|
-
this.assertLive();
|
|
2048
|
-
const offset = srcOffset ?? 0;
|
|
2049
|
-
if (!Number.isInteger(byteLength) || byteLength <= 0 || byteLength % 4 !== 0) {
|
|
2050
|
-
throw invalid$2("byteLength", byteLength, "a positive multiple of 4");
|
|
2051
|
-
}
|
|
2052
|
-
if (!Number.isInteger(offset) || offset < 0 || offset % 4 !== 0) {
|
|
2053
|
-
throw invalid$2("srcOffset", offset, "a non-negative multiple of 4");
|
|
2054
|
-
}
|
|
2055
|
-
if (offset + byteLength > src.size) {
|
|
2056
|
-
throw invalid$2("byteLength", byteLength, `srcOffset + byteLength <= src.size (${src.size})`);
|
|
2057
|
-
}
|
|
2058
|
-
if ((src.usage & BufferUsage.COPY_SRC) === 0) {
|
|
2059
|
-
throw invalid$2("src", src.label, "a buffer with COPY_SRC usage");
|
|
2060
|
-
}
|
|
2061
|
-
let out;
|
|
2062
|
-
let outBytes;
|
|
2063
|
-
if (dest === void 0) {
|
|
2064
|
-
out = new ArrayBuffer(byteLength);
|
|
2065
|
-
outBytes = new Uint8Array(out);
|
|
2066
|
-
} else {
|
|
2067
|
-
if (isSharedBuffer$1(dest.buffer)) {
|
|
2068
|
-
throw invalid$2("dest", "SharedArrayBuffer", "a view over a plain ArrayBuffer");
|
|
2069
|
-
}
|
|
2070
|
-
if (dest.byteLength < byteLength) {
|
|
2071
|
-
throw invalid$2("dest", dest.byteLength, `at least ${byteLength} bytes`);
|
|
2072
|
-
}
|
|
2073
|
-
out = dest.buffer;
|
|
2074
|
-
outBytes = new Uint8Array(out, dest.byteOffset, byteLength);
|
|
2075
|
-
}
|
|
2076
|
-
let done = 0;
|
|
2077
|
-
while (done < byteLength) {
|
|
2078
|
-
const chunk = Math.min(this.slotBytes, byteLength - done);
|
|
2079
|
-
const slot = this.borrowSlot(chunk);
|
|
2080
|
-
const entry = this.ring[slot.index];
|
|
2081
|
-
try {
|
|
2082
|
-
const encoder = this.device.createCommandEncoder({ label: "readback" });
|
|
2083
|
-
encoder.copyBufferToBuffer(src, offset + done, slot.buffer, 0, chunk);
|
|
2084
|
-
this.device.queue.submit([encoder.finish()]);
|
|
2085
|
-
await this.map(entry, chunk);
|
|
2086
|
-
outBytes.set(new Uint8Array(slot.buffer.getMappedRange(0, chunk)), done);
|
|
2087
|
-
} finally {
|
|
2088
|
-
this.returnSlot(slot);
|
|
2089
|
-
}
|
|
2090
|
-
done += chunk;
|
|
2091
|
-
}
|
|
2092
|
-
return out;
|
|
2093
|
-
}
|
|
2094
|
-
/**
|
|
2095
|
-
* Reads one u32 counter through the same ring.
|
|
2096
|
-
* @param src - a COPY_SRC buffer
|
|
2097
|
-
* @param byteOffset - the counter's byte offset (a multiple of 4)
|
|
2098
|
-
* @returns the counter value
|
|
2099
|
-
*/
|
|
2100
|
-
async readU32(src, byteOffset) {
|
|
2101
|
-
const bytes = await this.read(src, 4, void 0, byteOffset);
|
|
2102
|
-
return new Uint32Array(bytes)[0];
|
|
2103
|
-
}
|
|
2104
|
-
/**
|
|
2105
|
-
* Borrows an unmapped slot of at least byteLength (grows the ring when none is free); the borrower ALWAYS
|
|
2106
|
-
* returns it (spec 4.4).
|
|
2107
|
-
* @param byteLength - the bytes the borrower will copy into the slot
|
|
2108
|
-
* @returns the slot
|
|
2109
|
-
*/
|
|
2110
|
-
borrowSlot(byteLength) {
|
|
2111
|
-
this.assertLive();
|
|
2112
|
-
if (!Number.isInteger(byteLength) || byteLength <= 0) {
|
|
2113
|
-
throw invalid$2("byteLength", byteLength, "a positive integer");
|
|
2114
|
-
}
|
|
2115
|
-
for (const entry2 of this.ring) {
|
|
2116
|
-
if (!entry2.borrowed && entry2.slot.capacity >= byteLength) {
|
|
2117
|
-
entry2.borrowed = true;
|
|
2118
|
-
return entry2.slot;
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
const capacity = Math.max(this.slotBytes, Math.ceil(byteLength / SLOT_GRANULE) * SLOT_GRANULE);
|
|
2122
|
-
const entry = this.addSlot(capacity);
|
|
2123
|
-
entry.borrowed = true;
|
|
2124
|
-
return entry.slot;
|
|
2125
|
-
}
|
|
2126
|
-
/**
|
|
2127
|
-
* mapAsync(READ) on a borrowed slot, tracked by the ring: the slot counts as borrowed, and is neither unmapped
|
|
2128
|
-
* nor destroyed, until the map settles (see the file comment). Returns the runtime's own promise, untranslated:
|
|
2129
|
-
* the borrower classifies a rejection (a CommandBatch races it against device loss, spec 5.7).
|
|
2130
|
-
* @param slot - a slot borrowed from this ring and not yet returned
|
|
2131
|
-
* @param byteLength - the bytes to map from offset 0
|
|
2132
|
-
* @returns the mapAsync promise
|
|
2133
|
-
*/
|
|
2134
|
-
mapSlot(slot, byteLength) {
|
|
2135
|
-
this.assertLive();
|
|
2136
|
-
const entry = this.borrowedEntry(slot);
|
|
2137
|
-
if (entry.mapPending || entry.mapped) {
|
|
2138
|
-
throw invalid$2("slot", slot.index, "a borrowed slot that is not mapped and has no map pending");
|
|
2139
|
-
}
|
|
2140
|
-
return this.track(entry, slot.buffer.mapAsync(MapMode.READ, 0, byteLength));
|
|
2141
|
-
}
|
|
2142
|
-
/**
|
|
2143
|
-
* Returns a borrowed slot (unmapping it if mapped). A slot whose map through the ring is still pending stays
|
|
2144
|
-
* borrowed until the map settles, when the ring unmaps and returns it; a borrower that maps a slot itself must
|
|
2145
|
-
* unmap it before returning it (spec 4.4; `mapState` decides when the runtime exposes it, otherwise the ring's
|
|
2146
|
-
* own record does). A no-op after destroyAll() (the ring is gone; a borrower's finally must never mask the
|
|
2147
|
-
* E_DISPOSED / E_DEVICE_LOST of the read it was serving).
|
|
2148
|
-
* @param slot - a slot borrowed from this ring and not yet returned
|
|
2149
|
-
*/
|
|
2150
|
-
returnSlot(slot) {
|
|
2151
|
-
if (this.disposed) {
|
|
2152
|
-
return;
|
|
2153
|
-
}
|
|
2154
|
-
const entry = this.borrowedEntry(slot);
|
|
2155
|
-
if (entry.mapPending) {
|
|
2156
|
-
entry.returnRequested = true;
|
|
2157
|
-
return;
|
|
2158
|
-
}
|
|
2159
|
-
this.finishReturn(entry);
|
|
2160
|
-
}
|
|
2161
|
-
/**
|
|
2162
|
-
* The entry of a slot borrowed from this ring and not yet returned.
|
|
2163
|
-
* @param slot - the slot
|
|
2164
|
-
* @returns its entry
|
|
2165
|
-
*/
|
|
2166
|
-
borrowedEntry(slot) {
|
|
2167
|
-
const entry = this.ring[slot.index];
|
|
2168
|
-
if (entry === void 0 || entry.slot !== slot || !entry.borrowed) {
|
|
2169
|
-
throw invalid$2("slot", slot.index, "a slot borrowed from this ring and not yet returned");
|
|
2170
|
-
}
|
|
2171
|
-
return entry;
|
|
2172
|
-
}
|
|
2173
|
-
/**
|
|
2174
|
-
* Unmaps a returned slot when it is mapped and frees it.
|
|
2175
|
-
* @param entry - the slot's entry (no map pending)
|
|
2176
|
-
*/
|
|
2177
|
-
finishReturn(entry) {
|
|
2178
|
-
const state = entry.slot.buffer.mapState;
|
|
2179
|
-
const mapped = state === void 0 ? entry.mapped : state !== "unmapped";
|
|
2180
|
-
if (mapped) {
|
|
2181
|
-
entry.slot.buffer.unmap();
|
|
2182
|
-
}
|
|
2183
|
-
entry.mapped = false;
|
|
2184
|
-
entry.borrowed = false;
|
|
2185
|
-
entry.returnRequested = false;
|
|
2186
|
-
}
|
|
2187
|
-
/**
|
|
2188
|
-
* Tracks one mapAsync on a slot: mapPending until it settles, then the deferred work -- the destruction
|
|
2189
|
-
* destroyAll() left to the settle, or the return returnSlot() left to it. The handler is attached before the
|
|
2190
|
-
* promise is handed out, so it runs before any borrower's continuation.
|
|
2191
|
-
* @param entry - the slot's entry
|
|
2192
|
-
* @param pending - the runtime's mapAsync promise
|
|
2193
|
-
* @returns the same promise
|
|
2194
|
-
*/
|
|
2195
|
-
track(entry, pending) {
|
|
2196
|
-
entry.mapPending = true;
|
|
2197
|
-
const settle = (mapped) => {
|
|
2198
|
-
entry.mapPending = false;
|
|
2199
|
-
entry.mapped = mapped;
|
|
2200
|
-
if (this.disposed) {
|
|
2201
|
-
if (mapped) {
|
|
2202
|
-
entry.slot.buffer.unmap();
|
|
2203
|
-
}
|
|
2204
|
-
this.destroySlot(entry);
|
|
2205
|
-
entry.mapped = false;
|
|
2206
|
-
return;
|
|
2207
|
-
}
|
|
2208
|
-
if (entry.returnRequested) {
|
|
2209
|
-
this.finishReturn(entry);
|
|
2210
|
-
}
|
|
2211
|
-
};
|
|
2212
|
-
void pending.then(
|
|
2213
|
-
() => {
|
|
2214
|
-
settle(true);
|
|
2215
|
-
},
|
|
2216
|
-
() => {
|
|
2217
|
-
settle(false);
|
|
2218
|
-
}
|
|
2219
|
-
);
|
|
2220
|
-
return pending;
|
|
2221
|
-
}
|
|
2222
|
-
/**
|
|
2223
|
-
* Number of slots (grows).
|
|
2224
|
-
* @returns the slot count
|
|
2225
|
-
*/
|
|
2226
|
-
get slots() {
|
|
2227
|
-
return this.ring.length;
|
|
2228
|
-
}
|
|
2229
|
-
/**
|
|
2230
|
-
* Slots currently borrowed.
|
|
2231
|
-
* @returns the borrowed count
|
|
2232
|
-
*/
|
|
2233
|
-
get borrowed() {
|
|
2234
|
-
let count = 0;
|
|
2235
|
-
for (const entry of this.ring) {
|
|
2236
|
-
if (entry.borrowed) {
|
|
2237
|
-
count++;
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
return count;
|
|
2241
|
-
}
|
|
2242
|
-
/**
|
|
2243
|
-
* Destroys every staging buffer (ctx.dispose()); idempotent. A slot whose mapAsync is still pending is NOT
|
|
2244
|
-
* destroyed here (the file comment: dawn-node 0.4.0 settles the promise twice); its destruction is deferred to
|
|
2245
|
-
* the moment the map settles (track()), where a pending read() still rejects with E_DISPOSED as the contract
|
|
2246
|
-
* promises.
|
|
2247
|
-
* @internal
|
|
2248
|
-
*/
|
|
2249
|
-
destroyAll() {
|
|
2250
|
-
for (const entry of this.ring) {
|
|
2251
|
-
if (entry.mapPending) {
|
|
2252
|
-
continue;
|
|
2253
|
-
}
|
|
2254
|
-
this.destroySlot(entry);
|
|
2255
|
-
}
|
|
2256
|
-
this.ring.length = 0;
|
|
2257
|
-
this.disposed = true;
|
|
2258
|
-
}
|
|
2259
|
-
/**
|
|
2260
|
-
* Destroys one slot's buffer through the allocator when it was created there.
|
|
2261
|
-
* @param entry - the slot
|
|
2262
|
-
*/
|
|
2263
|
-
destroySlot(entry) {
|
|
2264
|
-
if (entry.tracked) {
|
|
2265
|
-
this.allocator.destroy(entry.slot.buffer);
|
|
2266
|
-
} else {
|
|
2267
|
-
entry.slot.buffer.destroy();
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
/**
|
|
2271
|
-
* Creates one slot at the end of the ring.
|
|
2272
|
-
* @param capacity - the slot's byte capacity
|
|
2273
|
-
* @returns the new entry
|
|
2274
|
-
*/
|
|
2275
|
-
addSlot(capacity) {
|
|
2276
|
-
const index = this.ring.length;
|
|
2277
|
-
const descriptor = {
|
|
2278
|
-
label: `readback:slot:${index}`,
|
|
2279
|
-
size: capacity,
|
|
2280
|
-
usage: STAGING_USAGE
|
|
2281
|
-
};
|
|
2282
|
-
const tracked = capacity >= OOM_SCOPE_THRESHOLD_BYTES;
|
|
2283
|
-
const buffer = tracked ? this.allocator.createBuffer(descriptor) : this.device.createBuffer(descriptor);
|
|
2284
|
-
const entry = {
|
|
2285
|
-
slot: Object.freeze({ index, buffer, capacity }),
|
|
2286
|
-
borrowed: false,
|
|
2287
|
-
mapped: false,
|
|
2288
|
-
mapPending: false,
|
|
2289
|
-
returnRequested: false,
|
|
2290
|
-
tracked
|
|
2291
|
-
};
|
|
2292
|
-
this.ring.push(entry);
|
|
2293
|
-
return entry;
|
|
2294
|
-
}
|
|
2295
|
-
/**
|
|
2296
|
-
* mapAsync(READ) on a slot for read(); a rejection becomes E_DISPOSED (after destroyAll) or E_DEVICE_LOST.
|
|
2297
|
-
* @param entry - the ring entry of the borrowed slot
|
|
2298
|
-
* @param byteLength - the bytes to map
|
|
2299
|
-
*/
|
|
2300
|
-
async map(entry, byteLength) {
|
|
2301
|
-
let mapped = false;
|
|
2302
|
-
let failure = null;
|
|
2303
|
-
try {
|
|
2304
|
-
await this.track(entry, entry.slot.buffer.mapAsync(MapMode.READ, 0, byteLength));
|
|
2305
|
-
mapped = true;
|
|
2306
|
-
} catch (err) {
|
|
2307
|
-
failure = err;
|
|
2308
|
-
}
|
|
2309
|
-
if (this.disposed) {
|
|
2310
|
-
throw new WebGpuGraphError("E_DISPOSED", "the readback ring was disposed while a read was pending", {
|
|
2311
|
-
label: "readback"
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
if (!mapped) {
|
|
2315
|
-
const message = failure instanceof Error ? failure.message : String(failure);
|
|
2316
|
-
throw new WebGpuGraphError("E_DEVICE_LOST", `mapAsync rejected: ${message}`, {
|
|
2317
|
-
reason: "mapAsync",
|
|
2318
|
-
message
|
|
2319
|
-
});
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
/** Throws E_DISPOSED after destroyAll(). */
|
|
2323
|
-
assertLive() {
|
|
2324
|
-
if (this.disposed) {
|
|
2325
|
-
throw new WebGpuGraphError("E_DISPOSED", "the readback ring was disposed", { label: "readback" });
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
const CORE_ORDER$1 = Object.freeze(["rowPtr", "colIdx", "weights", "arcToEdge", "edgeToArc"]);
|
|
2330
|
-
const ARC_INDEXED$1 = /* @__PURE__ */ new Set(["colIdx", "weights", "arcToEdge"]);
|
|
2331
|
-
function coreByteLengths(s) {
|
|
2332
|
-
const arcs = s.arcCount;
|
|
2333
|
-
const identity = s.flags.arcToEdgeIsIdentity;
|
|
2334
|
-
return {
|
|
2335
|
-
rowPtr: 4 * (s.nodeCount + 1),
|
|
2336
|
-
colIdx: 4 * arcs,
|
|
2337
|
-
weights: s.flags.weighted && arcs > 0 ? 4 * arcs : 0,
|
|
2338
|
-
arcToEdge: !identity && arcs > 0 ? 4 * arcs : 0,
|
|
2339
|
-
edgeToArc: !identity && s.edgeCount > 0 ? 4 * s.edgeCount : 0
|
|
2340
|
-
};
|
|
2341
|
-
}
|
|
2342
|
-
function neededNames(need, lengths) {
|
|
2343
|
-
const wanted = new Set(need);
|
|
2344
|
-
wanted.add("rowPtr");
|
|
2345
|
-
return CORE_ORDER$1.filter((name) => wanted.has(name) && lengths[name] > 0);
|
|
2346
|
-
}
|
|
2347
|
-
function alignDown(value) {
|
|
2348
|
-
return value - value % ARC_WINDOW_ALIGN;
|
|
2349
|
-
}
|
|
2350
|
-
function tooLarge(needed, limit, path) {
|
|
2351
|
-
return new WebGpuGraphError("E_TOO_LARGE", `${path}: ${needed} bytes exceed the device limit of ${limit} bytes`, {
|
|
2352
|
-
needed,
|
|
2353
|
-
limit,
|
|
2354
|
-
path,
|
|
2355
|
-
algorithm: null
|
|
2356
|
-
});
|
|
2357
|
-
}
|
|
2358
|
-
function invalid$1(argument, value, expected) {
|
|
2359
|
-
return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument}: expected ${expected}`, {
|
|
2360
|
-
argument,
|
|
2361
|
-
value,
|
|
2362
|
-
expected
|
|
2363
|
-
});
|
|
2364
|
-
}
|
|
2365
|
-
function arcsPerWindowFor(limits) {
|
|
2366
|
-
return alignDown(Math.floor(limits.maxStorageBufferBindingSize / 4));
|
|
2367
|
-
}
|
|
2368
|
-
function planArcWindows(rowPtr, arcCount, arcsPerWindow) {
|
|
2369
|
-
if (rowPtr.length === 0) {
|
|
2370
|
-
throw invalid$1("rowPtr", rowPtr.length, "nodeCount + 1 entries");
|
|
2371
|
-
}
|
|
2372
|
-
if (!Number.isInteger(arcsPerWindow) || arcsPerWindow < ARC_WINDOW_ALIGN || arcsPerWindow % ARC_WINDOW_ALIGN !== 0) {
|
|
2373
|
-
throw invalid$1("arcsPerWindow", arcsPerWindow, `a positive multiple of ${ARC_WINDOW_ALIGN}`);
|
|
2374
|
-
}
|
|
2375
|
-
const n = rowPtr.length - 1;
|
|
2376
|
-
if (rowPtr[n] !== arcCount) {
|
|
2377
|
-
throw invalid$1("arcCount", arcCount, `rowPtr[${n}] === ${rowPtr[n]}`);
|
|
2378
|
-
}
|
|
2379
|
-
const windows = [];
|
|
2380
|
-
if (arcCount === 0) {
|
|
2381
|
-
return windows;
|
|
2382
|
-
}
|
|
2383
|
-
let pos = 0;
|
|
2384
|
-
let v = 0;
|
|
2385
|
-
let rowFirst = 0;
|
|
2386
|
-
while (pos < arcCount) {
|
|
2387
|
-
while (rowPtr[v + 1] <= pos) {
|
|
2388
|
-
v++;
|
|
2389
|
-
}
|
|
2390
|
-
const start = alignDown(pos);
|
|
2391
|
-
const limit = start + arcsPerWindow;
|
|
2392
|
-
if (rowPtr[v + 1] > limit) {
|
|
2393
|
-
windows.push({ start, end: limit, rowFirst, rowLast: v, bufferIndex: 0, offset: 4 * start });
|
|
2394
|
-
pos = limit;
|
|
2395
|
-
rowFirst = v;
|
|
2396
|
-
} else {
|
|
2397
|
-
let u = v;
|
|
2398
|
-
while (u + 1 < n && rowPtr[u + 2] <= limit) {
|
|
2399
|
-
u++;
|
|
2400
|
-
}
|
|
2401
|
-
const end = rowPtr[u + 1];
|
|
2402
|
-
windows.push({ start, end, rowFirst, rowLast: u, bufferIndex: 0, offset: 4 * start });
|
|
2403
|
-
pos = end;
|
|
2404
|
-
rowFirst = u + 1;
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
return windows;
|
|
2408
|
-
}
|
|
2409
|
-
function placeWindows(windows, arcBytes, bufferLimit) {
|
|
2410
|
-
if (arcBytes <= bufferLimit) {
|
|
2411
|
-
return { buffers: [{ byteOffset: 0, byteLength: arcBytes }], placed: [...windows] };
|
|
2412
|
-
}
|
|
2413
|
-
const buffers = [];
|
|
2414
|
-
const placed = [];
|
|
2415
|
-
let first = 0;
|
|
2416
|
-
while (first < windows.length) {
|
|
2417
|
-
let last = first;
|
|
2418
|
-
while (last + 1 < windows.length && 4 * (windows[last + 1].end - windows[first].start) <= bufferLimit) {
|
|
2419
|
-
last++;
|
|
2420
|
-
}
|
|
2421
|
-
const base = windows[first].start;
|
|
2422
|
-
const bufferIndex = buffers.length;
|
|
2423
|
-
buffers.push({ byteOffset: 4 * base, byteLength: 4 * (windows[last].end - base) });
|
|
2424
|
-
for (let i = first; i <= last; i++) {
|
|
2425
|
-
const w = windows[i];
|
|
2426
|
-
placed.push({
|
|
2427
|
-
start: w.start,
|
|
2428
|
-
end: w.end,
|
|
2429
|
-
rowFirst: w.rowFirst,
|
|
2430
|
-
rowLast: w.rowLast,
|
|
2431
|
-
bufferIndex,
|
|
2432
|
-
offset: 4 * (w.start - base)
|
|
2433
|
-
});
|
|
2434
|
-
}
|
|
2435
|
-
first = last + 1;
|
|
2436
|
-
}
|
|
2437
|
-
return { buffers, placed };
|
|
2438
|
-
}
|
|
2439
|
-
function planUpload(s, caps, need) {
|
|
2440
|
-
const { limits } = caps;
|
|
2441
|
-
const binding = limits.maxStorageBufferBindingSize;
|
|
2442
|
-
const bufferLimit = limits.maxBufferSize;
|
|
2443
|
-
const fits = (bytes) => bytes <= binding && bytes <= bufferLimit;
|
|
2444
|
-
const violated = (bytes) => bytes > binding ? binding : bufferLimit;
|
|
2445
|
-
const lengths = coreByteLengths(s);
|
|
2446
|
-
const names = neededNames(need, lengths);
|
|
2447
|
-
if (!fits(lengths.rowPtr)) {
|
|
2448
|
-
throw tooLarge(lengths.rowPtr, violated(lengths.rowPtr), "rowPtr");
|
|
2449
|
-
}
|
|
2450
|
-
const { arena } = s;
|
|
2451
|
-
if (arena !== null) {
|
|
2452
|
-
const coldNeeded = names.some(
|
|
2453
|
-
(name) => (name === "arcToEdge" || name === "edgeToArc") && arena.segments[name] !== null
|
|
2454
|
-
);
|
|
2455
|
-
const includesCold = coldNeeded && arena.byteLength <= bufferLimit;
|
|
2456
|
-
const bytes = includesCold ? arena.byteLength : arena.hotByteLength;
|
|
2457
|
-
const bindable = names.every((name) => {
|
|
2458
|
-
const segment = arena.segments[name];
|
|
2459
|
-
return segment === null || fits(segment.byteLength);
|
|
2460
|
-
});
|
|
2461
|
-
if (bytes <= bufferLimit && bindable) {
|
|
2462
|
-
const segmentOf = (name) => {
|
|
2463
|
-
const segment = arena.segments[name];
|
|
2464
|
-
if (segment === null) {
|
|
2465
|
-
return null;
|
|
2466
|
-
}
|
|
2467
|
-
const offset = segment.byteOffset - arena.byteOffset;
|
|
2468
|
-
return offset + segment.byteLength <= bytes ? { offset, size: segment.byteLength } : null;
|
|
2469
|
-
};
|
|
2470
|
-
return {
|
|
2471
|
-
kind: "arena",
|
|
2472
|
-
bytes,
|
|
2473
|
-
includesCold,
|
|
2474
|
-
segments: {
|
|
2475
|
-
rowPtr: segmentOf("rowPtr"),
|
|
2476
|
-
colIdx: segmentOf("colIdx"),
|
|
2477
|
-
weights: segmentOf("weights"),
|
|
2478
|
-
arcToEdge: segmentOf("arcToEdge"),
|
|
2479
|
-
edgeToArc: segmentOf("edgeToArc")
|
|
2480
|
-
}
|
|
2481
|
-
};
|
|
2482
|
-
}
|
|
2483
|
-
}
|
|
2484
|
-
if (names.every((name) => fits(lengths[name]))) {
|
|
2485
|
-
return {
|
|
2486
|
-
kind: "perArray",
|
|
2487
|
-
arrays: names.map((name) => ({
|
|
2488
|
-
name,
|
|
2489
|
-
byteLength: lengths[name],
|
|
2490
|
-
buffers: [{ byteOffset: 0, byteLength: lengths[name] }]
|
|
2491
|
-
}))
|
|
2492
|
-
};
|
|
2493
|
-
}
|
|
2494
|
-
if (names.includes("edgeToArc") && !fits(lengths.edgeToArc)) {
|
|
2495
|
-
throw tooLarge(lengths.edgeToArc, violated(lengths.edgeToArc), "binding");
|
|
2496
|
-
}
|
|
2497
|
-
const arcsPerWindow = Math.min(arcsPerWindowFor(limits), alignDown(Math.floor(bufferLimit / 4)));
|
|
2498
|
-
if (arcsPerWindow < ARC_WINDOW_ALIGN) {
|
|
2499
|
-
throw tooLarge(4 * ARC_WINDOW_ALIGN, binding, "binding");
|
|
2500
|
-
}
|
|
2501
|
-
const windows = planArcWindows(s.rowPtr, s.arcCount, arcsPerWindow);
|
|
2502
|
-
const { buffers, placed } = placeWindows(windows, 4 * s.arcCount, bufferLimit);
|
|
2503
|
-
const arrays = names.map(
|
|
2504
|
-
(name) => ARC_INDEXED$1.has(name) ? { name, byteLength: lengths[name], buffers } : { name, byteLength: lengths[name], buffers: [{ byteOffset: 0, byteLength: lengths[name] }] }
|
|
2505
|
-
);
|
|
2506
|
-
return { kind: "windowed", arrays, windows: placed, arcsPerWindow };
|
|
2507
|
-
}
|
|
2508
|
-
const DEFAULT_NEED = Object.freeze(["rowPtr", "colIdx", "weights"]);
|
|
2509
|
-
const CORE_ORDER = Object.freeze(["rowPtr", "colIdx", "weights", "arcToEdge", "edgeToArc"]);
|
|
2510
|
-
const ARC_INDEXED = Object.freeze(["colIdx", "weights", "arcToEdge"]);
|
|
2511
|
-
const RESIDENT_USAGE = BufferUsage.STORAGE | BufferUsage.COPY_DST | BufferUsage.COPY_SRC;
|
|
2512
|
-
function isArcIndexed(name) {
|
|
2513
|
-
return ARC_INDEXED.includes(name);
|
|
2514
|
-
}
|
|
2515
|
-
function isSharedBuffer(buffer) {
|
|
2516
|
-
return Object.prototype.toString.call(buffer) === "[object SharedArrayBuffer]";
|
|
2517
|
-
}
|
|
2518
|
-
function invalid(argument, value, expected) {
|
|
2519
|
-
return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument}: expected ${expected}`, {
|
|
2520
|
-
argument,
|
|
2521
|
-
value,
|
|
2522
|
-
expected
|
|
2523
|
-
});
|
|
2524
|
-
}
|
|
2525
|
-
function assertAttached(s) {
|
|
2526
|
-
if (s.detached) {
|
|
2527
|
-
throw new WebGpuGraphError("E_SNAPSHOT", `snapshot ${s.serial} is detached (its core was transferred away)`, {
|
|
2528
|
-
reason: "detached",
|
|
2529
|
-
serial: s.serial
|
|
2530
|
-
});
|
|
2531
|
-
}
|
|
2532
|
-
}
|
|
2533
|
-
function isPresent(s, name) {
|
|
2534
|
-
switch (name) {
|
|
2535
|
-
case "rowPtr":
|
|
2536
|
-
return true;
|
|
2537
|
-
case "colIdx":
|
|
2538
|
-
return s.arcCount > 0;
|
|
2539
|
-
case "weights":
|
|
2540
|
-
return s.flags.weighted && s.arcCount > 0;
|
|
2541
|
-
case "arcToEdge":
|
|
2542
|
-
return !s.flags.arcToEdgeIsIdentity && s.arcCount > 0;
|
|
2543
|
-
case "edgeToArc":
|
|
2544
|
-
return !s.flags.arcToEdgeIsIdentity && s.edgeCount > 0;
|
|
2545
|
-
default:
|
|
2546
|
-
return false;
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
function coreArray(s, name) {
|
|
2550
|
-
switch (name) {
|
|
2551
|
-
case "rowPtr":
|
|
2552
|
-
return s.rowPtr;
|
|
2553
|
-
case "colIdx":
|
|
2554
|
-
return s.colIdx;
|
|
2555
|
-
case "weights": {
|
|
2556
|
-
const { weights } = s;
|
|
2557
|
-
if (weights === null) {
|
|
2558
|
-
throw invalid("weights", null, "a weighted snapshot");
|
|
2559
|
-
}
|
|
2560
|
-
return weights;
|
|
2561
|
-
}
|
|
2562
|
-
case "arcToEdge":
|
|
2563
|
-
return s.arcToEdge;
|
|
2564
|
-
case "edgeToArc":
|
|
2565
|
-
return s.edgeToArc;
|
|
2566
|
-
default:
|
|
2567
|
-
throw invalid("name", name, "a core array name");
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
class GraphResidency {
|
|
2571
|
-
/**
|
|
2572
|
-
* Creates an empty residency over a device.
|
|
2573
|
-
* @param device - the device (queue.writeBuffer)
|
|
2574
|
-
* @param caps - the device capabilities the planner reads
|
|
2575
|
-
* @param allocator - the context's OOM-scoped allocator (every buffer is created and destroyed through it)
|
|
2576
|
-
* @param options - the once-only warning
|
|
2577
|
-
* @param options.warnUnreleasedSnapshots - the resident-snapshot count above which the warning fires once
|
|
2578
|
-
* @param options.warn - the warning sink; default console.warn
|
|
2579
|
-
*/
|
|
2580
|
-
constructor(device, caps, allocator, options) {
|
|
2581
|
-
this.residents = /* @__PURE__ */ new WeakMap();
|
|
2582
|
-
this.records = /* @__PURE__ */ new WeakMap();
|
|
2583
|
-
this.bySerial = /* @__PURE__ */ new Map();
|
|
2584
|
-
this.orphans = /* @__PURE__ */ new Set();
|
|
2585
|
-
this.tombstones = /* @__PURE__ */ new Set();
|
|
2586
|
-
this.liveCount = 0;
|
|
2587
|
-
this.liveBytesValue = 0;
|
|
2588
|
-
this.warned = false;
|
|
2589
|
-
this.disposed = false;
|
|
2590
|
-
this.device = device;
|
|
2591
|
-
this.caps = caps;
|
|
2592
|
-
this.allocator = allocator;
|
|
2593
|
-
this.warnUnreleasedSnapshots = options.warnUnreleasedSnapshots;
|
|
2594
|
-
this.warn = options.warn ?? ((message) => {
|
|
2595
|
-
console.warn(message);
|
|
2596
|
-
});
|
|
2597
|
-
}
|
|
2598
|
-
/**
|
|
2599
|
-
* Uploads (or finds) the core; `need` defaults to ["rowPtr", "colIdx", "weights"]; cold segments on demand
|
|
2600
|
-
* (spec 4.2). Never materialises an identity permutation. A windowed plan (spec 4.2, PD-8) uploads every
|
|
2601
|
-
* arc-indexed array as the plan's buffer ranges and binds window 0 as its default binding; rowPtr and edgeToArc
|
|
2602
|
-
* stay whole. A tombstoned serial is lifted and re-uploaded (PLAN DECISION 7).
|
|
2603
|
-
* @param s - the snapshot
|
|
2604
|
-
* @param need - the core arrays to bind (rowPtr is always included; absent arrays are ignored)
|
|
2605
|
-
* @returns the core binding (a frozen object; grows as cold segments are added)
|
|
2606
|
-
*/
|
|
2607
|
-
core(s, need) {
|
|
2608
|
-
this.assertLive();
|
|
2609
|
-
assertAttached(s);
|
|
2610
|
-
const wanted = need ?? DEFAULT_NEED;
|
|
2611
|
-
const names = CORE_ORDER.filter((name) => (name === "rowPtr" || wanted.includes(name)) && isPresent(s, name));
|
|
2612
|
-
const plan = planUpload(s, this.caps, names);
|
|
2613
|
-
const record = this.ensureRecord(s);
|
|
2614
|
-
if (record.plan === null) {
|
|
2615
|
-
record.plan = plan.kind;
|
|
2616
|
-
if (plan.kind === "arena" && s.arena !== null) {
|
|
2617
|
-
const { arena } = s;
|
|
2618
|
-
const resident = this.upload(
|
|
2619
|
-
record,
|
|
2620
|
-
s.rowPtr,
|
|
2621
|
-
new Uint8Array(arena.buffer, arena.byteOffset, plan.bytes),
|
|
2622
|
-
`residency:core:${record.serial}:arena`
|
|
2623
|
-
);
|
|
2624
|
-
record.arena = { buffer: resident.buffer, segments: plan.segments };
|
|
2625
|
-
}
|
|
2626
|
-
if (plan.kind === "windowed") {
|
|
2627
|
-
record.windows = plan.windows;
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
for (const name of names) {
|
|
2631
|
-
if (!record.bindings.has(name)) {
|
|
2632
|
-
const planned = plan.kind === "windowed" ? plan.arrays.find((a) => a.name === name) : void 0;
|
|
2633
|
-
record.bindings.set(
|
|
2634
|
-
name,
|
|
2635
|
-
planned !== void 0 && planned.buffers.length > 0 && record.windows !== null && isArcIndexed(name) ? this.bindWindowed(record, s, name, planned.buffers, record.windows[0]) : this.bindCore(record, s, name)
|
|
2636
|
-
);
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
const rowPtr = record.bindings.get("rowPtr");
|
|
2640
|
-
if (rowPtr === void 0) {
|
|
2641
|
-
throw invalid("rowPtr", null, "a bound rowPtr (internal invariant)");
|
|
2642
|
-
}
|
|
2643
|
-
const weights = record.bindings.get("weights") ?? null;
|
|
2644
|
-
return Object.freeze({
|
|
2645
|
-
serial: record.serial,
|
|
2646
|
-
plan: plan.kind,
|
|
2647
|
-
rowPtr,
|
|
2648
|
-
colIdx: record.bindings.get("colIdx") ?? null,
|
|
2649
|
-
weights,
|
|
2650
|
-
arcToEdge: record.bindings.get("arcToEdge") ?? null,
|
|
2651
|
-
edgeToArc: record.bindings.get("edgeToArc") ?? null,
|
|
2652
|
-
windows: record.windows,
|
|
2653
|
-
arcBuffers: record.windows === null ? null : Object.freeze({
|
|
2654
|
-
colIdx: Object.freeze([...record.arcBuffers.get("colIdx") ?? []]),
|
|
2655
|
-
weights: Object.freeze([...record.arcBuffers.get("weights") ?? []]),
|
|
2656
|
-
arcToEdge: Object.freeze([...record.arcBuffers.get("arcToEdge") ?? []])
|
|
2657
|
-
}),
|
|
2658
|
-
hasWeights: weights !== null
|
|
2659
|
-
});
|
|
2660
|
-
}
|
|
2661
|
-
/**
|
|
2662
|
-
* Uploads (or finds) a view: outDegree / inDegree / degreeOrder / reverseDegreeOrder upload one array each;
|
|
2663
|
-
* reverse and edgeList (P7) upload their arrays perArray, never into the arena, and are memoised per record so
|
|
2664
|
-
* a second call uploads nothing (spec 4.3). coo uploads its per-arc `src` (P11; the rest aliases the core); mate ->
|
|
2665
|
-
* E_UNSUPPORTED. packViews concatenates a
|
|
2666
|
-
* reverse or edgeList view into ONE buffer at STORAGE_ALIGN offsets; on any other view `true` is E_UNSUPPORTED
|
|
2667
|
-
* { option: "packViews" }.
|
|
2668
|
-
* @param s - the snapshot
|
|
2669
|
-
* @param name - the view
|
|
2670
|
-
* @param options - view options
|
|
2671
|
-
* @param options.packViews - pack the reverse / edgeList arrays into one buffer (a no-op on an undirected reverse)
|
|
2672
|
-
* @returns the view binding
|
|
2673
|
-
*/
|
|
2674
|
-
view(s, name, options) {
|
|
2675
|
-
this.assertLive();
|
|
2676
|
-
assertAttached(s);
|
|
2677
|
-
const packed = options?.packViews === true;
|
|
2678
|
-
if (name === "reverse" || name === "edgeList") {
|
|
2679
|
-
this.assertNotReleased(s);
|
|
2680
|
-
this.assertNonEmpty(s);
|
|
2681
|
-
const memoKey = packed ? `${name}:packed` : name;
|
|
2682
|
-
const record2 = this.ensureRecord(s);
|
|
2683
|
-
const memo = record2.views.get(memoKey);
|
|
2684
|
-
if (memo !== void 0) {
|
|
2685
|
-
return memo;
|
|
2686
|
-
}
|
|
2687
|
-
const built = name === "reverse" ? this.buildReverse(s, record2, packed) : this.buildEdgeList(s, record2, packed);
|
|
2688
|
-
record2.views.set(memoKey, built);
|
|
2689
|
-
return built;
|
|
2690
|
-
}
|
|
2691
|
-
if (packed) {
|
|
2692
|
-
throw new WebGpuGraphError("E_UNSUPPORTED", "packViews applies to the reverse and edgeList views only", {
|
|
2693
|
-
option: "packViews",
|
|
2694
|
-
hint: `the ${name} view uploads one array`
|
|
2695
|
-
});
|
|
2696
|
-
}
|
|
2697
|
-
let array;
|
|
2698
|
-
let bindingName;
|
|
2699
|
-
let scalars = {};
|
|
2700
|
-
switch (name) {
|
|
2701
|
-
case "outDegree":
|
|
2702
|
-
this.assertNotReleased(s);
|
|
2703
|
-
this.assertNonEmpty(s);
|
|
2704
|
-
array = s.outDegree();
|
|
2705
|
-
bindingName = "outDegree";
|
|
2706
|
-
break;
|
|
2707
|
-
case "inDegree":
|
|
2708
|
-
this.assertNotReleased(s);
|
|
2709
|
-
this.assertNonEmpty(s);
|
|
2710
|
-
array = s.inDegree();
|
|
2711
|
-
bindingName = "inDegree";
|
|
2712
|
-
break;
|
|
2713
|
-
case "degreeOrder": {
|
|
2714
|
-
this.assertNotReleased(s);
|
|
2715
|
-
this.assertNonEmpty(s);
|
|
2716
|
-
const order = s.degreeOrder();
|
|
2717
|
-
array = order.perm;
|
|
2718
|
-
bindingName = "perm";
|
|
2719
|
-
scalars = { segmentOffsets: Array.from(order.segmentOffsets) };
|
|
2720
|
-
break;
|
|
2721
|
-
}
|
|
2722
|
-
case "reverseDegreeOrder": {
|
|
2723
|
-
this.assertNotReleased(s);
|
|
2724
|
-
this.assertNonEmpty(s);
|
|
2725
|
-
const order = s.degreeOrder({ of: "reverse" });
|
|
2726
|
-
array = order.perm;
|
|
2727
|
-
bindingName = "perm";
|
|
2728
|
-
scalars = { segmentOffsets: Array.from(order.segmentOffsets) };
|
|
2729
|
-
break;
|
|
2730
|
-
}
|
|
2731
|
-
case "coo":
|
|
2732
|
-
this.assertNotReleased(s);
|
|
2733
|
-
this.assertNonEmpty(s);
|
|
2734
|
-
if (s.arcCount === 0) {
|
|
2735
|
-
return Object.freeze({ view: name, bindings: Object.freeze({}), scalars: Object.freeze({}) });
|
|
2736
|
-
}
|
|
2737
|
-
array = s.coo().src;
|
|
2738
|
-
bindingName = "src";
|
|
2739
|
-
break;
|
|
2740
|
-
case "mate":
|
|
2741
|
-
throw new WebGpuGraphError("E_UNSUPPORTED", "the mate view is not uploaded: no kernel reads it", {
|
|
2742
|
-
feature: "view:mate",
|
|
2743
|
-
hint: "outDegree, inDegree, degreeOrder, reverseDegreeOrder, coo, reverse and edgeList are uploaded"
|
|
2744
|
-
});
|
|
2745
|
-
default:
|
|
2746
|
-
throw invalid("name", name, "a view name");
|
|
2747
|
-
}
|
|
2748
|
-
const record = this.ensureRecord(s);
|
|
2749
|
-
const resident = this.upload(record, array, array, `residency:view:${record.serial}:${name}`);
|
|
2750
|
-
const binding = { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
|
|
2751
|
-
return Object.freeze({
|
|
2752
|
-
view: name,
|
|
2753
|
-
bindings: Object.freeze({ [bindingName]: binding }),
|
|
2754
|
-
scalars: Object.freeze(scalars)
|
|
2755
|
-
});
|
|
2756
|
-
}
|
|
2757
|
-
/**
|
|
2758
|
-
* The reverse adjacency's bindings. On an UNDIRECTED snapshot graph-format invariant I7 makes reverse() return
|
|
2759
|
-
* the FORWARD arrays, so the core bindings ARE the reverse bindings and nothing is uploaded -- delegating to
|
|
2760
|
-
* core() rather than re-uploading the arrays is what makes that true on the arena plan as well, where the
|
|
2761
|
-
* per-array WeakMap key resolves to the whole-arena resident (spec 4.3 lines 1182-1186). `fwdArc` is never
|
|
2762
|
-
* uploaded (spec 13 row P7). `packed` is ignored when undirected: there is nothing to pack.
|
|
2763
|
-
* @param s - the snapshot
|
|
2764
|
-
* @param record - its residency record
|
|
2765
|
-
* @param packed - concatenate the arrays into one buffer (directed only)
|
|
2766
|
-
* @returns the view binding
|
|
2767
|
-
*/
|
|
2768
|
-
buildReverse(s, record, packed) {
|
|
2769
|
-
const rev = s.reverse();
|
|
2770
|
-
if (!s.directed) {
|
|
2771
|
-
const core = this.core(s, ["rowPtr", "colIdx", "weights"]);
|
|
2772
|
-
if (core.plan === "windowed") {
|
|
2773
|
-
throw new WebGpuGraphError(
|
|
2774
|
-
"E_TOO_LARGE",
|
|
2775
|
-
`snapshot ${s.serial}: the undirected reverse view is the core, which needs arc windows (${core.windows?.length ?? 0}) that no view executes`,
|
|
2776
|
-
{
|
|
2777
|
-
needed: 4 * s.arcCount,
|
|
2778
|
-
limit: this.caps.limits.maxStorageBufferBindingSize,
|
|
2779
|
-
path: "windowed",
|
|
2780
|
-
algorithm: null
|
|
2781
|
-
}
|
|
2782
|
-
);
|
|
2783
|
-
}
|
|
2784
|
-
const bindings2 = { rowPtr: core.rowPtr };
|
|
2785
|
-
if (core.colIdx !== null) {
|
|
2786
|
-
bindings2.colIdx = core.colIdx;
|
|
2787
|
-
}
|
|
2788
|
-
if (core.weights !== null) {
|
|
2789
|
-
bindings2.weights = core.weights;
|
|
2790
|
-
}
|
|
2791
|
-
return Object.freeze({
|
|
2792
|
-
view: "reverse",
|
|
2793
|
-
bindings: Object.freeze(bindings2),
|
|
2794
|
-
scalars: Object.freeze({ arcCount: [rev.arcCount], directed: [0] })
|
|
2795
|
-
});
|
|
2796
|
-
}
|
|
2797
|
-
const arrays = [
|
|
2798
|
-
["rowPtr", rev.rowPtr],
|
|
2799
|
-
["colIdx", rev.colIdx]
|
|
2800
|
-
];
|
|
2801
|
-
if (rev.weights !== null) {
|
|
2802
|
-
arrays.push(["weights", rev.weights]);
|
|
2803
|
-
}
|
|
2804
|
-
const bindings = packed ? this.packArrays(
|
|
2805
|
-
record,
|
|
2806
|
-
arrays,
|
|
2807
|
-
this.packKey(record, "reverse:packed"),
|
|
2808
|
-
`residency:view:${record.serial}:reverse:packed`
|
|
2809
|
-
) : this.separateArrays(record, arrays, `residency:view:${record.serial}:reverse`);
|
|
2810
|
-
return Object.freeze({
|
|
2811
|
-
view: "reverse",
|
|
2812
|
-
bindings,
|
|
2813
|
-
scalars: Object.freeze({ arcCount: [rev.arcCount], directed: [1] })
|
|
2814
|
-
});
|
|
2815
|
-
}
|
|
2816
|
-
/**
|
|
2817
|
-
* The each-edge-once binding an edge-parallel kernel uses on directed and undirected snapshots alike (spec 8.1
|
|
2818
|
-
* row 2); `arc` is not uploaded (no kernel of P7 reads it).
|
|
2819
|
-
* @param s - the snapshot
|
|
2820
|
-
* @param record - its residency record
|
|
2821
|
-
* @param packed - concatenate into one buffer
|
|
2822
|
-
* @returns the view binding
|
|
2823
|
-
*/
|
|
2824
|
-
buildEdgeList(s, record, packed) {
|
|
2825
|
-
const list = s.edgeList();
|
|
2826
|
-
const arrays = [
|
|
2827
|
-
["src", list.src],
|
|
2828
|
-
["dst", list.dst]
|
|
2829
|
-
];
|
|
2830
|
-
if (list.weights !== null) {
|
|
2831
|
-
arrays.push(["weights", list.weights]);
|
|
2832
|
-
}
|
|
2833
|
-
const bindings = packed ? this.packArrays(
|
|
2834
|
-
record,
|
|
2835
|
-
arrays,
|
|
2836
|
-
this.packKey(record, "edgeList:packed"),
|
|
2837
|
-
`residency:view:${record.serial}:edgeList:packed`
|
|
2838
|
-
) : this.separateArrays(record, arrays, `residency:view:${record.serial}:edgeList`);
|
|
2839
|
-
return Object.freeze({
|
|
2840
|
-
view: "edgeList",
|
|
2841
|
-
bindings,
|
|
2842
|
-
scalars: Object.freeze({ edgeCount: [s.edgeCount] })
|
|
2843
|
-
});
|
|
2844
|
-
}
|
|
2845
|
-
/**
|
|
2846
|
-
* One resident per array (spec 4.3: views upload in perArray mode, never into the arena). An empty array (the
|
|
2847
|
-
* colIdx of an edgeless directed reverse view, the src / dst of an edgeless edgeList) is skipped: spec 5.6 never
|
|
2848
|
-
* uploads a zero-length array, and Kernel.bind rejects a zero-size binding, so it is absent as in core().
|
|
2849
|
-
* @param record - the owning record
|
|
2850
|
-
* @param arrays - the named arrays
|
|
2851
|
-
* @param label - the buffer label prefix
|
|
2852
|
-
* @returns the bindings by name
|
|
2853
|
-
*/
|
|
2854
|
-
separateArrays(record, arrays, label) {
|
|
2855
|
-
const bindings = {};
|
|
2856
|
-
for (const [name, array] of arrays) {
|
|
2857
|
-
if (array.byteLength === 0) {
|
|
2858
|
-
continue;
|
|
2859
|
-
}
|
|
2860
|
-
const resident = this.upload(record, array, array, `${label}:${name}`);
|
|
2861
|
-
bindings[name] = { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
|
|
2862
|
-
}
|
|
2863
|
-
return Object.freeze(bindings);
|
|
2864
|
-
}
|
|
2865
|
-
/**
|
|
2866
|
-
* The arrays concatenated into ONE buffer at STORAGE_ALIGN-aligned offsets (spec 4.3 packViews): one
|
|
2867
|
-
* createBuffer and one writeBuffer instead of three of each, which is what the option buys. The resident is
|
|
2868
|
-
* keyed on `key`, a marker object owned by the record (packKey), NOT on one of the snapshot's arrays: upload()
|
|
2869
|
-
* returns an existing resident whenever the key matches and the serial matches, without comparing byte
|
|
2870
|
-
* lengths, so keying the packed buffer on `rev.rowPtr` would make the packed and the unpacked view of one
|
|
2871
|
-
* snapshot collide -- whichever was built second would get the other's buffer. The record still owns the
|
|
2872
|
-
* resident, so release(s) destroys it with the rest. Offsets are STORAGE_ALIGN-aligned because Kernel.bind
|
|
2873
|
-
* rejects any other offset synchronously (E_INVALID_ARGUMENT { argument: "offset" }). Empty arrays are left
|
|
2874
|
-
* out as in separateArrays; when nothing is left, nothing is uploaded.
|
|
2875
|
-
* @param record - the owning record
|
|
2876
|
-
* @param all - the named arrays, in buffer order
|
|
2877
|
-
* @param key - the marker object the resident is keyed on
|
|
2878
|
-
* @param label - the buffer label
|
|
2879
|
-
* @returns the bindings by name, all into the one buffer
|
|
2880
|
-
*/
|
|
2881
|
-
packArrays(record, all, key, label) {
|
|
2882
|
-
const arrays = all.filter(([, array]) => array.byteLength > 0);
|
|
2883
|
-
if (arrays.length === 0) {
|
|
2884
|
-
return Object.freeze({});
|
|
2885
|
-
}
|
|
2886
|
-
const offsets = [];
|
|
2887
|
-
let total = 0;
|
|
2888
|
-
for (const [, array] of arrays) {
|
|
2889
|
-
offsets.push(total);
|
|
2890
|
-
total += Math.ceil(array.byteLength / STORAGE_ALIGN) * STORAGE_ALIGN;
|
|
2891
|
-
}
|
|
2892
|
-
const staging = new Uint8Array(total);
|
|
2893
|
-
arrays.forEach(([, array], i) => {
|
|
2894
|
-
staging.set(new Uint8Array(array.buffer, array.byteOffset, array.byteLength), offsets[i]);
|
|
2895
|
-
});
|
|
2896
|
-
const resident = this.upload(record, key, staging, label);
|
|
2897
|
-
const bindings = {};
|
|
2898
|
-
arrays.forEach(([name, array], i) => {
|
|
2899
|
-
bindings[name] = { buffer: resident.buffer, offset: offsets[i], size: array.byteLength, window: null };
|
|
2900
|
-
});
|
|
2901
|
-
return Object.freeze(bindings);
|
|
2902
|
-
}
|
|
2903
|
-
/**
|
|
2904
|
-
* The upload key of a packed view: a marker object allocated once per record and memo name, never one of the
|
|
2905
|
-
* snapshot's arrays.
|
|
2906
|
-
* @param record - the owning record
|
|
2907
|
-
* @param memoKey - the `views` memo key of this packed view
|
|
2908
|
-
* @returns the stable marker object
|
|
2909
|
-
*/
|
|
2910
|
-
packKey(record, memoKey) {
|
|
2911
|
-
const existing = record.packKeys.get(memoKey);
|
|
2912
|
-
if (existing !== void 0) {
|
|
2913
|
-
return existing;
|
|
2914
|
-
}
|
|
2915
|
-
const created = {};
|
|
2916
|
-
record.packKeys.set(memoKey, created);
|
|
2917
|
-
return created;
|
|
2918
|
-
}
|
|
2919
|
-
/**
|
|
2920
|
-
* gpuView(name) + column.version; re-uploads in place when the version changed and the byte length did not
|
|
2921
|
-
* (spec 4.3). CONTRACT DECISION: `owner` is required so release(owner) can find the buffer (a table has no
|
|
2922
|
-
* back-reference to its snapshot). The key is the Column object (PLAN DECISION 8). E_GPU_INELIGIBLE and
|
|
2923
|
-
* E_UNKNOWN_COLUMN from the format pass through unchanged.
|
|
2924
|
-
* @param table - the table holding the column
|
|
2925
|
-
* @param name - the column name
|
|
2926
|
-
* @param owner - the snapshot the buffer is recorded against
|
|
2927
|
-
* @returns the column binding
|
|
2928
|
-
*/
|
|
2929
|
-
column(table, name, owner) {
|
|
2930
|
-
this.assertLive();
|
|
2931
|
-
assertAttached(owner);
|
|
2932
|
-
this.assertNotReleased(owner);
|
|
2933
|
-
const column = table.require(name);
|
|
2934
|
-
const data = table.gpuView(name);
|
|
2935
|
-
if (data.byteLength === 0) {
|
|
2936
|
-
throw invalid("column", name, "a column with at least one row (zero-length arrays are never uploaded)");
|
|
2937
|
-
}
|
|
2938
|
-
const record = this.ensureRecord(owner);
|
|
2939
|
-
const label = `residency:column:${record.serial}:${column.meta.domain}.${name}`;
|
|
2940
|
-
const existing = this.residents.get(column);
|
|
2941
|
-
let resident;
|
|
2942
|
-
if (existing !== void 0 && !existing.destroyed && existing.serial === record.serial && existing.byteLength === data.byteLength) {
|
|
2943
|
-
resident = existing;
|
|
2944
|
-
if (resident.version !== column.version) {
|
|
2945
|
-
this.device.queue.writeBuffer(resident.buffer, 0, data);
|
|
2946
|
-
resident.version = column.version;
|
|
2947
|
-
}
|
|
2948
|
-
} else {
|
|
2949
|
-
if (existing !== void 0) {
|
|
2950
|
-
this.residents.delete(column);
|
|
2951
|
-
}
|
|
2952
|
-
resident = this.upload(record, column, data, label);
|
|
2953
|
-
resident.version = column.version;
|
|
2954
|
-
}
|
|
2955
|
-
return Object.freeze({
|
|
2956
|
-
binding: { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null },
|
|
2957
|
-
column,
|
|
2958
|
-
version: resident.version,
|
|
2959
|
-
eligibility: column.gpu,
|
|
2960
|
-
components: column.meta.components
|
|
2961
|
-
});
|
|
2962
|
-
}
|
|
2963
|
-
/**
|
|
2964
|
-
* Any format array keyed on the object; registered against `owner` when given (spec 4.1).
|
|
2965
|
-
* @param key - the array (a non-empty, 4-byte-multiple array over a plain ArrayBuffer)
|
|
2966
|
-
* @param label - the buffer label suffix
|
|
2967
|
-
* @param owner - the snapshot to record the buffer against, or undefined for a caller-owned buffer
|
|
2968
|
-
* @returns the array binding; `destroy()` is idempotent (PLAN DECISION 12)
|
|
2969
|
-
*/
|
|
2970
|
-
array(key, label, owner) {
|
|
2971
|
-
this.assertLive();
|
|
2972
|
-
if (key.byteLength === 0 || key.byteLength % 4 !== 0) {
|
|
2973
|
-
throw invalid("key", key.byteLength, "a non-empty array whose byteLength is a multiple of 4");
|
|
2974
|
-
}
|
|
2975
|
-
if (isSharedBuffer(key.buffer)) {
|
|
2976
|
-
throw invalid("key", "SharedArrayBuffer", "an array over a plain ArrayBuffer");
|
|
2977
|
-
}
|
|
2978
|
-
let record = null;
|
|
2979
|
-
if (owner !== void 0) {
|
|
2980
|
-
assertAttached(owner);
|
|
2981
|
-
this.assertNotReleased(owner);
|
|
2982
|
-
record = this.ensureRecord(owner);
|
|
2983
|
-
}
|
|
2984
|
-
const resident = this.upload(record, key, key, `residency:array:${label}`);
|
|
2985
|
-
const binding = { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
|
|
2986
|
-
return {
|
|
2987
|
-
binding,
|
|
2988
|
-
byteLength: resident.byteLength,
|
|
2989
|
-
owner: owner ?? null,
|
|
2990
|
-
destroy: () => {
|
|
2991
|
-
this.destroyResident(resident, record);
|
|
2992
|
-
}
|
|
2993
|
-
};
|
|
2994
|
-
}
|
|
2995
|
-
/**
|
|
2996
|
-
* Destroys every buffer recorded for s.serial (siblings included, Q-27) and tombstones the serial; idempotent
|
|
2997
|
-
* (spec 4.5); safe on a snapshot never uploaded (nothing to tombstone, PLAN DECISION 7).
|
|
2998
|
-
* @param s - the snapshot (or any withColumns() sibling)
|
|
2999
|
-
*/
|
|
3000
|
-
release(s) {
|
|
3001
|
-
const record = this.bySerial.get(s.serial);
|
|
3002
|
-
if (record === void 0) {
|
|
3003
|
-
return;
|
|
3004
|
-
}
|
|
3005
|
-
for (const resident of [...record.entries]) {
|
|
3006
|
-
this.destroyResident(resident, record);
|
|
3007
|
-
}
|
|
3008
|
-
this.forget(record);
|
|
3009
|
-
this.tombstones.add(s.serial);
|
|
3010
|
-
}
|
|
3011
|
-
/** Drops every record WITHOUT destroying (device lost: the buffers are gone). @internal */
|
|
3012
|
-
clearOnLoss() {
|
|
3013
|
-
for (const record of [...this.bySerial.values()]) {
|
|
3014
|
-
for (const resident of record.entries) {
|
|
3015
|
-
resident.destroyed = true;
|
|
3016
|
-
if (this.residents.get(resident.key) === resident) {
|
|
3017
|
-
this.residents.delete(resident.key);
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
3020
|
-
record.entries.length = 0;
|
|
3021
|
-
this.forget(record);
|
|
3022
|
-
}
|
|
3023
|
-
for (const resident of this.orphans) {
|
|
3024
|
-
resident.destroyed = true;
|
|
3025
|
-
if (this.residents.get(resident.key) === resident) {
|
|
3026
|
-
this.residents.delete(resident.key);
|
|
3027
|
-
}
|
|
3028
|
-
}
|
|
3029
|
-
this.orphans.clear();
|
|
3030
|
-
this.liveCount = 0;
|
|
3031
|
-
this.liveBytesValue = 0;
|
|
3032
|
-
}
|
|
3033
|
-
/** Destroys everything (ctx.dispose()); idempotent. @internal */
|
|
3034
|
-
destroyAll() {
|
|
3035
|
-
for (const record of [...this.bySerial.values()]) {
|
|
3036
|
-
for (const resident of [...record.entries]) {
|
|
3037
|
-
this.destroyResident(resident, record);
|
|
3038
|
-
}
|
|
3039
|
-
this.forget(record);
|
|
3040
|
-
}
|
|
3041
|
-
for (const resident of [...this.orphans]) {
|
|
3042
|
-
this.destroyResident(resident, null);
|
|
3043
|
-
}
|
|
3044
|
-
this.disposed = true;
|
|
3045
|
-
}
|
|
3046
|
-
/**
|
|
3047
|
-
* The resident buffers, bytes and snapshots (spec 4.1).
|
|
3048
|
-
* @returns the statistics
|
|
3049
|
-
*/
|
|
3050
|
-
stats() {
|
|
3051
|
-
const perSnapshot = [...this.bySerial.values()].map((record) => ({
|
|
3052
|
-
serial: record.serial,
|
|
3053
|
-
label: record.label,
|
|
3054
|
-
bytes: record.entries.reduce((sum, resident) => sum + resident.byteLength, 0),
|
|
3055
|
-
buffers: record.entries.length
|
|
3056
|
-
}));
|
|
3057
|
-
return { buffers: this.liveCount, bytes: this.liveBytesValue, snapshots: this.bySerial.size, perSnapshot };
|
|
3058
|
-
}
|
|
3059
|
-
/**
|
|
3060
|
-
* Bytes of every resident buffer (graft: C R-18).
|
|
3061
|
-
* @returns the resident byte count
|
|
3062
|
-
*/
|
|
3063
|
-
get residentBytes() {
|
|
3064
|
-
return this.liveBytesValue;
|
|
3065
|
-
}
|
|
3066
|
-
/**
|
|
3067
|
-
* True when the serial was released and not re-uploaded. @internal
|
|
3068
|
-
* @param serial - a snapshot serial
|
|
3069
|
-
* @returns whether a live user holding bindings of that serial must stop
|
|
3070
|
-
*/
|
|
3071
|
-
isReleased(serial) {
|
|
3072
|
-
return this.tombstones.has(serial);
|
|
3073
|
-
}
|
|
3074
|
-
/**
|
|
3075
|
-
* Binds one core array: a segment of the arena buffer when the buffer holds it, else its own buffer keyed on
|
|
3076
|
-
* the array object (the cold-segment-on-demand path of spec 4.2, and the whole perArray path).
|
|
3077
|
-
* @param record - the snapshot's record
|
|
3078
|
-
* @param s - the snapshot
|
|
3079
|
-
* @param name - a PRESENT core array
|
|
3080
|
-
* @returns the binding
|
|
3081
|
-
*/
|
|
3082
|
-
bindCore(record, s, name) {
|
|
3083
|
-
if (record.arena !== null) {
|
|
3084
|
-
const segment = record.arena.segments[name];
|
|
3085
|
-
if (segment !== null) {
|
|
3086
|
-
return { buffer: record.arena.buffer, offset: segment.offset, size: segment.size, window: null };
|
|
3087
|
-
}
|
|
3088
|
-
}
|
|
3089
|
-
const array = coreArray(s, name);
|
|
3090
|
-
const resident = this.upload(record, array, array, `residency:core:${record.serial}:${name}`);
|
|
3091
|
-
return { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
|
|
3092
|
-
}
|
|
3093
|
-
/**
|
|
3094
|
-
* Uploads one arc-indexed array as the windowed plan's buffer ranges (spec 4.2: an array above maxBufferSize is
|
|
3095
|
-
* split across buffers at window boundaries), each keyed on a marker object of the record (windowKey), and
|
|
3096
|
-
* returns window 0's binding so a caller that ignores windows still binds something valid (PD-8).
|
|
3097
|
-
* @param record - the snapshot's record
|
|
3098
|
-
* @param s - the snapshot
|
|
3099
|
-
* @param name - a PRESENT arc-indexed core array
|
|
3100
|
-
* @param ranges - the plan's buffer ranges of the array
|
|
3101
|
-
* @param first - window 0
|
|
3102
|
-
* @returns the binding of window 0
|
|
3103
|
-
*/
|
|
3104
|
-
bindWindowed(record, s, name, ranges, first) {
|
|
3105
|
-
const array = coreArray(s, name);
|
|
3106
|
-
const buffers = ranges.map(
|
|
3107
|
-
(range, index) => this.upload(
|
|
3108
|
-
record,
|
|
3109
|
-
this.windowKey(record, `${name}:${index}`),
|
|
3110
|
-
new Uint8Array(array.buffer, array.byteOffset + range.byteOffset, range.byteLength),
|
|
3111
|
-
`residency:core:${record.serial}:${name}:w${index}`
|
|
3112
|
-
).buffer
|
|
3113
|
-
);
|
|
3114
|
-
record.arcBuffers.set(name, buffers);
|
|
3115
|
-
return {
|
|
3116
|
-
buffer: buffers[first.bufferIndex],
|
|
3117
|
-
offset: first.offset,
|
|
3118
|
-
size: 4 * (first.end - first.start),
|
|
3119
|
-
window: first
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
|
-
/**
|
|
3123
|
-
* The upload key of one window buffer: a marker object allocated once per record and `<name>:<bufferIndex>`.
|
|
3124
|
-
* @param record - the owning record
|
|
3125
|
-
* @param memoKey - `<name>:<bufferIndex>`
|
|
3126
|
-
* @returns the stable marker object
|
|
3127
|
-
*/
|
|
3128
|
-
windowKey(record, memoKey) {
|
|
3129
|
-
const existing = record.windowKeys.get(memoKey);
|
|
3130
|
-
if (existing !== void 0) {
|
|
3131
|
-
return existing;
|
|
3132
|
-
}
|
|
3133
|
-
const created = {};
|
|
3134
|
-
record.windowKeys.set(memoKey, created);
|
|
3135
|
-
return created;
|
|
3136
|
-
}
|
|
3137
|
-
/**
|
|
3138
|
-
* Uploads `data` into a new buffer keyed on `key`, or returns the resident already uploaded for that key by
|
|
3139
|
-
* the same owner (PLAN DECISION 11: a resident belongs to one record).
|
|
3140
|
-
* @param record - the owning record, or null for an owner-less array
|
|
3141
|
-
* @param key - the object the buffer is keyed on
|
|
3142
|
-
* @param data - the bytes (byteLength a multiple of 4, graph-format invariant I10; a view over a plain
|
|
3143
|
-
* ArrayBuffer, which is what writeBuffer's GPUAllowSharedBufferSource accepts under @webgpu/types 0.1.72)
|
|
3144
|
-
* @param label - the buffer label
|
|
3145
|
-
* @returns the resident
|
|
3146
|
-
*/
|
|
3147
|
-
upload(record, key, data, label) {
|
|
3148
|
-
const serial = record === null ? null : record.serial;
|
|
3149
|
-
const existing = this.residents.get(key);
|
|
3150
|
-
if (existing !== void 0 && !existing.destroyed && existing.serial === serial) {
|
|
3151
|
-
return existing;
|
|
3152
|
-
}
|
|
3153
|
-
const buffer = this.allocator.createBuffer({ label, size: data.byteLength, usage: RESIDENT_USAGE });
|
|
3154
|
-
this.device.queue.writeBuffer(buffer, 0, data);
|
|
3155
|
-
const resident = { key, buffer, byteLength: data.byteLength, serial, version: 0, destroyed: false };
|
|
3156
|
-
this.residents.set(key, resident);
|
|
3157
|
-
if (record === null) {
|
|
3158
|
-
this.orphans.add(resident);
|
|
3159
|
-
} else {
|
|
3160
|
-
record.entries.push(resident);
|
|
3161
|
-
}
|
|
3162
|
-
this.liveCount++;
|
|
3163
|
-
this.liveBytesValue += data.byteLength;
|
|
3164
|
-
return resident;
|
|
3165
|
-
}
|
|
3166
|
-
/**
|
|
3167
|
-
* Destroys one resident buffer and unregisters it; idempotent.
|
|
3168
|
-
* @param resident - the resident
|
|
3169
|
-
* @param record - its owning record, or null for an orphan
|
|
3170
|
-
*/
|
|
3171
|
-
destroyResident(resident, record) {
|
|
3172
|
-
if (resident.destroyed) {
|
|
3173
|
-
return;
|
|
3174
|
-
}
|
|
3175
|
-
resident.destroyed = true;
|
|
3176
|
-
this.allocator.destroy(resident.buffer);
|
|
3177
|
-
if (this.residents.get(resident.key) === resident) {
|
|
3178
|
-
this.residents.delete(resident.key);
|
|
3179
|
-
}
|
|
3180
|
-
if (record === null) {
|
|
3181
|
-
this.orphans.delete(resident);
|
|
3182
|
-
} else {
|
|
3183
|
-
const index = record.entries.indexOf(resident);
|
|
3184
|
-
if (index >= 0) {
|
|
3185
|
-
record.entries.splice(index, 1);
|
|
3186
|
-
}
|
|
3187
|
-
}
|
|
3188
|
-
this.liveCount--;
|
|
3189
|
-
this.liveBytesValue -= resident.byteLength;
|
|
3190
|
-
}
|
|
3191
|
-
/**
|
|
3192
|
-
* Marks a record released and drops it from the serial map (the snapshot WeakMap entries recognise it by the flag).
|
|
3193
|
-
* @param record - the record
|
|
3194
|
-
*/
|
|
3195
|
-
forget(record) {
|
|
3196
|
-
record.released = true;
|
|
3197
|
-
record.bindings.clear();
|
|
3198
|
-
record.views.clear();
|
|
3199
|
-
record.packKeys.clear();
|
|
3200
|
-
record.windowKeys.clear();
|
|
3201
|
-
record.arcBuffers.clear();
|
|
3202
|
-
record.windows = null;
|
|
3203
|
-
record.arena = null;
|
|
3204
|
-
record.plan = null;
|
|
3205
|
-
this.bySerial.delete(record.serial);
|
|
3206
|
-
}
|
|
3207
|
-
/**
|
|
3208
|
-
* The record of a snapshot's serial, created when absent (which lifts a tombstone and may fire the warning).
|
|
3209
|
-
* @param s - the snapshot
|
|
3210
|
-
* @returns the record
|
|
3211
|
-
*/
|
|
3212
|
-
ensureRecord(s) {
|
|
3213
|
-
let record = this.records.get(s);
|
|
3214
|
-
if (record !== void 0 && record.released) {
|
|
3215
|
-
record = void 0;
|
|
3216
|
-
}
|
|
3217
|
-
if (record === void 0) {
|
|
3218
|
-
record = this.bySerial.get(s.serial);
|
|
3219
|
-
}
|
|
3220
|
-
if (record === void 0) {
|
|
3221
|
-
record = {
|
|
3222
|
-
serial: s.serial,
|
|
3223
|
-
label: s.label,
|
|
3224
|
-
entries: [],
|
|
3225
|
-
plan: null,
|
|
3226
|
-
arena: null,
|
|
3227
|
-
bindings: /* @__PURE__ */ new Map(),
|
|
3228
|
-
views: /* @__PURE__ */ new Map(),
|
|
3229
|
-
packKeys: /* @__PURE__ */ new Map(),
|
|
3230
|
-
windowKeys: /* @__PURE__ */ new Map(),
|
|
3231
|
-
windows: null,
|
|
3232
|
-
arcBuffers: /* @__PURE__ */ new Map(),
|
|
3233
|
-
released: false
|
|
3234
|
-
};
|
|
3235
|
-
this.bySerial.set(s.serial, record);
|
|
3236
|
-
this.tombstones.delete(s.serial);
|
|
3237
|
-
if (!this.warned && this.bySerial.size > this.warnUnreleasedSnapshots) {
|
|
3238
|
-
this.warned = true;
|
|
3239
|
-
this.warn(
|
|
3240
|
-
`[webgpu-graph-algorithms] ${this.bySerial.size} snapshots are resident on the device (warnUnreleasedSnapshots = ${this.warnUnreleasedSnapshots}); call ctx.release(snapshot) for superseded snapshots -- GPU memory is never freed by garbage collection (spec 4.1)`
|
|
3241
|
-
);
|
|
3242
|
-
}
|
|
3243
|
-
}
|
|
3244
|
-
this.records.set(s, record);
|
|
3245
|
-
return record;
|
|
3246
|
-
}
|
|
3247
|
-
/**
|
|
3248
|
-
* Throws E_RELEASED for a tombstoned serial (a bind without a fresh core()).
|
|
3249
|
-
* @param s - the snapshot
|
|
3250
|
-
*/
|
|
3251
|
-
assertNotReleased(s) {
|
|
3252
|
-
if (this.tombstones.has(s.serial)) {
|
|
3253
|
-
throw new WebGpuGraphError(
|
|
3254
|
-
"E_RELEASED",
|
|
3255
|
-
`snapshot ${s.serial} was released; upload it again through core() before binding views, columns or arrays`,
|
|
3256
|
-
{ serial: s.serial }
|
|
3257
|
-
);
|
|
3258
|
-
}
|
|
3259
|
-
}
|
|
3260
|
-
/**
|
|
3261
|
-
* Throws E_INVALID_ARGUMENT for an empty snapshot (a view of it would be a zero-length array, spec 5.6).
|
|
3262
|
-
* @param s - the snapshot
|
|
3263
|
-
*/
|
|
3264
|
-
assertNonEmpty(s) {
|
|
3265
|
-
if (s.nodeCount === 0) {
|
|
3266
|
-
throw invalid("snapshot", 0, "nodeCount > 0 (an empty snapshot has no per-node view to upload)");
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
/** Throws E_DISPOSED after destroyAll() (PLAN DECISION 13). */
|
|
3270
|
-
assertLive() {
|
|
3271
|
-
if (this.disposed) {
|
|
3272
|
-
throw new WebGpuGraphError("E_DISPOSED", "the residency was disposed", { label: "residency" });
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
}
|
|
3276
|
-
const DEFAULT_LABEL = "webgpu-graph-algorithms";
|
|
3277
|
-
const DEFAULT_OPTIONAL_FEATURES = Object.freeze(["subgroups", "timestamp-query"]);
|
|
3278
|
-
const DEFAULT_POWER_PREFERENCE = "high-performance";
|
|
3279
|
-
async function adapterOf(options) {
|
|
3280
|
-
if (options.adapter !== void 0) {
|
|
3281
|
-
return options.adapter;
|
|
3282
|
-
}
|
|
3283
|
-
if (options.gpu !== void 0) {
|
|
3284
|
-
return requestAdapter(options.gpu, options.powerPreference ?? DEFAULT_POWER_PREFERENCE);
|
|
3285
|
-
}
|
|
3286
|
-
throw new WebGpuGraphError("E_NO_WEBGPU", "GpuContext.create needs one of gpu, adapter or device", {
|
|
3287
|
-
reason: "no gpu, adapter or device given",
|
|
3288
|
-
hint: "pass navigator.gpu (browser) or the gpu of createNodeGpu() (Node), or use the ./browser and ./node entries"
|
|
3289
|
-
});
|
|
3290
|
-
}
|
|
3291
|
-
class GpuContext {
|
|
3292
|
-
/**
|
|
3293
|
-
* Steps 5 and 6 of create() (spec 2.2): the sink, the loss watcher and the singletons.
|
|
3294
|
-
* @param init - the device, its caps and the create-time choices
|
|
3295
|
-
*/
|
|
3296
|
-
constructor(init) {
|
|
3297
|
-
this.currentState = "ready";
|
|
3298
|
-
this.lostError = null;
|
|
3299
|
-
this.slot = new PendingErrorSlot();
|
|
3300
|
-
this.lostListeners = /* @__PURE__ */ new Set();
|
|
3301
|
-
this.disposers = [];
|
|
3302
|
-
this.batchCounter = 0;
|
|
3303
|
-
this.device = init.device;
|
|
3304
|
-
this.caps = init.caps;
|
|
3305
|
-
this.ownsDevice = init.ownsDevice;
|
|
3306
|
-
this.label = init.label;
|
|
3307
|
-
this.debug = { inspect: false };
|
|
3308
|
-
this.workgroupSize = workgroupSizeFor(init.caps);
|
|
3309
|
-
this.allocator = new AllocationTracker(init.device);
|
|
3310
|
-
this.residency = new GraphResidency(init.device, init.caps, this.allocator, {
|
|
3311
|
-
warnUnreleasedSnapshots: init.warnUnreleasedSnapshots
|
|
3312
|
-
});
|
|
3313
|
-
this.pool = new BufferPool(init.device, this.allocator, init.caps.limits.maxBufferSize);
|
|
3314
|
-
this.readback = new Readback(init.device, this.allocator);
|
|
3315
|
-
this.pipelines = new PipelineCache(init.device, init.caps);
|
|
3316
|
-
this.profiler = init.device.features.has("timestamp-query") ? new Profiler(init.device, true, init.caps.runtime !== "node") : null;
|
|
3317
|
-
this.uninstallSink = installUncapturedErrorSink(init.device, this.slot, init.onError);
|
|
3318
|
-
this.lost = watchDeviceLost(init.device, (info) => {
|
|
3319
|
-
this.handleLost(info);
|
|
3320
|
-
});
|
|
3321
|
-
}
|
|
3322
|
-
/**
|
|
3323
|
-
* Probe without creating a device (spec 2.2); never throws. `gpu === undefined` -> E_NO_WEBGPU;
|
|
3324
|
-
* requestAdapter() null or a throw -> E_NO_ADAPTER; `rejectSoftware && software` -> E_SOFTWARE_ONLY (the
|
|
3325
|
-
* adapter and its summary are still reported); else OK with the UNUSED adapter and its summary.
|
|
3326
|
-
* @param options - the GPU object, the power preference and the software policy
|
|
3327
|
-
* @returns the probe result
|
|
3328
|
-
*/
|
|
3329
|
-
static async probe(options) {
|
|
3330
|
-
const { gpu } = options;
|
|
3331
|
-
if (gpu === void 0) {
|
|
3332
|
-
return {
|
|
3333
|
-
ok: false,
|
|
3334
|
-
code: "E_NO_WEBGPU",
|
|
3335
|
-
reason: "no GPU object: navigator.gpu is undefined (no WebGPU in this runtime)",
|
|
3336
|
-
adapter: null,
|
|
3337
|
-
summary: null
|
|
3338
|
-
};
|
|
3339
|
-
}
|
|
3340
|
-
let adapter;
|
|
3341
|
-
try {
|
|
3342
|
-
adapter = await requestAdapter(gpu, options.powerPreference ?? DEFAULT_POWER_PREFERENCE);
|
|
3343
|
-
} catch (err) {
|
|
3344
|
-
const reason = isWebGpuGraphError(err) ? err.message : String(err);
|
|
3345
|
-
return { ok: false, code: "E_NO_ADAPTER", reason, adapter: null, summary: null };
|
|
3346
|
-
}
|
|
3347
|
-
const summary = summarizeAdapter(adapter);
|
|
3348
|
-
if (options.rejectSoftware === true && summary.software) {
|
|
3349
|
-
return {
|
|
3350
|
-
ok: false,
|
|
3351
|
-
code: "E_SOFTWARE_ONLY",
|
|
3352
|
-
reason: `software adapter rejected: ${summary.vendor}/${summary.architecture}`,
|
|
3353
|
-
adapter,
|
|
3354
|
-
summary
|
|
3355
|
-
};
|
|
3356
|
-
}
|
|
3357
|
-
return { ok: true, code: "OK", reason: null, adapter, summary };
|
|
3358
|
-
}
|
|
3359
|
-
/**
|
|
3360
|
-
* The six-step acquisition of spec 2.2: adopt `device`, else take `adapter`, else request one from `gpu`
|
|
3361
|
-
* (step 1); the software test (step 2); the limits / features and requestDevice (step 3); the caps from
|
|
3362
|
-
* device.limits plus assertPlanLimits (step 4); the sink, the loss watcher and the singletons (steps 5-6,
|
|
3363
|
-
* in the constructor).
|
|
3364
|
-
* @param options - see GpuContextOptions
|
|
3365
|
-
* @returns the context
|
|
3366
|
-
*/
|
|
3367
|
-
static async create(options) {
|
|
3368
|
-
const runtime = options.runtime ?? "unknown";
|
|
3369
|
-
const label = options.label ?? DEFAULT_LABEL;
|
|
3370
|
-
const onError = options.onError ?? null;
|
|
3371
|
-
const warnUnreleasedSnapshots = options.warnUnreleasedSnapshots ?? DEFAULT_WARN_UNRELEASED_SNAPSHOTS;
|
|
3372
|
-
if (options.device !== void 0) {
|
|
3373
|
-
const adopted = capsFromDevice(options.device, { runtime });
|
|
3374
|
-
assertPlanLimits(adopted);
|
|
3375
|
-
return new GpuContext({
|
|
3376
|
-
device: options.device,
|
|
3377
|
-
caps: adopted,
|
|
3378
|
-
ownsDevice: false,
|
|
3379
|
-
label,
|
|
3380
|
-
onError,
|
|
3381
|
-
warnUnreleasedSnapshots
|
|
3382
|
-
});
|
|
3383
|
-
}
|
|
3384
|
-
const adapter = await adapterOf(options);
|
|
3385
|
-
const summary = summarizeAdapter(adapter);
|
|
3386
|
-
if (options.rejectSoftware === true && summary.software) {
|
|
3387
|
-
throw new WebGpuGraphError(
|
|
3388
|
-
"E_SOFTWARE_ONLY",
|
|
3389
|
-
`software adapter rejected by rejectSoftware: ${summary.vendor}/${summary.architecture}`,
|
|
3390
|
-
{ adapter: summary }
|
|
3391
|
-
);
|
|
3392
|
-
}
|
|
3393
|
-
const requiredLimits = buildRequiredLimits(adapter, options.limits ?? "raise");
|
|
3394
|
-
const requiredFeatures = buildRequiredFeatures(
|
|
3395
|
-
adapter,
|
|
3396
|
-
options.requiredFeatures ?? [],
|
|
3397
|
-
options.optionalFeatures ?? DEFAULT_OPTIONAL_FEATURES
|
|
3398
|
-
);
|
|
3399
|
-
const device = await requestDevice(adapter, { label, requiredLimits, requiredFeatures });
|
|
3400
|
-
const caps = captureCaps(device, adapter.info, runtime, options.gpu?.wgslLanguageFeatures ?? []);
|
|
3401
|
-
try {
|
|
3402
|
-
assertPlanLimits(caps);
|
|
3403
|
-
} catch (err) {
|
|
3404
|
-
device.destroy();
|
|
3405
|
-
throw err;
|
|
3406
|
-
}
|
|
3407
|
-
return new GpuContext({ device, caps, ownsDevice: true, label, onError, warnUnreleasedSnapshots });
|
|
3408
|
-
}
|
|
3409
|
-
/**
|
|
3410
|
-
* Zero-cost adoption of a device the caller owns (ownsDevice false, runtime "unknown" unless info says
|
|
3411
|
-
* otherwise). The context is labelled with the device's label, or "GpuContext.from" when it has none.
|
|
3412
|
-
* @param device - the device to adopt
|
|
3413
|
-
* @param info - what the caller knows about the adapter, if anything
|
|
3414
|
-
* @returns the context
|
|
3415
|
-
*/
|
|
3416
|
-
static from(device, info) {
|
|
3417
|
-
const caps = capsFromDevice(device, info);
|
|
3418
|
-
assertPlanLimits(caps);
|
|
3419
|
-
const deviceLabel = device.label ?? "";
|
|
3420
|
-
return new GpuContext({
|
|
3421
|
-
device,
|
|
3422
|
-
caps,
|
|
3423
|
-
ownsDevice: false,
|
|
3424
|
-
label: deviceLabel === "" ? "GpuContext.from" : deviceLabel,
|
|
3425
|
-
onError: null,
|
|
3426
|
-
warnUnreleasedSnapshots: DEFAULT_WARN_UNRELEASED_SNAPSHOTS
|
|
3427
|
-
});
|
|
3428
|
-
}
|
|
3429
|
-
/**
|
|
3430
|
-
* "ready" | "lost" | "disposed".
|
|
3431
|
-
* @returns the lifecycle state
|
|
3432
|
-
*/
|
|
3433
|
-
get state() {
|
|
3434
|
-
return this.currentState;
|
|
3435
|
-
}
|
|
3436
|
-
/**
|
|
3437
|
-
* Throws E_DEVICE_LOST / E_DISPOSED by state and rethrows a pending uncaptured error (spec 5.7: "thrown
|
|
3438
|
-
* from the next public call").
|
|
3439
|
-
* @internal
|
|
3440
|
-
*/
|
|
3441
|
-
assertReady() {
|
|
3442
|
-
if (this.currentState === "disposed") {
|
|
3443
|
-
throw new WebGpuGraphError("E_DISPOSED", `GpuContext "${this.label}" is disposed`, { label: this.label });
|
|
3444
|
-
}
|
|
3445
|
-
if (this.lostError !== null) {
|
|
3446
|
-
throw this.lostError;
|
|
3447
|
-
}
|
|
3448
|
-
const pending = this.slot.take();
|
|
3449
|
-
if (pending !== null) {
|
|
3450
|
-
throw pending;
|
|
3451
|
-
}
|
|
3452
|
-
}
|
|
3453
|
-
/**
|
|
3454
|
-
* Takes the pending uncaptured error (used by CommandBatch right after submit).
|
|
3455
|
-
* @internal
|
|
3456
|
-
* @returns the error, or null
|
|
3457
|
-
*/
|
|
3458
|
-
takePendingError() {
|
|
3459
|
-
return this.slot.take();
|
|
3460
|
-
}
|
|
3461
|
-
/**
|
|
3462
|
-
* Monotonically increasing batch ids, starting at 1.
|
|
3463
|
-
* @internal
|
|
3464
|
-
* @returns the next id
|
|
3465
|
-
*/
|
|
3466
|
-
nextBatchId() {
|
|
3467
|
-
this.batchCounter += 1;
|
|
3468
|
-
return this.batchCounter;
|
|
3469
|
-
}
|
|
3470
|
-
/**
|
|
3471
|
-
* Registers a device-loss listener (simulations enter "disposed"); returns the unregister function.
|
|
3472
|
-
* @internal
|
|
3473
|
-
* @param listener - called once with the lost info
|
|
3474
|
-
* @returns removes the listener
|
|
3475
|
-
*/
|
|
3476
|
-
onLost(listener) {
|
|
3477
|
-
this.lostListeners.add(listener);
|
|
3478
|
-
return () => {
|
|
3479
|
-
this.lostListeners.delete(listener);
|
|
3480
|
-
};
|
|
3481
|
-
}
|
|
3482
|
-
/**
|
|
3483
|
-
* A disposer run by dispose() (the Node entry attaches the GPU handle's dispose); attached after
|
|
3484
|
-
* dispose() it runs immediately.
|
|
3485
|
-
* @internal
|
|
3486
|
-
* @param dispose - the disposer
|
|
3487
|
-
*/
|
|
3488
|
-
attachDisposer(dispose) {
|
|
3489
|
-
if (this.currentState === "disposed") {
|
|
3490
|
-
dispose();
|
|
3491
|
-
return;
|
|
3492
|
-
}
|
|
3493
|
-
this.disposers.push(dispose);
|
|
3494
|
-
}
|
|
3495
|
-
/**
|
|
3496
|
-
* Destroys every buffer recorded for the snapshot (spec 4.5), then `pool.trim()` (spec 4.4: idle scratch
|
|
3497
|
-
* goes with the graph); idempotent; safe on a snapshot never uploaded; a no-op after dispose().
|
|
3498
|
-
* @param snapshot - the snapshot whose buffers are released
|
|
3499
|
-
*/
|
|
3500
|
-
release(snapshot) {
|
|
3501
|
-
if (this.currentState === "disposed") {
|
|
3502
|
-
return;
|
|
3503
|
-
}
|
|
3504
|
-
this.residency.release(snapshot);
|
|
3505
|
-
this.pool.trim();
|
|
3506
|
-
}
|
|
3507
|
-
/**
|
|
3508
|
-
* Rejects pending work with E_DISPOSED (every later public call), destroys residency, pool and staging
|
|
3509
|
-
* ring, destroys the device when owned, runs attached disposers; idempotent (spec 2.8).
|
|
3510
|
-
*/
|
|
3511
|
-
dispose() {
|
|
3512
|
-
if (this.currentState === "disposed") {
|
|
3513
|
-
return;
|
|
3514
|
-
}
|
|
3515
|
-
this.currentState = "disposed";
|
|
3516
|
-
this.uninstallSink();
|
|
3517
|
-
this.residency.destroyAll();
|
|
3518
|
-
this.pool.destroyAll();
|
|
3519
|
-
this.readback.destroyAll();
|
|
3520
|
-
if (this.profiler !== null) {
|
|
3521
|
-
this.profiler.destroy();
|
|
3522
|
-
}
|
|
3523
|
-
if (this.ownsDevice) {
|
|
3524
|
-
this.device.destroy();
|
|
3525
|
-
}
|
|
3526
|
-
const disposers = this.disposers.splice(0);
|
|
3527
|
-
for (const dispose of disposers) {
|
|
3528
|
-
dispose();
|
|
3529
|
-
}
|
|
3530
|
-
}
|
|
3531
|
-
/**
|
|
3532
|
-
* Step 5 of spec 2.2 on loss: state "lost", the residency cleared without destroying (the buffers are
|
|
3533
|
-
* gone with the device), every registered listener run once; a loss reported after dispose() (the owned
|
|
3534
|
-
* device was destroyed by dispose) changes nothing.
|
|
3535
|
-
* @param info - the device.lost result
|
|
3536
|
-
*/
|
|
3537
|
-
handleLost(info) {
|
|
3538
|
-
if (this.currentState === "disposed") {
|
|
3539
|
-
return;
|
|
3540
|
-
}
|
|
3541
|
-
this.currentState = "lost";
|
|
3542
|
-
this.lostError = deviceLostError(info);
|
|
3543
|
-
this.residency.clearOnLoss();
|
|
3544
|
-
for (const listener of Array.from(this.lostListeners)) {
|
|
3545
|
-
try {
|
|
3546
|
-
listener(info);
|
|
3547
|
-
} catch {
|
|
3548
|
-
}
|
|
3549
|
-
}
|
|
3550
|
-
}
|
|
3551
|
-
}
|
|
3552
|
-
export {
|
|
3553
|
-
SE_DEFAULTS as $,
|
|
3554
|
-
APSP_MAX_DISPATCHES_PER_SUBMIT as A,
|
|
3555
|
-
BufferUsage as B,
|
|
3556
|
-
PARTIAL_BYTES as C,
|
|
3557
|
-
GRID_HUB_CELL as D,
|
|
3558
|
-
EXACT_TILES_PER_PASS as E,
|
|
3559
|
-
FUSED_FRONTIER_MAX as F,
|
|
3560
|
-
GpuContext as G,
|
|
3561
|
-
LAYOUT_TUNING_DEFAULTS as H,
|
|
3562
|
-
INDIRECT_ARGS_STRIDE as I,
|
|
3563
|
-
EXACT_MAX_NODES as J,
|
|
3564
|
-
SETTLE_FLOOR_UNBOUNDED as K,
|
|
3565
|
-
LABEL_PROP_PASSES_PER_SUBMIT as L,
|
|
3566
|
-
MAX_WORKGROUPS_PER_DIM as M,
|
|
3567
|
-
GRID_BBOX_MARGIN as N,
|
|
3568
|
-
GRID_EXTENT_FLOOR as O,
|
|
3569
|
-
PARALLEL_MERGE_LIMIT as P,
|
|
3570
|
-
FR_ADAPTIVE_MAX_ITERATIONS as Q,
|
|
3571
|
-
RADIX_BINS as R,
|
|
3572
|
-
STATE_HEADER_BYTES as S,
|
|
3573
|
-
TRACE_RECORD_BYTES as T,
|
|
3574
|
-
UNIFORM_SLOT_BYTES as U,
|
|
3575
|
-
FR_START_TEMPERATURE as V,
|
|
3576
|
-
WebGpuGraphError as W,
|
|
3577
|
-
FA2_FLAG_ADAPTIVE as X,
|
|
3578
|
-
SETTLE_FLOOR_FRACTION as Y,
|
|
3579
|
-
FR_REHEAT_FRACTION as Z,
|
|
3580
|
-
FR_DEFAULTS as _,
|
|
3581
|
-
WGSL_RESERVED_WORDS as a,
|
|
3582
|
-
SETTLE_FLOOR_REFERENCE_NODES as a0,
|
|
3583
|
-
SE_SCALE_REFERENCE_NODES as a1,
|
|
3584
|
-
ARC_WINDOW_ALIGN as a2,
|
|
3585
|
-
PASSTHROUGH_FORMAT_CODES as a3,
|
|
3586
|
-
STORAGE_ALIGN as a4,
|
|
3587
|
-
WORKGROUP_SIZE as a5,
|
|
3588
|
-
isSoftwareAdapter as a6,
|
|
3589
|
-
U32_MAX as b,
|
|
3590
|
-
MAX_LEVELS_PER_SUBMIT as c,
|
|
3591
|
-
deviceLostError as d,
|
|
3592
|
-
BEAMER_BETA as e,
|
|
3593
|
-
SSSP_DELTA_FACTOR as f,
|
|
3594
|
-
F32_INF_BITS as g,
|
|
3595
|
-
BC_EDGE_PARALLEL_GAMMA as h,
|
|
3596
|
-
isWebGpuGraphError as i,
|
|
3597
|
-
BC_BATCH_BUDGET_FRACTION as j,
|
|
3598
|
-
BC_MAX_BATCH as k,
|
|
3599
|
-
BC_BACKWARD_LEVELS_PER_SUBMIT as l,
|
|
3600
|
-
BC_COUNT_MAX_GROUPS as m,
|
|
3601
|
-
APSP_TILE as n,
|
|
3602
|
-
GROUP_ROW_THREAD_MAX as o,
|
|
3603
|
-
GROUP_ROW_THREAD_LIMIT as p,
|
|
3604
|
-
GROUP_HASH_LOAD_FACTOR as q,
|
|
3605
|
-
BORUVKA_ROUNDS_PER_SUBMIT as r,
|
|
3606
|
-
GRID_COARSEST_SIDE as s,
|
|
3607
|
-
GRID_MIN_SIDE as t,
|
|
3608
|
-
GRID_SORT_BITS as u,
|
|
3609
|
-
FA2_DEFAULTS as v,
|
|
3610
|
-
MAX_ITERATIONS_PER_STEP as w,
|
|
3611
|
-
MAX_1D_ITEMS as x,
|
|
3612
|
-
hasErrorCode as y,
|
|
3613
|
-
FA2_FLAG_FIRST as z
|
|
3614
|
-
};
|
|
3615
|
-
//# sourceMappingURL=context-BZY6SMsM.js.map
|