@graphty/webgpu-graph-algorithms 0.6.3 → 0.6.5

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-hzGggHeM.js} +68 -24
  4. package/dist/chunks/context-hzGggHeM.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 +3 -1
  23. package/dist/src/algorithms/scope.d.ts.map +1 -1
  24. package/dist/src/algorithms/scope.js +1 -0
  25. package/dist/src/algorithms/scope.js.map +1 -1
  26. package/dist/src/algorithms/sssp.d.ts +72 -0
  27. package/dist/src/algorithms/sssp.d.ts.map +1 -0
  28. package/dist/src/algorithms/sssp.js +586 -0
  29. package/dist/src/algorithms/sssp.js.map +1 -0
  30. package/dist/src/constants.d.ts +10 -0
  31. package/dist/src/constants.d.ts.map +1 -1
  32. package/dist/src/constants.js +10 -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 +45 -4
  47. package/dist/src/kernels.d.ts.map +1 -1
  48. package/dist/src/kernels.js +368 -3
  49. package/dist/src/kernels.js.map +1 -1
  50. package/dist/src/primitives/advance.d.ts +63 -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 +151 -0
  63. package/dist/src/primitives/frontier.d.ts.map +1 -0
  64. package/dist/src/primitives/frontier.js +250 -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 +53 -0
  121. package/dist/src/wgsl/frontier-finalize.wgsl.d.ts.map +1 -0
  122. package/dist/src/wgsl/frontier-finalize.wgsl.js +164 -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 +3155 -384
  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 +4 -1
  140. package/src/algorithms/sssp.ts +768 -0
  141. package/src/constants.ts +10 -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 +447 -6
  146. package/src/primitives/advance.ts +131 -0
  147. package/src/primitives/compact.ts +323 -0
  148. package/src/primitives/core-shape.ts +41 -3
  149. package/src/primitives/frontier.ts +372 -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 +163 -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, R as RADIX_BINS, F as FUSED_FRONTIER_MAX, e as BEAMER_BETA, f as SSSP_DELTA_FACTOR, g as F32_INF_BITS, h as GRID_COARSEST_SIDE, j as GRID_MIN_SIDE, k as GRID_SORT_BITS, l as FA2_DEFAULTS, m as MAX_ITERATIONS_PER_STEP, n as MAX_1D_ITEMS, o as hasErrorCode, p as FA2_FLAG_FIRST, P as PARTIAL_BYTES, I as INDIRECT_ARGS_STRIDE, q as GRID_HUB_CELL, L as LAYOUT_TUNING_DEFAULTS, E as EXACT_MAX_NODES, T as TRACE_RECORD_BYTES, r as GRID_BBOX_MARGIN, s as GRID_EXTENT_FLOOR, t as FR_ADAPTIVE_MAX_ITERATIONS, u as FR_START_TEMPERATURE, v as FA2_FLAG_ADAPTIVE, w as FR_REHEAT_FRACTION, x as FR_DEFAULTS, y as SE_DEFAULTS, z as SE_SCALE_REFERENCE_NODES } from "./chunks/context-hzGggHeM.js";
2
+ import { A, G, C, D, H, J } from "./chunks/context-hzGggHeM.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,121 @@ 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
+ @compute @workgroup_size(WG)
1479
+ fn frontier_finalize(@builtin(local_invocation_id) lid: vec3<u32>) {
1480
+ if (lid.x != 0u) { return; } // one lane; no barrier follows (3.5 rule 1)
1481
+ if (P.role == 0u) { // the level boundary
1482
+ if (atomicLoad(&counters[15]) != 0u) { // done already: a no-op level the host recorded past the end
1483
+ atomicStore(&counters[24], 0u); // the path word is the only word that moves (P8-T6's levels formula reads the rest)
1484
+ return;
1485
+ }
1486
+ let finished = atomicLoad(&counters[0]);
1487
+ let next = atomicLoad(&counters[1]);
1488
+ let degSum = atomicLoad(&counters[2]);
1489
+ atomicStore(&counters[3], finished); // prevFrontierCount
1490
+ atomicStore(&counters[4], degSum); // prevDegreeSum
1491
+ atomicStore(&counters[0], next); // the rotation
1492
+ atomicStore(&counters[1], 0u);
1493
+ atomicStore(&counters[2], 0u);
1494
+ atomicStore(&counters[8], 0u); // edgeCount
1495
+ atomicStore(&counters[9], 0u); // edgeCountUnclamped
1496
+ atomicStore(&counters[12], atomicLoad(&counters[12]) + next); // visitedCount
1497
+ if (P.firstOfSubmit >= 1u) { // b = the boundary's index inside its submit, clamped to 2 (P8-T8, PD-18)
1498
+ atomicStore(&counters[5], atomicLoad(&counters[5]) - next); // unvisitedCount (exact): F_b was inside the submit's rebuilt sum iff b >= 1
1499
+ }
1500
+ if (P.firstOfSubmit >= 2u) {
1501
+ atomicStore(&counters[6], atomicLoad(&counters[6]) - degSum); // unvisitedDegreeSum (one level stale): F_{b-1} was inside it iff b >= 2
1502
+ }
1503
+ let level = atomicLoad(&counters[11]) + 1u; // the seed is U32_MAX, so the first boundary lands on 0
1504
+ atomicStore(&counters[11], level);
1505
+ let done = (next == 0u) || (level >= P.maxDepth);
1506
+ atomicStore(&counters[15], select(0u, 1u, done));
1507
+ var direction = atomicLoad(&counters[14]);
1508
+ if (P.mode == 1u) {
1509
+ direction = 0u; // top-down only (the test seam)
1510
+ } else if (direction == 0u) {
1511
+ if (degSum > atomicLoad(&counters[6]) / P.alpha && next > finished) { direction = 1u; } // m_f > m_u / alpha and growing
1512
+ } else {
1513
+ if (next * P.beta < atomicLoad(&counters[5]) && next < finished) { direction = 0u; } // next * beta < unvisited and shrinking
1514
+ }
1515
+ if (direction != atomicLoad(&counters[14])) { atomicStore(&counters[13], atomicLoad(&counters[13]) + 1u); } // switches
1516
+ 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)
1517
+ if (done) {
1518
+ path = 0u;
1519
+ } else if (direction == 1u) { // the bottom-up level (P8-T8): the bits fill, the bitset build, the sweep
1520
+ path = 3u;
1521
+ atomicStore(&counters[19], atomicLoad(&counters[19]) + 1u);
1522
+ } else if (next < P.fusedMax) { // P8-T7 makes this branch reachable (fusedMax is 0 until then)
1523
+ path = 2u; // bfs-fused: one WORKGROUP per frontier entry
1524
+ atomicStore(&counters[17], atomicLoad(&counters[17]) + 1u);
1525
+ } else {
1526
+ path = 1u; // advance-expand, then role 1 and bfs-contract
1527
+ }
1528
+ atomicStore(&counters[14], direction);
1529
+ atomicStore(&counters[24], path);
1530
+ } else if (P.role == 1u) { // the edge queue is filled
1531
+ if (atomicLoad(&counters[24]) != 1u) { // role 0 did not choose the two-phase path (done, fused or bottom-up): nothing to clamp, nothing to count
1532
+ return;
1533
+ }
1534
+ let clamped = min(atomicLoad(&counters[8]), P.edgeCapacity);
1535
+ atomicStore(&counters[8], clamped);
1536
+ if (atomicLoad(&counters[9]) > P.edgeCapacity) { // PD-23: the fused retry
1537
+ atomicStore(&counters[24], 4u); // the path word: bfs-fused runs the retry over frontierCount, bfs-contract nothing
1538
+ atomicStore(&counters[10], atomicLoad(&counters[10]) + 1u);
1539
+ atomicStore(&counters[17], atomicLoad(&counters[17]) + 1u);
1540
+ } else {
1541
+ atomicStore(&counters[18], atomicLoad(&counters[18]) + 1u); // twoPhaseLevels counts the CHOICE role 0 made, even for zero edges (P8-T7 Step 4's invariant)
1542
+ }
1543
+ } else if (P.role == 2u) { // the SSSP round boundary (P8-T9, PD-20): which pile this round relaxes
1544
+ 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
1545
+ atomicStore(&counters[9], 0u);
1546
+ atomicStore(&counters[24], 0u);
1547
+ if (atomicLoad(&counters[15]) != 0u) { // done already: a no-op round the host recorded past the end (rule 1)
1548
+ return;
1549
+ }
1550
+ let nearRaw = atomicLoad(&counters[1]); // the raw near half's appends, unclamped
1551
+ let farRaw = atomicLoad(&counters[21]); // the raw far half's appends, unclamped
1552
+ if (nearRaw > P.edgeCapacity || farRaw > P.edgeCapacity) { // a pile overflowed its half: the host raises E_TOO_LARGE
1553
+ atomicStore(&counters[15], 2u);
1554
+ return;
1555
+ }
1556
+ if (nearRaw != 0u) { // a near round: dedupe the near half into nearIn
1557
+ atomicStore(&counters[0], 0u); // the deduped near count, accumulated by dedupe-filter
1558
+ atomicStore(&counters[14], 0u); // mode 0
1559
+ atomicStore(&counters[8], nearRaw); // the near dedupe's count word (dedupe-claim, dedupe-filter over the near half)
1560
+ atomicStore(&counters[24], 5u); // the path word: sssp-relax role 0 runs, role 1 nothing
1561
+ atomicStore(&counters[11], atomicLoad(&counters[11]) + 1u); // rounds dispatched (the done boundary is not counted)
1562
+ } else if (farRaw != 0u) { // the near pile is empty: raise the threshold and re-bucket the far pile
1563
+ let threshold = bitcast<f32>(atomicLoad(&counters[22]));
1564
+ let raised = threshold + bitcast<f32>(atomicLoad(&counters[23])); // ONE f32 add on the bit patterns (PD-9)
1565
+ if (raised == threshold) { // the delta is below the threshold's ulp: the host raises E_UNSUPPORTED
1566
+ atomicStore(&counters[15], 3u);
1567
+ return;
1568
+ }
1569
+ atomicStore(&counters[4], atomicLoad(&counters[22])); // prevThresholdBits: what the pass-through drops below
1570
+ atomicStore(&counters[22], bitcast<u32>(raised));
1571
+ atomicStore(&counters[20], 0u); // the deduped far count, accumulated by dedupe-filter
1572
+ atomicStore(&counters[14], 1u); // mode 1
1573
+ atomicStore(&counters[9], farRaw); // the far dedupe's count word (dedupe-claim, dedupe-filter over the far half)
1574
+ atomicStore(&counters[24], 6u); // the path word: sssp-relax role 1 runs, role 0 nothing
1575
+ atomicStore(&counters[11], atomicLoad(&counters[11]) + 1u);
1576
+ } else { // both piles empty: finished
1577
+ atomicStore(&counters[15], 1u);
1578
+ }
1579
+ } else if (P.role == 3u) { // the piles are deduped: restart the raw half the round consumed
1580
+ if (atomicLoad(&counters[15]) != 0u) { return; } // role 2 chose no pile this round
1581
+ if (atomicLoad(&counters[14]) == 0u) {
1582
+ atomicStore(&counters[1], 0u); // the raw near half restarts (sssp-relax role 0 sizes itself from word 0)
1583
+ } else {
1584
+ atomicStore(&counters[21], 0u); // the raw far half restarts (the pass-through re-appends what stays far; role 1 sizes itself from word 20)
1585
+ }
1586
+ }
1587
+ }
1588
+ `
1589
+ );
991
1590
  const gridCellKeyWgsl = (
992
1591
  /* wgsl */
993
1592
  `
@@ -1729,6 +2328,108 @@ fn spmv_pull(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id
1729
2328
  }
1730
2329
  `
1731
2330
  );
2331
+ const ssspPredWgsl = (
2332
+ /* wgsl */
2333
+ `
2334
+ @compute @workgroup_size(WG)
2335
+ fn sssp_pred(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
2336
+ let first = linear_id(wid, lid.x);
2337
+ 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)
2338
+ 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)
2339
+ for (var u = first; u < P.n; u = u + P.stride) { // grid-stride over the rows (planGridStride(n)); no barrier anywhere
2340
+ let du = dist[u];
2341
+ var unreached = false;
2342
+ if (MODE == 1u) { unreached = du == INVALID_INDEX; } else { unreached = du == F32_INF_BITS; }
2343
+ if (unreached) { continue; }
2344
+ var hu = 0u; // u's hop count (MODE 0, roles 1 and 2)
2345
+ if (MODE == 0u && P.role != 0u) {
2346
+ hu = atomicLoad(&pred[hb + u]);
2347
+ if (hu == INVALID_INDEX) { // the key has not reached u yet
2348
+ if (P.role == 2u && u != P.source) { atomicAdd(&pred[2u * hb + 1u], 1u); } // an orphan: only bellmanFord can make one (P8-T10 Step 3)
2349
+ continue;
2350
+ }
2351
+ }
2352
+ let end = min(rowPtr[u + 1u], P.arcEnd);
2353
+ for (var a = max(rowPtr[u], P.arcBase); a < end; a = a + 1u) {
2354
+ let v = colIdx[a - P.arcBase];
2355
+ if (v == P.source) { continue; } // the source keeps INVALID_INDEX whatever attains it (a zero-weight arc could)
2356
+ let dv = dist[v];
2357
+ var tight = false; // the arc explains dist[v]
2358
+ var below = false; // and its source sits at a strictly smaller distance
2359
+ if (MODE == 1u) {
2360
+ tight = (du + 1u) == dv; // depth mode: BFS parent, one depth down
2361
+ } else {
2362
+ let w = select(1.0, weights[a - P.arcBase], HAS_WEIGHTS);
2363
+ 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)
2364
+ below = bitcast<f32>(du) < bitcast<f32>(dv);
2365
+ }
2366
+ if (!tight) { continue; }
2367
+ var admit = false; // this arc is one key step below v
2368
+ if (MODE == 1u) {
2369
+ admit = true;
2370
+ } else if (P.role == 0u) { // the roots pass (the plateau rule only; bellmanFord seeds the source alone)
2371
+ if (P.mode == 0u && below) { atomicMin(&pred[hb + v], 0u); }
2372
+ } else if (P.role == 1u) { // a hop pass: one hop along a plateau arc (mode 0) or along any tight arc (mode 1)
2373
+ if (P.mode == 1u || du == dv) {
2374
+ let old = atomicMin(&pred[hb + v], hu + 1u);
2375
+ if (hu + 1u < old) { atomicMax(&pred[2u * hb], P.iteration + 1u); } // this pass changed something
2376
+ }
2377
+ } else { // the predecessor pass: the smallest tight arc one key step below v
2378
+ let hv = atomicLoad(&pred[hb + v]);
2379
+ if (P.mode == 1u) { admit = hu + 1u == hv; } else if (hv == 0u) { admit = below; } else { admit = (du == dv) && (hu + 1u == hv); }
2380
+ }
2381
+ if (admit) { atomicMin(&pred[v], select(a, u, P.predKind == 1u)); }
2382
+ }
2383
+ }
2384
+ }
2385
+ `
2386
+ );
2387
+ const ssspRelaxWgsl = (
2388
+ /* wgsl */
2389
+ `
2390
+ @compute @workgroup_size(WG)
2391
+ fn sssp_relax(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
2392
+ let first = linear_id(wid, lid.x);
2393
+ let mine = select(5u, 6u, P.role == 1u); // the path word role 2 wrote: 5 a near round, 6 a far pass-through
2394
+ let chosen = atomicLoad(&counters[24]) == mine; // the other role's dispatch of the round is a no-op
2395
+ let count = select(0u, min(atomicLoad(&counters[select(0u, 20u, P.role == 1u)]), P.n), chosen); // the deduped near or far pile
2396
+ let threshold = bitcast<f32>(atomicLoad(&counters[22]));
2397
+ let cutoff = bitcast<f32>(P.cutoffBits);
2398
+ for (var i = first; i < count; i = i + P.stride) { // no barrier anywhere: the loops may be per lane
2399
+ let u = queueIn[i];
2400
+ let du = bitcast<f32>(atomicLoad(&dist[u]));
2401
+ if (P.role == 1u) { // the pass-through (PD-20): re-bucket a far entry
2402
+ if (du < bitcast<f32>(atomicLoad(&counters[4]))) { continue; } // below the previous threshold: relaxed in an earlier bucket
2403
+ if (du < threshold) {
2404
+ let q = atomicAdd(&counters[1], 1u);
2405
+ if (q < P.edgeCapacity) { queueOut[q] = u; }
2406
+ } else {
2407
+ let q = atomicAdd(&counters[21], 1u);
2408
+ if (q < P.edgeCapacity) { queueOut[P.edgeCapacity + q] = u; }
2409
+ }
2410
+ continue;
2411
+ }
2412
+ let end = rowPtr[u + 1u];
2413
+ for (var a = rowPtr[u]; a < end; a = a + 1u) { // the whole row: never windowed (DEP-P8-E)
2414
+ let v = colIdx[a];
2415
+ let nd = du + select(1.0, weights[a], HAS_WEIGHTS); // ONE f32 add (PD-9)
2416
+ if (nd > cutoff) { continue; } // SsspOptions.cutoff: the CPU port's dv <= cutoff
2417
+ let bits = bitcast<u32>(nd);
2418
+ let old = atomicMin(&dist[v], bits); // exact on non-negative floats
2419
+ if (bits < old) { // this lane improved v, so it owns the append
2420
+ if (nd < threshold) {
2421
+ let q = atomicAdd(&counters[1], 1u);
2422
+ if (q < P.edgeCapacity) { queueOut[q] = v; }
2423
+ } else {
2424
+ let q = atomicAdd(&counters[21], 1u);
2425
+ if (q < P.edgeCapacity) { queueOut[P.edgeCapacity + q] = v; }
2426
+ }
2427
+ }
2428
+ }
2429
+ }
2430
+ }
2431
+ `
2432
+ );
1732
2433
  const wccCompressWgsl = (
1733
2434
  /* wgsl */
1734
2435
  `
@@ -2029,6 +2730,81 @@ const RADIX_PARAMS = UniformBlock.define("RadixParams", [
2029
2730
  ["groups", "u32"],
2030
2731
  ["pad0", "u32"]
2031
2732
  ]);
2733
+ const COMPACT_PARAMS = UniformBlock.define("CompactParams", [
2734
+ ["count", "u32"],
2735
+ ["outIndex", "u32"],
2736
+ ["countIndex", "u32"],
2737
+ ["stride", "u32"]
2738
+ ]);
2739
+ const FRONTIER_COUNTERS = UniformBlock.define(
2740
+ "FrontierCounters",
2741
+ [
2742
+ ["frontierCount", "u32"],
2743
+ ["nextFrontierCount", "u32"],
2744
+ ["frontierDegreeSum", "u32"],
2745
+ ["prevFrontierCount", "u32"],
2746
+ ["prevDegreeSum", "u32"],
2747
+ ["unvisitedCount", "u32"],
2748
+ ["unvisitedDegreeSum", "u32"],
2749
+ ["unvisitedListLen", "u32"],
2750
+ ["edgeCount", "u32"],
2751
+ ["edgeCountUnclamped", "u32"],
2752
+ ["overflowLevels", "u32"],
2753
+ ["level", "u32"],
2754
+ ["visitedCount", "u32"],
2755
+ ["switches", "u32"],
2756
+ ["direction", "u32"],
2757
+ ["done", "u32"],
2758
+ ["arcsScanned", "u32"],
2759
+ ["fusedLevels", "u32"],
2760
+ ["twoPhaseLevels", "u32"],
2761
+ ["bottomUpLevels", "u32"],
2762
+ ["farCount", "u32"],
2763
+ ["nextFarCount", "u32"],
2764
+ ["thresholdBits", "u32"],
2765
+ ["deltaBits", "u32"],
2766
+ ["path", "u32"]
2767
+ ],
2768
+ { layout: "storage" }
2769
+ );
2770
+ const FRONTIER_PARAMS = UniformBlock.define("FrontierParams", [
2771
+ ["role", "u32"],
2772
+ ["wg", "u32"],
2773
+ ["alpha", "u32"],
2774
+ ["beta", "u32"],
2775
+ ["fusedMax", "u32"],
2776
+ ["edgeCapacity", "u32"],
2777
+ ["maxDepth", "u32"],
2778
+ ["n", "u32"],
2779
+ ["mode", "u32"],
2780
+ ["cutoffBits", "u32"],
2781
+ ["arcBase", "u32"],
2782
+ ["arcEnd", "u32"],
2783
+ ["predKind", "u32"],
2784
+ ["bitsBase", "u32"],
2785
+ ["source", "u32"],
2786
+ ["stride", "u32"],
2787
+ ["firstOfSubmit", "u32"],
2788
+ ["iteration", "u32"],
2789
+ ["pad1", "u32"],
2790
+ ["pad2", "u32"]
2791
+ ]);
2792
+ const BF_PARAMS = UniformBlock.define("BfParams", [
2793
+ ["edgeCount", "u32"],
2794
+ ["stride", "u32"],
2795
+ ["maxRetries", "u32"],
2796
+ ["cutoffBits", "u32"]
2797
+ ]);
2798
+ const BF_FLAGS = UniformBlock.define(
2799
+ "BfFlags",
2800
+ [
2801
+ ["changed", "u32"],
2802
+ ["retryExhausted", "u32"],
2803
+ ["pad0", "u32"],
2804
+ ["pad1", "u32"]
2805
+ ],
2806
+ { layout: "storage" }
2807
+ );
2032
2808
  function decl(group, binding, name, kind, wgslType) {
2033
2809
  return { group, binding, name, kind, wgslType };
2034
2810
  }
