@graphty/webgpu-graph-algorithms 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/README.md +104 -52
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-CRbw2Wyo.js → context-BXqgCifx.js} +225 -33
  4. package/dist/chunks/context-BXqgCifx.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +12 -10
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +32 -10
  9. package/dist/src/accelerator.js.map +1 -1
  10. package/dist/src/algorithms/components.d.ts.map +1 -1
  11. package/dist/src/algorithms/components.js +12 -13
  12. package/dist/src/algorithms/components.js.map +1 -1
  13. package/dist/src/algorithms/degree.d.ts +6 -8
  14. package/dist/src/algorithms/degree.d.ts.map +1 -1
  15. package/dist/src/algorithms/degree.js +58 -35
  16. package/dist/src/algorithms/degree.js.map +1 -1
  17. package/dist/src/algorithms/pagerank.d.ts.map +1 -1
  18. package/dist/src/algorithms/pagerank.js +16 -14
  19. package/dist/src/algorithms/pagerank.js.map +1 -1
  20. package/dist/src/algorithms/power-iteration.d.ts +2 -2
  21. package/dist/src/algorithms/power-iteration.d.ts.map +1 -1
  22. package/dist/src/algorithms/power-iteration.js +17 -14
  23. package/dist/src/algorithms/power-iteration.js.map +1 -1
  24. package/dist/src/constants.d.ts +85 -8
  25. package/dist/src/constants.d.ts.map +1 -1
  26. package/dist/src/constants.js +85 -8
  27. package/dist/src/constants.js.map +1 -1
  28. package/dist/src/errors.d.ts +3 -2
  29. package/dist/src/errors.d.ts.map +1 -1
  30. package/dist/src/errors.js +2 -1
  31. package/dist/src/errors.js.map +1 -1
  32. package/dist/src/index.d.ts +10 -5
  33. package/dist/src/index.d.ts.map +1 -1
  34. package/dist/src/index.js +14 -5
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/kernel/dispatch.d.ts +8 -3
  37. package/dist/src/kernel/dispatch.d.ts.map +1 -1
  38. package/dist/src/kernel/dispatch.js +18 -7
  39. package/dist/src/kernel/dispatch.js.map +1 -1
  40. package/dist/src/kernel/kernel.d.ts +30 -1
  41. package/dist/src/kernel/kernel.d.ts.map +1 -1
  42. package/dist/src/kernel/kernel.js +49 -5
  43. package/dist/src/kernel/kernel.js.map +1 -1
  44. package/dist/src/kernel/prelude.d.ts.map +1 -1
  45. package/dist/src/kernel/prelude.js +9 -1
  46. package/dist/src/kernel/prelude.js.map +1 -1
  47. package/dist/src/kernel/profiler.d.ts +15 -3
  48. package/dist/src/kernel/profiler.d.ts.map +1 -1
  49. package/dist/src/kernel/profiler.js +27 -4
  50. package/dist/src/kernel/profiler.js.map +1 -1
  51. package/dist/src/kernels.d.ts +18 -8
  52. package/dist/src/kernels.d.ts.map +1 -1
  53. package/dist/src/kernels.js +345 -22
  54. package/dist/src/kernels.js.map +1 -1
  55. package/dist/src/layouts/calibrate.d.ts +51 -0
  56. package/dist/src/layouts/calibrate.d.ts.map +1 -0
  57. package/dist/src/layouts/calibrate.js +172 -0
  58. package/dist/src/layouts/calibrate.js.map +1 -0
  59. package/dist/src/layouts/force-simulation.d.ts +42 -5
  60. package/dist/src/layouts/force-simulation.d.ts.map +1 -1
  61. package/dist/src/layouts/force-simulation.js +84 -22
  62. package/dist/src/layouts/force-simulation.js.map +1 -1
  63. package/dist/src/layouts/forceatlas2.d.ts +107 -38
  64. package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
  65. package/dist/src/layouts/forceatlas2.js +297 -290
  66. package/dist/src/layouts/forceatlas2.js.map +1 -1
  67. package/dist/src/layouts/fruchterman-reingold.d.ts +241 -0
  68. package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -0
  69. package/dist/src/layouts/fruchterman-reingold.js +739 -0
  70. package/dist/src/layouts/fruchterman-reingold.js.map +1 -0
  71. package/dist/src/layouts/model-common.d.ts +140 -0
  72. package/dist/src/layouts/model-common.d.ts.map +1 -0
  73. package/dist/src/layouts/model-common.js +269 -0
  74. package/dist/src/layouts/model-common.js.map +1 -0
  75. package/dist/src/layouts/repulsion-grid.d.ts +152 -0
  76. package/dist/src/layouts/repulsion-grid.d.ts.map +1 -0
  77. package/dist/src/layouts/repulsion-grid.js +318 -0
  78. package/dist/src/layouts/repulsion-grid.js.map +1 -0
  79. package/dist/src/layouts/spring-electrical.d.ts +224 -0
  80. package/dist/src/layouts/spring-electrical.d.ts.map +1 -0
  81. package/dist/src/layouts/spring-electrical.js +665 -0
  82. package/dist/src/layouts/spring-electrical.js.map +1 -0
  83. package/dist/src/memory/residency.d.ts +6 -2
  84. package/dist/src/memory/residency.d.ts.map +1 -1
  85. package/dist/src/memory/residency.js +84 -14
  86. package/dist/src/memory/residency.js.map +1 -1
  87. package/dist/src/primitives/core-shape.d.ts +38 -2
  88. package/dist/src/primitives/core-shape.d.ts.map +1 -1
  89. package/dist/src/primitives/core-shape.js +71 -3
  90. package/dist/src/primitives/core-shape.js.map +1 -1
  91. package/dist/src/primitives/grid-pyramid.d.ts +71 -0
  92. package/dist/src/primitives/grid-pyramid.d.ts.map +1 -0
  93. package/dist/src/primitives/grid-pyramid.js +143 -0
  94. package/dist/src/primitives/grid-pyramid.js.map +1 -0
  95. package/dist/src/primitives/grid.d.ts +118 -0
  96. package/dist/src/primitives/grid.d.ts.map +1 -0
  97. package/dist/src/primitives/grid.js +225 -0
  98. package/dist/src/primitives/grid.js.map +1 -0
  99. package/dist/src/primitives/histogram.d.ts +67 -0
  100. package/dist/src/primitives/histogram.d.ts.map +1 -0
  101. package/dist/src/primitives/histogram.js +190 -0
  102. package/dist/src/primitives/histogram.js.map +1 -0
  103. package/dist/src/primitives/radix-sort.d.ts +75 -0
  104. package/dist/src/primitives/radix-sort.d.ts.map +1 -0
  105. package/dist/src/primitives/radix-sort.js +168 -0
  106. package/dist/src/primitives/radix-sort.js.map +1 -0
  107. package/dist/src/primitives/scan.d.ts +44 -0
  108. package/dist/src/primitives/scan.d.ts.map +1 -0
  109. package/dist/src/primitives/scan.js +151 -0
  110. package/dist/src/primitives/scan.js.map +1 -0
  111. package/dist/src/primitives/segmented-reduce.d.ts +25 -17
  112. package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
  113. package/dist/src/primitives/segmented-reduce.js +166 -47
  114. package/dist/src/primitives/segmented-reduce.js.map +1 -1
  115. package/dist/src/primitives/spmv.d.ts +18 -14
  116. package/dist/src/primitives/spmv.d.ts.map +1 -1
  117. package/dist/src/primitives/spmv.js +94 -58
  118. package/dist/src/primitives/spmv.js.map +1 -1
  119. package/dist/src/primitives/verify.d.ts +49 -0
  120. package/dist/src/primitives/verify.d.ts.map +1 -0
  121. package/dist/src/primitives/verify.js +229 -0
  122. package/dist/src/primitives/verify.js.map +1 -0
  123. package/dist/src/types/accelerator.d.ts +7 -3
  124. package/dist/src/types/accelerator.d.ts.map +1 -1
  125. package/dist/src/types/context.d.ts +53 -0
  126. package/dist/src/types/context.d.ts.map +1 -1
  127. package/dist/src/types/layout.d.ts +52 -0
  128. package/dist/src/types/layout.d.ts.map +1 -1
  129. package/dist/src/types/options.d.ts +43 -1
  130. package/dist/src/types/options.d.ts.map +1 -1
  131. package/dist/src/wgsl/counting-scatter.wgsl.d.ts +8 -0
  132. package/dist/src/wgsl/counting-scatter.wgsl.d.ts.map +1 -0
  133. package/dist/src/wgsl/counting-scatter.wgsl.js +17 -0
  134. package/dist/src/wgsl/counting-scatter.wgsl.js.map +1 -0
  135. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +23 -8
  136. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
  137. package/dist/src/wgsl/fa2-attraction.wgsl.js +100 -17
  138. package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
  139. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +7 -2
  140. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -1
  141. package/dist/src/wgsl/fa2-integrate.wgsl.js +28 -2
  142. package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -1
  143. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +4 -2
  144. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -1
  145. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +14 -5
  146. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -1
  147. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +12 -1
  148. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
  149. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +54 -0
  150. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
  151. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts +8 -0
  152. package/dist/src/wgsl/grid-cell-key.wgsl.d.ts.map +1 -0
  153. package/dist/src/wgsl/grid-cell-key.wgsl.js +30 -0
  154. package/dist/src/wgsl/grid-cell-key.wgsl.js.map +1 -0
  155. package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts +8 -0
  156. package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts.map +1 -0
  157. package/dist/src/wgsl/grid-centroid-hub.wgsl.js +29 -0
  158. package/dist/src/wgsl/grid-centroid-hub.wgsl.js.map +1 -0
  159. package/dist/src/wgsl/grid-centroid.wgsl.d.ts +8 -0
  160. package/dist/src/wgsl/grid-centroid.wgsl.d.ts.map +1 -0
  161. package/dist/src/wgsl/grid-centroid.wgsl.js +29 -0
  162. package/dist/src/wgsl/grid-centroid.wgsl.js.map +1 -0
  163. package/dist/src/wgsl/grid-downsample.wgsl.d.ts +7 -0
  164. package/dist/src/wgsl/grid-downsample.wgsl.d.ts.map +1 -0
  165. package/dist/src/wgsl/grid-downsample.wgsl.js +28 -0
  166. package/dist/src/wgsl/grid-downsample.wgsl.js.map +1 -0
  167. package/dist/src/wgsl/grid-far-field.wgsl.d.ts +13 -0
  168. package/dist/src/wgsl/grid-far-field.wgsl.d.ts.map +1 -0
  169. package/dist/src/wgsl/grid-far-field.wgsl.js +98 -0
  170. package/dist/src/wgsl/grid-far-field.wgsl.js.map +1 -0
  171. package/dist/src/wgsl/grid-near-field.wgsl.d.ts +19 -0
  172. package/dist/src/wgsl/grid-near-field.wgsl.d.ts.map +1 -0
  173. package/dist/src/wgsl/grid-near-field.wgsl.js +129 -0
  174. package/dist/src/wgsl/grid-near-field.wgsl.js.map +1 -0
  175. package/dist/src/wgsl/histogram.wgsl.d.ts +7 -0
  176. package/dist/src/wgsl/histogram.wgsl.d.ts.map +1 -0
  177. package/dist/src/wgsl/histogram.wgsl.js +15 -0
  178. package/dist/src/wgsl/histogram.wgsl.js.map +1 -0
  179. package/dist/src/wgsl/indirect-finalize.wgsl.d.ts +8 -0
  180. package/dist/src/wgsl/indirect-finalize.wgsl.d.ts.map +1 -0
  181. package/dist/src/wgsl/indirect-finalize.wgsl.js +26 -0
  182. package/dist/src/wgsl/indirect-finalize.wgsl.js.map +1 -0
  183. package/dist/src/wgsl/radix-hist.wgsl.d.ts +9 -0
  184. package/dist/src/wgsl/radix-hist.wgsl.d.ts.map +1 -0
  185. package/dist/src/wgsl/radix-hist.wgsl.js +31 -0
  186. package/dist/src/wgsl/radix-hist.wgsl.js.map +1 -0
  187. package/dist/src/wgsl/radix-scatter.wgsl.d.ts +9 -0
  188. package/dist/src/wgsl/radix-scatter.wgsl.d.ts.map +1 -0
  189. package/dist/src/wgsl/radix-scatter.wgsl.js +40 -0
  190. package/dist/src/wgsl/radix-scatter.wgsl.js.map +1 -0
  191. package/dist/src/wgsl/scan-add.wgsl.d.ts +6 -0
  192. package/dist/src/wgsl/scan-add.wgsl.d.ts.map +1 -0
  193. package/dist/src/wgsl/scan-add.wgsl.js +14 -0
  194. package/dist/src/wgsl/scan-add.wgsl.js.map +1 -0
  195. package/dist/src/wgsl/scan-block.wgsl.d.ts +8 -0
  196. package/dist/src/wgsl/scan-block.wgsl.d.ts.map +1 -0
  197. package/dist/src/wgsl/scan-block.wgsl.js +30 -0
  198. package/dist/src/wgsl/scan-block.wgsl.js.map +1 -0
  199. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +22 -8
  200. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -1
  201. package/dist/src/wgsl/segmented-reduce.wgsl.js +84 -15
  202. package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -1
  203. package/dist/src/wgsl/spmv-pull.wgsl.d.ts +22 -11
  204. package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -1
  205. package/dist/src/wgsl/spmv-pull.wgsl.js +110 -36
  206. package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -1
  207. package/dist/tsconfig.build.tsbuildinfo +1 -1
  208. package/dist/webgpu-graph-algorithms.js +5016 -1130
  209. package/dist/webgpu-graph-algorithms.js.map +1 -1
  210. package/package.json +10 -7
  211. package/src/accelerator.ts +46 -12
  212. package/src/algorithms/components.ts +12 -16
  213. package/src/algorithms/degree.ts +58 -43
  214. package/src/algorithms/pagerank.ts +20 -18
  215. package/src/algorithms/power-iteration.ts +19 -18
  216. package/src/constants.ts +108 -8
  217. package/src/errors.ts +3 -1
  218. package/src/index.ts +25 -5
  219. package/src/kernel/dispatch.ts +18 -7
  220. package/src/kernel/kernel.ts +59 -5
  221. package/src/kernel/prelude.ts +15 -0
  222. package/src/kernel/profiler.ts +28 -4
  223. package/src/kernels.ts +378 -24
  224. package/src/layouts/calibrate.ts +187 -0
  225. package/src/layouts/force-simulation.ts +111 -26
  226. package/src/layouts/forceatlas2.ts +346 -324
  227. package/src/layouts/fruchterman-reingold.ts +918 -0
  228. package/src/layouts/model-common.ts +323 -0
  229. package/src/layouts/repulsion-grid.ts +451 -0
  230. package/src/layouts/spring-electrical.ts +845 -0
  231. package/src/memory/residency.ts +126 -20
  232. package/src/primitives/core-shape.ts +91 -4
  233. package/src/primitives/grid-pyramid.ts +221 -0
  234. package/src/primitives/grid.ts +349 -0
  235. package/src/primitives/histogram.ts +273 -0
  236. package/src/primitives/radix-sort.ts +246 -0
  237. package/src/primitives/scan.ts +197 -0
  238. package/src/primitives/segmented-reduce.ts +214 -56
  239. package/src/primitives/spmv.ts +125 -65
  240. package/src/primitives/verify.ts +249 -0
  241. package/src/types/accelerator.ts +15 -3
  242. package/src/types/context.ts +56 -0
  243. package/src/types/layout.ts +58 -0
  244. package/src/types/options.ts +45 -1
  245. package/src/wgsl/counting-scatter.wgsl.ts +16 -0
  246. package/src/wgsl/fa2-attraction.wgsl.ts +100 -17
  247. package/src/wgsl/fa2-integrate.wgsl.ts +28 -2
  248. package/src/wgsl/fa2-repulsion-exact.wgsl.ts +14 -5
  249. package/src/wgsl/fa2-stats-finalize.wgsl.ts +54 -0
  250. package/src/wgsl/grid-cell-key.wgsl.ts +29 -0
  251. package/src/wgsl/grid-centroid-hub.wgsl.ts +28 -0
  252. package/src/wgsl/grid-centroid.wgsl.ts +28 -0
  253. package/src/wgsl/grid-downsample.wgsl.ts +27 -0
  254. package/src/wgsl/grid-far-field.wgsl.ts +97 -0
  255. package/src/wgsl/grid-near-field.wgsl.ts +128 -0
  256. package/src/wgsl/histogram.wgsl.ts +14 -0
  257. package/src/wgsl/indirect-finalize.wgsl.ts +25 -0
  258. package/src/wgsl/radix-hist.wgsl.ts +30 -0
  259. package/src/wgsl/radix-scatter.wgsl.ts +39 -0
  260. package/src/wgsl/scan-add.wgsl.ts +13 -0
  261. package/src/wgsl/scan-block.wgsl.ts +29 -0
  262. package/src/wgsl/segmented-reduce.wgsl.ts +84 -15
  263. package/src/wgsl/spmv-pull.wgsl.ts +110 -36
  264. package/dist/chunks/context-CRbw2Wyo.js.map +0 -1
