@graphty/webgpu-graph-algorithms 0.5.0 → 0.6.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 (264) hide show
  1. package/README.md +104 -52
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-CRbw2Wyo.js → context-BXqgCifx.js} +225 -33
  4. package/dist/chunks/context-BXqgCifx.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +12 -10
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +32 -10
  9. package/dist/src/accelerator.js.map +1 -1
  10. package/dist/src/algorithms/components.d.ts.map +1 -1
  11. package/dist/src/algorithms/components.js +12 -13
  12. package/dist/src/algorithms/components.js.map +1 -1
  13. package/dist/src/algorithms/degree.d.ts +6 -8
  14. package/dist/src/algorithms/degree.d.ts.map +1 -1
  15. package/dist/src/algorithms/degree.js +58 -35
  16. package/dist/src/algorithms/degree.js.map +1 -1
  17. package/dist/src/algorithms/pagerank.d.ts.map +1 -1
  18. package/dist/src/algorithms/pagerank.js +16 -14
  19. package/dist/src/algorithms/pagerank.js.map +1 -1
  20. package/dist/src/algorithms/power-iteration.d.ts +2 -2
  21. package/dist/src/algorithms/power-iteration.d.ts.map +1 -1
  22. package/dist/src/algorithms/power-iteration.js +17 -14
  23. package/dist/src/algorithms/power-iteration.js.map +1 -1
  24. package/dist/src/constants.d.ts +85 -8
  25. package/dist/src/constants.d.ts.map +1 -1
  26. package/dist/src/constants.js +85 -8
  27. package/dist/src/constants.js.map +1 -1
  28. package/dist/src/errors.d.ts +3 -2
  29. package/dist/src/errors.d.ts.map +1 -1
  30. package/dist/src/errors.js +2 -1
  31. package/dist/src/errors.js.map +1 -1
  32. package/dist/src/index.d.ts +10 -5
  33. package/dist/src/index.d.ts.map +1 -1
  34. package/dist/src/index.js +14 -5
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/kernel/dispatch.d.ts +8 -3
  37. package/dist/src/kernel/dispatch.d.ts.map +1 -1
  38. package/dist/src/kernel/dispatch.js +18 -7
  39. package/dist/src/kernel/dispatch.js.map +1 -1
  40. package/dist/src/kernel/kernel.d.ts +30 -1
  41. package/dist/src/kernel/kernel.d.ts.map +1 -1
  42. package/dist/src/kernel/kernel.js +49 -5
  43. package/dist/src/kernel/kernel.js.map +1 -1
  44. package/dist/src/kernel/prelude.d.ts.map +1 -1
  45. package/dist/src/kernel/prelude.js +9 -1
  46. package/dist/src/kernel/prelude.js.map +1 -1
  47. package/dist/src/kernel/profiler.d.ts +15 -3
  48. package/dist/src/kernel/profiler.d.ts.map +1 -1
  49. package/dist/src/kernel/profiler.js +27 -4
  50. package/dist/src/kernel/profiler.js.map +1 -1
  51. package/dist/src/kernels.d.ts +18 -8
  52. package/dist/src/kernels.d.ts.map +1 -1
  53. package/dist/src/kernels.js +345 -22
  54. package/dist/src/kernels.js.map +1 -1
  55. package/dist/src/layouts/calibrate.d.ts +51 -0
  56. package/dist/src/layouts/calibrate.d.ts.map +1 -0
  57. package/dist/src/layouts/calibrate.js +172 -0
  58. package/dist/src/layouts/calibrate.js.map +1 -0
  59. package/dist/src/layouts/force-simulation.d.ts +42 -5
  60. package/dist/src/layouts/force-simulation.d.ts.map +1 -1
  61. package/dist/src/layouts/force-simulation.js +84 -22
  62. package/dist/src/layouts/force-simulation.js.map +1 -1
  63. package/dist/src/layouts/forceatlas2.d.ts +107 -38
  64. package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
  65. package/dist/src/layouts/forceatlas2.js +297 -290
  66. package/dist/src/layouts/forceatlas2.js.map +1 -1
  67. package/dist/src/layouts/fruchterman-reingold.d.ts +241 -0
  68. package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -0
  69. package/dist/src/layouts/fruchterman-reingold.js +739 -0
  70. package/dist/src/layouts/fruchterman-reingold.js.map +1 -0
  71. package/dist/src/layouts/model-common.d.ts +140 -0
  72. package/dist/src/layouts/model-common.d.ts.map +1 -0
  73. package/dist/src/layouts/model-common.js +269 -0
  74. package/dist/src/layouts/model-common.js.map +1 -0
  75. package/dist/src/layouts/repulsion-grid.d.ts +152 -0
  76. package/dist/src/layouts/repulsion-grid.d.ts.map +1 -0
  77. package/dist/src/layouts/repulsion-grid.js +318 -0
  78. package/dist/src/layouts/repulsion-grid.js.map +1 -0
  79. package/dist/src/layouts/spring-electrical.d.ts +224 -0
  80. package/dist/src/layouts/spring-electrical.d.ts.map +1 -0
  81. package/dist/src/layouts/spring-electrical.js +665 -0
  82. package/dist/src/layouts/spring-electrical.js.map +1 -0
  83. package/dist/src/memory/residency.d.ts +6 -2
  84. package/dist/src/memory/residency.d.ts.map +1 -1
  85. package/dist/src/memory/residency.js +84 -14
  86. package/dist/src/memory/residency.js.map +1 -1
  87. package/dist/src/primitives/core-shape.d.ts +38 -2
  88. package/dist/src/primitives/core-shape.d.ts.map +1 -1
  89. package/dist/src/primitives/core-shape.js +71 -3
  90. package/dist/src/primitives/core-shape.js.map +1 -1
  91. package/dist/src/primitives/grid-pyramid.d.ts +71 -0
  92. package/dist/src/primitives/grid-pyramid.d.ts.map +1 -0
  93. package/dist/src/primitives/grid-pyramid.js +143 -0
  94. package/dist/src/primitives/grid-pyramid.js.map +1 -0
  95. package/dist/src/primitives/grid.d.ts +118 -0
  96. package/dist/src/primitives/grid.d.ts.map +1 -0
  97. package/dist/src/primitives/grid.js +225 -0
  98. package/dist/src/primitives/grid.js.map +1 -0
  99. package/dist/src/primitives/histogram.d.ts +67 -0
  100. package/dist/src/primitives/histogram.d.ts.map +1 -0
  101. package/dist/src/primitives/histogram.js +190 -0
  102. package/dist/src/primitives/histogram.js.map +1 -0
  103. package/dist/src/primitives/radix-sort.d.ts +75 -0
  104. package/dist/src/primitives/radix-sort.d.ts.map +1 -0
  105. package/dist/src/primitives/radix-sort.js +168 -0
  106. package/dist/src/primitives/radix-sort.js.map +1 -0
  107. package/dist/src/primitives/scan.d.ts +44 -0
  108. package/dist/src/primitives/scan.d.ts.map +1 -0
  109. package/dist/src/primitives/scan.js +151 -0
  110. package/dist/src/primitives/scan.js.map +1 -0
  111. package/dist/src/primitives/segmented-reduce.d.ts +25 -17
  112. package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
  113. package/dist/src/primitives/segmented-reduce.js +166 -47
  114. package/dist/src/primitives/segmented-reduce.js.map +1 -1
  115. package/dist/src/primitives/spmv.d.ts +18 -14
  116. package/dist/src/primitives/spmv.d.ts.map +1 -1
  117. package/dist/src/primitives/spmv.js +94 -58
  118. package/dist/src/primitives/spmv.js.map +1 -1
  119. package/dist/src/primitives/verify.d.ts +49 -0
  120. package/dist/src/primitives/verify.d.ts.map +1 -0
  121. package/dist/src/primitives/verify.js +229 -0
  122. package/dist/src/primitives/verify.js.map +1 -0
  123. package/dist/src/types/accelerator.d.ts +7 -3
  124. package/dist/src/types/accelerator.d.ts.map +1 -1
  125. package/dist/src/types/context.d.ts +53 -0
  126. package/dist/src/types/context.d.ts.map +1 -1
  127. package/dist/src/types/layout.d.ts +52 -0
  128. package/dist/src/types/layout.d.ts.map +1 -1
  129. package/dist/src/types/options.d.ts +43 -1
  130. package/dist/src/types/options.d.ts.map +1 -1
  131. package/dist/src/wgsl/counting-scatter.wgsl.d.ts +8 -0
  132. package/dist/src/wgsl/counting-scatter.wgsl.d.ts.map +1 -0
  133. package/dist/src/wgsl/counting-scatter.wgsl.js +17 -0
  134. package/dist/src/wgsl/counting-scatter.wgsl.js.map +1 -0
  135. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +23 -8
  136. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
  137. package/dist/src/wgsl/fa2-attraction.wgsl.js +100 -17
  138. package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
  139. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +7 -2
  140. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -1
  141. package/dist/src/wgsl/fa2-integrate.wgsl.js +28 -2
  142. package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -1
  143. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +4 -2
  144. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -1
  145. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +14 -5
  146. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -1
  147. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +12 -1
  148. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
  149. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +54 -0
  150. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
  151. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts +8 -0
  152. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts.map +1 -0
  153. package/dist/src/wgsl/grid-cell-key.wgsl.js +30 -0
  154. package/dist/src/wgsl/grid-cell-key.wgsl.js.map +1 -0
  155. package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts +8 -0
  156. package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts.map +1 -0
  157. package/dist/src/wgsl/grid-centroid-hub.wgsl.js +29 -0
  158. package/dist/src/wgsl/grid-centroid-hub.wgsl.js.map +1 -0
  159. package/dist/src/wgsl/grid-centroid.wgsl.d.ts +8 -0
  160. package/dist/src/wgsl/grid-centroid.wgsl.d.ts.map +1 -0
  161. package/dist/src/wgsl/grid-centroid.wgsl.js +29 -0
  162. package/dist/src/wgsl/grid-centroid.wgsl.js.map +1 -0
  163. package/dist/src/wgsl/grid-downsample.wgsl.d.ts +7 -0
  164. package/dist/src/wgsl/grid-downsample.wgsl.d.ts.map +1 -0
  165. package/dist/src/wgsl/grid-downsample.wgsl.js +28 -0
  166. package/dist/src/wgsl/grid-downsample.wgsl.js.map +1 -0
  167. package/dist/src/wgsl/grid-far-field.wgsl.d.ts +13 -0
  168. package/dist/src/wgsl/grid-far-field.wgsl.d.ts.map +1 -0
  169. package/dist/src/wgsl/grid-far-field.wgsl.js +98 -0
  170. package/dist/src/wgsl/grid-far-field.wgsl.js.map +1 -0
  171. package/dist/src/wgsl/grid-near-field.wgsl.d.ts +19 -0
  172. package/dist/src/wgsl/grid-near-field.wgsl.d.ts.map +1 -0
  173. package/dist/src/wgsl/grid-near-field.wgsl.js +129 -0
  174. package/dist/src/wgsl/grid-near-field.wgsl.js.map +1 -0
  175. package/dist/src/wgsl/histogram.wgsl.d.ts +7 -0
  176. package/dist/src/wgsl/histogram.wgsl.d.ts.map +1 -0
  177. package/dist/src/wgsl/histogram.wgsl.js +15 -0
  178. package/dist/src/wgsl/histogram.wgsl.js.map +1 -0
  179. package/dist/src/wgsl/indirect-finalize.wgsl.d.ts +8 -0
  180. package/dist/src/wgsl/indirect-finalize.wgsl.d.ts.map +1 -0
  181. package/dist/src/wgsl/indirect-finalize.wgsl.js +26 -0
  182. package/dist/src/wgsl/indirect-finalize.wgsl.js.map +1 -0
  183. package/dist/src/wgsl/radix-hist.wgsl.d.ts +9 -0
  184. package/dist/src/wgsl/radix-hist.wgsl.d.ts.map +1 -0
  185. package/dist/src/wgsl/radix-hist.wgsl.js +31 -0
  186. package/dist/src/wgsl/radix-hist.wgsl.js.map +1 -0
  187. package/dist/src/wgsl/radix-scatter.wgsl.d.ts +9 -0
  188. package/dist/src/wgsl/radix-scatter.wgsl.d.ts.map +1 -0
  189. package/dist/src/wgsl/radix-scatter.wgsl.js +40 -0
  190. package/dist/src/wgsl/radix-scatter.wgsl.js.map +1 -0
  191. package/dist/src/wgsl/scan-add.wgsl.d.ts +6 -0
  192. package/dist/src/wgsl/scan-add.wgsl.d.ts.map +1 -0
  193. package/dist/src/wgsl/scan-add.wgsl.js +14 -0
  194. package/dist/src/wgsl/scan-add.wgsl.js.map +1 -0
  195. package/dist/src/wgsl/scan-block.wgsl.d.ts +8 -0
  196. package/dist/src/wgsl/scan-block.wgsl.d.ts.map +1 -0
  197. package/dist/src/wgsl/scan-block.wgsl.js +30 -0
  198. package/dist/src/wgsl/scan-block.wgsl.js.map +1 -0
  199. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +22 -8
  200. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -1
  201. package/dist/src/wgsl/segmented-reduce.wgsl.js +84 -15
  202. package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -1
  203. package/dist/src/wgsl/spmv-pull.wgsl.d.ts +22 -11
  204. package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -1
  205. package/dist/src/wgsl/spmv-pull.wgsl.js +110 -36
  206. package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -1
  207. package/dist/tsconfig.build.tsbuildinfo +1 -1
  208. package/dist/webgpu-graph-algorithms.js +5016 -1130
  209. package/dist/webgpu-graph-algorithms.js.map +1 -1
  210. package/package.json +10 -7
  211. package/src/accelerator.ts +46 -12
  212. package/src/algorithms/components.ts +12 -16
  213. package/src/algorithms/degree.ts +58 -43
  214. package/src/algorithms/pagerank.ts +20 -18
  215. package/src/algorithms/power-iteration.ts +19 -18
  216. package/src/constants.ts +108 -8
  217. package/src/errors.ts +3 -1
  218. package/src/index.ts +25 -5
  219. package/src/kernel/dispatch.ts +18 -7
  220. package/src/kernel/kernel.ts +59 -5
  221. package/src/kernel/prelude.ts +15 -0
  222. package/src/kernel/profiler.ts +28 -4
  223. package/src/kernels.ts +378 -24
  224. package/src/layouts/calibrate.ts +187 -0
  225. package/src/layouts/force-simulation.ts +111 -26
  226. package/src/layouts/forceatlas2.ts +346 -324
  227. package/src/layouts/fruchterman-reingold.ts +918 -0
  228. package/src/layouts/model-common.ts +323 -0
  229. package/src/layouts/repulsion-grid.ts +451 -0
  230. package/src/layouts/spring-electrical.ts +845 -0
  231. package/src/memory/residency.ts +126 -20
  232. package/src/primitives/core-shape.ts +91 -4
  233. package/src/primitives/grid-pyramid.ts +221 -0
  234. package/src/primitives/grid.ts +349 -0
  235. package/src/primitives/histogram.ts +273 -0
  236. package/src/primitives/radix-sort.ts +246 -0
  237. package/src/primitives/scan.ts +197 -0
  238. package/src/primitives/segmented-reduce.ts +214 -56
  239. package/src/primitives/spmv.ts +125 -65
  240. package/src/primitives/verify.ts +249 -0
  241. package/src/types/accelerator.ts +15 -3
  242. package/src/types/context.ts +56 -0
  243. package/src/types/layout.ts +58 -0
  244. package/src/types/options.ts +45 -1
  245. package/src/wgsl/counting-scatter.wgsl.ts +16 -0
  246. package/src/wgsl/fa2-attraction.wgsl.ts +100 -17
  247. package/src/wgsl/fa2-integrate.wgsl.ts +28 -2
  248. package/src/wgsl/fa2-repulsion-exact.wgsl.ts +14 -5
  249. package/src/wgsl/fa2-stats-finalize.wgsl.ts +54 -0
  250. package/src/wgsl/grid-cell-key.wgsl.ts +29 -0
  251. package/src/wgsl/grid-centroid-hub.wgsl.ts +28 -0
  252. package/src/wgsl/grid-centroid.wgsl.ts +28 -0
  253. package/src/wgsl/grid-downsample.wgsl.ts +27 -0
  254. package/src/wgsl/grid-far-field.wgsl.ts +97 -0
  255. package/src/wgsl/grid-near-field.wgsl.ts +128 -0
  256. package/src/wgsl/histogram.wgsl.ts +14 -0
  257. package/src/wgsl/indirect-finalize.wgsl.ts +25 -0
  258. package/src/wgsl/radix-hist.wgsl.ts +30 -0
  259. package/src/wgsl/radix-scatter.wgsl.ts +39 -0
  260. package/src/wgsl/scan-add.wgsl.ts +13 -0
  261. package/src/wgsl/scan-block.wgsl.ts +29 -0
  262. package/src/wgsl/segmented-reduce.wgsl.ts +84 -15
  263. package/src/wgsl/spmv-pull.wgsl.ts +110 -36
  264. package/dist/chunks/context-CRbw2Wyo.js.map +0 -1
