@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,350 @@
1
+ /**
2
+ * The pure upload planner of spec 4.2: from a snapshot's shape, the device limits and the core arrays a kernel
3
+ * needs, choose the one-buffer arena upload (the hot prefix, or the full arena when a cold segment is needed and
4
+ * fits), one buffer per array, or arc windows over the arc-indexed arrays. No GPU object is touched here:
5
+ * GraphResidency (./residency.ts) executes arena and perArray plans and P4 executes windowed plans (P1-P3 only
6
+ * plan them). Every byte length comes from counts and flags, so an identity permutation is never materialised
7
+ * (spec 4.1), and every arithmetic step uses `%` and Math.floor, never a bitwise operator: arc indices and byte
8
+ * offsets exceed 2^31 at the 10M / 100M tier (graph-format invariant I3, spec 4.2).
9
+ */
10
+
11
+ import { type CoreArrayName, type GraphSnapshot } from "@graphty/graph-format";
12
+
13
+ import { ARC_WINDOW_ALIGN } from "../constants.js";
14
+ import { WebGpuGraphError } from "../errors.js";
15
+ import { type PlanCaps, type PlanLimits } from "../types/context.js";
16
+ import { type ArcWindow } from "../types/memory.js";
17
+
18
+ /** The five core arrays in arena (hot-to-cold) order. */
19
+ const CORE_ORDER: readonly CoreArrayName[] = Object.freeze(["rowPtr", "colIdx", "weights", "arcToEdge", "edgeToArc"]);
20
+
21
+ /** The arc-indexed arrays: the only ones a window can cover (rowPtr is row-indexed, edgeToArc edge-indexed). */
22
+ const ARC_INDEXED: ReadonlySet<CoreArrayName> = new Set<CoreArrayName>(["colIdx", "weights", "arcToEdge"]);
23
+
24
+ /**
25
+ * Where one core array's bytes go: one or more buffers (split at window boundaries only when the array exceeds
26
+ * maxBufferSize). Exported with the plan union (contract 3.8): P4's windowed execution reads it; nothing at P1
27
+ * imports it by name.
28
+ * @public
29
+ */
30
+ export interface PlannedArray {
31
+ readonly name: CoreArrayName;
32
+ readonly byteLength: number;
33
+ readonly buffers: readonly { readonly byteOffset: number; readonly byteLength: number }[];
34
+ }
35
+
36
+ /**
37
+ * The arena path: ONE buffer of `bytes`, one writeBuffer, per-segment bindings at `segment.byteOffset - arena.byteOffset`.
38
+ * A member of the exported UploadPlan union (contract 3.8 exports the three plan shapes by name); nothing imports it
39
+ * by name, which knip 5.77 reports.
40
+ * @public
41
+ */
42
+ export interface ArenaPlan {
43
+ readonly kind: "arena";
44
+ readonly bytes: number;
45
+ readonly includesCold: boolean;
46
+ readonly segments: Readonly<Record<CoreArrayName, { readonly offset: number; readonly size: number } | null>>;
47
+ }
48
+
49
+ /**
50
+ * The per-array path: one buffer per needed array, whole-buffer bindings.
51
+ * A member of the exported UploadPlan union (contract 3.8 exports the three plan shapes by name); nothing imports it
52
+ * by name, which knip 5.77 reports.
53
+ * @public
54
+ */
55
+ export interface PerArrayPlan {
56
+ readonly kind: "perArray";
57
+ readonly arrays: readonly PlannedArray[];
58
+ }
59
+
60
+ /**
61
+ * The windowed path: per-array buffers plus the arc windows kernels iterate (P4 executes; P1-P3 only plan).
62
+ * A member of the exported UploadPlan union (contract 3.8 exports the three plan shapes by name); nothing imports it
63
+ * by name, which knip 5.77 reports.
64
+ * @public
65
+ */
66
+ export interface WindowedPlan {
67
+ readonly kind: "windowed";
68
+ readonly arrays: readonly PlannedArray[];
69
+ readonly windows: readonly ArcWindow[];
70
+ readonly arcsPerWindow: number;
71
+ }
72
+
73
+ /** One of the three plans. */
74
+ export type UploadPlan = ArenaPlan | PerArrayPlan | WindowedPlan;
75
+
76
+ /** One buffer range of a PlannedArray. */
77
+ interface BufferRange {
78
+ readonly byteOffset: number;
79
+ readonly byteLength: number;
80
+ }
81
+
82
+ /**
83
+ * Byte length of every core array from the counts and flags alone; 0 means absent (unweighted, an identity
84
+ * permutation, or zero length: spec 5.6 never binds a zero-length array). No array is read.
85
+ * @param s - the snapshot
86
+ * @returns the byte lengths keyed by core array name
87
+ */
88
+ function coreByteLengths(s: GraphSnapshot): Readonly<Record<CoreArrayName, number>> {
89
+ const arcs = s.arcCount;
90
+ const identity = s.flags.arcToEdgeIsIdentity;
91
+ return {
92
+ rowPtr: 4 * (s.nodeCount + 1),
93
+ colIdx: 4 * arcs,
94
+ weights: s.flags.weighted && arcs > 0 ? 4 * arcs : 0,
95
+ arcToEdge: !identity && arcs > 0 ? 4 * arcs : 0,
96
+ edgeToArc: !identity && s.edgeCount > 0 ? 4 * s.edgeCount : 0,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * rowPtr always, then every requested PRESENT array, in arena order (absent names are dropped, never an error).
102
+ * @param need - the requested names
103
+ * @param lengths - the byte lengths of coreByteLengths
104
+ * @returns the names to plan
105
+ */
106
+ function neededNames(
107
+ need: readonly CoreArrayName[],
108
+ lengths: Readonly<Record<CoreArrayName, number>>,
109
+ ): CoreArrayName[] {
110
+ const wanted = new Set<CoreArrayName>(need);
111
+ wanted.add("rowPtr");
112
+ return CORE_ORDER.filter((name) => wanted.has(name) && lengths[name] > 0);
113
+ }
114
+
115
+ /**
116
+ * Rounds an arc count down to a multiple of ARC_WINDOW_ALIGN with `%` (a bitwise `& ~63` breaks above 2^31).
117
+ * @param value - an arc count
118
+ * @returns the largest multiple of 64 not above it
119
+ */
120
+ function alignDown(value: number): number {
121
+ return value - (value % ARC_WINDOW_ALIGN);
122
+ }
123
+
124
+ /**
125
+ * The E_TOO_LARGE error of spec 5.7 for a plan that cannot exist.
126
+ * @param needed - the bytes that do not fit
127
+ * @param limit - the limit they exceed
128
+ * @param path - "rowPtr" (no window can hold rowPtr) or "binding" (an edge-indexed array cannot be windowed)
129
+ * @returns the error to throw
130
+ */
131
+ function tooLarge(needed: number, limit: number, path: "rowPtr" | "binding"): WebGpuGraphError {
132
+ return new WebGpuGraphError("E_TOO_LARGE", `${path}: ${needed} bytes exceed the device limit of ${limit} bytes`, {
133
+ needed,
134
+ limit,
135
+ path,
136
+ algorithm: null,
137
+ });
138
+ }
139
+
140
+ /**
141
+ * The E_INVALID_ARGUMENT error of a bad planner input.
142
+ * @param argument - the argument name
143
+ * @param value - the value given
144
+ * @param expected - what was expected
145
+ * @returns the error to throw
146
+ */
147
+ function invalid(argument: string, value: unknown, expected: string): WebGpuGraphError {
148
+ return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument}: expected ${expected}`, {
149
+ argument,
150
+ value,
151
+ expected,
152
+ });
153
+ }
154
+
155
+ /**
156
+ * The largest 64-aligned arc count one binding holds: floor(maxStorageBufferBindingSize / 4) rounded down to a
157
+ * multiple of ARC_WINDOW_ALIGN.
158
+ * @param limits - the device limits
159
+ * @returns the arcs per window (0 when the binding limit is below 256 bytes)
160
+ */
161
+ export function arcsPerWindowFor(limits: PlanLimits): number {
162
+ return alignDown(Math.floor(limits.maxStorageBufferBindingSize / 4));
163
+ }
164
+
165
+ /**
166
+ * The window list for a rowPtr (pure; unit-tested with a synthetic rowPtr above 2^31 arcs): start = rowPtr[v0] -
167
+ * rowPtr[v0] % ARC_WINDOW_ALIGN, at most `arcsPerWindow` arcs per window, a row longer than a window split across
168
+ * windows. Whole rows first: a window ends at the last row boundary within `start + arcsPerWindow`; a row that does
169
+ * not fit a window opened at its own aligned start is split into aligned chunks whose `rowFirst === rowLast` is that
170
+ * row, and the next window continues the row at the chunk end. Every row belongs to a window (leading and trailing
171
+ * empty rows included); every window is placed in buffer 0 at offset 4 x start (planUpload re-places them when an
172
+ * array is split across buffers).
173
+ * @param rowPtr - nodeCount + 1 row offsets (rowPtr[nodeCount] === arcCount)
174
+ * @param arcCount - the arc count
175
+ * @param arcsPerWindow - a positive multiple of ARC_WINDOW_ALIGN
176
+ * @returns the windows in arc order (empty for arcCount 0)
177
+ */
178
+ export function planArcWindows(rowPtr: Uint32Array, arcCount: number, arcsPerWindow: number): ArcWindow[] {
179
+ if (rowPtr.length === 0) {
180
+ throw invalid("rowPtr", rowPtr.length, "nodeCount + 1 entries");
181
+ }
182
+ if (
183
+ !Number.isInteger(arcsPerWindow) ||
184
+ arcsPerWindow < ARC_WINDOW_ALIGN ||
185
+ arcsPerWindow % ARC_WINDOW_ALIGN !== 0
186
+ ) {
187
+ throw invalid("arcsPerWindow", arcsPerWindow, `a positive multiple of ${ARC_WINDOW_ALIGN}`);
188
+ }
189
+ const n = rowPtr.length - 1;
190
+ if (rowPtr[n] !== arcCount) {
191
+ throw invalid("arcCount", arcCount, `rowPtr[${n}] === ${rowPtr[n]}`);
192
+ }
193
+ const windows: ArcWindow[] = [];
194
+ if (arcCount === 0) {
195
+ return windows;
196
+ }
197
+ let pos = 0;
198
+ let v = 0;
199
+ let rowFirst = 0;
200
+ while (pos < arcCount) {
201
+ while (rowPtr[v + 1] <= pos) {
202
+ v++;
203
+ }
204
+ const start = alignDown(pos);
205
+ const limit = start + arcsPerWindow;
206
+ if (rowPtr[v + 1] > limit) {
207
+ windows.push({ start, end: limit, rowFirst, rowLast: v, bufferIndex: 0, offset: 4 * start });
208
+ pos = limit;
209
+ rowFirst = v;
210
+ } else {
211
+ let u = v;
212
+ while (u + 1 < n && rowPtr[u + 2] <= limit) {
213
+ u++;
214
+ }
215
+ const end = rowPtr[u + 1];
216
+ windows.push({ start, end, rowFirst, rowLast: u, bufferIndex: 0, offset: 4 * start });
217
+ pos = end;
218
+ rowFirst = u + 1;
219
+ }
220
+ }
221
+ return windows;
222
+ }
223
+
224
+ /**
225
+ * Groups consecutive windows into buffers of at most `bufferLimit` bytes (spec 4.2: an array above maxBufferSize is
226
+ * split across buffers at window boundaries) and re-places every window with its buffer index and offset.
227
+ * @param windows - the windows of planArcWindows
228
+ * @param arcBytes - the byte length of one arc-indexed array
229
+ * @param bufferLimit - maxBufferSize
230
+ * @returns the buffer ranges shared by every arc-indexed array and the placed windows
231
+ */
232
+ function placeWindows(
233
+ windows: readonly ArcWindow[],
234
+ arcBytes: number,
235
+ bufferLimit: number,
236
+ ): { readonly buffers: BufferRange[]; readonly placed: ArcWindow[] } {
237
+ if (arcBytes <= bufferLimit) {
238
+ return { buffers: [{ byteOffset: 0, byteLength: arcBytes }], placed: [...windows] };
239
+ }
240
+ const buffers: BufferRange[] = [];
241
+ const placed: ArcWindow[] = [];
242
+ let first = 0;
243
+ while (first < windows.length) {
244
+ let last = first;
245
+ while (last + 1 < windows.length && 4 * (windows[last + 1].end - windows[first].start) <= bufferLimit) {
246
+ last++;
247
+ }
248
+ const base = windows[first].start;
249
+ const bufferIndex = buffers.length;
250
+ buffers.push({ byteOffset: 4 * base, byteLength: 4 * (windows[last].end - base) });
251
+ for (let i = first; i <= last; i++) {
252
+ const w = windows[i];
253
+ placed.push({
254
+ start: w.start,
255
+ end: w.end,
256
+ rowFirst: w.rowFirst,
257
+ rowLast: w.rowLast,
258
+ bufferIndex,
259
+ offset: 4 * (w.start - base),
260
+ });
261
+ }
262
+ first = last + 1;
263
+ }
264
+ return { buffers, placed };
265
+ }
266
+
267
+ /**
268
+ * The pure planner of spec 4.2: arena (hot prefix unless `need` names a cold segment and the full arena fits) ->
269
+ * perArray -> windowed, in that order. `need` always contains "rowPtr" (it is added when absent); absent arrays
270
+ * (unweighted `weights`, identity permutations, zero-length arrays) are dropped; a snapshot with arcCount === 0
271
+ * plans rowPtr only. The arena path needs `bytes <= maxBufferSize` and every needed segment within both limits;
272
+ * ArenaPlan.segments lists every non-null segment inside the uploaded bytes (a needed cold segment outside them is
273
+ * null: the residency uploads it as its own buffer). The perArray path needs every needed array within both limits;
274
+ * otherwise the arc-indexed arrays are windowed (rowPtr and edgeToArc must fit, else E_TOO_LARGE).
275
+ * @param s - the snapshot (only arena, the counts, the flags and rowPtr are read)
276
+ * @param caps - the device capabilities (only `limits` is read)
277
+ * @param need - the core arrays the kernel binds
278
+ * @returns the plan
279
+ */
280
+ export function planUpload(s: GraphSnapshot, caps: PlanCaps, need: readonly CoreArrayName[]): UploadPlan {
281
+ const { limits } = caps;
282
+ const binding = limits.maxStorageBufferBindingSize;
283
+ const bufferLimit = limits.maxBufferSize;
284
+ const fits = (bytes: number): boolean => bytes <= binding && bytes <= bufferLimit;
285
+ const violated = (bytes: number): number => (bytes > binding ? binding : bufferLimit);
286
+ const lengths = coreByteLengths(s);
287
+ const names = neededNames(need, lengths);
288
+ if (!fits(lengths.rowPtr)) {
289
+ throw tooLarge(lengths.rowPtr, violated(lengths.rowPtr), "rowPtr");
290
+ }
291
+ const { arena } = s;
292
+ if (arena !== null) {
293
+ const coldNeeded = names.some(
294
+ (name) => (name === "arcToEdge" || name === "edgeToArc") && arena.segments[name] !== null,
295
+ );
296
+ const includesCold = coldNeeded && arena.byteLength <= bufferLimit;
297
+ const bytes = includesCold ? arena.byteLength : arena.hotByteLength;
298
+ const bindable = names.every((name) => {
299
+ const segment = arena.segments[name];
300
+ return segment === null || fits(segment.byteLength);
301
+ });
302
+ if (bytes <= bufferLimit && bindable) {
303
+ const segmentOf = (name: CoreArrayName): { readonly offset: number; readonly size: number } | null => {
304
+ const segment = arena.segments[name];
305
+ if (segment === null) {
306
+ return null;
307
+ }
308
+ const offset = segment.byteOffset - arena.byteOffset;
309
+ return offset + segment.byteLength <= bytes ? { offset, size: segment.byteLength } : null;
310
+ };
311
+ return {
312
+ kind: "arena",
313
+ bytes,
314
+ includesCold,
315
+ segments: {
316
+ rowPtr: segmentOf("rowPtr"),
317
+ colIdx: segmentOf("colIdx"),
318
+ weights: segmentOf("weights"),
319
+ arcToEdge: segmentOf("arcToEdge"),
320
+ edgeToArc: segmentOf("edgeToArc"),
321
+ },
322
+ };
323
+ }
324
+ }
325
+ if (names.every((name) => fits(lengths[name]))) {
326
+ return {
327
+ kind: "perArray",
328
+ arrays: names.map((name) => ({
329
+ name,
330
+ byteLength: lengths[name],
331
+ buffers: [{ byteOffset: 0, byteLength: lengths[name] }],
332
+ })),
333
+ };
334
+ }
335
+ if (names.includes("edgeToArc") && !fits(lengths.edgeToArc)) {
336
+ throw tooLarge(lengths.edgeToArc, violated(lengths.edgeToArc), "binding");
337
+ }
338
+ const arcsPerWindow = Math.min(arcsPerWindowFor(limits), alignDown(Math.floor(bufferLimit / 4)));
339
+ if (arcsPerWindow < ARC_WINDOW_ALIGN) {
340
+ throw tooLarge(4 * ARC_WINDOW_ALIGN, binding, "binding");
341
+ }
342
+ const windows = planArcWindows(s.rowPtr, s.arcCount, arcsPerWindow);
343
+ const { buffers, placed } = placeWindows(windows, 4 * s.arcCount, bufferLimit);
344
+ const arrays: PlannedArray[] = names.map((name) =>
345
+ ARC_INDEXED.has(name)
346
+ ? { name, byteLength: lengths[name], buffers }
347
+ : { name, byteLength: lengths[name], buffers: [{ byteOffset: 0, byteLength: lengths[name] }] },
348
+ );
349
+ return { kind: "windowed", arrays, windows: placed, arcsPerWindow };
350
+ }
@@ -0,0 +1,230 @@
1
+ /// <reference types="@webgpu/types" preserve="true" />
2
+ /**
3
+ * The ./node entry (spec 2.1, 2.3, 3.4; contract 3.7): Dawn through the optional peer dependency `webgpu`. The
4
+ * ONLY file of the package that names the "webgpu" module, and only inside `await import("webgpu")` in a
5
+ * function body, so the root and browser bundles never carry the specifier (spec 2.5; test/build-output.test.ts,
6
+ * test/layers.test.ts). P0-T3 wrote createNodeGpu, dawnFlags and their types; P1-T1 adds createNodeGpuContext,
7
+ * probeNodeWebGpu and the `extends Omit<GpuContextOptions, ...>` clause of NodeGpuOptions.
8
+ *
9
+ * Rule of spec 2.5 item 5 (also in CLAUDE.md): "./node" is imported only by Node entry points and tests.
10
+ */
11
+
12
+ import { GpuContext } from "../context.js";
13
+ import { WebGpuGraphError } from "../errors.js";
14
+ import type { GpuContextOptions, ProbeResult } from "../types/context.js";
15
+
16
+ /** Options of the Node helpers (spec 3.4). */
17
+ export interface NodeGpuOptions extends Omit<GpuContextOptions, "gpu" | "adapter" | "device" | "runtime"> {
18
+ /** Dawn `adapter=<substring>`: a substring of the adapter name (`llvmpipe`, `4070`); an empty string is ignored; wins over `software`. */
19
+ readonly adapter?: string | undefined;
20
+ /** Dawn `backend=<name>`; `null` compiles pipelines and runs nothing (spec 5.1; the compile tests use it). */
21
+ readonly backend?: "vulkan" | "d3d12" | "d3d11" | "metal" | "opengl" | "opengles" | "null" | undefined;
22
+ /** Dawn toggles, emitted as `enable-dawn-features=a,b`. */
23
+ readonly dawnFeatures?: readonly string[] | undefined;
24
+ /** Shorthand for `adapter=llvmpipe` (Linux / Mesa specific, spec 2.3); ignored when `adapter` is given. */
25
+ readonly software?: boolean | undefined;
26
+ /** Install dawn.globals on globalThis (default true; spec 2.1 rule 2) so user code sees GPUBufferUsage and friends. */
27
+ readonly installGlobals?: boolean | undefined;
28
+ /**
29
+ * Test seam: replaces `() => import("webgpu")` so a missing / broken module can be simulated.
30
+ * @internal
31
+ */
32
+ readonly loadModule?: (() => Promise<unknown>) | undefined;
33
+ }
34
+
35
+ /** The Dawn GPU handle (spec 2.3): dispose() drops the reference so the process can exit. */
36
+ export interface NodeGpuHandle {
37
+ /** The GPU of `dawn.create(flags)`; reading it after dispose() throws E_DISPOSED. */
38
+ readonly gpu: GPU;
39
+ /** Drops the GPU reference; idempotent. */
40
+ dispose(): void;
41
+ }
42
+
43
+ /** The install hint of E_NO_WEBGPU (spec 2.5 item 4); P-ENV re-pins the version together with the devDependency. */
44
+ const INSTALL_HINT = "install the optional peer dependency webgpu@0.4.0";
45
+
46
+ /** The shape of the `webgpu` module (its types.d.ts: create(options: string[]): GPU; globals: Object). */
47
+ interface DawnModule {
48
+ create(options: string[]): GPU;
49
+ globals?: unknown;
50
+ }
51
+
52
+ /**
53
+ * Whether a loaded module is usable as Dawn.
54
+ * @param loaded - the module namespace
55
+ * @returns true when it has a create() function
56
+ */
57
+ function isDawnModule(loaded: unknown): loaded is DawnModule {
58
+ return (
59
+ typeof loaded === "object" && loaded !== null && typeof (loaded as { create?: unknown }).create === "function"
60
+ );
61
+ }
62
+
63
+ /**
64
+ * The message of a thrown value.
65
+ * @param err - what was caught
66
+ * @returns the Error message, or the value as a string
67
+ */
68
+ function messageOf(err: unknown): string {
69
+ return err instanceof Error ? err.message : String(err);
70
+ }
71
+
72
+ /** The handle returned by createNodeGpu(): `gpu` is a getter so dispose() can drop the reference. */
73
+ class DawnHandle implements NodeGpuHandle {
74
+ private ref: GPU | null;
75
+
76
+ /**
77
+ * Wraps a GPU object.
78
+ * @param gpu - the object dawn.create() returned
79
+ */
80
+ constructor(gpu: GPU) {
81
+ this.ref = gpu;
82
+ }
83
+
84
+ /**
85
+ * The GPU object; E_DISPOSED after dispose().
86
+ * @returns the GPU object
87
+ */
88
+ get gpu(): GPU {
89
+ if (this.ref === null) {
90
+ throw new WebGpuGraphError("E_DISPOSED", "the Dawn GPU handle was disposed", { label: "NodeGpuHandle" });
91
+ }
92
+ return this.ref;
93
+ }
94
+
95
+ /** Drops the reference; idempotent. */
96
+ dispose(): void {
97
+ this.ref = null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * The Dawn flag list a NodeGpuOptions maps to (exported for the tests and scripts/gpu-report.js):
103
+ * adapter=<s>, backend=<s>, enable-dawn-features=a,b, and software -> adapter=llvmpipe; an explicit non-empty
104
+ * `adapter` wins over `software`; an empty feature list emits nothing.
105
+ * @param options - the Node options, or undefined for no flags
106
+ * @returns the strings for dawn.create(), in the order adapter, backend, features
107
+ */
108
+ export function dawnFlags(options: NodeGpuOptions | undefined): string[] {
109
+ const flags: string[] = [];
110
+ if (options === undefined) {
111
+ return flags;
112
+ }
113
+ if (options.adapter !== undefined && options.adapter !== "") {
114
+ flags.push(`adapter=${options.adapter}`);
115
+ } else if (options.software === true) {
116
+ flags.push("adapter=llvmpipe");
117
+ }
118
+ if (options.backend !== undefined) {
119
+ flags.push(`backend=${options.backend}`);
120
+ }
121
+ if (options.dawnFeatures !== undefined && options.dawnFeatures.length > 0) {
122
+ flags.push(`enable-dawn-features=${options.dawnFeatures.join(",")}`);
123
+ }
124
+ return flags;
125
+ }
126
+
127
+ /**
128
+ * import("webgpu"), install dawn.globals unless installGlobals === false, dawn.create(flags) (spec 2.3).
129
+ * @param options - adapter / backend / dawnFeatures / software / installGlobals (and the test seam)
130
+ * @returns the handle; `dispose()` drops the GPU reference so the process can exit
131
+ * @throws WebGpuGraphError E_NO_WEBGPU { reason, hint } when the module does not load (missing, or its glibc is too old), has no create(), or create(flags) throws
132
+ */
133
+ export async function createNodeGpu(options?: NodeGpuOptions): Promise<NodeGpuHandle> {
134
+ const load = options?.loadModule ?? ((): Promise<unknown> => import("webgpu"));
135
+ let loaded: unknown;
136
+ try {
137
+ loaded = await load();
138
+ } catch (err) {
139
+ const reason = messageOf(err);
140
+ throw new WebGpuGraphError(
141
+ "E_NO_WEBGPU",
142
+ `the webgpu (Dawn) native module did not load: ${reason}; ${INSTALL_HINT}`,
143
+ { reason, hint: INSTALL_HINT },
144
+ );
145
+ }
146
+ if (!isDawnModule(loaded)) {
147
+ const reason = "the webgpu module exports no create() function";
148
+ throw new WebGpuGraphError("E_NO_WEBGPU", `${reason}; ${INSTALL_HINT}`, { reason, hint: INSTALL_HINT });
149
+ }
150
+ if (options?.installGlobals !== false && typeof loaded.globals === "object" && loaded.globals !== null) {
151
+ Object.assign(globalThis, loaded.globals);
152
+ }
153
+ let gpu: GPU;
154
+ try {
155
+ gpu = loaded.create(dawnFlags(options));
156
+ } catch (err) {
157
+ const reason = `dawn.create() threw: ${messageOf(err)}`;
158
+ throw new WebGpuGraphError("E_NO_WEBGPU", `${reason}; ${INSTALL_HINT}`, { reason, hint: INSTALL_HINT });
159
+ }
160
+ return new DawnHandle(gpu);
161
+ }
162
+
163
+ /**
164
+ * The GpuContextOptions part of a NodeGpuOptions, copied key by key so the Node-only `adapter` string never
165
+ * reaches GpuContext.create as a GPUAdapter.
166
+ * @param options - the Node options
167
+ * @returns the context options without gpu / adapter / device / runtime
168
+ */
169
+ function contextOptionsOf(options: NodeGpuOptions): Omit<GpuContextOptions, "gpu" | "adapter" | "device" | "runtime"> {
170
+ return {
171
+ powerPreference: options.powerPreference,
172
+ rejectSoftware: options.rejectSoftware,
173
+ limits: options.limits,
174
+ optionalFeatures: options.optionalFeatures,
175
+ requiredFeatures: options.requiredFeatures,
176
+ label: options.label,
177
+ onError: options.onError,
178
+ warnUnreleasedSnapshots: options.warnUnreleasedSnapshots,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * createNodeGpu + GpuContext.create({ gpu, runtime: "node", ...options }); ctx.dispose() also disposes the
184
+ * handle, and a create() failure disposes it before rethrowing. The handle is dropped once `ctx.lost` has
185
+ * settled, never before: under webgpu@0.4.0 a GPU object collected while the device it created is still
186
+ * tearing down (its lost / work-done callbacks in flight) crashes or deadlocks the process (PLAN DECISION,
187
+ * P1-T1; measured with tmp/p1t1/gc-race2.mjs), and device.destroy() reports the loss right away.
188
+ * @param options - the Node options
189
+ * @returns the context
190
+ */
191
+ export async function createNodeGpuContext(options?: NodeGpuOptions): Promise<GpuContext> {
192
+ const handle = await createNodeGpu(options);
193
+ let ctx: GpuContext;
194
+ try {
195
+ ctx = await GpuContext.create({ ...contextOptionsOf(options ?? {}), gpu: handle.gpu, runtime: "node" });
196
+ } catch (err) {
197
+ handle.dispose();
198
+ throw err;
199
+ }
200
+ const { lost } = ctx;
201
+ ctx.attachDisposer(() => {
202
+ void lost.then(() => {
203
+ handle.dispose();
204
+ });
205
+ });
206
+ return ctx;
207
+ }
208
+
209
+ /**
210
+ * createNodeGpu + GpuContext.probe + dispose; never throws (a load failure is { code: "E_NO_WEBGPU" }).
211
+ * @param options - the Node options
212
+ * @returns the probe result
213
+ */
214
+ export async function probeNodeWebGpu(options?: NodeGpuOptions): Promise<ProbeResult> {
215
+ let handle: NodeGpuHandle;
216
+ try {
217
+ handle = await createNodeGpu(options);
218
+ } catch (err) {
219
+ return { ok: false, code: "E_NO_WEBGPU", reason: messageOf(err), adapter: null, summary: null };
220
+ }
221
+ try {
222
+ return await GpuContext.probe({
223
+ gpu: handle.gpu,
224
+ powerPreference: options?.powerPreference ?? "high-performance",
225
+ rejectSoftware: options?.rejectSoftware,
226
+ });
227
+ } finally {
228
+ handle.dispose();
229
+ }
230
+ }