@@ -0,0 +1,187 @@
1
+ /**
2
+ * calibrateLayout (spec 2.2, 7.8, D26; contract 3.3): the layout-tier micro-benchmark a consumer runs ONCE, off the
3
+ * critical path, to measure the exact / grid crossover on the actual device. For every probed size it builds a seeded
4
+ * G(n, 10n) snapshot, runs ForceAtlas2 on the exact tier and on the grid tier -- one untimed `step(1)` after
5
+ * `load()` (which warms every pipeline through `PipelineCache.warm` of the model's specs, so the first timed step
6
+ * compiles nothing) and then TIMED_ITERATIONS timed `step(1)` calls whose `stats.msPerIteration` (the profiler's GPU
7
+ * time when "timestamp-query" was granted, else the batch wall time) are averaged -- and releases the probe.
8
+ *
9
+ * PLAN DECISION PD-23 (P4-T14): `src/` cannot import `test/helpers` or `benchmarks/`, so the probe graphs are built
10
+ * here from the package's own LCG (`Lcg`, seed.ts) with a fixed seed, and the spec 7.8 rule is re-implemented in
11
+ * `suggestedExactMaxNodes` below; its twin is `exactMaxNodesFromLadder` in benchmarks/layout-exact.bench.ts (the
12
+ * one copy the G3 / G4 records apply to the committed baselines), which test/layouts/calibrate.test.ts compares it
13
+ * with. The function reads `caps` for nothing: never `caps.software` (spec 7.8, Q-6), so on a software adapter it
14
+ * returns honest numbers a caller can compare in shape, never in value.
15
+ */
16
+
17
+ import { fromEdgeArrays, type GraphSnapshot } from "@graphty/graph-format";
18
+
19
+ import { type GpuContext } from "../context.js";
20
+ import { WebGpuGraphError } from "../errors.js";
21
+ import { assertDeviceComputes } from "../primitives/verify.js";
22
+ import { type CalibrateOptions, type GpuCalibration } from "../types/layout.js";
23
+ import { createForceAtlas2 } from "./forceatlas2.js";
24
+ import { Lcg } from "./seed.js";
25
+
26
+ /** The default probe sizes (spec 2.2): the 8k-65k subset of the exact ladder of spec 7.8 / 10.4 T-4. */
27
+ export const CALIBRATE_SIZES: readonly number[] = [8_192, 16_384, 32_768, 65_536];
28
+
29
+ /** The spec 7.8 per-iteration budget of the exact tier, milliseconds (the twin of EXACT_BUDGET_MS in benchmarks/layout-exact.bench.ts). */
30
+ const BUDGET_MS = 4;
31
+
32
+ /** Edges per node of every probe graph (E = 10n, the density of the exact ladder). */
33
+ const EDGES_PER_NODE = 10;
34
+
35
+ /** Timed iterations per size and tier (spec 2.2: "10 timed iterations"). */
36
+ const TIMED_ITERATIONS = 10;
37
+
38
+ /** The seed of every probe graph and of the simulation's own LCG. */
39
+ const SEED = 12345;
40
+
41
+ /**
42
+ * A seeded G(n, 10n) probe snapshot (self-loops and parallels allowed, unweighted, undirected).
43
+ * @param n - the node count
44
+ * @returns the snapshot
45
+ */
46
+ function probeSnapshot(n: number): GraphSnapshot {
47
+ const m = EDGES_PER_NODE * n;
48
+ const rng = new Lcg(SEED);
49
+ const src = new Uint32Array(m);
50
+ const dst = new Uint32Array(m);
51
+ for (let e = 0; e < m; e++) {
52
+ src[e] = Math.floor(rng.next() * n);
53
+ dst[e] = Math.floor(rng.next() * n);
54
+ }
55
+ return fromEdgeArrays({ directed: false, nodeCount: n, src, dst }, { label: `calibrate/${n}` });
56
+ }
57
+
58
+ /**
59
+ * The mean ms per iteration of one tier at one size: one untimed step after load(), then TIMED_ITERATIONS timed
60
+ * steps (reheat() before each so a settled simulation never short-circuits). With the profiler (a device that granted
61
+ * "timestamp-query") the sample is the GPU time of the iteration's passes, the quantity the spec 7.8 rule reads; without
62
+ * it the sample is the step's wall time, which also carries the JS submit and the 12n-byte readback -- a fixed cost of
63
+ * ~1-2 ms on a discrete card that is the same for either tier, so the wall ratio understates the grid tier's
64
+ * advantage at every size and the crossover it suggests is, if anything, too high.
65
+ * @param ctx - the context
66
+ * @param snapshot - the probe snapshot
67
+ * @param tier - the repulsion tier to force
68
+ * @returns milliseconds per iteration
69
+ */
70
+ async function probeTier(ctx: GpuContext, snapshot: GraphSnapshot, tier: "exact" | "grid"): Promise<number> {
71
+ const n = snapshot.nodeCount;
72
+ const positions = new Float32Array(3 * n).fill(Number.NaN);
73
+ const sim = createForceAtlas2(ctx, {
74
+ dim: 2,
75
+ seed: SEED,
76
+ maxIter: 1_000_000,
77
+ iterationsPerStep: 1,
78
+ maxInFlight: 1,
79
+ repulsion: tier,
80
+ compat: "paper",
81
+ });
82
+ try {
83
+ sim.load(snapshot, positions);
84
+ await sim.step(1);
85
+ let total = 0;
86
+ for (let i = 0; i < TIMED_ITERATIONS; i++) {
87
+ sim.reheat();
88
+ const start = performance.now();
89
+ await sim.step(1);
90
+ const wall = performance.now() - start;
91
+ total += sim.stats.msPerIteration ?? wall;
92
+ }
93
+ return total / TIMED_ITERATIONS;
94
+ } finally {
95
+ sim.dispose();
96
+ }
97
+ }
98
+
99
+ /**
100
+ * The largest power of two that is <= n (the twin of floorPow2 in benchmarks/layout-exact.bench.ts).
101
+ * @param n - a number >= 1
102
+ * @returns the largest power of two not above n
103
+ */
104
+ function floorPow2(n: number): number {
105
+ let p = 1;
106
+ while (p * 2 <= n) {
107
+ p *= 2;
108
+ }
109
+ return p;
110
+ }
111
+
112
+ /**
113
+ * The spec 7.8 rule over the probed sizes (the twin of exactMaxNodesFromLadder(rows, budget, gridRows) in
114
+ * benchmarks/layout-exact.bench.ts, which test/layouts/calibrate.test.ts compares it with): the largest probed n with
115
+ * exactMs(n) <= BUDGET_MS and exactMs(n) <= gridMs(n), rounded down to a power of two.
116
+ *
117
+ * CONTRACT DECISION (the review of P4-T14; spec 2.2 is silent): when NO probed size satisfies the rule -- the grid
118
+ * tier faster than the exact tier at every probe (the RTX 4070 SUPER with the default sizes: the grid beats the exact
119
+ * tier from 4k up), or every probe over the budget -- the benchmark twin throws and hands the value to an owner
120
+ * decision (spec 10.4), which a library call cannot do. The answer here is then the largest power of two STRICTLY
121
+ * BELOW the smallest probe (1 when the smallest probe is 1): the exact tier runs at no probed size, the unprobed range
122
+ * below keeps the exact tier (whose cost falls with n^2 and is the oracle), and the value is distinguishable from a
123
+ * rule answer, which is never below floorPow2(min(sizes)). A caller that wants the crossover itself probes smaller
124
+ * sizes (the T-4 ladder from 1k); one that wants the exact tier nowhere passes `repulsion: "grid"`.
125
+ * @param sizes - the probed sizes (integers >= 1)
126
+ * @param exactMs - ms per iteration of the exact tier per size
127
+ * @param gridMs - ms per iteration of the grid tier per size
128
+ * @returns the suggested exactMaxNodes
129
+ */
130
+ export function suggestedExactMaxNodes(
131
+ sizes: readonly number[],
132
+ exactMs: Readonly<Record<number, number>>,
133
+ gridMs: Readonly<Record<number, number>>,
134
+ ): number {
135
+ let best = 0;
136
+ for (const n of sizes) {
137
+ if (exactMs[n] <= BUDGET_MS && exactMs[n] <= gridMs[n] && n > best) {
138
+ best = n;
139
+ }
140
+ }
141
+ return floorPow2(best === 0 ? Math.max(1, Math.min(...sizes) - 1) : best);
142
+ }
143
+
144
+ /**
145
+ * Spec 2.2 calibrateLayout, verbatim: measures ms per iteration of the exact and the grid repulsion tier at every
146
+ * probed size on THIS device and suggests `exactMaxNodes` by the spec 7.8 rule (`suggestedExactMaxNodes` above, with
147
+ * its below-the-range answer when no probed size qualifies). Every probe graph is built here and released before the
148
+ * call returns; `firstCallMs` is the wall time of the whole call (pipeline compilation included the first time). The
149
+ * app passes `suggestedExactMaxNodes` through `createAccelerator(ctx, { layout: { exactMaxNodes } })`; the package
150
+ * never calls this implicitly and `"auto"` never depends on `caps.software`.
151
+ * @param ctx - the context (E_DISPOSED / E_DEVICE_LOST through assertReady)
152
+ * @param options - the probe sizes (positive integers; default CALIBRATE_SIZES)
153
+ * @returns the calibration record
154
+ */
155
+ export async function calibrateLayout(ctx: GpuContext, options?: CalibrateOptions): Promise<GpuCalibration> {
156
+ ctx.assertReady();
157
+ await assertDeviceComputes(ctx);
158
+ const sizes = options?.sizes ?? CALIBRATE_SIZES;
159
+ if (!Array.isArray(sizes) || sizes.length === 0 || !sizes.every((n) => Number.isInteger(n) && n >= 1)) {
160
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "sizes must be a non-empty list of integers >= 1", {
161
+ argument: "sizes",
162
+ value: sizes,
163
+ expected: "a non-empty list of integers >= 1",
164
+ });
165
+ }
166
+ const start = performance.now();
167
+ const exactMsPerIter: Record<number, number> = {};
168
+ const gridMsPerIter: Record<number, number> = {};
169
+ for (const n of sizes) {
170
+ const snapshot = probeSnapshot(n);
171
+ try {
172
+ exactMsPerIter[n] = await probeTier(ctx, snapshot, "exact");
173
+ gridMsPerIter[n] = await probeTier(ctx, snapshot, "grid");
174
+ } finally {
175
+ ctx.release(snapshot);
176
+ }
177
+ }
178
+ const largest = Math.max(...sizes);
179
+ const pairs = largest * (largest - 1);
180
+ return {
181
+ pairsPerSecond: (pairs / exactMsPerIter[largest]) * 1000,
182
+ exactMsPerIter,
183
+ gridMsPerIter,
184
+ suggestedExactMaxNodes: suggestedExactMaxNodes(sizes, exactMsPerIter, gridMsPerIter),
185
+ firstCallMs: performance.now() - start,
186
+ };
187
+ }
@@ -24,11 +24,15 @@ import { BufferUsage } from "../device/webgpu-constants.js";
24
24
  import { hasErrorCode, WebGpuGraphError } from "../errors.js";
