@graphty/webgpu-graph-algorithms 0.5.1 → 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 (243) hide show
  1. package/README.md +98 -52
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-BR7fx3vR.js → context-BXqgCifx.js} +190 -40
  4. package/dist/chunks/context-BXqgCifx.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/algorithms/components.d.ts.map +1 -1
  7. package/dist/src/algorithms/components.js +12 -13
  8. package/dist/src/algorithms/components.js.map +1 -1
  9. package/dist/src/algorithms/degree.d.ts +6 -8
  10. package/dist/src/algorithms/degree.d.ts.map +1 -1
  11. package/dist/src/algorithms/degree.js +58 -35
  12. package/dist/src/algorithms/degree.js.map +1 -1
  13. package/dist/src/algorithms/pagerank.d.ts.map +1 -1
  14. package/dist/src/algorithms/pagerank.js +16 -14
  15. package/dist/src/algorithms/pagerank.js.map +1 -1
  16. package/dist/src/algorithms/power-iteration.d.ts +2 -2
  17. package/dist/src/algorithms/power-iteration.d.ts.map +1 -1
  18. package/dist/src/algorithms/power-iteration.js +17 -14
  19. package/dist/src/algorithms/power-iteration.js.map +1 -1
  20. package/dist/src/constants.d.ts +38 -8
  21. package/dist/src/constants.d.ts.map +1 -1
  22. package/dist/src/constants.js +38 -8
  23. package/dist/src/constants.js.map +1 -1
  24. package/dist/src/errors.d.ts +3 -2
  25. package/dist/src/errors.d.ts.map +1 -1
  26. package/dist/src/errors.js +2 -1
  27. package/dist/src/errors.js.map +1 -1
  28. package/dist/src/index.d.ts +6 -4
  29. package/dist/src/index.d.ts.map +1 -1
  30. package/dist/src/index.js +8 -3
  31. package/dist/src/index.js.map +1 -1
  32. package/dist/src/kernel/dispatch.d.ts +8 -3
  33. package/dist/src/kernel/dispatch.d.ts.map +1 -1
  34. package/dist/src/kernel/dispatch.js +18 -7
  35. package/dist/src/kernel/dispatch.js.map +1 -1
  36. package/dist/src/kernel/kernel.d.ts +30 -1
  37. package/dist/src/kernel/kernel.d.ts.map +1 -1
  38. package/dist/src/kernel/kernel.js +49 -5
  39. package/dist/src/kernel/kernel.js.map +1 -1
  40. package/dist/src/kernel/prelude.d.ts.map +1 -1
  41. package/dist/src/kernel/prelude.js +6 -1
  42. package/dist/src/kernel/prelude.js.map +1 -1
  43. package/dist/src/kernel/profiler.d.ts +15 -3
  44. package/dist/src/kernel/profiler.d.ts.map +1 -1
  45. package/dist/src/kernel/profiler.js +27 -4
  46. package/dist/src/kernel/profiler.js.map +1 -1
  47. package/dist/src/kernels.d.ts +17 -7
  48. package/dist/src/kernels.d.ts.map +1 -1
  49. package/dist/src/kernels.js +323 -16
  50. package/dist/src/kernels.js.map +1 -1
  51. package/dist/src/layouts/calibrate.d.ts +51 -0
  52. package/dist/src/layouts/calibrate.d.ts.map +1 -0
  53. package/dist/src/layouts/calibrate.js +172 -0
  54. package/dist/src/layouts/calibrate.js.map +1 -0
  55. package/dist/src/layouts/force-simulation.d.ts +39 -4
  56. package/dist/src/layouts/force-simulation.d.ts.map +1 -1
  57. package/dist/src/layouts/force-simulation.js +71 -19
  58. package/dist/src/layouts/force-simulation.js.map +1 -1
  59. package/dist/src/layouts/forceatlas2.d.ts +107 -36
  60. package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
  61. package/dist/src/layouts/forceatlas2.js +296 -100
  62. package/dist/src/layouts/forceatlas2.js.map +1 -1
  63. package/dist/src/layouts/fruchterman-reingold.d.ts +73 -27
  64. package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -1
  65. package/dist/src/layouts/fruchterman-reingold.js +230 -70
  66. package/dist/src/layouts/fruchterman-reingold.js.map +1 -1
  67. package/dist/src/layouts/model-common.d.ts +41 -3
  68. package/dist/src/layouts/model-common.d.ts.map +1 -1
  69. package/dist/src/layouts/model-common.js +74 -3
  70. package/dist/src/layouts/model-common.js.map +1 -1
  71. package/dist/src/layouts/repulsion-grid.d.ts +152 -0
  72. package/dist/src/layouts/repulsion-grid.d.ts.map +1 -0
  73. package/dist/src/layouts/repulsion-grid.js +318 -0
  74. package/dist/src/layouts/repulsion-grid.js.map +1 -0
  75. package/dist/src/layouts/spring-electrical.d.ts +75 -30
  76. package/dist/src/layouts/spring-electrical.d.ts.map +1 -1
  77. package/dist/src/layouts/spring-electrical.js +231 -74
  78. package/dist/src/layouts/spring-electrical.js.map +1 -1
  79. package/dist/src/memory/residency.d.ts +6 -2
  80. package/dist/src/memory/residency.d.ts.map +1 -1
  81. package/dist/src/memory/residency.js +84 -14
  82. package/dist/src/memory/residency.js.map +1 -1
  83. package/dist/src/primitives/core-shape.d.ts +38 -2
  84. package/dist/src/primitives/core-shape.d.ts.map +1 -1
  85. package/dist/src/primitives/core-shape.js +71 -3
  86. package/dist/src/primitives/core-shape.js.map +1 -1
  87. package/dist/src/primitives/grid-pyramid.d.ts +71 -0
  88. package/dist/src/primitives/grid-pyramid.d.ts.map +1 -0
  89. package/dist/src/primitives/grid-pyramid.js +143 -0
  90. package/dist/src/primitives/grid-pyramid.js.map +1 -0
  91. package/dist/src/primitives/grid.d.ts +118 -0
  92. package/dist/src/primitives/grid.d.ts.map +1 -0
  93. package/dist/src/primitives/grid.js +225 -0
  94. package/dist/src/primitives/grid.js.map +1 -0
  95. package/dist/src/primitives/histogram.d.ts +67 -0
  96. package/dist/src/primitives/histogram.d.ts.map +1 -0
  97. package/dist/src/primitives/histogram.js +190 -0
  98. package/dist/src/primitives/histogram.js.map +1 -0
  99. package/dist/src/primitives/radix-sort.d.ts +75 -0
  100. package/dist/src/primitives/radix-sort.d.ts.map +1 -0
  101. package/dist/src/primitives/radix-sort.js +168 -0
  102. package/dist/src/primitives/radix-sort.js.map +1 -0
  103. package/dist/src/primitives/scan.d.ts +44 -0
  104. package/dist/src/primitives/scan.d.ts.map +1 -0
  105. package/dist/src/primitives/scan.js +151 -0
  106. package/dist/src/primitives/scan.js.map +1 -0
  107. package/dist/src/primitives/segmented-reduce.d.ts +25 -17
  108. package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
  109. package/dist/src/primitives/segmented-reduce.js +166 -47
  110. package/dist/src/primitives/segmented-reduce.js.map +1 -1
  111. package/dist/src/primitives/spmv.d.ts +18 -14
  112. package/dist/src/primitives/spmv.d.ts.map +1 -1
  113. package/dist/src/primitives/spmv.js +94 -58
  114. package/dist/src/primitives/spmv.js.map +1 -1
  115. package/dist/src/primitives/verify.d.ts +49 -0
  116. package/dist/src/primitives/verify.d.ts.map +1 -0
  117. package/dist/src/primitives/verify.js +229 -0
  118. package/dist/src/primitives/verify.js.map +1 -0
  119. package/dist/src/types/context.d.ts +53 -0
  120. package/dist/src/types/context.d.ts.map +1 -1
  121. package/dist/src/types/layout.d.ts +20 -0
  122. package/dist/src/types/layout.d.ts.map +1 -1
  123. package/dist/src/wgsl/counting-scatter.wgsl.d.ts +8 -0
  124. package/dist/src/wgsl/counting-scatter.wgsl.d.ts.map +1 -0
  125. package/dist/src/wgsl/counting-scatter.wgsl.js +17 -0
  126. package/dist/src/wgsl/counting-scatter.wgsl.js.map +1 -0
  127. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +23 -11
  128. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
  129. package/dist/src/wgsl/fa2-attraction.wgsl.js +98 -20
  130. package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
  131. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +6 -2
  132. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
  133. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +22 -1
  134. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
  135. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts +8 -0
  136. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts.map +1 -0
  137. package/dist/src/wgsl/grid-cell-key.wgsl.js +30 -0
  138. package/dist/src/wgsl/grid-cell-key.wgsl.js.map +1 -0
  139. package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts +8 -0
  140. package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts.map +1 -0
  141. package/dist/src/wgsl/grid-centroid-hub.wgsl.js +29 -0
  142. package/dist/src/wgsl/grid-centroid-hub.wgsl.js.map +1 -0
  143. package/dist/src/wgsl/grid-centroid.wgsl.d.ts +8 -0
  144. package/dist/src/wgsl/grid-centroid.wgsl.d.ts.map +1 -0
  145. package/dist/src/wgsl/grid-centroid.wgsl.js +29 -0
  146. package/dist/src/wgsl/grid-centroid.wgsl.js.map +1 -0
  147. package/dist/src/wgsl/grid-downsample.wgsl.d.ts +7 -0
  148. package/dist/src/wgsl/grid-downsample.wgsl.d.ts.map +1 -0
  149. package/dist/src/wgsl/grid-downsample.wgsl.js +28 -0
  150. package/dist/src/wgsl/grid-downsample.wgsl.js.map +1 -0
  151. package/dist/src/wgsl/grid-far-field.wgsl.d.ts +13 -0
  152. package/dist/src/wgsl/grid-far-field.wgsl.d.ts.map +1 -0
  153. package/dist/src/wgsl/grid-far-field.wgsl.js +98 -0
  154. package/dist/src/wgsl/grid-far-field.wgsl.js.map +1 -0
  155. package/dist/src/wgsl/grid-near-field.wgsl.d.ts +19 -0
  156. package/dist/src/wgsl/grid-near-field.wgsl.d.ts.map +1 -0
  157. package/dist/src/wgsl/grid-near-field.wgsl.js +129 -0
  158. package/dist/src/wgsl/grid-near-field.wgsl.js.map +1 -0
  159. package/dist/src/wgsl/histogram.wgsl.d.ts +7 -0
  160. package/dist/src/wgsl/histogram.wgsl.d.ts.map +1 -0
  161. package/dist/src/wgsl/histogram.wgsl.js +15 -0
  162. package/dist/src/wgsl/histogram.wgsl.js.map +1 -0
  163. package/dist/src/wgsl/indirect-finalize.wgsl.d.ts +8 -0
  164. package/dist/src/wgsl/indirect-finalize.wgsl.d.ts.map +1 -0
  165. package/dist/src/wgsl/indirect-finalize.wgsl.js +26 -0
  166. package/dist/src/wgsl/indirect-finalize.wgsl.js.map +1 -0
  167. package/dist/src/wgsl/radix-hist.wgsl.d.ts +9 -0
  168. package/dist/src/wgsl/radix-hist.wgsl.d.ts.map +1 -0
  169. package/dist/src/wgsl/radix-hist.wgsl.js +31 -0
  170. package/dist/src/wgsl/radix-hist.wgsl.js.map +1 -0
  171. package/dist/src/wgsl/radix-scatter.wgsl.d.ts +9 -0
  172. package/dist/src/wgsl/radix-scatter.wgsl.d.ts.map +1 -0
  173. package/dist/src/wgsl/radix-scatter.wgsl.js +40 -0
  174. package/dist/src/wgsl/radix-scatter.wgsl.js.map +1 -0
  175. package/dist/src/wgsl/scan-add.wgsl.d.ts +6 -0
  176. package/dist/src/wgsl/scan-add.wgsl.d.ts.map +1 -0
  177. package/dist/src/wgsl/scan-add.wgsl.js +14 -0
  178. package/dist/src/wgsl/scan-add.wgsl.js.map +1 -0
  179. package/dist/src/wgsl/scan-block.wgsl.d.ts +8 -0
  180. package/dist/src/wgsl/scan-block.wgsl.d.ts.map +1 -0
  181. package/dist/src/wgsl/scan-block.wgsl.js +30 -0
  182. package/dist/src/wgsl/scan-block.wgsl.js.map +1 -0
  183. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +22 -8
  184. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -1
  185. package/dist/src/wgsl/segmented-reduce.wgsl.js +84 -15
  186. package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -1
  187. package/dist/src/wgsl/spmv-pull.wgsl.d.ts +22 -11
  188. package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -1
  189. package/dist/src/wgsl/spmv-pull.wgsl.js +110 -36
  190. package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -1
  191. package/dist/tsconfig.build.tsbuildinfo +1 -1
  192. package/dist/webgpu-graph-algorithms.js +3815 -1003
  193. package/dist/webgpu-graph-algorithms.js.map +1 -1
  194. package/package.json +5 -4
  195. package/src/algorithms/components.ts +12 -16
  196. package/src/algorithms/degree.ts +58 -43
  197. package/src/algorithms/pagerank.ts +20 -18
  198. package/src/algorithms/power-iteration.ts +19 -18
  199. package/src/constants.ts +38 -8
  200. package/src/errors.ts +3 -1
  201. package/src/index.ts +14 -4
  202. package/src/kernel/dispatch.ts +18 -7
  203. package/src/kernel/kernel.ts +59 -5
  204. package/src/kernel/prelude.ts +9 -0
  205. package/src/kernel/profiler.ts +28 -4
  206. package/src/kernels.ts +356 -18
  207. package/src/layouts/calibrate.ts +187 -0
  208. package/src/layouts/force-simulation.ts +91 -23
  209. package/src/layouts/forceatlas2.ts +331 -106
  210. package/src/layouts/fruchterman-reingold.ts +255 -74
  211. package/src/layouts/model-common.ts +98 -3
  212. package/src/layouts/repulsion-grid.ts +451 -0
  213. package/src/layouts/spring-electrical.ts +257 -78
  214. package/src/memory/residency.ts +126 -20
  215. package/src/primitives/core-shape.ts +91 -4
  216. package/src/primitives/grid-pyramid.ts +221 -0
  217. package/src/primitives/grid.ts +349 -0
  218. package/src/primitives/histogram.ts +273 -0
  219. package/src/primitives/radix-sort.ts +246 -0
  220. package/src/primitives/scan.ts +197 -0
  221. package/src/primitives/segmented-reduce.ts +214 -56
  222. package/src/primitives/spmv.ts +125 -65
  223. package/src/primitives/verify.ts +249 -0
  224. package/src/types/context.ts +56 -0
  225. package/src/types/layout.ts +22 -0
  226. package/src/wgsl/counting-scatter.wgsl.ts +16 -0
  227. package/src/wgsl/fa2-attraction.wgsl.ts +98 -20
  228. package/src/wgsl/fa2-stats-finalize.wgsl.ts +22 -1
  229. package/src/wgsl/grid-cell-key.wgsl.ts +29 -0
  230. package/src/wgsl/grid-centroid-hub.wgsl.ts +28 -0
  231. package/src/wgsl/grid-centroid.wgsl.ts +28 -0
  232. package/src/wgsl/grid-downsample.wgsl.ts +27 -0
  233. package/src/wgsl/grid-far-field.wgsl.ts +97 -0
  234. package/src/wgsl/grid-near-field.wgsl.ts +128 -0
  235. package/src/wgsl/histogram.wgsl.ts +14 -0
  236. package/src/wgsl/indirect-finalize.wgsl.ts +25 -0
  237. package/src/wgsl/radix-hist.wgsl.ts +30 -0
  238. package/src/wgsl/radix-scatter.wgsl.ts +39 -0
  239. package/src/wgsl/scan-add.wgsl.ts +13 -0
  240. package/src/wgsl/scan-block.wgsl.ts +29 -0
  241. package/src/wgsl/segmented-reduce.wgsl.ts +84 -15
  242. package/src/wgsl/spmv-pull.wgsl.ts +110 -36
  243. package/dist/chunks/context-BR7fx3vR.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
