@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
@@ -5,8 +5,10 @@
5
5
  * withColumns() siblings, and tombstoned so a live user learns of the release through `isReleased` (spec 4.5).
6
6
  * Uploads follow the pure planner of ./upload-plan.ts: the arena path writes ONE buffer with ONE writeBuffer of the
7
7
  * hot prefix (or the full arena when a cold segment is needed and fits) and binds segments at
8
- * `segment.byteOffset - arena.byteOffset`; the perArray path writes one buffer per array; a windowed plan is
9
- * E_TOO_LARGE until P4 executes windows (PLAN DECISION 2). Identity permutations are never materialised: presence is
8
+ * `segment.byteOffset - arena.byteOffset`; the perArray path writes one buffer per array; the windowed path (P4-T7,
9
+ * PD-8) writes one buffer per PlannedArray range of every arc-indexed array and binds window 0 as the default
10
+ * colIdx / weights, the whole window list and the per-array buffers riding on the CoreBinding for the row-walking
11
+ * primitives to bind per window. Identity permutations are never materialised: presence is
10
12
  * decided from counts and flags, and the arcToEdge / edgeToArc getters are read only when a segment exists. Nothing
11
13
  * is freed by garbage collection; the once-only warning above warnUnreleasedSnapshots names the missing release.
12
14
  */
