@graphty/webgpu-graph-algorithms 0.0.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +378 -23
  2. package/dist/browser.d.ts +1 -0
  3. package/dist/browser.js +32 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/chunks/context-E6iKaeuJ.js +3136 -0
  6. package/dist/chunks/context-E6iKaeuJ.js.map +1 -0
  7. package/dist/node.d.ts +1 -0
  8. package/dist/node.js +131 -0
  9. package/dist/node.js.map +1 -0
  10. package/dist/src/accelerator.d.ts +26 -0
  11. package/dist/src/accelerator.d.ts.map +1 -0
  12. package/dist/src/accelerator.js +101 -0
  13. package/dist/src/accelerator.js.map +1 -0
  14. package/dist/src/algorithms/degree.d.ts +35 -0
  15. package/dist/src/algorithms/degree.d.ts.map +1 -0
  16. package/dist/src/algorithms/degree.js +119 -0
  17. package/dist/src/algorithms/degree.js.map +1 -0
  18. package/dist/src/browser/index.d.ts +23 -0
  19. package/dist/src/browser/index.d.ts.map +1 -0
  20. package/dist/src/browser/index.js +48 -0
  21. package/dist/src/browser/index.js.map +1 -0
  22. package/dist/src/constants.d.ts +92 -0
  23. package/dist/src/constants.d.ts.map +1 -0
  24. package/dist/src/constants.js +92 -0
  25. package/dist/src/constants.js.map +1 -0
  26. package/dist/src/context.d.ts +84 -0
  27. package/dist/src/context.d.ts.map +1 -0
  28. package/dist/src/context.js +304 -0
  29. package/dist/src/context.js.map +1 -0
  30. package/dist/src/device/acquire.d.ts +57 -0
  31. package/dist/src/device/acquire.d.ts.map +1 -0
  32. package/dist/src/device/acquire.js +232 -0
  33. package/dist/src/device/acquire.js.map +1 -0
  34. package/dist/src/device/caps.d.ts +43 -0
  35. package/dist/src/device/caps.d.ts.map +1 -0
  36. package/dist/src/device/caps.js +104 -0
  37. package/dist/src/device/caps.js.map +1 -0
  38. package/dist/src/device/error-scope.d.ts +75 -0
  39. package/dist/src/device/error-scope.d.ts.map +1 -0
  40. package/dist/src/device/error-scope.js +152 -0
  41. package/dist/src/device/error-scope.js.map +1 -0
  42. package/dist/src/device/lost.d.ts +51 -0
  43. package/dist/src/device/lost.d.ts.map +1 -0
  44. package/dist/src/device/lost.js +130 -0
  45. package/dist/src/device/lost.js.map +1 -0
  46. package/dist/src/device/webgpu-constants.d.ts +31 -0
  47. package/dist/src/device/webgpu-constants.d.ts.map +1 -0
  48. package/dist/src/device/webgpu-constants.js +31 -0
  49. package/dist/src/device/webgpu-constants.js.map +1 -0
  50. package/dist/src/errors.d.ts +56 -0
  51. package/dist/src/errors.d.ts.map +1 -0
  52. package/dist/src/errors.js +57 -0
  53. package/dist/src/errors.js.map +1 -0
  54. package/dist/src/index.d.ts +29 -0
  55. package/dist/src/index.d.ts.map +1 -0
  56. package/dist/src/index.js +27 -0
  57. package/dist/src/index.js.map +1 -0
  58. package/dist/src/kernel/batch.d.ts +116 -0
  59. package/dist/src/kernel/batch.d.ts.map +1 -0
  60. package/dist/src/kernel/batch.js +335 -0
  61. package/dist/src/kernel/batch.js.map +1 -0
  62. package/dist/src/kernel/dispatch.d.ts +59 -0
  63. package/dist/src/kernel/dispatch.d.ts.map +1 -0
  64. package/dist/src/kernel/dispatch.js +139 -0
  65. package/dist/src/kernel/dispatch.js.map +1 -0
  66. package/dist/src/kernel/kernel.d.ts +84 -0
  67. package/dist/src/kernel/kernel.d.ts.map +1 -0
  68. package/dist/src/kernel/kernel.js +239 -0
  69. package/dist/src/kernel/kernel.js.map +1 -0
  70. package/dist/src/kernel/pipeline-cache.d.ts +90 -0
  71. package/dist/src/kernel/pipeline-cache.d.ts.map +1 -0
  72. package/dist/src/kernel/pipeline-cache.js +251 -0
  73. package/dist/src/kernel/pipeline-cache.js.map +1 -0
  74. package/dist/src/kernel/prelude.d.ts +35 -0
  75. package/dist/src/kernel/prelude.d.ts.map +1 -0
  76. package/dist/src/kernel/prelude.js +211 -0
  77. package/dist/src/kernel/prelude.js.map +1 -0
  78. package/dist/src/kernel/profiler.d.ts +64 -0
  79. package/dist/src/kernel/profiler.d.ts.map +1 -0
  80. package/dist/src/kernel/profiler.js +120 -0
  81. package/dist/src/kernel/profiler.js.map +1 -0
  82. package/dist/src/kernel/struct-block.d.ts +122 -0
  83. package/dist/src/kernel/struct-block.d.ts.map +1 -0
  84. package/dist/src/kernel/struct-block.js +353 -0
  85. package/dist/src/kernel/struct-block.js.map +1 -0
  86. package/dist/src/kernel/uniform-ring.d.ts +70 -0
  87. package/dist/src/kernel/uniform-ring.d.ts.map +1 -0
  88. package/dist/src/kernel/uniform-ring.js +146 -0
  89. package/dist/src/kernel/uniform-ring.js.map +1 -0
  90. package/dist/src/kernel/wgsl.d.ts +88 -0
  91. package/dist/src/kernel/wgsl.d.ts.map +1 -0
  92. package/dist/src/kernel/wgsl.js +390 -0
  93. package/dist/src/kernel/wgsl.js.map +1 -0
  94. package/dist/src/kernels.d.ts +81 -0
  95. package/dist/src/kernels.d.ts.map +1 -0
  96. package/dist/src/kernels.js +417 -0
  97. package/dist/src/kernels.js.map +1 -0
  98. package/dist/src/layouts/force-simulation.d.ts +498 -0
  99. package/dist/src/layouts/force-simulation.d.ts.map +1 -0
  100. package/dist/src/layouts/force-simulation.js +1650 -0
  101. package/dist/src/layouts/force-simulation.js.map +1 -0
  102. package/dist/src/layouts/forceatlas2.d.ts +210 -0
  103. package/dist/src/layouts/forceatlas2.d.ts.map +1 -0
  104. package/dist/src/layouts/forceatlas2.js +759 -0
  105. package/dist/src/layouts/forceatlas2.js.map +1 -0
  106. package/dist/src/layouts/inputs.d.ts +40 -0
  107. package/dist/src/layouts/inputs.d.ts.map +1 -0
  108. package/dist/src/layouts/inputs.js +185 -0
  109. package/dist/src/layouts/inputs.js.map +1 -0
  110. package/dist/src/layouts/repulsion-exact.d.ts +85 -0
  111. package/dist/src/layouts/repulsion-exact.d.ts.map +1 -0
  112. package/dist/src/layouts/repulsion-exact.js +134 -0
  113. package/dist/src/layouts/repulsion-exact.js.map +1 -0
  114. package/dist/src/layouts/seed.d.ts +56 -0
  115. package/dist/src/layouts/seed.d.ts.map +1 -0
  116. package/dist/src/layouts/seed.js +173 -0
  117. package/dist/src/layouts/seed.js.map +1 -0
  118. package/dist/src/memory/buffer-pool.d.ts +73 -0
  119. package/dist/src/memory/buffer-pool.d.ts.map +1 -0
  120. package/dist/src/memory/buffer-pool.js +170 -0
  121. package/dist/src/memory/buffer-pool.js.map +1 -0
  122. package/dist/src/memory/lease.d.ts +53 -0
  123. package/dist/src/memory/lease.d.ts.map +1 -0
  124. package/dist/src/memory/lease.js +85 -0
  125. package/dist/src/memory/lease.js.map +1 -0
  126. package/dist/src/memory/readback.d.ts +143 -0
  127. package/dist/src/memory/readback.d.ts.map +1 -0
  128. package/dist/src/memory/readback.js +375 -0
  129. package/dist/src/memory/readback.js.map +1 -0
  130. package/dist/src/memory/residency.d.ts +83 -0
  131. package/dist/src/memory/residency.d.ts.map +1 -0
  132. package/dist/src/memory/residency.js +573 -0
  133. package/dist/src/memory/residency.js.map +1 -0
  134. package/dist/src/memory/upload-plan.d.ts +101 -0
  135. package/dist/src/memory/upload-plan.d.ts.map +1 -0
  136. package/dist/src/memory/upload-plan.js +265 -0
  137. package/dist/src/memory/upload-plan.js.map +1 -0
  138. package/dist/src/node/index.d.ts +64 -0
  139. package/dist/src/node/index.d.ts.map +1 -0
  140. package/dist/src/node/index.js +183 -0
  141. package/dist/src/node/index.js.map +1 -0
  142. package/dist/src/primitives/reduce.d.ts +57 -0
  143. package/dist/src/primitives/reduce.d.ts.map +1 -0
  144. package/dist/src/primitives/reduce.js +161 -0
  145. package/dist/src/primitives/reduce.js.map +1 -0
  146. package/dist/src/primitives/segmented-reduce.d.ts +38 -0
  147. package/dist/src/primitives/segmented-reduce.d.ts.map +1 -0
  148. package/dist/src/primitives/segmented-reduce.js +211 -0
  149. package/dist/src/primitives/segmented-reduce.js.map +1 -0
  150. package/dist/src/types/accelerator.d.ts +209 -0
  151. package/dist/src/types/accelerator.d.ts.map +1 -0
  152. package/dist/src/types/accelerator.js +8 -0
  153. package/dist/src/types/accelerator.js.map +1 -0
  154. package/dist/src/types/context.d.ts +114 -0
  155. package/dist/src/types/context.d.ts.map +1 -0
  156. package/dist/src/types/context.js +7 -0
  157. package/dist/src/types/context.js.map +1 -0
  158. package/dist/src/types/layout.d.ts +95 -0
  159. package/dist/src/types/layout.d.ts.map +1 -0
  160. package/dist/src/types/layout.js +6 -0
  161. package/dist/src/types/layout.js.map +1 -0
  162. package/dist/src/types/memory.d.ts +22 -0
  163. package/dist/src/types/memory.d.ts.map +1 -0
  164. package/dist/src/types/memory.js +7 -0
  165. package/dist/src/types/memory.js.map +1 -0
  166. package/dist/src/types/options.d.ts +78 -0
  167. package/dist/src/types/options.d.ts.map +1 -0
  168. package/dist/src/types/options.js +7 -0
  169. package/dist/src/types/options.js.map +1 -0
  170. package/dist/src/types/run.d.ts +13 -0
  171. package/dist/src/types/run.d.ts.map +1 -0
  172. package/dist/src/types/run.js +6 -0
  173. package/dist/src/types/run.js.map +1 -0
  174. package/dist/src/wgsl/degree.wgsl.d.ts +10 -0
  175. package/dist/src/wgsl/degree.wgsl.d.ts.map +1 -0
  176. package/dist/src/wgsl/degree.wgsl.js +25 -0
  177. package/dist/src/wgsl/degree.wgsl.js.map +1 -0
  178. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +12 -0
  179. package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -0
  180. package/dist/src/wgsl/fa2-attraction.wgsl.js +37 -0
  181. package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -0
  182. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +13 -0
  183. package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -0
  184. package/dist/src/wgsl/fa2-integrate.wgsl.js +69 -0
  185. package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -0
  186. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +12 -0
  187. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -0
  188. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +79 -0
  189. package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -0
  190. package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts +15 -0
  191. package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts.map +1 -0
  192. package/dist/src/wgsl/fa2-speed-finalize.wgsl.js +54 -0
  193. package/dist/src/wgsl/fa2-speed-finalize.wgsl.js.map +1 -0
  194. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +14 -0
  195. package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -0
  196. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +57 -0
  197. package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -0
  198. package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts +11 -0
  199. package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts.map +1 -0
  200. package/dist/src/wgsl/fa2-to-scene.wgsl.js +19 -0
  201. package/dist/src/wgsl/fa2-to-scene.wgsl.js.map +1 -0
  202. package/dist/src/wgsl/fill.wgsl.d.ts +7 -0
  203. package/dist/src/wgsl/fill.wgsl.d.ts.map +1 -0
  204. package/dist/src/wgsl/fill.wgsl.js +14 -0
  205. package/dist/src/wgsl/fill.wgsl.js.map +1 -0
  206. package/dist/src/wgsl/reduce.wgsl.d.ts +10 -0
  207. package/dist/src/wgsl/reduce.wgsl.d.ts.map +1 -0
  208. package/dist/src/wgsl/reduce.wgsl.js +63 -0
  209. package/dist/src/wgsl/reduce.wgsl.js.map +1 -0
  210. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +13 -0
  211. package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -0
  212. package/dist/src/wgsl/segmented-reduce.wgsl.js +35 -0
  213. package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -0
  214. package/dist/tsconfig.build.tsbuildinfo +1 -0
  215. package/dist/webgpu-graph-algorithms.d.ts +1 -0
  216. package/dist/webgpu-graph-algorithms.js +4454 -0
  217. package/dist/webgpu-graph-algorithms.js.map +1 -0
  218. package/package.json +108 -17
  219. package/src/accelerator.ts +117 -0
  220. package/src/algorithms/degree.ts +142 -0
  221. package/src/browser/index.ts +57 -0
  222. package/src/constants.ts +116 -0
  223. package/src/context.ts +399 -0
  224. package/src/device/acquire.ts +256 -0
  225. package/src/device/caps.ts +122 -0
  226. package/src/device/error-scope.ts +171 -0
  227. package/src/device/lost.ts +142 -0
  228. package/src/device/webgpu-constants.ts +44 -0
  229. package/src/errors.ts +94 -0
  230. package/src/index.ts +102 -0
  231. package/src/kernel/batch.ts +427 -0
  232. package/src/kernel/dispatch.ts +162 -0
  233. package/src/kernel/kernel.ts +311 -0
  234. package/src/kernel/pipeline-cache.ts +288 -0
  235. package/src/kernel/prelude.ts +229 -0
  236. package/src/kernel/profiler.ts +148 -0
  237. package/src/kernel/struct-block.ts +439 -0
  238. package/src/kernel/uniform-ring.ts +184 -0
  239. package/src/kernel/wgsl.ts +490 -0
  240. package/src/kernels.ts +511 -0
  241. package/src/layouts/force-simulation.ts +2111 -0
  242. package/src/layouts/forceatlas2.ts +942 -0
  243. package/src/layouts/inputs.ts +252 -0
  244. package/src/layouts/repulsion-exact.ts +183 -0
  245. package/src/layouts/seed.ts +198 -0
  246. package/src/memory/buffer-pool.ts +204 -0
  247. package/src/memory/lease.ts +93 -0
  248. package/src/memory/readback.ts +429 -0
  249. package/src/memory/residency.ts +753 -0
  250. package/src/memory/upload-plan.ts +350 -0
  251. package/src/node/index.ts +230 -0
  252. package/src/primitives/reduce.ts +233 -0
  253. package/src/primitives/segmented-reduce.ts +270 -0
  254. package/src/types/accelerator.ts +236 -0
  255. package/src/types/context.ts +135 -0
  256. package/src/types/layout.ts +103 -0
  257. package/src/types/memory.ts +23 -0
  258. package/src/types/options.ts +84 -0
  259. package/src/types/run.ts +13 -0
  260. package/src/wgsl/degree.wgsl.ts +24 -0
  261. package/src/wgsl/fa2-attraction.wgsl.ts +37 -0
  262. package/src/wgsl/fa2-integrate.wgsl.ts +69 -0
  263. package/src/wgsl/fa2-repulsion-exact.wgsl.ts +78 -0
  264. package/src/wgsl/fa2-speed-finalize.wgsl.ts +53 -0
  265. package/src/wgsl/fa2-stats-finalize.wgsl.ts +57 -0
  266. package/src/wgsl/fa2-to-scene.wgsl.ts +19 -0
  267. package/src/wgsl/fill.wgsl.ts +13 -0
  268. package/src/wgsl/reduce.wgsl.ts +62 -0
  269. package/src/wgsl/segmented-reduce.wgsl.ts +35 -0