+ }
@@ -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
+ }
@@ -137,3 +137,25 @@ export interface ResolvedLayoutTuning {
137
137
  readonly extentFactor: number;
138
138
  readonly compat: "paper" | "networkx";
139
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
+ }
@@ -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,35 +1,46 @@
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`). `LAW` (P5, spec 7.20) picks the
7
- * pair law: 0 = the FA2 text, 1 = Fruchterman-Reingold `d^2 / k` (unfloored), 2 = ngraph's Hooke spring
8
- * `k_s (d - L)`; under 1 / 2 the models compile `LINLOG = false` and `HAS_WEIGHTS = false`, and the law overwrites
9
- * `w` so weights are ignored either way.
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.
10
13
  *
11
- * 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.
12
24
  */
13
25
 
14
- /** 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). */
15
27
  export const fa2AttractionWgsl = /* wgsl */ `fn store_force(i: u32, f: vec3f) {
16
28
  force[3u * i] = f.x;
17
29
  force[3u * i + 1u] = f.y;
18
30
  force[3u * i + 2u] = f.z;
19
31
  }
20
-
21
- @compute @workgroup_size(WG)
22
- fn attraction(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
23
- let row = linear_id(wid, lid.x) + P.tierStart;
24
- if (row >= P.tierEnd) { return; } // no barrier follows in this tier (3.5 rule 1)
25
- 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)
26
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);
27
38
  var f = vec3f(0.0);
