@graphty/webgpu-graph-algorithms 0.6.3 → 0.6.4

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 (167) hide show
  1. package/README.md +62 -32
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-BXqgCifx.js → context-Dvq-Cc6v.js} +71 -25
  4. package/dist/chunks/context-Dvq-Cc6v.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +8 -6
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +57 -6
  9. package/dist/src/accelerator.js.map +1 -1
  10. package/dist/src/algorithms/bellman-ford.d.ts +60 -0
  11. package/dist/src/algorithms/bellman-ford.d.ts.map +1 -0
  12. package/dist/src/algorithms/bellman-ford.js +301 -0
  13. package/dist/src/algorithms/bellman-ford.js.map +1 -0
  14. package/dist/src/algorithms/bfs.d.ts +67 -0
  15. package/dist/src/algorithms/bfs.d.ts.map +1 -0
  16. package/dist/src/algorithms/bfs.js +534 -0
  17. package/dist/src/algorithms/bfs.js.map +1 -0
  18. package/dist/src/algorithms/closeness.d.ts +53 -0
  19. package/dist/src/algorithms/closeness.d.ts.map +1 -0
  20. package/dist/src/algorithms/closeness.js +323 -0
  21. package/dist/src/algorithms/closeness.js.map +1 -0
  22. package/dist/src/algorithms/scope.d.ts +5 -3
  23. package/dist/src/algorithms/scope.d.ts.map +1 -1
  24. package/dist/src/algorithms/scope.js +3 -0
  25. package/dist/src/algorithms/scope.js.map +1 -1
  26. package/dist/src/algorithms/sssp.d.ts +71 -0
  27. package/dist/src/algorithms/sssp.d.ts.map +1 -0
  28. package/dist/src/algorithms/sssp.js +585 -0
  29. package/dist/src/algorithms/sssp.js.map +1 -0
  30. package/dist/src/constants.d.ts +12 -0
  31. package/dist/src/constants.d.ts.map +1 -1
  32. package/dist/src/constants.js +12 -0
  33. package/dist/src/constants.js.map +1 -1
  34. package/dist/src/index.d.ts +8 -2
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +7 -1
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/kernel/prelude.d.ts +4 -4
  39. package/dist/src/kernel/prelude.d.ts.map +1 -1
  40. package/dist/src/kernel/prelude.js +39 -5
  41. package/dist/src/kernel/prelude.js.map +1 -1
  42. package/dist/src/kernel/uniform-ring.d.ts +8 -0
  43. package/dist/src/kernel/uniform-ring.d.ts.map +1 -1
  44. package/dist/src/kernel/uniform-ring.js +13 -0
  45. package/dist/src/kernel/uniform-ring.js.map +1 -1
  46. package/dist/src/kernels.d.ts +44 -4
  47. package/dist/src/kernels.d.ts.map +1 -1
  48. package/dist/src/kernels.js +371 -3
  49. package/dist/src/kernels.js.map +1 -1
  50. package/dist/src/primitives/advance.d.ts +62 -0
  51. package/dist/src/primitives/advance.d.ts.map +1 -0
  52. package/dist/src/primitives/advance.js +95 -0
  53. package/dist/src/primitives/advance.js.map +1 -0
  54. package/dist/src/primitives/compact.d.ts +89 -0
  55. package/dist/src/primitives/compact.d.ts.map +1 -0
  56. package/dist/src/primitives/compact.js +233 -0
  57. package/dist/src/primitives/compact.js.map +1 -0
  58. package/dist/src/primitives/core-shape.d.ts +22 -1
  59. package/dist/src/primitives/core-shape.d.ts.map +1 -1
  60. package/dist/src/primitives/core-shape.js +33 -3
  61. package/dist/src/primitives/core-shape.js.map +1 -1
  62. package/dist/src/primitives/frontier.d.ts +156 -0
  63. package/dist/src/primitives/frontier.d.ts.map +1 -0
  64. package/dist/src/primitives/frontier.js +259 -0
  65. package/dist/src/primitives/frontier.js.map +1 -0
  66. package/dist/src/types/accelerator.d.ts +16 -7
  67. package/dist/src/types/accelerator.d.ts.map +1 -1
  68. package/dist/src/types/traversal.d.ts +53 -0
  69. package/dist/src/types/traversal.d.ts.map +1 -0
  70. package/dist/src/types/traversal.js +10 -0
  71. package/dist/src/types/traversal.js.map +1 -0
  72. package/dist/src/wgsl/advance-expand.wgsl.d.ts +19 -0
  73. package/dist/src/wgsl/advance-expand.wgsl.d.ts.map +1 -0
  74. package/dist/src/wgsl/advance-expand.wgsl.js +69 -0
  75. package/dist/src/wgsl/advance-expand.wgsl.js.map +1 -0
  76. package/dist/src/wgsl/bf-relax.wgsl.d.ts +22 -0
  77. package/dist/src/wgsl/bf-relax.wgsl.d.ts.map +1 -0
  78. package/dist/src/wgsl/bf-relax.wgsl.js +58 -0
  79. package/dist/src/wgsl/bf-relax.wgsl.js.map +1 -0
  80. package/dist/src/wgsl/bfs-bitset-build.wgsl.d.ts +15 -0
  81. package/dist/src/wgsl/bfs-bitset-build.wgsl.d.ts.map +1 -0
  82. package/dist/src/wgsl/bfs-bitset-build.wgsl.js +24 -0
  83. package/dist/src/wgsl/bfs-bitset-build.wgsl.js.map +1 -0
  84. package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts +20 -0
  85. package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts.map +1 -0
  86. package/dist/src/wgsl/bfs-bottom-up.wgsl.js +67 -0
  87. package/dist/src/wgsl/bfs-bottom-up.wgsl.js.map +1 -0
  88. package/dist/src/wgsl/bfs-contract.wgsl.d.ts +20 -0
  89. package/dist/src/wgsl/bfs-contract.wgsl.d.ts.map +1 -0
  90. package/dist/src/wgsl/bfs-contract.wgsl.js +55 -0
  91. package/dist/src/wgsl/bfs-contract.wgsl.js.map +1 -0
  92. package/dist/src/wgsl/bfs-fused.wgsl.d.ts +25 -0
  93. package/dist/src/wgsl/bfs-fused.wgsl.d.ts.map +1 -0
  94. package/dist/src/wgsl/bfs-fused.wgsl.js +78 -0
  95. package/dist/src/wgsl/bfs-fused.wgsl.js.map +1 -0
  96. package/dist/src/wgsl/bfs-unvisited-flags.wgsl.d.ts +18 -0
  97. package/dist/src/wgsl/bfs-unvisited-flags.wgsl.d.ts.map +1 -0
  98. package/dist/src/wgsl/bfs-unvisited-flags.wgsl.js +42 -0
  99. package/dist/src/wgsl/bfs-unvisited-flags.wgsl.js.map +1 -0
  100. package/dist/src/wgsl/closeness-reduce.wgsl.d.ts +17 -0
  101. package/dist/src/wgsl/closeness-reduce.wgsl.d.ts.map +1 -0
  102. package/dist/src/wgsl/closeness-reduce.wgsl.js +65 -0
  103. package/dist/src/wgsl/closeness-reduce.wgsl.js.map +1 -0
  104. package/dist/src/wgsl/closeness-sweep.wgsl.d.ts +20 -0
  105. package/dist/src/wgsl/closeness-sweep.wgsl.d.ts.map +1 -0
  106. package/dist/src/wgsl/closeness-sweep.wgsl.js +96 -0
  107. package/dist/src/wgsl/closeness-sweep.wgsl.js.map +1 -0
  108. package/dist/src/wgsl/compact-scatter.wgsl.d.ts +9 -0
  109. package/dist/src/wgsl/compact-scatter.wgsl.d.ts.map +1 -0
  110. package/dist/src/wgsl/compact-scatter.wgsl.js +17 -0
  111. package/dist/src/wgsl/compact-scatter.wgsl.js.map +1 -0
  112. package/dist/src/wgsl/dedupe-claim.wgsl.d.ts +10 -0
  113. package/dist/src/wgsl/dedupe-claim.wgsl.d.ts.map +1 -0
  114. package/dist/src/wgsl/dedupe-claim.wgsl.js +19 -0
  115. package/dist/src/wgsl/dedupe-claim.wgsl.js.map +1 -0
  116. package/dist/src/wgsl/dedupe-filter.wgsl.d.ts +12 -0
  117. package/dist/src/wgsl/dedupe-filter.wgsl.d.ts.map +1 -0
  118. package/dist/src/wgsl/dedupe-filter.wgsl.js +46 -0
  119. package/dist/src/wgsl/dedupe-filter.wgsl.js.map +1 -0
  120. package/dist/src/wgsl/frontier-finalize.wgsl.d.ts +59 -0
  121. package/dist/src/wgsl/frontier-finalize.wgsl.d.ts.map +1 -0
  122. package/dist/src/wgsl/frontier-finalize.wgsl.js +210 -0
  123. package/dist/src/wgsl/frontier-finalize.wgsl.js.map +1 -0
  124. package/dist/src/wgsl/sssp-pred.wgsl.d.ts +28 -0
  125. package/dist/src/wgsl/sssp-pred.wgsl.d.ts.map +1 -0
  126. package/dist/src/wgsl/sssp-pred.wgsl.js +80 -0
  127. package/dist/src/wgsl/sssp-pred.wgsl.js.map +1 -0
  128. package/dist/src/wgsl/sssp-relax.wgsl.d.ts +30 -0
  129. package/dist/src/wgsl/sssp-relax.wgsl.d.ts.map +1 -0
  130. package/dist/src/wgsl/sssp-relax.wgsl.js +72 -0
  131. package/dist/src/wgsl/sssp-relax.wgsl.js.map +1 -0
  132. package/dist/webgpu-graph-algorithms.js +3207 -377
  133. package/dist/webgpu-graph-algorithms.js.map +1 -1
  134. package/package.json +5 -4
  135. package/src/accelerator.ts +65 -7
  136. package/src/algorithms/bellman-ford.ts +387 -0
  137. package/src/algorithms/bfs.ts +626 -0
  138. package/src/algorithms/closeness.ts +395 -0
  139. package/src/algorithms/scope.ts +13 -3
  140. package/src/algorithms/sssp.ts +767 -0
  141. package/src/constants.ts +12 -0
  142. package/src/index.ts +14 -1
  143. package/src/kernel/prelude.ts +39 -4
  144. package/src/kernel/uniform-ring.ts +14 -0
  145. package/src/kernels.ts +450 -6
  146. package/src/primitives/advance.ts +130 -0
  147. package/src/primitives/compact.ts +323 -0
  148. package/src/primitives/core-shape.ts +41 -3
  149. package/src/primitives/frontier.ts +388 -0
  150. package/src/types/accelerator.ts +18 -5
  151. package/src/types/traversal.ts +56 -0
  152. package/src/wgsl/advance-expand.wgsl.ts +68 -0
  153. package/src/wgsl/bf-relax.wgsl.ts +57 -0
  154. package/src/wgsl/bfs-bitset-build.wgsl.ts +23 -0
  155. package/src/wgsl/bfs-bottom-up.wgsl.ts +66 -0
  156. package/src/wgsl/bfs-contract.wgsl.ts +54 -0
  157. package/src/wgsl/bfs-fused.wgsl.ts +77 -0
  158. package/src/wgsl/bfs-unvisited-flags.wgsl.ts +41 -0
  159. package/src/wgsl/closeness-reduce.wgsl.ts +64 -0
  160. package/src/wgsl/closeness-sweep.wgsl.ts +95 -0
  161. package/src/wgsl/compact-scatter.wgsl.ts +16 -0
  162. package/src/wgsl/dedupe-claim.wgsl.ts +18 -0
  163. package/src/wgsl/dedupe-filter.wgsl.ts +45 -0
  164. package/src/wgsl/frontier-finalize.wgsl.ts +209 -0
  165. package/src/wgsl/sssp-pred.wgsl.ts +79 -0
  166. package/src/wgsl/sssp-relax.wgsl.ts +71 -0
  167. package/dist/chunks/context-BXqgCifx.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { W as WebGpuGraphError, U as UNIFORM_SLOT_BYTES, B as BufferUsage, M as MAX_WORKGROUPS_PER_DIM, a as WGSL_RESERVED_WORDS, S as STATE_HEADER_BYTES, d as deviceLostError, i as isWebGpuGraphError, b as U32_MAX$2, R as RADIX_BINS, c as GRID_COARSEST_SIDE, e as GRID_MIN_SIDE, f as GRID_SORT_BITS, F as FA2_DEFAULTS, g as MAX_ITERATIONS_PER_STEP, h as MAX_1D_ITEMS, j as hasErrorCode, k as FA2_FLAG_FIRST, P as PARTIAL_BYTES, I as INDIRECT_ARGS_STRIDE, l as GRID_HUB_CELL, L as LAYOUT_TUNING_DEFAULTS, E as EXACT_MAX_NODES, T as TRACE_RECORD_BYTES, m as GRID_BBOX_MARGIN, n as GRID_EXTENT_FLOOR, o as FR_ADAPTIVE_MAX_ITERATIONS, p as FR_START_TEMPERATURE, q as FA2_FLAG_ADAPTIVE, r as FR_REHEAT_FRACTION, s as FR_DEFAULTS, t as SE_DEFAULTS, u as SE_SCALE_REFERENCE_NODES } from "./chunks/context-BXqgCifx.js";
