pelican-engine 0.0.0-stage → 0.3.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/AGENTS.md +147 -0
- package/LICENSE +22 -0
- package/README.md +129 -2
- package/distribution.json +197 -0
- package/docs/AI-WORKFLOW.md +517 -0
- package/docs/ARCHITECTURE.md +268 -0
- package/docs/CAPABILITIES.md +264 -0
- package/docs/DECISIONS.md +107 -0
- package/docs/M3-USAGE.md +192 -0
- package/docs/PELICAN-MIGRATION.md +38 -0
- package/docs/RENDER-BACKENDS.md +50 -0
- package/docs/ROADMAP.md +239 -0
- package/docs/THIRD-PARTY-NOTICES.md +151 -0
- package/docs/api/REFERENCE.md +1435 -0
- package/docs/api/index.json +1139 -0
- package/docs/npm/DISTRIBUTION.md +130 -0
- package/docs/npm/GAME-AUTHOR.md +147 -0
- package/engine.manifest.json +15 -0
- package/lib/assets/audio.js +67 -0
- package/lib/assets/errors.js +36 -0
- package/lib/assets/glb.js +277 -0
- package/lib/assets/index.js +53 -0
- package/lib/assets/jpeg.js +199 -0
- package/lib/assets/model.js +558 -0
- package/lib/assets/optimize.js +95 -0
- package/lib/assets/texture.js +187 -0
- package/lib/compiler/distribution.js +17 -0
- package/lib/compiler/extract.js +425 -0
- package/lib/compiler/graph.js +366 -0
- package/lib/compiler/index.js +574 -0
- package/lib/compiler/normalize.js +1194 -0
- package/lib/compiler/source-map.js +25 -0
- package/lib/compiler/types.js +2 -0
- package/lib/compiler/version.js +93 -0
- package/lib/experiments/simulation/index.js +2 -0
- package/lib/experiments/simulation/math.js +41 -0
- package/lib/experiments/simulation/probe.js +215 -0
- package/lib/experiments/simulation/scenes.js +192 -0
- package/lib/experiments/simulation/solver.js +346 -0
- package/lib/experiments/simulation/types.js +1 -0
- package/lib/experiments/world/adapters.js +30 -0
- package/lib/experiments/world/geometry.js +180 -0
- package/lib/experiments/world/index.js +2 -0
- package/lib/experiments/world/probe.js +288 -0
- package/lib/experiments/world/types.js +1 -0
- package/lib/experiments/world/world.js +599 -0
- package/lib/navigation/index.js +157 -0
- package/lib/physics/index.js +909 -0
- package/lib/protocol/index.js +92 -0
- package/lib/renderer/acceleration.js +609 -0
- package/lib/renderer/browser.js +2015 -0
- package/lib/renderer/bsdf.js +260 -0
- package/lib/renderer/checkpoint.js +88 -0
- package/lib/renderer/compute-executor.js +1034 -0
- package/lib/renderer/compute-texture-assets.js +49 -0
- package/lib/renderer/compute-view-contract.js +1 -0
- package/lib/renderer/denoiser.js +361 -0
- package/lib/renderer/dielectric.js +360 -0
- package/lib/renderer/embedded-image-pixels.js +368 -0
- package/lib/renderer/emissive-lights.js +67 -0
- package/lib/renderer/environment-importance.js +55 -0
- package/lib/renderer/environment.js +346 -0
- package/lib/renderer/gameplay.js +464 -0
- package/lib/renderer/geometry.js +103 -0
- package/lib/renderer/graph-expressions.js +485 -0
- package/lib/renderer/graph-tsl.js +141 -0
- package/lib/renderer/graph.js +214 -0
- package/lib/renderer/managed-texture-sampling.js +29 -0
- package/lib/renderer/material-graphs.js +297 -0
- package/lib/renderer/medium.js +348 -0
- package/lib/renderer/models.js +1053 -0
- package/lib/renderer/motion-shader.js +40 -0
- package/lib/renderer/path-shader.js +196 -0
- package/lib/renderer/pathtrace.js +324 -0
- package/lib/renderer/postprocess-contract.js +1 -0
- package/lib/renderer/postprocess.js +494 -0
- package/lib/renderer/render-buffers.js +132 -0
- package/lib/renderer/render-textures.js +155 -0
- package/lib/renderer/startup.js +50 -0
- package/lib/renderer/surface-capture.js +199 -0
- package/lib/renderer/surface-gpu.js +270 -0
- package/lib/renderer/surface-motion.js +84 -0
- package/lib/renderer/surface-ray-shader.js +91 -0
- package/lib/renderer/surface-shader.js +88 -0
- package/lib/renderer/surface-tracer.js +1720 -0
- package/lib/renderer/surface.js +1425 -0
- package/lib/renderer/temporal-filter.js +512 -0
- package/lib/renderer/texture-pixels.js +207 -0
- package/lib/renderer/traversal-nodes.js +54 -0
- package/lib/renderer/ui.js +446 -0
- package/lib/renderer/vertex-bounds.js +127 -0
- package/lib/renderer/vertex-contract.js +1 -0
- package/lib/renderer/vertex-graphs.js +197 -0
- package/lib/renderer/volume-shader.js +50 -0
- package/lib/runtime/compute-contract.js +1 -0
- package/lib/runtime/compute-service.js +674 -0
- package/lib/runtime/contracts.js +1 -0
- package/lib/runtime/error.js +37 -0
- package/lib/runtime/events.js +189 -0
- package/lib/runtime/game-services.js +772 -0
- package/lib/runtime/host.js +839 -0
- package/lib/runtime/material-parameters.js +145 -0
- package/lib/runtime/plugins.js +1 -0
- package/lib/runtime/postprocess-contract.js +1 -0
- package/lib/runtime/postprocess.js +189 -0
- package/lib/runtime/prefabs.js +13 -0
- package/lib/runtime/random.js +88 -0
- package/lib/runtime/render-buffer-contract.js +1 -0
- package/lib/runtime/render-buffers.js +243 -0
- package/lib/runtime/render-capabilities.js +17 -0
- package/lib/runtime/render-settings.js +249 -0
- package/lib/runtime/render-texture-contract.js +1 -0
- package/lib/runtime/render-textures.js +271 -0
- package/lib/runtime/resources.js +648 -0
- package/lib/runtime/scheduler.js +765 -0
- package/lib/runtime/storage.js +169 -0
- package/lib/runtime/world.js +594 -0
- package/lib/schema/index.js +24 -0
- package/lib/sdk/graph-values.js +16 -0
- package/lib/sdk/index.d.ts +800 -0
- package/lib/sdk/index.js +82 -0
- package/lib/tools/api-index.js +113 -0
- package/lib/tools/browser-session.js +89 -0
- package/lib/tools/cli.js +513 -0
- package/lib/tools/compress-dist.js +59 -0
- package/lib/tools/control-client.js +153 -0
- package/lib/tools/control-server.js +633 -0
- package/lib/tools/diagnostic-code.js +3 -0
- package/lib/tools/doctor.js +496 -0
- package/lib/tools/npm/browser-cli.js +6 -0
- package/lib/tools/npm/entry.js +40 -0
- package/lib/tools/npm/layout.js +195 -0
- package/lib/tools/setup.js +153 -0
- package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
- package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@gltf-transform/core/LICENSE.md +21 -0
- package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
- package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
- package/licenses/@img/colour/LICENSE.md +82 -0
- package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
- package/licenses/@oxc-project/types/LICENSE +22 -0
- package/licenses/@rolldown/pluginutils/LICENSE +21 -0
- package/licenses/@tweenjs/tween.js/LICENSE +23 -0
- package/licenses/@types/ndarray/LICENSE +21 -0
- package/licenses/@types/node/LICENSE +21 -0
- package/licenses/@types/stats.js/LICENSE +21 -0
- package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@types/three/LICENSE +21 -0
- package/licenses/@types/webxr/LICENSE +21 -0
- package/licenses/@webgpu/types/LICENSE +26 -0
- package/licenses/THIRD-PARTY-NOTICES.md +139 -0
- package/licenses/cwise-compiler/LICENSE +22 -0
- package/licenses/detect-libc/LICENSE +201 -0
- package/licenses/esbuild/LICENSE.md +21 -0
- package/licenses/fast-png/LICENSE +21 -0
- package/licenses/fdir/LICENSE +7 -0
- package/licenses/fflate/LICENSE +21 -0
- package/licenses/iobuffer/LICENSE +22 -0
- package/licenses/iota-array/LICENSE +22 -0
- package/licenses/is-buffer/LICENSE +21 -0
- package/licenses/ktx-parse/LICENSE +21 -0
- package/licenses/lightningcss/LICENSE +373 -0
- package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
- package/licenses/meshoptimizer/LICENSE.md +21 -0
- package/licenses/nanoid/LICENSE +20 -0
- package/licenses/ndarray/LICENSE +22 -0
- package/licenses/ndarray-lanczos/LICENSE +21 -0
- package/licenses/ndarray-ops/LICENSE +22 -0
- package/licenses/ndarray-pixels/LICENSE +21 -0
- package/licenses/ngraph.path/LICENSE +21 -0
- package/licenses/picocolors/LICENSE +15 -0
- package/licenses/picomatch/LICENSE +21 -0
- package/licenses/playwright/LICENSE +202 -0
- package/licenses/playwright/NOTICE +5 -0
- package/licenses/playwright-core/LICENSE +202 -0
- package/licenses/playwright-core/NOTICE +5 -0
- package/licenses/postcss/LICENSE +20 -0
- package/licenses/prettier/LICENSE +7 -0
- package/licenses/property-graph/LICENSE +21 -0
- package/licenses/rolldown/LICENSE +25 -0
- package/licenses/semver/LICENSE +15 -0
- package/licenses/sharp/LICENSE +191 -0
- package/licenses/source-map-js/LICENSE +28 -0
- package/licenses/three/LICENSE +21 -0
- package/licenses/tinyglobby/LICENSE +21 -0
- package/licenses/tsx/LICENSE +21 -0
- package/licenses/typescript/LICENSE.txt +55 -0
- package/licenses/undici-types/LICENSE +21 -0
- package/licenses/uniq/LICENSE +22 -0
- package/licenses/vite/LICENSE.md +2084 -0
- package/package.json +49 -4
- package/web/index.html +34 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
import { graphValue, isGraphType } from "../sdk/graph-values.js";
|
|
2
|
+
import { lowerGraphWGSL } from "./graph-expressions.js";
|
|
3
|
+
const DEADLINE_MS = 10_000;
|
|
4
|
+
// Every exposed index and array length must be represented exactly by f32.
|
|
5
|
+
const MAX_COUNT = 2 ** 24;
|
|
6
|
+
const own = (value, name) => Object.hasOwn(value, name);
|
|
7
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
const error = (code, detail) => Error("TC_COMPUTE_" + code + ": " + detail);
|
|
9
|
+
function requireThat(value, code, detail) {
|
|
10
|
+
if (!value)
|
|
11
|
+
throw error(code, detail);
|
|
12
|
+
}
|
|
13
|
+
const width = (type) => type.startsWith("vec") ? Number(type[3]) : 1;
|
|
14
|
+
const unpack = (symbol, type) => type === "bool"
|
|
15
|
+
? "(" + symbol + ".x != 0.0)"
|
|
16
|
+
: symbol + "." + "xyzw".slice(0, width(type));
|
|
17
|
+
const packedResult = (symbol, type) => type === "bool"
|
|
18
|
+
? "vec4f(select(0.0, 1.0, " + symbol + "), 0.0, 0.0, 0.0)"
|
|
19
|
+
: type === "float"
|
|
20
|
+
? "vec4f(" + symbol + ", 0.0, 0.0, 0.0)"
|
|
21
|
+
: type === "vec2"
|
|
22
|
+
? "vec4f(" + symbol + ", 0.0, 0.0)"
|
|
23
|
+
: type === "vec3"
|
|
24
|
+
? "vec4f(" + symbol + ", 0.0)"
|
|
25
|
+
: symbol;
|
|
26
|
+
function put(data, offset, type, value, code) {
|
|
27
|
+
const checked = graphValue(type, value);
|
|
28
|
+
requireThat(checked !== null, code, "Expected a finite " + type + " value.");
|
|
29
|
+
if (typeof checked === "number" || typeof checked === "boolean")
|
|
30
|
+
data[offset] = Number(checked);
|
|
31
|
+
else
|
|
32
|
+
data.set(checked, offset);
|
|
33
|
+
}
|
|
34
|
+
function same(a, b) {
|
|
35
|
+
const aa = new Uint32Array(a.buffer, a.byteOffset, a.length);
|
|
36
|
+
const bb = new Uint32Array(b.buffer, b.byteOffset, b.length);
|
|
37
|
+
return aa.length === bb.length && aa.every((n, i) => n === bb[i]);
|
|
38
|
+
}
|
|
39
|
+
// rgba32float is deliberately unfilterable: filtering and addressing use textureLoad.
|
|
40
|
+
function textureHelper(i, binding, declaration) {
|
|
41
|
+
const normalize = (value, wrap = "clamp") => wrap === "repeat"
|
|
42
|
+
? `(${value} - floor(${value}))`
|
|
43
|
+
: wrap === "mirror"
|
|
44
|
+
? `(1.0 - abs((${value} - 2.0 * floor(${value} * 0.5)) - 1.0))`
|
|
45
|
+
: `clamp(${value}, 0.0, 1.0)`;
|
|
46
|
+
const address = (axis, wrap = "clamp") => {
|
|
47
|
+
const index = `index.${axis}`, size = `size.${axis}`;
|
|
48
|
+
if (wrap === "clamp")
|
|
49
|
+
return `clamp(${index}, 0, ${size} - 1)`;
|
|
50
|
+
const period = wrap === "mirror" ? `(2 * ${size})` : size;
|
|
51
|
+
const mod = `((${index} % ${period} + ${period}) % ${period})`;
|
|
52
|
+
return wrap === "mirror" ? `min(${mod}, 2 * ${size} - 1 - ${mod})` : mod;
|
|
53
|
+
};
|
|
54
|
+
return [
|
|
55
|
+
`@group(0) @binding(${binding}) var tc_texture_${i}: texture_2d<f32>;`,
|
|
56
|
+
`fn tc_load_${i}(index: vec2i, size: vec2i) -> vec4f {`,
|
|
57
|
+
` let coord = vec2i(${address("x", declaration.wrapU)}, ${address("y", declaration.wrapV)});`,
|
|
58
|
+
` return textureLoad(tc_texture_${i}, coord, 0);`,
|
|
59
|
+
`}`,
|
|
60
|
+
`fn tc_sample_${i}(uv: vec2f) -> vec4f {`,
|
|
61
|
+
` if (any((bitcast<vec2u>(uv) & vec2u(0x7f800000u)) == vec2u(0x7f800000u))) {`,
|
|
62
|
+
` atomicOr(&tc_fault, 4u);`,
|
|
63
|
+
` return vec4f(0.0);`,
|
|
64
|
+
` }`,
|
|
65
|
+
// Normalize finite UVs before multiplying or converting to i32, including huge f32 values.
|
|
66
|
+
` let addressed = vec2f(${normalize("uv.x", declaration.wrapU)}, ${normalize("uv.y", declaration.wrapV)});`,
|
|
67
|
+
` let size = vec2i(textureDimensions(tc_texture_${i}, 0));`,
|
|
68
|
+
...(declaration.filter === "nearest"
|
|
69
|
+
? [` return tc_load_${i}(vec2i(floor(addressed * vec2f(size))), size);`]
|
|
70
|
+
: [
|
|
71
|
+
` let pixel = addressed * vec2f(size) - vec2f(0.5);`,
|
|
72
|
+
` let base = vec2i(floor(pixel));`,
|
|
73
|
+
` let weight = fract(pixel);`,
|
|
74
|
+
` let top = mix(tc_load_${i}(base, size), tc_load_${i}(base + vec2i(1, 0), size), weight.x);`,
|
|
75
|
+
` let bottom = mix(tc_load_${i}(base + vec2i(0, 1), size), tc_load_${i}(base + vec2i(1, 1), size), weight.x);`,
|
|
76
|
+
` return mix(top, bottom, weight.y);`,
|
|
77
|
+
]),
|
|
78
|
+
`}`,
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
const sameShape = (a, b) => a?.width === b?.width && a?.height === b?.height;
|
|
82
|
+
/** Private renderer driver. Only the runtime owner commits the returned values. */
|
|
83
|
+
export function createComputeExecutor(device) {
|
|
84
|
+
const cache = new Map();
|
|
85
|
+
const pending = new Map();
|
|
86
|
+
const committed = new Map();
|
|
87
|
+
// Keep the highest CPU checkpoint/commit metadata when a captured older input replaces the upload cache.
|
|
88
|
+
const highWater = new Map();
|
|
89
|
+
const remember = (snapshot, computed) => {
|
|
90
|
+
if (snapshot.revision > (highWater.get(snapshot.key)?.revision ?? -1))
|
|
91
|
+
highWater.set(snapshot.key, {
|
|
92
|
+
key: snapshot.key,
|
|
93
|
+
type: snapshot.type,
|
|
94
|
+
revision: snapshot.revision,
|
|
95
|
+
data: snapshot.data,
|
|
96
|
+
texture: snapshot.texture,
|
|
97
|
+
computed,
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const pipelines = new Map();
|
|
101
|
+
const owned = new Set();
|
|
102
|
+
const ownedTextures = new Set();
|
|
103
|
+
let disposed = false, lost = false, allocations = 0, uploads = 0, dispatches = 0, retentions = 0, commits = 0, residentHits = 0, inputCacheHits = 0, inputUploads = 0, checkpointUploads = 0, textureUploads = 0, textureResidentHits = 0, storageTextureDispatches = 0;
|
|
104
|
+
let running;
|
|
105
|
+
const destroy = (resource) => {
|
|
106
|
+
if (owned.delete(resource) ||
|
|
107
|
+
ownedTextures.delete(resource))
|
|
108
|
+
resource.destroy();
|
|
109
|
+
};
|
|
110
|
+
const destroyEntry = (entry) => {
|
|
111
|
+
destroy(entry.buffer);
|
|
112
|
+
if (entry.gpuTexture)
|
|
113
|
+
destroy(entry.gpuTexture);
|
|
114
|
+
};
|
|
115
|
+
const evict = (key) => {
|
|
116
|
+
const entry = cache.get(key);
|
|
117
|
+
if (entry) {
|
|
118
|
+
cache.delete(key);
|
|
119
|
+
destroyEntry(entry);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const discard = (key) => {
|
|
123
|
+
const candidate = pending.get(key);
|
|
124
|
+
if (candidate) {
|
|
125
|
+
pending.delete(key);
|
|
126
|
+
candidate.signal.removeEventListener("abort", candidate.abort);
|
|
127
|
+
destroyEntry(candidate);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const releaseVersions = (key) => {
|
|
131
|
+
const versions = committed.get(key);
|
|
132
|
+
committed.delete(key);
|
|
133
|
+
if (versions)
|
|
134
|
+
for (const entry of versions.values())
|
|
135
|
+
destroyEntry(entry);
|
|
136
|
+
};
|
|
137
|
+
const clear = () => {
|
|
138
|
+
for (const key of pending.keys())
|
|
139
|
+
discard(key);
|
|
140
|
+
for (const key of committed.keys())
|
|
141
|
+
releaseVersions(key);
|
|
142
|
+
for (const key of cache.keys())
|
|
143
|
+
evict(key);
|
|
144
|
+
pipelines.clear();
|
|
145
|
+
highWater.clear();
|
|
146
|
+
for (const buffer of owned)
|
|
147
|
+
destroy(buffer);
|
|
148
|
+
for (const texture of ownedTextures)
|
|
149
|
+
destroy(texture);
|
|
150
|
+
};
|
|
151
|
+
const available = () => {
|
|
152
|
+
requireThat(!disposed, "DISPOSED", "The compute executor has been disposed.");
|
|
153
|
+
requireThat(!lost, "DEVICE_LOST", "The compute device was lost.");
|
|
154
|
+
};
|
|
155
|
+
const latest = (key) => {
|
|
156
|
+
let entry = highWater.get(key);
|
|
157
|
+
const cached = cache.get(key);
|
|
158
|
+
if (cached && (!entry || cached.revision > entry.revision))
|
|
159
|
+
entry = cached;
|
|
160
|
+
for (const version of committed.get(key)?.values() ?? [])
|
|
161
|
+
if (!entry || version.revision > entry.revision)
|
|
162
|
+
entry = version;
|
|
163
|
+
return entry;
|
|
164
|
+
};
|
|
165
|
+
const checkSnapshot = (snapshot) => {
|
|
166
|
+
const previous = latest(snapshot.key);
|
|
167
|
+
const exact = committed.get(snapshot.key)?.get(snapshot.revision) ??
|
|
168
|
+
(cache.get(snapshot.key)?.revision === snapshot.revision
|
|
169
|
+
? cache.get(snapshot.key)
|
|
170
|
+
: undefined);
|
|
171
|
+
const watermark = highWater.get(snapshot.key);
|
|
172
|
+
const known = exact ??
|
|
173
|
+
(watermark?.revision === snapshot.revision ? watermark : undefined);
|
|
174
|
+
if (previous) {
|
|
175
|
+
requireThat(previous.type === snapshot.type &&
|
|
176
|
+
previous.data.length === snapshot.data.length &&
|
|
177
|
+
sameShape(previous.texture, snapshot.texture), "BINDING", "A logical input key cannot change type, size or texture shape before release.");
|
|
178
|
+
requireThat((!known || same(known.data, snapshot.data)) &&
|
|
179
|
+
(!!exact ||
|
|
180
|
+
snapshot.revision > previous.revision ||
|
|
181
|
+
snapshot.captured === true ||
|
|
182
|
+
(watermark?.revision === snapshot.revision && !watermark.computed)), "REVISION", "Input revisions must increase when packed content changes.");
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const textureShape = (value, type, count) => {
|
|
186
|
+
if (value === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
requireThat(record(value) &&
|
|
189
|
+
Number.isSafeInteger(value.width) &&
|
|
190
|
+
Number.isSafeInteger(value.height) &&
|
|
191
|
+
value.width > 0 &&
|
|
192
|
+
value.height > 0 &&
|
|
193
|
+
type === "vec4" &&
|
|
194
|
+
value.width * value.height === count &&
|
|
195
|
+
count <= MAX_COUNT, "TEXTURE", "Textures require vec4 values and positive dimensions matching the texel count.");
|
|
196
|
+
const maximum = device.limits.maxTextureDimension2D;
|
|
197
|
+
requireThat(Number.isFinite(maximum) &&
|
|
198
|
+
value.width <= maximum &&
|
|
199
|
+
value.height <= maximum, "LIMIT", "Texture dimensions exceed maxTextureDimension2D.");
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
width: value.width,
|
|
202
|
+
height: value.height,
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
const snapshotInput = (input) => {
|
|
206
|
+
requireThat(record(input) &&
|
|
207
|
+
typeof input.key === "string" &&
|
|
208
|
+
input.key.length &&
|
|
209
|
+
isGraphType(input.type) &&
|
|
210
|
+
Number.isSafeInteger(input.revision) &&
|
|
211
|
+
input.revision >= 0 &&
|
|
212
|
+
Array.isArray(input.values) &&
|
|
213
|
+
input.values.length >= 1 &&
|
|
214
|
+
input.values.length <= MAX_COUNT, "BINDING", "Invalid input identity, revision, type or count.");
|
|
215
|
+
const texture = textureShape(input.texture, input.type, input.values.length);
|
|
216
|
+
const bytes = input.values.length * 16;
|
|
217
|
+
requireThat(bytes <= device.limits.maxBufferSize &&
|
|
218
|
+
bytes <= device.limits.maxStorageBufferBindingSize, "LIMIT", "The input does not fit device storage limits.");
|
|
219
|
+
const data = new Float32Array(input.values.length * 4);
|
|
220
|
+
for (let i = 0; i < input.values.length; i++)
|
|
221
|
+
put(data, i * 4, input.type, input.values[i], "VALUE");
|
|
222
|
+
const snapshot = {
|
|
223
|
+
key: input.key,
|
|
224
|
+
type: input.type,
|
|
225
|
+
revision: input.revision,
|
|
226
|
+
data,
|
|
227
|
+
texture,
|
|
228
|
+
captured: input.captured,
|
|
229
|
+
};
|
|
230
|
+
checkSnapshot(snapshot);
|
|
231
|
+
return snapshot;
|
|
232
|
+
};
|
|
233
|
+
// All data sources can also be copied into renderer-owned Three storage.
|
|
234
|
+
const storageUsage = () => GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST;
|
|
235
|
+
const exactBuffer = (snapshot) => {
|
|
236
|
+
const version = committed.get(snapshot.key)?.get(snapshot.revision);
|
|
237
|
+
if (version) {
|
|
238
|
+
residentHits++;
|
|
239
|
+
return version.buffer;
|
|
240
|
+
}
|
|
241
|
+
const input = cache.get(snapshot.key);
|
|
242
|
+
if (input?.revision === snapshot.revision) {
|
|
243
|
+
inputCacheHits++;
|
|
244
|
+
return input.buffer;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const makeTexture = (shape, output) => {
|
|
248
|
+
const texture = device.createTexture({
|
|
249
|
+
label: "pelican managed compute texture",
|
|
250
|
+
size: { width: shape.width, height: shape.height, depthOrArrayLayers: 1 },
|
|
251
|
+
dimension: "2d",
|
|
252
|
+
format: "rgba32float",
|
|
253
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC |
|
|
254
|
+
(output
|
|
255
|
+
? GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_SRC
|
|
256
|
+
: GPUTextureUsage.COPY_DST),
|
|
257
|
+
});
|
|
258
|
+
ownedTextures.add(texture);
|
|
259
|
+
allocations++;
|
|
260
|
+
return texture;
|
|
261
|
+
};
|
|
262
|
+
const uploadTexture = (entry) => {
|
|
263
|
+
if (entry.texture && entry.gpuTexture) {
|
|
264
|
+
device.queue.writeTexture({ texture: entry.gpuTexture }, entry.data, {
|
|
265
|
+
bytesPerRow: entry.texture.width * 16,
|
|
266
|
+
rowsPerImage: entry.texture.height,
|
|
267
|
+
}, {
|
|
268
|
+
width: entry.texture.width,
|
|
269
|
+
height: entry.texture.height,
|
|
270
|
+
depthOrArrayLayers: 1,
|
|
271
|
+
});
|
|
272
|
+
textureUploads++;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const exactEntry = (snapshot) => committed.get(snapshot.key)?.get(snapshot.revision) ??
|
|
276
|
+
(cache.get(snapshot.key)?.revision === snapshot.revision
|
|
277
|
+
? cache.get(snapshot.key)
|
|
278
|
+
: undefined);
|
|
279
|
+
const checkpoint = (snapshot, kind) => {
|
|
280
|
+
checkSnapshot(snapshot);
|
|
281
|
+
const exact = exactEntry(snapshot);
|
|
282
|
+
if (exact) {
|
|
283
|
+
// Preserve the existing buffer hit counters; sampling has its own counter too.
|
|
284
|
+
exactBuffer(snapshot);
|
|
285
|
+
return exact;
|
|
286
|
+
}
|
|
287
|
+
const previous = cache.get(snapshot.key);
|
|
288
|
+
let entry = previous;
|
|
289
|
+
let created = false;
|
|
290
|
+
try {
|
|
291
|
+
if (!entry ||
|
|
292
|
+
kind === "checkpoint" ||
|
|
293
|
+
snapshot.revision < entry.revision) {
|
|
294
|
+
const buffer = device.createBuffer({
|
|
295
|
+
label: "pelican compute " + kind,
|
|
296
|
+
size: snapshot.data.byteLength,
|
|
297
|
+
usage: storageUsage(),
|
|
298
|
+
});
|
|
299
|
+
owned.add(buffer);
|
|
300
|
+
allocations++;
|
|
301
|
+
entry = { ...snapshot, buffer };
|
|
302
|
+
created = true;
|
|
303
|
+
if (snapshot.texture)
|
|
304
|
+
entry.gpuTexture = makeTexture(snapshot.texture, false);
|
|
305
|
+
}
|
|
306
|
+
device.queue.writeBuffer(entry.buffer, 0, snapshot.data);
|
|
307
|
+
entry.data = snapshot.data;
|
|
308
|
+
uploadTexture(entry);
|
|
309
|
+
entry.revision = snapshot.revision;
|
|
310
|
+
if (created) {
|
|
311
|
+
cache.set(snapshot.key, entry);
|
|
312
|
+
if (previous)
|
|
313
|
+
destroyEntry(previous);
|
|
314
|
+
}
|
|
315
|
+
remember(snapshot);
|
|
316
|
+
uploads++;
|
|
317
|
+
if (kind === "input")
|
|
318
|
+
inputUploads++;
|
|
319
|
+
else
|
|
320
|
+
checkpointUploads++;
|
|
321
|
+
return entry;
|
|
322
|
+
}
|
|
323
|
+
catch (failure) {
|
|
324
|
+
if (created && entry)
|
|
325
|
+
destroyEntry(entry);
|
|
326
|
+
else
|
|
327
|
+
evict(snapshot.key);
|
|
328
|
+
throw failure;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
// One device-loss subscription per executor, not one retained closure per dispatch.
|
|
332
|
+
void device.lost.then(() => {
|
|
333
|
+
lost = true;
|
|
334
|
+
running?.stop(error("DEVICE_LOST", "The compute device was lost."));
|
|
335
|
+
clear();
|
|
336
|
+
});
|
|
337
|
+
function prepare(request) {
|
|
338
|
+
requireThat(record(request) &&
|
|
339
|
+
record(request.kernel) &&
|
|
340
|
+
record(request.output) &&
|
|
341
|
+
record(request.inputs) &&
|
|
342
|
+
record(request.parameters) &&
|
|
343
|
+
(request.textures === undefined || record(request.textures)) &&
|
|
344
|
+
request.signal &&
|
|
345
|
+
typeof request.signal.addEventListener === "function" &&
|
|
346
|
+
typeof request.signal.removeEventListener === "function" &&
|
|
347
|
+
typeof request.signal.aborted === "boolean", "REQUEST", "Expected a complete managed compute request and AbortSignal.");
|
|
348
|
+
const { kernel, output, inputs, parameters, time } = request;
|
|
349
|
+
requireThat(typeof kernel.id === "string" &&
|
|
350
|
+
kernel.id.length &&
|
|
351
|
+
typeof output.key === "string" &&
|
|
352
|
+
output.key.length, "REQUEST", "Kernel and output must have identities.");
|
|
353
|
+
requireThat(output.retain === undefined || typeof output.retain === "boolean", "REQUEST", "Output retain must be a boolean when supplied.");
|
|
354
|
+
const count = kernel.count, workgroup = kernel.workgroupSize;
|
|
355
|
+
const limit = (name, amount) => {
|
|
356
|
+
const maximum = device.limits[name];
|
|
357
|
+
requireThat(typeof maximum === "number" &&
|
|
358
|
+
Number.isFinite(maximum) &&
|
|
359
|
+
amount <= maximum, "LIMIT", String(name) + " does not accommodate " + amount + ".");
|
|
360
|
+
};
|
|
361
|
+
requireThat(Number.isSafeInteger(count) &&
|
|
362
|
+
count >= 1 &&
|
|
363
|
+
count <= MAX_COUNT &&
|
|
364
|
+
output.count === count, "COUNT", "Kernel/output counts must agree and fit exact f32 indices.");
|
|
365
|
+
requireThat(Number.isSafeInteger(workgroup) && workgroup >= 1, "WORKGROUP", "Workgroup size must be a positive integer.");
|
|
366
|
+
limit("maxComputeWorkgroupSizeX", workgroup);
|
|
367
|
+
limit("maxComputeInvocationsPerWorkgroup", workgroup);
|
|
368
|
+
limit("maxComputeWorkgroupsPerDimension", Math.ceil(count / workgroup));
|
|
369
|
+
limit("maxBufferSize", count * 16 + 4);
|
|
370
|
+
limit("maxStorageBufferBindingSize", count * 16);
|
|
371
|
+
requireThat(isGraphType(kernel.outputType) &&
|
|
372
|
+
output.type === kernel.outputType &&
|
|
373
|
+
record(kernel.graph) &&
|
|
374
|
+
kernel.graph.outputType === kernel.outputType &&
|
|
375
|
+
Array.isArray(kernel.graph.nodes), "TYPE", "Kernel, graph and output types must agree.");
|
|
376
|
+
const graph = kernel.graph;
|
|
377
|
+
for (const node of graph.nodes) {
|
|
378
|
+
requireThat(record(node), "GRAPH", "Malformed graph node.");
|
|
379
|
+
requireThat(node.op !== "input" ||
|
|
380
|
+
["index", "pixelCoord", "outputSize", "uv"].includes(node.name), "DOMAIN", "Unsupported compute graph input.");
|
|
381
|
+
}
|
|
382
|
+
const outputTexture = textureShape(output.texture, output.type, count);
|
|
383
|
+
const previousOutput = latest(output.key) ?? pending.get(output.key);
|
|
384
|
+
if (previousOutput)
|
|
385
|
+
requireThat(previousOutput.type === output.type &&
|
|
386
|
+
previousOutput.data.length === count * 4 &&
|
|
387
|
+
sameShape(previousOutput.texture, outputTexture), "TYPE", "Output must preserve the resource type, count and texture shape.");
|
|
388
|
+
requireThat(typeof time === "number" &&
|
|
389
|
+
Number.isFinite(time) &&
|
|
390
|
+
Number.isFinite(Math.fround(time)), "PARAMETER", "Time must be finite f32.");
|
|
391
|
+
requireThat(kernel.buffers === undefined || Array.isArray(kernel.buffers), "BINDING", "Invalid buffer declarations.");
|
|
392
|
+
const declarations = kernel.buffers ?? [];
|
|
393
|
+
const names = new Set();
|
|
394
|
+
for (const binding of declarations) {
|
|
395
|
+
requireThat(record(binding) &&
|
|
396
|
+
typeof binding.name === "string" &&
|
|
397
|
+
/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(binding.name) &&
|
|
398
|
+
isGraphType(binding.type) &&
|
|
399
|
+
!names.has(binding.name), "BINDING", "Buffer declarations must be unique and typed.");
|
|
400
|
+
names.add(binding.name);
|
|
401
|
+
}
|
|
402
|
+
requireThat(Object.keys(inputs).length === names.size &&
|
|
403
|
+
Object.keys(inputs).every((n) => names.has(n)), "BINDING", "Supply exactly the kernel named input bindings.");
|
|
404
|
+
requireThat(graph.bufferTypes === undefined || record(graph.bufferTypes), "BINDING", "Invalid graph buffer metadata.");
|
|
405
|
+
for (const [name, type] of Object.entries(graph.bufferTypes ?? {}))
|
|
406
|
+
requireThat(declarations.some((d) => d.name === name && d.type === type), "TYPE", "Graph binding metadata disagrees with the kernel.");
|
|
407
|
+
requireThat(kernel.textures === undefined || Array.isArray(kernel.textures), "TEXTURE", "Invalid texture declarations.");
|
|
408
|
+
const textureDeclarations = kernel.textures ?? [];
|
|
409
|
+
const textureNames = new Set();
|
|
410
|
+
for (const binding of textureDeclarations) {
|
|
411
|
+
requireThat(record(binding) &&
|
|
412
|
+
typeof binding.name === "string" &&
|
|
413
|
+
/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(binding.name) &&
|
|
414
|
+
!textureNames.has(binding.name) &&
|
|
415
|
+
(binding.filter === undefined ||
|
|
416
|
+
["nearest", "linear"].includes(binding.filter)) &&
|
|
417
|
+
[binding.wrapU, binding.wrapV].every((wrap) => wrap === undefined ||
|
|
418
|
+
["clamp", "repeat", "mirror"].includes(wrap)), "TEXTURE", "Texture declarations require unique names and supported filtering/addressing.");
|
|
419
|
+
textureNames.add(binding.name);
|
|
420
|
+
}
|
|
421
|
+
const textures = request.textures ?? {};
|
|
422
|
+
requireThat(Object.keys(textures).length === textureNames.size &&
|
|
423
|
+
Object.keys(textures).every((name) => textureNames.has(name)), "TEXTURE", "Supply exactly the kernel named texture bindings.");
|
|
424
|
+
for (const node of graph.nodes)
|
|
425
|
+
if (node.op === "sampleTexture") {
|
|
426
|
+
const name = typeof node.texture === "string" ? node.texture : node.texture?.id;
|
|
427
|
+
requireThat(typeof name === "string" && textureNames.has(name), "TEXTURE", "Unknown sampleTexture binding.");
|
|
428
|
+
}
|
|
429
|
+
limit("maxBindGroups", 1);
|
|
430
|
+
limit("maxBindingsPerBindGroup", declarations.length +
|
|
431
|
+
textureDeclarations.length +
|
|
432
|
+
(outputTexture ? 1 : 0) +
|
|
433
|
+
3);
|
|
434
|
+
if (textureDeclarations.length)
|
|
435
|
+
limit("maxSampledTexturesPerShaderStage", textureDeclarations.length);
|
|
436
|
+
if (outputTexture)
|
|
437
|
+
limit("maxStorageTexturesPerShaderStage", 1);
|
|
438
|
+
limit("maxStorageBuffersPerShaderStage", declarations.length + 2);
|
|
439
|
+
limit("maxUniformBuffersPerShaderStage", 1);
|
|
440
|
+
const snapshots = [], byKey = new Map();
|
|
441
|
+
const textureSnapshots = [];
|
|
442
|
+
const capture = (name, input, sampled, type) => {
|
|
443
|
+
requireThat(record(input) &&
|
|
444
|
+
input.type === type &&
|
|
445
|
+
(sampled ? input.texture !== undefined : input.texture === undefined), "BINDING", "Buffers and texture bindings must match their declarations.");
|
|
446
|
+
const snapshot = { name, ...snapshotInput(input) };
|
|
447
|
+
const previous = byKey.get(input.key);
|
|
448
|
+
if (previous)
|
|
449
|
+
requireThat(input.type === previous.type &&
|
|
450
|
+
snapshot.revision === previous.revision &&
|
|
451
|
+
sameShape(snapshot.texture, previous.texture) &&
|
|
452
|
+
same(snapshot.data, previous.data), "REVISION", "Aliased input bindings must describe the same snapshot.");
|
|
453
|
+
if (input.key === output.key)
|
|
454
|
+
requireThat(input.type === output.type &&
|
|
455
|
+
input.values.length === count &&
|
|
456
|
+
sameShape(snapshot.texture, outputTexture), "TYPE", "Feedback must match the output type, count and shape.");
|
|
457
|
+
byKey.set(input.key, snapshot);
|
|
458
|
+
return snapshot;
|
|
459
|
+
};
|
|
460
|
+
for (const declaration of declarations) {
|
|
461
|
+
requireThat(own(inputs, declaration.name), "BINDING", "Missing input binding.");
|
|
462
|
+
snapshots.push(capture(declaration.name, inputs[declaration.name], false, declaration.type));
|
|
463
|
+
}
|
|
464
|
+
for (const declaration of textureDeclarations) {
|
|
465
|
+
requireThat(own(textures, declaration.name), "TEXTURE", "Missing texture binding.");
|
|
466
|
+
textureSnapshots.push(capture(declaration.name, textures[declaration.name], true, "vec4"));
|
|
467
|
+
}
|
|
468
|
+
requireThat(graph.parameters === undefined || Array.isArray(graph.parameters), "PARAMETER", "Invalid parameter declarations.");
|
|
469
|
+
const params = graph.parameters ?? [];
|
|
470
|
+
const uniformBytes = (params.length + 1) * 16;
|
|
471
|
+
limit("maxUniformBufferBindingSize", uniformBytes);
|
|
472
|
+
limit("maxBufferSize", uniformBytes);
|
|
473
|
+
const uniform = new Float32Array((params.length + 1) * 4);
|
|
474
|
+
uniform[0] = time;
|
|
475
|
+
const parameterIndices = new Map();
|
|
476
|
+
params.forEach((p, i) => {
|
|
477
|
+
requireThat(record(p) &&
|
|
478
|
+
typeof p.name === "string" &&
|
|
479
|
+
p.name.length &&
|
|
480
|
+
isGraphType(p.type) &&
|
|
481
|
+
!parameterIndices.has(p.name), "PARAMETER", "Parameters must have unique names and valid types.");
|
|
482
|
+
requireThat(graphValue(p.type, p.default) !== null, "PARAMETER", "Invalid parameter default.");
|
|
483
|
+
put(uniform, (i + 1) * 4, p.type, own(parameters, p.name) ? parameters[p.name] : p.default, "PARAMETER");
|
|
484
|
+
parameterIndices.set(p.name, i + 1);
|
|
485
|
+
});
|
|
486
|
+
requireThat(Object.keys(parameters).every((name) => parameterIndices.has(name)), "PARAMETER", "Unknown parameter override.");
|
|
487
|
+
const outputWidth = outputTexture?.width ?? count;
|
|
488
|
+
const outputHeight = outputTexture?.height ?? 1;
|
|
489
|
+
const textureBinding = snapshots.length + 3;
|
|
490
|
+
const storageBinding = textureBinding + textureSnapshots.length;
|
|
491
|
+
const lowered = lowerGraphWGSL(graph, {
|
|
492
|
+
prefix: "tc",
|
|
493
|
+
input: (name) => {
|
|
494
|
+
if (name === "time")
|
|
495
|
+
return "tc_params[0].x";
|
|
496
|
+
if (name === "index")
|
|
497
|
+
return "f32(tc_id.x)";
|
|
498
|
+
if (name === "pixelCoord")
|
|
499
|
+
return "vec2f(tc_pixel)";
|
|
500
|
+
if (name === "outputSize")
|
|
501
|
+
return `vec2f(${outputWidth}.0, ${outputHeight}.0)`;
|
|
502
|
+
if (name === "uv")
|
|
503
|
+
return `(vec2f(tc_pixel) + vec2f(0.5)) / vec2f(${outputWidth}.0, ${outputHeight}.0)`;
|
|
504
|
+
throw error("DOMAIN", "Unsupported input " + name + ".");
|
|
505
|
+
},
|
|
506
|
+
parameter: (name, type) => {
|
|
507
|
+
const i = parameterIndices.get(name);
|
|
508
|
+
requireThat(i !== undefined && params[i - 1].type === type, "PARAMETER", "Unknown or mistyped parameter.");
|
|
509
|
+
return unpack("tc_params[" + i + "]", type);
|
|
510
|
+
},
|
|
511
|
+
readBuffer: (name, index, type) => {
|
|
512
|
+
const i = snapshots.findIndex((input) => input.name === name);
|
|
513
|
+
requireThat(i >= 0 && snapshots[i].type === type, "BINDING", "Unknown or mistyped readBuffer binding.");
|
|
514
|
+
return unpack("tc_read_" + i + "(" + index + ")", type);
|
|
515
|
+
},
|
|
516
|
+
sampleTexture: (name, uv) => {
|
|
517
|
+
const i = textureSnapshots.findIndex((input) => input.name === name);
|
|
518
|
+
requireThat(i >= 0, "TEXTURE", "Unknown sampleTexture binding.");
|
|
519
|
+
return "tc_sample_" + i + "(" + uv + ")";
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
const source = [
|
|
523
|
+
"@group(0) @binding(0) var<storage, read_write> tc_output: array<vec4f>;",
|
|
524
|
+
"@group(0) @binding(1) var<uniform> tc_params: array<vec4f, " +
|
|
525
|
+
(params.length + 1) +
|
|
526
|
+
">;",
|
|
527
|
+
"@group(0) @binding(2) var<storage, read_write> tc_fault: atomic<u32>;",
|
|
528
|
+
...snapshots.map((_, i) => [
|
|
529
|
+
"@group(0) @binding(" +
|
|
530
|
+
(i + 3) +
|
|
531
|
+
") var<storage, read> tc_input_" +
|
|
532
|
+
i +
|
|
533
|
+
": array<vec4f>;",
|
|
534
|
+
"fn tc_read_" + i + "(index: f32) -> vec4f {",
|
|
535
|
+
" if ((bitcast<u32>(index) & 0x7f800000u) == 0x7f800000u ||",
|
|
536
|
+
" index < 0.0 || index != floor(index) || index >= f32(arrayLength(&tc_input_" +
|
|
537
|
+
i +
|
|
538
|
+
"))) {",
|
|
539
|
+
" atomicOr(&tc_fault, 1u);",
|
|
540
|
+
" return vec4f(0.0);",
|
|
541
|
+
" }",
|
|
542
|
+
" return tc_input_" + i + "[u32(index)];",
|
|
543
|
+
"}",
|
|
544
|
+
].join("\n")),
|
|
545
|
+
...textureSnapshots.map((_, i) => textureHelper(i, textureBinding + i, textureDeclarations[i])),
|
|
546
|
+
...(outputTexture
|
|
547
|
+
? [
|
|
548
|
+
`@group(0) @binding(${storageBinding}) var tc_output_texture: texture_storage_2d<rgba32float, write>;`,
|
|
549
|
+
]
|
|
550
|
+
: []),
|
|
551
|
+
lowered.helpers,
|
|
552
|
+
"@compute @workgroup_size(" + workgroup + ")",
|
|
553
|
+
"fn main(@builtin(global_invocation_id) tc_id: vec3u) {",
|
|
554
|
+
" if (tc_id.x >= " + count + "u) { return; }",
|
|
555
|
+
` let tc_pixel = vec2u(tc_id.x % ${outputWidth}u, tc_id.x / ${outputWidth}u);`,
|
|
556
|
+
lowered.code,
|
|
557
|
+
" let tc_value = " +
|
|
558
|
+
packedResult(lowered.result, kernel.outputType) +
|
|
559
|
+
";",
|
|
560
|
+
" if (any((bitcast<vec4u>(tc_value) & vec4u(0x7f800000u)) == vec4u(0x7f800000u))) {",
|
|
561
|
+
" atomicOr(&tc_fault, 2u);",
|
|
562
|
+
" }",
|
|
563
|
+
" tc_output[tc_id.x] = tc_value;",
|
|
564
|
+
...(outputTexture
|
|
565
|
+
? [" textureStore(tc_output_texture, vec2i(tc_pixel), tc_value);"]
|
|
566
|
+
: []),
|
|
567
|
+
"}",
|
|
568
|
+
].join("\n");
|
|
569
|
+
return {
|
|
570
|
+
source,
|
|
571
|
+
snapshots,
|
|
572
|
+
textureSnapshots,
|
|
573
|
+
outputTexture,
|
|
574
|
+
textureBinding,
|
|
575
|
+
storageBinding,
|
|
576
|
+
uniform,
|
|
577
|
+
count,
|
|
578
|
+
workgroup,
|
|
579
|
+
outputType: kernel.outputType,
|
|
580
|
+
outputKey: output.key,
|
|
581
|
+
retain: output.retain === true,
|
|
582
|
+
minimumRevision: Math.max(latest(output.key)?.revision ?? -1, ...[...snapshots, ...textureSnapshots]
|
|
583
|
+
.filter((input) => input.key === output.key)
|
|
584
|
+
.map((input) => input.revision)),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
inspect: () => Object.freeze({
|
|
589
|
+
cachedInputs: cache.size,
|
|
590
|
+
pendingCandidates: pending.size,
|
|
591
|
+
committedVersions: [...committed.values()].reduce((n, versions) => n + versions.size, 0),
|
|
592
|
+
residentBytes: [...committed.values()].reduce((n, versions) => n +
|
|
593
|
+
[...versions.values()].reduce((bytes, entry) => bytes + entry.data.byteLength * (entry.gpuTexture ? 2 : 1), 0), 0),
|
|
594
|
+
retentions,
|
|
595
|
+
commits,
|
|
596
|
+
residentHits,
|
|
597
|
+
inputCacheHits,
|
|
598
|
+
inputUploads,
|
|
599
|
+
checkpointUploads,
|
|
600
|
+
cachedPipelines: pipelines.size,
|
|
601
|
+
cachedBytes: [...cache.values()].reduce((n, input) => n + input.data.byteLength, 0),
|
|
602
|
+
liveBuffers: owned.size,
|
|
603
|
+
liveTextures: ownedTextures.size,
|
|
604
|
+
textureUploads,
|
|
605
|
+
textureResidentHits,
|
|
606
|
+
storageTextureDispatches,
|
|
607
|
+
allocations,
|
|
608
|
+
uploads,
|
|
609
|
+
dispatches,
|
|
610
|
+
active: !!running,
|
|
611
|
+
disposed,
|
|
612
|
+
deviceLost: lost,
|
|
613
|
+
}),
|
|
614
|
+
resident(input) {
|
|
615
|
+
available();
|
|
616
|
+
const snapshot = snapshotInput(input);
|
|
617
|
+
let entry;
|
|
618
|
+
try {
|
|
619
|
+
entry = checkpoint(snapshot, "checkpoint");
|
|
620
|
+
}
|
|
621
|
+
catch (failure) {
|
|
622
|
+
throw error("GPU", failure instanceof Error ? failure.message : String(failure));
|
|
623
|
+
}
|
|
624
|
+
const buffer = entry.buffer;
|
|
625
|
+
return Object.freeze({
|
|
626
|
+
buffer,
|
|
627
|
+
type: snapshot.type,
|
|
628
|
+
count: snapshot.data.length / 4,
|
|
629
|
+
revision: snapshot.revision,
|
|
630
|
+
});
|
|
631
|
+
},
|
|
632
|
+
textureResident(input) {
|
|
633
|
+
available();
|
|
634
|
+
const snapshot = snapshotInput(input);
|
|
635
|
+
requireThat(snapshot.texture, "TEXTURE", "textureResident requires a texture descriptor.");
|
|
636
|
+
const hit = exactEntry(snapshot);
|
|
637
|
+
let entry;
|
|
638
|
+
try {
|
|
639
|
+
entry = checkpoint(snapshot, "checkpoint");
|
|
640
|
+
}
|
|
641
|
+
catch (failure) {
|
|
642
|
+
throw error("GPU", failure instanceof Error ? failure.message : String(failure));
|
|
643
|
+
}
|
|
644
|
+
if (hit)
|
|
645
|
+
textureResidentHits++;
|
|
646
|
+
return Object.freeze({
|
|
647
|
+
texture: entry.gpuTexture,
|
|
648
|
+
width: snapshot.texture.width,
|
|
649
|
+
height: snapshot.texture.height,
|
|
650
|
+
revision: snapshot.revision,
|
|
651
|
+
});
|
|
652
|
+
},
|
|
653
|
+
commit(key, revision) {
|
|
654
|
+
available();
|
|
655
|
+
requireThat(typeof key === "string" && key.length, "REQUEST", "Commit needs a resource key.");
|
|
656
|
+
requireThat(!running?.keys.has(key), "BUSY", "Cannot commit a resource during its dispatch.");
|
|
657
|
+
const candidate = pending.get(key);
|
|
658
|
+
requireThat(candidate &&
|
|
659
|
+
candidate.key === key &&
|
|
660
|
+
owned.has(candidate.buffer) &&
|
|
661
|
+
(!candidate.texture ||
|
|
662
|
+
(candidate.gpuTexture && ownedTextures.has(candidate.gpuTexture))), "CANDIDATE", "No validated pending output exists for this key.");
|
|
663
|
+
requireThat(!candidate.signal.aborted, "CANCELLED", "The candidate was aborted.");
|
|
664
|
+
const previous = latest(key);
|
|
665
|
+
requireThat(isGraphType(candidate.type) &&
|
|
666
|
+
candidate.data.length >= 4 &&
|
|
667
|
+
candidate.data.length % 4 === 0 &&
|
|
668
|
+
candidate.data.length / 4 <= MAX_COUNT &&
|
|
669
|
+
(!previous ||
|
|
670
|
+
(previous.type === candidate.type &&
|
|
671
|
+
previous.data.length === candidate.data.length &&
|
|
672
|
+
sameShape(previous.texture, candidate.texture))), "TYPE", "The candidate must preserve the resource type and count.");
|
|
673
|
+
requireThat(Number.isSafeInteger(revision) &&
|
|
674
|
+
revision >= 0 &&
|
|
675
|
+
revision >
|
|
676
|
+
Math.max(candidate.minimumRevision, previous?.revision ?? -1), "REVISION", "A commit must advance the resource revision.");
|
|
677
|
+
// Stage everything before changing visible metadata. No GPU calls publish a result.
|
|
678
|
+
const entry = {
|
|
679
|
+
key,
|
|
680
|
+
revision,
|
|
681
|
+
type: candidate.type,
|
|
682
|
+
data: candidate.data,
|
|
683
|
+
buffer: candidate.buffer,
|
|
684
|
+
texture: candidate.texture,
|
|
685
|
+
gpuTexture: candidate.gpuTexture,
|
|
686
|
+
};
|
|
687
|
+
const versions = new Map(committed.get(key));
|
|
688
|
+
versions.set(revision, entry);
|
|
689
|
+
committed.set(key, versions);
|
|
690
|
+
remember(entry, true);
|
|
691
|
+
pending.delete(key);
|
|
692
|
+
candidate.signal.removeEventListener("abort", candidate.abort);
|
|
693
|
+
commits++;
|
|
694
|
+
},
|
|
695
|
+
discard(key) {
|
|
696
|
+
if (running?.keys.has(key))
|
|
697
|
+
running.stop(error("CANCELLED", "The compute candidate was discarded."));
|
|
698
|
+
discard(key);
|
|
699
|
+
},
|
|
700
|
+
collect(live) {
|
|
701
|
+
available();
|
|
702
|
+
requireThat(!running, "BUSY", "Collect only after the compute flush finishes.");
|
|
703
|
+
requireThat(Array.isArray(live), "REQUEST", "Collect needs live resource revisions.");
|
|
704
|
+
const current = new Map();
|
|
705
|
+
for (const input of live) {
|
|
706
|
+
requireThat(record(input) &&
|
|
707
|
+
typeof input.key === "string" &&
|
|
708
|
+
input.key.length &&
|
|
709
|
+
typeof input.revision === "number" &&
|
|
710
|
+
Number.isSafeInteger(input.revision) &&
|
|
711
|
+
input.revision >= 0 &&
|
|
712
|
+
!current.get(input.key)?.has(input.revision), "REVISION", "Live resources need unique key/revision pairs and valid revisions.");
|
|
713
|
+
let revisions = current.get(input.key);
|
|
714
|
+
if (!revisions)
|
|
715
|
+
current.set(input.key, (revisions = new Set()));
|
|
716
|
+
revisions.add(input.revision);
|
|
717
|
+
}
|
|
718
|
+
for (const [key, versions] of committed) {
|
|
719
|
+
for (const [revision, entry] of versions) {
|
|
720
|
+
if (!current.get(key)?.has(revision)) {
|
|
721
|
+
versions.delete(revision);
|
|
722
|
+
destroyEntry(entry);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (!versions.size)
|
|
726
|
+
committed.delete(key);
|
|
727
|
+
}
|
|
728
|
+
for (const [key, entry] of cache)
|
|
729
|
+
if (!current.get(key)?.has(entry.revision) ||
|
|
730
|
+
committed.get(key)?.has(entry.revision))
|
|
731
|
+
evict(key);
|
|
732
|
+
},
|
|
733
|
+
release(key) {
|
|
734
|
+
if (running?.keys.has(key))
|
|
735
|
+
running.stop(error("CANCELLED", "An in-flight compute resource was released."));
|
|
736
|
+
discard(key);
|
|
737
|
+
releaseVersions(key);
|
|
738
|
+
evict(key);
|
|
739
|
+
highWater.delete(key);
|
|
740
|
+
},
|
|
741
|
+
dispose() {
|
|
742
|
+
if (disposed)
|
|
743
|
+
return;
|
|
744
|
+
disposed = true;
|
|
745
|
+
running?.stop(error("CANCELLED", "The compute executor was disposed."));
|
|
746
|
+
clear();
|
|
747
|
+
},
|
|
748
|
+
async run(request) {
|
|
749
|
+
requireThat(!disposed, "DISPOSED", "The compute executor has been disposed.");
|
|
750
|
+
requireThat(!lost, "DEVICE_LOST", "The compute device was lost.");
|
|
751
|
+
requireThat(!running, "BUSY", "This executor accepts one dispatch at a time.");
|
|
752
|
+
const prepared = prepare(request);
|
|
753
|
+
const signal = request.signal;
|
|
754
|
+
requireThat(!signal.aborted, "CANCELLED", "The compute request was aborted.");
|
|
755
|
+
discard(prepared.outputKey);
|
|
756
|
+
const { count, workgroup, uniform, snapshots, textureSnapshots } = prepared;
|
|
757
|
+
const allSnapshots = [...snapshots, ...textureSnapshots];
|
|
758
|
+
const ephemeral = new Set(), usedKeys = new Set();
|
|
759
|
+
const validations = [];
|
|
760
|
+
let stopped, rejectStop;
|
|
761
|
+
const stopPromise = new Promise((_, reject) => {
|
|
762
|
+
rejectStop = reject;
|
|
763
|
+
});
|
|
764
|
+
const cleanup = () => {
|
|
765
|
+
for (const buffer of ephemeral)
|
|
766
|
+
destroy(buffer);
|
|
767
|
+
ephemeral.clear();
|
|
768
|
+
};
|
|
769
|
+
const stop = (reason) => {
|
|
770
|
+
if (stopped)
|
|
771
|
+
return;
|
|
772
|
+
stopped = reason;
|
|
773
|
+
cleanup();
|
|
774
|
+
rejectStop(reason);
|
|
775
|
+
};
|
|
776
|
+
const active = () => {
|
|
777
|
+
if (stopped)
|
|
778
|
+
throw stopped;
|
|
779
|
+
};
|
|
780
|
+
running = {
|
|
781
|
+
keys: new Set([
|
|
782
|
+
prepared.outputKey,
|
|
783
|
+
...allSnapshots.map((input) => input.key),
|
|
784
|
+
]),
|
|
785
|
+
stop,
|
|
786
|
+
};
|
|
787
|
+
const abort = () => stop(error("CANCELLED", "The compute request was aborted."));
|
|
788
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
789
|
+
const timer = setTimeout(() => stop(error("TIMEOUT", "Compute exceeded " + DEADLINE_MS + " ms.")), DEADLINE_MS);
|
|
790
|
+
// Scope push/pop stay synchronous; no other device user can interleave the stack.
|
|
791
|
+
const scoped = (work) => {
|
|
792
|
+
device.pushErrorScope("out-of-memory");
|
|
793
|
+
device.pushErrorScope("internal");
|
|
794
|
+
device.pushErrorScope("validation");
|
|
795
|
+
try {
|
|
796
|
+
return work();
|
|
797
|
+
}
|
|
798
|
+
finally {
|
|
799
|
+
for (let i = 0; i < 3; i++) {
|
|
800
|
+
const pending = device.popErrorScope();
|
|
801
|
+
void pending.catch(() => undefined);
|
|
802
|
+
validations.push(pending);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
const make = (size, usage, temporary) => {
|
|
807
|
+
active();
|
|
808
|
+
const buffer = device.createBuffer({
|
|
809
|
+
label: "pelican managed compute",
|
|
810
|
+
size,
|
|
811
|
+
usage,
|
|
812
|
+
});
|
|
813
|
+
owned.add(buffer);
|
|
814
|
+
if (temporary)
|
|
815
|
+
ephemeral.add(buffer);
|
|
816
|
+
allocations++;
|
|
817
|
+
return buffer;
|
|
818
|
+
};
|
|
819
|
+
const validate = async () => {
|
|
820
|
+
const failures = await Promise.all(validations);
|
|
821
|
+
active();
|
|
822
|
+
requireThat(!failures.some(Boolean), "GPU", failures
|
|
823
|
+
.filter(Boolean)
|
|
824
|
+
.map((failure) => failure.message)
|
|
825
|
+
.join("; "));
|
|
826
|
+
};
|
|
827
|
+
let outputBuffer;
|
|
828
|
+
let outputGpuTexture;
|
|
829
|
+
const work = async () => {
|
|
830
|
+
active();
|
|
831
|
+
let pipeline = pipelines.get(prepared.source);
|
|
832
|
+
if (!pipeline) {
|
|
833
|
+
const module = scoped(() => device.createShaderModule({
|
|
834
|
+
label: "pelican managed compute",
|
|
835
|
+
code: prepared.source,
|
|
836
|
+
}));
|
|
837
|
+
const info = await module.getCompilationInfo();
|
|
838
|
+
active();
|
|
839
|
+
const errors = info.messages.filter((message) => message.type === "error");
|
|
840
|
+
requireThat(!errors.length, "SHADER", errors.map((message) => message.message).join("; "));
|
|
841
|
+
pipeline = await scoped(() => {
|
|
842
|
+
const layout = device.createBindGroupLayout({
|
|
843
|
+
entries: [
|
|
844
|
+
{
|
|
845
|
+
binding: 0,
|
|
846
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
847
|
+
buffer: { type: "storage" },
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
binding: 1,
|
|
851
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
852
|
+
buffer: { type: "uniform" },
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
binding: 2,
|
|
856
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
857
|
+
buffer: { type: "storage" },
|
|
858
|
+
},
|
|
859
|
+
...snapshots.map((_, i) => ({
|
|
860
|
+
binding: i + 3,
|
|
861
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
862
|
+
buffer: { type: "read-only-storage" },
|
|
863
|
+
})),
|
|
864
|
+
...textureSnapshots.map((_, i) => ({
|
|
865
|
+
binding: prepared.textureBinding + i,
|
|
866
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
867
|
+
texture: {
|
|
868
|
+
sampleType: "unfilterable-float",
|
|
869
|
+
viewDimension: "2d",
|
|
870
|
+
multisampled: false,
|
|
871
|
+
},
|
|
872
|
+
})),
|
|
873
|
+
...(prepared.outputTexture
|
|
874
|
+
? [
|
|
875
|
+
{
|
|
876
|
+
binding: prepared.storageBinding,
|
|
877
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
878
|
+
storageTexture: {
|
|
879
|
+
access: "write-only",
|
|
880
|
+
format: "rgba32float",
|
|
881
|
+
viewDimension: "2d",
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
]
|
|
885
|
+
: []),
|
|
886
|
+
],
|
|
887
|
+
});
|
|
888
|
+
return device.createComputePipelineAsync({
|
|
889
|
+
layout: device.createPipelineLayout({
|
|
890
|
+
bindGroupLayouts: [layout],
|
|
891
|
+
}),
|
|
892
|
+
compute: { module, entryPoint: "main" },
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
// Publish only fully validated creation, never a cancelled late completion.
|
|
896
|
+
await validate();
|
|
897
|
+
active();
|
|
898
|
+
pipelines.set(prepared.source, pipeline);
|
|
899
|
+
}
|
|
900
|
+
active();
|
|
901
|
+
const readyPipeline = pipeline;
|
|
902
|
+
const readback = scoped(() => {
|
|
903
|
+
const inputEntries = allSnapshots.map((snapshot) => {
|
|
904
|
+
usedKeys.add(snapshot.key);
|
|
905
|
+
const hit = exactEntry(snapshot);
|
|
906
|
+
const entry = checkpoint(snapshot, "input");
|
|
907
|
+
if (snapshot.texture && hit)
|
|
908
|
+
textureResidentHits++;
|
|
909
|
+
return entry;
|
|
910
|
+
});
|
|
911
|
+
if (prepared.outputTexture) {
|
|
912
|
+
outputGpuTexture = makeTexture(prepared.outputTexture, true);
|
|
913
|
+
ephemeral.add(outputGpuTexture);
|
|
914
|
+
}
|
|
915
|
+
const output = (outputBuffer = make(count * 16, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, true));
|
|
916
|
+
const params = make(uniform.byteLength, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, true);
|
|
917
|
+
const fault = make(4, GPUBufferUsage.STORAGE |
|
|
918
|
+
GPUBufferUsage.COPY_SRC |
|
|
919
|
+
GPUBufferUsage.COPY_DST, true);
|
|
920
|
+
const read = make(count * 16 + 4, GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, true);
|
|
921
|
+
device.queue.writeBuffer(params, 0, uniform);
|
|
922
|
+
device.queue.writeBuffer(fault, 0, new Uint32Array(1));
|
|
923
|
+
const group = device.createBindGroup({
|
|
924
|
+
layout: readyPipeline.getBindGroupLayout(0),
|
|
925
|
+
entries: [
|
|
926
|
+
{ binding: 0, resource: { buffer: output } },
|
|
927
|
+
{ binding: 1, resource: { buffer: params } },
|
|
928
|
+
{ binding: 2, resource: { buffer: fault } },
|
|
929
|
+
...inputEntries.slice(0, snapshots.length).map((entry, i) => ({
|
|
930
|
+
binding: i + 3,
|
|
931
|
+
resource: { buffer: entry.buffer },
|
|
932
|
+
})),
|
|
933
|
+
...inputEntries.slice(snapshots.length).map((entry, i) => ({
|
|
934
|
+
binding: prepared.textureBinding + i,
|
|
935
|
+
resource: entry.gpuTexture.createView(),
|
|
936
|
+
})),
|
|
937
|
+
...(outputGpuTexture
|
|
938
|
+
? [
|
|
939
|
+
{
|
|
940
|
+
binding: prepared.storageBinding,
|
|
941
|
+
resource: outputGpuTexture.createView(),
|
|
942
|
+
},
|
|
943
|
+
]
|
|
944
|
+
: []),
|
|
945
|
+
],
|
|
946
|
+
});
|
|
947
|
+
const encoder = device.createCommandEncoder();
|
|
948
|
+
const pass = encoder.beginComputePass();
|
|
949
|
+
pass.setPipeline(readyPipeline);
|
|
950
|
+
pass.setBindGroup(0, group);
|
|
951
|
+
pass.dispatchWorkgroups(Math.ceil(count / workgroup));
|
|
952
|
+
pass.end();
|
|
953
|
+
encoder.copyBufferToBuffer(output, 0, read, 0, count * 16);
|
|
954
|
+
encoder.copyBufferToBuffer(fault, 0, read, count * 16, 4);
|
|
955
|
+
device.queue.submit([encoder.finish()]);
|
|
956
|
+
dispatches++;
|
|
957
|
+
if (outputGpuTexture)
|
|
958
|
+
storageTextureDispatches++;
|
|
959
|
+
return read;
|
|
960
|
+
});
|
|
961
|
+
await readback.mapAsync(GPUMapMode.READ);
|
|
962
|
+
active();
|
|
963
|
+
const bytes = readback.getMappedRange().slice(0);
|
|
964
|
+
readback.unmap();
|
|
965
|
+
const faults = new Uint32Array(bytes, count * 16, 1)[0];
|
|
966
|
+
requireThat(!(faults & 1), "BOUNDS", "A readBuffer index was nonfinite, fractional or out of range.");
|
|
967
|
+
requireThat(!(faults & 2), "NONFINITE", "The compute result was nonfinite.");
|
|
968
|
+
requireThat(!textureSnapshots.length || !(faults & 4), "TEXTURE_COORD", "A sampleTexture UV coordinate was nonfinite.");
|
|
969
|
+
requireThat(faults === 0, "GPU", "Unknown compute fault flags.");
|
|
970
|
+
const data = new Float32Array(bytes, 0, count * 4);
|
|
971
|
+
requireThat(data.every(Number.isFinite), "NONFINITE", "The decoded compute result was nonfinite.");
|
|
972
|
+
const values = [];
|
|
973
|
+
for (let i = 0; i < count; i++) {
|
|
974
|
+
const offset = i * 4;
|
|
975
|
+
if (prepared.outputType === "bool") {
|
|
976
|
+
requireThat(data[offset] === 0 || data[offset] === 1, "GPU", "Invalid bool result encoding.");
|
|
977
|
+
values.push(data[offset] === 1);
|
|
978
|
+
}
|
|
979
|
+
else if (prepared.outputType === "float")
|
|
980
|
+
values.push(data[offset]);
|
|
981
|
+
else
|
|
982
|
+
values.push(Array.from(data.subarray(offset, offset + width(prepared.outputType))));
|
|
983
|
+
}
|
|
984
|
+
await validate();
|
|
985
|
+
active();
|
|
986
|
+
if (prepared.retain) {
|
|
987
|
+
// The CPU descriptor must describe exactly the retained vec4 bytes,
|
|
988
|
+
// including bool encoding and the unused lanes of narrower values.
|
|
989
|
+
const packed = new Float32Array(count * 4);
|
|
990
|
+
values.forEach((value, i) => put(packed, i * 4, prepared.outputType, value, "GPU"));
|
|
991
|
+
requireThat(same(packed, data), "GPU", "Invalid packed result encoding.");
|
|
992
|
+
const buffer = outputBuffer;
|
|
993
|
+
const candidate = {
|
|
994
|
+
key: prepared.outputKey,
|
|
995
|
+
type: prepared.outputType,
|
|
996
|
+
data: packed,
|
|
997
|
+
buffer,
|
|
998
|
+
texture: prepared.outputTexture,
|
|
999
|
+
gpuTexture: outputGpuTexture,
|
|
1000
|
+
signal,
|
|
1001
|
+
minimumRevision: prepared.minimumRevision,
|
|
1002
|
+
abort: () => discard(prepared.outputKey),
|
|
1003
|
+
};
|
|
1004
|
+
pending.set(prepared.outputKey, candidate);
|
|
1005
|
+
signal.addEventListener("abort", candidate.abort, { once: true });
|
|
1006
|
+
ephemeral.delete(buffer);
|
|
1007
|
+
if (outputGpuTexture)
|
|
1008
|
+
ephemeral.delete(outputGpuTexture);
|
|
1009
|
+
retentions++;
|
|
1010
|
+
}
|
|
1011
|
+
return values;
|
|
1012
|
+
};
|
|
1013
|
+
try {
|
|
1014
|
+
return await Promise.race([work(), stopPromise]);
|
|
1015
|
+
}
|
|
1016
|
+
catch (failure) {
|
|
1017
|
+
// A rejected upload/validation must not leave a seemingly current cache entry.
|
|
1018
|
+
for (const key of usedKeys)
|
|
1019
|
+
evict(key);
|
|
1020
|
+
if (failure instanceof Error &&
|
|
1021
|
+
/^TC_(COMPUTE|GRAPH)_/.test(failure.message))
|
|
1022
|
+
throw failure;
|
|
1023
|
+
throw error("GPU", failure instanceof Error ? failure.message : String(failure));
|
|
1024
|
+
}
|
|
1025
|
+
finally {
|
|
1026
|
+
stopped ??= error("CANCELLED", "The compute request is no longer active.");
|
|
1027
|
+
clearTimeout(timer);
|
|
1028
|
+
signal.removeEventListener("abort", abort);
|
|
1029
|
+
cleanup();
|
|
1030
|
+
running = undefined;
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
};
|
|
1034
|
+
}
|