@graphty/webgpu-graph-algorithms 0.0.0 → 0.2.1

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