2
- import { A, G, v, w, x, y } from "./chunks/context-BXqgCifx.js";
3
- import { renumberPartition, makeMask, maskTest, expandEdges, fromEdgeArrays } from "@graphty/graph-format";
1
+ import { W as WebGpuGraphError, U as UNIFORM_SLOT_BYTES, B as BufferUsage, M as MAX_WORKGROUPS_PER_DIM, a as WGSL_RESERVED_WORDS, S as STATE_HEADER_BYTES, d as deviceLostError, i as isWebGpuGraphError, b as U32_MAX$2, c as MAX_LEVELS_PER_SUBMIT, F as FRONTIER_CANDIDATES, I as INDIRECT_ARGS_STRIDE, R as RADIX_BINS, e as FUSED_FRONTIER_MAX, f as BEAMER_BETA, g as SSSP_DELTA_FACTOR, h as F32_INF_BITS, j as GRID_COARSEST_SIDE, k as GRID_MIN_SIDE, l as GRID_SORT_BITS, m as FA2_DEFAULTS, n as MAX_ITERATIONS_PER_STEP, o as MAX_1D_ITEMS, p as hasErrorCode, q as FA2_FLAG_FIRST, P as PARTIAL_BYTES, r as GRID_HUB_CELL, L as LAYOUT_TUNING_DEFAULTS, E as EXACT_MAX_NODES, T as TRACE_RECORD_BYTES, s as GRID_BBOX_MARGIN, t as GRID_EXTENT_FLOOR, u as FR_ADAPTIVE_MAX_ITERATIONS, v as FR_START_TEMPERATURE, w as FA2_FLAG_ADAPTIVE, x as FR_REHEAT_FRACTION, y as FR_DEFAULTS, z as SE_DEFAULTS, A as SE_SCALE_REFERENCE_NODES } from "./chunks/context-Dvq-Cc6v.js";
2
+ import { C, G, D, H, J, K } from "./chunks/context-Dvq-Cc6v.js";
3
+ import { renumberPartition, INVALID_INDEX, makeMask, maskTest, expandEdges, fromEdgeArrays } from "@graphty/graph-format";
4
4
  class UniformRing {
5
5
  /**
6
6
  * Creates the ring buffer (`slots x UNIFORM_SLOT_BYTES` bytes, UNIFORM | COPY_DST) through the allocator.
@@ -10,6 +10,7 @@ class UniformRing {
10
10
  * @param label - the buffer label
11
11
  */
12
12
  constructor(device, allocator, slots, label) {
13
+ this.overruns = 0;
13
14
  this.next = 0;
14
15
  this.dirtyLo = -1;
15
16
  this.dirtyHi = -1;
@@ -31,6 +32,7 @@ class UniformRing {
31
32
  this.label = label;
32
33
  this.shadow = new ArrayBuffer(slots * UNIFORM_SLOT_BYTES);
33
34
  this.view = new DataView(this.shadow);
35
+ this.dirty = new Uint8Array(slots);
34
36
  this.buffer = allocator.createBuffer({
35
37
  label,
36
38
  size: slots * UNIFORM_SLOT_BYTES,
@@ -79,6 +81,9 @@ class UniformRing {
79
81
  }
80
82
  const first = this.next;
81
83
  this.next += count;
84
+ if (this.dirty.subarray(first, first + count).includes(1)) {
85
+ this.overruns += 1;
86
+ }
82
87
  return first;
83
88
  }
84
89
  /**
@@ -91,6 +96,7 @@ class UniformRing {
91
96
  this.checkSlot(slot);
92
97
  this.assertFits(block);
93
98
  block.write(this.view, values, slot * UNIFORM_SLOT_BYTES);
99
+ this.dirty[slot] = 1;
94
100
  if (this.dirtyLo < 0 || slot < this.dirtyLo) {
95
101
  this.dirtyLo = slot;
96
102
  }
@@ -110,6 +116,7 @@ class UniformRing {
110
116
  const begin = this.dirtyLo * UNIFORM_SLOT_BYTES;
111
117
  const end = (this.dirtyHi + 1) * UNIFORM_SLOT_BYTES;
112
118
  this.device.queue.writeBuffer(this.buffer, begin, this.shadow, begin, end - begin);
119
+ this.dirty.fill(0);
113
120
  this.dirtyLo = -1;
114
121
  this.dirtyHi = -1;
115
122
  }
@@ -153,7 +160,7 @@ class UniformRing {
153
160
  function perDimension(caps) {
154
161
  return Math.min(MAX_WORKGROUPS_PER_DIM, caps.limits.maxComputeWorkgroupsPerDimension);
155
162
  }
156
- function assertCount(argument, value) {
163
+ function assertCount$1(argument, value) {
157
164
  if (!Number.isInteger(value) || value < 0) {
158
165
  throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument} must be a non-negative integer, got ${value}`, {
159
166
  argument,
@@ -183,8 +190,8 @@ function grid(groups, items, caps) {
183
190
  if (groups <= limit) {
184
191
  return { x: groups, y: 1, z: 1, items, stride: null };
185
192
  }
186
- const y2 = Math.ceil(groups / limit);
187
- if (y2 > limit) {
193
+ const y = Math.ceil(groups / limit);
194
+ if (y > limit) {
188
195
  throw new WebGpuGraphError(
189
196
  "E_TOO_LARGE",
190
197
  `${groups} workgroups exceed the 2D dispatch limit of ${limit * limit}`,
@@ -197,15 +204,15 @@ function grid(groups, items, caps) {
197
204
  }
198
205
  );
199
206
  }
200
- return { x: limit, y: y2, z: 1, items, stride: null };
207
+ return { x: limit, y, z: 1, items, stride: null };
201
208
  }
202
209
  function plan1d(items, wg, caps) {
203
- assertCount("items", items);
210
+ assertCount$1("items", items);
204
211
  assertWorkgroupSize(wg);
205
212
  return grid(Math.ceil(items / wg), items, caps);
206
213
  }
207
214
  function planGridStride(items, wg, caps, maxGroups) {
208
- assertCount("items", items);
215
+ assertCount$1("items", items);
209
216
  assertWorkgroupSize(wg);
210
217
  if (items === 0) {
211
218
  return { x: 0, y: 1, z: 1, items, stride: null };
@@ -544,6 +551,432 @@ ${lines.join("\n")}
544
551
  return lanes;
545
552
  }
546
553
  }
554
+ const advanceExpandWgsl = (
555
+ /* wgsl */
556
+ `
557
+ var<workgroup> sh: array<u32, WG>; // the block's degrees, then their inclusive scan
558
+ var<workgroup> rowStart: array<u32, WG>; // the first bound arc of each entry's row
559
+ var<workgroup> base: u32; // the block's reserved span in the edge queue
560
+ var<workgroup> wcount: u32; // the frontier's length on a two-phase level, 0 on any other
561
+
562
+ @compute @workgroup_size(WG)
563
+ fn advance_expand(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
564
+ if (lid.x == 0u) { wcount = select(0u, atomicLoad(&counters[0]), atomicLoad(&counters[24]) == 1u); } // frontierCount, on the two-phase path only (the path word)
565
+ let count = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
566
+ for (var b0 = group_id(wid) * WG; b0 < count; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries (a direct dispatch of the plan's groups)
567
+ let i = b0 + lid.x; // this lane's frontier entry
568
+ var deg = 0u;
569
+ var start = 0u;
570
+ if (i < count) { // guarded loads into locals (3.5 rule 1)
571
+ let v = frontierIn[i];
572
+ let lo = max(rowPtr[v], P.arcBase); // the row clipped to the bound window (P8-T12)
573
+ let hi = min(rowPtr[v + 1u], P.arcEnd);
574
+ start = lo;
575
+ deg = select(0u, hi - lo, hi > lo);
576
+ }
577
+ let inclusive = wg_scan_u32(deg, lid.x); // the prelude's inclusive scan in LANE order (P8-T1 Step 5); the twin
578
+ sh[lid.x] = inclusive; // sh is monotone in lid.x, the index the binary search walks
579
+ rowStart[lid.x] = start;
580
+ workgroupBarrier();
581
+ let aggregate = workgroupUniformLoad(&sh[WG - 1u]); // uniform; includes a barrier
582
+ if (lid.x == 0u) {
583
+ base = atomicAdd(&counters[8], aggregate); // edgeCount: ONE reservation per workgroup, not one per arc
584
+ atomicAdd(&counters[9], aggregate); // edgeCountUnclamped: the overflow detector (PD-23)
585
+ atomicAdd(&counters[2], aggregate); // frontierDegreeSum: Beamer's m_f (P8-T8)
586
+ }
587
+ workgroupBarrier();
588
+ for (var p = lid.x; p < aggregate; p = p + WG) { // strip [0, aggregate): lane j takes j, j + WG, ...
589
+ var lo = 0u; // upper_bound: the first k with sh[k] > p owns arc p
590
+ var hi = WG;
591
+ loop {
592
+ if (lo >= hi) { break; }
593
+ let mid = (lo + hi) / 2u;
594
+ if (sh[mid] > p) { hi = mid; } else { lo = mid + 1u; }
595
+ }
596
+ let k = lo;
597
+ var exclusive = 0u;
598
+ if (k > 0u) { exclusive = sh[k - 1u]; }
599
+ let arc = rowStart[k] + (p - exclusive);
600
+ let q = base + p;
601
+ if (q < P.edgeCapacity) { edgeQueue[q] = colIdx[arc - P.arcBase]; } // the clamp of PD-23; the queue holds the target vertex only (PD-24)
602
+ }
603
+ workgroupBarrier(); // sh, rowStart and base are reused by the next block
604
+ }
605
+ }
606
+ `
607
+ );
608
+ const bfRelaxWgsl = (
609
+ /* wgsl */
610
+ `
611
+ fn relax(v: u32, nd: f32) {
612
+ var cur = atomicLoad(&dist[v]);
613
+ var tries = 0u;
614
+ loop {
615
+ if (!(nd < bitcast<f32>(cur))) { break; } // no improvement; +Inf is greater than every finite nd
616
+ let r = atomicCompareExchangeWeak(&dist[v], cur, bitcast<u32>(nd));
617
+ if (r.exchanged) { atomicStore(&flags[0], 1u); break; } // changed
618
+ cur = r.old_value;
619
+ tries = tries + 1u;
620
+ if (tries >= P.maxRetries) { atomicStore(&flags[1], 1u); break; } // retryExhausted (PD-12)
621
+ }
622
+ }
623
+
624
+ @compute @workgroup_size(WG)
625
+ fn bf_relax(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
626
+ let first = linear_id(wid, lid.x);
627
+ let cutoff = bitcast<f32>(P.cutoffBits);
628
+ for (var e = first; e < P.edgeCount; e = e + P.stride) { // each logical edge once (edgeList)
629
+ let u = edgeSrc[e];
630
+ let v = edgeDst[e];
631
+ let w = weights[edgeToArc[e]]; // the edge's weight through its forward arc
632
+ let du = atomicLoad(&dist[u]);
633
+ if (du != F32_INF_BITS) { // unreached is tested as the bit pattern, as sssp-pred does
634
+ let nd = bitcast<f32>(du) + w;
635
+ if (nd <= cutoff) { relax(v, nd); }
636
+ }
637
+ if (UNDIRECTED) { // the other direction of an undirected edge
638
+ let dv = atomicLoad(&dist[v]);
639
+ if (dv != F32_INF_BITS) {
640
+ let nd = bitcast<f32>(dv) + w;
641
+ if (nd <= cutoff) { relax(u, nd); }
642
+ }
643
+ }
644
+ }
645
+ }
646
+ `
647
+ );
648
+ const bfsBitsetBuildWgsl = (
649
+ /* wgsl */
650
+ `
651
+ @compute @workgroup_size(WG)
652
+ fn bfs_bitset_build(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
653
+ let count = select(0u, atomicLoad(&counters[0]), atomicLoad(&counters[24]) == 3u); // frontierCount, on the bottom-up path only (the path word)
654
+ for (var i = linear_id(wid, lid.x); i < count; i = i + P.stride) { // grid-stride; no barrier anywhere
655
+ let v = frontierIn[i];
656
+ atomicOr(&bits[P.bitsBase + (v >> 5u)], 1u << (v & 31u));
657
+ }
658
+ }
659
+ `
660
+ );
661
+ const bfsBottomUpWgsl = (
662
+ /* wgsl */
663
+ `
664
+ var<workgroup> sh: array<u32, WG>;
665
+ var<workgroup> base: u32;
666
+ var<workgroup> wcount: u32; // the unvisited list's length on a bottom-up level, 0 on any other
667
+
668
+ @compute @workgroup_size(WG)
669
+ fn bfs_bottom_up(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
670
+ let claim = atomicLoad(&counters[11]) + 1u;
671
+ if (lid.x == 0u) { wcount = select(0u, atomicLoad(&counters[7]), atomicLoad(&counters[24]) == 3u); } // unvisitedListLen, on the bottom-up path only (the path word)
672
+ let len = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
673
+ for (var b0 = group_id(wid) * WG; b0 < len; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries
674
+ let i = b0 + lid.x;
675
+ var won = 0u;
676
+ var v = 0u;
677
+ var reads = 0u;
678
+ if (i < len) { // guarded work into locals
679
+ v = sweepIn[i];
680
+ if (atomicLoad(&depth[v]) == INVALID_INDEX) { // a stale entry, claimed since the rebuild, is skipped
681
+ let end = min(rowPtr[v + 1u], P.arcEnd);
682
+ for (var a = max(rowPtr[v], P.arcBase); a < end; a = a + 1u) { // in-neighbours through the reverse core
683
+ reads = reads + 1u;
684
+ let u = colIdx[a - P.arcBase];
685
+ if (mask_bit(sweepIn[P.bitsBase + (u >> 5u)], u)) { won = 1u; break; } // the early exit: a real break, never a flag
686
+ }
687
+ }
688
+ }
689
+ sh[lid.x] = won;
690
+ workgroupBarrier();
691
+ for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of won (bfs-contract's)
692
+ var t = 0u;
693
+ if (lid.x >= s) { t = sh[lid.x - s]; }
694
+ workgroupBarrier();
695
+ sh[lid.x] = sh[lid.x] + t;
696
+ workgroupBarrier();
697
+ }
698
+ let inclusive = sh[lid.x];
699
+ let readsTotal = wg_reduce_u32(reads, lid.x, 0u); // arcsScanned, one atomic per workgroup (the sabotage witness, Step 6)
700
+ if (lid.x == WG - 1u) { base = atomicAdd(&counters[1], inclusive); }
701
+ if (lid.x == 0u) { atomicAdd(&counters[16], readsTotal); }
702
+ workgroupBarrier();
703
+ if (won == 1u) {
704
+ atomicStore(&depth[v], claim); // no claim race: the list holds v once and the sweep is vertex-parallel
705
+ frontierOut[base + inclusive - 1u] = v;
706
+ }
707
+ workgroupBarrier(); // sh and base are reused by the next block
708
+ }
709
+ }
710
+ `
711
+ );
712
+ const bfsContractWgsl = (
713
+ /* wgsl */
714
+ `
715
+ var<workgroup> sh: array<u32, WG>;
716
+ var<workgroup> base: u32;
717
+ var<workgroup> wcount: u32; // the clamped edge count on a two-phase level, 0 on any other
718
+
719
+ @compute @workgroup_size(WG)
720
+ fn bfs_contract(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
721
+ if (lid.x == 0u) { wcount = select(0u, atomicLoad(&counters[8]), atomicLoad(&counters[24]) == 1u); } // edgeCount, clamped by role 1; the two-phase path only (the path word)
722
+ let count = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
723
+ let claim = atomicLoad(&counters[11]) + 1u; // the depth this level assigns
724
+ for (var b0 = group_id(wid) * WG; b0 < count; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries
725
+ let i = b0 + lid.x;
726
+ var won = 0u;
727
+ var v = 0u;
728
+ if (i < count) { // guarded work into locals
729
+ v = edgeQueue[i];
730
+ let old = atomicMin(&depth[v], claim);
731
+ won = select(0u, 1u, old == INVALID_INDEX); // PD-6: the unique winner
732
+ }
733
+ sh[lid.x] = won;
734
+ workgroupBarrier();
735
+ for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of won
736
+ var t = 0u;
737
+ if (lid.x >= s) { t = sh[lid.x - s]; }
738
+ workgroupBarrier();
739
+ sh[lid.x] = sh[lid.x] + t;
740
+ workgroupBarrier();
741
+ }
742
+ let inclusive = sh[lid.x];
743
+ if (lid.x == WG - 1u) { base = atomicAdd(&counters[1], inclusive); } // nextFrontierCount: one reservation per workgroup
744
+ workgroupBarrier();
745
+ if (won == 1u) { frontierOut[base + inclusive - 1u] = v; }
746
+ workgroupBarrier(); // sh and base are reused by the next block
747
+ }
748
+ }
749
+ `
750
+ );
751
+ const bfsFusedWgsl = (
752
+ /* wgsl */
753
+ `
754
+ var<workgroup> sh: array<u32, WG>;
755
+ var<workgroup> wdeg: u32;
756
+ var<workgroup> wstart: u32;
757
+ var<workgroup> base: u32;
758
+ var<workgroup> wcount: u32; // the frontier's length on a fused or retry level, 0 on any other
759
+
760
+ @compute @workgroup_size(WG)
761
+ fn bfs_fused(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
762
+ let claim = atomicLoad(&counters[11]) + 1u;
763
+ if (lid.x == 0u) {
764
+ let path = atomicLoad(&counters[24]); // 2 the fused level, 4 the overflow retry (PD-23)
765
+ wcount = select(0u, atomicLoad(&counters[0]), path == 2u || path == 4u);
766
+ }
767
+ let count = workgroupUniformLoad(&wcount); // uniform: the entry loop below holds barriers
768
+ for (var g = group_id(wid); g < count; g = g + P.stride) { // one workgroup per frontier entry; P.stride is the dispatch's GROUP count
769
+ if (lid.x == 0u) {
770
+ let u = frontierIn[g];
771
+ let a0 = max(rowPtr[u], P.arcBase);
772
+ let a1 = min(rowPtr[u + 1u], P.arcEnd);
773
+ let d = select(0u, a1 - a0, a1 > a0);
774
+ wdeg = d;
775
+ wstart = a0;
776
+ atomicAdd(&counters[2], d); // frontierDegreeSum, so Beamer's test (P8-T8) sees fused levels too
777
+ }
778
+ let deg = workgroupUniformLoad(&wdeg); // uniform: the loop below may hold barriers
779
+ let start = workgroupUniformLoad(&wstart);
780
+ for (var p0 = 0u; p0 < deg; p0 = p0 + WG) { // strip the row WG arcs at a time
781
+ let p = p0 + lid.x;
782
+ var won = 0u;
783
+ var v = 0u;
784
+ if (p < deg) { // guarded claim into locals
785
+ v = colIdx[start + p - P.arcBase];
786
+ let old = atomicMin(&depth[v], claim);
787
+ won = select(0u, 1u, old == INVALID_INDEX);
788
+ }
789
+ sh[lid.x] = won;
790
+ workgroupBarrier();
791
+ for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of won (bfs-contract's, verbatim)
792
+ var t = 0u;
793
+ if (lid.x >= s) { t = sh[lid.x - s]; }
794
+ workgroupBarrier();
795
+ sh[lid.x] = sh[lid.x] + t;
796
+ workgroupBarrier();
797
+ }
798
+ let inclusive = sh[lid.x];
799
+ if (lid.x == WG - 1u) { base = atomicAdd(&counters[1], inclusive); }
800
+ workgroupBarrier();
801
+ if (won == 1u) { frontierOut[base + inclusive - 1u] = v; }
802
+ workgroupBarrier(); // sh and base are reused by the next strip
803
+ }
804
+ }
805
+ }
806
+ `
807
+ );
808
+ const bfsUnvisitedFlagsWgsl = (
809
+ /* wgsl */
810
+ `
811
+ @compute @workgroup_size(WG)
812
+ fn bfs_unvisited_flags(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
813
+ let first = linear_id(wid, lid.x);
814
+ var cnt = 0u;
815
+ var degSum = 0u;
816
+ var len = 0u;
817
+ for (var v = first; v < P.n; v = v + P.stride) { // no barrier inside: the trip count is per lane
818
+ let unv = depth[v] == INVALID_INDEX;
819
+ let listed = unv && (inDegree[v] != 0u);
820
+ flags[v] = select(0u, 1u, listed);
821
+ cnt = cnt + select(0u, 1u, unv);
822
+ degSum = degSum + select(0u, outDegree[v], unv); // the OUT-degree: Beamer's m_u counts the edges top-down would examine
823
+ len = len + select(0u, 1u, listed);
824
+ }
825
+ let c = wg_reduce_u32(cnt, lid.x, 0u); // the prelude's workgroup sum (combine_u's sum code); uniform: after the loop
826
+ let d = wg_reduce_u32(degSum, lid.x, 0u);
827
+ let l = wg_reduce_u32(len, lid.x, 0u);
828
+ if (lid.x == 0u) { // ONE atomic per word per workgroup
829
+ atomicAdd(&counters[5], c);
830
+ atomicAdd(&counters[6], d);
831
+ atomicAdd(&counters[7], l);
832
+ }
833
+ }
834
+ `
835
+ );
836
+ const closenessReduceWgsl = (
837
+ /* wgsl */
838
+ `
839
+ @compute @workgroup_size(WG)
840
+ fn closeness_reduce(@builtin(local_invocation_id) lid: vec3<u32>) {
841
+ if (lid.x != 0u) { return; } // one lane; no barrier follows (3.5 rule 1)
842
+ if (P.role == 1u) { // the seed of a batch: P.source is its first source
843
+ let k = min(32u, P.n - P.source);
844
+ for (var s = 0u; s < k; s = s + 1u) {
845
+ let v = P.source + s;
846
+ let bit = 1u << s;
847
+ bits[v] = bit; // visited
848
+ bits[P.bitsBase + v] = bit; // the frontier level 0 reads (region 1: level 0's parity is 0)
849
+ bits[3u * P.bitsBase + v] = 1u; // flags: level 0's compact turns them into the list
850
+ }
851
+ atomicStore(&counters[0], k); // not done
852
+ atomicStore(&counters[11], U32_MAX); // level: the first boundary brings it to 0
853
+ atomicStore(&counters[15], 0u); // done
854
+ return;
855
+ }
856
+ // role 0: the level boundary -- done from the previous level's compacted count, then the accumulation
857
+ let count = atomicLoad(&counters[0]);
858
+ atomicStore(&counters[15], select(0u, 1u, count == 0u));
859
+ let level = atomicLoad(&counters[11]);
860
+ let d = level + 1u; // the distance of the claims the level just run made
861
+ for (var s = 0u; s < 32u; s = s + 1u) {
862
+ let c = atomicLoad(&perSource[s]); // newCount[s]
863
+ atomicStore(&perSource[32u + s], atomicLoad(&perSource[32u + s]) + c); // reached[s]
864
+ // sum[s] += c x d in 64 bits: the 16-bit split product (pLo, pHi), then the add with its carry
865
+ let cLo = c & 0xFFFFu;
866
+ let cHi = c >> 16u;
867
+ let dLo = d & 0xFFFFu;
868
+ let dHi = d >> 16u;
869
+ let ll = cLo * dLo;
870
+ let lh = cLo * dHi;
871
+ let hl = cHi * dLo;
872
+ let mid = (ll >> 16u) + (lh & 0xFFFFu) + (hl & 0xFFFFu);
873
+ let pLo = (ll & 0xFFFFu) | ((mid & 0xFFFFu) << 16u);
874
+ let pHi = (cHi * dHi) + (lh >> 16u) + (hl >> 16u) + (mid >> 16u);
875
+ var lo = atomicLoad(&perSource[64u + s]); // sumLo[s]
876
+ var hi = atomicLoad(&perSource[96u + s]); // sumHi[s]
877
+ let before = lo;
878
+ lo = lo + pLo;
879
+ hi = hi + pHi + select(0u, 1u, lo < before);
880
+ atomicStore(&perSource[64u + s], lo);
881
+ atomicStore(&perSource[96u + s], hi);
882
+ atomicStore(&perSource[s], 0u);
883
+ }
884
+ atomicStore(&counters[11], level + 1u);
885
+ }
886
+ `
887
+ );
888
+ const closenessSweepWgsl = (
889
+ /* wgsl */
890
+ `
891
+ var<workgroup> sh: array<u32, WG>; // the block's degrees, then their inclusive scan
892
+ var<workgroup> rowStart: array<u32, WG>; // the first bound arc of each entry's row
893
+ var<workgroup> rowOf: array<u32, WG>; // the frontier vertex of each entry (the source end of its arcs)
894
+ var<workgroup> local: array<atomic<u32>, 32>; // this workgroup's fresh claims per source
895
+ var<workgroup> wcount: u32; // the frontier list's length
896
+
897
+ @compute @workgroup_size(WG)
898
+ fn closeness_sweep(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
899
+ if (lid.x == 0u) { wcount = atomicLoad(&counters[0]); } // the frontier list's length (compact's total)
900
+ let count = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
901
+ let nextBase = select(2u * P.bitsBase, P.bitsBase, P.mode == 1u); // the region that is next this level
902
+ let frontierBase = 3u * P.bitsBase - nextBase; // the other one: the region that is the frontier
903
+ for (var b0 = group_id(wid) * WG; b0 < count; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries
904
+ let i = b0 + lid.x; // this lane's frontier entry
905
+ var deg = 0u;
906
+ var start = 0u;
907
+ var u = 0u;
908
+ if (i < count) { // guarded loads into locals (3.5 rule 1)
909
+ u = frontierList[i];
910
+ let lo = max(rowPtr[u], P.arcBase);
911
+ let hi = min(rowPtr[u + 1u], P.arcEnd);
912
+ start = lo;
913
+ deg = select(0u, hi - lo, hi > lo);
914
+ }
915
+ if (lid.x < 32u) { atomicStore(&local[lid.x], 0u); } // zeroed before the strip loop (WebGPU zero-initialises workgroup memory; said anyway)
916
+ sh[lid.x] = deg;
917
+ rowStart[lid.x] = start;
918
+ rowOf[lid.x] = u;
919
+ workgroupBarrier();
920
+ for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of the degrees (bfs-contract's, inlined)
921
+ var t = 0u;
922
+ if (lid.x >= s) { t = sh[lid.x - s]; }
923
+ workgroupBarrier();
924
+ sh[lid.x] = sh[lid.x] + t;
925
+ workgroupBarrier();
926
+ }
927
+ let aggregate = workgroupUniformLoad(&sh[WG - 1u]); // uniform; includes a barrier
928
+ for (var p = lid.x; p < aggregate; p = p + WG) { // strip [0, aggregate): lane j takes j, j + WG, ...
929
+ var lo = 0u; // upper_bound: the first k with sh[k] > p owns arc p
930
+ var hi = WG;
931
+ loop {
932
+ if (lo >= hi) { break; }
933
+ let mid = (lo + hi) / 2u;
934
+ if (sh[mid] > p) { hi = mid; } else { lo = mid + 1u; }
935
+ }
936
+ let k = lo;
937
+ var exclusive = 0u;
938
+ if (k > 0u) { exclusive = sh[k - 1u]; }
939
+ let arc = rowStart[k] + (p - exclusive);
940
+ let x = colIdx[arc - P.arcBase];
941
+ let mask = atomicLoad(&bits[frontierBase + rowOf[k]]) & ~atomicLoad(&bits[x]); // the sources at u that have not reached x
942
+ if (mask != 0u) {
943
+ let old = atomicOr(&bits[x], mask); // visited: the claim, one read-modify-write
944
+ let fresh = mask & ~old; // the sources whose claim this lane won
945
+ if (fresh != 0u) {
946
+ atomicOr(&bits[nextBase + x], fresh);
947
+ atomicStore(&bits[3u * P.bitsBase + x], 1u); // flags: x is in the next frontier list (compact reads it)
948
+ var b = fresh;
949
+ loop { // one tally per set bit of fresh
950
+ if (b == 0u) { break; }
951
+ let s = firstTrailingBit(b);
952
+ atomicAdd(&local[s], 1u);
953
+ b = b & (b - 1u);
954
+ }
955
+ }
956
+ }
957
+ }
958
+ workgroupBarrier(); // uniform: the loop's bound is the uniform aggregate
959
+ if (lid.x < 32u) { // ONE global atomic per source per workgroup
960
+ let c = atomicLoad(&local[lid.x]);
961
+ if (c != 0u) { atomicAdd(&perSource[lid.x], c); } // newCount[s]
962
+ }
963
+ workgroupBarrier(); // sh, rowStart, rowOf and local are reused by the next block
964
+ }
965
+ }
966
+ `
967
+ );
968
+ const compactScatterWgsl = (
969
+ /* wgsl */
970
+ `
971
+ @compute @workgroup_size(WG)
972
+ fn compact_scatter(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
973
+ let i = linear_id(wid, lid.x);
974
+ if (i == 0u) { outCount[P.outIndex] = offsets[P.count - 1u] + flags[P.count - 1u]; } // the exclusive scan's total; the planner never dispatches for count 0
975
+ if (i >= P.count) { return; } // no barrier follows
976
+ if (flags[i] != 0u) { out[offsets[i]] = queue[i]; }
977
+ }
978
+ `
979
+ );
547
980
  const countingScatterWgsl = (
548
981
  /* wgsl */
549
982
  `
@@ -557,6 +990,57 @@ fn counting_scatter(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invoca
557
990
  }
558
991
  `
559
992
  );
993
+ const dedupeClaimWgsl = (
994
+ /* wgsl */
995
+ `
996
+ @compute @workgroup_size(WG)
997
+ fn dedupe_claim(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
998
+ var count = P.count;
999
+ if (P.countIndex != U32_MAX) { count = min(atomicLoad(&counters[P.countIndex]), P.count); } // a device-side count, clamped to the capacity
1000
+ for (var i = linear_id(wid, lid.x); i < count; i = i + P.stride) { // grid-stride; no barrier anywhere
1001
+ atomicStore(&owner[queue[i]], i);
1002
+ }
1003
+ }
1004
+ `
1005
+ );
1006
+ const dedupeFilterWgsl = (
1007
+ /* wgsl */
1008
+ `
1009
+ var<workgroup> sh: array<u32, WG>;
1010
+ var<workgroup> base: u32;
1011
+ var<workgroup> wcount: u32;
1012
+
1013
+ @compute @workgroup_size(WG)
1014
+ fn dedupe_filter(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
1015
+ if (lid.x == 0u) {
1016
+ var count = P.count;
1017
+ if (P.countIndex != U32_MAX) { count = min(atomicLoad(&outCount[P.countIndex]), P.count); } // the same count source as the claim
1018
+ wcount = count;
1019
+ }
1020
+ let count = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
1021
+ for (var b0 = group_id(wid) * WG; b0 < count; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries
1022
+ let i = b0 + lid.x;
1023
+ var keep = 0u;
1024
+ var v = 0u;
1025
+ if (i < count) { v = queue[i]; keep = select(0u, 1u, atomicLoad(&owner[v]) == i); } // guarded work into locals
1026
+ sh[lid.x] = keep;
1027
+ workgroupBarrier(); // every lane, unconditionally (3.5 rule 1)
1028
+ for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of keep
1029
+ var t = 0u;
1030
+ if (lid.x >= s) { t = sh[lid.x - s]; }
1031
+ workgroupBarrier();
1032
+ sh[lid.x] = sh[lid.x] + t;
1033
+ workgroupBarrier();
1034
+ }
1035
+ let inclusive = sh[lid.x];
1036
+ if (lid.x == WG - 1u) { base = atomicAdd(&outCount[P.outIndex], inclusive); } // ONE atomic per workgroup: the block's aggregate
1037
+ workgroupBarrier();
1038
+ if (keep == 1u) { out[base + inclusive - 1u] = v; }
1039
+ workgroupBarrier(); // sh and base are reused by the next block
1040
+ }
1041
+ }
1042
+ `
1043
+ );
560
1044
  const degreeWgsl = (
561
1045
  /* wgsl */
562
1046
  `
@@ -988,6 +1472,161 @@ fn fill(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid
988
1472
  }
989
1473
  `
990
1474
  );
1475
+ const frontierFinalizeWgsl = (
1476
+ /* wgsl */
1477
+ `
1478
+ fn write_slot_groups(slot: u32, groups: u32, count: u32) { // groups workgroups, split in 2D above the per-dim limit
1479
+ var x = groups;
1480
+ var y = 1u;
1481
+ if (groups > MAX_WORKGROUPS_PER_DIM) {
1482
+ x = MAX_WORKGROUPS_PER_DIM;
1483
+ y = (groups + MAX_WORKGROUPS_PER_DIM - 1u) / MAX_WORKGROUPS_PER_DIM;
1484
+ }
1485
+ let base = 4u * (P.slotBase + slot); // 16-byte slots: (x, y, 1, count)
1486
+ args[base] = x; args[base + 1u] = y; args[base + 2u] = 1u; args[base + 3u] = count;
1487
+ }
1488
+ fn write_slot(slot: u32, count: u32) { // one INVOCATION per entry: ceil(count / wg) workgroups
1489
+ let groups = count / P.wg + select(0u, 1u, count % P.wg != 0u); // ceil(count / wg) without the u32 wrap (indirect-finalize's rule)
1490
+ write_slot_groups(slot, groups, count);
1491
+ }
1492
+ fn zero_slot(slot: u32) {
1493
+ let base = 4u * (P.slotBase + slot);
1494
+ args[base] = 0u; args[base + 1u] = 0u; args[base + 2u] = 1u; args[base + 3u] = 0u;
1495
+ }
1496
+
1497
+ @compute @workgroup_size(WG)
1498
+ fn frontier_finalize(@builtin(local_invocation_id) lid: vec3<u32>) {
1499
+ if (lid.x != 0u) { return; } // one lane; no barrier follows (3.5 rule 1)
1500
+ if (P.role == 0u) { // the level boundary
1501
+ if (atomicLoad(&counters[15]) != 0u) { // done already: a no-op level the host recorded past the end
1502
+ for (var s = 0u; s < 7u; s = s + 1u) { zero_slot(s); } // this slotBase holds the previous submit's args
1503
+ return; // no counter word moves (P8-T6's levels formula reads them)
1504
+ }
1505
+ let finished = atomicLoad(&counters[0]);
1506
+ let next = atomicLoad(&counters[1]);
1507
+ let degSum = atomicLoad(&counters[2]);
1508
+ atomicStore(&counters[3], finished); // prevFrontierCount
1509
+ atomicStore(&counters[4], degSum); // prevDegreeSum
1510
+ atomicStore(&counters[0], next); // the rotation
1511
+ atomicStore(&counters[1], 0u);
1512
+ atomicStore(&counters[2], 0u);
1513
+ atomicStore(&counters[8], 0u); // edgeCount
1514
+ atomicStore(&counters[9], 0u); // edgeCountUnclamped
1515
+ atomicStore(&counters[12], atomicLoad(&counters[12]) + next); // visitedCount
1516
+ if (P.firstOfSubmit >= 1u) { // b = the boundary's index inside its submit, clamped to 2 (P8-T8, PD-18)
1517
+ atomicStore(&counters[5], atomicLoad(&counters[5]) - next); // unvisitedCount (exact): F_b was inside the submit's rebuilt sum iff b >= 1
1518
+ }
1519
+ if (P.firstOfSubmit >= 2u) {
1520
+ atomicStore(&counters[6], atomicLoad(&counters[6]) - degSum); // unvisitedDegreeSum (one level stale): F_{b-1} was inside it iff b >= 2
1521
+ }
1522
+ let level = atomicLoad(&counters[11]) + 1u; // the seed is U32_MAX, so the first boundary lands on 0
1523
+ atomicStore(&counters[11], level);
1524
+ let done = (next == 0u) || (level >= P.maxDepth);
1525
+ atomicStore(&counters[15], select(0u, 1u, done));
1526
+ var direction = atomicLoad(&counters[14]);
1527
+ if (P.mode == 1u) {
1528
+ direction = 0u; // top-down only (the test seam)
1529
+ } else if (direction == 0u) {
1530
+ if (degSum > atomicLoad(&counters[6]) / P.alpha && next > finished) { direction = 1u; } // m_f > m_u / alpha and growing
1531
+ } else {
1532
+ if (next * P.beta < atomicLoad(&counters[5]) && next < finished) { direction = 0u; } // next * beta < unvisited and shrinking
1533
+ }
1534
+ if (direction != atomicLoad(&counters[14])) { atomicStore(&counters[13], atomicLoad(&counters[13]) + 1u); } // switches
1535
+ var path = 0u; // word 24: what the level's kernels run (0 nothing, 1 two-phase, 2 fused, 3 bottom-up; role 1 writes 4 for the retry)
1536
+ if (done) {
1537
+ for (var s = 0u; s < 7u; s = s + 1u) { zero_slot(s); }
1538
+ } else if (direction == 1u) { // the bottom-up level (P8-T8): the bits fill, the bitset build, the sweep
1539
+ zero_slot(0u); zero_slot(1u); zero_slot(2u); zero_slot(6u);
1540
+ write_slot(3u, (P.n + 31u) / 32u); write_slot(4u, next); write_slot(5u, atomicLoad(&counters[7]));
1541
+ path = 3u;
1542
+ atomicStore(&counters[19], atomicLoad(&counters[19]) + 1u);
1543
+ } else if (next < P.fusedMax) { // P8-T7 makes this branch reachable (fusedMax is 0 until then)
1544
+ zero_slot(0u); zero_slot(1u); zero_slot(3u); zero_slot(4u); zero_slot(5u); zero_slot(6u);
1545
+ write_slot_groups(2u, next, next); // bfs-fused is one WORKGROUP per frontier entry
1546
+ path = 2u;
1547
+ atomicStore(&counters[17], atomicLoad(&counters[17]) + 1u);
1548
+ } else {
1549
+ zero_slot(2u); zero_slot(3u); zero_slot(4u); zero_slot(5u);
1550
+ write_slot(0u, next); // slots 1 and 6 are role 1's
1551
+ path = 1u;
1552
+ }
1553
+ atomicStore(&counters[14], direction);
1554
+ atomicStore(&counters[24], path);
1555
+ } else if (P.role == 1u) { // the edge queue is filled
1556
+ if (args[4u * P.slotBase] == 0u) { // role 0 did not choose the two-phase path (done, fused or bottom-up): nothing to size, nothing to count
1557
+ zero_slot(1u); zero_slot(6u);
1558
+ return;
1559
+ }
1560
+ let clamped = min(atomicLoad(&counters[8]), P.edgeCapacity);
1561
+ atomicStore(&counters[8], clamped);
1562
+ if (atomicLoad(&counters[9]) > P.edgeCapacity) { // PD-23: the fused retry
1563
+ let entries = atomicLoad(&counters[0]);
1564
+ zero_slot(1u); write_slot_groups(6u, entries, entries); // one workgroup per frontier entry, as slot 2
1565
+ atomicStore(&counters[24], 4u); // the path word: bfs-fused runs the retry, bfs-contract nothing
1566
+ atomicStore(&counters[10], atomicLoad(&counters[10]) + 1u);
1567
+ atomicStore(&counters[17], atomicLoad(&counters[17]) + 1u);
1568
+ } else {
1569
+ write_slot(1u, clamped); zero_slot(6u);
1570
+ atomicStore(&counters[18], atomicLoad(&counters[18]) + 1u); // twoPhaseLevels counts the CHOICE role 0 made, even for zero edges (P8-T7 Step 4's invariant)
1571
+ }
1572
+ } else if (P.role == 2u) { // the SSSP round boundary (P8-T9, PD-20): which pile this round relaxes
1573
+ atomicStore(&counters[8], 0u); // the dedupe counts (words 8 and 9, the SSSP sense) and the path word: nothing unless a pile is chosen below
1574
+ atomicStore(&counters[9], 0u);
1575
+ atomicStore(&counters[24], 0u);
1576
+ if (atomicLoad(&counters[15]) != 0u) { // done already: a no-op round the host recorded past the end (rule 1)
1577
+ for (var s = 0u; s < 7u; s = s + 1u) { zero_slot(s); }
1578
+ return;
1579
+ }
1580
+ let nearRaw = atomicLoad(&counters[1]); // the raw near half's appends, unclamped
1581
+ let farRaw = atomicLoad(&counters[21]); // the raw far half's appends, unclamped
1582
+ if (nearRaw > P.edgeCapacity || farRaw > P.edgeCapacity) { // a pile overflowed its half: the host raises E_TOO_LARGE
1583
+ atomicStore(&counters[15], 2u);
1584
+ for (var s = 0u; s < 7u; s = s + 1u) { zero_slot(s); }
1585
+ return;
1586
+ }
1587
+ zero_slot(2u); zero_slot(5u); zero_slot(6u); // role 3 sizes the relax slots once the piles are deduped
1588
+ if (nearRaw != 0u) { // a near round: dedupe the near half into nearIn
1589
+ atomicStore(&counters[0], 0u); // the deduped near count, accumulated by dedupe-filter
1590
+ atomicStore(&counters[14], 0u); // mode 0
1591
+ write_slot(0u, nearRaw); write_slot(1u, nearRaw); // dedupe-claim, dedupe-filter over the near half
1592
+ atomicStore(&counters[8], nearRaw); // the near dedupe's count word
1593
+ atomicStore(&counters[24], 5u); // the path word: sssp-relax role 0 runs, role 1 nothing
1594
+ zero_slot(3u); zero_slot(4u);
1595
+ atomicStore(&counters[11], atomicLoad(&counters[11]) + 1u); // rounds dispatched (the done boundary is not counted)
1596
+ } else if (farRaw != 0u) { // the near pile is empty: raise the threshold and re-bucket the far pile
1597
+ let threshold = bitcast<f32>(atomicLoad(&counters[22]));
1598
+ let raised = threshold + bitcast<f32>(atomicLoad(&counters[23])); // ONE f32 add on the bit patterns (PD-9)
1599
+ if (raised == threshold) { // the delta is below the threshold's ulp: the host raises E_UNSUPPORTED
1600
+ atomicStore(&counters[15], 3u);
1601
+ for (var s = 0u; s < 7u; s = s + 1u) { zero_slot(s); }
1602
+ return;
1603
+ }
1604
+ atomicStore(&counters[4], atomicLoad(&counters[22])); // prevThresholdBits: what the pass-through drops below
1605
+ atomicStore(&counters[22], bitcast<u32>(raised));
1606
+ atomicStore(&counters[20], 0u); // the deduped far count, accumulated by dedupe-filter
1607
+ atomicStore(&counters[14], 1u); // mode 1
1608
+ zero_slot(0u); zero_slot(1u);
1609
+ write_slot(3u, farRaw); write_slot(4u, farRaw); // dedupe-claim, dedupe-filter over the far half
1610
+ atomicStore(&counters[9], farRaw); // the far dedupe's count word
1611
+ atomicStore(&counters[24], 6u); // the path word: sssp-relax role 1 runs, role 0 nothing
1612
+ atomicStore(&counters[11], atomicLoad(&counters[11]) + 1u);
1613
+ } else { // both piles empty: finished
1614
+ atomicStore(&counters[15], 1u);
1615
+ for (var s = 0u; s < 7u; s = s + 1u) { zero_slot(s); }
1616
+ }
1617
+ } else if (P.role == 3u) { // the piles are deduped: size the relax
1618
+ if (atomicLoad(&counters[15]) != 0u) { return; } // role 2 zeroed every slot of the round
1619
+ if (atomicLoad(&counters[14]) == 0u) {
1620
+ write_slot(2u, atomicLoad(&counters[0])); // the near round over nearIn
1621
+ atomicStore(&counters[1], 0u); // the raw near half restarts
1622
+ } else {
1623
+ write_slot(5u, atomicLoad(&counters[20])); // the pass-through over farIn
1624
+ atomicStore(&counters[21], 0u); // the raw far half restarts (the pass-through re-appends what stays far)
1625
+ }
1626
+ }
1627
+ }
1628
+ `
1629
+ );
991
1630
  const gridCellKeyWgsl = (
992
1631
  /* wgsl */
993
1632
  `
@@ -1729,6 +2368,108 @@ fn spmv_pull(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id
1729
2368
  }
1730
2369
  `
1731
2370
  );
2371
+ const ssspPredWgsl = (
2372
+ /* wgsl */
2373
+ `
2374
+ @compute @workgroup_size(WG)
2375
+ fn sssp_pred(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
2376
+ let first = linear_id(wid, lid.x);
2377
+ let hb = ((P.n + 63u) / 64u) * 64u; // MODE 0 only: hops[v] is pred[hb + v]; pred[2 hb] is the changed word, pred[2 hb + 1] the orphan word (PD-27)
2378
+ if (P.role == 1u && P.iteration != 0u && atomicLoad(&pred[2u * hb]) < P.iteration) { return; } // the previous hop pass changed nothing: converged (no barrier anywhere, so the early return is legal)
2379
+ for (var u = first; u < P.n; u = u + P.stride) { // grid-stride over the rows (planGridStride(n)); no barrier anywhere
2380
+ let du = dist[u];
2381
+ var unreached = false;
2382
+ if (MODE == 1u) { unreached = du == INVALID_INDEX; } else { unreached = du == F32_INF_BITS; }
2383
+ if (unreached) { continue; }
2384
+ var hu = 0u; // u's hop count (MODE 0, roles 1 and 2)
2385
+ if (MODE == 0u && P.role != 0u) {
2386
+ hu = atomicLoad(&pred[hb + u]);
2387
+ if (hu == INVALID_INDEX) { // the key has not reached u yet
2388
+ if (P.role == 2u && u != P.source) { atomicAdd(&pred[2u * hb + 1u], 1u); } // an orphan: only bellmanFord can make one (P8-T10 Step 3)
2389
+ continue;
2390
+ }
2391
+ }
2392
+ let end = min(rowPtr[u + 1u], P.arcEnd);
2393
+ for (var a = max(rowPtr[u], P.arcBase); a < end; a = a + 1u) {
2394
+ let v = colIdx[a - P.arcBase];
2395
+ if (v == P.source) { continue; } // the source keeps INVALID_INDEX whatever attains it (a zero-weight arc could)
2396
+ let dv = dist[v];
2397
+ var tight = false; // the arc explains dist[v]
2398
+ var below = false; // and its source sits at a strictly smaller distance
2399
+ if (MODE == 1u) {
2400
+ tight = (du + 1u) == dv; // depth mode: BFS parent, one depth down
2401
+ } else {
2402
+ let w = select(1.0, weights[a - P.arcBase], HAS_WEIGHTS);
2403
+ tight = (dv != F32_INF_BITS) && (bitcast<u32>(bitcast<f32>(du) + w) == dv); // one f32 add, compared as the bit pattern PD-9 stores; never into an unreached v (an overflowed sum is +Inf too)
2404
+ below = bitcast<f32>(du) < bitcast<f32>(dv);
2405
+ }
2406
+ if (!tight) { continue; }
2407
+ var admit = false; // this arc is one key step below v
2408
+ if (MODE == 1u) {
2409
+ admit = true;
2410
+ } else if (P.role == 0u) { // the roots pass (the plateau rule only; bellmanFord seeds the source alone)
2411
+ if (P.mode == 0u && below) { atomicMin(&pred[hb + v], 0u); }
2412
+ } else if (P.role == 1u) { // a hop pass: one hop along a plateau arc (mode 0) or along any tight arc (mode 1)
2413
+ if (P.mode == 1u || du == dv) {
2414
+ let old = atomicMin(&pred[hb + v], hu + 1u);
2415
+ if (hu + 1u < old) { atomicMax(&pred[2u * hb], P.iteration + 1u); } // this pass changed something
2416
+ }
2417
+ } else { // the predecessor pass: the smallest tight arc one key step below v
2418
+ let hv = atomicLoad(&pred[hb + v]);
2419
+ if (P.mode == 1u) { admit = hu + 1u == hv; } else if (hv == 0u) { admit = below; } else { admit = (du == dv) && (hu + 1u == hv); }
2420
+ }
2421
+ if (admit) { atomicMin(&pred[v], select(a, u, P.predKind == 1u)); }
2422
+ }
2423
+ }
2424
+ }
2425
+ `
2426
+ );
2427
+ const ssspRelaxWgsl = (
2428
+ /* wgsl */
2429
+ `
2430
+ @compute @workgroup_size(WG)
2431
+ fn sssp_relax(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
2432
+ let first = linear_id(wid, lid.x);
2433
+ let mine = select(5u, 6u, P.role == 1u); // the path word role 2 wrote: 5 a near round, 6 a far pass-through
2434
+ let chosen = atomicLoad(&counters[24]) == mine; // the other role's dispatch of the round is a no-op
2435
+ let count = select(0u, min(atomicLoad(&counters[select(0u, 20u, P.role == 1u)]), P.n), chosen); // the deduped near or far pile
2436
+ let threshold = bitcast<f32>(atomicLoad(&counters[22]));
2437
+ let cutoff = bitcast<f32>(P.cutoffBits);
2438
+ for (var i = first; i < count; i = i + P.stride) { // no barrier anywhere: the loops may be per lane
2439
+ let u = queueIn[i];
2440
+ let du = bitcast<f32>(atomicLoad(&dist[u]));
2441
+ if (P.role == 1u) { // the pass-through (PD-20): re-bucket a far entry
2442
+ if (du < bitcast<f32>(atomicLoad(&counters[4]))) { continue; } // below the previous threshold: relaxed in an earlier bucket
2443
+ if (du < threshold) {
2444
+ let q = atomicAdd(&counters[1], 1u);
2445
+ if (q < P.edgeCapacity) { queueOut[q] = u; }
2446
+ } else {
2447
+ let q = atomicAdd(&counters[21], 1u);
2448
+ if (q < P.edgeCapacity) { queueOut[P.edgeCapacity + q] = u; }
2449
+ }
2450
+ continue;
2451
+ }
2452
+ let end = rowPtr[u + 1u];
2453
+ for (var a = rowPtr[u]; a < end; a = a + 1u) { // the whole row: never windowed (DEP-P8-E)
2454
+ let v = colIdx[a];
2455
+ let nd = du + select(1.0, weights[a], HAS_WEIGHTS); // ONE f32 add (PD-9)
2456
+ if (nd > cutoff) { continue; } // SsspOptions.cutoff: the CPU port's dv <= cutoff
2457
+ let bits = bitcast<u32>(nd);
2458
+ let old = atomicMin(&dist[v], bits); // exact on non-negative floats
2459
+ if (bits < old) { // this lane improved v, so it owns the append
2460
+ if (nd < threshold) {
2461
+ let q = atomicAdd(&counters[1], 1u);
2462
+ if (q < P.edgeCapacity) { queueOut[q] = v; }
2463
+ } else {
2464
+ let q = atomicAdd(&counters[21], 1u);
2465
+ if (q < P.edgeCapacity) { queueOut[P.edgeCapacity + q] = v; }
2466
+ }
2467
+ }
2468
+ }
2469
+ }
2470
+ }
2471
+ `
2472
+ );
1732
2473
  const wccCompressWgsl = (
1733
2474
  /* wgsl */
1734
2475
  `
@@ -2029,6 +2770,81 @@ const RADIX_PARAMS = UniformBlock.define("RadixParams", [
2029
2770
  ["groups", "u32"],
2030
2771
  ["pad0", "u32"]
2031
2772
  ]);
2773
+ const COMPACT_PARAMS = UniformBlock.define("CompactParams", [
2774
+ ["count", "u32"],
2775
+ ["outIndex", "u32"],
2776
+ ["countIndex", "u32"],
2777
+ ["stride", "u32"]
2778
+ ]);
2779
+ const FRONTIER_COUNTERS = UniformBlock.define(
2780
+ "FrontierCounters",
2781
+ [
2782
+ ["frontierCount", "u32"],
2783
+ ["nextFrontierCount", "u32"],
2784
+ ["frontierDegreeSum", "u32"],
2785
+ ["prevFrontierCount", "u32"],
2786
+ ["prevDegreeSum", "u32"],
2787
+ ["unvisitedCount", "u32"],
2788
+ ["unvisitedDegreeSum", "u32"],
2789
+ ["unvisitedListLen", "u32"],
2790
+ ["edgeCount", "u32"],
2791
+ ["edgeCountUnclamped", "u32"],
2792
+ ["overflowLevels", "u32"],
2793
+ ["level", "u32"],
2794
+ ["visitedCount", "u32"],
2795
+ ["switches", "u32"],
2796
+ ["direction", "u32"],
2797
+ ["done", "u32"],
2798
+ ["arcsScanned", "u32"],
2799
+ ["fusedLevels", "u32"],
2800
+ ["twoPhaseLevels", "u32"],
2801
+ ["bottomUpLevels", "u32"],
2802
+ ["farCount", "u32"],
2803
+ ["nextFarCount", "u32"],
2804
+ ["thresholdBits", "u32"],
2805
+ ["deltaBits", "u32"],
2806
+ ["path", "u32"]
2807
+ ],
2808
+ { layout: "storage" }
2809
+ );
2810
+ const FRONTIER_PARAMS = UniformBlock.define("FrontierParams", [
2811
+ ["role", "u32"],
2812
+ ["slotBase", "u32"],
2813
+ ["wg", "u32"],
2814
+ ["alpha", "u32"],
2815
+ ["beta", "u32"],
2816
+ ["fusedMax", "u32"],
2817
+ ["edgeCapacity", "u32"],
2818
+ ["maxDepth", "u32"],
2819
+ ["n", "u32"],
2820
+ ["mode", "u32"],
2821
+ ["cutoffBits", "u32"],
2822
+ ["arcBase", "u32"],
2823
+ ["arcEnd", "u32"],
2824
+ ["predKind", "u32"],
2825
+ ["bitsBase", "u32"],
2826
+ ["source", "u32"],
2827
+ ["stride", "u32"],
2828
+ ["firstOfSubmit", "u32"],
2829
+ ["iteration", "u32"],
2830
+ ["pad1", "u32"]
2831
+ ]);
2832
+ const BF_PARAMS = UniformBlock.define("BfParams", [
2833
+ ["edgeCount", "u32"],
2834
+ ["stride", "u32"],
2835
+ ["maxRetries", "u32"],
2836
+ ["cutoffBits", "u32"]
2837
+ ]);
2838
+ const BF_FLAGS = UniformBlock.define(
2839
+ "BfFlags",
2840
+ [
2841
+ ["changed", "u32"],
2842
+ ["retryExhausted", "u32"],
2843
+ ["pad0", "u32"],
2844
+ ["pad1", "u32"]
2845
+ ],
2846
+ { layout: "storage" }
2847
+ );
2032
2848
  function decl(group, binding, name, kind, wgslType) {
2033
2849
  return { group, binding, name, kind, wgslType };
2034
2850
  }
@@ -2535,26 +3351,277 @@ const GRID_NEAR_FIELD = {
2535
3351
  snippetSlots: [],
2536
3352
  phase: "P4"
2537
3353
  };
2538
- const REGISTRY = Object.freeze({
2539
- degree: DEGREE,
2540
- reduce: REDUCE,
2541
- fill: FILL,
2542
- "segmented-reduce": SEGMENTED_REDUCE,
2543
- "fa2-stats-finalize": FA2_STATS_FINALIZE,
2544
- "fa2-attraction": FA2_ATTRACTION,
2545
- "fa2-repulsion-exact": FA2_REPULSION_EXACT,
2546
- "fa2-speed-finalize": FA2_SPEED_FINALIZE,
2547
- "fa2-integrate": FA2_INTEGRATE,
2548
- "fa2-to-scene": FA2_TO_SCENE,
2549
- "spmv-pull": SPMV_PULL,
2550
- "pr-scale": PR_SCALE,
2551
- "pr-finalize": PR_FINALIZE,
2552
- "wcc-link-sample": WCC_LINK_SAMPLE,
2553
- "wcc-link-edges": WCC_LINK_EDGES,
2554
- "wcc-compress": WCC_COMPRESS,
2555
- "wcc-sample": WCC_SAMPLE,
2556
- "indirect-finalize": INDIRECT_FINALIZE,
2557
- "scan-block": SCAN_BLOCK,
3354
+ const COMPACT_SCATTER = {
3355
+ id: "compact-scatter",
3356
+ body: compactScatterWgsl,
3357
+ entryPoint: "compact_scatter",
3358
+ bindings: [
3359
+ decl(1, 0, "queue", "storage-ro", "array<u32>"),
3360
+ decl(1, 1, "flags", "storage-ro", "array<u32>"),
3361
+ decl(1, 2, "offsets", "storage-ro", "array<u32>"),
3362
+ decl(1, 3, "out", "storage", "array<u32>"),
3363
+ decl(1, 4, "outCount", "storage", "array<u32>"),
3364
+ decl(2, 0, "P", "uniform", "CompactParams")
3365
+ ],
3366
+ overrideDecls: [],
3367
+ uniforms: [COMPACT_PARAMS],
3368
+ needs: [],
3369
+ snippetSlots: [],
3370
+ phase: "P8"
3371
+ };
3372
+ const DEDUPE_CLAIM = {
3373
+ id: "dedupe-claim",
3374
+ body: dedupeClaimWgsl,
3375
+ entryPoint: "dedupe_claim",
3376
+ bindings: [
3377
+ decl(1, 0, "queue", "storage-ro", "array<u32>"),
3378
+ decl(1, 1, "owner", "storage", "array<atomic<u32>>"),
3379
+ decl(1, 2, "counters", "storage", "array<atomic<u32>>"),
3380
+ decl(2, 0, "P", "uniform", "CompactParams")
3381
+ ],
3382
+ overrideDecls: [],
3383
+ uniforms: [COMPACT_PARAMS],
3384
+ needs: [],
3385
+ snippetSlots: [],
3386
+ phase: "P8"
3387
+ };
3388
+ const DEDUPE_FILTER = {
3389
+ id: "dedupe-filter",
3390
+ body: dedupeFilterWgsl,
3391
+ entryPoint: "dedupe_filter",
3392
+ bindings: [
3393
+ decl(1, 0, "queue", "storage-ro", "array<u32>"),
3394
+ decl(1, 1, "owner", "storage", "array<atomic<u32>>"),
3395
+ decl(1, 2, "out", "storage", "array<u32>"),
3396
+ decl(1, 3, "outCount", "storage", "array<atomic<u32>>"),
3397
+ decl(2, 0, "P", "uniform", "CompactParams")
3398
+ ],
3399
+ overrideDecls: [],
3400
+ uniforms: [COMPACT_PARAMS],
3401
+ needs: [],
3402
+ snippetSlots: [],
3403
+ phase: "P8"
3404
+ };
3405
+ const FRONTIER_FINALIZE = {
3406
+ id: "frontier-finalize",
3407
+ body: frontierFinalizeWgsl,
3408
+ entryPoint: "frontier_finalize",
3409
+ bindings: [
3410
+ decl(1, 0, "counters", "storage", "array<atomic<u32>>"),
3411
+ decl(1, 1, "args", "storage", "array<u32>"),
3412
+ decl(2, 0, "P", "uniform", "FrontierParams")
3413
+ ],
3414
+ overrideDecls: [],
3415
+ uniforms: [FRONTIER_PARAMS],
3416
+ needs: [],
3417
+ snippetSlots: [],
3418
+ phase: "P8"
3419
+ };
3420
+ const ADVANCE_EXPAND = {
3421
+ id: "advance-expand",
3422
+ body: advanceExpandWgsl,
3423
+ entryPoint: "advance_expand",
3424
+ bindings: GRAPH_SLOTS.concat(
3425
+ decl(1, 0, "frontierIn", "storage-ro", "array<u32>"),
3426
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3427
+ decl(1, 2, "edgeQueue", "storage", "array<u32>"),
3428
+ decl(2, 0, "P", "uniform", "FrontierParams")
3429
+ ),
3430
+ overrideDecls: [],
3431
+ uniforms: [FRONTIER_PARAMS],
3432
+ needs: ["subgroups"],
3433
+ snippetSlots: [],
3434
+ phase: "P8"
3435
+ };
3436
+ const BFS_CONTRACT = {
3437
+ id: "bfs-contract",
3438
+ body: bfsContractWgsl,
3439
+ entryPoint: "bfs_contract",
3440
+ bindings: [
3441
+ decl(1, 0, "edgeQueue", "storage-ro", "array<u32>"),
3442
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3443
+ decl(1, 2, "depth", "storage", "array<atomic<u32>>"),
3444
+ decl(1, 3, "frontierOut", "storage", "array<u32>"),
3445
+ decl(2, 0, "P", "uniform", "FrontierParams")
3446
+ ],
3447
+ overrideDecls: [],
3448
+ uniforms: [FRONTIER_PARAMS],
3449
+ needs: [],
3450
+ snippetSlots: [],
3451
+ phase: "P8"
3452
+ };
3453
+ const SSSP_PRED = {
3454
+ id: "sssp-pred",
3455
+ body: ssspPredWgsl,
3456
+ entryPoint: "sssp_pred",
3457
+ bindings: GRAPH_SLOTS.concat(
3458
+ decl(1, 0, "dist", "storage-ro", "array<u32>"),
3459
+ decl(1, 1, "pred", "storage", "array<atomic<u32>>"),
3460
+ decl(2, 0, "P", "uniform", "FrontierParams")
3461
+ ),
3462
+ overrideDecls: [{ name: "MODE", type: "u32", default: 0 }],
3463
+ uniforms: [FRONTIER_PARAMS],
3464
+ needs: [],
3465
+ snippetSlots: [],
3466
+ phase: "P8"
3467
+ };
3468
+ const BFS_FUSED = {
3469
+ id: "bfs-fused",
3470
+ body: bfsFusedWgsl,
3471
+ entryPoint: "bfs_fused",
3472
+ bindings: GRAPH_SLOTS.concat(
3473
+ decl(1, 0, "frontierIn", "storage-ro", "array<u32>"),
3474
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3475
+ decl(1, 2, "depth", "storage", "array<atomic<u32>>"),
3476
+ decl(1, 3, "frontierOut", "storage", "array<u32>"),
3477
+ decl(2, 0, "P", "uniform", "FrontierParams")
3478
+ ),
3479
+ overrideDecls: [],
3480
+ uniforms: [FRONTIER_PARAMS],
3481
+ needs: [],
3482
+ snippetSlots: [],
3483
+ phase: "P8"
3484
+ };
3485
+ const BFS_BOTTOM_UP = {
3486
+ id: "bfs-bottom-up",
3487
+ body: bfsBottomUpWgsl,
3488
+ entryPoint: "bfs_bottom_up",
3489
+ bindings: GRAPH_SLOTS.concat(
3490
+ decl(1, 0, "sweepIn", "storage-ro", "array<u32>"),
3491
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3492
+ decl(1, 2, "depth", "storage", "array<atomic<u32>>"),
3493
+ decl(1, 3, "frontierOut", "storage", "array<u32>"),
3494
+ decl(2, 0, "P", "uniform", "FrontierParams")
3495
+ ),
3496
+ overrideDecls: [],
3497
+ uniforms: [FRONTIER_PARAMS],
3498
+ needs: ["subgroups"],
3499
+ snippetSlots: [],
3500
+ phase: "P8"
3501
+ };
3502
+ const BFS_BITSET_BUILD = {
3503
+ id: "bfs-bitset-build",
3504
+ body: bfsBitsetBuildWgsl,
3505
+ entryPoint: "bfs_bitset_build",
3506
+ bindings: [
3507
+ decl(1, 0, "frontierIn", "storage-ro", "array<u32>"),
3508
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3509
+ decl(1, 2, "bits", "storage", "array<atomic<u32>>"),
3510
+ decl(2, 0, "P", "uniform", "FrontierParams")
3511
+ ],
3512
+ overrideDecls: [],
3513
+ uniforms: [FRONTIER_PARAMS],
3514
+ needs: [],
3515
+ snippetSlots: [],
3516
+ phase: "P8"
3517
+ };
3518
+ const BFS_UNVISITED_FLAGS = {
3519
+ id: "bfs-unvisited-flags",
3520
+ body: bfsUnvisitedFlagsWgsl,
3521
+ entryPoint: "bfs_unvisited_flags",
3522
+ bindings: [
3523
+ decl(1, 0, "outDegree", "storage-ro", "array<u32>"),
3524
+ decl(1, 1, "inDegree", "storage-ro", "array<u32>"),
3525
+ decl(1, 2, "depth", "storage-ro", "array<u32>"),
3526
+ decl(1, 3, "flags", "storage", "array<u32>"),
3527
+ decl(1, 4, "counters", "storage", "array<atomic<u32>>"),
3528
+ decl(2, 0, "P", "uniform", "FrontierParams")
3529
+ ],
3530
+ overrideDecls: [],
3531
+ uniforms: [FRONTIER_PARAMS],
3532
+ needs: ["subgroups"],
3533
+ snippetSlots: [],
3534
+ phase: "P8"
3535
+ };
3536
+ const SSSP_RELAX = {
3537
+ id: "sssp-relax",
3538
+ body: ssspRelaxWgsl,
3539
+ entryPoint: "sssp_relax",
3540
+ bindings: GRAPH_SLOTS.concat(
3541
+ decl(1, 0, "dist", "storage", "array<atomic<u32>>"),
3542
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3543
+ decl(1, 2, "queueIn", "storage-ro", "array<u32>"),
3544
+ decl(1, 3, "queueOut", "storage", "array<u32>"),
3545
+ decl(2, 0, "P", "uniform", "FrontierParams")
3546
+ ),
3547
+ overrideDecls: [],
3548
+ uniforms: [FRONTIER_PARAMS],
3549
+ needs: [],
3550
+ snippetSlots: [],
3551
+ phase: "P8"
3552
+ };
3553
+ const BF_RELAX = {
3554
+ id: "bf-relax",
3555
+ body: bfRelaxWgsl,
3556
+ entryPoint: "bf_relax",
3557
+ bindings: [
3558
+ decl(1, 0, "edgeSrc", "storage-ro", "array<u32>"),
3559
+ decl(1, 1, "edgeDst", "storage-ro", "array<u32>"),
3560
+ decl(1, 2, "edgeToArc", "storage-ro", "array<u32>"),
3561
+ decl(1, 3, "weights", "storage-ro", "array<f32>"),
3562
+ decl(1, 4, "dist", "storage", "array<atomic<u32>>"),
3563
+ decl(1, 5, "flags", "storage", "array<atomic<u32>>"),
3564
+ decl(2, 0, "P", "uniform", "BfParams")
3565
+ ],
3566
+ overrideDecls: [{ name: "UNDIRECTED", type: "bool", default: false }],
3567
+ uniforms: [BF_PARAMS],
3568
+ needs: [],
3569
+ snippetSlots: [],
3570
+ phase: "P8"
3571
+ };
3572
+ const CLOSENESS_SWEEP = {
3573
+ id: "closeness-sweep",
3574
+ body: closenessSweepWgsl,
3575
+ entryPoint: "closeness_sweep",
3576
+ bindings: GRAPH_SLOTS.concat(
3577
+ decl(1, 0, "frontierList", "storage-ro", "array<u32>"),
3578
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3579
+ decl(1, 2, "bits", "storage", "array<atomic<u32>>"),
3580
+ decl(1, 3, "perSource", "storage", "array<atomic<u32>>"),
3581
+ decl(2, 0, "P", "uniform", "FrontierParams")
3582
+ ),
3583
+ overrideDecls: [],
3584
+ uniforms: [FRONTIER_PARAMS],
3585
+ needs: [],
3586
+ snippetSlots: [],
3587
+ phase: "P8"
3588
+ };
3589
+ const CLOSENESS_REDUCE = {
3590
+ id: "closeness-reduce",
3591
+ body: closenessReduceWgsl,
3592
+ entryPoint: "closeness_reduce",
3593
+ bindings: [
3594
+ decl(1, 0, "counters", "storage", "array<atomic<u32>>"),
3595
+ decl(1, 1, "perSource", "storage", "array<atomic<u32>>"),
3596
+ decl(1, 2, "bits", "storage", "array<u32>"),
3597
+ decl(2, 0, "P", "uniform", "FrontierParams")
3598
+ ],
3599
+ overrideDecls: [],
3600
+ uniforms: [FRONTIER_PARAMS],
3601
+ needs: [],
3602
+ snippetSlots: [],
3603
+ phase: "P8"
3604
+ };
3605
+ const REGISTRY = Object.freeze({
3606
+ degree: DEGREE,
3607
+ reduce: REDUCE,
3608
+ fill: FILL,
3609
+ "segmented-reduce": SEGMENTED_REDUCE,
3610
+ "fa2-stats-finalize": FA2_STATS_FINALIZE,
3611
+ "fa2-attraction": FA2_ATTRACTION,
3612
+ "fa2-repulsion-exact": FA2_REPULSION_EXACT,
3613
+ "fa2-speed-finalize": FA2_SPEED_FINALIZE,
3614
+ "fa2-integrate": FA2_INTEGRATE,
3615
+ "fa2-to-scene": FA2_TO_SCENE,
3616
+ "spmv-pull": SPMV_PULL,
3617
+ "pr-scale": PR_SCALE,
3618
+ "pr-finalize": PR_FINALIZE,
3619
+ "wcc-link-sample": WCC_LINK_SAMPLE,
3620
+ "wcc-link-edges": WCC_LINK_EDGES,
3621
+ "wcc-compress": WCC_COMPRESS,
3622
+ "wcc-sample": WCC_SAMPLE,
3623
+ "indirect-finalize": INDIRECT_FINALIZE,
3624
+ "scan-block": SCAN_BLOCK,
2558
3625
  "scan-add": SCAN_ADD,
2559
3626
  histogram: HISTOGRAM,
2560
3627
  "counting-scatter": COUNTING_SCATTER,
@@ -2565,7 +3632,22 @@ const REGISTRY = Object.freeze({
2565
3632
  "grid-centroid-hub": GRID_CENTROID_HUB,
2566
3633
  "grid-downsample": GRID_DOWNSAMPLE,
2567
3634
  "grid-far-field": GRID_FAR_FIELD,
2568
- "grid-near-field": GRID_NEAR_FIELD
3635
+ "grid-near-field": GRID_NEAR_FIELD,
3636
+ "compact-scatter": COMPACT_SCATTER,
3637
+ "dedupe-claim": DEDUPE_CLAIM,
3638
+ "dedupe-filter": DEDUPE_FILTER,
3639
+ "frontier-finalize": FRONTIER_FINALIZE,
3640
+ "advance-expand": ADVANCE_EXPAND,
3641
+ "bfs-contract": BFS_CONTRACT,
3642
+ "sssp-pred": SSSP_PRED,
3643
+ "bfs-fused": BFS_FUSED,
3644
+ "bfs-bottom-up": BFS_BOTTOM_UP,
3645
+ "bfs-bitset-build": BFS_BITSET_BUILD,
3646
+ "bfs-unvisited-flags": BFS_UNVISITED_FLAGS,
3647
+ "sssp-relax": SSSP_RELAX,
3648
+ "bf-relax": BF_RELAX,
3649
+ "closeness-sweep": CLOSENESS_SWEEP,
3650
+ "closeness-reduce": CLOSENESS_REDUCE
2569
3651
  });
2570
3652
  const bodyOverrides = /* @__PURE__ */ new Map();
2571
3653
  function entryOf(id) {
@@ -2724,7 +3806,7 @@ class ScanPlannerImpl {
2724
3806
  }
2725
3807
  const POISON = 3735928559;
2726
3808
  const CHECK_BLOCKS = 32;
2727
- const RING_SLOTS$3 = 8;
3809
+ const RING_SLOTS$6 = 8;
2728
3810
  const checked = /* @__PURE__ */ new WeakMap();
2729
3811
  function inputAt(i) {
2730
3812
  return i + 1;
@@ -2752,7 +3834,7 @@ async function runCheck(ctx) {
2752
3834
  const count = CHECK_BLOCKS * wg + 1;
2753
3835
  const bytes = 4 * count;
2754
3836
  const lease = ctx.pool.lease();
2755
- const ring = new UniformRing(ctx.device, ctx.allocator, RING_SLOTS$3, "device-check/ring");
3837
+ const ring = new UniformRing(ctx.device, ctx.allocator, RING_SLOTS$6, "device-check/ring");
2756
3838
  try {
2757
3839
  const scope = {
2758
3840
  device: ctx.device,
@@ -2892,8 +3974,8 @@ function rowCountOf(core, primitive) {
2892
3974
  function arcCountOf(core) {
2893
3975
  return core.colIdx === null ? 0 : core.colIdx.size / 4;
2894
3976
  }
2895
- function windowBinding(core, name, w2) {
2896
- const buffer = core.arcBuffers?.[name][w2.bufferIndex];
3977
+ function windowBinding(core, name, w) {
3978
+ const buffer = core.arcBuffers?.[name][w.bufferIndex];
2897
3979
  if (buffer === void 0) {
2898
3980
  throw new WebGpuGraphError("E_INVALID_ARGUMENT", `windowBinding: the core has no window buffers of ${name}`, {
2899
3981
  argument: "core",
@@ -2901,7 +3983,21 @@ function windowBinding(core, name, w2) {
2901
3983
  expected: "a windowed core whose plan uploaded the array"
2902
3984
  });
2903
3985
  }
2904
- return { buffer, offset: w2.offset, size: 4 * (w2.end - w2.start), window: w2 };
3986
+ return { buffer, offset: w.offset, size: 4 * (w.end - w.start), window: w };
3987
+ }
3988
+ function coreWindows(core) {
3989
+ if (core.windows === null) {
3990
+ return [{ arcBase: 0, arcEnd: arcCountOf(core), core }];
3991
+ }
3992
+ return core.windows.map((w, k, all) => ({
3993
+ arcBase: w.start,
3994
+ arcEnd: k + 1 < all.length ? all[k + 1].start : w.end,
3995
+ core: {
3996
+ ...core,
3997
+ colIdx: windowBinding(core, "colIdx", w),
3998
+ weights: core.weights === null ? null : windowBinding(core, "weights", w)
3999
+ }
4000
+ }));
2905
4001
  }
2906
4002
  function assertNotWindowed(core, primitive) {
2907
4003
  if (core.plan === "windowed" || core.windows !== null) {
@@ -2919,22 +4015,22 @@ function assertWholeCore(core, arcCount, limit, algorithm) {
2919
4015
  );
2920
4016
  }
2921
4017
  }
2922
- function coreOfView(v2, arcCount) {
2923
- const { rowPtr } = v2.bindings;
4018
+ function coreOfView(v, arcCount) {
4019
+ const { rowPtr } = v.bindings;
2924
4020
  if (rowPtr === void 0) {
2925
- throw new WebGpuGraphError("E_INVALID_ARGUMENT", `the ${v2.view} view has no rowPtr binding`, {
4021
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `the ${v.view} view has no rowPtr binding`, {
2926
4022
  argument: "view",
2927
- value: v2.view,
4023
+ value: v.view,
2928
4024
  expected: "a view with a rowPtr binding (reverse)"
2929
4025
  });
2930
4026
  }
2931
- const colIdx = v2.bindings.colIdx ?? null;
2932
- const weights = v2.bindings.weights ?? null;
4027
+ const colIdx = v.bindings.colIdx === void 0 || v.bindings.colIdx.size === 0 ? null : v.bindings.colIdx;
4028
+ const weights = v.bindings.weights === void 0 || v.bindings.weights.size === 0 ? null : v.bindings.weights;
2933
4029
  const bound = colIdx === null ? 0 : colIdx.size / 4;
2934
4030
  if (bound !== arcCount) {
2935
4031
  throw new WebGpuGraphError(
2936
4032
  "E_INVALID_ARGUMENT",
2937
- `the ${v2.view} view binds ${bound} arcs but its arcCount scalar says ${arcCount}`,
4033
+ `the ${v.view} view binds ${bound} arcs but its arcCount scalar says ${arcCount}`,
2938
4034
  {
2939
4035
  argument: "arcCount",
2940
4036
  value: arcCount,
@@ -2955,7 +4051,7 @@ function coreOfView(v2, arcCount) {
2955
4051
  hasWeights: weights !== null
2956
4052
  });
2957
4053
  }
2958
- function checkDest$3(dest, n) {
4054
+ function checkDest$5(dest, n) {
2959
4055
  if (dest === void 0) {
2960
4056
  return null;
2961
4057
  }
@@ -2984,7 +4080,7 @@ async function degree(ctx, s, options) {
2984
4080
  ctx.assertReady();
2985
4081
  await assertDeviceComputes(ctx);
2986
4082
  const n = s.nodeCount;
2987
- const dest = checkDest$3(options?.dest, n);
4083
+ const dest = checkDest$5(options?.dest, n);
2988
4084
  if (options?.signal?.aborted) {
2989
4085
  throw new WebGpuGraphError("E_ABORTED", "degree: the signal was aborted before any work started", {});
2990
4086
  }
@@ -3021,22 +4117,22 @@ async function degree(ctx, s, options) {
3021
4117
  const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
3022
4118
  const zero = fill.bind({ dst: outBinding, P: params(FILL_PARAMS, { count: n, value: 0, mode: 0 }) });
3023
4119
  fill.dispatch(pass, zero, plan1d(n, ctx.workgroupSize, ctx.caps), [0]);
3024
- for (const w2 of core.windows) {
4120
+ for (const w of core.windows) {
3025
4121
  const windowed = {
3026
4122
  ...core,
3027
- colIdx: windowBinding(core, "colIdx", w2),
3028
- weights: core.weights === null ? null : windowBinding(core, "weights", w2)
4123
+ colIdx: windowBinding(core, "colIdx", w),
4124
+ weights: core.weights === null ? null : windowBinding(core, "weights", w)
3029
4125
  };
3030
4126
  const P = params(RANGE_PARAMS, {
3031
- start: w2.rowFirst,
3032
- end: w2.rowLast + 1,
3033
- arcBase: w2.start,
3034
- arcEnd: w2.end,
4127
+ start: w.rowFirst,
4128
+ end: w.rowLast + 1,
4129
+ arcBase: w.start,
4130
+ arcEnd: w.end,
3035
4131
  accumulate: 1,
3036
4132
  n
3037
4133
  });
3038
4134
  const bound = kernel.bind({ ...graphBindings(windowed, null), out: outBinding, P });
3039
- kernel.dispatch(pass, bound, plan1d(w2.rowLast - w2.rowFirst + 1, ctx.workgroupSize, ctx.caps), [0]);
4135
+ kernel.dispatch(pass, bound, plan1d(w.rowLast - w.rowFirst + 1, ctx.workgroupSize, ctx.caps), [0]);
3040
4136
  }
3041
4137
  }
3042
4138
  pass.end();
@@ -3342,9 +4438,9 @@ class CommandBatch {
3342
4438
  countDispatches(pass) {
3343
4439
  const direct = pass.dispatchWorkgroups.bind(pass);
3344
4440
  const indirect = pass.dispatchWorkgroupsIndirect.bind(pass);
3345
- pass.dispatchWorkgroups = (x2, y2, z) => {
4441
+ pass.dispatchWorkgroups = (x, y, z) => {
3346
4442
  this.dispatchCount += 1;
3347
- direct(x2, y2, z);
4443
+ direct(x, y, z);
3348
4444
  return void 0;
3349
4445
  };
3350
4446
  pass.dispatchWorkgroupsIndirect = (buffer, offset) => {
@@ -3390,6 +4486,11 @@ function algorithmScope(ctx, label, slots) {
3390
4486
  pool: ctx.pool,
3391
4487
  workgroupSize: ctx.workgroupSize,
3392
4488
  scratch: (byteLength, scratchLabel) => lease.storage(byteLength, `${label}/${scratchLabel}`),
4489
+ indirect: (byteLength, indirectLabel) => lease.acquire(
4490
+ byteLength,
4491
+ BufferUsage.STORAGE | BufferUsage.INDIRECT | BufferUsage.COPY_DST | BufferUsage.COPY_SRC,
4492
+ `${label}/${indirectLabel}`
4493
+ ),
3393
4494
  params(block, values) {
3394
4495
  const slot = ring.reserve(1);
3395
4496
  ring.write(slot, block, values);
@@ -3401,16 +4502,17 @@ function algorithmScope(ctx, label, slots) {
3401
4502
  dispose() {
3402
4503
  ring.destroy();
3403
4504
  lease.release();
3404
- }
4505
+ },
4506
+ ringOverruns: () => ring.overruns
3405
4507
  };
3406
4508
  }
3407
- const ALGORITHM = "connectedComponents";
3408
- const ROUNDS_PER_BATCH = 4;
4509
+ const ALGORITHM$4 = "connectedComponents";
4510
+ const ROUNDS_PER_BATCH$1 = 4;
3409
4511
  const MAX_WCC_ROUNDS = 64;
3410
4512
  const SAMPLE_SIZE = 1024;
3411
4513
  const MAX_STEPS = 1024;
3412
- const RING_SLOTS$2 = 2 * ROUNDS_PER_BATCH;
3413
- function checkDest$2(dest, n) {
4514
+ const RING_SLOTS$5 = 2 * ROUNDS_PER_BATCH$1;
4515
+ function checkDest$4(dest, n) {
3414
4516
  if (dest === void 0) {
3415
4517
  return null;
3416
4518
  }
@@ -3419,7 +4521,7 @@ function checkDest$2(dest, n) {
3419
4521
  }
3420
4522
  throw new WebGpuGraphError(
3421
4523
  "E_INVALID_ARGUMENT",
3422
- `${ALGORITHM}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
4524
+ `${ALGORITHM$4}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
3423
4525
  {
3424
4526
  argument: "dest",
3425
4527
  value: `${dest.constructor.name}(${dest.length})`,
@@ -3429,10 +4531,10 @@ function checkDest$2(dest, n) {
3429
4531
  }
3430
4532
  function coreOf$2(ctx, s) {
3431
4533
  const core = ctx.residency.core(s);
3432
- assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM);
4534
+ assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM$4);
3433
4535
  return core;
3434
4536
  }
3435
- function bindingOf$1(buffer, size) {
4537
+ function bindingOf$3(buffer, size) {
3436
4538
  return { buffer, offset: 0, size, window: null };
3437
4539
  }
3438
4540
  function labelResult(labels, count) {
@@ -3448,19 +4550,19 @@ function labelResult(labels, count) {
3448
4550
  const dense = new Uint32Array(n).fill(U32_MAX$2);
3449
4551
  const sizes = new Uint32Array(count);
3450
4552
  let next = 0;
3451
- for (let v2 = 0; v2 < n; v2++) {
3452
- let k = dense[labels[v2]];
4553
+ for (let v = 0; v < n; v++) {
4554
+ let k = dense[labels[v]];
3453
4555
  if (k === U32_MAX$2) {
3454
4556
  k = next++;
3455
- dense[labels[v2]] = k;
4557
+ dense[labels[v]] = k;
3456
4558
  }
3457
4559
  sizes[k]++;
3458
4560
  }
3459
4561
  const built = Array.from(sizes, (size) => new Uint32Array(size));
3460
4562
  const filled = new Uint32Array(count);
3461
- for (let v2 = 0; v2 < n; v2++) {
3462
- const k = dense[labels[v2]];
3463
- built[k][filled[k]++] = v2;
4563
+ for (let v = 0; v < n; v++) {
4564
+ const k = dense[labels[v]];
4565
+ built[k][filled[k]++] = v;
3464
4566
  }
3465
4567
  groups = built;
3466
4568
  return built;
@@ -3485,11 +4587,11 @@ function checkLabels(raw) {
3485
4587
  const n = raw.length;
3486
4588
  const seen = new Uint8Array(n);
3487
4589
  let count = 0;
3488
- for (let v2 = 0; v2 < n; v2++) {
3489
- const label = raw[v2];
4590
+ for (let v = 0; v < n; v++) {
4591
+ const label = raw[v];
3490
4592
  if (label >= n) {
3491
- throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: labels[${v2}] = ${label} is not a node index`, {
3492
- label: `${ALGORITHM}/labels`,
4593
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$4}: labels[${v}] = ${label} is not a node index`, {
4594
+ label: `${ALGORITHM$4}/labels`,
3493
4595
  message: `the device produced a label outside [0, ${n})`
3494
4596
  });
3495
4597
  }
@@ -3505,9 +4607,9 @@ async function connectedComponents(ctx, s, options) {
3505
4607
  await assertDeviceComputes(ctx);
3506
4608
  const n = s.nodeCount;
3507
4609
  const renumber = options?.renumber !== false;
3508
- const dest = checkDest$2(options?.dest, n);
4610
+ const dest = checkDest$4(options?.dest, n);
3509
4611
  if (options?.signal?.aborted) {
3510
- throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted before any work started`, {});
4612
+ throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM$4}: the signal was aborted before any work started`, {});
3511
4613
  }
3512
4614
  if (n === 0) {
3513
4615
  options?.onProgress?.(1, 1);
@@ -3516,15 +4618,15 @@ async function connectedComponents(ctx, s, options) {
3516
4618
  const core = coreOf$2(ctx, s);
3517
4619
  if (s.arcCount === 0) {
3518
4620
  const labels = dest ?? new Uint32Array(n);
3519
- for (let v2 = 0; v2 < n; v2++) {
3520
- labels[v2] = v2;
4621
+ for (let v = 0; v < n; v++) {
4622
+ labels[v] = v;
3521
4623
  }
3522
4624
  options?.onProgress?.(1, 1);
3523
4625
  return labelResult(labels, n);
3524
4626
  }
3525
4627
  const edges = ctx.residency.view(s, "edgeList");
3526
4628
  const edgeCount = edges.scalars.edgeCount[0];
3527
- const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS$2);
4629
+ const scope = algorithmScope(ctx, ALGORITHM$4, RING_SLOTS$5);
3528
4630
  try {
3529
4631
  const compBytes = 4 * (n + 1);
3530
4632
  const comp = scope.scratch(compBytes, "comp");
@@ -3537,8 +4639,8 @@ async function connectedComponents(ctx, s, options) {
3537
4639
  const compress = await ctx.pipelines.kernel(kernelSpec("wcc-compress", {}));
3538
4640
  const sample = await ctx.pipelines.kernel(kernelSpec("wcc-sample", {}));
3539
4641
  const { queue } = ctx.device;
3540
- const compBinding = bindingOf$1(comp, compBytes);
3541
- const histBinding = bindingOf$1(hist, 4 * items);
4642
+ const compBinding = bindingOf$3(comp, compBytes);
4643
+ const histBinding = bindingOf$3(hist, 4 * items);
3542
4644
  const rowPlan = planGridStride(n, ctx.workgroupSize, ctx.caps);
3543
4645
  const edgePlan = planGridStride(edgeCount, ctx.workgroupSize, ctx.caps);
3544
4646
  const flagIndex = n;
@@ -3553,12 +4655,12 @@ async function connectedComponents(ctx, s, options) {
3553
4655
  return batch.submit();
3554
4656
  };
3555
4657
  queue.writeBuffer(comp, 4 * flagIndex, zero);
3556
- const setup = new CommandBatch(ctx, `${ALGORITHM}/setup`);
4658
+ const setup = new CommandBatch(ctx, `${ALGORITHM$4}/setup`);
3557
4659
  let pass = setup.pass("sample-rounds");
3558
4660
  const fillParams = scope.params(FILL_PARAMS, { count: n, value: 0, mode: 1, pad0: 0 });
3559
4661
  fill.dispatch(
3560
4662
  pass,
3561
- fill.bind({ dst: bindingOf$1(comp, 4 * n), P: fillParams.binding }),
4663
+ fill.bind({ dst: bindingOf$3(comp, 4 * n), P: fillParams.binding }),
3562
4664
  plan1d(n, ctx.workgroupSize, ctx.caps),
3563
4665
  [fillParams.offset]
3564
4666
  );
@@ -3572,7 +4674,7 @@ async function connectedComponents(ctx, s, options) {
3572
4674
  setup.endPass();
3573
4675
  await submit(setup).readback;
3574
4676
  ctx.assertReady();
3575
- const sampler = new CommandBatch(ctx, `${ALGORITHM}/sample`);
4677
+ const sampler = new CommandBatch(ctx, `${ALGORITHM$4}/sample`);
3576
4678
  pass = sampler.pass("sample");
3577
4679
  const sampleParams = wccParams({ items, stride: 0, r: 0, giant: U32_MAX$2 });
3578
4680
  sample.dispatch(
@@ -3590,9 +4692,9 @@ async function connectedComponents(ctx, s, options) {
3590
4692
  let rounds = 0;
3591
4693
  for (; ; ) {
3592
4694
  queue.writeBuffer(comp, 4 * flagIndex, zero);
3593
- const batch = new CommandBatch(ctx, `${ALGORITHM}/rounds`);
4695
+ const batch = new CommandBatch(ctx, `${ALGORITHM$4}/rounds`);
3594
4696
  pass = batch.pass("edge-rounds");
3595
- for (let i = 0; i < ROUNDS_PER_BATCH; i++) {
4697
+ for (let i = 0; i < ROUNDS_PER_BATCH$1; i++) {
3596
4698
  const params = wccParams({ items: edgeCount, stride: edgePlan.stride ?? edgeCount, r: 0, giant });
3597
4699
  const bound = linkEdges.bind({ ...edgeBindings, P: params.binding });
3598
4700
  linkEdges.dispatch(pass, bound, edgePlan, [params.offset]);
@@ -3602,10 +4704,10 @@ async function connectedComponents(ctx, s, options) {
3602
4704
  const flagRequest = batch.readback(comp, 4 * flagIndex, 4);
3603
4705
  const submitted = submit(batch);
3604
4706
  const back = await submitted.readback;
3605
- rounds += ROUNDS_PER_BATCH;
4707
+ rounds += ROUNDS_PER_BATCH$1;
3606
4708
  ctx.assertReady();
3607
4709
  if (options?.signal?.aborted) {
3608
- throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted`, {
4710
+ throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM$4}: the signal was aborted`, {
3609
4711
  batchId: submitted.id
3610
4712
  });
3611
4713
  }
@@ -3615,12 +4717,12 @@ async function connectedComponents(ctx, s, options) {
3615
4717
  if (rounds >= MAX_WCC_ROUNDS) {
3616
4718
  throw new WebGpuGraphError(
3617
4719
  "E_VALIDATION",
3618
- `${ALGORITHM}: the changed flag never settled in ${MAX_WCC_ROUNDS} rounds`,
3619
- { label: ALGORITHM, message: `the changed flag never settled in ${MAX_WCC_ROUNDS} rounds` }
4720
+ `${ALGORITHM$4}: the changed flag never settled in ${MAX_WCC_ROUNDS} rounds`,
4721
+ { label: ALGORITHM$4, message: `the changed flag never settled in ${MAX_WCC_ROUNDS} rounds` }
3620
4722
  );
3621
4723
  }
3622
4724
  }
3623
- const final = new CommandBatch(ctx, `${ALGORITHM}/final`);
4725
+ const final = new CommandBatch(ctx, `${ALGORITHM$4}/final`);
3624
4726
  recordCompress(final.pass("compress"));
3625
4727
  final.endPass();
3626
4728
  await submit(final).readback;
@@ -3801,13 +4903,13 @@ class TieredPlanner {
3801
4903
  this.dispatches++;
3802
4904
  }
3803
4905
  const { windows } = core;
3804
- windows.forEach((w2, k) => {
4906
+ windows.forEach((w, k) => {
3805
4907
  const windowed = {
3806
4908
  ...core,
3807
- colIdx: windowBinding(core, "colIdx", w2),
3808
- weights: core.weights === null ? null : windowBinding(core, "weights", w2)
4909
+ colIdx: windowBinding(core, "colIdx", w),
4910
+ weights: core.weights === null ? null : windowBinding(core, "weights", w)
3809
4911
  };
3810
- this.recordWindow(pass, windowed, out, n, w2, k + 1 < windows.length ? windows[k + 1].start : w2.end);
4912
+ this.recordWindow(pass, windowed, out, n, w, k + 1 < windows.length ? windows[k + 1].start : w.end);
3811
4913
  });
3812
4914
  }
3813
4915
  /**
@@ -3824,16 +4926,16 @@ class TieredPlanner {
3824
4926
  * @param w - the window, or null for the single whole-core dispatch
3825
4927
  * @param nextStart - the next window's first arc (w.end for the last window)
3826
4928
  */
3827
- recordWindow(pass, core, out, n, w2, nextStart) {
4929
+ recordWindow(pass, core, out, n, w, nextStart) {
3828
4930
  const wg = this.scope.workgroupSize;
3829
4931
  for (const { tier, kernel, start, end } of this.tiers) {
3830
4932
  let first = start;
3831
4933
  let last = tier === 0 ? n : end;
3832
- let arcEnd = w2 === null ? arcCountOf(core) : Math.min(w2.end, nextStart);
3833
- if (w2 !== null && this.perm === null) {
3834
- first = w2.rowFirst;
3835
- last = w2.rowLast + 1;
3836
- arcEnd = w2.end;
4934
+ let arcEnd = w === null ? arcCountOf(core) : Math.min(w.end, nextStart);
4935
+ if (w !== null && this.perm === null) {
4936
+ first = w.rowFirst;
4937
+ last = w.rowLast + 1;
4938
+ arcEnd = w.end;
3837
4939
  }
3838
4940
  const rows = last - first;
3839
4941
  if (rows <= 0) {
@@ -3842,9 +4944,9 @@ class TieredPlanner {
3842
4944
  const params = this.scope.params(RANGE_PARAMS, {
3843
4945
  start: first,
3844
4946
  end: last,
3845
- arcBase: w2 === null ? 0 : w2.start,
4947
+ arcBase: w === null ? 0 : w.start,
3846
4948
  arcEnd,
3847
- accumulate: w2 !== null || this.accumulate ? 1 : 0,
4949
+ accumulate: w !== null || this.accumulate ? 1 : 0,
3848
4950
  n
3849
4951
  });
3850
4952
  const bound = kernel.bind({ ...graphBindings(core, this.perm), out, P: params.binding });
@@ -3986,8 +5088,8 @@ async function prepareSpmvPull(scope, rev, options) {
3986
5088
  return new SpmvPullPlannerImpl(scope, compiled, perm, options.weights);
3987
5089
  }
3988
5090
  const PR_BATCH = 8;
3989
- const RING_SLOTS$1 = 2 * PR_BATCH + 1;
3990
- function checkDest$1(dest, n, algorithm) {
5091
+ const RING_SLOTS$4 = 2 * PR_BATCH + 1;
5092
+ function checkDest$3(dest, n, algorithm) {
3991
5093
  if (dest === void 0) {
3992
5094
  return null;
3993
5095
  }
@@ -4009,10 +5111,10 @@ function coreOf$1(ctx, s, algorithm) {
4009
5111
  assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, algorithm);
4010
5112
  return core;
4011
5113
  }
4012
- function bindingOf(buffer, size) {
5114
+ function bindingOf$2(buffer, size) {
4013
5115
  return { buffer, offset: 0, size, window: null };
4014
5116
  }
4015
- function aborted$1(algorithm, batchId) {
5117
+ function aborted$3(algorithm, batchId) {
4016
5118
  return new WebGpuGraphError(
4017
5119
  "E_ABORTED",
4018
5120
  `${algorithm}: the signal was aborted`,
@@ -4034,9 +5136,9 @@ async function run(ctx, s, personalization, options, algorithm) {
4034
5136
  expected: "a positive integer"
4035
5137
  });
4036
5138
  }
4037
- const dest = checkDest$1(options?.dest, n, algorithm);
5139
+ const dest = checkDest$3(options?.dest, n, algorithm);
4038
5140
  if (options?.signal?.aborted) {
4039
- throw aborted$1(algorithm);
5141
+ throw aborted$3(algorithm);
4040
5142
  }
4041
5143
  if (n === 0) {
4042
5144
  options?.onProgress?.(maxIterations, maxIterations);
@@ -4064,7 +5166,7 @@ async function run(ctx, s, personalization, options, algorithm) {
4064
5166
  const weights = useWeights ? void 0 : null;
4065
5167
  const weightedCore = useWeights ? core : { ...core, weights: null, hasWeights: false };
4066
5168
  const weightedRev = useWeights ? rev : { ...rev, weights: null, hasWeights: false };
4067
- const scope = algorithmScope(ctx, algorithm, RING_SLOTS$1);
5169
+ const scope = algorithmScope(ctx, algorithm, RING_SLOTS$4);
4068
5170
  let uploaded = null;
4069
5171
  try {
4070
5172
  const bytes = 4 * n;
@@ -4100,10 +5202,10 @@ async function run(ctx, s, personalization, options, algorithm) {
4100
5202
  const header = new ArrayBuffer(PR_PARTIAL.byteLength);
4101
5203
  PR_PARTIAL.write(new DataView(header), { firstConverged: U32_MAX$2, iteration: 0 });
4102
5204
  queue.writeBuffer(partials, 0, header);
4103
- const rank = [bindingOf(rankA, bytes), bindingOf(rankB, bytes)];
4104
- const xNormBinding = bindingOf(xNorm, bytes);
4105
- const outWeightSumBinding = bindingOf(outWeightSum, bytes);
4106
- const partialsBinding = bindingOf(partials, partialsBytes);
5205
+ const rank = [bindingOf$2(rankA, bytes), bindingOf$2(rankB, bytes)];
5206
+ const xNormBinding = bindingOf$2(xNorm, bytes);
5207
+ const outWeightSumBinding = bindingOf$2(outWeightSum, bytes);
5208
+ const partialsBinding = bindingOf$2(partials, partialsBytes);
4107
5209
  const coefficients = { alpha, beta: 1 - alpha, uniformP: 1 / n };
4108
5210
  let cur = 0;
4109
5211
  let iterationsRun = 0;
@@ -4156,7 +5258,7 @@ async function run(ctx, s, personalization, options, algorithm) {
4156
5258
  iterationsRun += k;
4157
5259
  ctx.assertReady();
4158
5260
  if (options?.signal?.aborted) {
4159
- throw aborted$1(algorithm, submitted.id);
5261
+ throw aborted$3(algorithm, submitted.id);
4160
5262
  }
4161
5263
  options?.onProgress?.(iterationsRun, maxIterations);
4162
5264
  const folded = PR_PARTIAL.read(new DataView(back), headerRequest.offset);
@@ -4199,8 +5301,8 @@ async function personalizedPageRank(ctx, s, personalization, options) {
4199
5301
  );
4200
5302
  }
4201
5303
  let total = 0;
4202
- for (let v2 = 0; v2 < n; v2++) {
4203
- const mass = personalization[v2];
5304
+ for (let v = 0; v < n; v++) {
5305
+ const mass = personalization[v];
4204
5306
  if (!Number.isFinite(mass) || mass < 0) {
4205
5307
  throw invalid2(mass, "finite and non-negative in every entry");
4206
5308
  }
@@ -4210,14 +5312,14 @@ async function personalizedPageRank(ctx, s, personalization, options) {
4210
5312
  throw invalid2(total, "a vector whose entries sum to a positive number");
4211
5313
  }
4212
5314
  const normalised2 = new Float32Array(n);
4213
- for (let v2 = 0; v2 < n; v2++) {
4214
- normalised2[v2] = personalization[v2] / total;
5315
+ for (let v = 0; v < n; v++) {
5316
+ normalised2[v] = personalization[v] / total;
4215
5317
  }
4216
5318
  return run(ctx, s, normalised2, options, "personalizedPageRank");
4217
5319
  }
4218
5320
  const BATCH = 8;
4219
- const RING_SLOTS = 4 * BATCH + 8;
4220
- function checkDest(dest, n, algorithm) {
5321
+ const RING_SLOTS$3 = 4 * BATCH + 8;
5322
+ function checkDest$2(dest, n, algorithm) {
4221
5323
  if (dest === void 0) {
4222
5324
  return null;
4223
5325
  }
@@ -4243,7 +5345,7 @@ function reverseOf(ctx, s) {
4243
5345
  const view = ctx.residency.view(s, "reverse");
4244
5346
  return coreOfView(view, view.scalars.arcCount[0]);
4245
5347
  }
4246
- function aborted(algorithm, batchId) {
5348
+ function aborted$2(algorithm, batchId) {
4247
5349
  return new WebGpuGraphError(
4248
5350
  "E_ABORTED",
4249
5351
  `${algorithm}: the signal was aborted`,
@@ -4255,7 +5357,7 @@ function whole(buffer, size) {
4255
5357
  }
4256
5358
  async function runPowerIteration(ctx, n, config) {
4257
5359
  await assertDeviceComputes(ctx);
4258
- const scope = algorithmScope(ctx, config.label, RING_SLOTS);
5360
+ const scope = algorithmScope(ctx, config.label, RING_SLOTS$3);
4259
5361
  try {
4260
5362
  const bytes = 4 * n;
4261
5363
  const ring = (config.alternate === null ? ["rankA", "rankB"] : ["rankA", "rankB", "rankC"]).map(
@@ -4328,7 +5430,7 @@ async function runPowerIteration(ctx, n, config) {
4328
5430
  iterationsRun += k;
4329
5431
  ctx.assertReady();
4330
5432
  if (config.signal?.aborted === true) {
4331
- throw aborted(config.label, submitted.id);
5433
+ throw aborted$2(config.label, submitted.id);
4332
5434
  }
4333
5435
  config.onProgress?.(iterationsRun, config.maxIterations);
4334
5436
  const folded = PR_PARTIAL.read(new DataView(back), headerRequest.offset);
@@ -4363,9 +5465,9 @@ function resolve(ctx, s, options, algorithm) {
4363
5465
  expected: "a positive integer"
4364
5466
  });
4365
5467
  }
4366
- const dest = checkDest(options?.dest, n, algorithm);
5468
+ const dest = checkDest$2(options?.dest, n, algorithm);
4367
5469
  if (options?.signal?.aborted) {
4368
- throw aborted(algorithm);
5470
+ throw aborted$2(algorithm);
4369
5471
  }
4370
5472
  return { n, maxIterations, tolerance, weights: options?.weighted === false ? null : void 0, dest };
4371
5473
  }
@@ -4382,13 +5484,13 @@ function finite(value, fallback, argument) {
4382
5484
  }
4383
5485
  function normalised(raw, dest, norm) {
4384
5486
  let acc = 0;
4385
- for (const v2 of raw) {
4386
- acc += norm === "sum" ? Math.abs(v2) : v2 * v2;
5487
+ for (const v of raw) {
5488
+ acc += norm === "sum" ? Math.abs(v) : v * v;
4387
5489
  }
4388
5490
  const scale = norm === "sum" ? acc : Math.sqrt(acc);
4389
5491
  const out = dest ?? raw;
4390
- for (let v2 = 0; v2 < raw.length; v2++) {
4391
- out[v2] = scale > 0 ? raw[v2] / scale : raw[v2];
5492
+ for (let v = 0; v < raw.length; v++) {
5493
+ out[v] = scale > 0 ? raw[v] / scale : raw[v];
4392
5494
  }
4393
5495
  return out;
4394
5496
  }
@@ -4433,76 +5535,1867 @@ async function eigenvectorCentrality(ctx, s, options) {
4433
5535
  });
4434
5536
  return scoresOf(run2, r.dest, "l2");
4435
5537
  }
4436
- async function katzCentrality(ctx, s, options) {
4437
- const algorithm = "katzCentrality";
4438
- const alpha = finite(options?.alpha, 0.1, "alpha");
4439
- const beta = finite(options?.beta, 1, "beta");
4440
- const r = resolve(ctx, s, options, algorithm);
4441
- if (r.n === 0) {
4442
- return empty(r.dest, r.maxIterations, options?.onProgress);
5538
+ async function katzCentrality(ctx, s, options) {
5539
+ const algorithm = "katzCentrality";
5540
+ const alpha = finite(options?.alpha, 0.1, "alpha");
5541
+ const beta = finite(options?.beta, 1, "beta");
5542
+ const r = resolve(ctx, s, options, algorithm);
5543
+ if (r.n === 0) {
5544
+ return empty(r.dest, r.maxIterations, options?.onProgress);
5545
+ }
5546
+ const run2 = await runPowerIteration(ctx, r.n, {
5547
+ normMode: 4,
5548
+ adjacency: reverseOf(ctx, s),
5549
+ alternate: null,
5550
+ alpha,
5551
+ beta,
5552
+ uniformP: 1,
5553
+ maxIterations: r.maxIterations,
5554
+ tolerance: r.tolerance,
5555
+ weights: r.weights,
5556
+ label: algorithm,
5557
+ signal: options?.signal,
5558
+ onProgress: options?.onProgress
5559
+ });
5560
+ return scoresOf(run2, r.dest, "l2");
5561
+ }
5562
+ async function hits(ctx, s, options) {
5563
+ const algorithm = "hits";
5564
+ const r = resolve(ctx, s, options, algorithm);
5565
+ const total = 2 * r.maxIterations;
5566
+ if (r.n === 0) {
5567
+ const { scores, iterations, converged, precision } = empty(r.dest, total, options?.onProgress);
5568
+ return { hubs: scores, authorities: new Float32Array(0), iterations, converged, precision };
5569
+ }
5570
+ const onProgress = options?.onProgress;
5571
+ const shared = {
5572
+ normMode: 1,
5573
+ alpha: 1,
5574
+ beta: 0,
5575
+ uniformP: 0,
5576
+ maxIterations: r.maxIterations,
5577
+ tolerance: r.tolerance,
5578
+ weights: r.weights,
5579
+ signal: options?.signal
5580
+ };
5581
+ const forward = coreOf(ctx, s, algorithm);
5582
+ const reverse = reverseOf(ctx, s);
5583
+ const hubSeeded = await runPowerIteration(ctx, r.n, {
5584
+ ...shared,
5585
+ adjacency: reverse,
5586
+ alternate: forward,
5587
+ label: `${algorithm}/hub-seeded`,
5588
+ onProgress: onProgress === void 0 ? void 0 : (done) => {
5589
+ onProgress(done, total);
5590
+ }
5591
+ });
5592
+ const authoritySeeded = await runPowerIteration(ctx, r.n, {
5593
+ ...shared,
5594
+ adjacency: forward,
5595
+ alternate: reverse,
5596
+ label: `${algorithm}/authority-seeded`,
5597
+ onProgress: onProgress === void 0 ? void 0 : (done) => {
5598
+ onProgress(r.maxIterations + done, total);
5599
+ }
5600
+ });
5601
+ return {
5602
+ hubs: normalised(latestOfKind(hubSeeded, authoritySeeded), r.dest, "sum"),
5603
+ authorities: normalised(latestOfKind(authoritySeeded, hubSeeded), null, "sum"),
5604
+ iterations: Math.max(hubSeeded.iterations, authoritySeeded.iterations),
5605
+ converged: hubSeeded.converged && authoritySeeded.converged,
5606
+ precision: "f32"
5607
+ };
5608
+ }
5609
+ async function prepareCompact(scope) {
5610
+ const scatter = await scope.pipelines.kernel(kernelSpec("compact-scatter"));
5611
+ const claim = await scope.pipelines.kernel(kernelSpec("dedupe-claim"));
5612
+ const filter = await scope.pipelines.kernel(kernelSpec("dedupe-filter"));
5613
+ const scan = await prepareScan(scope);
5614
+ return new CompactPlannerImpl(scope, scatter, claim, filter, scan);
5615
+ }
5616
+ function checkCount(what, count) {
5617
+ if (!Number.isSafeInteger(count) || count < 0 || count > U32_MAX$2) {
5618
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${what}: count must be a non-negative integer below 2^32`, {
5619
+ argument: "count",
5620
+ value: count
5621
+ });
5622
+ }
5623
+ }
5624
+ function checkWords$1(what, name, binding, words) {
5625
+ if (binding.size < 4 * words) {
5626
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${what}: ${name} is smaller than 4 x ${words} bytes`, {
5627
+ argument: name,
5628
+ value: binding.size,
5629
+ expected: 4 * words
5630
+ });
5631
+ }
5632
+ }
5633
+ function checkWordIndex(what, name, index, block) {
5634
+ if (!Number.isSafeInteger(index) || index < 0 || 4 * (index + 1) > block.size) {
5635
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${what}: ${name} ${index} is outside the block`, {
5636
+ argument: name,
5637
+ value: index,
5638
+ expected: `0 <= ${name} < ${Math.floor(block.size / 4)}`
5639
+ });
5640
+ }
5641
+ }
5642
+ function checkCompactArguments(r) {
5643
+ checkCount("compact", r.count);
5644
+ checkWords$1("compact", "queue", r.queue, r.count);
5645
+ checkWords$1("compact", "flags", r.flags, r.count);
5646
+ checkWords$1("compact", "out", r.out, r.count);
5647
+ checkWordIndex("compact", "outIndex", r.outIndex, r.outCount);
5648
+ }
5649
+ function checkDedupeArguments(r) {
5650
+ checkCount("dedupe", r.count);
5651
+ checkWords$1("dedupe", "queue", r.queue, r.count);
5652
+ checkWords$1("dedupe", "out", r.out, Math.min(r.count, Math.floor(r.owner.size / 4)));
5653
+ checkWordIndex("dedupe", "outIndex", r.outIndex, r.outCount);
5654
+ if (r.countIndex === U32_MAX$2) {
5655
+ return;
5656
+ }
5657
+ checkWordIndex("dedupe", "countIndex", r.countIndex, r.counters);
5658
+ if (r.countIndex === r.outIndex) {
5659
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "dedupe: countIndex must not be the output word outIndex", {
5660
+ argument: "countIndex",
5661
+ value: r.countIndex
5662
+ });
5663
+ }
5664
+ if (r.counters.buffer !== r.outCount.buffer || r.counters.offset !== r.outCount.offset || r.counters.size !== r.outCount.size) {
5665
+ throw new WebGpuGraphError(
5666
+ "E_INVALID_ARGUMENT",
5667
+ "dedupe: counters must be the same range as outCount when countIndex names a device word",
5668
+ {
5669
+ argument: "counters",
5670
+ value: r.counters.size,
5671
+ expected: r.outCount.size
5672
+ }
5673
+ );
5674
+ }
5675
+ }
5676
+ class CompactPlannerImpl {
5677
+ /**
5678
+ * Wraps the resolved kernels; use prepareCompact().
5679
+ * @param scope - the caller's scope
5680
+ * @param scatter - the `compact-scatter` kernel
5681
+ * @param claim - the `dedupe-claim` kernel
5682
+ * @param filter - the `dedupe-filter` kernel
5683
+ * @param scan - the scan planner of the same scope
5684
+ */
5685
+ constructor(scope, scatter, claim, filter, scan) {
5686
+ this.dispatches = 0;
5687
+ this.scope = scope;
5688
+ this.scatter = scatter;
5689
+ this.claim = claim;
5690
+ this.filter = filter;
5691
+ this.scan = scan;
5692
+ }
5693
+ /**
5694
+ * Dispatches the last record*() issued.
5695
+ * @returns the count
5696
+ */
5697
+ get lastDispatches() {
5698
+ return this.dispatches;
5699
+ }
5700
+ /**
5701
+ * Records the scan and the scatter (see the interface).
5702
+ * @param pass - the compute pass
5703
+ * @param r - the record
5704
+ */
5705
+ record(pass, r) {
5706
+ checkCompactArguments(r);
5707
+ if (r.count === 0) {
5708
+ this.dispatches = 0;
5709
+ return;
5710
+ }
5711
+ const size = 4 * r.count;
5712
+ const offsets = { buffer: this.scope.scratch(size, "compact/offsets"), offset: 0, size, window: null };
5713
+ this.scan.record(pass, r.flags, r.count, offsets);
5714
+ const params = this.scope.params(COMPACT_PARAMS, {
5715
+ count: r.count,
5716
+ outIndex: r.outIndex,
5717
+ countIndex: U32_MAX$2,
5718
+ stride: 0
5719
+ });
5720
+ const bound = this.scatter.bind({
5721
+ queue: r.queue,
5722
+ flags: r.flags,
5723
+ offsets,
5724
+ out: r.out,
5725
+ outCount: r.outCount,
5726
+ P: params.binding
5727
+ });
5728
+ this.scatter.dispatch(pass, bound, plan1d(r.count, this.scope.workgroupSize, this.scope.caps), [params.offset]);
5729
+ this.dispatches = this.scan.lastDispatches + 1;
5730
+ }
5731
+ /**
5732
+ * Records the claim and the filter over plan1d(count) (see the interface).
5733
+ * @param pass - the compute pass
5734
+ * @param r - the record
5735
+ */
5736
+ recordDedupe(pass, r) {
5737
+ checkDedupeArguments(r);
5738
+ if (r.count === 0) {
5739
+ this.dispatches = 0;
5740
+ return;
5741
+ }
5742
+ const plan = planGridStride(r.count, this.scope.workgroupSize, this.scope.caps);
5743
+ const bound = this.bindDedupe(r, plan.stride ?? this.scope.workgroupSize);
5744
+ this.claim.dispatch(pass, bound.claim, plan, [bound.offset]);
5745
+ this.filter.dispatch(pass, bound.filter, plan, [bound.offset]);
5746
+ this.dispatches = 2;
5747
+ }
5748
+ /**
5749
+ * One params record (both kernels read the same values) and the two bind groups.
5750
+ * @param r - the record
5751
+ * @param stride - the grid-stride plan's stride (entries per pass over the grid)
5752
+ * @returns the bound kernels and the record's dynamic offset
5753
+ */
5754
+ bindDedupe(r, stride) {
5755
+ const params = this.scope.params(COMPACT_PARAMS, {
5756
+ count: r.count,
5757
+ outIndex: r.outIndex,
5758
+ countIndex: r.countIndex,
5759
+ stride
5760
+ });
5761
+ return {
5762
+ claim: this.claim.bind({ queue: r.queue, owner: r.owner, counters: r.counters, P: params.binding }),
5763
+ filter: this.filter.bind({
5764
+ queue: r.queue,
5765
+ owner: r.owner,
5766
+ out: r.out,
5767
+ outCount: r.outCount,
5768
+ P: params.binding
5769
+ }),
5770
+ offset: params.offset
5771
+ };
5772
+ }
5773
+ }
5774
+ const W = Object.freeze({
5775
+ frontierCount: 0,
5776
+ nextFrontierCount: 1,
5777
+ frontierDegreeSum: 2,
5778
+ prevFrontierCount: 3,
5779
+ prevDegreeSum: 4,
5780
+ unvisitedCount: 5,
5781
+ unvisitedDegreeSum: 6,
5782
+ unvisitedListLen: 7,
5783
+ edgeCount: 8,
5784
+ edgeCountUnclamped: 9,
5785
+ overflowLevels: 10,
5786
+ level: 11,
5787
+ visitedCount: 12,
5788
+ switches: 13,
5789
+ direction: 14,
5790
+ done: 15,
5791
+ arcsScanned: 16,
5792
+ fusedLevels: 17,
5793
+ twoPhaseLevels: 18,
5794
+ bottomUpLevels: 19,
5795
+ farCount: 20,
5796
+ nextFarCount: 21,
5797
+ thresholdBits: 22,
5798
+ deltaBits: 23,
5799
+ path: 24
5800
+ });
5801
+ function definedWords(words) {
5802
+ const out = {};
5803
+ for (const [name, value] of Object.entries(words)) {
5804
+ if (value !== void 0) {
5805
+ out[name] = value;
5806
+ }
5807
+ }
5808
+ return out;
5809
+ }
5810
+ const MAX_ROLE = 3;
5811
+ function assertCount(argument, value) {
5812
+ if (!Number.isSafeInteger(value) || value < 0) {
5813
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: ${argument} must be a non-negative integer`, {
5814
+ argument,
5815
+ value,
5816
+ expected: "a non-negative integer"
5817
+ });
5818
+ }
5819
+ }
5820
+ class Frontier {
5821
+ /**
5822
+ * Wraps the leased buffers; use prepareFrontier().
5823
+ * @param vertices - the two vertex queues
5824
+ * @param counters - the counters block
5825
+ * @param args - the args buffer
5826
+ * @param edgeQueue - the edge queue
5827
+ * @param edgeCapacity - the edge queue's entry count
5828
+ * @param n - the vertex count
5829
+ */
5830
+ constructor(vertices, counters, args, edgeQueue, edgeCapacity, n) {
5831
+ this.sideIndex = 0;
5832
+ this.vertices = vertices;
5833
+ this.counters = counters;
5834
+ this.args = args;
5835
+ this.edgeQueue = edgeQueue;
5836
+ this.edgeCapacity = edgeCapacity;
5837
+ this.n = n;
5838
+ }
5839
+ /**
5840
+ * Which vertex queue is the input of the current level (a host-side index between two cached bind-group sets).
5841
+ * @returns 0 or 1
5842
+ */
5843
+ get side() {
5844
+ return this.sideIndex;
5845
+ }
5846
+ /**
5847
+ * The vertex queue the current level expands from.
5848
+ * @returns `vertices[side]`
5849
+ */
5850
+ get input() {
5851
+ return this.sideIndex === 0 ? this.vertices[0] : this.vertices[1];
5852
+ }
5853
+ /**
5854
+ * The vertex queue the current level's claims append to.
5855
+ * @returns `vertices[1 - side]`
5856
+ */
5857
+ get output() {
5858
+ return this.sideIndex === 0 ? this.vertices[1] : this.vertices[0];
5859
+ }
5860
+ /** Flips the two vertex queues; the counts rotate inside the block, so nothing else moves. */
5861
+ swap() {
5862
+ this.sideIndex = this.sideIndex === 0 ? 1 : 0;
5863
+ }
5864
+ /**
5865
+ * Seeds a traversal: one `queue.writeBuffer` of the whole 96-byte block (zero except the caller's words) and one of
5866
+ * `vertices[0][0] = source`, both ordered before the submit that follows; the source is on side 0 afterwards.
5867
+ * `frontierCount` is not a word to seed: the first boundary rotates word 1 into it (the BFS seed is
5868
+ * `{ nextFrontierCount: 1, level: U32_MAX }`). A source outside `[0, n)`, an unknown word or a value that is not a
5869
+ * u32 is E_INVALID_ARGUMENT before anything is written.
5870
+ * @param queue - the device queue
5871
+ * @param source - the source vertex
5872
+ * @param seed - the words to seed
5873
+ */
5874
+ reset(queue, source, seed) {
5875
+ if (!Number.isInteger(source) || source < 0 || source >= this.n) {
5876
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: source ${source} is outside [0, ${this.n})`, {
5877
+ argument: "source",
5878
+ value: source,
5879
+ expected: `an integer in [0, ${this.n})`
5880
+ });
5881
+ }
5882
+ const bytes = new ArrayBuffer(FRONTIER_COUNTERS.byteLength);
5883
+ FRONTIER_COUNTERS.write(new DataView(bytes), definedWords(seed));
5884
+ this.sideIndex = 0;
5885
+ queue.writeBuffer(this.counters.buffer, this.counters.offset, bytes);
5886
+ queue.writeBuffer(this.vertices[0].buffer, this.vertices[0].offset, Uint32Array.of(source));
5887
+ }
5888
+ }
5889
+ async function prepareFrontier(scope, n, arcCount, edgeCapacity) {
5890
+ assertCount("n", n);
5891
+ assertCount("arcCount", arcCount);
5892
+ const capacity = edgeCapacity ?? Math.max(1, Math.min(arcCount, Math.floor(scope.caps.limits.maxStorageBufferBindingSize / 4)));
5893
+ if (!Number.isSafeInteger(capacity) || capacity < 1 || capacity > U32_MAX$2) {
5894
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "frontier: edgeCapacity must be an integer >= 1", {
5895
+ argument: "edgeCapacity",
5896
+ value: capacity,
5897
+ expected: "an integer in [1, 2^32)"
5898
+ });
5899
+ }
5900
+ const kernel = await scope.pipelines.kernel(kernelSpec("frontier-finalize"));
5901
+ const queueBytes = 4 * Math.max(1, n);
5902
+ const argsBytes = MAX_LEVELS_PER_SUBMIT * FRONTIER_CANDIDATES * INDIRECT_ARGS_STRIDE;
5903
+ const vertices = [
5904
+ { buffer: scope.scratch(queueBytes, "frontier/vertices-0"), offset: 0, size: queueBytes, window: null },
5905
+ { buffer: scope.scratch(queueBytes, "frontier/vertices-1"), offset: 0, size: queueBytes, window: null }
5906
+ ];
5907
+ const counters = {
5908
+ buffer: scope.scratch(FRONTIER_COUNTERS.byteLength, "frontier/counters"),
5909
+ offset: 0,
5910
+ size: FRONTIER_COUNTERS.byteLength,
5911
+ window: null
5912
+ };
5913
+ const args = {
5914
+ buffer: scope.indirect(argsBytes, "frontier/args"),
5915
+ offset: 0,
5916
+ size: argsBytes,
5917
+ window: null
5918
+ };
5919
+ const edgeQueue = {
5920
+ buffer: scope.scratch(4 * capacity, "frontier/edge-queue"),
5921
+ offset: 0,
5922
+ size: 4 * capacity,
5923
+ window: null
5924
+ };
5925
+ const frontier = new Frontier(vertices, counters, args, edgeQueue, capacity, n);
5926
+ return new FrontierPlannerImpl(scope, kernel, frontier);
5927
+ }
5928
+ class FrontierPlannerImpl {
5929
+ /**
5930
+ * Wraps the resolved kernel; use prepareFrontier().
5931
+ * @param scope - the caller's scope
5932
+ * @param kernel - the `frontier-finalize` kernel
5933
+ * @param frontier - the leased queue
5934
+ */
5935
+ constructor(scope, kernel, frontier) {
5936
+ this.scope = scope;
5937
+ this.kernel = kernel;
5938
+ this.frontier = frontier;
5939
+ }
5940
+ /**
5941
+ * Records one selector dispatch (see the interface).
5942
+ * @param pass - the compute pass
5943
+ * @param role - the role
5944
+ * @param level - the level inside the submit
5945
+ * @param fields - the caller's fields
5946
+ */
5947
+ recordFinalize(pass, role, level, fields) {
5948
+ if (!Number.isInteger(level) || level < 0 || level >= MAX_LEVELS_PER_SUBMIT) {
5949
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: level ${level} is outside the submit`, {
5950
+ argument: "level",
5951
+ value: level,
5952
+ expected: `an integer in [0, ${MAX_LEVELS_PER_SUBMIT})`
5953
+ });
5954
+ }
5955
+ if (!Number.isInteger(role) || role < 0 || role > MAX_ROLE) {
5956
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: role ${role} is not a finalize role`, {
5957
+ argument: "role",
5958
+ value: role,
5959
+ expected: `an integer in [0, ${MAX_ROLE}]`
5960
+ });
5961
+ }
5962
+ const { scope, frontier } = this;
5963
+ const params = scope.params(FRONTIER_PARAMS, {
5964
+ ...definedWords(fields),
5965
+ role,
5966
+ slotBase: level * FRONTIER_CANDIDATES,
5967
+ wg: scope.workgroupSize,
5968
+ edgeCapacity: frontier.edgeCapacity,
5969
+ n: frontier.n
5970
+ });
5971
+ const bound = this.kernel.bind({ counters: frontier.counters, args: frontier.args, P: params.binding });
5972
+ this.kernel.dispatch(pass, bound, plan1d(1, scope.workgroupSize, scope.caps), [params.offset]);
5973
+ }
5974
+ }
5975
+ async function prepareAdvance(scope, core) {
5976
+ const kernel = await scope.pipelines.kernel(kernelSpec("advance-expand", graphOverrides(core, null)));
5977
+ return new AdvancePlannerImpl(scope, core, kernel);
5978
+ }
5979
+ class AdvancePlannerImpl {
5980
+ /**
5981
+ * Wraps the resolved kernel; use prepareAdvance().
5982
+ * @param scope - the caller's scope
5983
+ * @param core - the core the kernel was compiled for
5984
+ * @param kernel - the `advance-expand` kernel
5985
+ */
5986
+ constructor(scope, core, kernel) {
5987
+ this.scope = scope;
5988
+ this.kernel = kernel;
5989
+ this.windows = coreWindows(core);
5990
+ this.n = rowCountOf(core, "advance");
5991
+ }
5992
+ /**
5993
+ * Records one direct expansion per window (see the interface).
5994
+ * @param pass - the compute pass
5995
+ * @param frontier - the queue to expand
5996
+ */
5997
+ record(pass, frontier) {
5998
+ if (frontier.n !== this.n) {
5999
+ throw new WebGpuGraphError(
6000
+ "E_INVALID_ARGUMENT",
6001
+ `advance: the frontier holds ${frontier.n} vertices, the core ${this.n} rows`,
6002
+ {
6003
+ argument: "frontier",
6004
+ value: frontier.n,
6005
+ expected: this.n
6006
+ }
6007
+ );
6008
+ }
6009
+ const { scope } = this;
6010
+ const plan = planGridStride(frontier.n, scope.workgroupSize, scope.caps);
6011
+ for (const w of this.windows) {
6012
+ const params = scope.params(FRONTIER_PARAMS, {
6013
+ wg: scope.workgroupSize,
6014
+ edgeCapacity: frontier.edgeCapacity,
6015
+ n: frontier.n,
6016
+ arcBase: w.arcBase,
6017
+ arcEnd: w.arcEnd,
6018
+ stride: plan.stride ?? scope.workgroupSize
6019
+ });
6020
+ const bound = this.kernel.bind({
6021
+ ...graphBindings(w.core, null),
6022
+ frontierIn: frontier.input,
6023
+ counters: frontier.counters,
6024
+ edgeQueue: frontier.edgeQueue,
6025
+ P: params.binding
6026
+ });
6027
+ this.kernel.dispatch(pass, bound, plan, [params.offset]);
6028
+ }
6029
+ }
6030
+ }
6031
+ const RADIX_DIGIT_BITS = 8;
6032
+ function radixHistBytes(count, wg) {
6033
+ return 4 * RADIX_BINS * Math.ceil(count / wg);
6034
+ }
6035
+ async function prepareRadixSort(scope) {
6036
+ const hist = await scope.pipelines.kernel(kernelSpec("radix-hist"));
6037
+ const scatter = await scope.pipelines.kernel(kernelSpec("radix-scatter"));
6038
+ const scan = await prepareScan(scope);
6039
+ return new RadixSortPlannerImpl(scope, hist, scatter, scan);
6040
+ }
6041
+ function checkBinding(argument, binding, bytes) {
6042
+ if (binding.size < bytes) {
6043
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `radixSort: ${argument} is smaller than ${bytes} bytes`, {
6044
+ argument,
6045
+ value: binding.size,
6046
+ expected: bytes
6047
+ });
6048
+ }
6049
+ }
6050
+ function checkRecordArguments(keys, vals, count, bits, scratch, wg) {
6051
+ if (bits !== 8 && bits !== 16 && bits !== 24 && bits !== 32) {
6052
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: bits must be 8, 16, 24 or 32", {
6053
+ argument: "bits",
6054
+ value: bits,
6055
+ expected: [8, 16, 24, 32]
6056
+ });
6057
+ }
6058
+ if (!Number.isSafeInteger(count) || count < 0 || count > U32_MAX$2) {
6059
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: count must be a non-negative integer below 2^32", {
6060
+ argument: "count",
6061
+ value: count
6062
+ });
6063
+ }
6064
+ const pairBytes = 4 * count;
6065
+ checkBinding("keys", keys, pairBytes);
6066
+ checkBinding("vals", vals, pairBytes);
6067
+ checkBinding("scratch.keys", scratch.keys, pairBytes);
6068
+ checkBinding("scratch.vals", scratch.vals, pairBytes);
6069
+ const tableBytes = radixHistBytes(count, wg);
6070
+ checkBinding("scratch.hist", scratch.hist, tableBytes);
6071
+ checkBinding("scratch.offsets", scratch.offsets, tableBytes);
6072
+ }
6073
+ class RadixSortPlannerImpl {
6074
+ /**
6075
+ * Wraps the resolved kernels; use prepareRadixSort().
6076
+ * @param scope - the caller's scope
6077
+ * @param hist - the `radix-hist` kernel
6078
+ * @param scatter - the `radix-scatter` kernel
6079
+ * @param scan - the scan planner of the same scope
6080
+ */
6081
+ constructor(scope, hist, scatter, scan) {
6082
+ this.dispatches = 0;
6083
+ this.scope = scope;
6084
+ this.hist = hist;
6085
+ this.scatter = scatter;
6086
+ this.scan = scan;
6087
+ }
6088
+ /**
6089
+ * Dispatches the last record() issued.
6090
+ * @returns the count
6091
+ */
6092
+ get lastDispatches() {
6093
+ return this.dispatches;
6094
+ }
6095
+ /**
6096
+ * Records the passes into the pass (see the interface).
6097
+ * @param pass - the compute pass
6098
+ * @param keys - the keys
6099
+ * @param vals - the values
6100
+ * @param count - the pair count
6101
+ * @param bits - the key width
6102
+ * @param scratch - the second pair, the histogram table and its scanned twin
6103
+ * @returns the pair the result lives in
6104
+ */
6105
+ record(pass, keys, vals, count, bits, scratch) {
6106
+ const wg = this.scope.workgroupSize;
6107
+ checkRecordArguments(keys, vals, count, bits, scratch, wg);
6108
+ if (count === 0) {
6109
+ this.dispatches = 0;
6110
+ return { keys, vals };
6111
+ }
6112
+ const groups = Math.ceil(count / wg);
6113
+ const plan = plan1d(count, wg, this.scope.caps);
6114
+ const tableWords = RADIX_BINS * groups;
6115
+ const tableBytes = 4 * tableWords;
6116
+ const histTable = { ...scratch.hist, size: tableBytes };
6117
+ const offsets = { ...scratch.offsets, size: tableBytes };
6118
+ let src = { keys, vals };
6119
+ let dst = { keys: scratch.keys, vals: scratch.vals };
6120
+ let dispatches = 0;
6121
+ const passes = bits / RADIX_DIGIT_BITS;
6122
+ for (let p = 0; p < passes; p++) {
6123
+ const params = this.scope.params(RADIX_PARAMS, { count, shift: RADIX_DIGIT_BITS * p, groups, pad0: 0 });
6124
+ const histBound = this.hist.bind({ keys: src.keys, hist: histTable, P: params.binding });
6125
+ this.hist.dispatch(pass, histBound, plan, [params.offset]);
6126
+ this.scan.record(pass, histTable, tableWords, offsets);
6127
+ const scatterBound = this.scatter.bind({
6128
+ keys: src.keys,
6129
+ vals: src.vals,
6130
+ offsets,
6131
+ keysOut: dst.keys,
6132
+ valsOut: dst.vals,
6133
+ P: params.binding
6134
+ });
6135
+ this.scatter.dispatch(pass, scatterBound, plan, [params.offset]);
6136
+ dispatches += 2 + this.scan.lastDispatches;
6137
+ [src, dst] = [dst, src];
6138
+ }
6139
+ this.dispatches = dispatches;
6140
+ return src;
6141
+ }
6142
+ }
6143
+ const ALGORITHM$3 = "breadthFirstSearch";
6144
+ function bfsRingSlots(windows, levelsPerSubmit) {
6145
+ return Math.max((5 + 4 * windows) * levelsPerSubmit + 16, RESULT_BATCH_SLOTS + windows);
6146
+ }
6147
+ const RESULT_BATCH_SLOTS = 2 + 4 * 8;
6148
+ function normaliseMaxDepth(maxDepth) {
6149
+ if (maxDepth === void 0 || Number.isNaN(maxDepth) || maxDepth >= U32_MAX$2) {
6150
+ return U32_MAX$2;
6151
+ }
6152
+ return Math.max(0, Math.ceil(maxDepth));
6153
+ }
6154
+ function checkDest$1(dest, n) {
6155
+ if (dest === void 0) {
6156
+ return null;
6157
+ }
6158
+ if (dest instanceof Uint32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
6159
+ return dest;
6160
+ }
6161
+ throw new WebGpuGraphError(
6162
+ "E_INVALID_ARGUMENT",
6163
+ `${ALGORITHM$3}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
6164
+ {
6165
+ argument: "dest",
6166
+ value: `${dest.constructor.name}(${dest.length})`,
6167
+ expected: `Uint32Array(${n}) over an ArrayBuffer`
6168
+ }
6169
+ );
6170
+ }
6171
+ function bindingOf$1(buffer, size) {
6172
+ return { buffer, offset: 0, size, window: null };
6173
+ }
6174
+ function degreeView(ctx, s, name) {
6175
+ const { bindings } = ctx.residency.view(s, name);
6176
+ const { [name]: binding } = bindings;
6177
+ if (binding === void 0) {
6178
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$3}: the ${name} view has no ${name} binding`, {
6179
+ label: `${ALGORITHM$3}/${name}`,
6180
+ message: `the ${name} view has no ${name} binding`
6181
+ });
6182
+ }
6183
+ return binding;
6184
+ }
6185
+ function aborted$1(batchId) {
6186
+ return new WebGpuGraphError(
6187
+ "E_ABORTED",
6188
+ `${ALGORITHM$3}: the signal was aborted`,
6189
+ batchId === void 0 ? {} : { batchId }
6190
+ );
6191
+ }
6192
+ function wordOf$1(block, name) {
6193
+ const value = block[name];
6194
+ if (typeof value !== "number") {
6195
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$3}: counters.${name} did not decode to a number`, {
6196
+ label: `${ALGORITHM$3}/counters`,
6197
+ message: `the field ${name} did not decode to a number`
6198
+ });
6199
+ }
6200
+ return value;
6201
+ }
6202
+ async function bfsWithTuning(ctx, s, source, options, tuning) {
6203
+ ctx.assertReady();
6204
+ await assertDeviceComputes(ctx);
6205
+ const n = s.nodeCount;
6206
+ if (!Number.isInteger(source) || source < 0 || source >= n) {
6207
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM$3}: source ${source} is outside [0, ${n})`, {
6208
+ argument: "source",
6209
+ value: source,
6210
+ expected: `an integer in [0, ${n})`
6211
+ });
6212
+ }
6213
+ const levelsPerSubmit = tuning.levelsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
6214
+ if (!Number.isInteger(levelsPerSubmit) || levelsPerSubmit < 1 || levelsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
6215
+ throw new WebGpuGraphError(
6216
+ "E_INVALID_ARGUMENT",
6217
+ `${ALGORITHM$3}: levelsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
6218
+ {
6219
+ argument: "levelsPerSubmit",
6220
+ value: levelsPerSubmit,
6221
+ expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
6222
+ }
6223
+ );
6224
+ }
6225
+ const dest = checkDest$1(options?.dest, n);
6226
+ const maxDepth = normaliseMaxDepth(options?.maxDepth);
6227
+ const predKind = tuning.predKind ?? 1;
6228
+ if (options?.signal?.aborted) {
6229
+ throw aborted$1();
6230
+ }
6231
+ const core = ctx.residency.core(s);
6232
+ const forward = coreWindows(core);
6233
+ if (s.directed && 4 * s.arcCount > ctx.caps.limits.maxStorageBufferBindingSize) {
6234
+ throw new WebGpuGraphError(
6235
+ "E_TOO_LARGE",
6236
+ `${ALGORITHM$3}: the reverse adjacency of a directed snapshot (${4 * s.arcCount} bytes) needs arc windows, which no view executes (spec 4.3); the bottom-up sweep binds it whole`,
6237
+ {
6238
+ needed: 4 * s.arcCount,
6239
+ limit: ctx.caps.limits.maxStorageBufferBindingSize,
6240
+ path: "windowed",
6241
+ algorithm: ALGORITHM$3
6242
+ }
6243
+ );
6244
+ }
6245
+ const reverse = s.directed ? coreOfView(ctx.residency.view(s, "reverse"), s.arcCount) : core;
6246
+ const backward = coreWindows(reverse);
6247
+ const outDegree = degreeView(ctx, s, "outDegree");
6248
+ const inDegree = degreeView(ctx, s, "inDegree");
6249
+ const scope = algorithmScope(ctx, ALGORITHM$3, bfsRingSlots(forward.length, levelsPerSubmit));
6250
+ tuning.onScope?.(scope);
6251
+ try {
6252
+ const bytes = 4 * n;
6253
+ const wg = ctx.workgroupSize;
6254
+ const depth = bindingOf$1(scope.scratch(bytes, "depth"), bytes);
6255
+ const bitsBase = Math.ceil(n / 64) * 64;
6256
+ const bitsWords = Math.ceil(n / 32);
6257
+ const sweepBytes = 4 * (bitsBase + bitsWords);
6258
+ const sweepIn = bindingOf$1(scope.scratch(sweepBytes, "sweep-in"), sweepBytes);
6259
+ const unvisitedList = { buffer: sweepIn.buffer, offset: 0, size: bytes, window: null };
6260
+ const frontierBits = {
6261
+ buffer: sweepIn.buffer,
6262
+ offset: 4 * bitsBase,
6263
+ size: 4 * bitsWords,
6264
+ window: null
6265
+ };
6266
+ const flags = bindingOf$1(scope.scratch(bytes, "unvisited-flags"), bytes);
6267
+ const iota = bindingOf$1(scope.scratch(bytes, "iota"), bytes);
6268
+ const compactCount = bindingOf$1(scope.scratch(4, "compact-count"), 4);
6269
+ await ctx.allocator.check();
6270
+ const planner = await prepareFrontier(scope, n, s.arcCount, tuning.edgeCapacity);
6271
+ const advance = await prepareAdvance(scope, core);
6272
+ const compact = await prepareCompact(scope);
6273
+ const contract = await ctx.pipelines.kernel(kernelSpec("bfs-contract"));
6274
+ const fused = await ctx.pipelines.kernel(kernelSpec("bfs-fused", graphOverrides(core, null)));
6275
+ const bitset = await ctx.pipelines.kernel(kernelSpec("bfs-bitset-build"));
6276
+ const bottomUp = await ctx.pipelines.kernel(kernelSpec("bfs-bottom-up", graphOverrides(reverse, null)));
6277
+ const unvisited = await ctx.pipelines.kernel(kernelSpec("bfs-unvisited-flags"));
6278
+ const pred = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...graphOverrides(core, null), MODE: 1 }));
6279
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
6280
+ const sort = await prepareRadixSort(scope);
6281
+ const { frontier } = planner;
6282
+ const { counters } = frontier;
6283
+ const { queue } = ctx.device;
6284
+ const fillPlan = plan1d(n, wg, ctx.caps);
6285
+ const levelPlan = planGridStride(Math.max(n, frontier.edgeCapacity), wg, ctx.caps);
6286
+ const sweepPlan = planGridStride(n, wg, ctx.caps);
6287
+ const fusedPlan = planGridStride(n * wg, wg, ctx.caps);
6288
+ const bitsPlan = plan1d(bitsWords, wg, ctx.caps);
6289
+ const recordFill = (pass2, dst, value, mode) => {
6290
+ const params = scope.params(FILL_PARAMS, { count: n, value, mode, pad0: 0 });
6291
+ fill.dispatch(pass2, fill.bind({ dst, P: params.binding }), fillPlan, [params.offset]);
6292
+ };
6293
+ const flagsPlan = planGridStride(n, wg, ctx.caps);
6294
+ const recordRebuild = (pass2) => {
6295
+ const params = scope.params(FRONTIER_PARAMS, { wg, n, stride: flagsPlan.stride ?? n });
6296
+ const bound = unvisited.bind({ outDegree, inDegree, depth, flags, counters, P: params.binding });
6297
+ unvisited.dispatch(pass2, bound, flagsPlan, [params.offset]);
6298
+ compact.record(pass2, {
6299
+ queue: iota,
6300
+ flags,
6301
+ count: n,
6302
+ out: unvisitedList,
6303
+ outCount: compactCount,
6304
+ outIndex: 0
6305
+ });
6306
+ };
6307
+ const submit = (batch) => {
6308
+ scope.flush();
6309
+ return batch.submit();
6310
+ };
6311
+ const setup = new CommandBatch(ctx, `${ALGORITHM$3}/setup`);
6312
+ const setupPass = setup.pass("fill");
6313
+ recordFill(setupPass, depth, INVALID_INDEX, 0);
6314
+ recordFill(setupPass, iota, 0, 1);
6315
+ setup.endPass();
6316
+ await submit(setup).readback;
6317
+ ctx.assertReady();
6318
+ queue.writeBuffer(depth.buffer, depth.offset + 4 * source, Uint32Array.of(0));
6319
+ frontier.reset(queue, source, { nextFrontierCount: 1, level: U32_MAX$2 });
6320
+ const fields = {
6321
+ mode: tuning.direction === "top-down" ? 1 : 0,
6322
+ alpha: tuning.alpha ?? Math.max(1, Math.floor(s.arcCount / n)),
6323
+ beta: tuning.beta ?? BEAMER_BETA,
6324
+ fusedMax: tuning.fusedMax ?? FUSED_FRONTIER_MAX,
6325
+ maxDepth
6326
+ };
6327
+ let levelsRecorded = 0;
6328
+ let submits = 0;
6329
+ for (; ; ) {
6330
+ queue.writeBuffer(counters.buffer, counters.offset + 4 * W.unvisitedCount, new Uint32Array(3));
6331
+ const batch = new CommandBatch(ctx, `${ALGORITHM$3}/levels`);
6332
+ const pass2 = batch.pass("bfs");
6333
+ recordRebuild(pass2);
6334
+ const bitsParams = scope.params(FILL_PARAMS, { count: bitsWords, value: 0, mode: 0, pad0: 0 });
6335
+ const boundBitsFill = fill.bind({ dst: frontierBits, P: bitsParams.binding });
6336
+ for (let level2 = 0; level2 < levelsPerSubmit; level2++) {
6337
+ planner.recordFinalize(pass2, 0, level2, { ...fields, firstOfSubmit: Math.min(level2, 2) });
6338
+ advance.record(pass2, frontier);
6339
+ planner.recordFinalize(pass2, 1, level2, fields);
6340
+ const params = scope.params(FRONTIER_PARAMS, {
6341
+ wg,
6342
+ n,
6343
+ edgeCapacity: frontier.edgeCapacity,
6344
+ arcBase: 0,
6345
+ arcEnd: s.arcCount,
6346
+ bitsBase,
6347
+ stride: levelPlan.stride ?? wg
6348
+ });
6349
+ const boundContract = contract.bind({
6350
+ edgeQueue: frontier.edgeQueue,
6351
+ counters,
6352
+ depth,
6353
+ frontierOut: frontier.output,
6354
+ P: params.binding
6355
+ });
6356
+ contract.dispatch(pass2, boundContract, levelPlan, [params.offset]);
6357
+ for (const w of forward) {
6358
+ const fusedParams = scope.params(FRONTIER_PARAMS, {
6359
+ wg,
6360
+ n,
6361
+ edgeCapacity: frontier.edgeCapacity,
6362
+ arcBase: w.arcBase,
6363
+ arcEnd: w.arcEnd,
6364
+ stride: fusedPlan.x * fusedPlan.y
6365
+ });
6366
+ const boundFused = fused.bind({
6367
+ ...graphBindings(w.core, null),
6368
+ frontierIn: frontier.input,
6369
+ counters,
6370
+ depth,
6371
+ frontierOut: frontier.output,
6372
+ P: fusedParams.binding
6373
+ });
6374
+ fused.dispatch(pass2, boundFused, fusedPlan, [fusedParams.offset]);
6375
+ }
6376
+ fill.dispatch(pass2, boundBitsFill, bitsPlan, [bitsParams.offset]);
6377
+ const boundBitset = bitset.bind({
6378
+ frontierIn: frontier.input,
6379
+ counters,
6380
+ bits: sweepIn,
6381
+ P: params.binding
6382
+ });
6383
+ bitset.dispatch(pass2, boundBitset, levelPlan, [params.offset]);
6384
+ for (const w of backward) {
6385
+ const sweepParams = scope.params(FRONTIER_PARAMS, {
6386
+ wg,
6387
+ n,
6388
+ arcBase: w.arcBase,
6389
+ arcEnd: w.arcEnd,
6390
+ bitsBase,
6391
+ stride: sweepPlan.stride ?? wg
6392
+ });
6393
+ const boundSweep = bottomUp.bind({
6394
+ ...graphBindings(w.core, null),
6395
+ sweepIn,
6396
+ counters,
6397
+ depth,
6398
+ frontierOut: frontier.output,
6399
+ P: sweepParams.binding
6400
+ });
6401
+ bottomUp.dispatch(pass2, boundSweep, sweepPlan, [sweepParams.offset]);
6402
+ }
6403
+ frontier.swap();
6404
+ }
6405
+ batch.endPass();
6406
+ const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
6407
+ const inspect = tuning.onLevel === void 0 ? null : {
6408
+ block: batch.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength),
6409
+ frontier: batch.readback(frontier.input.buffer, frontier.input.offset, frontier.input.size),
6410
+ count: batch.readback(compactCount.buffer, compactCount.offset, 4)
6411
+ };
6412
+ const submitted = submit(batch);
6413
+ const back2 = await submitted.readback;
6414
+ levelsRecorded += levelsPerSubmit;
6415
+ submits += 1;
6416
+ ctx.assertReady();
6417
+ if (options?.signal?.aborted) {
6418
+ throw aborted$1(submitted.id);
6419
+ }
6420
+ options?.onProgress?.(Math.min(levelsRecorded, n), n);
6421
+ if (inspect !== null && tuning.onLevel !== void 0) {
6422
+ const block2 = FRONTIER_COUNTERS.read(new DataView(back2), inspect.block.offset);
6423
+ const claimed = new Uint32Array(back2, inspect.frontier.offset, wordOf$1(block2, "nextFrontierCount"));
6424
+ const rebuilt = new Uint32Array(back2, inspect.count.offset, 1)[0];
6425
+ tuning.onLevel(levelsRecorded - 1, block2, claimed.slice(), rebuilt);
6426
+ }
6427
+ if (new Uint32Array(back2, doneRequest.offset, 1)[0] !== 0) {
6428
+ break;
6429
+ }
6430
+ if (submits > n + 1) {
6431
+ throw new WebGpuGraphError(
6432
+ "E_VALIDATION",
6433
+ `${ALGORITHM$3}: the done flag never rose in ${submits} submits (a traversal has at most ${n} levels)`,
6434
+ { label: ALGORITHM$3, message: `the done flag never rose in ${submits} submits` }
6435
+ );
6436
+ }
6437
+ }
6438
+ const keys = bindingOf$1(scope.scratch(bytes, "order/keys"), bytes);
6439
+ const vals = bindingOf$1(scope.scratch(bytes, "order/vals"), bytes);
6440
+ const histBytes = radixHistBytes(n, wg);
6441
+ const scratch = {
6442
+ keys: bindingOf$1(scope.scratch(bytes, "order/keys-scratch"), bytes),
6443
+ vals: bindingOf$1(scope.scratch(bytes, "order/vals-scratch"), bytes),
6444
+ hist: bindingOf$1(scope.scratch(histBytes, "order/hist"), histBytes),
6445
+ offsets: bindingOf$1(scope.scratch(histBytes, "order/offsets"), histBytes)
6446
+ };
6447
+ const parent = bindingOf$1(scope.scratch(bytes, "parent"), bytes);
6448
+ const result = new CommandBatch(ctx, `${ALGORITHM$3}/result`);
6449
+ result.copy(depth, keys, bytes);
6450
+ const pass = result.pass("result");
6451
+ recordFill(pass, vals, 0, 1);
6452
+ const sorted = sort.record(pass, keys, vals, n, 32, scratch);
6453
+ recordFill(pass, parent, INVALID_INDEX, 0);
6454
+ const predPlan = planGridStride(n, wg, ctx.caps);
6455
+ for (const w of forward) {
6456
+ const predParams = scope.params(FRONTIER_PARAMS, {
6457
+ wg,
6458
+ n,
6459
+ arcBase: w.arcBase,
6460
+ arcEnd: w.arcEnd,
6461
+ predKind,
6462
+ source,
6463
+ stride: predPlan.stride ?? n
6464
+ });
6465
+ const predBound = pred.bind({
6466
+ ...graphBindings(w.core, null),
6467
+ dist: depth,
6468
+ pred: parent,
6469
+ P: predParams.binding
6470
+ });
6471
+ pred.dispatch(pass, predBound, predPlan, [predParams.offset]);
6472
+ }
6473
+ result.endPass();
6474
+ const depthRequest = result.readback(depth.buffer, depth.offset, bytes);
6475
+ const parentRequest = result.readback(parent.buffer, parent.offset, bytes);
6476
+ const orderRequest = result.readback(sorted.vals.buffer, sorted.vals.offset, bytes);
6477
+ const blockRequest = result.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength);
6478
+ const back = await submit(result).readback;
6479
+ ctx.assertReady();
6480
+ const block = FRONTIER_COUNTERS.read(new DataView(back), blockRequest.offset);
6481
+ const visitedCount = wordOf$1(block, "visitedCount");
6482
+ if (visitedCount > n) {
6483
+ throw new WebGpuGraphError(
6484
+ "E_VALIDATION",
6485
+ `${ALGORITHM$3}: visitedCount ${visitedCount} exceeds the ${n} vertices (a duplicate claim)`,
6486
+ {
6487
+ label: `${ALGORITHM$3}/visitedCount`,
6488
+ message: `the device counted ${visitedCount} visits of ${n} vertices`
6489
+ }
6490
+ );
6491
+ }
6492
+ const level = wordOf$1(block, "level");
6493
+ const levels = wordOf$1(block, "frontierCount") === 0 ? level : level + 1;
6494
+ const depthOut = dest ?? new Uint32Array(n);
6495
+ depthOut.set(new Uint32Array(back, depthRequest.offset, n));
6496
+ return {
6497
+ depth: depthOut,
6498
+ parent: new Uint32Array(back, parentRequest.offset, n).slice(),
6499
+ order: new Uint32Array(back, orderRequest.offset, visitedCount).slice(),
6500
+ visitedCount,
6501
+ levels,
6502
+ switches: wordOf$1(block, "switches")
6503
+ };
6504
+ } finally {
6505
+ scope.dispose();
6506
+ }
6507
+ }
6508
+ function breadthFirstSearch(ctx, s, source, options) {
6509
+ return bfsWithTuning(ctx, s, source, options, {});
6510
+ }
6511
+ const ALGORITHM$2 = "sssp";
6512
+ const HALF_ALIGN = 64;
6513
+ const RING_SLOTS$2 = 4 * MAX_LEVELS_PER_SUBMIT + 40;
6514
+ function bitsOf(value) {
6515
+ return new Uint32Array(Float32Array.of(value).buffer)[0];
6516
+ }
6517
+ function bindingOf(buffer, size) {
6518
+ return { buffer, offset: 0, size, window: null };
6519
+ }
6520
+ function aborted(algorithm, batchId) {
6521
+ return new WebGpuGraphError(
6522
+ "E_ABORTED",
6523
+ `${algorithm}: the signal was aborted`,
6524
+ batchId === void 0 ? {} : { batchId }
6525
+ );
6526
+ }
6527
+ function assertSource(algorithm, source, n) {
6528
+ if (!Number.isInteger(source) || source < 0 || source >= n) {
6529
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: source ${source} is outside [0, ${n})`, {
6530
+ argument: "source",
6531
+ value: source,
6532
+ expected: `an integer in [0, ${n})`
6533
+ });
6534
+ }
6535
+ }
6536
+ function wordOf(block, name) {
6537
+ const value = block[name];
6538
+ if (typeof value !== "number") {
6539
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$2}: counters.${name} did not decode to a number`, {
6540
+ label: `${ALGORITHM$2}/counters`,
6541
+ message: `the field ${name} did not decode to a number`
6542
+ });
6543
+ }
6544
+ return value;
6545
+ }
6546
+ function checkDest(algorithm, dest, n) {
6547
+ if (dest === void 0) {
6548
+ return null;
6549
+ }
6550
+ if (dest instanceof Float32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
6551
+ return dest;
6552
+ }
6553
+ throw new WebGpuGraphError(
6554
+ "E_INVALID_ARGUMENT",
6555
+ `${algorithm}: dest must be a Float32Array of length ${n} over an ArrayBuffer`,
6556
+ {
6557
+ argument: "dest",
6558
+ value: `${dest.constructor.name}(${dest.length})`,
6559
+ expected: `Float32Array(${n}) over an ArrayBuffer`
6560
+ }
6561
+ );
6562
+ }
6563
+ function normaliseCutoff(algorithm, cutoff) {
6564
+ if (cutoff === void 0) {
6565
+ return Infinity;
6566
+ }
6567
+ if (Number.isNaN(cutoff)) {
6568
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: cutoff must not be NaN`, {
6569
+ argument: "cutoff",
6570
+ value: cutoff,
6571
+ expected: "a number (the CPU port and the kernel would disagree on NaN)"
6572
+ });
6573
+ }
6574
+ return cutoff;
6575
+ }
6576
+ function resolveWeights$1(algorithm, s, weights) {
6577
+ if (weights === void 0) {
6578
+ if (s.weights === null) {
6579
+ return null;
6580
+ }
6581
+ let sum2 = 0;
6582
+ for (const w of s.weights) {
6583
+ sum2 += w;
6584
+ }
6585
+ return {
6586
+ values: s.weights,
6587
+ override: null,
6588
+ allOne: s.flags.allWeightsOne,
6589
+ nonNegative: s.flags.nonNegativeWeights,
6590
+ finite: s.flags.finiteWeights,
6591
+ sum: sum2
6592
+ };
6593
+ }
6594
+ if (weights.length !== s.arcCount) {
6595
+ throw new WebGpuGraphError(
6596
+ "E_INVALID_ARGUMENT",
6597
+ `${algorithm}: weights has ${weights.length} entries, the snapshot ${s.arcCount} arcs`,
6598
+ { argument: "weights", value: weights.length, expected: s.arcCount }
6599
+ );
6600
+ }
6601
+ const values = weights instanceof Float32Array ? weights : Float32Array.from(weights);
6602
+ let allOne = true;
6603
+ let nonNegative = true;
6604
+ let finite2 = true;
6605
+ let sum = 0;
6606
+ for (const w of values) {
6607
+ if (w !== 1) {
6608
+ allOne = false;
6609
+ }
6610
+ if (w < 0) {
6611
+ nonNegative = false;
6612
+ }
6613
+ if (!Number.isFinite(w)) {
6614
+ finite2 = false;
6615
+ }
6616
+ sum += w;
6617
+ }
6618
+ return { values, override: values, allOne, nonNegative, finite: finite2, sum };
6619
+ }
6620
+ async function unitWeightRoute(ctx, s, source, cutoff, dest, options) {
6621
+ const maxDepth = cutoff === Infinity ? void 0 : Math.floor(cutoff);
6622
+ const bfs = await bfsWithTuning(
6623
+ ctx,
6624
+ s,
6625
+ source,
6626
+ { maxDepth, signal: options?.signal, onProgress: options?.onProgress },
6627
+ { predKind: 0 }
6628
+ );
6629
+ const dist = dest ?? new Float32Array(s.nodeCount);
6630
+ for (let v = 0; v < s.nodeCount; v++) {
6631
+ const d = bfs.depth[v];
6632
+ dist[v] = d === INVALID_INDEX ? Infinity : d;
6633
+ }
6634
+ return { dist, predArc: bfs.parent, reachedCount: bfs.visitedCount };
6635
+ }
6636
+ function predBufferWords(n) {
6637
+ return 2 * Math.ceil(n / 64) * 64 + 64;
6638
+ }
6639
+ async function predecessorPass(input) {
6640
+ const { algorithm, ctx, scope, predKernel, recordFill, graph, dist, pred, n, arcCount, source, mode } = input;
6641
+ const wg = ctx.workgroupSize;
6642
+ const { queue } = ctx.device;
6643
+ const bytes = 4 * n;
6644
+ const hb = Math.ceil(n / 64) * 64;
6645
+ const predArcs = { buffer: pred.buffer, offset: pred.offset, size: bytes, window: null };
6646
+ queue.writeBuffer(pred.buffer, pred.offset + 4 * (hb + source), Uint32Array.of(0));
6647
+ queue.writeBuffer(pred.buffer, pred.offset + 4 * 2 * hb, new Uint32Array(2));
6648
+ const predPlan = planGridStride(n, wg, ctx.caps);
6649
+ const predFields = {
6650
+ wg,
6651
+ n,
6652
+ arcBase: 0,
6653
+ arcEnd: arcCount,
6654
+ predKind: 0,
6655
+ source,
6656
+ stride: predPlan.stride ?? n,
6657
+ mode
6658
+ };
6659
+ const maxBatches = Math.ceil((n + 1) / MAX_LEVELS_PER_SUBMIT) + 1;
6660
+ let batches = 0;
6661
+ for (; ; ) {
6662
+ const batch = new CommandBatch(ctx, `${algorithm}/pred`);
6663
+ const pass = batch.pass("pred");
6664
+ const recordRole = (role, iteration) => {
6665
+ const params = scope.params(FRONTIER_PARAMS, { ...predFields, role, iteration });
6666
+ const bound = predKernel.bind({ ...graph, dist, pred, P: params.binding });
6667
+ predKernel.dispatch(pass, bound, predPlan, [params.offset]);
6668
+ };
6669
+ if (batches === 0 && mode === 0) {
6670
+ recordRole(0, 0);
6671
+ }
6672
+ for (let iteration = 0; iteration < MAX_LEVELS_PER_SUBMIT; iteration++) {
6673
+ recordRole(1, iteration);
6674
+ }
6675
+ recordFill(pass, predArcs, n, INVALID_INDEX);
6676
+ recordRole(2, 0);
6677
+ batch.endPass();
6678
+ const distRequest = batch.readback(dist.buffer, dist.offset, bytes);
6679
+ const predRequest = batch.readback(pred.buffer, pred.offset, bytes);
6680
+ const flagsRequest = batch.readback(pred.buffer, pred.offset + 4 * 2 * hb, 8);
6681
+ scope.flush();
6682
+ const back = await batch.submit().readback;
6683
+ ctx.assertReady();
6684
+ batches += 1;
6685
+ const [changed, orphans] = new Uint32Array(back, flagsRequest.offset, 2);
6686
+ if (changed < MAX_LEVELS_PER_SUBMIT) {
6687
+ return {
6688
+ dist: new Float32Array(back, distRequest.offset, n),
6689
+ predArc: new Uint32Array(back, predRequest.offset, n).slice(),
6690
+ orphans
6691
+ };
6692
+ }
6693
+ if (batches > maxBatches) {
6694
+ throw new WebGpuGraphError(
6695
+ "E_VALIDATION",
6696
+ `${algorithm}: the hop passes still changed something after ${batches} batches (a hop count is at most ${n})`,
6697
+ { label: `${algorithm}/pred`, message: `the hop passes did not converge in ${batches} batches` }
6698
+ );
6699
+ }
6700
+ queue.writeBuffer(pred.buffer, pred.offset + 4 * 2 * hb, new Uint32Array(2));
6701
+ }
6702
+ }
6703
+ async function ssspWithTuning(ctx, s, source, options, tuning) {
6704
+ ctx.assertReady();
6705
+ await assertDeviceComputes(ctx);
6706
+ const n = s.nodeCount;
6707
+ assertSource(ALGORITHM$2, source, n);
6708
+ const roundsPerSubmit = tuning.roundsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
6709
+ if (!Number.isInteger(roundsPerSubmit) || roundsPerSubmit < 1 || roundsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
6710
+ throw new WebGpuGraphError(
6711
+ "E_INVALID_ARGUMENT",
6712
+ `${ALGORITHM$2}: roundsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
6713
+ {
6714
+ argument: "roundsPerSubmit",
6715
+ value: roundsPerSubmit,
6716
+ expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
6717
+ }
6718
+ );
6719
+ }
6720
+ if (tuning.delta !== void 0 && !(Number.isFinite(tuning.delta) && tuning.delta > 0)) {
6721
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM$2}: delta must be a finite positive number`, {
6722
+ argument: "delta",
6723
+ value: tuning.delta,
6724
+ expected: "a finite positive number"
6725
+ });
6726
+ }
6727
+ const dest = checkDest(ALGORITHM$2, options?.dest, n);
6728
+ const vector2 = resolveWeights$1(ALGORITHM$2, s, options?.weights);
6729
+ const cutoff = normaliseCutoff(ALGORITHM$2, options?.cutoff);
6730
+ if (options?.signal?.aborted) {
6731
+ throw aborted(ALGORITHM$2);
6732
+ }
6733
+ if (vector2 === null || vector2.allOne) {
6734
+ return unitWeightRoute(ctx, s, source, cutoff, dest, options);
6735
+ }
6736
+ if (!vector2.nonNegative) {
6737
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM$2}: a negative weight has no shortest path here`, {
6738
+ feature: "sssp.negativeWeights",
6739
+ hint: "use bellmanFord"
6740
+ });
6741
+ }
6742
+ if (!vector2.finite) {
6743
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM$2}: a NaN or infinite weight has no bit-pattern order`, {
6744
+ feature: "sssp.nonFiniteWeights"
6745
+ });
6746
+ }
6747
+ const { arcCount } = s;
6748
+ const core = ctx.residency.core(s);
6749
+ const limit = ctx.caps.limits.maxStorageBufferBindingSize;
6750
+ assertWholeCore(core, arcCount, limit, ALGORITHM$2);
6751
+ const cap = Math.ceil(Math.max(1, arcCount) / HALF_ALIGN) * HALF_ALIGN;
6752
+ if (8 * cap > limit) {
6753
+ throw new WebGpuGraphError(
6754
+ "E_TOO_LARGE",
6755
+ `${ALGORITHM$2}: the near-far queue of ${cap} entries per half needs ${8 * cap} bytes, above the ${limit}-byte binding limit (the relax is never windowed)`,
6756
+ { needed: 8 * cap, limit, path: "sssp.queue", algorithm: ALGORITHM$2 }
6757
+ );
6758
+ }
6759
+ const delta = Math.fround(
6760
+ tuning.delta ?? (vector2.sum === 0 ? 1 : SSSP_DELTA_FACTOR * (vector2.sum / arcCount) / (arcCount / n))
6761
+ );
6762
+ const deltaBits = bitsOf(delta);
6763
+ const maxRounds = n + Math.ceil(vector2.sum / delta) + 1;
6764
+ const maxSubmits = Math.ceil((maxRounds + 1) / roundsPerSubmit) + 1;
6765
+ const scope = algorithmScope(ctx, ALGORITHM$2, RING_SLOTS$2);
6766
+ try {
6767
+ const wg = ctx.workgroupSize;
6768
+ const bytes = 4 * n;
6769
+ const dist = bindingOf(scope.scratch(bytes, "dist"), bytes);
6770
+ const owner = bindingOf(scope.scratch(bytes, "owner"), bytes);
6771
+ const queueOut = bindingOf(scope.scratch(8 * cap, "queue-out"), 8 * cap);
6772
+ const nearHalf = { buffer: queueOut.buffer, offset: 0, size: 4 * cap, window: null };
6773
+ const farHalf = { buffer: queueOut.buffer, offset: 4 * cap, size: 4 * cap, window: null };
6774
+ const predWords = predBufferWords(n);
6775
+ const pred = bindingOf(scope.scratch(4 * predWords, "pred"), 4 * predWords);
6776
+ const { queue } = ctx.device;
6777
+ let weightsBinding;
6778
+ if (vector2.override !== null) {
6779
+ const uploaded = scope.scratch(4 * arcCount, "weights");
6780
+ queue.writeBuffer(uploaded, 0, vector2.override);
6781
+ weightsBinding = bindingOf(uploaded, 4 * arcCount);
6782
+ }
6783
+ await ctx.allocator.check();
6784
+ const planner = await prepareFrontier(scope, n, arcCount, cap);
6785
+ const compact = await prepareCompact(scope);
6786
+ const overrides = graphOverrides(core, null, weightsBinding);
6787
+ const relax = await ctx.pipelines.kernel(kernelSpec("sssp-relax", overrides));
6788
+ const predKernel = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...overrides, MODE: 0 }));
6789
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
6790
+ const graph = graphBindings(core, null, weightsBinding);
6791
+ const { frontier } = planner;
6792
+ const { counters } = frontier;
6793
+ const nearIn = frontier.vertices[0];
6794
+ const farIn = frontier.vertices[1];
6795
+ const recordFill = (pass, dst, count, value) => {
6796
+ const params = scope.params(FILL_PARAMS, { count, value, mode: 0, pad0: 0 });
6797
+ fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
6798
+ };
6799
+ const submit = (batch) => {
6800
+ scope.flush();
6801
+ return batch.submit();
6802
+ };
6803
+ const setup = new CommandBatch(ctx, `${ALGORITHM$2}/setup`);
6804
+ const setupPass = setup.pass("fill");
6805
+ recordFill(setupPass, dist, n, F32_INF_BITS);
6806
+ recordFill(setupPass, pred, predWords, INVALID_INDEX);
6807
+ setup.endPass();
6808
+ await submit(setup).readback;
6809
+ ctx.assertReady();
6810
+ queue.writeBuffer(dist.buffer, dist.offset + 4 * source, Uint32Array.of(0));
6811
+ frontier.reset(queue, source, { nextFrontierCount: 1, thresholdBits: deltaBits, deltaBits });
6812
+ queue.writeBuffer(queueOut.buffer, queueOut.offset, Uint32Array.of(source));
6813
+ const nearDedupe = {
6814
+ queue: nearHalf,
6815
+ count: cap,
6816
+ countIndex: W.edgeCount,
6817
+ // role 2 writes the raw near count here when it chooses a near round, 0 otherwise
6818
+ counters,
6819
+ owner,
6820
+ out: nearIn,
6821
+ outCount: counters,
6822
+ outIndex: W.frontierCount
6823
+ };
6824
+ const farDedupe = {
6825
+ queue: farHalf,
6826
+ count: cap,
6827
+ countIndex: W.edgeCountUnclamped,
6828
+ // and the raw far count here on a far round
6829
+ counters,
6830
+ owner,
6831
+ out: farIn,
6832
+ outCount: counters,
6833
+ outIndex: W.farCount
6834
+ };
6835
+ const relaxPlan = planGridStride(n, wg, ctx.caps);
6836
+ const relaxFields = {
6837
+ wg,
6838
+ n,
6839
+ edgeCapacity: cap,
6840
+ arcBase: 0,
6841
+ arcEnd: arcCount,
6842
+ cutoffBits: bitsOf(cutoff),
6843
+ source,
6844
+ stride: relaxPlan.stride ?? wg
6845
+ };
6846
+ let roundsRecorded = 0;
6847
+ let submits = 0;
6848
+ for (; ; ) {
6849
+ const batch = new CommandBatch(ctx, `${ALGORITHM$2}/rounds`);
6850
+ const pass = batch.pass("sssp");
6851
+ const near = scope.params(FRONTIER_PARAMS, { ...relaxFields, role: 0 });
6852
+ const far = scope.params(FRONTIER_PARAMS, { ...relaxFields, role: 1 });
6853
+ const boundNear = relax.bind({ ...graph, dist, counters, queueIn: nearIn, queueOut, P: near.binding });
6854
+ const boundFar = relax.bind({ ...graph, dist, counters, queueIn: farIn, queueOut, P: far.binding });
6855
+ for (let round = 0; round < roundsPerSubmit; round++) {
6856
+ planner.recordFinalize(pass, 2, round, {});
6857
+ compact.recordDedupe(pass, nearDedupe);
6858
+ compact.recordDedupe(pass, farDedupe);
6859
+ planner.recordFinalize(pass, 3, round, {});
6860
+ relax.dispatch(pass, boundNear, relaxPlan, [near.offset]);
6861
+ relax.dispatch(pass, boundFar, relaxPlan, [far.offset]);
6862
+ }
6863
+ batch.endPass();
6864
+ const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
6865
+ const inspect = tuning.onRound === void 0 ? null : batch.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength);
6866
+ const submitted = submit(batch);
6867
+ const back = await submitted.readback;
6868
+ roundsRecorded += roundsPerSubmit;
6869
+ submits += 1;
6870
+ ctx.assertReady();
6871
+ if (options?.signal?.aborted) {
6872
+ throw aborted(ALGORITHM$2, submitted.id);
6873
+ }
6874
+ options?.onProgress?.(Math.min(roundsRecorded, maxRounds), maxRounds);
6875
+ if (inspect !== null && tuning.onRound !== void 0) {
6876
+ tuning.onRound(roundsRecorded - 1, FRONTIER_COUNTERS.read(new DataView(back), inspect.offset));
6877
+ }
6878
+ const done = new Uint32Array(back, doneRequest.offset, 1)[0];
6879
+ if (done === 1) {
6880
+ break;
6881
+ }
6882
+ if (done !== 0) {
6883
+ const block = FRONTIER_COUNTERS.read(
6884
+ new DataView(
6885
+ await ctx.readback.read(
6886
+ counters.buffer,
6887
+ FRONTIER_COUNTERS.byteLength,
6888
+ void 0,
6889
+ counters.offset
6890
+ )
6891
+ )
6892
+ );
6893
+ if (done === 2) {
6894
+ const needed = Math.max(wordOf(block, "nextFrontierCount"), wordOf(block, "nextFarCount"));
6895
+ throw new WebGpuGraphError(
6896
+ "E_TOO_LARGE",
6897
+ `${ALGORITHM$2}: a raw pile of ${needed} entries overflowed its ${cap}-entry half`,
6898
+ { needed, limit: cap, path: "sssp.pile", algorithm: ALGORITHM$2 }
6899
+ );
6900
+ }
6901
+ throw new WebGpuGraphError(
6902
+ "E_UNSUPPORTED",
6903
+ `${ALGORITHM$2}: the f32 threshold ${wordOf(block, "thresholdBits")} absorbed the delta ${deltaBits} (as bit patterns); the far pile can no longer be bucketed`,
6904
+ { feature: "sssp.thresholdAbsorbed", hint: "the distances outgrew the delta's f32 precision" }
6905
+ );
6906
+ }
6907
+ if (submits > maxSubmits) {
6908
+ throw new WebGpuGraphError(
6909
+ "E_VALIDATION",
6910
+ `${ALGORITHM$2}: the done flag never rose in ${submits} submits (at most ${maxRounds} rounds)`,
6911
+ { label: `${ALGORITHM$2}/rounds`, message: `the done flag never rose in ${submits} submits` }
6912
+ );
6913
+ }
6914
+ }
6915
+ const passed = await predecessorPass({
6916
+ algorithm: ALGORITHM$2,
6917
+ ctx,
6918
+ scope,
6919
+ predKernel,
6920
+ recordFill,
6921
+ graph,
6922
+ dist,
6923
+ pred,
6924
+ n,
6925
+ arcCount,
6926
+ source,
6927
+ mode: 0
6928
+ });
6929
+ if (passed.orphans !== 0) {
6930
+ throw new WebGpuGraphError(
6931
+ "E_VALIDATION",
6932
+ `${ALGORITHM$2}: ${passed.orphans} reached node(s) the predecessor key never reached (a kernel bug)`,
6933
+ { label: `${ALGORITHM$2}/pred`, message: `${passed.orphans} orphan(s) in the predecessor pass` }
6934
+ );
6935
+ }
6936
+ const distOut = dest ?? new Float32Array(n);
6937
+ distOut.set(passed.dist);
6938
+ let reachedCount = 0;
6939
+ for (const d of distOut) {
6940
+ if (d !== Infinity) {
6941
+ reachedCount += 1;
6942
+ }
6943
+ }
6944
+ return { dist: distOut, predArc: passed.predArc, reachedCount };
6945
+ } finally {
6946
+ scope.dispose();
6947
+ }
6948
+ }
6949
+ function sssp(ctx, s, source, options) {
6950
+ return ssspWithTuning(ctx, s, source, options, {});
6951
+ }
6952
+ const ALGORITHM$1 = "bellmanFord";
6953
+ const ROUNDS_PER_BATCH = 8;
6954
+ const MAX_RETRIES = 16;
6955
+ const RING_SLOTS$1 = MAX_LEVELS_PER_SUBMIT + 16;
6956
+ function assertSymmetric(s, vector2) {
6957
+ const { arcToEdge, edgeToArc } = s;
6958
+ for (let a = 0; a < s.arcCount; a++) {
6959
+ const forward = edgeToArc[arcToEdge[a]];
6960
+ if (vector2[a] !== vector2[forward]) {
6961
+ throw new WebGpuGraphError(
6962
+ "E_UNSUPPORTED",
6963
+ `${ALGORITHM$1}: weights[${a}] = ${vector2[a]} differs from weights[${forward}] = ${vector2[forward]}, the forward arc of the same undirected edge; the kernel reads one weight per edge`,
6964
+ { feature: "bellmanFord.asymmetricUndirectedWeights", hint: "use a directed snapshot" }
6965
+ );
6966
+ }
6967
+ }
6968
+ }
6969
+ async function bellmanFordWithTuning(ctx, s, source, options, tuning) {
6970
+ ctx.assertReady();
6971
+ await assertDeviceComputes(ctx);
6972
+ const n = s.nodeCount;
6973
+ assertSource(ALGORITHM$1, source, n);
6974
+ const maxRetries = tuning.maxRetries ?? MAX_RETRIES;
6975
+ if (!Number.isInteger(maxRetries) || maxRetries < 1) {
6976
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM$1}: maxRetries must be an integer >= 1`, {
6977
+ argument: "maxRetries",
6978
+ value: maxRetries,
6979
+ expected: "an integer >= 1"
6980
+ });
6981
+ }
6982
+ const roundsPerBatch = tuning.roundsPerBatch ?? ROUNDS_PER_BATCH;
6983
+ if (!Number.isInteger(roundsPerBatch) || roundsPerBatch < 1 || roundsPerBatch > MAX_LEVELS_PER_SUBMIT) {
6984
+ throw new WebGpuGraphError(
6985
+ "E_INVALID_ARGUMENT",
6986
+ `${ALGORITHM$1}: roundsPerBatch must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
6987
+ {
6988
+ argument: "roundsPerBatch",
6989
+ value: roundsPerBatch,
6990
+ expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
6991
+ }
6992
+ );
6993
+ }
6994
+ const dest = checkDest(ALGORITHM$1, options?.dest, n);
6995
+ const vector2 = resolveWeights$1(ALGORITHM$1, s, options?.weights);
6996
+ const cutoff = normaliseCutoff(ALGORITHM$1, options?.cutoff);
6997
+ if (options?.signal?.aborted) {
6998
+ throw aborted(ALGORITHM$1);
6999
+ }
7000
+ if (vector2 === null || vector2.allOne) {
7001
+ const unit = await unitWeightRoute(ctx, s, source, cutoff, dest, options);
7002
+ return { result: { ...unit, hasNegativeCycle: false }, rounds: 0, retryExhaustedRounds: 0 };
7003
+ }
7004
+ if (!vector2.finite) {
7005
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM$1}: a NaN or infinite weight has no shortest path`, {
7006
+ feature: "bellmanFord.nonFiniteWeights"
7007
+ });
7008
+ }
7009
+ if (!s.directed && vector2.override !== null) {
7010
+ assertSymmetric(s, vector2.override);
7011
+ }
7012
+ const { arcCount } = s;
7013
+ const core = ctx.residency.core(s, ["rowPtr", "colIdx", "weights", "edgeToArc"]);
7014
+ assertWholeCore(core, arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM$1);
7015
+ const edges = ctx.residency.view(s, "edgeList");
7016
+ const edgeCount = edges.scalars.edgeCount[0];
7017
+ const scope = algorithmScope(ctx, ALGORITHM$1, RING_SLOTS$1);
7018
+ try {
7019
+ const wg = ctx.workgroupSize;
7020
+ const bytes = 4 * n;
7021
+ const dist = bindingOf(scope.scratch(bytes, "dist"), bytes);
7022
+ const predWords = predBufferWords(n);
7023
+ const pred = bindingOf(scope.scratch(4 * predWords, "pred"), 4 * predWords);
7024
+ const flags = bindingOf(scope.scratch(BF_FLAGS.byteLength, "flags"), BF_FLAGS.byteLength);
7025
+ let iota = null;
7026
+ let { edgeToArc } = core;
7027
+ if (edgeToArc === null) {
7028
+ iota = bindingOf(scope.scratch(4 * edgeCount, "iota"), 4 * edgeCount);
7029
+ edgeToArc = iota;
7030
+ }
7031
+ const { queue } = ctx.device;
7032
+ let weightsBinding;
7033
+ if (vector2.override !== null) {
7034
+ const uploaded = scope.scratch(4 * arcCount, "weights");
7035
+ queue.writeBuffer(uploaded, 0, vector2.override);
7036
+ weightsBinding = bindingOf(uploaded, 4 * arcCount);
7037
+ }
7038
+ await ctx.allocator.check();
7039
+ const overrides = graphOverrides(core, null, weightsBinding);
7040
+ const relax = await ctx.pipelines.kernel(kernelSpec("bf-relax", { UNDIRECTED: !s.directed }));
7041
+ const predKernel = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...overrides, MODE: 0 }));
7042
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
7043
+ const graph = graphBindings(core, null, weightsBinding);
7044
+ const recordFill = (pass, dst, count, value, mode = 0) => {
7045
+ const params = scope.params(FILL_PARAMS, { count, value, mode, pad0: 0 });
7046
+ fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
7047
+ };
7048
+ const submit = (batch) => {
7049
+ scope.flush();
7050
+ return batch.submit();
7051
+ };
7052
+ const setup = new CommandBatch(ctx, `${ALGORITHM$1}/setup`);
7053
+ const setupPass = setup.pass("fill");
7054
+ recordFill(setupPass, dist, n, F32_INF_BITS);
7055
+ recordFill(setupPass, pred, predWords, INVALID_INDEX);
7056
+ if (iota !== null) {
7057
+ recordFill(setupPass, iota, edgeCount, 0, 1);
7058
+ }
7059
+ setup.endPass();
7060
+ await submit(setup).readback;
7061
+ ctx.assertReady();
7062
+ queue.writeBuffer(dist.buffer, dist.offset + 4 * source, Uint32Array.of(0));
7063
+ const edgePlan = planGridStride(edgeCount, wg, ctx.caps);
7064
+ const relaxBindings = {
7065
+ edgeSrc: edges.bindings.src,
7066
+ edgeDst: edges.bindings.dst,
7067
+ edgeToArc,
7068
+ weights: graph.weights,
7069
+ dist,
7070
+ flags
7071
+ };
7072
+ const relaxFields = {
7073
+ edgeCount,
7074
+ stride: edgePlan.stride ?? edgeCount,
7075
+ maxRetries,
7076
+ cutoffBits: bitsOf(cutoff)
7077
+ };
7078
+ const zero = new Uint32Array(BF_FLAGS.byteLength / 4);
7079
+ const runRounds = async (count, label) => {
7080
+ queue.writeBuffer(flags.buffer, flags.offset, zero);
7081
+ const batch = new CommandBatch(ctx, `${ALGORITHM$1}/${label}`);
7082
+ const pass = batch.pass("relax");
7083
+ const params = scope.params(BF_PARAMS, relaxFields);
7084
+ const bound = relax.bind({ ...relaxBindings, P: params.binding });
7085
+ for (let round = 0; round < count; round++) {
7086
+ relax.dispatch(pass, bound, edgePlan, [params.offset]);
7087
+ }
7088
+ batch.endPass();
7089
+ const request = batch.readback(flags.buffer, flags.offset, BF_FLAGS.byteLength);
7090
+ const submitted = submit(batch);
7091
+ const back = await submitted.readback;
7092
+ ctx.assertReady();
7093
+ const block = BF_FLAGS.read(new DataView(back), request.offset);
7094
+ return { changed: Number(block.changed), retryExhausted: Number(block.retryExhausted), id: submitted.id };
7095
+ };
7096
+ let rounds = 0;
7097
+ let retryExhaustedRounds = 0;
7098
+ let hasNegativeCycle = false;
7099
+ for (; ; ) {
7100
+ const remaining = n - 1 - rounds;
7101
+ if (remaining <= 0) {
7102
+ const decision = await runRounds(1, "decision");
7103
+ if (decision.retryExhausted !== 0) {
7104
+ throw new WebGpuGraphError(
7105
+ "E_VALIDATION",
7106
+ `${ALGORITHM$1}: a lane exhausted the ${maxRetries}-retry compare-exchange bound in the decision round, so its change is not a verdict`,
7107
+ {
7108
+ label: `${ALGORITHM$1}/retry`,
7109
+ message: "retryExhausted in the decision round",
7110
+ batchId: decision.id
7111
+ }
7112
+ );
7113
+ }
7114
+ hasNegativeCycle = decision.changed !== 0;
7115
+ break;
7116
+ }
7117
+ const count = Math.min(roundsPerBatch, remaining);
7118
+ const batch = await runRounds(count, "rounds");
7119
+ rounds += count;
7120
+ if (batch.retryExhausted !== 0) {
7121
+ retryExhaustedRounds += 1;
7122
+ }
7123
+ if (options?.signal?.aborted) {
7124
+ throw aborted(ALGORITHM$1, batch.id);
7125
+ }
7126
+ options?.onProgress?.(rounds, n);
7127
+ if (batch.changed === 0 && batch.retryExhausted === 0) {
7128
+ break;
7129
+ }
7130
+ }
7131
+ const passed = await predecessorPass({
7132
+ algorithm: ALGORITHM$1,
7133
+ ctx,
7134
+ scope,
7135
+ predKernel,
7136
+ recordFill,
7137
+ graph,
7138
+ dist,
7139
+ pred,
7140
+ n,
7141
+ arcCount,
7142
+ source,
7143
+ mode: 1
7144
+ });
7145
+ if (passed.orphans !== 0 && !hasNegativeCycle) {
7146
+ throw new WebGpuGraphError(
7147
+ "E_UNSUPPORTED",
7148
+ `${ALGORITHM$1}: ${passed.orphans} reached node(s) the tight subgraph never reaches (a cycle of weights below one f32 ulp relaxed once)`,
7149
+ {
7150
+ feature: "bellmanFord.roundedCycle",
7151
+ hint: "a cycle of weights below one f32 ulp relaxed once at a distance above 2^24; scale the weights or shorten the distances"
7152
+ }
7153
+ );
7154
+ }
7155
+ const distOut = dest ?? new Float32Array(n);
7156
+ distOut.set(passed.dist);
7157
+ let reachedCount = 0;
7158
+ for (const d of distOut) {
7159
+ if (d !== Infinity) {
7160
+ reachedCount += 1;
7161
+ }
7162
+ }
7163
+ return {
7164
+ result: { dist: distOut, predArc: passed.predArc, reachedCount, hasNegativeCycle },
7165
+ rounds,
7166
+ retryExhaustedRounds
7167
+ };
7168
+ } finally {
7169
+ scope.dispose();
7170
+ }
7171
+ }
7172
+ async function bellmanFord(ctx, s, source, options) {
7173
+ return (await bellmanFordWithTuning(ctx, s, source, options, {})).result;
7174
+ }
7175
+ const ALGORITHM = "closenessCentrality";
7176
+ const SOURCES_PER_BATCH = 32;
7177
+ const PER_SOURCE_WORDS = 4 * SOURCES_PER_BATCH;
7178
+ const RING_SLOTS = 8 * MAX_LEVELS_PER_SUBMIT + 16;
7179
+ function reusingScratch(scope) {
7180
+ const held = /* @__PURE__ */ new Map();
7181
+ return {
7182
+ ...scope,
7183
+ scratch: (byteLength, label) => {
7184
+ const key = `${label}/${byteLength}`;
7185
+ let buffer = held.get(key);
7186
+ if (buffer === void 0) {
7187
+ buffer = scope.scratch(byteLength, label);
7188
+ held.set(key, buffer);
7189
+ }
7190
+ return buffer;
7191
+ }
7192
+ };
7193
+ }
7194
+ async function weightedRoute(ctx, s, scores, options) {
7195
+ const n = s.nodeCount;
7196
+ for (let source = 0; source < n; source++) {
7197
+ if (options?.signal?.aborted) {
7198
+ throw aborted(ALGORITHM);
7199
+ }
7200
+ const { dist } = await sssp(ctx, s, source, { signal: options?.signal });
7201
+ let sum = 0;
7202
+ for (let v = 0; v < n; v++) {
7203
+ const d = dist[v];
7204
+ if (v !== source && d !== Infinity) {
7205
+ sum += d;
7206
+ }
7207
+ }
7208
+ scores[source] = sum === 0 ? 0 : 1 / sum;
7209
+ options?.onProgress?.(source + 1, n);
7210
+ }
7211
+ return { scores, iterations: n, converged: true, precision: "f32" };
7212
+ }
7213
+ async function sweepRoute(ctx, s, scores, levelsPerSubmit, options, tuning) {
7214
+ const n = s.nodeCount;
7215
+ if (n === 0) {
7216
+ return { scores, iterations: 0, converged: true, precision: "f32" };
7217
+ }
7218
+ const core = ctx.residency.core(s);
7219
+ assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM);
7220
+ const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS);
7221
+ try {
7222
+ const wg = ctx.workgroupSize;
7223
+ const bytes = 4 * n;
7224
+ const bitsBase = Math.ceil(n / 64) * 64;
7225
+ const regionBytes = 4 * bitsBase;
7226
+ const bits = bindingOf(scope.scratch(4 * regionBytes, "bits"), 4 * regionBytes);
7227
+ const region = (index) => ({
7228
+ buffer: bits.buffer,
7229
+ offset: index * regionBytes,
7230
+ size: regionBytes,
7231
+ window: null
7232
+ });
7233
+ const flags = region(3);
7234
+ const frontierList = bindingOf(scope.scratch(bytes, "frontier-list"), bytes);
7235
+ const iota = bindingOf(scope.scratch(bytes, "iota"), bytes);
7236
+ const counters = bindingOf(
7237
+ scope.scratch(FRONTIER_COUNTERS.byteLength, "counters"),
7238
+ FRONTIER_COUNTERS.byteLength
7239
+ );
7240
+ const perSourceBytes = 4 * PER_SOURCE_WORDS;
7241
+ const perSource = bindingOf(scope.scratch(perSourceBytes, "per-source"), perSourceBytes);
7242
+ await ctx.allocator.check();
7243
+ const compact = await prepareCompact(reusingScratch(scope));
7244
+ const sweep = await ctx.pipelines.kernel(kernelSpec("closeness-sweep", graphOverrides(core, null)));
7245
+ const reduce = await ctx.pipelines.kernel(kernelSpec("closeness-reduce"));
7246
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
7247
+ const graph = graphBindings(core, null);
7248
+ const onePlan = plan1d(1, wg, ctx.caps);
7249
+ const regionPlan = plan1d(bitsBase, wg, ctx.caps);
7250
+ const sweepPlan = planGridStride(n, wg, ctx.caps);
7251
+ const recordFill = (pass, dst, count, mode) => {
7252
+ const params = scope.params(FILL_PARAMS, { count, value: 0, mode, pad0: 0 });
7253
+ fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
7254
+ };
7255
+ const submit = (batch) => {
7256
+ scope.flush();
7257
+ return batch.submit();
7258
+ };
7259
+ const setup = new CommandBatch(ctx, `${ALGORITHM}/setup`);
7260
+ recordFill(setup.pass("fill"), iota, n, 1);
7261
+ setup.endPass();
7262
+ await submit(setup).readback;
7263
+ ctx.assertReady();
7264
+ let batches = 0;
7265
+ for (let batchStart = 0; batchStart < n; batchStart += SOURCES_PER_BATCH) {
7266
+ let level = 0;
7267
+ for (let first = true; ; first = false) {
7268
+ const batch = new CommandBatch(ctx, `${ALGORITHM}/levels`);
7269
+ const pass = batch.pass("closeness");
7270
+ if (first) {
7271
+ recordFill(pass, bits, 4 * bitsBase, 0);
7272
+ recordFill(pass, perSource, PER_SOURCE_WORDS, 0);
7273
+ const seed = scope.params(FRONTIER_PARAMS, { role: 1, n, bitsBase, source: batchStart });
7274
+ reduce.dispatch(pass, reduce.bind({ counters, perSource, bits, P: seed.binding }), onePlan, [
7275
+ seed.offset
7276
+ ]);
7277
+ }
7278
+ const boundary = scope.params(FRONTIER_PARAMS, { role: 0, n, bitsBase });
7279
+ const boundBoundary = reduce.bind({ counters, perSource, bits, P: boundary.binding });
7280
+ const clear = scope.params(FILL_PARAMS, { count: bitsBase, value: 0, mode: 0, pad0: 0 });
7281
+ const boundClearNext = [region(2), region(1)].map((dst) => fill.bind({ dst, P: clear.binding }));
7282
+ const boundClearFlags = fill.bind({ dst: flags, P: clear.binding });
7283
+ const boundSweep = [0, 1].map((mode) => {
7284
+ const params = scope.params(FRONTIER_PARAMS, {
7285
+ wg,
7286
+ n,
7287
+ bitsBase,
7288
+ arcBase: 0,
7289
+ arcEnd: s.arcCount,
7290
+ mode,
7291
+ stride: sweepPlan.stride ?? wg
7292
+ });
7293
+ return {
7294
+ bound: sweep.bind({ ...graph, frontierList, counters, bits, perSource, P: params.binding }),
7295
+ offset: params.offset
7296
+ };
7297
+ });
7298
+ for (let k = 0; k < levelsPerSubmit; k++, level++) {
7299
+ const parity = level % 2;
7300
+ reduce.dispatch(pass, boundBoundary, onePlan, [boundary.offset]);
7301
+ compact.record(pass, {
7302
+ queue: iota,
7303
+ flags,
7304
+ count: n,
7305
+ out: frontierList,
7306
+ outCount: counters,
7307
+ outIndex: W.frontierCount
7308
+ });
7309
+ fill.dispatch(pass, boundClearNext[parity], regionPlan, [clear.offset]);
7310
+ fill.dispatch(pass, boundClearFlags, regionPlan, [clear.offset]);
7311
+ sweep.dispatch(pass, boundSweep[parity].bound, sweepPlan, [boundSweep[parity].offset]);
7312
+ }
7313
+ batch.endPass();
7314
+ const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
7315
+ const blockRequest = batch.readback(perSource.buffer, perSource.offset, perSourceBytes);
7316
+ const submitted = submit(batch);
7317
+ const back = await submitted.readback;
7318
+ ctx.assertReady();
7319
+ if (options?.signal?.aborted) {
7320
+ throw aborted(ALGORITHM, submitted.id);
7321
+ }
7322
+ if (new Uint32Array(back, doneRequest.offset, 1)[0] !== 0) {
7323
+ const block = new Uint32Array(back, blockRequest.offset, PER_SOURCE_WORDS);
7324
+ const count = Math.min(SOURCES_PER_BATCH, n - batchStart);
7325
+ for (let i = 0; i < count; i++) {
7326
+ const sum = block[3 * SOURCES_PER_BATCH + i] * 2 ** 32 + block[2 * SOURCES_PER_BATCH + i];
7327
+ scores[batchStart + i] = sum === 0 ? 0 : 1 / sum;
7328
+ }
7329
+ tuning.onBatch?.(batchStart, block.slice());
7330
+ break;
7331
+ }
7332
+ if (level > n + 3) {
7333
+ throw new WebGpuGraphError(
7334
+ "E_VALIDATION",
7335
+ `${ALGORITHM}: the done flag never rose in ${level} levels of the batch at ${batchStart}`,
7336
+ { label: ALGORITHM, message: `the done flag never rose in ${level} levels` }
7337
+ );
7338
+ }
7339
+ }
7340
+ batches += 1;
7341
+ options?.onProgress?.(Math.min(batchStart + SOURCES_PER_BATCH, n), n);
7342
+ }
7343
+ return { scores, iterations: batches, converged: true, precision: "f32" };
7344
+ } finally {
7345
+ scope.dispose();
4443
7346
  }
4444
- const run2 = await runPowerIteration(ctx, r.n, {
4445
- normMode: 4,
4446
- adjacency: reverseOf(ctx, s),
4447
- alternate: null,
4448
- alpha,
4449
- beta,
4450
- uniformP: 1,
4451
- maxIterations: r.maxIterations,
4452
- tolerance: r.tolerance,
4453
- weights: r.weights,
4454
- label: algorithm,
4455
- signal: options?.signal,
4456
- onProgress: options?.onProgress
4457
- });
4458
- return scoresOf(run2, r.dest, "l2");
4459
7347
  }
4460
- async function hits(ctx, s, options) {
4461
- const algorithm = "hits";
4462
- const r = resolve(ctx, s, options, algorithm);
4463
- const total = 2 * r.maxIterations;
4464
- if (r.n === 0) {
4465
- const { scores, iterations, converged, precision } = empty(r.dest, total, options?.onProgress);
4466
- return { hubs: scores, authorities: new Float32Array(0), iterations, converged, precision };
7348
+ async function closenessWithTuning(ctx, s, options, tuning) {
7349
+ ctx.assertReady();
7350
+ await assertDeviceComputes(ctx);
7351
+ for (const key of ["maxIterations", "tolerance"]) {
7352
+ if (options?.[key] !== void 0) {
7353
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: ${key} has no meaning for an exact traversal`, {
7354
+ option: key,
7355
+ hint: "closeness is an exact traversal; the option has no meaning here"
7356
+ });
7357
+ }
4467
7358
  }
