@graphty/webgpu-graph-algorithms 0.0.0 → 0.2.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 (269) hide show
  1. package/README.md +344 -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,429 @@
1
+ /**
2
+ * The staging ring of spec 4.4: a ring of MAP_READ | COPY_DST buffers (default DEFAULT_STAGING_SLOTS slots of 4 MiB,
3
+ * PLAN DECISION 15) through which every readback of the package flows. `read()` copies with its own encoder and
4
+ * submit, awaits mapAsync, copies out of the mapped range BEFORE unmap (the range is detached at unmap, design 10.7)
5
+ * and never polls onSubmittedWorkDone; requests above the slot size are chunked. Ownership is one-directional: the
6
+ * ring owns its buffers; a CommandBatch (P2) BORROWS one slot through borrowSlot, maps it through mapSlot and always
7
+ * returns it through returnSlot, which unmaps it. Slots at or above OOM_SCOPE_THRESHOLD_BYTES are created through the
8
+ * AllocationTracker (the out-of-memory scope of spec 5.7), the default-size ones directly on the device, so the
9
+ * allocator counts residency and pool buffers only. A mapAsync rejection is E_DEVICE_LOST (or E_DISPOSED after
10
+ * destroyAll).
11
+ *
12
+ * The one invariant every path keeps: a staging buffer whose mapAsync is PENDING is never unmapped and never
13
+ * destroyed. dawn-node 0.4.0 settles the pending promise synchronously on unmap() / destroy() and settles it a
14
+ * second time when Dawn's map callback arrives (a SIGSEGV in AsyncRunner::Reject); on the Vulkan backends the
15
+ * callback has already run inside device.destroy(), on Dawn's Metal backend it arrives after the device-lost
16
+ * fan-out, which is where the macOS host lane lost its vitest worker (CLAUDE.md "Verified Platform Facts"). The
17
+ * ring therefore tracks every map on its slots (mapPending) and defers an unmap, a return or a destroy that
18
+ * arrives during one to the moment the map settles.
19
+ */
20
+
21
+ import { DEFAULT_STAGING_SLOTS, OOM_SCOPE_THRESHOLD_BYTES } from "../constants.js";
22
+ import { type AllocationTracker } from "../device/error-scope.js";
23
+ import { BufferUsage, MapMode } from "../device/webgpu-constants.js";
24
+ import { WebGpuGraphError } from "../errors.js";
25
+
26
+ /** Default capacity of one staging slot (PLAN DECISION 15). */
27
+ const DEFAULT_SLOT_BYTES = 4 * 1024 * 1024;
28
+
29
+ /** Every staging buffer is MAP_READ | COPY_DST. */
30
+ const STAGING_USAGE = BufferUsage.MAP_READ | BufferUsage.COPY_DST;
31
+
32
+ /** Grown slots are sized in 256-byte steps. */
33
+ const SLOT_GRANULE = 256;
34
+
35
+ /**
36
+ * One MAP_READ | COPY_DST staging buffer of the ring. Exported for the borrowers of borrowSlot / returnSlot
37
+ * (contract 3.8): P2-T1's CommandBatch imports it as a type; nothing at P1 imports it by name.
38
+ * @public
39
+ */
40
+ export interface StagingSlot {
41
+ readonly index: number;
42
+ readonly buffer: GPUBuffer;
43
+ readonly capacity: number;
44
+ }
45
+
46
+ /** The ring's bookkeeping of one slot. */
47
+ interface SlotEntry {
48
+ readonly slot: StagingSlot;
49
+ borrowed: boolean;
50
+ /** True between a successful mapAsync through the ring and the unmap of returnSlot (a fallback when mapState is absent). */
51
+ mapped: boolean;
52
+ /** True while a mapAsync through the ring (read() or mapSlot) is pending: returnSlot and destroyAll defer to the settle. */
53
+ mapPending: boolean;
54
+ /** True when returnSlot ran during a pending map: the settle finishes the return. */
55
+ returnRequested: boolean;
56
+ /** True when the buffer was created through the allocator (and is destroyed through it). */
57
+ readonly tracked: boolean;
58
+ }
59
+
60
+ /**
61
+ * Whether an ArrayBuffer-like is a SharedArrayBuffer (checked by tag so the global need not exist).
62
+ * @param buffer - the buffer to test
63
+ * @returns true for a SharedArrayBuffer
64
+ */
65
+ function isSharedBuffer(buffer: ArrayBufferLike): boolean {
66
+ return Object.prototype.toString.call(buffer) === "[object SharedArrayBuffer]";
67
+ }
68
+
69
+ /**
70
+ * The E_INVALID_ARGUMENT error of a bad readback argument.
71
+ * @param argument - the argument name
72
+ * @param value - the value given
73
+ * @param expected - what was expected
74
+ * @returns the error to throw
75
+ */
76
+ function invalid(argument: string, value: unknown, expected: string): WebGpuGraphError {
77
+ return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument}: expected ${expected}`, {
78
+ argument,
79
+ value,
80
+ expected,
81
+ });
82
+ }
83
+
84
+ /** The staging ring (spec 4.4): default 3 slots; grows when every slot is busy; always unmaps and destroys its own buffers. */
85
+ export class Readback {
86
+ private readonly device: GPUDevice;
87
+ private readonly allocator: AllocationTracker;
88
+ private readonly slotBytes: number;
89
+ private readonly ring: SlotEntry[] = [];
90
+ private disposed = false;
91
+
92
+ /**
93
+ * Creates the ring with `slots` slots of `slotBytes` each.
94
+ * @param device - the device the copies run on
95
+ * @param allocator - the context's OOM-scoped allocator (used for slots at or above OOM_SCOPE_THRESHOLD_BYTES)
96
+ * @param options - ring options
97
+ * @param options.slots - slots created up front; default DEFAULT_STAGING_SLOTS
98
+ * @param options.slotBytes - capacity of each slot and the chunk size of read(); default 4 MiB; a positive multiple of 4
99
+ */
100
+ constructor(
101
+ device: GPUDevice,
102
+ allocator: AllocationTracker,
103
+ options?: { readonly slots?: number | undefined; readonly slotBytes?: number | undefined },
104
+ ) {
105
+ const slotBytes = options?.slotBytes ?? DEFAULT_SLOT_BYTES;
106
+ if (!Number.isInteger(slotBytes) || slotBytes <= 0 || slotBytes % 4 !== 0) {
107
+ throw invalid("slotBytes", slotBytes, "a positive multiple of 4");
108
+ }
109
+ const slots = options?.slots ?? DEFAULT_STAGING_SLOTS;
110
+ if (!Number.isInteger(slots) || slots < 0) {
111
+ throw invalid("slots", slots, "a non-negative integer");
112
+ }
113
+ this.device = device;
114
+ this.allocator = allocator;
115
+ this.slotBytes = slotBytes;
116
+ for (let i = 0; i < slots; i++) {
117
+ this.addSlot(slotBytes);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Copies `byteLength` bytes from `src` at `srcOffset` (own encoder, own submit), maps, copies out BEFORE unmap;
123
+ * resolves the bytes (a fresh ArrayBuffer, or `dest.buffer` after `dest.set` when given). Requests above the slot
124
+ * size are chunked.
125
+ * @param src - a COPY_SRC buffer
126
+ * @param byteLength - bytes to read (a positive multiple of 4, within src)
127
+ * @param dest - an optional destination view of at least byteLength bytes over a plain ArrayBuffer
128
+ * @param srcOffset - the byte offset in src (default 0; a multiple of 4)
129
+ * @returns the bytes read
130
+ */
131
+ async read(src: GPUBuffer, byteLength: number, dest?: ArrayBufferView, srcOffset?: number): Promise<ArrayBuffer> {
132
+ this.assertLive();
133
+ const offset = srcOffset ?? 0;
134
+ if (!Number.isInteger(byteLength) || byteLength <= 0 || byteLength % 4 !== 0) {
135
+ throw invalid("byteLength", byteLength, "a positive multiple of 4");
136
+ }
137
+ if (!Number.isInteger(offset) || offset < 0 || offset % 4 !== 0) {
138
+ throw invalid("srcOffset", offset, "a non-negative multiple of 4");
139
+ }
140
+ if (offset + byteLength > src.size) {
141
+ throw invalid("byteLength", byteLength, `srcOffset + byteLength <= src.size (${src.size})`);
142
+ }
143
+ if ((src.usage & BufferUsage.COPY_SRC) === 0) {
144
+ throw invalid("src", src.label, "a buffer with COPY_SRC usage");
145
+ }
146
+ let out: ArrayBuffer;
147
+ let outBytes: Uint8Array;
148
+ if (dest === undefined) {
149
+ out = new ArrayBuffer(byteLength);
150
+ outBytes = new Uint8Array(out);
151
+ } else {
152
+ if (isSharedBuffer(dest.buffer)) {
153
+ throw invalid("dest", "SharedArrayBuffer", "a view over a plain ArrayBuffer");
154
+ }
155
+ if (dest.byteLength < byteLength) {
156
+ throw invalid("dest", dest.byteLength, `at least ${byteLength} bytes`);
157
+ }
158
+ out = dest.buffer as ArrayBuffer;
159
+ outBytes = new Uint8Array(out, dest.byteOffset, byteLength);
160
+ }
161
+ let done = 0;
162
+ while (done < byteLength) {
163
+ const chunk = Math.min(this.slotBytes, byteLength - done);
164
+ const slot = this.borrowSlot(chunk);
165
+ const entry = this.ring[slot.index];
166
+ try {
167
+ const encoder = this.device.createCommandEncoder({ label: "readback" });
168
+ encoder.copyBufferToBuffer(src, offset + done, slot.buffer, 0, chunk);
169
+ this.device.queue.submit([encoder.finish()]);
170
+ await this.map(entry, chunk);
171
+ outBytes.set(new Uint8Array(slot.buffer.getMappedRange(0, chunk)), done);
172
+ } finally {
173
+ this.returnSlot(slot);
174
+ }
175
+ done += chunk;
176
+ }
177
+ return out;
178
+ }
179
+
180
+ /**
181
+ * Reads one u32 counter through the same ring.
182
+ * @param src - a COPY_SRC buffer
183
+ * @param byteOffset - the counter's byte offset (a multiple of 4)
184
+ * @returns the counter value
185
+ */
186
+ async readU32(src: GPUBuffer, byteOffset: number): Promise<number> {
187
+ const bytes = await this.read(src, 4, undefined, byteOffset);
188
+ return new Uint32Array(bytes)[0];
189
+ }
190
+
191
+ /**
192
+ * Borrows an unmapped slot of at least byteLength (grows the ring when none is free); the borrower ALWAYS
193
+ * returns it (spec 4.4).
194
+ * @param byteLength - the bytes the borrower will copy into the slot
195
+ * @returns the slot
196
+ */
197
+ borrowSlot(byteLength: number): StagingSlot {
198
+ this.assertLive();
199
+ if (!Number.isInteger(byteLength) || byteLength <= 0) {
200
+ throw invalid("byteLength", byteLength, "a positive integer");
201
+ }
202
+ // a free slot is unmapped by construction: returnSlot unmaps, and only a borrower maps
203
+ for (const entry of this.ring) {
204
+ if (!entry.borrowed && entry.slot.capacity >= byteLength) {
205
+ entry.borrowed = true;
206
+ return entry.slot;
207
+ }
208
+ }
209
+ const capacity = Math.max(this.slotBytes, Math.ceil(byteLength / SLOT_GRANULE) * SLOT_GRANULE);
210
+ const entry = this.addSlot(capacity);
211
+ entry.borrowed = true;
212
+ return entry.slot;
213
+ }
214
+
215
+ /**
216
+ * mapAsync(READ) on a borrowed slot, tracked by the ring: the slot counts as borrowed, and is neither unmapped
217
+ * nor destroyed, until the map settles (see the file comment). Returns the runtime's own promise, untranslated:
218
+ * the borrower classifies a rejection (a CommandBatch races it against device loss, spec 5.7).
219
+ * @param slot - a slot borrowed from this ring and not yet returned
220
+ * @param byteLength - the bytes to map from offset 0
221
+ * @returns the mapAsync promise
222
+ */
223
+ mapSlot(slot: StagingSlot, byteLength: number): Promise<void> {
224
+ this.assertLive();
225
+ const entry = this.borrowedEntry(slot);
226
+ if (entry.mapPending || entry.mapped) {
227
+ throw invalid("slot", slot.index, "a borrowed slot that is not mapped and has no map pending");
228
+ }
229
+ return this.track(entry, slot.buffer.mapAsync(MapMode.READ, 0, byteLength));
230
+ }
231
+
232
+ /**
233
+ * Returns a borrowed slot (unmapping it if mapped). A slot whose map through the ring is still pending stays
234
+ * borrowed until the map settles, when the ring unmaps and returns it; a borrower that maps a slot itself must
235
+ * unmap it before returning it (spec 4.4; `mapState` decides when the runtime exposes it, otherwise the ring's
236
+ * own record does). A no-op after destroyAll() (the ring is gone; a borrower's finally must never mask the
237
+ * E_DISPOSED / E_DEVICE_LOST of the read it was serving).
238
+ * @param slot - a slot borrowed from this ring and not yet returned
239
+ */
240
+ returnSlot(slot: StagingSlot): void {
241
+ if (this.disposed) {
242
+ return;
243
+ }
244
+ const entry = this.borrowedEntry(slot);
245
+ if (entry.mapPending) {
246
+ entry.returnRequested = true;
247
+ return;
248
+ }
249
+ this.finishReturn(entry);
250
+ }
251
+
252
+ /**
253
+ * The entry of a slot borrowed from this ring and not yet returned.
254
+ * @param slot - the slot
255
+ * @returns its entry
256
+ */
257
+ private borrowedEntry(slot: StagingSlot): SlotEntry {
258
+ const entry = this.ring[slot.index];
259
+ if (entry === undefined || entry.slot !== slot || !entry.borrowed) {
260
+ throw invalid("slot", slot.index, "a slot borrowed from this ring and not yet returned");
261
+ }
262
+ return entry;
263
+ }
264
+
265
+ /**
266
+ * Unmaps a returned slot when it is mapped and frees it.
267
+ * @param entry - the slot's entry (no map pending)
268
+ */
269
+ private finishReturn(entry: SlotEntry): void {
270
+ const state: string | undefined = entry.slot.buffer.mapState;
271
+ const mapped = state === undefined ? entry.mapped : state !== "unmapped";
272
+ if (mapped) {
273
+ entry.slot.buffer.unmap();
274
+ }
275
+ entry.mapped = false;
276
+ entry.borrowed = false;
277
+ entry.returnRequested = false;
278
+ }
279
+
280
+ /**
281
+ * Tracks one mapAsync on a slot: mapPending until it settles, then the deferred work -- the destruction
282
+ * destroyAll() left to the settle, or the return returnSlot() left to it. The handler is attached before the
283
+ * promise is handed out, so it runs before any borrower's continuation.
284
+ * @param entry - the slot's entry
285
+ * @param pending - the runtime's mapAsync promise
286
+ * @returns the same promise
287
+ */
288
+ private track(entry: SlotEntry, pending: Promise<void>): Promise<void> {
289
+ entry.mapPending = true;
290
+ const settle = (mapped: boolean): void => {
291
+ entry.mapPending = false;
292
+ entry.mapped = mapped;
293
+ if (this.disposed) {
294
+ if (mapped) {
295
+ entry.slot.buffer.unmap();
296
+ }
297
+ this.destroySlot(entry);
298
+ entry.mapped = false;
299
+ return;
300
+ }
301
+ if (entry.returnRequested) {
302
+ this.finishReturn(entry);
303
+ }
304
+ };
305
+ void pending.then(
306
+ () => {
307
+ settle(true);
308
+ },
309
+ () => {
310
+ settle(false);
311
+ },
312
+ );
313
+ return pending;
314
+ }
315
+
316
+ /**
317
+ * Number of slots (grows).
318
+ * @returns the slot count
319
+ */
320
+ get slots(): number {
321
+ return this.ring.length;
322
+ }
323
+
324
+ /**
325
+ * Slots currently borrowed.
326
+ * @returns the borrowed count
327
+ */
328
+ get borrowed(): number {
329
+ let count = 0;
330
+ for (const entry of this.ring) {
331
+ if (entry.borrowed) {
332
+ count++;
333
+ }
334
+ }
335
+ return count;
336
+ }
337
+
338
+ /**
339
+ * Destroys every staging buffer (ctx.dispose()); idempotent. A slot whose mapAsync is still pending is NOT
340
+ * destroyed here (the file comment: dawn-node 0.4.0 settles the promise twice); its destruction is deferred to
341
+ * the moment the map settles (track()), where a pending read() still rejects with E_DISPOSED as the contract
342
+ * promises.
343
+ * @internal
344
+ */
345
+ destroyAll(): void {
346
+ for (const entry of this.ring) {
347
+ if (entry.mapPending) {
348
+ continue;
349
+ }
350
+ this.destroySlot(entry);
351
+ }
352
+ this.ring.length = 0;
353
+ this.disposed = true;
354
+ }
355
+
356
+ /**
357
+ * Destroys one slot's buffer through the allocator when it was created there.
358
+ * @param entry - the slot
359
+ */
360
+ private destroySlot(entry: SlotEntry): void {
361
+ if (entry.tracked) {
362
+ this.allocator.destroy(entry.slot.buffer);
363
+ } else {
364
+ entry.slot.buffer.destroy();
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Creates one slot at the end of the ring.
370
+ * @param capacity - the slot's byte capacity
371
+ * @returns the new entry
372
+ */
373
+ private addSlot(capacity: number): SlotEntry {
374
+ const index = this.ring.length;
375
+ const descriptor: GPUBufferDescriptor = {
376
+ label: `readback:slot:${index}`,
377
+ size: capacity,
378
+ usage: STAGING_USAGE,
379
+ };
380
+ const tracked = capacity >= OOM_SCOPE_THRESHOLD_BYTES;
381
+ const buffer = tracked ? this.allocator.createBuffer(descriptor) : this.device.createBuffer(descriptor);
382
+ const entry: SlotEntry = {
383
+ slot: Object.freeze({ index, buffer, capacity }),
384
+ borrowed: false,
385
+ mapped: false,
386
+ mapPending: false,
387
+ returnRequested: false,
388
+ tracked,
389
+ };
390
+ this.ring.push(entry);
391
+ return entry;
392
+ }
393
+
394
+ /**
395
+ * mapAsync(READ) on a slot for read(); a rejection becomes E_DISPOSED (after destroyAll) or E_DEVICE_LOST.
396
+ * @param entry - the ring entry of the borrowed slot
397
+ * @param byteLength - the bytes to map
398
+ */
399
+ private async map(entry: SlotEntry, byteLength: number): Promise<void> {
400
+ let mapped = false;
401
+ let failure: unknown = null;
402
+ try {
403
+ await this.track(entry, entry.slot.buffer.mapAsync(MapMode.READ, 0, byteLength));
404
+ mapped = true;
405
+ } catch (err: unknown) {
406
+ failure = err;
407
+ }
408
+ if (this.disposed) {
409
+ // destroyAll() ran while the map was pending: track() finished the deferred destruction just before this
410
+ throw new WebGpuGraphError("E_DISPOSED", "the readback ring was disposed while a read was pending", {
411
+ label: "readback",
412
+ });
413
+ }
414
+ if (!mapped) {
415
+ const message = failure instanceof Error ? failure.message : String(failure);
416
+ throw new WebGpuGraphError("E_DEVICE_LOST", `mapAsync rejected: ${message}`, {
417
+ reason: "mapAsync",
418
+ message,
419
+ });
420
+ }
421
+ }
422
+
423
+ /** Throws E_DISPOSED after destroyAll(). */
424
+ private assertLive(): void {
425
+ if (this.disposed) {
426
+ throw new WebGpuGraphError("E_DISPOSED", "the readback ring was disposed", { label: "readback" });
427
+ }
428
+ }
429
+ }