@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,439 @@
1
+ /**
2
+ * UniformBlock (spec 5.3, D20): one field table generates BOTH the padded WGSL `struct` text spliced into a module
3
+ * and the byte writer / reader the host uses, so the two cannot disagree. The layout is the strict WGSL layout
4
+ * every runtime accepts (Chromium 139 lacks `uniform_buffer_standard_layout`): scalars 4-aligned, vec2 8-aligned,
5
+ * vec4 16-aligned, no vec3, no bool, no arrays, and the total padded to a multiple of 16 (or to `padTo`) through an
6
+ * explicit `@size` on the last member.
7
+ */
8
+
9
+ import { WebGpuGraphError } from "../errors.js";
10
+ import { WGSL_RESERVED_WORDS } from "./prelude.js";
11
+
12
+ /** Field types a block may hold; no vec3 (spec 5.3), no bool (not host-shareable), no arrays. */
13
+ export type UniformFieldType = "u32" | "i32" | "f32" | "vec2f" | "vec2u" | "vec4f" | "vec4u";
14
+ /** One field. */
15
+ export type UniformField = readonly [name: string, type: UniformFieldType];
16
+ /** The values written into or read from a block: scalars as numbers, vectors as number arrays of the vector's width. */
17
+ export type UniformValues = Readonly<Record<string, number | readonly number[]>>;
18
+
19
+ /** The scalar a field is made of. */
20
+ type ScalarKind = "u32" | "i32" | "f32";
21
+
22
+ /** Size, alignment and composition of each field type (WGSL 14.4.1). */
23
+ interface TypeInfo {
24
+ readonly size: number;
25
+ readonly align: number;
26
+ readonly width: number;
27
+ readonly scalar: ScalarKind;
28
+ }
29
+
30
+ const TYPE_INFO: Readonly<Record<UniformFieldType, TypeInfo>> = Object.freeze({
31
+ u32: { size: 4, align: 4, width: 1, scalar: "u32" },
32
+ i32: { size: 4, align: 4, width: 1, scalar: "i32" },
33
+ f32: { size: 4, align: 4, width: 1, scalar: "f32" },
34
+ vec2f: { size: 8, align: 8, width: 2, scalar: "f32" },
35
+ vec2u: { size: 8, align: 8, width: 2, scalar: "u32" },
36
+ vec4f: { size: 16, align: 16, width: 4, scalar: "f32" },
37
+ vec4u: { size: 16, align: 16, width: 4, scalar: "u32" },
38
+ });
39
+
40
+ /** One laid-out field. */
41
+ interface FieldLayout {
42
+ readonly name: string;
43
+ readonly type: UniformFieldType;
44
+ readonly offset: number;
45
+ readonly info: TypeInfo;
46
+ }
47
+
48
+ const STRUCT_ALIGN = 16;
49
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
50
+ const U32_LIMIT = 0xffffffff;
51
+ const I32_MIN = -2147483648;
52
+ const I32_MAX = 2147483647;
53
+
54
+ /**
55
+ * Rounds `value` up to the next multiple of `align` (arithmetic, never a bit trick: byte offsets are never operated
56
+ * on bitwise, spec 3.6).
57
+ * @param value - the value to round
58
+ * @param align - the alignment, a positive integer
59
+ * @returns the rounded value
60
+ */
61
+ function roundUp(value: number, align: number): number {
62
+ return Math.ceil(value / align) * align;
63
+ }
64
+
65
+ /**
66
+ * Validates a WGSL identifier for a struct or field name.
67
+ * @param argument - the E_INVALID_ARGUMENT `argument` to report
68
+ * @param name - the identifier
69
+ */
70
+ function assertIdentifier(argument: string, name: string): void {
71
+ if (!IDENTIFIER.test(name) || WGSL_RESERVED_WORDS.includes(name)) {
72
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock: "${name}" is not a usable WGSL identifier`, {
73
+ argument,
74
+ value: name,
75
+ expected: "a WGSL identifier that is not a reserved word",
76
+ });
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Validates one scalar component of a value against its scalar kind.
82
+ * @param field - the field name (for the error)
83
+ * @param kind - the scalar kind
84
+ * @param value - the component
85
+ */
86
+ function assertScalar(field: string, kind: ScalarKind, value: unknown): asserts value is number {
87
+ if (typeof value !== "number") {
88
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock: field "${field}" expects a number`, {
89
+ argument: field,
90
+ value,
91
+ expected: kind,
92
+ });
93
+ }
94
+ if (kind === "u32" && !(Number.isInteger(value) && value >= 0 && value <= U32_LIMIT)) {
95
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock: field "${field}" is not a u32`, {
96
+ argument: field,
97
+ value,
98
+ expected: "an integer in [0, 4294967295]",
99
+ });
100
+ }
101
+ if (kind === "i32" && !(Number.isInteger(value) && value >= I32_MIN && value <= I32_MAX)) {
102
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock: field "${field}" is not an i32`, {
103
+ argument: field,
104
+ value,
105
+ expected: "an integer in [-2147483648, 2147483647]",
106
+ });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Writes one scalar component little-endian.
112
+ * @param view - the target view
113
+ * @param kind - the scalar kind
114
+ * @param offset - the byte offset
115
+ * @param value - the component
116
+ */
117
+ function setScalar(view: DataView, kind: ScalarKind, offset: number, value: number): void {
118
+ if (kind === "u32") {
119
+ view.setUint32(offset, value, true);
120
+ } else if (kind === "i32") {
121
+ view.setInt32(offset, value, true);
122
+ } else {
123
+ view.setFloat32(offset, value, true);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Reads one scalar component little-endian.
129
+ * @param view - the source view
130
+ * @param kind - the scalar kind
131
+ * @param offset - the byte offset
132
+ * @returns the component
133
+ */
134
+ function getScalar(view: DataView, kind: ScalarKind, offset: number): number {
135
+ if (kind === "u32") {
136
+ return view.getUint32(offset, true);
137
+ }
138
+ if (kind === "i32") {
139
+ return view.getInt32(offset, true);
140
+ }
141
+ return view.getFloat32(offset, true);
142
+ }
143
+
144
+ /** A generated struct: the padded WGSL text and the byte writer / reader share one field table, so they cannot disagree (D20). */
145
+ export class UniformBlock {
146
+ /** The WGSL struct name. */
147
+ readonly name: string;
148
+ /** "uniform" (the default) or "storage"; both lay out identically, the tag documents the block's address space. */
149
+ readonly layout: "uniform" | "storage";
150
+ /** The fields in declaration (= byte) order. */
151
+ readonly fields: readonly UniformField[];
152
+ /** The padded byte length (a multiple of 16). */
153
+ readonly byteLength: number;
154
+ /** The `struct <name> { ... }` text with explicit `@size` / `@align` where padding is needed. */
155
+ readonly wgsl: string;
156
+ /** The laid-out fields by name. */
157
+ private readonly table: ReadonlyMap<string, FieldLayout>;
158
+ /** The laid-out fields in order. */
159
+ private readonly laidOut: readonly FieldLayout[];
160
+
161
+ /**
162
+ * Declares a block; fields are laid out in order with 16-byte alignment for vec4 / the struct, 8 for vec2, 4 for
163
+ * scalars; the total is padded to 16 (uniform, storage) or to `padTo` when given.
164
+ * PLAN DECISION: rejects an empty field list and a struct or field name that is not a WGSL identifier or is a
165
+ * reserved word (spec 3.6: every throwing call leaves state unchanged; a reserved name would be invalid WGSL).
166
+ * PLAN DECISION: optional PARAMETERS (`options?` here, `byteOffset?` of write / read / readField) are spelled
167
+ * `?: T` rather than the contract's `?: T | undefined` because the root ESLint rule
168
+ * no-duplicate-type-constituents rejects the explicit undefined on an optional parameter; every call site is
169
+ * identical, and optional PROPERTIES keep `?: T | undefined`.
170
+ * @param name - the WGSL struct name
171
+ * @param fields - the fields in byte order
172
+ * @param options - the layout tag and the optional padding
173
+ * @param options.layout - "uniform" (default) or "storage"; the tag documents the address space, the layout is the same
174
+ * @param options.padTo - the byte length to pad to (a multiple of 16 not below the natural size)
175
+ * @returns the block
176
+ */
177
+ static define(
178
+ name: string,
179
+ fields: readonly UniformField[],
180
+ options?: { readonly layout?: "uniform" | "storage" | undefined; readonly padTo?: number | undefined },
181
+ ): UniformBlock {
182
+ assertIdentifier("name", name);
183
+ if (fields.length === 0) {
184
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${name}: a block needs at least one field`, {
185
+ argument: "fields",
186
+ value: fields.length,
187
+ expected: ">= 1 field",
188
+ });
189
+ }
190
+ const laidOut: FieldLayout[] = [];
191
+ const table = new Map<string, FieldLayout>();
192
+ let cursor = 0;
193
+ for (const [fieldName, type] of fields) {
194
+ assertIdentifier("fields", fieldName);
195
+ if (table.has(fieldName)) {
196
+ throw new WebGpuGraphError(
197
+ "E_INVALID_ARGUMENT",
198
+ `UniformBlock ${name}: duplicate field "${fieldName}"`,
199
+ {
200
+ argument: "fields",
201
+ value: fieldName,
202
+ expected: "unique field names",
203
+ },
204
+ );
205
+ }
206
+ const info = TYPE_INFO[type] as TypeInfo | undefined;
207
+ if (info === undefined) {
208
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${name}: unknown field type "${type}"`, {
209
+ argument: "fields",
210
+ value: type,
211
+ expected: Object.keys(TYPE_INFO).join(" | "),
212
+ });
213
+ }
214
+ const offset = roundUp(cursor, info.align);
215
+ const field: FieldLayout = { name: fieldName, type, offset, info };
216
+ laidOut.push(field);
217
+ table.set(fieldName, field);
218
+ cursor = offset + info.size;
219
+ }
220
+ const natural = roundUp(cursor, STRUCT_ALIGN);
221
+ const padTo = options?.padTo;
222
+ if (padTo !== undefined && (!Number.isInteger(padTo) || padTo % STRUCT_ALIGN !== 0 || padTo < natural)) {
223
+ throw new WebGpuGraphError(
224
+ "E_INVALID_ARGUMENT",
225
+ `UniformBlock ${name}: padTo ${padTo} is not a multiple of 16 >= ${natural}`,
226
+ {
227
+ argument: "padTo",
228
+ value: padTo,
229
+ expected: `a multiple of ${STRUCT_ALIGN} not below ${natural}`,
230
+ },
231
+ );
232
+ }
233
+ const byteLength = padTo ?? natural;
234
+ const lines = laidOut.map((field, index) => {
235
+ const last = index === laidOut.length - 1;
236
+ const end = field.offset + field.info.size;
237
+ const size = last && end < byteLength ? `@size(${byteLength - field.offset}) ` : "";
238
+ return ` ${size}${field.name}: ${field.type},`;
239
+ });
240
+ const wgsl = `struct ${name} {\n${lines.join("\n")}\n}`;
241
+ return new UniformBlock(name, options?.layout ?? "uniform", fields, byteLength, wgsl, laidOut, table);
242
+ }
243
+
244
+ /**
245
+ * Built by `define()` only.
246
+ * @param name - the struct name
247
+ * @param layout - the address-space tag
248
+ * @param fields - the declared fields
249
+ * @param byteLength - the padded byte length
250
+ * @param wgsl - the struct text
251
+ * @param laidOut - the laid-out fields in order
252
+ * @param table - the laid-out fields by name
253
+ */
254
+ private constructor(
255
+ name: string,
256
+ layout: "uniform" | "storage",
257
+ fields: readonly UniformField[],
258
+ byteLength: number,
259
+ wgsl: string,
260
+ laidOut: readonly FieldLayout[],
261
+ table: ReadonlyMap<string, FieldLayout>,
262
+ ) {
263
+ this.name = name;
264
+ this.layout = layout;
265
+ this.fields = Object.freeze(fields.map(([fieldName, type]) => Object.freeze([fieldName, type] as const)));
266
+ this.byteLength = byteLength;
267
+ this.wgsl = wgsl;
268
+ this.laidOut = laidOut;
269
+ this.table = table;
270
+ }
271
+
272
+ /**
273
+ * Byte offset of a field; E_INVALID_ARGUMENT for an unknown field.
274
+ * @param field - the field name
275
+ * @returns the byte offset inside the block
276
+ */
277
+ offsetOf(field: string): number {
278
+ return this.fieldLayout(field).offset;
279
+ }
280
+
281
+ /**
282
+ * Writes `values` at `byteOffset` (default 0); a missing field is written as 0; an unknown key is
283
+ * E_INVALID_ARGUMENT; a vector of the wrong width is E_INVALID_ARGUMENT. Always little-endian. Every byte of the
284
+ * block's region is written (padding as zero), so a written block is byte-exact.
285
+ * PLAN DECISION: every value is validated first (u32 / vec*u components integers in [0, 2^32 - 1], i32 in
286
+ * [-2^31, 2^31 - 1]) and only then is the region zeroed and written, so a rejected call leaves the view unchanged.
287
+ * @param view - the target view (the region must lie inside it)
288
+ * @param values - the field values
289
+ * @param byteOffset - the region's start inside the view
290
+ */
291
+ write(view: DataView, values: UniformValues, byteOffset?: number): void {
292
+ const base = this.regionStart(view, byteOffset);
293
+ for (const key of Object.keys(values)) {
294
+ if (!this.table.has(key)) {
295
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${this.name}: unknown field "${key}"`, {
296
+ argument: "values",
297
+ value: key,
298
+ expected: this.laidOut.map((f) => f.name).join(", "),
299
+ });
300
+ }
301
+ }
302
+ // validate every value first, so a rejected call leaves the view unchanged (spec 3.6)
303
+ const components: (readonly number[] | null)[] = this.laidOut.map((field) =>
304
+ this.components(field, values[field.name]),
305
+ );
306
+ for (let at = 0; at < this.byteLength; at += 4) {
307
+ view.setUint32(base + at, 0, true);
308
+ }
309
+ this.laidOut.forEach((field, index) => {
310
+ const lanes = components[index];
311
+ if (lanes === null) {
312
+ return;
313
+ }
314
+ lanes.forEach((component, lane) => {
315
+ setScalar(view, field.info.scalar, base + field.offset + 4 * lane, component);
316
+ });
317
+ });
318
+ }
319
+
320
+ /**
321
+ * The validated components of one field's value: null for an absent field, else `width` numbers.
322
+ * @param field - the laid-out field
323
+ * @param value - the value given for it (undefined when absent)
324
+ * @returns the components or null
325
+ */
326
+ private components(field: FieldLayout, value: number | readonly number[] | undefined): readonly number[] | null {
327
+ if (value === undefined) {
328
+ return null;
329
+ }
330
+ const { scalar, width } = field.info;
331
+ if (width === 1) {
332
+ assertScalar(field.name, scalar, value);
333
+ return [value];
334
+ }
335
+ if (!Array.isArray(value) || value.length !== width) {
336
+ throw new WebGpuGraphError(
337
+ "E_INVALID_ARGUMENT",
338
+ `UniformBlock ${this.name}: field "${field.name}" expects ${width} components`,
339
+ {
340
+ argument: field.name,
341
+ value,
342
+ expected: `an array of ${width} numbers`,
343
+ },
344
+ );
345
+ }
346
+ const lanes: number[] = [];
347
+ for (let lane = 0; lane < width; lane++) {
348
+ const component: unknown = value[lane];
349
+ assertScalar(field.name, scalar, component);
350
+ lanes.push(component);
351
+ }
352
+ return lanes;
353
+ }
354
+
355
+ /**
356
+ * Reads every field at `byteOffset` (storage mode's reader; also used by tests on uniform blocks).
357
+ * @param view - the source view
358
+ * @param byteOffset - the region's start inside the view
359
+ * @returns the field values (vectors as arrays)
360
+ */
361
+ read(view: DataView, byteOffset?: number): UniformValues {
362
+ const base = this.regionStart(view, byteOffset);
363
+ const out: Record<string, number | readonly number[]> = {};
364
+ for (const field of this.laidOut) {
365
+ out[field.name] = this.decode(view, field, base);
366
+ }
367
+ return out;
368
+ }
369
+
370
+ /**
371
+ * Reads one field.
372
+ * @param view - the source view
373
+ * @param field - the field name
374
+ * @param byteOffset - the region's start inside the view
375
+ * @returns the value (a number, or an array for a vector)
376
+ */
377
+ readField(view: DataView, field: string, byteOffset?: number): number | readonly number[] {
378
+ const layout = this.fieldLayout(field);
379
+ return this.decode(view, layout, this.regionStart(view, byteOffset));
380
+ }
381
+
382
+ /**
383
+ * The laid-out field of a name.
384
+ * @param field - the field name
385
+ * @returns the layout
386
+ */
387
+ private fieldLayout(field: string): FieldLayout {
388
+ const layout = this.table.get(field);
389
+ if (layout === undefined) {
390
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${this.name}: unknown field "${field}"`, {
391
+ argument: "field",
392
+ value: field,
393
+ expected: this.laidOut.map((f) => f.name).join(", "),
394
+ });
395
+ }
396
+ return layout;
397
+ }
398
+
399
+ /**
400
+ * Validates the region `[byteOffset, byteOffset + byteLength)` against the view.
401
+ * @param view - the view
402
+ * @param byteOffset - the requested start (default 0)
403
+ * @returns the start
404
+ */
405
+ private regionStart(view: DataView, byteOffset: number | undefined): number {
406
+ const base = byteOffset ?? 0;
407
+ if (!Number.isInteger(base) || base < 0 || base + this.byteLength > view.byteLength) {
408
+ throw new WebGpuGraphError(
409
+ "E_INVALID_ARGUMENT",
410
+ `UniformBlock ${this.name}: region [${base}, ${base + this.byteLength}) is outside the view`,
411
+ {
412
+ argument: "byteOffset",
413
+ value: base,
414
+ expected: `0 <= byteOffset <= ${view.byteLength - this.byteLength}`,
415
+ },
416
+ );
417
+ }
418
+ return base;
419
+ }
420
+
421
+ /**
422
+ * Decodes one field from a view.
423
+ * @param view - the source view
424
+ * @param field - the laid-out field
425
+ * @param base - the region start
426
+ * @returns the value
427
+ */
428
+ private decode(view: DataView, field: FieldLayout, base: number): number | readonly number[] {
429
+ const { scalar, width } = field.info;
430
+ if (width === 1) {
431
+ return getScalar(view, scalar, base + field.offset);
432
+ }
433
+ const lanes: number[] = [];
434
+ for (let lane = 0; lane < width; lane++) {
435
+ lanes.push(getScalar(view, scalar, base + field.offset + 4 * lane));
436
+ }
437
+ return lanes;
438
+ }
439
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The UniformRing of spec 5.3: one UNIFORM buffer holding UNIFORM_SLOT_BYTES-stride slots for the
3
+ * per-iteration params of a batch. A batch reserves k contiguous slots, writes their values into the
4
+ * host shadow, flushes them with ONE queue.writeBuffer and records k dispatches that select their slot
5
+ * through a dynamic offset -- no per-iteration writeBuffer and no host round trip. The stride is 256
6
+ * because minUniformBufferOffsetAlignment is 256 on every runtime the package targets (the Dawn-node
7
+ * ADAPTER advertises 64, which the package never requests; spec 2.6).
8
+ */
9
+
10
+ import { UNIFORM_SLOT_BYTES } from "../constants.js";
11
+ import type { AllocationTracker } from "../device/error-scope.js";
12
+ import { BufferUsage } from "../device/webgpu-constants.js";
13
+ import { WebGpuGraphError } from "../errors.js";
14
+ import type { Binding } from "../types/memory.js";
15
+ import type { UniformBlock, UniformValues } from "./struct-block.js";
16
+
17
+ /** One UNIFORM buffer with UNIFORM_SLOT_BYTES-stride slots for the per-iteration params of a batch (spec 5.3). */
18
+ export class UniformRing {
19
+ /** Number of slots. */
20
+ readonly slots: number;
21
+ private readonly device: GPUDevice;
22
+ private readonly allocator: AllocationTracker;
23
+ private readonly buffer: GPUBuffer;
24
+ private readonly label: string;
25
+ private readonly shadow: ArrayBuffer;
26
+ private readonly view: DataView;
27
+ private next = 0;
28
+ private dirtyLo = -1;
29
+ private dirtyHi = -1;
30
+ private destroyed = false;
31
+
32
+ /**
33
+ * Creates the ring buffer (`slots x UNIFORM_SLOT_BYTES` bytes, UNIFORM | COPY_DST) through the allocator.
34
+ * @param device - the device the buffer belongs to
35
+ * @param allocator - the context's OOM-scoped allocator (the buffer is destroyed through it)
36
+ * @param slots - number of slots; E_INVALID_ARGUMENT unless a positive integer
37
+ * @param label - the buffer label
38
+ */
39
+ constructor(device: GPUDevice, allocator: AllocationTracker, slots: number, label: string) {
40
+ if (!Number.isInteger(slots) || slots < 1) {
41
+ throw new WebGpuGraphError(
42
+ "E_INVALID_ARGUMENT",
43
+ `UniformRing "${label}": slots must be a positive integer, got ${slots}`,
44
+ {
45
+ argument: "slots",
46
+ value: slots,
47
+ expected: "an integer >= 1",
48
+ },
49
+ );
50
+ }
51
+ this.device = device;
52
+ this.allocator = allocator;
53
+ this.slots = slots;
54
+ this.label = label;
55
+ this.shadow = new ArrayBuffer(slots * UNIFORM_SLOT_BYTES);
56
+ this.view = new DataView(this.shadow);
57
+ this.buffer = allocator.createBuffer({
58
+ label,
59
+ size: slots * UNIFORM_SLOT_BYTES,
60
+ usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
61
+ });
62
+ }
63
+
64
+ /**
65
+ * The whole-buffer binding a kernel binds once (size = the block's byteLength; the slot is the dynamic offset).
66
+ * @param block - the params block the kernel declares for this binding
67
+ * @returns the binding at offset 0 of the ring buffer
68
+ */
69
+ binding(block: UniformBlock): Binding {
70
+ this.assertLive();
71
+ this.assertFits(block);
72
+ return { buffer: this.buffer, offset: 0, size: block.byteLength, window: null };
73
+ }
74
+
75
+ /**
76
+ * Byte offset of a slot (the dynamic offset a dispatch passes for it).
77
+ * @param slot - a slot index in [0, slots)
78
+ * @returns slot x UNIFORM_SLOT_BYTES
79
+ */
80
+ offsetOf(slot: number): number {
81
+ this.checkSlot(slot);
82
+ return slot * UNIFORM_SLOT_BYTES;
83
+ }
84
+
85
+ /**
86
+ * Reserves `count` contiguous slots for a batch, wrapping to 0 when the tail is too short; E_INVALID_ARGUMENT when count > slots.
87
+ * @param count - slots needed, in [1, slots]
88
+ * @returns the first slot of the reservation
89
+ */
90
+ reserve(count: number): number {
91
+ this.assertLive();
92
+ if (!Number.isInteger(count) || count < 1 || count > this.slots) {
93
+ throw new WebGpuGraphError(
94
+ "E_INVALID_ARGUMENT",
95
+ `UniformRing "${this.label}": cannot reserve ${count} of ${this.slots} slots`,
96
+ {
97
+ argument: "count",
98
+ value: count,
99
+ expected: `an integer in [1, ${this.slots}]`,
100
+ },
101
+ );
102
+ }
103
+ if (this.next + count > this.slots) {
104
+ this.next = 0;
105
+ }
106
+ const first = this.next;
107
+ this.next += count;
108
+ return first;
109
+ }
110
+
111
+ /**
112
+ * Writes one block's values into a slot of the host shadow; `flush()` sends the dirty range with one writeBuffer.
113
+ * @param slot - the slot to fill
114
+ * @param block - the block whose layout the bytes follow
115
+ * @param values - the field values (a missing field is written as 0 by the block)
116
+ */
117
+ write(slot: number, block: UniformBlock, values: UniformValues): void {
118
+ this.checkSlot(slot);
119
+ this.assertFits(block);
120
+ block.write(this.view, values, slot * UNIFORM_SLOT_BYTES);
121
+ if (this.dirtyLo < 0 || slot < this.dirtyLo) {
122
+ this.dirtyLo = slot;
123
+ }
124
+ if (slot > this.dirtyHi) {
125
+ this.dirtyHi = slot;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * queue.writeBuffer of the dirty slots (called by the batch driver before submit): one call covering
131
+ * [lowest dirty slot, highest dirty slot]; nothing when no slot is dirty.
132
+ */
133
+ flush(): void {
134
+ this.assertLive();
135
+ if (this.dirtyLo < 0) {
136
+ return;
137
+ }
138
+ const begin = this.dirtyLo * UNIFORM_SLOT_BYTES;
139
+ const end = (this.dirtyHi + 1) * UNIFORM_SLOT_BYTES;
140
+ this.device.queue.writeBuffer(this.buffer, begin, this.shadow, begin, end - begin);
141
+ this.dirtyLo = -1;
142
+ this.dirtyHi = -1;
143
+ }
144
+
145
+ /** Destroys the buffer (through the allocator); idempotent. */
146
+ destroy(): void {
147
+ if (this.destroyed) {
148
+ return;
149
+ }
150
+ this.destroyed = true;
151
+ this.allocator.destroy(this.buffer);
152
+ }
153
+
154
+ private assertLive(): void {
155
+ if (this.destroyed) {
156
+ throw new WebGpuGraphError("E_DISPOSED", `UniformRing "${this.label}" is destroyed`, { label: this.label });
157
+ }
158
+ }
159
+
160
+ private checkSlot(slot: number): void {
161
+ this.assertLive();
162
+ if (!Number.isInteger(slot) || slot < 0 || slot >= this.slots) {
163
+ throw new WebGpuGraphError(
164
+ "E_INVALID_ARGUMENT",
165
+ `UniformRing "${this.label}": slot ${slot} is outside [0, ${this.slots})`,
166
+ {
167
+ argument: "slot",
168
+ value: slot,
169
+ expected: `an integer in [0, ${this.slots})`,
170
+ },
171
+ );
172
+ }
173
+ }
174
+
175
+ private assertFits(block: UniformBlock): void {
176
+ if (block.byteLength > UNIFORM_SLOT_BYTES) {
177
+ throw new WebGpuGraphError(
178
+ "E_INVALID_ARGUMENT",
179
+ `UniformRing "${this.label}": block ${block.name} is ${block.byteLength} bytes, a slot holds ${UNIFORM_SLOT_BYTES}`,
180
+ { argument: "block", value: block.name, expected: `byteLength <= ${UNIFORM_SLOT_BYTES}` },
181
+ );
182
+ }
183
+ }
184
+ }