4468
- const onProgress = options?.onProgress;
4469
- const shared = {
4470
- normMode: 1,
4471
- alpha: 1,
4472
- beta: 0,
4473
- uniformP: 0,
4474
- maxIterations: r.maxIterations,
4475
- tolerance: r.tolerance,
4476
- weights: r.weights,
4477
- signal: options?.signal
4478
- };
4479
- const forward = coreOf(ctx, s, algorithm);
4480
- const reverse = reverseOf(ctx, s);
4481
- const hubSeeded = await runPowerIteration(ctx, r.n, {
4482
- ...shared,
4483
- adjacency: reverse,
4484
- alternate: forward,
4485
- label: `${algorithm}/hub-seeded`,
4486
- onProgress: onProgress === void 0 ? void 0 : (done) => {
4487
- onProgress(done, total);
7359
+ const n = s.nodeCount;
7360
+ const levelsPerSubmit = tuning.levelsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
7361
+ if (!Number.isInteger(levelsPerSubmit) || levelsPerSubmit < 1 || levelsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
7362
+ throw new WebGpuGraphError(
7363
+ "E_INVALID_ARGUMENT",
7364
+ `${ALGORITHM}: levelsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
7365
+ {
7366
+ argument: "levelsPerSubmit",
7367
+ value: levelsPerSubmit,
7368
+ expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
7369
+ }
7370
+ );
7371
+ }
7372
+ const scores = checkDest(ALGORITHM, options?.dest, n) ?? new Float32Array(n);
7373
+ const weighted = options?.weighted ?? s.flags.weighted;
7374
+ if (options?.signal?.aborted) {
7375
+ throw aborted(ALGORITHM);
7376
+ }
7377
+ if (weighted && s.weights !== null && !s.flags.allWeightsOne) {
7378
+ if (!s.flags.nonNegativeWeights) {
7379
+ throw new WebGpuGraphError(
7380
+ "E_UNSUPPORTED",
7381
+ `${ALGORITHM}: a negative weight has no shortest-path distance to sum`,
7382
+ {
7383
+ feature: "closenessCentrality.negativeWeights",
7384
+ hint: "pass weighted: false to ignore the column"
7385
+ }
7386
+ );
4488
7387
  }
4489
- });
4490
- const authoritySeeded = await runPowerIteration(ctx, r.n, {
4491
- ...shared,
4492
- adjacency: forward,
4493
- alternate: reverse,
4494
- label: `${algorithm}/authority-seeded`,
4495
- onProgress: onProgress === void 0 ? void 0 : (done) => {
4496
- onProgress(r.maxIterations + done, total);
7388
+ if (!s.flags.finiteWeights) {
7389
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: a NaN or infinite weight has no shortest path`, {
7390
+ feature: "closenessCentrality.nonFiniteWeights"
7391
+ });
4497
7392
  }
4498
- });
4499
- return {
4500
- hubs: normalised(latestOfKind(hubSeeded, authoritySeeded), r.dest, "sum"),
4501
- authorities: normalised(latestOfKind(authoritySeeded, hubSeeded), null, "sum"),
4502
- iterations: Math.max(hubSeeded.iterations, authoritySeeded.iterations),
4503
- converged: hubSeeded.converged && authoritySeeded.converged,
4504
- precision: "f32"
4505
- };
7393
+ return weightedRoute(ctx, s, scores, options);
7394
+ }
7395
+ return sweepRoute(ctx, s, scores, levelsPerSubmit, options, tuning);
7396
+ }
7397
+ function closenessCentrality(ctx, s, options) {
7398
+ return closenessWithTuning(ctx, s, options, {});
4506
7399
  }