25
25
  import { CommandBatch, type ReadbackRequest, type SubmittedBatch } from "../kernel/batch.js";
26
26
  import { type PipelineCache } from "../kernel/pipeline-cache.js";
27
+ import { type PassTiming } from "../kernel/profiler.js";
27
28
  import { type UniformBlock, type UniformFieldType, type UniformValues } from "../kernel/struct-block.js";
28
29
  import { UniformRing } from "../kernel/uniform-ring.js";
29
30
  import { type WgslModuleSpec } from "../kernel/wgsl.js";
30
31
  import { graphOverrides } from "../kernels.js";
32
+ import { type BufferPool } from "../memory/buffer-pool.js";
31
33
  import { type ArrayBinding, type CoreBinding } from "../memory/residency.js";
34
+ import { type DegreeTiers, degreeTiersOf } from "../primitives/core-shape.js";
35
+ import { assertDeviceComputes } from "../primitives/verify.js";
32
36
  import { type PlanCaps } from "../types/context.js";
33
37
  import {
34
38
  type GpuLayoutSimulation,
@@ -65,7 +69,14 @@ export interface ModelResources {
65
69
  readonly caps: PlanCaps;
66
70
  readonly pipelines: PipelineCache;
67
71
  readonly core: CoreBinding;
72
+ /** `tiers.perm`, or null when no row has degree >= 32 (P4 PD-7: the rowPtr dummy is bound and USE_PERM is false). */
68
73
  readonly perm: Binding | null;
74
+ /**
75
+ * The degree tiers of the snapshot's degreeOrder view (the perm binding and the CPU-side segment offsets
76
+ * [0, hiEnd, midEnd, lowEnd, n]), or null when no row has degree >= 32 -- then every K2 dispatch is the
77
+ * thread-per-row TIER 0 over node indices, as before P4.
78
+ */
79
+ readonly tiers: DegreeTiers | null;
69
80
  /**
70
81
  * The RESOLVED weights binding (model.inputs(): source "arcs" -> core.weights (null on an unweighted snapshot),
71
82
  * "column" -> the registered ArrayBinding's binding, "none" -> null); group 0 is built as
@@ -76,6 +87,8 @@ export interface ModelResources {
76
87
  readonly dim: 2 | 3;
77
88
  readonly tier: "exact" | "grid";
78
89
  readonly ring: UniformRing;
90
+ /** The context's buffer pool (the grid stage's lease of sort scratch draws on it, P4 PD-11). */
91
+ readonly pool: BufferPool;
79
92
  /** A shared or model-owned buffer by name: "positions", "scenePositions", "fixed", "partials", "state", "trace", plus every BufferSpec name. */
80
93
  buffer(name: string): Binding;
81
94
  }
@@ -84,6 +97,8 @@ export interface ModelResources {
84
97
  export interface ModelInputs {
85
98
  readonly mass: F32;
86
99
  readonly weights: ResolvedWeights;
100
+ /** A mask to apply at load (the FR `fixed` option, PD-6); validated against `ceil(n / 32)` words; absent / null leaves the words as they are (kept on a same-size reload, cleared on a resize, spec 7.12). */
101
+ readonly fixed?: NodeMask | null | undefined;
87
102
  }
88
103
 
89
104
  /**
@@ -123,6 +138,8 @@ export interface ForceModel<Options, Stats extends LayoutStatsBase> {
123
138
  onReheat(state: StateWriter): void;
124
139
  onSetParams(patch: Partial<Options>, state: StateWriter): void;
125
140
  readStats(state: DataView, trace: DataView): Stats;
141
+ /** Releases what the model holds outside the simulation's buffers (the grid stage's lease, P4-T10); called by dispose() once every in-flight batch has settled. */
142
+ dispose?(): void;
126
143
  }
127
144
 
128
145
  // ============================================================ module-private helpers
@@ -154,6 +171,21 @@ const SHARED_STATE_FIELDS: readonly (readonly [string, number])[] = [
154
171
  /** The names of the shared buffers a BufferSpec may not reuse. */
155
172
  const SHARED_BUFFER_NAMES: readonly string[] = ["positions", "scenePositions", "fixed", "partials", "state", "trace"];
156
173
 
174
+ /** The buffers inspect() returns as Uint32Array: the mask, the trace records and the grid tier's index / count arrays (P4). */
175
+ const U32_BUFFER_NAMES: ReadonlySet<string> = new Set([
176
+ "fixed",
177
+ "trace",
178
+ "cellKey",
179
+ "cellVal",
180
+ "sortedKey",
181
+ "sortedIdx",
182
+ "cellHist",
183
+ "cellStart",
184
+ "hubList",
185
+ "hubCounters",
186
+ "hubArgs",
187
+ ]);
188
+
157
189
  /** The per-batch epilogue stage: iterations 0..k-2 stop after the stage that precedes it (PLAN DECISION 2). */
158
190
  const EPILOGUE_STAGE = "toScene";
159
191
 
@@ -353,12 +385,13 @@ function centerOf(options: CommonLayoutOptions): [number, number, number] {
353
385
  }
354
386
 
355
387
  /**
356
- * The repulsion tier of a node count under a tuning (PLAN DECISION 11).
388
+ * The repulsion tier of a node count under a tuning (PLAN DECISION 11; spec 7.8: `"auto"` by `n` alone). Exported
389
+ * so a model's `buffers(n, dim)` decides the tier by the same rule `load()` applies (P4 PD-18).
357
390
  * @param tuning - the resolved tuning
358
391
  * @param n - the node count
359
392
  * @returns "exact" or "grid"
360
393
  */
361
- function tierFor(tuning: ResolvedLayoutTuning, n: number): "exact" | "grid" {
394
+ export function tierFor(tuning: ResolvedLayoutTuning, n: number): "exact" | "grid" {
362
395
  if (tuning.repulsion === "exact") {
363
396
  return "exact";
364
397
  }
@@ -619,6 +652,7 @@ export class ForceSimulation<
619
652
  private settledValue = false;
620
653
  private firstPending = true;
621
654
  private statsValue: Stats | null = null;
655
+ private lastPassTimingsValue: readonly PassTiming[] | null = null;
622
656
  private lastSubmittedBatchIdValue = 0;
623
657
  /** The id of the last batch submitted BEFORE the most recent reheat() (PLAN DECISION 21); 0 = none. */
624
658
  private reheatedAfterBatchId = 0;
@@ -729,13 +763,24 @@ export class ForceSimulation<
729
763
  }
730
764
 
731
765
  /**
732
- * The repulsion tier of the current load ("exact" until P4 lifts the grid tier).
766
+ * The repulsion tier of the current load (spec 7.8).
733
767
  * @returns the tier
734
768
  */
735
769
  get tier(): "exact" | "grid" {
736
770
  return this.tierValue;
737
771
  }
738
772
 
773
+ /**
774
+ * The profiler's per-pass timings of the last landed batch (pass order; the grid tier's `fa2-k1` /
775
+ * `fa2-attraction` / `fa2-grid` / `fa2-to-scene` rows give T-7's attraction figure, P4 PD-16), or null before a
776
+ * batch landed or without `timestamp-query`.
777
+ * @internal
778
+ * @returns the timings
779
+ */
780
+ get lastPassTimings(): readonly PassTiming[] | null {
781
+ return this.lastPassTimingsValue;
782
+ }
783
+
739
784
  /**
740
785
  * The generation counter bumped by every load() (stale readbacks are discarded).
741
786
  * @internal
@@ -886,16 +931,6 @@ export class ForceSimulation<
886
931
  const scale = scaleOf(this.optionsValue);
887
932
  const center = centerOf(this.optionsValue);
888
933
  const tier = tierFor(this.tuning, n);
889
- if (tier === "grid") {
890
- throw new WebGpuGraphError(
891
- "E_UNSUPPORTED",
892
- `the grid repulsion tier lands at P4 (n = ${n}, exactMaxNodes = ${this.tuning.exactMaxNodes})`,
893
- {
894
- feature: "repulsion.grid",
895
- hint: 'pass repulsion: "exact" or raise exactMaxNodes',
896
- },
897
- );
898
- }
899
934
  const positionsBytes = 16 * n;
900
935
  if (positionsBytes > this.ctx.caps.limits.maxBufferSize) {
901
936
  throw new WebGpuGraphError("E_TOO_LARGE", `${positionsBytes} bytes of positions exceed maxBufferSize`, {
@@ -906,6 +941,7 @@ export class ForceSimulation<
906
941
  });
907
942
  }
908
943
  let core: CoreBinding | null = null;
944
+ let tiers: DegreeTiers | null = null;
909
945
  let inputs: ModelInputs | null = null;
910
946
  if (n > 0) {
911
947
  core = this.ctx.residency.core(snapshot);
@@ -921,6 +957,11 @@ export class ForceSimulation<
921
957
  },
922
958
  );
923
959
  }
960
+ // PD-7 (the 7.3 rule): the permutation is bound iff a row of degree >= 32 exists; the view is uploaded
961
+ // only then, so a low-degree snapshot compiles and runs the same pipelines as before P4
962
+ if (snapshot.degreeOrder().segmentOffsets[2] > 0) {
963
+ tiers = degreeTiersOf(this.ctx.residency.view(snapshot, "degreeOrder"));
964
+ }
924
965
  inputs = this.model.inputs(snapshot, this.optionsValue);
925
966
  if (inputs.mass.length !== n) {
926
967
  throw invalidArgument(
@@ -930,6 +971,15 @@ export class ForceSimulation<
930
971
  `the model resolved ${inputs.mass.length} masses for ${n} nodes`,
931
972
  );
932
973
  }
974
+ const fixedWords = Math.ceil(n / 32);
975
+ if (inputs.fixed !== undefined && inputs.fixed !== null && inputs.fixed.length < fixedWords) {
976
+ throw invalidArgument(
977
+ "fixed",
978
+ inputs.fixed.length,
979
+ fixedWords,
980
+ `the model resolved a fixed mask of ${inputs.fixed.length} words, ${fixedWords} needed for ${n} nodes`,
981
+ );
982
+ }
933
983
  }
934
984
 
935
985
  // ---- every check passed: mutate
@@ -956,6 +1006,10 @@ export class ForceSimulation<
956
1006
  this.fixedWords = makeMask(n);
957
1007
  this.fixedDirty = false;
958
1008
  }
1009
+ if (inputs !== null && inputs.fixed !== undefined && inputs.fixed !== null) {
1010
+ this.fixedWords.set(inputs.fixed.subarray(0, Math.ceil(n / 32)));
1011
+ this.fixedDirty = true;
1012
+ }
959
1013
  if (n === 0 || core === null || inputs === null) {
960
1014
  // PLAN DECISION 10: an empty graph loads with no GPU work
961
1015
  this.weightsUpload = null;
@@ -976,8 +1030,11 @@ export class ForceSimulation<
976
1030
  seedPositions(snapshot, positions, this.optionsValue.seed ?? null, this.dimValue, scale, center, range);
977
1031
  this.uploadPositions(buffers, positions, inputs.mass);
978
1032
  const weights = this.resolveWeightsBinding(snapshot, core, inputs.weights);
979
- const overrides = { ...this.model.overrides(this.optionsValue), ...graphOverrides(core, null, weights) };
980
- const resources = this.makeResources(core, weights, buffers);
1033
+ const overrides = {
1034
+ ...this.model.overrides(this.optionsValue),
1035
+ ...graphOverrides(core, tiers?.perm ?? null, weights),
1036
+ };
1037
+ const resources = this.makeResources(core, tiers, weights, buffers);
981
1038
  this.resources = resources;
982
1039
  this.startBind(resources, overrides);
983
1040
  this.settledValue = false;
@@ -1252,7 +1309,10 @@ export class ForceSimulation<
1252
1309
  this.center = center;
1253
1310
  const { resources, core } = this;
1254
1311
  if (this.stateValue === "loaded" && resources !== null && core !== null && before !== after) {
1255
- const overrides = { ...this.model.overrides(next), ...graphOverrides(core, null, resources.weights) };
1312
+ const overrides = {
1313
+ ...this.model.overrides(next),
1314
+ ...graphOverrides(core, resources.perm, resources.weights),
1315
+ };
1256
1316
  this.startBind(resources, overrides);
1257
1317
  }
1258
1318
  this.model.onSetParams(patch, this.writer);
@@ -1469,7 +1529,10 @@ export class ForceSimulation<
1469
1529
  }
1470
1530
 
1471
1531
  /**
1472
- * The batch's duration in milliseconds: the profiler's pass timings summed when present, else wall time.
1532
+ * The batch's duration in milliseconds: the profiler's pass timings summed when present and complete, else wall
1533
+ * time. The profiler budgets PROFILER_QUERY_SLOTS / 2 passes per batch; the grid tier records three passes per
1534
+ * iteration (P4 PD-16), so a grid batch above (PROFILER_QUERY_SLOTS / 2 - 1) / 3 iterations is timed partially:
1535
+ * its rows stay in `lastPassTimings` but the duration is the wall time, never the sum of a prefix.
1473
1536
  * @param record - the batch
1474
1537
  * @param bytes - its readback (the profiler's resolve lands in it)
1475
1538
  * @returns milliseconds
@@ -1478,7 +1541,8 @@ export class ForceSimulation<
1478
1541
  const { profiler } = this.ctx;
1479
1542
  if (profiler !== null && record.profile !== null) {
1480
1543
  const timings = profiler.timings(bytes, record.profile);
1481
- if (timings.length > 0) {
1544
+ this.lastPassTimingsValue = timings;
1545
+ if (timings.length > 0 && !profiler.partial(record.profile)) {
1482
1546
  let ns = 0;
1483
1547
  for (const timing of timings) {
1484
1548
  ns += timing.ns;
@@ -1542,22 +1606,24 @@ export class ForceSimulation<
1542
1606
  * The uniform values of one iteration slot: the model's values with the shared fields on top (PLAN DECISION 6).
1543
1607
  * @param global - the global iteration index (iterationsSubmitted + i)
1544
1608
  * @param index - the slot index within the batch (the trace slot)
1545
- * @param flags - FA2_FLAG_FIRST for the first iteration after load(), else 0
1609
+ * @param flags - FA2_FLAG_FIRST for the first iteration after load(), else 0; the model's own flag bits are OR-ed in
1546
1610
  * @returns the values
1547
1611
  */
1548
1612
  private paramsForSlot(global: number, index: number, flags: number): UniformValues {
1549
1613
  const [cx, cy, cz] = this.center;
1614
+ const model = this.model.paramsFor(global, this.optionsValue);
1615
+ const modelFlags = typeof model.flags === "number" ? model.flags : 0;
1550
1616
  const shared: UniformValues = {
1551
1617
  n: this.n,
1552
1618
  dim: this.dimValue,
1553
- flags,
1619
+ flags: flags | modelFlags,
1554
1620
  iterationIndex: index,
1555
1621
  seed: this.seedU32(),
1556
1622
  scale: this.scale,
1557
1623
  center: [cx, cy, cz, 0],
1558
1624
  settleThreshold: this.settleThreshold(),
1559
1625
  };
1560
- return { ...this.model.paramsFor(global, this.optionsValue), ...shared };
1626
+ return { ...model, ...shared };
1561
1627
  }
1562
1628
 
1563
1629
  /**
@@ -1917,23 +1983,31 @@ export class ForceSimulation<
1917
1983
  /**
1918
1984
  * The ModelResources of a load.
1919
1985
  * @param core - the core
1986
+ * @param tiers - the degree tiers, or null when no row has degree >= 32
1920
1987
  * @param weights - the resolved weights binding
1921
1988
  * @param buffers - the buffers
1922
1989
  * @returns the resources
1923
1990
  */
1924
- private makeResources(core: CoreBinding, weights: Binding | null, buffers: SimulationBuffers): ModelResources {
1991
+ private makeResources(
1992
+ core: CoreBinding,
1993
+ tiers: DegreeTiers | null,
1994
+ weights: Binding | null,
1995
+ buffers: SimulationBuffers,
1996
+ ): ModelResources {
1925
1997
  const { bindings } = buffers;
1926
1998
  return {
1927
1999
  device: this.ctx.device,
1928
2000
  caps: this.ctx.caps,
1929
2001
  pipelines: this.ctx.pipelines,
1930
2002
  core,
1931
- perm: null,
2003
+ perm: tiers?.perm ?? null,
2004
+ tiers,
1932
2005
  weights,
1933
2006
  n: this.n,
1934
2007
  dim: this.dimValue,
1935
2008
  tier: this.tierValue,
1936
2009
  ring: this.ring,
2010
+ pool: this.ctx.pool,
1937
2011
  buffer: (name: string): Binding => {
1938
2012
  const binding = bindings.get(name);
1939
2013
  if (binding === undefined) {
@@ -1963,7 +2037,16 @@ export class ForceSimulation<
1963
2037
  }
1964
2038
 
1965
2039
  /**
1966
- * warm(model.specs()) then model.bind(); the await re-checks that the load is current before binding.
2040
+ * assertDeviceComputes() then warm(model.specs()) then model.bind(); the await re-checks that the load is
2041
+ * current before binding.
2042
+ *
2043
+ * The device self-check sits HERE rather than in submitBatch (its first home) because this promise is the one
2044
+ * submitBatch already awaits before every batch: the check is therefore settled before the simulation can
2045
+ * submit a single iteration, yet costs the frame loop nothing per batch. An extra await on the per-batch path
2046
+ * moves the moment a submission becomes visible to a step() issued in the same tick, which is precisely what
2047
+ * coalescing is measured on (test/layouts/frame-loop.test.ts, "a coalesced step() returns the same promise").
2048
+ * A device that computes incorrectly rejects this promise, so the first step() rejects E_DEVICE_INCORRECT
2049
+ * before any position is written; a check that could not RUN rejects with its own error, unchanged.
1967
2050
  * @param resources - the resources
1968
2051
  * @param overrides - the merged override set
1969
2052
  * @param generation - the generation the bind belongs to
@@ -1973,6 +2056,7 @@ export class ForceSimulation<
1973
2056
  overrides: Readonly<Record<string, number | boolean>>,
1974
2057
  generation: number,
1975
2058
  ): Promise<void> {
2059
+ await assertDeviceComputes(this.ctx);
1976
2060
  const subgroups = this.ctx.caps.features.has("subgroups");
1977
2061
  await this.ctx.pipelines.warm(this.model.specs(overrides, subgroups));
1978
2062
  if (this.stateValue !== "loaded" || generation !== this.generationValue) {
@@ -1984,7 +2068,7 @@ export class ForceSimulation<
1984
2068
  // ---------------------------------------------------------------- private: inspect and debug runs
1985
2069
 
1986
2070
  /**
1987
- * inspect(name): flush, read the named buffer back, Uint32Array for "fixed" and "trace", Float32Array otherwise.
2071
+ * inspect(name): flush, read the named buffer back, Uint32Array for the u32 buffers (U32_BUFFER_NAMES), Float32Array otherwise.
1988
2072
  * @param name - a shared or BufferSpec name
1989
2073
  * @returns the words
1990
2074
  */
@@ -2002,7 +2086,7 @@ export class ForceSimulation<
2002
2086
  }
2003
2087
  await this.flush();
2004
2088
  const bytes = await this.ctx.readback.read(binding.buffer, binding.size, undefined, binding.offset);
2005
- return name === "fixed" || name === "trace" ? new Uint32Array(bytes) : new Float32Array(bytes);
2089
+ return U32_BUFFER_NAMES.has(name) ? new Uint32Array(bytes) : new Float32Array(bytes);
2006
2090
  }
2007
2091
 
2008
2092
  /**
@@ -2083,6 +2167,7 @@ export class ForceSimulation<
2083
2167
  this.destroyBuffers();
2084
2168
  this.afterInFlight(() => {
2085
2169
  this.ring.destroy();
2170
+ this.model.dispose?.();
2086
2171
  });
2087
2172
  if (this.ctx.state === "ready") {
2088
2173
  this.ctx.pool.trim();