@graphty/webgpu-graph-algorithms 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +459 -58
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-BR7fx3vR.js → context-BXqgCifx.js} +190 -40
- package/dist/chunks/context-BXqgCifx.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/src/algorithms/components.d.ts.map +1 -1
- package/dist/src/algorithms/components.js +12 -13
- package/dist/src/algorithms/components.js.map +1 -1
- package/dist/src/algorithms/degree.d.ts +6 -8
- package/dist/src/algorithms/degree.d.ts.map +1 -1
- package/dist/src/algorithms/degree.js +58 -35
- package/dist/src/algorithms/degree.js.map +1 -1
- package/dist/src/algorithms/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/pagerank.js +16 -14
- package/dist/src/algorithms/pagerank.js.map +1 -1
- package/dist/src/algorithms/power-iteration.d.ts +2 -2
- package/dist/src/algorithms/power-iteration.d.ts.map +1 -1
- package/dist/src/algorithms/power-iteration.js +17 -14
- package/dist/src/algorithms/power-iteration.js.map +1 -1
- package/dist/src/constants.d.ts +38 -8
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +38 -8
- package/dist/src/constants.js.map +1 -1
- package/dist/src/errors.d.ts +3 -2
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +2 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/dispatch.d.ts +8 -3
- package/dist/src/kernel/dispatch.d.ts.map +1 -1
- package/dist/src/kernel/dispatch.js +18 -7
- package/dist/src/kernel/dispatch.js.map +1 -1
- package/dist/src/kernel/kernel.d.ts +30 -1
- package/dist/src/kernel/kernel.d.ts.map +1 -1
- package/dist/src/kernel/kernel.js +49 -5
- package/dist/src/kernel/kernel.js.map +1 -1
- package/dist/src/kernel/prelude.d.ts.map +1 -1
- package/dist/src/kernel/prelude.js +6 -1
- package/dist/src/kernel/prelude.js.map +1 -1
- package/dist/src/kernel/profiler.d.ts +15 -3
- package/dist/src/kernel/profiler.d.ts.map +1 -1
- package/dist/src/kernel/profiler.js +27 -4
- package/dist/src/kernel/profiler.js.map +1 -1
- package/dist/src/kernels.d.ts +17 -7
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +323 -16
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/layouts/calibrate.d.ts +51 -0
- package/dist/src/layouts/calibrate.d.ts.map +1 -0
- package/dist/src/layouts/calibrate.js +172 -0
- package/dist/src/layouts/calibrate.js.map +1 -0
- package/dist/src/layouts/force-simulation.d.ts +39 -4
- package/dist/src/layouts/force-simulation.d.ts.map +1 -1
- package/dist/src/layouts/force-simulation.js +71 -19
- package/dist/src/layouts/force-simulation.js.map +1 -1
- package/dist/src/layouts/forceatlas2.d.ts +107 -36
- package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
- package/dist/src/layouts/forceatlas2.js +296 -100
- package/dist/src/layouts/forceatlas2.js.map +1 -1
- package/dist/src/layouts/fruchterman-reingold.d.ts +73 -27
- package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -1
- package/dist/src/layouts/fruchterman-reingold.js +230 -70
- package/dist/src/layouts/fruchterman-reingold.js.map +1 -1
- package/dist/src/layouts/model-common.d.ts +41 -3
- package/dist/src/layouts/model-common.d.ts.map +1 -1
- package/dist/src/layouts/model-common.js +74 -3
- package/dist/src/layouts/model-common.js.map +1 -1
- package/dist/src/layouts/repulsion-grid.d.ts +152 -0
- package/dist/src/layouts/repulsion-grid.d.ts.map +1 -0
- package/dist/src/layouts/repulsion-grid.js +318 -0
- package/dist/src/layouts/repulsion-grid.js.map +1 -0
- package/dist/src/layouts/spring-electrical.d.ts +75 -30
- package/dist/src/layouts/spring-electrical.d.ts.map +1 -1
- package/dist/src/layouts/spring-electrical.js +231 -74
- package/dist/src/layouts/spring-electrical.js.map +1 -1
- package/dist/src/memory/residency.d.ts +6 -2
- package/dist/src/memory/residency.d.ts.map +1 -1
- package/dist/src/memory/residency.js +84 -14
- package/dist/src/memory/residency.js.map +1 -1
- package/dist/src/primitives/core-shape.d.ts +38 -2
- package/dist/src/primitives/core-shape.d.ts.map +1 -1
- package/dist/src/primitives/core-shape.js +71 -3
- package/dist/src/primitives/core-shape.js.map +1 -1
- package/dist/src/primitives/grid-pyramid.d.ts +71 -0
- package/dist/src/primitives/grid-pyramid.d.ts.map +1 -0
- package/dist/src/primitives/grid-pyramid.js +143 -0
- package/dist/src/primitives/grid-pyramid.js.map +1 -0
- package/dist/src/primitives/grid.d.ts +118 -0
- package/dist/src/primitives/grid.d.ts.map +1 -0
- package/dist/src/primitives/grid.js +225 -0
- package/dist/src/primitives/grid.js.map +1 -0
- package/dist/src/primitives/histogram.d.ts +67 -0
- package/dist/src/primitives/histogram.d.ts.map +1 -0
- package/dist/src/primitives/histogram.js +190 -0
- package/dist/src/primitives/histogram.js.map +1 -0
- package/dist/src/primitives/radix-sort.d.ts +75 -0
- package/dist/src/primitives/radix-sort.d.ts.map +1 -0
- package/dist/src/primitives/radix-sort.js +168 -0
- package/dist/src/primitives/radix-sort.js.map +1 -0
- package/dist/src/primitives/scan.d.ts +44 -0
- package/dist/src/primitives/scan.d.ts.map +1 -0
- package/dist/src/primitives/scan.js +151 -0
- package/dist/src/primitives/scan.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts +25 -17
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
- package/dist/src/primitives/segmented-reduce.js +166 -47
- package/dist/src/primitives/segmented-reduce.js.map +1 -1
- package/dist/src/primitives/spmv.d.ts +18 -14
- package/dist/src/primitives/spmv.d.ts.map +1 -1
- package/dist/src/primitives/spmv.js +94 -58
- package/dist/src/primitives/spmv.js.map +1 -1
- package/dist/src/primitives/verify.d.ts +49 -0
- package/dist/src/primitives/verify.d.ts.map +1 -0
- package/dist/src/primitives/verify.js +229 -0
- package/dist/src/primitives/verify.js.map +1 -0
- package/dist/src/types/context.d.ts +53 -0
- package/dist/src/types/context.d.ts.map +1 -1
- package/dist/src/types/layout.d.ts +20 -0
- package/dist/src/types/layout.d.ts.map +1 -1
- package/dist/src/wgsl/counting-scatter.wgsl.d.ts +8 -0
- package/dist/src/wgsl/counting-scatter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/counting-scatter.wgsl.js +17 -0
- package/dist/src/wgsl/counting-scatter.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +23 -11
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-attraction.wgsl.js +98 -20
- package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +6 -2
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +22 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
- package/dist/src/wgsl/grid-cell-key.wgsl.d.ts +8 -0
- package/dist/src/wgsl/grid-cell-key.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-cell-key.wgsl.js +30 -0
- package/dist/src/wgsl/grid-cell-key.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts +8 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.js +29 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-centroid.wgsl.d.ts +8 -0
- package/dist/src/wgsl/grid-centroid.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-centroid.wgsl.js +29 -0
- package/dist/src/wgsl/grid-centroid.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-downsample.wgsl.d.ts +7 -0
- package/dist/src/wgsl/grid-downsample.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-downsample.wgsl.js +28 -0
- package/dist/src/wgsl/grid-downsample.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-far-field.wgsl.d.ts +13 -0
- package/dist/src/wgsl/grid-far-field.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-far-field.wgsl.js +98 -0
- package/dist/src/wgsl/grid-far-field.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-near-field.wgsl.d.ts +19 -0
- package/dist/src/wgsl/grid-near-field.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-near-field.wgsl.js +129 -0
- package/dist/src/wgsl/grid-near-field.wgsl.js.map +1 -0
- package/dist/src/wgsl/histogram.wgsl.d.ts +7 -0
- package/dist/src/wgsl/histogram.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/histogram.wgsl.js +15 -0
- package/dist/src/wgsl/histogram.wgsl.js.map +1 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.d.ts +8 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.js +26 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/radix-hist.wgsl.d.ts +9 -0
- package/dist/src/wgsl/radix-hist.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/radix-hist.wgsl.js +31 -0
- package/dist/src/wgsl/radix-hist.wgsl.js.map +1 -0
- package/dist/src/wgsl/radix-scatter.wgsl.d.ts +9 -0
- package/dist/src/wgsl/radix-scatter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/radix-scatter.wgsl.js +40 -0
- package/dist/src/wgsl/radix-scatter.wgsl.js.map +1 -0
- package/dist/src/wgsl/scan-add.wgsl.d.ts +6 -0
- package/dist/src/wgsl/scan-add.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/scan-add.wgsl.js +14 -0
- package/dist/src/wgsl/scan-add.wgsl.js.map +1 -0
- package/dist/src/wgsl/scan-block.wgsl.d.ts +8 -0
- package/dist/src/wgsl/scan-block.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/scan-block.wgsl.js +30 -0
- package/dist/src/wgsl/scan-block.wgsl.js.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +22 -8
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/segmented-reduce.wgsl.js +84 -15
- package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -1
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts +22 -11
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/spmv-pull.wgsl.js +110 -36
- package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/webgpu-graph-algorithms.js +3815 -1003
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +9 -8
- package/src/algorithms/components.ts +12 -16
- package/src/algorithms/degree.ts +58 -43
- package/src/algorithms/pagerank.ts +20 -18
- package/src/algorithms/power-iteration.ts +19 -18
- package/src/constants.ts +38 -8
- package/src/errors.ts +3 -1
- package/src/index.ts +14 -4
- package/src/kernel/dispatch.ts +18 -7
- package/src/kernel/kernel.ts +59 -5
- package/src/kernel/prelude.ts +9 -0
- package/src/kernel/profiler.ts +28 -4
- package/src/kernels.ts +356 -18
- package/src/layouts/calibrate.ts +187 -0
- package/src/layouts/force-simulation.ts +91 -23
- package/src/layouts/forceatlas2.ts +331 -106
- package/src/layouts/fruchterman-reingold.ts +255 -74
- package/src/layouts/model-common.ts +98 -3
- package/src/layouts/repulsion-grid.ts +451 -0
- package/src/layouts/spring-electrical.ts +257 -78
- package/src/memory/residency.ts +126 -20
- package/src/primitives/core-shape.ts +91 -4
- package/src/primitives/grid-pyramid.ts +221 -0
- package/src/primitives/grid.ts +349 -0
- package/src/primitives/histogram.ts +273 -0
- package/src/primitives/radix-sort.ts +246 -0
- package/src/primitives/scan.ts +197 -0
- package/src/primitives/segmented-reduce.ts +214 -56
- package/src/primitives/spmv.ts +125 -65
- package/src/primitives/verify.ts +249 -0
- package/src/types/context.ts +56 -0
- package/src/types/layout.ts +22 -0
- package/src/wgsl/counting-scatter.wgsl.ts +16 -0
- package/src/wgsl/fa2-attraction.wgsl.ts +98 -20
- package/src/wgsl/fa2-stats-finalize.wgsl.ts +22 -1
- package/src/wgsl/grid-cell-key.wgsl.ts +29 -0
- package/src/wgsl/grid-centroid-hub.wgsl.ts +28 -0
- package/src/wgsl/grid-centroid.wgsl.ts +28 -0
- package/src/wgsl/grid-downsample.wgsl.ts +27 -0
- package/src/wgsl/grid-far-field.wgsl.ts +97 -0
- package/src/wgsl/grid-near-field.wgsl.ts +128 -0
- package/src/wgsl/histogram.wgsl.ts +14 -0
- package/src/wgsl/indirect-finalize.wgsl.ts +25 -0
- package/src/wgsl/radix-hist.wgsl.ts +30 -0
- package/src/wgsl/radix-scatter.wgsl.ts +39 -0
- package/src/wgsl/scan-add.wgsl.ts +13 -0
- package/src/wgsl/scan-block.wgsl.ts +29 -0
- package/src/wgsl/segmented-reduce.wgsl.ts +84 -15
- package/src/wgsl/spmv-pull.wgsl.ts +110 -36
- package/dist/chunks/context-BR7fx3vR.js.map +0 -1
|
@@ -4,20 +4,26 @@
|
|
|
4
4
|
* record (spec 7.2 with the 4.6 NetworkX corrections), the per-iteration Fa2Params values, the controller resets of
|
|
5
5
|
* spec 7.17 and the stats decoder -- plus the two option resolvers and `createForceAtlas2`. Positions are vec4f
|
|
6
6
|
* (xyz + mass) in layout units on the device (D23, 7.18); the speed controller runs on the device (D15); no
|
|
7
|
-
* displacement clamp (D25); K2 runs the
|
|
7
|
+
* displacement clamp (D25); K2 runs the degree tiers over `degreeOrder()` (P4 PD-7).
|
|
8
8
|
*
|
|
9
9
|
* Model decisions this file fixes (the plan of P3-T2 lists the reasons): recordIteration with no `upTo` records every
|
|
10
|
-
* stage including toScene; the K1-K5 dispatches of every iteration of one batch share ONE compute
|
|
11
|
-
* batch's first recordIteration and remembered by batch id) and toScene runs in a second pass
|
|
12
|
-
* 4.4); the fill kernel takes its FillParams from a model-owned 256-byte uniform buffer
|
|
13
|
-
* iteration after every load() zeroes oldForce with a fill (paper mode)
|
|
14
|
-
*
|
|
10
|
+
* stage including toScene; on the exact tier the K1-K5 dispatches of every iteration of one batch share ONE compute
|
|
11
|
+
* pass (opened by the batch's first recordIteration and remembered by batch id) and toScene runs in a second pass
|
|
12
|
+
* that ends it (contract 4.4); the fill kernel takes its FillParams from a model-owned 256-byte uniform buffer
|
|
13
|
+
* ("fillParams"); the first iteration after every load() zeroes oldForce with a fill (paper mode). The grid tier
|
|
14
|
+
* (P4-T10) is reached through `RepulsionGrid` when `tierFor(tuning, n)` says so (PD-18): `buffers()` adds the grid
|
|
15
|
+
* buffers, K1 derives the frame under `gridMax > 0` (PD-14), the iteration is recorded as the three passes `fa2-k1`
|
|
16
|
+
* / `fa2-attraction` / `fa2-grid` before `fa2-to-scene` (PD-16), and `stages` is the union list of both tiers
|
|
17
|
+
* (PD-17: `upTo` stops after the last stage recorded at or before its position).
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { type GraphSnapshot } from "@graphty/graph-format";
|
|
18
21
|
|
|
19
22
|
import {
|
|
23
|
+
EXACT_MAX_NODES,
|
|
20
24
|
FA2_DEFAULTS,
|
|
25
|
+
GRID_BBOX_MARGIN,
|
|
26
|
+
GRID_EXTENT_FLOOR,
|
|
21
27
|
LAYOUT_TUNING_DEFAULTS,
|
|
22
28
|
MAX_ITERATIONS_PER_STEP,
|
|
23
29
|
TRACE_RECORD_BYTES,
|
|
@@ -31,7 +37,9 @@ import { type DispatchPlan, plan1d } from "../kernel/dispatch.js";
|
|
|
31
37
|
import { type BoundKernel, type Kernel } from "../kernel/kernel.js";
|
|
32
38
|
import { type UniformBlock, type UniformValues } from "../kernel/struct-block.js";
|
|
33
39
|
import { type WgslModuleSpec } from "../kernel/wgsl.js";
|
|
34
|
-
import { FA2_PARAMS, FA2_STATE, FA2_TRACE, FILL_PARAMS,
|
|
40
|
+
import { FA2_PARAMS, FA2_STATE, FA2_TRACE, FILL_PARAMS, kernelSpec } from "../kernels.js";
|
|
41
|
+
import { arcCountOf } from "../primitives/core-shape.js";
|
|
42
|
+
import { type GridSpec, gridSpecFor } from "../primitives/grid.js";
|
|
35
43
|
import {
|
|
36
44
|
type ForceAtlas2Stats,
|
|
37
45
|
type ForceAtlas2TraceRecord,
|
|
@@ -48,9 +56,12 @@ import {
|
|
|
48
56
|
type ModelInputs,
|
|
49
57
|
type ModelResources,
|
|
50
58
|
type StateWriter,
|
|
59
|
+
tierFor,
|
|
51
60
|
} from "./force-simulation.js";
|
|
52
61
|
import { resolveNodeMass, resolveWeights } from "./inputs.js";
|
|
53
62
|
import {
|
|
63
|
+
type AttractionBound,
|
|
64
|
+
bindAttraction,
|
|
54
65
|
describeValue,
|
|
55
66
|
FILL_PARAMS_BUFFER,
|
|
56
67
|
FORCE_BYTES_PER_NODE,
|
|
@@ -62,17 +73,28 @@ import {
|
|
|
62
73
|
pickDim,
|
|
63
74
|
pickNumber,
|
|
64
75
|
pickSeed,
|
|
76
|
+
recordAttraction,
|
|
65
77
|
scalar,
|
|
66
78
|
seedWord,
|
|
67
79
|
subset,
|
|
68
80
|
vector,
|
|
69
81
|
} from "./model-common.js";
|
|
70
82
|
import { RepulsionExact, type RepulsionExactOverrides } from "./repulsion-exact.js";
|
|
83
|
+
import { type GridStage, RepulsionGrid, type RepulsionGridOverrides } from "./repulsion-grid.js";
|
|
71
84
|
|
|
72
85
|
// ============================================================ constants and small helpers
|
|
73
86
|
|
|
74
|
-
/** The stage names of
|
|
75
|
-
const FA2_STAGES = ["K1", "K2", "K3", "K4", "K5", "toScene"] as const;
|
|
87
|
+
/** The stage names of both tiers in dispatch order plus the per-batch toScene (spec 7.4; contract 3.13; P4 PD-17): the exact tier records K1 K2 K3 K4 K5, the grid tier K1 K2 G1..G7 K4 K5. */
|
|
88
|
+
const FA2_STAGES = ["K1", "K2", "K3", "G1", "G2", "G3", "G4", "G5", "G6", "G7", "K4", "K5", "toScene"] as const;
|
|
89
|
+
|
|
90
|
+
/** The FA2_STAGES index of the first grid stage, of K4, of K5 and of toScene. */
|
|
91
|
+
const STAGE_G1 = 3;
|
|
92
|
+
const STAGE_K4 = 10;
|
|
93
|
+
const STAGE_K5 = 11;
|
|
94
|
+
const STAGE_TO_SCENE = 12;
|
|
95
|
+
|
|
96
|
+
/** The name of the model-owned hub-counter buffer K1 binds on every tier (P4 PD-14). */
|
|
97
|
+
const HUB_COUNTERS_BUFFER = "hubCounters";
|
|
76
98
|
|
|
77
99
|
/** The one-workgroup dispatch of K1 (spec 7.4). */
|
|
78
100
|
const ONE_WORKGROUP: DispatchPlan = { x: 1, y: 1, z: 1, items: 1, stride: null };
|
|
@@ -106,6 +128,15 @@ function repulsionOverrides(merged: Overrides): RepulsionExactOverrides {
|
|
|
106
128
|
};
|
|
107
129
|
}
|
|
108
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The G6 / G7 / K4 override values of a merged set (typed for RepulsionGrid): K3's three and the FA2 law (P4-T13).
|
|
133
|
+
* @param merged - the merged override set
|
|
134
|
+
* @returns the grid stage's overrides
|
|
135
|
+
*/
|
|
136
|
+
function gridOverrides(merged: Overrides): RepulsionGridOverrides {
|
|
137
|
+
return { ...repulsionOverrides(merged), LAW: 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
109
140
|
// ============================================================ the resolvers
|
|
110
141
|
|
|
111
142
|
/**
|
|
@@ -175,8 +206,8 @@ export function resolveForceAtlas2Options(
|
|
|
175
206
|
}
|
|
176
207
|
|
|
177
208
|
/**
|
|
178
|
-
* Applies LAYOUT_TUNING_DEFAULTS (spec 7.14
|
|
179
|
-
*
|
|
209
|
+
* Applies LAYOUT_TUNING_DEFAULTS (spec 7.14; contract 3.3). `nearMax` is an integer >= 2 (P4 DEP-P4-M: the
|
|
210
|
+
* near-field estimator needs at least one sampled entry besides the node itself).
|
|
180
211
|
* @param tuning - the GPU-only knobs given (any object carrying them, e.g. the createForceAtlas2 options)
|
|
181
212
|
* @returns the frozen resolved tuning
|
|
182
213
|
*/
|
|
@@ -199,7 +230,13 @@ export function resolveLayoutTuning(tuning: GpuLayoutTuning | undefined): Resolv
|
|
|
199
230
|
isPositiveInteger,
|
|
200
231
|
"an integer >= 1",
|
|
201
232
|
),
|
|
202
|
-
nearMax: pickNumber(
|
|
233
|
+
nearMax: pickNumber(
|
|
234
|
+
"nearMax",
|
|
235
|
+
t.nearMax,
|
|
236
|
+
LAYOUT_TUNING_DEFAULTS.nearMax,
|
|
237
|
+
(v) => isPositiveInteger(v) && v >= 2,
|
|
238
|
+
"an integer >= 2",
|
|
239
|
+
),
|
|
203
240
|
deterministic: pickBoolean("deterministic", t.deterministic, LAYOUT_TUNING_DEFAULTS.deterministic),
|
|
204
241
|
gridMax2D: pickNumber(
|
|
205
242
|
"gridMax2D",
|
|
@@ -238,10 +275,12 @@ interface BoundModel {
|
|
|
238
275
|
readonly fillPlan: DispatchPlan;
|
|
239
276
|
readonly k1: Kernel;
|
|
240
277
|
readonly k1Bound: BoundKernel;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
readonly repulsion: RepulsionExact;
|
|
278
|
+
/** The K2 tier dispatches (P4 PD-7); null when arcCount === 0 (K2 is not recorded; the fill below zeroes force instead, spec 7.5). */
|
|
279
|
+
readonly attraction: AttractionBound | null;
|
|
280
|
+
/** The exact-tier stage (K3 and K4), or null on the grid tier (P4 PD-18): one tier's kernels compile per load. */
|
|
281
|
+
readonly repulsion: RepulsionExact | null;
|
|
282
|
+
/** The grid-tier stage (G1-G7 and K4), or null on the exact tier (P4 PD-18). */
|
|
283
|
+
readonly grid: RepulsionGrid | null;
|
|
245
284
|
readonly k5: Kernel;
|
|
246
285
|
readonly k5Bound: BoundKernel;
|
|
247
286
|
readonly toScene: Kernel;
|
|
@@ -253,12 +292,12 @@ interface BoundModel {
|
|
|
253
292
|
readonly fillOldBound: BoundKernel | null;
|
|
254
293
|
}
|
|
255
294
|
|
|
256
|
-
/** The ForceAtlas2 model (spec 7.4: K1 K2 K3 K4 K5 per iteration; toScene once per batch). Stages:
|
|
295
|
+
/** The ForceAtlas2 model (spec 7.4: K1 K2 K3 K4 K5 per iteration on the exact tier, K1 K2 G1..G7 K4 K5 on the grid tier; toScene once per batch). Stages: the union list of PD-17. */
|
|
257
296
|
export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtlas2Stats> {
|
|
258
297
|
/** The model kind of spec 7.19. */
|
|
259
298
|
readonly kind = "forceatlas2";
|
|
260
299
|
/** The stage names in dispatch order (the `upTo` vocabulary of recordIteration and debugRunStages). */
|
|
261
|
-
readonly stages:
|
|
300
|
+
readonly stages: typeof FA2_STAGES = FA2_STAGES;
|
|
262
301
|
/** Fa2Params: the per-iteration uniform block (the simulation writes the shared fields into it). */
|
|
263
302
|
readonly params: UniformBlock = FA2_PARAMS;
|
|
264
303
|
/** Fa2State: the state header block (the simulation allocates and initialises it through this layout). */
|
|
@@ -281,6 +320,8 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
281
320
|
private bound: BoundModel | null = null;
|
|
282
321
|
/** Armed by onLoad(): the next recordIteration zeroes oldForce first (paper mode). */
|
|
283
322
|
private resetOldForce = false;
|
|
323
|
+
/** The grid of the load inputs() last resolved (null on the exact tier): onLoad() writes its frame, specs() lists its kernels. */
|
|
324
|
+
private nextGrid: GridSpec | null = null;
|
|
284
325
|
/**
|
|
285
326
|
* The K1-K5 compute pass of the batch being recorded, keyed by CommandBatch.id (unique per batch): every
|
|
286
327
|
* recordIteration of one batch dispatches into it (ONE pass per batch, contract 4.4); null between batches and
|
|
@@ -309,15 +350,19 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
309
350
|
|
|
310
351
|
/**
|
|
311
352
|
* force 12n and oldForce 12n (zeroed) in BOTH swing modes (3.10.1: a writable slot is never aliased; mode 1 leaves
|
|
312
|
-
* oldForce unread and unwritten),
|
|
313
|
-
*
|
|
353
|
+
* oldForce unread and unwritten), the 256-byte FillParams uniform buffer the fill dispatches read, the 16-byte
|
|
354
|
+
* `hubCounters` K1 binds on every tier (P4 PD-14), and the grid buffers of `RepulsionGrid.buffers` exactly when
|
|
355
|
+
* `tierFor(tuning, n)` is the grid tier (PD-18). n = 0 reports one node's worth of bytes so no zero-length buffer
|
|
356
|
+
* is ever created (spec 3.6).
|
|
314
357
|
* @param n - the node count
|
|
315
|
-
* @param
|
|
316
|
-
* @returns the
|
|
358
|
+
* @param dim - the layout dimension (the force arrays are stride 3 in both; the grid's geometry differs)
|
|
359
|
+
* @returns the model-owned buffer specs
|
|
317
360
|
*/
|
|
318
|
-
buffers(n: number,
|
|
361
|
+
buffers(n: number, dim: 2 | 3): readonly BufferSpec[] {
|
|
319
362
|
const bytes = Math.max(1, n) * FORCE_BYTES_PER_NODE;
|
|
320
363
|
const usage = BufferUsage.STORAGE | BufferUsage.COPY_SRC | BufferUsage.COPY_DST;
|
|
364
|
+
const grid =
|
|
365
|
+
tierFor(this.tuning, n) === "grid" ? RepulsionGrid.buffers(n, gridSpecFor(n, dim, this.tuning)) : [];
|
|
321
366
|
return [
|
|
322
367
|
{ name: "force", byteLength: bytes, usage, zero: true },
|
|
323
368
|
{ name: "oldForce", byteLength: bytes, usage, zero: true },
|
|
@@ -327,34 +372,26 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
327
372
|
usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
|
|
328
373
|
zero: false,
|
|
329
374
|
},
|
|
375
|
+
{ name: HUB_COUNTERS_BUFFER, byteLength: 16, usage, zero: true },
|
|
376
|
+
...grid,
|
|
330
377
|
];
|
|
331
378
|
}
|
|
332
379
|
|
|
333
380
|
/**
|
|
334
|
-
* { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) } (3.13 inputs.ts)
|
|
335
|
-
*
|
|
336
|
-
*
|
|
381
|
+
* { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) } (3.13 inputs.ts).
|
|
382
|
+
* Also remembers the grid of this load (`tierFor(tuning, n)`, spec 7.8) for onLoad() and specs(): the simulation
|
|
383
|
+
* calls inputs() first, then onLoad() before bind().
|
|
337
384
|
* @param s - the snapshot being loaded
|
|
338
385
|
* @param options - the simulation's current option record
|
|
339
386
|
* @returns the per-load inputs
|
|
340
387
|
*/
|
|
341
388
|
inputs(s: GraphSnapshot, options: ForceAtlas2Options): ModelInputs {
|
|
342
389
|
const resolved = resolveForceAtlas2Options(options, this.current);
|
|
343
|
-
const { repulsion, exactMaxNodes } = this.tuning;
|
|
344
390
|
const n = s.nodeCount;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
? 'repulsion: "grid" is not available yet (the grid tier lands in P4)'
|
|
350
|
-
: `the graph has ${n} nodes, above exactMaxNodes ${exactMaxNodes}, and the grid tier lands in P4`,
|
|
351
|
-
{
|
|
352
|
-
feature: "repulsion.grid",
|
|
353
|
-
hint: 'pass repulsion: "exact" (or raise exactMaxNodes) to run the exact tier at this size',
|
|
354
|
-
},
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
return { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) };
|
|
391
|
+
// resolve first: a throwing mass / weight resolution leaves the remembered grid of the previous load intact
|
|
392
|
+
const inputs = { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) };
|
|
393
|
+
this.nextGrid = tierFor(this.tuning, n) === "grid" ? gridSpecFor(n, resolved.dim, this.tuning) : null;
|
|
394
|
+
return inputs;
|
|
358
395
|
}
|
|
359
396
|
|
|
360
397
|
/**
|
|
@@ -380,7 +417,9 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
380
417
|
|
|
381
418
|
/**
|
|
382
419
|
* The seven module specs of an override set in dispatch order -- K1, K2, K3, K4, K5, toScene, fill -- each with
|
|
383
|
-
* only the override names its entry declares (K2 also USE_PERM / HAS_WEIGHTS), for warm() and the compile matrix
|
|
420
|
+
* only the override names its entry declares (K2 also USE_PERM / HAS_WEIGHTS), for warm() and the compile matrix,
|
|
421
|
+
* followed by the grid tier's specs (`RepulsionGrid.specs`) when the load inputs() last resolved is a grid load
|
|
422
|
+
* (the pipeline key carries no geometry, so the spec's size is immaterial).
|
|
384
423
|
* @param overrides - the merged override set (the model's plus USE_PERM / HAS_WEIGHTS)
|
|
385
424
|
* @param _subgroups - accepted for the ForceModel interface and unused: every reducing FA2 body carries
|
|
386
425
|
* needs: ["subgroups"] in its registry entry and the composer picks the twin from caps.features (contract 4.3)
|
|
@@ -388,6 +427,10 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
388
427
|
*/
|
|
389
428
|
specs(overrides: Overrides, _subgroups: boolean): readonly WgslModuleSpec[] {
|
|
390
429
|
const [repulsionSpec, speedSpec] = RepulsionExact.specs(repulsionOverrides(overrides));
|
|
430
|
+
const grid =
|
|
431
|
+
this.nextGrid === null
|
|
432
|
+
? []
|
|
433
|
+
: RepulsionGrid.specs(gridOverrides(overrides), gridSpecFor(EXACT_MAX_NODES + 1, 2, this.tuning));
|
|
391
434
|
return [
|
|
392
435
|
kernelSpec("fa2-stats-finalize"),
|
|
393
436
|
kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS)),
|
|
@@ -396,45 +439,64 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
396
439
|
kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS)),
|
|
397
440
|
kernelSpec("fa2-to-scene"),
|
|
398
441
|
kernelSpec("fill"),
|
|
442
|
+
...grid,
|
|
399
443
|
];
|
|
400
444
|
}
|
|
401
445
|
|
|
402
446
|
/**
|
|
403
|
-
* Compiles (through the cache) and binds every kernel against the buffers of this load(): K1, K2
|
|
404
|
-
* force when arcCount === 0), K3 + K4 through RepulsionExact, K5,
|
|
405
|
-
* FillParams { count: 3n, value: 0, mode: 0 } into the model's
|
|
447
|
+
* Compiles (through the cache) and binds every kernel against the buffers of this load(): K1, K2 over the degree
|
|
448
|
+
* tiers through bindAttraction (or the fill of force when arcCount === 0), K3 + K4 through RepulsionExact, K5,
|
|
449
|
+
* toScene, and the fill of oldForce; writes the FillParams { count: 3n, value: 0, mode: 0 } into the model's
|
|
450
|
+
* uniform buffer. With n === 0 nothing is bound. The K2 TIER 1 / 2 pipelines compile on the first load whose
|
|
451
|
+
* degrees need them (P4 PD-7), a one-time cost at that load.
|
|
406
452
|
* @param resources - the graph, the shared and model buffers, the ring and the cache
|
|
407
453
|
* @param overrides - the merged override set
|
|
408
454
|
*/
|
|
409
455
|
async bind(resources: ModelResources, overrides: Overrides): Promise<void> {
|
|
410
456
|
this.dropBound();
|
|
411
457
|
this.resources = resources;
|
|
412
|
-
const { n, pipelines, caps, core,
|
|
458
|
+
const { n, pipelines, caps, core, ring, device } = resources;
|
|
413
459
|
if (n === 0) {
|
|
414
460
|
return;
|
|
415
461
|
}
|
|
416
|
-
const
|
|
462
|
+
const pos = resources.buffer("positions");
|
|
463
|
+
const force = resources.buffer("force");
|
|
464
|
+
const params = ring.binding(FA2_PARAMS);
|
|
465
|
+
const hasArcs = core.colIdx !== null;
|
|
466
|
+
const [k1, k5, toScene, fill] = await Promise.all([
|
|
417
467
|
pipelines.kernel(kernelSpec("fa2-stats-finalize")),
|
|
418
|
-
pipelines.kernel(kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS))),
|
|
419
468
|
pipelines.kernel(kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS))),
|
|
420
469
|
pipelines.kernel(kernelSpec("fa2-to-scene")),
|
|
421
470
|
pipelines.kernel(kernelSpec("fill")),
|
|
422
471
|
]);
|
|
423
|
-
const repulsion =
|
|
472
|
+
const repulsion =
|
|
473
|
+
resources.tier === "grid"
|
|
474
|
+
? null
|
|
475
|
+
: await RepulsionExact.create(pipelines, caps, repulsionOverrides(overrides));
|
|
476
|
+
const attraction = hasArcs
|
|
477
|
+
? await bindAttraction(resources, subset(overrides, K2_DEFAULTS), { pos, force, params })
|
|
478
|
+
: null;
|
|
479
|
+
const grid =
|
|
480
|
+
resources.tier === "grid"
|
|
481
|
+
? await RepulsionGrid.create(
|
|
482
|
+
resources,
|
|
483
|
+
k1.workgroupSize,
|
|
484
|
+
gridOverrides(overrides),
|
|
485
|
+
gridSpecFor(n, resources.dim, this.tuning),
|
|
486
|
+
)
|
|
487
|
+
: null;
|
|
424
488
|
if (this.resources !== resources) {
|
|
425
489
|
// a newer bind() superseded this one while the pipelines compiled; its own bind groups stand
|
|
490
|
+
grid?.dispose();
|
|
426
491
|
return;
|
|
427
492
|
}
|
|
428
|
-
const pos = resources.buffer("positions");
|
|
429
493
|
const scene = resources.buffer("scenePositions");
|
|
430
494
|
const fixed = resources.buffer("fixed");
|
|
431
495
|
const partials = resources.buffer("partials");
|
|
432
496
|
const state = resources.buffer("state");
|
|
433
497
|
const trace = resources.buffer("trace");
|
|
434
|
-
const force = resources.buffer("force");
|
|
435
498
|
const oldForce = resources.buffer("oldForce");
|
|
436
499
|
const fillParamsBuffer = resources.buffer(FILL_PARAMS_BUFFER);
|
|
437
|
-
const params = ring.binding(FA2_PARAMS);
|
|
438
500
|
const fillParams: Binding = {
|
|
439
501
|
buffer: fillParamsBuffer.buffer,
|
|
440
502
|
offset: fillParamsBuffer.offset,
|
|
@@ -444,20 +506,41 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
444
506
|
const fillBytes = new ArrayBuffer(FILL_PARAMS.byteLength);
|
|
445
507
|
FILL_PARAMS.write(new DataView(fillBytes), { count: 3 * n, value: 0, mode: 0 });
|
|
446
508
|
device.queue.writeBuffer(fillParamsBuffer.buffer, fillParamsBuffer.offset, fillBytes);
|
|
447
|
-
const
|
|
448
|
-
|
|
509
|
+
const hubCounters = resources.buffer(HUB_COUNTERS_BUFFER);
|
|
510
|
+
const exact = { pos, state, trace, force, oldForce, fixedMask: fixed, partials, params };
|
|
511
|
+
repulsion?.bind(exact);
|
|
512
|
+
grid?.bind({
|
|
513
|
+
...exact,
|
|
514
|
+
cellKey: resources.buffer("cellKey"),
|
|
515
|
+
cellVal: resources.buffer("cellVal"),
|
|
516
|
+
sortedKey: resources.buffer("sortedKey"),
|
|
517
|
+
sortedIdx: resources.buffer("sortedIdx"),
|
|
518
|
+
cellHist: resources.buffer("cellHist"),
|
|
519
|
+
cellStart: resources.buffer("cellStart"),
|
|
520
|
+
hubList: resources.buffer("hubList"),
|
|
521
|
+
hubCounters,
|
|
522
|
+
hubArgs: resources.buffer("hubArgs"),
|
|
523
|
+
pyramid: resources.buffer("pyramid"),
|
|
524
|
+
});
|
|
449
525
|
const wg = k1.workgroupSize;
|
|
450
526
|
this.bound = {
|
|
451
527
|
n,
|
|
452
528
|
plan: plan1d(n, wg, caps),
|
|
453
529
|
fillPlan: plan1d(3 * n, wg, caps),
|
|
454
530
|
k1,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
:
|
|
531
|
+
// PD-14: on the exact tier K1's grid slots take dummies (cellHist := partials, both read-only; hubCounters
|
|
532
|
+
// is the model's 16-byte buffer on every tier) and the block is dead under gridMax 0
|
|
533
|
+
k1Bound: k1.bind({
|
|
534
|
+
partials,
|
|
535
|
+
S: state,
|
|
536
|
+
T: trace,
|
|
537
|
+
cellHist: grid === null ? partials : resources.buffer("cellHist"),
|
|
538
|
+
hubCounters,
|
|
539
|
+
P: params,
|
|
540
|
+
}),
|
|
541
|
+
attraction,
|
|
460
542
|
repulsion,
|
|
543
|
+
grid,
|
|
461
544
|
k5,
|
|
462
545
|
k5Bound: k5.bind({ force, oldForce, fixedMask: fixed, S: state, pos, partials, P: params }),
|
|
463
546
|
toScene,
|
|
@@ -476,14 +559,19 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
476
559
|
* @returns the uniform values
|
|
477
560
|
*/
|
|
478
561
|
paramsFor(iteration: number, options: ForceAtlas2Options): UniformValues {
|
|
479
|
-
const { n } = this.requireResources();
|
|
562
|
+
const { n, core, tiers, tier, dim } = this.requireResources();
|
|
480
563
|
const resolved = resolveForceAtlas2Options(options, this.current);
|
|
481
564
|
const { nearMax, extentFactor } = this.tuning;
|
|
565
|
+
const grid = tier === "grid" ? gridSpecFor(n, dim, this.tuning) : null;
|
|
566
|
+
// P4 PD-7: TIER 2 reads [0, hiEnd), TIER 1 [hiEnd, midEnd), TIER 0 [tierStart, tierEnd) = [midEnd, n)
|
|
567
|
+
const so = tiers?.segmentOffsets;
|
|
568
|
+
const hiEnd = so?.[1] ?? 0;
|
|
569
|
+
const midEnd = so?.[2] ?? 0;
|
|
482
570
|
return {
|
|
483
571
|
n,
|
|
484
572
|
dim: resolved.dim,
|
|
485
573
|
flags: 0,
|
|
486
|
-
tierStart:
|
|
574
|
+
tierStart: midEnd,
|
|
487
575
|
tierEnd: n,
|
|
488
576
|
iterationIndex: iteration,
|
|
489
577
|
seed: seedWord(resolved.seed),
|
|
@@ -495,34 +583,38 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
495
583
|
center: [resolved.center[0], resolved.center[1], resolved.center[2], 0],
|
|
496
584
|
settleThreshold: resolved.settleThreshold,
|
|
497
585
|
extentFactor,
|
|
498
|
-
gridMax: 0,
|
|
499
|
-
levels: 0,
|
|
500
|
-
|
|
586
|
+
gridMax: grid?.g ?? 0,
|
|
587
|
+
levels: grid?.levels ?? 0,
|
|
588
|
+
arcBase: 0,
|
|
589
|
+
arcEnd: arcCountOf(core),
|
|
590
|
+
accumulate: 0,
|
|
591
|
+
hiEnd,
|
|
592
|
+
midEnd,
|
|
501
593
|
};
|
|
502
594
|
}
|
|
503
595
|
|
|
504
596
|
/**
|
|
505
|
-
* Records one iteration into the batch
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
597
|
+
* Records one iteration into the batch, stopping after stage `upTo` when given (spec 7.4; debugRunStages /
|
|
598
|
+
* inspect, spec 11.9 item 2; PD-17: `upTo` names a position in the union list and the recording stops after the
|
|
599
|
+
* last stage recorded at or before it, so "K3" on the grid tier stops after K2 and "G5" on the exact tier after
|
|
600
|
+
* K3). The exact tier: K1, K2 (or the fill of force when arcCount === 0), K3, K4, K5 in the batch's ONE compute
|
|
601
|
+
* pass (opened by the first call of a batch and reused by every later call with the same batch.id, PLAN
|
|
602
|
+
* DECISION 2), then toScene in a second pass that ends it. The grid tier (PD-16): the passes `fa2-k1` (K1),
|
|
603
|
+
* `fa2-attraction` (K2's tiers) and `fa2-grid` (G1-G7, K4, K5) per iteration, then `fa2-to-scene`. The profiler
|
|
604
|
+
* budgets PROFILER_QUERY_SLOTS / 2 = 128 passes per batch, so a grid batch above 42 iterations is timed only in
|
|
605
|
+
* part (the exact tier's two passes per batch always fit): the simulation then reports msPerIteration from the
|
|
606
|
+
* wall time, never from the sum of the timed prefix (`ForceSimulation.batchMilliseconds`). The simulation
|
|
607
|
+
* passes "K5" for iterations 0..k-2 and undefined for the last, so toScene runs once per batch. The first call
|
|
608
|
+
* after load() zeroes oldForce before K1 (paper mode). With n === 0 nothing is recorded (PLAN DECISION 9); a
|
|
609
|
+
* call before bind() completed is E_NOT_LOADED (never a silent no-op).
|
|
512
610
|
* @param batch - the batch being recorded
|
|
513
611
|
* @param slot - the UniformRing slot holding this iteration's Fa2Params
|
|
514
|
-
* @param tier -
|
|
612
|
+
* @param tier - the tier the simulation resolved at load() (the same rule bind() applied, PD-18)
|
|
515
613
|
* @param upTo - a stage name to stop after; undefined records every stage including toScene
|
|
516
614
|
*/
|
|
517
615
|
recordIteration(batch: CommandBatch, slot: number, tier: "exact" | "grid", upTo?: string): void {
|
|
518
|
-
if (tier === "grid") {
|
|
519
|
-
throw new WebGpuGraphError("E_UNSUPPORTED", "the grid repulsion tier lands in P4", {
|
|
520
|
-
feature: "repulsion.grid",
|
|
521
|
-
hint: 'pass repulsion: "exact"',
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
616
|
const resources = this.requireResources();
|
|
525
|
-
const stop = upTo === undefined ?
|
|
617
|
+
const stop = upTo === undefined ? STAGE_TO_SCENE : this.stageIndex(upTo);
|
|
526
618
|
const { bound } = this;
|
|
527
619
|
if (bound === null) {
|
|
528
620
|
if (resources.n === 0) {
|
|
@@ -537,39 +629,118 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
537
629
|
);
|
|
538
630
|
}
|
|
539
631
|
const offset = resources.ring.offsetOf(slot);
|
|
632
|
+
if (tier === "grid") {
|
|
633
|
+
this.recordGridIteration(batch, bound, offset, stop);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const { repulsion } = bound;
|
|
637
|
+
if (repulsion === null) {
|
|
638
|
+
throw new WebGpuGraphError("E_NOT_LOADED", "the ForceAtlas2 model was bound on the grid tier", {
|
|
639
|
+
state: "loaded",
|
|
640
|
+
});
|
|
641
|
+
}
|
|
540
642
|
const pass = this.openPass !== null && this.openPass.id === batch.id ? this.openPass.pass : batch.pass("fa2");
|
|
541
643
|
this.openPass = { id: batch.id, pass };
|
|
542
|
-
|
|
543
|
-
this.resetOldForce = false;
|
|
544
|
-
if (bound.fillOldBound !== null) {
|
|
545
|
-
bound.fill.dispatch(pass, bound.fillOldBound, bound.fillPlan, [0]);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP, [offset]);
|
|
644
|
+
this.recordK1(pass, bound, offset);
|
|
549
645
|
if (stop < 1) {
|
|
550
646
|
return;
|
|
551
647
|
}
|
|
552
|
-
|
|
553
|
-
bound.k2.dispatch(pass, bound.k2Bound, bound.plan, [offset]);
|
|
554
|
-
} else if (bound.fillForceBound !== null) {
|
|
555
|
-
bound.fill.dispatch(pass, bound.fillForceBound, bound.fillPlan, [0]);
|
|
556
|
-
}
|
|
648
|
+
this.recordK2(pass, bound, offset);
|
|
557
649
|
if (stop < 2) {
|
|
558
650
|
return;
|
|
559
651
|
}
|
|
560
|
-
|
|
561
|
-
if (stop <
|
|
652
|
+
repulsion.recordRepulsion(pass, bound.n, offset);
|
|
653
|
+
if (stop < STAGE_K4) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
repulsion.recordSpeedFinalize(pass, offset);
|
|
657
|
+
if (stop < STAGE_K5) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
bound.k5.dispatch(pass, bound.k5Bound, bound.plan, [offset]);
|
|
661
|
+
if (stop < STAGE_TO_SCENE) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
this.recordToScene(batch, bound, offset);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* The grid tier's iteration (PD-16): three compute passes before toScene.
|
|
669
|
+
* @param batch - the batch being recorded
|
|
670
|
+
* @param bound - the bound model
|
|
671
|
+
* @param offset - the Fa2Params dynamic offset of the iteration
|
|
672
|
+
* @param stop - the FA2_STAGES index to stop after
|
|
673
|
+
*/
|
|
674
|
+
private recordGridIteration(batch: CommandBatch, bound: BoundModel, offset: number, stop: number): void {
|
|
675
|
+
const { grid } = bound;
|
|
676
|
+
if (grid === null) {
|
|
677
|
+
throw new WebGpuGraphError("E_NOT_LOADED", "the ForceAtlas2 model was bound on the exact tier", {
|
|
678
|
+
state: "loaded",
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
this.openPass = null;
|
|
682
|
+
this.recordK1(batch.pass("fa2-k1"), bound, offset);
|
|
683
|
+
if (stop < 1) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
this.recordK2(batch.pass("fa2-attraction"), bound, offset);
|
|
687
|
+
if (stop < STAGE_G1) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const pass = batch.pass("fa2-grid");
|
|
691
|
+
const gridStop = stop < STAGE_K4 ? (FA2_STAGES[stop] as GridStage) : undefined;
|
|
692
|
+
grid.recordRepulsion(pass, bound.n, offset, gridStop);
|
|
693
|
+
if (stop < STAGE_K4) {
|
|
562
694
|
return;
|
|
563
695
|
}
|
|
564
|
-
|
|
565
|
-
if (stop <
|
|
696
|
+
grid.recordSpeedFinalize(pass, offset);
|
|
697
|
+
if (stop < STAGE_K5) {
|
|
566
698
|
return;
|
|
567
699
|
}
|
|
568
700
|
bound.k5.dispatch(pass, bound.k5Bound, bound.plan, [offset]);
|
|
569
|
-
if (stop <
|
|
701
|
+
if (stop < STAGE_TO_SCENE) {
|
|
570
702
|
return;
|
|
571
703
|
}
|
|
572
|
-
|
|
704
|
+
this.recordToScene(batch, bound, offset);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* The oldForce reset of the first iteration after load() (paper mode), then K1 (one workgroup).
|
|
709
|
+
* @param pass - the open compute pass
|
|
710
|
+
* @param bound - the bound model
|
|
711
|
+
* @param offset - the Fa2Params dynamic offset
|
|
712
|
+
*/
|
|
713
|
+
private recordK1(pass: GPUComputePassEncoder, bound: BoundModel, offset: number): void {
|
|
714
|
+
if (this.resetOldForce) {
|
|
715
|
+
this.resetOldForce = false;
|
|
716
|
+
if (bound.fillOldBound !== null) {
|
|
717
|
+
bound.fill.dispatch(pass, bound.fillOldBound, bound.fillPlan, [0]);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP, [offset]);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* K2's tier dispatches, or the fill of force when the graph has no arcs (spec 7.5).
|
|
725
|
+
* @param pass - the open compute pass
|
|
726
|
+
* @param bound - the bound model
|
|
727
|
+
* @param offset - the Fa2Params dynamic offset
|
|
728
|
+
*/
|
|
729
|
+
private recordK2(pass: GPUComputePassEncoder, bound: BoundModel, offset: number): void {
|
|
730
|
+
if (bound.attraction !== null) {
|
|
731
|
+
recordAttraction(pass, bound.attraction, offset);
|
|
732
|
+
} else if (bound.fillForceBound !== null) {
|
|
733
|
+
bound.fill.dispatch(pass, bound.fillForceBound, bound.fillPlan, [0]);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* The toScene pass that ends the iteration's pass; the batch is complete after it, so nothing reuses the pass.
|
|
739
|
+
* @param batch - the batch
|
|
740
|
+
* @param bound - the bound model
|
|
741
|
+
* @param offset - the Fa2Params dynamic offset
|
|
742
|
+
*/
|
|
743
|
+
private recordToScene(batch: CommandBatch, bound: BoundModel, offset: number): void {
|
|
573
744
|
this.openPass = null;
|
|
574
745
|
const scenePass = batch.pass("fa2-to-scene");
|
|
575
746
|
bound.toScene.dispatch(scenePass, bound.toSceneBound, bound.plan, [offset]);
|
|
@@ -577,7 +748,9 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
577
748
|
|
|
578
749
|
/**
|
|
579
750
|
* speed = 1, speedEfficiency = 1, swing = 1, traction = 1 (mode 1 accumulates from 1; mode 0 overwrites them each
|
|
580
|
-
* iteration, the initial value is irrelevant); arms the oldForce reset of the next recordIteration.
|
|
751
|
+
* iteration, the initial value is irrelevant); arms the oldForce reset of the next recordIteration. On a grid
|
|
752
|
+
* load the frame of the first build (K1 folds nothing on the first iteration): the same six values K1 derives,
|
|
753
|
+
* in f32 with the kernel's order of operations, from the host-written min / max / centroid / rmsRadius.
|
|
581
754
|
* @param state - the state writer of the simulation
|
|
582
755
|
*/
|
|
583
756
|
onLoad(state: StateWriter): void {
|
|
@@ -586,6 +759,9 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
586
759
|
state.set("swing", 1);
|
|
587
760
|
state.set("traction", 1);
|
|
588
761
|
this.resetOldForce = true;
|
|
762
|
+
if (this.nextGrid !== null) {
|
|
763
|
+
writeGridFrame(state, this.nextGrid, this.tuning.extentFactor);
|
|
764
|
+
}
|
|
589
765
|
}
|
|
590
766
|
|
|
591
767
|
/**
|
|
@@ -623,7 +799,9 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
623
799
|
|
|
624
800
|
/**
|
|
625
801
|
* Decodes the state header and the k trace records of a completed batch (k = trace.byteLength / 32) into
|
|
626
|
-
* ForceAtlas2Stats: the
|
|
802
|
+
* ForceAtlas2Stats: `repulsionTier` is the bound tier, the grid fields are the header's on the grid tier
|
|
803
|
+
* (`maxCellOccupancy` / `outsideGrid`: the counts of the iteration before the last K1) and null on the exact
|
|
804
|
+
* tier; msPerIteration null (the simulation owns the clock).
|
|
627
805
|
* @param state - a DataView over the 256-byte state header
|
|
628
806
|
* @param trace - a DataView over the k Fa2Trace records of the batch
|
|
629
807
|
* @returns the stats
|
|
@@ -644,15 +822,16 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
644
822
|
settledCount: scalar(record, "settledCount"),
|
|
645
823
|
});
|
|
646
824
|
}
|
|
825
|
+
const grid = this.resources?.tier === "grid";
|
|
647
826
|
return {
|
|
648
827
|
iteration: scalar(header, "iteration"),
|
|
649
828
|
meanDisplacement: scalar(header, "meanDisplacement"),
|
|
650
829
|
rmsRadius: scalar(header, "rmsRadius"),
|
|
651
830
|
layoutRadius: scalar(header, "radius"),
|
|
652
831
|
centroid: [centroid[0], centroid[1], centroid[2]],
|
|
653
|
-
repulsionTier: "exact",
|
|
654
|
-
maxCellOccupancy: null,
|
|
655
|
-
outsideGrid: null,
|
|
832
|
+
repulsionTier: grid ? "grid" : "exact",
|
|
833
|
+
maxCellOccupancy: grid ? scalar(header, "maxCellOccupancy") : null,
|
|
834
|
+
outsideGrid: grid ? scalar(header, "outsideGrid") : null,
|
|
656
835
|
msPerIteration: null,
|
|
657
836
|
swing: scalar(header, "swing"),
|
|
658
837
|
traction: scalar(header, "traction"),
|
|
@@ -689,10 +868,18 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
689
868
|
throw invalid("upTo", upTo, FA2_STAGES.join(" | "));
|
|
690
869
|
}
|
|
691
870
|
|
|
871
|
+
/**
|
|
872
|
+
* Releases the grid stage's lease and the bind groups (the simulation calls it from dispose() once every
|
|
873
|
+
* in-flight batch has settled).
|
|
874
|
+
*/
|
|
875
|
+
dispose(): void {
|
|
876
|
+
this.dropBound();
|
|
877
|
+
}
|
|
878
|
+
|
|
692
879
|
/**
|
|
693
880
|
* Drops the bind groups of the previous bind() (the buffers changed) so the cached kernels do not accumulate stale
|
|
694
|
-
* groups across reloads; K3 / K4 live inside RepulsionExact and keep the P1-T6 behaviour
|
|
695
|
-
* of a batch recorded before the rebind.
|
|
881
|
+
* groups across reloads; K3 / K4 live inside RepulsionExact and keep the P1-T6 behaviour; the grid stage
|
|
882
|
+
* releases its lease. Also forgets the pass of a batch recorded before the rebind.
|
|
696
883
|
*/
|
|
697
884
|
private dropBound(): void {
|
|
698
885
|
this.openPass = null;
|
|
@@ -700,13 +887,50 @@ export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtl
|
|
|
700
887
|
if (bound === null) {
|
|
701
888
|
return;
|
|
702
889
|
}
|
|
703
|
-
for (const kernel of [bound.k1, bound.
|
|
890
|
+
for (const kernel of [bound.k1, bound.k5, bound.toScene, bound.fill]) {
|
|
891
|
+
kernel.invalidate();
|
|
892
|
+
}
|
|
893
|
+
for (const [kernel] of bound.attraction?.kernels ?? []) {
|
|
704
894
|
kernel.invalidate();
|
|
705
895
|
}
|
|
896
|
+
bound.grid?.dispose();
|
|
706
897
|
this.bound = null;
|
|
707
898
|
}
|
|
708
899
|
}
|
|
709
900
|
|
|
901
|
+
/**
|
|
902
|
+
* The grid frame of the first build after load() (spec 7.7 geometry table; PD-10): K1's text in f32 with the same
|
|
903
|
+
* order of operations -- `box = (max - min) * GRID_BBOX_MARGIN`, `extent = max(min(max(box), extentFactor *
|
|
904
|
+
* rmsRadius), GRID_EXTENT_FLOOR)`, `cellSize = extent / G`, `gridMin = centroid - extent / 2` (cellSize in `.w`),
|
|
905
|
+
* `invCellSize = 1 / cellSize`, `eps = 0.25 cellSize` -- plus zero counts. Shared with the FR and spring-electrical
|
|
906
|
+
* models' onLoad (P4-T13).
|
|
907
|
+
* @param state - the state writer (min / max / centroid / rmsRadius already written by the simulation)
|
|
908
|
+
* @param spec - the grid of the load
|
|
909
|
+
* @param extentFactor - the tuning's extent factor
|
|
910
|
+
*/
|
|
911
|
+
export function writeGridFrame(state: StateWriter, spec: GridSpec, extentFactor: number): void {
|
|
912
|
+
const f = Math.fround;
|
|
913
|
+
const axis = (name: string): readonly number[] => {
|
|
914
|
+
const value = state.get(name);
|
|
915
|
+
return typeof value === "number" ? [value, value, value] : value;
|
|
916
|
+
};
|
|
917
|
+
const min = axis("min");
|
|
918
|
+
const max = axis("max");
|
|
919
|
+
const centroid = axis("centroid");
|
|
920
|
+
const rms = state.get("rmsRadius");
|
|
921
|
+
const box = [0, 1, 2].map((a) => f(f(f(max[a]) - f(min[a])) * f(GRID_BBOX_MARGIN)));
|
|
922
|
+
const bboxExtent = spec.dim === 3 ? Math.max(box[0], box[1], box[2]) : Math.max(box[0], box[1]);
|
|
923
|
+
const rmsTerm = f(f(extentFactor) * f(typeof rms === "number" ? rms : 0));
|
|
924
|
+
const extent = Math.max(Math.min(bboxExtent, rmsTerm), f(GRID_EXTENT_FLOOR));
|
|
925
|
+
const cellSize = f(extent / spec.g);
|
|
926
|
+
const half = f(0.5 * extent);
|
|
927
|
+
state.set("gridMin", [f(f(centroid[0]) - half), f(f(centroid[1]) - half), f(f(centroid[2]) - half), cellSize]);
|
|
928
|
+
state.set("invCellSize", f(1 / cellSize));
|
|
929
|
+
state.set("eps", f(0.25 * cellSize));
|
|
930
|
+
state.set("outsideGrid", 0);
|
|
931
|
+
state.set("maxCellOccupancy", 0);
|
|
932
|
+
}
|
|
933
|
+
|
|
710
934
|
// ============================================================ the factory
|
|
711
935
|
|
|
712
936
|
/**
|
|
@@ -721,8 +945,9 @@ function resolvePatch(patch: Partial<ForceAtlas2Options>, current: ForceAtlas2Op
|
|
|
721
945
|
}
|
|
722
946
|
|
|
723
947
|
/**
|
|
724
|
-
* Spec 3.3 createForceAtlas2, verbatim: a GpuLayoutSimulation running ForceAtlas2 on the exact
|
|
725
|
-
* the option defaults of spec 7.14 and the GPU-only tuning of GpuLayoutTuning (contract 3.13
|
|
948
|
+
* Spec 3.3 createForceAtlas2, verbatim: a GpuLayoutSimulation running ForceAtlas2 on the exact or the grid repulsion
|
|
949
|
+
* tier (spec 7.8) with the option defaults of spec 7.14 and the GPU-only tuning of GpuLayoutTuning (contract 3.13
|
|
950
|
+
* "Contracts").
|
|
726
951
|
* @param ctx - the context (E_DISPOSED / E_DEVICE_LOST through assertReady)
|
|
727
952
|
* @param options - the ForceAtlas2 options and the GPU-only tuning knobs in one record
|
|
728
953
|
* @returns the simulation in state "created"; load() next
|