@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,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WGSL composer (spec 3.5; contract 3.9, 4.2): ONE declaration per module (`WgslModuleSpec`) generates
|
|
3
|
+
* everything the host and the shader must agree on -- the `@group / @binding` block, the `override` lines, the
|
|
4
|
+
* struct texts, the explicit bind-group layouts and the bind() record keys -- by string concatenation in the fixed
|
|
5
|
+
* order of contract 4.2. Every disagreement that would otherwise be a GPUPipelineError on the first device that
|
|
6
|
+
* creates the variant is a compose-time `E_SHADER_COMPILE { stage: "compose" }` on every device.
|
|
7
|
+
*
|
|
8
|
+
* PLAN DECISION: this file imports `workgroupSizeFor` from `../device/caps.js` (one copy of the WG rule) and
|
|
9
|
+
* `ShaderStage` from `../device/webgpu-constants.js` (spec 2.1 rule 1: the core never reads the `GPUShaderStage`
|
|
10
|
+
* global at module top level); `../constants.js` is not imported because every constant the composer emits reaches
|
|
11
|
+
* it through `./prelude.js`. The layer rule (contract 2.4: device < context < memory < kernel) allows both edges.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { workgroupSizeFor } from "../device/caps.js";
|
|
15
|
+
import { ShaderStage } from "../device/webgpu-constants.js";
|
|
16
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
17
|
+
import { type PlanCaps } from "../types/context.js";
|
|
18
|
+
import {
|
|
19
|
+
PRELUDE_WGSL,
|
|
20
|
+
REDUCE_HELPER_NAMES,
|
|
21
|
+
REDUCE_HELPERS_SUBGROUP_WGSL,
|
|
22
|
+
REDUCE_HELPERS_WORKGROUP_WGSL,
|
|
23
|
+
WGSL_RESERVED_WORDS,
|
|
24
|
+
wgslF32Literal,
|
|
25
|
+
} from "./prelude.js";
|
|
26
|
+
import { type UniformBlock } from "./struct-block.js";
|
|
27
|
+
|
|
28
|
+
/** One storage or uniform binding of a module (spec 3.5). `wgslType` is the element / struct type text ("array<u32>", "array<vec4f>", "Fa2State"). */
|
|
29
|
+
export interface BindingDecl {
|
|
30
|
+
readonly group: 0 | 1 | 2 | 3;
|
|
31
|
+
readonly binding: number;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly kind: "storage" | "storage-ro" | "uniform";
|
|
34
|
+
readonly wgslType: string;
|
|
35
|
+
}
|
|
36
|
+
/** One module-specific override (spec 3.5); the five standard ones (WG, USE_PERM, HAS_WEIGHTS, SUBGROUP_MIN, SUBGROUP_MAX) come from the prelude and are never listed here. */
|
|
37
|
+
export interface OverrideDecl {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly type: "u32" | "bool" | "f32";
|
|
40
|
+
readonly default: number | boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Spec 3.5 WgslModuleSpec, verbatim. `needs: ["subgroups"]` means "this body calls a wg_reduce_* helper": the composer splices the subgroup helper block when the device has the feature and the workgroup-memory twin otherwise. */
|
|
43
|
+
export interface WgslModuleSpec {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly body: string;
|
|
46
|
+
readonly bindings: readonly BindingDecl[];
|
|
47
|
+
readonly overrideDecls: readonly OverrideDecl[];
|
|
48
|
+
readonly overrides: Readonly<Record<string, number | boolean>>;
|
|
49
|
+
readonly needs: readonly "subgroups"[];
|
|
50
|
+
readonly uniforms: readonly UniformBlock[];
|
|
51
|
+
readonly snippets?: Readonly<Record<string, string>> | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** The names the prelude declares as overrides; spec.overrides may set them without an OverrideDecl. */
|
|
54
|
+
export const STANDARD_OVERRIDES = ["WG", "USE_PERM", "HAS_WEIGHTS", "SUBGROUP_MIN", "SUBGROUP_MAX"] as const;
|
|
55
|
+
/** What composeWgsl returns: the text, the body's first line in it (for compilation-info formatting), the effective overrides (WG, SUBGROUP_MIN, SUBGROUP_MAX filled) and the features to enable. */
|
|
56
|
+
export interface ComposedModule {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly code: string;
|
|
59
|
+
readonly bodyLine: number;
|
|
60
|
+
readonly overrides: Readonly<Record<string, number | boolean>>;
|
|
61
|
+
/**
|
|
62
|
+
* The subset of `overrides` the composed code REFERENCES outside its own declaration -- what the pipeline
|
|
63
|
+
* descriptor's `constants` carries. WebKit (Safari) fails pipeline creation with "Compute library failed
|
|
64
|
+
* creation" when a constant is supplied for an override the entry point never reads (HAS_WEIGHTS in `degree`,
|
|
65
|
+
* TIER in the thread-per-row tiers); Dawn tolerates it. The cache key still uses `overrides` (contract 3.9).
|
|
66
|
+
*/
|
|
67
|
+
readonly constants: Readonly<Record<string, number | boolean>>;
|
|
68
|
+
readonly entryPoint: string;
|
|
69
|
+
readonly subgroups: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The type of each standard override (the prelude's declarations). */
|
|
73
|
+
const STANDARD_OVERRIDE_TYPES: Readonly<Record<(typeof STANDARD_OVERRIDES)[number], OverrideDecl["type"]>> =
|
|
74
|
+
Object.freeze({
|
|
75
|
+
WG: "u32",
|
|
76
|
+
USE_PERM: "bool",
|
|
77
|
+
HAS_WEIGHTS: "bool",
|
|
78
|
+
SUBGROUP_MIN: "u32",
|
|
79
|
+
SUBGROUP_MAX: "u32",
|
|
80
|
+
});
|
|
81
|
+
/** The only feature a body may need (spec 3.5). */
|
|
82
|
+
const KNOWN_NEEDS: readonly string[] = Object.freeze(["subgroups"]);
|
|
83
|
+
/** The smallest legal subgroup size; the scratch of 4.3 is sized by the MINIMUM size. */
|
|
84
|
+
const SUBGROUP_FLOOR = 4;
|
|
85
|
+
/** Builtin WGSL element types a binding may name without a struct declaration. */
|
|
86
|
+
const BUILTIN_TYPES: ReadonlySet<string> = new Set([
|
|
87
|
+
"u32",
|
|
88
|
+
"i32",
|
|
89
|
+
"f32",
|
|
90
|
+
"f16",
|
|
91
|
+
"bool",
|
|
92
|
+
"vec2f",
|
|
93
|
+
"vec3f",
|
|
94
|
+
"vec4f",
|
|
95
|
+
"vec2u",
|
|
96
|
+
"vec3u",
|
|
97
|
+
"vec4u",
|
|
98
|
+
"vec2i",
|
|
99
|
+
"vec3i",
|
|
100
|
+
"vec4i",
|
|
101
|
+
"vec2h",
|
|
102
|
+
"vec3h",
|
|
103
|
+
"vec4h",
|
|
104
|
+
]);
|
|
105
|
+
const TEMPLATED_BUILTIN = /^(vec[234]|mat[234]x[234]|atomic|array)</;
|
|
106
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
107
|
+
/** The `var<...>` form of each binding kind (contract 4.2). */
|
|
108
|
+
const ADDRESS_SPACE: Readonly<Record<BindingDecl["kind"], string>> = Object.freeze({
|
|
109
|
+
storage: "var<storage, read_write>",
|
|
110
|
+
"storage-ro": "var<storage, read>",
|
|
111
|
+
uniform: "var<uniform>",
|
|
112
|
+
});
|
|
113
|
+
/** The reserved words as a set (the frozen list stays the public form). */
|
|
114
|
+
const RESERVED: ReadonlySet<string> = new Set(WGSL_RESERVED_WORDS);
|
|
115
|
+
const MARKER = /\/\/@@([A-Za-z0-9_]+)@@/;
|
|
116
|
+
const IDENTIFIER_TOKENS = /(?<![A-Za-z0-9_])[A-Za-z_][A-Za-z0-9_]*/g;
|
|
117
|
+
const COMPUTE_ATTRIBUTE = /@compute\b/g;
|
|
118
|
+
const ENTRY_FN = /\bfn\s+([A-Za-z_][A-Za-z0-9_]*)/;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The compose-time error of contract 3.9.
|
|
122
|
+
* @param id - the module id
|
|
123
|
+
* @param slot - what failed (`override:<key>`, `snippet:<key>`, `marker:<name>`, `body:<text>`, `binding:<name>`, `type:<name>`, `needs:<feature>`, `helper:<name>`, `reserved:<word>`, `entry`)
|
|
124
|
+
* @param message - the human-readable reason
|
|
125
|
+
* @returns the error (thrown by the caller)
|
|
126
|
+
*/
|
|
127
|
+
function composeError(id: string, slot: string, message: string): WebGpuGraphError {
|
|
128
|
+
return new WebGpuGraphError("E_SHADER_COMPILE", `${id}: ${message}`, { id, stage: "compose", slot });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Removes block and line comments so a word inside a comment is never taken for an identifier.
|
|
133
|
+
* @param text - WGSL text
|
|
134
|
+
* @returns the text without comments
|
|
135
|
+
*/
|
|
136
|
+
function stripComments(text: string): string {
|
|
137
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, "");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The struct a binding's element type names, or null for a builtin type.
|
|
142
|
+
* @param wgslType - the binding's type text ("array<u32>", "array<Fa2Partial>", "Fa2State")
|
|
143
|
+
* @returns the struct name or null
|
|
144
|
+
*/
|
|
145
|
+
function structNameOf(wgslType: string): string | null {
|
|
146
|
+
const trimmed = wgslType.trim();
|
|
147
|
+
const inner = trimmed.startsWith("array<")
|
|
148
|
+
? trimmed.slice("array<".length).replace(/\s*(,[^>]*)?>\s*$/, "")
|
|
149
|
+
: trimmed;
|
|
150
|
+
if (BUILTIN_TYPES.has(inner) || TEMPLATED_BUILTIN.test(inner)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return inner;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Checks that an override value matches its declared type.
|
|
158
|
+
* PLAN DECISION: a value of the wrong type (a number for a bool, a bool / negative / fractional for a u32) is a
|
|
159
|
+
* compose-time `E_SHADER_COMPILE { slot: "override:<name>" }` instead of a GPUPipelineError on the first device.
|
|
160
|
+
* @param id - the module id
|
|
161
|
+
* @param name - the override name
|
|
162
|
+
* @param type - the declared type
|
|
163
|
+
* @param value - the value
|
|
164
|
+
*/
|
|
165
|
+
function assertOverrideValue(id: string, name: string, type: OverrideDecl["type"], value: number | boolean): void {
|
|
166
|
+
const ok =
|
|
167
|
+
type === "bool"
|
|
168
|
+
? typeof value === "boolean"
|
|
169
|
+
: typeof value === "number" &&
|
|
170
|
+
Number.isFinite(value) &&
|
|
171
|
+
(type === "f32" || (Number.isInteger(value) && value >= 0));
|
|
172
|
+
if (!ok) {
|
|
173
|
+
throw composeError(id, `override:${name}`, `override ${name} (${type}) cannot take the value ${String(value)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The WGSL literal of an override default (u32 literals carry the `u` suffix, f32 literals a decimal point, bools true / false).
|
|
179
|
+
* @param decl - the override
|
|
180
|
+
* @returns the literal text
|
|
181
|
+
*/
|
|
182
|
+
function overrideLiteral(decl: OverrideDecl): string {
|
|
183
|
+
if (decl.type === "bool") {
|
|
184
|
+
return decl.default ? "true" : "false";
|
|
185
|
+
}
|
|
186
|
+
const value = decl.default as number;
|
|
187
|
+
return decl.type === "u32" ? `${value}u` : wgslF32Literal(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The `var<...>` declaration of one binding (contract 4.2).
|
|
192
|
+
* @param decl - the binding
|
|
193
|
+
* @returns the declaration line
|
|
194
|
+
*/
|
|
195
|
+
function bindingLine(decl: BindingDecl): string {
|
|
196
|
+
const space = ADDRESS_SPACE[decl.kind];
|
|
197
|
+
return `@group(${decl.group}) @binding(${decl.binding}) ${space} ${decl.name}: ${decl.wgslType};`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The entry-point name of a body, or null when the body does not carry exactly one `@compute` attribute followed by `fn <name>`.
|
|
202
|
+
* @param body - the kernel body
|
|
203
|
+
* @returns the name or null
|
|
204
|
+
*/
|
|
205
|
+
function findEntryPoint(body: string): string | null {
|
|
206
|
+
const stripped = stripComments(body);
|
|
207
|
+
const matches = [...stripped.matchAll(COMPUTE_ATTRIBUTE)];
|
|
208
|
+
if (matches.length !== 1 || matches[0].index === undefined) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const fn = ENTRY_FN.exec(stripped.slice(matches[0].index));
|
|
212
|
+
return fn === null ? null : fn[1];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The entry-point name of a body: the identifier after `fn` on the line following `@compute` (every body has exactly one entry point).
|
|
217
|
+
* PLAN DECISION: a body without exactly one `@compute` entry point is `E_SHADER_COMPILE { stage: "compose", slot: "entry" }`.
|
|
218
|
+
* @param body - the kernel body
|
|
219
|
+
* @returns the entry-point function name
|
|
220
|
+
*/
|
|
221
|
+
export function entryPointOf(body: string): string {
|
|
222
|
+
const name = findEntryPoint(body);
|
|
223
|
+
if (name === null) {
|
|
224
|
+
throw new WebGpuGraphError(
|
|
225
|
+
"E_SHADER_COMPILE",
|
|
226
|
+
"a body must carry exactly one @compute attribute followed by `fn <name>`",
|
|
227
|
+
{
|
|
228
|
+
id: null,
|
|
229
|
+
stage: "compose",
|
|
230
|
+
slot: "entry",
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return name;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The bind-group-layout descriptors derived from spec.bindings: one per group 0..maxGroup (empty groups get an empty entry list); uniform decls carry hasDynamicOffset true (spec 5.1).
|
|
239
|
+
* PLAN DECISION: a uniform decl whose struct is one of `spec.uniforms` also carries `minBindingSize = block.byteLength`,
|
|
240
|
+
* so a wrong-size uniform binding fails at createBindGroup (synchronously, with the bind group's label in the message)
|
|
241
|
+
* instead of at the first dispatch (verified on Dawn 0.4.0: without it the error names only the pipeline).
|
|
242
|
+
* @param spec - the module
|
|
243
|
+
* @returns the descriptors in group order
|
|
244
|
+
*/
|
|
245
|
+
export function bindGroupLayoutDescriptors(spec: WgslModuleSpec): GPUBindGroupLayoutDescriptor[] {
|
|
246
|
+
const maxGroup = spec.bindings.reduce((acc, b) => Math.max(acc, b.group), -1);
|
|
247
|
+
const blocks = new Map(spec.uniforms.map((block) => [block.name, block] as const));
|
|
248
|
+
const descriptors: GPUBindGroupLayoutDescriptor[] = [];
|
|
249
|
+
for (let group = 0; group <= maxGroup; group++) {
|
|
250
|
+
const entries: GPUBindGroupLayoutEntry[] = spec.bindings
|
|
251
|
+
.filter((b) => b.group === group)
|
|
252
|
+
.sort((a, b) => a.binding - b.binding)
|
|
253
|
+
.map((b) => {
|
|
254
|
+
if (b.kind === "uniform") {
|
|
255
|
+
const block = blocks.get(b.wgslType);
|
|
256
|
+
const buffer: GPUBufferBindingLayout = { type: "uniform", hasDynamicOffset: true };
|
|
257
|
+
if (block !== undefined) {
|
|
258
|
+
buffer.minBindingSize = block.byteLength;
|
|
259
|
+
}
|
|
260
|
+
return { binding: b.binding, visibility: ShaderStage.COMPUTE, buffer };
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
binding: b.binding,
|
|
264
|
+
visibility: ShaderStage.COMPUTE,
|
|
265
|
+
buffer: { type: b.kind === "storage" ? "storage" : "read-only-storage" },
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
descriptors.push({ entries });
|
|
269
|
+
}
|
|
270
|
+
return descriptors;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The names of the bindings of one group in binding order.
|
|
275
|
+
* @param spec - the module
|
|
276
|
+
* @param group - the group index
|
|
277
|
+
* @returns the names
|
|
278
|
+
*/
|
|
279
|
+
export function bindingNames(spec: WgslModuleSpec, group: number): readonly string[] {
|
|
280
|
+
return spec.bindings
|
|
281
|
+
.filter((b) => b.group === group)
|
|
282
|
+
.sort((a, b) => a.binding - b.binding)
|
|
283
|
+
.map((b) => b.name);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Validates `spec.needs`, `spec.overrideDecls`, `spec.overrides` and `spec.bindings` (contract 3.9's compose-time checks).
|
|
288
|
+
* PLAN DECISION: an `overrideDecls` entry that redeclares a standard override, repeats a name or names a reserved
|
|
289
|
+
* word is `E_SHADER_COMPILE { slot: "override:<name>" }` (otherwise a duplicate-declaration error on the first device).
|
|
290
|
+
* @param spec - the module
|
|
291
|
+
*/
|
|
292
|
+
function validateDeclarations(spec: WgslModuleSpec): void {
|
|
293
|
+
const { id } = spec;
|
|
294
|
+
for (const need of spec.needs) {
|
|
295
|
+
if (!KNOWN_NEEDS.includes(need)) {
|
|
296
|
+
throw composeError(id, `needs:${need}`, `unknown feature "${need}" in needs`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const declared = new Map<string, OverrideDecl["type"]>();
|
|
300
|
+
for (const decl of spec.overrideDecls) {
|
|
301
|
+
if (!IDENTIFIER.test(decl.name) || RESERVED.has(decl.name)) {
|
|
302
|
+
throw composeError(id, `override:${decl.name}`, `override name "${decl.name}" is not a usable identifier`);
|
|
303
|
+
}
|
|
304
|
+
if ((STANDARD_OVERRIDES as readonly string[]).includes(decl.name) || declared.has(decl.name)) {
|
|
305
|
+
throw composeError(
|
|
306
|
+
id,
|
|
307
|
+
`override:${decl.name}`,
|
|
308
|
+
`override "${decl.name}" is declared twice (the prelude declares the standard ones)`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
assertOverrideValue(id, decl.name, decl.type, decl.default);
|
|
312
|
+
declared.set(decl.name, decl.type);
|
|
313
|
+
}
|
|
314
|
+
for (const [key, value] of Object.entries(spec.overrides)) {
|
|
315
|
+
const standard = (STANDARD_OVERRIDE_TYPES as Readonly<Record<string, OverrideDecl["type"] | undefined>>)[key];
|
|
316
|
+
const type = standard ?? declared.get(key);
|
|
317
|
+
if (type === undefined) {
|
|
318
|
+
throw composeError(
|
|
319
|
+
id,
|
|
320
|
+
`override:${key}`,
|
|
321
|
+
`override "${key}" is neither a standard override nor declared in overrideDecls`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
assertOverrideValue(id, key, type, value);
|
|
325
|
+
}
|
|
326
|
+
const slots = new Set<string>();
|
|
327
|
+
const names = new Set<string>();
|
|
328
|
+
const blocks = new Set(spec.uniforms.map((block) => block.name));
|
|
329
|
+
for (const b of spec.bindings) {
|
|
330
|
+
if (!IDENTIFIER.test(b.name) || RESERVED.has(b.name)) {
|
|
331
|
+
throw composeError(id, `binding:${b.name}`, `binding name "${b.name}" is not a usable identifier`);
|
|
332
|
+
}
|
|
333
|
+
const slot = `${b.group}.${b.binding}`;
|
|
334
|
+
if (slots.has(slot)) {
|
|
335
|
+
throw composeError(id, `binding:${b.name}`, `two bindings share group ${b.group} binding ${b.binding}`);
|
|
336
|
+
}
|
|
337
|
+
if (names.has(b.name)) {
|
|
338
|
+
throw composeError(id, `binding:${b.name}`, `two bindings share the name "${b.name}"`);
|
|
339
|
+
}
|
|
340
|
+
slots.add(slot);
|
|
341
|
+
names.add(b.name);
|
|
342
|
+
const struct = structNameOf(b.wgslType);
|
|
343
|
+
if (struct !== null && !blocks.has(struct)) {
|
|
344
|
+
throw composeError(
|
|
345
|
+
id,
|
|
346
|
+
`type:${struct}`,
|
|
347
|
+
`binding "${b.name}" names struct "${struct}" but no block in spec.uniforms declares it`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Substitutes the snippets into the body (every `//@@NAME@@` marker replaced by `spec.snippets[NAME]`) and checks the markers.
|
|
355
|
+
* @param spec - the module
|
|
356
|
+
* @returns the substituted body
|
|
357
|
+
*/
|
|
358
|
+
function substituteSnippets(spec: WgslModuleSpec): string {
|
|
359
|
+
const { id } = spec;
|
|
360
|
+
let { body } = spec;
|
|
361
|
+
for (const [key, snippet] of Object.entries(spec.snippets ?? {})) {
|
|
362
|
+
const marker = `//@@${key}@@`;
|
|
363
|
+
if (!body.includes(marker)) {
|
|
364
|
+
throw composeError(id, `snippet:${key}`, `snippet "${key}" has no ${marker} marker in the body`);
|
|
365
|
+
}
|
|
366
|
+
body = body.split(marker).join(snippet);
|
|
367
|
+
}
|
|
368
|
+
const left = MARKER.exec(body);
|
|
369
|
+
if (left !== null) {
|
|
370
|
+
throw composeError(id, `marker:${left[1]}`, `marker //@@${left[1]}@@ is not filled by any snippet`);
|
|
371
|
+
}
|
|
372
|
+
return body;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The textual checks on the substituted body: no `@group(`, no `override `, no reserved identifier, no helper call without `needs`.
|
|
377
|
+
* PLAN DECISION: a body calling a `wg_reduce_*` helper without `needs: ["subgroups"]` is
|
|
378
|
+
* `E_SHADER_COMPILE { slot: "helper:<name>" }` (otherwise an unresolved identifier on the first device).
|
|
379
|
+
* @param spec - the module
|
|
380
|
+
* @param body - the substituted body
|
|
381
|
+
*/
|
|
382
|
+
function validateBody(spec: WgslModuleSpec, body: string): void {
|
|
383
|
+
const { id } = spec;
|
|
384
|
+
for (const forbidden of ["@group(", "override "]) {
|
|
385
|
+
if (body.includes(forbidden)) {
|
|
386
|
+
throw composeError(
|
|
387
|
+
id,
|
|
388
|
+
`body:${forbidden}`,
|
|
389
|
+
`a body must not contain "${forbidden}" (the composer emits those lines)`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const stripped = stripComments(body);
|
|
394
|
+
const helpers = REDUCE_HELPER_NAMES as readonly string[];
|
|
395
|
+
for (const match of stripped.matchAll(IDENTIFIER_TOKENS)) {
|
|
396
|
+
const word = match[0];
|
|
397
|
+
if (RESERVED.has(word)) {
|
|
398
|
+
throw composeError(
|
|
399
|
+
id,
|
|
400
|
+
`reserved:${word}`,
|
|
401
|
+
`"${word}" is a WGSL reserved word (spec 16.2) and cannot be an identifier`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (helpers.includes(word) && !spec.needs.includes("subgroups")) {
|
|
405
|
+
throw composeError(id, `helper:${word}`, `the body calls ${word} but spec.needs does not list "subgroups"`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* String concatenation of spec 3.5: prelude (constants, standard overrides, helpers), module overrides, struct texts, the bind declarations, then the body with snippets substituted (4.2 gives the exact emitted format).
|
|
412
|
+
* @param spec - the module
|
|
413
|
+
* @param caps - the device capabilities (WG, the subgroup sizes and the feature set)
|
|
414
|
+
* @returns the composed module
|
|
415
|
+
*/
|
|
416
|
+
export function composeWgsl(spec: WgslModuleSpec, caps: PlanCaps): ComposedModule {
|
|
417
|
+
validateDeclarations(spec);
|
|
418
|
+
const entryPoint = findEntryPoint(spec.body);
|
|
419
|
+
if (entryPoint === null) {
|
|
420
|
+
throw composeError(
|
|
421
|
+
spec.id,
|
|
422
|
+
"entry",
|
|
423
|
+
"a body must carry exactly one @compute attribute followed by `fn <name>`",
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const body = substituteSnippets(spec);
|
|
427
|
+
validateBody(spec, body);
|
|
428
|
+
const wantsHelpers = spec.needs.includes("subgroups");
|
|
429
|
+
const subgroups = wantsHelpers && caps.features.has("subgroups");
|
|
430
|
+
const overrides: Record<string, number | boolean> = { ...spec.overrides };
|
|
431
|
+
if (overrides.WG === undefined) {
|
|
432
|
+
overrides.WG = workgroupSizeFor(caps);
|
|
433
|
+
}
|
|
434
|
+
if (subgroups) {
|
|
435
|
+
overrides.SUBGROUP_MAX = caps.subgroupMaxSize;
|
|
436
|
+
overrides.SUBGROUP_MIN = Math.max(SUBGROUP_FLOOR, caps.subgroupMinSize);
|
|
437
|
+
}
|
|
438
|
+
const parts: string[] = [];
|
|
439
|
+
if (subgroups) {
|
|
440
|
+
parts.push("enable subgroups;");
|
|
441
|
+
}
|
|
442
|
+
parts.push(PRELUDE_WGSL);
|
|
443
|
+
for (const decl of spec.overrideDecls) {
|
|
444
|
+
parts.push(`override ${decl.name}: ${decl.type} = ${overrideLiteral(decl)};`);
|
|
445
|
+
}
|
|
446
|
+
for (const block of spec.uniforms) {
|
|
447
|
+
parts.push(block.wgsl);
|
|
448
|
+
}
|
|
449
|
+
if (wantsHelpers) {
|
|
450
|
+
parts.push(subgroups ? REDUCE_HELPERS_SUBGROUP_WGSL : REDUCE_HELPERS_WORKGROUP_WGSL);
|
|
451
|
+
}
|
|
452
|
+
for (const decl of spec.bindings) {
|
|
453
|
+
parts.push(bindingLine(decl));
|
|
454
|
+
}
|
|
455
|
+
const head = parts.join("\n");
|
|
456
|
+
const code = `${head}\n${body}`;
|
|
457
|
+
return Object.freeze({
|
|
458
|
+
id: spec.id,
|
|
459
|
+
code,
|
|
460
|
+
bodyLine: head.split("\n").length + 1,
|
|
461
|
+
overrides: Object.freeze(overrides),
|
|
462
|
+
constants: Object.freeze(referencedOverrides(code, overrides)),
|
|
463
|
+
entryPoint,
|
|
464
|
+
subgroups,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The overrides whose names occur in the code outside comments and outside their own `override` declaration lines
|
|
470
|
+
* (the constants a pipeline may carry on every backend, see ComposedModule.constants).
|
|
471
|
+
* @param code - the composed module text
|
|
472
|
+
* @param overrides - the effective override values
|
|
473
|
+
* @returns the referenced subset, in the same order
|
|
474
|
+
*/
|
|
475
|
+
function referencedOverrides(
|
|
476
|
+
code: string,
|
|
477
|
+
overrides: Readonly<Record<string, number | boolean>>,
|
|
478
|
+
): Record<string, number | boolean> {
|
|
479
|
+
const stripped = code
|
|
480
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
481
|
+
.replace(/\/\/[^\n]*/g, " ")
|
|
482
|
+
.replace(/^\s*override\s+\w+\s*:[^\n]*$/gm, " ");
|
|
483
|
+
const referenced: Record<string, number | boolean> = {};
|
|
484
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
485
|
+
if (new RegExp(`\\b${name}\\b`).test(stripped)) {
|
|
486
|
+
referenced[name] = value;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return referenced;
|
|
490
|
+
}
|