@graphty/webgpu-graph-algorithms 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +344 -23
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +32 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunks/context-E6iKaeuJ.js +3136 -0
- package/dist/chunks/context-E6iKaeuJ.js.map +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +131 -0
- package/dist/node.js.map +1 -0
- package/dist/src/accelerator.d.ts +26 -0
- package/dist/src/accelerator.d.ts.map +1 -0
- package/dist/src/accelerator.js +101 -0
- package/dist/src/accelerator.js.map +1 -0
- package/dist/src/algorithms/degree.d.ts +35 -0
- package/dist/src/algorithms/degree.d.ts.map +1 -0
- package/dist/src/algorithms/degree.js +119 -0
- package/dist/src/algorithms/degree.js.map +1 -0
- package/dist/src/browser/index.d.ts +23 -0
- package/dist/src/browser/index.d.ts.map +1 -0
- package/dist/src/browser/index.js +48 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/constants.d.ts +92 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +92 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/context.d.ts +84 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +304 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/device/acquire.d.ts +57 -0
- package/dist/src/device/acquire.d.ts.map +1 -0
- package/dist/src/device/acquire.js +232 -0
- package/dist/src/device/acquire.js.map +1 -0
- package/dist/src/device/caps.d.ts +43 -0
- package/dist/src/device/caps.d.ts.map +1 -0
- package/dist/src/device/caps.js +104 -0
- package/dist/src/device/caps.js.map +1 -0
- package/dist/src/device/error-scope.d.ts +75 -0
- package/dist/src/device/error-scope.d.ts.map +1 -0
- package/dist/src/device/error-scope.js +152 -0
- package/dist/src/device/error-scope.js.map +1 -0
- package/dist/src/device/lost.d.ts +51 -0
- package/dist/src/device/lost.d.ts.map +1 -0
- package/dist/src/device/lost.js +130 -0
- package/dist/src/device/lost.js.map +1 -0
- package/dist/src/device/webgpu-constants.d.ts +31 -0
- package/dist/src/device/webgpu-constants.d.ts.map +1 -0
- package/dist/src/device/webgpu-constants.js +31 -0
- package/dist/src/device/webgpu-constants.js.map +1 -0
- package/dist/src/errors.d.ts +56 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +57 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +27 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/kernel/batch.d.ts +116 -0
- package/dist/src/kernel/batch.d.ts.map +1 -0
- package/dist/src/kernel/batch.js +335 -0
- package/dist/src/kernel/batch.js.map +1 -0
- package/dist/src/kernel/dispatch.d.ts +59 -0
- package/dist/src/kernel/dispatch.d.ts.map +1 -0
- package/dist/src/kernel/dispatch.js +139 -0
- package/dist/src/kernel/dispatch.js.map +1 -0
- package/dist/src/kernel/kernel.d.ts +84 -0
- package/dist/src/kernel/kernel.d.ts.map +1 -0
- package/dist/src/kernel/kernel.js +239 -0
- package/dist/src/kernel/kernel.js.map +1 -0
- package/dist/src/kernel/pipeline-cache.d.ts +90 -0
- package/dist/src/kernel/pipeline-cache.d.ts.map +1 -0
- package/dist/src/kernel/pipeline-cache.js +251 -0
- package/dist/src/kernel/pipeline-cache.js.map +1 -0
- package/dist/src/kernel/prelude.d.ts +35 -0
- package/dist/src/kernel/prelude.d.ts.map +1 -0
- package/dist/src/kernel/prelude.js +211 -0
- package/dist/src/kernel/prelude.js.map +1 -0
- package/dist/src/kernel/profiler.d.ts +64 -0
- package/dist/src/kernel/profiler.d.ts.map +1 -0
- package/dist/src/kernel/profiler.js +120 -0
- package/dist/src/kernel/profiler.js.map +1 -0
- package/dist/src/kernel/struct-block.d.ts +122 -0
- package/dist/src/kernel/struct-block.d.ts.map +1 -0
- package/dist/src/kernel/struct-block.js +353 -0
- package/dist/src/kernel/struct-block.js.map +1 -0
- package/dist/src/kernel/uniform-ring.d.ts +70 -0
- package/dist/src/kernel/uniform-ring.d.ts.map +1 -0
- package/dist/src/kernel/uniform-ring.js +146 -0
- package/dist/src/kernel/uniform-ring.js.map +1 -0
- package/dist/src/kernel/wgsl.d.ts +88 -0
- package/dist/src/kernel/wgsl.d.ts.map +1 -0
- package/dist/src/kernel/wgsl.js +390 -0
- package/dist/src/kernel/wgsl.js.map +1 -0
- package/dist/src/kernels.d.ts +81 -0
- package/dist/src/kernels.d.ts.map +1 -0
- package/dist/src/kernels.js +417 -0
- package/dist/src/kernels.js.map +1 -0
- package/dist/src/layouts/force-simulation.d.ts +498 -0
- package/dist/src/layouts/force-simulation.d.ts.map +1 -0
- package/dist/src/layouts/force-simulation.js +1650 -0
- package/dist/src/layouts/force-simulation.js.map +1 -0
- package/dist/src/layouts/forceatlas2.d.ts +210 -0
- package/dist/src/layouts/forceatlas2.d.ts.map +1 -0
- package/dist/src/layouts/forceatlas2.js +759 -0
- package/dist/src/layouts/forceatlas2.js.map +1 -0
- package/dist/src/layouts/inputs.d.ts +40 -0
- package/dist/src/layouts/inputs.d.ts.map +1 -0
- package/dist/src/layouts/inputs.js +185 -0
- package/dist/src/layouts/inputs.js.map +1 -0
- package/dist/src/layouts/repulsion-exact.d.ts +85 -0
- package/dist/src/layouts/repulsion-exact.d.ts.map +1 -0
- package/dist/src/layouts/repulsion-exact.js +134 -0
- package/dist/src/layouts/repulsion-exact.js.map +1 -0
- package/dist/src/layouts/seed.d.ts +56 -0
- package/dist/src/layouts/seed.d.ts.map +1 -0
- package/dist/src/layouts/seed.js +173 -0
- package/dist/src/layouts/seed.js.map +1 -0
- package/dist/src/memory/buffer-pool.d.ts +73 -0
- package/dist/src/memory/buffer-pool.d.ts.map +1 -0
- package/dist/src/memory/buffer-pool.js +170 -0
- package/dist/src/memory/buffer-pool.js.map +1 -0
- package/dist/src/memory/lease.d.ts +53 -0
- package/dist/src/memory/lease.d.ts.map +1 -0
- package/dist/src/memory/lease.js +85 -0
- package/dist/src/memory/lease.js.map +1 -0
- package/dist/src/memory/readback.d.ts +143 -0
- package/dist/src/memory/readback.d.ts.map +1 -0
- package/dist/src/memory/readback.js +375 -0
- package/dist/src/memory/readback.js.map +1 -0
- package/dist/src/memory/residency.d.ts +83 -0
- package/dist/src/memory/residency.d.ts.map +1 -0
- package/dist/src/memory/residency.js +573 -0
- package/dist/src/memory/residency.js.map +1 -0
- package/dist/src/memory/upload-plan.d.ts +101 -0
- package/dist/src/memory/upload-plan.d.ts.map +1 -0
- package/dist/src/memory/upload-plan.js +265 -0
- package/dist/src/memory/upload-plan.js.map +1 -0
- package/dist/src/node/index.d.ts +64 -0
- package/dist/src/node/index.d.ts.map +1 -0
- package/dist/src/node/index.js +183 -0
- package/dist/src/node/index.js.map +1 -0
- package/dist/src/primitives/reduce.d.ts +57 -0
- package/dist/src/primitives/reduce.d.ts.map +1 -0
- package/dist/src/primitives/reduce.js +161 -0
- package/dist/src/primitives/reduce.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts +38 -0
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -0
- package/dist/src/primitives/segmented-reduce.js +211 -0
- package/dist/src/primitives/segmented-reduce.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +209 -0
- package/dist/src/types/accelerator.d.ts.map +1 -0
- package/dist/src/types/accelerator.js +8 -0
- package/dist/src/types/accelerator.js.map +1 -0
- package/dist/src/types/context.d.ts +114 -0
- package/dist/src/types/context.d.ts.map +1 -0
- package/dist/src/types/context.js +7 -0
- package/dist/src/types/context.js.map +1 -0
- package/dist/src/types/layout.d.ts +95 -0
- package/dist/src/types/layout.d.ts.map +1 -0
- package/dist/src/types/layout.js +6 -0
- package/dist/src/types/layout.js.map +1 -0
- package/dist/src/types/memory.d.ts +22 -0
- package/dist/src/types/memory.d.ts.map +1 -0
- package/dist/src/types/memory.js +7 -0
- package/dist/src/types/memory.js.map +1 -0
- package/dist/src/types/options.d.ts +78 -0
- package/dist/src/types/options.d.ts.map +1 -0
- package/dist/src/types/options.js +7 -0
- package/dist/src/types/options.js.map +1 -0
- package/dist/src/types/run.d.ts +13 -0
- package/dist/src/types/run.d.ts.map +1 -0
- package/dist/src/types/run.js +6 -0
- package/dist/src/types/run.js.map +1 -0
- package/dist/src/wgsl/degree.wgsl.d.ts +10 -0
- package/dist/src/wgsl/degree.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/degree.wgsl.js +25 -0
- package/dist/src/wgsl/degree.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +12 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.js +37 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +13 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.js +69 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +12 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +79 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts +15 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.js +54 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +14 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +57 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts +11 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.js +19 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.js.map +1 -0
- package/dist/src/wgsl/fill.wgsl.d.ts +7 -0
- package/dist/src/wgsl/fill.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fill.wgsl.js +14 -0
- package/dist/src/wgsl/fill.wgsl.js.map +1 -0
- package/dist/src/wgsl/reduce.wgsl.d.ts +10 -0
- package/dist/src/wgsl/reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/reduce.wgsl.js +63 -0
- package/dist/src/wgsl/reduce.wgsl.js.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +13 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.js +35 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/webgpu-graph-algorithms.d.ts +1 -0
- package/dist/webgpu-graph-algorithms.js +4454 -0
- package/dist/webgpu-graph-algorithms.js.map +1 -0
- package/package.json +108 -17
- package/src/accelerator.ts +117 -0
- package/src/algorithms/degree.ts +142 -0
- package/src/browser/index.ts +57 -0
- package/src/constants.ts +116 -0
- package/src/context.ts +399 -0
- package/src/device/acquire.ts +256 -0
- package/src/device/caps.ts +122 -0
- package/src/device/error-scope.ts +171 -0
- package/src/device/lost.ts +142 -0
- package/src/device/webgpu-constants.ts +44 -0
- package/src/errors.ts +94 -0
- package/src/index.ts +102 -0
- package/src/kernel/batch.ts +427 -0
- package/src/kernel/dispatch.ts +162 -0
- package/src/kernel/kernel.ts +311 -0
- package/src/kernel/pipeline-cache.ts +288 -0
- package/src/kernel/prelude.ts +229 -0
- package/src/kernel/profiler.ts +148 -0
- package/src/kernel/struct-block.ts +439 -0
- package/src/kernel/uniform-ring.ts +184 -0
- package/src/kernel/wgsl.ts +490 -0
- package/src/kernels.ts +511 -0
- package/src/layouts/force-simulation.ts +2111 -0
- package/src/layouts/forceatlas2.ts +942 -0
- package/src/layouts/inputs.ts +252 -0
- package/src/layouts/repulsion-exact.ts +183 -0
- package/src/layouts/seed.ts +198 -0
- package/src/memory/buffer-pool.ts +204 -0
- package/src/memory/lease.ts +93 -0
- package/src/memory/readback.ts +429 -0
- package/src/memory/residency.ts +753 -0
- package/src/memory/upload-plan.ts +350 -0
- package/src/node/index.ts +230 -0
- package/src/primitives/reduce.ts +233 -0
- package/src/primitives/segmented-reduce.ts +270 -0
- package/src/types/accelerator.ts +236 -0
- package/src/types/context.ts +135 -0
- package/src/types/layout.ts +103 -0
- package/src/types/memory.ts +23 -0
- package/src/types/options.ts +84 -0
- package/src/types/run.ts +13 -0
- package/src/wgsl/degree.wgsl.ts +24 -0
- package/src/wgsl/fa2-attraction.wgsl.ts +37 -0
- package/src/wgsl/fa2-integrate.wgsl.ts +69 -0
- package/src/wgsl/fa2-repulsion-exact.wgsl.ts +78 -0
- package/src/wgsl/fa2-speed-finalize.wgsl.ts +53 -0
- package/src/wgsl/fa2-stats-finalize.wgsl.ts +57 -0
- package/src/wgsl/fa2-to-scene.wgsl.ts +19 -0
- package/src/wgsl/fill.wgsl.ts +13 -0
- package/src/wgsl/reduce.wgsl.ts +62 -0
- package/src/wgsl/segmented-reduce.wgsl.ts +35 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ForceAtlas2 on the exact repulsion tier (spec 7.1-7.18; contract 3.13): the ForceModel that ForceSimulation
|
|
3
|
+
* drives -- the K1 K2 K3 K4 K5 sequence per iteration and toScene per batch (spec 7.4), the override set of the option
|
|
4
|
+
* record (spec 7.2 with the 4.6 NetworkX corrections), the per-iteration Fa2Params values, the controller resets of
|
|
5
|
+
* spec 7.17 and the stats decoder -- plus the two option resolvers and `createForceAtlas2`. Positions are vec4f
|
|
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 thread-per-row tier over [0, n) with USE_PERM false (P3, spec 7.5).
|
|
8
|
+
*
|
|
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 pass (opened by the
|
|
11
|
+
* batch's first recordIteration and remembered by batch id) and toScene runs in a second pass that ends it (contract
|
|
12
|
+
* 4.4); the fill kernel takes its FillParams from a model-owned 256-byte uniform buffer ("fillParams"); the first
|
|
13
|
+
* iteration after every load() zeroes oldForce with a fill (paper mode); `repulsion: "grid"` is E_UNSUPPORTED at
|
|
14
|
+
* load() for any n and `"auto"` above exactMaxNodes.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type GraphSnapshot } from "@graphty/graph-format";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
FA2_DEFAULTS,
|
|
21
|
+
LAYOUT_TUNING_DEFAULTS,
|
|
22
|
+
MAX_ITERATIONS_PER_STEP,
|
|
23
|
+
TRACE_RECORD_BYTES,
|
|
24
|
+
UNIFORM_SLOT_BYTES,
|
|
25
|
+
} from "../constants.js";
|
|
26
|
+
import { type GpuContext } from "../context.js";
|
|
27
|
+
import { BufferUsage } from "../device/webgpu-constants.js";
|
|
28
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
29
|
+
import { type CommandBatch } from "../kernel/batch.js";
|
|
30
|
+
import { type DispatchPlan, plan1d } from "../kernel/dispatch.js";
|
|
31
|
+
import { type BoundKernel, type Kernel } from "../kernel/kernel.js";
|
|
32
|
+
import { type UniformBlock, type UniformValues } from "../kernel/struct-block.js";
|
|
33
|
+
import { type WgslModuleSpec } from "../kernel/wgsl.js";
|
|
34
|
+
import { FA2_PARAMS, FA2_STATE, FA2_TRACE, FILL_PARAMS, graphBindings, kernelSpec } from "../kernels.js";
|
|
35
|
+
import {
|
|
36
|
+
type ForceAtlas2Stats,
|
|
37
|
+
type ForceAtlas2TraceRecord,
|
|
38
|
+
type GpuLayoutSimulation,
|
|
39
|
+
type GpuLayoutTuning,
|
|
40
|
+
type ResolvedLayoutTuning,
|
|
41
|
+
} from "../types/layout.js";
|
|
42
|
+
import { type Binding } from "../types/memory.js";
|
|
43
|
+
import { type ForceAtlas2Options, type ResolvedForceAtlas2Options } from "../types/options.js";
|
|
44
|
+
import {
|
|
45
|
+
type BufferSpec,
|
|
46
|
+
type ForceModel,
|
|
47
|
+
ForceSimulation,
|
|
48
|
+
type ModelInputs,
|
|
49
|
+
type ModelResources,
|
|
50
|
+
type StateWriter,
|
|
51
|
+
} from "./force-simulation.js";
|
|
52
|
+
import { resolveNodeMass, resolveWeights } from "./inputs.js";
|
|
53
|
+
import { RepulsionExact, type RepulsionExactOverrides } from "./repulsion-exact.js";
|
|
54
|
+
|
|
55
|
+
// ============================================================ constants and small helpers
|
|
56
|
+
|
|
57
|
+
/** An override record as the kernel layer takes it. */
|
|
58
|
+
type Overrides = Readonly<Record<string, number | boolean>>;
|
|
59
|
+
|
|
60
|
+
/** The stage names of one iteration in dispatch order plus the per-batch toScene (spec 7.4; contract 3.13). */
|
|
61
|
+
const FA2_STAGES = ["K1", "K2", "K3", "K4", "K5", "toScene"] as const;
|
|
62
|
+
|
|
63
|
+
/** Bytes of the stride-3 f32 force arrays per node. */
|
|
64
|
+
const FORCE_BYTES_PER_NODE = 12;
|
|
65
|
+
|
|
66
|
+
/** The name of the model-owned FillParams buffer (a BufferSpec, reached through ModelResources.buffer). */
|
|
67
|
+
const FILL_PARAMS_BUFFER = "fillParams";
|
|
68
|
+
|
|
69
|
+
/** The one-workgroup dispatch of K1 (spec 7.4). */
|
|
70
|
+
const ONE_WORKGROUP: DispatchPlan = { x: 1, y: 1, z: 1, items: 1, stride: null };
|
|
71
|
+
|
|
72
|
+
/** Every override K2 accepts, with its default (contract 3.10.1 plus the two standard graph overrides). */
|
|
73
|
+
const K2_DEFAULTS: Overrides = { LINLOG: false, DISTRIBUTED: false, TIER: 0, USE_PERM: false, HAS_WEIGHTS: false };
|
|
74
|
+
|
|
75
|
+
/** Every override K5 accepts, with its default. */
|
|
76
|
+
const K5_DEFAULTS: Overrides = { SWING_MODE: 0 };
|
|
77
|
+
|
|
78
|
+
/** 2^32, the modulus of the u32 seed word (computed with `%`, never a bitwise operator). */
|
|
79
|
+
const U32_MODULUS = 4294967296;
|
|
80
|
+
|
|
81
|
+
/** The resolved record with no option given: FA2_DEFAULTS plus the null / origin defaults of spec 7.14. */
|
|
82
|
+
const DEFAULT_RESOLVED: ResolvedForceAtlas2Options = Object.freeze<ResolvedForceAtlas2Options>({
|
|
83
|
+
...FA2_DEFAULTS,
|
|
84
|
+
nodeMass: null,
|
|
85
|
+
nodeSize: null,
|
|
86
|
+
weight: null,
|
|
87
|
+
center: [0, 0, 0],
|
|
88
|
+
seed: null,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A short, safe rendering of an argument value for error messages (never String() on an object).
|
|
93
|
+
* @param value - the value
|
|
94
|
+
* @returns the rendering
|
|
95
|
+
*/
|
|
96
|
+
function describeValue(value: unknown): string {
|
|
97
|
+
if (value === null) {
|
|
98
|
+
return "null";
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
|
|
101
|
+
return String(value);
|
|
102
|
+
}
|
|
103
|
+
if (typeof value === "undefined") {
|
|
104
|
+
return "undefined";
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === "object" && "length" in value && typeof value.length === "number") {
|
|
107
|
+
return `[${value.length} values]`;
|
|
108
|
+
}
|
|
109
|
+
return typeof value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The E_INVALID_ARGUMENT error of an option check (contract 3.1: { argument, value, expected }).
|
|
114
|
+
* @param argument - the option name
|
|
115
|
+
* @param value - the value given
|
|
116
|
+
* @param expected - what was expected
|
|
117
|
+
* @returns the error (not thrown here)
|
|
118
|
+
*/
|
|
119
|
+
function invalid(argument: string, value: unknown, expected: string): WebGpuGraphError {
|
|
120
|
+
return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument} must be ${expected}; got ${describeValue(value)}`, {
|
|
121
|
+
argument,
|
|
122
|
+
value,
|
|
123
|
+
expected,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A numeric option: the given value when defined, else the fallback; validated by `check` (the value is checked as
|
|
129
|
+
* `unknown` so a JS caller's string or object is E_INVALID_ARGUMENT too).
|
|
130
|
+
* @param name - the option name
|
|
131
|
+
* @param given - the value given (undefined = absent)
|
|
132
|
+
* @param fallback - the previous record's value or the default
|
|
133
|
+
* @param check - the range predicate over a finite number
|
|
134
|
+
* @param expected - the range in words (the error message)
|
|
135
|
+
* @returns the value
|
|
136
|
+
*/
|
|
137
|
+
function pickNumber(
|
|
138
|
+
name: string,
|
|
139
|
+
given: number | undefined,
|
|
140
|
+
fallback: number,
|
|
141
|
+
check: (value: number) => boolean,
|
|
142
|
+
expected: string,
|
|
143
|
+
): number {
|
|
144
|
+
const value: unknown = given === undefined ? fallback : given;
|
|
145
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !check(value)) {
|
|
146
|
+
throw invalid(name, value, expected);
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A boolean option: the given value when defined, else the fallback; a non-boolean is E_INVALID_ARGUMENT.
|
|
153
|
+
* @param name - the option name
|
|
154
|
+
* @param given - the value given (undefined = absent)
|
|
155
|
+
* @param fallback - the previous record's value or the default
|
|
156
|
+
* @returns the value
|
|
157
|
+
*/
|
|
158
|
+
function pickBoolean(name: string, given: boolean | undefined, fallback: boolean): boolean {
|
|
159
|
+
const value: unknown = given === undefined ? fallback : given;
|
|
160
|
+
if (typeof value !== "boolean") {
|
|
161
|
+
throw invalid(name, value, "a boolean");
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The layout dimension: 2 or 3.
|
|
168
|
+
* @param given - the value given (undefined = absent)
|
|
169
|
+
* @param fallback - the previous record's value or the default
|
|
170
|
+
* @returns 2 or 3
|
|
171
|
+
*/
|
|
172
|
+
function pickDim(given: 2 | 3 | undefined, fallback: 2 | 3): 2 | 3 {
|
|
173
|
+
const value: unknown = given === undefined ? fallback : given;
|
|
174
|
+
if (value !== 2 && value !== 3) {
|
|
175
|
+
throw invalid("dim", value, "2 or 3");
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The scene-unit center: an array-like of 2 (z = 0) or 3 finite numbers.
|
|
182
|
+
* @param given - the value given (undefined = absent)
|
|
183
|
+
* @param fallback - the previous record's value or the default
|
|
184
|
+
* @returns the three components
|
|
185
|
+
*/
|
|
186
|
+
function pickCenter(
|
|
187
|
+
given: ArrayLike<number> | undefined,
|
|
188
|
+
fallback: readonly [number, number, number],
|
|
189
|
+
): readonly [number, number, number] {
|
|
190
|
+
if (given === undefined) {
|
|
191
|
+
return fallback;
|
|
192
|
+
}
|
|
193
|
+
const expected = "an array of 2 or 3 finite numbers";
|
|
194
|
+
const value: unknown = given;
|
|
195
|
+
if (typeof value !== "object" || value === null || !("length" in value)) {
|
|
196
|
+
throw invalid("center", given, expected);
|
|
197
|
+
}
|
|
198
|
+
const { length } = value;
|
|
199
|
+
if (length !== 2 && length !== 3) {
|
|
200
|
+
throw invalid("center", given, expected);
|
|
201
|
+
}
|
|
202
|
+
const x: unknown = given[0];
|
|
203
|
+
const y: unknown = given[1];
|
|
204
|
+
const z: unknown = length === 3 ? given[2] : 0;
|
|
205
|
+
if (
|
|
206
|
+
typeof x !== "number" ||
|
|
207
|
+
typeof y !== "number" ||
|
|
208
|
+
typeof z !== "number" ||
|
|
209
|
+
!Number.isFinite(x) ||
|
|
210
|
+
!Number.isFinite(y) ||
|
|
211
|
+
!Number.isFinite(z)
|
|
212
|
+
) {
|
|
213
|
+
throw invalid("center", given, expected);
|
|
214
|
+
}
|
|
215
|
+
return [x, y, z];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The seed: a finite number, or null (unseeded; 0 keeps the port's "0 = unseeded" quirk through the Lcg).
|
|
220
|
+
* @param given - the value given (undefined = absent)
|
|
221
|
+
* @param fallback - the previous record's value or the default
|
|
222
|
+
* @returns the seed or null
|
|
223
|
+
*/
|
|
224
|
+
function pickSeed(given: number | null | undefined, fallback: number | null): number | null {
|
|
225
|
+
if (given === undefined) {
|
|
226
|
+
return fallback;
|
|
227
|
+
}
|
|
228
|
+
const value: unknown = given;
|
|
229
|
+
if (value !== null && (typeof value !== "number" || !Number.isFinite(value))) {
|
|
230
|
+
throw invalid("seed", given, "a finite number or null");
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Integer >= 1.
|
|
237
|
+
* @param value - a finite number
|
|
238
|
+
* @returns whether it is a positive integer
|
|
239
|
+
*/
|
|
240
|
+
function isPositiveInteger(value: number): boolean {
|
|
241
|
+
return Number.isInteger(value) && value >= 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The u32 word written into Fa2Params.seed: 0 for null, else floor(|seed|) mod 2^32.
|
|
246
|
+
* @param seed - the resolved seed
|
|
247
|
+
* @returns the u32 value
|
|
248
|
+
*/
|
|
249
|
+
function seedWord(seed: number | null): number {
|
|
250
|
+
if (seed === null) {
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
return Math.floor(Math.abs(seed)) % U32_MODULUS;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A scalar field of a block's read() result.
|
|
258
|
+
* @param values - the values read
|
|
259
|
+
* @param name - the field name
|
|
260
|
+
* @returns the number
|
|
261
|
+
*/
|
|
262
|
+
function scalar(values: UniformValues, name: string): number {
|
|
263
|
+
const value = values[name];
|
|
264
|
+
if (typeof value !== "number") {
|
|
265
|
+
throw invalid(name, value, "a scalar field");
|
|
266
|
+
}
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A vector field of a block's read() result.
|
|
272
|
+
* @param values - the values read
|
|
273
|
+
* @param name - the field name
|
|
274
|
+
* @returns the components
|
|
275
|
+
*/
|
|
276
|
+
function vector(values: UniformValues, name: string): readonly number[] {
|
|
277
|
+
const value = values[name];
|
|
278
|
+
if (typeof value === "number") {
|
|
279
|
+
throw invalid(name, value, "a vector field");
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The override record a kernel gets: its defaults overlaid with the values present in the merged set (contract 3.9:
|
|
286
|
+
* a name a spec does not declare is rejected at compose time, so nothing else is passed through).
|
|
287
|
+
* @param merged - the merged override set of the model (plus USE_PERM / HAS_WEIGHTS from the simulation)
|
|
288
|
+
* @param defaults - the kernel's accepted names with their defaults
|
|
289
|
+
* @returns the kernel's override record, every accepted name explicit
|
|
290
|
+
*/
|
|
291
|
+
function subset(merged: Overrides, defaults: Overrides): Overrides {
|
|
292
|
+
const out: Record<string, number | boolean> = {};
|
|
293
|
+
for (const name of Object.keys(defaults)) {
|
|
294
|
+
out[name] = name in merged ? merged[name] : defaults[name];
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The K3 / K4 override values of a merged set (typed for RepulsionExact).
|
|
301
|
+
* @param merged - the merged override set
|
|
302
|
+
* @returns the three K3 / K4 overrides
|
|
303
|
+
*/
|
|
304
|
+
function repulsionOverrides(merged: Overrides): RepulsionExactOverrides {
|
|
305
|
+
return {
|
|
306
|
+
SWING_MODE: merged.SWING_MODE === 1 ? 1 : 0,
|
|
307
|
+
STRONG_GRAVITY: merged.STRONG_GRAVITY === true,
|
|
308
|
+
GRAVITY_CENTER: merged.GRAVITY_CENTER === 1 ? 1 : 0,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ============================================================ the resolvers
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Applies FA2_DEFAULTS to the option record; validates ranges (spec 7.14; contract 3.13). With `previous` the record
|
|
316
|
+
* is a PATCH over it (an absent or explicitly undefined field keeps the previous value) and `maxInFlight` may not
|
|
317
|
+
* change (the uniform ring is sized by it at construction). `nodeSize` is E_UNSUPPORTED { option: "nodeSize" } (the
|
|
318
|
+
* adjustSizes correction is deferred); `dissuadeHubs` is kept and ignored, exactly like the CPU.
|
|
319
|
+
* @param options - the caller's options (or a setParams patch)
|
|
320
|
+
* @param previous - the current resolved record when resolving a patch
|
|
321
|
+
* @returns the frozen resolved record
|
|
322
|
+
*/
|
|
323
|
+
export function resolveForceAtlas2Options(
|
|
324
|
+
options: ForceAtlas2Options | undefined,
|
|
325
|
+
previous?: ResolvedForceAtlas2Options,
|
|
326
|
+
): ResolvedForceAtlas2Options {
|
|
327
|
+
const o: ForceAtlas2Options = options ?? {};
|
|
328
|
+
const base = previous ?? DEFAULT_RESOLVED;
|
|
329
|
+
if (previous !== undefined && o.maxInFlight !== undefined && o.maxInFlight !== previous.maxInFlight) {
|
|
330
|
+
throw new WebGpuGraphError(
|
|
331
|
+
"E_INVALID_ARGUMENT",
|
|
332
|
+
`maxInFlight cannot change after creation (the uniform ring is sized by it): got ${describeValue(o.maxInFlight)}, current ${previous.maxInFlight}`,
|
|
333
|
+
{ argument: "maxInFlight", value: o.maxInFlight, expected: previous.maxInFlight },
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const nodeSize = o.nodeSize === undefined ? base.nodeSize : o.nodeSize;
|
|
337
|
+
if (nodeSize !== null) {
|
|
338
|
+
throw new WebGpuGraphError(
|
|
339
|
+
"E_UNSUPPORTED",
|
|
340
|
+
"nodeSize (the adjustSizes correction) is not supported by the GPU ForceAtlas2 yet (spec 7.14)",
|
|
341
|
+
{ option: "nodeSize", hint: "pass nodeSize: null; the size-aware repulsion is deferred (spec 7.2, Q-25)" },
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const resolved: ResolvedForceAtlas2Options = {
|
|
345
|
+
maxIter: pickNumber("maxIter", o.maxIter, base.maxIter, isPositiveInteger, "an integer >= 1"),
|
|
346
|
+
jitterTolerance: pickNumber("jitterTolerance", o.jitterTolerance, base.jitterTolerance, (v) => v > 0, "> 0"),
|
|
347
|
+
scalingRatio: pickNumber("scalingRatio", o.scalingRatio, base.scalingRatio, (v) => v > 0, "> 0"),
|
|
348
|
+
gravity: pickNumber("gravity", o.gravity, base.gravity, (v) => v >= 0, ">= 0"),
|
|
349
|
+
strongGravity: pickBoolean("strongGravity", o.strongGravity, base.strongGravity),
|
|
350
|
+
distributedAction: pickBoolean("distributedAction", o.distributedAction, base.distributedAction),
|
|
351
|
+
linlog: pickBoolean("linlog", o.linlog, base.linlog),
|
|
352
|
+
nodeMass: o.nodeMass === undefined ? base.nodeMass : o.nodeMass,
|
|
353
|
+
nodeSize,
|
|
354
|
+
weight: o.weight === undefined ? base.weight : o.weight,
|
|
355
|
+
dissuadeHubs: pickBoolean("dissuadeHubs", o.dissuadeHubs, base.dissuadeHubs),
|
|
356
|
+
dim: pickDim(o.dim, base.dim),
|
|
357
|
+
scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
|
|
358
|
+
center: pickCenter(o.center, base.center),
|
|
359
|
+
seed: pickSeed(o.seed, base.seed),
|
|
360
|
+
settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
|
|
361
|
+
settleWindow: pickNumber(
|
|
362
|
+
"settleWindow",
|
|
363
|
+
o.settleWindow,
|
|
364
|
+
base.settleWindow,
|
|
365
|
+
isPositiveInteger,
|
|
366
|
+
"an integer >= 1",
|
|
367
|
+
),
|
|
368
|
+
iterationsPerStep: pickNumber(
|
|
369
|
+
"iterationsPerStep",
|
|
370
|
+
o.iterationsPerStep,
|
|
371
|
+
base.iterationsPerStep,
|
|
372
|
+
(v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
|
|
373
|
+
`an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`,
|
|
374
|
+
),
|
|
375
|
+
maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1"),
|
|
376
|
+
};
|
|
377
|
+
return Object.freeze(resolved);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Applies LAYOUT_TUNING_DEFAULTS (spec 7.14); the grid knobs are validated and stored but only `repulsion`,
|
|
382
|
+
* `exactMaxNodes`, `deterministic` and `compat` have an effect in P3 (contract 3.3).
|
|
383
|
+
* @param tuning - the GPU-only knobs given (any object carrying them, e.g. the createForceAtlas2 options)
|
|
384
|
+
* @returns the frozen resolved tuning
|
|
385
|
+
*/
|
|
386
|
+
export function resolveLayoutTuning(tuning: GpuLayoutTuning | undefined): ResolvedLayoutTuning {
|
|
387
|
+
const t: GpuLayoutTuning = tuning ?? {};
|
|
388
|
+
const repulsion: unknown = t.repulsion ?? LAYOUT_TUNING_DEFAULTS.repulsion;
|
|
389
|
+
if (repulsion !== "exact" && repulsion !== "grid" && repulsion !== "auto") {
|
|
390
|
+
throw invalid("repulsion", repulsion, '"exact", "grid" or "auto"');
|
|
391
|
+
}
|
|
392
|
+
const compat: unknown = t.compat ?? LAYOUT_TUNING_DEFAULTS.compat;
|
|
393
|
+
if (compat !== "paper" && compat !== "networkx") {
|
|
394
|
+
throw invalid("compat", compat, '"paper" or "networkx"');
|
|
395
|
+
}
|
|
396
|
+
const resolved: ResolvedLayoutTuning = {
|
|
397
|
+
repulsion,
|
|
398
|
+
exactMaxNodes: pickNumber(
|
|
399
|
+
"exactMaxNodes",
|
|
400
|
+
t.exactMaxNodes,
|
|
401
|
+
LAYOUT_TUNING_DEFAULTS.exactMaxNodes,
|
|
402
|
+
isPositiveInteger,
|
|
403
|
+
"an integer >= 1",
|
|
404
|
+
),
|
|
405
|
+
nearMax: pickNumber("nearMax", t.nearMax, LAYOUT_TUNING_DEFAULTS.nearMax, isPositiveInteger, "an integer >= 1"),
|
|
406
|
+
deterministic: pickBoolean("deterministic", t.deterministic, LAYOUT_TUNING_DEFAULTS.deterministic),
|
|
407
|
+
gridMax2D: pickNumber(
|
|
408
|
+
"gridMax2D",
|
|
409
|
+
t.gridMax2D,
|
|
410
|
+
LAYOUT_TUNING_DEFAULTS.gridMax2D,
|
|
411
|
+
isPositiveInteger,
|
|
412
|
+
"an integer >= 1",
|
|
413
|
+
),
|
|
414
|
+
gridMax3D: pickNumber(
|
|
415
|
+
"gridMax3D",
|
|
416
|
+
t.gridMax3D,
|
|
417
|
+
LAYOUT_TUNING_DEFAULTS.gridMax3D,
|
|
418
|
+
isPositiveInteger,
|
|
419
|
+
"an integer >= 1",
|
|
420
|
+
),
|
|
421
|
+
extentFactor: pickNumber(
|
|
422
|
+
"extentFactor",
|
|
423
|
+
t.extentFactor,
|
|
424
|
+
LAYOUT_TUNING_DEFAULTS.extentFactor,
|
|
425
|
+
(v) => v > 0,
|
|
426
|
+
"> 0",
|
|
427
|
+
),
|
|
428
|
+
compat,
|
|
429
|
+
};
|
|
430
|
+
return Object.freeze(resolved);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ============================================================ the model
|
|
434
|
+
|
|
435
|
+
/** Everything bind() produced for one load(): the kernels, their bind groups and the dispatch plans of this n. */
|
|
436
|
+
interface BoundModel {
|
|
437
|
+
readonly n: number;
|
|
438
|
+
/** plan1d(n): K2, K5, toScene (every kernel compiles with the same device-derived WG). */
|
|
439
|
+
readonly plan: DispatchPlan;
|
|
440
|
+
/** plan1d(3n): the fills of force / oldForce (3 words per node). */
|
|
441
|
+
readonly fillPlan: DispatchPlan;
|
|
442
|
+
readonly k1: Kernel;
|
|
443
|
+
readonly k1Bound: BoundKernel;
|
|
444
|
+
readonly k2: Kernel;
|
|
445
|
+
/** null when arcCount === 0 (K2 is not recorded; the fill below zeroes force instead, spec 7.5). */
|
|
446
|
+
readonly k2Bound: BoundKernel | null;
|
|
447
|
+
readonly repulsion: RepulsionExact;
|
|
448
|
+
readonly k5: Kernel;
|
|
449
|
+
readonly k5Bound: BoundKernel;
|
|
450
|
+
readonly toScene: Kernel;
|
|
451
|
+
readonly toSceneBound: BoundKernel;
|
|
452
|
+
readonly fill: Kernel;
|
|
453
|
+
/** The fill of `force` (arcCount === 0 only). */
|
|
454
|
+
readonly fillForceBound: BoundKernel | null;
|
|
455
|
+
/** The fill of `oldForce` on the first iteration after load() (paper mode only). */
|
|
456
|
+
readonly fillOldBound: BoundKernel | null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** The ForceAtlas2 model (spec 7.4: K1 K2 K3 K4 K5 per iteration; toScene once per batch). Stages: ["K1", "K2", "K3", "K4", "K5", "toScene"]. */
|
|
460
|
+
export class ForceAtlas2Model implements ForceModel<ForceAtlas2Options, ForceAtlas2Stats> {
|
|
461
|
+
/** The model kind of spec 7.19. */
|
|
462
|
+
readonly kind = "forceatlas2";
|
|
463
|
+
/** The stage names in dispatch order (the `upTo` vocabulary of recordIteration and debugRunStages). */
|
|
464
|
+
readonly stages: readonly ["K1", "K2", "K3", "K4", "K5", "toScene"] = FA2_STAGES;
|
|
465
|
+
/** Fa2Params: the per-iteration uniform block (the simulation writes the shared fields into it). */
|
|
466
|
+
readonly params: UniformBlock = FA2_PARAMS;
|
|
467
|
+
/** Fa2State: the state header block (the simulation allocates and initialises it through this layout). */
|
|
468
|
+
readonly state: UniformBlock = FA2_STATE;
|
|
469
|
+
/** Fa2Trace: one record per iteration of a batch. */
|
|
470
|
+
readonly trace: UniformBlock = FA2_TRACE;
|
|
471
|
+
/** The resolved GPU-only tuning this model was created with. */
|
|
472
|
+
readonly tuning: ResolvedLayoutTuning;
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The option record the model holds: the constructor's record, replaced by onSetParams() ONLY. The query hooks
|
|
476
|
+
* (inputs, overrides, paramsFor) never assign it: the simulation calls overrides(next) BEFORE onSetParams(patch)
|
|
477
|
+
* (3.13 setParams: the recompile decision precedes the controller reset), so a hook that tracked the record would
|
|
478
|
+
* hide every law change from onSetParams (PLAN DECISION 12).
|
|
479
|
+
*/
|
|
480
|
+
private current: ResolvedForceAtlas2Options;
|
|
481
|
+
/** The resources of the last bind(), or null before the first. */
|
|
482
|
+
private resources: ModelResources | null = null;
|
|
483
|
+
/** The kernels and bind groups of the last bind(), or null before it (and for n === 0). */
|
|
484
|
+
private bound: BoundModel | null = null;
|
|
485
|
+
/** Armed by onLoad(): the next recordIteration zeroes oldForce first (paper mode). */
|
|
486
|
+
private resetOldForce = false;
|
|
487
|
+
/**
|
|
488
|
+
* The K1-K5 compute pass of the batch being recorded, keyed by CommandBatch.id (unique per batch): every
|
|
489
|
+
* recordIteration of one batch dispatches into it (ONE pass per batch, contract 4.4); null between batches and
|
|
490
|
+
* after the toScene pass ended it (PLAN DECISION 2).
|
|
491
|
+
*/
|
|
492
|
+
private openPass: { readonly id: number; readonly pass: GPUComputePassEncoder } | null = null;
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Creates the model for one simulation.
|
|
496
|
+
* @param tuning - the resolved GPU-only tuning (compat selects SWING_MODE / GRAVITY_CENTER; repulsion and
|
|
497
|
+
* exactMaxNodes the tier rule)
|
|
498
|
+
* @param resolved - the resolved option record at creation
|
|
499
|
+
*/
|
|
500
|
+
constructor(tuning: ResolvedLayoutTuning, resolved: ResolvedForceAtlas2Options) {
|
|
501
|
+
this.tuning = tuning;
|
|
502
|
+
this.current = resolved;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The SWING_MODE of this model: 1 in networkx mode (accumulated, position-mixed sums; m|F| local swing), else 0.
|
|
507
|
+
* @returns 0 or 1
|
|
508
|
+
*/
|
|
509
|
+
private get swingMode(): 0 | 1 {
|
|
510
|
+
return this.tuning.compat === "networkx" ? 1 : 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* force 12n and oldForce 12n (zeroed) in BOTH swing modes (3.10.1: a writable slot is never aliased; mode 1 leaves
|
|
515
|
+
* oldForce unread and unwritten), plus the 256-byte FillParams uniform buffer the fill dispatches read. n = 0
|
|
516
|
+
* reports one node's worth of bytes so no zero-length buffer is ever created (spec 3.6).
|
|
517
|
+
* @param n - the node count
|
|
518
|
+
* @param _dim - the layout dimension (the force arrays are stride 3 in both)
|
|
519
|
+
* @returns the three model-owned buffer specs
|
|
520
|
+
*/
|
|
521
|
+
buffers(n: number, _dim: 2 | 3): readonly BufferSpec[] {
|
|
522
|
+
const bytes = Math.max(1, n) * FORCE_BYTES_PER_NODE;
|
|
523
|
+
const usage = BufferUsage.STORAGE | BufferUsage.COPY_SRC | BufferUsage.COPY_DST;
|
|
524
|
+
return [
|
|
525
|
+
{ name: "force", byteLength: bytes, usage, zero: true },
|
|
526
|
+
{ name: "oldForce", byteLength: bytes, usage, zero: true },
|
|
527
|
+
{
|
|
528
|
+
name: FILL_PARAMS_BUFFER,
|
|
529
|
+
byteLength: UNIFORM_SLOT_BYTES,
|
|
530
|
+
usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
|
|
531
|
+
zero: false,
|
|
532
|
+
},
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) } (3.13 inputs.ts),
|
|
538
|
+
* after the tier rule of spec 7.8 / lead f: `repulsion: "grid"` is E_UNSUPPORTED { feature: "repulsion.grid" }
|
|
539
|
+
* for any n and `"auto"` when n > exactMaxNodes (the grid tier lands in P4); `"exact"` always runs.
|
|
540
|
+
* @param s - the snapshot being loaded
|
|
541
|
+
* @param options - the simulation's current option record
|
|
542
|
+
* @returns the per-load inputs
|
|
543
|
+
*/
|
|
544
|
+
inputs(s: GraphSnapshot, options: ForceAtlas2Options): ModelInputs {
|
|
545
|
+
const resolved = resolveForceAtlas2Options(options, this.current);
|
|
546
|
+
const { repulsion, exactMaxNodes } = this.tuning;
|
|
547
|
+
const n = s.nodeCount;
|
|
548
|
+
if (repulsion === "grid" || (repulsion === "auto" && n > exactMaxNodes)) {
|
|
549
|
+
throw new WebGpuGraphError(
|
|
550
|
+
"E_UNSUPPORTED",
|
|
551
|
+
repulsion === "grid"
|
|
552
|
+
? 'repulsion: "grid" is not available yet (the grid tier lands in P4)'
|
|
553
|
+
: `the graph has ${n} nodes, above exactMaxNodes ${exactMaxNodes}, and the grid tier lands in P4`,
|
|
554
|
+
{
|
|
555
|
+
feature: "repulsion.grid",
|
|
556
|
+
hint: 'pass repulsion: "exact" (or raise exactMaxNodes) to run the exact tier at this size',
|
|
557
|
+
},
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* { LINLOG, DISTRIBUTED, TIER: 0, SWING_MODE: compat === "networkx" ? 1 : 0, STRONG_GRAVITY, GRAVITY_CENTER:
|
|
565
|
+
* compat === "networkx" ? 1 : 0 }; USE_PERM / HAS_WEIGHTS are merged in by the simulation from ModelResources
|
|
566
|
+
* (3.10, never from core.hasWeights). A pure query: the simulation calls it with the current AND the next record
|
|
567
|
+
* inside setParams() to decide the recompile, so it never touches the model's record (PLAN DECISION 12).
|
|
568
|
+
* @param options - an option record (the simulation's current one, or the next one of a setParams patch)
|
|
569
|
+
* @returns the model's own override set
|
|
570
|
+
*/
|
|
571
|
+
overrides(options: ForceAtlas2Options): Overrides {
|
|
572
|
+
const resolved = resolveForceAtlas2Options(options, this.current);
|
|
573
|
+
const mode = this.swingMode;
|
|
574
|
+
return {
|
|
575
|
+
LINLOG: resolved.linlog,
|
|
576
|
+
DISTRIBUTED: resolved.distributedAction,
|
|
577
|
+
TIER: 0,
|
|
578
|
+
SWING_MODE: mode,
|
|
579
|
+
STRONG_GRAVITY: resolved.strongGravity,
|
|
580
|
+
GRAVITY_CENTER: mode,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* The seven module specs of an override set in dispatch order -- K1, K2, K3, K4, K5, toScene, fill -- each with
|
|
586
|
+
* only the override names its entry declares (K2 also USE_PERM / HAS_WEIGHTS), for warm() and the compile matrix.
|
|
587
|
+
* @param overrides - the merged override set (the model's plus USE_PERM / HAS_WEIGHTS)
|
|
588
|
+
* @param _subgroups - accepted for the ForceModel interface and unused: every reducing FA2 body carries
|
|
589
|
+
* needs: ["subgroups"] in its registry entry and the composer picks the twin from caps.features (contract 4.3)
|
|
590
|
+
* @returns the specs
|
|
591
|
+
*/
|
|
592
|
+
specs(overrides: Overrides, _subgroups: boolean): readonly WgslModuleSpec[] {
|
|
593
|
+
const [repulsionSpec, speedSpec] = RepulsionExact.specs(repulsionOverrides(overrides));
|
|
594
|
+
return [
|
|
595
|
+
kernelSpec("fa2-stats-finalize"),
|
|
596
|
+
kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS)),
|
|
597
|
+
repulsionSpec,
|
|
598
|
+
speedSpec,
|
|
599
|
+
kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS)),
|
|
600
|
+
kernelSpec("fa2-to-scene"),
|
|
601
|
+
kernelSpec("fill"),
|
|
602
|
+
];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Compiles (through the cache) and binds every kernel against the buffers of this load(): K1, K2 (or the fill of
|
|
607
|
+
* force when arcCount === 0), K3 + K4 through RepulsionExact, K5, toScene, and the fill of oldForce; writes the
|
|
608
|
+
* FillParams { count: 3n, value: 0, mode: 0 } into the model's uniform buffer. With n === 0 nothing is bound.
|
|
609
|
+
* @param resources - the graph, the shared and model buffers, the ring and the cache
|
|
610
|
+
* @param overrides - the merged override set
|
|
611
|
+
*/
|
|
612
|
+
async bind(resources: ModelResources, overrides: Overrides): Promise<void> {
|
|
613
|
+
this.dropBound();
|
|
614
|
+
this.resources = resources;
|
|
615
|
+
const { n, pipelines, caps, core, perm, ring, device } = resources;
|
|
616
|
+
if (n === 0) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const [k1, k2, k5, toScene, fill] = await Promise.all([
|
|
620
|
+
pipelines.kernel(kernelSpec("fa2-stats-finalize")),
|
|
621
|
+
pipelines.kernel(kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS))),
|
|
622
|
+
pipelines.kernel(kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS))),
|
|
623
|
+
pipelines.kernel(kernelSpec("fa2-to-scene")),
|
|
624
|
+
pipelines.kernel(kernelSpec("fill")),
|
|
625
|
+
]);
|
|
626
|
+
const repulsion = await RepulsionExact.create(pipelines, caps, repulsionOverrides(overrides));
|
|
627
|
+
if (this.resources !== resources) {
|
|
628
|
+
// a newer bind() superseded this one while the pipelines compiled; its own bind groups stand
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const pos = resources.buffer("positions");
|
|
632
|
+
const scene = resources.buffer("scenePositions");
|
|
633
|
+
const fixed = resources.buffer("fixed");
|
|
634
|
+
const partials = resources.buffer("partials");
|
|
635
|
+
const state = resources.buffer("state");
|
|
636
|
+
const trace = resources.buffer("trace");
|
|
637
|
+
const force = resources.buffer("force");
|
|
638
|
+
const oldForce = resources.buffer("oldForce");
|
|
639
|
+
const fillParamsBuffer = resources.buffer(FILL_PARAMS_BUFFER);
|
|
640
|
+
const params = ring.binding(FA2_PARAMS);
|
|
641
|
+
const fillParams: Binding = {
|
|
642
|
+
buffer: fillParamsBuffer.buffer,
|
|
643
|
+
offset: fillParamsBuffer.offset,
|
|
644
|
+
size: FILL_PARAMS.byteLength,
|
|
645
|
+
window: null,
|
|
646
|
+
};
|
|
647
|
+
const fillBytes = new ArrayBuffer(FILL_PARAMS.byteLength);
|
|
648
|
+
FILL_PARAMS.write(new DataView(fillBytes), { count: 3 * n, value: 0, mode: 0 });
|
|
649
|
+
device.queue.writeBuffer(fillParamsBuffer.buffer, fillParamsBuffer.offset, fillBytes);
|
|
650
|
+
const hasArcs = core.colIdx !== null;
|
|
651
|
+
repulsion.bind({ pos, state, trace, force, oldForce, fixedMask: fixed, partials, params });
|
|
652
|
+
const wg = k1.workgroupSize;
|
|
653
|
+
this.bound = {
|
|
654
|
+
n,
|
|
655
|
+
plan: plan1d(n, wg, caps),
|
|
656
|
+
fillPlan: plan1d(3 * n, wg, caps),
|
|
657
|
+
k1,
|
|
658
|
+
k1Bound: k1.bind({ partials, S: state, T: trace, P: params }),
|
|
659
|
+
k2,
|
|
660
|
+
k2Bound: hasArcs
|
|
661
|
+
? k2.bind({ ...graphBindings(core, perm, resources.weights), pos, force, P: params })
|
|
662
|
+
: null,
|
|
663
|
+
repulsion,
|
|
664
|
+
k5,
|
|
665
|
+
k5Bound: k5.bind({ force, oldForce, fixedMask: fixed, S: state, pos, partials, P: params }),
|
|
666
|
+
toScene,
|
|
667
|
+
toSceneBound: toScene.bind({ pos, scene, P: params }),
|
|
668
|
+
fill,
|
|
669
|
+
fillForceBound: hasArcs ? null : fill.bind({ dst: force, P: fillParams }),
|
|
670
|
+
fillOldBound: this.swingMode === 0 ? fill.bind({ dst: oldForce, P: fillParams }) : null,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* The Fa2Params values of one iteration (the simulation overwrites the shared fields n, dim, flags,
|
|
676
|
+
* iterationIndex, seed, scale, center and settleThreshold with the same values plus the flags).
|
|
677
|
+
* @param iteration - the trace slot of the iteration inside its batch
|
|
678
|
+
* @param options - the simulation's current option record
|
|
679
|
+
* @returns the uniform values
|
|
680
|
+
*/
|
|
681
|
+
paramsFor(iteration: number, options: ForceAtlas2Options): UniformValues {
|
|
682
|
+
const { n } = this.requireResources();
|
|
683
|
+
const resolved = resolveForceAtlas2Options(options, this.current);
|
|
684
|
+
const { nearMax, extentFactor } = this.tuning;
|
|
685
|
+
return {
|
|
686
|
+
n,
|
|
687
|
+
dim: resolved.dim,
|
|
688
|
+
flags: 0,
|
|
689
|
+
tierStart: 0,
|
|
690
|
+
tierEnd: n,
|
|
691
|
+
iterationIndex: iteration,
|
|
692
|
+
seed: seedWord(resolved.seed),
|
|
693
|
+
nearMax,
|
|
694
|
+
scalingRatio: resolved.scalingRatio,
|
|
695
|
+
gravity: resolved.gravity,
|
|
696
|
+
jitterTolerance: resolved.jitterTolerance,
|
|
697
|
+
scale: resolved.scale,
|
|
698
|
+
center: [resolved.center[0], resolved.center[1], resolved.center[2], 0],
|
|
699
|
+
settleThreshold: resolved.settleThreshold,
|
|
700
|
+
extentFactor,
|
|
701
|
+
gridMax: 0,
|
|
702
|
+
levels: 0,
|
|
703
|
+
pad: [0, 0, 0, 0],
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Records one iteration into the batch: K1, K2 (or the fill of force when arcCount === 0), K3, K4, K5 in the
|
|
709
|
+
* batch's ONE K1-K5 compute pass (opened by the first call of a batch and reused by every later call with the
|
|
710
|
+
* same batch.id, PLAN DECISION 2), then toScene in a second pass that ends it, stopping after stage `upTo` when
|
|
711
|
+
* given (spec 7.4; debugRunStages / inspect, spec 11.9 item 2). The simulation passes "K5" for iterations
|
|
712
|
+
* 0..k-2 and undefined for the last, so toScene runs once per batch. The first call after load() zeroes
|
|
713
|
+
* oldForce before K1 (paper mode). With n === 0 nothing is recorded (PLAN DECISION 9); a call before bind()
|
|
714
|
+
* completed is E_NOT_LOADED (never a silent no-op).
|
|
715
|
+
* @param batch - the batch being recorded
|
|
716
|
+
* @param slot - the UniformRing slot holding this iteration's Fa2Params
|
|
717
|
+
* @param tier - "exact" (the grid tier is E_UNSUPPORTED until P4; the simulation never passes "grid")
|
|
718
|
+
* @param upTo - a stage name to stop after; undefined records every stage including toScene
|
|
719
|
+
*/
|
|
720
|
+
recordIteration(batch: CommandBatch, slot: number, tier: "exact" | "grid", upTo?: string): void {
|
|
721
|
+
if (tier === "grid") {
|
|
722
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", "the grid repulsion tier lands in P4", {
|
|
723
|
+
feature: "repulsion.grid",
|
|
724
|
+
hint: 'pass repulsion: "exact"',
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
const resources = this.requireResources();
|
|
728
|
+
const stop = upTo === undefined ? FA2_STAGES.length - 1 : this.stageIndex(upTo);
|
|
729
|
+
const { bound } = this;
|
|
730
|
+
if (bound === null) {
|
|
731
|
+
if (resources.n === 0) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
throw new WebGpuGraphError(
|
|
735
|
+
"E_NOT_LOADED",
|
|
736
|
+
"the ForceAtlas2 model is not bound (bind() has not completed)",
|
|
737
|
+
{
|
|
738
|
+
state: "loaded",
|
|
739
|
+
},
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
const offset = resources.ring.offsetOf(slot);
|
|
743
|
+
const pass = this.openPass !== null && this.openPass.id === batch.id ? this.openPass.pass : batch.pass("fa2");
|
|
744
|
+
this.openPass = { id: batch.id, pass };
|
|
745
|
+
if (this.resetOldForce) {
|
|
746
|
+
this.resetOldForce = false;
|
|
747
|
+
if (bound.fillOldBound !== null) {
|
|
748
|
+
bound.fill.dispatch(pass, bound.fillOldBound, bound.fillPlan, [0]);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP, [offset]);
|
|
752
|
+
if (stop < 1) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (bound.k2Bound !== null) {
|
|
756
|
+
bound.k2.dispatch(pass, bound.k2Bound, bound.plan, [offset]);
|
|
757
|
+
} else if (bound.fillForceBound !== null) {
|
|
758
|
+
bound.fill.dispatch(pass, bound.fillForceBound, bound.fillPlan, [0]);
|
|
759
|
+
}
|
|
760
|
+
if (stop < 2) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
bound.repulsion.recordRepulsion(pass, bound.n, offset);
|
|
764
|
+
if (stop < 3) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
bound.repulsion.recordSpeedFinalize(pass, offset);
|
|
768
|
+
if (stop < 4) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
bound.k5.dispatch(pass, bound.k5Bound, bound.plan, [offset]);
|
|
772
|
+
if (stop < 5) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// the second pass ends the K1-K5 pass; the batch is complete after toScene, so nothing reuses it
|
|
776
|
+
this.openPass = null;
|
|
777
|
+
const scenePass = batch.pass("fa2-to-scene");
|
|
778
|
+
bound.toScene.dispatch(scenePass, bound.toSceneBound, bound.plan, [offset]);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* speed = 1, speedEfficiency = 1, swing = 1, traction = 1 (mode 1 accumulates from 1; mode 0 overwrites them each
|
|
783
|
+
* iteration, the initial value is irrelevant); arms the oldForce reset of the next recordIteration.
|
|
784
|
+
* @param state - the state writer of the simulation
|
|
785
|
+
*/
|
|
786
|
+
onLoad(state: StateWriter): void {
|
|
787
|
+
state.set("speed", 1);
|
|
788
|
+
state.set("speedEfficiency", 1);
|
|
789
|
+
state.set("swing", 1);
|
|
790
|
+
state.set("traction", 1);
|
|
791
|
+
this.resetOldForce = true;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Mode 0: nothing (D8). Mode 1 (networkx): swing = traction = 1 (spec 7.2 "load() and reheat() reset them to 1").
|
|
796
|
+
* @param state - the state writer of the simulation
|
|
797
|
+
*/
|
|
798
|
+
onReheat(state: StateWriter): void {
|
|
799
|
+
if (this.swingMode === 1) {
|
|
800
|
+
state.set("swing", 1);
|
|
801
|
+
state.set("traction", 1);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Resets speed / speedEfficiency to 1 only when linlog, strongGravity or distributedAction changed (spec 7.17);
|
|
807
|
+
* a numeric tweak leaves the controller alone. The patch is validated by the same resolver the simulation uses
|
|
808
|
+
* and applied over the record of the constructor / the previous onSetParams -- the only place `current` moves
|
|
809
|
+
* (PLAN DECISION 12), so the comparison sees the record from BEFORE this setParams even though the simulation
|
|
810
|
+
* already queried overrides(next).
|
|
811
|
+
* @param patch - the setParams patch
|
|
812
|
+
* @param state - the state writer of the simulation
|
|
813
|
+
*/
|
|
814
|
+
onSetParams(patch: Partial<ForceAtlas2Options>, state: StateWriter): void {
|
|
815
|
+
const next = resolveForceAtlas2Options(patch, this.current);
|
|
816
|
+
const lawChanged =
|
|
817
|
+
next.linlog !== this.current.linlog ||
|
|
818
|
+
next.strongGravity !== this.current.strongGravity ||
|
|
819
|
+
next.distributedAction !== this.current.distributedAction;
|
|
820
|
+
this.current = next;
|
|
821
|
+
if (lawChanged) {
|
|
822
|
+
state.set("speed", 1);
|
|
823
|
+
state.set("speedEfficiency", 1);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Decodes the state header and the k trace records of a completed batch (k = trace.byteLength / 32) into
|
|
829
|
+
* ForceAtlas2Stats: the exact tier with null grid fields; msPerIteration null (the simulation owns the clock).
|
|
830
|
+
* @param state - a DataView over the 256-byte state header
|
|
831
|
+
* @param trace - a DataView over the k Fa2Trace records of the batch
|
|
832
|
+
* @returns the stats
|
|
833
|
+
*/
|
|
834
|
+
readStats(state: DataView, trace: DataView): ForceAtlas2Stats {
|
|
835
|
+
const header = FA2_STATE.read(state);
|
|
836
|
+
const centroid = vector(header, "centroid");
|
|
837
|
+
const records: ForceAtlas2TraceRecord[] = [];
|
|
838
|
+
const count = Math.floor(trace.byteLength / TRACE_RECORD_BYTES);
|
|
839
|
+
for (let i = 0; i < count; i++) {
|
|
840
|
+
const record = FA2_TRACE.read(trace, i * TRACE_RECORD_BYTES);
|
|
841
|
+
records.push({
|
|
842
|
+
swing: scalar(record, "swing"),
|
|
843
|
+
traction: scalar(record, "traction"),
|
|
844
|
+
speed: scalar(record, "speed"),
|
|
845
|
+
speedEfficiency: scalar(record, "speedEfficiency"),
|
|
846
|
+
meanDisplacement: scalar(record, "meanDisplacement"),
|
|
847
|
+
settledCount: scalar(record, "settledCount"),
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
iteration: scalar(header, "iteration"),
|
|
852
|
+
meanDisplacement: scalar(header, "meanDisplacement"),
|
|
853
|
+
rmsRadius: scalar(header, "rmsRadius"),
|
|
854
|
+
layoutRadius: scalar(header, "radius"),
|
|
855
|
+
centroid: [centroid[0], centroid[1], centroid[2]],
|
|
856
|
+
repulsionTier: "exact",
|
|
857
|
+
maxCellOccupancy: null,
|
|
858
|
+
outsideGrid: null,
|
|
859
|
+
msPerIteration: null,
|
|
860
|
+
swing: scalar(header, "swing"),
|
|
861
|
+
traction: scalar(header, "traction"),
|
|
862
|
+
speed: scalar(header, "speed"),
|
|
863
|
+
speedEfficiency: scalar(header, "speedEfficiency"),
|
|
864
|
+
trace: records,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* The resources of the last bind(), or E_NOT_LOADED before it.
|
|
870
|
+
* @returns the resources
|
|
871
|
+
*/
|
|
872
|
+
private requireResources(): ModelResources {
|
|
873
|
+
if (this.resources === null) {
|
|
874
|
+
throw new WebGpuGraphError("E_NOT_LOADED", "the ForceAtlas2 model has not been bound (load() first)", {
|
|
875
|
+
state: "created",
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
return this.resources;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* The index of a stage name in FA2_STAGES, or E_INVALID_ARGUMENT.
|
|
883
|
+
* @param upTo - the stage name
|
|
884
|
+
* @returns its index
|
|
885
|
+
*/
|
|
886
|
+
private stageIndex(upTo: string): number {
|
|
887
|
+
for (let i = 0; i < FA2_STAGES.length; i++) {
|
|
888
|
+
if (FA2_STAGES[i] === upTo) {
|
|
889
|
+
return i;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
throw invalid("upTo", upTo, FA2_STAGES.join(" | "));
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Drops the bind groups of the previous bind() (the buffers changed) so the cached kernels do not accumulate stale
|
|
897
|
+
* groups across reloads; K3 / K4 live inside RepulsionExact and keep the P1-T6 behaviour. Also forgets the pass
|
|
898
|
+
* of a batch recorded before the rebind.
|
|
899
|
+
*/
|
|
900
|
+
private dropBound(): void {
|
|
901
|
+
this.openPass = null;
|
|
902
|
+
const { bound } = this;
|
|
903
|
+
if (bound === null) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
for (const kernel of [bound.k1, bound.k2, bound.k5, bound.toScene, bound.fill]) {
|
|
907
|
+
kernel.invalidate();
|
|
908
|
+
}
|
|
909
|
+
this.bound = null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ============================================================ the factory
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* The resolve callback of the simulation's setParams: the patch over the current record, re-validated (the current
|
|
917
|
+
* record is re-resolved first so the callback is typed without a cast).
|
|
918
|
+
* @param patch - the setParams patch
|
|
919
|
+
* @param current - the simulation's current option record
|
|
920
|
+
* @returns the new record
|
|
921
|
+
*/
|
|
922
|
+
function resolvePatch(patch: Partial<ForceAtlas2Options>, current: ForceAtlas2Options): ForceAtlas2Options {
|
|
923
|
+
return resolveForceAtlas2Options(patch, resolveForceAtlas2Options(current));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Spec 3.3 createForceAtlas2, verbatim: a GpuLayoutSimulation running ForceAtlas2 on the exact repulsion tier with
|
|
928
|
+
* the option defaults of spec 7.14 and the GPU-only tuning of GpuLayoutTuning (contract 3.13 "Contracts").
|
|
929
|
+
* @param ctx - the context (E_DISPOSED / E_DEVICE_LOST through assertReady)
|
|
930
|
+
* @param options - the ForceAtlas2 options and the GPU-only tuning knobs in one record
|
|
931
|
+
* @returns the simulation in state "created"; load() next
|
|
932
|
+
*/
|
|
933
|
+
export function createForceAtlas2(
|
|
934
|
+
ctx: GpuContext,
|
|
935
|
+
options?: ForceAtlas2Options & GpuLayoutTuning,
|
|
936
|
+
): GpuLayoutSimulation<ForceAtlas2Options, ForceAtlas2Stats> {
|
|
937
|
+
ctx.assertReady();
|
|
938
|
+
const resolved = resolveForceAtlas2Options(options);
|
|
939
|
+
const tuning = resolveLayoutTuning(options);
|
|
940
|
+
const model = new ForceAtlas2Model(tuning, resolved);
|
|
941
|
+
return new ForceSimulation<ForceAtlas2Options, ForceAtlas2Stats>(ctx, model, resolved, tuning, resolvePatch);
|
|
942
|
+
}
|