@graphty/webgpu-graph-algorithms 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +344 -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,311 @@
1
+ /**
2
+ * Kernel (spec 5.1; contract 3.9): a compiled pipeline bound to the binding list of its spec. `bind()` creates the
3
+ * bind groups from a `Record<name, Binding>` keyed by the declared names and caches them by the identity of the
4
+ * buffers, offsets and sizes (a layout's bind groups are created once per load(), not per iteration); `dispatch()`
5
+ * records setPipeline / setBindGroup / dispatchWorkgroups and skips an empty plan (spec 5.6).
6
+ */
7
+
8
+ import { STORAGE_ALIGN } from "../constants.js";
9
+ import { WebGpuGraphError } from "../errors.js";
10
+ import { type Binding } from "../types/memory.js";
11
+ import { type DispatchPlan } from "./dispatch.js";
12
+ import { type BindingDecl, type ComposedModule, type WgslModuleSpec } from "./wgsl.js";
13
+
14
+ /** The resources of one bind(): one Binding per BindingDecl name; a uniform decl takes the ring's whole-buffer binding (the slot is chosen by the dynamic offset at dispatch). */
15
+ export type KernelBindings = Readonly<Record<string, Binding>>;
16
+ /** A kernel with its bind groups created (spec 5.1: cached per set of buffers and offsets). */
17
+ export interface BoundKernel {
18
+ readonly kernel: Kernel;
19
+ readonly bindGroups: readonly GPUBindGroup[];
20
+ /** Group indices whose bind group takes a dynamic offset (the uniform groups), in group order. */
21
+ readonly dynamicGroups: readonly number[];
22
+ }
23
+
24
+ /** Buffer identities for the bind-group cache key: every GPUBuffer seen by any Kernel gets one number, once. */
25
+ const bufferIds = new WeakMap<GPUBuffer, number>();
26
+ let nextBufferId = 1;
27
+
28
+ /**
29
+ * The identity number of a buffer.
30
+ * @param buffer - the buffer
31
+ * @returns its number
32
+ */
33
+ function bufferId(buffer: GPUBuffer): number {
34
+ let id = bufferIds.get(buffer);
35
+ if (id === undefined) {
36
+ id = nextBufferId;
37
+ nextBufferId += 1;
38
+ bufferIds.set(buffer, id);
39
+ }
40
+ return id;
41
+ }
42
+
43
+ /**
44
+ * True when two byte ranges intersect.
45
+ * @param a - one binding
46
+ * @param b - the other binding
47
+ * @returns whether [a.offset, a.offset + a.size) and [b.offset, b.offset + b.size) overlap
48
+ */
49
+ function rangesIntersect(a: Binding, b: Binding): boolean {
50
+ return a.offset < b.offset + b.size && b.offset < a.offset + a.size;
51
+ }
52
+
53
+ /** A compiled pipeline plus the binding list of its spec (spec 5.1). */
54
+ export class Kernel {
55
+ /** The module spec the pipeline was compiled from (its bindings drive bind()). */
56
+ readonly spec: WgslModuleSpec;
57
+ /** The compiled compute pipeline. */
58
+ readonly pipeline: GPUComputePipeline;
59
+ /** The explicit bind group layouts, one per group index (an empty layout for an unused index). */
60
+ readonly layouts: readonly GPUBindGroupLayout[];
61
+ /** The workgroup size the pipeline was compiled with (the effective WG override). */
62
+ readonly workgroupSize: number;
63
+ /** The entry point name the pipeline was compiled with. */
64
+ readonly entryPoint: string;
65
+ private readonly device: GPUDevice;
66
+ /** The declarations of each group in binding order (index = group). */
67
+ private readonly groups: readonly (readonly BindingDecl[])[];
68
+ /** The uniform-binding count of each group (the dynamic offsets a setBindGroup takes). */
69
+ private readonly dynamicCounts: readonly number[];
70
+ /** Cached bind groups by buffer identity + offset + size. */
71
+ private readonly cache = new Map<string, BoundKernel>();
72
+
73
+ /**
74
+ * Wraps a compiled pipeline.
75
+ * @param device - the device the bind groups are created on
76
+ * @param spec - the module
77
+ * @param composed - the composed text (workgroup size and entry point)
78
+ * @param pipeline - the compiled pipeline
79
+ * @param layouts - the explicit bind-group layouts, one per group 0..maxGroup
80
+ */
81
+ constructor(
82
+ device: GPUDevice,
83
+ spec: WgslModuleSpec,
84
+ composed: ComposedModule,
85
+ pipeline: GPUComputePipeline,
86
+ layouts: readonly GPUBindGroupLayout[],
87
+ ) {
88
+ this.device = device;
89
+ this.spec = spec;
90
+ this.pipeline = pipeline;
91
+ this.layouts = layouts;
92
+ this.workgroupSize = composed.overrides.WG as number;
93
+ this.entryPoint = composed.entryPoint;
94
+ const groups: BindingDecl[][] = layouts.map(() => []);
95
+ for (const decl of spec.bindings) {
96
+ groups[decl.group].push(decl);
97
+ }
98
+ for (const group of groups) {
99
+ group.sort((a, b) => a.binding - b.binding);
100
+ }
101
+ this.groups = groups;
102
+ this.dynamicCounts = groups.map((group) => group.filter((decl) => decl.kind === "uniform").length);
103
+ }
104
+
105
+ /**
106
+ * Creates (or reuses, keyed by every buffer identity + offset + size) the bind groups, each labelled
107
+ * `<spec.id>/<group>`; a missing or extra name is E_INVALID_ARGUMENT; two bindings of one call whose ranges
108
+ * intersect on one buffer while either slot is `storage` (read_write) is E_INVALID_ARGUMENT
109
+ * { argument: "aliasing" } (the host-side mirror of WebGPU's writable buffer-binding-aliasing rule, 3.10.1, so
110
+ * the failure is synchronous and labelled). CONTRACT DECISION: bind() is synchronous, so a createBindGroup
111
+ * validation error (a wrong-size uniform binding, a usage mismatch) is NOT thrown here -- it reaches the
112
+ * pending-error slot as E_VALIDATION { label: "<spec.id>/<group>" } and is thrown by the batch's readback
113
+ * (Dawn-node) or the next assertReady() (browser), spec 5.7. An empty bind group is created for every empty
114
+ * layout index so setBindGroup is called for 0..maxGroup.
115
+ * PLAN DECISION (verified on Dawn 0.4.0 / lavapipe): one buffer bound with two DIFFERENT access modes when one is
116
+ * `storage` is also E_INVALID_ARGUMENT { argument: "aliasing", reason: "usage" } even with disjoint ranges, because
117
+ * WebGPU's usage-scope rule rejects "writable usage and another usage in the same synchronization scope" per
118
+ * BUFFER; two `storage` slots with disjoint ranges (the state header / trace pattern) are accepted; a zero-size
119
+ * binding (never bind a zero-length buffer, spec 3.6) and an offset that is not a multiple of STORAGE_ALIGN = 256
120
+ * (spec 2.6; Dawn: "does not satisfy the minimum ... alignment (256)") are E_INVALID_ARGUMENT.
121
+ * @param resources - one Binding per declared name
122
+ * @returns the bound kernel (the same object for the same buffers, offsets and sizes until invalidate())
123
+ */
124
+ bind(resources: KernelBindings): BoundKernel {
125
+ const { id } = this.spec;
126
+ const declared = new Map(this.spec.bindings.map((decl) => [decl.name, decl] as const));
127
+ for (const name of Object.keys(resources)) {
128
+ if (!declared.has(name)) {
129
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${id}: no binding named "${name}"`, {
130
+ argument: "bindings",
131
+ value: name,
132
+ expected: [...declared.keys()].join(", "),
133
+ });
134
+ }
135
+ }
136
+ const resolved: { readonly decl: BindingDecl; readonly binding: Binding }[] = [];
137
+ for (const decl of this.spec.bindings) {
138
+ const binding = resources[decl.name] as Binding | undefined;
139
+ if (binding === undefined) {
140
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${id}: binding "${decl.name}" is missing`, {
141
+ argument: "bindings",
142
+ value: decl.name,
143
+ expected: "a Binding for every declared name",
144
+ });
145
+ }
146
+ if (!(Number.isInteger(binding.size) && binding.size > 0)) {
147
+ throw new WebGpuGraphError(
148
+ "E_INVALID_ARGUMENT",
149
+ `${id}: binding "${decl.name}" has size ${binding.size}`,
150
+ {
151
+ argument: decl.name,
152
+ value: binding.size,
153
+ expected: "size > 0 (a zero-length buffer is never bound)",
154
+ },
155
+ );
156
+ }
157
+ if (!(Number.isInteger(binding.offset) && binding.offset >= 0 && binding.offset % STORAGE_ALIGN === 0)) {
158
+ throw new WebGpuGraphError(
159
+ "E_INVALID_ARGUMENT",
160
+ `${id}: binding "${decl.name}" has offset ${binding.offset}`,
161
+ {
162
+ argument: decl.name,
163
+ value: binding.offset,
164
+ expected: `a multiple of ${STORAGE_ALIGN} (spec 2.6: the offset alignment the package always honours)`,
165
+ },
166
+ );
167
+ }
168
+ resolved.push({ decl, binding });
169
+ }
170
+ for (let i = 0; i < resolved.length; i++) {
171
+ for (let j = i + 1; j < resolved.length; j++) {
172
+ const a = resolved[i];
173
+ const b = resolved[j];
174
+ if (a.binding.buffer !== b.binding.buffer) {
175
+ continue;
176
+ }
177
+ const writable = a.decl.kind === "storage" || b.decl.kind === "storage";
178
+ if (!writable) {
179
+ continue;
180
+ }
181
+ if (a.decl.kind !== b.decl.kind) {
182
+ throw new WebGpuGraphError(
183
+ "E_INVALID_ARGUMENT",
184
+ `${id}: "${a.decl.name}" and "${b.decl.name}" bind one buffer with different access modes (writable usage next to another usage)`,
185
+ {
186
+ argument: "aliasing",
187
+ value: [a.decl.name, b.decl.name],
188
+ expected: "one access mode per buffer when a slot is read_write",
189
+ reason: "usage",
190
+ },
191
+ );
192
+ }
193
+ if (rangesIntersect(a.binding, b.binding)) {
194
+ throw new WebGpuGraphError(
195
+ "E_INVALID_ARGUMENT",
196
+ `${id}: "${a.decl.name}" and "${b.decl.name}" bind intersecting ranges of one read_write buffer`,
197
+ {
198
+ argument: "aliasing",
199
+ value: [a.decl.name, b.decl.name],
200
+ expected: "disjoint ranges",
201
+ reason: "range",
202
+ },
203
+ );
204
+ }
205
+ }
206
+ }
207
+ const key = resolved
208
+ .map(({ binding }) => `${bufferId(binding.buffer)}:${binding.offset}:${binding.size}`)
209
+ .join("|");
210
+ const cached = this.cache.get(key);
211
+ if (cached !== undefined) {
212
+ return cached;
213
+ }
214
+ const byName = new Map(resolved.map(({ decl, binding }) => [decl.name, binding] as const));
215
+ const bindGroups = this.layouts.map((layout, group) =>
216
+ this.device.createBindGroup({
217
+ label: `${id}/${group}`,
218
+ layout,
219
+ entries: this.groups[group].map((decl) => {
220
+ const binding = byName.get(decl.name) as Binding;
221
+ return {
222
+ binding: decl.binding,
223
+ resource: { buffer: binding.buffer, offset: binding.offset, size: binding.size },
224
+ };
225
+ }),
226
+ }),
227
+ );
228
+ const dynamicGroups: number[] = [];
229
+ this.dynamicCounts.forEach((count, group) => {
230
+ if (count > 0) {
231
+ dynamicGroups.push(group);
232
+ }
233
+ });
234
+ const bound: BoundKernel = Object.freeze({
235
+ kernel: this,
236
+ bindGroups: Object.freeze(bindGroups),
237
+ dynamicGroups: Object.freeze(dynamicGroups),
238
+ });
239
+ this.cache.set(key, bound);
240
+ return bound;
241
+ }
242
+
243
+ /**
244
+ * 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).
245
+ * PLAN DECISION: one dynamic offset per dynamic GROUP, replicated over every uniform binding of that group (every
246
+ * P1-P3 kernel has exactly one params uniform per group); absent offsets mean 0; a BoundKernel of another kernel
247
+ * or an offset list of the wrong length is E_INVALID_ARGUMENT.
248
+ * PLAN DECISION: `dynamicOffsets?` is spelled `?: readonly number[]` rather than the contract's
249
+ * `?: readonly number[] | undefined` because the root ESLint rule no-duplicate-type-constituents rejects the
250
+ * explicit undefined on an optional parameter (the call signature is identical).
251
+ * @param pass - the open compute pass
252
+ * @param bound - a BoundKernel of THIS kernel
253
+ * @param plan - the dispatch shape
254
+ * @param dynamicOffsets - one byte offset per entry of bound.dynamicGroups
255
+ */
256
+ dispatch(
257
+ pass: GPUComputePassEncoder,
258
+ bound: BoundKernel,
259
+ plan: DispatchPlan,
260
+ dynamicOffsets?: readonly number[],
261
+ ): void {
262
+ const { id } = this.spec;
263
+ if (bound.kernel !== this) {
264
+ throw new WebGpuGraphError(
265
+ "E_INVALID_ARGUMENT",
266
+ `${id}: the bound kernel belongs to "${bound.kernel.spec.id}"`,
267
+ {
268
+ argument: "bound",
269
+ value: bound.kernel.spec.id,
270
+ expected: id,
271
+ },
272
+ );
273
+ }
274
+ if (dynamicOffsets !== undefined && dynamicOffsets.length !== bound.dynamicGroups.length) {
275
+ throw new WebGpuGraphError(
276
+ "E_INVALID_ARGUMENT",
277
+ `${id}: ${dynamicOffsets.length} dynamic offsets for ${bound.dynamicGroups.length} dynamic groups`,
278
+ {
279
+ argument: "dynamicOffsets",
280
+ value: dynamicOffsets.length,
281
+ expected: bound.dynamicGroups.length,
282
+ },
283
+ );
284
+ }
285
+ if (plan.x === 0) {
286
+ return;
287
+ }
288
+ pass.setPipeline(this.pipeline);
289
+ let next = 0;
290
+ bound.bindGroups.forEach((bindGroup, group) => {
291
+ const count = this.dynamicCounts[group];
292
+ if (count > 0) {
293
+ const offset = dynamicOffsets?.[next] ?? 0;
294
+ next += 1;
295
+ const offsets: number[] = [];
296
+ for (let k = 0; k < count; k++) {
297
+ offsets.push(offset);
298
+ }
299
+ pass.setBindGroup(group, bindGroup, offsets);
300
+ } else {
301
+ pass.setBindGroup(group, bindGroup);
302
+ }
303
+ });
304
+ pass.dispatchWorkgroups(plan.x, plan.y, 1);
305
+ }
306
+
307
+ /** Drops cached bind groups (a layout's buffers changed). */
308
+ invalidate(): void {
309
+ this.cache.clear();
310
+ }
311
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * PipelineCache (spec 5.1; contract 3.9): compile-once compute pipelines keyed by
3
+ * `(id, overrides, needs present on the device, snippets)` with EXPLICIT bind-group layouts derived from
4
+ * `spec.bindings` (never `layout: "auto"`, so one layout serves every dummy-binding variant and bind groups are
5
+ * reused across variants). A WGSL error is `E_SHADER_COMPILE { stage: "compile", messages, lines }` with
6
+ * body-relative line numbers; a pipeline error that is not a compilation message (an oversized workgroup, an
7
+ * invalid layout) is `E_VALIDATION`.
8
+ */
9
+
10
+ import { formatCompilationInfo } from "../device/error-scope.js";
11
+ import { WebGpuGraphError } from "../errors.js";
12
+ import { type PlanCaps } from "../types/context.js";
13
+ import { Kernel } from "./kernel.js";
14
+ import { bindGroupLayoutDescriptors, type ComposedModule, composeWgsl, type WgslModuleSpec } from "./wgsl.js";
15
+
16
+ /** One cached entry: the pipeline, its layouts and the Kernel built on them. */
17
+ interface CacheEntry {
18
+ readonly key: string;
19
+ readonly composed: ComposedModule;
20
+ readonly pipeline: GPUComputePipeline;
21
+ readonly layouts: readonly GPUBindGroupLayout[];
22
+ readonly kernel: Kernel;
23
+ }
24
+
25
+ /**
26
+ * JSON with the keys of every (nested) object sorted, so two override records with the same content give one key.
27
+ * @param value - a JSON-compatible value
28
+ * @returns the canonical text
29
+ */
30
+ function stableJson(value: unknown): string {
31
+ if (Array.isArray(value)) {
32
+ return `[${value.map((item: unknown) => stableJson(item)).join(",")}]`;
33
+ }
34
+ if (typeof value === "object" && value !== null) {
35
+ const record = value as Record<string, unknown>;
36
+ const keys = Object.keys(record).sort();
37
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
38
+ }
39
+ return JSON.stringify(value);
40
+ }
41
+
42
+ /**
43
+ * FNV-1a (32-bit) of a text as 8 hex digits; "0" for an absent or empty snippet record. Hashes text, never an arc
44
+ * index or a byte offset, so the bitwise operators are within the house rule.
45
+ * @param snippets - the snippet record
46
+ * @returns the hash text
47
+ */
48
+ function hashSnippets(snippets: Readonly<Record<string, string>> | undefined): string {
49
+ if (snippets === undefined || Object.keys(snippets).length === 0) {
50
+ return "0";
51
+ }
52
+ const text = stableJson(snippets);
53
+ let hash = 0x811c9dc5;
54
+ for (let i = 0; i < text.length; i++) {
55
+ hash ^= text.charCodeAt(i);
56
+ hash = Math.imul(hash, 0x01000193) >>> 0;
57
+ }
58
+ return hash.toString(16).padStart(8, "0");
59
+ }
60
+
61
+ /**
62
+ * The cache key of a spec on a device (pure; PLAN DECISION: exported so the override-matrix helper of P2-T2 can
63
+ * compute keys without a device): `id + "|" + stableJson(overrides) + "|" + needs.filter(present).join(",") + "|" + hash(snippets)`.
64
+ * @param spec - the module
65
+ * @param caps - the device capabilities (the feature set decides which needs are present)
66
+ * @returns the key
67
+ */
68
+ export function pipelineKey(spec: WgslModuleSpec, caps: PlanCaps): string {
69
+ const present = spec.needs.filter((need) => caps.features.has(need)).join(",");
70
+ return `${spec.id}|${stableJson(spec.overrides)}|${present}|${hashSnippets(spec.snippets)}`;
71
+ }
72
+
73
+ /**
74
+ * The message of an unknown error value.
75
+ * @param err - the thrown value
76
+ * @returns its message text
77
+ */
78
+ function messageOf(err: unknown): string {
79
+ return err instanceof Error ? err.message : String(err);
80
+ }
81
+
82
+ /** Compile-once pipelines keyed by (id, overrides, needs present on the device, snippets) with explicit bind-group layouts (spec 5.1). */
83
+ export class PipelineCache {
84
+ private readonly device: GPUDevice;
85
+ private readonly caps: PlanCaps;
86
+ /** In-flight and finished compiles by key (a promise, so two concurrent get() calls share one compile). */
87
+ private readonly pending = new Map<string, Promise<CacheEntry>>();
88
+ /** Finished compiles by key. */
89
+ private readonly entries = new Map<string, CacheEntry>();
90
+ /** Keys in the order their compiles were started (a failed compile is removed again). */
91
+ private readonly order: string[] = [];
92
+ private disposed = false;
93
+
94
+ /**
95
+ * Builds a cache over a device.
96
+ * @param device - the device pipelines are created on
97
+ * @param caps - its capabilities (WG, subgroup sizes, features)
98
+ */
99
+ constructor(device: GPUDevice, caps: PlanCaps) {
100
+ this.device = device;
101
+ this.caps = caps;
102
+ }
103
+
104
+ /**
105
+ * The cache key of a spec on this device: `id + "|" + stableJson(overrides) + "|" + needs.filter(present).join(",") + "|" + hash(snippets)`.
106
+ * @param spec - the module
107
+ * @returns the key
108
+ */
109
+ key(spec: WgslModuleSpec): string {
110
+ return pipelineKey(spec, this.caps);
111
+ }
112
+
113
+ /**
114
+ * 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.
115
+ * @param spec - the module
116
+ * @returns the pipeline (the same object for the same key)
117
+ */
118
+ async get(spec: WgslModuleSpec): Promise<GPUComputePipeline> {
119
+ return (await this.entry(spec)).pipeline;
120
+ }
121
+
122
+ /**
123
+ * The Kernel (pipeline + layouts + binding names) for a spec; cached with the pipeline.
124
+ * @param spec - the module
125
+ * @returns the kernel (the same object for the same key)
126
+ */
127
+ async kernel(spec: WgslModuleSpec): Promise<Kernel> {
128
+ return (await this.entry(spec)).kernel;
129
+ }
130
+
131
+ /**
132
+ * Compiles every spec not yet cached (load() calls it so the first step() does not compile).
133
+ * @param specs - the modules
134
+ */
135
+ async warm(specs: readonly WgslModuleSpec[]): Promise<void> {
136
+ await Promise.all(specs.map((spec) => this.entry(spec)));
137
+ }
138
+
139
+ /**
140
+ * Number of cached pipelines.
141
+ * @returns the count of finished compiles
142
+ */
143
+ get size(): number {
144
+ return this.entries.size;
145
+ }
146
+
147
+ /**
148
+ * Every key created so far, in creation order (the override-matrix coverage test reads it).
149
+ * PLAN DECISION: keys are listed in compile-START order and only those whose compile finished (warm() compiles
150
+ * concurrently, so completion order is not deterministic; a failed compile is removed).
151
+ * @returns the keys
152
+ * @internal
153
+ */
154
+ keys(): readonly string[] {
155
+ return this.order.filter((key) => this.entries.has(key));
156
+ }
157
+
158
+ /**
159
+ * The bind-group layouts of a cached kernel by key.
160
+ * @param key - a key from keys()
161
+ * @returns the layouts, or null when the key is not cached
162
+ * @internal
163
+ */
164
+ layoutsOf(key: string): readonly GPUBindGroupLayout[] | null {
165
+ return this.entries.get(key)?.layouts ?? null;
166
+ }
167
+
168
+ /**
169
+ * PLAN DECISION: forgets every pipeline and refuses further compiles with E_DISPOSED (the context's dispose()
170
+ * may call it; a GPUComputePipeline has no destroy(), so the objects are simply released).
171
+ */
172
+ dispose(): void {
173
+ this.disposed = true;
174
+ this.pending.clear();
175
+ this.entries.clear();
176
+ this.order.length = 0;
177
+ }
178
+
179
+ /**
180
+ * The cached entry of a spec, compiling it once per key.
181
+ * @param spec - the module
182
+ * @returns the entry
183
+ */
184
+ private entry(spec: WgslModuleSpec): Promise<CacheEntry> {
185
+ if (this.disposed) {
186
+ return Promise.reject(
187
+ new WebGpuGraphError("E_DISPOSED", "PipelineCache: disposed", { label: "PipelineCache" }),
188
+ );
189
+ }
190
+ const key = this.key(spec);
191
+ const inFlight = this.pending.get(key);
192
+ if (inFlight !== undefined) {
193
+ return inFlight;
194
+ }
195
+ this.order.push(key);
196
+ const compile = this.compile(spec, key).then(
197
+ (entry) => {
198
+ if (!this.disposed) {
199
+ this.entries.set(key, entry);
200
+ }
201
+ return entry;
202
+ },
203
+ (err: unknown) => {
204
+ this.pending.delete(key);
205
+ const at = this.order.indexOf(key);
206
+ if (at >= 0) {
207
+ this.order.splice(at, 1);
208
+ }
209
+ throw err;
210
+ },
211
+ );
212
+ this.pending.set(key, compile);
213
+ return compile;
214
+ }
215
+
216
+ /**
217
+ * One compile: compose, create the shader module inside a synchronous push / pop validation pair (so concurrent
218
+ * compiles never interleave scopes), check the compilation info, create the layouts and the pipeline.
219
+ * PLAN DECISION: `E_SHADER_COMPILE` at stage "compile" also carries `lines` (the body-relative 1-based line of every
220
+ * error message, from `composed.bodyLine`) next to the contract's `messages`; a `createComputePipelineAsync`
221
+ * rejection (a GPUPipelineError, which pushes nothing into a validation scope on Dawn 0.4.0) is
222
+ * `E_VALIDATION { label: "<id>|pipeline", message }`; bool overrides travel as the pipeline constants 1 / 0
223
+ * because dawn.node's WebIDL layer rejects a boolean constant.
224
+ * @param spec - the module
225
+ * @param key - its key (the pipeline label)
226
+ * @returns the entry
227
+ */
228
+ private async compile(spec: WgslModuleSpec, key: string): Promise<CacheEntry> {
229
+ const composed = composeWgsl(spec, this.caps);
230
+ const { device } = this;
231
+ const label = `${spec.id}|pipeline`;
232
+ device.pushErrorScope("validation");
233
+ const module = device.createShaderModule({ code: composed.code, label: spec.id });
234
+ const layouts = bindGroupLayoutDescriptors(spec).map((descriptor, group) =>
235
+ device.createBindGroupLayout({ ...descriptor, label: `${spec.id}/layout${group}` }),
236
+ );
237
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: layouts, label: `${spec.id}|layout` });
238
+ const popped = device.popErrorScope();
239
+ const info = await module.getCompilationInfo();
240
+ const scopeError = await popped;
241
+ const errors = info.messages.filter((message) => message.type === "error");
242
+ if (errors.length > 0) {
243
+ const preludeLines = composed.bodyLine - 1;
244
+ throw new WebGpuGraphError(
245
+ "E_SHADER_COMPILE",
246
+ `${spec.id}: WGSL compilation failed (${errors.length} error(s))`,
247
+ {
248
+ id: spec.id,
249
+ stage: "compile",
250
+ messages: formatCompilationInfo(info, preludeLines),
251
+ lines: errors.map((message) => message.lineNum - preludeLines),
252
+ },
253
+ );
254
+ }
255
+ if (scopeError !== null) {
256
+ throw new WebGpuGraphError("E_VALIDATION", `${spec.id}: ${scopeError.message}`, {
257
+ label,
258
+ message: scopeError.message,
259
+ });
260
+ }
261
+ // dawn.node's WebIDL layer rejects a boolean pipeline constant ("value is not a number"), so bools travel as 1 / 0;
262
+ // only the REFERENCED overrides are supplied (composed.constants): WebKit rejects a constant for an unread override
263
+ const constants: Record<string, number> = {};
264
+ for (const [name, value] of Object.entries(composed.constants)) {
265
+ if (typeof value === "boolean") {
266
+ constants[name] = value ? 1 : 0;
267
+ } else {
268
+ constants[name] = value;
269
+ }
270
+ }
271
+ let pipeline: GPUComputePipeline;
272
+ try {
273
+ pipeline = await device.createComputePipelineAsync({
274
+ label: key,
275
+ layout: pipelineLayout,
276
+ compute: { module, entryPoint: composed.entryPoint, constants },
277
+ });
278
+ } catch (err: unknown) {
279
+ const message = messageOf(err);
280
+ throw new WebGpuGraphError("E_VALIDATION", `${spec.id}: pipeline creation failed: ${message}`, {
281
+ label,
282
+ message,
283
+ });
284
+ }
285
+ const kernel = new Kernel(device, spec, composed, pipeline, layouts);
286
+ return { key, composed, pipeline, layouts, kernel };
287
+ }
288
+ }