28
- for (var a = rowPtr[i]; a < rowPtr[i + 1u]; a = a + 1u) {
29
- let j = colIdx[a];
39
+ for (var a = a0 + lane; a < a1; a = a + step) {
40
+ let j = colIdx[a - P.arcBase];
30
41
  if (j == i) { continue; } // a self-loop exerts no force
31
42
  var w = 1.0;
32
- if (HAS_WEIGHTS) { w = weights[a]; }
43
+ if (HAS_WEIGHTS) { w = weights[a - P.arcBase]; }
33
44
  let d = pos[j].xyz - pi.xyz; // toward j
34
45
  let len = max(length(d), FA2_DIST_FLOOR);
35
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
@@ -37,6 +48,73 @@ fn attraction(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_i
37
48
  let mag = select(w, w * log(1.0 + len) / len, LINLOG); // linear: |F| = w len; linlog: |F| = w log(1 + len)
38
49
  f = f + d * mag;
39
50
  }
40
- 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)
41
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); }
42
120
  }`;
@@ -10,7 +10,11 @@
10
10
  * free nodes into `partials.swingTraction.x`) against `S.frEnergy` grows the temperature by 1 / FR_COOLING_STEP after
11
11
  * FR_COOLING_PATIENCE consecutive falls and shrinks it by FR_COOLING_STEP on a rise (Yifan Hu 2005, section 3.2);
12
12
  * 2 = the spring-electrical kinetic energy K5 folded into `partials.swingTraction.x` (PD-4) into `S.kineticEnergy`
13
- * and the trace.
13
+ * and the trace. On the grid tier (`P.gridMax > 0`, P4-T10, PD-14) it also derives the grid frame of the next build
14
+ * from the fold (`extent = max(min(bboxExtent * GRID_BBOX_MARGIN, extentFactor * rmsRadius), GRID_EXTENT_FLOOR)`,
15
+ * `cellSize = extent / G`, `gridMin = centroid - extent / 2` with `cellSize` in `.w`, `invCellSize`, `eps = 0.25
16
+ * cellSize`), copies the previous iteration's pseudo-cell count and occupancy max into the state, and resets the
17
+ * hub counters; the exact tier writes `gridMax: 0` and binds two dummies, so the block is dead there.
14
18
  *
15
19
  * This file holds the kernel BODY only (spec 3.5, D9): no bind-group lines and no `override` lines -- the composer
16
20
  * emits them from the registry entry in src/kernels.ts (contract 3.10.1). The text is normative (contract 4.5) and
@@ -63,6 +67,23 @@ fn stats_finalize(@builtin(local_invocation_id) lid: vec3<u32>) {
63
67
  T[P.iterationIndex].meanDisplacement = S.meanDisplacement;
64
68
  T[P.iterationIndex].settledCount = S.settledCount;
65
69
  T[P.iterationIndex].iteration = S.iteration;
70
+ if (P.gridMax > 0u) { // the grid tier (7.7): the robust extent, the cell size, eps, last iteration's counts, the hub counter reset (PD-14)
71
+ let cells = P.gridMax * P.gridMax * select(1u, P.gridMax, P.dim == 3u);
72
+ if (fold) {
73
+ let box = (S.max.xyz - S.min.xyz) * GRID_BBOX_MARGIN;
74
+ var bboxExtent = max(box.x, box.y);
75
+ if (P.dim == 3u) { bboxExtent = max(bboxExtent, box.z); }
76
+ let extent = max(min(bboxExtent, P.extentFactor * S.rmsRadius), GRID_EXTENT_FLOOR); // min(bbox, extentFactor x rms), floored (7.7)
77
+ let cellSize = extent / f32(P.gridMax);
78
+ S.gridMin = vec4f(S.centroid.xyz - vec3f(0.5 * extent), cellSize); // gridMin.w carries cellSize
79
+ S.invCellSize = 1.0 / cellSize;
80
+ S.eps = 0.25 * cellSize;
81
+ }
82
+ S.outsideGrid = cellHist[cells]; // the previous iteration's pseudo-cell count (0 after load)
83
+ S.maxCellOccupancy = atomicLoad(&hubCounters[1]);
84
+ atomicStore(&hubCounters[0], 0u);
85
+ atomicStore(&hubCounters[1], 0u);
86
+ }
66
87
  if (STATS_MODE == 1u) { // FR: this iteration's temperature (7.20) into the state and the trace
67
88
  if ((P.flags & FA2_FLAG_ADAPTIVE) != 0u) { // adaptive cooling (Yifan Hu 2005 3.2): tKe is the previous iteration's sum |F|^2 over free nodes
68
89
  if (fold) {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * G1, the `grid-cell-key` kernel body (spec 7.7; P4-T8): the finest cell of every node from the state's robust extent,
3
+ * `floor((p - gridMin) * invCellSize)` (a multiply, correctly rounded everywhere: PD-10), linearised when every axis
4
+ * is in [0, G) and the outside pseudo-cell `cells` otherwise; `cellVal[i] = i`. The clamp before the floor keeps a
5
+ * far-away or NaN coordinate out of an out-of-range float-to-int conversion. Body only; normative text.
6
+ */
7
+ export const gridCellKeyWgsl = /* wgsl */ `
8
+ fn grid_cells() -> u32 { return P.gridMax * P.gridMax * select(1u, P.gridMax, P.dim == 3u); }
9
+
10
+ @compute @workgroup_size(WG)
11
+ fn grid_cell_key(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
12
+ let i = linear_id(wid, lid.x);
13
+ if (i >= P.n) { return; } // no barrier follows
14
+ let cells = grid_cells();
15
+ let gf = f32(P.gridMax);
16
+ let q = (pos[i].xyz - S.gridMin.xyz) * S.invCellSize; // PD-10: never a division
17
+ let c = vec3<i32>(floor(clamp(q, vec3f(-1.0), vec3f(gf + 1.0))));
18
+ let g = i32(P.gridMax);
19
+ var inside = c.x >= 0 && c.x < g && c.y >= 0 && c.y < g;
20
+ if (P.dim == 3u) { inside = inside && c.z >= 0 && c.z < g; }
21
+ var key = cells; // the outside pseudo-cell (7.7)
22
+ if (inside) {
23
+ key = u32(c.x) + P.gridMax * u32(c.y);
24
+ if (P.dim == 3u) { key = key + P.gridMax * P.gridMax * u32(c.z); }
25
+ }
26
+ cellKey[i] = key;
27
+ cellVal[i] = i;
28
+ }
29
+ `;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * G4b, the `grid-centroid-hub` kernel body (spec 7.7; P4-T9): one workgroup per hub cell of hubList, dispatched
3
+ * indirectly from hubArgs (the T1 finalize over hubCounters[0]); a WG-strided mass-weighted sum reduced by the
4
+ * prelude's tree. The work is guarded by `valid`, never an early return, so the reduction is uniform (PD-13). Body
5
+ * only; normative text.
6
+ */
7
+ export const gridCentroidHubWgsl = /* wgsl */ `
8
+ @compute @workgroup_size(WG)
9
+ fn grid_centroid_hub(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
10
+ let h = group_id(wid);
11
+ let valid = h < hubCount[0]; // a workgroup past the count sums nothing
12
+ var c = 0u;
13
+ var start = 0u;
14
+ var count = 0u;
15
+ if (valid) {
16
+ c = hubList[h];
17
+ start = cellStart[c];
18
+ count = cellStart[c + 1u] - start;
19
+ }
20
+ var acc = vec4f(0.0);
21
+ for (var k = start + lid.x; k < start + count; k = k + WG) { // strided over the cell's sorted range
22
+ let p = pos[sortedIdx[k]];
23
+ acc = acc + vec4f(p.xyz * p.w, p.w);
24
+ }
25
+ let t = wg_reduce_vec4(acc, lid.x, 0u); // uniform control flow: 256 -> 1
26
+ if (valid && lid.x == 0u) { pyramid[c] = t; }
27
+ }
28
+ `;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * G4, the `grid-centroid` kernel body (spec 7.7; P4-T9): thread per finest cell, the pseudo-cell included; the
3
+ * mass-weighted position sum of a cell's sorted range in index order (no atomics: deterministic), the largest
4
+ * occupancy into hubCounters[1], and cells above GRID_HUB_CELL entries appended to hubList for G4b (PD-13). Body
5
+ * only; normative text.
6
+ */
7
+ export const gridCentroidWgsl = /* wgsl */ `
8
+ fn grid_cells() -> u32 { return P.gridMax * P.gridMax * select(1u, P.gridMax, P.dim == 3u); }
9
+
10
+ @compute @workgroup_size(WG)
11
+ fn grid_centroid(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
12
+ let c = linear_id(wid, lid.x);
13
+ if (c > grid_cells()) { return; } // cells [0, cells]: the pseudo-cell is index cells; no barrier follows
14
+ let start = cellStart[c];
15
+ let count = cellStart[c + 1u] - start;
16
+ atomicMax(&hubCounters[1], count); // maxCellOccupancy, read by K1 next iteration
17
+ if (count > GRID_HUB_CELL) { // a hub cell: G4b sums it (PD-13)
18
+ hubList[atomicAdd(&hubCounters[0], 1u)] = c;
19
+ return;
20
+ }
21
+ var acc = vec4f(0.0);
22
+ for (var k = start; k < start + count; k = k + 1u) { // sorted order: deterministic
23
+ let p = pos[sortedIdx[k]];
24
+ acc = acc + vec4f(p.xyz * p.w, p.w); // (sum m x, sum m y, sum m z, sum m)
25
+ }
26
+ pyramid[c] = acc;
27
+ }
28
+ `;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * G5, the `grid-downsample` kernel body (spec 7.7; P4-T9): one dispatch per coarser level; every parent cell is the
3
+ * sum of its 4 (2D) or 8 (3D) children at the level below, read at P.childBase and written at P.parentBase (the
4
+ * pseudo-cell, index cells of level 0, is never a child). No atomics. Body only; normative text.
5
+ */
6
+ export const gridDownsampleWgsl = /* wgsl */ `
7
+ @compute @workgroup_size(WG)
8
+ fn grid_downsample(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
9
+ let pc = linear_id(wid, lid.x); // the parent cell inside its level
10
+ if (pc >= P.parentCells) { return; } // no barrier follows
11
+ let side = P.parentSide;
12
+ let cs = 2u * side; // the child level's side
13
+ let px = pc % side;
14
+ let py = (pc / side) % side;
15
+ let pz = pc / (side * side);
16
+ var acc = vec4f(0.0);
17
+ for (var dz = 0u; dz < P.depth; dz = dz + 1u) {
18
+ for (var dy = 0u; dy < 2u; dy = dy + 1u) {
19
+ for (var dx = 0u; dx < 2u; dx = dx + 1u) {
20
+ let child = (2u * px + dx) + cs * ((2u * py + dy) + cs * (2u * pz + dz));
21
+ acc = acc + pyramid[P.childBase + child];
22
+ }
23
+ }
24
+ }
25
+ pyramid[P.parentBase + pc] = acc;
26
+ }
27
+ `;