@@ -0,0 +1,753 @@
1
+ /**
2
+ * The upload cache of spec 4.1 / 4.3 / 4.5: every core array, view array, column and ad hoc array a kernel binds
3
+ * lives on the device exactly once per snapshot core, keyed on the typed-array object (or the Column object for
4
+ * columns, PLAN DECISION 8), recorded per serial so `release(snapshot)` destroys every buffer of a snapshot and its
5
+ * withColumns() siblings, and tombstoned so a live user learns of the release through `isReleased` (spec 4.5).
6
+ * Uploads follow the pure planner of ./upload-plan.ts: the arena path writes ONE buffer with ONE writeBuffer of the
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
10
+ * decided from counts and flags, and the arcToEdge / edgeToArc getters are read only when a segment exists. Nothing
11
+ * is freed by garbage collection; the once-only warning above warnUnreleasedSnapshots names the missing release.
12
+ */
13
+
14
+ import {
15
+ type AttributeTable,
16
+ type Column,
17
+ type CoreArrayName,
18
+ type GpuEligibility,
19
+ type GraphSnapshot,
20
+ type TypedArrayData,
21
+ } from "@graphty/graph-format";
22
+
23
+ import { type AllocationTracker } from "../device/error-scope.js";
24
+ import { BufferUsage } from "../device/webgpu-constants.js";
25
+ import { WebGpuGraphError } from "../errors.js";
26
+ import { type PlanCaps } from "../types/context.js";
27
+ import { type ArcWindow, type Binding } from "../types/memory.js";
28
+ import { planUpload } from "./upload-plan.js";
29
+
30
+ /** The core arrays a kernel binds by default (spec 4.1); the cold segments are named explicitly by the caller. */
31
+ const DEFAULT_NEED: readonly CoreArrayName[] = Object.freeze(["rowPtr", "colIdx", "weights"]);
32
+
33
+ /** The five core arrays in arena (hot-to-cold) order. */
34
+ const CORE_ORDER: readonly CoreArrayName[] = Object.freeze(["rowPtr", "colIdx", "weights", "arcToEdge", "edgeToArc"]);
35
+
36
+ /** Every resident buffer is bound by kernels, filled by writeBuffer and readable back (tests, readbacks of state). */
37
+ const RESIDENT_USAGE = BufferUsage.STORAGE | BufferUsage.COPY_DST | BufferUsage.COPY_SRC;
38
+
39
+ /**
40
+ * The core arrays of a snapshot on the device (spec 4.1); a null member is absent (zero-length, unweighted or an
41
+ * identity permutation) and is bound as a dummy by the kernel layer (3.9). `Binding` itself is declared in
42
+ * src/types/memory.ts (3.3). Exported by contract 3.8: P1-T4's graphBindings / graphOverrides and every later
43
+ * kernel driver take it; nothing at P1-T2 imports it by name.
44
+ * @public
45
+ */
46
+ export interface CoreBinding {
47
+ readonly serial: number;
48
+ readonly plan: "arena" | "perArray" | "windowed";
49
+ readonly rowPtr: Binding;
50
+ readonly colIdx: Binding | null;
51
+ readonly weights: Binding | null;
52
+ readonly arcToEdge: Binding | null;
53
+ readonly edgeToArc: Binding | null;
54
+ readonly windows: readonly ArcWindow[] | null;
55
+ readonly hasWeights: boolean;
56
+ }
57
+
58
+ /**
59
+ * A view's arrays on the device plus the CPU-side scalars (degreeOrder's segmentOffsets) (spec 4.3). Exported by
60
+ * contract 3.8: the degreeOrder permutation of P4's kernels is a ViewBinding; nothing at P1-T2 imports it by name.
61
+ * @public
62
+ */
63
+ export interface ViewBinding {
64
+ readonly view:
65
+ "reverse" | "coo" | "edgeList" | "outDegree" | "inDegree" | "degreeOrder" | "reverseDegreeOrder" | "mate";
66
+ readonly bindings: Readonly<Record<string, Binding>>;
67
+ readonly scalars: Readonly<Record<string, readonly number[]>>;
68
+ }
69
+
70
+ /**
71
+ * A column's gpuView() on the device (spec 4.3). Exported by contract 3.8: P3's layout inputs register columns
72
+ * through it; nothing at P1-T2 imports it by name.
73
+ * @public
74
+ */
75
+ export interface ColumnBinding {
76
+ readonly binding: Binding;
77
+ readonly column: Column;
78
+ readonly version: number;
79
+ readonly eligibility: GpuEligibility;
80
+ readonly components: number;
81
+ }
82
+
83
+ /**
84
+ * An ad hoc array on the device (spec 4.1 last row); `destroy()` is the caller's when no owner was given. Exported
85
+ * by contract 3.8: P3's model resolves column inputs to a registered ArrayBinding; nothing at P1-T2 imports it by
86
+ * name.
87
+ * @public
88
+ */
89
+ export interface ArrayBinding {
90
+ readonly binding: Binding;
91
+ readonly byteLength: number;
92
+ readonly owner: GraphSnapshot | null;
93
+ destroy(): void;
94
+ }
95
+
96
+ /**
97
+ * stats() shape (spec 4.1). Exported by contract 3.8 for the callers of ctx.residency.stats(); nothing at P1-T2
98
+ * imports it by name.
99
+ * @public
100
+ */
101
+ export interface ResidencyStats {
102
+ readonly buffers: number;
103
+ readonly bytes: number;
104
+ readonly snapshots: number;
105
+ readonly perSnapshot: readonly {
106
+ readonly serial: number;
107
+ readonly label: string | null;
108
+ readonly bytes: number;
109
+ readonly buffers: number;
110
+ }[];
111
+ }
112
+
113
+ /** One buffer the residency owns. */
114
+ interface Resident {
115
+ /** The typed-array object or the Column object the buffer was uploaded for. */
116
+ readonly key: object;
117
+ readonly buffer: GPUBuffer;
118
+ readonly byteLength: number;
119
+ /** The owning record's serial, or null for an owner-less ad hoc array. */
120
+ readonly serial: number | null;
121
+ /** For columns: the column.version the buffer holds. */
122
+ version: number;
123
+ destroyed: boolean;
124
+ }
125
+
126
+ /** One uploaded segment of the arena buffer. */
127
+ interface ArenaSegmentBinding {
128
+ readonly offset: number;
129
+ readonly size: number;
130
+ }
131
+
132
+ /** Everything recorded for one serial (a snapshot and its withColumns() siblings). */
133
+ interface ResidencyRecord {
134
+ readonly serial: number;
135
+ readonly label: string | null;
136
+ readonly entries: Resident[];
137
+ plan: "arena" | "perArray" | "windowed" | null;
138
+ arena: {
139
+ readonly buffer: GPUBuffer;
140
+ readonly segments: Readonly<Record<CoreArrayName, ArenaSegmentBinding | null>>;
141
+ } | null;
142
+ readonly bindings: Map<CoreArrayName, Binding>;
143
+ released: boolean;
144
+ }
145
+
146
+ /**
147
+ * Whether an ArrayBuffer-like is a SharedArrayBuffer (checked by tag so the global need not exist).
148
+ * @param buffer - the buffer to test
149
+ * @returns true for a SharedArrayBuffer
150
+ */
151
+ function isSharedBuffer(buffer: ArrayBufferLike): boolean {
152
+ return Object.prototype.toString.call(buffer) === "[object SharedArrayBuffer]";
153
+ }
154
+
155
+ /**
156
+ * The E_INVALID_ARGUMENT error of a bad residency argument.
157
+ * @param argument - the argument name
158
+ * @param value - the value given
159
+ * @param expected - what was expected
160
+ * @returns the error to throw
161
+ */
162
+ function invalid(argument: string, value: unknown, expected: string): WebGpuGraphError {
163
+ return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument}: expected ${expected}`, {
164
+ argument,
165
+ value,
166
+ expected,
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Throws E_SNAPSHOT { reason: "detached" } for a snapshot whose core was transferred away.
172
+ * @param s - the snapshot
173
+ */
174
+ function assertAttached(s: GraphSnapshot): void {
175
+ if (s.detached) {
176
+ throw new WebGpuGraphError("E_SNAPSHOT", `snapshot ${s.serial} is detached (its core was transferred away)`, {
177
+ reason: "detached",
178
+ serial: s.serial,
179
+ });
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Whether a core array exists on the snapshot, from counts and flags alone (mirrors coreByteLengths of
185
+ * ./upload-plan.ts; spec 5.6: zero-length arrays are never uploaded; spec 4.1: identity permutations never
186
+ * materialised).
187
+ * @param s - the snapshot
188
+ * @param name - the core array
189
+ * @returns true when the array has bytes to upload
190
+ */
191
+ function isPresent(s: GraphSnapshot, name: CoreArrayName): boolean {
192
+ switch (name) {
193
+ case "rowPtr":
194
+ return true;
195
+ case "colIdx":
196
+ return s.arcCount > 0;
197
+ case "weights":
198
+ return s.flags.weighted && s.arcCount > 0;
199
+ case "arcToEdge":
200
+ return !s.flags.arcToEdgeIsIdentity && s.arcCount > 0;
201
+ case "edgeToArc":
202
+ return !s.flags.arcToEdgeIsIdentity && s.edgeCount > 0;
203
+ default:
204
+ return false;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * The array object of a PRESENT core array (the caller checked isPresent, so the two getters never materialise).
210
+ * @param s - the snapshot
211
+ * @param name - the core array
212
+ * @returns the array
213
+ */
214
+ function coreArray(s: GraphSnapshot, name: CoreArrayName): TypedArrayData {
215
+ switch (name) {
216
+ case "rowPtr":
217
+ return s.rowPtr;
218
+ case "colIdx":
219
+ return s.colIdx;
220
+ case "weights": {
221
+ const { weights } = s;
222
+ if (weights === null) {
223
+ throw invalid("weights", null, "a weighted snapshot");
224
+ }
225
+ return weights;
226
+ }
227
+ case "arcToEdge":
228
+ return s.arcToEdge;
229
+ case "edgeToArc":
230
+ return s.edgeToArc;
231
+ default:
232
+ throw invalid("name", name, "a core array name");
233
+ }
234
+ }
235
+
236
+ /** The upload cache (spec 4.1): WeakMap on array objects, WeakMap on snapshots, a strong Map by serial; @internal (reached as ctx.residency). */
237
+ export class GraphResidency {
238
+ private readonly device: GPUDevice;
239
+ private readonly caps: PlanCaps;
240
+ private readonly allocator: AllocationTracker;
241
+ private readonly warnUnreleasedSnapshots: number;
242
+ private readonly warn: (message: string) => void;
243
+ /** Fast lookup by the uploaded object (a typed array or a Column). */
244
+ private readonly residents = new WeakMap<object, Resident>();
245
+ /** Fast lookup of a snapshot's record (entries of released records are recognised by `released`). */
246
+ private readonly records = new WeakMap<GraphSnapshot, ResidencyRecord>();
247
+ /** The strong map by serial: withColumns() siblings share one record; cleared by release. */
248
+ private readonly bySerial = new Map<number, ResidencyRecord>();
249
+ /** Owner-less ad hoc arrays. */
250
+ private readonly orphans = new Set<Resident>();
251
+ /** Serials released and not re-uploaded. */
252
+ private readonly tombstones = new Set<number>();
253
+ private liveCount = 0;
254
+ private liveBytesValue = 0;
255
+ private warned = false;
256
+ private disposed = false;
257
+
258
+ /**
259
+ * Creates an empty residency over a device.
260
+ * @param device - the device (queue.writeBuffer)
261
+ * @param caps - the device capabilities the planner reads
262
+ * @param allocator - the context's OOM-scoped allocator (every buffer is created and destroyed through it)
263
+ * @param options - the once-only warning
264
+ * @param options.warnUnreleasedSnapshots - the resident-snapshot count above which the warning fires once
265
+ * @param options.warn - the warning sink; default console.warn
266
+ */
267
+ constructor(
268
+ device: GPUDevice,
269
+ caps: PlanCaps,
270
+ allocator: AllocationTracker,
271
+ options: { readonly warnUnreleasedSnapshots: number; readonly warn?: ((message: string) => void) | undefined },
272
+ ) {
273
+ this.device = device;
274
+ this.caps = caps;
275
+ this.allocator = allocator;
276
+ this.warnUnreleasedSnapshots = options.warnUnreleasedSnapshots;
277
+ this.warn =
278
+ options.warn ??
279
+ ((message: string): void => {
280
+ console.warn(message);
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Uploads (or finds) the core; `need` defaults to ["rowPtr", "colIdx", "weights"]; cold segments on demand
286
+ * (spec 4.2). Never materialises an identity permutation. A windowed plan throws E_TOO_LARGE { path: "windowed" }
287
+ * until P4 (PLAN DECISION 2); a tombstoned serial is lifted and re-uploaded (PLAN DECISION 7).
288
+ * @param s - the snapshot
289
+ * @param need - the core arrays to bind (rowPtr is always included; absent arrays are ignored)
290
+ * @returns the core binding (a frozen object; grows as cold segments are added)
291
+ */
292
+ core(s: GraphSnapshot, need?: readonly CoreArrayName[]): CoreBinding {
293
+ this.assertLive();
294
+ assertAttached(s);
295
+ const wanted = need ?? DEFAULT_NEED;
296
+ const names = CORE_ORDER.filter((name) => (name === "rowPtr" || wanted.includes(name)) && isPresent(s, name));
297
+ const plan = planUpload(s, this.caps, names);
298
+ if (plan.kind === "windowed") {
299
+ throw new WebGpuGraphError(
300
+ "E_TOO_LARGE",
301
+ `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`,
302
+ {
303
+ needed: 4 * s.arcCount,
304
+ limit: this.caps.limits.maxStorageBufferBindingSize,
305
+ path: "windowed",
306
+ algorithm: null,
307
+ },
308
+ );
309
+ }
310
+ const record = this.ensureRecord(s);
311
+ if (record.plan === null) {
312
+ record.plan = plan.kind;
313
+ if (plan.kind === "arena" && s.arena !== null) {
314
+ const { arena } = s;
315
+ const resident = this.upload(
316
+ record,
317
+ s.rowPtr,
318
+ new Uint8Array(arena.buffer, arena.byteOffset, plan.bytes),
319
+ `residency:core:${record.serial}:arena`,
320
+ );
321
+ record.arena = { buffer: resident.buffer, segments: plan.segments };
322
+ }
323
+ }
324
+ for (const name of names) {
325
+ if (!record.bindings.has(name)) {
326
+ record.bindings.set(name, this.bindCore(record, s, name));
327
+ }
328
+ }
329
+ const rowPtr = record.bindings.get("rowPtr");
330
+ if (rowPtr === undefined) {
331
+ throw invalid("rowPtr", null, "a bound rowPtr (internal invariant)");
332
+ }
333
+ const weights = record.bindings.get("weights") ?? null;
334
+ return Object.freeze({
335
+ serial: record.serial,
336
+ plan: plan.kind,
337
+ rowPtr,
338
+ colIdx: record.bindings.get("colIdx") ?? null,
339
+ weights,
340
+ arcToEdge: record.bindings.get("arcToEdge") ?? null,
341
+ edgeToArc: record.bindings.get("edgeToArc") ?? null,
342
+ windows: null,
343
+ hasWeights: weights !== null,
344
+ });
345
+ }
346
+
347
+ /**
348
+ * Uploads (or finds) a view; P1-P3 support outDegree / inDegree / degreeOrder / reverseDegreeOrder; the others
349
+ * -> E_UNSUPPORTED until P7. packViews is accepted and ignored until P7 (a `true` request is E_UNSUPPORTED
350
+ * { option: "packViews" }, PLAN DECISION 9).
351
+ * @param s - the snapshot
352
+ * @param name - the view
353
+ * @param options - view options
354
+ * @param options.packViews - P7's packed reverse upload; `true` is E_UNSUPPORTED until then
355
+ * @returns the view binding
356
+ */
357
+ view(
358
+ s: GraphSnapshot,
359
+ name: "reverse" | "coo" | "edgeList" | "outDegree" | "inDegree" | "degreeOrder" | "reverseDegreeOrder" | "mate",
360
+ options?: { readonly packViews?: boolean | undefined },
361
+ ): ViewBinding {
362
+ this.assertLive();
363
+ assertAttached(s);
364
+ if (options?.packViews === true) {
365
+ throw new WebGpuGraphError("E_UNSUPPORTED", "packViews is not supported before the P7 view uploads", {
366
+ option: "packViews",
367
+ hint: "omit packViews; the four reverse arrays are uploaded separately",
368
+ });
369
+ }
370
+ let array: TypedArrayData;
371
+ let bindingName: string;
372
+ let scalars: Readonly<Record<string, readonly number[]>> = {};
373
+ switch (name) {
374
+ case "outDegree":
375
+ this.assertNotReleased(s);
376
+ this.assertNonEmpty(s);
377
+ array = s.outDegree();
378
+ bindingName = "outDegree";
379
+ break;
380
+ case "inDegree":
381
+ this.assertNotReleased(s);
382
+ this.assertNonEmpty(s);
383
+ array = s.inDegree();
384
+ bindingName = "inDegree";
385
+ break;
386
+ case "degreeOrder": {
387
+ this.assertNotReleased(s);
388
+ this.assertNonEmpty(s);
389
+ const order = s.degreeOrder();
390
+ array = order.perm;
391
+ bindingName = "perm";
392
+ scalars = { segmentOffsets: Array.from(order.segmentOffsets) };
393
+ break;
394
+ }
395
+ case "reverseDegreeOrder": {
396
+ this.assertNotReleased(s);
397
+ this.assertNonEmpty(s);
398
+ const order = s.degreeOrder({ of: "reverse" });
399
+ array = order.perm;
400
+ bindingName = "perm";
401
+ scalars = { segmentOffsets: Array.from(order.segmentOffsets) };
402
+ break;
403
+ }
404
+ case "reverse":
405
+ case "coo":
406
+ case "edgeList":
407
+ case "mate":
408
+ throw new WebGpuGraphError("E_UNSUPPORTED", `the ${name} view is not uploaded before P7`, {
409
+ feature: `view:${name}`,
410
+ hint: "P1-P3 upload outDegree, inDegree, degreeOrder and reverseDegreeOrder",
411
+ });
412
+ default:
413
+ throw invalid("name", name, "a view name");
414
+ }
415
+ const record = this.ensureRecord(s);
416
+ const resident = this.upload(record, array, array, `residency:view:${record.serial}:${name}`);
417
+ const binding: Binding = { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
418
+ return Object.freeze({
419
+ view: name,
420
+ bindings: Object.freeze({ [bindingName]: binding }),
421
+ scalars: Object.freeze(scalars),
422
+ });
423
+ }
424
+
425
+ /**
426
+ * gpuView(name) + column.version; re-uploads in place when the version changed and the byte length did not
427
+ * (spec 4.3). CONTRACT DECISION: `owner` is required so release(owner) can find the buffer (a table has no
428
+ * back-reference to its snapshot). The key is the Column object (PLAN DECISION 8). E_GPU_INELIGIBLE and
429
+ * E_UNKNOWN_COLUMN from the format pass through unchanged.
430
+ * @param table - the table holding the column
431
+ * @param name - the column name
432
+ * @param owner - the snapshot the buffer is recorded against
433
+ * @returns the column binding
434
+ */
435
+ column(table: AttributeTable, name: string, owner: GraphSnapshot): ColumnBinding {
436
+ this.assertLive();
437
+ assertAttached(owner);
438
+ this.assertNotReleased(owner);
439
+ const column = table.require(name);
440
+ const data = table.gpuView(name);
441
+ if (data.byteLength === 0) {
442
+ throw invalid("column", name, "a column with at least one row (zero-length arrays are never uploaded)");
443
+ }
444
+ const record = this.ensureRecord(owner);
445
+ const label = `residency:column:${record.serial}:${column.meta.domain}.${name}`;
446
+ const existing = this.residents.get(column);
447
+ let resident: Resident;
448
+ if (
449
+ existing !== undefined &&
450
+ !existing.destroyed &&
451
+ existing.serial === record.serial &&
452
+ existing.byteLength === data.byteLength
453
+ ) {
454
+ resident = existing;
455
+ if (resident.version !== column.version) {
456
+ this.device.queue.writeBuffer(resident.buffer, 0, data);
457
+ resident.version = column.version;
458
+ }
459
+ } else {
460
+ if (existing !== undefined) {
461
+ // the stale buffer stays in its record until release (spec 4.1: no leak, no stale read)
462
+ this.residents.delete(column);
463
+ }
464
+ resident = this.upload(record, column, data, label);
465
+ resident.version = column.version;
466
+ }
467
+ return Object.freeze({
468
+ binding: { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null },
469
+ column,
470
+ version: resident.version,
471
+ eligibility: column.gpu,
472
+ components: column.meta.components,
473
+ });
474
+ }
475
+
476
+ /**
477
+ * Any format array keyed on the object; registered against `owner` when given (spec 4.1).
478
+ * @param key - the array (a non-empty, 4-byte-multiple array over a plain ArrayBuffer)
479
+ * @param label - the buffer label suffix
480
+ * @param owner - the snapshot to record the buffer against, or undefined for a caller-owned buffer
481
+ * @returns the array binding; `destroy()` is idempotent (PLAN DECISION 12)
482
+ */
483
+ array(key: TypedArrayData, label: string, owner?: GraphSnapshot): ArrayBinding {
484
+ this.assertLive();
485
+ if (key.byteLength === 0 || key.byteLength % 4 !== 0) {
486
+ throw invalid("key", key.byteLength, "a non-empty array whose byteLength is a multiple of 4");
487
+ }
488
+ if (isSharedBuffer(key.buffer)) {
489
+ throw invalid("key", "SharedArrayBuffer", "an array over a plain ArrayBuffer");
490
+ }
491
+ let record: ResidencyRecord | null = null;
492
+ if (owner !== undefined) {
493
+ assertAttached(owner);
494
+ this.assertNotReleased(owner);
495
+ record = this.ensureRecord(owner);
496
+ }
497
+ const resident = this.upload(record, key, key, `residency:array:${label}`);
498
+ const binding: Binding = { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
499
+ return {
500
+ binding,
501
+ byteLength: resident.byteLength,
502
+ owner: owner ?? null,
503
+ destroy: (): void => {
504
+ this.destroyResident(resident, record);
505
+ },
506
+ };
507
+ }
508
+
509
+ /**
510
+ * Destroys every buffer recorded for s.serial (siblings included, Q-27) and tombstones the serial; idempotent
511
+ * (spec 4.5); safe on a snapshot never uploaded (nothing to tombstone, PLAN DECISION 7).
512
+ * @param s - the snapshot (or any withColumns() sibling)
513
+ */
514
+ release(s: GraphSnapshot): void {
515
+ const record = this.bySerial.get(s.serial);
516
+ if (record === undefined) {
517
+ return;
518
+ }
519
+ for (const resident of [...record.entries]) {
520
+ this.destroyResident(resident, record);
521
+ }
522
+ this.forget(record);
523
+ this.tombstones.add(s.serial);
524
+ }
525
+
526
+ /** Drops every record WITHOUT destroying (device lost: the buffers are gone). @internal */
527
+ clearOnLoss(): void {
528
+ for (const record of [...this.bySerial.values()]) {
529
+ for (const resident of record.entries) {
530
+ resident.destroyed = true;
531
+ if (this.residents.get(resident.key) === resident) {
532
+ this.residents.delete(resident.key);
533
+ }
534
+ }
535
+ record.entries.length = 0;
536
+ this.forget(record);
537
+ }
538
+ for (const resident of this.orphans) {
539
+ resident.destroyed = true;
540
+ if (this.residents.get(resident.key) === resident) {
541
+ this.residents.delete(resident.key);
542
+ }
543
+ }
544
+ this.orphans.clear();
545
+ this.liveCount = 0;
546
+ this.liveBytesValue = 0;
547
+ }
548
+
549
+ /** Destroys everything (ctx.dispose()); idempotent. @internal */
550
+ destroyAll(): void {
551
+ for (const record of [...this.bySerial.values()]) {
552
+ for (const resident of [...record.entries]) {
553
+ this.destroyResident(resident, record);
554
+ }
555
+ this.forget(record);
556
+ }
557
+ for (const resident of [...this.orphans]) {
558
+ this.destroyResident(resident, null);
559
+ }
560
+ this.disposed = true;
561
+ }
562
+
563
+ /**
564
+ * The resident buffers, bytes and snapshots (spec 4.1).
565
+ * @returns the statistics
566
+ */
567
+ stats(): ResidencyStats {
568
+ const perSnapshot = [...this.bySerial.values()].map((record) => ({
569
+ serial: record.serial,
570
+ label: record.label,
571
+ bytes: record.entries.reduce((sum, resident) => sum + resident.byteLength, 0),
572
+ buffers: record.entries.length,
573
+ }));
574
+ return { buffers: this.liveCount, bytes: this.liveBytesValue, snapshots: this.bySerial.size, perSnapshot };
575
+ }
576
+
577
+ /**
578
+ * Bytes of every resident buffer (graft: C R-18).
579
+ * @returns the resident byte count
580
+ */
581
+ get residentBytes(): number {
582
+ return this.liveBytesValue;
583
+ }
584
+
585
+ /**
586
+ * True when the serial was released and not re-uploaded. @internal
587
+ * @param serial - a snapshot serial
588
+ * @returns whether a live user holding bindings of that serial must stop
589
+ */
590
+ isReleased(serial: number): boolean {
591
+ return this.tombstones.has(serial);
592
+ }
593
+
594
+ /**
595
+ * Binds one core array: a segment of the arena buffer when the buffer holds it, else its own buffer keyed on
596
+ * the array object (the cold-segment-on-demand path of spec 4.2, and the whole perArray path).
597
+ * @param record - the snapshot's record
598
+ * @param s - the snapshot
599
+ * @param name - a PRESENT core array
600
+ * @returns the binding
601
+ */
602
+ private bindCore(record: ResidencyRecord, s: GraphSnapshot, name: CoreArrayName): Binding {
603
+ if (record.arena !== null) {
604
+ const segment = record.arena.segments[name];
605
+ if (segment !== null) {
606
+ return { buffer: record.arena.buffer, offset: segment.offset, size: segment.size, window: null };
607
+ }
608
+ }
609
+ const array = coreArray(s, name);
610
+ const resident = this.upload(record, array, array, `residency:core:${record.serial}:${name}`);
611
+ return { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
612
+ }
613
+
614
+ /**
615
+ * Uploads `data` into a new buffer keyed on `key`, or returns the resident already uploaded for that key by
616
+ * the same owner (PLAN DECISION 11: a resident belongs to one record).
617
+ * @param record - the owning record, or null for an owner-less array
618
+ * @param key - the object the buffer is keyed on
619
+ * @param data - the bytes (byteLength a multiple of 4, graph-format invariant I10; a view over a plain
620
+ * ArrayBuffer, which is what writeBuffer's GPUAllowSharedBufferSource accepts under @webgpu/types 0.1.72)
621
+ * @param label - the buffer label
622
+ * @returns the resident
623
+ */
624
+ private upload(
625
+ record: ResidencyRecord | null,
626
+ key: object,
627
+ data: ArrayBufferView<ArrayBuffer>,
628
+ label: string,
629
+ ): Resident {
630
+ const serial = record === null ? null : record.serial;
631
+ const existing = this.residents.get(key);
632
+ if (existing !== undefined && !existing.destroyed && existing.serial === serial) {
633
+ return existing;
634
+ }
635
+ const buffer = this.allocator.createBuffer({ label, size: data.byteLength, usage: RESIDENT_USAGE });
636
+ this.device.queue.writeBuffer(buffer, 0, data);
637
+ const resident: Resident = { key, buffer, byteLength: data.byteLength, serial, version: 0, destroyed: false };
638
+ this.residents.set(key, resident);
639
+ if (record === null) {
640
+ this.orphans.add(resident);
641
+ } else {
642
+ record.entries.push(resident);
643
+ }
644
+ this.liveCount++;
645
+ this.liveBytesValue += data.byteLength;
646
+ return resident;
647
+ }
648
+
649
+ /**
650
+ * Destroys one resident buffer and unregisters it; idempotent.
651
+ * @param resident - the resident
652
+ * @param record - its owning record, or null for an orphan
653
+ */
654
+ private destroyResident(resident: Resident, record: ResidencyRecord | null): void {
655
+ if (resident.destroyed) {
656
+ return;
657
+ }
658
+ resident.destroyed = true;
659
+ this.allocator.destroy(resident.buffer);
660
+ if (this.residents.get(resident.key) === resident) {
661
+ this.residents.delete(resident.key);
662
+ }
663
+ if (record === null) {
664
+ this.orphans.delete(resident);
665
+ } else {
666
+ const index = record.entries.indexOf(resident);
667
+ if (index >= 0) {
668
+ record.entries.splice(index, 1);
669
+ }
670
+ }
671
+ this.liveCount--;
672
+ this.liveBytesValue -= resident.byteLength;
673
+ }
674
+
675
+ /**
676
+ * Marks a record released and drops it from the serial map (the snapshot WeakMap entries recognise it by the flag).
677
+ * @param record - the record
678
+ */
679
+ private forget(record: ResidencyRecord): void {
680
+ record.released = true;
681
+ record.bindings.clear();
682
+ record.arena = null;
683
+ record.plan = null;
684
+ this.bySerial.delete(record.serial);
685
+ }
686
+
687
+ /**
688
+ * The record of a snapshot's serial, created when absent (which lifts a tombstone and may fire the warning).
689
+ * @param s - the snapshot
690
+ * @returns the record
691
+ */
692
+ private ensureRecord(s: GraphSnapshot): ResidencyRecord {
693
+ let record = this.records.get(s);
694
+ if (record !== undefined && record.released) {
695
+ record = undefined;
696
+ }
697
+ if (record === undefined) {
698
+ record = this.bySerial.get(s.serial);
699
+ }
700
+ if (record === undefined) {
701
+ record = {
702
+ serial: s.serial,
703
+ label: s.label,
704
+ entries: [],
705
+ plan: null,
706
+ arena: null,
707
+ bindings: new Map<CoreArrayName, Binding>(),
708
+ released: false,
709
+ };
710
+ this.bySerial.set(s.serial, record);
711
+ this.tombstones.delete(s.serial);
712
+ if (!this.warned && this.bySerial.size > this.warnUnreleasedSnapshots) {
713
+ this.warned = true;
714
+ this.warn(
715
+ `[webgpu-graph-algorithms] ${this.bySerial.size} snapshots are resident on the device (warnUnreleasedSnapshots = ${this.warnUnreleasedSnapshots}); call ctx.release(snapshot) for superseded snapshots -- GPU memory is never freed by garbage collection (spec 4.1)`,
716
+ );
717
+ }
718
+ }
719
+ this.records.set(s, record);
720
+ return record;
721
+ }
722
+
723
+ /**
724
+ * Throws E_RELEASED for a tombstoned serial (a bind without a fresh core()).
725
+ * @param s - the snapshot
726
+ */
727
+ private assertNotReleased(s: GraphSnapshot): void {
728
+ if (this.tombstones.has(s.serial)) {
729
+ throw new WebGpuGraphError(
730
+ "E_RELEASED",
731
+ `snapshot ${s.serial} was released; upload it again through core() before binding views, columns or arrays`,
732
+ { serial: s.serial },
733
+ );
734
+ }
735
+ }
736
+
737
+ /**
738
+ * Throws E_INVALID_ARGUMENT for an empty snapshot (a view of it would be a zero-length array, spec 5.6).
739
+ * @param s - the snapshot
740
+ */
741
+ private assertNonEmpty(s: GraphSnapshot): void {
742
+ if (s.nodeCount === 0) {
743
+ throw invalid("snapshot", 0, "nodeCount > 0 (an empty snapshot has no per-node view to upload)");
744
+ }
745
+ }
746
+
747
+ /** Throws E_DISPOSED after destroyAll() (PLAN DECISION 13). */
748
+ private assertLive(): void {
749
+ if (this.disposed) {
750
+ throw new WebGpuGraphError("E_DISPOSED", "the residency was disposed", { label: "residency" });
751
+ }
752
+ }
753
+ }