@graphty/webgpu-graph-algorithms 0.0.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +378 -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,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The upload cache of spec 4.1 / 4.3 / 4.5: every core array, view array, column and ad hoc array a kernel binds
|
|
3
|
+
* lives on the device exactly once per snapshot core, keyed on the typed-array object (or the Column object for
|
|
4
|
+
* columns, PLAN DECISION 8), recorded per serial so `release(snapshot)` destroys every buffer of a snapshot and its
|
|
5
|
+
* withColumns() siblings, and tombstoned so a live user learns of the release through `isReleased` (spec 4.5).
|
|
6
|
+
* Uploads follow the pure planner of ./upload-plan.ts: the arena path writes ONE buffer with ONE writeBuffer of the
|
|
7
|
+
* hot prefix (or the full arena when a cold segment is needed and fits) and binds segments at
|
|
8
|
+
* `segment.byteOffset - arena.byteOffset`; the perArray path writes one buffer per array; a windowed plan is
|
|
9
|
+
* E_TOO_LARGE until P4 executes windows (PLAN DECISION 2). Identity permutations are never materialised: presence is
|
|
10
|
+
* decided from counts and flags, and the arcToEdge / edgeToArc getters are read only when a segment exists. Nothing
|
|
11
|
+
* is freed by garbage collection; the once-only warning above warnUnreleasedSnapshots names the missing release.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type AttributeTable,
|
|
16
|
+
type Column,
|
|
17
|
+
type CoreArrayName,
|
|
18
|
+
type GpuEligibility,
|
|
19
|
+
type GraphSnapshot,
|
|
20
|
+
type TypedArrayData,
|
|
21
|
+
} from "@graphty/graph-format";
|
|
22
|
+
|
|
23
|
+
import { type AllocationTracker } from "../device/error-scope.js";
|
|
24
|
+
import { BufferUsage } from "../device/webgpu-constants.js";
|
|
25
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
26
|
+
import { type PlanCaps } from "../types/context.js";
|
|
27
|
+
import { type ArcWindow, type Binding } from "../types/memory.js";
|
|
28
|
+
import { planUpload } from "./upload-plan.js";
|
|
29
|
+
|
|
30
|
+
/** The core arrays a kernel binds by default (spec 4.1); the cold segments are named explicitly by the caller. */
|
|
31
|
+
const DEFAULT_NEED: readonly CoreArrayName[] = Object.freeze(["rowPtr", "colIdx", "weights"]);
|
|
32
|
+
|
|
33
|
+
/** The five core arrays in arena (hot-to-cold) order. */
|
|
34
|
+
const CORE_ORDER: readonly CoreArrayName[] = Object.freeze(["rowPtr", "colIdx", "weights", "arcToEdge", "edgeToArc"]);
|
|
35
|
+
|
|
36
|
+
/** Every resident buffer is bound by kernels, filled by writeBuffer and readable back (tests, readbacks of state). */
|
|
37
|
+
const RESIDENT_USAGE = BufferUsage.STORAGE | BufferUsage.COPY_DST | BufferUsage.COPY_SRC;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The core arrays of a snapshot on the device (spec 4.1); a null member is absent (zero-length, unweighted or an
|
|
41
|
+
* identity permutation) and is bound as a dummy by the kernel layer (3.9). `Binding` itself is declared in
|
|
42
|
+
* src/types/memory.ts (3.3). Exported by contract 3.8: P1-T4's graphBindings / graphOverrides and every later
|
|
43
|
+
* kernel driver take it; nothing at P1-T2 imports it by name.
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
export interface CoreBinding {
|
|
47
|
+
readonly serial: number;
|
|
48
|
+
readonly plan: "arena" | "perArray" | "windowed";
|
|
49
|
+
readonly rowPtr: Binding;
|
|
50
|
+
readonly colIdx: Binding | null;
|
|
51
|
+
readonly weights: Binding | null;
|
|
52
|
+
readonly arcToEdge: Binding | null;
|
|
53
|
+
readonly edgeToArc: Binding | null;
|
|
54
|
+
readonly windows: readonly ArcWindow[] | null;
|
|
55
|
+
readonly hasWeights: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A view's arrays on the device plus the CPU-side scalars (degreeOrder's segmentOffsets) (spec 4.3). Exported by
|
|
60
|
+
* contract 3.8: the degreeOrder permutation of P4's kernels is a ViewBinding; nothing at P1-T2 imports it by name.
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
export interface ViewBinding {
|
|
64
|
+
readonly view:
|
|
65
|
+
"reverse" | "coo" | "edgeList" | "outDegree" | "inDegree" | "degreeOrder" | "reverseDegreeOrder" | "mate";
|
|
66
|
+
readonly bindings: Readonly<Record<string, Binding>>;
|
|
67
|
+
readonly scalars: Readonly<Record<string, readonly number[]>>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A column's gpuView() on the device (spec 4.3). Exported by contract 3.8: P3's layout inputs register columns
|
|
72
|
+
* through it; nothing at P1-T2 imports it by name.
|
|
73
|
+
* @public
|
|
74
|
+
*/
|
|
75
|
+
export interface ColumnBinding {
|
|
76
|
+
readonly binding: Binding;
|
|
77
|
+
readonly column: Column;
|
|
78
|
+
readonly version: number;
|
|
79
|
+
readonly eligibility: GpuEligibility;
|
|
80
|
+
readonly components: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* An ad hoc array on the device (spec 4.1 last row); `destroy()` is the caller's when no owner was given. Exported
|
|
85
|
+
* by contract 3.8: P3's model resolves column inputs to a registered ArrayBinding; nothing at P1-T2 imports it by
|
|
86
|
+
* name.
|
|
87
|
+
* @public
|
|
88
|
+
*/
|
|
89
|
+
export interface ArrayBinding {
|
|
90
|
+
readonly binding: Binding;
|
|
91
|
+
readonly byteLength: number;
|
|
92
|
+
readonly owner: GraphSnapshot | null;
|
|
93
|
+
destroy(): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* stats() shape (spec 4.1). Exported by contract 3.8 for the callers of ctx.residency.stats(); nothing at P1-T2
|
|
98
|
+
* imports it by name.
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
export interface ResidencyStats {
|
|
102
|
+
readonly buffers: number;
|
|
103
|
+
readonly bytes: number;
|
|
104
|
+
readonly snapshots: number;
|
|
105
|
+
readonly perSnapshot: readonly {
|
|
106
|
+
readonly serial: number;
|
|
107
|
+
readonly label: string | null;
|
|
108
|
+
readonly bytes: number;
|
|
109
|
+
readonly buffers: number;
|
|
110
|
+
}[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** One buffer the residency owns. */
|
|
114
|
+
interface Resident {
|
|
115
|
+
/** The typed-array object or the Column object the buffer was uploaded for. */
|
|
116
|
+
readonly key: object;
|
|
117
|
+
readonly buffer: GPUBuffer;
|
|
118
|
+
readonly byteLength: number;
|
|
119
|
+
/** The owning record's serial, or null for an owner-less ad hoc array. */
|
|
120
|
+
readonly serial: number | null;
|
|
121
|
+
/** For columns: the column.version the buffer holds. */
|
|
122
|
+
version: number;
|
|
123
|
+
destroyed: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** One uploaded segment of the arena buffer. */
|
|
127
|
+
interface ArenaSegmentBinding {
|
|
128
|
+
readonly offset: number;
|
|
129
|
+
readonly size: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Everything recorded for one serial (a snapshot and its withColumns() siblings). */
|
|
133
|
+
interface ResidencyRecord {
|
|
134
|
+
readonly serial: number;
|
|
135
|
+
readonly label: string | null;
|
|
136
|
+
readonly entries: Resident[];
|
|
137
|
+
plan: "arena" | "perArray" | "windowed" | null;
|
|
138
|
+
arena: {
|
|
139
|
+
readonly buffer: GPUBuffer;
|
|
140
|
+
readonly segments: Readonly<Record<CoreArrayName, ArenaSegmentBinding | null>>;
|
|
141
|
+
} | null;
|
|
142
|
+
readonly bindings: Map<CoreArrayName, Binding>;
|
|
143
|
+
released: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Whether an ArrayBuffer-like is a SharedArrayBuffer (checked by tag so the global need not exist).
|
|
148
|
+
* @param buffer - the buffer to test
|
|
149
|
+
* @returns true for a SharedArrayBuffer
|
|
150
|
+
*/
|
|
151
|
+
function isSharedBuffer(buffer: ArrayBufferLike): boolean {
|
|
152
|
+
return Object.prototype.toString.call(buffer) === "[object SharedArrayBuffer]";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The E_INVALID_ARGUMENT error of a bad residency argument.
|
|
157
|
+
* @param argument - the argument name
|
|
158
|
+
* @param value - the value given
|
|
159
|
+
* @param expected - what was expected
|
|
160
|
+
* @returns the error to throw
|
|
161
|
+
*/
|
|
162
|
+
function invalid(argument: string, value: unknown, expected: string): WebGpuGraphError {
|
|
163
|
+
return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument}: expected ${expected}`, {
|
|
164
|
+
argument,
|
|
165
|
+
value,
|
|
166
|
+
expected,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Throws E_SNAPSHOT { reason: "detached" } for a snapshot whose core was transferred away.
|
|
172
|
+
* @param s - the snapshot
|
|
173
|
+
*/
|
|
174
|
+
function assertAttached(s: GraphSnapshot): void {
|
|
175
|
+
if (s.detached) {
|
|
176
|
+
throw new WebGpuGraphError("E_SNAPSHOT", `snapshot ${s.serial} is detached (its core was transferred away)`, {
|
|
177
|
+
reason: "detached",
|
|
178
|
+
serial: s.serial,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Whether a core array exists on the snapshot, from counts and flags alone (mirrors coreByteLengths of
|
|
185
|
+
* ./upload-plan.ts; spec 5.6: zero-length arrays are never uploaded; spec 4.1: identity permutations never
|
|
186
|
+
* materialised).
|
|
187
|
+
* @param s - the snapshot
|
|
188
|
+
* @param name - the core array
|
|
189
|
+
* @returns true when the array has bytes to upload
|
|
190
|
+
*/
|
|
191
|
+
function isPresent(s: GraphSnapshot, name: CoreArrayName): boolean {
|
|
192
|
+
switch (name) {
|
|
193
|
+
case "rowPtr":
|
|
194
|
+
return true;
|
|
195
|
+
case "colIdx":
|
|
196
|
+
return s.arcCount > 0;
|
|
197
|
+
case "weights":
|
|
198
|
+
return s.flags.weighted && s.arcCount > 0;
|
|
199
|
+
case "arcToEdge":
|
|
200
|
+
return !s.flags.arcToEdgeIsIdentity && s.arcCount > 0;
|
|
201
|
+
case "edgeToArc":
|
|
202
|
+
return !s.flags.arcToEdgeIsIdentity && s.edgeCount > 0;
|
|
203
|
+
default:
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The array object of a PRESENT core array (the caller checked isPresent, so the two getters never materialise).
|
|
210
|
+
* @param s - the snapshot
|
|
211
|
+
* @param name - the core array
|
|
212
|
+
* @returns the array
|
|
213
|
+
*/
|
|
214
|
+
function coreArray(s: GraphSnapshot, name: CoreArrayName): TypedArrayData {
|
|
215
|
+
switch (name) {
|
|
216
|
+
case "rowPtr":
|
|
217
|
+
return s.rowPtr;
|
|
218
|
+
case "colIdx":
|
|
219
|
+
return s.colIdx;
|
|
220
|
+
case "weights": {
|
|
221
|
+
const { weights } = s;
|
|
222
|
+
if (weights === null) {
|
|
223
|
+
throw invalid("weights", null, "a weighted snapshot");
|
|
224
|
+
}
|
|
225
|
+
return weights;
|
|
226
|
+
}
|
|
227
|
+
case "arcToEdge":
|
|
228
|
+
return s.arcToEdge;
|
|
229
|
+
case "edgeToArc":
|
|
230
|
+
return s.edgeToArc;
|
|
231
|
+
default:
|
|
232
|
+
throw invalid("name", name, "a core array name");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The upload cache (spec 4.1): WeakMap on array objects, WeakMap on snapshots, a strong Map by serial; @internal (reached as ctx.residency). */
|
|
237
|
+
export class GraphResidency {
|
|
238
|
+
private readonly device: GPUDevice;
|
|
239
|
+
private readonly caps: PlanCaps;
|
|
240
|
+
private readonly allocator: AllocationTracker;
|
|
241
|
+
private readonly warnUnreleasedSnapshots: number;
|
|
242
|
+
private readonly warn: (message: string) => void;
|
|
243
|
+
/** Fast lookup by the uploaded object (a typed array or a Column). */
|
|
244
|
+
private readonly residents = new WeakMap<object, Resident>();
|
|
245
|
+
/** Fast lookup of a snapshot's record (entries of released records are recognised by `released`). */
|
|
246
|
+
private readonly records = new WeakMap<GraphSnapshot, ResidencyRecord>();
|
|
247
|
+
/** The strong map by serial: withColumns() siblings share one record; cleared by release. */
|
|
248
|
+
private readonly bySerial = new Map<number, ResidencyRecord>();
|
|
249
|
+
/** Owner-less ad hoc arrays. */
|
|
250
|
+
private readonly orphans = new Set<Resident>();
|
|
251
|
+
/** Serials released and not re-uploaded. */
|
|
252
|
+
private readonly tombstones = new Set<number>();
|
|
253
|
+
private liveCount = 0;
|
|
254
|
+
private liveBytesValue = 0;
|
|
255
|
+
private warned = false;
|
|
256
|
+
private disposed = false;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Creates an empty residency over a device.
|
|
260
|
+
* @param device - the device (queue.writeBuffer)
|
|
261
|
+
* @param caps - the device capabilities the planner reads
|
|
262
|
+
* @param allocator - the context's OOM-scoped allocator (every buffer is created and destroyed through it)
|
|
263
|
+
* @param options - the once-only warning
|
|
264
|
+
* @param options.warnUnreleasedSnapshots - the resident-snapshot count above which the warning fires once
|
|
265
|
+
* @param options.warn - the warning sink; default console.warn
|
|
266
|
+
*/
|
|
267
|
+
constructor(
|
|
268
|
+
device: GPUDevice,
|
|
269
|
+
caps: PlanCaps,
|
|
270
|
+
allocator: AllocationTracker,
|
|
271
|
+
options: { readonly warnUnreleasedSnapshots: number; readonly warn?: ((message: string) => void) | undefined },
|
|
272
|
+
) {
|
|
273
|
+
this.device = device;
|
|
274
|
+
this.caps = caps;
|
|
275
|
+
this.allocator = allocator;
|
|
276
|
+
this.warnUnreleasedSnapshots = options.warnUnreleasedSnapshots;
|
|
277
|
+
this.warn =
|
|
278
|
+
options.warn ??
|
|
279
|
+
((message: string): void => {
|
|
280
|
+
console.warn(message);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Uploads (or finds) the core; `need` defaults to ["rowPtr", "colIdx", "weights"]; cold segments on demand
|
|
286
|
+
* (spec 4.2). Never materialises an identity permutation. A windowed plan throws E_TOO_LARGE { path: "windowed" }
|
|
287
|
+
* until P4 (PLAN DECISION 2); a tombstoned serial is lifted and re-uploaded (PLAN DECISION 7).
|
|
288
|
+
* @param s - the snapshot
|
|
289
|
+
* @param need - the core arrays to bind (rowPtr is always included; absent arrays are ignored)
|
|
290
|
+
* @returns the core binding (a frozen object; grows as cold segments are added)
|
|
291
|
+
*/
|
|
292
|
+
core(s: GraphSnapshot, need?: readonly CoreArrayName[]): CoreBinding {
|
|
293
|
+
this.assertLive();
|
|
294
|
+
assertAttached(s);
|
|
295
|
+
const wanted = need ?? DEFAULT_NEED;
|
|
296
|
+
const names = CORE_ORDER.filter((name) => (name === "rowPtr" || wanted.includes(name)) && isPresent(s, name));
|
|
297
|
+
const plan = planUpload(s, this.caps, names);
|
|
298
|
+
if (plan.kind === "windowed") {
|
|
299
|
+
throw new WebGpuGraphError(
|
|
300
|
+
"E_TOO_LARGE",
|
|
301
|
+
`snapshot ${s.serial}: an arc array of ${4 * s.arcCount} bytes needs arc windows (${plan.windows.length}), which P1-P3 plan but do not execute`,
|
|
302
|
+
{
|
|
303
|
+
needed: 4 * s.arcCount,
|
|
304
|
+
limit: this.caps.limits.maxStorageBufferBindingSize,
|
|
305
|
+
path: "windowed",
|
|
306
|
+
algorithm: null,
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const record = this.ensureRecord(s);
|
|
311
|
+
if (record.plan === null) {
|
|
312
|
+
record.plan = plan.kind;
|
|
313
|
+
if (plan.kind === "arena" && s.arena !== null) {
|
|
314
|
+
const { arena } = s;
|
|
315
|
+
const resident = this.upload(
|
|
316
|
+
record,
|
|
317
|
+
s.rowPtr,
|
|
318
|
+
new Uint8Array(arena.buffer, arena.byteOffset, plan.bytes),
|
|
319
|
+
`residency:core:${record.serial}:arena`,
|
|
320
|
+
);
|
|
321
|
+
record.arena = { buffer: resident.buffer, segments: plan.segments };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const name of names) {
|
|
325
|
+
if (!record.bindings.has(name)) {
|
|
326
|
+
record.bindings.set(name, this.bindCore(record, s, name));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const rowPtr = record.bindings.get("rowPtr");
|
|
330
|
+
if (rowPtr === undefined) {
|
|
331
|
+
throw invalid("rowPtr", null, "a bound rowPtr (internal invariant)");
|
|
332
|
+
}
|
|
333
|
+
const weights = record.bindings.get("weights") ?? null;
|
|
334
|
+
return Object.freeze({
|
|
335
|
+
serial: record.serial,
|
|
336
|
+
plan: plan.kind,
|
|
337
|
+
rowPtr,
|
|
338
|
+
colIdx: record.bindings.get("colIdx") ?? null,
|
|
339
|
+
weights,
|
|
340
|
+
arcToEdge: record.bindings.get("arcToEdge") ?? null,
|
|
341
|
+
edgeToArc: record.bindings.get("edgeToArc") ?? null,
|
|
342
|
+
windows: null,
|
|
343
|
+
hasWeights: weights !== null,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Uploads (or finds) a view; P1-P3 support outDegree / inDegree / degreeOrder / reverseDegreeOrder; the others
|
|
349
|
+
* -> E_UNSUPPORTED until P7. packViews is accepted and ignored until P7 (a `true` request is E_UNSUPPORTED
|
|
350
|
+
* { option: "packViews" }, PLAN DECISION 9).
|
|
351
|
+
* @param s - the snapshot
|
|
352
|
+
* @param name - the view
|
|
353
|
+
* @param options - view options
|
|
354
|
+
* @param options.packViews - P7's packed reverse upload; `true` is E_UNSUPPORTED until then
|
|
355
|
+
* @returns the view binding
|
|
356
|
+
*/
|
|
357
|
+
view(
|
|
358
|
+
s: GraphSnapshot,
|
|
359
|
+
name: "reverse" | "coo" | "edgeList" | "outDegree" | "inDegree" | "degreeOrder" | "reverseDegreeOrder" | "mate",
|
|
360
|
+
options?: { readonly packViews?: boolean | undefined },
|
|
361
|
+
): ViewBinding {
|
|
362
|
+
this.assertLive();
|
|
363
|
+
assertAttached(s);
|
|
364
|
+
if (options?.packViews === true) {
|
|
365
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", "packViews is not supported before the P7 view uploads", {
|
|
366
|
+
option: "packViews",
|
|
367
|
+
hint: "omit packViews; the four reverse arrays are uploaded separately",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
let array: TypedArrayData;
|
|
371
|
+
let bindingName: string;
|
|
372
|
+
let scalars: Readonly<Record<string, readonly number[]>> = {};
|
|
373
|
+
switch (name) {
|
|
374
|
+
case "outDegree":
|
|
375
|
+
this.assertNotReleased(s);
|
|
376
|
+
this.assertNonEmpty(s);
|
|
377
|
+
array = s.outDegree();
|
|
378
|
+
bindingName = "outDegree";
|
|
379
|
+
break;
|
|
380
|
+
case "inDegree":
|
|
381
|
+
this.assertNotReleased(s);
|
|
382
|
+
this.assertNonEmpty(s);
|
|
383
|
+
array = s.inDegree();
|
|
384
|
+
bindingName = "inDegree";
|
|
385
|
+
break;
|
|
386
|
+
case "degreeOrder": {
|
|
387
|
+
this.assertNotReleased(s);
|
|
388
|
+
this.assertNonEmpty(s);
|
|
389
|
+
const order = s.degreeOrder();
|
|
390
|
+
array = order.perm;
|
|
391
|
+
bindingName = "perm";
|
|
392
|
+
scalars = { segmentOffsets: Array.from(order.segmentOffsets) };
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
case "reverseDegreeOrder": {
|
|
396
|
+
this.assertNotReleased(s);
|
|
397
|
+
this.assertNonEmpty(s);
|
|
398
|
+
const order = s.degreeOrder({ of: "reverse" });
|
|
399
|
+
array = order.perm;
|
|
400
|
+
bindingName = "perm";
|
|
401
|
+
scalars = { segmentOffsets: Array.from(order.segmentOffsets) };
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case "reverse":
|
|
405
|
+
case "coo":
|
|
406
|
+
case "edgeList":
|
|
407
|
+
case "mate":
|
|
408
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `the ${name} view is not uploaded before P7`, {
|
|
409
|
+
feature: `view:${name}`,
|
|
410
|
+
hint: "P1-P3 upload outDegree, inDegree, degreeOrder and reverseDegreeOrder",
|
|
411
|
+
});
|
|
412
|
+
default:
|
|
413
|
+
throw invalid("name", name, "a view name");
|
|
414
|
+
}
|
|
415
|
+
const record = this.ensureRecord(s);
|
|
416
|
+
const resident = this.upload(record, array, array, `residency:view:${record.serial}:${name}`);
|
|
417
|
+
const binding: Binding = { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
|
|
418
|
+
return Object.freeze({
|
|
419
|
+
view: name,
|
|
420
|
+
bindings: Object.freeze({ [bindingName]: binding }),
|
|
421
|
+
scalars: Object.freeze(scalars),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* gpuView(name) + column.version; re-uploads in place when the version changed and the byte length did not
|
|
427
|
+
* (spec 4.3). CONTRACT DECISION: `owner` is required so release(owner) can find the buffer (a table has no
|
|
428
|
+
* back-reference to its snapshot). The key is the Column object (PLAN DECISION 8). E_GPU_INELIGIBLE and
|
|
429
|
+
* E_UNKNOWN_COLUMN from the format pass through unchanged.
|
|
430
|
+
* @param table - the table holding the column
|
|
431
|
+
* @param name - the column name
|
|
432
|
+
* @param owner - the snapshot the buffer is recorded against
|
|
433
|
+
* @returns the column binding
|
|
434
|
+
*/
|
|
435
|
+
column(table: AttributeTable, name: string, owner: GraphSnapshot): ColumnBinding {
|
|
436
|
+
this.assertLive();
|
|
437
|
+
assertAttached(owner);
|
|
438
|
+
this.assertNotReleased(owner);
|
|
439
|
+
const column = table.require(name);
|
|
440
|
+
const data = table.gpuView(name);
|
|
441
|
+
if (data.byteLength === 0) {
|
|
442
|
+
throw invalid("column", name, "a column with at least one row (zero-length arrays are never uploaded)");
|
|
443
|
+
}
|
|
444
|
+
const record = this.ensureRecord(owner);
|
|
445
|
+
const label = `residency:column:${record.serial}:${column.meta.domain}.${name}`;
|
|
446
|
+
const existing = this.residents.get(column);
|
|
447
|
+
let resident: Resident;
|
|
448
|
+
if (
|
|
449
|
+
existing !== undefined &&
|
|
450
|
+
!existing.destroyed &&
|
|
451
|
+
existing.serial === record.serial &&
|
|
452
|
+
existing.byteLength === data.byteLength
|
|
453
|
+
) {
|
|
454
|
+
resident = existing;
|
|
455
|
+
if (resident.version !== column.version) {
|
|
456
|
+
this.device.queue.writeBuffer(resident.buffer, 0, data);
|
|
457
|
+
resident.version = column.version;
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
if (existing !== undefined) {
|
|
461
|
+
// the stale buffer stays in its record until release (spec 4.1: no leak, no stale read)
|
|
462
|
+
this.residents.delete(column);
|
|
463
|
+
}
|
|
464
|
+
resident = this.upload(record, column, data, label);
|
|
465
|
+
resident.version = column.version;
|
|
466
|
+
}
|
|
467
|
+
return Object.freeze({
|
|
468
|
+
binding: { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null },
|
|
469
|
+
column,
|
|
470
|
+
version: resident.version,
|
|
471
|
+
eligibility: column.gpu,
|
|
472
|
+
components: column.meta.components,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Any format array keyed on the object; registered against `owner` when given (spec 4.1).
|
|
478
|
+
* @param key - the array (a non-empty, 4-byte-multiple array over a plain ArrayBuffer)
|
|
479
|
+
* @param label - the buffer label suffix
|
|
480
|
+
* @param owner - the snapshot to record the buffer against, or undefined for a caller-owned buffer
|
|
481
|
+
* @returns the array binding; `destroy()` is idempotent (PLAN DECISION 12)
|
|
482
|
+
*/
|
|
483
|
+
array(key: TypedArrayData, label: string, owner?: GraphSnapshot): ArrayBinding {
|
|
484
|
+
this.assertLive();
|
|
485
|
+
if (key.byteLength === 0 || key.byteLength % 4 !== 0) {
|
|
486
|
+
throw invalid("key", key.byteLength, "a non-empty array whose byteLength is a multiple of 4");
|
|
487
|
+
}
|
|
488
|
+
if (isSharedBuffer(key.buffer)) {
|
|
489
|
+
throw invalid("key", "SharedArrayBuffer", "an array over a plain ArrayBuffer");
|
|
490
|
+
}
|
|
491
|
+
let record: ResidencyRecord | null = null;
|
|
492
|
+
if (owner !== undefined) {
|
|
493
|
+
assertAttached(owner);
|
|
494
|
+
this.assertNotReleased(owner);
|
|
495
|
+
record = this.ensureRecord(owner);
|
|
496
|
+
}
|
|
497
|
+
const resident = this.upload(record, key, key, `residency:array:${label}`);
|
|
498
|
+
const binding: Binding = { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
|
|
499
|
+
return {
|
|
500
|
+
binding,
|
|
501
|
+
byteLength: resident.byteLength,
|
|
502
|
+
owner: owner ?? null,
|
|
503
|
+
destroy: (): void => {
|
|
504
|
+
this.destroyResident(resident, record);
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Destroys every buffer recorded for s.serial (siblings included, Q-27) and tombstones the serial; idempotent
|
|
511
|
+
* (spec 4.5); safe on a snapshot never uploaded (nothing to tombstone, PLAN DECISION 7).
|
|
512
|
+
* @param s - the snapshot (or any withColumns() sibling)
|
|
513
|
+
*/
|
|
514
|
+
release(s: GraphSnapshot): void {
|
|
515
|
+
const record = this.bySerial.get(s.serial);
|
|
516
|
+
if (record === undefined) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
for (const resident of [...record.entries]) {
|
|
520
|
+
this.destroyResident(resident, record);
|
|
521
|
+
}
|
|
522
|
+
this.forget(record);
|
|
523
|
+
this.tombstones.add(s.serial);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Drops every record WITHOUT destroying (device lost: the buffers are gone). @internal */
|
|
527
|
+
clearOnLoss(): void {
|
|
528
|
+
for (const record of [...this.bySerial.values()]) {
|
|
529
|
+
for (const resident of record.entries) {
|
|
530
|
+
resident.destroyed = true;
|
|
531
|
+
if (this.residents.get(resident.key) === resident) {
|
|
532
|
+
this.residents.delete(resident.key);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
record.entries.length = 0;
|
|
536
|
+
this.forget(record);
|
|
537
|
+
}
|
|
538
|
+
for (const resident of this.orphans) {
|
|
539
|
+
resident.destroyed = true;
|
|
540
|
+
if (this.residents.get(resident.key) === resident) {
|
|
541
|
+
this.residents.delete(resident.key);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
this.orphans.clear();
|
|
545
|
+
this.liveCount = 0;
|
|
546
|
+
this.liveBytesValue = 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Destroys everything (ctx.dispose()); idempotent. @internal */
|
|
550
|
+
destroyAll(): void {
|
|
551
|
+
for (const record of [...this.bySerial.values()]) {
|
|
552
|
+
for (const resident of [...record.entries]) {
|
|
553
|
+
this.destroyResident(resident, record);
|
|
554
|
+
}
|
|
555
|
+
this.forget(record);
|
|
556
|
+
}
|
|
557
|
+
for (const resident of [...this.orphans]) {
|
|
558
|
+
this.destroyResident(resident, null);
|
|
559
|
+
}
|
|
560
|
+
this.disposed = true;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* The resident buffers, bytes and snapshots (spec 4.1).
|
|
565
|
+
* @returns the statistics
|
|
566
|
+
*/
|
|
567
|
+
stats(): ResidencyStats {
|
|
568
|
+
const perSnapshot = [...this.bySerial.values()].map((record) => ({
|
|
569
|
+
serial: record.serial,
|
|
570
|
+
label: record.label,
|
|
571
|
+
bytes: record.entries.reduce((sum, resident) => sum + resident.byteLength, 0),
|
|
572
|
+
buffers: record.entries.length,
|
|
573
|
+
}));
|
|
574
|
+
return { buffers: this.liveCount, bytes: this.liveBytesValue, snapshots: this.bySerial.size, perSnapshot };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Bytes of every resident buffer (graft: C R-18).
|
|
579
|
+
* @returns the resident byte count
|
|
580
|
+
*/
|
|
581
|
+
get residentBytes(): number {
|
|
582
|
+
return this.liveBytesValue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* True when the serial was released and not re-uploaded. @internal
|
|
587
|
+
* @param serial - a snapshot serial
|
|
588
|
+
* @returns whether a live user holding bindings of that serial must stop
|
|
589
|
+
*/
|
|
590
|
+
isReleased(serial: number): boolean {
|
|
591
|
+
return this.tombstones.has(serial);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Binds one core array: a segment of the arena buffer when the buffer holds it, else its own buffer keyed on
|
|
596
|
+
* the array object (the cold-segment-on-demand path of spec 4.2, and the whole perArray path).
|
|
597
|
+
* @param record - the snapshot's record
|
|
598
|
+
* @param s - the snapshot
|
|
599
|
+
* @param name - a PRESENT core array
|
|
600
|
+
* @returns the binding
|
|
601
|
+
*/
|
|
602
|
+
private bindCore(record: ResidencyRecord, s: GraphSnapshot, name: CoreArrayName): Binding {
|
|
603
|
+
if (record.arena !== null) {
|
|
604
|
+
const segment = record.arena.segments[name];
|
|
605
|
+
if (segment !== null) {
|
|
606
|
+
return { buffer: record.arena.buffer, offset: segment.offset, size: segment.size, window: null };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const array = coreArray(s, name);
|
|
610
|
+
const resident = this.upload(record, array, array, `residency:core:${record.serial}:${name}`);
|
|
611
|
+
return { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Uploads `data` into a new buffer keyed on `key`, or returns the resident already uploaded for that key by
|
|
616
|
+
* the same owner (PLAN DECISION 11: a resident belongs to one record).
|
|
617
|
+
* @param record - the owning record, or null for an owner-less array
|
|
618
|
+
* @param key - the object the buffer is keyed on
|
|
619
|
+
* @param data - the bytes (byteLength a multiple of 4, graph-format invariant I10; a view over a plain
|
|
620
|
+
* ArrayBuffer, which is what writeBuffer's GPUAllowSharedBufferSource accepts under @webgpu/types 0.1.72)
|
|
621
|
+
* @param label - the buffer label
|
|
622
|
+
* @returns the resident
|
|
623
|
+
*/
|
|
624
|
+
private upload(
|
|
625
|
+
record: ResidencyRecord | null,
|
|
626
|
+
key: object,
|
|
627
|
+
data: ArrayBufferView<ArrayBuffer>,
|
|
628
|
+
label: string,
|
|
629
|
+
): Resident {
|
|
630
|
+
const serial = record === null ? null : record.serial;
|
|
631
|
+
const existing = this.residents.get(key);
|
|
632
|
+
if (existing !== undefined && !existing.destroyed && existing.serial === serial) {
|
|
633
|
+
return existing;
|
|
634
|
+
}
|
|
635
|
+
const buffer = this.allocator.createBuffer({ label, size: data.byteLength, usage: RESIDENT_USAGE });
|
|
636
|
+
this.device.queue.writeBuffer(buffer, 0, data);
|
|
637
|
+
const resident: Resident = { key, buffer, byteLength: data.byteLength, serial, version: 0, destroyed: false };
|
|
638
|
+
this.residents.set(key, resident);
|
|
639
|
+
if (record === null) {
|
|
640
|
+
this.orphans.add(resident);
|
|
641
|
+
} else {
|
|
642
|
+
record.entries.push(resident);
|
|
643
|
+
}
|
|
644
|
+
this.liveCount++;
|
|
645
|
+
this.liveBytesValue += data.byteLength;
|
|
646
|
+
return resident;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Destroys one resident buffer and unregisters it; idempotent.
|
|
651
|
+
* @param resident - the resident
|
|
652
|
+
* @param record - its owning record, or null for an orphan
|
|
653
|
+
*/
|
|
654
|
+
private destroyResident(resident: Resident, record: ResidencyRecord | null): void {
|
|
655
|
+
if (resident.destroyed) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
resident.destroyed = true;
|
|
659
|
+
this.allocator.destroy(resident.buffer);
|
|
660
|
+
if (this.residents.get(resident.key) === resident) {
|
|
661
|
+
this.residents.delete(resident.key);
|
|
662
|
+
}
|
|
663
|
+
if (record === null) {
|
|
664
|
+
this.orphans.delete(resident);
|
|
665
|
+
} else {
|
|
666
|
+
const index = record.entries.indexOf(resident);
|
|
667
|
+
if (index >= 0) {
|
|
668
|
+
record.entries.splice(index, 1);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
this.liveCount--;
|
|
672
|
+
this.liveBytesValue -= resident.byteLength;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Marks a record released and drops it from the serial map (the snapshot WeakMap entries recognise it by the flag).
|
|
677
|
+
* @param record - the record
|
|
678
|
+
*/
|
|
679
|
+
private forget(record: ResidencyRecord): void {
|
|
680
|
+
record.released = true;
|
|
681
|
+
record.bindings.clear();
|
|
682
|
+
record.arena = null;
|
|
683
|
+
record.plan = null;
|
|
684
|
+
this.bySerial.delete(record.serial);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The record of a snapshot's serial, created when absent (which lifts a tombstone and may fire the warning).
|
|
689
|
+
* @param s - the snapshot
|
|
690
|
+
* @returns the record
|
|
691
|
+
*/
|
|
692
|
+
private ensureRecord(s: GraphSnapshot): ResidencyRecord {
|
|
693
|
+
let record = this.records.get(s);
|
|
694
|
+
if (record !== undefined && record.released) {
|
|
695
|
+
record = undefined;
|
|
696
|
+
}
|
|
697
|
+
if (record === undefined) {
|
|
698
|
+
record = this.bySerial.get(s.serial);
|
|
699
|
+
}
|
|
700
|
+
if (record === undefined) {
|
|
701
|
+
record = {
|
|
702
|
+
serial: s.serial,
|
|
703
|
+
label: s.label,
|
|
704
|
+
entries: [],
|
|
705
|
+
plan: null,
|
|
706
|
+
arena: null,
|
|
707
|
+
bindings: new Map<CoreArrayName, Binding>(),
|
|
708
|
+
released: false,
|
|
709
|
+
};
|
|
710
|
+
this.bySerial.set(s.serial, record);
|
|
711
|
+
this.tombstones.delete(s.serial);
|
|
712
|
+
if (!this.warned && this.bySerial.size > this.warnUnreleasedSnapshots) {
|
|
713
|
+
this.warned = true;
|
|
714
|
+
this.warn(
|
|
715
|
+
`[webgpu-graph-algorithms] ${this.bySerial.size} snapshots are resident on the device (warnUnreleasedSnapshots = ${this.warnUnreleasedSnapshots}); call ctx.release(snapshot) for superseded snapshots -- GPU memory is never freed by garbage collection (spec 4.1)`,
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
this.records.set(s, record);
|
|
720
|
+
return record;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Throws E_RELEASED for a tombstoned serial (a bind without a fresh core()).
|
|
725
|
+
* @param s - the snapshot
|
|
726
|
+
*/
|
|
727
|
+
private assertNotReleased(s: GraphSnapshot): void {
|
|
728
|
+
if (this.tombstones.has(s.serial)) {
|
|
729
|
+
throw new WebGpuGraphError(
|
|
730
|
+
"E_RELEASED",
|
|
731
|
+
`snapshot ${s.serial} was released; upload it again through core() before binding views, columns or arrays`,
|
|
732
|
+
{ serial: s.serial },
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Throws E_INVALID_ARGUMENT for an empty snapshot (a view of it would be a zero-length array, spec 5.6).
|
|
739
|
+
* @param s - the snapshot
|
|
740
|
+
*/
|
|
741
|
+
private assertNonEmpty(s: GraphSnapshot): void {
|
|
742
|
+
if (s.nodeCount === 0) {
|
|
743
|
+
throw invalid("snapshot", 0, "nodeCount > 0 (an empty snapshot has no per-node view to upload)");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** Throws E_DISPOSED after destroyAll() (PLAN DECISION 13). */
|
|
748
|
+
private assertLive(): void {
|
|
749
|
+
if (this.disposed) {
|
|
750
|
+
throw new WebGpuGraphError("E_DISPOSED", "the residency was disposed", { label: "residency" });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|