@@ -34,6 +36,9 @@ const DEFAULT_NEED: readonly CoreArrayName[] = Object.freeze(["rowPtr", "colIdx"
34
36
  /** The five core arrays in arena (hot-to-cold) order. */
35
37
  const CORE_ORDER: readonly CoreArrayName[] = Object.freeze(["rowPtr", "colIdx", "weights", "arcToEdge", "edgeToArc"]);
36
38
 
39
+ /** The arc-indexed core arrays: the ones a windowed plan splits into window buffers (upload-plan.ts ARC_INDEXED). */
40
+ const ARC_INDEXED: readonly ("colIdx" | "weights" | "arcToEdge")[] = Object.freeze(["colIdx", "weights", "arcToEdge"]);
41
+
37
42
  /** Every resident buffer is bound by kernels, filled by writeBuffer and readable back (tests, readbacks of state). */
38
43
  const RESIDENT_USAGE = BufferUsage.STORAGE | BufferUsage.COPY_DST | BufferUsage.COPY_SRC;
39
44
 
@@ -53,6 +58,8 @@ export interface CoreBinding {
53
58
  readonly arcToEdge: Binding | null;
54
59
  readonly edgeToArc: Binding | null;
55
60
  readonly windows: readonly ArcWindow[] | null;
61
+ /** The buffers each arc-indexed array was split into by the windowed plan (indexed by ArcWindow.bufferIndex; an absent array has none); non-null iff `plan === "windowed"`. */
62
+ readonly arcBuffers: Readonly<Record<"colIdx" | "weights" | "arcToEdge", readonly GPUBuffer[]>> | null;
56
63
  readonly hasWeights: boolean;
57
64
  }
58
65
 
@@ -63,7 +70,14 @@ export interface CoreBinding {
63
70
  */
64
71
  export interface ViewBinding {
65
72
  readonly view:
66
- "reverse" | "coo" | "edgeList" | "outDegree" | "inDegree" | "degreeOrder" | "reverseDegreeOrder" | "mate";
73
+ | "reverse"
74
+ | "coo"
75
+ | "edgeList"
76
+ | "outDegree"
77
+ | "inDegree"
78
+ | "degreeOrder"
79
+ | "reverseDegreeOrder"
80
+ | "mate";
67
81
  readonly bindings: Readonly<Record<string, Binding>>;
68
82
  readonly scalars: Readonly<Record<string, readonly number[]>>;
69
83
  }
@@ -151,9 +165,24 @@ interface ResidencyRecord {
151
165
  * `rev.rowPtr` object.
152
166
  */
153
167
  readonly packKeys: Map<string, object>;
168
+ /** One marker object per window buffer, keyed `<name>:<bufferIndex>` (the packKey pattern: never one of the snapshot's own arrays). */
169
+ readonly windowKeys: Map<string, object>;
170
+ /** The windowed plan's windows, or null on the arena / perArray plans. */
171
+ windows: readonly ArcWindow[] | null;
172
+ /** The window buffers of every arc-indexed array uploaded so far (windowed plan only). */
173
+ readonly arcBuffers: Map<CoreArrayName, GPUBuffer[]>;
154
174
  released: boolean;
155
175
  }
156
176
 
177
+ /**
178
+ * Whether a core array is arc-indexed (split into window buffers by a windowed plan).
179
+ * @param name - the core array
180
+ * @returns true for colIdx, weights and arcToEdge
181
+ */
182
+ function isArcIndexed(name: CoreArrayName): name is "colIdx" | "weights" | "arcToEdge" {
183
+ return (ARC_INDEXED as readonly CoreArrayName[]).includes(name);
184
+ }
185
+
157
186
  /**
158
187
  * Whether an ArrayBuffer-like is a SharedArrayBuffer (checked by tag so the global need not exist).
159
188
  * @param buffer - the buffer to test
@@ -294,8 +323,9 @@ export class GraphResidency {
294
323
 
295
324
  /**
296
325
  * Uploads (or finds) the core; `need` defaults to ["rowPtr", "colIdx", "weights"]; cold segments on demand
297
- * (spec 4.2). Never materialises an identity permutation. A windowed plan throws E_TOO_LARGE { path: "windowed" }
298
- * until P4 (PLAN DECISION 2); a tombstoned serial is lifted and re-uploaded (PLAN DECISION 7).
326
+ * (spec 4.2). Never materialises an identity permutation. A windowed plan (spec 4.2, PD-8) uploads every
327
+ * arc-indexed array as the plan's buffer ranges and binds window 0 as its default binding; rowPtr and edgeToArc
328
+ * stay whole. A tombstoned serial is lifted and re-uploaded (PLAN DECISION 7).
299
329
  * @param s - the snapshot
300
330
  * @param need - the core arrays to bind (rowPtr is always included; absent arrays are ignored)
301
331
  * @returns the core binding (a frozen object; grows as cold segments are added)
@@ -306,18 +336,6 @@ export class GraphResidency {
306
336
  const wanted = need ?? DEFAULT_NEED;
307
337
  const names = CORE_ORDER.filter((name) => (name === "rowPtr" || wanted.includes(name)) && isPresent(s, name));
308
338
  const plan = planUpload(s, this.caps, names);
309
- if (plan.kind === "windowed") {
310
- throw new WebGpuGraphError(
311
- "E_TOO_LARGE",
312
- `snapshot ${s.serial}: an arc array of ${4 * s.arcCount} bytes needs arc windows (${plan.windows.length}), which P1-P3 plan but do not execute`,
313
- {
314
- needed: 4 * s.arcCount,
315
- limit: this.caps.limits.maxStorageBufferBindingSize,
316
- path: "windowed",
317
- algorithm: null,
318
- },
319
- );
320
- }
321
339
  const record = this.ensureRecord(s);
322
340
  if (record.plan === null) {
323
341
  record.plan = plan.kind;
@@ -331,10 +349,19 @@ export class GraphResidency {
331
349
  );
332
350
  record.arena = { buffer: resident.buffer, segments: plan.segments };
333
351
  }
352
+ if (plan.kind === "windowed") {
353
+ record.windows = plan.windows;
354
+ }
334
355
  }
335
356
  for (const name of names) {
336
357
  if (!record.bindings.has(name)) {
337
- record.bindings.set(name, this.bindCore(record, s, name));
358
+ const planned = plan.kind === "windowed" ? plan.arrays.find((a) => a.name === name) : undefined;
359
+ record.bindings.set(
360
+ name,
361
+ planned !== undefined && planned.buffers.length > 0 && record.windows !== null && isArcIndexed(name)
362
+ ? this.bindWindowed(record, s, name, planned.buffers, record.windows[0])
363
+ : this.bindCore(record, s, name),
364
+ );
338
365
  }
339
366
  }
340
367
  const rowPtr = record.bindings.get("rowPtr");
@@ -350,7 +377,15 @@ export class GraphResidency {
350
377
  weights,
351
378
  arcToEdge: record.bindings.get("arcToEdge") ?? null,
352
379
  edgeToArc: record.bindings.get("edgeToArc") ?? null,
353
- windows: null,
380
+ windows: record.windows,
381
+ arcBuffers:
382
+ record.windows === null
383
+ ? null
384
+ : Object.freeze({
385
+ colIdx: Object.freeze([...(record.arcBuffers.get("colIdx") ?? [])]),
386
+ weights: Object.freeze([...(record.arcBuffers.get("weights") ?? [])]),
387
+ arcToEdge: Object.freeze([...(record.arcBuffers.get("arcToEdge") ?? [])]),
388
+ }),
354
389
  hasWeights: weights !== null,
355
390
  });
356
391
  }
@@ -448,7 +483,6 @@ export class GraphResidency {
448
483
  });
449
484
  }
450
485
 
451
-
452
486
  /**
453
487
  * The reverse adjacency's bindings. On an UNDIRECTED snapshot graph-format invariant I7 makes reverse() return
454
488
  * the FORWARD arrays, so the core bindings ARE the reverse bindings and nothing is uploaded -- delegating to
@@ -464,6 +498,19 @@ export class GraphResidency {
464
498
  const rev = s.reverse();
465
499
  if (!s.directed) {
466
500
  const core = this.core(s, ["rowPtr", "colIdx", "weights"]);
501
+ if (core.plan === "windowed") {
502
+ // a view is never windowed (spec 4.3): window 0's colIdx must not pose as the whole reverse adjacency
503
+ throw new WebGpuGraphError(
504
+ "E_TOO_LARGE",
505
+ `snapshot ${s.serial}: the undirected reverse view is the core, which needs arc windows (${core.windows?.length ?? 0}) that no view executes`,
506
+ {
507
+ needed: 4 * s.arcCount,
508
+ limit: this.caps.limits.maxStorageBufferBindingSize,
509
+ path: "windowed",
510
+ algorithm: null,
511
+ },
512
+ );
513
+ }
467
514
  const bindings: Record<string, Binding> = { rowPtr: core.rowPtr };
468
515
  if (core.colIdx !== null) {
469
516
  bindings.colIdx = core.colIdx;
@@ -796,6 +843,59 @@ export class GraphResidency {
796
843
  return { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
797
844
  }
798
845
 
846
+ /**
847
+ * Uploads one arc-indexed array as the windowed plan's buffer ranges (spec 4.2: an array above maxBufferSize is
848
+ * split across buffers at window boundaries), each keyed on a marker object of the record (windowKey), and
849
+ * returns window 0's binding so a caller that ignores windows still binds something valid (PD-8).
850
+ * @param record - the snapshot's record
851
+ * @param s - the snapshot
852
+ * @param name - a PRESENT arc-indexed core array
853
+ * @param ranges - the plan's buffer ranges of the array
854
+ * @param first - window 0
855
+ * @returns the binding of window 0
856
+ */
857
+ private bindWindowed(
858
+ record: ResidencyRecord,
859
+ s: GraphSnapshot,
860
+ name: "colIdx" | "weights" | "arcToEdge",
861
+ ranges: readonly { readonly byteOffset: number; readonly byteLength: number }[],
862
+ first: ArcWindow,
863
+ ): Binding {
864
+ const array = coreArray(s, name);
865
+ const buffers = ranges.map(
866
+ (range, index) =>
867
+ this.upload(
868
+ record,
869
+ this.windowKey(record, `${name}:${index}`),
870
+ new Uint8Array(array.buffer, array.byteOffset + range.byteOffset, range.byteLength),
871
+ `residency:core:${record.serial}:${name}:w${index}`,
872
+ ).buffer,
873
+ );
874
+ record.arcBuffers.set(name, buffers);
875
+ return {
876
+ buffer: buffers[first.bufferIndex],
877
+ offset: first.offset,
878
+ size: 4 * (first.end - first.start),
879
+ window: first,
880
+ };
881
+ }
882
+
883
+ /**
884
+ * The upload key of one window buffer: a marker object allocated once per record and `<name>:<bufferIndex>`.
885
+ * @param record - the owning record
886
+ * @param memoKey - `<name>:<bufferIndex>`
887
+ * @returns the stable marker object
888
+ */
889
+ private windowKey(record: ResidencyRecord, memoKey: string): object {
890
+ const existing = record.windowKeys.get(memoKey);
891
+ if (existing !== undefined) {
892
+ return existing;
893
+ }
894
+ const created = {};
895
+ record.windowKeys.set(memoKey, created);
896
+ return created;
897
+ }
898
+
799
899
  /**
800
900
  * Uploads `data` into a new buffer keyed on `key`, or returns the resident already uploaded for that key by
801
901
  * the same owner (PLAN DECISION 11: a resident belongs to one record).
@@ -866,6 +966,9 @@ export class GraphResidency {
866
966
  record.bindings.clear();
867
967
  record.views.clear();
868
968
  record.packKeys.clear();
969
+ record.windowKeys.clear();
970
+ record.arcBuffers.clear();
971
+ record.windows = null;
869
972
  record.arena = null;
870
973
  record.plan = null;
871
974
  this.bySerial.delete(record.serial);
@@ -894,6 +997,9 @@ export class GraphResidency {
894
997
  bindings: new Map<CoreArrayName, Binding>(),
895
998
  views: new Map<string, ViewBinding>(),
896
999
  packKeys: new Map<string, object>(),
1000
+ windowKeys: new Map<string, object>(),
1001
+ windows: null,
1002
+ arcBuffers: new Map<CoreArrayName, GPUBuffer[]>(),
897
1003
  released: false,
898
1004
  };
899
1005
  this.bySerial.set(s.serial, record);
@@ -1,13 +1,60 @@
1
1
  /**
2
2
  * The shape helpers every core-walking primitive shares (spec 4.1): the row count and the arc count a CoreBinding
3
- * implies, the windowed rejection of P4, and the ViewBinding -> CoreBinding adapter the P7 pull kernels need.
3
+ * implies, the per-window binding of a windowed core (P4-T7, PD-8), the windowed rejections the pull and the
4
+ * whole-core drivers keep (DEP-P4-B), and the ViewBinding -> CoreBinding adapter the P7 pull kernels need.
4
5
  * Moved out of segmented-reduce.ts by M8b-T4 so spmv.ts can use them without duplicating them; the `primitive`
5
6
  * argument keeps each caller's error details byte-identical to what they were when the helpers were private.
6
7
  */
7
8
 
8
9
  import { WebGpuGraphError } from "../errors.js";
9
10
  import { type CoreBinding, type ViewBinding } from "../memory/residency.js";
10
- import { type Binding } from "../types/memory.js";
11
+ import { type ArcWindow, type Binding } from "../types/memory.js";
12
+
13
+ /** The lanes one TIER 1 (mid-degree) row is folded by, in segmentedReduce and spmvPull alike (PD-6: bitwise the same on every subgroup size). */
14
+ export const MID_TIER_LANES = 32;
15
+
16
+ /** The degree tiers of degreeOrder(): the permutation binding and the CPU-side segmentOffsets [0, hiEnd, midEnd, lowEnd, n]. */
17
+ export interface DegreeTiers {
18
+ readonly perm: Binding;
19
+ readonly segmentOffsets: readonly [number, number, number, number, number];
20
+ }
21
+
22
+ /**
23
+ * The DegreeTiers of a degreeOrder / reverseDegreeOrder view (the perm binding and the five segment offsets
24
+ * [0, hiEnd, midEnd, lowEnd, n] of graph-format's cuGraph thresholds 1024 / 32 / 1).
25
+ * @param view - residency.view(s, "degreeOrder") or view(s, "reverseDegreeOrder")
26
+ * @returns the tiers; E_INVALID_ARGUMENT when the view has no perm binding or its segmentOffsets are not five
27
+ * ascending numbers ending at the row count (4 bytes per row of the perm binding)
28
+ */
29
+ export function degreeTiersOf(view: ViewBinding): DegreeTiers {
30
+ const { perm }: { readonly perm?: Binding | undefined } = view.bindings;
31
+ if (perm === undefined) {
32
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `the ${view.view} view has no perm binding`, {
33
+ argument: "view",
34
+ value: view.view,
35
+ expected: "a view with a perm binding (degreeOrder, reverseDegreeOrder)",
36
+ });
37
+ }
38
+ const so: readonly number[] | undefined = view.scalars.segmentOffsets;
39
+ const ascending =
40
+ so !== undefined &&
41
+ so.length === 5 &&
42
+ so[0] === 0 &&
43
+ so.every((value, k) => Number.isInteger(value) && (k === 0 || value >= so[k - 1]));
44
+ if (!ascending || so[4] !== perm.size / 4) {
45
+ throw new WebGpuGraphError(
46
+ "E_INVALID_ARGUMENT",
47
+ `the ${view.view} view's segmentOffsets are not five ascending numbers ending at the row count`,
48
+ {
49
+ argument: "view",
50
+ value: so === undefined ? null : Array.from(so),
51
+ expected: `[0, hiEnd, midEnd, lowEnd, ${perm.size / 4}] ascending`,
52
+ },
53
+ );
54
+ }
55
+ const segmentOffsets: readonly [number, number, number, number, number] = [so[0], so[1], so[2], so[3], so[4]];
56
+ return Object.freeze({ perm, segmentOffsets });
57
+ }
11
58
 
12
59
  /**
13
60
  * The row count of a core from its rowPtr binding (4(n + 1) bytes).
@@ -41,18 +88,57 @@ export function arcCountOf(core: CoreBinding): number {
41
88
  }
42
89
 
43
90
  /**
44
- * Rejects a windowed core (executed at P4).
91
+ * The binding of one arc window of a windowed core's arc-indexed array (spec 4.2): the buffer the window was placed
92
+ * in, at the window's offset, over its arcs; the kernel reads `array[arc - P.arcBase]` with `arcBase = w.start`.
93
+ * @param core - a windowed core (arcBuffers non-null)
94
+ * @param name - the arc-indexed array
95
+ * @param w - one of core.windows
96
+ * @returns the binding; E_INVALID_ARGUMENT when the core is not windowed or the array is absent
97
+ */
98
+ export function windowBinding(core: CoreBinding, name: "colIdx" | "weights" | "arcToEdge", w: ArcWindow): Binding {
99
+ const buffer = core.arcBuffers?.[name][w.bufferIndex];
100
+ if (buffer === undefined) {
101
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `windowBinding: the core has no window buffers of ${name}`, {
102
+ argument: "core",
103
+ value: name,
104
+ expected: "a windowed core whose plan uploaded the array",
105
+ });
106
+ }
107
+ return { buffer, offset: w.offset, size: 4 * (w.end - w.start), window: w };
108
+ }
109
+
110
+ /**
111
+ * Rejects a windowed core: the pull cannot accumulate its affine epilogue across windows (DEP-P4-B).
45
112
  * @param core - the core
46
113
  * @param primitive - the caller's name, used in the message and the feature detail
47
114
  */
48
115
  export function assertNotWindowed(core: CoreBinding, primitive: string): void {
49
116
  if (core.plan === "windowed" || core.windows !== null) {
50
- throw new WebGpuGraphError("E_UNSUPPORTED", `${primitive}: windowed cores are executed at P4`, {
117
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${primitive}: windowed cores are not executed by the pull`, {
51
118
  feature: `${primitive}.windowed`,
52
119
  });
53
120
  }
54
121
  }
55
122
 
123
+ /**
124
+ * Rejects a windowed core for a driver that walks the whole core (DEP-P4-B: pageRank, the power iterations and
125
+ * connectedComponents): `E_TOO_LARGE { needed, limit, path: "windowed", algorithm }` (spec 3.8 / 3.12), the refusal
126
+ * GraphResidency.core() itself issued before P4-T7 executed windows for degree and segmentedReduce.
127
+ * @param core - the core
128
+ * @param arcCount - the snapshot's arc count (4 bytes per arc of each arc-indexed array)
129
+ * @param limit - the device's maxStorageBufferBindingSize
130
+ * @param algorithm - the driver's name
131
+ */
132
+ export function assertWholeCore(core: CoreBinding, arcCount: number, limit: number, algorithm: string): void {
133
+ if (core.plan === "windowed") {
134
+ throw new WebGpuGraphError(
135
+ "E_TOO_LARGE",
136
+ `${algorithm}: the arc arrays need a windowed upload (${core.windows?.length ?? 0} windows), which ${algorithm} does not execute (DEP-P4-B)`,
137
+ { needed: 4 * arcCount, limit, path: "windowed", algorithm },
138
+ );
139
+ }
140
+ }
141
+
56
142
  /**
57
143
  * The CoreBinding shape of a residency VIEW (spec 4.3). residency.view() returns a ViewBinding -- a name -> Binding
58
144
  * record -- while graphBindings / graphOverrides and every core-walking primitive read rowPtr / colIdx / weights as
@@ -98,6 +184,7 @@ export function coreOfView(v: ViewBinding, arcCount: number): CoreBinding {
98
184
  arcToEdge: null,
99
185
  edgeToArc: null,
100
186
  windows: null,
187
+ arcBuffers: null,
101
188
  hasWeights: weights !== null,
102
189
  });
103
190
  }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * The grid pyramid (spec 6 row 12, 7.7 G4-G5; P4-T9): the planner that records, into the caller's pass, the finest
3
+ * centroids (G4, `grid-centroid`: thread per cell over `cells + 1`, the pseudo-cell included), the hub-cell
4
+ * completion (G4a: the T1 `indirect-finalize` over `hubCounters[0]` into `hubArgs` with `wg = 1`, so the finalize's
5
+ * `ceil(count / wg)` is ONE workgroup per hub cell; G4b: `grid-centroid-hub`, one workgroup per hub cell, dispatched
6
+ * indirectly; PD-13, DEP-P4-I) and one `grid-downsample` dispatch per coarser
7
+ * level (G5). Level 0 holds `[sum m x, sum m y, sum m z, sum m]` per cell; every parent is the sum of its 2^dim
8
+ * children; the pseudo-cell (index `cells` of level 0) is never a child. No atomics touch the sums (design 6 row 12:
9
+ * bitwise reproducible); the only atomics are the hub append and the occupancy max.
10
+ *
11
+ * The named grid buffers (`pyramid`, `hubList`, `hubCounters`, `hubArgs`) are the caller's (the model's
12
+ * `BufferSpec`s, so `inspect(name)` reaches them); the static params of the finalize and of every level are written
13
+ * ONCE at bind() through the scope's params writer (PD-11), so record() writes no uniform. `src/primitives/**` never
14
+ * imports `src/context.ts`.
15
+ */
16
+
17
+ import { WebGpuGraphError } from "../errors.js";
18
+ import { type DispatchPlan, plan1d } from "../kernel/dispatch.js";
19
+ import { type BoundKernel, type Kernel } from "../kernel/kernel.js";
20
+ import { GRID_LEVEL_PARAMS, INDIRECT_PARAMS, kernelSpec } from "../kernels.js";
21
+ import { type Binding } from "../types/memory.js";
22
+ import { type GridSpec } from "./grid.js";
23
+ import { type ReduceScope } from "./reduce.js";
24
+
25
+ /**
26
+ * The buffers the pyramid build reads and writes (the model's named buffers). The parameter type of
27
+ * GridPyramidPlanner.bind (knip: exported for the signature, not imported by name).
28
+ * @public
29
+ */
30
+ export interface GridPyramidBindings {
31
+ /** `array<vec4f>` positions (xyz, mass). */
32
+ readonly pos: Binding;
33
+ /** The `Fa2Params` uniform ring (`dim`, `gridMax`); `record()` takes the iteration's dynamic offset. */
34
+ readonly params: Binding;
35
+ /** `n` words: the sorted node indices (the T8 build). */
36
+ readonly sortedIdx: Binding;
37
+ /** `cells + 2` words: the exclusive scan of the cell histogram (the T8 build). */
38
+ readonly cellStart: Binding;
39
+ /** `pyramidCells` vec4f: every level, level 0 first with the pseudo-cell at index `cells`. */
40
+ readonly pyramid: Binding;
41
+ /** The hub cells' indices, appended by G4 (at least one word; at most `floor(n / (GRID_HUB_CELL + 1))` are ever written, so `ceil(n / GRID_HUB_CELL)` words always suffice). */
42
+ readonly hubList: Binding;
43
+ /** Two u32 (bound whole, at least 8 bytes): `[0]` the hub count, `[1]` the largest cell occupancy; the caller zeroes both before every build (K1, T10). */
44
+ readonly hubCounters: Binding;
45
+ /** One 16-byte indirect args slot with the INDIRECT usage: G4a writes it, G4b dispatches from it. */
46
+ readonly hubArgs: Binding;
47
+ }
48
+
49
+ /** Where `record()` stops: after level 0 is whole (G4, G4a and G4b) or after every coarser level (G5, the default). */
50
+ export type GridPyramidStage = "G4" | "G5";
51
+
52
+ /** A prepared pyramid build (spec 7.7 G4-G5): binds once per load, records the stages of one iteration into a pass. */
53
+ export interface GridPyramidPlanner {
54
+ /**
55
+ * Binds the named buffers and writes the static params of the finalize and of every level (PD-11); called once
56
+ * per load (a second call rebinds and writes fresh params, so it belongs to a reload, never to an iteration).
57
+ * @param bindings - the buffers
58
+ */
59
+ bind(bindings: GridPyramidBindings): void;
60
+ /**
61
+ * Records G4, G4a, G4b and then G5 for every coarser level at the `Fa2Params` slot `paramsOffset`; `upTo: "G4"`
62
+ * stops after G4b (level 0 is whole: G4a and G4b are G4's completion).
63
+ * @param pass - the compute pass
64
+ * @param paramsOffset - the dynamic offset of this iteration's `Fa2Params`
65
+ * @param upTo - the last stage to record (default "G5")
66
+ */
67
+ record(pass: GPUComputePassEncoder, paramsOffset: number, upTo?: GridPyramidStage): void;
68
+ /** Dispatches the last record() issued: 3 after `upTo: "G4"`, `3 + (levels - 1)` for a full record. */
69
+ readonly lastDispatches: number;
70
+ }
71
+
72
+ /**
73
+ * Prepares the pyramid's pipelines over a scope (G4, the finalize, G4b and G5; compiles once) so bind() and record()
74
+ * are synchronous. The planner lives exactly as long as the scope.
75
+ * @param scope - the caller's scope (device, caps, cache, scratch, params)
76
+ * @param spec - the grid
77
+ * @returns the planner
78
+ */
79
+ export async function preparePyramid(scope: ReduceScope, spec: GridSpec): Promise<GridPyramidPlanner> {
80
+ const centroid = await scope.pipelines.kernel(kernelSpec("grid-centroid"));
81
+ const finalize = await scope.pipelines.kernel(kernelSpec("indirect-finalize"));
82
+ const hub = await scope.pipelines.kernel(kernelSpec("grid-centroid-hub"));
83
+ const downsample = await scope.pipelines.kernel(kernelSpec("grid-downsample"));
84
+ return new GridPyramidPlannerImpl(scope, spec, { centroid, finalize, hub, downsample });
85
+ }
86
+
87
+ /** The four kernels of the build. */
88
+ interface Kernels {
89
+ readonly centroid: Kernel;
90
+ readonly finalize: Kernel;
91
+ readonly hub: Kernel;
92
+ readonly downsample: Kernel;
93
+ }
94
+
95
+ /** What bind() prepared: the bound groups and, per coarser level, its bound group with its params offset. */
96
+ interface Bound {
97
+ readonly hubArgs: Binding;
98
+ readonly centroid: BoundKernel;
99
+ readonly finalize: BoundKernel;
100
+ readonly finalizeOffset: number;
101
+ readonly hub: BoundKernel;
102
+ readonly levels: readonly { readonly bound: BoundKernel; readonly offset: number; readonly parentCells: number }[];
103
+ }
104
+
105
+ /** The planner: G4, G4a, G4b and G5 over one scope. */
106
+ class GridPyramidPlannerImpl implements GridPyramidPlanner {
107
+ private readonly scope: ReduceScope;
108
+ private readonly spec: GridSpec;
109
+ private readonly kernels: Kernels;
110
+ private bound: Bound | null = null;
111
+ private dispatches = 0;
112
+
113
+ /**
114
+ * Wraps the resolved kernels; use preparePyramid().
115
+ * @param scope - the caller's scope
116
+ * @param spec - the grid
117
+ * @param kernels - the four kernels
118
+ */
119
+ constructor(scope: ReduceScope, spec: GridSpec, kernels: Kernels) {
120
+ this.scope = scope;
121
+ this.spec = spec;
122
+ this.kernels = kernels;
123
+ }
124
+
125
+ /**
126
+ * Dispatches the last record() issued.
127
+ * @returns the count
128
+ */
129
+ get lastDispatches(): number {
130
+ return this.dispatches;
131
+ }
132
+
133
+ /**
134
+ * Binds the buffers and writes the static params (see the interface).
135
+ * @param bindings - the buffers
136
+ */
137
+ bind(bindings: GridPyramidBindings): void {
138
+ const { centroid, finalize, hub, downsample } = this.kernels;
139
+ const { spec, scope } = this;
140
+ const b = bindings;
141
+ const finalizeParams = scope.params(INDIRECT_PARAMS, {
142
+ countIndex: 0,
143
+ wg: 1, // the finalize plans ceil(count / wg) workgroups over ITEMS; G4b's item is a hub cell, one workgroup each
144
+ slot: 0,
145
+ pad0: 0,
146
+ });
147
+ const levels: { readonly bound: BoundKernel; readonly offset: number; readonly parentCells: number }[] = [];
148
+ let parentSide = spec.g;
149
+ for (let level = 0; level + 1 < spec.levels; level++) {
150
+ parentSide /= 2;
151
+ const parentCells = parentSide ** spec.dim;
152
+ const params = scope.params(GRID_LEVEL_PARAMS, {
153
+ childBase: spec.levelOffsets[level],
154
+ parentBase: spec.levelOffsets[level + 1],
155
+ parentSide,
156
+ parentCells,
157
+ depth: spec.dim === 3 ? 2 : 1,
158
+ pad0: 0,
159
+ pad1: 0,
160
+ pad2: 0,
161
+ });
162
+ levels.push({
163
+ bound: downsample.bind({ pyramid: b.pyramid, P: params.binding }),
164
+ offset: params.offset,
165
+ parentCells,
166
+ });
167
+ }
168
+ this.bound = {
169
+ hubArgs: b.hubArgs,
170
+ centroid: centroid.bind({
171
+ sortedIdx: b.sortedIdx,
172
+ cellStart: b.cellStart,
173
+ pos: b.pos,
174
+ pyramid: b.pyramid,
175
+ hubList: b.hubList,
176
+ hubCounters: b.hubCounters,
177
+ P: b.params,
178
+ }),
179
+ finalize: finalize.bind({ counters: b.hubCounters, args: b.hubArgs, P: finalizeParams.binding }),
180
+ finalizeOffset: finalizeParams.offset,
181
+ hub: hub.bind({
182
+ sortedIdx: b.sortedIdx,
183
+ cellStart: b.cellStart,
184
+ pos: b.pos,
185
+ pyramid: b.pyramid,
186
+ hubList: b.hubList,
187
+ hubCount: b.hubCounters,
188
+ P: b.params,
189
+ }),
190
+ levels,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Records the stages (see the interface).
196
+ * @param pass - the compute pass
197
+ * @param paramsOffset - the `Fa2Params` dynamic offset
198
+ * @param upTo - the last stage
199
+ */
200
+ record(pass: GPUComputePassEncoder, paramsOffset: number, upTo?: GridPyramidStage): void {
201
+ const { bound, scope, spec } = this;
202
+ if (bound === null) {
203
+ throw new WebGpuGraphError("E_NOT_LOADED", "gridPyramid: record() before bind()", { argument: "bind" });
204
+ }
205
+ const { centroid, finalize, hub, downsample } = this.kernels;
206
+ const one: DispatchPlan = { x: 1, y: 1, z: 1, items: 1, stride: null };
207
+ centroid.dispatch(pass, bound.centroid, plan1d(spec.cells + 1, scope.workgroupSize, scope.caps), [paramsOffset]);
208
+ finalize.dispatch(pass, bound.finalize, one, [bound.finalizeOffset]);
209
+ hub.dispatchIndirect(pass, bound.hub, bound.hubArgs, 0, [paramsOffset]);
210
+ this.dispatches = 3;
211
+ if ((upTo ?? "G5") === "G4") {
212
+ return;
213
+ }
214
+ for (const level of bound.levels) {
215
+ downsample.dispatch(pass, level.bound, plan1d(level.parentCells, scope.workgroupSize, scope.caps), [
216
+ level.offset,
217
+ ]);
218
+ this.dispatches += 1;
219
+ }
220
+ }
221
+ }