@@ -2535,37 +3311,299 @@ const GRID_NEAR_FIELD = {
2535
3311
  snippetSlots: [],
2536
3312
  phase: "P4"
2537
3313
  };
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,
2558
- "scan-add": SCAN_ADD,
2559
- histogram: HISTOGRAM,
2560
- "counting-scatter": COUNTING_SCATTER,
2561
- "radix-hist": RADIX_HIST,
2562
- "radix-scatter": RADIX_SCATTER,
2563
- "grid-cell-key": GRID_CELL_KEY,
2564
- "grid-centroid": GRID_CENTROID,
2565
- "grid-centroid-hub": GRID_CENTROID_HUB,
3314
+ const COMPACT_SCATTER = {
3315
+ id: "compact-scatter",
3316
+ body: compactScatterWgsl,
3317
+ entryPoint: "compact_scatter",
3318
+ bindings: [
3319
+ decl(1, 0, "queue", "storage-ro", "array<u32>"),
3320
+ decl(1, 1, "flags", "storage-ro", "array<u32>"),
3321
+ decl(1, 2, "offsets", "storage-ro", "array<u32>"),
3322
+ decl(1, 3, "out", "storage", "array<u32>"),
3323
+ decl(1, 4, "outCount", "storage", "array<u32>"),
3324
+ decl(2, 0, "P", "uniform", "CompactParams")
3325
+ ],
3326
+ overrideDecls: [],
3327
+ uniforms: [COMPACT_PARAMS],
3328
+ needs: [],
3329
+ snippetSlots: [],
3330
+ phase: "P8"
3331
+ };
3332
+ const DEDUPE_CLAIM = {
3333
+ id: "dedupe-claim",
3334
+ body: dedupeClaimWgsl,
3335
+ entryPoint: "dedupe_claim",
3336
+ bindings: [
3337
+ decl(1, 0, "queue", "storage-ro", "array<u32>"),
3338
+ decl(1, 1, "owner", "storage", "array<atomic<u32>>"),
3339
+ decl(1, 2, "counters", "storage", "array<atomic<u32>>"),
3340
+ decl(2, 0, "P", "uniform", "CompactParams")
3341
+ ],
3342
+ overrideDecls: [],
3343
+ uniforms: [COMPACT_PARAMS],
3344
+ needs: [],
3345
+ snippetSlots: [],
3346
+ phase: "P8"
3347
+ };
3348
+ const DEDUPE_FILTER = {
3349
+ id: "dedupe-filter",
3350
+ body: dedupeFilterWgsl,
3351
+ entryPoint: "dedupe_filter",
3352
+ bindings: [
3353
+ decl(1, 0, "queue", "storage-ro", "array<u32>"),
3354
+ decl(1, 1, "owner", "storage", "array<atomic<u32>>"),
3355
+ decl(1, 2, "out", "storage", "array<u32>"),
3356
+ decl(1, 3, "outCount", "storage", "array<atomic<u32>>"),
3357
+ decl(2, 0, "P", "uniform", "CompactParams")
3358
+ ],
3359
+ overrideDecls: [],
3360
+ uniforms: [COMPACT_PARAMS],
3361
+ needs: [],
3362
+ snippetSlots: [],
3363
+ phase: "P8"
3364
+ };
3365
+ const FRONTIER_FINALIZE = {
3366
+ id: "frontier-finalize",
3367
+ body: frontierFinalizeWgsl,
3368
+ entryPoint: "frontier_finalize",
3369
+ bindings: [decl(1, 0, "counters", "storage", "array<atomic<u32>>"), decl(2, 0, "P", "uniform", "FrontierParams")],
3370
+ overrideDecls: [],
3371
+ uniforms: [FRONTIER_PARAMS],
3372
+ needs: [],
3373
+ snippetSlots: [],
3374
+ phase: "P8"
3375
+ };
3376
+ const ADVANCE_EXPAND = {
3377
+ id: "advance-expand",
3378
+ body: advanceExpandWgsl,
3379
+ entryPoint: "advance_expand",
3380
+ bindings: GRAPH_SLOTS.concat(
3381
+ decl(1, 0, "frontierIn", "storage-ro", "array<u32>"),
3382
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3383
+ decl(1, 2, "edgeQueue", "storage", "array<u32>"),
3384
+ decl(2, 0, "P", "uniform", "FrontierParams")
3385
+ ),
3386
+ overrideDecls: [],
3387
+ uniforms: [FRONTIER_PARAMS],
3388
+ needs: ["subgroups"],
3389
+ snippetSlots: [],
3390
+ phase: "P8"
3391
+ };
3392
+ const BFS_CONTRACT = {
3393
+ id: "bfs-contract",
3394
+ body: bfsContractWgsl,
3395
+ entryPoint: "bfs_contract",
3396
+ bindings: [
3397
+ decl(1, 0, "edgeQueue", "storage-ro", "array<u32>"),
3398
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3399
+ decl(1, 2, "depth", "storage", "array<atomic<u32>>"),
3400
+ decl(1, 3, "frontierOut", "storage", "array<u32>"),
3401
+ decl(2, 0, "P", "uniform", "FrontierParams")
3402
+ ],
3403
+ overrideDecls: [],
3404
+ uniforms: [FRONTIER_PARAMS],
3405
+ needs: [],
3406
+ snippetSlots: [],
3407
+ phase: "P8"
3408
+ };
3409
+ const SSSP_PRED = {
3410
+ id: "sssp-pred",
3411
+ body: ssspPredWgsl,
3412
+ entryPoint: "sssp_pred",
3413
+ bindings: GRAPH_SLOTS.concat(
3414
+ decl(1, 0, "dist", "storage-ro", "array<u32>"),
3415
+ decl(1, 1, "pred", "storage", "array<atomic<u32>>"),
3416
+ decl(2, 0, "P", "uniform", "FrontierParams")
3417
+ ),
3418
+ overrideDecls: [{ name: "MODE", type: "u32", default: 0 }],
3419
+ uniforms: [FRONTIER_PARAMS],
3420
+ needs: [],
3421
+ snippetSlots: [],
3422
+ phase: "P8"
3423
+ };
3424
+ const BFS_FUSED = {
3425
+ id: "bfs-fused",
3426
+ body: bfsFusedWgsl,
3427
+ entryPoint: "bfs_fused",
3428
+ bindings: GRAPH_SLOTS.concat(
3429
+ decl(1, 0, "frontierIn", "storage-ro", "array<u32>"),
3430
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3431
+ decl(1, 2, "depth", "storage", "array<atomic<u32>>"),
3432
+ decl(1, 3, "frontierOut", "storage", "array<u32>"),
3433
+ decl(2, 0, "P", "uniform", "FrontierParams")
3434
+ ),
3435
+ overrideDecls: [],
3436
+ uniforms: [FRONTIER_PARAMS],
3437
+ needs: [],
3438
+ snippetSlots: [],
3439
+ phase: "P8"
3440
+ };
3441
+ const BFS_BOTTOM_UP = {
3442
+ id: "bfs-bottom-up",
3443
+ body: bfsBottomUpWgsl,
3444
+ entryPoint: "bfs_bottom_up",
3445
+ bindings: GRAPH_SLOTS.concat(
3446
+ decl(1, 0, "sweepIn", "storage-ro", "array<u32>"),
3447
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3448
+ decl(1, 2, "depth", "storage", "array<atomic<u32>>"),
3449
+ decl(1, 3, "frontierOut", "storage", "array<u32>"),
3450
+ decl(2, 0, "P", "uniform", "FrontierParams")
3451
+ ),
3452
+ overrideDecls: [],
3453
+ uniforms: [FRONTIER_PARAMS],
3454
+ needs: ["subgroups"],
3455
+ snippetSlots: [],
3456
+ phase: "P8"
3457
+ };
3458
+ const BFS_BITSET_BUILD = {
3459
+ id: "bfs-bitset-build",
3460
+ body: bfsBitsetBuildWgsl,
3461
+ entryPoint: "bfs_bitset_build",
3462
+ bindings: [
3463
+ decl(1, 0, "frontierIn", "storage-ro", "array<u32>"),
3464
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3465
+ decl(1, 2, "bits", "storage", "array<atomic<u32>>"),
3466
+ decl(2, 0, "P", "uniform", "FrontierParams")
3467
+ ],
3468
+ overrideDecls: [],
3469
+ uniforms: [FRONTIER_PARAMS],
3470
+ needs: [],
3471
+ snippetSlots: [],
3472
+ phase: "P8"
3473
+ };
3474
+ const BFS_UNVISITED_FLAGS = {
3475
+ id: "bfs-unvisited-flags",
3476
+ body: bfsUnvisitedFlagsWgsl,
3477
+ entryPoint: "bfs_unvisited_flags",
3478
+ bindings: [
3479
+ decl(1, 0, "outDegree", "storage-ro", "array<u32>"),
3480
+ decl(1, 1, "inDegree", "storage-ro", "array<u32>"),
3481
+ decl(1, 2, "depth", "storage-ro", "array<u32>"),
3482
+ decl(1, 3, "flags", "storage", "array<u32>"),
3483
+ decl(1, 4, "counters", "storage", "array<atomic<u32>>"),
3484
+ decl(2, 0, "P", "uniform", "FrontierParams")
3485
+ ],
3486
+ overrideDecls: [],
3487
+ uniforms: [FRONTIER_PARAMS],
3488
+ needs: ["subgroups"],
3489
+ snippetSlots: [],
3490
+ phase: "P8"
3491
+ };
3492
+ const SSSP_RELAX = {
3493
+ id: "sssp-relax",
3494
+ body: ssspRelaxWgsl,
3495
+ entryPoint: "sssp_relax",
3496
+ bindings: GRAPH_SLOTS.concat(
3497
+ decl(1, 0, "dist", "storage", "array<atomic<u32>>"),
3498
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3499
+ decl(1, 2, "queueIn", "storage-ro", "array<u32>"),
3500
+ decl(1, 3, "queueOut", "storage", "array<u32>"),
3501
+ decl(2, 0, "P", "uniform", "FrontierParams")
3502
+ ),
3503
+ overrideDecls: [],
3504
+ uniforms: [FRONTIER_PARAMS],
3505
+ needs: [],
3506
+ snippetSlots: [],
3507
+ phase: "P8"
3508
+ };
3509
+ const BF_RELAX = {
3510
+ id: "bf-relax",
3511
+ body: bfRelaxWgsl,
3512
+ entryPoint: "bf_relax",
3513
+ bindings: [
3514
+ decl(1, 0, "edgeSrc", "storage-ro", "array<u32>"),
3515
+ decl(1, 1, "edgeDst", "storage-ro", "array<u32>"),
3516
+ decl(1, 2, "edgeToArc", "storage-ro", "array<u32>"),
3517
+ decl(1, 3, "weights", "storage-ro", "array<f32>"),
3518
+ decl(1, 4, "dist", "storage", "array<atomic<u32>>"),
3519
+ decl(1, 5, "flags", "storage", "array<atomic<u32>>"),
3520
+ decl(2, 0, "P", "uniform", "BfParams")
3521
+ ],
3522
+ overrideDecls: [{ name: "UNDIRECTED", type: "bool", default: false }],
3523
+ uniforms: [BF_PARAMS],
3524
+ needs: [],
3525
+ snippetSlots: [],
3526
+ phase: "P8"
3527
+ };
3528
+ const CLOSENESS_SWEEP = {
3529
+ id: "closeness-sweep",
3530
+ body: closenessSweepWgsl,
3531
+ entryPoint: "closeness_sweep",
3532
+ bindings: GRAPH_SLOTS.concat(
3533
+ decl(1, 0, "frontierList", "storage-ro", "array<u32>"),
3534
+ decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
3535
+ decl(1, 2, "bits", "storage", "array<atomic<u32>>"),
3536
+ decl(1, 3, "perSource", "storage", "array<atomic<u32>>"),
3537
+ decl(2, 0, "P", "uniform", "FrontierParams")
3538
+ ),
3539
+ overrideDecls: [],
3540
+ uniforms: [FRONTIER_PARAMS],
3541
+ needs: [],
3542
+ snippetSlots: [],
3543
+ phase: "P8"
3544
+ };
3545
+ const CLOSENESS_REDUCE = {
3546
+ id: "closeness-reduce",
3547
+ body: closenessReduceWgsl,
3548
+ entryPoint: "closeness_reduce",
3549
+ bindings: [
3550
+ decl(1, 0, "counters", "storage", "array<atomic<u32>>"),
3551
+ decl(1, 1, "perSource", "storage", "array<atomic<u32>>"),
3552
+ decl(1, 2, "bits", "storage", "array<u32>"),
3553
+ decl(2, 0, "P", "uniform", "FrontierParams")
3554
+ ],
3555
+ overrideDecls: [],
3556
+ uniforms: [FRONTIER_PARAMS],
3557
+ needs: [],
3558
+ snippetSlots: [],
3559
+ phase: "P8"
3560
+ };
3561
+ const REGISTRY = Object.freeze({
3562
+ degree: DEGREE,
3563
+ reduce: REDUCE,
3564
+ fill: FILL,
3565
+ "segmented-reduce": SEGMENTED_REDUCE,
3566
+ "fa2-stats-finalize": FA2_STATS_FINALIZE,
3567
+ "fa2-attraction": FA2_ATTRACTION,
3568
+ "fa2-repulsion-exact": FA2_REPULSION_EXACT,
3569
+ "fa2-speed-finalize": FA2_SPEED_FINALIZE,
3570
+ "fa2-integrate": FA2_INTEGRATE,
3571
+ "fa2-to-scene": FA2_TO_SCENE,
3572
+ "spmv-pull": SPMV_PULL,
3573
+ "pr-scale": PR_SCALE,
3574
+ "pr-finalize": PR_FINALIZE,
3575
+ "wcc-link-sample": WCC_LINK_SAMPLE,
3576
+ "wcc-link-edges": WCC_LINK_EDGES,
3577
+ "wcc-compress": WCC_COMPRESS,
3578
+ "wcc-sample": WCC_SAMPLE,
3579
+ "indirect-finalize": INDIRECT_FINALIZE,
3580
+ "scan-block": SCAN_BLOCK,
3581
+ "scan-add": SCAN_ADD,
3582
+ histogram: HISTOGRAM,
3583
+ "counting-scatter": COUNTING_SCATTER,
3584
+ "radix-hist": RADIX_HIST,
3585
+ "radix-scatter": RADIX_SCATTER,
3586
+ "grid-cell-key": GRID_CELL_KEY,
3587
+ "grid-centroid": GRID_CENTROID,
3588
+ "grid-centroid-hub": GRID_CENTROID_HUB,
2566
3589
  "grid-downsample": GRID_DOWNSAMPLE,
2567
3590
  "grid-far-field": GRID_FAR_FIELD,
2568
- "grid-near-field": GRID_NEAR_FIELD
3591
+ "grid-near-field": GRID_NEAR_FIELD,
3592
+ "compact-scatter": COMPACT_SCATTER,
3593
+ "dedupe-claim": DEDUPE_CLAIM,
3594
+ "dedupe-filter": DEDUPE_FILTER,
3595
+ "frontier-finalize": FRONTIER_FINALIZE,
3596
+ "advance-expand": ADVANCE_EXPAND,
3597
+ "bfs-contract": BFS_CONTRACT,
3598
+ "sssp-pred": SSSP_PRED,
3599
+ "bfs-fused": BFS_FUSED,
3600
+ "bfs-bottom-up": BFS_BOTTOM_UP,
3601
+ "bfs-bitset-build": BFS_BITSET_BUILD,
3602
+ "bfs-unvisited-flags": BFS_UNVISITED_FLAGS,
3603
+ "sssp-relax": SSSP_RELAX,
3604
+ "bf-relax": BF_RELAX,
3605
+ "closeness-sweep": CLOSENESS_SWEEP,
3606
+ "closeness-reduce": CLOSENESS_REDUCE
2569
3607
  });
2570
3608
  const bodyOverrides = /* @__PURE__ */ new Map();