4507
7400
  const U32_MAX = 4294967295;
4508
7401
  async function prepareHistogram(scope) {
@@ -4646,128 +7539,16 @@ class CountingSortPlannerImpl {
4646
7539
  this.dispatches += 1;
4647
7540
  }
4648
7541
  }
4649
- const RADIX_DIGIT_BITS = 8;
4650
- function radixHistBytes(count, wg) {
4651
- return 4 * RADIX_BINS * Math.ceil(count / wg);
4652
- }
4653
- async function prepareRadixSort(scope) {
4654
- const hist = await scope.pipelines.kernel(kernelSpec("radix-hist"));
4655
- const scatter = await scope.pipelines.kernel(kernelSpec("radix-scatter"));
4656
- const scan = await prepareScan(scope);
4657
- return new RadixSortPlannerImpl(scope, hist, scatter, scan);
4658
- }
4659
- function checkBinding(argument, binding, bytes) {
4660
- if (binding.size < bytes) {
4661
- throw new WebGpuGraphError("E_INVALID_ARGUMENT", `radixSort: ${argument} is smaller than ${bytes} bytes`, {
4662
- argument,
4663
- value: binding.size,
4664
- expected: bytes
4665
- });
4666
- }
4667
- }
4668
- function checkRecordArguments(keys, vals, count, bits, scratch, wg) {
4669
- if (bits !== 8 && bits !== 16 && bits !== 24 && bits !== 32) {
4670
- throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: bits must be 8, 16, 24 or 32", {
4671
- argument: "bits",
4672
- value: bits,
4673
- expected: [8, 16, 24, 32]
4674
- });
4675
- }
4676
- if (!Number.isSafeInteger(count) || count < 0 || count > U32_MAX$2) {
4677
- throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: count must be a non-negative integer below 2^32", {
4678
- argument: "count",
4679
- value: count
4680
- });
4681
- }
4682
- const pairBytes = 4 * count;
4683
- checkBinding("keys", keys, pairBytes);
4684
- checkBinding("vals", vals, pairBytes);
4685
- checkBinding("scratch.keys", scratch.keys, pairBytes);
4686
- checkBinding("scratch.vals", scratch.vals, pairBytes);
4687
- const tableBytes = radixHistBytes(count, wg);
4688
- checkBinding("scratch.hist", scratch.hist, tableBytes);
4689
- checkBinding("scratch.offsets", scratch.offsets, tableBytes);
4690
- }
4691
- class RadixSortPlannerImpl {
4692
- /**
4693
- * Wraps the resolved kernels; use prepareRadixSort().
4694
- * @param scope - the caller's scope
4695
- * @param hist - the `radix-hist` kernel
4696
- * @param scatter - the `radix-scatter` kernel
4697
- * @param scan - the scan planner of the same scope
4698
- */
4699
- constructor(scope, hist, scatter, scan) {
4700
- this.dispatches = 0;
4701
- this.scope = scope;
4702
- this.hist = hist;
4703
- this.scatter = scatter;
4704
- this.scan = scan;
4705
- }
4706
- /**
4707
- * Dispatches the last record() issued.
4708
- * @returns the count
4709
- */
4710
- get lastDispatches() {
4711
- return this.dispatches;
4712
- }
4713
- /**
4714
- * Records the passes into the pass (see the interface).
4715
- * @param pass - the compute pass
4716
- * @param keys - the keys
4717
- * @param vals - the values
4718
- * @param count - the pair count
4719
- * @param bits - the key width
4720
- * @param scratch - the second pair, the histogram table and its scanned twin
4721
- * @returns the pair the result lives in
4722
- */
4723
- record(pass, keys, vals, count, bits, scratch) {
4724
- const wg = this.scope.workgroupSize;
4725
- checkRecordArguments(keys, vals, count, bits, scratch, wg);
4726
- if (count === 0) {
4727
- this.dispatches = 0;
4728
- return { keys, vals };
4729
- }
4730
- const groups = Math.ceil(count / wg);
4731
- const plan = plan1d(count, wg, this.scope.caps);
4732
- const tableWords = RADIX_BINS * groups;
4733
- const tableBytes = 4 * tableWords;
4734
- const histTable = { ...scratch.hist, size: tableBytes };
4735
- const offsets = { ...scratch.offsets, size: tableBytes };
4736
- let src = { keys, vals };
4737
- let dst = { keys: scratch.keys, vals: scratch.vals };
4738
- let dispatches = 0;
4739
- const passes = bits / RADIX_DIGIT_BITS;
4740
- for (let p = 0; p < passes; p++) {
4741
- const params = this.scope.params(RADIX_PARAMS, { count, shift: RADIX_DIGIT_BITS * p, groups, pad0: 0 });
4742
- const histBound = this.hist.bind({ keys: src.keys, hist: histTable, P: params.binding });
4743
- this.hist.dispatch(pass, histBound, plan, [params.offset]);
4744
- this.scan.record(pass, histTable, tableWords, offsets);
4745
- const scatterBound = this.scatter.bind({
4746
- keys: src.keys,
4747
- vals: src.vals,
4748
- offsets,
4749
- keysOut: dst.keys,
4750
- valsOut: dst.vals,
4751
- P: params.binding
4752
- });
4753
- this.scatter.dispatch(pass, scatterBound, plan, [params.offset]);
4754
- dispatches += 2 + this.scan.lastDispatches;
4755
- [src, dst] = [dst, src];
4756
- }
4757
- this.dispatches = dispatches;
4758
- return src;
4759
- }
4760
- }
4761
- function nextPow2(x2) {
7542
+ function nextPow2(x) {
4762
7543
  let p = 1;
4763
- while (p < x2) {
7544
+ while (p < x) {
4764
7545
  p *= 2;
4765
7546
  }
4766
7547
  return p;
4767
7548
  }
4768
- function floorPow2$1(x2) {
7549
+ function floorPow2$1(x) {
4769
7550
  let p = 1;
4770
- while (p * 2 <= x2) {
7551
+ while (p * 2 <= x) {
4771
7552
  p *= 2;
4772
7553
  }
4773
7554
  return p;
@@ -4951,15 +7732,15 @@ function resolveCenter(center) {
4951
7732
  return out;
4952
7733
  }
4953
7734
  for (let axis = 0; axis < 3 && axis < center.length; axis++) {
4954
- const v2 = center[axis];
4955
- if (!Number.isFinite(v2)) {
7735
+ const v = center[axis];
7736
+ if (!Number.isFinite(v)) {
4956
7737
  throw new WebGpuGraphError("E_INVALID_ARGUMENT", `center[${axis}] is not finite`, {
4957
7738
  argument: "center",
4958
- value: v2,
7739
+ value: v,
4959
7740
  expected: "finite components"
4960
7741
  });
4961
7742
  }
4962
- out[axis] = v2;
7743
+ out[axis] = v;
4963
7744
  }
4964
7745
  return out;
4965
7746
  }
@@ -4998,13 +7779,13 @@ function seedPositions(s, positions, seed, dim, scale, center, range) {
4998
7779
  for (let i = 0; i < n; i++) {
4999
7780
  let finite2 = true;
5000
7781
  for (let axis = 0; axis < dim; axis++) {
5001
- const v2 = positions[3 * i + axis];
5002
- if (Number.isFinite(v2)) {
5003
- if (v2 < lo[axis]) {
5004
- lo[axis] = v2;
7782
+ const v = positions[3 * i + axis];
7783
+ if (Number.isFinite(v)) {
7784
+ if (v < lo[axis]) {
7785
+ lo[axis] = v;
5005
7786
  }
5006
- if (v2 > hi[axis]) {
5007
- hi[axis] = v2;
7787
+ if (v > hi[axis]) {
7788
+ hi[axis] = v;
5008
7789
  }
5009
7790
  } else {
5010
7791
  finite2 = false;
@@ -5046,8 +7827,8 @@ function seedPositions(s, positions, seed, dim, scale, center, range) {
5046
7827
  const at = 3 * i + axis;
5047
7828
  if (!Number.isFinite(positions[at])) {
5048
7829
  const u = rng.next();
5049
- const v2 = boxLo[axis] + u * (boxHi[axis] - boxLo[axis]);
5050
- positions[at] = v2 * scale + c[axis];
7830
+ const v = boxLo[axis] + u * (boxHi[axis] - boxLo[axis]);
7831
+ positions[at] = v * scale + c[axis];
5051
7832
  }
5052
7833
  }
5053
7834
  if (dim === 2) {
@@ -5188,11 +7969,11 @@ function centerOf(options) {
5188
7969
  return out;
5189
7970
  }
5190
7971
  for (let axis = 0; axis < 3 && axis < center.length; axis++) {
5191
- const v2 = center[axis];
5192
- if (!Number.isFinite(v2)) {
5193
- throw invalidArgument("center", v2, "finite components", `center[${axis}] is not finite`);
7972
+ const v = center[axis];
7973
+ if (!Number.isFinite(v)) {
7974
+ throw invalidArgument("center", v, "finite components", `center[${axis}] is not finite`);
5194
7975
  }
5195
- out[axis] = v2;
7976
+ out[axis] = v;
5196
7977
  }
5197
7978
  return out;
5198
7979
  }
@@ -5840,12 +8621,12 @@ class ForceSimulation {
5840
8621
  );
5841
8622
  }
5842
8623
  let unpinned = false;
5843
- for (let w2 = 0; w2 < words && !unpinned; w2++) {
5844
- if (this.fixedWords[w2] === mask[w2]) {
8624
+ for (let w = 0; w < words && !unpinned; w++) {
8625
+ if (this.fixedWords[w] === mask[w]) {
5845
8626
  continue;
5846
8627
  }
5847
- const last = Math.min(n, w2 * 32 + 32);
5848
- for (let i = w2 * 32; i < last; i++) {
8628
+ const last = Math.min(n, w * 32 + 32);
8629
+ for (let i = w * 32; i < last; i++) {
5849
8630
  if (maskTest(this.fixedWords, i) && !maskTest(mask, i)) {
5850
8631
  unpinned = true;
5851
8632
  break;
@@ -5867,7 +8648,7 @@ class ForceSimulation {
5867
8648
  * @param y - scene y
5868
8649
  * @param z - scene z (ignored on the device in 2D)
5869
8650
  */
5870
- setPosition(index, x2, y2, z) {
8651
+ setPosition(index, x, y, z) {
5871
8652
  this.assertLoaded();
5872
8653
  const { n } = this;
5873
8654
  if (!Number.isInteger(index) || index < 0 || index >= n) {
@@ -5878,10 +8659,10 @@ class ForceSimulation {
5878
8659
  `setPosition(${index}): index out of range`
5879
8660
  );
5880
8661
  }
5881
- if (!Number.isFinite(x2) || !Number.isFinite(y2) || !Number.isFinite(z)) {
8662
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
5882
8663
  throw invalidArgument(
5883
8664
  "position",
5884
- [x2, y2, z],
8665
+ [x, y, z],
5885
8666
  "finite coordinates",
5886
8667
  "setPosition: coordinates must be finite"
5887
8668
  );
@@ -5890,13 +8671,13 @@ class ForceSimulation {
5890
8671
  if (owner === null || buffers === null) {
5891
8672
  throw new WebGpuGraphError("E_NOT_LOADED", `${this.label}: load() first`, { state: this.stateValue });
5892
8673
  }
5893
- owner[3 * index] = x2;
5894
- owner[3 * index + 1] = y2;
8674
+ owner[3 * index] = x;
8675
+ owner[3 * index + 1] = y;
5895
8676
  owner[3 * index + 2] = z;
5896
8677
  const [cx, cy, cz] = this.center;
5897
8678
  const layout = new Float32Array([
5898
- (x2 - cx) / this.scale,
5899
- (y2 - cy) / this.scale,
8679
+ (x - cx) / this.scale,
8680
+ (y - cy) / this.scale,
5900
8681
  this.dimValue === 2 ? 0 : (z - cz) / this.scale
5901
8682
  ]);
5902
8683
  this.ctx.device.queue.writeBuffer(buffers.positions, 16 * index, layout);
@@ -6517,17 +9298,17 @@ class ForceSimulation {
6517
9298
  const lo = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
6518
9299
  const hi = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
6519
9300
  for (let i = 0; i < n; i++) {
6520
- const x2 = packed[4 * i];
6521
- const y2 = packed[4 * i + 1];
9301
+ const x = packed[4 * i];
9302
+ const y = packed[4 * i + 1];
6522
9303
  const z = packed[4 * i + 2];
6523
- sx += x2;
6524
- sy += y2;
9304
+ sx += x;
9305
+ sy += y;
6525
9306
  sz += z;
6526
- lo[0] = Math.min(lo[0], x2);
6527
- lo[1] = Math.min(lo[1], y2);
9307
+ lo[0] = Math.min(lo[0], x);
9308
+ lo[1] = Math.min(lo[1], y);
6528
9309
  lo[2] = Math.min(lo[2], z);
6529
- hi[0] = Math.max(hi[0], x2);
6530
- hi[1] = Math.max(hi[1], y2);
9310
+ hi[0] = Math.max(hi[0], x);
9311
+ hi[1] = Math.max(hi[1], y);
6531
9312
  hi[2] = Math.max(hi[2], z);
6532
9313
  }
6533
9314
  const mx = sx / n;
@@ -6788,14 +9569,14 @@ function invalid$1(argument, value, expected, message) {
6788
9569
  }
6789
9570
  function checkMass(mass, source) {
6790
9571
  for (let i = 0; i < mass.length; i++) {
6791
- const v2 = mass[i];
6792
- if (!Number.isFinite(v2) || v2 <= 0) {
9572
+ const v = mass[i];
9573
+ if (!Number.isFinite(v) || v <= 0) {
6793
9574
  throw new WebGpuGraphError(
6794
9575
  "E_INVALID_ARGUMENT",
6795
- `${source}: mass[${i}] = ${v2} is not a finite number > 0`,
9576
+ `${source}: mass[${i}] = ${v} is not a finite number > 0`,
6796
9577
  {
6797
9578
  argument: "nodeMass",
6798
- value: v2,
9579
+ value: v,
6799
9580
  index: i,
6800
9581
  expected: "finite masses > 0"
6801
9582
  }
@@ -7061,13 +9842,13 @@ function pickCenter(given, fallback) {
7061
9842
  if (length !== 2 && length !== 3) {
7062
9843
  throw invalid("center", given, expected);
7063
9844
  }
7064
- const x2 = given[0];
7065
- const y2 = given[1];
9845
+ const x = given[0];
9846
+ const y = given[1];
7066
9847
  const z = length === 3 ? given[2] : 0;
7067
- if (typeof x2 !== "number" || typeof y2 !== "number" || typeof z !== "number" || !Number.isFinite(x2) || !Number.isFinite(y2) || !Number.isFinite(z)) {
9848
+ if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number" || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
7068
9849
  throw invalid("center", given, expected);
7069
9850
  }
7070
- return [x2, y2, z];
9851
+ return [x, y, z];
7071
9852
  }
7072
9853
  function pickSeed(given, fallback) {
7073
9854
  if (given === void 0) {
@@ -7669,9 +10450,9 @@ function resolveForceAtlas2Options(options, previous) {
7669
10450
  }
7670
10451
  const resolved = {
7671
10452
  maxIter: pickNumber("maxIter", o.maxIter, base.maxIter, isPositiveInteger, "an integer >= 1"),
7672
- jitterTolerance: pickNumber("jitterTolerance", o.jitterTolerance, base.jitterTolerance, (v2) => v2 > 0, "> 0"),
7673
- scalingRatio: pickNumber("scalingRatio", o.scalingRatio, base.scalingRatio, (v2) => v2 > 0, "> 0"),
7674
- gravity: pickNumber("gravity", o.gravity, base.gravity, (v2) => v2 >= 0, ">= 0"),
10453
+ jitterTolerance: pickNumber("jitterTolerance", o.jitterTolerance, base.jitterTolerance, (v) => v > 0, "> 0"),
10454
+ scalingRatio: pickNumber("scalingRatio", o.scalingRatio, base.scalingRatio, (v) => v > 0, "> 0"),
10455
+ gravity: pickNumber("gravity", o.gravity, base.gravity, (v) => v >= 0, ">= 0"),
7675
10456
  strongGravity: pickBoolean("strongGravity", o.strongGravity, base.strongGravity),
7676
10457
  distributedAction: pickBoolean("distributedAction", o.distributedAction, base.distributedAction),
7677
10458
  linlog: pickBoolean("linlog", o.linlog, base.linlog),
@@ -7680,10 +10461,10 @@ function resolveForceAtlas2Options(options, previous) {
7680
10461
  weight: o.weight === void 0 ? base.weight : o.weight,
7681
10462
  dissuadeHubs: pickBoolean("dissuadeHubs", o.dissuadeHubs, base.dissuadeHubs),
7682
10463
  dim: pickDim(o.dim, base.dim),
7683
- scale: pickNumber("scale", o.scale, base.scale, (v2) => v2 > 0, "> 0"),
10464
+ scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
7684
10465
  center: pickCenter(o.center, base.center),
7685
10466
  seed: pickSeed(o.seed, base.seed),
7686
- settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v2) => v2 >= 0, ">= 0"),
10467
+ settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
7687
10468
  settleWindow: pickNumber(
7688
10469
  "settleWindow",
7689
10470
  o.settleWindow,
@@ -7695,7 +10476,7 @@ function resolveForceAtlas2Options(options, previous) {
7695
10476
  "iterationsPerStep",
7696
10477
  o.iterationsPerStep,
7697
10478
  base.iterationsPerStep,
7698
- (v2) => isPositiveInteger(v2) && v2 <= MAX_ITERATIONS_PER_STEP,
10479
+ (v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
7699
10480
  `an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
7700
10481
  ),
7701
10482
  maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
@@ -7725,7 +10506,7 @@ function resolveLayoutTuning(tuning) {
7725
10506
  "nearMax",
7726
10507
  t.nearMax,
7727
10508
  LAYOUT_TUNING_DEFAULTS.nearMax,
7728
- (v2) => isPositiveInteger(v2) && v2 >= 2,
10509
+ (v) => isPositiveInteger(v) && v >= 2,
7729
10510
  "an integer >= 2"
7730
10511
  ),
7731
10512
  deterministic: pickBoolean("deterministic", t.deterministic, LAYOUT_TUNING_DEFAULTS.deterministic),
@@ -7747,7 +10528,7 @@ function resolveLayoutTuning(tuning) {
7747
10528
  "extentFactor",
7748
10529
  t.extentFactor,
7749
10530
  LAYOUT_TUNING_DEFAULTS.extentFactor,
7750
- (v2) => v2 > 0,
10531
+ (v) => v > 0,
7751
10532
  "> 0"
7752
10533
  ),
7753
10534
  compat
@@ -8374,7 +11155,7 @@ function resolveK(given, fallback) {
8374
11155
  if (given === null || given === 0 || Number.isNaN(given)) {
8375
11156
  return null;
8376
11157
  }
8377
- return pickNumber("k", given, 1, (v2) => v2 > 0, "> 0 or null");
11158
+ return pickNumber("k", given, 1, (v) => v > 0, "> 0 or null");
8378
11159
  }
8379
11160
  function pickCooling(given, fallback) {
8380
11161
  if (given === void 0) {
@@ -8407,16 +11188,16 @@ function resolveFruchtermanReingoldOptions(options, previous) {
8407
11188
  "iterations",
8408
11189
  o.iterations,
8409
11190
  iterationsFallback,
8410
- (v2) => Number.isInteger(v2) && v2 >= 0,
11191
+ (v) => Number.isInteger(v) && v >= 0,
8411
11192
  "an integer >= 0"
8412
11193
  ),
8413
11194
  cooling,
8414
11195
  fixed,
8415
11196
  dim: pickDim(o.dim, base.dim),
8416
- scale: pickNumber("scale", o.scale, base.scale, (v2) => v2 > 0, "> 0"),
11197
+ scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
8417
11198
  center: pickCenter(o.center, base.center),
8418
11199
  seed: pickSeed(o.seed, base.seed),
8419
- settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v2) => v2 >= 0, ">= 0"),
11200
+ settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
8420
11201
  settleWindow: pickNumber(
8421
11202
  "settleWindow",
8422
11203
  o.settleWindow,
@@ -8428,7 +11209,7 @@ function resolveFruchtermanReingoldOptions(options, previous) {
8428
11209
  "iterationsPerStep",
8429
11210
  o.iterationsPerStep,
8430
11211
  base.iterationsPerStep,
8431
- (v2) => isPositiveInteger(v2) && v2 <= MAX_ITERATIONS_PER_STEP,
11212
+ (v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
8432
11213
  `an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
8433
11214
  ),
8434
11215
  maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
@@ -9058,22 +11839,22 @@ function resolveSpringElectricalOptions(options, previous) {
9058
11839
  );
9059
11840
  }
9060
11841
  const resolved = {
9061
- springLength: pickNumber("springLength", o.springLength, base.springLength, (v2) => v2 > 0, "> 0"),
11842
+ springLength: pickNumber("springLength", o.springLength, base.springLength, (v) => v > 0, "> 0"),
9062
11843
  springCoefficient: pickNullable(
9063
11844
  "springCoefficient",
9064
11845
  o.springCoefficient,
9065
11846
  base.springCoefficient,
9066
- (v2) => v2 > 0,
11847
+ (v) => v > 0,
9067
11848
  "> 0"
9068
11849
  ),
9069
11850
  gravity: pickNullable("gravity", o.gravity, base.gravity, () => true, "a finite number (negative repels)"),
9070
- dragCoefficient: pickNumber("dragCoefficient", o.dragCoefficient, base.dragCoefficient, (v2) => v2 >= 0, ">= 0"),
9071
- timeStep: pickNumber("timeStep", o.timeStep, base.timeStep, (v2) => v2 > 0, "> 0"),
11851
+ dragCoefficient: pickNumber("dragCoefficient", o.dragCoefficient, base.dragCoefficient, (v) => v >= 0, ">= 0"),
11852
+ timeStep: pickNumber("timeStep", o.timeStep, base.timeStep, (v) => v > 0, "> 0"),
9072
11853
  dim: pickDim(o.dim, base.dim),
9073
- scale: pickNumber("scale", o.scale, base.scale, (v2) => v2 > 0, "> 0"),
11854
+ scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
9074
11855
  center: pickCenter(o.center, base.center),
9075
11856
  seed: pickSeed(o.seed, base.seed),
9076
- settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v2) => v2 >= 0, ">= 0"),
11857
+ settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
9077
11858
  settleWindow: pickNumber(
9078
11859
  "settleWindow",
9079
11860
  o.settleWindow,
@@ -9085,7 +11866,7 @@ function resolveSpringElectricalOptions(options, previous) {
9085
11866
  "iterationsPerStep",
9086
11867
  o.iterationsPerStep,
9087
11868
  base.iterationsPerStep,
9088
- (v2) => isPositiveInteger(v2) && v2 <= MAX_ITERATIONS_PER_STEP,
11869
+ (v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
9089
11870
  `an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
9090
11871
  ),
9091
11872
  maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
@@ -9718,6 +12499,51 @@ function createAccelerator(ctx, options) {
9718
12499
  ctx.assertReady();
9719
12500
  return await connectedComponents(ctx, gs, o);
9720
12501
  },
12502
+ /**
12503
+ * Breadth-first search on the device (spec 8.4; P8-T13). The result is bitwise reproducible (P8 PD-14).
12504
+ * @param gs - the snapshot
12505
+ * @param source - the source node index
12506
+ * @param o - the seam's `BfsOptions` (`maxDepth`)
12507
+ * @returns depth, parent, the level-grouped order, visitedCount, levels and switches (spec 3.3 line 830)
12508
+ */
12509
+ async breadthFirstSearch(gs, source, o) {
12510
+ ctx.assertReady();
12511
+ return await breadthFirstSearch(ctx, gs, source, o);
12512
+ },
12513
+ /**
12514
+ * Single-source shortest paths on the device (spec 8.4; P8-T13): the near-far queue over f32 distances, or the
12515
+ * breadth-first route when every weight is one.
12516
+ * @param gs - the snapshot
12517
+ * @param source - the source node index
12518
+ * @param o - the seam's `SsspOptions` (`cutoff`, `weights`)
12519
+ * @returns dist, predArc and reachedCount (spec 3.3 line 831)
12520
+ */
12521
+ async sssp(gs, source, o) {
12522
+ ctx.assertReady();
12523
+ return await sssp(ctx, gs, source, o);
12524
+ },
12525
+ /**
12526
+ * Bellman-Ford on the device with negative-cycle detection (spec 8.4; P8-T13).
12527
+ * @param gs - the snapshot
12528
+ * @param source - the source node index
12529
+ * @param o - the seam's `SsspOptions` (`cutoff`, `weights`)
12530
+ * @returns dist, predArc, reachedCount and hasNegativeCycle (spec 3.3 line 832)
12531
+ */
12532
+ async bellmanFord(gs, source, o) {
12533
+ ctx.assertReady();
12534
+ return await bellmanFord(ctx, gs, source, o);
12535
+ },
12536
+ /**
12537
+ * Closeness centrality on the device (spec 8.4; P8-T13): the bit-parallel multi-source sweep, or one `sssp`
12538
+ * per source when `weighted`. `maxIterations` / `tolerance` are refused when defined (P8 PD-25).
12539
+ * @param gs - the snapshot
12540
+ * @param o - the seam's placeholder `HitsOptionsLike` (`weighted`)
12541
+ * @returns the f32 scores with `precision: "f32"` (spec 9.7)
12542
+ */
12543
+ async closenessCentrality(gs, o) {
12544
+ ctx.assertReady();
12545
+ return await closenessCentrality(ctx, gs, o);
12546
+ },
9721
12547
  /**
9722
12548
  * Destroys every device buffer recorded for the snapshot (spec 4.5); delegates to ctx.release.
9723
12549
  * @param s - the snapshot the app is done with
@@ -9827,7 +12653,7 @@ async function calibrateLayout(ctx, options) {
9827
12653
  };
9828
12654
  }
9829
12655
  export {
9830
- A as ARC_WINDOW_ALIGN,
12656
+ C as ARC_WINDOW_ALIGN,
9831
12657
  EXACT_MAX_NODES,
9832
12658
  FA2_DEFAULTS,
9833
12659
  FR_DEFAULTS,
@@ -9835,12 +12661,15 @@ export {
9835
12661
  LAYOUT_TUNING_DEFAULTS,
9836
12662
  MAX_1D_ITEMS,
9837
12663
  MAX_WORKGROUPS_PER_DIM,
9838
- v as PASSTHROUGH_FORMAT_CODES,
12664
+ D as PASSTHROUGH_FORMAT_CODES,
9839
12665
  SE_DEFAULTS,
9840
- w as STORAGE_ALIGN,
9841
- x as WORKGROUP_SIZE,
12666
+ H as STORAGE_ALIGN,
12667
+ J as WORKGROUP_SIZE,
9842
12668
  WebGpuGraphError,
12669
+ bellmanFord,
12670
+ breadthFirstSearch,
9843
12671
  calibrateLayout,
12672
+ closenessCentrality,
9844
12673
  connectedComponents,
9845
12674
  createAccelerator,
9846
12675
  createForceAtlas2,
@@ -9850,12 +12679,13 @@ export {
9850
12679
  eigenvectorCentrality,
9851
12680
  hasErrorCode,
9852
12681
  hits,
9853
- y as isSoftwareAdapter,
12682
+ K as isSoftwareAdapter,
9854
12683
  isWebGpuGraphError,
9855
12684
  katzCentrality,
9856
12685
  pageRank,
9857
12686
  personalizedPageRank,
9858
12687
  seedPositions,
12688
+ sssp,
9859
12689
  verifyDevice
9860
12690
  };
9861
12691
  //# sourceMappingURL=webgpu-graph-algorithms.js.map