@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,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UniformBlock (spec 5.3, D20): one field table generates BOTH the padded WGSL `struct` text spliced into a module
|
|
3
|
+
* and the byte writer / reader the host uses, so the two cannot disagree. The layout is the strict WGSL layout
|
|
4
|
+
* every runtime accepts (Chromium 139 lacks `uniform_buffer_standard_layout`): scalars 4-aligned, vec2 8-aligned,
|
|
5
|
+
* vec4 16-aligned, no vec3, no bool, no arrays, and the total padded to a multiple of 16 (or to `padTo`) through an
|
|
6
|
+
* explicit `@size` on the last member.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
10
|
+
import { WGSL_RESERVED_WORDS } from "./prelude.js";
|
|
11
|
+
|
|
12
|
+
/** Field types a block may hold; no vec3 (spec 5.3), no bool (not host-shareable), no arrays. */
|
|
13
|
+
export type UniformFieldType = "u32" | "i32" | "f32" | "vec2f" | "vec2u" | "vec4f" | "vec4u";
|
|
14
|
+
/** One field. */
|
|
15
|
+
export type UniformField = readonly [name: string, type: UniformFieldType];
|
|
16
|
+
/** The values written into or read from a block: scalars as numbers, vectors as number arrays of the vector's width. */
|
|
17
|
+
export type UniformValues = Readonly<Record<string, number | readonly number[]>>;
|
|
18
|
+
|
|
19
|
+
/** The scalar a field is made of. */
|
|
20
|
+
type ScalarKind = "u32" | "i32" | "f32";
|
|
21
|
+
|
|
22
|
+
/** Size, alignment and composition of each field type (WGSL 14.4.1). */
|
|
23
|
+
interface TypeInfo {
|
|
24
|
+
readonly size: number;
|
|
25
|
+
readonly align: number;
|
|
26
|
+
readonly width: number;
|
|
27
|
+
readonly scalar: ScalarKind;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TYPE_INFO: Readonly<Record<UniformFieldType, TypeInfo>> = Object.freeze({
|
|
31
|
+
u32: { size: 4, align: 4, width: 1, scalar: "u32" },
|
|
32
|
+
i32: { size: 4, align: 4, width: 1, scalar: "i32" },
|
|
33
|
+
f32: { size: 4, align: 4, width: 1, scalar: "f32" },
|
|
34
|
+
vec2f: { size: 8, align: 8, width: 2, scalar: "f32" },
|
|
35
|
+
vec2u: { size: 8, align: 8, width: 2, scalar: "u32" },
|
|
36
|
+
vec4f: { size: 16, align: 16, width: 4, scalar: "f32" },
|
|
37
|
+
vec4u: { size: 16, align: 16, width: 4, scalar: "u32" },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** One laid-out field. */
|
|
41
|
+
interface FieldLayout {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly type: UniformFieldType;
|
|
44
|
+
readonly offset: number;
|
|
45
|
+
readonly info: TypeInfo;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const STRUCT_ALIGN = 16;
|
|
49
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
50
|
+
const U32_LIMIT = 0xffffffff;
|
|
51
|
+
const I32_MIN = -2147483648;
|
|
52
|
+
const I32_MAX = 2147483647;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Rounds `value` up to the next multiple of `align` (arithmetic, never a bit trick: byte offsets are never operated
|
|
56
|
+
* on bitwise, spec 3.6).
|
|
57
|
+
* @param value - the value to round
|
|
58
|
+
* @param align - the alignment, a positive integer
|
|
59
|
+
* @returns the rounded value
|
|
60
|
+
*/
|
|
61
|
+
function roundUp(value: number, align: number): number {
|
|
62
|
+
return Math.ceil(value / align) * align;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validates a WGSL identifier for a struct or field name.
|
|
67
|
+
* @param argument - the E_INVALID_ARGUMENT `argument` to report
|
|
68
|
+
* @param name - the identifier
|
|
69
|
+
*/
|
|
70
|
+
function assertIdentifier(argument: string, name: string): void {
|
|
71
|
+
if (!IDENTIFIER.test(name) || WGSL_RESERVED_WORDS.includes(name)) {
|
|
72
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock: "${name}" is not a usable WGSL identifier`, {
|
|
73
|
+
argument,
|
|
74
|
+
value: name,
|
|
75
|
+
expected: "a WGSL identifier that is not a reserved word",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validates one scalar component of a value against its scalar kind.
|
|
82
|
+
* @param field - the field name (for the error)
|
|
83
|
+
* @param kind - the scalar kind
|
|
84
|
+
* @param value - the component
|
|
85
|
+
*/
|
|
86
|
+
function assertScalar(field: string, kind: ScalarKind, value: unknown): asserts value is number {
|
|
87
|
+
if (typeof value !== "number") {
|
|
88
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock: field "${field}" expects a number`, {
|
|
89
|
+
argument: field,
|
|
90
|
+
value,
|
|
91
|
+
expected: kind,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (kind === "u32" && !(Number.isInteger(value) && value >= 0 && value <= U32_LIMIT)) {
|
|
95
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock: field "${field}" is not a u32`, {
|
|
96
|
+
argument: field,
|
|
97
|
+
value,
|
|
98
|
+
expected: "an integer in [0, 4294967295]",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (kind === "i32" && !(Number.isInteger(value) && value >= I32_MIN && value <= I32_MAX)) {
|
|
102
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock: field "${field}" is not an i32`, {
|
|
103
|
+
argument: field,
|
|
104
|
+
value,
|
|
105
|
+
expected: "an integer in [-2147483648, 2147483647]",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Writes one scalar component little-endian.
|
|
112
|
+
* @param view - the target view
|
|
113
|
+
* @param kind - the scalar kind
|
|
114
|
+
* @param offset - the byte offset
|
|
115
|
+
* @param value - the component
|
|
116
|
+
*/
|
|
117
|
+
function setScalar(view: DataView, kind: ScalarKind, offset: number, value: number): void {
|
|
118
|
+
if (kind === "u32") {
|
|
119
|
+
view.setUint32(offset, value, true);
|
|
120
|
+
} else if (kind === "i32") {
|
|
121
|
+
view.setInt32(offset, value, true);
|
|
122
|
+
} else {
|
|
123
|
+
view.setFloat32(offset, value, true);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reads one scalar component little-endian.
|
|
129
|
+
* @param view - the source view
|
|
130
|
+
* @param kind - the scalar kind
|
|
131
|
+
* @param offset - the byte offset
|
|
132
|
+
* @returns the component
|
|
133
|
+
*/
|
|
134
|
+
function getScalar(view: DataView, kind: ScalarKind, offset: number): number {
|
|
135
|
+
if (kind === "u32") {
|
|
136
|
+
return view.getUint32(offset, true);
|
|
137
|
+
}
|
|
138
|
+
if (kind === "i32") {
|
|
139
|
+
return view.getInt32(offset, true);
|
|
140
|
+
}
|
|
141
|
+
return view.getFloat32(offset, true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A generated struct: the padded WGSL text and the byte writer / reader share one field table, so they cannot disagree (D20). */
|
|
145
|
+
export class UniformBlock {
|
|
146
|
+
/** The WGSL struct name. */
|
|
147
|
+
readonly name: string;
|
|
148
|
+
/** "uniform" (the default) or "storage"; both lay out identically, the tag documents the block's address space. */
|
|
149
|
+
readonly layout: "uniform" | "storage";
|
|
150
|
+
/** The fields in declaration (= byte) order. */
|
|
151
|
+
readonly fields: readonly UniformField[];
|
|
152
|
+
/** The padded byte length (a multiple of 16). */
|
|
153
|
+
readonly byteLength: number;
|
|
154
|
+
/** The `struct <name> { ... }` text with explicit `@size` / `@align` where padding is needed. */
|
|
155
|
+
readonly wgsl: string;
|
|
156
|
+
/** The laid-out fields by name. */
|
|
157
|
+
private readonly table: ReadonlyMap<string, FieldLayout>;
|
|
158
|
+
/** The laid-out fields in order. */
|
|
159
|
+
private readonly laidOut: readonly FieldLayout[];
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Declares a block; fields are laid out in order with 16-byte alignment for vec4 / the struct, 8 for vec2, 4 for
|
|
163
|
+
* scalars; the total is padded to 16 (uniform, storage) or to `padTo` when given.
|
|
164
|
+
* PLAN DECISION: rejects an empty field list and a struct or field name that is not a WGSL identifier or is a
|
|
165
|
+
* reserved word (spec 3.6: every throwing call leaves state unchanged; a reserved name would be invalid WGSL).
|
|
166
|
+
* PLAN DECISION: optional PARAMETERS (`options?` here, `byteOffset?` of write / read / readField) are spelled
|
|
167
|
+
* `?: T` rather than the contract's `?: T | undefined` because the root ESLint rule
|
|
168
|
+
* no-duplicate-type-constituents rejects the explicit undefined on an optional parameter; every call site is
|
|
169
|
+
* identical, and optional PROPERTIES keep `?: T | undefined`.
|
|
170
|
+
* @param name - the WGSL struct name
|
|
171
|
+
* @param fields - the fields in byte order
|
|
172
|
+
* @param options - the layout tag and the optional padding
|
|
173
|
+
* @param options.layout - "uniform" (default) or "storage"; the tag documents the address space, the layout is the same
|
|
174
|
+
* @param options.padTo - the byte length to pad to (a multiple of 16 not below the natural size)
|
|
175
|
+
* @returns the block
|
|
176
|
+
*/
|
|
177
|
+
static define(
|
|
178
|
+
name: string,
|
|
179
|
+
fields: readonly UniformField[],
|
|
180
|
+
options?: { readonly layout?: "uniform" | "storage" | undefined; readonly padTo?: number | undefined },
|
|
181
|
+
): UniformBlock {
|
|
182
|
+
assertIdentifier("name", name);
|
|
183
|
+
if (fields.length === 0) {
|
|
184
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${name}: a block needs at least one field`, {
|
|
185
|
+
argument: "fields",
|
|
186
|
+
value: fields.length,
|
|
187
|
+
expected: ">= 1 field",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const laidOut: FieldLayout[] = [];
|
|
191
|
+
const table = new Map<string, FieldLayout>();
|
|
192
|
+
let cursor = 0;
|
|
193
|
+
for (const [fieldName, type] of fields) {
|
|
194
|
+
assertIdentifier("fields", fieldName);
|
|
195
|
+
if (table.has(fieldName)) {
|
|
196
|
+
throw new WebGpuGraphError(
|
|
197
|
+
"E_INVALID_ARGUMENT",
|
|
198
|
+
`UniformBlock ${name}: duplicate field "${fieldName}"`,
|
|
199
|
+
{
|
|
200
|
+
argument: "fields",
|
|
201
|
+
value: fieldName,
|
|
202
|
+
expected: "unique field names",
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const info = TYPE_INFO[type] as TypeInfo | undefined;
|
|
207
|
+
if (info === undefined) {
|
|
208
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${name}: unknown field type "${type}"`, {
|
|
209
|
+
argument: "fields",
|
|
210
|
+
value: type,
|
|
211
|
+
expected: Object.keys(TYPE_INFO).join(" | "),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const offset = roundUp(cursor, info.align);
|
|
215
|
+
const field: FieldLayout = { name: fieldName, type, offset, info };
|
|
216
|
+
laidOut.push(field);
|
|
217
|
+
table.set(fieldName, field);
|
|
218
|
+
cursor = offset + info.size;
|
|
219
|
+
}
|
|
220
|
+
const natural = roundUp(cursor, STRUCT_ALIGN);
|
|
221
|
+
const padTo = options?.padTo;
|
|
222
|
+
if (padTo !== undefined && (!Number.isInteger(padTo) || padTo % STRUCT_ALIGN !== 0 || padTo < natural)) {
|
|
223
|
+
throw new WebGpuGraphError(
|
|
224
|
+
"E_INVALID_ARGUMENT",
|
|
225
|
+
`UniformBlock ${name}: padTo ${padTo} is not a multiple of 16 >= ${natural}`,
|
|
226
|
+
{
|
|
227
|
+
argument: "padTo",
|
|
228
|
+
value: padTo,
|
|
229
|
+
expected: `a multiple of ${STRUCT_ALIGN} not below ${natural}`,
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
const byteLength = padTo ?? natural;
|
|
234
|
+
const lines = laidOut.map((field, index) => {
|
|
235
|
+
const last = index === laidOut.length - 1;
|
|
236
|
+
const end = field.offset + field.info.size;
|
|
237
|
+
const size = last && end < byteLength ? `@size(${byteLength - field.offset}) ` : "";
|
|
238
|
+
return ` ${size}${field.name}: ${field.type},`;
|
|
239
|
+
});
|
|
240
|
+
const wgsl = `struct ${name} {\n${lines.join("\n")}\n}`;
|
|
241
|
+
return new UniformBlock(name, options?.layout ?? "uniform", fields, byteLength, wgsl, laidOut, table);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Built by `define()` only.
|
|
246
|
+
* @param name - the struct name
|
|
247
|
+
* @param layout - the address-space tag
|
|
248
|
+
* @param fields - the declared fields
|
|
249
|
+
* @param byteLength - the padded byte length
|
|
250
|
+
* @param wgsl - the struct text
|
|
251
|
+
* @param laidOut - the laid-out fields in order
|
|
252
|
+
* @param table - the laid-out fields by name
|
|
253
|
+
*/
|
|
254
|
+
private constructor(
|
|
255
|
+
name: string,
|
|
256
|
+
layout: "uniform" | "storage",
|
|
257
|
+
fields: readonly UniformField[],
|
|
258
|
+
byteLength: number,
|
|
259
|
+
wgsl: string,
|
|
260
|
+
laidOut: readonly FieldLayout[],
|
|
261
|
+
table: ReadonlyMap<string, FieldLayout>,
|
|
262
|
+
) {
|
|
263
|
+
this.name = name;
|
|
264
|
+
this.layout = layout;
|
|
265
|
+
this.fields = Object.freeze(fields.map(([fieldName, type]) => Object.freeze([fieldName, type] as const)));
|
|
266
|
+
this.byteLength = byteLength;
|
|
267
|
+
this.wgsl = wgsl;
|
|
268
|
+
this.laidOut = laidOut;
|
|
269
|
+
this.table = table;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Byte offset of a field; E_INVALID_ARGUMENT for an unknown field.
|
|
274
|
+
* @param field - the field name
|
|
275
|
+
* @returns the byte offset inside the block
|
|
276
|
+
*/
|
|
277
|
+
offsetOf(field: string): number {
|
|
278
|
+
return this.fieldLayout(field).offset;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Writes `values` at `byteOffset` (default 0); a missing field is written as 0; an unknown key is
|
|
283
|
+
* E_INVALID_ARGUMENT; a vector of the wrong width is E_INVALID_ARGUMENT. Always little-endian. Every byte of the
|
|
284
|
+
* block's region is written (padding as zero), so a written block is byte-exact.
|
|
285
|
+
* PLAN DECISION: every value is validated first (u32 / vec*u components integers in [0, 2^32 - 1], i32 in
|
|
286
|
+
* [-2^31, 2^31 - 1]) and only then is the region zeroed and written, so a rejected call leaves the view unchanged.
|
|
287
|
+
* @param view - the target view (the region must lie inside it)
|
|
288
|
+
* @param values - the field values
|
|
289
|
+
* @param byteOffset - the region's start inside the view
|
|
290
|
+
*/
|
|
291
|
+
write(view: DataView, values: UniformValues, byteOffset?: number): void {
|
|
292
|
+
const base = this.regionStart(view, byteOffset);
|
|
293
|
+
for (const key of Object.keys(values)) {
|
|
294
|
+
if (!this.table.has(key)) {
|
|
295
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${this.name}: unknown field "${key}"`, {
|
|
296
|
+
argument: "values",
|
|
297
|
+
value: key,
|
|
298
|
+
expected: this.laidOut.map((f) => f.name).join(", "),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// validate every value first, so a rejected call leaves the view unchanged (spec 3.6)
|
|
303
|
+
const components: (readonly number[] | null)[] = this.laidOut.map((field) =>
|
|
304
|
+
this.components(field, values[field.name]),
|
|
305
|
+
);
|
|
306
|
+
for (let at = 0; at < this.byteLength; at += 4) {
|
|
307
|
+
view.setUint32(base + at, 0, true);
|
|
308
|
+
}
|
|
309
|
+
this.laidOut.forEach((field, index) => {
|
|
310
|
+
const lanes = components[index];
|
|
311
|
+
if (lanes === null) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
lanes.forEach((component, lane) => {
|
|
315
|
+
setScalar(view, field.info.scalar, base + field.offset + 4 * lane, component);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The validated components of one field's value: null for an absent field, else `width` numbers.
|
|
322
|
+
* @param field - the laid-out field
|
|
323
|
+
* @param value - the value given for it (undefined when absent)
|
|
324
|
+
* @returns the components or null
|
|
325
|
+
*/
|
|
326
|
+
private components(field: FieldLayout, value: number | readonly number[] | undefined): readonly number[] | null {
|
|
327
|
+
if (value === undefined) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const { scalar, width } = field.info;
|
|
331
|
+
if (width === 1) {
|
|
332
|
+
assertScalar(field.name, scalar, value);
|
|
333
|
+
return [value];
|
|
334
|
+
}
|
|
335
|
+
if (!Array.isArray(value) || value.length !== width) {
|
|
336
|
+
throw new WebGpuGraphError(
|
|
337
|
+
"E_INVALID_ARGUMENT",
|
|
338
|
+
`UniformBlock ${this.name}: field "${field.name}" expects ${width} components`,
|
|
339
|
+
{
|
|
340
|
+
argument: field.name,
|
|
341
|
+
value,
|
|
342
|
+
expected: `an array of ${width} numbers`,
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const lanes: number[] = [];
|
|
347
|
+
for (let lane = 0; lane < width; lane++) {
|
|
348
|
+
const component: unknown = value[lane];
|
|
349
|
+
assertScalar(field.name, scalar, component);
|
|
350
|
+
lanes.push(component);
|
|
351
|
+
}
|
|
352
|
+
return lanes;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Reads every field at `byteOffset` (storage mode's reader; also used by tests on uniform blocks).
|
|
357
|
+
* @param view - the source view
|
|
358
|
+
* @param byteOffset - the region's start inside the view
|
|
359
|
+
* @returns the field values (vectors as arrays)
|
|
360
|
+
*/
|
|
361
|
+
read(view: DataView, byteOffset?: number): UniformValues {
|
|
362
|
+
const base = this.regionStart(view, byteOffset);
|
|
363
|
+
const out: Record<string, number | readonly number[]> = {};
|
|
364
|
+
for (const field of this.laidOut) {
|
|
365
|
+
out[field.name] = this.decode(view, field, base);
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Reads one field.
|
|
372
|
+
* @param view - the source view
|
|
373
|
+
* @param field - the field name
|
|
374
|
+
* @param byteOffset - the region's start inside the view
|
|
375
|
+
* @returns the value (a number, or an array for a vector)
|
|
376
|
+
*/
|
|
377
|
+
readField(view: DataView, field: string, byteOffset?: number): number | readonly number[] {
|
|
378
|
+
const layout = this.fieldLayout(field);
|
|
379
|
+
return this.decode(view, layout, this.regionStart(view, byteOffset));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The laid-out field of a name.
|
|
384
|
+
* @param field - the field name
|
|
385
|
+
* @returns the layout
|
|
386
|
+
*/
|
|
387
|
+
private fieldLayout(field: string): FieldLayout {
|
|
388
|
+
const layout = this.table.get(field);
|
|
389
|
+
if (layout === undefined) {
|
|
390
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${this.name}: unknown field "${field}"`, {
|
|
391
|
+
argument: "field",
|
|
392
|
+
value: field,
|
|
393
|
+
expected: this.laidOut.map((f) => f.name).join(", "),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return layout;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Validates the region `[byteOffset, byteOffset + byteLength)` against the view.
|
|
401
|
+
* @param view - the view
|
|
402
|
+
* @param byteOffset - the requested start (default 0)
|
|
403
|
+
* @returns the start
|
|
404
|
+
*/
|
|
405
|
+
private regionStart(view: DataView, byteOffset: number | undefined): number {
|
|
406
|
+
const base = byteOffset ?? 0;
|
|
407
|
+
if (!Number.isInteger(base) || base < 0 || base + this.byteLength > view.byteLength) {
|
|
408
|
+
throw new WebGpuGraphError(
|
|
409
|
+
"E_INVALID_ARGUMENT",
|
|
410
|
+
`UniformBlock ${this.name}: region [${base}, ${base + this.byteLength}) is outside the view`,
|
|
411
|
+
{
|
|
412
|
+
argument: "byteOffset",
|
|
413
|
+
value: base,
|
|
414
|
+
expected: `0 <= byteOffset <= ${view.byteLength - this.byteLength}`,
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return base;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Decodes one field from a view.
|
|
423
|
+
* @param view - the source view
|
|
424
|
+
* @param field - the laid-out field
|
|
425
|
+
* @param base - the region start
|
|
426
|
+
* @returns the value
|
|
427
|
+
*/
|
|
428
|
+
private decode(view: DataView, field: FieldLayout, base: number): number | readonly number[] {
|
|
429
|
+
const { scalar, width } = field.info;
|
|
430
|
+
if (width === 1) {
|
|
431
|
+
return getScalar(view, scalar, base + field.offset);
|
|
432
|
+
}
|
|
433
|
+
const lanes: number[] = [];
|
|
434
|
+
for (let lane = 0; lane < width; lane++) {
|
|
435
|
+
lanes.push(getScalar(view, scalar, base + field.offset + 4 * lane));
|
|
436
|
+
}
|
|
437
|
+
return lanes;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The UniformRing of spec 5.3: one UNIFORM buffer holding UNIFORM_SLOT_BYTES-stride slots for the
|
|
3
|
+
* per-iteration params of a batch. A batch reserves k contiguous slots, writes their values into the
|
|
4
|
+
* host shadow, flushes them with ONE queue.writeBuffer and records k dispatches that select their slot
|
|
5
|
+
* through a dynamic offset -- no per-iteration writeBuffer and no host round trip. The stride is 256
|
|
6
|
+
* because minUniformBufferOffsetAlignment is 256 on every runtime the package targets (the Dawn-node
|
|
7
|
+
* ADAPTER advertises 64, which the package never requests; spec 2.6).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { UNIFORM_SLOT_BYTES } from "../constants.js";
|
|
11
|
+
import type { AllocationTracker } from "../device/error-scope.js";
|
|
12
|
+
import { BufferUsage } from "../device/webgpu-constants.js";
|
|
13
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
14
|
+
import type { Binding } from "../types/memory.js";
|
|
15
|
+
import type { UniformBlock, UniformValues } from "./struct-block.js";
|
|
16
|
+
|
|
17
|
+
/** One UNIFORM buffer with UNIFORM_SLOT_BYTES-stride slots for the per-iteration params of a batch (spec 5.3). */
|
|
18
|
+
export class UniformRing {
|
|
19
|
+
/** Number of slots. */
|
|
20
|
+
readonly slots: number;
|
|
21
|
+
private readonly device: GPUDevice;
|
|
22
|
+
private readonly allocator: AllocationTracker;
|
|
23
|
+
private readonly buffer: GPUBuffer;
|
|
24
|
+
private readonly label: string;
|
|
25
|
+
private readonly shadow: ArrayBuffer;
|
|
26
|
+
private readonly view: DataView;
|
|
27
|
+
private next = 0;
|
|
28
|
+
private dirtyLo = -1;
|
|
29
|
+
private dirtyHi = -1;
|
|
30
|
+
private destroyed = false;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates the ring buffer (`slots x UNIFORM_SLOT_BYTES` bytes, UNIFORM | COPY_DST) through the allocator.
|
|
34
|
+
* @param device - the device the buffer belongs to
|
|
35
|
+
* @param allocator - the context's OOM-scoped allocator (the buffer is destroyed through it)
|
|
36
|
+
* @param slots - number of slots; E_INVALID_ARGUMENT unless a positive integer
|
|
37
|
+
* @param label - the buffer label
|
|
38
|
+
*/
|
|
39
|
+
constructor(device: GPUDevice, allocator: AllocationTracker, slots: number, label: string) {
|
|
40
|
+
if (!Number.isInteger(slots) || slots < 1) {
|
|
41
|
+
throw new WebGpuGraphError(
|
|
42
|
+
"E_INVALID_ARGUMENT",
|
|
43
|
+
`UniformRing "${label}": slots must be a positive integer, got ${slots}`,
|
|
44
|
+
{
|
|
45
|
+
argument: "slots",
|
|
46
|
+
value: slots,
|
|
47
|
+
expected: "an integer >= 1",
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
this.device = device;
|
|
52
|
+
this.allocator = allocator;
|
|
53
|
+
this.slots = slots;
|
|
54
|
+
this.label = label;
|
|
55
|
+
this.shadow = new ArrayBuffer(slots * UNIFORM_SLOT_BYTES);
|
|
56
|
+
this.view = new DataView(this.shadow);
|
|
57
|
+
this.buffer = allocator.createBuffer({
|
|
58
|
+
label,
|
|
59
|
+
size: slots * UNIFORM_SLOT_BYTES,
|
|
60
|
+
usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The whole-buffer binding a kernel binds once (size = the block's byteLength; the slot is the dynamic offset).
|
|
66
|
+
* @param block - the params block the kernel declares for this binding
|
|
67
|
+
* @returns the binding at offset 0 of the ring buffer
|
|
68
|
+
*/
|
|
69
|
+
binding(block: UniformBlock): Binding {
|
|
70
|
+
this.assertLive();
|
|
71
|
+
this.assertFits(block);
|
|
72
|
+
return { buffer: this.buffer, offset: 0, size: block.byteLength, window: null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Byte offset of a slot (the dynamic offset a dispatch passes for it).
|
|
77
|
+
* @param slot - a slot index in [0, slots)
|
|
78
|
+
* @returns slot x UNIFORM_SLOT_BYTES
|
|
79
|
+
*/
|
|
80
|
+
offsetOf(slot: number): number {
|
|
81
|
+
this.checkSlot(slot);
|
|
82
|
+
return slot * UNIFORM_SLOT_BYTES;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Reserves `count` contiguous slots for a batch, wrapping to 0 when the tail is too short; E_INVALID_ARGUMENT when count > slots.
|
|
87
|
+
* @param count - slots needed, in [1, slots]
|
|
88
|
+
* @returns the first slot of the reservation
|
|
89
|
+
*/
|
|
90
|
+
reserve(count: number): number {
|
|
91
|
+
this.assertLive();
|
|
92
|
+
if (!Number.isInteger(count) || count < 1 || count > this.slots) {
|
|
93
|
+
throw new WebGpuGraphError(
|
|
94
|
+
"E_INVALID_ARGUMENT",
|
|
95
|
+
`UniformRing "${this.label}": cannot reserve ${count} of ${this.slots} slots`,
|
|
96
|
+
{
|
|
97
|
+
argument: "count",
|
|
98
|
+
value: count,
|
|
99
|
+
expected: `an integer in [1, ${this.slots}]`,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (this.next + count > this.slots) {
|
|
104
|
+
this.next = 0;
|
|
105
|
+
}
|
|
106
|
+
const first = this.next;
|
|
107
|
+
this.next += count;
|
|
108
|
+
return first;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Writes one block's values into a slot of the host shadow; `flush()` sends the dirty range with one writeBuffer.
|
|
113
|
+
* @param slot - the slot to fill
|
|
114
|
+
* @param block - the block whose layout the bytes follow
|
|
115
|
+
* @param values - the field values (a missing field is written as 0 by the block)
|
|
116
|
+
*/
|
|
117
|
+
write(slot: number, block: UniformBlock, values: UniformValues): void {
|
|
118
|
+
this.checkSlot(slot);
|
|
119
|
+
this.assertFits(block);
|
|
120
|
+
block.write(this.view, values, slot * UNIFORM_SLOT_BYTES);
|
|
121
|
+
if (this.dirtyLo < 0 || slot < this.dirtyLo) {
|
|
122
|
+
this.dirtyLo = slot;
|
|
123
|
+
}
|
|
124
|
+
if (slot > this.dirtyHi) {
|
|
125
|
+
this.dirtyHi = slot;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* queue.writeBuffer of the dirty slots (called by the batch driver before submit): one call covering
|
|
131
|
+
* [lowest dirty slot, highest dirty slot]; nothing when no slot is dirty.
|
|
132
|
+
*/
|
|
133
|
+
flush(): void {
|
|
134
|
+
this.assertLive();
|
|
135
|
+
if (this.dirtyLo < 0) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const begin = this.dirtyLo * UNIFORM_SLOT_BYTES;
|
|
139
|
+
const end = (this.dirtyHi + 1) * UNIFORM_SLOT_BYTES;
|
|
140
|
+
this.device.queue.writeBuffer(this.buffer, begin, this.shadow, begin, end - begin);
|
|
141
|
+
this.dirtyLo = -1;
|
|
142
|
+
this.dirtyHi = -1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Destroys the buffer (through the allocator); idempotent. */
|
|
146
|
+
destroy(): void {
|
|
147
|
+
if (this.destroyed) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.destroyed = true;
|
|
151
|
+
this.allocator.destroy(this.buffer);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private assertLive(): void {
|
|
155
|
+
if (this.destroyed) {
|
|
156
|
+
throw new WebGpuGraphError("E_DISPOSED", `UniformRing "${this.label}" is destroyed`, { label: this.label });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private checkSlot(slot: number): void {
|
|
161
|
+
this.assertLive();
|
|
162
|
+
if (!Number.isInteger(slot) || slot < 0 || slot >= this.slots) {
|
|
163
|
+
throw new WebGpuGraphError(
|
|
164
|
+
"E_INVALID_ARGUMENT",
|
|
165
|
+
`UniformRing "${this.label}": slot ${slot} is outside [0, ${this.slots})`,
|
|
166
|
+
{
|
|
167
|
+
argument: "slot",
|
|
168
|
+
value: slot,
|
|
169
|
+
expected: `an integer in [0, ${this.slots})`,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private assertFits(block: UniformBlock): void {
|
|
176
|
+
if (block.byteLength > UNIFORM_SLOT_BYTES) {
|
|
177
|
+
throw new WebGpuGraphError(
|
|
178
|
+
"E_INVALID_ARGUMENT",
|
|
179
|
+
`UniformRing "${this.label}": block ${block.name} is ${block.byteLength} bytes, a slot holds ${UNIFORM_SLOT_BYTES}`,
|
|
180
|
+
{ argument: "block", value: block.name, expected: `byteLength <= ${UNIFORM_SLOT_BYTES}` },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|