2571
3609
  function entryOf(id) {
@@ -2724,7 +3762,7 @@ class ScanPlannerImpl {
2724
3762
  }
2725
3763
  const POISON = 3735928559;
2726
3764
  const CHECK_BLOCKS = 32;
2727
- const RING_SLOTS$3 = 8;
3765
+ const RING_SLOTS$6 = 8;
2728
3766
  const checked = /* @__PURE__ */ new WeakMap();
2729
3767
  function inputAt(i) {
2730
3768
  return i + 1;
@@ -2752,7 +3790,7 @@ async function runCheck(ctx) {
2752
3790
  const count = CHECK_BLOCKS * wg + 1;
2753
3791
  const bytes = 4 * count;
2754
3792
  const lease = ctx.pool.lease();
2755
- const ring = new UniformRing(ctx.device, ctx.allocator, RING_SLOTS$3, "device-check/ring");
3793
+ const ring = new UniformRing(ctx.device, ctx.allocator, RING_SLOTS$6, "device-check/ring");
2756
3794
  try {
2757
3795
  const scope = {
2758
3796
  device: ctx.device,
@@ -2892,8 +3930,8 @@ function rowCountOf(core, primitive) {
2892
3930
  function arcCountOf(core) {
2893
3931
  return core.colIdx === null ? 0 : core.colIdx.size / 4;
2894
3932
  }
2895
- function windowBinding(core, name, w2) {
2896
- const buffer = core.arcBuffers?.[name][w2.bufferIndex];
3933
+ function windowBinding(core, name, w) {
3934
+ const buffer = core.arcBuffers?.[name][w.bufferIndex];
2897
3935
  if (buffer === void 0) {
2898
3936
  throw new WebGpuGraphError("E_INVALID_ARGUMENT", `windowBinding: the core has no window buffers of ${name}`, {
2899
3937
  argument: "core",
@@ -2901,7 +3939,21 @@ function windowBinding(core, name, w2) {
2901
3939
  expected: "a windowed core whose plan uploaded the array"
2902
3940
  });
2903
3941
  }
2904
- return { buffer, offset: w2.offset, size: 4 * (w2.end - w2.start), window: w2 };
3942
+ return { buffer, offset: w.offset, size: 4 * (w.end - w.start), window: w };
3943
+ }
3944
+ function coreWindows(core) {
3945
+ if (core.windows === null) {
3946
+ return [{ arcBase: 0, arcEnd: arcCountOf(core), core }];
3947
+ }
3948
+ return core.windows.map((w, k, all) => ({
3949
+ arcBase: w.start,
3950
+ arcEnd: k + 1 < all.length ? all[k + 1].start : w.end,
3951
+ core: {
3952
+ ...core,
3953
+ colIdx: windowBinding(core, "colIdx", w),
3954
+ weights: core.weights === null ? null : windowBinding(core, "weights", w)
3955
+ }
3956
+ }));
2905
3957
  }
2906
3958
  function assertNotWindowed(core, primitive) {
2907
3959
  if (core.plan === "windowed" || core.windows !== null) {
@@ -2919,22 +3971,22 @@ function assertWholeCore(core, arcCount, limit, algorithm) {
2919
3971
  );
2920
3972
  }
2921
3973
  }
2922
- function coreOfView(v2, arcCount) {
2923
- const { rowPtr } = v2.bindings;
3974
+ function coreOfView(v, arcCount) {
3975
+ const { rowPtr } = v.bindings;
2924
3976
  if (rowPtr === void 0) {
2925
- throw new WebGpuGraphError("E_INVALID_ARGUMENT", `the ${v2.view} view has no rowPtr binding`, {
3977
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `the ${v.view} view has no rowPtr binding`, {
2926
3978
  argument: "view",
2927
- value: v2.view,
3979
+ value: v.view,
2928
3980
  expected: "a view with a rowPtr binding (reverse)"
2929
3981
  });
2930
3982
  }
2931
- const colIdx = v2.bindings.colIdx ?? null;
2932
- const weights = v2.bindings.weights ?? null;
3983
+ const colIdx = v.bindings.colIdx === void 0 || v.bindings.colIdx.size === 0 ? null : v.bindings.colIdx;
3984
+ const weights = v.bindings.weights === void 0 || v.bindings.weights.size === 0 ? null : v.bindings.weights;
2933
3985
  const bound = colIdx === null ? 0 : colIdx.size / 4;
2934
3986
  if (bound !== arcCount) {
2935
3987
  throw new WebGpuGraphError(
2936
3988
  "E_INVALID_ARGUMENT",
2937
- `the ${v2.view} view binds ${bound} arcs but its arcCount scalar says ${arcCount}`,
3989
+ `the ${v.view} view binds ${bound} arcs but its arcCount scalar says ${arcCount}`,
2938
3990
  {
2939
3991
  argument: "arcCount",
2940
3992
  value: arcCount,
@@ -2955,7 +4007,7 @@ function coreOfView(v2, arcCount) {
2955
4007
  hasWeights: weights !== null
2956
4008
  });
2957
4009
  }
2958
- function checkDest$3(dest, n) {
4010
+ function checkDest$5(dest, n) {
2959
4011
  if (dest === void 0) {
2960
4012
  return null;
2961
4013
  }
@@ -2984,7 +4036,7 @@ async function degree(ctx, s, options) {
2984
4036
  ctx.assertReady();
2985
4037
  await assertDeviceComputes(ctx);
2986
4038
  const n = s.nodeCount;
2987
- const dest = checkDest$3(options?.dest, n);
4039
+ const dest = checkDest$5(options?.dest, n);
2988
4040
  if (options?.signal?.aborted) {
2989
4041
  throw new WebGpuGraphError("E_ABORTED", "degree: the signal was aborted before any work started", {});
2990
4042
  }
@@ -3021,22 +4073,22 @@ async function degree(ctx, s, options) {
3021
4073
  const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
3022
4074
  const zero = fill.bind({ dst: outBinding, P: params(FILL_PARAMS, { count: n, value: 0, mode: 0 }) });
3023
4075
  fill.dispatch(pass, zero, plan1d(n, ctx.workgroupSize, ctx.caps), [0]);
3024
- for (const w2 of core.windows) {
4076
+ for (const w of core.windows) {
3025
4077
  const windowed = {
3026
4078
  ...core,
3027
- colIdx: windowBinding(core, "colIdx", w2),
3028
- weights: core.weights === null ? null : windowBinding(core, "weights", w2)
4079
+ colIdx: windowBinding(core, "colIdx", w),
4080
+ weights: core.weights === null ? null : windowBinding(core, "weights", w)
3029
4081
  };
3030
4082
  const P = params(RANGE_PARAMS, {
3031
- start: w2.rowFirst,
3032
- end: w2.rowLast + 1,
3033
- arcBase: w2.start,
3034
- arcEnd: w2.end,
4083
+ start: w.rowFirst,
4084
+ end: w.rowLast + 1,
4085
+ arcBase: w.start,
4086
+ arcEnd: w.end,
3035
4087
  accumulate: 1,
3036
4088
  n
3037
4089
  });
3038
4090
  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]);
4091
+ kernel.dispatch(pass, bound, plan1d(w.rowLast - w.rowFirst + 1, ctx.workgroupSize, ctx.caps), [0]);
3040
4092
  }
3041
4093
  }
3042
4094
  pass.end();
@@ -3342,9 +4394,9 @@ class CommandBatch {
3342
4394
  countDispatches(pass) {
3343
4395
  const direct = pass.dispatchWorkgroups.bind(pass);
3344
4396
  const indirect = pass.dispatchWorkgroupsIndirect.bind(pass);
3345
- pass.dispatchWorkgroups = (x2, y2, z) => {
4397
+ pass.dispatchWorkgroups = (x, y, z) => {
3346
4398
  this.dispatchCount += 1;
3347
- direct(x2, y2, z);
4399
+ direct(x, y, z);
3348
4400
  return void 0;
3349
4401
  };
3350
4402
  pass.dispatchWorkgroupsIndirect = (buffer, offset) => {
@@ -3401,16 +4453,17 @@ function algorithmScope(ctx, label, slots) {
3401
4453
  dispose() {
3402
4454
  ring.destroy();
3403
4455
  lease.release();
3404
- }
4456
+ },
4457
+ ringOverruns: () => ring.overruns
3405
4458
  };
3406
4459
  }
3407
- const ALGORITHM = "connectedComponents";
3408
- const ROUNDS_PER_BATCH = 4;
4460
+ const ALGORITHM$4 = "connectedComponents";
4461
+ const ROUNDS_PER_BATCH$1 = 4;
3409
4462
  const MAX_WCC_ROUNDS = 64;
3410
4463
  const SAMPLE_SIZE = 1024;
3411
4464
  const MAX_STEPS = 1024;
3412
- const RING_SLOTS$2 = 2 * ROUNDS_PER_BATCH;
3413
- function checkDest$2(dest, n) {
4465
+ const RING_SLOTS$5 = 2 * ROUNDS_PER_BATCH$1;
4466
+ function checkDest$4(dest, n) {
3414
4467
  if (dest === void 0) {
3415
4468
  return null;
3416
4469
  }
@@ -3419,7 +4472,7 @@ function checkDest$2(dest, n) {
3419
4472
  }
3420
4473
  throw new WebGpuGraphError(
3421
4474
  "E_INVALID_ARGUMENT",
3422
- `${ALGORITHM}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
4475
+ `${ALGORITHM$4}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
3423
4476
  {
3424
4477
  argument: "dest",
3425
4478
  value: `${dest.constructor.name}(${dest.length})`,
@@ -3429,10 +4482,10 @@ function checkDest$2(dest, n) {
3429
4482
  }
3430
4483
  function coreOf$2(ctx, s) {
3431
4484
  const core = ctx.residency.core(s);
3432
- assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM);
4485
+ assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM$4);
3433
4486
  return core;
3434
4487
  }
3435
- function bindingOf$1(buffer, size) {
4488
+ function bindingOf$3(buffer, size) {
3436
4489
  return { buffer, offset: 0, size, window: null };
3437
4490
  }
3438
4491
  function labelResult(labels, count) {
@@ -3448,19 +4501,19 @@ function labelResult(labels, count) {
3448
4501
  const dense = new Uint32Array(n).fill(U32_MAX$2);
3449
4502
  const sizes = new Uint32Array(count);
3450
4503
  let next = 0;
3451
- for (let v2 = 0; v2 < n; v2++) {
3452
- let k = dense[labels[v2]];
4504
+ for (let v = 0; v < n; v++) {
4505
+ let k = dense[labels[v]];
3453
4506
  if (k === U32_MAX$2) {
3454
4507
  k = next++;
3455
- dense[labels[v2]] = k;
4508
+ dense[labels[v]] = k;
3456
4509
  }
3457
4510
  sizes[k]++;
3458
4511
  }
3459
4512
  const built = Array.from(sizes, (size) => new Uint32Array(size));
3460
4513
  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;
4514
+ for (let v = 0; v < n; v++) {
4515
+ const k = dense[labels[v]];
4516
+ built[k][filled[k]++] = v;
3464
4517
  }
3465
4518
  groups = built;
3466
4519
  return built;
@@ -3485,11 +4538,11 @@ function checkLabels(raw) {
3485
4538
  const n = raw.length;
3486
4539
  const seen = new Uint8Array(n);
3487
4540
  let count = 0;
3488
- for (let v2 = 0; v2 < n; v2++) {
3489
- const label = raw[v2];
4541
+ for (let v = 0; v < n; v++) {
4542
+ const label = raw[v];
3490
4543
  if (label >= n) {
3491
- throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: labels[${v2}] = ${label} is not a node index`, {
3492
- label: `${ALGORITHM}/labels`,
4544
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$4}: labels[${v}] = ${label} is not a node index`, {
4545
+ label: `${ALGORITHM$4}/labels`,
3493
4546
  message: `the device produced a label outside [0, ${n})`
3494
4547
  });
3495
4548
  }
@@ -3505,9 +4558,9 @@ async function connectedComponents(ctx, s, options) {
3505
4558
  await assertDeviceComputes(ctx);
3506
4559
  const n = s.nodeCount;
3507
4560
  const renumber = options?.renumber !== false;
3508
- const dest = checkDest$2(options?.dest, n);
4561
+ const dest = checkDest$4(options?.dest, n);
3509
4562
  if (options?.signal?.aborted) {
3510
- throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted before any work started`, {});
4563
+ throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM$4}: the signal was aborted before any work started`, {});
3511
4564
  }
3512
4565
  if (n === 0) {
3513
4566
  options?.onProgress?.(1, 1);
@@ -3516,15 +4569,15 @@ async function connectedComponents(ctx, s, options) {
3516
4569
  const core = coreOf$2(ctx, s);
3517
4570
  if (s.arcCount === 0) {
3518
4571
  const labels = dest ?? new Uint32Array(n);
3519
- for (let v2 = 0; v2 < n; v2++) {
3520
- labels[v2] = v2;
4572
+ for (let v = 0; v < n; v++) {
4573
+ labels[v] = v;
3521
4574
  }
3522
4575
  options?.onProgress?.(1, 1);
3523
4576
  return labelResult(labels, n);
3524
4577
  }
3525
4578
  const edges = ctx.residency.view(s, "edgeList");
3526
4579
  const edgeCount = edges.scalars.edgeCount[0];
3527
- const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS$2);
4580
+ const scope = algorithmScope(ctx, ALGORITHM$4, RING_SLOTS$5);
3528
4581
  try {
3529
4582
  const compBytes = 4 * (n + 1);
3530
4583
  const comp = scope.scratch(compBytes, "comp");
@@ -3537,8 +4590,8 @@ async function connectedComponents(ctx, s, options) {
3537
4590
  const compress = await ctx.pipelines.kernel(kernelSpec("wcc-compress", {}));
3538
4591
  const sample = await ctx.pipelines.kernel(kernelSpec("wcc-sample", {}));
3539
4592
  const { queue } = ctx.device;
3540
- const compBinding = bindingOf$1(comp, compBytes);
3541
- const histBinding = bindingOf$1(hist, 4 * items);
4593
+ const compBinding = bindingOf$3(comp, compBytes);
4594
+ const histBinding = bindingOf$3(hist, 4 * items);
3542
4595
  const rowPlan = planGridStride(n, ctx.workgroupSize, ctx.caps);
3543
4596
  const edgePlan = planGridStride(edgeCount, ctx.workgroupSize, ctx.caps);
3544
4597
  const flagIndex = n;
@@ -3553,12 +4606,12 @@ async function connectedComponents(ctx, s, options) {
3553
4606
  return batch.submit();
3554
4607
  };
3555
4608
  queue.writeBuffer(comp, 4 * flagIndex, zero);
3556
- const setup = new CommandBatch(ctx, `${ALGORITHM}/setup`);
4609
+ const setup = new CommandBatch(ctx, `${ALGORITHM$4}/setup`);
3557
4610
  let pass = setup.pass("sample-rounds");
3558
4611
  const fillParams = scope.params(FILL_PARAMS, { count: n, value: 0, mode: 1, pad0: 0 });
3559
4612
  fill.dispatch(
3560
4613
  pass,
3561
- fill.bind({ dst: bindingOf$1(comp, 4 * n), P: fillParams.binding }),
4614
+ fill.bind({ dst: bindingOf$3(comp, 4 * n), P: fillParams.binding }),
3562
4615
  plan1d(n, ctx.workgroupSize, ctx.caps),
3563
4616
  [fillParams.offset]
3564
4617
  );
@@ -3572,7 +4625,7 @@ async function connectedComponents(ctx, s, options) {
3572
4625
  setup.endPass();
3573
4626
  await submit(setup).readback;
3574
4627
  ctx.assertReady();
3575
- const sampler = new CommandBatch(ctx, `${ALGORITHM}/sample`);
4628
+ const sampler = new CommandBatch(ctx, `${ALGORITHM$4}/sample`);
3576
4629
  pass = sampler.pass("sample");
3577
4630
  const sampleParams = wccParams({ items, stride: 0, r: 0, giant: U32_MAX$2 });
3578
4631
  sample.dispatch(
@@ -3590,9 +4643,9 @@ async function connectedComponents(ctx, s, options) {
3590
4643
  let rounds = 0;
3591
4644
  for (; ; ) {
3592
4645
  queue.writeBuffer(comp, 4 * flagIndex, zero);
3593
- const batch = new CommandBatch(ctx, `${ALGORITHM}/rounds`);
4646
+ const batch = new CommandBatch(ctx, `${ALGORITHM$4}/rounds`);
3594
4647
  pass = batch.pass("edge-rounds");
3595
- for (let i = 0; i < ROUNDS_PER_BATCH; i++) {
4648
+ for (let i = 0; i < ROUNDS_PER_BATCH$1; i++) {
3596
4649
  const params = wccParams({ items: edgeCount, stride: edgePlan.stride ?? edgeCount, r: 0, giant });
3597
4650
  const bound = linkEdges.bind({ ...edgeBindings, P: params.binding });
3598
4651
  linkEdges.dispatch(pass, bound, edgePlan, [params.offset]);
@@ -3602,10 +4655,10 @@ async function connectedComponents(ctx, s, options) {
3602
4655
  const flagRequest = batch.readback(comp, 4 * flagIndex, 4);
3603
4656
  const submitted = submit(batch);
3604
4657
  const back = await submitted.readback;
3605
- rounds += ROUNDS_PER_BATCH;
4658
+ rounds += ROUNDS_PER_BATCH$1;
3606
4659
  ctx.assertReady();
3607
4660
  if (options?.signal?.aborted) {
3608
- throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted`, {
4661
+ throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM$4}: the signal was aborted`, {
3609
4662
  batchId: submitted.id
3610
4663
  });
3611
4664
  }
@@ -3615,12 +4668,12 @@ async function connectedComponents(ctx, s, options) {
3615
4668
  if (rounds >= MAX_WCC_ROUNDS) {
3616
4669
  throw new WebGpuGraphError(
3617
4670
  "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` }
4671
+ `${ALGORITHM$4}: the changed flag never settled in ${MAX_WCC_ROUNDS} rounds`,
4672
+ { label: ALGORITHM$4, message: `the changed flag never settled in ${MAX_WCC_ROUNDS} rounds` }
3620
4673
  );
3621
4674
  }
3622
4675
  }
3623
- const final = new CommandBatch(ctx, `${ALGORITHM}/final`);
4676
+ const final = new CommandBatch(ctx, `${ALGORITHM$4}/final`);
3624
4677
  recordCompress(final.pass("compress"));
3625
4678
  final.endPass();
3626
4679
  await submit(final).readback;
@@ -3801,13 +4854,13 @@ class TieredPlanner {
3801
4854
  this.dispatches++;
3802
4855
  }
3803
4856
  const { windows } = core;
3804
- windows.forEach((w2, k) => {
4857
+ windows.forEach((w, k) => {
3805
4858
  const windowed = {
3806
4859
  ...core,
3807
- colIdx: windowBinding(core, "colIdx", w2),
3808
- weights: core.weights === null ? null : windowBinding(core, "weights", w2)
4860
+ colIdx: windowBinding(core, "colIdx", w),
4861
+ weights: core.weights === null ? null : windowBinding(core, "weights", w)
3809
4862
  };
3810
- this.recordWindow(pass, windowed, out, n, w2, k + 1 < windows.length ? windows[k + 1].start : w2.end);
4863
+ this.recordWindow(pass, windowed, out, n, w, k + 1 < windows.length ? windows[k + 1].start : w.end);
3811
4864
  });
3812
4865
  }
3813
4866
  /**
@@ -3824,16 +4877,16 @@ class TieredPlanner {
3824
4877
  * @param w - the window, or null for the single whole-core dispatch
3825
4878
  * @param nextStart - the next window's first arc (w.end for the last window)
3826
4879
  */
3827
- recordWindow(pass, core, out, n, w2, nextStart) {
4880
+ recordWindow(pass, core, out, n, w, nextStart) {
3828
4881
  const wg = this.scope.workgroupSize;
3829
4882
  for (const { tier, kernel, start, end } of this.tiers) {
3830
4883
  let first = start;
3831
4884
  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;
4885
+ let arcEnd = w === null ? arcCountOf(core) : Math.min(w.end, nextStart);
4886
+ if (w !== null && this.perm === null) {
4887
+ first = w.rowFirst;
4888
+ last = w.rowLast + 1;
4889
+ arcEnd = w.end;
3837
4890
  }
3838
4891
  const rows = last - first;
3839
4892
  if (rows <= 0) {
@@ -3842,9 +4895,9 @@ class TieredPlanner {
3842
4895
  const params = this.scope.params(RANGE_PARAMS, {
3843
4896
  start: first,
3844
4897
  end: last,
3845
- arcBase: w2 === null ? 0 : w2.start,
4898
+ arcBase: w === null ? 0 : w.start,
3846
4899
  arcEnd,
3847
- accumulate: w2 !== null || this.accumulate ? 1 : 0,
4900
+ accumulate: w !== null || this.accumulate ? 1 : 0,
3848
4901
  n
3849
4902
  });
3850
4903
  const bound = kernel.bind({ ...graphBindings(core, this.perm), out, P: params.binding });
@@ -3986,8 +5039,8 @@ async function prepareSpmvPull(scope, rev, options) {
3986
5039
  return new SpmvPullPlannerImpl(scope, compiled, perm, options.weights);
3987
5040
  }
3988
5041
  const PR_BATCH = 8;
3989
- const RING_SLOTS$1 = 2 * PR_BATCH + 1;
3990
- function checkDest$1(dest, n, algorithm) {
5042
+ const RING_SLOTS$4 = 2 * PR_BATCH + 1;
5043
+ function checkDest$3(dest, n, algorithm) {
3991
5044
  if (dest === void 0) {
3992
5045
  return null;
3993
5046
  }
@@ -4009,10 +5062,10 @@ function coreOf$1(ctx, s, algorithm) {
4009
5062
  assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, algorithm);
4010
5063
  return core;
4011
5064
  }
4012
- function bindingOf(buffer, size) {
5065
+ function bindingOf$2(buffer, size) {
4013
5066
  return { buffer, offset: 0, size, window: null };
4014
5067
  }
4015
- function aborted$1(algorithm, batchId) {
5068
+ function aborted$3(algorithm, batchId) {
4016
5069
  return new WebGpuGraphError(
4017
5070
  "E_ABORTED",
4018
5071
  `${algorithm}: the signal was aborted`,
@@ -4034,9 +5087,9 @@ async function run(ctx, s, personalization, options, algorithm) {
4034
5087
  expected: "a positive integer"
4035
5088
  });
4036
5089
  }
4037
- const dest = checkDest$1(options?.dest, n, algorithm);
5090
+ const dest = checkDest$3(options?.dest, n, algorithm);
4038
5091
  if (options?.signal?.aborted) {
4039
- throw aborted$1(algorithm);
5092
+ throw aborted$3(algorithm);
4040
5093
  }
4041
5094
  if (n === 0) {
4042
5095
  options?.onProgress?.(maxIterations, maxIterations);
@@ -4064,7 +5117,7 @@ async function run(ctx, s, personalization, options, algorithm) {
4064
5117
  const weights = useWeights ? void 0 : null;
4065
5118
  const weightedCore = useWeights ? core : { ...core, weights: null, hasWeights: false };
4066
5119
  const weightedRev = useWeights ? rev : { ...rev, weights: null, hasWeights: false };
4067
- const scope = algorithmScope(ctx, algorithm, RING_SLOTS$1);
5120
+ const scope = algorithmScope(ctx, algorithm, RING_SLOTS$4);
4068
5121
  let uploaded = null;
4069
5122
  try {
4070
5123
  const bytes = 4 * n;
@@ -4100,10 +5153,10 @@ async function run(ctx, s, personalization, options, algorithm) {
4100
5153
  const header = new ArrayBuffer(PR_PARTIAL.byteLength);
4101
5154
  PR_PARTIAL.write(new DataView(header), { firstConverged: U32_MAX$2, iteration: 0 });
4102
5155
  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);
5156
+ const rank = [bindingOf$2(rankA, bytes), bindingOf$2(rankB, bytes)];
5157
+ const xNormBinding = bindingOf$2(xNorm, bytes);
5158
+ const outWeightSumBinding = bindingOf$2(outWeightSum, bytes);
5159
+ const partialsBinding = bindingOf$2(partials, partialsBytes);
4107
5160
  const coefficients = { alpha, beta: 1 - alpha, uniformP: 1 / n };
4108
5161
  let cur = 0;
4109
5162
  let iterationsRun = 0;
@@ -4156,7 +5209,7 @@ async function run(ctx, s, personalization, options, algorithm) {
4156
5209
  iterationsRun += k;
4157
5210
  ctx.assertReady();
4158
5211
  if (options?.signal?.aborted) {
4159
- throw aborted$1(algorithm, submitted.id);
5212
+ throw aborted$3(algorithm, submitted.id);
4160
5213
  }
4161
5214
  options?.onProgress?.(iterationsRun, maxIterations);
4162
5215
  const folded = PR_PARTIAL.read(new DataView(back), headerRequest.offset);
@@ -4199,8 +5252,8 @@ async function personalizedPageRank(ctx, s, personalization, options) {
4199
5252
  );
4200
5253
  }
4201
5254
  let total = 0;
4202
- for (let v2 = 0; v2 < n; v2++) {
4203
- const mass = personalization[v2];
5255
+ for (let v = 0; v < n; v++) {
5256
+ const mass = personalization[v];
4204
5257
  if (!Number.isFinite(mass) || mass < 0) {
4205
5258
  throw invalid2(mass, "finite and non-negative in every entry");
4206
5259
  }
@@ -4210,14 +5263,14 @@ async function personalizedPageRank(ctx, s, personalization, options) {
4210
5263
  throw invalid2(total, "a vector whose entries sum to a positive number");
4211
5264
  }
4212
5265
  const normalised2 = new Float32Array(n);
4213
- for (let v2 = 0; v2 < n; v2++) {
4214
- normalised2[v2] = personalization[v2] / total;
5266
+ for (let v = 0; v < n; v++) {
5267
+ normalised2[v] = personalization[v] / total;
4215
5268
  }
4216
5269
  return run(ctx, s, normalised2, options, "personalizedPageRank");
4217
5270
  }
4218
5271
  const BATCH = 8;
4219
- const RING_SLOTS = 4 * BATCH + 8;
4220
- function checkDest(dest, n, algorithm) {
5272
+ const RING_SLOTS$3 = 4 * BATCH + 8;
5273
+ function checkDest$2(dest, n, algorithm) {
4221
5274
  if (dest === void 0) {
4222
5275
  return null;
4223
5276
  }
@@ -4243,7 +5296,7 @@ function reverseOf(ctx, s) {
4243
5296
  const view = ctx.residency.view(s, "reverse");
4244
5297
  return coreOfView(view, view.scalars.arcCount[0]);
4245
5298
  }
4246
- function aborted(algorithm, batchId) {
5299
+ function aborted$2(algorithm, batchId) {
4247
5300
  return new WebGpuGraphError(
4248
5301
  "E_ABORTED",
4249
5302
  `${algorithm}: the signal was aborted`,
@@ -4255,7 +5308,7 @@ function whole(buffer, size) {
4255
5308
  }
4256
5309
  async function runPowerIteration(ctx, n, config) {
4257
5310
  await assertDeviceComputes(ctx);
4258
- const scope = algorithmScope(ctx, config.label, RING_SLOTS);
5311
+ const scope = algorithmScope(ctx, config.label, RING_SLOTS$3);
4259
5312
  try {
4260
5313
  const bytes = 4 * n;
4261
5314
  const ring = (config.alternate === null ? ["rankA", "rankB"] : ["rankA", "rankB", "rankC"]).map(
@@ -4328,7 +5381,7 @@ async function runPowerIteration(ctx, n, config) {
4328
5381
  iterationsRun += k;
4329
5382
  ctx.assertReady();
4330
5383
  if (config.signal?.aborted === true) {
4331
- throw aborted(config.label, submitted.id);
5384
+ throw aborted$2(config.label, submitted.id);
4332
5385
  }
4333
5386
  config.onProgress?.(iterationsRun, config.maxIterations);
4334
5387
  const folded = PR_PARTIAL.read(new DataView(back), headerRequest.offset);
@@ -4363,9 +5416,9 @@ function resolve(ctx, s, options, algorithm) {
4363
5416
  expected: "a positive integer"
4364
5417
  });
4365
5418
  }
4366
- const dest = checkDest(options?.dest, n, algorithm);
5419
+ const dest = checkDest$2(options?.dest, n, algorithm);
4367
5420
  if (options?.signal?.aborted) {
4368
- throw aborted(algorithm);
5421
+ throw aborted$2(algorithm);
4369
5422
  }
4370
5423
  return { n, maxIterations, tolerance, weights: options?.weighted === false ? null : void 0, dest };
4371
5424
  }
@@ -4382,13 +5435,13 @@ function finite(value, fallback, argument) {
4382
5435
  }
4383
5436
  function normalised(raw, dest, norm) {
4384
5437
  let acc = 0;
4385
- for (const v2 of raw) {
4386
- acc += norm === "sum" ? Math.abs(v2) : v2 * v2;
5438
+ for (const v of raw) {
5439
+ acc += norm === "sum" ? Math.abs(v) : v * v;
4387
5440
  }
4388
5441
  const scale = norm === "sum" ? acc : Math.sqrt(acc);
4389
5442
  const out = dest ?? raw;
4390
- for (let v2 = 0; v2 < raw.length; v2++) {
4391
- out[v2] = scale > 0 ? raw[v2] / scale : raw[v2];
5443
+ for (let v = 0; v < raw.length; v++) {
5444
+ out[v] = scale > 0 ? raw[v] / scale : raw[v];
4392
5445
  }
4393
5446
  return out;
4394
5447
  }
@@ -4433,76 +5486,1857 @@ async function eigenvectorCentrality(ctx, s, options) {
4433
5486
  });
4434
5487
  return scoresOf(run2, r.dest, "l2");
4435
5488
  }
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);
5489
+ async function katzCentrality(ctx, s, options) {
5490
+ const algorithm = "katzCentrality";
5491
+ const alpha = finite(options?.alpha, 0.1, "alpha");
5492
+ const beta = finite(options?.beta, 1, "beta");
5493
+ const r = resolve(ctx, s, options, algorithm);
5494
+ if (r.n === 0) {
5495
+ return empty(r.dest, r.maxIterations, options?.onProgress);
5496
+ }
5497
+ const run2 = await runPowerIteration(ctx, r.n, {
5498
+ normMode: 4,
5499
+ adjacency: reverseOf(ctx, s),
5500
+ alternate: null,
5501
+ alpha,
5502
+ beta,
5503
+ uniformP: 1,
5504
+ maxIterations: r.maxIterations,
5505
+ tolerance: r.tolerance,
5506
+ weights: r.weights,
5507
+ label: algorithm,
5508
+ signal: options?.signal,
5509
+ onProgress: options?.onProgress
5510
+ });
5511
+ return scoresOf(run2, r.dest, "l2");
5512
+ }
5513
+ async function hits(ctx, s, options) {
5514
+ const algorithm = "hits";
5515
+ const r = resolve(ctx, s, options, algorithm);
5516
+ const total = 2 * r.maxIterations;
5517
+ if (r.n === 0) {
5518
+ const { scores, iterations, converged, precision } = empty(r.dest, total, options?.onProgress);
5519
+ return { hubs: scores, authorities: new Float32Array(0), iterations, converged, precision };
5520
+ }
5521
+ const onProgress = options?.onProgress;
5522
+ const shared = {
5523
+ normMode: 1,
5524
+ alpha: 1,
5525
+ beta: 0,
5526
+ uniformP: 0,
5527
+ maxIterations: r.maxIterations,
5528
+ tolerance: r.tolerance,
5529
+ weights: r.weights,
5530
+ signal: options?.signal
5531
+ };
5532
+ const forward = coreOf(ctx, s, algorithm);
5533
+ const reverse = reverseOf(ctx, s);
5534
+ const hubSeeded = await runPowerIteration(ctx, r.n, {
5535
+ ...shared,
5536
+ adjacency: reverse,
5537
+ alternate: forward,
5538
+ label: `${algorithm}/hub-seeded`,
5539
+ onProgress: onProgress === void 0 ? void 0 : (done) => {
5540
+ onProgress(done, total);
5541
+ }
5542
+ });
5543
+ const authoritySeeded = await runPowerIteration(ctx, r.n, {
5544
+ ...shared,
5545
+ adjacency: forward,
5546
+ alternate: reverse,
5547
+ label: `${algorithm}/authority-seeded`,
5548
+ onProgress: onProgress === void 0 ? void 0 : (done) => {
5549
+ onProgress(r.maxIterations + done, total);
5550
+ }
5551
+ });
5552
+ return {
5553
+ hubs: normalised(latestOfKind(hubSeeded, authoritySeeded), r.dest, "sum"),
5554
+ authorities: normalised(latestOfKind(authoritySeeded, hubSeeded), null, "sum"),
5555
+ iterations: Math.max(hubSeeded.iterations, authoritySeeded.iterations),
5556
+ converged: hubSeeded.converged && authoritySeeded.converged,
5557
+ precision: "f32"
5558
+ };
5559
+ }
5560
+ async function prepareCompact(scope) {
5561
+ const scatter = await scope.pipelines.kernel(kernelSpec("compact-scatter"));
5562
+ const claim = await scope.pipelines.kernel(kernelSpec("dedupe-claim"));
5563
+ const filter = await scope.pipelines.kernel(kernelSpec("dedupe-filter"));
5564
+ const scan = await prepareScan(scope);
5565
+ return new CompactPlannerImpl(scope, scatter, claim, filter, scan);
5566
+ }
5567
+ function checkCount(what, count) {
5568
+ if (!Number.isSafeInteger(count) || count < 0 || count > U32_MAX$2) {
5569
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${what}: count must be a non-negative integer below 2^32`, {
5570
+ argument: "count",
5571
+ value: count
5572
+ });
5573
+ }
5574
+ }
5575
+ function checkWords$1(what, name, binding, words) {
5576
+ if (binding.size < 4 * words) {
5577
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${what}: ${name} is smaller than 4 x ${words} bytes`, {
5578
+ argument: name,
5579
+ value: binding.size,
5580
+ expected: 4 * words
5581
+ });
5582
+ }
5583
+ }
5584
+ function checkWordIndex(what, name, index, block) {
5585
+ if (!Number.isSafeInteger(index) || index < 0 || 4 * (index + 1) > block.size) {
5586
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${what}: ${name} ${index} is outside the block`, {
5587
+ argument: name,
5588
+ value: index,
5589
+ expected: `0 <= ${name} < ${Math.floor(block.size / 4)}`
5590
+ });
5591
+ }
5592
+ }
5593
+ function checkCompactArguments(r) {
5594
+ checkCount("compact", r.count);
5595
+ checkWords$1("compact", "queue", r.queue, r.count);
5596
+ checkWords$1("compact", "flags", r.flags, r.count);
5597
+ checkWords$1("compact", "out", r.out, r.count);
5598
+ checkWordIndex("compact", "outIndex", r.outIndex, r.outCount);
5599
+ }
5600
+ function checkDedupeArguments(r) {
5601
+ checkCount("dedupe", r.count);
5602
+ checkWords$1("dedupe", "queue", r.queue, r.count);
5603
+ checkWords$1("dedupe", "out", r.out, Math.min(r.count, Math.floor(r.owner.size / 4)));
5604
+ checkWordIndex("dedupe", "outIndex", r.outIndex, r.outCount);
5605
+ if (r.countIndex === U32_MAX$2) {
5606
+ return;
5607
+ }
5608
+ checkWordIndex("dedupe", "countIndex", r.countIndex, r.counters);
5609
+ if (r.countIndex === r.outIndex) {
5610
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "dedupe: countIndex must not be the output word outIndex", {
5611
+ argument: "countIndex",
5612
+ value: r.countIndex
5613
+ });
5614
+ }
5615
+ if (r.counters.buffer !== r.outCount.buffer || r.counters.offset !== r.outCount.offset || r.counters.size !== r.outCount.size) {
5616
+ throw new WebGpuGraphError(
5617
+ "E_INVALID_ARGUMENT",
5618
+ "dedupe: counters must be the same range as outCount when countIndex names a device word",
5619
+ {
5620
+ argument: "counters",
5621
+ value: r.counters.size,
5622
+ expected: r.outCount.size
5623
+ }
5624
+ );
5625
+ }
5626
+ }
5627
+ class CompactPlannerImpl {
5628
+ /**
5629
+ * Wraps the resolved kernels; use prepareCompact().
5630
+ * @param scope - the caller's scope
5631
+ * @param scatter - the `compact-scatter` kernel
5632
+ * @param claim - the `dedupe-claim` kernel
5633
+ * @param filter - the `dedupe-filter` kernel
5634
+ * @param scan - the scan planner of the same scope
5635
+ */
5636
+ constructor(scope, scatter, claim, filter, scan) {
5637
+ this.dispatches = 0;
5638
+ this.scope = scope;
5639
+ this.scatter = scatter;
5640
+ this.claim = claim;
5641
+ this.filter = filter;
5642
+ this.scan = scan;
5643
+ }
5644
+ /**
5645
+ * Dispatches the last record*() issued.
5646
+ * @returns the count
5647
+ */
5648
+ get lastDispatches() {
5649
+ return this.dispatches;
5650
+ }
5651
+ /**
5652
+ * Records the scan and the scatter (see the interface).
5653
+ * @param pass - the compute pass
5654
+ * @param r - the record
5655
+ */
5656
+ record(pass, r) {
5657
+ checkCompactArguments(r);
5658
+ if (r.count === 0) {
5659
+ this.dispatches = 0;
5660
+ return;
5661
+ }
5662
+ const size = 4 * r.count;
5663
+ const offsets = { buffer: this.scope.scratch(size, "compact/offsets"), offset: 0, size, window: null };
5664
+ this.scan.record(pass, r.flags, r.count, offsets);
5665
+ const params = this.scope.params(COMPACT_PARAMS, {
5666
+ count: r.count,
5667
+ outIndex: r.outIndex,
5668
+ countIndex: U32_MAX$2,
5669
+ stride: 0
5670
+ });
5671
+ const bound = this.scatter.bind({
5672
+ queue: r.queue,
5673
+ flags: r.flags,
5674
+ offsets,
5675
+ out: r.out,
5676
+ outCount: r.outCount,
5677
+ P: params.binding
5678
+ });
5679
+ this.scatter.dispatch(pass, bound, plan1d(r.count, this.scope.workgroupSize, this.scope.caps), [params.offset]);
5680
+ this.dispatches = this.scan.lastDispatches + 1;
5681
+ }
5682
+ /**
5683
+ * Records the claim and the filter over plan1d(count) (see the interface).
5684
+ * @param pass - the compute pass
5685
+ * @param r - the record
5686
+ */
5687
+ recordDedupe(pass, r) {
5688
+ checkDedupeArguments(r);
5689
+ if (r.count === 0) {
5690
+ this.dispatches = 0;
5691
+ return;
5692
+ }
5693
+ const plan = planGridStride(r.count, this.scope.workgroupSize, this.scope.caps);
5694
+ const bound = this.bindDedupe(r, plan.stride ?? this.scope.workgroupSize);
5695
+ this.claim.dispatch(pass, bound.claim, plan, [bound.offset]);
5696
+ this.filter.dispatch(pass, bound.filter, plan, [bound.offset]);
5697
+ this.dispatches = 2;
5698
+ }
5699
+ /**
5700
+ * One params record (both kernels read the same values) and the two bind groups.
5701
+ * @param r - the record
5702
+ * @param stride - the grid-stride plan's stride (entries per pass over the grid)
5703
+ * @returns the bound kernels and the record's dynamic offset
5704
+ */
5705
+ bindDedupe(r, stride) {
5706
+ const params = this.scope.params(COMPACT_PARAMS, {
5707
+ count: r.count,
5708
+ outIndex: r.outIndex,
5709
+ countIndex: r.countIndex,
5710
+ stride
5711
+ });
5712
+ return {
5713
+ claim: this.claim.bind({ queue: r.queue, owner: r.owner, counters: r.counters, P: params.binding }),
5714
+ filter: this.filter.bind({
5715
+ queue: r.queue,
5716
+ owner: r.owner,
5717
+ out: r.out,
5718
+ outCount: r.outCount,
5719
+ P: params.binding
5720
+ }),
5721
+ offset: params.offset
5722
+ };
5723
+ }
5724
+ }
5725
+ const W = Object.freeze({
5726
+ frontierCount: 0,
5727
+ nextFrontierCount: 1,
5728
+ frontierDegreeSum: 2,
5729
+ prevFrontierCount: 3,
5730
+ prevDegreeSum: 4,
5731
+ unvisitedCount: 5,
5732
+ unvisitedDegreeSum: 6,
5733
+ unvisitedListLen: 7,
5734
+ edgeCount: 8,
5735
+ edgeCountUnclamped: 9,
5736
+ overflowLevels: 10,
5737
+ level: 11,
5738
+ visitedCount: 12,
5739
+ switches: 13,
5740
+ direction: 14,
5741
+ done: 15,
5742
+ arcsScanned: 16,
5743
+ fusedLevels: 17,
5744
+ twoPhaseLevels: 18,
5745
+ bottomUpLevels: 19,
5746
+ farCount: 20,
5747
+ nextFarCount: 21,
5748
+ thresholdBits: 22,
5749
+ deltaBits: 23,
5750
+ path: 24
5751
+ });
5752
+ function definedWords(words) {
5753
+ const out = {};
5754
+ for (const [name, value] of Object.entries(words)) {
5755
+ if (value !== void 0) {
5756
+ out[name] = value;
5757
+ }
5758
+ }
5759
+ return out;
5760
+ }
5761
+ const MAX_ROLE = 3;
5762
+ function assertCount(argument, value) {
5763
+ if (!Number.isSafeInteger(value) || value < 0) {
5764
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: ${argument} must be a non-negative integer`, {
5765
+ argument,
5766
+ value,
5767
+ expected: "a non-negative integer"
5768
+ });
5769
+ }
5770
+ }
5771
+ class Frontier {
5772
+ /**
5773
+ * Wraps the leased buffers; use prepareFrontier().
5774
+ * @param vertices - the two vertex queues
5775
+ * @param counters - the counters block
5776
+ * @param edgeQueue - the edge queue
5777
+ * @param edgeCapacity - the edge queue's entry count
5778
+ * @param n - the vertex count
5779
+ */
5780
+ constructor(vertices, counters, edgeQueue, edgeCapacity, n) {
5781
+ this.sideIndex = 0;
5782
+ this.vertices = vertices;
5783
+ this.counters = counters;
5784
+ this.edgeQueue = edgeQueue;
5785
+ this.edgeCapacity = edgeCapacity;
5786
+ this.n = n;
5787
+ }
5788
+ /**
5789
+ * Which vertex queue is the input of the current level (a host-side index between two cached bind-group sets).
5790
+ * @returns 0 or 1
5791
+ */
5792
+ get side() {
5793
+ return this.sideIndex;
5794
+ }
5795
+ /**
5796
+ * The vertex queue the current level expands from.
5797
+ * @returns `vertices[side]`
5798
+ */
5799
+ get input() {
5800
+ return this.sideIndex === 0 ? this.vertices[0] : this.vertices[1];
5801
+ }
5802
+ /**
5803
+ * The vertex queue the current level's claims append to.
5804
+ * @returns `vertices[1 - side]`
5805
+ */
5806
+ get output() {
5807
+ return this.sideIndex === 0 ? this.vertices[1] : this.vertices[0];
5808
+ }
5809
+ /** Flips the two vertex queues; the counts rotate inside the block, so nothing else moves. */
5810
+ swap() {
5811
+ this.sideIndex = this.sideIndex === 0 ? 1 : 0;
5812
+ }
5813
+ /**
5814
+ * Seeds a traversal: one `queue.writeBuffer` of the whole 112-byte block (zero except the caller's words) and one of
5815
+ * `vertices[0][0] = source`, both ordered before the submit that follows; the source is on side 0 afterwards.
5816
+ * `frontierCount` is not a word to seed: the first boundary rotates word 1 into it (the BFS seed is
5817
+ * `{ nextFrontierCount: 1, level: U32_MAX }`). A source outside `[0, n)`, an unknown word or a value that is not a
5818
+ * u32 is E_INVALID_ARGUMENT before anything is written.
5819
+ * @param queue - the device queue
5820
+ * @param source - the source vertex
5821
+ * @param seed - the words to seed
5822
+ */
5823
+ reset(queue, source, seed) {
5824
+ if (!Number.isInteger(source) || source < 0 || source >= this.n) {
5825
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: source ${source} is outside [0, ${this.n})`, {
5826
+ argument: "source",
5827
+ value: source,
5828
+ expected: `an integer in [0, ${this.n})`
5829
+ });
5830
+ }
5831
+ const bytes = new ArrayBuffer(FRONTIER_COUNTERS.byteLength);
5832
+ FRONTIER_COUNTERS.write(new DataView(bytes), definedWords(seed));
5833
+ this.sideIndex = 0;
5834
+ queue.writeBuffer(this.counters.buffer, this.counters.offset, bytes);
5835
+ queue.writeBuffer(this.vertices[0].buffer, this.vertices[0].offset, Uint32Array.of(source));
5836
+ }
5837
+ }
5838
+ async function prepareFrontier(scope, n, arcCount, edgeCapacity) {
5839
+ assertCount("n", n);
5840
+ assertCount("arcCount", arcCount);
5841
+ const capacity = edgeCapacity ?? Math.max(1, Math.min(arcCount, Math.floor(scope.caps.limits.maxStorageBufferBindingSize / 4)));
5842
+ if (!Number.isSafeInteger(capacity) || capacity < 1 || capacity > U32_MAX$2) {
5843
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "frontier: edgeCapacity must be an integer >= 1", {
5844
+ argument: "edgeCapacity",
5845
+ value: capacity,
5846
+ expected: "an integer in [1, 2^32)"
5847
+ });
5848
+ }
5849
+ const kernel = await scope.pipelines.kernel(kernelSpec("frontier-finalize"));
5850
+ const queueBytes = 4 * Math.max(1, n);
5851
+ const vertices = [
5852
+ { buffer: scope.scratch(queueBytes, "frontier/vertices-0"), offset: 0, size: queueBytes, window: null },
5853
+ { buffer: scope.scratch(queueBytes, "frontier/vertices-1"), offset: 0, size: queueBytes, window: null }
5854
+ ];
5855
+ const counters = {
5856
+ buffer: scope.scratch(FRONTIER_COUNTERS.byteLength, "frontier/counters"),
5857
+ offset: 0,
5858
+ size: FRONTIER_COUNTERS.byteLength,
5859
+ window: null
5860
+ };
5861
+ const edgeQueue = {
5862
+ buffer: scope.scratch(4 * capacity, "frontier/edge-queue"),
5863
+ offset: 0,
5864
+ size: 4 * capacity,
5865
+ window: null
5866
+ };
5867
+ const frontier = new Frontier(vertices, counters, edgeQueue, capacity, n);
5868
+ return new FrontierPlannerImpl(scope, kernel, frontier);
5869
+ }
5870
+ class FrontierPlannerImpl {
5871
+ /**
5872
+ * Wraps the resolved kernel; use prepareFrontier().
5873
+ * @param scope - the caller's scope
5874
+ * @param kernel - the `frontier-finalize` kernel
5875
+ * @param frontier - the leased queue
5876
+ */
5877
+ constructor(scope, kernel, frontier) {
5878
+ this.scope = scope;
5879
+ this.kernel = kernel;
5880
+ this.frontier = frontier;
5881
+ }
5882
+ /**
5883
+ * Records one selector dispatch (see the interface).
5884
+ * @param pass - the compute pass
5885
+ * @param role - the role
5886
+ * @param level - the level inside the submit
5887
+ * @param fields - the caller's fields
5888
+ */
5889
+ recordFinalize(pass, role, level, fields) {
5890
+ if (!Number.isInteger(level) || level < 0 || level >= MAX_LEVELS_PER_SUBMIT) {
5891
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: level ${level} is outside the submit`, {
5892
+ argument: "level",
5893
+ value: level,
5894
+ expected: `an integer in [0, ${MAX_LEVELS_PER_SUBMIT})`
5895
+ });
5896
+ }
5897
+ if (!Number.isInteger(role) || role < 0 || role > MAX_ROLE) {
5898
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: role ${role} is not a finalize role`, {
5899
+ argument: "role",
5900
+ value: role,
5901
+ expected: `an integer in [0, ${MAX_ROLE}]`
5902
+ });
5903
+ }
5904
+ const { scope, frontier } = this;
5905
+ const params = scope.params(FRONTIER_PARAMS, {
5906
+ ...definedWords(fields),
5907
+ role,
5908
+ wg: scope.workgroupSize,
5909
+ edgeCapacity: frontier.edgeCapacity,
5910
+ n: frontier.n
5911
+ });
5912
+ const bound = this.kernel.bind({ counters: frontier.counters, P: params.binding });
5913
+ this.kernel.dispatch(pass, bound, plan1d(1, scope.workgroupSize, scope.caps), [params.offset]);
5914
+ }
5915
+ }
5916
+ async function prepareAdvance(scope, core) {
5917
+ const kernel = await scope.pipelines.kernel(kernelSpec("advance-expand", graphOverrides(core, null)));
5918
+ return new AdvancePlannerImpl(scope, core, kernel);
5919
+ }
5920
+ class AdvancePlannerImpl {
5921
+ /**
5922
+ * Wraps the resolved kernel; use prepareAdvance().
5923
+ * @param scope - the caller's scope
5924
+ * @param core - the core the kernel was compiled for
5925
+ * @param kernel - the `advance-expand` kernel
5926
+ */
5927
+ constructor(scope, core, kernel) {
5928
+ this.scope = scope;
5929
+ this.kernel = kernel;
5930
+ this.windows = coreWindows(core);
5931
+ this.n = rowCountOf(core, "advance");
5932
+ }
5933
+ /**
5934
+ * Records one direct expansion per window (see the interface).
5935
+ * @param pass - the compute pass
5936
+ * @param frontier - the queue to expand
5937
+ */
5938
+ record(pass, frontier) {
5939
+ if (frontier.n !== this.n) {
5940
+ throw new WebGpuGraphError(
5941
+ "E_INVALID_ARGUMENT",
5942
+ `advance: the frontier holds ${frontier.n} vertices, the core ${this.n} rows`,
5943
+ {
5944
+ argument: "frontier",
5945
+ value: frontier.n,
5946
+ expected: this.n
5947
+ }
5948
+ );
5949
+ }
5950
+ const { scope } = this;
5951
+ const plan = planGridStride(frontier.n, scope.workgroupSize, scope.caps);
5952
+ for (const w of this.windows) {
5953
+ const params = scope.params(FRONTIER_PARAMS, {
5954
+ wg: scope.workgroupSize,
5955
+ edgeCapacity: frontier.edgeCapacity,
5956
+ n: frontier.n,
5957
+ arcBase: w.arcBase,
5958
+ arcEnd: w.arcEnd,
5959
+ stride: plan.stride ?? scope.workgroupSize
5960
+ });
5961
+ const bound = this.kernel.bind({
5962
+ ...graphBindings(w.core, null),
5963
+ frontierIn: frontier.input,
5964
+ counters: frontier.counters,
5965
+ edgeQueue: frontier.edgeQueue,
5966
+ P: params.binding
5967
+ });
5968
+ this.kernel.dispatch(pass, bound, plan, [params.offset]);
5969
+ }
5970
+ }
5971
+ }
5972
+ const RADIX_DIGIT_BITS = 8;
5973
+ function radixHistBytes(count, wg) {
5974
+ return 4 * RADIX_BINS * Math.ceil(count / wg);
5975
+ }
5976
+ async function prepareRadixSort(scope) {
5977
+ const hist = await scope.pipelines.kernel(kernelSpec("radix-hist"));
5978
+ const scatter = await scope.pipelines.kernel(kernelSpec("radix-scatter"));
5979
+ const scan = await prepareScan(scope);
5980
+ return new RadixSortPlannerImpl(scope, hist, scatter, scan);
5981
+ }
5982
+ function checkBinding(argument, binding, bytes) {
5983
+ if (binding.size < bytes) {
5984
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `radixSort: ${argument} is smaller than ${bytes} bytes`, {
5985
+ argument,
5986
+ value: binding.size,
5987
+ expected: bytes
5988
+ });
5989
+ }
5990
+ }
5991
+ function checkRecordArguments(keys, vals, count, bits, scratch, wg) {
5992
+ if (bits !== 8 && bits !== 16 && bits !== 24 && bits !== 32) {
5993
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: bits must be 8, 16, 24 or 32", {
5994
+ argument: "bits",
5995
+ value: bits,
5996
+ expected: [8, 16, 24, 32]
5997
+ });
5998
+ }
5999
+ if (!Number.isSafeInteger(count) || count < 0 || count > U32_MAX$2) {
6000
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: count must be a non-negative integer below 2^32", {
6001
+ argument: "count",
6002
+ value: count
6003
+ });
6004
+ }
6005
+ const pairBytes = 4 * count;
6006
+ checkBinding("keys", keys, pairBytes);
6007
+ checkBinding("vals", vals, pairBytes);
6008
+ checkBinding("scratch.keys", scratch.keys, pairBytes);
6009
+ checkBinding("scratch.vals", scratch.vals, pairBytes);
6010
+ const tableBytes = radixHistBytes(count, wg);
6011
+ checkBinding("scratch.hist", scratch.hist, tableBytes);
6012
+ checkBinding("scratch.offsets", scratch.offsets, tableBytes);
6013
+ }
6014
+ class RadixSortPlannerImpl {
6015
+ /**
6016
+ * Wraps the resolved kernels; use prepareRadixSort().
6017
+ * @param scope - the caller's scope
6018
+ * @param hist - the `radix-hist` kernel
6019
+ * @param scatter - the `radix-scatter` kernel
6020
+ * @param scan - the scan planner of the same scope
6021
+ */
6022
+ constructor(scope, hist, scatter, scan) {
6023
+ this.dispatches = 0;
6024
+ this.scope = scope;
6025
+ this.hist = hist;
6026
+ this.scatter = scatter;
6027
+ this.scan = scan;
6028
+ }
6029
+ /**
6030
+ * Dispatches the last record() issued.
6031
+ * @returns the count
6032
+ */
6033
+ get lastDispatches() {
6034
+ return this.dispatches;
6035
+ }
6036
+ /**
6037
+ * Records the passes into the pass (see the interface).
6038
+ * @param pass - the compute pass
6039
+ * @param keys - the keys
6040
+ * @param vals - the values
6041
+ * @param count - the pair count
6042
+ * @param bits - the key width
6043
+ * @param scratch - the second pair, the histogram table and its scanned twin
6044
+ * @returns the pair the result lives in
6045
+ */
6046
+ record(pass, keys, vals, count, bits, scratch) {
6047
+ const wg = this.scope.workgroupSize;
6048
+ checkRecordArguments(keys, vals, count, bits, scratch, wg);
6049
+ if (count === 0) {
6050
+ this.dispatches = 0;
6051
+ return { keys, vals };
6052
+ }
6053
+ const groups = Math.ceil(count / wg);
6054
+ const plan = plan1d(count, wg, this.scope.caps);
6055
+ const tableWords = RADIX_BINS * groups;
6056
+ const tableBytes = 4 * tableWords;
6057
+ const histTable = { ...scratch.hist, size: tableBytes };
6058
+ const offsets = { ...scratch.offsets, size: tableBytes };
6059
+ let src = { keys, vals };
6060
+ let dst = { keys: scratch.keys, vals: scratch.vals };
6061
+ let dispatches = 0;
6062
+ const passes = bits / RADIX_DIGIT_BITS;
6063
+ for (let p = 0; p < passes; p++) {
6064
+ const params = this.scope.params(RADIX_PARAMS, { count, shift: RADIX_DIGIT_BITS * p, groups, pad0: 0 });
6065
+ const histBound = this.hist.bind({ keys: src.keys, hist: histTable, P: params.binding });
6066
+ this.hist.dispatch(pass, histBound, plan, [params.offset]);
6067
+ this.scan.record(pass, histTable, tableWords, offsets);
6068
+ const scatterBound = this.scatter.bind({
6069
+ keys: src.keys,
6070
+ vals: src.vals,
6071
+ offsets,
6072
+ keysOut: dst.keys,
6073
+ valsOut: dst.vals,
6074
+ P: params.binding
6075
+ });
6076
+ this.scatter.dispatch(pass, scatterBound, plan, [params.offset]);
6077
+ dispatches += 2 + this.scan.lastDispatches;
6078
+ [src, dst] = [dst, src];
6079
+ }
6080
+ this.dispatches = dispatches;
6081
+ return src;
6082
+ }
6083
+ }
6084
+ const ALGORITHM$3 = "breadthFirstSearch";
6085
+ function bfsRingSlots(windows, levelsPerSubmit) {
6086
+ return Math.max((5 + 4 * windows) * levelsPerSubmit + 16, RESULT_BATCH_SLOTS + windows);
6087
+ }
6088
+ const RESULT_BATCH_SLOTS = 2 + 4 * 8;
6089
+ function normaliseMaxDepth(maxDepth) {
6090
+ if (maxDepth === void 0 || Number.isNaN(maxDepth) || maxDepth >= U32_MAX$2) {
6091
+ return U32_MAX$2;
6092
+ }
6093
+ return Math.max(0, Math.ceil(maxDepth));
6094
+ }
6095
+ function checkDest$1(dest, n) {
6096
+ if (dest === void 0) {
6097
+ return null;
6098
+ }
6099
+ if (dest instanceof Uint32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
6100
+ return dest;
6101
+ }
6102
+ throw new WebGpuGraphError(
6103
+ "E_INVALID_ARGUMENT",
6104
+ `${ALGORITHM$3}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
6105
+ {
6106
+ argument: "dest",
6107
+ value: `${dest.constructor.name}(${dest.length})`,
6108
+ expected: `Uint32Array(${n}) over an ArrayBuffer`
6109
+ }
6110
+ );
6111
+ }
6112
+ function bindingOf$1(buffer, size) {
6113
+ return { buffer, offset: 0, size, window: null };
6114
+ }
6115
+ function degreeView(ctx, s, name) {
6116
+ const { bindings } = ctx.residency.view(s, name);
6117
+ const { [name]: binding } = bindings;
6118
+ if (binding === void 0) {
6119
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$3}: the ${name} view has no ${name} binding`, {
6120
+ label: `${ALGORITHM$3}/${name}`,
6121
+ message: `the ${name} view has no ${name} binding`
6122
+ });
6123
+ }
6124
+ return binding;
6125
+ }
6126
+ function aborted$1(batchId) {
6127
+ return new WebGpuGraphError(
6128
+ "E_ABORTED",
6129
+ `${ALGORITHM$3}: the signal was aborted`,
6130
+ batchId === void 0 ? {} : { batchId }
6131
+ );
6132
+ }
6133
+ function wordOf$1(block, name) {
6134
+ const value = block[name];
6135
+ if (typeof value !== "number") {
6136
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$3}: counters.${name} did not decode to a number`, {
6137
+ label: `${ALGORITHM$3}/counters`,
6138
+ message: `the field ${name} did not decode to a number`
6139
+ });
6140
+ }
6141
+ return value;
6142
+ }
6143
+ async function bfsWithTuning(ctx, s, source, options, tuning) {
6144
+ ctx.assertReady();
6145
+ await assertDeviceComputes(ctx);
6146
+ const n = s.nodeCount;
6147
+ if (!Number.isInteger(source) || source < 0 || source >= n) {
6148
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM$3}: source ${source} is outside [0, ${n})`, {
6149
+ argument: "source",
6150
+ value: source,
6151
+ expected: `an integer in [0, ${n})`
6152
+ });
6153
+ }
6154
+ const levelsPerSubmit = tuning.levelsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
6155
+ if (!Number.isInteger(levelsPerSubmit) || levelsPerSubmit < 1 || levelsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
6156
+ throw new WebGpuGraphError(
6157
+ "E_INVALID_ARGUMENT",
6158
+ `${ALGORITHM$3}: levelsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
6159
+ {
6160
+ argument: "levelsPerSubmit",
6161
+ value: levelsPerSubmit,
6162
+ expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
6163
+ }
6164
+ );
6165
+ }
6166
+ const dest = checkDest$1(options?.dest, n);
6167
+ const maxDepth = normaliseMaxDepth(options?.maxDepth);
6168
+ const predKind = tuning.predKind ?? 1;
6169
+ if (options?.signal?.aborted) {
6170
+ throw aborted$1();
6171
+ }
6172
+ const core = ctx.residency.core(s);
6173
+ const forward = coreWindows(core);
6174
+ if (s.directed && 4 * s.arcCount > ctx.caps.limits.maxStorageBufferBindingSize) {
6175
+ throw new WebGpuGraphError(
6176
+ "E_TOO_LARGE",
6177
+ `${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`,
6178
+ {
6179
+ needed: 4 * s.arcCount,
6180
+ limit: ctx.caps.limits.maxStorageBufferBindingSize,
6181
+ path: "windowed",
6182
+ algorithm: ALGORITHM$3
6183
+ }
6184
+ );
6185
+ }
6186
+ const reverse = s.directed ? coreOfView(ctx.residency.view(s, "reverse"), s.arcCount) : core;
6187
+ const backward = coreWindows(reverse);
6188
+ const outDegree = degreeView(ctx, s, "outDegree");
6189
+ const inDegree = degreeView(ctx, s, "inDegree");
6190
+ const scope = algorithmScope(ctx, ALGORITHM$3, bfsRingSlots(forward.length, levelsPerSubmit));
6191
+ tuning.onScope?.(scope);
6192
+ try {
6193
+ const bytes = 4 * n;
6194
+ const wg = ctx.workgroupSize;
6195
+ const depth = bindingOf$1(scope.scratch(bytes, "depth"), bytes);
6196
+ const bitsBase = Math.ceil(n / 64) * 64;
6197
+ const bitsWords = Math.ceil(n / 32);
6198
+ const sweepBytes = 4 * (bitsBase + bitsWords);
6199
+ const sweepIn = bindingOf$1(scope.scratch(sweepBytes, "sweep-in"), sweepBytes);
6200
+ const unvisitedList = { buffer: sweepIn.buffer, offset: 0, size: bytes, window: null };
6201
+ const frontierBits = {
6202
+ buffer: sweepIn.buffer,
6203
+ offset: 4 * bitsBase,
6204
+ size: 4 * bitsWords,
6205
+ window: null
6206
+ };
6207
+ const flags = bindingOf$1(scope.scratch(bytes, "unvisited-flags"), bytes);
6208
+ const iota = bindingOf$1(scope.scratch(bytes, "iota"), bytes);
6209
+ const compactCount = bindingOf$1(scope.scratch(4, "compact-count"), 4);
6210
+ await ctx.allocator.check();
6211
+ const planner = await prepareFrontier(scope, n, s.arcCount, tuning.edgeCapacity);
6212
+ const advance = await prepareAdvance(scope, core);
6213
+ const compact = await prepareCompact(scope);
6214
+ const contract = await ctx.pipelines.kernel(kernelSpec("bfs-contract"));
6215
+ const fused = await ctx.pipelines.kernel(kernelSpec("bfs-fused", graphOverrides(core, null)));
6216
+ const bitset = await ctx.pipelines.kernel(kernelSpec("bfs-bitset-build"));
6217
+ const bottomUp = await ctx.pipelines.kernel(kernelSpec("bfs-bottom-up", graphOverrides(reverse, null)));
6218
+ const unvisited = await ctx.pipelines.kernel(kernelSpec("bfs-unvisited-flags"));
6219
+ const pred = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...graphOverrides(core, null), MODE: 1 }));
6220
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
6221
+ const sort = await prepareRadixSort(scope);
6222
+ const { frontier } = planner;
6223
+ const { counters } = frontier;
6224
+ const { queue } = ctx.device;
6225
+ const fillPlan = plan1d(n, wg, ctx.caps);
6226
+ const levelPlan = planGridStride(Math.max(n, frontier.edgeCapacity), wg, ctx.caps);
6227
+ const sweepPlan = planGridStride(n, wg, ctx.caps);
6228
+ const fusedPlan = planGridStride(n * wg, wg, ctx.caps);
6229
+ const bitsPlan = plan1d(bitsWords, wg, ctx.caps);
6230
+ const recordFill = (pass2, dst, value, mode) => {
6231
+ const params = scope.params(FILL_PARAMS, { count: n, value, mode, pad0: 0 });
6232
+ fill.dispatch(pass2, fill.bind({ dst, P: params.binding }), fillPlan, [params.offset]);
6233
+ };
6234
+ const flagsPlan = planGridStride(n, wg, ctx.caps);
6235
+ const recordRebuild = (pass2) => {
6236
+ const params = scope.params(FRONTIER_PARAMS, { wg, n, stride: flagsPlan.stride ?? n });
6237
+ const bound = unvisited.bind({ outDegree, inDegree, depth, flags, counters, P: params.binding });
6238
+ unvisited.dispatch(pass2, bound, flagsPlan, [params.offset]);
6239
+ compact.record(pass2, {
6240
+ queue: iota,
6241
+ flags,
6242
+ count: n,
6243
+ out: unvisitedList,
6244
+ outCount: compactCount,
6245
+ outIndex: 0
6246
+ });
6247
+ };
6248
+ const submit = (batch) => {
6249
+ scope.flush();
6250
+ return batch.submit();
6251
+ };
6252
+ const setup = new CommandBatch(ctx, `${ALGORITHM$3}/setup`);
6253
+ const setupPass = setup.pass("fill");
6254
+ recordFill(setupPass, depth, INVALID_INDEX, 0);
6255
+ recordFill(setupPass, iota, 0, 1);
6256
+ setup.endPass();
6257
+ await submit(setup).readback;
6258
+ ctx.assertReady();
6259
+ queue.writeBuffer(depth.buffer, depth.offset + 4 * source, Uint32Array.of(0));
6260
+ frontier.reset(queue, source, { nextFrontierCount: 1, level: U32_MAX$2 });
6261
+ const fields = {
6262
+ mode: tuning.direction === "top-down" ? 1 : 0,
6263
+ alpha: tuning.alpha ?? Math.max(1, Math.floor(s.arcCount / n)),
6264
+ beta: tuning.beta ?? BEAMER_BETA,
6265
+ fusedMax: tuning.fusedMax ?? FUSED_FRONTIER_MAX,
6266
+ maxDepth
6267
+ };
6268
+ let levelsRecorded = 0;
6269
+ let submits = 0;
6270
+ for (; ; ) {
6271
+ queue.writeBuffer(counters.buffer, counters.offset + 4 * W.unvisitedCount, new Uint32Array(3));
6272
+ const batch = new CommandBatch(ctx, `${ALGORITHM$3}/levels`);
6273
+ const pass2 = batch.pass("bfs");
6274
+ recordRebuild(pass2);
6275
+ const bitsParams = scope.params(FILL_PARAMS, { count: bitsWords, value: 0, mode: 0, pad0: 0 });
6276
+ const boundBitsFill = fill.bind({ dst: frontierBits, P: bitsParams.binding });
6277
+ for (let level2 = 0; level2 < levelsPerSubmit; level2++) {
6278
+ planner.recordFinalize(pass2, 0, level2, { ...fields, firstOfSubmit: Math.min(level2, 2) });
6279
+ advance.record(pass2, frontier);
6280
+ planner.recordFinalize(pass2, 1, level2, fields);
6281
+ const params = scope.params(FRONTIER_PARAMS, {
6282
+ wg,
6283
+ n,
6284
+ edgeCapacity: frontier.edgeCapacity,
6285
+ arcBase: 0,
6286
+ arcEnd: s.arcCount,
6287
+ bitsBase,
6288
+ stride: levelPlan.stride ?? wg
6289
+ });
6290
+ const boundContract = contract.bind({
6291
+ edgeQueue: frontier.edgeQueue,
6292
+ counters,
6293
+ depth,
6294
+ frontierOut: frontier.output,
6295
+ P: params.binding
6296
+ });
6297
+ contract.dispatch(pass2, boundContract, levelPlan, [params.offset]);
6298
+ for (const w of forward) {
6299
+ const fusedParams = scope.params(FRONTIER_PARAMS, {
6300
+ wg,
6301
+ n,
6302
+ edgeCapacity: frontier.edgeCapacity,
6303
+ arcBase: w.arcBase,
6304
+ arcEnd: w.arcEnd,
6305
+ stride: fusedPlan.x * fusedPlan.y
6306
+ });
6307
+ const boundFused = fused.bind({
6308
+ ...graphBindings(w.core, null),
6309
+ frontierIn: frontier.input,
6310
+ counters,
6311
+ depth,
6312
+ frontierOut: frontier.output,
6313
+ P: fusedParams.binding
6314
+ });
6315
+ fused.dispatch(pass2, boundFused, fusedPlan, [fusedParams.offset]);
6316
+ }
6317
+ fill.dispatch(pass2, boundBitsFill, bitsPlan, [bitsParams.offset]);
6318
+ const boundBitset = bitset.bind({
6319
+ frontierIn: frontier.input,
6320
+ counters,
6321
+ bits: sweepIn,
6322
+ P: params.binding
6323
+ });
6324
+ bitset.dispatch(pass2, boundBitset, levelPlan, [params.offset]);
6325
+ for (const w of backward) {
6326
+ const sweepParams = scope.params(FRONTIER_PARAMS, {
6327
+ wg,
6328
+ n,
6329
+ arcBase: w.arcBase,
6330
+ arcEnd: w.arcEnd,
6331
+ bitsBase,
6332
+ stride: sweepPlan.stride ?? wg
6333
+ });
6334
+ const boundSweep = bottomUp.bind({
6335
+ ...graphBindings(w.core, null),
6336
+ sweepIn,
6337
+ counters,
6338
+ depth,
6339
+ frontierOut: frontier.output,
6340
+ P: sweepParams.binding
6341
+ });
6342
+ bottomUp.dispatch(pass2, boundSweep, sweepPlan, [sweepParams.offset]);
6343
+ }
6344
+ frontier.swap();
6345
+ }
6346
+ batch.endPass();
6347
+ const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
6348
+ const inspect = tuning.onLevel === void 0 ? null : {
6349
+ block: batch.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength),
6350
+ frontier: batch.readback(frontier.input.buffer, frontier.input.offset, frontier.input.size),
6351
+ count: batch.readback(compactCount.buffer, compactCount.offset, 4)
6352
+ };
6353
+ const submitted = submit(batch);
6354
+ const back2 = await submitted.readback;
6355
+ levelsRecorded += levelsPerSubmit;
6356
+ submits += 1;
6357
+ ctx.assertReady();
6358
+ if (options?.signal?.aborted) {
6359
+ throw aborted$1(submitted.id);
6360
+ }
6361
+ options?.onProgress?.(Math.min(levelsRecorded, n), n);
6362
+ if (inspect !== null && tuning.onLevel !== void 0) {
6363
+ const block2 = FRONTIER_COUNTERS.read(new DataView(back2), inspect.block.offset);
6364
+ const claimed = new Uint32Array(back2, inspect.frontier.offset, wordOf$1(block2, "nextFrontierCount"));
6365
+ const rebuilt = new Uint32Array(back2, inspect.count.offset, 1)[0];
6366
+ tuning.onLevel(levelsRecorded - 1, block2, claimed.slice(), rebuilt);
6367
+ }
6368
+ if (new Uint32Array(back2, doneRequest.offset, 1)[0] !== 0) {
6369
+ break;
6370
+ }
6371
+ if (submits > n + 1) {
6372
+ throw new WebGpuGraphError(
6373
+ "E_VALIDATION",
6374
+ `${ALGORITHM$3}: the done flag never rose in ${submits} submits (a traversal has at most ${n} levels)`,
6375
+ { label: ALGORITHM$3, message: `the done flag never rose in ${submits} submits` }
6376
+ );
6377
+ }
6378
+ }
6379
+ const keys = bindingOf$1(scope.scratch(bytes, "order/keys"), bytes);
6380
+ const vals = bindingOf$1(scope.scratch(bytes, "order/vals"), bytes);
6381
+ const histBytes = radixHistBytes(n, wg);
6382
+ const scratch = {
6383
+ keys: bindingOf$1(scope.scratch(bytes, "order/keys-scratch"), bytes),
6384
+ vals: bindingOf$1(scope.scratch(bytes, "order/vals-scratch"), bytes),
6385
+ hist: bindingOf$1(scope.scratch(histBytes, "order/hist"), histBytes),
6386
+ offsets: bindingOf$1(scope.scratch(histBytes, "order/offsets"), histBytes)
6387
+ };
6388
+ const parent = bindingOf$1(scope.scratch(bytes, "parent"), bytes);
6389
+ const result = new CommandBatch(ctx, `${ALGORITHM$3}/result`);
6390
+ result.copy(depth, keys, bytes);
6391
+ const pass = result.pass("result");
6392
+ recordFill(pass, vals, 0, 1);
6393
+ const sorted = sort.record(pass, keys, vals, n, 32, scratch);
6394
+ recordFill(pass, parent, INVALID_INDEX, 0);
6395
+ const predPlan = planGridStride(n, wg, ctx.caps);
6396
+ for (const w of forward) {
6397
+ const predParams = scope.params(FRONTIER_PARAMS, {
6398
+ wg,
6399
+ n,
6400
+ arcBase: w.arcBase,
6401
+ arcEnd: w.arcEnd,
6402
+ predKind,
6403
+ source,
6404
+ stride: predPlan.stride ?? n
6405
+ });
6406
+ const predBound = pred.bind({
6407
+ ...graphBindings(w.core, null),
6408
+ dist: depth,
6409
+ pred: parent,
6410
+ P: predParams.binding
6411
+ });
6412
+ pred.dispatch(pass, predBound, predPlan, [predParams.offset]);
6413
+ }
6414
+ result.endPass();
6415
+ const depthRequest = result.readback(depth.buffer, depth.offset, bytes);
6416
+ const parentRequest = result.readback(parent.buffer, parent.offset, bytes);
6417
+ const orderRequest = result.readback(sorted.vals.buffer, sorted.vals.offset, bytes);
6418
+ const blockRequest = result.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength);
6419
+ const back = await submit(result).readback;
6420
+ ctx.assertReady();
6421
+ const block = FRONTIER_COUNTERS.read(new DataView(back), blockRequest.offset);
6422
+ const visitedCount = wordOf$1(block, "visitedCount");
6423
+ if (visitedCount > n) {
6424
+ throw new WebGpuGraphError(
6425
+ "E_VALIDATION",
6426
+ `${ALGORITHM$3}: visitedCount ${visitedCount} exceeds the ${n} vertices (a duplicate claim)`,
6427
+ {
6428
+ label: `${ALGORITHM$3}/visitedCount`,
6429
+ message: `the device counted ${visitedCount} visits of ${n} vertices`
6430
+ }
6431
+ );
6432
+ }
6433
+ const level = wordOf$1(block, "level");
6434
+ const levels = wordOf$1(block, "frontierCount") === 0 ? level : level + 1;
6435
+ const depthOut = dest ?? new Uint32Array(n);
6436
+ depthOut.set(new Uint32Array(back, depthRequest.offset, n));
6437
+ return {
6438
+ depth: depthOut,
6439
+ parent: new Uint32Array(back, parentRequest.offset, n).slice(),
6440
+ order: new Uint32Array(back, orderRequest.offset, visitedCount).slice(),
6441
+ visitedCount,
6442
+ levels,
6443
+ switches: wordOf$1(block, "switches")
6444
+ };
6445
+ } finally {
6446
+ scope.dispose();
6447
+ }
6448
+ }
6449
+ function breadthFirstSearch(ctx, s, source, options) {
6450
+ return bfsWithTuning(ctx, s, source, options, {});
6451
+ }
6452
+ const ALGORITHM$2 = "sssp";
6453
+ const HALF_ALIGN = 64;
6454
+ const RING_SLOTS$2 = 4 * MAX_LEVELS_PER_SUBMIT + 40;
6455
+ function bitsOf(value) {
6456
+ return new Uint32Array(Float32Array.of(value).buffer)[0];
6457
+ }
6458
+ function bindingOf(buffer, size) {
6459
+ return { buffer, offset: 0, size, window: null };
6460
+ }
6461
+ function aborted(algorithm, batchId) {
6462
+ return new WebGpuGraphError(
6463
+ "E_ABORTED",
6464
+ `${algorithm}: the signal was aborted`,
6465
+ batchId === void 0 ? {} : { batchId }
6466
+ );
6467
+ }
6468
+ function assertSource(algorithm, source, n) {
6469
+ if (!Number.isInteger(source) || source < 0 || source >= n) {
6470
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: source ${source} is outside [0, ${n})`, {
6471
+ argument: "source",
6472
+ value: source,
6473
+ expected: `an integer in [0, ${n})`
6474
+ });
6475
+ }
6476
+ }
6477
+ function wordOf(block, name) {
6478
+ const value = block[name];
6479
+ if (typeof value !== "number") {
6480
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$2}: counters.${name} did not decode to a number`, {
6481
+ label: `${ALGORITHM$2}/counters`,
6482
+ message: `the field ${name} did not decode to a number`
6483
+ });
6484
+ }
6485
+ return value;
6486
+ }
6487
+ function checkDest(algorithm, dest, n) {
6488
+ if (dest === void 0) {
6489
+ return null;
6490
+ }
6491
+ if (dest instanceof Float32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
6492
+ return dest;
6493
+ }
6494
+ throw new WebGpuGraphError(
6495
+ "E_INVALID_ARGUMENT",
6496
+ `${algorithm}: dest must be a Float32Array of length ${n} over an ArrayBuffer`,
6497
+ {
6498
+ argument: "dest",
6499
+ value: `${dest.constructor.name}(${dest.length})`,
6500
+ expected: `Float32Array(${n}) over an ArrayBuffer`
6501
+ }
6502
+ );
6503
+ }
6504
+ function normaliseCutoff(algorithm, cutoff) {
6505
+ if (cutoff === void 0) {
6506
+ return Infinity;
6507
+ }
6508
+ if (Number.isNaN(cutoff)) {
6509
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: cutoff must not be NaN`, {
6510
+ argument: "cutoff",
6511
+ value: cutoff,
6512
+ expected: "a number (the CPU port and the kernel would disagree on NaN)"
6513
+ });
6514
+ }
6515
+ return cutoff;
6516
+ }
6517
+ function resolveWeights$1(algorithm, s, weights) {
6518
+ if (weights === void 0) {
6519
+ if (s.weights === null) {
6520
+ return null;
6521
+ }
6522
+ let sum2 = 0;
6523
+ for (const w of s.weights) {
6524
+ sum2 += w;
6525
+ }
6526
+ return {
6527
+ values: s.weights,
6528
+ override: null,
6529
+ allOne: s.flags.allWeightsOne,
6530
+ nonNegative: s.flags.nonNegativeWeights,
6531
+ finite: s.flags.finiteWeights,
6532
+ sum: sum2
6533
+ };
6534
+ }
6535
+ if (weights.length !== s.arcCount) {
6536
+ throw new WebGpuGraphError(
6537
+ "E_INVALID_ARGUMENT",
6538
+ `${algorithm}: weights has ${weights.length} entries, the snapshot ${s.arcCount} arcs`,
6539
+ { argument: "weights", value: weights.length, expected: s.arcCount }
6540
+ );
6541
+ }
6542
+ const values = weights instanceof Float32Array ? weights : Float32Array.from(weights);
6543
+ let allOne = true;
6544
+ let nonNegative = true;
6545
+ let finite2 = true;
6546
+ let sum = 0;
6547
+ for (const w of values) {
6548
+ if (w !== 1) {
6549
+ allOne = false;
6550
+ }
6551
+ if (w < 0) {
6552
+ nonNegative = false;
6553
+ }
6554
+ if (!Number.isFinite(w)) {
6555
+ finite2 = false;
6556
+ }
6557
+ sum += w;
6558
+ }
6559
+ return { values, override: values, allOne, nonNegative, finite: finite2, sum };
6560
+ }
6561
+ async function unitWeightRoute(ctx, s, source, cutoff, dest, options) {
6562
+ const maxDepth = cutoff === Infinity ? void 0 : Math.floor(cutoff);
6563
+ const bfs = await bfsWithTuning(
6564
+ ctx,
6565
+ s,
6566
+ source,
6567
+ { maxDepth, signal: options?.signal, onProgress: options?.onProgress },
6568
+ { predKind: 0 }
6569
+ );
6570
+ const dist = dest ?? new Float32Array(s.nodeCount);
6571
+ for (let v = 0; v < s.nodeCount; v++) {
6572
+ const d = bfs.depth[v];
6573
+ dist[v] = d === INVALID_INDEX ? Infinity : d;
6574
+ }
6575
+ return { dist, predArc: bfs.parent, reachedCount: bfs.visitedCount };
6576
+ }
6577
+ function predBufferWords(n) {
6578
+ return 2 * Math.ceil(n / 64) * 64 + 64;
6579
+ }
6580
+ async function predecessorPass(input) {
6581
+ const { algorithm, ctx, scope, predKernel, recordFill, graph, dist, pred, n, arcCount, source, mode } = input;
6582
+ const wg = ctx.workgroupSize;
6583
+ const { queue } = ctx.device;
6584
+ const bytes = 4 * n;
6585
+ const hb = Math.ceil(n / 64) * 64;
6586
+ const predArcs = { buffer: pred.buffer, offset: pred.offset, size: bytes, window: null };
6587
+ queue.writeBuffer(pred.buffer, pred.offset + 4 * (hb + source), Uint32Array.of(0));
6588
+ queue.writeBuffer(pred.buffer, pred.offset + 4 * 2 * hb, new Uint32Array(2));
6589
+ const predPlan = planGridStride(n, wg, ctx.caps);
6590
+ const predFields = {
6591
+ wg,
6592
+ n,
6593
+ arcBase: 0,
6594
+ arcEnd: arcCount,
6595
+ predKind: 0,
6596
+ source,
6597
+ stride: predPlan.stride ?? n,
6598
+ mode
6599
+ };
6600
+ const maxBatches = Math.ceil((n + 1) / MAX_LEVELS_PER_SUBMIT) + 1;
6601
+ let batches = 0;
6602
+ for (; ; ) {
6603
+ const batch = new CommandBatch(ctx, `${algorithm}/pred`);
6604
+ const pass = batch.pass("pred");
6605
+ const recordRole = (role, iteration) => {
6606
+ const params = scope.params(FRONTIER_PARAMS, { ...predFields, role, iteration });
6607
+ const bound = predKernel.bind({ ...graph, dist, pred, P: params.binding });
6608
+ predKernel.dispatch(pass, bound, predPlan, [params.offset]);
6609
+ };
6610
+ if (batches === 0 && mode === 0) {
6611
+ recordRole(0, 0);
6612
+ }
6613
+ for (let iteration = 0; iteration < MAX_LEVELS_PER_SUBMIT; iteration++) {
6614
+ recordRole(1, iteration);
6615
+ }
6616
+ recordFill(pass, predArcs, n, INVALID_INDEX);
6617
+ recordRole(2, 0);
6618
+ batch.endPass();
6619
+ const distRequest = batch.readback(dist.buffer, dist.offset, bytes);
6620
+ const predRequest = batch.readback(pred.buffer, pred.offset, bytes);
6621
+ const flagsRequest = batch.readback(pred.buffer, pred.offset + 4 * 2 * hb, 8);
6622
+ scope.flush();
6623
+ const back = await batch.submit().readback;
6624
+ ctx.assertReady();
6625
+ batches += 1;
6626
+ const [changed, orphans] = new Uint32Array(back, flagsRequest.offset, 2);
6627
+ if (changed < MAX_LEVELS_PER_SUBMIT) {
6628
+ return {
6629
+ dist: new Float32Array(back, distRequest.offset, n),
6630
+ predArc: new Uint32Array(back, predRequest.offset, n).slice(),
6631
+ orphans
6632
+ };
6633
+ }
6634
+ if (batches > maxBatches) {
6635
+ throw new WebGpuGraphError(
6636
+ "E_VALIDATION",
6637
+ `${algorithm}: the hop passes still changed something after ${batches} batches (a hop count is at most ${n})`,
6638
+ { label: `${algorithm}/pred`, message: `the hop passes did not converge in ${batches} batches` }
6639
+ );
6640
+ }
6641
+ queue.writeBuffer(pred.buffer, pred.offset + 4 * 2 * hb, new Uint32Array(2));
6642
+ }
6643
+ }
6644
+ async function ssspWithTuning(ctx, s, source, options, tuning) {
6645
+ ctx.assertReady();
6646
+ await assertDeviceComputes(ctx);
6647
+ const n = s.nodeCount;
6648
+ assertSource(ALGORITHM$2, source, n);
6649
+ const roundsPerSubmit = tuning.roundsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
6650
+ if (!Number.isInteger(roundsPerSubmit) || roundsPerSubmit < 1 || roundsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
6651
+ throw new WebGpuGraphError(
6652
+ "E_INVALID_ARGUMENT",
6653
+ `${ALGORITHM$2}: roundsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
6654
+ {
6655
+ argument: "roundsPerSubmit",
6656
+ value: roundsPerSubmit,
6657
+ expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
6658
+ }
6659
+ );
6660
+ }
6661
+ if (tuning.delta !== void 0 && !(Number.isFinite(tuning.delta) && tuning.delta > 0)) {
6662
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM$2}: delta must be a finite positive number`, {
6663
+ argument: "delta",
6664
+ value: tuning.delta,
6665
+ expected: "a finite positive number"
6666
+ });
6667
+ }
6668
+ const dest = checkDest(ALGORITHM$2, options?.dest, n);
6669
+ const vector2 = resolveWeights$1(ALGORITHM$2, s, options?.weights);
6670
+ const cutoff = normaliseCutoff(ALGORITHM$2, options?.cutoff);
6671
+ if (options?.signal?.aborted) {
6672
+ throw aborted(ALGORITHM$2);
6673
+ }
6674
+ if (vector2 === null || vector2.allOne) {
6675
+ return unitWeightRoute(ctx, s, source, cutoff, dest, options);
6676
+ }
6677
+ if (!vector2.nonNegative) {
6678
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM$2}: a negative weight has no shortest path here`, {
6679
+ feature: "sssp.negativeWeights",
6680
+ hint: "use bellmanFord"
6681
+ });
6682
+ }
6683
+ if (!vector2.finite) {
6684
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM$2}: a NaN or infinite weight has no bit-pattern order`, {
6685
+ feature: "sssp.nonFiniteWeights"
6686
+ });
6687
+ }
6688
+ const { arcCount } = s;
6689
+ const core = ctx.residency.core(s);
6690
+ const limit = ctx.caps.limits.maxStorageBufferBindingSize;
6691
+ assertWholeCore(core, arcCount, limit, ALGORITHM$2);
6692
+ const cap = Math.ceil(Math.max(1, arcCount) / HALF_ALIGN) * HALF_ALIGN;
6693
+ if (8 * cap > limit) {
6694
+ throw new WebGpuGraphError(
6695
+ "E_TOO_LARGE",
6696
+ `${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)`,
6697
+ { needed: 8 * cap, limit, path: "sssp.queue", algorithm: ALGORITHM$2 }
6698
+ );
6699
+ }
6700
+ const delta = Math.fround(
6701
+ tuning.delta ?? (vector2.sum === 0 ? 1 : SSSP_DELTA_FACTOR * (vector2.sum / arcCount) / (arcCount / n))
6702
+ );
6703
+ const deltaBits = bitsOf(delta);
6704
+ const maxRounds = n + Math.ceil(vector2.sum / delta) + 1;
6705
+ const maxSubmits = Math.ceil((maxRounds + 1) / roundsPerSubmit) + 1;
6706
+ const scope = algorithmScope(ctx, ALGORITHM$2, RING_SLOTS$2);
6707
+ try {
6708
+ const wg = ctx.workgroupSize;
6709
+ const bytes = 4 * n;
6710
+ const dist = bindingOf(scope.scratch(bytes, "dist"), bytes);
6711
+ const owner = bindingOf(scope.scratch(bytes, "owner"), bytes);
6712
+ const queueOut = bindingOf(scope.scratch(8 * cap, "queue-out"), 8 * cap);
6713
+ const nearHalf = { buffer: queueOut.buffer, offset: 0, size: 4 * cap, window: null };
6714
+ const farHalf = { buffer: queueOut.buffer, offset: 4 * cap, size: 4 * cap, window: null };
6715
+ const predWords = predBufferWords(n);
6716
+ const pred = bindingOf(scope.scratch(4 * predWords, "pred"), 4 * predWords);
6717
+ const { queue } = ctx.device;
6718
+ let weightsBinding;
6719
+ if (vector2.override !== null) {
6720
+ const uploaded = scope.scratch(4 * arcCount, "weights");
6721
+ queue.writeBuffer(uploaded, 0, vector2.override);
6722
+ weightsBinding = bindingOf(uploaded, 4 * arcCount);
6723
+ }
6724
+ await ctx.allocator.check();
6725
+ const planner = await prepareFrontier(scope, n, arcCount, cap);
6726
+ const compact = await prepareCompact(scope);
6727
+ const overrides = graphOverrides(core, null, weightsBinding);
6728
+ const relax = await ctx.pipelines.kernel(kernelSpec("sssp-relax", overrides));
6729
+ const predKernel = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...overrides, MODE: 0 }));
6730
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
6731
+ const graph = graphBindings(core, null, weightsBinding);
6732
+ const { frontier } = planner;
6733
+ const { counters } = frontier;
6734
+ const nearIn = frontier.vertices[0];
6735
+ const farIn = frontier.vertices[1];
6736
+ const recordFill = (pass, dst, count, value) => {
6737
+ const params = scope.params(FILL_PARAMS, { count, value, mode: 0, pad0: 0 });
6738
+ fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
6739
+ };
6740
+ const submit = (batch) => {
6741
+ scope.flush();
6742
+ return batch.submit();
6743
+ };
6744
+ const setup = new CommandBatch(ctx, `${ALGORITHM$2}/setup`);
6745
+ const setupPass = setup.pass("fill");
6746
+ recordFill(setupPass, dist, n, F32_INF_BITS);
6747
+ recordFill(setupPass, pred, predWords, INVALID_INDEX);
6748
+ setup.endPass();
6749
+ await submit(setup).readback;
6750
+ ctx.assertReady();
6751
+ queue.writeBuffer(dist.buffer, dist.offset + 4 * source, Uint32Array.of(0));
6752
+ frontier.reset(queue, source, { nextFrontierCount: 1, thresholdBits: deltaBits, deltaBits });
6753
+ queue.writeBuffer(queueOut.buffer, queueOut.offset, Uint32Array.of(source));
6754
+ const nearDedupe = {
6755
+ queue: nearHalf,
6756
+ count: cap,
6757
+ countIndex: W.edgeCount,
6758
+ // role 2 writes the raw near count here when it chooses a near round, 0 otherwise
6759
+ counters,
6760
+ owner,
6761
+ out: nearIn,
6762
+ outCount: counters,
6763
+ outIndex: W.frontierCount
6764
+ };
6765
+ const farDedupe = {
6766
+ queue: farHalf,
6767
+ count: cap,
6768
+ countIndex: W.edgeCountUnclamped,
6769
+ // and the raw far count here on a far round
6770
+ counters,
6771
+ owner,
6772
+ out: farIn,
6773
+ outCount: counters,
6774
+ outIndex: W.farCount
6775
+ };
6776
+ const relaxPlan = planGridStride(n, wg, ctx.caps);
6777
+ const relaxFields = {
6778
+ wg,
6779
+ n,
6780
+ edgeCapacity: cap,
6781
+ arcBase: 0,
6782
+ arcEnd: arcCount,
6783
+ cutoffBits: bitsOf(cutoff),
6784
+ source,
6785
+ stride: relaxPlan.stride ?? wg
6786
+ };
6787
+ let roundsRecorded = 0;
6788
+ let submits = 0;
6789
+ for (; ; ) {
6790
+ const batch = new CommandBatch(ctx, `${ALGORITHM$2}/rounds`);
6791
+ const pass = batch.pass("sssp");
6792
+ const near = scope.params(FRONTIER_PARAMS, { ...relaxFields, role: 0 });
6793
+ const far = scope.params(FRONTIER_PARAMS, { ...relaxFields, role: 1 });
6794
+ const boundNear = relax.bind({ ...graph, dist, counters, queueIn: nearIn, queueOut, P: near.binding });
6795
+ const boundFar = relax.bind({ ...graph, dist, counters, queueIn: farIn, queueOut, P: far.binding });
6796
+ for (let round = 0; round < roundsPerSubmit; round++) {
6797
+ planner.recordFinalize(pass, 2, round, {});
6798
+ compact.recordDedupe(pass, nearDedupe);
6799
+ compact.recordDedupe(pass, farDedupe);
6800
+ planner.recordFinalize(pass, 3, round, {});
6801
+ relax.dispatch(pass, boundNear, relaxPlan, [near.offset]);
6802
+ relax.dispatch(pass, boundFar, relaxPlan, [far.offset]);
6803
+ }
6804
+ batch.endPass();
6805
+ const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
6806
+ const inspect = tuning.onRound === void 0 ? null : batch.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength);
6807
+ const submitted = submit(batch);
6808
+ const back = await submitted.readback;
6809
+ roundsRecorded += roundsPerSubmit;
6810
+ submits += 1;
6811
+ ctx.assertReady();
6812
+ if (options?.signal?.aborted) {
6813
+ throw aborted(ALGORITHM$2, submitted.id);
6814
+ }
6815
+ options?.onProgress?.(Math.min(roundsRecorded, maxRounds), maxRounds);
6816
+ if (inspect !== null && tuning.onRound !== void 0) {
6817
+ tuning.onRound(roundsRecorded - 1, FRONTIER_COUNTERS.read(new DataView(back), inspect.offset));
6818
+ }
6819
+ const done = new Uint32Array(back, doneRequest.offset, 1)[0];
6820
+ if (done === 1) {
6821
+ break;
6822
+ }
6823
+ if (done !== 0) {
6824
+ const block = FRONTIER_COUNTERS.read(
6825
+ new DataView(
6826
+ await ctx.readback.read(
6827
+ counters.buffer,
6828
+ FRONTIER_COUNTERS.byteLength,
6829
+ void 0,
6830
+ counters.offset
6831
+ )
6832
+ )
6833
+ );
6834
+ if (done === 2) {
6835
+ const needed = Math.max(wordOf(block, "nextFrontierCount"), wordOf(block, "nextFarCount"));
6836
+ throw new WebGpuGraphError(
6837
+ "E_TOO_LARGE",
6838
+ `${ALGORITHM$2}: a raw pile of ${needed} entries overflowed its ${cap}-entry half`,
6839
+ { needed, limit: cap, path: "sssp.pile", algorithm: ALGORITHM$2 }
6840
+ );
6841
+ }
6842
+ throw new WebGpuGraphError(
6843
+ "E_UNSUPPORTED",
6844
+ `${ALGORITHM$2}: the f32 threshold ${wordOf(block, "thresholdBits")} absorbed the delta ${deltaBits} (as bit patterns); the far pile can no longer be bucketed`,
6845
+ { feature: "sssp.thresholdAbsorbed", hint: "the distances outgrew the delta's f32 precision" }
6846
+ );
6847
+ }
6848
+ if (submits > maxSubmits) {
6849
+ throw new WebGpuGraphError(
6850
+ "E_VALIDATION",
6851
+ `${ALGORITHM$2}: the done flag never rose in ${submits} submits (at most ${maxRounds} rounds)`,
6852
+ { label: `${ALGORITHM$2}/rounds`, message: `the done flag never rose in ${submits} submits` }
6853
+ );
6854
+ }
6855
+ }
6856
+ const passed = await predecessorPass({
6857
+ algorithm: ALGORITHM$2,
6858
+ ctx,
6859
+ scope,
6860
+ predKernel,
6861
+ recordFill,
6862
+ graph,
6863
+ dist,
6864
+ pred,
6865
+ n,
6866
+ arcCount,
6867
+ source,
6868
+ mode: 0
6869
+ });
6870
+ if (passed.orphans !== 0) {
6871
+ throw new WebGpuGraphError(
6872
+ "E_VALIDATION",
6873
+ `${ALGORITHM$2}: ${passed.orphans} reached node(s) the predecessor key never reached (a kernel bug)`,
6874
+ { label: `${ALGORITHM$2}/pred`, message: `${passed.orphans} orphan(s) in the predecessor pass` }
6875
+ );
6876
+ }
6877
+ const distOut = dest ?? new Float32Array(n);
6878
+ distOut.set(passed.dist);
6879
+ let reachedCount = 0;
6880
+ for (const d of distOut) {
6881
+ if (d !== Infinity) {
6882
+ reachedCount += 1;
6883
+ }
6884
+ }
6885
+ return { dist: distOut, predArc: passed.predArc, reachedCount };
6886
+ } finally {
6887
+ scope.dispose();
6888
+ }
6889
+ }
6890
+ function sssp(ctx, s, source, options) {
6891
+ return ssspWithTuning(ctx, s, source, options, {});
6892
+ }
6893
+ const ALGORITHM$1 = "bellmanFord";
6894
+ const ROUNDS_PER_BATCH = 8;
6895
+ const MAX_RETRIES = 16;
6896
+ const RING_SLOTS$1 = MAX_LEVELS_PER_SUBMIT + 16;
6897
+ function assertSymmetric(s, vector2) {
6898
+ const { arcToEdge, edgeToArc } = s;
6899
+ for (let a = 0; a < s.arcCount; a++) {
6900
+ const forward = edgeToArc[arcToEdge[a]];
6901
+ if (vector2[a] !== vector2[forward]) {
6902
+ throw new WebGpuGraphError(
6903
+ "E_UNSUPPORTED",
6904
+ `${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`,
6905
+ { feature: "bellmanFord.asymmetricUndirectedWeights", hint: "use a directed snapshot" }
6906
+ );
6907
+ }
6908
+ }
6909
+ }
6910
+ async function bellmanFordWithTuning(ctx, s, source, options, tuning) {
6911
+ ctx.assertReady();
6912
+ await assertDeviceComputes(ctx);
6913
+ const n = s.nodeCount;
6914
+ assertSource(ALGORITHM$1, source, n);
6915
+ const maxRetries = tuning.maxRetries ?? MAX_RETRIES;
6916
+ if (!Number.isInteger(maxRetries) || maxRetries < 1) {
6917
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM$1}: maxRetries must be an integer >= 1`, {
6918
+ argument: "maxRetries",
6919
+ value: maxRetries,
6920
+ expected: "an integer >= 1"
6921
+ });
6922
+ }
6923
+ const roundsPerBatch = tuning.roundsPerBatch ?? ROUNDS_PER_BATCH;
6924
+ if (!Number.isInteger(roundsPerBatch) || roundsPerBatch < 1 || roundsPerBatch > MAX_LEVELS_PER_SUBMIT) {
6925
+ throw new WebGpuGraphError(
6926
+ "E_INVALID_ARGUMENT",
6927
+ `${ALGORITHM$1}: roundsPerBatch must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
6928
+ {
6929
+ argument: "roundsPerBatch",
6930
+ value: roundsPerBatch,
6931
+ expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
6932
+ }
6933
+ );
6934
+ }
6935
+ const dest = checkDest(ALGORITHM$1, options?.dest, n);
6936
+ const vector2 = resolveWeights$1(ALGORITHM$1, s, options?.weights);
6937
+ const cutoff = normaliseCutoff(ALGORITHM$1, options?.cutoff);
6938
+ if (options?.signal?.aborted) {
6939
+ throw aborted(ALGORITHM$1);
6940
+ }
6941
+ if (vector2 === null || vector2.allOne) {
6942
+ const unit = await unitWeightRoute(ctx, s, source, cutoff, dest, options);
6943
+ return { result: { ...unit, hasNegativeCycle: false }, rounds: 0, retryExhaustedRounds: 0 };
6944
+ }
6945
+ if (!vector2.finite) {
6946
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM$1}: a NaN or infinite weight has no shortest path`, {
6947
+ feature: "bellmanFord.nonFiniteWeights"
6948
+ });
6949
+ }
6950
+ if (!s.directed && vector2.override !== null) {
6951
+ assertSymmetric(s, vector2.override);
6952
+ }
6953
+ const { arcCount } = s;
6954
+ const core = ctx.residency.core(s, ["rowPtr", "colIdx", "weights", "edgeToArc"]);
6955
+ assertWholeCore(core, arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM$1);
6956
+ const edges = ctx.residency.view(s, "edgeList");
6957
+ const edgeCount = edges.scalars.edgeCount[0];
6958
+ const scope = algorithmScope(ctx, ALGORITHM$1, RING_SLOTS$1);
6959
+ try {
6960
+ const wg = ctx.workgroupSize;
6961
+ const bytes = 4 * n;
6962
+ const dist = bindingOf(scope.scratch(bytes, "dist"), bytes);
6963
+ const predWords = predBufferWords(n);
6964
+ const pred = bindingOf(scope.scratch(4 * predWords, "pred"), 4 * predWords);
6965
+ const flags = bindingOf(scope.scratch(BF_FLAGS.byteLength, "flags"), BF_FLAGS.byteLength);
6966
+ let iota = null;
6967
+ let { edgeToArc } = core;
6968
+ if (edgeToArc === null) {
6969
+ iota = bindingOf(scope.scratch(4 * edgeCount, "iota"), 4 * edgeCount);
6970
+ edgeToArc = iota;
6971
+ }
6972
+ const { queue } = ctx.device;
6973
+ let weightsBinding;
6974
+ if (vector2.override !== null) {
6975
+ const uploaded = scope.scratch(4 * arcCount, "weights");
6976
+ queue.writeBuffer(uploaded, 0, vector2.override);
6977
+ weightsBinding = bindingOf(uploaded, 4 * arcCount);
6978
+ }
6979
+ await ctx.allocator.check();
6980
+ const overrides = graphOverrides(core, null, weightsBinding);
6981
+ const relax = await ctx.pipelines.kernel(kernelSpec("bf-relax", { UNDIRECTED: !s.directed }));
6982
+ const predKernel = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...overrides, MODE: 0 }));
6983
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
6984
+ const graph = graphBindings(core, null, weightsBinding);
6985
+ const recordFill = (pass, dst, count, value, mode = 0) => {
6986
+ const params = scope.params(FILL_PARAMS, { count, value, mode, pad0: 0 });
6987
+ fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
6988
+ };
6989
+ const submit = (batch) => {
6990
+ scope.flush();
6991
+ return batch.submit();
6992
+ };
6993
+ const setup = new CommandBatch(ctx, `${ALGORITHM$1}/setup`);
6994
+ const setupPass = setup.pass("fill");
6995
+ recordFill(setupPass, dist, n, F32_INF_BITS);
6996
+ recordFill(setupPass, pred, predWords, INVALID_INDEX);
6997
+ if (iota !== null) {
6998
+ recordFill(setupPass, iota, edgeCount, 0, 1);
6999
+ }
7000
+ setup.endPass();
7001
+ await submit(setup).readback;
7002
+ ctx.assertReady();
7003
+ queue.writeBuffer(dist.buffer, dist.offset + 4 * source, Uint32Array.of(0));
7004
+ const edgePlan = planGridStride(edgeCount, wg, ctx.caps);
7005
+ const relaxBindings = {
7006
+ edgeSrc: edges.bindings.src,
7007
+ edgeDst: edges.bindings.dst,
7008
+ edgeToArc,
7009
+ weights: graph.weights,
7010
+ dist,
7011
+ flags
7012
+ };
7013
+ const relaxFields = {
7014
+ edgeCount,
7015
+ stride: edgePlan.stride ?? edgeCount,
7016
+ maxRetries,
7017
+ cutoffBits: bitsOf(cutoff)
7018
+ };
7019
+ const zero = new Uint32Array(BF_FLAGS.byteLength / 4);
7020
+ const runRounds = async (count, label) => {
7021
+ queue.writeBuffer(flags.buffer, flags.offset, zero);
7022
+ const batch = new CommandBatch(ctx, `${ALGORITHM$1}/${label}`);
7023
+ const pass = batch.pass("relax");
7024
+ const params = scope.params(BF_PARAMS, relaxFields);
7025
+ const bound = relax.bind({ ...relaxBindings, P: params.binding });
7026
+ for (let round = 0; round < count; round++) {
7027
+ relax.dispatch(pass, bound, edgePlan, [params.offset]);
7028
+ }
7029
+ batch.endPass();
7030
+ const request = batch.readback(flags.buffer, flags.offset, BF_FLAGS.byteLength);
7031
+ const submitted = submit(batch);
7032
+ const back = await submitted.readback;
7033
+ ctx.assertReady();
7034
+ const block = BF_FLAGS.read(new DataView(back), request.offset);
7035
+ return { changed: Number(block.changed), retryExhausted: Number(block.retryExhausted), id: submitted.id };
7036
+ };
7037
+ let rounds = 0;
7038
+ let retryExhaustedRounds = 0;
7039
+ let hasNegativeCycle = false;
7040
+ for (; ; ) {
7041
+ const remaining = n - 1 - rounds;
7042
+ if (remaining <= 0) {
7043
+ const decision = await runRounds(1, "decision");
7044
+ if (decision.retryExhausted !== 0) {
7045
+ throw new WebGpuGraphError(
7046
+ "E_VALIDATION",
7047
+ `${ALGORITHM$1}: a lane exhausted the ${maxRetries}-retry compare-exchange bound in the decision round, so its change is not a verdict`,
7048
+ {
7049
+ label: `${ALGORITHM$1}/retry`,
7050
+ message: "retryExhausted in the decision round",
7051
+ batchId: decision.id
7052
+ }
7053
+ );
7054
+ }
7055
+ hasNegativeCycle = decision.changed !== 0;
7056
+ break;
7057
+ }
7058
+ const count = Math.min(roundsPerBatch, remaining);
7059
+ const batch = await runRounds(count, "rounds");
7060
+ rounds += count;
7061
+ if (batch.retryExhausted !== 0) {
7062
+ retryExhaustedRounds += 1;
7063
+ }
7064
+ if (options?.signal?.aborted) {
7065
+ throw aborted(ALGORITHM$1, batch.id);
7066
+ }
7067
+ options?.onProgress?.(rounds, n);
7068
+ if (batch.changed === 0 && batch.retryExhausted === 0) {
7069
+ break;
7070
+ }
7071
+ }
7072
+ const passed = await predecessorPass({
7073
+ algorithm: ALGORITHM$1,
7074
+ ctx,
7075
+ scope,
7076
+ predKernel,
7077
+ recordFill,
7078
+ graph,
7079
+ dist,
7080
+ pred,
7081
+ n,
7082
+ arcCount,
7083
+ source,
7084
+ mode: 1
7085
+ });
7086
+ if (passed.orphans !== 0 && !hasNegativeCycle) {
7087
+ throw new WebGpuGraphError(
7088
+ "E_UNSUPPORTED",
7089
+ `${ALGORITHM$1}: ${passed.orphans} reached node(s) the tight subgraph never reaches (a cycle of weights below one f32 ulp relaxed once)`,
7090
+ {
7091
+ feature: "bellmanFord.roundedCycle",
7092
+ hint: "a cycle of weights below one f32 ulp relaxed once at a distance above 2^24; scale the weights or shorten the distances"
7093
+ }
7094
+ );
7095
+ }
7096
+ const distOut = dest ?? new Float32Array(n);
7097
+ distOut.set(passed.dist);
7098
+ let reachedCount = 0;
7099
+ for (const d of distOut) {
7100
+ if (d !== Infinity) {
7101
+ reachedCount += 1;
7102
+ }
7103
+ }
7104
+ return {
7105
+ result: { dist: distOut, predArc: passed.predArc, reachedCount, hasNegativeCycle },
7106
+ rounds,
7107
+ retryExhaustedRounds
7108
+ };
7109
+ } finally {
7110
+ scope.dispose();
7111
+ }
7112
+ }
7113
+ async function bellmanFord(ctx, s, source, options) {
7114
+ return (await bellmanFordWithTuning(ctx, s, source, options, {})).result;
7115
+ }
7116
+ const ALGORITHM = "closenessCentrality";
7117
+ const SOURCES_PER_BATCH = 32;
7118
+ const PER_SOURCE_WORDS = 4 * SOURCES_PER_BATCH;
7119
+ const RING_SLOTS = 8 * MAX_LEVELS_PER_SUBMIT + 16;
7120
+ function reusingScratch(scope) {
7121
+ const held = /* @__PURE__ */ new Map();
7122
+ return {
7123
+ ...scope,
7124
+ scratch: (byteLength, label) => {
7125
+ const key = `${label}/${byteLength}`;
7126
+ let buffer = held.get(key);
7127
+ if (buffer === void 0) {
7128
+ buffer = scope.scratch(byteLength, label);
7129
+ held.set(key, buffer);
7130
+ }
7131
+ return buffer;
7132
+ }
7133
+ };
7134
+ }
7135
+ async function weightedRoute(ctx, s, scores, options) {
7136
+ const n = s.nodeCount;
7137
+ for (let source = 0; source < n; source++) {
7138
+ if (options?.signal?.aborted) {
7139
+ throw aborted(ALGORITHM);
7140
+ }
7141
+ const { dist } = await sssp(ctx, s, source, { signal: options?.signal });
7142
+ let sum = 0;
7143
+ for (let v = 0; v < n; v++) {
7144
+ const d = dist[v];
7145
+ if (v !== source && d !== Infinity) {
7146
+ sum += d;
7147
+ }
7148
+ }
7149
+ scores[source] = sum === 0 ? 0 : 1 / sum;
7150
+ options?.onProgress?.(source + 1, n);
7151
+ }
7152
+ return { scores, iterations: n, converged: true, precision: "f32" };
7153
+ }
7154
+ async function sweepRoute(ctx, s, scores, levelsPerSubmit, options, tuning) {
7155
+ const n = s.nodeCount;
7156
+ if (n === 0) {
7157
+ return { scores, iterations: 0, converged: true, precision: "f32" };
7158
+ }
7159
+ const core = ctx.residency.core(s);
7160
+ assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM);
7161
+ const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS);
7162
+ try {
7163
+ const wg = ctx.workgroupSize;
7164
+ const bytes = 4 * n;
7165
+ const bitsBase = Math.ceil(n / 64) * 64;
7166
+ const regionBytes = 4 * bitsBase;
7167
+ const bits = bindingOf(scope.scratch(4 * regionBytes, "bits"), 4 * regionBytes);
7168
+ const region = (index) => ({
7169
+ buffer: bits.buffer,
7170
+ offset: index * regionBytes,
7171
+ size: regionBytes,
7172
+ window: null
7173
+ });
7174
+ const flags = region(3);
7175
+ const frontierList = bindingOf(scope.scratch(bytes, "frontier-list"), bytes);
7176
+ const iota = bindingOf(scope.scratch(bytes, "iota"), bytes);
7177
+ const counters = bindingOf(
7178
+ scope.scratch(FRONTIER_COUNTERS.byteLength, "counters"),
7179
+ FRONTIER_COUNTERS.byteLength
7180
+ );
7181
+ const perSourceBytes = 4 * PER_SOURCE_WORDS;
7182
+ const perSource = bindingOf(scope.scratch(perSourceBytes, "per-source"), perSourceBytes);
7183
+ await ctx.allocator.check();
7184
+ const compact = await prepareCompact(reusingScratch(scope));
7185
+ const sweep = await ctx.pipelines.kernel(kernelSpec("closeness-sweep", graphOverrides(core, null)));
7186
+ const reduce = await ctx.pipelines.kernel(kernelSpec("closeness-reduce"));
7187
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
7188
+ const graph = graphBindings(core, null);
7189
+ const onePlan = plan1d(1, wg, ctx.caps);
7190
+ const regionPlan = plan1d(bitsBase, wg, ctx.caps);
7191
+ const sweepPlan = planGridStride(n, wg, ctx.caps);
7192
+ const recordFill = (pass, dst, count, mode) => {
7193
+ const params = scope.params(FILL_PARAMS, { count, value: 0, mode, pad0: 0 });
7194
+ fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
7195
+ };
7196
+ const submit = (batch) => {
7197
+ scope.flush();
7198
+ return batch.submit();
7199
+ };
7200
+ const setup = new CommandBatch(ctx, `${ALGORITHM}/setup`);
7201
+ recordFill(setup.pass("fill"), iota, n, 1);
7202
+ setup.endPass();
7203
+ await submit(setup).readback;
7204
+ ctx.assertReady();
7205
+ let batches = 0;
7206
+ for (let batchStart = 0; batchStart < n; batchStart += SOURCES_PER_BATCH) {
7207
+ let level = 0;
7208
+ for (let first = true; ; first = false) {
7209
+ const batch = new CommandBatch(ctx, `${ALGORITHM}/levels`);
7210
+ const pass = batch.pass("closeness");
7211
+ if (first) {
7212
+ recordFill(pass, bits, 4 * bitsBase, 0);
7213
+ recordFill(pass, perSource, PER_SOURCE_WORDS, 0);
7214
+ const seed = scope.params(FRONTIER_PARAMS, { role: 1, n, bitsBase, source: batchStart });
7215
+ reduce.dispatch(pass, reduce.bind({ counters, perSource, bits, P: seed.binding }), onePlan, [
7216
+ seed.offset
7217
+ ]);
7218
+ }
7219
+ const boundary = scope.params(FRONTIER_PARAMS, { role: 0, n, bitsBase });
7220
+ const boundBoundary = reduce.bind({ counters, perSource, bits, P: boundary.binding });
7221
+ const clear = scope.params(FILL_PARAMS, { count: bitsBase, value: 0, mode: 0, pad0: 0 });
7222
+ const boundClearNext = [region(2), region(1)].map((dst) => fill.bind({ dst, P: clear.binding }));
7223
+ const boundClearFlags = fill.bind({ dst: flags, P: clear.binding });
7224
+ const boundSweep = [0, 1].map((mode) => {
7225
+ const params = scope.params(FRONTIER_PARAMS, {
7226
+ wg,
7227
+ n,
7228
+ bitsBase,
7229
+ arcBase: 0,
7230
+ arcEnd: s.arcCount,
7231
+ mode,
7232
+ stride: sweepPlan.stride ?? wg
7233
+ });
7234
+ return {
7235
+ bound: sweep.bind({ ...graph, frontierList, counters, bits, perSource, P: params.binding }),
7236
+ offset: params.offset
7237
+ };
7238
+ });
7239
+ for (let k = 0; k < levelsPerSubmit; k++, level++) {
7240
+ const parity = level % 2;
7241
+ reduce.dispatch(pass, boundBoundary, onePlan, [boundary.offset]);
7242
+ compact.record(pass, {
7243
+ queue: iota,
7244
+ flags,
7245
+ count: n,
7246
+ out: frontierList,
7247
+ outCount: counters,
7248
+ outIndex: W.frontierCount
7249
+ });
7250
+ fill.dispatch(pass, boundClearNext[parity], regionPlan, [clear.offset]);
7251
+ fill.dispatch(pass, boundClearFlags, regionPlan, [clear.offset]);
7252
+ sweep.dispatch(pass, boundSweep[parity].bound, sweepPlan, [boundSweep[parity].offset]);
7253
+ }
7254
+ batch.endPass();
7255
+ const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
7256
+ const blockRequest = batch.readback(perSource.buffer, perSource.offset, perSourceBytes);
7257
+ const submitted = submit(batch);
7258
+ const back = await submitted.readback;
7259
+ ctx.assertReady();
7260
+ if (options?.signal?.aborted) {
7261
+ throw aborted(ALGORITHM, submitted.id);
7262
+ }
7263
+ if (new Uint32Array(back, doneRequest.offset, 1)[0] !== 0) {
7264
+ const block = new Uint32Array(back, blockRequest.offset, PER_SOURCE_WORDS);
7265
+ const count = Math.min(SOURCES_PER_BATCH, n - batchStart);
7266
+ for (let i = 0; i < count; i++) {
7267
+ const sum = block[3 * SOURCES_PER_BATCH + i] * 2 ** 32 + block[2 * SOURCES_PER_BATCH + i];
7268
+ scores[batchStart + i] = sum === 0 ? 0 : 1 / sum;
7269
+ }
7270
+ tuning.onBatch?.(batchStart, block.slice());
7271
+ break;
7272
+ }
7273
+ if (level > n + 3) {
7274
+ throw new WebGpuGraphError(
7275
+ "E_VALIDATION",
7276
+ `${ALGORITHM}: the done flag never rose in ${level} levels of the batch at ${batchStart}`,
7277
+ { label: ALGORITHM, message: `the done flag never rose in ${level} levels` }
7278
+ );
7279
+ }
7280
+ }
7281
+ batches += 1;
7282
+ options?.onProgress?.(Math.min(batchStart + SOURCES_PER_BATCH, n), n);
7283
+ }
7284
+ return { scores, iterations: batches, converged: true, precision: "f32" };
7285
+ } finally {
7286
+ scope.dispose();
4443
7287
  }
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
7288
  }
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 };
7289
+ async function closenessWithTuning(ctx, s, options, tuning) {
7290
+ ctx.assertReady();
7291
+ await assertDeviceComputes(ctx);
7292
+ for (const key of ["maxIterations", "tolerance"]) {
7293
+ if (options?.[key] !== void 0) {
7294
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: ${key} has no meaning for an exact traversal`, {
7295
+ option: key,
7296
+ hint: "closeness is an exact traversal; the option has no meaning here"
7297
+ });
7298
+ }
4467
7299
  }
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);
7300
+ const n = s.nodeCount;
7301
+ const levelsPerSubmit = tuning.levelsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
7302
+ if (!Number.isInteger(levelsPerSubmit) || levelsPerSubmit < 1 || levelsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
7303
+ throw new WebGpuGraphError(
7304
+ "E_INVALID_ARGUMENT",
7305
+ `${ALGORITHM}: levelsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
7306
+ {
7307
+ argument: "levelsPerSubmit",
7308
+ value: levelsPerSubmit,
7309
+ expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
7310
+ }
7311
+ );
7312
+ }
7313
+ const scores = checkDest(ALGORITHM, options?.dest, n) ?? new Float32Array(n);
7314
+ const weighted = options?.weighted ?? s.flags.weighted;
7315
+ if (options?.signal?.aborted) {
7316
+ throw aborted(ALGORITHM);
7317
+ }
7318
+ if (weighted && s.weights !== null && !s.flags.allWeightsOne) {
7319
+ if (!s.flags.nonNegativeWeights) {
7320
+ throw new WebGpuGraphError(
7321
+ "E_UNSUPPORTED",
7322
+ `${ALGORITHM}: a negative weight has no shortest-path distance to sum`,
7323
+ {
7324
+ feature: "closenessCentrality.negativeWeights",
7325
+ hint: "pass weighted: false to ignore the column"
7326
+ }
7327
+ );
4488
7328
  }
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);
7329
+ if (!s.flags.finiteWeights) {
7330
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: a NaN or infinite weight has no shortest path`, {
7331
+ feature: "closenessCentrality.nonFiniteWeights"
7332
+ });
4497
7333
  }
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
- };
7334
+ return weightedRoute(ctx, s, scores, options);
7335
+ }
7336
+ return sweepRoute(ctx, s, scores, levelsPerSubmit, options, tuning);
7337
+ }
7338
+ function closenessCentrality(ctx, s, options) {
7339
+ return closenessWithTuning(ctx, s, options, {});
4506
7340
  }
4507
7341
  const U32_MAX = 4294967295;
4508
7342
  async function prepareHistogram(scope) {
@@ -4646,128 +7480,16 @@ class CountingSortPlannerImpl {
4646
7480
  this.dispatches += 1;
4647
7481
  }
4648
7482
  }
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) {
7483
+ function nextPow2(x) {
4762
7484
  let p = 1;
4763
- while (p < x2) {
7485
+ while (p < x) {
4764
7486
  p *= 2;
4765
7487
  }
4766
7488
  return p;
4767
7489
  }
4768
- function floorPow2$1(x2) {
7490
+ function floorPow2$1(x) {
4769
7491
  let p = 1;
4770
- while (p * 2 <= x2) {
7492
+ while (p * 2 <= x) {
4771
7493
  p *= 2;
4772
7494
  }
4773
7495
  return p;
@@ -4951,15 +7673,15 @@ function resolveCenter(center) {
4951
7673
  return out;
4952
7674
  }
4953
7675
  for (let axis = 0; axis < 3 && axis < center.length; axis++) {
4954
- const v2 = center[axis];
4955
- if (!Number.isFinite(v2)) {
7676
+ const v = center[axis];
7677
+ if (!Number.isFinite(v)) {
4956
7678
  throw new WebGpuGraphError("E_INVALID_ARGUMENT", `center[${axis}] is not finite`, {
4957
7679
  argument: "center",
4958
- value: v2,
7680
+ value: v,
4959
7681
  expected: "finite components"
4960
7682
  });
4961
7683
  }
4962
- out[axis] = v2;
7684
+ out[axis] = v;
4963
7685
  }
4964
7686
  return out;
4965
7687
  }
@@ -4998,13 +7720,13 @@ function seedPositions(s, positions, seed, dim, scale, center, range) {
4998
7720
  for (let i = 0; i < n; i++) {
4999
7721
  let finite2 = true;
5000
7722
  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;
7723
+ const v = positions[3 * i + axis];
7724
+ if (Number.isFinite(v)) {
7725
+ if (v < lo[axis]) {
7726
+ lo[axis] = v;
5005
7727
  }
5006
- if (v2 > hi[axis]) {
5007
- hi[axis] = v2;
7728
+ if (v > hi[axis]) {
7729
+ hi[axis] = v;
5008
7730
  }
5009
7731
  } else {
5010
7732
  finite2 = false;
@@ -5046,8 +7768,8 @@ function seedPositions(s, positions, seed, dim, scale, center, range) {
5046
7768
  const at = 3 * i + axis;
5047
7769
  if (!Number.isFinite(positions[at])) {
5048
7770
  const u = rng.next();
5049
- const v2 = boxLo[axis] + u * (boxHi[axis] - boxLo[axis]);
5050
- positions[at] = v2 * scale + c[axis];
7771
+ const v = boxLo[axis] + u * (boxHi[axis] - boxLo[axis]);
7772
+ positions[at] = v * scale + c[axis];
5051
7773
  }
5052
7774
  }
5053
7775
  if (dim === 2) {
@@ -5188,11 +7910,11 @@ function centerOf(options) {
5188
7910
  return out;
5189
7911
  }
5190
7912
  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`);
7913
+ const v = center[axis];
7914
+ if (!Number.isFinite(v)) {
7915
+ throw invalidArgument("center", v, "finite components", `center[${axis}] is not finite`);
5194
7916
  }
5195
- out[axis] = v2;
7917
+ out[axis] = v;
5196
7918
  }
5197
7919
  return out;
5198
7920
  }
@@ -5840,12 +8562,12 @@ class ForceSimulation {
5840
8562
  );
5841
8563
  }
5842
8564
  let unpinned = false;
5843
- for (let w2 = 0; w2 < words && !unpinned; w2++) {
5844
- if (this.fixedWords[w2] === mask[w2]) {
8565
+ for (let w = 0; w < words && !unpinned; w++) {
8566
+ if (this.fixedWords[w] === mask[w]) {
5845
8567
  continue;
5846
8568
  }
5847
- const last = Math.min(n, w2 * 32 + 32);
5848
- for (let i = w2 * 32; i < last; i++) {
8569
+ const last = Math.min(n, w * 32 + 32);
8570
+ for (let i = w * 32; i < last; i++) {
5849
8571
  if (maskTest(this.fixedWords, i) && !maskTest(mask, i)) {
5850
8572
  unpinned = true;
5851
8573
  break;
@@ -5867,7 +8589,7 @@ class ForceSimulation {
5867
8589
  * @param y - scene y
5868
8590
  * @param z - scene z (ignored on the device in 2D)
5869
8591
  */
5870
- setPosition(index, x2, y2, z) {
8592
+ setPosition(index, x, y, z) {
5871
8593
  this.assertLoaded();
5872
8594
  const { n } = this;
5873
8595
  if (!Number.isInteger(index) || index < 0 || index >= n) {
@@ -5878,10 +8600,10 @@ class ForceSimulation {
5878
8600
  `setPosition(${index}): index out of range`
5879
8601
  );
5880
8602
  }
5881
- if (!Number.isFinite(x2) || !Number.isFinite(y2) || !Number.isFinite(z)) {
8603
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
5882
8604
  throw invalidArgument(
5883
8605
  "position",
5884
- [x2, y2, z],
8606
+ [x, y, z],
5885
8607
  "finite coordinates",
5886
8608
  "setPosition: coordinates must be finite"
5887
8609
  );
@@ -5890,13 +8612,13 @@ class ForceSimulation {
5890
8612
  if (owner === null || buffers === null) {
5891
8613
  throw new WebGpuGraphError("E_NOT_LOADED", `${this.label}: load() first`, { state: this.stateValue });
5892
8614
  }
5893
- owner[3 * index] = x2;
5894
- owner[3 * index + 1] = y2;
8615
+ owner[3 * index] = x;
8616
+ owner[3 * index + 1] = y;
5895
8617
  owner[3 * index + 2] = z;
5896
8618
  const [cx, cy, cz] = this.center;
5897
8619
  const layout = new Float32Array([
5898
- (x2 - cx) / this.scale,
5899
- (y2 - cy) / this.scale,
8620
+ (x - cx) / this.scale,
8621
+ (y - cy) / this.scale,
5900
8622
  this.dimValue === 2 ? 0 : (z - cz) / this.scale
5901
8623
  ]);
5902
8624
  this.ctx.device.queue.writeBuffer(buffers.positions, 16 * index, layout);
@@ -6517,17 +9239,17 @@ class ForceSimulation {
6517
9239
  const lo = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
6518
9240
  const hi = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
6519
9241
  for (let i = 0; i < n; i++) {
6520
- const x2 = packed[4 * i];
6521
- const y2 = packed[4 * i + 1];
9242
+ const x = packed[4 * i];
9243
+ const y = packed[4 * i + 1];
6522
9244
  const z = packed[4 * i + 2];
6523
- sx += x2;
6524
- sy += y2;
9245
+ sx += x;
9246
+ sy += y;
6525
9247
  sz += z;
6526
- lo[0] = Math.min(lo[0], x2);
6527
- lo[1] = Math.min(lo[1], y2);
9248
+ lo[0] = Math.min(lo[0], x);
9249
+ lo[1] = Math.min(lo[1], y);
6528
9250
  lo[2] = Math.min(lo[2], z);
6529
- hi[0] = Math.max(hi[0], x2);
6530
- hi[1] = Math.max(hi[1], y2);
9251
+ hi[0] = Math.max(hi[0], x);
9252
+ hi[1] = Math.max(hi[1], y);
6531
9253
  hi[2] = Math.max(hi[2], z);
6532
9254
  }
6533
9255
  const mx = sx / n;
@@ -6788,14 +9510,14 @@ function invalid$1(argument, value, expected, message) {
6788
9510
  }
6789
9511
  function checkMass(mass, source) {
6790
9512
  for (let i = 0; i < mass.length; i++) {
6791
- const v2 = mass[i];
6792
- if (!Number.isFinite(v2) || v2 <= 0) {
9513
+ const v = mass[i];
9514
+ if (!Number.isFinite(v) || v <= 0) {
6793
9515
  throw new WebGpuGraphError(
6794
9516
  "E_INVALID_ARGUMENT",
6795
- `${source}: mass[${i}] = ${v2} is not a finite number > 0`,
9517
+ `${source}: mass[${i}] = ${v} is not a finite number > 0`,
6796
9518
  {
6797
9519
  argument: "nodeMass",
6798
- value: v2,
9520
+ value: v,
6799
9521
  index: i,
6800
9522
  expected: "finite masses > 0"
6801
9523
  }
@@ -7061,13 +9783,13 @@ function pickCenter(given, fallback) {
7061
9783
  if (length !== 2 && length !== 3) {
7062
9784
  throw invalid("center", given, expected);
7063
9785
  }
7064
- const x2 = given[0];
7065
- const y2 = given[1];
9786
+ const x = given[0];
9787
+ const y = given[1];
7066
9788
  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)) {
9789
+ if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number" || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
7068
9790
  throw invalid("center", given, expected);
7069
9791
  }
7070
- return [x2, y2, z];
9792
+ return [x, y, z];
7071
9793
  }
7072
9794
  function pickSeed(given, fallback) {
7073
9795
  if (given === void 0) {
@@ -7669,9 +10391,9 @@ function resolveForceAtlas2Options(options, previous) {
7669
10391
  }
7670
10392
  const resolved = {
7671
10393
  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"),
10394
+ jitterTolerance: pickNumber("jitterTolerance", o.jitterTolerance, base.jitterTolerance, (v) => v > 0, "> 0"),
10395
+ scalingRatio: pickNumber("scalingRatio", o.scalingRatio, base.scalingRatio, (v) => v > 0, "> 0"),
10396
+ gravity: pickNumber("gravity", o.gravity, base.gravity, (v) => v >= 0, ">= 0"),
7675
10397
  strongGravity: pickBoolean("strongGravity", o.strongGravity, base.strongGravity),
7676
10398
  distributedAction: pickBoolean("distributedAction", o.distributedAction, base.distributedAction),
7677
10399
  linlog: pickBoolean("linlog", o.linlog, base.linlog),
@@ -7680,10 +10402,10 @@ function resolveForceAtlas2Options(options, previous) {
7680
10402
  weight: o.weight === void 0 ? base.weight : o.weight,
7681
10403
  dissuadeHubs: pickBoolean("dissuadeHubs", o.dissuadeHubs, base.dissuadeHubs),
7682
10404
  dim: pickDim(o.dim, base.dim),
7683
- scale: pickNumber("scale", o.scale, base.scale, (v2) => v2 > 0, "> 0"),
10405
+ scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
7684
10406
  center: pickCenter(o.center, base.center),
7685
10407
  seed: pickSeed(o.seed, base.seed),
7686
- settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v2) => v2 >= 0, ">= 0"),
10408
+ settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
7687
10409
  settleWindow: pickNumber(
7688
10410
  "settleWindow",
7689
10411
  o.settleWindow,
@@ -7695,7 +10417,7 @@ function resolveForceAtlas2Options(options, previous) {
7695
10417
  "iterationsPerStep",
7696
10418
  o.iterationsPerStep,
7697
10419
  base.iterationsPerStep,
7698
- (v2) => isPositiveInteger(v2) && v2 <= MAX_ITERATIONS_PER_STEP,
10420
+ (v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
7699
10421
  `an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
7700
10422
  ),
7701
10423
  maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
@@ -7725,7 +10447,7 @@ function resolveLayoutTuning(tuning) {
7725
10447
  "nearMax",
7726
10448
  t.nearMax,
7727
10449
  LAYOUT_TUNING_DEFAULTS.nearMax,
7728
- (v2) => isPositiveInteger(v2) && v2 >= 2,
10450
+ (v) => isPositiveInteger(v) && v >= 2,
7729
10451
  "an integer >= 2"
7730
10452
  ),
7731
10453
  deterministic: pickBoolean("deterministic", t.deterministic, LAYOUT_TUNING_DEFAULTS.deterministic),
@@ -7747,7 +10469,7 @@ function resolveLayoutTuning(tuning) {
7747
10469
  "extentFactor",
7748
10470
  t.extentFactor,
7749
10471
  LAYOUT_TUNING_DEFAULTS.extentFactor,
7750
- (v2) => v2 > 0,
10472
+ (v) => v > 0,
7751
10473
  "> 0"
7752
10474
  ),
7753
10475
  compat
@@ -8374,7 +11096,7 @@ function resolveK(given, fallback) {
8374
11096
  if (given === null || given === 0 || Number.isNaN(given)) {
8375
11097
  return null;
8376
11098
  }
8377
- return pickNumber("k", given, 1, (v2) => v2 > 0, "> 0 or null");
11099
+ return pickNumber("k", given, 1, (v) => v > 0, "> 0 or null");
8378
11100
  }
8379
11101
  function pickCooling(given, fallback) {
8380
11102
  if (given === void 0) {
@@ -8407,16 +11129,16 @@ function resolveFruchtermanReingoldOptions(options, previous) {
8407
11129
  "iterations",
8408
11130
  o.iterations,
8409
11131
  iterationsFallback,
8410
- (v2) => Number.isInteger(v2) && v2 >= 0,
11132
+ (v) => Number.isInteger(v) && v >= 0,
8411
11133
  "an integer >= 0"
8412
11134
  ),
8413
11135
  cooling,
8414
11136
  fixed,
8415
11137
  dim: pickDim(o.dim, base.dim),
8416
- scale: pickNumber("scale", o.scale, base.scale, (v2) => v2 > 0, "> 0"),
11138
+ scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
8417
11139
  center: pickCenter(o.center, base.center),
8418
11140
  seed: pickSeed(o.seed, base.seed),
8419
- settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v2) => v2 >= 0, ">= 0"),
11141
+ settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
8420
11142
  settleWindow: pickNumber(
8421
11143
  "settleWindow",
8422
11144
  o.settleWindow,
@@ -8428,7 +11150,7 @@ function resolveFruchtermanReingoldOptions(options, previous) {
8428
11150
  "iterationsPerStep",
8429
11151
  o.iterationsPerStep,
8430
11152
  base.iterationsPerStep,
8431
- (v2) => isPositiveInteger(v2) && v2 <= MAX_ITERATIONS_PER_STEP,
11153
+ (v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
8432
11154
  `an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
8433
11155
  ),
8434
11156
  maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
@@ -9058,22 +11780,22 @@ function resolveSpringElectricalOptions(options, previous) {
9058
11780
  );
9059
11781
  }
9060
11782
  const resolved = {
9061
- springLength: pickNumber("springLength", o.springLength, base.springLength, (v2) => v2 > 0, "> 0"),
11783
+ springLength: pickNumber("springLength", o.springLength, base.springLength, (v) => v > 0, "> 0"),
9062
11784
  springCoefficient: pickNullable(
9063
11785
  "springCoefficient",
9064
11786
  o.springCoefficient,
9065
11787
  base.springCoefficient,
9066
- (v2) => v2 > 0,
11788
+ (v) => v > 0,
9067
11789
  "> 0"
9068
11790
  ),
9069
11791
  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"),
11792
+ dragCoefficient: pickNumber("dragCoefficient", o.dragCoefficient, base.dragCoefficient, (v) => v >= 0, ">= 0"),
11793
+ timeStep: pickNumber("timeStep", o.timeStep, base.timeStep, (v) => v > 0, "> 0"),
9072
11794
  dim: pickDim(o.dim, base.dim),
9073
- scale: pickNumber("scale", o.scale, base.scale, (v2) => v2 > 0, "> 0"),
11795
+ scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
9074
11796
  center: pickCenter(o.center, base.center),
9075
11797
  seed: pickSeed(o.seed, base.seed),
9076
- settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v2) => v2 >= 0, ">= 0"),
11798
+ settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
9077
11799
  settleWindow: pickNumber(
9078
11800
  "settleWindow",
9079
11801
  o.settleWindow,
@@ -9085,7 +11807,7 @@ function resolveSpringElectricalOptions(options, previous) {
9085
11807
  "iterationsPerStep",
9086
11808
  o.iterationsPerStep,
9087
11809
  base.iterationsPerStep,
9088
- (v2) => isPositiveInteger(v2) && v2 <= MAX_ITERATIONS_PER_STEP,
11810
+ (v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
9089
11811
  `an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
9090
11812
  ),
9091
11813
  maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
@@ -9718,6 +12440,51 @@ function createAccelerator(ctx, options) {
9718
12440
  ctx.assertReady();
9719
12441
  return await connectedComponents(ctx, gs, o);
9720
12442
  },
12443
+ /**
12444
+ * Breadth-first search on the device (spec 8.4; P8-T13). The result is bitwise reproducible (P8 PD-14).
12445
+ * @param gs - the snapshot
12446
+ * @param source - the source node index
12447
+ * @param o - the seam's `BfsOptions` (`maxDepth`)
12448
+ * @returns depth, parent, the level-grouped order, visitedCount, levels and switches (spec 3.3 line 830)
12449
+ */
12450
+ async breadthFirstSearch(gs, source, o) {
12451
+ ctx.assertReady();
12452
+ return await breadthFirstSearch(ctx, gs, source, o);
12453
+ },
12454
+ /**
12455
+ * Single-source shortest paths on the device (spec 8.4; P8-T13): the near-far queue over f32 distances, or the
12456
+ * breadth-first route when every weight is one.
12457
+ * @param gs - the snapshot
12458
+ * @param source - the source node index
12459
+ * @param o - the seam's `SsspOptions` (`cutoff`, `weights`)
12460
+ * @returns dist, predArc and reachedCount (spec 3.3 line 831)
12461
+ */
12462
+ async sssp(gs, source, o) {
12463
+ ctx.assertReady();
12464
+ return await sssp(ctx, gs, source, o);
12465
+ },
12466
+ /**
12467
+ * Bellman-Ford on the device with negative-cycle detection (spec 8.4; P8-T13).
12468
+ * @param gs - the snapshot
12469
+ * @param source - the source node index
12470
+ * @param o - the seam's `SsspOptions` (`cutoff`, `weights`)
12471
+ * @returns dist, predArc, reachedCount and hasNegativeCycle (spec 3.3 line 832)
12472
+ */
12473
+ async bellmanFord(gs, source, o) {
12474
+ ctx.assertReady();
12475
+ return await bellmanFord(ctx, gs, source, o);
12476
+ },
12477
+ /**
12478
+ * Closeness centrality on the device (spec 8.4; P8-T13): the bit-parallel multi-source sweep, or one `sssp`
12479
+ * per source when `weighted`. `maxIterations` / `tolerance` are refused when defined (P8 PD-25).
12480
+ * @param gs - the snapshot
12481
+ * @param o - the seam's placeholder `HitsOptionsLike` (`weighted`)
12482
+ * @returns the f32 scores with `precision: "f32"` (spec 9.7)
12483
+ */
12484
+ async closenessCentrality(gs, o) {
12485
+ ctx.assertReady();
12486
+ return await closenessCentrality(ctx, gs, o);
12487
+ },
9721
12488
  /**
9722
12489
  * Destroys every device buffer recorded for the snapshot (spec 4.5); delegates to ctx.release.
9723
12490
  * @param s - the snapshot the app is done with
@@ -9835,12 +12602,15 @@ export {
9835
12602
  LAYOUT_TUNING_DEFAULTS,
9836
12603
  MAX_1D_ITEMS,
9837
12604
  MAX_WORKGROUPS_PER_DIM,
9838
- v as PASSTHROUGH_FORMAT_CODES,
12605
+ C as PASSTHROUGH_FORMAT_CODES,
9839
12606
  SE_DEFAULTS,
9840
- w as STORAGE_ALIGN,
9841
- x as WORKGROUP_SIZE,
12607
+ D as STORAGE_ALIGN,
12608
+ H as WORKGROUP_SIZE,
9842
12609
  WebGpuGraphError,
12610
+ bellmanFord,
12611
+ breadthFirstSearch,
9843
12612
  calibrateLayout,
12613
+ closenessCentrality,
9844
12614
  connectedComponents,
9845
12615
  createAccelerator,
9846
12616
  createForceAtlas2,
@@ -9850,12 +12620,13 @@ export {
9850
12620
  eigenvectorCentrality,
9851
12621
  hasErrorCode,
9852
12622
  hits,
9853
- y as isSoftwareAdapter,
12623
+ J as isSoftwareAdapter,
9854
12624
  isWebGpuGraphError,
9855
12625
  katzCentrality,
9856
12626
  pageRank,
9857
12627
  personalizedPageRank,
9858
12628
  seedPositions,
12629
+ sssp,
9859
12630
  verifyDevice
9860
12631
  };
9861
12632
  //# sourceMappingURL=webgpu-graph-algorithms.js.map