@@ -0,0 +1,249 @@
1
+ /**
2
+ * The device self-check: one real exclusive scan of known numbers, run once per device before this package
3
+ * computes anything on it, so a device that returns wrong numbers is refused instead of believed.
4
+ *
5
+ * WHY IT EXISTS. On the Windows host lane -- Dawn's D3D12 backend over the Microsoft Basic Render Driver, the
6
+ * software renderer built into Windows -- every exclusive scan of more than one workgroup came back wrong: a
7
+ * block total of 1 where 256 belonged, or a second block nothing wrote at all, differing between two runs of
8
+ * identical code in one job. Everything built on the scan was wrong with it (the histogram, the radix sort, the
9
+ * grid build and every layout above them, eighteen test files), while every single-block scan and every earlier
10
+ * primitive stayed right. Three shader repairs failed; copying Microsoft's current redistributable renderer
11
+ * beside the runner's Node executable made all of it pass untouched. The defect is the driver's, and the danger
12
+ * to a caller is not a crash but silence: a graph laid out from wrong numbers with no error anywhere.
13
+ *
14
+ * WHY IT SCANS RATHER THAN PROBING A CONSTRUCT. An earlier hand-written probe matrix of workgroup constructs
15
+ * (test/primitives/workgroup-id-probe.test.ts) was measured PASSING on that renderer in the same job in which the
16
+ * real scan returned the wrong answer, so a copy of the shader's shape is not evidence about the shader. This
17
+ * check therefore drives the shipped `prepareScan` through the shipped composer and pipeline cache: it tests the
18
+ * code that runs, and it cannot drift away from it. One scan covers both recorded failures -- a value crossing
19
+ * `workgroupBarrier` inside a block, and a block total crossing from one dispatch of a compute pass to the next
20
+ * dispatch that reads it, which is the shape of every driver in this package.
21
+ *
22
+ * WHY IT REFUSES RATHER THAN ROUTING AROUND THE DEFECT. See docs/decisions/device-self-check.md: a barrier-free second
23
+ * implementation was designed and costed, and rejected because it would ship unverified on the only platform it
24
+ * exists for.
25
+ *
26
+ * WHERE IT RUNS. Lazily, at the first compute entry point a caller reaches, memoised per GPUDevice: the
27
+ * algorithms (degree, connectedComponents, the power-iteration family) and calibrateLayout await it on entry, and
28
+ * a layout simulation awaits it in the compile-and-bind promise `load()` starts -- the promise every batch and
29
+ * every debug run already awaits before it may submit. So a caller is covered whether the context was created,
30
+ * adopted through `create({ device })` or adopted through `GpuContext.from`, and a simulation refuses before its
31
+ * first iteration without the check sitting on the per-batch path, where an extra await would move the moment a
32
+ * submission becomes visible to a step() issued in the same tick and so change how the frame loop coalesces.
33
+ * It deliberately does NOT run in GpuContext.create:
34
+ * a context that only compiles -- Dawn's null backend, which computes nothing by design -- must stay usable, and
35
+ * `src/context.ts` may not import a primitive (the layer rule of spec 3.2).
36
+ */
37
+
38
+ import { WebGpuGraphError } from "../errors.js";
39
+ import { UniformRing } from "../kernel/uniform-ring.js";
40
+ import { type CheckedContext, type DeviceCheck, type DeviceCheckMismatch } from "../types/context.js";
41
+ import { type Binding } from "../types/memory.js";
42
+ import { type ReduceScope } from "./reduce.js";
43
+ import { prepareScan } from "./scan.js";
44
+
45
+ /**
46
+ * The word written into every output slot before the dispatch, so a word NOTHING wrote is distinguishable from
47
+ * a word written wrongly. The Windows lane returned this value from the second block, which is how the failure
48
+ * was read as "the block was never written" rather than "the block was miscomputed".
49
+ */
50
+ const POISON = 0xdeadbeef;
51
+
52
+ /** Full workgroups the check scans; one partial workgroup of a single word follows them. */
53
+ const CHECK_BLOCKS = 32;
54
+
55
+ /** Params slots the scan's levels need (two levels of block scan plus one add-back, with room to spare). */
56
+ const RING_SLOTS = 8;
57
+
58
+ /** The settled check per device: the one work a second caller never repeats. A rejection is dropped, so a check that failed to RUN is retried. */
59
+ const checked = new WeakMap<GPUDevice, Promise<DeviceCheck>>();
60
+
61
+ /**
62
+ * The input word at `i`: 1, 2, 3, ... so no two workgroups have the same block total (all-ones would give every
63
+ * block the same one, and a block total stored at the wrong index would still read correct -- which is exactly
64
+ * the Windows symptom) and every output word is a different number.
65
+ * @param i - the word index
66
+ * @returns the input value
67
+ */
68
+ function inputAt(i: number): number {
69
+ return i + 1;
70
+ }
71
+
72
+ /**
73
+ * The verdict: the first output word or total that disagrees with the arithmetic answer, or null when nothing
74
+ * does. Pure, so test/primitives/verify.test.ts can drive it past a fabricated wrong result and exercise the
75
+ * refusal without a broken device.
76
+ * @param words - the `count` output words read back from the device
77
+ * @param total - the total word read back from the device
78
+ * @param count - the words scanned
79
+ * @returns the first disagreement, or null
80
+ * @internal
81
+ */
82
+ export function checkScanWords(words: Uint32Array, total: number, count: number): DeviceCheckMismatch | null {
83
+ let running = 0;
84
+ for (let i = 0; i < count; i++) {
85
+ const got = words[i];
86
+ if (got !== running) {
87
+ return { where: `out[${String(i)}]`, expected: running, actual: got, poison: got === POISON };
88
+ }
89
+ // u32 addition wraps in WGSL, so the host oracle wraps too: at WORKGROUP_SIZE 256 the total cannot
90
+ // reach 2^32, but a false refusal would deny every device on earth, so it is not left to that constant
91
+ running = (running + inputAt(i)) >>> 0;
92
+ }
93
+ if (total !== running) {
94
+ return { where: "total", expected: running, actual: total, poison: total === POISON };
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Binds a whole scratch buffer.
101
+ * @param buffer - the buffer
102
+ * @param size - the bytes bound
103
+ * @returns the binding
104
+ */
105
+ function whole(buffer: GPUBuffer, size: number): Binding {
106
+ return { buffer, offset: 0, size, window: null };
107
+ }
108
+
109
+ /**
110
+ * Runs the check once: one scan of `CHECK_BLOCKS * workgroupSize + 1` words recorded into one compute pass, one
111
+ * submit, the output and the total read back and verified on the host.
112
+ *
113
+ * It never throws for a wrong ANSWER -- that is the returned record's job. A failure of the machinery (a lost
114
+ * device, a validation error, out of memory) propagates with its own code, so "the driver computes incorrectly"
115
+ * is never said about a device that merely died.
116
+ * @param ctx - the context to check
117
+ * @returns what the check found
118
+ */
119
+ async function runCheck(ctx: CheckedContext): Promise<DeviceCheck> {
120
+ const started = performance.now();
121
+ const wg = ctx.workgroupSize;
122
+ const count = CHECK_BLOCKS * wg + 1;
123
+ const bytes = 4 * count;
124
+ const lease = ctx.pool.lease();
125
+ const ring = new UniformRing(ctx.device, ctx.allocator, RING_SLOTS, "device-check/ring");
126
+ try {
127
+ const scope: ReduceScope = {
128
+ device: ctx.device,
129
+ caps: ctx.caps,
130
+ pipelines: ctx.pipelines,
131
+ pool: ctx.pool,
132
+ workgroupSize: wg,
133
+ scratch: (byteLength, label) => lease.storage(byteLength, `device-check/${label}`),
134
+ params(block, values) {
135
+ const slot = ring.reserve(1);
136
+ ring.write(slot, block, values);
137
+ return { binding: ring.binding(block), offset: ring.offsetOf(slot) };
138
+ },
139
+ };
140
+ const planner = await prepareScan(scope);
141
+ const src = lease.storage(bytes, "device-check/src");
142
+ const out = lease.storage(bytes, "device-check/out");
143
+ const input = new Uint32Array(count);
144
+ for (let i = 0; i < count; i++) {
145
+ input[i] = inputAt(i);
146
+ }
147
+ ctx.device.queue.writeBuffer(src, 0, input);
148
+ ctx.device.queue.writeBuffer(out, 0, new Uint32Array(count).fill(POISON));
149
+ const encoder = ctx.device.createCommandEncoder({ label: "device-check" });
150
+ const pass = encoder.beginComputePass({ label: "device-check" });
151
+ const total = planner.record(pass, whole(src, bytes), count, whole(out, bytes));
152
+ pass.end();
153
+ ring.flush();
154
+ ctx.device.queue.submit([encoder.finish()]);
155
+ const words = new Uint32Array(await ctx.readback.read(out, bytes));
156
+ const totalWord = new Uint32Array(
157
+ await ctx.readback.read(total.binding.buffer, 4, undefined, total.binding.offset + 4 * total.index),
158
+ );
159
+ const mismatch = checkScanWords(words, totalWord[0], count);
160
+ return {
161
+ check: "exclusive-scan",
162
+ ok: mismatch === null,
163
+ workgroupSize: wg,
164
+ count,
165
+ blocks: CHECK_BLOCKS,
166
+ ms: performance.now() - started,
167
+ vendor: ctx.caps.vendor,
168
+ architecture: ctx.caps.architecture,
169
+ description: ctx.caps.description,
170
+ mismatch,
171
+ };
172
+ } finally {
173
+ ring.destroy();
174
+ lease.release();
175
+ }
176
+ }
177
+
178
+ /**
179
+ * What this device computed when it was asked for an answer this package already knows, run once per device and
180
+ * remembered. A caller may await it before committing work to a context; every compute entry point of the
181
+ * package awaits it too, so the cost is paid once whoever asks first.
182
+ *
183
+ * It reports rather than throws, so a caller can ask a device a question without having to catch the answer. The
184
+ * entry points use `assertDeviceComputes`, which turns a failing report into E_DEVICE_INCORRECT.
185
+ * @param ctx - the context whose device is checked
186
+ * @returns the capability record; `ok` false means this device returns wrong numbers
187
+ */
188
+ export function verifyDevice(ctx: CheckedContext): Promise<DeviceCheck> {
189
+ const cached = checked.get(ctx.device);
190
+ if (cached !== undefined) {
191
+ return cached;
192
+ }
193
+ const running = runCheck(ctx).catch((err: unknown) => {
194
+ // only a settled verdict is worth keeping: a check that could not RUN (a lost device, a transient
195
+ // out-of-memory) must not answer for the device forever
196
+ checked.delete(ctx.device);
197
+ throw err;
198
+ });
199
+ checked.set(ctx.device, running);
200
+ return running;
201
+ }
202
+
203
+ /**
204
+ * The guard every compute entry point awaits: resolves on a device that computed the check correctly, throws
205
+ * E_DEVICE_INCORRECT on one that did not, carrying the first wrong word, what was required there, and the
206
+ * adapter strings that separate a broken driver from a fixed one.
207
+ * @param ctx - the context about to be computed on
208
+ * @internal
209
+ */
210
+ export async function assertDeviceComputes(ctx: CheckedContext): Promise<void> {
211
+ const check = await verifyDevice(ctx);
212
+ const { mismatch } = check;
213
+ if (mismatch === null) {
214
+ return;
215
+ }
216
+ const adapter = {
217
+ vendor: check.vendor,
218
+ architecture: check.architecture,
219
+ device: ctx.caps.device,
220
+ description: check.description,
221
+ };
222
+ const hint =
223
+ check.architecture === "warp"
224
+ ? "this is the Microsoft Basic Render Driver; the Microsoft.Direct3D.WARP redistributable (1.0.21 or newer) beside the host executable computes it correctly"
225
+ : undefined;
226
+ const observed = mismatch.poison
227
+ ? `${mismatch.where} was never written (the ${String(POISON)} the check wrote beforehand survived the dispatch)`
228
+ : `${mismatch.where} came back as ${String(mismatch.actual)} where ${String(mismatch.expected)} was required`;
229
+ const message =
230
+ `this device computes multi-workgroup shaders incorrectly: an exclusive scan of ${String(check.count)} ` +
231
+ `known numbers is wrong -- ${observed}. Refusing to run: every number this package computed here would ` +
232
+ `be unreliable. adapter: ${adapter.vendor}/${adapter.architecture} "${adapter.description}"`;
233
+ throw new WebGpuGraphError(
234
+ "E_DEVICE_INCORRECT",
235
+ hint === undefined ? message : `${message}. ${hint}`,
236
+ {
237
+ check: check.check,
238
+ where: mismatch.where,
239
+ expected: mismatch.expected,
240
+ actual: mismatch.actual,
241
+ poison: mismatch.poison,
242
+ count: check.count,
243
+ blocks: check.blocks,
244
+ workgroupSize: check.workgroupSize,
245
+ adapter,
246
+ ...(hint === undefined ? {} : { hint }),
247
+ },
248
+ );
249
+ }
@@ -37,8 +37,14 @@ import type {
37
37
  KatzOptions,
38
38
  PageRankOptions,
39
39
  } from "./algorithms.js";
40
- import type { ForceAtlas2Stats, GpuLayoutSimulation, GpuLayoutTuning } from "./layout.js";
41
- import type { ForceAtlas2Options } from "./options.js";
40
+ import type {
41
+ ForceAtlas2Stats,
42
+ FruchtermanReingoldStats,
43
+ GpuLayoutSimulation,
44
+ GpuLayoutTuning,
45
+ SpringElectricalStats,
46
+ } from "./layout.js";
47
+ import type { ForceAtlas2Options, FruchtermanReingoldOptions, SpringElectricalOptions } from "./options.js";
42
48
 
43
49
  // ---- the real @graphty/layout interfaces (spec 9.3, D27): imported at W1b, re-exported so the package's public
44
50
  // surface is unchanged and src/types/layout.ts keeps resolving them from here. `export type`, never a bare
@@ -86,7 +92,9 @@ export interface AcceleratorOptions {
86
92
  }
87
93
 
88
94
  /**
89
- * The injectable object (spec 3.3): P3's forceAtlas2, release and dispose, plus P7's seven algorithm members
95
+ * The injectable object (spec 3.3): P3's forceAtlas2, release and dispose, P5's fruchtermanReingold and
96
+ * springElectrical (the two other optional members of the real LayoutAccelerator, spec 9.3; the CPU option types in,
97
+ * the GPU simulations out), plus P7's seven algorithm members
90
98
  * (spec 8.2, 8.3; M8b-T8), non-optional here and returning the `Gpu*Result` shapes, which satisfy the `*ResultLike`
91
99
  * mirrors (spec 9.7: `precision` is an extra field, `F32` is a `NumericVector`). `connectedComponents` and
92
100
  * `weaklyConnectedComponents` are the same algorithm (spec 3.3: WCC semantics on directed input) under both names
@@ -100,6 +108,10 @@ export interface GpuAccelerator extends AlgorithmAccelerator, LayoutAccelerator
100
108
  readonly ctx: GpuContext;
101
109
  readonly options: Readonly<AcceleratorOptions>;
102
110
  forceAtlas2(options?: ForceAtlas2Options): GpuLayoutSimulation<ForceAtlas2Options, ForceAtlas2Stats>;
111
+ fruchtermanReingold(
112
+ options?: FruchtermanReingoldOptions,
113
+ ): GpuLayoutSimulation<FruchtermanReingoldOptions, FruchtermanReingoldStats>;
114
+ springElectrical(options?: SpringElectricalOptions): GpuLayoutSimulation<SpringElectricalOptions, SpringElectricalStats>;
103
115
  pageRank(s: GraphSnapshot, options?: PageRankOptions): Promise<GpuPageRankResult>;
104
116
  personalizedPageRank(
105
117
  s: GraphSnapshot,
@@ -4,6 +4,7 @@
4
4
  * package eslint config). P0-T3 wrote AdapterInfoLike and AdapterSummary; P1-T1 completes the file.
5
5
  */
6
6
 
7
+ import type { GpuContext } from "../context.js";
7
8
  import type { WebGpuGraphError } from "../errors.js";
8
9
 
9
10
  /** The limits `"raise"` takes from the adapter (spec 2.2); maxComputeWorkgroupsPerDimension is deliberately absent. */
@@ -133,3 +134,58 @@ export interface PlanCaps {
133
134
  export interface GpuDebugFlags {
134
135
  inspect: boolean;
135
136
  }
137
+
138
+ /**
139
+ * The context the device self-check runs on. `src/primitives/**` may not name GpuContext (the layer rule of
140
+ * spec 3.2), and this file may, so the alias is how src/primitives/verify.ts types its argument. It is
141
+ * GpuContext and nothing else: pass the context you were going to compute on.
142
+ */
143
+ export type CheckedContext = GpuContext;
144
+
145
+ /**
146
+ * The first word a device got wrong in the self-check. `poison` is true when the word still held the
147
+ * 0xdeadbeef the check wrote before the dispatch, which means NOTHING wrote it -- a different failure from a
148
+ * word written with the wrong value, and the distinction that made the Windows diagnosis possible.
149
+ */
150
+ export interface DeviceCheckMismatch {
151
+ /** Where it was found: `out[<index>]`, or `total`. */
152
+ readonly where: string;
153
+ /** The value arithmetic requires there. */
154
+ readonly expected: number;
155
+ /** The value the device returned. */
156
+ readonly actual: number;
157
+ /** True when `actual` is the poison word the check wrote beforehand: nothing wrote this word. */
158
+ readonly poison: boolean;
159
+ }
160
+
161
+ /**
162
+ * What one device self-check found (spec 3.3): the capability record a caller may read BEFORE it commits work
163
+ * to a device, through `verifyDevice`. `mismatch === null` (equivalently `ok`) means every one of `count`
164
+ * output words and the total matched the arithmetic answer; a non-null `mismatch` is the device returning
165
+ * wrong numbers, which every compute entry point of this package turns into E_DEVICE_INCORRECT.
166
+ *
167
+ * It reports one property -- that values crossing a workgroup barrier and block totals crossing dispatches of
168
+ * one compute pass survive -- and is therefore a refusal mechanism, not a certificate of correctness.
169
+ */
170
+ export interface DeviceCheck {
171
+ /** The check that ran; one today, named so a second can be added without changing the shape. */
172
+ readonly check: "exclusive-scan";
173
+ /** True when nothing disagreed. */
174
+ readonly ok: boolean;
175
+ /** The device's workgroup size (spec 5.1). */
176
+ readonly workgroupSize: number;
177
+ /** The words scanned: `blocks * workgroupSize + 1`, so the last workgroup is a partial one. */
178
+ readonly count: number;
179
+ /** The full workgroups of the scan (one partial one follows). */
180
+ readonly blocks: number;
181
+ /** Wall time of this check in milliseconds, pipeline compile included on the first call. */
182
+ readonly ms: number;
183
+ /** caps.vendor of the device that ran it. */
184
+ readonly vendor: string;
185
+ /** caps.architecture of the device that ran it. */
186
+ readonly architecture: string;
187
+ /** caps.description: the string that separates a broken driver from a fixed one. */
188
+ readonly description: string;
189
+ /** The first disagreement, or null when there was none. */
190
+ readonly mismatch: DeviceCheckMismatch | null;
191
+ }
@@ -43,6 +43,42 @@ export interface ForceAtlas2Stats extends LayoutStatsBase {
43
43
  readonly trace: ReadonlyArray<ForceAtlas2TraceRecord>;
44
44
  }
45
45
 
46
+ /**
47
+ * One per-iteration trace record of the last completed batch of a Fruchterman-Reingold simulation (spec 3.3
48
+ * FruchtermanReingoldStats.trace element): K1 writes `temperature` from the iteration's uniform slot (spec 7.20).
49
+ * @public
50
+ */
51
+ export interface FruchtermanReingoldTraceRecord {
52
+ readonly temperature: number;
53
+ readonly meanDisplacement: number;
54
+ readonly settledCount: number;
55
+ }
56
+
57
+ /** Spec 3.3 FruchtermanReingoldStats, verbatim: the cooling schedule's value replaces the controller fields. */
58
+ export interface FruchtermanReingoldStats extends LayoutStatsBase {
59
+ readonly temperature: number;
60
+ readonly trace: ReadonlyArray<FruchtermanReingoldTraceRecord>;
61
+ }
62
+
63
+ /**
64
+ * One per-iteration trace record of the last completed batch of a spring-electrical simulation (spec 3.3
65
+ * SpringElectricalStats.trace element): `kineticEnergy` is `0.5 * sum m |v|^2` over the free nodes after the
66
+ * PREVIOUS iteration's integrate: K5 writes it into partials B and the NEXT iteration's K1 folds it (PD-4), so the
67
+ * first record after load() carries 0 and record i carries the energy of iteration i - 1.
68
+ * @public
69
+ */
70
+ export interface SpringElectricalTraceRecord {
71
+ readonly kineticEnergy: number;
72
+ readonly meanDisplacement: number;
73
+ readonly settledCount: number;
74
+ }
75
+
76
+ /** Spec 3.3 SpringElectricalStats, verbatim; `kineticEnergy` is the last folded value, one iteration behind the last integrate (PD-4). */
77
+ export interface SpringElectricalStats extends LayoutStatsBase {
78
+ readonly kineticEnergy: number;
79
+ readonly trace: ReadonlyArray<SpringElectricalTraceRecord>;
80
+ }
81
+
46
82
  /** Options of GpuLayoutSimulation.run (spec 3.3). */
47
83
  export interface RunOptions {
48
84
  readonly maxIter?: number | undefined;
@@ -101,3 +137,25 @@ export interface ResolvedLayoutTuning {
101
137
  readonly extentFactor: number;
102
138
  readonly compat: "paper" | "networkx";
103
139
  }
140
+
141
+ /** Spec 2.2 CalibrateOptions, verbatim: the probe sizes of calibrateLayout (default 8k / 16k / 32k / 65k). */
142
+ export interface CalibrateOptions {
143
+ readonly sizes?: readonly number[] | undefined;
144
+ }
145
+
146
+ /**
147
+ * Spec 2.2 GpuCalibration, verbatim: the per-size ms per iteration of both repulsion tiers on the actual device,
148
+ * `pairsPerSecond` of the exact tier at the largest probed size, `suggestedExactMaxNodes` by the spec 7.8 rule
149
+ * (the largest probed n with exactMs(n) <= min(4 ms, gridMs(n)), rounded down to a power of two; when NO probed size
150
+ * qualifies -- the grid tier faster at every probe, or every probe over the budget -- the largest power of two strictly
151
+ * below the smallest probe, so the exact tier runs at no probed size and a value below floorPow2(min(sizes)) tells the
152
+ * caller the rule found nothing in the range: src/layouts/calibrate.ts) and the wall time of the whole call (pipeline
153
+ * compilation included on the first call).
154
+ */
155
+ export interface GpuCalibration {
156
+ readonly pairsPerSecond: number;
157
+ readonly exactMsPerIter: Readonly<Record<number, number>>;
158
+ readonly gridMsPerIter: Readonly<Record<number, number>>;
159
+ readonly suggestedExactMaxNodes: number;
160
+ readonly firstCallMs: number;
161
+ }
@@ -6,7 +6,7 @@
6
6
  * import.
7
7
  */
8
8
 
9
- import type { F32, NodeId } from "@graphty/graph-format";
9
+ import type { F32, NodeId, NodeMask } from "@graphty/graph-format";
10
10
  import type {
11
11
  CommonLayoutOptions,
12
12
  ForceAtlas2Options,
@@ -51,3 +51,47 @@ export interface ResolvedForceAtlas2Options {
51
51
  readonly iterationsPerStep: number;
52
52
  readonly maxInFlight: number;
53
53
  }
54
+
55
+ /**
56
+ * The resolved (defaults applied) Fruchterman-Reingold option record (spec 7.20, 9.3): `k` null means `1 / sqrt(n)`
57
+ * at load; `fixed` is applied at load through ModelInputs.fixed (PD-6). Exported for src/layouts/fruchterman-reingold.ts
58
+ * and the option tests.
59
+ * @public
60
+ */
61
+ export interface ResolvedFruchtermanReingoldOptions {
62
+ readonly k: number | null;
63
+ readonly iterations: number;
64
+ readonly cooling: "linear" | "adaptive";
65
+ readonly fixed: NodeMask | string | null;
66
+ readonly dim: 2 | 3;
67
+ readonly scale: number;
68
+ readonly center: readonly [number, number, number];
69
+ readonly seed: number | null;
70
+ readonly settleThreshold: number;
71
+ readonly settleWindow: number;
72
+ readonly iterationsPerStep: number;
73
+ readonly maxInFlight: number;
74
+ }
75
+
76
+ /**
77
+ * The resolved spring-electrical option record (spec 7.20, 9.3; ngraph's names): `gravity` is the Coulomb constant
78
+ * (negative repels), never FA2's centre gravity. `gravity` and `springCoefficient` are null when left to their
79
+ * defaults: ngraph's constant scaled by min(1, SE_SCALE_REFERENCE_NODES / n) once n is known at load. Exported for
80
+ * src/layouts/spring-electrical.ts and the option tests.
81
+ * @public
82
+ */
83
+ export interface ResolvedSpringElectricalOptions {
84
+ readonly springLength: number;
85
+ readonly springCoefficient: number | null;
86
+ readonly gravity: number | null;
87
+ readonly dragCoefficient: number;
88
+ readonly timeStep: number;
89
+ readonly dim: 2 | 3;
90
+ readonly scale: number;
91
+ readonly center: readonly [number, number, number];
92
+ readonly seed: number | null;
93
+ readonly settleThreshold: number;
94
+ readonly settleWindow: number;
95
+ readonly iterationsPerStep: number;
96
+ readonly maxInFlight: number;
97
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The `counting-scatter` kernel body (spec 6 row 5; P4-T3): the scatter of a counting sort. `start[k]` is the
3
+ * exclusive scan of the histogram, `cursor[k]` a zeroed per-bin atomic; an element takes the slot `start[k] +
4
+ * atomicAdd(&cursor[k], 1u)`. The order inside a bin depends on the schedule (set-deterministic, design 6). Body
5
+ * only; normative text.
6
+ */
7
+ export const countingScatterWgsl = /* wgsl */ `
8
+ @compute @workgroup_size(WG)
9
+ fn counting_scatter(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
10
+ let i = linear_id(wid, lid.x);
11
+ if (i >= P.count) { return; } // no barrier follows
12
+ let k = keys[i];
13
+ let slot = atomicAdd(&cursor[k], 1u); // the per-bin cursor (6 row 5)
14
+ outIndex[start[k] + slot] = i;
15
+ }
16
+ `;
@@ -1,37 +1,120 @@
1
1
  /**
2
- * K2 of the ForceAtlas2 iteration, `fa2-attraction` (spec 7.5; contract 4.5): the thread-per-row gather of the
3
- * attraction force over the undirected CSR rows (both arcs present, so the sum is symmetric with no atomics), the
4
- * linear or linlog law, the optional weights, the distributed-action division by the mass in `pos.w`, written as
5
- * the FIRST writer of `force` each iteration. P3 ships the thread-per-row tier over `[tierStart, tierEnd)` = `[0, n)`
6
- * with `USE_PERM = false`; the subgroup / workgroup tiers arrive with P4 (`TIER`).
2
+ * K2 of the ForceAtlas2 iteration, `fa2-attraction` (spec 7.5; contract 4.5): the gather of the attraction force
3
+ * over the undirected CSR rows (both arcs present, so the sum is symmetric with no atomics), the linear or linlog
4
+ * law, the optional weights, the distributed-action division by the mass in `pos.w`, written as the FIRST writer of
5
+ * `force` each iteration (or combined into it under `P.accumulate`, the windowed pattern of 4.2). The arcs are read
6
+ * inside the bound window [P.arcBase, P.arcEnd). P4-T5 (PD-6, PD-7) gives it the three degree tiers: TIER 0 is one
7
+ * row per thread over `[P.tierStart, P.tierEnd)`; TIER 1 is 32 lanes per row over `[P.hiEnd, P.midEnd)` with a
8
+ * five-step tree in workgroup memory; TIER 2 is one workgroup per row over `[0, P.hiEnd)` through `wg_reduce_vec4`
9
+ * (hence `needs: ["subgroups"]`); the row is `perm[row]` under USE_PERM. `LAW` (P5, spec 7.20) picks the pair law:
10
+ * 0 = the FA2 text, 1 = Fruchterman-Reingold `d^2 / k` (unfloored), 2 = ngraph's Hooke spring `k_s (d - L)`; under
11
+ * 1 / 2 the models compile `LINLOG = false` and `HAS_WEIGHTS = false`, and the law overwrites `w` so weights are
12
+ * ignored either way.
7
13
  *
8
- * Body only (spec 3.5, D9); normative text (contract 4.5); the K2 sabotage mutations (P3-T5) are textual edits of it.
14
+ * Body only (spec 3.5, D9); normative text (contract 4.5); the K2 sabotage mutations (P3-T5, P5-T7, P4-T5 / T6) are
15
+ * textual edits of it.
16
+ *
17
+ * TIER 0 folds its row through `row_force_dense`, a stride-one copy of `row_force`, because the shader compiler
18
+ * emits `row_force(i, 0u, 1u)` as a call and leaves the stride in a parameter: the loop then walks the row with a
19
+ * runtime step, which costs the strength-reduced addressing into colIdx / weights and the unrolling that keeps
20
+ * several loads in flight per thread. TIER 0 runs on every load -- alone when no row reaches degree 32, and over
21
+ * the low-degree rows, which are most of them, when the tiers are bound. The two folds spell their locals apart
22
+ * (`arc` / `nbr` / `weight` / `total` against `a` / `j` / `w` / `f`) so that each sabotage row names exactly one
23
+ * of them.
9
24
  */
10
25
 
11
- /** The K2 body: entry point `attraction`; an early return is legal here because no barrier follows (spec 3.5 rule 1). */
26
+ /** The K2 body: entry point `attraction`; the tier bodies are functions called under the uniform `TIER` override, so the barriers of `tiered` are reached in uniform control flow and `tier0`'s early return is legal (spec 3.5 rule 1). */
12
27
  export const fa2AttractionWgsl = /* wgsl */ `fn store_force(i: u32, f: vec3f) {
13
28
  force[3u * i] = f.x;
14
29
  force[3u * i + 1u] = f.y;
15
30
  force[3u * i + 2u] = f.z;
16
31
  }
17
-
18
- @compute @workgroup_size(WG)
19
- fn attraction(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
20
- let row = linear_id(wid, lid.x) + P.tierStart;
21
- if (row >= P.tierEnd) { return; } // no barrier follows in this tier (3.5 rule 1)
22
- let i = select(row, perm[row], USE_PERM);
32
+ fn load_force(i: u32) -> vec3f { return vec3f(force[3u * i], force[3u * i + 1u], force[3u * i + 2u]); }
33
+ fn row_node(row: u32) -> u32 { return select(row, perm[row], USE_PERM); }
34
+ fn row_force(i: u32, lane: u32, step: u32) -> vec3f { // the arcs of row i this lane walks inside the bound window [P.arcBase, P.arcEnd) (4.2)
23
35
  let pi = pos[i]; // xyz + mass in one load (D23)
36
+ let a0 = max(rowPtr[i], P.arcBase);
37
+ let a1 = min(rowPtr[i + 1u], P.arcEnd);
24
38
  var f = vec3f(0.0);
25
- for (var a = rowPtr[i]; a < rowPtr[i + 1u]; a = a + 1u) {
26
- let j = colIdx[a];
39
+ for (var a = a0 + lane; a < a1; a = a + step) {
40
+ let j = colIdx[a - P.arcBase];
27
41
  if (j == i) { continue; } // a self-loop exerts no force
28
42
  var w = 1.0;
29
- if (HAS_WEIGHTS) { w = weights[a]; }
43
+ if (HAS_WEIGHTS) { w = weights[a - P.arcBase]; }
30
44
  let d = pos[j].xyz - pi.xyz; // toward j
31
45
  let len = max(length(d), FA2_DIST_FLOOR);
46
+ if (LAW == 1u) { w = length(d) / P.frK; } // LAW 1 (FR, 7.20): |F| = d^2 / k along d / d, unfloored; the linear select below applies w as is
47
+ if (LAW == 2u) { w = P.springCoefficient * (len - P.springLength) / len; } // LAW 2 (spring, ngraph generateCreateSpringForce.js:33-36): Hooke k_s (d - L) toward j
32
48
  let mag = select(w, w * log(1.0 + len) / len, LINLOG); // linear: |F| = w len; linlog: |F| = w log(1 + len)
33
49
  f = f + d * mag;
34
50
  }
35
- if (DISTRIBUTED) { f = f / pi.w; }
51
+ return f;
52
+ }
53
+ fn row_force_dense(i: u32) -> vec3f { // TIER 0's stride-one twin of row_force (see the header)
54
+ let pi = pos[i];
55
+ let lo = max(rowPtr[i], P.arcBase);
56
+ let hi = min(rowPtr[i + 1u], P.arcEnd);
57
+ var total = vec3f(0.0);
58
+ for (var arc = lo; arc < hi; arc = arc + 1u) {
59
+ let k = arc - P.arcBase; // the window-local index; this walk is contiguous
60
+ let nbr = colIdx[k];
61
+ if (nbr == i) { continue; } // a self-loop exerts no force
62
+ var weight = 1.0;
63
+ if (HAS_WEIGHTS) { weight = weights[k]; }
64
+ let d = pos[nbr].xyz - pi.xyz; // toward the neighbour
65
+ let len = max(length(d), FA2_DIST_FLOOR);
66
+ if (LAW == 1u) { weight = length(d) / P.frK; } // LAW 1 (FR, 7.20), as in row_force
67
+ if (LAW == 2u) { weight = P.springCoefficient * (len - P.springLength) / len; } // LAW 2 (spring), as in row_force
68
+ let mag = select(weight, weight * log(1.0 + len) / len, LINLOG);
69
+ total = total + d * mag;
70
+ }
71
+ return total;
72
+ }
73
+ fn finish(i: u32, f0: vec3f) {
74
+ var f = f0;
75
+ if (DISTRIBUTED) { f = f / pos[i].w; }
76
+ if (P.accumulate == 1u) { f = f + load_force(i); } // the windowed loop of 4.2 (arcBase != 0 dispatches after the first)
36
77
  store_force(i, f); // overwrites: attraction is the first writer of force each iteration
78
+ }
79
+ fn tier0(wid: vec3<u32>, lane: u32) { // TIER 0: one row per thread over [tierStart, tierEnd); no barrier, so the early return is legal (3.5 rule 1)
80
+ let row = linear_id(wid, lane) + P.tierStart;
81
+ if (row >= P.tierEnd) { return; }
82
+ let i = row_node(row);
83
+ finish(i, row_force_dense(i));
84
+ }
85
+
86
+ var<workgroup> sh: array<vec3f, WG>;
87
+
88
+ fn tiered(wid: vec3<u32>, lid: u32) { // TIER 1: 32 lanes per row over [hiEnd, midEnd); TIER 2: WG lanes per row over [0, hiEnd) (PD-6, PD-7)
89
+ let g = group_id(wid);
90
+ var row = g;
91
+ var end = P.hiEnd;
92
+ var lane = lid;
93
+ var step = WG;
94
+ if (TIER == 1u) { row = P.hiEnd + g * (WG / 32u) + lid / 32u; end = P.midEnd; lane = lid % 32u; step = 32u; }
95
+ let valid = row < end;
96
+ var i = 0u;
97
+ var f = vec3f(0.0);
98
+ if (valid) { i = row_node(row); f = row_force(i, lane, step); }
99
+ if (TIER == 1u) {
100
+ sh[lid] = f;
101
+ workgroupBarrier();
102
+ for (var s = 16u; s >= 1u; s = s / 2u) { // the five-step tree over each 32-lane group; every lane runs every step
103
+ var t = vec3f(0.0);
104
+ if (lane < s) { t = sh[lid + s]; }
105
+ workgroupBarrier();
106
+ sh[lid] = sh[lid] + t;
107
+ workgroupBarrier();
108
+ }
109
+ if (valid && lane == 0u) { finish(i, sh[lid]); }
110
+ }
111
+ if (TIER == 2u) {
112
+ let t = wg_reduce_vec4(vec4f(f, 0.0), lid, 0u);
113
+ if (valid && lid == 0u) { finish(i, t.xyz); }
114
+ }
115
+ }
116
+
117
+ @compute @workgroup_size(WG)
118
+ fn attraction(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
119
+ if (TIER == 0u) { tier0(wid, lid.x); } else { tiered(wid, lid.x); }
37
120
  }`;