@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
package/src/context.ts ADDED
@@ -0,0 +1,399 @@
1
+ /**
2
+ * GpuContext (spec 2.2, 2.8, 3.3, 5.7; contract 3.5): one device, its caps and the singletons built on it --
3
+ * the composition root of D26. probe() never creates a device; create() runs the six steps of spec 2.2 in
4
+ * order; from() adopts a device the caller owns. Step 5 (the uncapturederror sink and the device.lost
5
+ * watcher) and step 6 (the AllocationTracker, GraphResidency, BufferPool, Readback and PipelineCache) run
6
+ * in the private constructor. The Profiler is constructed when "timestamp-query" is on the device (contract
7
+ * 3.5; P2-T1) with `quantised` = runtime !== "node" (Dawn-node is the only runtime whose ticks are known
8
+ * unquantised, spec 2.6). This file never imports ./kernels.js or ./wgsl/**.
9
+ */
10
+
11
+ import type { GraphSnapshot } from "@graphty/graph-format";
12
+
13
+ import { DEFAULT_WARN_UNRELEASED_SNAPSHOTS } from "./constants.js";
14
+ import {
15
+ buildRequiredFeatures,
16
+ buildRequiredLimits,
17
+ requestAdapter,
18
+ requestDevice,
19
+ summarizeAdapter,
20
+ } from "./device/acquire.js";
21
+ import { assertPlanLimits, capsFromDevice, captureCaps, workgroupSizeFor } from "./device/caps.js";
22
+ import { AllocationTracker } from "./device/error-scope.js";
23
+ import { deviceLostError, installUncapturedErrorSink, PendingErrorSlot, watchDeviceLost } from "./device/lost.js";
24
+ import { isWebGpuGraphError, WebGpuGraphError } from "./errors.js";
25
+ import { PipelineCache } from "./kernel/pipeline-cache.js";
26
+ import { Profiler } from "./kernel/profiler.js";
27
+ import { BufferPool } from "./memory/buffer-pool.js";
28
+ import { Readback } from "./memory/readback.js";
29
+ import { GraphResidency } from "./memory/residency.js";
30
+ import type { GpuCaps, GpuContextOptions, GpuDebugFlags, ProbeOptions, ProbeResult } from "./types/context.js";
31
+
32
+ /** The label a context and its device carry when create() is given none. */
33
+ const DEFAULT_LABEL = "webgpu-graph-algorithms";
34
+ /** The optionalFeatures default of spec 2.2. */
35
+ const DEFAULT_OPTIONAL_FEATURES: readonly GPUFeatureName[] = Object.freeze(["subgroups", "timestamp-query"]);
36
+ /** The powerPreference default of spec 2.2 (Chrome 145 needs it; Dawn-node ignores it). */
37
+ const DEFAULT_POWER_PREFERENCE: GPUPowerPreference = "high-performance";
38
+
39
+ /** What the private constructor takes: the device, its caps and the create-time choices. */
40
+ interface ContextInit {
41
+ readonly device: GPUDevice;
42
+ readonly caps: GpuCaps;
43
+ readonly ownsDevice: boolean;
44
+ readonly label: string;
45
+ readonly onError: ((error: WebGpuGraphError) => void) | null;
46
+ readonly warnUnreleasedSnapshots: number;
47
+ }
48
+
49
+ /**
50
+ * Step 1 of create(): the caller's UNUSED adapter, else one requested from `gpu`; neither -> E_NO_WEBGPU.
51
+ * @param options - the create options
52
+ * @returns an adapter no device was requested from
53
+ */
54
+ async function adapterOf(options: GpuContextOptions): Promise<GPUAdapter> {
55
+ if (options.adapter !== undefined) {
56
+ return options.adapter;
57
+ }
58
+ if (options.gpu !== undefined) {
59
+ return requestAdapter(options.gpu, options.powerPreference ?? DEFAULT_POWER_PREFERENCE);
60
+ }
61
+ throw new WebGpuGraphError("E_NO_WEBGPU", "GpuContext.create needs one of gpu, adapter or device", {
62
+ reason: "no gpu, adapter or device given",
63
+ hint: "pass navigator.gpu (browser) or the gpu of createNodeGpu() (Node), or use the ./browser and ./node entries",
64
+ });
65
+ }
66
+
67
+ /** One device, its caps and the singletons built on it (spec 3.3; the composition root of D26). */
68
+ export class GpuContext {
69
+ /** The device (owned when create() made it, adopted otherwise). */
70
+ readonly device: GPUDevice;
71
+ /** The capability record captured from device.limits / device.features (spec 3.3). */
72
+ readonly caps: GpuCaps;
73
+ /** Resolves with the lost info once the device is lost or destroyed (spec 2.8). */
74
+ readonly lost: Promise<GPUDeviceLostInfo>;
75
+ /**
76
+ * The upload cache (spec 3.3).
77
+ * @internal
78
+ */
79
+ readonly residency: GraphResidency;
80
+ /** Non-null when "timestamp-query" was granted (P2-T1 makes it functional; P1 always null). */
81
+ readonly profiler: Profiler | null;
82
+ /**
83
+ * The compile-once pipeline cache.
84
+ * @internal
85
+ */
86
+ readonly pipelines: PipelineCache;
87
+ /**
88
+ * The scratch pool.
89
+ * @internal
90
+ */
91
+ readonly pool: BufferPool;
92
+ /**
93
+ * The staging ring.
94
+ * @internal
95
+ */
96
+ readonly readback: Readback;
97
+ /**
98
+ * The OOM-scoped allocator shared by residency and pool.
99
+ * @internal
100
+ */
101
+ readonly allocator: AllocationTracker;
102
+ /**
103
+ * The workgroup size of this device (spec 5.1).
104
+ * @internal
105
+ */
106
+ readonly workgroupSize: number;
107
+ /**
108
+ * True when create() made the device (dispose() destroys it).
109
+ * @internal
110
+ */
111
+ readonly ownsDevice: boolean;
112
+ /**
113
+ * The label given to create().
114
+ * @internal
115
+ */
116
+ readonly label: string;
117
+ /**
118
+ * Mutable test-build flags (GpuDebugFlags).
119
+ * @internal
120
+ */
121
+ readonly debug: GpuDebugFlags;
122
+
123
+ private currentState: "ready" | "lost" | "disposed" = "ready";
124
+ private lostError: WebGpuGraphError | null = null;
125
+ private readonly slot = new PendingErrorSlot();
126
+ private readonly uninstallSink: () => void;
127
+ private readonly lostListeners = new Set<(info: GPUDeviceLostInfo) => void>();
128
+ private readonly disposers: (() => void)[] = [];
129
+ private batchCounter = 0;
130
+
131
+ /**
132
+ * Steps 5 and 6 of create() (spec 2.2): the sink, the loss watcher and the singletons.
133
+ * @param init - the device, its caps and the create-time choices
134
+ */
135
+ private constructor(init: ContextInit) {
136
+ this.device = init.device;
137
+ this.caps = init.caps;
138
+ this.ownsDevice = init.ownsDevice;
139
+ this.label = init.label;
140
+ this.debug = { inspect: false };
141
+ this.workgroupSize = workgroupSizeFor(init.caps);
142
+ this.allocator = new AllocationTracker(init.device);
143
+ this.residency = new GraphResidency(init.device, init.caps, this.allocator, {
144
+ warnUnreleasedSnapshots: init.warnUnreleasedSnapshots,
145
+ });
146
+ this.pool = new BufferPool(init.device, this.allocator, init.caps.limits.maxBufferSize);
147
+ this.readback = new Readback(init.device, this.allocator);
148
+ this.pipelines = new PipelineCache(init.device, init.caps);
149
+ this.profiler = init.device.features.has("timestamp-query")
150
+ ? new Profiler(init.device, true, init.caps.runtime !== "node")
151
+ : null;
152
+ this.uninstallSink = installUncapturedErrorSink(init.device, this.slot, init.onError);
153
+ this.lost = watchDeviceLost(init.device, (info) => {
154
+ this.handleLost(info);
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Probe without creating a device (spec 2.2); never throws. `gpu === undefined` -> E_NO_WEBGPU;
160
+ * requestAdapter() null or a throw -> E_NO_ADAPTER; `rejectSoftware && software` -> E_SOFTWARE_ONLY (the
161
+ * adapter and its summary are still reported); else OK with the UNUSED adapter and its summary.
162
+ * @param options - the GPU object, the power preference and the software policy
163
+ * @returns the probe result
164
+ */
165
+ static async probe(options: ProbeOptions): Promise<ProbeResult> {
166
+ const { gpu } = options;
167
+ if (gpu === undefined) {
168
+ return {
169
+ ok: false,
170
+ code: "E_NO_WEBGPU",
171
+ reason: "no GPU object: navigator.gpu is undefined (no WebGPU in this runtime)",
172
+ adapter: null,
173
+ summary: null,
174
+ };
175
+ }
176
+ let adapter: GPUAdapter;
177
+ try {
178
+ adapter = await requestAdapter(gpu, options.powerPreference ?? DEFAULT_POWER_PREFERENCE);
179
+ } catch (err) {
180
+ const reason = isWebGpuGraphError(err) ? err.message : String(err);
181
+ return { ok: false, code: "E_NO_ADAPTER", reason, adapter: null, summary: null };
182
+ }
183
+ const summary = summarizeAdapter(adapter);
184
+ if (options.rejectSoftware === true && summary.software) {
185
+ return {
186
+ ok: false,
187
+ code: "E_SOFTWARE_ONLY",
188
+ reason: `software adapter rejected: ${summary.vendor}/${summary.architecture}`,
189
+ adapter,
190
+ summary,
191
+ };
192
+ }
193
+ return { ok: true, code: "OK", reason: null, adapter, summary };
194
+ }
195
+
196
+ /**
197
+ * The six-step acquisition of spec 2.2: adopt `device`, else take `adapter`, else request one from `gpu`
198
+ * (step 1); the software test (step 2); the limits / features and requestDevice (step 3); the caps from
199
+ * device.limits plus assertPlanLimits (step 4); the sink, the loss watcher and the singletons (steps 5-6,
200
+ * in the constructor).
201
+ * @param options - see GpuContextOptions
202
+ * @returns the context
203
+ */
204
+ static async create(options: GpuContextOptions): Promise<GpuContext> {
205
+ const runtime = options.runtime ?? "unknown";
206
+ const label = options.label ?? DEFAULT_LABEL;
207
+ const onError = options.onError ?? null;
208
+ const warnUnreleasedSnapshots = options.warnUnreleasedSnapshots ?? DEFAULT_WARN_UNRELEASED_SNAPSHOTS;
209
+ if (options.device !== undefined) {
210
+ const adopted = capsFromDevice(options.device, { runtime });
211
+ assertPlanLimits(adopted);
212
+ return new GpuContext({
213
+ device: options.device,
214
+ caps: adopted,
215
+ ownsDevice: false,
216
+ label,
217
+ onError,
218
+ warnUnreleasedSnapshots,
219
+ });
220
+ }
221
+ const adapter = await adapterOf(options);
222
+ const summary = summarizeAdapter(adapter);
223
+ if (options.rejectSoftware === true && summary.software) {
224
+ throw new WebGpuGraphError(
225
+ "E_SOFTWARE_ONLY",
226
+ `software adapter rejected by rejectSoftware: ${summary.vendor}/${summary.architecture}`,
227
+ { adapter: summary },
228
+ );
229
+ }
230
+ const requiredLimits = buildRequiredLimits(adapter, options.limits ?? "raise");
231
+ const requiredFeatures = buildRequiredFeatures(
232
+ adapter,
233
+ options.requiredFeatures ?? [],
234
+ options.optionalFeatures ?? DEFAULT_OPTIONAL_FEATURES,
235
+ );
236
+ const device = await requestDevice(adapter, { label, requiredLimits, requiredFeatures });
237
+ const caps = captureCaps(device, adapter.info, runtime, options.gpu?.wgslLanguageFeatures ?? []);
238
+ try {
239
+ assertPlanLimits(caps);
240
+ } catch (err) {
241
+ device.destroy();
242
+ throw err;
243
+ }
244
+ return new GpuContext({ device, caps, ownsDevice: true, label, onError, warnUnreleasedSnapshots });
245
+ }
246
+
247
+ /**
248
+ * Zero-cost adoption of a device the caller owns (ownsDevice false, runtime "unknown" unless info says
249
+ * otherwise). The context is labelled with the device's label, or "GpuContext.from" when it has none.
250
+ * @param device - the device to adopt
251
+ * @param info - what the caller knows about the adapter, if anything
252
+ * @returns the context
253
+ */
254
+ static from(device: GPUDevice, info?: Partial<GpuCaps>): GpuContext {
255
+ const caps = capsFromDevice(device, info);
256
+ assertPlanLimits(caps);
257
+ const deviceLabel: string = device.label ?? "";
258
+ return new GpuContext({
259
+ device,
260
+ caps,
261
+ ownsDevice: false,
262
+ label: deviceLabel === "" ? "GpuContext.from" : deviceLabel,
263
+ onError: null,
264
+ warnUnreleasedSnapshots: DEFAULT_WARN_UNRELEASED_SNAPSHOTS,
265
+ });
266
+ }
267
+
268
+ /**
269
+ * "ready" | "lost" | "disposed".
270
+ * @returns the lifecycle state
271
+ */
272
+ get state(): "ready" | "lost" | "disposed" {
273
+ return this.currentState;
274
+ }
275
+
276
+ /**
277
+ * Throws E_DEVICE_LOST / E_DISPOSED by state and rethrows a pending uncaptured error (spec 5.7: "thrown
278
+ * from the next public call").
279
+ * @internal
280
+ */
281
+ assertReady(): void {
282
+ if (this.currentState === "disposed") {
283
+ throw new WebGpuGraphError("E_DISPOSED", `GpuContext "${this.label}" is disposed`, { label: this.label });
284
+ }
285
+ if (this.lostError !== null) {
286
+ throw this.lostError;
287
+ }
288
+ const pending = this.slot.take();
289
+ if (pending !== null) {
290
+ throw pending;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Takes the pending uncaptured error (used by CommandBatch right after submit).
296
+ * @internal
297
+ * @returns the error, or null
298
+ */
299
+ takePendingError(): WebGpuGraphError | null {
300
+ return this.slot.take();
301
+ }
302
+
303
+ /**
304
+ * Monotonically increasing batch ids, starting at 1.
305
+ * @internal
306
+ * @returns the next id
307
+ */
308
+ nextBatchId(): number {
309
+ this.batchCounter += 1;
310
+ return this.batchCounter;
311
+ }
312
+
313
+ /**
314
+ * Registers a device-loss listener (simulations enter "disposed"); returns the unregister function.
315
+ * @internal
316
+ * @param listener - called once with the lost info
317
+ * @returns removes the listener
318
+ */
319
+ onLost(listener: (info: GPUDeviceLostInfo) => void): () => void {
320
+ this.lostListeners.add(listener);
321
+ return () => {
322
+ this.lostListeners.delete(listener);
323
+ };
324
+ }
325
+
326
+ /**
327
+ * A disposer run by dispose() (the Node entry attaches the GPU handle's dispose); attached after
328
+ * dispose() it runs immediately.
329
+ * @internal
330
+ * @param dispose - the disposer
331
+ */
332
+ attachDisposer(dispose: () => void): void {
333
+ if (this.currentState === "disposed") {
334
+ dispose();
335
+ return;
336
+ }
337
+ this.disposers.push(dispose);
338
+ }
339
+
340
+ /**
341
+ * Destroys every buffer recorded for the snapshot (spec 4.5), then `pool.trim()` (spec 4.4: idle scratch
342
+ * goes with the graph); idempotent; safe on a snapshot never uploaded; a no-op after dispose().
343
+ * @param snapshot - the snapshot whose buffers are released
344
+ */
345
+ release(snapshot: GraphSnapshot): void {
346
+ if (this.currentState === "disposed") {
347
+ return;
348
+ }
349
+ this.residency.release(snapshot);
350
+ this.pool.trim();
351
+ }
352
+
353
+ /**
354
+ * Rejects pending work with E_DISPOSED (every later public call), destroys residency, pool and staging
355
+ * ring, destroys the device when owned, runs attached disposers; idempotent (spec 2.8).
356
+ */
357
+ dispose(): void {
358
+ if (this.currentState === "disposed") {
359
+ return;
360
+ }
361
+ this.currentState = "disposed";
362
+ this.uninstallSink();
363
+ this.residency.destroyAll();
364
+ this.pool.destroyAll();
365
+ this.readback.destroyAll();
366
+ if (this.profiler !== null) {
367
+ this.profiler.destroy();
368
+ }
369
+ if (this.ownsDevice) {
370
+ this.device.destroy();
371
+ }
372
+ const disposers = this.disposers.splice(0);
373
+ for (const dispose of disposers) {
374
+ dispose();
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Step 5 of spec 2.2 on loss: state "lost", the residency cleared without destroying (the buffers are
380
+ * gone with the device), every registered listener run once; a loss reported after dispose() (the owned
381
+ * device was destroyed by dispose) changes nothing.
382
+ * @param info - the device.lost result
383
+ */
384
+ private handleLost(info: GPUDeviceLostInfo): void {
385
+ if (this.currentState === "disposed") {
386
+ return;
387
+ }
388
+ this.currentState = "lost";
389
+ this.lostError = deviceLostError(info);
390
+ this.residency.clearOnLoss();
391
+ for (const listener of Array.from(this.lostListeners)) {
392
+ try {
393
+ listener(info);
394
+ } catch {
395
+ // a throwing listener must not stop the fan-out; the others still learn of the loss
396
+ }
397
+ }
398
+ }
399
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Device acquisition (spec 2.2 steps 1-3, 2.6; contract 3.4): the adapter request, the software-adapter
3
+ * test, the adapter summary, the requiredLimits / requiredFeatures builders and the device request. Nothing
4
+ * above the device layer is constructed here (D26); src/context.ts sequences these calls. An adapter is
5
+ * consumed by its first requestDevice (WebGPU 3.5.1; Dawn-node rejects the second call with
6
+ * `OperationError: adapter is "consumed"`), so every device the package creates comes from a fresh adapter.
7
+ */
8
+
9
+ import { WebGpuGraphError } from "../errors.js";
10
+ import type { AdapterInfoLike, AdapterSummary, LimitPolicy, RaisableLimit } from "../types/context.js";
11
+
12
+ /** The six raisable limits in the order create() requests them (spec 2.2). */
13
+ export const RAISABLE_LIMITS: readonly RaisableLimit[] = Object.freeze([
14
+ "maxBufferSize",
15
+ "maxStorageBufferBindingSize",
16
+ "maxStorageBuffersPerShaderStage",
17
+ "maxComputeWorkgroupStorageSize",
18
+ "maxComputeInvocationsPerWorkgroup",
19
+ "maxComputeWorkgroupSizeX",
20
+ ]);
21
+
22
+ /**
23
+ * The WebGPU limit names summarizeAdapter reads (the GPUSupportedLimits members of @webgpu/types 0.1.72).
24
+ * A fixed list because Chromium exposes the limits as prototype getters (Object.keys is empty) and Dawn-node as an
25
+ * addon object; reading each name works identically on both, and a name the runtime lacks is simply absent.
26
+ */
27
+ const LIMIT_NAMES: readonly string[] = [
28
+ "maxTextureDimension1D",
29
+ "maxTextureDimension2D",
30
+ "maxTextureDimension3D",
31
+ "maxTextureArrayLayers",
32
+ "maxBindGroups",
33
+ "maxBindGroupsPlusVertexBuffers",
34
+ "maxBindingsPerBindGroup",
35
+ "maxDynamicUniformBuffersPerPipelineLayout",
36
+ "maxDynamicStorageBuffersPerPipelineLayout",
37
+ "maxSampledTexturesPerShaderStage",
38
+ "maxSamplersPerShaderStage",
39
+ "maxStorageBuffersPerShaderStage",
40
+ "maxStorageBuffersInVertexStage",
41
+ "maxStorageBuffersInFragmentStage",
42
+ "maxStorageTexturesPerShaderStage",
43
+ "maxStorageTexturesInVertexStage",
44
+ "maxStorageTexturesInFragmentStage",
45
+ "maxUniformBuffersPerShaderStage",
46
+ "maxUniformBufferBindingSize",
47
+ "maxStorageBufferBindingSize",
48
+ "minUniformBufferOffsetAlignment",
49
+ "minStorageBufferOffsetAlignment",
50
+ "maxVertexBuffers",
51
+ "maxBufferSize",
52
+ "maxVertexAttributes",
53
+ "maxVertexBufferArrayStride",
54
+ "maxInterStageShaderVariables",
55
+ "maxColorAttachments",
56
+ "maxColorAttachmentBytesPerSample",
57
+ "maxComputeWorkgroupStorageSize",
58
+ "maxComputeInvocationsPerWorkgroup",
59
+ "maxComputeWorkgroupSizeX",
60
+ "maxComputeWorkgroupSizeY",
61
+ "maxComputeWorkgroupSizeZ",
62
+ "maxComputeWorkgroupsPerDimension",
63
+ "maxImmediateSize",
64
+ ];
65
+
66
+ /** Adapters that created a device through requestDevice(): a second create({ adapter }) fails without a round trip. */
67
+ const consumedAdapters = new WeakSet<GPUAdapter>();
68
+
69
+ /**
70
+ * The message of a thrown value.
71
+ * @param err - what was caught
72
+ * @returns the Error message, or the value as a string
73
+ */
74
+ function messageOf(err: unknown): string {
75
+ return err instanceof Error ? err.message : String(err);
76
+ }
77
+
78
+ /**
79
+ * Software adapter test (spec 2.2 step 2): architecture "software" (Dawn llvmpipe) or "swiftshader", or
80
+ * isFallbackAdapter === true (Chromium); the ONE reader of these fields.
81
+ * @param info - the adapter info (structural, so tests can fake it)
82
+ * @returns true for a software adapter
83
+ */
84
+ export function isSoftwareAdapter(info: AdapterInfoLike): boolean {
85
+ // "software" = Dawn-node on llvmpipe, "swiftshader" = Chromium's fallback, "warp" = Dawn's D3D12 backend on
86
+ // Microsoft's WARP rasterizer (the Windows host lane: vendor "microsoft", isFallbackAdapter absent under Dawn-node).
87
+ return (
88
+ info.architecture === "software" ||
89
+ info.architecture === "swiftshader" ||
90
+ info.architecture === "warp" ||
91
+ info.isFallbackAdapter === true
92
+ );
93
+ }
94
+
95
+ /**
96
+ * The AdapterSummary of an adapter (info + features + limits); never requests a device.
97
+ * @param adapter - an adapter (unused or not; only its info, features and limits are read)
98
+ * @returns a frozen summary; `subgroupMinSize` / `subgroupMaxSize` are 0 when the info lacks them
99
+ */
100
+ export function summarizeAdapter(adapter: GPUAdapter): AdapterSummary {
101
+ const { info } = adapter;
102
+ const source = adapter.limits as unknown as Readonly<Record<string, unknown>>;
103
+ const limits: Record<string, number> = {};
104
+ for (const name of LIMIT_NAMES) {
105
+ const value = source[name];
106
+ if (typeof value === "number") {
107
+ limits[name] = value;
108
+ }
109
+ }
110
+ return Object.freeze({
111
+ vendor: info.vendor,
112
+ architecture: info.architecture,
113
+ device: info.device,
114
+ description: info.description,
115
+ software: isSoftwareAdapter(info),
116
+ subgroupMinSize: info.subgroupMinSize ?? 0,
117
+ subgroupMaxSize: info.subgroupMaxSize ?? 0,
118
+ features: Object.freeze(Array.from(adapter.features).sort()),
119
+ limits: Object.freeze(limits),
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Step 1 of create(): requestAdapter with the power preference; null -> E_NO_ADAPTER.
125
+ * @param gpu - navigator.gpu or the Dawn handle
126
+ * @param powerPreference - honoured by browsers, ignored by Dawn-node (spec 2.6)
127
+ * @returns an UNUSED adapter
128
+ */
129
+ export async function requestAdapter(gpu: GPU, powerPreference: GPUPowerPreference): Promise<GPUAdapter> {
130
+ let adapter: GPUAdapter | null;
131
+ try {
132
+ adapter = await gpu.requestAdapter({ powerPreference });
133
+ } catch (err) {
134
+ const reason = `requestAdapter() threw: ${messageOf(err)}`;
135
+ throw new WebGpuGraphError("E_NO_ADAPTER", reason, { reason });
136
+ }
137
+ if (adapter === null) {
138
+ const reason = "requestAdapter() returned null: no usable WebGPU adapter";
139
+ throw new WebGpuGraphError("E_NO_ADAPTER", reason, { reason });
140
+ }
141
+ return adapter;
142
+ }
143
+
144
+ /**
145
+ * Step 3 of create(): the requiredLimits record for a policy, clamped to the adapter; an explicit value
146
+ * above the adapter -> E_NO_DEVICE { reason: "limit" } before any requestDevice call, so the failure is
147
+ * diagnosable.
148
+ * @param adapter - the adapter the device will come from
149
+ * @param policy - "default" (nothing), "raise" (every raisable limit at the adapter's value) or explicit values
150
+ * @returns the record to pass as requiredLimits
151
+ */
152
+ export function buildRequiredLimits(adapter: GPUAdapter, policy: LimitPolicy): Record<string, number> {
153
+ const limits: Record<string, number> = {};
154
+ if (policy === "default") {
155
+ return limits;
156
+ }
157
+ if (policy === "raise") {
158
+ for (const name of RAISABLE_LIMITS) {
159
+ limits[name] = adapter.limits[name];
160
+ }
161
+ return limits;
162
+ }
163
+ for (const name of RAISABLE_LIMITS) {
164
+ const requested = policy[name];
165
+ if (requested === undefined) {
166
+ continue;
167
+ }
168
+ const available = adapter.limits[name];
169
+ if (requested > available) {
170
+ throw new WebGpuGraphError(
171
+ "E_NO_DEVICE",
172
+ `requested ${name} = ${requested} exceeds the adapter's ${available}`,
173
+ { reason: "limit", adapter: summarizeAdapter(adapter), limit: name, requested, available },
174
+ );
175
+ }
176
+ limits[name] = requested;
177
+ }
178
+ return limits;
179
+ }
180
+
181
+ /**
182
+ * Step 3 of create(): the requiredFeatures list = required + (optional intersect adapter.features); a missing
183
+ * required feature -> E_NO_DEVICE { reason: "feature", requested: <name> }.
184
+ * @param adapter - the adapter the device will come from
185
+ * @param required - features the caller cannot do without
186
+ * @param optional - features requested only when the adapter has them
187
+ * @returns the deduplicated list, required first
188
+ */
189
+ export function buildRequiredFeatures(
190
+ adapter: GPUAdapter,
191
+ required: readonly GPUFeatureName[],
192
+ optional: readonly GPUFeatureName[],
193
+ ): GPUFeatureName[] {
194
+ const features: GPUFeatureName[] = [];
195
+ for (const feature of required) {
196
+ if (!adapter.features.has(feature)) {
197
+ throw new WebGpuGraphError("E_NO_DEVICE", `required feature "${feature}" is not supported by the adapter`, {
198
+ reason: "feature",
199
+ adapter: summarizeAdapter(adapter),
200
+ requested: feature,
201
+ });
202
+ }
203
+ if (!features.includes(feature)) {
204
+ features.push(feature);
205
+ }
206
+ }
207
+ for (const feature of optional) {
208
+ if (adapter.features.has(feature) && !features.includes(feature)) {
209
+ features.push(feature);
210
+ }
211
+ }
212
+ return features;
213
+ }
214
+
215
+ /**
216
+ * The E_NO_DEVICE of a consumed adapter; the runtime's text lives in the error message only (contract 3.1
217
+ * documents `details` as { reason, adapter, limit?, requested?, available? }).
218
+ * @param adapter - the adapter that already created a device
219
+ * @param message - the runtime's rejection message when there was a round trip
220
+ * @returns E_NO_DEVICE { reason: "consumed", adapter }
221
+ */
222
+ function consumedError(adapter: GPUAdapter, message: string): WebGpuGraphError {
223
+ return new WebGpuGraphError(
224
+ "E_NO_DEVICE",
225
+ `the adapter already created a device (an adapter is consumed by one requestDevice, spec 2.2 step 1): ${message}`,
226
+ { reason: "consumed", adapter: summarizeAdapter(adapter) },
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Step 3 of create(): requestDevice; a rejection -> E_NO_DEVICE with the adapter summary ("consumed" when
232
+ * the adapter already created a device, detected by the OperationError message or a prior-use record).
233
+ * @param adapter - an UNUSED adapter
234
+ * @param descriptor - requiredLimits, requiredFeatures and the label
235
+ * @returns the device
236
+ */
237
+ export async function requestDevice(adapter: GPUAdapter, descriptor: GPUDeviceDescriptor): Promise<GPUDevice> {
238
+ if (consumedAdapters.has(adapter)) {
239
+ throw consumedError(adapter, "recorded by this package");
240
+ }
241
+ let device: GPUDevice;
242
+ try {
243
+ device = await adapter.requestDevice(descriptor);
244
+ } catch (err) {
245
+ const message = messageOf(err);
246
+ if (/consumed|already been used/i.test(message)) {
247
+ throw consumedError(adapter, message);
248
+ }
249
+ throw new WebGpuGraphError("E_NO_DEVICE", `requestDevice() rejected: ${message}`, {
250
+ reason: "requestDevice",
251
+ adapter: summarizeAdapter(adapter),
252
+ });
253
+ }
254
+ consumedAdapters.add(adapter);
255
+ return device;
256
+ }