@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,427 @@
1
+ /**
2
+ * The CommandBatch of spec 5.8: one GPUCommandEncoder, one compute pass per "phase" (a pass may hold
3
+ * many dispatches; dispatches inside a pass are ordered), the staging copies of every readback
4
+ * request at the end, and ONE queue.submit. The batch borrows one slot of the Readback ring for its
5
+ * lifetime (spec 4.4) and returns it when its readback settles -- resolved, discarded or rejected --
6
+ * so the ring never grows because of a slot left pending. mapAsync on that slot is the completion
7
+ * signal; a batch never calls onSubmittedWorkDone.
8
+ *
9
+ * Errors (spec 5.7): under Dawn-node an uncapturederror is delivered SYNCHRONOUSLY inside the API
10
+ * call that fails, so an error pending in the context's slot when submit() runs belongs to this batch
11
+ * and rejects ITS OWN readback (an invalid command buffer is a no-op submit; the staging bytes would be
12
+ * stale). In browsers delivery is asynchronous: the batch takes the slot again after its map completes,
13
+ * and whatever arrives later is thrown by the context's next public call. Device loss is raced against
14
+ * the map, so a readback rejects E_DEVICE_LOST whether the runtime rejects, resolves or never settles
15
+ * the pending map of a destroyed buffer; the slot goes back only once the map has settled (or the grace
16
+ * ran out), because a staging buffer is never unmapped while its map is pending (readback.ts).
17
+ */
18
+
19
+ import type { AllocationTracker } from "../device/error-scope.js";
20
+ import { deviceLostError } from "../device/lost.js";
21
+ import { isWebGpuGraphError, WebGpuGraphError } from "../errors.js";
22
+ import type { Readback, StagingSlot } from "../memory/readback.js";
23
+ import type { Binding } from "../types/memory.js";
24
+ import type { Profiler } from "./profiler.js";
25
+
26
+ /** How long a batch waits for the context's loss fan-out after its map rejected with a foreign error (a destroyed buffer rejects before device.lost settles), and for the runtime to settle a pending map after the loss won the race. */
27
+ const LOSS_GRACE_MS = 2000;
28
+
29
+ /**
30
+ * Resolves when the promise settles either way, or after the grace (a runtime that never settles a pending map).
31
+ * @param pending - the promise
32
+ * @param graceMs - the ceiling
33
+ */
34
+ async function settled(pending: Promise<unknown>, graceMs: number): Promise<void> {
35
+ let timer: ReturnType<typeof setTimeout> | undefined;
36
+ const grace = new Promise<void>((resolve) => {
37
+ timer = setTimeout(resolve, graceMs);
38
+ });
39
+ await Promise.race([
40
+ pending.then(
41
+ () => undefined,
42
+ () => undefined,
43
+ ),
44
+ grace,
45
+ ]);
46
+ clearTimeout(timer);
47
+ }
48
+
49
+ /**
50
+ * A plain message for a thrown value (the house pattern of graph-io's report.ts).
51
+ * @param error - whatever the runtime rejected with
52
+ * @returns the Error message, the string itself, or a typeof note
53
+ */
54
+ function messageOf(error: unknown): string {
55
+ if (error instanceof Error) {
56
+ return error.message;
57
+ }
58
+ if (typeof error === "string") {
59
+ return error;
60
+ }
61
+ return `non-error thrown (${typeof error})`;
62
+ }
63
+
64
+ /** What a CommandBatch needs of its owner (GpuContext satisfies it structurally; kernel/ never imports context.ts). */
65
+ export interface BatchHost {
66
+ readonly device: GPUDevice;
67
+ readonly readback: Readback;
68
+ readonly profiler: Profiler | null;
69
+ readonly allocator: AllocationTracker;
70
+ assertReady(): void;
71
+ takePendingError(): WebGpuGraphError | null;
72
+ nextBatchId(): number;
73
+ /** Registers a device-loss listener and returns the unregister function (GpuContext.onLost, contract 3.5): the batch races its map against loss. PLAN DECISION 1: an amendment to contract 3.9, which declares the seven members above. */
74
+ onLost(listener: (info: GPUDeviceLostInfo) => void): () => void;
75
+ }
76
+
77
+ /** A readback scheduled into the batch's staging slot: `offset` is the slot-relative byte offset the caller reads at. */
78
+ export interface ReadbackRequest {
79
+ readonly src: GPUBuffer;
80
+ readonly srcOffset: number;
81
+ readonly byteLength: number;
82
+ readonly offset: number;
83
+ }
84
+
85
+ /** What submit() returns (spec 5.8). `readback` resolves with the mapped bytes COPIED out (one ArrayBuffer holding every request at its offset) after allocator.check(); it rejects with E_VALIDATION { batchId } when the pending-error slot held an error right after submit and with E_DEVICE_LOST after loss; a discarded batch resolves an empty ArrayBuffer (see discard()). */
86
+ export interface SubmittedBatch {
87
+ readonly id: number;
88
+ readonly generation: number;
89
+ readonly readback: Promise<ArrayBuffer>;
90
+ /** Marks the batch stale: its readback still awaits mapAsync (so the slot is returned) but resolves with an empty ArrayBuffer and the caller ignores it (spec 7.19 item 6). */
91
+ discard(): void;
92
+ }
93
+
94
+ /** The discard flag shared by a handle and its readback. */
95
+ interface DiscardState {
96
+ discarded: boolean;
97
+ }
98
+
99
+ /** Records dispatches / copies into one encoder and submits once (spec 5.8). */
100
+ export class CommandBatch {
101
+ readonly id: number;
102
+ readonly generation: number;
103
+ readonly label: string;
104
+ private readonly host: BatchHost;
105
+ private readonly encoder: GPUCommandEncoder;
106
+ private readonly requests: ReadbackRequest[] = [];
107
+ private openPass: GPUComputePassEncoder | null = null;
108
+ private dispatchCount = 0;
109
+ private stagingBytes = 0;
110
+ private slot: StagingSlot | null = null;
111
+ private submitted = false;
112
+
113
+ /**
114
+ * Creates the encoder; the id comes from host.nextBatchId().
115
+ * @param host - the owner (the GpuContext)
116
+ * @param label - names the encoder, its passes and the E_VALIDATION details
117
+ * @param generation - the simulation generation the batch carries (default 0)
118
+ */
119
+ constructor(host: BatchHost, label: string, generation?: number) {
120
+ this.host = host;
121
+ this.id = host.nextBatchId();
122
+ this.generation = generation ?? 0;
123
+ this.label = label;
124
+ this.encoder = host.device.createCommandEncoder({ label: `batch/${label}#${this.id}` });
125
+ }
126
+
127
+ /**
128
+ * Dispatch count recorded so far (tests bound it).
129
+ * @returns the number of dispatchWorkgroups / dispatchWorkgroupsIndirect calls on this batch's passes
130
+ */
131
+ get dispatches(): number {
132
+ return this.dispatchCount;
133
+ }
134
+
135
+ /**
136
+ * Begins a compute pass (ending the previous one); with a profiler present the pass carries timestampWrites and
137
+ * `label` names it in Profiler.resolveInto / timings.
138
+ * @param label - the pass name
139
+ * @returns the pass encoder kernels dispatch into
140
+ */
141
+ pass(label: string): GPUComputePassEncoder {
142
+ this.assertOpen("pass");
143
+ this.endPass();
144
+ const descriptor: GPUComputePassDescriptor = { label: `batch/${this.label}#${this.id}/${label}` };
145
+ const timestampWrites = this.host.profiler?.beginPass(label);
146
+ if (timestampWrites !== undefined) {
147
+ descriptor.timestampWrites = timestampWrites;
148
+ }
149
+ const pass = this.encoder.beginComputePass(descriptor);
150
+ this.countDispatches(pass);
151
+ this.openPass = pass;
152
+ return pass;
153
+ }
154
+
155
+ /** Ends the open pass, if any. */
156
+ endPass(): void {
157
+ if (this.openPass !== null) {
158
+ this.openPass.end();
159
+ this.openPass = null;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * copyBufferToBuffer between two bindings (after endPass).
165
+ * @param src - the source range
166
+ * @param dst - the destination range
167
+ * @param byteLength - a positive multiple of 4 no larger than either binding
168
+ */
169
+ copy(src: Binding, dst: Binding, byteLength: number): void {
170
+ this.assertOpen("copy");
171
+ this.checkByteLength(byteLength);
172
+ if (byteLength > src.size || byteLength > dst.size) {
173
+ throw new WebGpuGraphError(
174
+ "E_INVALID_ARGUMENT",
175
+ `copy of ${byteLength} bytes exceeds a binding (src ${src.size}, dst ${dst.size})`,
176
+ {
177
+ argument: "byteLength",
178
+ value: byteLength,
179
+ expected: `<= ${Math.min(src.size, dst.size)}`,
180
+ },
181
+ );
182
+ }
183
+ this.endPass();
184
+ this.encoder.copyBufferToBuffer(src.buffer, src.offset, dst.buffer, dst.offset, byteLength);
185
+ }
186
+
187
+ /**
188
+ * Schedules a copy of `byteLength` bytes from `src` into the borrowed staging slot; the slot is borrowed at the
189
+ * first request and re-borrowed larger when the sum of requests outgrows it (nothing is recorded before submit).
190
+ * @param src - the source buffer
191
+ * @param srcOffset - a non-negative multiple of 4
192
+ * @param byteLength - a positive multiple of 4 with srcOffset + byteLength <= src.size
193
+ * @returns the request; `offset` is where the caller reads inside the readback bytes
194
+ */
195
+ readback(src: GPUBuffer, srcOffset: number, byteLength: number): ReadbackRequest {
196
+ this.assertOpen("readback");
197
+ this.checkByteLength(byteLength);
198
+ if (!Number.isInteger(srcOffset) || srcOffset < 0 || srcOffset % 4 !== 0 || srcOffset + byteLength > src.size) {
199
+ throw new WebGpuGraphError(
200
+ "E_INVALID_ARGUMENT",
201
+ `readback of [${srcOffset}, ${srcOffset + byteLength}) is outside the ${src.size}-byte source or not 4-aligned`,
202
+ {
203
+ argument: "srcOffset",
204
+ value: srcOffset,
205
+ expected: `a multiple of 4 with srcOffset + byteLength <= ${src.size}`,
206
+ },
207
+ );
208
+ }
209
+ const request: ReadbackRequest = { src, srcOffset, byteLength, offset: this.stagingBytes };
210
+ this.requests.push(request);
211
+ this.stagingBytes += byteLength;
212
+ this.ensureSlot(this.stagingBytes);
213
+ return request;
214
+ }
215
+
216
+ /**
217
+ * Records `resolveQuerySet` after ending the open pass; used by Profiler.resolveInto only.
218
+ * @internal
219
+ * @param querySet - the timestamp query set
220
+ * @param firstQuery - first query index
221
+ * @param queryCount - number of queries
222
+ * @param destination - a QUERY_RESOLVE | COPY_SRC buffer written at offset 0
223
+ */
224
+ resolveQuerySet(querySet: GPUQuerySet, firstQuery: number, queryCount: number, destination: GPUBuffer): void {
225
+ this.assertOpen("resolveQuerySet");
226
+ this.endPass();
227
+ this.encoder.resolveQuerySet(querySet, firstQuery, queryCount, destination, 0);
228
+ }
229
+
230
+ /**
231
+ * Ends passes, records the staging copies, submits, checks the pending-error slot (spec 5.7), returns the handle.
232
+ * A batch can be submitted once; submit() after device loss throws E_DEVICE_LOST.
233
+ * @returns the submitted-batch handle
234
+ */
235
+ submit(): SubmittedBatch {
236
+ this.assertOpen("submit");
237
+ this.submitted = true;
238
+ this.endPass();
239
+ const { slot } = this;
240
+ const pendingBefore = this.host.takePendingError();
241
+ try {
242
+ this.host.assertReady();
243
+ } catch (error) {
244
+ this.releaseSlot();
245
+ throw error;
246
+ }
247
+ if (slot !== null) {
248
+ for (const request of this.requests) {
249
+ this.encoder.copyBufferToBuffer(
250
+ request.src,
251
+ request.srcOffset,
252
+ slot.buffer,
253
+ request.offset,
254
+ request.byteLength,
255
+ );
256
+ }
257
+ }
258
+ const commandBuffer = this.encoder.finish({ label: `batch/${this.label}#${this.id}` });
259
+ this.host.device.queue.submit([commandBuffer]);
260
+ // the slot is drained a second time so a finish() / submit() error raised by a command buffer the first error
261
+ // already invalidated never leaks to the next public call; the FIRST error is the one reported
262
+ const pendingAfter = this.host.takePendingError();
263
+ const pending = pendingBefore ?? pendingAfter;
264
+ const state: DiscardState = { discarded: false };
265
+ const readback = pending === null ? this.awaitReadback(slot, state) : this.rejectReadback(pending);
266
+ return {
267
+ id: this.id,
268
+ generation: this.generation,
269
+ readback,
270
+ discard: (): void => {
271
+ state.discarded = true;
272
+ },
273
+ };
274
+ }
275
+
276
+ private rejectReadback(pending: WebGpuGraphError): Promise<ArrayBuffer> {
277
+ this.releaseSlot();
278
+ return Promise.reject(this.attribute(pending));
279
+ }
280
+
281
+ private async awaitReadback(slot: StagingSlot | null, state: DiscardState): Promise<ArrayBuffer> {
282
+ if (slot === null) {
283
+ await this.host.allocator.check();
284
+ return new ArrayBuffer(0);
285
+ }
286
+ const total = this.stagingBytes;
287
+ let unregister: () => void = (): void => undefined;
288
+ const lost = new Promise<never>((_resolve, reject) => {
289
+ unregister = this.host.onLost((info) => {
290
+ reject(deviceLostError(info));
291
+ });
292
+ });
293
+ void lost.catch(() => undefined);
294
+ const mapped = this.host.readback.mapSlot(slot, total);
295
+ void mapped.catch(() => undefined);
296
+ let bytes: ArrayBuffer;
297
+ try {
298
+ await Promise.race([mapped, lost]);
299
+ this.host.assertReady();
300
+ bytes = state.discarded ? new ArrayBuffer(0) : slot.buffer.getMappedRange(0, total).slice(0);
301
+ } catch (error) {
302
+ // the loss may have won the race: let the runtime settle the map before the slot is unmapped and returned
303
+ await settled(mapped, LOSS_GRACE_MS);
304
+ this.releaseSlot();
305
+ throw await this.classify(error, lost);
306
+ } finally {
307
+ unregister();
308
+ }
309
+ this.releaseSlot();
310
+ await this.host.allocator.check();
311
+ return bytes;
312
+ }
313
+
314
+ private async classify(error: unknown, lost: Promise<never>): Promise<WebGpuGraphError> {
315
+ if (isWebGpuGraphError(error)) {
316
+ return error.code === "E_DEVICE_LOST" || error.code === "E_DISPOSED" ? error : this.attribute(error);
317
+ }
318
+ try {
319
+ this.host.assertReady();
320
+ } catch (state) {
321
+ if (isWebGpuGraphError(state)) {
322
+ return state.code === "E_DEVICE_LOST" || state.code === "E_DISPOSED" ? state : this.attribute(state);
323
+ }
324
+ }
325
+ let timer: ReturnType<typeof setTimeout> | undefined;
326
+ const grace = new Promise<null>((resolve) => {
327
+ timer = setTimeout(() => {
328
+ resolve(null);
329
+ }, LOSS_GRACE_MS);
330
+ });
331
+ const late = await Promise.race([
332
+ lost.then(
333
+ () => null,
334
+ (reason: unknown) => reason,
335
+ ),
336
+ grace,
337
+ ]);
338
+ clearTimeout(timer);
339
+ if (isWebGpuGraphError(late)) {
340
+ return late;
341
+ }
342
+ const message = messageOf(error);
343
+ return new WebGpuGraphError(
344
+ "E_VALIDATION",
345
+ `mapAsync failed for batch ${this.id} "${this.label}": ${message}`,
346
+ {
347
+ label: this.label,
348
+ message,
349
+ batchId: this.id,
350
+ },
351
+ );
352
+ }
353
+
354
+ private attribute(error: WebGpuGraphError): WebGpuGraphError {
355
+ return new WebGpuGraphError(error.code, `${error.message} (delivered to batch ${this.id} "${this.label}")`, {
356
+ ...error.details,
357
+ batchId: this.id,
358
+ batchLabel: this.label,
359
+ });
360
+ }
361
+
362
+ private ensureSlot(byteLength: number): void {
363
+ if (this.slot !== null && this.slot.capacity >= byteLength) {
364
+ return;
365
+ }
366
+ this.releaseSlot();
367
+ this.slot = this.host.readback.borrowSlot(byteLength);
368
+ }
369
+
370
+ private releaseSlot(): void {
371
+ if (this.slot === null) {
372
+ return;
373
+ }
374
+ const { slot } = this;
375
+ this.slot = null;
376
+ try {
377
+ this.host.readback.returnSlot(slot);
378
+ } catch (error) {
379
+ if (!isWebGpuGraphError(error) || error.code !== "E_DISPOSED") {
380
+ throw error;
381
+ }
382
+ }
383
+ }
384
+
385
+ private countDispatches(pass: GPUComputePassEncoder): void {
386
+ const direct = pass.dispatchWorkgroups.bind(pass);
387
+ const indirect = pass.dispatchWorkgroupsIndirect.bind(pass);
388
+ pass.dispatchWorkgroups = (x: number, y?: number, z?: number): undefined => {
389
+ this.dispatchCount += 1;
390
+ direct(x, y, z);
391
+ return undefined;
392
+ };
393
+ pass.dispatchWorkgroupsIndirect = (buffer: GPUBuffer, offset: number): undefined => {
394
+ this.dispatchCount += 1;
395
+ indirect(buffer, offset);
396
+ return undefined;
397
+ };
398
+ }
399
+
400
+ private assertOpen(operation: string): void {
401
+ if (this.submitted) {
402
+ throw new WebGpuGraphError(
403
+ "E_INVALID_ARGUMENT",
404
+ `${operation}() after submit() on batch ${this.id} "${this.label}"`,
405
+ {
406
+ argument: operation,
407
+ value: this.id,
408
+ expected: "a batch that has not been submitted",
409
+ },
410
+ );
411
+ }
412
+ }
413
+
414
+ private checkByteLength(byteLength: number): void {
415
+ if (!Number.isInteger(byteLength) || byteLength <= 0 || byteLength % 4 !== 0) {
416
+ throw new WebGpuGraphError(
417
+ "E_INVALID_ARGUMENT",
418
+ `byteLength ${byteLength} is not a positive multiple of 4`,
419
+ {
420
+ argument: "byteLength",
421
+ value: byteLength,
422
+ expected: "a positive multiple of 4",
423
+ },
424
+ );
425
+ }
426
+ }
427
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Dispatch planning (spec 5.2; contract 3.9): the 16,776,960 rule. `ceil(items / wg)` workgroups as a 1D dispatch
3
+ * while the count fits one dimension (65,535 groups x 256 = 16,776,960 items, NOT 2^24), else a 2D grid whose
4
+ * kernels linearise their id with the prelude's `linear_id()` and guard `id >= items`. Pure functions with no
5
+ * device: every branch is unit-tested against faked capability tables.
6
+ */
7
+
8
+ import { MAX_WORKGROUPS_PER_DIM } from "../constants.js";
9
+ import { WebGpuGraphError } from "../errors.js";
10
+ import { type PlanCaps } from "../types/context.js";
11
+
12
+ /** A dispatch shape (spec 5.2). `stride` is the grid-stride step (null for plain 1D / 2D plans). */
13
+ export interface DispatchPlan {
14
+ readonly x: number;
15
+ readonly y: number;
16
+ readonly z: 1;
17
+ readonly items: number;
18
+ readonly stride: number | null;
19
+ }
20
+
21
+ /**
22
+ * The per-dimension workgroup limit a plan honours. PLAN DECISION: the minimum of the spec constant and the table's
23
+ * `maxComputeWorkgroupsPerDimension`; the two are asserted equal on a real device at create() (spec 2.2), so every
24
+ * capability table of the tests yields the same plans and a faked table below the constant is still honoured.
25
+ * @param caps - the capability table
26
+ * @returns the limit
27
+ */
28
+ function perDimension(caps: PlanCaps): number {
29
+ return Math.min(MAX_WORKGROUPS_PER_DIM, caps.limits.maxComputeWorkgroupsPerDimension);
30
+ }
31
+
32
+ /**
33
+ * Validates a non-negative integer argument.
34
+ * @param argument - the argument name
35
+ * @param value - the value
36
+ */
37
+ function assertCount(argument: string, value: number): void {
38
+ if (!Number.isInteger(value) || value < 0) {
39
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument} must be a non-negative integer, got ${value}`, {
40
+ argument,
41
+ value,
42
+ expected: "a non-negative integer",
43
+ });
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Validates a workgroup size: a power of two >= 1 (the reduction trees of 4.3 need it). Arithmetic, no bit tricks.
49
+ * @param wg - the workgroup size
50
+ */
51
+ function assertWorkgroupSize(wg: number): void {
52
+ let rest = wg;
53
+ while (Number.isInteger(rest) && rest > 1 && rest % 2 === 0) {
54
+ rest /= 2;
55
+ }
56
+ if (rest !== 1) {
57
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `wg must be a power of two, got ${wg}`, {
58
+ argument: "wg",
59
+ value: wg,
60
+ expected: "a power of two >= 1",
61
+ });
62
+ }
63
+ }
64
+
65
+ /**
66
+ * The grid of `groups` workgroups: 1D up to the per-dimension limit, else `x = limit`, `y = ceil(groups / limit)`.
67
+ * @param groups - the workgroup count
68
+ * @param items - the item count the plan covers (recorded in the plan)
69
+ * @param caps - the capability table
70
+ * @returns the plan
71
+ */
72
+ function grid(groups: number, items: number, caps: PlanCaps): DispatchPlan {
73
+ const limit = perDimension(caps);
74
+ if (groups === 0) {
75
+ return { x: 0, y: 1, z: 1, items, stride: null };
76
+ }
77
+ if (groups <= limit) {
78
+ return { x: groups, y: 1, z: 1, items, stride: null };
79
+ }
80
+ const y = Math.ceil(groups / limit);
81
+ if (y > limit) {
82
+ throw new WebGpuGraphError(
83
+ "E_TOO_LARGE",
84
+ `${groups} workgroups exceed the 2D dispatch limit of ${limit * limit}`,
85
+ {
86
+ needed: groups,
87
+ limit: limit * limit,
88
+ path: "dispatch",
89
+ algorithm: null,
90
+ items,
91
+ },
92
+ );
93
+ }
94
+ return { x: limit, y, z: 1, items, stride: null };
95
+ }
96
+
97
+ /**
98
+ * ceil(items / wg) groups as 1D up to MAX_WORKGROUPS_PER_DIM, else a 2D grid; items 0 -> { x: 0, y: 1 }; y above the limit -> E_TOO_LARGE.
99
+ * @param items - the item count
100
+ * @param wg - the workgroup size (a power of two)
101
+ * @param caps - the capability table
102
+ * @returns the plan
103
+ */
104
+ export function plan1d(items: number, wg: number, caps: PlanCaps): DispatchPlan {
105
+ assertCount("items", items);
106
+ assertWorkgroupSize(wg);
107
+ return grid(Math.ceil(items / wg), items, caps);
108
+ }
109
+
110
+ /**
111
+ * The 2D form for a group count (x = MAX_WORKGROUPS_PER_DIM, y = ceil(groups / x)); exported for the indirect finalize kernel's host twin (P4).
112
+ * PLAN DECISION: plan1d's rule applied to a group count (spec 5.4: the finalize kernel uses "the same rule as
113
+ * plan1d") -- 1D `{ x: groups, y: 1 }` up to the per-dimension limit and the `x = limit` split only above it, through
114
+ * the same `grid()` as plan1d so the two cannot drift; P4's finalize WGSL mirrors exactly this rule. `items` of the
115
+ * returned plan is `groups` (the caller knows its own wg); groups 0 -> { x: 0, y: 1 }.
116
+ * @param groups - the workgroup count
117
+ * @param caps - the capability table
118
+ * @returns the plan
119
+ */
120
+ export function plan2d(groups: number, caps: PlanCaps): DispatchPlan {
121
+ assertCount("groups", groups);
122
+ return grid(groups, groups, caps);
123
+ }
124
+
125
+ /**
126
+ * P7 (spec 5.2): groups = min(groups, maxGroups ?? (caps.software ? 64 : 4096)) with the kernel looping by stride. P1-P3: throws E_UNSUPPORTED { feature: "planGridStride" } (lead f).
127
+ * PLAN DECISION: `maxGroups?` is spelled `?: number` rather than the contract's `?: number | undefined` because the
128
+ * root ESLint rule no-duplicate-type-constituents rejects the explicit undefined on an optional parameter (the call
129
+ * signature is identical).
130
+ * @param items - the item count
131
+ * @param wg - the workgroup size
132
+ * @param caps - the capability table
133
+ * @param maxGroups - the group cap
134
+ */
135
+ export function planGridStride(items: number, wg: number, caps: PlanCaps, maxGroups?: number): DispatchPlan {
136
+ throw new WebGpuGraphError("E_UNSUPPORTED", "planGridStride lands with the grid-stride kernels of P7 (spec 5.2)", {
137
+ feature: "planGridStride",
138
+ hint: `items ${items}, wg ${wg}, maxGroups ${maxGroups ?? (caps.software ? 64 : 4096)}`,
139
+ });
140
+ }
141
+
142
+ /**
143
+ * P4 (spec 5.4): the (x, y, 1) args a device-side finalize kernel writes for a count. P1-P3: throws E_UNSUPPORTED { feature: "planIndirect" } (lead f).
144
+ * @param count - the device-side count
145
+ * @param wg - the workgroup size
146
+ * @param caps - the capability table
147
+ */
148
+ export function planIndirect(count: number, wg: number, caps: PlanCaps): DispatchPlan {
149
+ throw new WebGpuGraphError("E_UNSUPPORTED", "planIndirect lands with the indirect dispatch of P4 (spec 5.4)", {
150
+ feature: "planIndirect",
151
+ hint: `count ${count}, wg ${wg}, limit ${perDimension(caps)}`,
152
+ });
153
+ }
154
+
155
+ /**
156
+ * The workgroup count of a plan (x * y).
157
+ * @param plan - the plan
158
+ * @returns the count
159
+ */
160
+ export function groupsOf(plan: DispatchPlan): number {
161
+ return plan.x * plan.y;
162
+ }