@graphty/webgpu-graph-algorithms 0.6.27 → 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.
Files changed (116) hide show
  1. package/README.md +56 -6
  2. package/dist/acquire.d.ts +2 -0
  3. package/dist/browser.js +18 -1
  4. package/dist/browser.js.map +1 -1
  5. package/dist/chunks/accelerator-B-FjQwaA.js +19173 -0
  6. package/dist/chunks/accelerator-B-FjQwaA.js.map +1 -0
  7. package/dist/chunks/managed-D_GdQtnu.js +98 -0
  8. package/dist/chunks/managed-D_GdQtnu.js.map +1 -0
  9. package/dist/node.js +18 -1
  10. package/dist/node.js.map +1 -1
  11. package/dist/src/accelerator.d.ts.map +1 -1
  12. package/dist/src/accelerator.js +5 -3
  13. package/dist/src/accelerator.js.map +1 -1
  14. package/dist/src/algorithms/all-pairs.d.ts.map +1 -1
  15. package/dist/src/algorithms/all-pairs.js +72 -47
  16. package/dist/src/algorithms/all-pairs.js.map +1 -1
  17. package/dist/src/algorithms/betweenness.d.ts +1 -1
  18. package/dist/src/algorithms/betweenness.js +2 -2
  19. package/dist/src/algorithms/closeness.d.ts +45 -42
  20. package/dist/src/algorithms/closeness.d.ts.map +1 -1
  21. package/dist/src/algorithms/closeness.js +295 -226
  22. package/dist/src/algorithms/closeness.js.map +1 -1
  23. package/dist/src/browser/index.d.ts +10 -0
  24. package/dist/src/browser/index.d.ts.map +1 -1
  25. package/dist/src/browser/index.js +22 -0
  26. package/dist/src/browser/index.js.map +1 -1
  27. package/dist/src/constants.d.ts +13 -3
  28. package/dist/src/constants.d.ts.map +1 -1
  29. package/dist/src/constants.js +13 -3
  30. package/dist/src/constants.js.map +1 -1
  31. package/dist/src/kernels.d.ts +14 -4
  32. package/dist/src/kernels.d.ts.map +1 -1
  33. package/dist/src/kernels.js +62 -28
  34. package/dist/src/kernels.js.map +1 -1
  35. package/dist/src/layouts/force-simulation.d.ts.map +1 -1
  36. package/dist/src/layouts/force-simulation.js +0 -1
  37. package/dist/src/layouts/force-simulation.js.map +1 -1
  38. package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
  39. package/dist/src/layouts/forceatlas2.js +0 -1
  40. package/dist/src/layouts/forceatlas2.js.map +1 -1
  41. package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -1
  42. package/dist/src/layouts/fruchterman-reingold.js +0 -1
  43. package/dist/src/layouts/fruchterman-reingold.js.map +1 -1
  44. package/dist/src/layouts/repulsion-grid.d.ts +1 -3
  45. package/dist/src/layouts/repulsion-grid.d.ts.map +1 -1
  46. package/dist/src/layouts/repulsion-grid.js +1 -6
  47. package/dist/src/layouts/repulsion-grid.js.map +1 -1
  48. package/dist/src/layouts/spring-electrical.d.ts.map +1 -1
  49. package/dist/src/layouts/spring-electrical.js +0 -1
  50. package/dist/src/layouts/spring-electrical.js.map +1 -1
  51. package/dist/src/managed.d.ts +11 -0
  52. package/dist/src/managed.d.ts.map +1 -0
  53. package/dist/src/managed.js +129 -0
  54. package/dist/src/managed.js.map +1 -0
  55. package/dist/src/node/index.d.ts +11 -0
  56. package/dist/src/node/index.d.ts.map +1 -1
  57. package/dist/src/node/index.js +21 -0
  58. package/dist/src/node/index.js.map +1 -1
  59. package/dist/src/primitives/grid-pyramid.d.ts +16 -15
  60. package/dist/src/primitives/grid-pyramid.d.ts.map +1 -1
  61. package/dist/src/primitives/grid-pyramid.js +20 -28
  62. package/dist/src/primitives/grid-pyramid.js.map +1 -1
  63. package/dist/src/types/accelerator.d.ts +2 -0
  64. package/dist/src/types/accelerator.d.ts.map +1 -1
  65. package/dist/src/types/managed.d.ts +81 -0
  66. package/dist/src/types/managed.d.ts.map +1 -0
  67. package/dist/src/types/managed.js +7 -0
  68. package/dist/src/types/managed.js.map +1 -0
  69. package/dist/src/wgsl/bc-forward.wgsl.d.ts +1 -1
  70. package/dist/src/wgsl/bc-forward.wgsl.js +1 -1
  71. package/dist/src/wgsl/closeness-level.wgsl.d.ts +37 -0
  72. package/dist/src/wgsl/closeness-level.wgsl.d.ts.map +1 -0
  73. package/dist/src/wgsl/closeness-level.wgsl.js +204 -0
  74. package/dist/src/wgsl/closeness-level.wgsl.js.map +1 -0
  75. package/dist/src/wgsl/closeness-rowsum.wgsl.d.ts +11 -0
  76. package/dist/src/wgsl/closeness-rowsum.wgsl.d.ts.map +1 -0
  77. package/dist/src/wgsl/closeness-rowsum.wgsl.js +42 -0
  78. package/dist/src/wgsl/closeness-rowsum.wgsl.js.map +1 -0
  79. package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts +2 -2
  80. package/dist/src/wgsl/grid-centroid-hub.wgsl.js +2 -2
  81. package/dist/webgpu-graph-algorithms.js +142 -15586
  82. package/dist/webgpu-graph-algorithms.js.map +1 -1
  83. package/package.json +10 -4
  84. package/src/accelerator.ts +5 -3
  85. package/src/algorithms/all-pairs.ts +86 -56
  86. package/src/algorithms/betweenness.ts +2 -2
  87. package/src/algorithms/closeness.ts +353 -256
  88. package/src/browser/index.ts +37 -0
  89. package/src/constants.ts +13 -3
  90. package/src/kernels.ts +65 -36
  91. package/src/layouts/force-simulation.ts +0 -1
  92. package/src/layouts/forceatlas2.ts +0 -1
  93. package/src/layouts/fruchterman-reingold.ts +0 -1
  94. package/src/layouts/repulsion-grid.ts +2 -7
  95. package/src/layouts/spring-electrical.ts +0 -1
  96. package/src/managed.ts +172 -0
  97. package/src/node/index.ts +36 -0
  98. package/src/primitives/grid-pyramid.ts +29 -41
  99. package/src/types/accelerator.ts +2 -0
  100. package/src/types/managed.ts +86 -0
  101. package/src/wgsl/bc-forward.wgsl.ts +1 -1
  102. package/src/wgsl/closeness-level.wgsl.ts +203 -0
  103. package/src/wgsl/closeness-rowsum.wgsl.ts +41 -0
  104. package/src/wgsl/grid-centroid-hub.wgsl.ts +2 -2
  105. package/dist/chunks/context-BZY6SMsM.js +0 -3615
  106. package/dist/chunks/context-BZY6SMsM.js.map +0 -1
  107. package/dist/src/wgsl/closeness-reduce.wgsl.d.ts +0 -20
  108. package/dist/src/wgsl/closeness-reduce.wgsl.d.ts.map +0 -1
  109. package/dist/src/wgsl/closeness-reduce.wgsl.js +0 -69
  110. package/dist/src/wgsl/closeness-reduce.wgsl.js.map +0 -1
  111. package/dist/src/wgsl/closeness-sweep.wgsl.d.ts +0 -22
  112. package/dist/src/wgsl/closeness-sweep.wgsl.d.ts.map +0 -1
  113. package/dist/src/wgsl/closeness-sweep.wgsl.js +0 -106
  114. package/dist/src/wgsl/closeness-sweep.wgsl.js.map +0 -1
  115. package/src/wgsl/closeness-reduce.wgsl.ts +0 -68
  116. 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